手把手教会javascrip 函数的执行上下文

87 阅读6分钟

JavaScript执行上下文:深入理解代码执行机制

在JavaScript开发中,你是否曾经遇到过这样的困惑:为什么变量可以在声明前使用?为什么函数内部可以访问外部变量?为什么this的指向有时会让人摸不着头脑?这些看似神奇的现象背后,都隐藏着一个核心概念——执行上下文

本文将带你深入理解JavaScript执行上下文,通过丰富的图解和代码示例,让你彻底掌握这一重要概念。

一、什么是执行上下文?

**执行上下文(Execution Context)**是JavaScript代码执行时的环境,它决定了变量、函数和对象的可访问性,以及代码的执行顺序。

1.1 执行上下文的类型

JavaScript中有三种执行上下文:

  • 全局执行上下文(Global Execution Context):程序开始执行时创建,只有一个
  • 函数执行上下文(Function Execution Context):每次调用函数时创建,可以有多个
  • eval执行上下文(Eval Execution Context):使用eval()函数时创建(不推荐使用)

1.2 执行上下文栈(Execution Context Stack)

JavaScript引擎使用执行上下文栈(也称为调用栈)来管理执行上下文。栈遵循**后进先出(LIFO)**原则。

// 示例代码
console.log('Global start');

function func1() {
  console.log('func1 start');
  func2();
  console.log('func1 end');
}

function func2() {
  console.log('func2 start');
  func3();
  console.log('func2 end');
}

function func3() {
  console.log('func3 start');
  console.log('func3 end');
}

func1();
console.log('Global end');

执行顺序分析:

  1. 创建全局执行上下文,推入栈底
  2. 执行console.log('Global start')
  3. 调用func1(),创建func1执行上下文,推入栈顶
  4. 执行console.log('func1 start')
  5. 调用func2(),创建func2执行上下文,推入栈顶
  6. 执行console.log('func2 start')
  7. 调用func3(),创建func3执行上下文,推入栈顶
  8. 执行console.log('func3 start')console.log('func3 end')
  9. func3执行完毕,弹出栈顶
  10. 执行console.log('func2 end')
  11. func2执行完毕,弹出栈顶
  12. 执行console.log('func1 end')
  13. func1执行完毕,弹出栈顶
  14. 执行console.log('Global end')
  15. 程序结束,弹出全局执行上下文

视频演示:

二、执行上下文的生命周期

每个执行上下文都经历三个主要阶段:

2.1 创建阶段(Creation Phase)

在代码执行前,执行上下文会经历创建阶段:

  1. 创建变量对象(Variable Object)

    • 函数参数(函数执行上下文)
    • 函数声明(函数和变量名)
    • 变量声明(初始值为undefined)
  2. 建立作用域链(Scope Chain)

    • 确定变量的查找顺序
  3. 确定this指向(This Binding)

    • 确定this关键字的值

2.2 执行阶段(Execution Phase)

代码逐行执行,包括:

  • 变量赋值
  • 函数调用
  • 表达式计算

2.3 回收阶段(Recycling Phase)

执行完毕后,执行上下文被标记为可回收,等待垃圾回收机制处理。

三、执行上下文的组成部分

3.1 变量对象(Variable Object)与活动对象(Activation Object)

变量对象(VO)是执行上下文中存储变量和函数声明的地方。在函数执行上下文中,变量对象被称为活动对象(AO)

// 变量提升示例
console.log(myVar); // undefined(变量提升)
console.log(myFunc); // [Function: myFunc](函数声明提升)

var myVar = 'Hello World';

function myFunc() {
  console.log('Function executed');
}

myFunc(); // "Function executed"

创建阶段的变量对象:

// 伪代码表示
Variable Object = {
  myVar: undefined,
  myFunc: <function reference>
}

3.2 作用域链(Scope Chain)

作用域链决定了变量的查找顺序,从当前作用域开始,逐级向上查找。

var globalVar = 'Global';

function outer() {
  var outerVar = 'Outer';
  
  function inner() {
    var innerVar = 'Inner';
    
    console.log(innerVar);    // "Inner"(当前作用域)
    console.log(outerVar);    // "Outer"(外部作用域)
    console.log(globalVar);   // "Global"(全局作用域)
    // console.log(notDefined); // ReferenceError(未定义)
  }
  
  inner();
}

outer();

作用域链示意图:

inner作用域 → outer作用域 → 全局作用域 → null

3.3 this绑定(This Binding)

this的指向取决于函数的调用方式:

// 1. 默认绑定(全局或严格模式)
function showThis() {
  console.log(this);
}
showThis(); // window(非严格模式)或 undefined(严格模式)

// 2. 隐式绑定(方法调用)
const obj = {
  name: 'JavaScript',
  sayName: function() {
    console.log(this.name);
  }
};
obj.sayName(); // "JavaScript"

// 3. 显式绑定(call/apply/bind)
function introduce(language) {
  console.log(`I love ${this.name} and ${language}`);
}

const person = { name: 'Alice' };
introduce.call(person, 'Python'); // "I love Alice and Python"

// 4. new绑定(构造函数)
function Person(name) {
  this.name = name;
}
const john = new Person('John');
console.log(john.name); // "John"

四、执行上下文与作用域的关系

4.1 词法作用域(Lexical Scope)

JavaScript采用词法作用域(静态作用域),作用域在代码编写时就已经确定,而不是在运行时。

var x = 10;

function foo() {
  console.log(x);
}

function bar() {
  var x = 20;
  foo(); // 输出10,而不是20
}

bar();

4.2 闭包(Closure)

闭包是函数和其词法环境的组合,允许函数访问其外部作用域的变量。

function createCounter() {
  let count = 0;
  
  return function() {
    count++;
    console.log(count);
  };
}

const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3

闭包的执行上下文: 即使createCounter函数执行完毕,其执行上下文也不会立即销毁,因为返回的函数仍然引用着count变量。

五、深入理解变量提升

5.1 函数声明 vs 函数表达式

// 函数声明(会提升)
console.log(declaredFunc()); // "Function declared"

function declaredFunc() {
  return "Function declared";
}

// 函数表达式(不会提升)
// console.log(expressedFunc()); // TypeError: expressedFunc is not a function

var expressedFunc = function() {
  return "Function expressed";
};

5.2 let/const 与 var 的区别

// var 的变量提升
console.log(varVariable); // undefined
var varVariable = 'var value';

// let/const 的暂时性死区(Temporal Dead Zone)
// console.log(letVariable); // ReferenceError: Cannot access 'letVariable' before initialization
let letVariable = 'let value';

// const 必须初始化
// const constVariable; // SyntaxError: Missing initializer in const declaration
const constVariable = 'const value';

六、实际应用场景

6.1 事件处理中的this指向

class Button {
  constructor() {
    this.text = 'Click me';
    this.button = document.createElement('button');
    this.button.textContent = this.text;
    
    // 错误:this指向button元素
    // this.button.addEventListener('click', this.handleClick);
    
    // 正确:使用箭头函数或bind
    this.button.addEventListener('click', () => this.handleClick());
    // 或:this.button.addEventListener('click', this.handleClick.bind(this));
  }
  
  handleClick() {
    console.log(this.text); // 正确输出"Click me"
  }
}

6.2 模块模式与私有变量

const Module = (function() {
  // 私有变量
  let privateCounter = 0;
  
  // 私有函数
  function privateIncrement() {
    privateCounter++;
  }
  
  // 公共接口
  return {
    increment: function() {
      privateIncrement();
    },
    getCount: function() {
      return privateCounter;
    }
  };
})();

Module.increment();
Module.increment();
console.log(Module.getCount()); // 2
// console.log(Module.privateCounter); // undefined(无法访问私有变量)

七、常见问题与解答

Q1: 为什么函数可以在声明前调用?

A: 因为函数声明会在创建阶段被提升到当前作用域的顶部。

Q2: 什么是暂时性死区?

A:letconst声明之前访问变量会抛出错误,这个区域称为暂时性死区。

Q3: 箭头函数有执行上下文吗?

A: 箭头函数没有自己的thisargumentssupernew.target,它们继承自外层函数。

Q4: 如何避免this指向问题?

A: 使用箭头函数、bind()方法,或在类中使用类字段语法。

八、总结

JavaScript执行上下文是理解JavaScript运行机制的核心概念。通过掌握执行上下文的创建、执行和回收过程,以及变量对象、作用域链和this绑定的工作原理,你能够:

  1. 深入理解变量提升:明白为什么变量可以在声明前使用
  2. 掌握作用域链:理解变量的查找机制和闭包原理
  3. 正确使用this:避免常见的this指向错误
  4. 编写高质量代码:基于对执行机制的理解,写出更健壮的程序

执行上下文就像JavaScript引擎的"幕后导演",它决定了代码的执行顺序、变量的可访问性以及函数的调用关系。只有深入理解这个"导演"的工作方式,你才能真正掌握JavaScript这门语言。


互动思考: 你能解释下面代码的执行结果吗?

var a = 1;
function test() {
  console.log(a);
  var a = 2;
}
test();

欢迎在评论区分享你的答案和思考!