从执行上下文和作用域谈谈变量提升和闭包的底层原理

执行上下文与执行上下文栈

首先介绍下执行上下文和执行上下文栈的原理,从而引出变量提升和闭包的底层原理。

这里先简单提下变量提升的原理和含义,具体底层实现请继续往下看。

变量提升与函数提升

  1. 提升的内部原理

    函数在运行的时候,会首先创建执行上下文,然后将执行上下文入栈,然后当此执行上下文处于栈顶时,开始运行执行上下文。

    在创建执行上下文的过程中会做三件事:创建变量对象,创建作用域链,确定 this 指向

    其中创建变量对象的过程中,首先会为 arguments 创建一个属性,值为 arguments,然后会扫码 function 函数声明,创建一个同名属性,值为函数的引用,接着会扫码 var 变量声明,创建一个同名属性,值为 undefined,这就是变量提升。

  2. 变量声明提升

    • 通过var定义(声明)的变量, 在定义语句之前就可以访问到
    • 值: undefined
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
  3. 函数声明提升

    • 通过function声明的函数, 在之前就可以直接调用
    • 值: 函数定义(对象)
    • 如果是通过函数表达式定义一个函数,属于变量提升,不属于函数提升,不可直接调用
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  4. 问题: 变量提升和函数提升是如何产生的?

    • 先有变量提升, 再有函数提升(函数提升优先级高于变量提升,同名的话函数会覆盖变量)

执行上下文

  1. 代码分类(位置)

    • 全局代码
    • 函数代码
  2. 全局执行上下文

    • 在执行全局代码前将window确定为全局执行上下文
    • 对全局数据进行预处理
      • var定义的全局变量==>undefined, 添加为window的属性
      • function声明的全局函数==>赋值(fun), 添加为window的方法
      • this==>赋值(window)
    • 开始执行全局代码
  3. 函数执行上下文

    • 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象
    • 对局部数据进行预处理
      • 形参变量==>赋值(实参)==>添加为执行上下文的属性
      • arguments==>赋值(实参列表), 添加为执行上下文的属性
      • var定义的局部变量==>undefined, 添加为执行上下文的属性
      • function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
      • this==>赋值(调用函数的对象)
    • 开始执行函数体代码

生命周期

执行上下文的生命周期包括三个阶段:创建阶段 → 执行阶段 → 回收阶段

创建阶段

创建阶段即当函数被调用,但未执行任何其内部代码之前

创建阶段做了三件事:

  • 确定 this 的值,也被称为 This Binding(this的值是在执行的时候才能确认)
  • LexicalEnvironment(词法环境) 组件被创建
  • VariableEnvironment(变量环境) 组件被创建
  • 创建作用域链

词法环境

词法环境有两个组成部分:

  • 全局环境:是一个没有外部环境的词法环境,其外部环境引用为null,有一个全局对象,this 的值指向这个全局对象
  • 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了arguments 对象,外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境
dart 复制代码
GlobalExectionContext = {  // 全局执行上下文
  LexicalEnvironment: {       // 词法环境
    EnvironmentRecord: {     // 环境记录
      Type: "Object",           // 全局环境
      // 标识符绑定在这里 
      outer: <null>           // 对外部环境的引用
  }  
}

FunctionExectionContext = { // 函数执行上下文
  LexicalEnvironment: {     // 词法环境
    EnvironmentRecord: {    // 环境记录
      Type: "Declarative",      // 函数环境
      // 标识符绑定在这里      // 对外部环境的引用
      outer: <Global or outer function environment reference>  
  }  
}

变量环境

变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性

在 ES6 中,词法环境和变量环境的区别在于前者用于存储函数声明和变量( letconst )绑定,而后者仅用于存储变量( var )绑定

ini 复制代码
let a = 20;  
const b = 30;  
var c;

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = multiply(20, 30);

执行上下文如下

yaml 复制代码
GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {  // 词法环境
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      a: < uninitialized >,  
      b: < uninitialized >,  
      multiply: < func >  
    }  
    outer: <null>  
  },

  VariableEnvironment: {  // 变量环境
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      c: undefined,  
    }  
    outer: <null>  
  }  
}

FunctionExectionContext = {  
   
  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      Arguments: {0: 20, 1: 30, length: 2},  
    },  
    outer: <GlobalLexicalEnvironment>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}

letconst定义的变量ab在创建阶段没有被赋值,为< uninitialized >,但var声明的变量从在创建阶段被赋值为undefined

这就是变量提升的实际原因

执行阶段

在这阶段,执行变量赋值、代码执行

如果 Javascript 引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配 undefined

回收阶段

执行上下文出栈等待虚拟机回收执行上下文

执行上下文栈

  1. 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象

  2. 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)

  3. 在函数执行上下文创建后, 将其添加到栈中(压栈)

  4. 在当前函数执行完后,将栈顶的对象移除(出栈)

  5. 当所有的代码执行完后, 栈中只剩下window

面试题(妙啊)

javascript 复制代码
/*

  测试题1: 先变量提升,再函数提升

  */

  function a() {}

  var a;

  console.log(typeof a)   //'function'

  /*

  测试题2: 

   */

  if (!(b in window)) {

    var b = 1;

  }

  console.log(b)  //undefined

  /*

  测试题3: 

   */

  var c = 1

  function c(c) {

    console.log(c)

    var c = 3

  }

  c(2)  //报错!!! 秒啊!!!
  
  //以上代码相当于
  var c 
  function c(c) {
    console.log(c)
    var c = 3
  }
  c=1
  c(2)  
  //先变量提升,再函数提升,再赋值!!!

复习

markdown 复制代码
- 理解
    - 执行上下文: 由js引擎自动创建的对象, 包含对应作用域中的所有变量属性
    - 执行上下文栈: 用来管理产生的多个执行上下文
- 分类:
    - 全局: window
    - 函数: 对程序员来说是透明的
- 生命周期
    - 全局 : 准备执行全局代码前产生, 当页面刷新/关闭页面时死亡
    - 函数 : 调用函数时产生, 函数执行完时死亡
- 包含哪些属性:
    - 全局 :
        - 用var定义的全局变量 ==>undefined
        - 使用function声明的函数 ===>function
        - this ===>window
    - 函数
        - 用var定义的局部变量 ==>undefined
        - 使用function声明的函数 ===>function
        - this ===> 调用函数的对象, 如果没有指定就是window
        - 形参变量 ===>对应实参值
        - arguments ===>实参列表的伪数组
- 执行上下文创建和初始化的过程
    - 全局:
        - 在全局代码执行前最先创建一个全局执行上下文(window)
        - 收集一些全局变量, 并初始化
        - 将这些变量设置为window的属性
    - 函数:
        - 在调用函数时, 在执行函数体之前先创建一个函数执行上下文
        - 收集一些局部变量, 并初始化
        - 将这些变量设置为执行上下文的属性

作用域与作用域链

作用域

  1. 理解

    • 就是一块"地盘", 一个代码段所在的区域
    • 它是静态的(相对于上下文对象), 在编写代码时就确定了
    • 这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

    静态作用域:输出1

    动态作用域:输出2

scss 复制代码
var value = 1;

function foo() {
    console.log(value);
}

function bar() {
    var value = 2;
    foo();
}

bar(); //1
  1. 分类
    • 全局作用域
    • 函数作用域
    • 没有块作用域(ES6有了)
  1. 作用
    • 隔离变量,不同作用域下同名变量不会有冲突
  2. 为什么要有块级作用域
    • 解决变量覆盖问题
    • 解决循环变量变成全局变量

作用域与执行上下文

  1. 区别1
    • 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
    • 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
    • 函数执行上下文环境是在调用函数时, 函数体代码执行之前创建
  2. 区别2
    • 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
    • 执行上下文环境是动态的, 调用函数时创建, 函数调用结束时上下文环境就会被释放
  3. 联系
    • 执行上下文环境(对象)是从属于所在的作用域
    • 全局上下文环境==>全局作用域
    • 函数上下文环境==>对应的函数使用域

作用域链

  1. 理解
    • 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
    • 查找变量时就是沿着作用域链来查找的
  2. 查找一个变量的查找规则
    1. 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
    2. 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
    3. 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常

完整执行过程

ini 复制代码
var scope = "global scope";

function checkscope(){

  var scope2 = 'local scope';

  return scope2;

}

checkscope();

执行过程如下:

总结:

  1. 函数被创建,保存作用域链到 内部属性[[scope]]
  2. 创建 checkscope 函数执行上下文,并压栈
  3. 函数执行前的准备工作:
    • 复制函数[[scope]]属性创建作用域链,
    • 初始化活动对象,加入形参、函数声明、变量声明
    • 将活动对象压入 checkscope 作用域链顶端
  4. 执行函数
  5. 出栈

1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]

lua 复制代码
checkscope.[[scope]] = [

  globalContext.VO

];

2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈

ini 复制代码
ECStack = [

  checkscopeContext,

  globalContext

];

3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链

lua 复制代码
checkscopeContext = {

  Scope: checkscope.[[scope]],

}

4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

css 复制代码
checkscopeContext = {

  AO: {

    arguments: {

      length: 0

    },

    scope2: undefined

  },

  Scope: checkscope.[[scope]],

}

5.第三步:将活动对象压入 checkscope 作用域链顶端

css 复制代码
checkscopeContext = {

  AO: {

    arguments: {

      length: 0

    },

    scope2: undefined

  },

  Scope: [AO, [[Scope]]]

}

6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值

css 复制代码
checkscopeContext = {

  AO: {

    arguments: {

      length: 0

    },

    scope2: 'local scope'

  },

  Scope: [AO, [[Scope]]]

}

7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

ini 复制代码
ECStack = [

  globalContext

];

所以闭包的底层原理就可以知道了

闭包底层原理

从执行上下文的角度来看:

压栈入栈顺序为:全局入栈---fn1入栈---fn1出栈---fn2入栈---fn2出栈---全局出栈

问题:fn1的执行上下文已经被销毁了,为什么fn2还可以访问fn1的变量a??

理由:fn2的作用域链还保存着fn1的活动对象

ini 复制代码
fn2Context = {
    Scope: [AO, fn1Context.AO, globalContext.VO],
}

面试题(妙啊)

javascript 复制代码
  //面试题1
  var x = 10;
  function fn() {
    console.log(x);    //10   因为作用域在函数定义时就已经确定了,fn的上级是window
  }
  function show(f) {
    var x = 20;
    f();
  }
  show(fn);
  
  //面试题2
  var fn = function () {
    console.log(fn)   //function () {console.log(fn) }
  }
  fn()
  
  var obj = {
    fn2: function () {
      console.log(this.fn2)   //function () {console.log(fn2);console.log(fn2);}
      console.log(fn2)   //报错,找不到fn2
    }
  }
  obj.fn2()

复习

  • 理解:
    • 作用域: 一块代码区域, 在编码时就确定了, 不会再变化
    • 作用域链: 多个嵌套的作用域形成的由内向外的结构, 用于查找变量
  • 分类:
    • 全局
    • 函数
    • js没有块作用域(在ES6之前)
  • 作用
    • 作用域: 隔离变量, 可以在不同作用域定义同名的变量不冲突
    • 作用域链: 查找变量
  • 区别作用域与执行上下文
    • 作用域: 静态的, 编码时就确定了(不是在运行时), 一旦确定就不会变化了
    • 执行上下文: 动态的, 执行代码时动态创建, 当执行结束消失
    • 联系: 执行上下文环境是在对应的作用域中的
相关推荐
众生回避6 分钟前
鸿蒙ms参考
前端·javascript·vue.js
洛千陨6 分钟前
Vue + element-ui实现动态表单项以及动态校验规则
前端·vue.js
笃励43 分钟前
Angular面试题五
javascript·ecmascript·angular.js
GHUIJS1 小时前
【vue3】vue3.5
前端·javascript·vue.js
-seventy-1 小时前
对 JavaScript 原型的理解
javascript·原型
&白帝&1 小时前
uniapp中使用picker-view选择时间
前端·uni-app
魔术师卡颂1 小时前
如何让“学源码”变得轻松、有意义
前端·面试·源码
谢尔登1 小时前
Babel
前端·react.js·node.js
ling1s1 小时前
C#基础(13)结构体
前端·c#
卸任2 小时前
使用高阶组件封装路由拦截逻辑
前端·react.js