JavaScript 作用域与闭包:停止死记硬背,开始理解

发布: (2025年12月26日 GMT+8 18:56)
9 min read
原文: Dev.to

Source: Dev.to

让我们坦率直言

当你走进一次 JavaScript 技术面试时,面试官并不在找会从 Stack Overflow 复制粘贴的人。他们想要的是了解机器工作原理的人。

有两个概念比其他任何东西更容易让开发者卡壳:ScopeClosure

大多数开发者对它们只有模糊的、“概念性”的理解。他们知道它能工作,却不知道为什么。在面试中,这种“概念性”根本不够。当你无法解释为什么一个变量是 undefined,或为什么循环打印出错误的数字时,你就会挂掉。

要掌握这些概念,别再把 JavaScript 当作黑箱,而要像 JavaScript 引擎一样思考。

第1部分 – 范围是一套规则

开发者常犯的第一个错误是把 作用域 当作变量所在的物理“位置”。

更好的理解是把作用域看作 JavaScript 引擎用来确定根据标识符名称去哪里查找变量的 一套规则

JavaScript 使用一种叫做 词法作用域 的机制。这是你今天会看到的最重要的术语。“词法”与编译过程中的 词法分析(lexing)或解析阶段有关。

通俗解释: 作用域由你(作者)实际编写代码的位置决定。函数的嵌套层级决定了作用域的嵌套层级。一旦写好,这些规则(大多数情况下)在代码运行前就已经固定。

思维模型:办公大楼

想象你的程序是一座多层办公大楼。

层级描述
底层(大堂)全局作用域
每个你声明的函数在当前所在层之上新增的私有层

当引擎在函数内部执行代码(比如在第3层)并且需要查找变量(我们称之为 starthroat)时,它会遵循以下严格的步骤:

  1. 先在本层查找 – 第3层是否有自己的 starthroat?如果有,直接使用。
  2. 向外查找 – 如果没有,向下走楼梯到第2层。那层有吗?
  3. 重复 – 一层层向下,直到到达大堂(全局作用域)。
  4. 放弃 – 如果大堂也没有,则抛出 ReferenceError。表示变量不存在。

关键面试提示: 作用域查找只能向(向外)进行,永远不会向(向内)查找。大堂看不到第3层发生了什么。

var buildingName = "JS Towers"; // Lobby (global)

function floorTwo() {
    var manager = "Kyle"; // 2nd‑floor scope

    function floorThree() {
        var developer = "You"; // 3rd‑floor scope

        // Engine looks here (floor 3), finds nothing.
        // Goes down to floor 2, finds `manager`. Success.
        console.log(manager);
    }
}

Source:

第 2 部分 – 闭包不是魔法

如果说作用域是查找规则的集合,闭包 就是当你弯曲这些规则时会发生的事情。

很多人对闭包的定义模糊。我们来给出精确定义:

闭包 是指当一个函数在其词法作用域之外执行时,仍然能够访问该作用域的现象。

通常情况下,当函数执行完毕,它的作用域会被垃圾回收,内存被释放。闭包阻止了这种情况的发生。

思维模型:背包

如果在 函数 A 中定义 函数 B,函数 B 会获得指向函数 A 所在作用域的“隐藏链接”。

当函数 B 被从函数 A 中传出以供其他地方使用时,它并不会空手而出。它会把那条隐藏链接一起带走。

可以把它想象成一个背包。函数 B 背着一个背包,里面装着函数 A 在创建函数 B 时所存在的所有变量。无论函数 B 在哪里运行,或者在时间上相隔多久,它都可以打开这个背包并访问这些变量。

function outer() {
    var secret = "XYZ_123"; // 按照作用域规则,这应该在 outer() 完成时消失。

    function inner() {
        // `inner` 对 `secret` 变量形成闭包。
        // 它把 `secret` 放进自己的背包里。
        console.log("The secret is: " + secret);
    }

    return inner; // 我们把 `inner` 发送到外部世界。
}

// outer() 完全运行并结束。
var myRef = outer(); // `inner` 现在存储在 `myRef` 中

// ... 几个小时后 ...

// `myRef` 在全局作用域中被执行。
// 然而,它仍然记得 `outer` 的作用域。
myRef(); // "The secret is: XYZ_123"

在面试中,不要只说“它记得”。应该说:

“由于闭包,inner 保留了对 outer 词法作用域的引用,防止该作用域被垃圾回收。”

Source:

第 3 部分 – 经典面试陷阱

如果在面试中被问及闭包,你有约 90 % 的概率会看到下面这个循环题的变体。它用于检验你是否理解 作用域边界值引用 之间的区别。

题目

for (var i = 1; i  // (original code omitted in source)

专业提示: 要解决此问题,可以为每次迭代创建一个新的词法环境(使用 let 而不是 var),或使用 IIFE 或 bind 捕获当前值。

使用 let(块级作用域)

// Using let (block‑scoped)
for (let i = 1; i  console.log(i), i * 1000);
}

使用 IIFE

// Using an IIFE
for (var i = 1; i  console.log(j), j * 1000);
    })(i);
}

结论

  • 作用域 = 引擎定位标识符时遵循的 规则
  • 闭包 = 让函数在其原始词法环境本应消失后仍能访问这些规则的 机制

掌握这些思维模型,你就能自信地回答任何关于作用域或闭包的面试问题——更重要的是,你会明白 为什么 JavaScript 会表现出这样的行为。

Source:

修复方案(Pre‑ES6 IIFE 模式)

你需要为循环的每一次迭代创建一个作用域,以“捕获”当前的 i 值。

for (var i = 1; i <= 3; i++) {
    // 创建一个立即调用函数表达式 (IIFE)
    // 这会为每一次循环迭代创建一个新的作用域气泡。
    (function (j) { 
        setTimeout(function timer() {
            // timer 现在闭包了 'j',它在本次迭代的作用域中是唯一的
            console.log(j); 
        }, j * 1000);
    })(i); // 传入当前的 'i' 值
}

(注意:在现代 JavaScript 中,你只需把 for 循环头部的 var i 改为 let i,因为 let 会为每一次迭代自动创建一个新的块级作用域。这里同时提及两种写法是为了展示历史背景和现代用法。)

摘要

不要死记硬背代码片段。要记住模型。

  • Scope(作用域) 关注变量在何处可访问,由写入时(词法上)决定。可以把它想象成办公楼的楼层。
  • Closure(闭包) 关注变量何时可访问。函数即使在以后执行,也会记住其词法作用域。可以把它想象成背包。

在面试中查看代码时,脑中追踪作用域线。自问:

  1. “这个变量属于哪个作用域桶?”
  2. “这个函数的背包里装的是什么?”

这样做,你不仅能通过面试,还能真正理解你每天使用的语言。

Back to Blog

相关文章

阅读更多 »

VAR、LET 和 CONST 在 JavaScript 中

🧠 JavaScript 如何运行你的代码 非常重要:JavaScript 并不会立即逐行执行你的代码。它会先准备好所有内容,然后再运行它。 备忘录…

JavaScript 中的对象

什么是对象? - 对象是一种可以容纳多个变量的变量。 - 它是键‑值对的集合,每个键都有对应的值。 - 组合……