A.3 在 jQuery 中创建闭包

我们曾经介绍过的jQuery库中的许多方法都至少要接收一个函数作为参数。为方便起见,我们通常都在这种情况下使用匿名函数,以便在必需时再定义函数的行为。但是,这也意味着我们很少在顶级命名空间中定义函数;也就是说,这些函数都是内部函数,而内部函数很容易就会变成闭包。

A.3.1 $(document).ready()的参数

我们使用jQuery编写的几乎全部代码都要放在作为$(document).ready()参数的一个函数内部。这样做是为了保证在代码运行之前DOM已经就绪,而DOM就绪通常是运行jQuery代码的一个必要条件。当创建了一个函数并把它传递给.ready()之后,这个函数的引用就会被保存为全局jQuery对象的一部分。在稍后的某个时间——当DOM就绪时,这个引用就会被调用。

由于我们通常把$(document).ready()放在代码结构的顶层,因而这个函数不会成为闭包。但是,我们的代码通常都是在这个函数内部编写的,所以这些代码都处于一个内部函数中:

代码清单A-10

  1. $(document).ready(function() {
  2. var readyVar = 0;
  3. function innerFn() {
  4. readyVar++;
  5. console.log('readyVar = ' + readyVar);
  6. }
  7. innerFn();
  8. innerFn();
  9. });

这看上去同前面的很多例子都差不多,只不过外部函数是传入到$(document).ready()中的一个回调函数。由于innerFn()定义在这个回调函数中,而且引用了位于回调函数作用域中的readyVar,因此innerFn()及其环境就创建了一个闭包。我们两次调用这个内部函数,通过观察两次输出之间保持的readyVar的值,就可以证明这一点:

  1. readyVar = 1
  2. readyVar = 2

把大多数jQuery代码都放在一个函数体中是很有用的,因为这样可以避免某些命名空间冲突。例如,正是这个特性可以使我们通过调用jQuery.noConflict()为其他库释放简写方式$,但我们仍然能够定义在$(document).ready()中使用的局部简写方式。

A.3.2 绑定事件处理程序

.ready()结构通常用于包装其他的jQuery代码,包括事件处理程序的赋值。因为处理程序是函数,它们也就变成了内部函数;而且,因为这些内部函数会被保存并在以后调用,于是它们也会创建闭包。以一个简单的单击处理程序为例:

代码清单A-11

  1. $(document).ready(function() {
  2. var counter = 0;
  3. $('#button-1').click(function(event) {
  4. event.preventDefault();
  5. counter++;
  6. console.log('counter = ' + counter);
  7. });
  8. });

由于变量counter是在.ready()处理程序中声明的,所以它只对位于这个块中的jQuery代码有效,对.ready()处理程序外部的代码无效。然而,这个变量可以被.click()处理程序中的代码引用,在这个例子中.click()应用程序会递增并显示该变量的值。由于创建了闭包,每次单击按钮都会引用counter的同一个实例。也就是说,消息会持续显示一组递增的值,而不是每次都显示1。

  1. counter = 1
  2. counter = 2
  3. counter = 3

事件处理程序同其他函数一样,也能够共享它们的封闭环境:

代码清单A-12

  1. $(document).ready(function() {
  2. var counter = 0;
  3. $('#button-1').click(function(event) {
  4. event.preventDefault();
  5. counter++;
  6. console.log('counter = ' + counter);
  7. });
  8. $('#button-2').click(function(event) {
  9. event.preventDefault();
  10. counter--;
  11. console.log('counter = ' + counter);
  12. });
  13. });

因为这两个函数引用的是同一个变量counter,所以两个链接的递增和递减操作会影响同一个值,而不是各自独立的值。

  1. counter = 1
  2. counter = 2
  3. counter = 1
  4. counter = 0

A.3.3 在循环中绑定处理程序

鉴于闭包的独特运行方式,在循环中绑定处理程序需要一些特殊的技巧。假设我们想在一个循环中创建多个元素,然后基于循环的索引为这些元素绑定行为:

代码清单A-13

  1. $(document).ready(function() {
  2. for (var i = 0; i < 5; i++) {
  3. $('<div>Print ' + i + '</div>')
  4. .click(function() {
  5. console.log(i);
  6. }).insertBefore('#results');
  7. }
  8. });

变量i依次被设置为0~4,而每次循环都会创建一个新的<div>元素。每个新元素都有一个不同的文本标签:

  1. Print 0
  2. Print 1
  3. Print 2
  4. Print 3
  5. Print 4

你可能会认为单击其中一项会看到相应的编号出现在控制台中。可是,单击页面中的任何一个元素都会显示数值5。换句话说,即使在绑定处理程序时i的值每次都不一样,每个click处理程序最终引用的i都相同,都等于单击事件实际发生时i的最终值(5)。

解决这个问题的方式有很多。首先,可以使用jQuery的$.each()函数来代替for循环:

代码清单A-14

  1. $(document).ready(function() {
  2. $.each([0, 1, 2, 3, 4], function(index, value) {
  3. $('<div>Print ' + value + '</div>')
  4. .click(function() {
  5. console.log(value);
  6. }).insertBefore('#results');
  7. });
  8. });

因为函数的参数类似于在函数中定义的变量,所以每次循环的value实际上都是不同的变量。结果,每个click处理程序都指向一个不同的value变量,因而每次单击输出的值会与元素的标签文本匹配。

同样利用函数参数的这个特性,不必使用$.each()也可以解决这个问题。在for循环内部,可以定义并执行一个新函数,让它负责把变量i的值分配到不同的变量中去:

代码清单A-15

  1. $(document).ready(function() {
  2. for (var i = 0; i < 5; i++) {
  3. (function(value) {
  4. $('<div>Print ' + value + '</div>')
  5. .click(function() {
  6. console.log(value);
  7. }).insertBefore('#results');
  8. })(i);
  9. }
  10. });

这种结构我们在第8章看到过,它的名字叫立即调用的函数表达式(IIFE),前面曾在调用$.noConflict()之后利用它为jQuery对象重新定义别名$。在这里,我们利用它将i传给变量value,以便value在每个单击处理程序中都有不同的值。

最后,还可以使用jQuery的事件系统换个角度来解决这个问题。我们知道,.on()方法接受一个对象参数,该参数以event.data的形式传入事件处理程序中:

代码清单A-16

  1. $(document).ready(function() {
  2. for (var i = 0; i < 5; i++) {
  3. $('<div>Print ' + i + '</div>')
  4. .on('click', {value: i}, function(event) {
  5. console.log(event.data.value);
  6. }).insertBefore('#results');
  7. }
  8. });

在这里,我们将 i 作为 .on() 方法的数据,而在事件处理程序内部,能够通过event.data.value取得它的值。同样,因为event是函数的参数,每次调用处理程序时它都是一个独立的实例,而不是在所有调用中共享的一个值。

A.3.4 命名及匿名函数

这些例子都和我们常规的jQuery代码一样使用了匿名函数。但是,这不会影响到闭包的创建。换句话说,无论命名函数还是匿名函数,都可以用来创建闭包。例如,我们可以编写一个匿名函数,报告jQuery对象中每个<input>按钮的索引:

代码清单A-17

  1. $(document).ready(function() {
  2. $('input').each(function(index) {
  3. $(this).click(function(event) {
  4. event.preventDefault();
  5. console.log('index = ' + index);
  6. });
  7. });
  8. });

由于最里面的函数是在.each()回调函数中定义的,因而以上代码实际上创建了同存在的按钮一样多的函数。这些函数分别作为一个单击处理程序被添加给了相应的按钮。而且,由于.each()回调函数拥有参数index,所以在这些函数的封闭环境中都有各自的index变量。这就如同把单击处理程序的代码写成一个命名函数:

代码清单A-18

  1. $(document).ready(function() {
  2. $('input').each(function(index) {
  3. function clickHandler(event) {
  4. event.preventDefault();
  5. console.log('index = ' + index);
  6. }
  7.  
  8. $(this).click(clickHandler);
  9. });
  10. });

只不过使用匿名函数的版本更短一些而已。然而,这个命名函数的位置也是很重要的。比如,以下代码会在按钮被单击时触发JavaScript错误:

代码清单A-19

  1. $(document).ready(function() {
  2. function clickHandler(event) {
  3. event.preventDefault();
  4. console.log('index = ' + index);
  5. }
  6. $('input').each(function(index) {
  7. $(this).click(clickHandler);
  8. });
  9. });

之所以会触发JavaScript错误,是因为在clickHandler()的封闭环境中找不到index。此时,index仍然是一个自由变量,但它在这个环境中没有定义。