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);
});
}