这里我想跟你聊聊我理解的回调函数(callback)。
回调函数,我觉得可以理解成作为参数传递的函数对象。因为在 JavaScript 中函数有比较高的等级(是一种基本类型,嗯,应该可以这样讲吧),可以独立地进行使用。
而在 Java 中,可能只有作为类的方法的“函数”存在吧,不存在可以单独使用的函数。当然在 Java 中,我“揣测”函数,或者叫方法(method)更合适,可能只有被调用的使用方式,例如: foo.method()
。
在 JavaScript 中,函数还是挺独立的。例如,尽管有的时候一个函数在声明的时候就是作为某个对象的方法的形式,但仍然可以撇开这个对象来使用。这里用到了函数的 .call()
或 .apply()
方法(这里就叫方法吧,不再绕来绕去了)。例如:
var Person = function (name) {
this.name = name;
this.getName = function () {
return this.name;
};
};
我先用自己的话跟你大致说下上面的代码里面做了什么事情,有些东西是我之前的文章还没有讲到的,不过相信你可能也都懂的:
首先,我声明了一个变量和一个函数,函数本身没有名称,将其作为值赋给了变量
Person
,这是最主要的结构;然后,再来看这个匿名函数的内部,我给
this
对象(且这么叫吧)设置了两个属性,属性name
的值为匿名函数的参数name
,属性getName
的值为一个匿名函数;最后,我们看下
getName
对应的匿名函数干了什么:它返回了this
的属性name
的值。
对于 Person
的使用方式,之前提到过(不过定义的形式可能稍有不同,这不是重点),可以这样用:
var me = new Person("luobo");
me.getName(); // "luobo"
这里说了挺多题外的东西,现在我们来看上面 me
这个对象的方法 getName()
。尽管形式上是对象的方法,而且在一开始定义的时候就确定了其方法的目的(这种 this.foo = ...
的方式我会在讲继承的时候再跟你多聊一些),但是由于其函数的本质,仍然可以单独来使用:
me.getName.call({ name: "Mickey" }); // "Mickey"
注意,这里 me.getName
只是为了引用到实际的函数(对象),以 .call()
来调用时传入了新的“上下文对象” { name: "Mickey" }
,而这个上下文对象在函数执行时会替换函数内部使用的 this
,有点了解了吗?
所以,实际上对于上文中声明的 Person 所对应的匿名函数,也可以这样来使用:
var me = {};
Person.call(me, "luobo");
me.getName(); // "luobo"
再引申一点来讲,使用 new Person()
这样的形式来获得一个对象,那么在 Person
内部的 this
就是对应这个即将返回的对象 。(严格来说不完全是,在 Person
对应的匿名函数有明确定义的其他返回值时就不再返回这个 this
对象啦)
虽然是要讲回调函数,可是我花了好多时间来讲每个函数都可以被独立调用这件事,还说到函数中的 this
在函数执行时可以明确指定(通过 .call()
传入的第一个参数即作为函数内部 this
所指向的对象),这些都是会用到的。
下面我举个使用回调函数的栗子:
$("#myDiv").on("click", function () {
this.innerHTML = "you clicked me!";
});
这个栗子中,我们使用 jQuery 给一个 id 为 myDiv 的元素绑定了 click 事件的处理程序,这里的事件处理程序就是一个回调函数。当然回调函数并非是一种特殊类型的对象,其实就是普通的函数,但是被作为其他函数的参数传递,在某个时刻才会被选择性地使用。
单纯看回调函数不是特别有趣,不过你应该注意到上面这个回调函数的定义中使用 this
,那么这个 this
又是指向什么对象呢?
实际上,这是由 jQuery 提供的机制,在事件处理程序被调用时,this
会指向事件的目标对象,这里也就是被点击的 myDiv 元素,而 HTML 元素有 innerHTML 属性,就是上面的使用方式了。这是怎么实现的呢,合理揣测下的话,肯定是这个回调函数在调用是被显式指定了“上下文对象” this
,有可能就是通过 .call()
或 .apply()
的方式。
另外,由于回调函数会在什么时候被执行,可能是不确定,甚至也可能永远不会被执行。而且在上面的这种情况下,这个回调函数甚至要在未来的某个时候才会执行,这就有了一个“异步”的感觉,也就是说代码不是从上到下依次执行,前面的代码执行完毕后面的才会执行(这个可以叫做“同步”啦)。
在使用 Ajax 时,我们可以指定请求是同步还是异步的方式,同步的方式下比较好理解,一定是这个请求接收到服务器响应或者超时失败后,后面的代码才会执行。这种情况下我们编写需要在一个 Ajax 请求后才能做的时候,例如 alert("Ajax 请求已完成!")
,是比较容易的。不过坏处是代码在执行到这里的时候会等待 Ajax 请求被响应,一切都停了下来。如果不想让世界暂停下来,就可以使用 Ajax 异步的模式(这也是通常的使用方式),然后把想要做的事情以回调函数的形式包装一下,等待请求在未来有个结果(响应、超时、意外终止等)后执行。举个栗子:
$.ajax("/foo.jsp").done(function () {
alert("成功!");
}).fail(function () {
alert("失败!");
});
这里分别为请求成功和失败两种情况指定了回调函数。
关于回调函数,咱们就聊这么多吧。
扩展:
- 关于“异步”执行,有没有考虑过专门去使用它的情况?
例如,代码执行时间预期会很长,为避免浏览器不响应用户交互,人为地将代码分为多个部分分别执行,从而使得每个部分执行时间不会很长。 - 主动地让代码“异步”执行的方法?
常见的有setTimeout
,另外还有借助浏览器的事件机制实现的,如为新创建的 img 元素指定 onload 事件的方式等等。 - 异步执行的代码多层嵌套的处理
因为要以回调函数的方式来编写异步执行代码,可能会很多级匿名函数互相嵌套的情况,代码可读性会比较差。这种情况下一个选择是可以使用一些第三方的库,如 when.js。其实 jQuery 也提供了异步机制,可以看下 deffered,我们常用的 jQuery 的 ajax 函数也基于这个东东改造过。
接下来还想跟你聊聊:
- JavaScript - 作用域和“闭包”
- JavaScript - 继承和“类”