A.3 在 jQuery 中创建闭包
我们曾经介绍过的jQuery库中的许多方法都至少要接收一个函数作为参数。为方便起见,我们通常都在这种情况下使用匿名函数,以便在必需时再定义函数的行为。但是,这也意味着我们很少在顶级命名空间中定义函数;也就是说,这些函数都是内部函数,而内部函数很容易就会变成闭包。
A.3.1 $(document).ready()
的参数
我们使用jQuery编写的几乎全部代码都要放在作为$(document).ready()
参数的一个函数内部。这样做是为了保证在代码运行之前DOM已经就绪,而DOM就绪通常是运行jQuery代码的一个必要条件。当创建了一个函数并把它传递给.ready()
之后,这个函数的引用就会被保存为全局jQuery对象的一部分。在稍后的某个时间——当DOM就绪时,这个引用就会被调用。
由于我们通常把$(document).ready()
放在代码结构的顶层,因而这个函数不会成为闭包。但是,我们的代码通常都是在这个函数内部编写的,所以这些代码都处于一个内部函数中:
代码清单A-10
$(document).ready(function() {
var readyVar = 0;
function innerFn() {
readyVar++;
console.log('readyVar = ' + readyVar);
}
innerFn();
innerFn();
});
这看上去同前面的很多例子都差不多,只不过外部函数是传入到$(document).ready()
中的一个回调函数。由于innerFn()
定义在这个回调函数中,而且引用了位于回调函数作用域中的readyVar
,因此innerFn()
及其环境就创建了一个闭包。我们两次调用这个内部函数,通过观察两次输出之间保持的readyVar
的值,就可以证明这一点:
readyVar = 1
readyVar = 2
把大多数jQuery代码都放在一个函数体中是很有用的,因为这样可以避免某些命名空间冲突。例如,正是这个特性可以使我们通过调用jQuery.noConflict()
为其他库释放简写方式$
,但我们仍然能够定义在$(document).ready()
中使用的局部简写方式。
A.3.2 绑定事件处理程序
.ready()
结构通常用于包装其他的jQuery代码,包括事件处理程序的赋值。因为处理程序是函数,它们也就变成了内部函数;而且,因为这些内部函数会被保存并在以后调用,于是它们也会创建闭包。以一个简单的单击处理程序为例:
代码清单A-11
$(document).ready(function() {
var counter = 0;
$('#button-1').click(function(event) {
event.preventDefault();
counter++;
console.log('counter = ' + counter);
});
});
由于变量counter
是在.ready()
处理程序中声明的,所以它只对位于这个块中的jQuery代码有效,对.ready()
处理程序外部的代码无效。然而,这个变量可以被.click()
处理程序中的代码引用,在这个例子中.click()
应用程序会递增并显示该变量的值。由于创建了闭包,每次单击按钮都会引用counter
的同一个实例。也就是说,消息会持续显示一组递增的值,而不是每次都显示1。
counter = 1
counter = 2
counter = 3
事件处理程序同其他函数一样,也能够共享它们的封闭环境:
代码清单A-12
- $(document).ready(function() {
- var counter = 0;
- $('#button-1').click(function(event) {
- event.preventDefault();
- counter++;
- console.log('counter = ' + counter);
- });
- $('#button-2').click(function(event) {
- event.preventDefault();
- counter--;
- console.log('counter = ' + counter);
- });
- });
因为这两个函数引用的是同一个变量counter
,所以两个链接的递增和递减操作会影响同一个值,而不是各自独立的值。
counter = 1
counter = 2
counter = 1
counter = 0
A.3.3 在循环中绑定处理程序
鉴于闭包的独特运行方式,在循环中绑定处理程序需要一些特殊的技巧。假设我们想在一个循环中创建多个元素,然后基于循环的索引为这些元素绑定行为:
代码清单A-13
$(document).ready(function() {
for (var i = 0; i < 5; i++) {
$('<div>Print ' + i + '</div>')
.click(function() {
console.log(i);
}).insertBefore('#results');
}
});
变量i
依次被设置为0~4,而每次循环都会创建一个新的<div>
元素。每个新元素都有一个不同的文本标签:
- Print 0
- Print 1
- Print 2
- Print 3
- Print 4
你可能会认为单击其中一项会看到相应的编号出现在控制台中。可是,单击页面中的任何一个元素都会显示数值5
。换句话说,即使在绑定处理程序时i
的值每次都不一样,每个click
处理程序最终引用的i
都相同,都等于单击事件实际发生时i
的最终值(5)。
解决这个问题的方式有很多。首先,可以使用jQuery的$.each()
函数来代替for
循环:
代码清单A-14
- $(document).ready(function() {
- $.each([0, 1, 2, 3, 4], function(index, value) {
- $('<div>Print ' + value + '</div>')
- .click(function() {
- console.log(value);
- }).insertBefore('#results');
- });
- });
因为函数的参数类似于在函数中定义的变量,所以每次循环的value
实际上都是不同的变量。结果,每个click
处理程序都指向一个不同的value
变量,因而每次单击输出的值会与元素的标签文本匹配。
同样利用函数参数的这个特性,不必使用$.each()
也可以解决这个问题。在for
循环内部,可以定义并执行一个新函数,让它负责把变量i
的值分配到不同的变量中去:
代码清单A-15
- $(document).ready(function() {
- for (var i = 0; i < 5; i++) {
- (function(value) {
- $('<div>Print ' + value + '</div>')
- .click(function() {
- console.log(value);
- }).insertBefore('#results');
- })(i);
- }
- });
这种结构我们在第8章看到过,它的名字叫立即调用的函数表达式(IIFE),前面曾在调用$.noConflict()
之后利用它为jQuery
对象重新定义别名$
。在这里,我们利用它将i
传给变量value
,以便value
在每个单击处理程序中都有不同的值。
最后,还可以使用jQuery的事件系统换个角度来解决这个问题。我们知道,.on()
方法接受一个对象参数,该参数以event.data
的形式传入事件处理程序中:
代码清单A-16
- $(document).ready(function() {
- for (var i = 0; i < 5; i++) {
- $('<div>Print ' + i + '</div>')
- .on('click', {value: i}, function(event) {
- console.log(event.data.value);
- }).insertBefore('#results');
- }
- });
在这里,我们将 i
作为 .on()
方法的数据,而在事件处理程序内部,能够通过event.data.value
取得它的值。同样,因为event
是函数的参数,每次调用处理程序时它都是一个独立的实例,而不是在所有调用中共享的一个值。
A.3.4 命名及匿名函数
这些例子都和我们常规的jQuery代码一样使用了匿名函数。但是,这不会影响到闭包的创建。换句话说,无论命名函数还是匿名函数,都可以用来创建闭包。例如,我们可以编写一个匿名函数,报告jQuery对象中每个<input>
按钮的索引:
代码清单A-17
$(document).ready(function() {
$('input').each(function(index) {
$(this).click(function(event) {
event.preventDefault();
console.log('index = ' + index);
});
});
});
由于最里面的函数是在.each()
回调函数中定义的,因而以上代码实际上创建了同存在的按钮一样多的函数。这些函数分别作为一个单击处理程序被添加给了相应的按钮。而且,由于.each()
回调函数拥有参数index
,所以在这些函数的封闭环境中都有各自的index
变量。这就如同把单击处理程序的代码写成一个命名函数:
代码清单A-18
- $(document).ready(function() {
- $('input').each(function(index) {
- function clickHandler(event) {
- event.preventDefault();
- console.log('index = ' + index);
- }
- $(this).click(clickHandler);
- });
- });
只不过使用匿名函数的版本更短一些而已。然而,这个命名函数的位置也是很重要的。比如,以下代码会在按钮被单击时触发JavaScript错误:
代码清单A-19
- $(document).ready(function() {
- function clickHandler(event) {
- event.preventDefault();
- console.log('index = ' + index);
- }
- $('input').each(function(index) {
- $(this).click(clickHandler);
- });
- });
之所以会触发JavaScript错误,是因为在clickHandler()
的封闭环境中找不到index
。此时,index
仍然是一个自由变量,但它在这个环境中没有定义。