0基础学习javascript之:你知道作用域闭包吗?let 为什么能和闭包联手?你对作用域了解多少?

125 阅读5分钟

在 JavaScript 的世界中,作用域(Scope)闭包(Closure) 是两个核心但又常被误解的概念。它们不仅是语言设计的精妙之处,更是前端工程师必须掌握的底层逻辑。无论是日常开发中的异步回调、模块封装,还是大厂面试中的高频考点,闭包都无处不在。而 ES6 引入的 let 关键字,更是与闭包产生了奇妙的化学反应——它让原本“失控”的循环变量变得可控,让闭包行为更加符合直觉。

那么,什么是闭包?为什么 for (var i = 1; i <= 5; i++) { setTimeout(() => console.log(i), 100) } 会输出 6 个 6,而用 let 就能输出 1 到 5?闭包是如何“记住”变量的?它真的会引发内存泄漏吗?本文将系统梳理 作用域与闭包的核心知识点,结合经典案例与大厂面试题,带你彻底掌握这一 JavaScript 的灵魂机制。


一、什么是闭包?

闭包(Closure)是指:一个函数能够记住并访问其词法作用域(Lexical Scope),即使该函数在其词法作用域之外执行。

1.1 词法作用域是基础

JavaScript 采用词法作用域(也叫静态作用域),意味着函数的作用域在代码书写时就已确定,而非运行时动态决定。

js
编辑
function foo() {
    var a = 2;
    function bar() {
        console.log(a); // bar 能访问 foo 的变量 a
    }
    return bar;
}

var baz = foo();
baz(); // 输出 2
  • bar 在 foo 内部定义,因此它能访问 foo 的作用域。
  • 即使 foo 执行完毕,bar 被返回并在外部调用,它依然能访问 a
  • 这种“跨越作用域边界”的能力,就是闭包。

1.2 闭包的本质:函数 + 作用域链

当函数被创建时,它会捕获当前的作用域环境,形成一个“闭包包”。这个包包含:

  • 函数自身的代码
  • 对外部作用域中变量的引用(不是复制!)

因此,闭包不是某个特殊语法,而是 JavaScript 函数作用域机制的自然结果


二、闭包的经典场景

2.1 异步回调中的闭包

js
编辑
function wait(message) {
    setTimeout(function timer() {
        console.log(message); // message 被闭包“记住”
    }, 1000);
}
wait("hello, closure!");
  • wait 执行完后,message 按理应被销毁。
  • 但 timer 函数通过闭包保留了对 message 的引用,1秒后仍能正确输出。

2.2 模块模式(模拟私有变量)

js
编辑
function createCounter() {
    let count = 0;
    return {
        increment: () => ++count,
        decrement: () => --count,
        getCount: () => count
    };
}

const counter = createCounter();
counter.increment(); // 1
counter.getCount();  // 1
// 外部无法直接访问 count,实现“私有”

这是闭包在封装和数据隐藏中的典型应用。


三、循环与闭包:经典陷阱与解决方案

3.1 问题:var 导致的“变量共享”

js
编辑
for (var i = 1; i <= 5; i++) {
    setTimeout(() => {
        console.log(i); // 输出 6, 6, 6, 6, 6
    }, 100);
}

原因分析:

  • var 声明的 i 属于函数作用域(或全局),整个循环共享同一个 i
  • setTimeout 是异步的,等到回调执行时,循环早已结束,i = 6
  • 所有回调函数通过闭包引用的是同一个 i,因此都输出 6。

3.2 解决方案一:IIFE(立即执行函数表达式)

js
编辑
for (var i = 1; i <= 5; i++) {
    (function(j) {
        setTimeout(() => {
            console.log(j); // 输出 1, 2, 3, 4, 5
        }, 100);
    })(i);
}
  • 每次循环创建一个新作用域,j 是该作用域的局部变量。
  • 每个 setTimeout 回调闭包捕获的是各自的 j

3.3 解决方案二:使用 let(推荐)

js
编辑
for (let i = 1; i <= 5; i++) {
    setTimeout(() => {
        console.log(i); // 输出 1, 2, 3, 4, 5
    }, 100);
}

为什么 let 能解决?

  • let 具有块级作用域
  • 关键机制:在 for 循环中,let 会在每次迭代时创建一个新的绑定(binding)
  • 每个 setTimeout 回调闭包捕获的是当前迭代的 i 副本,而非共享变量。

✅ 这就是 let 与闭包的“联手”:let 为每次循环创建独立作用域,闭包则精准捕获该作用域中的值。


四、闭包的注意事项

4.1 内存泄漏?

闭包会延长变量的生命周期,但这不等于内存泄漏。现代 JavaScript 引擎(如 V8)能智能回收不再被引用的闭包环境。

只有当闭包意外持有大对象且长期不释放时,才可能造成内存问题。

4.2 性能影响?

闭包会增加作用域链的查找层级,但现代引擎已高度优化,正常使用无需担心性能


五、大厂面试题精选

面试题 1:解释以下代码输出

js
编辑
var a = [];
for (var i = 0; i < 10; i++) {
    a[i] = function () {
        console.log(i);
    };
}
a[6](); // 输出什么?

答案: 10
解析: 所有函数共享同一个 i,循环结束后 i = 10


面试题 2:如何修改上题,使 a[6]() 输出 6

答案(两种):

js
编辑
// 方案1:IIFE
for (var i = 0; i < 10; i++) {
    a[i] = (function(j) {
        return function() { console.log(j); };
    })(i);
}

// 方案2:let
for (let i = 0; i < 10; i++) {
    a[i] = function() { console.log(i); };
}

面试题 3:实现一个缓存函数(记忆化)

js
编辑
function memoize(fn) {
    const cache = {};
    return function(...args) {
        const key = JSON.stringify(args);
        if (key in cache) return cache[key];
        return cache[key] = fn.apply(this, args);
    };
}

考察点: 闭包用于封装私有缓存对象。


六、总结

  • 闭包 = 函数 + 词法作用域的引用
  • 它让函数能“记住”定义时的环境,是 JavaScript 异步、模块化、回调等特性的基石。
  • var 在循环中因作用域问题导致闭包“失效”,而 let 通过块级作用域为每次迭代创建独立绑定,完美解决该问题。
  • 闭包不是 bug,而是特性;合理使用可提升代码封装性与可维护性。

记住:闭包不是你主动“写”出来的,而是你写函数时,JavaScript 自动为你“生成”的。

掌握作用域与闭包,你就掌握了 JavaScript 的灵魂。下次面试官问:“说说你对闭包的理解”,你可以自信地说: “闭包,是我最熟悉的陌生人。”