点击阅读原文
本文是在看完《你不知道的 JavaScript 》这本书之后整理而成。 本文中所有的代码可以在这里找到。如果你不想读下面的文字,可以直接 clone 代码并运行,然后结合代码和注释,试着去理解作用域和闭包的概念。
JavaScript 采用词法作用域。所谓词法作用域就是代码书写完成之后,作用域随即确定,所写既所得。
看下面的代码;
var printSpace = require('./printspace');
var showmore = true;
var yoho = "yoho!我在全局作用域";
function awfulSayYoho() {
console.log("*****一般情况*****");
console.log(yoho);
}
function normalSayYoho() {
console.log("*****局部变量*****");
var yoho = "yoho!我在函数作用域";
console.log(yoho);
}
function advanceSayYoho() {
// var yoho; // B 行, 自动提升声明
console.log("*****自动提升*****");
console.log(yoho); // C 行,打印的是 B 行的声明,既 undefined
if (showmore) {
var yoho = "yoho!我在函数作用域"; // A 行,声明 yoho ,并为其赋值。
// yoho = "yoho!我在函数作用域"; 执行语句不提升,留在原地
function sayYohoAgain() {
console.log(yoho);
}
sayYohoAgain();
}
}
printSpace();
awfulSayYoho();
printSpace();
normalSayYoho();
printSpace();
advanceSayYoho();
printSpace();
上面这些代码的运行结果如下:
*****一般情况*****
yoho!我在全局作用域
*****局部变量*****
yoho!我在函数作用域
*****自动提升*****
undefined
yoho!我在函数作用域
上面这段平淡无奇的代码,相信大多数 JSer 理解起来并不难。值得一提的是声明的自动提升。在 advanceSayYoho 函数里的 A 行,声明了变量 yoho 并为其赋值,既看上去一行代码做了两件事:声明且赋值。但是,在函数被调用时, JS 引擎总是将 A 行这样的"声明且赋值"的语句拆分成声明和赋值两步执行,既
var yoho = "yoho!我在函数作用域";
被分成了:
var yoho;
yoho = "yoho!我在函数作用域";
其中声明语句
var yoho;
这行被自动提升到函数体的顶部,既相当于自动提升到了 advanceSayYoho 函数的 B 行。
而赋值语句
yoho = "yoho!我在函数作用域";
不会提升,留在原地
由此便不难理解为什么 C 行打印的是 undefined 了。
在进一步讨论作用域及作用域对象的之前,先简单的说明一下 JS 里的“赋值”和"引用"。
JS 里有两大类数据类型: 原始数据类型和引用数据类型(及对象数据类型)。下面的代码试图说明这两者的区别:
var printSpace = require('./printspace');
// 值传递
console.log("*****值传递*****");
var num = 10; // num 持有了数字 10
var a = num; // a 持有了数字 10
var b = num; // b 持有了数字 10
console.log("------初始-----");
console.log(num);
console.log(a);
console.log(b);
num = 8; // num 持有的数字变为了 8
console.log("------num 值改变后-----");
console.log(num);
console.log(a);
console.log(b);
printSpace();
// 对象的引用
console.log("*****对象的引用*****");
var yoho = { // yoho 引用了对象 {value: “潮流”}
value: "潮流"
}
var kris = yoho; // kris 引用了对象 {value: “潮流”}
var me = yoho; // me 引用了对象 {value: “潮流”}
console.log("------初始----");
console.log(yoho.value);
console.log(kris.value);
console.log(me.value);
me.value = "还是潮流"; // A 行, "被 me 引用的对象".value 发生了
console.log("------me.value 改变后-----");
console.log(yoho.value);
console.log(kris.value);
console.log(me.value);
运行 node reference ,可以看到:
*****值传递*****
------初始-----
10
10
10
------num 值改变后-----
8
10
10
*****对象的引用*****
------初始----
潮流
潮流
潮流
------me.value 改变后-----
还是潮流
还是潮流
还是潮流
可能有些新 JSer 不太能理解的是:上面代码的 A 行只修改了 me.value 的值,为什么 yoho.value 和 kris.value 也发生了变化?
其实上面代码中, yoho 、 me 、 kris 这三个变量是等同的,他们都指向同一块内存空间,既对象 { value: “潮流” }所在的内存空间。也就是 yoho 、 me 、 kris 三个变量同时引用了对象{ value: “潮流” }。
所以 me.value 、 kris.value 、 yoho.value 都是同一块内存空间,而那块内存里的值一开始的值是“潮流”, 然后在 A 行,这个值被改为了"还是潮流"。
如果还是不能理解"引用"这个概念,建议可以去看看 C 语言里指针的概念。
请看下面的代码:
// JS 代码由此开始运行,创建了 globalScope = {}
// 紧接着 JS 引擎对整个代码内容进行检查,搜索出全局变量,并将他们的声明设置为 globalScope 的属性
// 既 globalScope = {
// yohobuy: yohobuy,
// PRINTSPACE: PRINTSPACE,
// sayYoho: sayYoho
// }
// globalScope.PRINTSPACE = require('./printspace');
var PRINTSPACE = require('./printspace');
// globalScope.yohobuy = "YOHO!buy";
var yohobuy = "YOHO!buy";
// globalScope.sayYoho = function() {......}
function sayYoho() {
// 函数开始执行,创建了 scope = {}
// 设置 scope chain, 既 scope.parentScope = globalScope
// 提升开始: scope.yoho = undefined;
var yoho = "yoho!"; // scope.yoho = "yoho" , 此时 scope = {yoho: "yoho"}
console.log(yoho); // 打印的是 scope.yoho
console.log(yohobuy); //打印的是 scope.parentScope.yohobuy, 既 scope.globalScope.yohobuy
}
PRINTSPACE();
sayYoho();
// 函数调用完成,在函数调用过程中创建的 scope 对象此时没有被任何人引用,那么这个 scope 对象被自动回收
PRINTSPACE();
console.log(yoho); // 打印的是 globalScope.yoho, 而 globalScope 上并没有定义 yoho 属性
运行结果如你所料:
yoho!
YOHO!buy
/Users/bill/Documents/Work/closure/lifecycle.js:36
console.log(yoho); // 打印的是 globalScope.yoho, 而 globalScope 上并没有定义 yoho 属性
ReferenceError: yoho is not defined
at Object.<anonymous> (/Users/bill/Documents/Work/closure/lifecycle.js:36:13)
at Module._compile (module.js:413:34)
at Object.Module._extensions..js (module.js:422:10)
at Module.load (module.js:357:32)
at Function.Module._load (module.js:314:12)
at Function.Module.runMain (module.js:447:10)
at startup (node.js:140:18)
at node.js:1001:3
依然是一段有些无聊的代码,但愿你看到他们的时候不要打呵欠或是关掉这个网页:)))让我们来看看这里发生的一些有趣的事情:
很高兴你能看到这里。我们终于来到了闭包的概念。可是别激动,我还是只能提供一段最稀松平常的闭包代码,你一定看过类似下面这样的代码:
// 此处有 globalScope 对象; .... 不再赘述
var printSpace = require('./printspace');
function outerFunc() {
// 函数被调用时,产生了新的 outerScope 对象, 既 outerScope = {}
//
// 设置 scope chain ,既 outerScope.parentScope = globalScope;
//
// 提升开始: outerScope.counter = undefined;
// outerScope.counter = 0;
var counter = 0;
return function() {
// 函数被调用的话,产生新的 insideScope = {}
//
// 设置 scope chain ,既 insideScope.parentScope = outerScope;
//
// 此时完整的 scope chain 可以想成这样的 insideScope.parentScope.parentScope
// 既 insideScope.outerScope.globalScope
//
// 函数内部没有 var 定义的变量,所以 insideScope 没有变化
console.log(counter); // 此处需要打印的是 insideScope.outerScope.counter 。既 outerScope 对象被引用了!!
counter += 1;
}
}
var insideFunc = outerFunc(); // 因为 outerFunc 的调用,内部函数被 return 出来
// return 出来的内部函数被 insideFunc 引用
// 由于 insideFunc 引用的函数内部引用了 outerScope 上的属性 counter,
// 所以 outerScope 对象不会被回收,从效果上来讲就是局部变量 counter 一直存活了
/*
* 不妨假想成下面的代码
*
* var insideFunc = function() {
* console.log(counter);
* counter += 1;
* }
*
* 其中的 counter 变量来自 outerFunc 被调用时所创建的作用域对象
*
*/
printSpace();
insideFunc();
insideFunc();
insideFunc();
insideFunc();
insideFunc();
insideFunc();
printSpace();
var anotherInsideFunc = outerFunc(); //A 行, outerFunc 又被调用一次,产生了新的 scope chain
anotherInsideFunc();
anotherInsideFunc();
anotherInsideFunc();
anotherInsideFunc();
printSpace();
运行一下 node closure ,看到下面的结果:
0
1
2
3
4
5
0
1
2
3
从结果上来讲,这段代码只是达到了将 counter 这个一般情况下会随着函数运行结束而被销毁的局部变量保留住了的效果。到底是怎么做到这点的呢,简单来说,过程是这样的:
为了理解闭包,需要想通下面几点:
要产生闭包,关键就是:
对本文有任何问题和建议可以在这里一起讨论。
按照《 JavaScript 权威指南》里的说法,闭包并不是 JS 特有的,它是一种计算机术语,在计算机科学中,将函数和作用域联系起来的这种机制就是闭包,所以理论上所有的 JS 函数都可以被认为是闭包。但是我们平时说的闭包,更多的是指的外部函数运行时产生的作用域对象被保留的现象。
《你不知道的 JavaScript 》这本书在 github 上有英文的开源版本,任何人都可以为其贡献内容,该项目已经有接近 30000star 。目前其中的部分章节被翻译并出版成纸质书籍,感兴趣的话可以去看看。
赵彪原创,转载请注明作者和出处
1
xuwenmang 2016-04-29 03:18:26 +08:00
维基百科
静态作用域又叫做词法作用域,采用词法作用域的变量叫词法变量。词法变量有一个在编译时静态确定的作用域。词法变量的作用域可以是一个函数或一段代码,该变量在这段代码区域内可见( visibility );在这段区域以外该变量不可见(或无法访问)。词法作用域里,取变量的值时,会检查函数定义时的文本环境,捕捉函数定义时对该变量的绑定。 大多数现在程序设计语言都是采用静态作用域规则,如 C/C++、 C#、 Python 、 Java 、 JavaScript …… 来源: https://zh.wikipedia.org/zh-hans/%E4%BD%9C%E7%94%A8%E5%9F%9F |
2
Mark24 2016-04-29 13:14:10 +08:00
赞,最近正在看这本书,竟然出 Kindle 版了,秒收
|
3
Martox 2016-04-29 19:14:08 +08:00
好人一生平安,终于懂了
|