谈谈我对JS作用域的理解
Javascript语言在设计之初,就将函数设计成一种包含可执行代码逻辑的特殊对象。作为对象,函数可以像普通对象变量一样拥有可以编程读写的属性,也可以像普通变量一样传递、被引用。但是问题也来了,当函数执行时,解释器如何对代码内部的标示符进行解析呢?JS是这样做的,当函数对象被创建时,或者说函数被定义时,函数对象内部不仅包含了代码逻辑,还定义了一个内部属性[[Scope]]引用了一条作用域链(可以理解成为一个对象列表)。如果这个函数在全局环境下被定义,那这个作用域链里就只有全局作用域。
这样说比较抽象,我们来直接分析下面的全局函数:
var a = function(x){ var b = 'bb'; return b; };
当这样简单一个函数进入浏览器,浏览器中的js解释器开始解释代码,因为在解释代码的时候会有标示符解析,所以在这之前,js解释器需要扫描全局,初始化全局作用域,我们编程定义的a变量也会在这个时候就被放到全局作用域里(定义提前),但是a变量仍然是undefined。当解释器自上而下解释到这段代码时,它会首先创建一个匿名函数,并将全局作用域压进函数对象的内部属性所引用的作用域链(对象列表)里。然后再把它赋值给变量a。
如下图所示:
那么当我们执行a函数时又会发生什么呢?
a();
首先,会创建一个内部对象,我们称这个内部对象为该函数的“执行期上下文”,一个函数的执行期上下文定义了一个函数执行的环境。函数每次执行都会创建独一无二的执行期上下文,当函数执行完毕,该执行期上下文就会被销毁。每个执行期上下文对象里都有自己的作用域链(对象列表),用来解析标示符。当执行期上下文被创建时,它的作用域链会被初始化为当前执行的函数对象里的[[Scope]]属性中所包含的对象。这些值按照它们原有的顺序被复制到执行期上下文的作用链里。这个过程一旦完成,就如图所示:
然后,解释器创建一个称为“活动对象(activation object)”的新的内部对象,这个活动对象包含当前这个执行函数里所有的局部变量(内部var 声明的),命名参数(形参),参数合集(arguments)以及this。然后此对象会被推入执行期上下文里作用域链的前端。如下图:
这时候大家是不是想到了javascript函数作用域,变量声明提前的问题呢?这个问题的根源就在这里。从上到下执行函数内部指令之前,其实解释器就已经提前获取了变量声明了^_^。(变量声明提前还有很多细节,比如声明提前但赋值不提前,函数的两种定义方式表现不一致的问题,在此就不细说了)。当函数执行上下文被创建好之后,解释器开始自上至下的解释代码了,这时每遇到一个标示符,都会遍历这个执行上下文里的作用域链:首先在最顶层的活动对象里去找,如果没找到,再找下一层,最终找到全局作用域,如果还没找到一般会报语法错误,但是当对未定义的变量执行赋值运算时,解释器会在全局作用域创建该变量并赋值。这一点很重要,编程时要小心,防止污染全局环境,或者造成内存泄露的问题。
以上是个简单的函数,如果出现了函数嵌套又会怎么样呢?上代码:
var a = function(x){ var b = 'bb'; var inner = function(){ var c = 'cc'; }; return b; };
在a函数里又定义了个inner函数,a函数在执行时,inner函数被定义,也就是说inner函数对象被创建,码字太辛苦直接上图了:
从图中很明显看到,inner函数对象在创建时内部[[Scope]]属性指向的作用域链初始化为a函数执行期上下文的作用域链。当执行inner函数时也是先创建执行期上下文。执行期上下文的创建过程和执行a函数时的一样,这里就不重复了。
如果再嵌套几层,就会这样迭代下去。。。。。
好吧,先说到这里,本来还想说说闭包的(不过相信大家已经找到他们之间的联系了吧........^_^),不过时间有限,下次再说吧。
本文参照了《高性能Javascript》《Javascript权威指南》。
如有错误,不用客气直接指出,大家一起学习一起成长。。。