|
| 1 | +# 面试官:JavaScript中执行上下文和执行栈是什么? |
| 2 | + |
| 3 | + |
| 4 | + |
| 5 | + |
| 6 | +## 一、执行上下文 |
| 7 | + |
| 8 | +简单的来说,执行上下文是一种对`Javascript`代码执行环境的抽象概念,也就是说只要有`Javascript`代码运行,那么它就一定是运行在执行上下文中 |
| 9 | + |
| 10 | +执行上下文的类型分为三种: |
| 11 | + |
| 12 | +- 全局执行上下文:只有一个,浏览器中的全局对象就是 `window `对象,`this` 指向这个全局对象 |
| 13 | +- 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文 |
| 14 | +- Eval 函数执行上下文: 指的是运行在 `eval` 函数中的代码,很少用而且不建议使用 |
| 15 | + |
| 16 | +下面给出全局上下文和函数上下文的例子: |
| 17 | + |
| 18 | +  |
| 19 | + |
| 20 | +紫色框住的部分为全局上下文,蓝色和橘色框起来的是不同的函数上下文。只有全局上下文(的变量)能被其他任何上下文访问 |
| 21 | + |
| 22 | +可以有任意多个函数上下文,每次调用函数创建一个新的上下文,会创建一个私有作用域,函数内部声明的任何变量都不能在当前函数作用域外部直接访问 |
| 23 | + |
| 24 | + |
| 25 | + |
| 26 | +## 二、生命周期 |
| 27 | + |
| 28 | +执行上下文的生命周期包括三个阶段:创建阶段 → 执行阶段 → 回收阶段 |
| 29 | + |
| 30 | +### 创建阶段 |
| 31 | + |
| 32 | +创建阶段即当函数被调用,但未执行任何其内部代码之前 |
| 33 | + |
| 34 | +创建阶段做了三件事: |
| 35 | + |
| 36 | +- 确定 this 的值,也被称为 `This Binding` |
| 37 | +- LexicalEnvironment(词法环境) 组件被创建 |
| 38 | +- VariableEnvironment(变量环境) 组件被创建 |
| 39 | + |
| 40 | +伪代码如下: |
| 41 | + |
| 42 | +```js |
| 43 | +ExecutionContext = { |
| 44 | + ThisBinding = <this value>, // 确定this |
| 45 | + LexicalEnvironment = { ... }, // 词法环境 |
| 46 | + VariableEnvironment = { ... }, // 变量环境 |
| 47 | +} |
| 48 | +``` |
| 49 | + |
| 50 | + |
| 51 | + |
| 52 | +#### This Binding |
| 53 | + |
| 54 | +确定`this`的值我们前面讲到,`this`的值是在执行的时候才能确认,定义的时候不能确认 |
| 55 | + |
| 56 | + |
| 57 | +#### 词法环境 |
| 58 | + |
| 59 | +词法环境有两个组成部分: |
| 60 | + |
| 61 | +- 全局环境:是一个没有外部环境的词法环境,其外部环境引用为` null`,有一个全局对象,`this` 的值指向这个全局对象 |
| 62 | + |
| 63 | +- 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了`arguments` 对象,外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境 |
| 64 | + |
| 65 | +伪代码如下: |
| 66 | + |
| 67 | +```js |
| 68 | +GlobalExectionContext = { // 全局执行上下文 |
| 69 | + LexicalEnvironment: { // 词法环境 |
| 70 | + EnvironmentRecord: { // 环境记录 |
| 71 | + Type: "Object", // 全局环境 |
| 72 | + // 标识符绑定在这里 |
| 73 | + outer: <null> // 对外部环境的引用 |
| 74 | + } |
| 75 | +} |
| 76 | + |
| 77 | +FunctionExectionContext = { // 函数执行上下文 |
| 78 | + LexicalEnvironment: { // 词法环境 |
| 79 | + EnvironmentRecord: { // 环境记录 |
| 80 | + Type: "Declarative", // 函数环境 |
| 81 | + // 标识符绑定在这里 // 对外部环境的引用 |
| 82 | + outer: <Global or outer function environment reference> |
| 83 | + } |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | + |
| 88 | + |
| 89 | +#### 变量环境 |
| 90 | + |
| 91 | +变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性 |
| 92 | + |
| 93 | +在 ES6 中,词法环境和变量环境的区别在于前者用于存储函数声明和变量( `let` 和 `const` )绑定,而后者仅用于存储变量( `var` )绑定 |
| 94 | + |
| 95 | +举个例子 |
| 96 | + |
| 97 | +```js |
| 98 | +let a = 20; |
| 99 | +const b = 30; |
| 100 | +var c; |
| 101 | + |
| 102 | +function multiply(e, f) { |
| 103 | + var g = 20; |
| 104 | + return e * f * g; |
| 105 | +} |
| 106 | + |
| 107 | +c = multiply(20, 30); |
| 108 | +``` |
| 109 | +
|
| 110 | +执行上下文如下: |
| 111 | +
|
| 112 | +```js |
| 113 | +GlobalExectionContext = { |
| 114 | + |
| 115 | + ThisBinding: <Global Object>, |
| 116 | + |
| 117 | + LexicalEnvironment: { // 词法环境 |
| 118 | + EnvironmentRecord: { |
| 119 | + Type: "Object", |
| 120 | + // 标识符绑定在这里 |
| 121 | + a: < uninitialized >, |
| 122 | + b: < uninitialized >, |
| 123 | + multiply: < func > |
| 124 | + } |
| 125 | + outer: <null> |
| 126 | + }, |
| 127 | + |
| 128 | + VariableEnvironment: { // 变量环境 |
| 129 | + EnvironmentRecord: { |
| 130 | + Type: "Object", |
| 131 | + // 标识符绑定在这里 |
| 132 | + c: undefined, |
| 133 | + } |
| 134 | + outer: <null> |
| 135 | + } |
| 136 | +} |
| 137 | + |
| 138 | +FunctionExectionContext = { |
| 139 | + |
| 140 | + ThisBinding: <Global Object>, |
| 141 | + |
| 142 | + LexicalEnvironment: { |
| 143 | + EnvironmentRecord: { |
| 144 | + Type: "Declarative", |
| 145 | + // 标识符绑定在这里 |
| 146 | + Arguments: {0: 20, 1: 30, length: 2}, |
| 147 | + }, |
| 148 | + outer: <GlobalLexicalEnvironment> |
| 149 | + }, |
| 150 | + |
| 151 | + VariableEnvironment: { |
| 152 | + EnvironmentRecord: { |
| 153 | + Type: "Declarative", |
| 154 | + // 标识符绑定在这里 |
| 155 | + g: undefined |
| 156 | + }, |
| 157 | + outer: <GlobalLexicalEnvironment> |
| 158 | + } |
| 159 | +} |
| 160 | +``` |
| 161 | +
|
| 162 | +留意上面的代码,`let`和`const`定义的变量`a`和`b`在创建阶段没有被赋值,但`var`声明的变量从在创建阶段被赋值为`undefined` |
| 163 | + |
| 164 | +这是因为,创建阶段,会在代码中扫描变量和函数声明,然后将函数声明存储在环境中 |
| 165 | + |
| 166 | +但变量会被初始化为`undefined`(`var`声明的情况下)和保持`uninitialized`(未初始化状态)(使用`let`和`const`声明的情况下) |
| 167 | + |
| 168 | +这就是变量提升的实际原因 |
| 169 | + |
| 170 | + |
| 171 | + |
| 172 | +### 执行阶段 |
| 173 | + |
| 174 | +在这阶段,执行变量赋值、代码执行 |
| 175 | + |
| 176 | +如果 `Javascript` 引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配 `undefined` 值 |
| 177 | + |
| 178 | + |
| 179 | + |
| 180 | +### 回收阶段 |
| 181 | + |
| 182 | +执行上下文出栈等待虚拟机回收执行上下文 |
| 183 | + |
| 184 | + |
| 185 | + |
| 186 | +## 二、执行栈 |
| 187 | + |
| 188 | +执行栈,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文 |
| 189 | + |
| 190 | +  |
| 191 | + |
| 192 | +当`Javascript`引擎开始执行你第一行脚本代码的时候,它就会创建一个全局执行上下文然后将它压到执行栈中 |
| 193 | + |
| 194 | +每当引擎碰到一个函数的时候,它就会创建一个函数执行上下文,然后将这个执行上下文压到执行栈中 |
| 195 | + |
| 196 | +引擎会执行位于执行栈栈顶的执行上下文(一般是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程到达执行栈的下一个执行上下文 |
| 197 | + |
| 198 | +举个例子: |
| 199 | + |
| 200 | +```js |
| 201 | +let a = 'Hello World!'; |
| 202 | +function first() { |
| 203 | + console.log('Inside first function'); |
| 204 | + second(); |
| 205 | + console.log('Again inside first function'); |
| 206 | +} |
| 207 | +function second() { |
| 208 | + console.log('Inside second function'); |
| 209 | +} |
| 210 | +first(); |
| 211 | +console.log('Inside Global Execution Context'); |
| 212 | +``` |
| 213 | + |
| 214 | +转化成图的形式 |
| 215 | + |
| 216 | +  |
| 217 | + |
| 218 | +简单分析一下流程: |
| 219 | + |
| 220 | +- 创建全局上下文请压入执行栈 |
| 221 | +- `first`函数被调用,创建函数执行上下文并压入栈 |
| 222 | +- 执行`first`函数过程遇到`second`函数,再创建一个函数执行上下文并压入栈 |
| 223 | +- `second`函数执行完毕,对应的函数执行上下文被推出执行栈,执行下一个执行上下文`first`函数 |
| 224 | +- `first`函数执行完毕,对应的函数执行上下文也被推出栈中,然后执行全局上下文 |
| 225 | +- 所有代码执行完毕,全局上下文也会被推出栈中,程序结束 |
| 226 | + |
| 227 | + |
| 228 | +## 参考文献 |
| 229 | + |
| 230 | +- https://zhuanlan.zhihu.com/p/107552264 |
0 commit comments