之前写到的 JavaScript - 作用域 中有提到作用域以及在 JavaScript - 提升 中提到了变量声明和函数声明的提升,在这里做一些深入的探究。
先来了解两个概念:
变量对象
每一个执行环境(Execution Context)都有一个与之关联的变量对象(Variable Object),在全局的执行环境中,window 就是其变量对象。
在函数执行环境中,其变量对象就是所有的局部变量以及局部函数的集合,在最开始时只有 arguments 对象。
函数的 [[Scope]]
函数的生命周期会经历几个阶段:
1、函数的创建
2、函数的调用
3、函数的销毁
在函数创建阶段,会创建内部属性 [[Scope]],[[Scope]] 是所有父变量对象的层级链。[[Scope]] 属性是静态的,并且会永远存在,直到函数被销毁。
思考如下代码:
1 | var b = 2 |
以上代码的执行过程如下:
1、初始化阶段,执行环境栈中只包含全局执行环境:
1 | ECStack = [ |
全局环境执行代码完毕之后,foo 函数也就创建完毕,[[Scope]] 属性也已经定义好:
1 | foo.[[Scope]] = [ |
2、准备执行 foo 函数,此时进入了 foo 的执行环境,其执行环境被推入栈中:
1 | ECStack = [ |
此时可以发现 foo 中的 a 变量已经被赋值为 undefined,这就是「提升」。
foo 函数的执行环境已经准备好,作用域链也创建完毕,可以执行 foo 的代码。
3、执行 foo 函数的代码:
1 | ECStack = [ |
执行 foo 函数的代码时,需要对 b 变量赋值,于是顺着 foo 函数执行环境的作用域链一层层往上搜索,当搜索 fooContext.AO 时,没有搜索到 b 标识符,继续往上搜索 foo.[[Scpoe]] 也就是 globalContext.VO,搜索到了 b 标识符,随即对其进行赋值。
这个搜索过程只能够从作用域链的最前端往后开始搜索,不能从后端往前端方向搜索。这就是作用域链的作用。
总结
函数在创建时会创建 [[Scope]] 内部属性,定义为父变量对象的层级链。静态的,不可变,可以不调用函数,但是 [[Scope]] 内部属性会一直存在,直到函数被销毁。
在执行函数中的代码时,会把函数执行环境关联的变量对象与函数的 [[Scope]] 内部属性组成作用域链,执行代码时通过这个作用域链查找变量。
参考
深入理解 JavaScript 系列(14):作用域链(Scope Chain)
深入理解 JavaScript 系列(12):变量对象(Variable Object)
《JavaScript 高级程序设计(第三版)》