回调与作用域

在前面的例子中,执行回调的函数一般都如下所示:

1
    a(b);

而回调一般都如下所示:

1
    b(parameters);

这在一般情况下都可以正常的工作,但在某些时候,回调并不仅仅是单一的函数,而是某个对象的方法,并且如果该回调使用 this 来引用其所属的对象的话,就会导致错误发生。

假如有一个 add 的回调函数,它是 app 对象的一个方法:

1
2
3
4
5
	var app = {};  
	app.number = 1;  
	app.add = function (n) {  
		n = n + this.number;  
	};

makeArray 函数做了类似下面的事:

1
2
3
4
5
6
7
var makeArray = function (callback) {  
	// ...  
	if (typeof callback === "function") {  
		callback(i);  
	}  
	// ...  
};

调用

1
makeArray(app.add);

当调用的时候,它并不能按照预期的那样工作,因为 this.number 将会是 undefined。这里 this 将会指向全局对象,因为 makeArray 是一个全局函数。

如果 makeArray 是某个对象的方法,那么在回调函数中的 this 将会指向这个对象而不是期望的 app。(详情请了解下 javascript 中的 this)

解决这个问题的方法就是传递一个回调函数,此外再传递这个回调函数属于的对象作为一个参数:

1
makeArray(app.add, app);

紧跟着,我们需要去修改 makeArray 去绑定(bind)传递进来的对象:

1
2
3
4
5
6
7
var makeArray = function (callback, callback_obj) {  
	//...  
	if (typeof callback === "function") {  
		callback.call(callback_obj, i);  
	}  
	// ...  
};

对于传递一个对象和一个被用来回调的方法,另一个可选的方法就是将方法作为字符串传递,那么你就不会重复对象两次。换言之:

1
makeArray(app.add, app);

会变成:

1
makeArray("add", app);

那么 makeArray 将会变为如下面几行所示:

1
2
3
4
5
6
7
8
9
10
var makeArray = function (callback, callback_obj) {  
	if (typeof callback === "string") {  
		callback = callback_obj[callback];  
	}  
	//...  
	if (typeof callback === "function") {  
		callback.call(callback_obj, i);  
	}  
	// ...  
};

匿名的事件监听器

回调模式在日常中被经常使用,比如,当你附加一个事件监听器给页面上的某个元素时,你实际上提供了一个指向了回调函数的引用,并且在事件发生时被调用。

这里有个例子,怎么将 console.log() 作为一个回调函数监听文档的 click 事件:

1
	document.addEventListener("click", console.log, false);

绝大部分客户端浏览器都是事件驱动的(event-driven),当一个页面加载完成,会触发load事件,然后用户可以通过和页面交互触发各种各样的事件,比如:click, keypress, mouseover, mousemove等等,因为回调模式,JavaScript特别适合事件驱动编程,能让你的程序异步的工作,换言之,就是不受顺序限制。

在异步的事件驱动的JavaScript,可以提供一个回调函数用于在正确的时候被调用;有时候甚至可能提供比实际请求还要多的回调函数,因为某些事件可能不会发生,比如:如果用户不点击“购买”按钮,那么你用于验证表单格式的函数永远不会被调用。

定时

另一个使用回调模式的例子就是使用浏览器的window对象的setTimeout()和setInterval()方法,这些方法也可以接受和执行回调函数:

1
2
3
4
	var thePlotThickens = function () {  
		console.log('500ms later...');  
	};  
	setTimeout(thePlotThickens, 500);

再次注意一下,thePlotThickens 是如何被作为一个参数传递的,注意这里仍旧没有使用括号,因为你不想它立即执行,传递字符串 "thePlotThickens()" 取代函数的引用和eval()类似,是不好的模式。

当然可以提供匿名函数的方式进行调用。

1
	setTimeout(function(){thePlotThickens()}, 500);

回调

在 javascript 中,函数都是对象,这也就表明了他们也可以被作为参数,从而传递给其他函数。例如,将函数 a() 作为参数传递给 b() 时,那么,在某一时刻 b() 可能会执行或者说调用 a(); 在这种情况下,a() 就被称作毁掉函数(callback function),也可简称为回调(callback)。

代码示例:

1
2
3
4
5
6
7
8
9
10
    function b(callback) {
        // do something
        callback();
    }
 
    function a() {
        // do something
    }
 
    b(a);

这里需要注意的是,在 b(a) 中,作为参数的 a 函数,是不带括号的,括号表示要执行(此函数),而此处只需要将 a 函数传递给 b 函数,让 b 函数在适当的时候,加上括号调用它即可。

注:可以使用 火狐,或者谷歌的 javascript 控制台,如果调用 console.log(a) 的话,就会得到 a 的函数的定义代码,而前面说了括号表示要执行,所以加上括号之后,表示要执行 a 函数的代码,a 其实只是一个名字而已。

代码改写示例

首先不使用回调的方式来定义两个函数,然后使用回调的方式重构这两个函数。

使函数具有通用的,单一功能的行为,这是非常好的思想,可以对函数的功能一目了然,阅读和修改也更方便。下面两个函数都是用这种思想来定义。

makeArray 函数用来生成一个大数组,并且返回这个大数组。

1
2
3
4
5
6
7
8
9
    var makeArray = function () {
        var i     = 100000,
            array = [];
        while (i) {
            i --;
            array.push(i);
        } 
        return array;
    }

arrayPlus 函数的功能只对数组的元素进行处理。

1
2
3
4
5
6
7
    var arrayPlus = function (array) {
        var i   = 0,
            max = array.length;
        for(; i < max; i++) {
            console.log(array[i] + 1);
        }
    }

执行这两个函数

1
    arrayPlus(makeArray());  // 生成数组,并给所有的数字元素 +1 再返回此数组

函数达到了我们的目的,但是对于客户端 javascript 来说,性能在一定程度上也是至关重要的,但是上面的实现是比较低效率的,因为生成数组的 makeArray 有一遍大循环,而处理数组元素的 arrayPlus 函数,做了同样的一遍大循环。如果将 makeArray 中的 array.push(i); 改为 array.push(i+1); 即可,这样是高效的,但是这样生成数组和处理数组高度耦合,其并不再是一个通用的具有单一功能的函数,不符合我们的思想。

那么我们尝试使用回调的方式来重构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    var makeArray = function (callback) {
        var i     = 100000,
            array = [];
        while (i) {
            i --;
            if(typeof callback === 'function'){
                callback(i);
            }
            array.push(i);
        } 
        return array;
    }
 
    var arrayPlus = function (n) {
        console.log(++n);
    }

重构之后的代码,makeArray 唯一额外的任务是要检查是否有提供回调函数,如果有则执行,如果没有则不执行,并没有破坏原来 makeArray 函数的功能,makeArray 仍然可以像以前一样使用。而 arrayPlus 则变得更加简单。

调用,执行即可

1
    makeArray(arrayPlus);  // 注意和重构前的调用对比

对于回调函数来说,可以是已经定义好的函数,也可以是匿名函数,在调用主函数时创建回调函数,例如:

1
2
3
    makeArray(function(n){
       console.log(++n); 
    });