学习javascript时最困惑的就是函数及其作用域,钻研了这么久也有了一些心得,以下为个人见解,如果有错误请指出,谢谢~
本文中没有讨论通过Function()构造函数创建函数时的情况,因为这种情况下会动态地创建和编译javascript代码,并且创建出来的函数并不使用词法作用域(请参考《JavaScript权威指南》),而是被当作顶级函数来编译。
作用域指的是一个抽象的范围,如果解析一个变量名时能够找到它则说明这个变量存在于这个作用域中。而具体查找的过程则是顺着作用域链从头到尾来查找的。
先来看一段代码,默认在全局作用域内执行,方便起见可以直接在Firebug控制台执行:
var a=0; var b=1; function F(x) { var b=0; arguments.callee.c=1; alert(F.c); alert(F.e); alert(arguments.callee === F); function G(y){ alert(y); alert(F.e); }; G(2); } F.e=0;
我们知道每个javascript执行环境都有一个和它相关联的作用域链,在顶层代码中(不属于任何函数定义的代码),作用域链只由一个对象构成,即全局对象。因此执行之后作用域链就是全局对象,如下所示(注意我们这时候只是定义了函数,但并没有执行):
接下来进入重点了,执行以下代码:
F(1);
在执行一个函数时,会根据函数体创建一个调用对象并且把它添加到作用域链的头部,并且更新指向作用域链头部的指针为新创建的调用对象。函数体内部通过var语句声明的局部变量,有名称的嵌套函数以及函数的形式参数都是这个调用对象的属性。此时作用域链如下所示:
注意F引用的是函数对象,不是调用对象(从作用域链头部开始解析“F”,在全局对象中找到了“F”)。调用对象的属性arguments具有特殊的作用,arguments.callee引用的是该函数的函数对象,即F。因此执行
arguments.callee.c=1;
函数对象F增加了一个属性c,并且定义了嵌套函数G,此时作用域链如下:
因此执行
alert(F.c); alert(F.e);
依次显示1,0。并且执行
alert(arguments.callee === F);
显示true。
执行
G(2);
由于调用了函数,因此又创建了新的调用对象并且添加到作用域链头部,此时作用域链如下:
所以执行
alert(y); alert(F.e);
分别显示2,0。
每当退出一个函数便会移除并销毁相对应的调用对象(因为没有任何引用了,所以被垃圾回收了),作用域链也就恢复到了之前的状态,形成了一种类似堆栈的效果。
最后来看一下闭包,执行以下代码:
function F(){ var id=0; function G(){ return id++; } return G; } var H=F();
执行完毕之后虽然已经退出了函数,但是其调用对象依旧存在,因为全局对象H引用了其内部的函数(弧形箭头所示),如下:
然后执行
H();
注意此时由于H引用的是G,因此指向作用域链头部的指针会从指向全局对象直接改变为指向根据G函数新创建的调用对象B,B成为了作用域链的头部,然后是之前就已经存在的调用对象A和全局对象。作用域链如下:
退出函数之后指向作用域链头部的指针则又重新指向了全局对象,B会被垃圾回收销毁,但是A依然会存在,因为仍然有H引用。