JavaScript 的这个陷阱,坑了多少开发者?


闭包 (Closure) 无疑是 JavaScript
中最强大、最迷人的特性之一。它赋予了函数访问其定义时所在词法环境的能力,即使该函数在其定义的作用域之外执行。凭借闭包,我们可以实现数据封装、模块化、柯里化等高级编程技巧。

然而,硬币的另一面是,闭包也常常被视为 JavaScript
中最容易误解、最容易出错的特性之一。稍有不慎,就会掉入闭包的“陷阱”,导致内存泄漏、意外的变量共享等问题。

内存泄漏:“永不消逝” 的变量

闭包最常见的陷阱就是内存泄漏。当一个闭包引用了外部函数的变量,而这个闭包又被长期持有(例如,作为事件处理程序或定时器回调),那么外部函数的变量就无法被垃圾回收,导致内存泄漏。

function createHandler() {  
  let largeObject = new Array(1000000).fill("data"); // 创建一个大对象  
  
  return function() {  
    console.log("Handler clicked");  
    //  没有直接使用 largeObject, 但由于闭包的存在, largeObject 无法被回收  
  };  
}  
  
document.getElementById("myButton").addEventListener("click", createHandler());  

在这个例子中, createHandler 函数返回一个事件处理函数(闭包)。这个闭包引用了 createHandler 函数的 largeObject 变量。即使我们没有在事件处理函数中直接使用 largeObject ,但由于闭包的存在, largeObject
无法被垃圾回收,导致内存泄漏。

解决方法:

  • 解除引用: 在不需要闭包时,手动解除对闭包的引用,例如:

    let handler = createHandler();  
    

    document.getElementById(“myButton”).addEventListener(“click”, handler);
    // … 当不再需要事件处理程序时 …
    document.getElementById(“myButton”).removeEventListener(“click”, handler);
    handler = null; // 解除对闭包的引用

  • 避免不必要的闭包: 如果不需要访问外部函数的变量,就不要创建闭包。

  • 将变量设置为null : 在闭包中, 将不再需要的外部变量手动设置为 null

循环中的闭包:“意料之外” 的共享

在循环中使用闭包时,很容易出现意外的变量共享问题。

在这个例子中,我们期望 setTimeout 的回调函数(闭包)分别输出 0, 1, 2, 3, 4。但实际输出的却是 5 次 5。这是因为 setTimeout 是异步执行的,当回调函数执行时,循环已经结束, i 的值已经变成了 5。而且,由于使用了 var 声明 i
,所有的回调函数共享的是同一个 i 变量。

解决方法:

  • 使用 let 声明循环变量: let 具有块级作用域,每次循环都会创建一个新的 i 变量,避免了变量共享。

  • 使用立即执行函数 (IIFE): 创建一个立即执行函数,将循环变量 i 作为参数传递进去,形成一个闭包,每次循环都会创建一个新的作用域。

  • 使用 bind 方法: 使用 bind 方法将循环变量 i 绑定到回调函数上。

意外的副作用:修改共享变量

由于闭包可以访问外部函数的变量,如果不小心修改了这些变量,可能会导致意想不到的副作用。

function outer() {  
  let counter = 0;  
  
  return {  
    increment: function() { counter++; },  
    getCount: function() { return counter; }  
  };  
}  
  
const myCounter = outer();  
myCounter.increment();  
myCounter.increment();  
console.log(myCounter.getCount()); // 输出 2  

在这个例子中, 虽然我们希望 counter 变量是 outer 函数的私有变量, 但是通过闭包, 我们仍然可以在外部修改它.

解决方法:

  • 最小化共享: 尽量减少闭包对外部变量的修改,优先使用局部变量。

  • 使用不可变数据: 如果外部变量是对象或数组,尽量使用不可变数据结构,避免意外修改。

  • 更明确的接口: 如果确实需要修改, 那么就通过定义明确的接口来修改。