Where is JavaScript?

JavaScript入坑之作用域与执行上下文栈

本文讨论JavaScript的词法作用域和函数执行过程中的执行上下文栈具体如何变化响应。

作用域

作用域是指程序源代码中所d定义变量的生效区间
作用域规定了如何查找变量,确定执行代码对变量的控制范围
JavaScript 采用 lexical scoping,即词法作用域
核心就一句话:函数的作用域在函数定义的时候就决定了

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope();

这段代码会打印 local scope

JavaScript 函数的执行依赖于作用域,而作用域是在函数定义的时候创建的。函数 f() 定义在 checkscope() 内,将当前函数 checkscope 的局部作用域放在作用域链的顶端,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。

执行上下文栈

函数是 JavaScript 中的第一公民,因此,JavaScript 代码执行也是按照函数分段执行。
函数拥有自主的执行上下文栈

每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
function foo(i) { if(i == 3) { return } foo(i+1) console.log(i) } foo(0)

分析前先约定一些通用的名词

EC: 执行上下文
ECS: 执行环境栈
VO(变量对象): 创建执行上下文时与之关联的会有一个变量对象,该上下文中的所有变量和函数全都保存在这个对象中
AO(活动对象): 进入到一个执行上下文时,此执行上下文中的变量和函数都可以被访问到,可以理解为被激活
scope chain: 作用域链

由于函数内部有递归,在递归条件满足并跳出后,此时 ECS 的顺序是
https://static.littlewin.wang/blog/35-1.png
依次出栈输出结果 2, 1, 0

EC的建立过程

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
建立阶段:(函数被调用,但是还未执行函数中的代码) 1. 创建变量,参数,函数,arguments 对象 2. 建立作用域链 3. 确定this的值 执行阶段:变量赋值,函数引用,执行代码

执行上下文为一个对象,包含 VO,作用域链和 this

              
  • 1
  • 2
  • 3
  • 4
  • 5
executionContextObj = { variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ }, scopeChain: { /* variableObject 以及所有父执行上下文中的variableObject */ }, this: {} }

具体过程:

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
1. 找到当前上下文调用函数的代码 2. 执行代码之前,先创建执行上下文 3. 创建阶段: 3-1. 创建变量对象(VO): 1. 创建arguments对象,检查当前上下文的参数,建立该对象下的属性和属性值 2. 扫描上下文的函数申明: 1. 每扫描到一个函数什么就会在VO里面用函数名创建一个属性, 为一个指针,指向该函数在内存中的地址 2. 如果函数名在VO中已经存在,对应的属性值会被新的引用覆盖 3. 扫描上下文的变量申明: 1. 每扫描到一个变量就会用变量名作为属性名,其值初始化为undefined 2. 如果该变量名在VO中已经存在,则直接跳过继续扫描 3-2. 初始化作用域链 3-3. 确定上下文中this的指向 4. 代码执行阶段 4-1. 执行函数体中的代码,给VO中的变量赋值

举个例子

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
function foo(i) { var a = 'hello'; var b = function privateB() {}; function c() {} } foo(22);
  1. foo 函数创建,保存作用域链到内部属性[[scope]]

                  
    • 1
    • 2
    • 3
    foo.[[scope]] = [ globalContext.VO ]
  2. 执行 foo 函数,创建 foo 函数执行上下文,foo 函数执行上下文被压入执行上下文栈

                  
    • 1
    • 2
    • 3
    • 4
    ECStack = [ fooContext, globalContext ]
  3. 做 foo 函数执行钱的准备工作

    • 复制函数[[scope]]创建作用域链
    • 用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
    • 将活动对象压入 checkscope 作用域链顶端
                    
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      fooContext = { AO: { arguments: { 0: undefined, length: 1 }, i: undefined, c: pointer to function c(), a: 'hello', b: pointer to function privateB() }, Scope: [AO, [[Scope]]] }
  4. 准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值

                  
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    fooContext = { AO: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c(), a: 'hello', b: pointer to function privateB() }, Scope: [AO, [[Scope]]] }
  5. 函数执行完毕,函数上下文从 ECS中 弹出

                  
    • 1
    • 2
    • 3
    ECStack = [ globalContext ]

一些阐述

VO 和 AO 到底是什么关系

AO = VO + function parameters + arguments

未进入执行阶段之前,变量对象 VO 中的属性都不能访问;但是进入执行阶段之后,活动对象 AO 粉末登场,AO 除了 VO 之外,还包括函数执行时传入的参数和 arguments 这个特殊对象。它们作用在执行上下文的不同生命周期。

函数提升 和 变量提升
              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
function test() { console.log(a) console.log(foo()) var a = 1 function foo () { return 2; } } test()

我们都知道,变量声明被提到最前(不会报出变量不存在的错误),所以上述代码会打印 undefined2

换种写法

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
function test() { console.log(a) console.log(foo()) var a =1 var foo = function () { return 2 } } test()

先给结果

              
  • 1
  • 2
# undefined # TypeError: foo is not a function

JavaScript解析器会在自身作用域内将变量和函数声明提前(hoist)
同一作用域下,函数提升比变量提升得更靠前

也就是说,上面的例子其实被解析器理解解析成了以下形式:

              
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
function test() { var a var foo console.log(a) console.log(foo()) a = 1 foo = function () { return2 } } test()

这样也就可以解释,为什么在函数表达式之前调用函数,会返回错误了,因为它还没有被赋值,只是一个未定义变量,当然无法被执行。

建议大家

  • 变量和函数声明一定要放在作用域/函数的开头
  • 函数表达式在赋值后再去使用

本文于 2015-10-4  发布在  编程  分类下, 当前已被围观 380 次

并被添加「 JavaScript 」标签

本站使用「 署名 4.0 国际」创作共享协议