js 内存泄漏

时间:2024-01-20 19:24:05

JScript 对象和 COM 对象使用了不同的垃圾回收机制,所以闭包在这些旧版本 IE 中可能会导致问题。在这些版本的 IE 中,把 HTML 元素保存在某个闭包的作用域中,就相当于宣布该元素不能被销毁。来看下面的例子:

let element = document.getElementById('someElement'); 
 element.onclick = () => console.log(element.id); 
}

以上代码创建了一个闭包,即 element 元素的事件处理程序(事件处理程序将在第 13 章讨论)。而这个处理程序又创建了一个循环引用。匿名函数引用着 assignHandler()的活动对象,阻止了对element 的引用计数归零。只要这个匿名函数存在,element 的引用计数就至少等于 1。也就是说,内存不会被回收。其实只要这个例子稍加修改,就可以避免这种情况,比如:

let element = document.getElementById('someElement'); 
 let id = element.id; 
 element.onclick = () => console.log(id);
 element = null; 
}

在这个修改后的版本中,闭包改为引用一个保存着 element.id 的变量 id,从而消除了循环引用。不过,光有这一步还不足以解决内存问题。因为闭包还是会引用包含函数的活动对象,而其中包含element。即使闭包没有直接引用 element,包含函数的活动对象上还是保存着对它的引用。因此,必须再把 element 设置为 null。这样就解除了对这个 COM 对象的引用,其引用计数也会减少,从而确保其内存可以在适当的时候被回收。

立即调用的函数表达式 立即调用的匿名函数又被称作立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)。它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式。下面是一个简单的例子:

// 块级作用域 
})();

使用 IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。ECMAScript 5 尚未支持块级作用域,使用 IIFE模拟块级作用域是相当普遍的。比如下面的例子:

(function () { 
 for (var i = 0; i < count; i++) { 
 console.log(i); 
 } 
})(); 
console.log(i); // 抛出错误

前面的代码在执行到 IIFE 外部的 console.log()时会出错,因为它访问的变量是在 IIFE 内部定义的,在外部访问不到。在 ECMAScript 5.1 及以前,为了防止变量定义外泄,IIFE 是个非常有效的方式。这样也不会导致闭包相关的内存问题,因为不存在对这个匿名函数的引用。为此,只要函数执行完毕,其作用域链就可以被销毁。 在 ECMAScript 6 以后,IIFE 就没有那么必要了,因为块级作用域中的变量无须 IIFE 就可以实现同样的隔离。下面展示了两种不同的块级作用域形式:

{ 
 let i; 
 for (i = 0; i < count; i++) { 
 console.log(i); 
 } 
} 
console.log(i); // 抛出错误
// 循环的块级作用域
for (let i = 0; i < count; i++) { 
 console.log(i); 
} 
console.log(i); // 抛出错误
说明 IIFE 用途的一个实际的例子,就是可以用它锁定参数值。比如:
let divs = document.querySelectorAll('div'); 
// 达不到目的! 
for (var i = 0; i < divs.length; ++i) { 
 divs[i].addEventListener('click', function() { 
 console.log(i); 
 }); 
}

这里使用 var 关键字声明了循环迭代变量 i,但这个变量并不会被限制在 for 循环的块级作用域内。 因此,渲染到页面上之后,点击每个都会弹出元素总数。这是因为在执行单击处理程序时,迭代变量的值是循环结束时的最终值,即元素的个数。而且,这个变量 i 存在于循环体外部,随时可以访问。

以前,为了实现点击第几个就显示相应的索引值,需要借助 IIFE 来执行一个函数表达式,传入每次循环的当前索引,从而“锁定”点击时应该显示的索引值:

for (var i = 0; i < divs.length; ++i) { 
 divs[i].addEventListener('click', (function(frozenCounter) {
 return function() { 
 console.log(frozenCounter); 
 }; 
 })(i)); 
}

而使用 ECMAScript 块级作用域变量,就不用这么大动干戈了:

for (let i = 0; i < divs.length; ++i) { 
 divs[i].addEventListener('click', function() {
  console.log(i); 
 }); 
} 
这样就可以让每次点击都显示正确的索引了。这里,事件处理程序执行时就会引用 for 循环块级作用域中的索引值。这是因为在 ECMAScript 6 中,如果对 for 循环使用块级作用域变量关键字,在这里就是 let,那么循环就会为每个循环创建独立的变量,从而让每个单击处理程序都能引用特定的索引。
但要注意,如果把变量声明拿到 for 循环外部,那就不行了。下面这种写法会碰到跟在循环中使用var i = 0 同样的问题:
```let divs = document.querySelectorAll('div'); 
// 达不到目的!
let i; 
for (i = 0; i < divs.length; ++i) { 
 divs[i].addEventListener('click', function() { 
 console.log(i); 
 }); 
}