Skip to content

作用域 & 执行上下文

先看一段源代码,分析一下变量提升和预编译的过程:

js
var GLOBAL = {}
function fn(c) {
  console.log(c) // true
  var c = false
  console.log(a) // function a(){}
  var a = 1
  console.log(a) // 1
  function a() {
    // wo can do something
  }
  console.log(a) // 1
  b() // 函数声明
  var b = function () {
    console.log('函数表达式')
  }
  function b() {
    console.log('函数声明')
  }
  b() // 函数表达式
  var d = 4
}
fn(true)

JS 解释引擎工作流程:

  • 当页面打开时,JS 解释引擎会首先查看 JavaScript 代码中有没有低级的语法错误,如果有,代码就不会执行。如果没有,js 代码就会进入预解析(编译)阶段。
  • 预解析阶段,首先创建 GO 全局对象,然后逐行的执行代码,碰到函数执行,则创建 AO 局部对象。

JS 的执行分为:解释和执行两个阶段

  1. 解释阶段
  • 词法分析
  • 语法分析
  • 作用域规则确定
  1. 执行阶段
  • 创建执行上下文
  • 执行函数代码
  • 垃圾回收

一、什么是 GO(Global Object):

  • 含义:GO 是指全局作用域的全局对象(window)
  • 形成时间:页面打开时
  • 销毁时间:页面关闭时

_js 全局预编译阶段,GO 对象的形成步骤:

  • 创建一个 GO{}对象
  • 找变量申明,将变量名作为 GO 对象的属性名,值是 undefined
  • 找函数申明,值赋值函数体(function)

比如上面

二、什么是 AO(Activation Object)

  • 含义:AO 是指局部作用域的局部对象
  • 形成时间:函数执行时
  • 销毁时间:函数执行完毕

当一个函数执行时,AO 对象的整个赋值形成过程分为四个步骤:

  • 创建一个 AO{}对象
  • 找到函数中的形参和变量,作为 AO 对象的属性名,这些属性名的属性值为 undefined
  • 形参与实参相统一,即将实参的值赋值给形参
  • 找到函数中的函数声明(非函数表达式),将函数名作为 AO 对象的属性名,值为函数体

比如上面 fn 函数执行时,AO 局部对象的整个赋值过程如下:

js
function fn(c) {
  // 第一步:创建AO{}对象
  /**
   * AO {}
   */

  // 第二步:找形参和变量
  /**
        AO {
            c: undefined,
            a: undefined,
            b: undefined,
            d: undefined
        }
    */

  // 第三步:形参与实参相统一
  /**
        AO {
            c: true,
            a: undefined,
            b: undefined,
            d: undefined
        }
    */

  // 第四步:找函数声明,函数名作为AO的属性名
  /**
        AO {
            c: true,
            a: function(){},
            b: function(){},
            d: undefined
        }
    */
  // 经过上面四个步骤,下面这一行代码执行时,AO对象如上所示
  console.log(c) // true
  var c = false
  console.log(a) // function a(){}
  var a = 1 // Tips: 这里将 AO对象中 a属性赋值为1
  console.log(a) // 1
  function a() {
    // wo can do something
  }
  console.log(a) // 1
  b() // 函数声明
  var b = function () {
    // Tips: 这里将 AO对象中的 b属性赋值为一个函数表达式
    console.log('函数表达式')
  }
  function b() {
    console.log('函数声明')
  }
  b() // 函数表达式
  var d = 4 // Tips: 这里将 AO对象中 d属性赋值为4
}
fn(true)

当程序执行到 ‘console.log(c)’ 这一行时,AO 对象如下图所示:

An image

留个坑后面再写......

三、什么是 VO(Variable Object)

  • 含义:VO 是变量对象,是一个与执行上下文相关的特殊对象
  • 包含:
    • 变量 (var, 变量声明);
    • 函数声明 (FunctionDeclaration, 缩写为 FD);
    • 函数的形参;
  • VO 与 GO/AO 的关系,如下所示:
js
抽象变量对象VO (变量初始化过程的一般行为)

  ╠══> 全局上下文变量对象GlobalContextVO
  ║        (VO === this === global)

  ╚══> 函数上下文变量对象FunctionContextVO
           (VO === AO, 并且添加了<arguments><formal parameters>)

四、 作用域

当前正在执行的代码能够访问到变量的范围。

  • 词法作用域:是在写代码或者定义的时候确定的,关注函数在何处申明
  • 动态作用域:是在运行时确定的,关注函数从何处调用

js 采用词法作用域,其作用域由你在写代码是将变量和块作用域写在哪里决定的,因此当词法分析器处理代码时会保持作用域不变,

作用域链

每个函数执行都需要开辟一个私有的执行上下文,创建执行上下文会初始化作用域链。 作用域链包括当前函数存在的作用域以及所有父级作用域,当函数访问变量的时候,就会顺着这个作用域链去查找变量。

分类

  • 全局作用域
  • 函数作用域
  • 块级作用域(es6 let const)

五、 执行上下文(EC)

简述:当前 js 代码被解析和执行时所在的环境
由 js 引擎自动创建对象,包含对应作用域中的所有变量属性
执行上下文是指:代码执行或者函数调用时 在执行栈中产生的变量对象,这个变量对象我们无法直接访问,但是可以访问其中的变量,this 对象等

执行上下文包含三个部分

  1. 创建变量对象(Variable object ,VO)
  2. 创建作用域链(scope chain)
  3. 确定 this 值

代码模拟执行上下文如下:

js
//执行上下文对象创建
ExecutionContext:{
  //变量对象|活动对象(VO | AO)
  [variable object | activation object]:{
    arguments, //函数参数,全局执行上下文中没有arguments
    variable, // 变量
    funcion, // 函数声明
  },
  //作用域链接
  scope chain:variable object + all parents scope, //变量对象和所有的父作用域
  //this指向
  thisValue: context object

}

类型

  • 全局执行上下文:默认最基础的执行上下文,不在任何函数中的代码都位于全局上下文中,一个程序中只能存在一个全局上下文
  • 函数执行上下文:每次函数调用(包括同一个函数多次调用)时,都会为该函数创建一个新的执行上下文,但是只有函数被调用时才会被创建,一个程序中可以存在多个函数执行上下文。
  • eval 执行上下文

执行上下文的生命周期

一共有三个阶段:创建阶段、入栈执行阶段、出栈回收阶段

  • 全局执行上下文:准备执行代码前产生,当页面关闭/刷新时销毁
  • 函数执行上下文:调用函数时产生,函数执行完销毁

执行栈(Ecstack)

执行栈又叫调用栈,是用来存储 js 程序中创建的“执行上下文”的一种先进后出的结构,入栈和出栈对应的是内存的申请和释放。

当 js 代码执行的时候,首先会创建一个全局的执行上下文,并压入到当前执行栈中,之后当发生函数调用的时候,js 引擎会为该函数创建一个“函数执行上下文”并压入到栈顶,当该函数执行完毕,其对应的“执行上下文”会被移除当前执行栈,调用栈最上面的那个“执行上下文”一直处于正在执行状态。

六、 作用域和执行上下文区别

  • 作用域:是静态的,代码和函数定义的时候就确定了,一旦确定就不会变化
  • 执行上下文:动态的,代码和函数执行的时候创建的,代码执行完就消失
  • 联系:执行上下文环境是在对应的作用域中

js 的代码在解释阶段就会确定作用域规则,因此作用域在函数定义的时候就已经确定了,而不是在函数执行的时候才确定,但是执行上下文是函数执行之前创建的,执行上下文最明显的就是 this 的指向是执行的时确定的,而作用域访问变量是编写代码的结构确定的。

七、 总结

一个作用域下

  • 可能包含多个执行上下文环境(全局作用下有若干个函数被调用)
  • 有可能从来没有过上下文环境(函数从来没有被调用过)
  • 有可能有过(函数被调用过,上下文环境被销毁)
  • 有可能同时存在一个或多个上下文环境(闭包)
  • 同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量值。

作用域只是一个“地盘”,其中没有变量。变量是通过作用域对应的执行上下文环境中的变量对象来实现的。所以作用域是静态观念的,而执行上下文环境是动态上的,两者并不一样。 同一个作用域下,对同一个函数的不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值,所以,作用域中变量的值是在执行过程中确定的,而作用域是在函数创建时就确定的。 如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中找到变量的值。

引申

与通过 var 声明的有初始化值 undefined 的变量不同,通过 let/const 声明的变量直到它们的定义被执行时才初始化。在变量初始化前访问该变量会导致 ReferenceError。该变量处在一个自块顶部到初始化处理的“暂存死区”中。

使用 let / const 声明的全局变量,会被绑定到 Script 对象而不是 Window 对象,不能以 Window.xx 的形式使用;使用 var 声明的全局变量会被绑定到 Window 对象;使用 var / let / const 声明的局部变量都会被绑定到 Local 对象。注:Script 对象、Window 对象、Local 对象三者是平行并列关系。

Script 对象,如下图所示

An image