前面讲过,当前上下文(作用域)内声明的变量或函数,是以属性的形式,放到一个变量对象(Variable Object)上的。但由于 VO 是无法通过代码访问的,因此在函数调用的时候 VO 被激活形成一个活动对象(Activation Object),它是可以被访问到的(可以简单的理解为 AO 是 VO 浅拷贝的一个引用)。
但是,AO 是没有原型的。假设我们在当前作用域下查找一个变量 a,相当于从 AO 上查找 a 属性。假设 AO 本身没有该属性,自然会往 AO 原型上查找,但很遗憾 AO 没有原型,即当前作用域下查找不到该变量(或称为属性)。然后往作用域链的上一级 AO 中查找......查找规律同理......直到全局作用域(其 VO 就是 window 对象)下的 window 对象查找。由于 window 对象是有原型的,如果自身找不到 a 属性,就会往 window 的原型上查找,查到就返回,查不到就抛出 ReferenceError。
继上一篇文章 JavaScript 脚本编译与执行过程简述,再来介绍一下 JavaScript 中的作用域链(Scope Chain)。
函数的
[[scope]]
也是与闭包直接相关。并推荐专题:作用域链的形成
作用域链(以下简称
Scope
)与执行上下文相关。因此,我们可以大致当前上下文的作用域链:
Scope = AO + function.[[scope]]
。函数内部 [[scope]] 属性的形成
当函数被创建的时候,属性
[[scope]]
会保存所有的父级变量对象。举个例子:
上述例子,函数
foo
处于全局上下文。而全局上下文中所声明的函数,它们的[[scope]]
是GlobalContext.VO
,即window
对象。因此foo.[[scope]] = [ GlobalContext.VO ]
。再看:
同样地,函数
foo
的[[scope]]
属性为GlobalContext.VO
。然后调用foo
函数,进入foo
函数上下文并进行初始化,包括以下过程:foo.[[scope]]
为基础初始化函数上下文的Scope
。AO
对象,包括Arguments
、形参、函数声明、变量声明。 该过程若有函数声明,对应函数的[[scope]]
也将会被确定,其值就是Scope
。AO
初始化完成后,将AO
插入上下文的Scope
中。因此有两个结论:
注意,即便是函数表达式,它在代码执行的时候,才会确定其
[[scope]]
,由于执行过程中AO
也会跟着更新,且它们是引用关系,因此总能确保,当前作用域内的函数(函数声明或函数表达式)的[[scope]]
总是AO + 各父级上下文的 AO/VO
。但是使用
Function
构造器来创建一个新的函数,该函数的[[scope]]
只有GlobalContext.VO
。下面的示例中,执行bar
函数会去作用域链上查找a
变量,可它的作用域链只含全局对象,导致找不到a
变量而抛出ReferenceError
。因此,尽量不要使用构造函数的方式来创建函数。
影响作用域链的一些例子
一般情况下,一个作用域链
Scope
包括父级变量对象、函数上下文的活动对象AO
,并从当前上下文逐级往上查询。其实作用域链的原理跟原型链很类似,当前如果这个变量在自己的作用域中没有,那么它会往父级查找,直至最顶层(全局对象),再查找不到就会抛出
ReferenceError
。前面讲过,当前上下文(作用域)内声明的变量或函数,是以属性的形式,放到一个变量对象(Variable Object)上的。但由于
VO
是无法通过代码访问的,因此在函数调用的时候VO
被激活形成一个活动对象(Activation Object),它是可以被访问到的(可以简单的理解为AO
是VO
浅拷贝的一个引用)。但是,AO 是没有原型的。假设我们在当前作用域下查找一个变量
a
,相当于从AO
上查找a
属性。假设AO
本身没有该属性,自然会往AO
原型上查找,但很遗憾AO
没有原型,即当前作用域下查找不到该变量(或称为属性)。然后往作用域链的上一级AO
中查找......查找规律同理......直到全局作用域(其VO
就是window
对象)下的window
对象查找。由于window
对象是有原型的,如果自身找不到a
属性,就会往window
的原型上查找,查到就返回,查不到就抛出ReferenceError
。说那么多,还不如看个例子更清晰:
从例子可以看出
foo
函数上下文下并没有声明a
变量,于是往上一级查找(即全局上下文),那么从window
自身查找,是没有的。但是window
是基于Object
创建的(window instanceof Object
结果为true
),于是从Object.prototype
上查找,并找到a
属性,属性值为"proto"
。如何证明 AO 是没有原型的?
过程就不在赘述了,假设
AO
是有原型的,那么bar
函数上下文中查找a
变量是,应该会取到AO
对象原型上的a
属性"proto"
,但实际情况a
取到的结果是"inner"
。因此可以证明:活动对象 AO 是没有原型的。全局和 eval 上下文中的作用域链
全局上下文的作用域链仅包含全局对象。而 eval 上下文与当前的调用上下文(calling context)拥有同样的作用域链。
代码执行时对作用域链的影响
有些情况下也会包含其他对象,例如执行期间,动态加入作用域链中的,例如
with
语句或者catch
语句。此时作用域链如下:举个例子:
它的作用域链变成了:
Scope = foo + (AO | VO) + [[Scope]]
。上面这个例子可能没有体现出来,我们修改一下:我们来分析一下:
x
、y
、foo
变量。with
语句,会将foo
对象添加至作用域链顶端。with
内部的x
、y
前面已被解析添加,因此它只是一个赋值语句,并不会重新赋值语句。with
内部,给 x、y 赋值,究竟是对应哪个变量。前面提到遇到with
语句会往作用域链顶端插入该对象foo
(注意不会创建一个全新的作用域上下文,只是修改了作用域链而已)。console.log(x)
查找x
变量时,从foo
对象上查找x
属性,并找到,因此foo.x
被修改为3
。foo
对象上没有(其原型也没有),因此往上一级作用域查找(即全局作用域),因此全局作用域下的y
被修改为4
。with
内部的x
、y
分别打印出:3
和4
。with
执行完,作用域链上的foo
对象会被移除。即作用域链上只剩下window
对象。x
、y
、foo
变量都是从全局作用域下查找的,因此会分别打印出1
和4
。foo
对象是更新变为:{ x: 3 }
。The end.