执行上下文:JavaScript 代码执行的核心环境
简介
执行上下文可以说是 JavaScript 代码执行的一个环境,存放了代码执行所需的变量、变量查找的作用域链规则以及 this
指向等。它是 JavaScript 很底层的东西,许多问题如变量提升、作用域链和闭包等都可以在执行上下文中找到答案。因此,理解执行上下文对于我们深入学习 JavaScript 至关重要。
执行上下文的分类
执行上下文主要分为三种:
- 全局执行上下文:当进入全局代码时会进行编译,在编译中创建全局执行上下文,并生成可执行代码。
- 函数执行上下文:执行代码的过程中,如果遇到函数调用,会编译函数内的代码并创建函数执行上下文,生成可执行代码。
- eval 执行上下文:当使用
eval
函数时,eval
的代码也会被编译,并创建执行上下文。(基本不使用)
因为执行上下文是在编译阶段创建的,所以我们先了解一下 JavaScript 代码的执行过程。
执行上下文的生命周期
-
创建(Creation Phase) :
- 当 JavaScript 引擎开始执行一段代码时,会创建相应的执行上下文。
- 编译阶段会初始化变量环境和词法环境,并生成可执行代码。
-
执行(Execution Phase) :
- 编译阶段完成后,JavaScript 引擎开始逐行执行代码。
- 在执行过程中,如果遇到函数调用,会创建新的执行上下文并将其压入调用栈。
-
销毁(Destruction Phase) :
- 当一个函数执行完毕后,其对应的执行上下文会从调用栈中出栈。
- 与该执行上下文相关的变量和函数会被销毁,释放内存资源。
示例:
function foo() {
var a = 1;
let b = 2;
const c = 3;
function inner() {
console.log('Inner function');
}
}
foo();
调用栈
调用栈(Call Stack)是 JavaScript 的执行机制。它是一个后进先出(LIFO)的数据结构,记录了程序中当前正在执行的所有函数调用。调用栈帮助 JavaScript 引擎跟踪函数的调用顺序,确保每个函数在执行完毕后能够正确返回到调用它的函数。
执行代码中的块级作用域
执行代码过程中遇到代码块时,会先将里面通过 let
或 const
声明的变量存放在词法环境中并设置为初始化。词法环境内部维护了一个小型的栈结构,栈底是函数最外层的变量,每遇到一个代码块,就将所包含的变量压入词法环境的栈结构,代码块执行结束后,将包含的变量弹出,销毁。
function foo() {
var a = 1;
let b = 2;
{
let b = 3;
var c = 4;
let d = 5;
console.log(a); // 1
console.log(b); // 3
}
console.log(b); // 2
console.log(c); // 4
console.log(d); // ReferenceError: d is not defined
}
foo();
这里为什么 console.log(d); 打印的是ReferenceError: d is not defined。如下图解释:
单个执行上下文中变量的查找规则
沿着词法环境的栈顶向下查询,如果在词法环境的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有找到,就继续在变量环境中查找。
函数执行上下文
函数执行上下文里面outer指针的理解:
在 JavaScript 中,每个执行上下文都有一个 outer
指针,这个指针指向当前执行上下文的外部(父级)执行上下文。outer
指针是词法环境(Lexical Environment)的一部分,用于实现词法作用域和闭包。
词法环境(Lexical Environment)
词法环境是一个对象,包含两个主要部分:
-
环境记录(Environment Record) :
- 存储变量和函数的绑定。
-
outer
指针:- 指向当前词法环境的外部词法环境。
outer
指针的作用
-
变量查找:
- 当在一个执行上下文中查找变量时,如果当前词法环境中没有找到该变量,JavaScript 引擎会沿着
outer
指针向上查找,直到找到变量或到达全局词法环境。
- 当在一个执行上下文中查找变量时,如果当前词法环境中没有找到该变量,JavaScript 引擎会沿着
-
闭包:
- 闭包是指一个函数可以访问其外部作用域中的变量。
outer
指针使得内部函数可以访问外部函数的变量。
- 闭包是指一个函数可以访问其外部作用域中的变量。
示例
function bar(){
console.log(myname);// 打印lisi
}
function foo(){
var myname = "Alies";
bar()
console.log(myname);// 打印Alies
}
var myname = "lisi";
foo();
变量提升
变量提升是指在 JavaScript 代码执行过程中,JavaScript 引擎将变量的声明部分和函数声明部分提升到代码开头的行为。变量被提升后,会给变量设置默认值 undefined
。
-
变量提升的实现:
- 并不是物理地移动代码的位置,而是在编译阶段被 JavaScript 引擎放入内存中。
- var变量提升会赋值为
undefined
,函数变量名会将整个函数提升。 - let、const 不存在变量提升。
// hoisting
console.log(a,func);
console.log(b); // 词法环境中的变量/常量, 在声明之前不可以访问。
// ReferenceError: Cannot access 'b' before initialization
// 暂时性死区 TDZ temporary dead zone
var a = 1;
function func(){
}
let b = 2;
b++;// 词法环境里找b
-
函数表达式:
- 只会将变量提升,不会将函数体提升。
示例
console.log(add(2, 3));
// ReferenceError: Cannot access 'add' before initialization
const add = function(a, b) {
return a + b;
};
-
声明冲突:
- 当有多个相同类型的声明时,最后一个声明会覆盖之前的声明。
- 当函数声明与变量声明同时出现时,函数声明优先级更高。
作用域
作用域是一套定义函数调用和变量使用的规则,主要有三种作用域:
-
全局作用域:
- 对象在代码的任何地方都能访问,其生命周期伴随着页面的生命周期。
-
函数作用域:
- 在函数内部定义的变量和函数,只能在内部访问,外部不能访问到。函数执行结束后,函数内部定义的变量会被销毁。
-
块级作用域:
- 由代码块包含的代码会形成一个块级作用域(ES6 之前没有),类似于函数作用域。
通过 var
声明的变量没有块级作用域,通过 let
和 const
声明的变量有块级作用域。
作用域链
在函数中,如果在当前作用域中找不到所需要的变量,就会沿着作用域链往上查找,直到找到为止。
-
变量查找规则:
- 首先从执行上下文的词法环境中查找。
- 如果找不到,就在变量环境中查找。
- 再找不到,就会沿着
outer
指针去外部的执行上下文中查找。 outer
指针的具体引用由词法作用域决定。
总结
通过理解执行上下文、变量提升、作用域链和块级作用域等概念,我们可以更好地管理变量和函数的生命周期,避免常见的陷阱,写出更加清晰和高效的 JavaScript 代码。希望这篇文章对你有所帮助