【十条总结】理解JS执行上下文、VO、AO、Scope、闭包、this

JS 执行上下文

一、含义:

当 JS 引擎解析到可执行代码片段(通常是函数调用阶段)的时候,就会先做一些执行前的准备工作,这个 "准备工作" ,就叫做 "执行上下文(execution context 简称 EC)" 或者也可以叫做执行环境。

二、类型(ES3)

  1. 全局执行上下文 :这是默认或者说是最基础的执行上下文,一个程序中只会存在一个全局上下文,它在整个 javascript 脚本的生命周期内都会存在于执行堆栈的最底部不会被栈弹出销毁。全局上下文会生成一个全局对象(以浏览器环境为例,这个全局对象是 window),并且将 this 值绑定到这个全局对象上。

  2. 函数执行上下文:每当一个函数被调用时,都会创建一个新的函数执行上下文(不管这个函数是不是被重复调用的)

  3. Eval 函数执行上下文 :执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于并不经常使用 eval,所以在这里不做分析。

三、包括(ES3)

1. 变量对象VO(variable object)

VO即Variable Object 变量对象,存储全局变量和函数,例如:

css 复制代码
let a = 1
let arr = [1,2,3]
let obj = {id:107}
function fn(){ ... }

// globalEC
globalEC = {
  VO:{
    a: 1,
    arr: [1,2,3],
    obj: {id:107},
    fn: function fn(){ ... }
  }
}

有一点需要注意,只有函数声明(function declaration)会被加入到变量对象中,而函数表达式(function expression)会被忽略。

php 复制代码
// 这种叫做函数声明,会被加入变量对象
function a () {}

// b 是变量声明,也会被加入变量对象,但是作为一个函数表达式 _b 不会被加入变量对象
var b = function _b () {}

全局执行上下文和函数执行上下文中的变量对象还略有不同,它们之间的差别简单来说:

(1) 全局上下文中的变量对象就是全局对象 ,以浏览器环境来说,就是 window 对象。

(2) 函数执行上下文中的变量对象内部定义的属性 ,是不能被直接访问的,只有当函数被调用时,变量对象(VO)被激活为活动对象(AO)时,我们才能访问到其中的属性和方法。

2. 活动对象AO(activation object)

AO即Activation Object 活跃对象,,存储局部变量和子函数以及arguments。

函数进入执行阶段时,原本不能访问的变量对象被激活成为一个活动对象,自此,我们可以访问到其中的各种属性。 其实变量对象和活动对象是一个东西,只不过处于不同的状态和阶段而已。例如:

scss 复制代码
function fn(a,b){
  var c = 3,
  var fn2 = function(){
    let d = 4
    console.log(a+b+c+d)
  }
  fn2()
}
fn(1,2) // 10

// fn函数开始执行前,创建fnEC
fnEC = {
  AO:{
    arguments:{
      '0':1,
      '1':2,
      length:2
    },
    a:1,
    b:2,
    c:undefined, 
    fn2:undefined, 
  }
}
// 将fnEC推入执行上下文栈
ECStack.push(fnEC) // [globalEC,fnEC]

// fn函数执行的过程中慢慢填装AO
fnEC = {
  AO:{
    arguments:{ ... },
    a:1,
    b:2,
    c:3,
    fn2:function(){ ... }
  }
}
// 执行内部函数fn2,也是如此
fn2EC = {
  AO:{
    arguments:{
      length:0
    },
    d:4
  }
}
// 将fn2EC推入栈
ECStack.push(fn2EC) // [globalEC,fnEC,fn2EC]

// 执行fn2结束
ECStack.pop() // fn2EC销毁
// 执行fn结束
ECStack.pop() // fnEC销毁

3. 作用域链Scope

作用域 规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做 作用域链。

函数的作用域在函数创建时就已经确定了。当函数创建时,会有一个名为 [[scope]] 的内部属性保存所有父变量对象到其中。当函数执行时,会创建一个执行环境,然后通过复制函数的 [[scope]] 属性中的对象构建起执行环境的作用域链,然后,变量对象 VO 被激活生成 AO 并添加到作用域链的前端,完整作用域链创建完成:

lua 复制代码
Scope = [AO].concat([[Scope]]);

4. 调用者this

如果当前函数被作为对象方法调用或使用 bind call apply 等 API 进行委托调用,则将当前代码块的调用者信息(this value)存入当前执行上下文,否则默认为全局对象调用。

总结:

如果将上述一个完整的执行上下文使用代码形式表现出来的话,应该类似于下面这种:

less 复制代码
executionContext:{
    [variable object | activation object]:{
        arguments,
        variables: [...],
        funcions: [...]
    },
    scope chain: variable object + all parents scopes
    thisValue: context object
}

四、生命周期(ES3)

1.创建阶段

2.执行阶段

3.销毁阶段

五、总结(ES3)

对于 ES3 中的执行上下文,我们可以用下面这个列表来概括程序执行的整个过程:

1. 函数被调用

2. 在执行具体的函数代码之前,创建了执行上下文

3. 进入执行上下文的创建阶段:

1.初始化作用域链

2.创建 arguments object 检查上下文中的参数,初始化名称和值并创建引用副本

3.扫描上下文找到所有函数声明:

(1)对于每个找到的函数,用它们的原生函数名,在变量对象中创建一个属性,该属性里存放的是一个指向实际内存地址的指针

(2)如果函数名称已经存在了,属性的引用指针将会被覆盖

4.扫描上下文找到所有 var 的变量声明:

(1)对于每个找到的变量声明,用它们的原生变量名,在变量对象中创建一个属性,并且使用 undefined 来初始化

(2)如果变量名作为属性在变量对象中已存在,则不做任何处理并接着扫描

  1. 确定 this 值

4.进入执行上下文的执行阶段:

1.在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。

六、ES5 中的执行上下文

ES5 规范又对 ES3 中执行上下文的部分概念做了调整,最主要的调整,就是去除了 ES3 中变量对象和活动对象,以 词法环境组件( LexicalEnvironment component) 和 变量环境组件( VariableEnvironment component) 替代。所以 ES5 的执行上下文概念上表示大概如下:

ini 复制代码
ExecutionContext = {
  ThisBinding = <this value>,
  LexicalEnvironment = { ... },
  VariableEnvironment = { ... },
}

变量环境 它也是一个 词法环境 ,所以它有着词法环境的所有特性。

之所以在 ES5 的规范力要单独分出一个变量环境的概念是为 ES6 服务的: 在 ES6 中,词法环境 组件和 变量环境 的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。

在上下文创建阶段,引擎检查代码找出变量和函数声明,变量最初会设置为 undefined(var 情况下),或者未初始化(let 和 const 情况下)。这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误。

七、ES5 执行上下文总结

对于 ES5 中的执行上下文,我们可以用下面这个列表来概括程序执行的整个过程:

  1. 程序启动,全局上下文被创建

    1. 创建全局上下文的 词法环境

      (1) 创建 对象环境记录器 ,它用来定义出现在 全局上下文 中的变量和函数的关系(负责处理 let 和 const 定义的变量)

      (2) 创建 外部环境引用 ,值为 null

    2. 创建全局上下文的 变量环境

      (1)创建 对象环境记录器 ,它持有 变量声明语句 在执行上下文中创建的绑定关系(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)

      (2)创建 外部环境引用 ,值为 null

    3. 确定 this 值为全局对象(以浏览器为例,就是 window )

  2. 函数被调用,函数上下文被创建

    1. 创建函数上下文的 词法环境

      (1)创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length 。(负责处理 let 和 const 定义的变量)

      (2)创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)

    2. 创建函数上下文的 变量环境

      (1)创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length 。(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)

      (2)创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)

    3. 确定 this 值

  3. 进入函数执行上下文的执行阶段:

    1. 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。

八、闭包

闭包的定义:有权访问另一个函数内部变量的函数。简单说来,如果一个函数被作为另一个函数的返回值,并在外部被引用,那么这个函数就被称为闭包。

九、this

  1. this在函数执行时才确认,本质上为函数的实际调用者
  2. 当没有调用者时,默认调用者为window
  3. this存储在AO中
  4. 箭头函数的AO没有this

十、总结:

  • 当函数运行的时候,会生成一个叫做 "执行上下文" 的东西,也可以叫做执行环境,它用于保存函数运行时需要的一些信息。
  • 所有的执行上下文都会被交给系统的 "执行上下文栈" 来管理,它是一个栈结构数据,全局上下文永远在该栈的最底部,每当一个函数执行生成了新的上下文,该上下文对象就会被压入栈,但是上下文栈有容量限制,如果超出容量就会栈溢出。
  • 执行上下文内部存储了包括:变量对象 、作用域链 、this 指向 这些函数运行时的必须数据。
  • 变量对象构建的过程中会触发变量和函数的声明提升。
  • 函数内部代码执行时,会先访问本地的变量对象去尝试获取变量,找不到的话就会攀爬作用域链层层寻找,找到目标变量则返回,找不到则 undefined。
  • 一个函数能够访问到的上层作用域,在函数创建的时候就已经被确定且保存在函数的 [[scope]] 属性里,和函数拿到哪里去执行没有关系。
  • 一个函数调用时的 this 指向,取决于它的调用者,通常有以下几种方式可以改变函数的 this 值:对象调用、call、bind、apply。

相关参考

1面试官:说说执行上下文吧 (juejin.cn/post/684490...)

2一文搞懂执行上下文、VO、AO、Scope、\[scope]、作用域链、闭包、this (juejin.cn/post/712172...)

相关推荐
zttbee30 分钟前
echarts图表tooltip色块失效
前端·javascript·echarts
excel2 小时前
IndexNow:让搜索引擎更快发现网站更新的主动通知协议
前端·后端·架构
子兮曰2 小时前
CSS 复现 23 种设计风格:从拟物到液态玻璃的成本阶梯与兜底写法【实战】
前端·css·视觉设计
虚无境3 小时前
如何用AI帮我写出实用的插件
前端·javascript·ai编程
柳杉3 小时前
从数字化到数智化:3D 可视化大屏的下一个五年
前端·ai编程·数据可视化
zimoyin4 小时前
在 C# 里嵌入 JavaScript:用 Jint 做可热更新的自动化脚本(从入门到踩坑)
javascript·c#·自动化
风骏时光牛马4 小时前
从零到实战:AI全栈学习进阶路线
前端
FRing034 小时前
JavaScript入门到精通二十六(变量提升、函数剩余参数、展开运算符、ES6箭头函数)
开发语言·前端·javascript·es6·箭头函数
用户360055579004 小时前
Kestrel_VLLM - 15 · decode 优化四连击:19ms → 8ms
前端
IT_陈寒5 小时前
JavaScript的闭包陷阱,这个坑我躺了三天才爬出来
前端·人工智能·后端