继续一个人自言自语_。
今天想聊聊 JavaScript 的作用域,以及“闭包”。当然,仍旧带着我的个人特色。
作用域
JavaScript 据我了解只有一种作用域,叫做“函数作用域”,先看栗子:
var a = "a-out";
(function () {
console.log(a);
var a = "a-in";
})();
你觉得上面匿名函数执行后,会输出什么?
实际试下,会发现是undefined
,这个我慢慢来说吧。
首先,JavaScript 没有“块级作用域”,不能在花括号中定义“局部变量”。所有的变量的作用域都是函数级的,也就是说,在函数内部任意的地方声明的变量,在函数内的任意位置都可以使用。
当然,例外就是,不在任何函数内部的变量,就成了“全局变量”啦。同样,在函数内部,忘记使用 var
声明就直接使用的变量,也会成为全局变量,所以很多时候会把函数内部要使用的变量都在头部进行显式声明,以尽量避免麻烦。
回过头来看上面的栗子:
第一行,声明了一个全局变量
a
,然后进行了赋值。匿名函数的第一行,向控制台输出变量
a
的值,由于函数内部的确声明了a
,所以这里输出内部变量a
的值。但是对内部变量a
的赋值在当前行的后面,目前该变量没有值,所以输出的是undefined
。匿名函数的第二行,声明了内部变量
a
,然后进行了赋值。
对于这件事情我的理解:
既然 JavaScript 只有“函数作用域”(我说的),所以脚本解释器会先扫描下函数内部,识别出所有的声明的变量,记录下来。然后,在执行函数的这个阶段,遇到一个“变量名”,就先在函数内部的变量列表里面进行查找,找到了就把这个内部变量的当前值交给当前语句使用(当然如果是赋值语句,则为变量“绑定”了一个新的值)。
那么,如果在函数内部,使用一个变量时,该变量并没有在函数内部声明呢?
我把上面的栗子修改下:
var a = "a-out";
(function () {
console.log(a);
// var a = "a-in";
})();
试一下,会发现这里匿名函数打印出的是"a-out"
。所以,在函数内部没有声明这个变量的话,就在函数的外部来找啦。对于多级嵌套的函数来说,就该是一层一层往外找,直到找到同名的变量,或者到“顶层”也找不到就停下来(这个情况下如果执行函数就出错了,提示变量没有定义)。
当然,这个查找变量的过程并不直观,仅仅是我的描述而已,希望能帮你理解而不是相反。
对于函数的参数,我也看作是函数的内部变量(我不用“局部变量”的说法,但我想你大概知道我指的是什么),不过其值是要等到函数执行时才能确定的。
把函数的定义,和函数的执行分开来看。
函数定义时,是在一个静态的环境下,函数的内外部的环境是固定的。尽管有很多变量的值还在变动,甚至只在每次执行时才能确定,但这并不妨碍我们去“引用”它。在函数执行的过程中,在每个具体的引用到变量的位置,都会被变量当时的值所替代(同样,声明语句例外,是重新赋值)。
特别地,在函数定义阶段(或者说解释器解析而非执行函数时),能够使用哪些变量,也是确定的了。例如,函数内部声明了一个 a
变量,那么函数内部使用到 a
变量的语句,就是跟这个 a
关联的了。而如果内部没有,则在一层层的外部作用域(外部函数的作用域,如果有的话)里查找,如果还没有的话呢?那么你执行函数时就报错了呗。
接着这个话题,我们引出“闭包”。
闭包
对于“闭包”这样严肃的东西,还是来看维基百科的定义:
在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
来看一个栗子:
var count = (function () {
var i = 0;
return function () {
return ++i;
};
})();
count(); // 1
count(); // 2
在这个栗子中,有两个匿名函数,其中一个嵌套在另一个的内部。外层的匿名函数被立即执行了(通过 (function () {})()
的方式),然后的是内部的匿名函数,被作为返回值赋给了变量 count。所以,经过上面的过程,count 的值是一个函数对象。
而 count 所关联的这个函数比较特别,在定义时,它引用了外部函数的变量 i
,于是,外部变量 i
和这个函数构成了上面定义中的“闭包”,也就产生了上面的现象。
那么这一切,和这个诡异的名字一起,有什么作用呢?只是让人觉得迷惑,或者让不熟悉的人大呼“NB”?
我曾经看到过一种说法,是说使用闭包是为了在 JavaScript 中提供一种使用局部变量的机制。且不论这个对于闭包的评价本身正确与否,我们来试着理解下“局部变量”这回事。还是举个栗子:
function Person(name) {
this.name = name;
this.getName = function () {
return this.name;
};
}
假设我们希望,其他人在使用到这个 Person
类(暂且叫做“类”吧,后面我想专门写一篇东西来聊聊 JavaScript 的继承和类)时,只能通过 getName()
来获取 name
,而不能直接访问 name
并改变其值的话,我感觉不太现实。因为 JavaScript 没有提供像 private, public 这样的东东,所有的东西都默认是公开的。而如果非要这样做,毕竟这样做也是有着合理的应用场景的,通常可以通过闭包来严格实现(只是把属性名改为类似 _name
这样来提示他人不要乱改,毕竟不严格不是):
function Person(name) {
var _name = name;
this.getName = function () {
return _name;
};
}
当然,前面提到过,也可以把函数的参数看作是内部变量,所以也可以直接这样:
function Person(name) {
this.getName = function () {
return name;
};
}
我们来使用下这个“类”:
var me = new Person("luobo");
me.getName(); // "luobo"
显然,没有 name
属性,也就无法直接修改这个值啦。(当然,如果要作为“私有”成员使用的是对象,那么即便采用上述方法,由于返回的是对象本身,所以仍旧可以修改对象)
回到上面的问题,关于闭包的作用,我还是觉得:闭包是语言本身提供的一种机制,并不见得就一定是为了什么特定目的而创造的,更加不会是为了创造“局部变量”的这一个目的。根据自己的需要,在合适的地方使用它就是了,只要你是真的会用就好_。
小结
抱歉,今天情绪不佳,写东西不是很有激情,所以上面的文字尽管我的确花了心思,但自己都不太满意。作用域和“闭包”是我理解的 JavaScript 中的一个很重要的主题,花了很长时间我才有了上面的那些体会,但是叙述地有点没有头绪啦。
关于作用域,我认为先要对于 JavaScript 代码的执行有一定的理解。(我说说我的理解吧,欢迎交流。)在浏览器环境下,没有写在任何函数内部的语句,就直接执行了。写在函数内部的代码,则只有等到函数被调用的时候,才会执行。但浏览器虽然没有执行函数,还是会先把函数“读”一遍,“理解”了之后记录下来。这样当任何时候需要使用这个函数的时候,就能直接拿来,结合当时的环境来执行啦。
这个过程的细节中就有关作用域、闭包的身影啦。
当然上面是我个人的理解,描述也不够准确和正确。但是我想对于函数的定义和执行的机制有更深入的理解还是很必要的,特别是想真的对 JavaScript 这门语言有更深入的理解的话。显然,我也还需要加油啊!
关于“闭包”,这真的是一个“高级”的话题。而且,在不领会闭包的原理的情况下,很有可能不知不觉就给自己挖了一个坑出来,我就干过。
var arr = ['a', 'b', 'c'], funcs = [];
for (var i = 0, len = arr.length; i < len; i++) {
funcs[i] = function () {
return arr[i];
};
}
上面这个造作的栗子中,我的本意是:得到一个数组 funcs
,该数组的每一项都是一个返回数组 arr
中对应位置的值的函数。有点绕,不过我想你能明白是什么意思。但是,结果却并非如此:
funcs[1](); // undefined
奇怪,不应该返回 arr[1]
也就是 'b'
吗?
曾经我为此烦恼过....后来我意识到,我不知不觉用闭包给自己挖了这个坑。
如果你还没有看明白(当然,我的叙述本身就乱,难为你了),我来给些提示:
- 试着在控制台输出变量
i
,看下当前值 - 然后在控制台输出
arr[i]
,看下当前值 - for 循环中,其实每次构建的匿名函数,返回的就是
arr[i]
- 通过
i = 1
把变量i
的值改为 1 - 再执行下上面的
funcs[1]()
,或者funcs[0]()
funcs[2]()
都一样,看下结果你应该就能明白了....
好吧,这就是闭包。