揭秘JavaScript执行机制:作用域链、词法作用域与闭包

51 阅读7分钟

揭秘JavaScript执行机制:作用域链、词法作用域与闭包

你以为JavaScript代码只是从上到下执行?那可就太小看它了!本文将带你深入JS引擎核心,揭示代码执行背后的精妙机制。

引言:一个令人困惑的代码案例

先来看这段代码,猜猜它会输出什么?

function outer() {
  const secret = "🍪";
  return function inner() {
    console.log(secret);
  };
}

const revealSecret = outer();
revealSecret(); // 输出什么?

如果你猜到输出🍪,恭喜你!但为什么outer函数执行完毕后,inner函数还能访问它的变量?这正是闭包的魔力所在!让我们深入探索背后的机制。

JavaScript 执行机制基础

JavaScript 引擎在执行代码时依赖这些关键机制:

  • 调用栈:按LIFO(后进先出)原则,记录函数调用序列与执行上下文

  • 作用域:严格定义了变量可访问性规则(当前作用域→上级作用域→全局)

  • 作用域链:变量查找的完整路径链条

  • 执行上下文:包含变量环境、词法环境和this绑定的执行环境,

  • 变量环境:处理var声明和变量提升

  • 词法环境:管理let/const声明和暂时性死区(TDZ)

🏗️ 调用栈:函数执行的指挥部

JavaScript引擎使用调用栈(Call Stack) 来管理函数执行顺序:

function greet() {
  console.log("Hello!");
  sayName();
}

function sayName() {
  console.log("Alice");
}

greet();

当执行这段代码时:

  1. greet()被调用,创建执行上下文并入栈
  2. console.log("Hello!")入栈执行后出栈
  3. sayName()被调用,新执行上下文入栈
  4. console.log("Alice")入栈执行后出栈
  5. sayName()执行完毕出栈
  6. greet()执行完毕出栈

🔍 作用域与作用域链:变量的寻宝地图

作用域的三层结构

// 全局作用域
const globalVar = "🌍";

function outer() {
  // outer函数作用域
  const outerVar = "🏰";
  
  function inner() {
    // inner函数作用域
    const innerVar = "🔮";
    console.log(globalVar + outerVar + innerVar); // 🌍🏰🔮
  }
  
  inner();
}

outer();

作用域链的查找规则

4cc93089245e45944d7b537f76214c7e.png

为什么全局作用域会率先进入调用栈中?

  • 它是程序执行的逻辑起点、作用域链的根节点,以及宿主环境与 JavaScript 代码的接口。这一设计确保了单线程执行模型的有序性,以及变量查找、函数调用等机制的一致性。
  • 即使在无显式函数调用的代码中(如纯声明式代码),全局上下文也必须存在,以承载变量声明、执行顶级语句。可以说,全局作用域是 JavaScript 执行机制的 “地基” ,所有后续的函数调用都建立在这个基础之上。

当访问变量时,JS引擎会:

  1. 在当前作用域查找
  2. 找不到则向外层作用域查找
  3. 直到全局作用域
  4. 如果全局作用域也没有,则报错

作用域链就是这个查找的路径链!

🧱 执行上下文:代码运行的宇宙飞船

当函数被调用时,会创建执行上下文(Execution Context),它包含两个核心组件:

1. 变量环境(Variable Environment)

  • 存储var声明的变量
  • 存储函数声明
  • 变量提升的发生地

2. 词法环境(Lexical Environment)

  • 存储let/const声明的变量
  • 包含 暂时性死区(TDZ) 特性
  • 记录外部引用(outer)
function demo() {
  console.log(varVar); // undefined (变量提升,提升到当前作用域的顶部)
  console.log(letVar); // ReferenceError (暂时性死区)
  
  var varVar = "var变量";
  let letVar = "let变量";
}

4cc93089245e45944d7b537f76214c7e.png

回答图中的问题:当执行到这段代码时,myName的值应该是使用全局执行上下文的,还是使用foo函数执行上下文的?

  • 答案:全局执行上下文,即“极客时间”

  • 原因:

  1. 词法作用域规则:JavaScript采用词法作用域,变量作用域由定义位置决定,而非调用位置。
  2. bar函数定义位置bar在全局作用域定义,其作用域链的上层是全局作用域。
  3. 变量查找路径bar内部访问myName时,先查自身作用域(无),再沿作用域链找到全局的myName
  4. foo的局部变量屏蔽foomyName仅在foo的执行上下文中有效,无法被bar访问。
  5. 闭包特性:若barfoo内部定义,则会捕获foo的作用域,但此处bar定义在全局。

📜 词法作用域:写代码时就决定的命运

词法作用域是指变量和函数的作用域由其在代码中定义的位置(而非调用位置)静态决定,其作用域链在编译阶段即被确定,使得函数能够捕获并持续访问其定义时的环境变量。

词法作用域(Lexical Scope)是理解闭包的关键:

function bar() {
    console.log(myName); // 访问的是全局的myName
}

function foo() {
    var myName = "极客"; // 该变量仅在foo作用域内有效
    bar(); // 调用bar,但bar的词法作用域指向全局
}
var myName = "骑士";
foo(); // 输出"骑士"而非"极客"

这个示例清晰展示了词法作用域的本质:bar函数在全局定义,其变量查找路径指向全局环境,与它在哪里被调用无关。

关键点

  • 函数的作用域在声明时确定,而非调用时
  • 无论bar在哪里调用,它访问的都是它声明时位置food
  • 这就是词法作用域的静态特性
  • 词法作用域是变量可见性的静态规则(由代码定义位置决定),而作用域链是运行时基于词法作用域构建的动态变量查找路径(由嵌套的词法环境链表构成)。
function bar() {
    var myName = "极客世界"
    let test1 = 5
    if (1) {
        let myName = "Chrome浏览器"
        console.log(test)
    }
}
function foo() {
    var myName = "极客邦"
    let test = 2
    {//块级作用域
        let test = 3//因为有了花括号,可以重复申明
        bar()
    }
}
var myName = "极客时间"
let myAge = 10
let test = 1
foo()//输出1

image.png 16b1569d0c37495ee66b5a5298eb9dd1.png

🎁 闭包:函数的专属背包

回到开头的例子,解释闭包的形成:

function createCounter() {
  let count = 0; // 闭包捕获的变量
  
  return {
    increment: () => count++,
    getValue: () => count
  };
}

const counter = createCounter();
counter.increment();
console.log(counter.getValue()); // 1

闭包的形成过程

  1. createCounter执行,创建执行上下文
  2. 返回的对象方法引用了count变量
  3. createCounter执行完毕,但其执行上下文并未销毁
  4. count被保存在闭包中,成为"函数的专属背包"

image.png

闭包的实际应用

  1. 数据封装:创建私有变量
  2. 函数工厂:生成配置不同的函数
  3. 模块模式:实现模块化编程
  4. 事件处理:保存状态信息
// 使用闭包实现私有变量
function createBankAccount(initialBalance) {
  let balance = initialBalance; // 私有变量
  
  return {
    deposit: amount => balance += amount,
    withdraw: amount => balance -= amount,
    getBalance: () => balance
  };
}

const account = createBankAccount(100);
account.withdraw(30);
console.log(account.getBalance()); // 70

💡 关键概念关系图

graph TD
    A[词法作用域] -->|决定| B[作用域链];
    B -->|确定| C[变量查找路径];
    D[函数调用] -->|创建| E[执行上下文];
    E -->|包含| F[变量环境];
    E -->|包含| G[词法环境];
    G -->|指向| H[外部引用outer];
    H -->|形成| I[闭包];

作用域高手调试大法

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="./3.js"> // 将你要调试的代码放在这里,然后在控制台中执行即可
    </script>
</body>
</html>

屏幕截图 2025-06-02 215437.png

在最后运行的那一步打上断点,然后运行,便可以在这里观察到作用域的分布

🌟 总结:概念间的完美交响曲

  1. 词法作用域在代码编写时确定作用域链
  2. 函数调用时创建执行上下文
  3. 执行上下文通过**外部引用(outer)**连接形成作用域链
  4. 当函数访问外部变量时,形成闭包
  5. 闭包让函数可以"记住"其词法作用域的环境

JavaScript的这些机制就像一场精心编排的交响乐🎻,每个概念都是不可或缺的乐器,共同奏响代码执行的和谐乐章。

理解这些机制,你就能

  • 避免常见的变量作用域陷阱
  • 写出更安全可靠的闭包代码
  • 优化内存使用,防止泄漏
  • 真正掌握JavaScript的核心运行原理

下次当你看到闭包时,记得它不只是个神秘的概念,而是词法作用域和执行上下文共同创造的魔法!✨