«

在 JavaScript 里闭包的一些陷阱

时间:2026-3-30 23:53     作者:独元殇     分类: 前端技术


欢迎关注我的公众号,名叫「串串狗小刊」

作为一个前端程序员,我每天的任务大部分时间都是在和 js 斗智斗勇。虽然有了 AI 的辅助,但是懂点常见的坑,还是能节省大把的时间和 词元 token 的。js 太美了:

img

今天说的这些陷阱主要是闭包里的。

闭包就是一直反常识的,在函数内部创建的变量,在执行完函数后居然还能贮存到内存里的一种情况。很容易造成内存泄漏。

比如这个例子:

function createCounter() {
    let count = 0; // 这个变量被“封闭”在内部

    return function() {
        count++;
        console.log(count);
    };
}

const counter = createCounter();

counter(); // 输出: 1
counter(); // 输出: 2
counter(); // 输出: 3

你会发现,函数内部的 count 阴魂不散,每次执行都会加一。

这就是一个典型的闭包。

循环里面的闭包

大家在控制台执行下面的代码试试:

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

// 解决方案 IIFE 作用域
for (var i = 0; i < 5; i++) {
  (function(k) {
    setTimeout(function() {
      console.log(k);
    }, 1000);
  })(i);
}

它会一口气输出 5 个 5!

为什么,很简单,当 setTimeout 执行时,循环已经结束。

循环是一瞬间就跑完了 5 次,这个时候 5 个 setTimeout 都时间距离确实很近,但不至于都是同一个值,因此值这个时候没定死,而是拿的 i 的引用。它们 1s 后执行的时候,去访问的是 i ,而不是 1 2 3 4 5 ,循环早已结束了,i 肯定是 5 了!所以输出了 5 个 5 。

这是因为 var 的变量,能在函数作用域外提升。

解决方案有两种,要么你使用 let ,这个更符合人的思维,不会出现这些你很难预料的 bug 。要么就加一个 (function(){})(i); 包起来,这叫 IIFE (立即调用函数表达式)。它每次创建都会新建一个函数作用域,然后将我们的 i 拍一个快照,传递给 k 。

至于 var 和 let,可以这样理解,var 的 5 个 i ,其实是一个共用的 i ,而 let 的 五个 i ,是 5 个独立的 i 。互不干扰。

this 绑定

看看这个:

const obj = {
  name: 'oliver',
  report: function() {
    return function() {
      console.log(`I am ${this.name}`);
    };
  }
};

obj.report()(); // I am undefined

// 解决方案 1
// 输出时 bind 原来那个 obj
obj.report().bind(obj)();  // I am oliver

// 解决方案 2
// 使用更符合人逻辑直觉的 ()=>{} 箭头函数
const obj = {
  name: 'oliver',
  report: function() {
    return () => {
      console.log(`I am ${this.name}`);
    };
  }
};

理论上,this 貌似应该指向 name ,但却输出了 undefined 。原因很简单,return 的那个函数,有自己的生态,它有自己的「新 this」,覆盖了以前那个 this。

解决方案 1 很鸡肋。看起来不美观。还得上 ES6 大法!你意味 箭头函数 只是写着简单了一些?不不不,它也是让一些东西变得更加的直观符合逻辑。箭头函数是可以捕获上文的 this 的。

普通函数,和 箭头函数 还是不一样的。普通函数,this 默认指向 window 的,而箭头函数,压根没 自己的 this ,都是借用的外层的 this 。

内存泄漏

和我们最初那个例子差不多,这个更直观:

function object() {
  const largeObj = new Array(1000000).fill('*');
  return function() {
    console.log('hi');
  };
}

object();  // 这样执行,不会造成内存泄漏,因为 V8 引擎会优化
const leak = object();  // 这种就会永久贮存内存了!!!
let leak2 = object();   // 这种也会

// 解决方案
leak2 = null;  // 注意,const 的 leak 不行,必须是 let 的 leak2

首先,如果 leak 和 leak2 是在作用域内,那么这个作用域销毁时,会自动销毁 largeObj。

但是如果 leak 和 leak2 ,是在全局变量,那... 如果你不 null 一下主动清除,就会一直保持在内存里。

标签: 原创 JS