JS之执行上下文

87 阅读6分钟

执行上下文

当 JavaScript 执行一段代码时,会创建一个对应的执行环境,这个环境就是执行上下文。它就像是一个容器,包含了该段代码运行所需的所有信息。

执行上下文的类型
  • 全局执行上下文:代码首次运行时创建,只有一个
  • 函数执行上下文:每次调用函数时创建
  • eval 执行上下文:使用 eval() 函数时创建(很少用)

执行上下文的工作流程

创建阶段
  1. 创建变量对象(VO)

    • 建立arguments对象
    • 扫描函数声明
    • 扫描变量声明
  2. 建立作用域链

  3. 确定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}实参传入后覆盖原始参数值
aundefined10参数被赋值为调用时传入的实参
bundefined20同上
cundefined1变量赋值操作执行
eundefinedfunction() {}函数表达式被赋值

特点

  • 函数声明会完整提升(函数声明 > 参数 > 变量声明
  • 变量声明会提升但值为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();

作用域链的构建过程:

  1. inner函数的作用域链:[inner.VO, outer.VO, global.VO]
  2. 查找变量时按照这个顺序依次查找

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');

执行上下文的工作流程

创建阶段
  1. 创建变量对象(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);

执行过程解析

  1. 全局上下文创建:

    • VO: { a: undefined, test: function }
  2. 执行全局代码:

    • a赋值为1
    • 调用test(2)
  3. test函数上下文创建:

    • VO: { arguments: {0: 2, length: 1}, b: 2, a: undefined, c: function, d: undefined }
  4. 执行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 仍可访问)