为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
JavaScript

说实话。当你走进 JavaScript 技术面试时,面试官寻找的不是一个能从 Stack Overflow 复制粘贴的人。他们寻找的是理解机器如何工作的人。

有两个概念比其他任何概念都更容易让开发者栽跟头:作用域闭包

大多数开发者对它们只有一种模糊的、只能靠比划来解释的理解。他们知道它能工作,但不知道为什么。在面试中,这种模糊的理解是行不通的。当你无法解释为什么一个变量是 undefined,或者为什么一个循环打印出错误的数字时,你就无法通过测试。

要掌握这些概念,你必须停止把 JavaScript 当作一个黑盒子,并开始像 JavaScript 引擎一样思考。

以下是你需要内化的心智模型。


第一部分:作用域是一套规则

开发者犯的第一个错误是认为作用域是变量存放的物理"位置"。

更好的理解是,将作用域视为 JavaScript 引擎使用的一套规则,用于根据标识符名称来查找变量。

JavaScript 使用一种叫做词法作用域的东西。这是你今天读到的最重要的术语。"词法"与编译的词法分析或解析阶段有关。

通俗地说: 作用域是由你(代码作者)物理上编写代码的位置决定的。你的函数嵌套方式决定了你的作用域的嵌套方式。一旦编写完成,这些规则在代码运行之前就(基本)固定不变了。

心智模型:办公楼

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

  • 一楼(大堂)是全局作用域
  • 你声明的每个函数都会在你当前所在的楼层之上创建一个新的、私有的楼层。

当引擎在函数内部(比如,在 3 楼)执行代码,并且它需要查找一个变量(比如 manager)时,它会遵循一个严格的程序:

  1. 本地查找: 三楼有自己的 manager 吗?如果有,使用它。
  2. 向外查找: 如果没有,走下楼梯到二楼。他们有吗?
  3. 重复: 继续一次下一层楼,直到到达大堂(全局作用域)。
  4. 放弃: 如果大堂里也没有,抛出一个 ReferenceError。它不存在。

关键的面试注意点: 作用域查找只向上(向外)进行。它们从不向下(向内)进行。大堂看不到三楼发生的事情。

var buildingName = "JS Towers"; // 大堂 (全局)

function floorTwo() {
    var manager = "Kyle"; // 二楼作用域

    function floorThree() {
        var developer = "You"; // 三楼作用域

        // 引擎在这里(三楼)查找,没找到。
        // 下到二楼,找到了 'manager'。成功。
        console.log(manager);
    }
}

第二部分:闭包并非魔法

如果作用域是用于查找的规则集,那么闭包就是你弯曲这些规则时发生的情况。

许多人对闭包的定义很模糊。让我们来精确地定义它。

闭包是这样一个现象:一个函数即使是在它的词法作用域之外被执行,它仍然能够访问定义时的那个作用域。

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

心智模型:背包

如果你在函数 A 内部定义了函数 B,那么函数 B 会获得一个指向其外围函数 A 作用域的"隐藏链接"。

当函数 B 被传递到函数 A 之外,在你程序的其他地方使用时,它并非空手而去。它带走了那个隐藏链接。

这就像一个背包:函数 B 携带了一个背包,里面装着函数 B 被创建时,函数 A 中存在的所有变量。

无论函数 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();

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

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

在面试中,不要只说"它记住了"。应该说:"由于闭包,inner 保留了对 outer 词法作用域的引用,阻止了该作用域被垃圾回收。"


第三部分:经典的面试陷阱

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

问题:

for (var i = 1; i <= 3; i++) {
    setTimeout(function timer() {
        console.log(i);
    }, i * 1000);
}

初级开发者期望的结果: 分别打印 123,间隔一秒。
实际发生的情况: 分别打印 444,间隔一秒。

如何像专家一样解释:

  1. 识别作用域: "变量 i 是用 var 声明的。这意味着它属于全局作用域(或者如果被包裹在函数中,则属于函数作用域),而不是循环块级作用域。"
  2. 识别闭包: "setTimeout 内部的 timer 函数闭包了那个单一的、共享的全局变量 i。"
  3. 执行过程: "循环几乎瞬间就运行完了。当它结束时,i 的值是 4(因为此时循环条件 i <= 3 不再满足)。"
  4. 结果: "当三个 timer 函数在几秒钟后最终执行时,它们都查看自己的闭包背包,找到了对 i完全相同的引用,并打印出它当前的值:4。"

修复方法(ES6 之前的 IIFE 模式):
你需要为循环的每次迭代创建一个新的作用域来"捕获" i 的当前值。

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

(注意:在现代 JS 中,你只需在 for 循环头中将 var i 改为 let i,因为 let 会自动为每次迭代创建一个新的块级作用域。同时提到两者,表明你了解历史背景和现代知识。)


总结

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

  • 作用域是关于变量在哪里可访问,在编写时(词法上)决定。想想办公楼的楼层。
  • 闭包是关于变量何时可访问。一个函数即使在稍后执行时也能记住它的词法作用域。想想背包。

当你在面试中查看代码时,在心里追踪作用域线。问自己:"这个变量属于哪个作用域桶?"以及"这个函数在它的背包里携带了什么?"

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

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消