JS执行过程

要了解JS的执行,首先要理解 原型链 执行上下文 变量对象 作用域链 This

作用域链: JS查找 变量 的规则,会沿着作用域链[[scope]]向下查找

原型链: JS查找 对象属性 的规则,会沿着原型链依次向上查找该属性.

This: JS函数内查找 其调用对象 的规则.

函数内this.x: 先按 This规则 找到函数的调用对象,再按 原型链规则 从该对象上找到x属性.

1. 原型链

prototype:函数 都有且只有函数才有的属性。该属性指向的对象,就是该函数作为构造函数创建的实例的__原型__。

__proto__:对象(除null)都有的属性,这个属性会指向该对象的__原型__。

constructor:原型__都有的属性,该属性指向关联这个原型的__构造函数

每一个JS对象在创建时都会与之关联一个对象,这个对象就是原型

每一个对象都会从原型"继承"属性,查找对象的属性时,会沿着原型链依次向上查找该属性.

2. 执行上下文(ES3时代)

JS执行机制: 以代码块为单位 先编译后执行,分为 创建阶段执行阶段.

("代码块"有三种:全局代码、函数代码、eval代码。)

创建阶段: 会生成两部分 执行上下文可执行代码

执行上下文: 是一段代码时的运行环境,包括执行期间要用到的 变量对象 / 作用域链 / This

执行上下文栈: 用来管理这些执行上下文,是一个 先进后出的 栈结构.

执行上下文栈 其实就是 JavaScript 引擎追踪函数执行的一个机制,和函数执行时的变量查找没什么关系

当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。

JS执行时首先会遇到全局代码,栈内压入 全局执行上下文.

然后执行到一个函数时,会 从 全局执行上下文 中取出这段函数的代码进行编译

创建该函数的执行上下文,又压入栈内.函数执行完毕,又弹出该执行上下文.

变量对象 / 作用域链 / This ,是执行上下文的三个重要属性

3. 变量对象(ES3时代)

变量对象 包含 当前上下文定义的 形参(arguments) 函数声明 变量 的对象,用于可执行代码执行时查找变量,

在进入执行上下文时(创建阶段) 时创建

全局对象(GO) 全局执行上下文 的 变量对象 就是 全局变量的宿主, 就是全局对象 其 window属性指向自己

活动对象(AO) 函数执行上下文 的 变量对象.

变量对象 只是一个储存了所有变量的对象的概念

4. 作用域链(ES3时代)

作用域

其实就是 变量对象, 代表 函数和变量的可被访问的范围,生效范围.

变量存在于哪个 作用域 或 属于哪个变量对象,就可以在这个变量对象内被访问.

作用域链

是JS查找变量的一套规则,本质上是由多个执行上下文的变量对象构成的链表,

链表的层级由 词法作用域 决定(而不是执行上下文栈),查找变量时,会沿着作用域链查找.

作用域链1 (函数创建时产生)

外层代码段编译时, 作为一个对象, 会产生一个内部属性 [[scope]],

并根据 词法作用域,从内到外层层将父代码段的 变量对象 保存其中.

词法作用域/静态作用域,JS的 作用域 由函数定义的位置决定(而不是执行的位置).

函数他所能引用的外部变量[[scope]],在函数创建的时候就已经决定了,而不是函数执行的时候

作用域链2 (函数调用时产生)

(进入自己的创建阶段,创建自己的执行上下文,压入执行上下文栈)

复制自己 [[scope]]属性 创建作用域链2, 创建自己的AO并初始化, 压入作用域链顶端.

5. This

This 的目的是在函数 能访问其调用者

注意,普通函数中的 this 默认指向全局对象 window,在严格模式下,是 undefined

ES6箭头函数解决的问题:嵌套函数中的 this 不会从外层函数中继承

6. JS执行(ES3时代)

JS是 解释型语言 ,边编译边执行.

JS执行 以 代码段 为单位,主要分为 三个阶段:语法分析 / 创建阶段 / 执行阶段

代码段 有三种: 全局代码 函数代码 eval代码

第一步,语法分析

将代码分成 token,将 token解析成AST语法树(语法检查,报错)

分析该js脚本代码块的语法是否正确,

如果语法不正确,则抛出一个语法错误,停止该代码块的执行,然后继续查找并加载下一个代码块

如果语法正确,则进入创建阶段。

第二步,创建阶段

创建执行上下文,压入执行上下文栈

每进入一个 代码段, 或者说函数被调用时,便会创建相应的 执行上下文 , 压入 执行上下文栈

初始化执行上下文

主要初始化 执行上下文 的三个重要属性, 变量对象(Variable object) 作用域链(Outer) This

作用域链(Scope Chain)

复制函数 [[scope]] 属性创建作用域链

注意是复制而不是修改,因为函数可能被多次调用,应当保证每次调用时,变量初始状态相同

变量对象(Variable Object)

依次检查 形参(Arguments对象) 函数声明 var声明变量(已有同名函数声明则跳过) 加入 变量对象VO

将 VO 压入 作用域链 顶端

此时的 VO 不能被直接访问,只有进入执行阶段,成为 活动对象(Active Object) 才能被访问。

This

第三步,执行阶段

代码开始逐条执行,在这个阶段,JS 引擎开始对定义的变量赋值、开始顺着作用域链访问变量,

如果内部有函数调用就创建一个新的执行上下文压入执行栈并把控制权交出

第四步,销毁阶段

当前执行上下文 被弹出 执行上下文栈 并且毁,控制权被交给执行栈上一层的执行上下文

7. 执行上下文(ES5时代)

执行上下文: 是一段代码时的运行环境,包括执行期间要用到的 词法环境 / 变量环境 / This

作用域 --> 词法环境 VO/AO --> 环境记录 作用域链 --> outer引用

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

8. 词法环境 和 变量环境(ES5时代)

词法环境 和 变量环境 是 执行上下文的重要组成

词法环境(Lexical Environment)

一个 标识符(变量/函数的名称) 与 变量(实际对象/原始值的引用) 相映射的结构,包括

1.环境记录(environment record) 储存变量和函数生命的实际位置

2.对外部环境的引用 意味着可以访问外部词法环境,

词法环境 存在两种类型

1.全局环境 (在全局上下文中,或with中)对外部的环境引用为null

2.函数环境 (在函数环境中)包含了一个 arguments对象

环境记录 存在两种类型

2.对象环境记录 (在全局环境中)用于定义 全局执行上下文出现的变量和函数的关联

1.声明性环境记录 (在函数环境中)储存 变量/函数/参数

Arguments对象,伪数组,包含了 函数参数 与 索引 的映射,及 参数个数.

// 词法环境伪代码
GlobalExectionContext = { // 全局执行上下文
  LexicalEnvironment: { // 词法环境(全局环境)
    EnvironmentRecord: {  // 环境记录(对象)
      Type: "Object",  
      // 标识符绑定在这里 
    outer: <null>  
  }  
}

FunctionExectionContext = { // 函数执行上下文
  LexicalEnvironment: { // 词法环境(函数环境)
    EnvironmentRecord: {  // 环境记录(声明性)
      Type: "Declarative",
      // 标识符绑定在这里 
    outer: <Global or outer function environment reference>  
  }  
}

变量环境(VariableEnvironment)

1.变量环境 就是 词法环境的一种,具有 词法环境 的所有属性

2.在ES6中, 变量环境 与 词法环境 的区别在于,

词法环境 储存 函数声明/变量(let和const)的绑定,

变量环境 仅储存 变量(var)的绑定

var 和 let/const

var 没有块级作用域 可以重复申明 可以先使用后申明 存在强制提升 不存在暂时性死区

var变量提升是JS的一个重要设计缺陷, 会导致 变量污染 变量覆盖,

在ES6中 用 块级作用域(词法环境) 配合 const let 避免这种缺陷。

执行上下文创建阶段

变量类型 是否创建 是否初始化 是否赋值
形参 创建 初始化 并赋值实参
函数声明 创建 初始化 并赋值函数体
var变量 创建 初始化
let/const 创建

let/const 创建了但是没有初始化,所以在赋值前被调用即报错 未初始化,暂时性死区

9. JS执行(ES5时代)

创建阶段 与ES3类似,创建当前执行上下文内的 this,词法环境,变量环境,outer

注意 执行阶段 变量查找时,同一 执行上下文内 代表 块级作用域的 词法环境 优先级更高

变量查找顺序:

1.沿着当前 执行上下文内 的 词法环境栈 顶向下查找,找到栈底,再找 变量环境,

2.沿着 作用域链,在外部作用域中查找 外一层 的执行上下文,重复第一步。

10. 闭包

理解闭包的产生比理解闭包的概念更重要,

闭包的产生

根据词法作用域规则,函数的作用域链由函数声明位置决定,声明位置在内部的函数总是可以访问外部函数的变量,

即使内部函数的调用位置,不在其声明的外层函数内,甚至外部函数已经执行结束,

但内部函数 引用的外部函数变量依旧保存在内存中,这些变量的集合称为闭包.

闭包

函数与其引用的外部函数词法环境的集合.(函数,自己内部的变量,外部词法环境,可访问的外部变量)

闭包存在两种情况,一种是 内部函数正常在内部执行时,外部词法环境变量均存在,只要函数执行就存在闭包

一种是 内部函数执行时外部函数执行上下文已被销毁,

当外部函数已被销毁,内部函数 引用了的外部变量依旧会被保存,

而 未被引用的外部变量 会通过 tree-shaking的方式 被销毁。

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
  // 虽然自执行函数已经执行完了,但是当时的i已经被内部函数的闭包记住
  // 不再去全局vo中找i,而是在 自执行函数中就已经被记住
}

data[0](); // 0
data[1](); // 1
data[2](); // 2

感谢

看了二十多篇文章,都把我看懵了...

面试官:说说执行上下文吧

JS夯实之执行上下文与词法环境

彻底搞懂作用域、执行上下文、词法环境

[译] 理解 JavaScript 中的执行上下文和执行栈

冴羽 JavaScript深入之执行上下文

李兵《浏览器工作原理与实践》浏览器中的JavaScript执行机制