You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
前面讲过,当前上下文(作用域)内声明的变量或函数,是以属性的形式,放到一个变量对象(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.
The text was updated successfully, but these errors were encountered: