注:本文大致相当于《Professional JavaScript for Web Developer 2nd Version》关于变量作用域和闭包的摘录总结,如果你已经看过,请无视
对于习惯于使用C++/Java等语言的同学,对于JavaScript中的变量作用域会感到非常困惑,好像颠覆了自己原来的认识,但是慢慢学习和梳理,你会发现这种设计有它的理由,也很符合JavaScript的使用场景,在这个过程中对于脚本语言的特点也会有一点体会(暂时还无法语言系统地表达)。
要想搞懂JavaScript中变量的作用域,首先要理解以下两个概念:
1.Excuation Context(运行上下文)
运行上下文(以下简称context),决定了一个变量或者函数能够访问的其它数据,以及这个变量或者函数的行为。这让说还是有点抽象,我们可以将context理解为一个对象,这个对象中包含了我们定义的变量和函数,处于同一个context中的对象或者函数,对于这个context所能访问的数据具有同样的访问权,而处于不同context中的则不相同,事实上JavaScript的解释器实现也存在这么一个幕后的对象。
再说得直观一点,context可以分为两种,一种是全局(global)的,一种是局部(local)的,对于浏览器环境,全局context可以认为是window对象,因此在JavaScript中定义的全局变量和全局函数都是window对象的属性和方法。
相对于全局context,函数体内定义的变量和函数则属于一个局部context对象,这个对象程序员无法访问,跟闭包有关系,后面会详细描述。当一个函数被调用时,它的context被压入context栈中,当它运行完成时,这个context被弹出然后销毁,然后整个JavaScript的执行过程就是各种context切换的过程,有点像某部电影+_+
2.Scope Chain(作用域链)
当一个context中的代码被执行,也即是一个函数被调用的时候,会创建一个scope对象(同样程序员无法访问,据说FireFox中这个对象叫_parent_,试了一下好像没有找到),这个scope对象是一个链表式的数据结构,因此被称为scope chain。这个链的每个节点都是一个context对象的引用,最后一个节点是全局context,当代码运行需要访问一个变量时,就会沿着这个链表的,从头到尾在context中搜索这个变量,直到找到为止,如果直到全局context中都没有找到,就会报错。
context中变量的访问规则:被包含的context(内层)中的变量和函数可以访问包含context(外层)中的变量和函数,但是反过来却不行。
来看一个例子吧:
var color = “blue”;
function changeColor(){
var anotherColor = “red”;
function swapColors(){
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
//color, anotherColor, and tempColor are all accessible here
}
//color and anotherColor are accessible here, but not tempColor
swapColors();
}
//only color is accessible here
changeColor();
上面的代码中,函数swapColors被定义在函数changeColor中,它可以访问全局变量color以及外层函数changeColor中的变量anotherColor,但是同样在changeColor函数中却无法访问到swapColors中的tempColor对象。下面的图可以帮助理解:
这张图描述了上面那段代码的context关系,全局context window中包含有全局属性color和全局函数changeColor,changeColor中包含了swapColors函数,因此它的context中包含了swapColors函数的context,根据上面的context访问原则,可以判断出tempColor只能在swapColors的context中被访问,但是这个context中却可以访问到color和anotherColor。
再来看一个关于scope chain的例子:
var color = “blue”;
function getColor(){
return color;
}
alert(getColor()); //”blue”
在这段代码中,函数getColor中并没有定义一个叫color的变量,但是它却可以正确运行。这是因为解释器会沿着getColor函数调用时创建的scope chain向上查找,然后返回了全局变量color。
理解了上面两点,对于闭包的概念和原理也就很好理解了。
闭包的概念是:闭包是由函数和与其相关的引用环境组合而成的实体。
直接看这个概念,可能难以理解,简单来说,闭包可以理解为可以访问包含它的外层函数的作用域链对象中的context的函数。看下面的代码:
function createComparisonFunction(propertyName) {
return function(object1, object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2){
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
};
}
//create function
var compareNames = createComparisonFunction(“name”);
//call function
var result = compareNames({ name: “Nicholas” }, { name: “Greg”});
//dereference function - memory can now be reclaimed
compareNames = null;
其中函数createComparisonFunction返回的匿名函数就是一个闭包。它可以访问到包含它的外层函数createComparisonFunction的参数propertyName(参数也属于外层函数local context中的变量),即使在createComparisonFunction函数已经返回之后。
这里面的原理也就是之前所说的Scope Chain(作用域链),就是返回的匿名函数的Scope Chain中包含了外层函数的context对象,具体的过程是这样的:
在每个函数被调用时,它的scope chain被创建并初始化,在scope chain最前面的是一个叫activation object的调用对象,这个对象包含有argument数组对象和this指针,以及传入的参数,在activation之后是这个函数的外层函数的context对象,然后是再外层的,一直到全局的context。函数执行时就会在这个链上查找变量,函数返回之后,scope chain以及其中的对象都会销毁。
但是对于闭包的情况却不是这样,对于一个充当闭包的匿名函数,它在被创建的时候,它的scope chain就已经被创建并且初始化好了,里面包含有外层函数直到全局的context对象,但是acivation对象还是要等到被调用时才会被置入(这听起来有点让人困惑,但是想一想,如果等到这个匿名函数的scope chain在被调用的时候才创建,对于顺序解析的解释型语言,似乎已经无法知道它的外层context是什么了)。由于这个匿名函数的scope chain中包含有对外层函数的context对象的引用,这些context对象在外层函数返回之后也不会被销毁,使得返回的匿名函数可以正常地工作,除非显式地将引用匿名函数的变量置为null,这些context对象才会被销毁。
compareNames函数调用时scope chain中的对象及其关系如下图:
compareNames包含了一个匿名函数,以及外层函数createComparisonFunction的context的引用,这和上面的闭包定义是一致的。
上面就是JavaScript中的闭包的用法和实现原理,关于闭包的作用和实际使用,接下来会另外总结。
闭包的定义参考了http://blog.dccmx.com/2011/02/closure/,里面关于闭包的含义和作用讲解也非常清楚。