前言
在第一节中,我们介绍了可执行代码以及执行上下文的相关知识,对于每个执行上下文,都有三个重要属性:
- 变量对象(
Variable object, VO
) - 作用域、作用域链(
Scope chain
) this
指向问题
这节介绍作用域与作用域链,也是面试重中之重。
一、作用域
JavaScript采用的是词法作用域,即静态作用域。函数的作用域在函数定义的时候就决定了。在之前的章节中,我们提到了执行上下文,函数的执行上下文是在函数调用的时候生成的。
所以两者的区别在于作用域:静态的(写完代码就确定),而执行上下文:动态的(调用的时候才产生)。
二、作用域链
当执行到函数内部(ES6中描述为块级作用域中),当查找一个变量的时候,会先从当前上下文的变量对象中进行查找,如果没有找到,就会从父级执行上下文中的变量对象中进行查找,一直找到顶级的全局上下文中。这种查找路径形成的关系称为作用域链。
关于这个查找过程,下面结合第2节谈到的变量对象进行演示。
三、函数内部属性[[scope]]
首先,JavaScript在运行过程中,函数首先会被创建,然后才能被执行。函数中有一个内部属性[[scope]]
,当函数创建的时候,会保存所有父变量对象
到这个[[scope]]
中,注意,它不会保存子级的变量对象,所以[[scope]]
并不代表完整的作用域链。
例如:
var a = 100;
function f1() {
function f2() {}
}
当函数创建的时候,f1
、f2
各自[[scope]]
情况如下:
f1.[[scope]] = [
globalContext.VO
]
f2.[[scope]] = [
f1Context.VO
globalContext.VO
]
当函数激活的时候,创建函数上下文,创建VO/AO
,将活动对象添加到作用域链的前端。
此时,当前函数的作用域链为:
Scope = [
AO,
[[scope]]
]
注意:问题来了,为什么要把AO
添加到作用域链呢?
我们通过一段代码来演示一下函数创建到执行的过程中,变量对象和作用域链的生成过程是如何的。
四、生成过程
var a = 100;
function f1() {
var b = 200;
function f2(c, d) {
console.log(a, b)
console.log(c, d)
}
f2();
}
f1();
执行过程如下:
-
初始化全局上下文,
f1
函数被创建,保存父作用域链到内部属性[[scope]]
,初始全局VO// 1. 初始化全局上下文 window.[[scope]] = globalContext.VO globalContext = { VO: { a: undefined } } // 2. 创建函数作用域链 f1.[[scope]] = [ globalContext.VO ]
-
调用
f1
函数,进行前期准备工作,创建其函数执行上下文,进行压栈操作,之后复制函数自己的[[scope]]
属性创建当前函数作用域链到自己的上下文中f1Context = { Scope: f1.[[scope]] }
-
创建A0对象,初始化A0对象,如加入形参、函数声明、变量声明。
f1Context = { AO: { arguments: { length: 0 }, f2: f2Function() {} b: undefined, }, Scope: f1.[[scope]] }
-
将活动对象压入
f1
作用域链顶端f1Context = { AO: { arguments: { length: 0 }, f2: f2Function() {} b: undefined, }, Scope: [ AO, globalContext.VO ] }
-
准备工作完成,开始执行函数代码,修改AO对象属性
f1Context = { AO: { arguments: { length: 0 }, f2: f2Function() {} b: 200, }, Scope: [ AO, globalContext.VO ] }
-
注意,因为
f1
这里内部还有函数声明f2
,过程同理,在进入f1
的时候,也会创建f2
函数,创建函数作用域链,创建执行上下文,进行压栈操作,初始化AO对象,运行过程中进行赋值AO对象属性,最后打印的时候根据当前函数的Scope
。回答前面问题,为什么要把AO
添加到作用域链中,即查找顺序是先从作用域链最前端查找,即当前AO查找是否存在,没有的话再查找上一级[[scope]]
,如此进行递归查找。f2.[[scope]] = [ f1Context.VO, globalContext.VO ] f2Context = { AO: { arguments: { 0: c, 1: d length: 2 }, }, Scope: [ AO, f1Context.VO, globalContext.VO ] }
五、总结
本节介绍了作用域与作用域链的关系,同时作用域链形成的原因,再介绍JS在运行过程中是如何编译函数内部的代码,最后通过案例演示了作用域链形成和查找的全过程,在遇到此类问题的时候,可以直接套这个方式进行拆解分析。