likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 10

JS 执行上下文

一、含义:

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

二、类型(ES3)

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

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

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

三、包括(ES3)

1. 变量对象VO(variable object

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

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)会被忽略。

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

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

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

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

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

2. 活动对象AO(activation object

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

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

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 并添加到作用域链的前端,完整作用域链创建完成:

Scope = [AO].concat([[Scope]]);

4. 调用者this

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

总结:

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

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 的执行上下文概念上表示大概如下:

ExecutionContext = {
  ThisBinding = <this value>,
  LexicalEnvironment = { ... },
  VariableEnvironment = { ... },
}

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

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

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

七、ES5 执行上下文总结

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

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

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

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

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

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

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

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

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

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

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

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

      (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 值:对象调用、callbindapply

相关参考

转载自:https://juejin.cn/post/7275220813618921535
评论
请登录