面试官:说说闭包吧。我:那还要从执行上下文说起。

前言

面试中经常会问及闭包,以及在实际开发过程中我们也接触过很多闭包,为了更好的解闭包、使用闭包,全面的了解闭包的过往今来还是很有必要的。

说到闭包,离不开作用域链和执行上下文。为了追其根本,我们先从执行上下文开始说起。

本文为面试专题闭包相关之执行上下文。

2024年,龙年大吉吧。裁员,内卷,逆流而上,万字面经梳理 – 掘金 (juejin.cn)

执行上下文

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

执行上下文的类型

  • 全局执行上下文:一个程序中只会存在一个全局上下文,全局上下文会生成一个全局对象(以浏览器环境为例,这个全局对象是 window),并且将 this 值绑定到这个全局对象上。它在整个 javascript 脚本的生命周期内都会存在于执行堆栈的最底部不会被栈弹出销毁
  • 函数执行上下文:每当一个函数被调用时,都会创建一个新的函数执行上下文(不管这个函数是不是被重复调用的)
  • Eval 函数执行上下文:执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于并不经常使用 eval,所以在这里不做分析。

想要搞懂执行上下文,我们需要了解执行上下文的三个阶段(生命周期):

  • 创建阶段
  • 执行阶段
  • 销毁阶段

创建阶段

在创建阶段会涉及3个内容,你需要了解下,它们分别是👇:

  1. this 值的决定
  2. 创建词法环境组件
  3. 创建变量环境组件

抽象其概念结构描述的话,类似下面这样👇:

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

👀需要注意的是:创建阶段会把变量声明提升 —— 变量和函数声明都会提升,但是函数提升更靠前

1. this 值的决定

这里的this指的是当前可执行代码块的调用者

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

2. 词法环境组件

词法环境:是一种持有标识符—变量映射的结构。

这里的标识符指的是变量/函数名字,而变量是对实际对象(包含函数类型对象)或原始数据的引用

其内部有两个组件,分别为:

  • 环境记录器:是存储变量和函数声明的实际位置

    • 全局环境中:环境记录器是对象环境记录器(用来定义出现在全局上下文中的变量和函数的关系)
    • 函数环境中:环境记录器是声明式环境记录器(存储变量、函数和参数)
  • 外部环境的引用:意味着它可以访问其父级词法环境(作用域)

3. 变量环境组件

变量环境:它同样是一个词法环境,它有着上面定义的词法环境的所有属性。

在 ES6 中,词法环境组件和变量环境组件的一个不同就是前者被用来存储函数声明和变量(letconst)绑定,而后者只用来存储 var 变量绑定。

4. 示例分析

这里分为两种情况:全局环境/函数环境

全局上下文

  • 创建全局上下文的词法环境
    • 创建对象环境记录器:它用来定义出现在 全局上下文 中的变量和函数的关系(负责处理 letconst 定义的变量)
    • 创建 外部环境引用:值为 null
  • 创建全局上下文的变量环境
    • 创建 对象环境记录器:它持有 变量声明语句 在执行上下文中创建的绑定关系(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)
    • 创建 外部环境引用:值为 null

函数上下文

  • 创建函数上下文的词法环境
    • 创建 声明式环境记录器:存储变量、函数和参数(负责处理 letconst 定义的变量)
    • 创建 外部环境引用:值为全局对象,或者为父级词法环境(作用域)
  • 创建函数上下文的变量环境
    • 创建 声明式环境记录器:存储变量、函数和参数(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)
    • 创建 外部环境引用:值为全局对象,或者为父级词法环境(作用域)

👀存储变量、函数和参数:它包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。

代码演示

原代码如下 ↓ ↓ ↓

let a = 20;
const b = 30;
var c;

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

c = multiply(20, 30);

概念结构如下 ↓ ↓ ↓

// 全局上下文
GlobalExectionContext = {
  // this绑定
  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>
  }
}
  • 注意看 letconstvar定义的区别:letconst 定义的变量并没有关联任何值,但 var 定义的变量被设成了 undefined
    • 变量声明提升
  • 只有遇到调用函数 multiply 时,函数执行上下文才会被创建。

执行阶段

在此阶段,完成对所有这些变量的分配(相当于赋值操作),最后执行代码。如果内部有函数调用就创建一个新的执行上下文压入执行栈并把控制权交出。

👀注:在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined

销毁阶段

一般来讲当函数执行完成后,当前执行上下文(局部环境)会被弹出执行上下文栈并且销毁,控制权被重新交给执行栈上一层的执行上下文。

在此,可以简单思考下闭包的问题,以及为什么会造成内存泄漏,这一块放到下一章再讲 => 作用域和闭包

执行栈

执行上下文是以执行栈的形式进行管理的,遵循后进先出(LIFO)全局作用域会一直存在栈的底部

引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

示例:

let a = 'Hello World!';

function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}

function second() {
  console.log('Inside second function');
}

first();
console.log('Inside Global Execution Context');

面试官:说说闭包吧。我:那还要从执行上下文说起。

练习题目

第一题:

var a = 1

function foo() {
  var a = 2
  return function () {
    console.log(this.a)
  }
}

var bar = foo()
bar()

bar(foo())函数的调用者是 windows,因此 a=1

简单调整下:

// 在上面代码的基础上添加如下代码
var A = {
  a: 0,
}

var bar = foo().bind(A)

通过 bind改变了 this的指向,这里结果是 a=0

第二题:

foo();

var foo = function foo() {
    console.log('foo1');
}

function foo() {
    console.log('foo2');
}

foo();

注意关键一点:变量和函数声明都会提升,但是函数提升更靠前

由于函数声明提升更加靠前,且如果 var 定义变量的时候发现已有同名函数定义则跳过变量定义,上面的代码其实可以写成下面这样:

function foo () {
    console.log('foo2');
}

foo();

foo = function foo() {
    console.log('foo1');
};

foo();

执行上下文总结

  • 当函数运行的时候,会生成一个叫做 “执行上下文” 的东西,也可以叫做执行环境,它用于保存函数运行时需要的一些信息。
  • 执行上下文内部存储了包括:变量对象作用域链this 指向 这些函数运行时的必须数据。
  • 变量对象构建的过程中会触发变量和函数的声明提升
  • 函数内部代码执行时,会先访问本地的变量对象去尝试获取变量,找不到的话就会攀爬作用域链层层寻找,找到目标变量则返回,找不到则 undefined
  • 一个函数能够访问到的上层作用域,在函数创建的时候就已经被确定且保存在函数的 [[scope]] 属性里(console.dir 方法可打印查看),和函数拿到哪里去执行没有关系。
  • 一个函数调用时的 this 指向,取决于它的调用者,通常有以下几种方式可以改变函数的 this 值:对象调用callbindapply

交流

面试相关的文章及代码demo,后续打算在这个仓库(JS-banana/interview: 面试不完全指北 (github.com))进行维护,欢迎✨star,提建议,一起进步~

资料

原文链接:https://juejin.cn/post/7347956542308499468 作者:小帅不太帅

(0)
上一篇 2024年3月20日 上午10:55
下一篇 2024年3月20日 上午11:05

相关推荐

发表回复

登录后才能评论