理解执行上下文和执行栈,有助于理解提升机制、作用域、闭包等。
目录
4.1.1 确定 this 的值, 绑定 this (This Binding)
4.1.2 创建 词法环境(LexicalEnvironment)
4.1.3 创建 变量环境(VariableEnvironment)
1.什么是执行上下文
举个栗子~:
- 生活中,相同的话在不同的场合说,可能会有不同的意思,而这个说话的场合,就是我们说话的语境;
- 同理,编程中, 对程序语言进行“解读”的时候,也必须在特定的语境中,这个语境就是 JavaScript 中的执行上下文;
执行上下文 —— JavaScript 代码被解析和执行时,所在的环境;
概念分析~:
当代码运行时,会产生一个执行环境,在这个环境中:
- 所有变量会被事先提出来(变量提升),有的未初始化(let、const),有的被设置默认值 undefined(var);
- 方法也会被提出,并初始化;
- 代码从上往下开始执行;
综上叫做 JavaScript 执行上下文;
2.执行上下文的三种类别
全局执行上下文(只有一个):不在任何函数中的代码,都位于全局执行上下文中;它创建了一个全局对象,也就是浏览器对象(即 window 对象),this 指向这个全局对象;
函数执行上下文(可以有多个):在函数被调用时,才会被创建;每次调用函数时,都会创建一个新的执行上下文;
eval() 函数执行上下文:JavaScript 的 eval() 函数,执行其内部的代码时,会创建属于自己的执行上下文,(很少用而且不建议使用);
由概念和类别可以得出,执行上下文有以下特点:
- 单线程,只在主线程上运行;
- 同步执行,从上向下按顺序执行;
- 全局上下文只有一个,也就是 window 对象;
- 函数执行上下文没有限制,可以有多个;
- 函数每调用一次,就会产生一个新的执行上下文环境;
3.什么是执行栈(调用栈)
JavaScript 是单线程的,只能同时做一件事;当多个执行上下文存在时,如何控制它们的执行顺序呢?
执行栈就是负责管理多个执行上下文的,它存储了代码执行期间所有的执行上下文;
举个栗子~:
- 执行全局代码时,会产生一个执行上下文环境A;
- 调用函数时,会产生一个执行上下文环境B;
- 函数调用完成时,执行上下文环境B 以及 其中的数据都会被消除;
- 再重新回到全局上下文环境A;
这就是一个压栈出栈(后进先出 / LIFO last-in, first-out)的过程,如下图所示:
综上所述,处于 活动状态 的执行上下文环境,始终只有一个:
- JavaScript 代码首次执行时,创建全局执行上下文,将它 push 进执行栈;
- 函数调用时,创建函数执行上下文,将它 push 进执行栈;
- 函数调用完成后,将 函数执行上下文 pop 出栈;
再举个相对复杂的栗子~
var a = 'Hello World!'; // 1.进入全局上下文执行环境
function first() {
console.log('Inside first function');
second(); // 3.进入 second 函数上下文执行环境;second 函数执行完成后,返回 first 函数上下文执行环境
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first(); // 2.进入 first 函数上下文执行环境
console.log('Inside Global Execution Context'); // 4.first 函数执行完成后,返回 全局上下文执行环境;如果没有其他代码需要执行,则执行栈会把 全局执行上下文 从执行栈中弹出 pop
// 最终打印结果:
// Inside first function
// Inside second function
// Again inside first function
// Inside Global Execution Context
4.执行上下文的生命周期
1)创建阶段 —— 确定 this 指向、生成变量对象、建立作用域链
2)执行阶段 —— 变量赋值、函数引用、执行其他代码
3)销毁阶段 —— 执行完毕出栈,等待回收被销毁
刚看上面的三个阶段,你肯定是一脸懵逼的,我们挨个分析
4.1 创建阶段
创建阶段的工作分为:
- 确定 this 的值, 绑定 this (This Binding)
- 创建 词法环境(LexicalEnvironment)
- 创建 变量环境(VariableEnvironment)
4.1.1 确定 this 的值, 绑定 this (This Binding)
this 在不同的执行上下文中,是指向不同内容的:
- 全局执行上下文中,this 指向全局对象(浏览器中指向 window 对象, nodejs 中指向这个文件的 module 对象);
- 函数执行上下文中,this 的指向取决于函数的调用方式(默认绑定、隐式绑定、显式绑定/硬绑定、new 绑定、箭头函数)
函数执行上下文的 this 指向,可以参考下面的文章:
JavaScript深入之史上最全--5种this绑定全面解析 | 木易杨前端进阶高级前端进阶之路https://muyiy.cn/blog/3/3.1.html
简单概括 函数执行上下文 中的 this 绑定:
- 默认绑定:window 上
- 隐式绑定:obj.foo() —— obj 对象调用了 foo 方法,则 foo 方法中的 this 指向 obj(严格模式下,函数内的 this 指向 undefined)
- 显式绑定/硬绑定:call、apply、bind
- new 绑定:使用构造函数创建出来的对象
- 箭头函数绑定:取决于外层执行上下文 this 绑定在哪儿
let person = {
name: 'peter',
birthYear: 1994,
calcAge: function() {
console.log(2018 - this.birthYear);
}
}
person.calcAge();
// 'this' 指向 'person', 因为 'calcAge' 是被 'person' 对象引用调用的。
let calculateAge = person.calcAge;
calculateAge();
// 'this' 指向全局 window 对象,因为没有给出任何对象引用
4.1.2 创建 词法环境(LexicalEnvironment)
词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联关系。词法环境由环境记录(environment record)和可能为空引用(null)的外部词法环境组成。
简而言之,词法环境是一个包含标识符变量映射的结构。(这里的标识符表示变量/函数的名称,变量是对实际对象【包括函数类型对象】或原始值的引用)
词法环境包含两个部分:
- 环境记录 —— 存储 let 和 const 声明的变量 以及 函数声明 的实际位置
- 对外部环境的引用 —— 此处可以 访问 其外部词法环境
不同执行上下文的词法环境不同(以 全局、函数 为例)
全局执行上下文中:
- 词法环境的环境记录包括:window 对象、window 关联的方法属性(比如数组方法 splice)、用户自定义的全局变量;也就是 this 绑定在这里
- 词法环境的对外部环境的引用:null
GlobalExectionContext = { // 全局执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Object", // 全局环境
// 对外部环境的引用
outer: <null>
}
}
函数执行上下文中:
- 词法环境的环境记录包括:在函数内部定义的 变量、在函数内部定义的 函数、arguments 对象;也就是 this 绑定在这里
- 词法环境的对外部环境的引用:可以是全局环境,也可以是外部函数环境(比如一个函数调用了另一个函数)
FunctionExectionContext = { // 函数执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Declarative", // 函数环境
// 对外部环境的引用
outer: <Global or outer function environment reference>
}
}
对于 函数执行上下文 而言,环境记录 还包含了一个 arguments
对象,该对象包含了索引和传递给函数的参数之间的映射以及传递给函数的参数的 长度(数量)
function foo(a, b) {
var c = a + b;
}
foo(2, 3);
// arguments 对象
Arguments: {0: 2, 1: 3, length: 2},
4.1.3 创建 变量环境(VariableEnvironment)
变量环境的本质,也是一个词法环境,它具有上面定义的 词法环境 的所有属性
在 ES6 中,词法环境 和 变量环境有以下区别:
- 词法环境存储:let / const 声明的变量、函数声明
- 变量环境存储:var 声明的变量
举个栗子~:
var a;
var b = 1;
let c = 2;
const d = 3;
function fn (e, f) {
var g = 4;
return e + f + g;
}
a = fn(10, 20); // 函数被调用了,才会创建 函数执行上下文
把上方代码的执行上下文拆解一下,全局执行上下文包括:
- 词法环境 —— const、let 声明的变量 d、c / 函数 fn 声明
- 变量环境 —— var 声明的变量 a、b
GlobalExectionContext = { // 全局执行上下文
ThisBinding: <Global Object>,
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Object", // 全局环境
c: < uninitialized >,
d: < uninitialized >,
fn: < func >
},
outer: <null> // 外部环境引用
},
VariableEnvironment: { // 变量环境
EnvironmentRecord: { // 环境记录
Type: "Object",
a: < undefined >,
b: < undefined >
},
outer: <null>
}
}
把上方代码的执行上下文拆解一下,函数执行上下文包括:
- 词法环境 —— arguments
- 变量环境 —— var 声明的变量 g
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>
}
}
4.1.4 为什么会发生变量提升?
在 执行上下文环境 的创建阶段:
- 函数声明 会被存储到 词法环境中
- let / const 声明的变量,会被存储到 词法环境 中,并且是未初始化状态 uninitialized
- var 声明的变量,会被存储到 变量环境中,并且被设置为 undefined,也就是已经初始化了
因此,可以在变量声明之前:
- 访问 var 变量(undefind);这种变量可以进行变量提升
- 不可以访问 let / const 变量(引用错误,未初始化);这种变量无法进行变量提升
举个栗子~
function fn(){
console.log(a); // undefined;
var a = 1;
}
fn();
相当于
function fn(){
var a;
console.log(a);
a = 1;
}
fn();
4.2 执行阶段
执行阶段主要做这些事情:
- 变量赋值(包括 let、const、var)
- 函数引用
- 执行其他的代码
如果 Javascript 引擎在 变量声明 的实际位置,找不到 let 变量的值,那么将为 let 变量分配 undefined 值
4.3 销毁阶段
执行完毕出栈,等待回收被销毁