执行上下文
当 JavaScript 执行一段代码时,会创建一个对应的执行环境,这个环境就是执行上下文。它就像是一个容器,包含了该段代码运行所需的所有信息。
执行上下文的类型
全局执行上下文:代码首次运行时创建,只有一个函数执行上下文:每次调用函数时创建eval 执行上下文:使用 eval() 函数时创建(很少用)
执行上下文的工作流程
创建阶段
-
创建变量对象(VO)
- 建立arguments对象
- 扫描函数声明
- 扫描变量声明
-
建立作用域链
-
确定this指向
执行阶段
- 变量赋值
- 函数调用
- 执行其他代码
变量对象
当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。
对于每个执行上下文,都有三个重要属性:
- 变量对象(
Variable object,VO); - 作用域链(
Scope chain); - this;
变量对象
变量对象(Variable Object, VO)是执行上下文的重要组成部分,用于存储该上下文中定义的:
- 变量(
var) - 函数声明(
function) - 函数参数(
arguments)
全局上下文中的变量对象
- 全局 VO 就是全局对象本身(浏览器中是
window,Node.js 中是global) var声明的变量会成为全局对象的属性(let/const声明的不会)- 生命周期与程序运行周期一致(页面关闭时销毁)
创建阶段:
// 全局代码示例
var globalVar = 1;
function globalFunc() {}
// 对应的全局 VO 结构:
GlobalVO = {
globalVar: undefined, // 创建阶段初始化为 undefined,执行阶段才赋值 1
globalFunc: function globalFunc() {}, // 函数声明完整提升
// 其他内置属性和方法(如 window.location)...
}
代码执行阶段:
// 执行阶段会完成变量赋值
globalVar = 1;
console.log(window.globalVar); // 1(浏览器中全局变量挂载到 window)
函数上下文中的变量对象(活动对象AO)
- 称为活动对象(Activation Object, AO)
- 包含arguments对象
- 只在函数执行期间存在
创建阶段:
function test(a, b) {
var c = 1;
function d() {}
var e = function() {};
}
// 进入执行上下文时的 AO 结构:
AO = {
arguments: {0: a, 1: b, length: 2}, // 参数对象
a: undefined, // 参数初始化为 undefined(未传参则保持此值)
b: undefined, // 参数初始化为 undefined
c: undefined, // var 变量声明提升,初始为 undefined
d: function d() {}, // 函数声明完整提升(优先级高于变量)
e: undefined // 函数表达式只提升变量声明(赋值留在执行阶段)
}
代码执行阶段
function test(a, b) {
var c = 1;
function d() {}
var e = function() {};
}
// 假设调用
test(10, 20)
// ---------- 执行阶段后的 AO ----------
AO = {
arguments: {0: 10, 1: 20, length: 2}, // 参数被实际传入的值覆盖
a: 10, // 参数被赋值为实参 10
b: 20, // 参数被赋值为实参 20
c: 1, // var 变量被赋值 1
d: function d() {}, // 函数声明已在创建阶段完全提升(无变化)
e: function() {} // 函数表达式被赋值(匿名函数)
}
关键变化说明
| 属性 | 创建阶段 | 执行阶段 | 变化原因 |
|---|---|---|---|
arguments | {0: a, 1: b} | {0: 10, 1: 20} | 实参传入后覆盖原始参数值 |
a | undefined | 10 | 参数被赋值为调用时传入的实参 |
b | undefined | 20 | 同上 |
c | undefined | 1 | 变量赋值操作执行 |
e | undefined | function() {} | 函数表达式被赋值 |
特点:
- 函数声明会完整提升(函数声明 > 参数 > 变量声明)
- 变量声明会提升但值为undefined
- 执行阶段才会进行赋值操作
经典面试题
题目:以下代码的输出是什么?请详细解释原因
var b = 2
function testA() {
console.log(a);
a = 1;
}
testA(); // ???
function testB() {
a = 1;
console.log(a);
}
testB(); // ???
function testC() {
console.log(b); // ???
var b = 20;
console.log(b);
}
testC(); // ???
✅ 正确答案
function testA() {
console.log(a);
a = 1;
}
testA(); //:`Uncaught ReferenceError: a is not defined`。
function testB() {
a = 1;
console.log(a);
}
testB(); //:1
function testC() {
console.log(b); // :undefined
var b = 20;
console.log(b); // :20
}
testC(); //:第一次打印 是undefined, 第二次打印是 20
🔍 详细解析
执行上下文分两个阶段
- 创建阶段(变量对象初始化)
- 执行阶段(代码逐行执行
// 全局执行上下文
// 创建阶段(变量对象初始化)
GlobalVO = {
b: undefined, // 创建阶段初始化为 undefined
testA: function testA() {}, // 函数声明提升
testB: function testB() {}, // 函数声明提升
testC: function testC() {}, // 函数声明提升
}
// 执行阶段
testC(); // 第1次调用 testC()
b // 参数被赋值为2
testA(); // 调用函数 → 触发错误
testB(); // 调用函数
testC(); // 第2调用 testC()
// 函数的执行上下文分析
// 创建阶段------testA()------
AO = {
arguments: { length: 0 } // 无参数
}
// 执行阶段
// 第1步:console.log(a)
console.log(a); // 查找变量 a → 作用域链查找顺序:
// 1. 当前 AO_testA → 无
// 2. 全局 VO → 无
// → 报错:Uncaught ReferenceError: a is not defined
// 第2步:a = 1 → 未执行到此处(已报错)
// 创建阶段------testB()------
AO_testB = {
arguments: { length: 0 } // 无参数,无局部变量声明
}
// 执行阶段
// 第1步:a = 1 → 隐式创建全局变量(非严格模式)
a = 1; // 隐式声明全局变量 → GlobalVO.a = 1
// 第2步:console.log(a)
console.log(a); // 查找变量 a → 作用域链查找顺序:
// 1. 当前 AO_test → 无
// 2. 全局 VO → 找到 a = 1
// → 输出 1
// 创建阶段------testC()------
AO_testC= {
arguments: { length: 0 }, // 无参数,无局部变量声明
b: undefined // 函数内部 var b 声明提升
}
// 执行阶段
// 第1次调用 testC()(全局 b 尚未赋值)
console.log(b); // undefined(访问 AO_testC.b)
b = 20; // 修改 AO_testC.b = 20
console.log(b); // 20
// 第2次调用 testC()(全局 b = 2,但函数内部 b 仍独立)
console.log(b); // undefined(AO_testC.b 重新初始化为 undefined)
b = 20; // 修改 AO_testC.b = 20
console.log(b); // 20
作用域链(Scope Chain)
作用域链决定了代码中变量的访问顺序,它是由当前变量对象和所有父级变量对象组成的链式结构。
var global = 1;
function outer() {
var outerVar = 2;
function inner() {
var innerVar = 3;
console.log(global + outerVar + innerVar); // 6
}
inner();
}
outer();
作用域链的构建过程:
- inner函数的作用域链:[inner.VO, outer.VO, global.VO]
- 查找变量时按照这个顺序依次查找
this指向
this的值在函数被调用时确定,主要取决于调用方式:
// 1. 默认绑定
function foo() {
console.log(this); // 浏览器中指向window
}
foo();
// 2. 隐式绑定
var obj = {
bar: function() {
console.log(this); // 指向obj
}
};
obj.bar();
// 3. 显式绑定
function baz() {
console.log(this); // 指向指定的对象
}
baz.call({name: 'obj'});
// 4. new绑定
function Person(name) {
this.name = name; // this指向新创建的对象
}
var p = new Person('John');
执行上下文的工作流程
创建阶段
-
创建变量对象(VO)
- 建立arguments对象
- 扫描函数声明
- 扫描变量声明
2 建立作用域链
3 确定this指向
代码执行阶段
- 变量赋值
- 函数调用
- 执行其他代码
实际案例分析
var a = 1;
function test(b) {
console.log(a); // undefined
console.log(b); // 2
console.log(c); // function c() {}
var a = 3;
function c() {}
var d = function() {};
}
test(2);
执行过程解析:
-
全局上下文创建:
VO: { a: undefined, test: function }
-
执行全局代码:
a赋值为1调用test(2)
-
test函数上下文创建:
VO: { arguments: {0: 2, length: 1}, b: 2, a: undefined, c: function, d: undefined }
-
执行test函数:
- 按顺序执行console.log语句
- 然后进行赋值操作
闭包
闭包的本质是函数能够访问并保留其作用域链中的自由变量
// 闭包是指能够访问自由变量的函数
function outer() {
const a = 1; // 自由变量(对 inner 而言)
function inner() {
console.log(a); // a 是 inner 的自由变量
}
return inner;
}
const fn = outer();
fn(); // 输出 1(即使 outer 已执行完毕,a 仍可访问)