一文搞懂闭包和闭包问题
1.闭包和闭包的应用场景
介绍闭包
闭包(Closure)是一种在编程中常见的概念,它指的是一个函数可以访问并操作其词法作用域(函数定义时所在的作用域)之外的变量的能力。换句话说,闭包允许函数“捕获”其外部环境中的变量,即使在函数执行后,这些变量也能保持在内存中。
形成闭包的条件:
形成闭包的条件是在一个函数内部定义了另一个函数,并且内部函数引用了外部函数作用域中的变量。具体来说,以下两个条件需要同时满足才能形成闭包:
- 函数内部定义了另一个函数:闭包是由函数内部的函数(嵌套函数)形成的。这个内部函数可以是在另一个函数的代码块中定义的,也可以是作为函数表达式或返回值返回的。
- 内部函数引用了外部函数作用域中的变量:闭包的关键是内部函数捕获了外部函数作用域中的变量。这意味着在内部函数中可以访问并使用外部函数的局部变量、参数或者函数声明。
闭包的特点在于:即使外部函数执行结束,内部函数仍然可以访问外部函数作用域中的变量,因为内部函数的作用域链仍然保留了对外部函数作用域的引用。这样就使得外部函数的局部变量在内部函数中继续可用,即使外部函数已经执行完毕。
应用场景
以下是一些常见的情况,你可能会在这些情况下使用闭包:
保护变量作用域: 闭包可以创建一个私有作用域,防止变量被外部访问或修改。这在模块化和封装中非常有用。
jsfunction counter() { let count = 0; return function () { return ++count; }; } const increment = counter(); console.log(increment()); // 输出:1 console.log(increment()); // 输出:2
回调函数: 闭包可以用于创建回调函数,使函数能够访问其定义时的上下文,而不是调用时的上下文。
jsfunction fetchData(url, callback) { fetch(url) .then((response) => response.json()) .then((data) => callback(data)) .catch((error) => console.error(error)); } fetchData("https://api.example.com/data", function (data) { console.log(data); });
循环中的问题:(事件循环机制导致的闭包问题) 在循环中使用闭包可以解决变量在异步操作中的问题。——> 创建一个新的闭包(存储外面的循环变量到内存中)
jsfor (var i = 0; i < 5; i++) { setTimeout(function () { console.log(i); // 输出:5 5 5 5 5 }, 1000); } // 使用闭包解决:注意这里可以用let解决,但是原理不是闭包,而是块级作用域 for (var i = 0; i < 5; i++) { //其实实际上这里是两层闭包了! (function (j) { setTimeout(function () { console.log(j); // 输出:0 1 2 3 4 }, 1000); })(i); }
缓存数据: 闭包可以用于缓存函数的结果,避免重复计算。
jsfunction memoize(func) { const cache = {}; //一个hash表 return function (arg) { if (cache[arg] === undefined) { cache[arg] = func(arg); //根据传进来的参数,判断是否计算过这个参数相关的结果了,如果缓存没有值,就执行函数 } return cache[arg]; //如果缓存有值就用缓存值 }; } const expensiveCalculation = memoize(function (n) { console.log(`Calculating for ${n}`); return n * 2; }); console.log(expensiveCalculation(5)); // 输出:Calculating for 5,然后返回 10 console.log(expensiveCalculation(5)); // 直接返回 10,无需重新计算
这些只是使用闭包的一些例子,实际上闭包在 JavaScript 编程中有广泛的应用,可以用于实现许多不同的模式和技术。
总结:说白了主要是在定义高阶函数的时候,返回一个函数,这个函数去使用外层函数的参数或者外层定义的变量,这样就相当于应用了闭包
1.构造回调函数高阶函数:比如 react 中构造一个 input 的 onChange 事件回调函数:根据属性名(这个就是参数)的不同,生成用于绑定不同属性的回调函数(返回值),将表单数据保存到状态中
2.比如构造一个可以缓存函数的执行结果的闭包函数,结果定义为变量放在外层函数,执行逻辑放在内层(根据缓存是否有值判断应该返回缓存值,还是再执行一次函数)
3.解决循环中的异步任务获取的作用域(应该给循环的每一个异步任务单独开启一个作用域)
4.构造计数器,计数变量应该被保护作用域,用闭包来保护作用域
2.闭包问题以及两种解决方案

在这段代码中,循环将会执行 10 次,每次循环都会设置一个定时器 setTimeout
,每个定时器都会在 1000 毫秒(1 秒)后执行一个函数。——> 实际上本身已经存在一层闭包了,每一个 setTimeout 都是一个闭包函数(for 定义的变量是外层函数的变量)
然而,由于 JavaScript 的事件循环机制,循环会在很短的时间内完成所有的迭代,而定时器的回调函数在循环执行完毕后才会被调用(因为定时器是宏任务)(但是没有被调用的时候变量无法持久化在内存里面,这是导致闭包问题的根本原因)。这意味着定时器回调函数实际上会在循环结束后,统一在一个时间点被调用,而不是在每次循环中分别被调用。
由于这个机制,当定时器回调函数执行时,循环已经执行完毕,此时 i
的值已经变成了 10。因此,无论定时器执行多少次,输出的结果都将是 10,而不是从 0 到 9。
重点:
首先要明确闭包把变量持久化到内存的时机是在函数被调用的时候
在闭包的情况下,当一个函数被定义时,它会创建一个闭包,其中包含了函数内部引用的外部变量。这些外部变量在闭包中被“捕获”,并且在函数执行时存储在内存中。然后,当这个闭包被返回并保存在其他变量或数据结构中时,闭包内的变量将继续存在,即使函数调用已经完成。
考点:事件循环机制导致的闭包问题
事件循环机制导致的闭包问题通常与异步编程有关。在 JavaScript 中,事件循环是一种处理异步代码的机制,它允许在主线程上执行代码,并在非阻塞的情况下处理异步任务(先把异步任务放到异步线程)。这使得 JavaScript 可以处理网络请求、定时器、事件监听等异步操作。
闭包问题(也就是作用域导致获取变量值不准确的问题)通常在使用循环中创建闭包时会出现。
闭包是指函数可以访问并操作其定义时所在的词法作用域中的变量。
闭包问题的概念:在循环中使用闭包时,由于闭包延迟执行或在异步场景中使用,闭包中可能会捕获到循环变量的引用,而不是其当前值。这可能导致在回调函数执行时,循环已经结束,循环变量具有最终值,而不是在闭包中创建时的期望值。
上面的题干就形成了闭包问题!
闭包问题形成的三个原因:
1.事件循环机制,定时器在最后才延迟运行
2.闭包时闭包函数(内层函数)没有被调用的时候变量无法持久化在内存里面
3.let 声明的变量是函数作用域,会在每一次 for 循环中共享,而不是重新创建
闭包问题的解决
为了解决闭包问题,我们需要在每次循环迭代中创建一个新的作用域,以便捕获正确的 i
值。通常可以使用立即调用函数表达式(IIFE)或 let
关键字来解决这个问题:
使用 IIFE:其实实际上这里是创建一个新的闭包,一共变成了两层闭包!
for (var i = 0; i < 3; i++) {
(function (index) {
setTimeout(function () {
console.log(index);
}, 1000);
})(i); //这是一个立即调用函数表达式。它的目的是在每次循环迭代时创建一个新的函数作用域,以保留当前循环迭代的 i 的值,从而避免 JavaScript 事件循环机制导致的闭包问题。
}
使用 let
:
for (let i = 0; i < 3; i++) {
//let 变量具有局部作用域(块级作用域),在使用 let 关键字定义的变量,在每次循环迭代时都会创建一个新的绑定。这意味着在循环中使用 let 声明的变量在每次迭代时都拥有一个全新的独立的作用域,而不像使用 var 声明的变量那样只有一个共享的作用域。
setTimeout(function () {
console.log(i);
}, 1000);
}
通过这样的方式,每个定时器回调函数都会捕获到正确的 i
值,输出结果将是 0、1 和 2。
3.let 块级作用域的理解,以及为什么可以解决闭包问题
函数作用域和块级作用域的对比
1.函数作用域 function{}:函数里面任意层次(任意块级)定义的变量在函数任何地方都可以访问(块的外层也可以访问)——> 相当于在函数里层定义的变量也会被提升到函数的第一层来,这样里面任意一层都可以访问!
例子:
function example() {
var x = 10;
if (true) {
var x = 20; // 在同一个函数作用域内,因为这行代码在后面,所以x 被覆盖了
}
console.log(x); // 输出 20,访问的是if里层定义的x
}
2.块级作用域{}:函数里面的块里面定义的变量只能在本块或者更里层可以访问,块的外层不可以访问
例子:
function example() {
let x = 10; //if里面是新的块级作用域,新定义的x不会影响外面x的访问!
if (true) {
let x = 20; // 在不同的块级作用域内,不会被外面访问
}
console.log(x); // 输出 10,访问的还是外层块的x
}
var 定义的变量的函数作用域产生的问题: 用 let 定义的变量的块级作用域来解决
var
定义的变量在 JavaScript 中具有函数作用域(function scope)而不是块级作用域(block scope)。这意味着使用 var
定义的变量在整个函数体内都是可见的,而不仅仅是在定义它的代码块内部。
这种行为可能会导致一些意外和不符合预期的结果。下面是一些体现 var
没有块级作用域的示例:
变量泄漏到外部的问题:
jsfunction example() { if (true) { var x = 10; } console.log(x); // 输出 10,x 在 if 块外部仍然可见 }
循环中的问题:(闭包问题)
jsfor (var i = 0; i < 5; i++) { setTimeout(function () { console.log(i); // 输出 5 五次,因为 i 是共享的变量 }, 1000); }
由于 var 声明的变量是函数作用域,会在每一次 for 循环中共享,而不是重新创建,所以最后定时器获取的都是那一个变成 5 的变量!
使用 let 声明变量来解决
要解决这个问题,你可以使用
let
关键字代替var
来声明变量,let
具有块级作用域,因为在每次循环迭代时都会新建一个块级作用域,所以每次循环都会创建一个新的变量实例,从而避免上述问题。jsfor (let i = 0; i < 5; i++) { setTimeout(function () { console.log(i); }, 1000); }
在使用
let
声明的情况下,每个闭包都持有一个不同的i
值,因此会按预期输出从 0 到 4。函数内部变量被覆盖的问题:——> 其实也是变量泄漏到外部的问题
jsfunction example() { var x = 10; if (true) { var x = 20; // 在同一个函数作用域内,因为这行代码在后面,x 被覆盖了 } console.log(x); // 输出 20,访问的是if里层定义的x }
使用 let 定义变量来解决
如果在同一个作用域内使用
let
声明变量,它会遵循块级作用域规则,不会像var
那样被覆盖。因此,使用let
声明变量的情况下,内部作用域的变量不会影响外部作用域的同名变量。让我们通过示例来理解:
jsfunction example() { let x = 10; //if里面是新的块级作用域,新定义的x不会影响外面x的访问! if (true) { let x = 20; // 在不同的块级作用域内,不会被外面访问 } console.log(x); // 输出 10,访问的还是外层块的x }
上面我们了解到因为
let
具有块级作用域(每次 for 的时候都新建 let 变量)从而解决了闭包问题!那么 js 都在什么情况下会新建块级作用域{}?
在 JavaScript 中,块级作用域(block scope)是指由一对花括号
{} 创建的范围
,其中的变量在这个范围内是可见的,并且在范围外不可访问。在以下情况下会创建新的块级作用域:——> 每一个{}就是 let 的作用域
注意:var 的作用域是函数作用域 function{},变量的访问范围:函数作用域>块级作用域
函数体内部: 函数是 JavaScript 中的一个主要的块级作用域单元。在函数内部定义的变量在函数范围内可见,而在函数外部不可访问。(类似函数作用域,区别在于只能在这个函数的第一层使用)
条件语句中的代码块: 使用
if
、else
、switch
等条件语句时,它们的代码块中也会创建新的块级作用域。在这些代码块中定义的变量仅在其所在代码块内部可见。——> 所以使用 let 可以解决上面的闭包问题循环语句中的代码块: 类似于条件语句,循环语句如
for
、while
、do-while
也会创建新的块级作用域。在循环体内部定义的变量只在循环体范围内有效。——> 重点因为let具有块级作用域,所以在for循环新建块级作用域的时候就会新建for里面定义的let变量!
总之,在任何花括号 {}
内部定义的 let 块级作用域变量都会限制在只能在其所在的块级作用域中可见,这种作用域限制可以提高代码的可维护性和避免变量冲突问题。