一、什么是回调
在谈什么是回调之前,我们先来看看什么是回调函数(callback)。
在JavaScript中,函数是对象。因此,函数可以将函数作为参数,并且可以由其他函数返回。执行此操作的函数称为高阶函数。任何作为参数传递的函数都称为回调函数。
说完了回调函数,我们来再看看回调(callbacks)。
如果你曾经查过什么是回调的话,你可能会发现关于回调的定义众说纷纭,这真的很让人为难。于是在查阅了很多文章之后,我对回调做出了如下解释:
以下内容仅代表我个人观点
回调是一种确保某些代码在其它代码已经完成执行之前不会执行的方法。
引用于:JavaScript: What the heck is a Callback
我承认,这段话确实很抽象,但我认为没有比这更贴切的描述了。
当然,若要简单理解的话,也可以认为回调就是一个回调函数的调用过程。
二、回调的运行方式
在讲回调的运行方式之前,我们先来给回调分以下类。
按照一些人的观点,回调可以分为同步回调和异步回调。
但在了解同步回调和异步回调之前,我觉得有必要先了解一下同步和异步。
在了解完同步与异步之后,我们先来看看同步回调:
function F1() {
console.log("F1 finished");
}
function F2(fun) {
fun();
console.log("F2 finished");
}
F2(F1);
//F1 finished
//F2 finished
这是一个典型的同步回调的例子。
我想这个应该很容易理解,那接下来将它改造成一个异步回调试试看:
function F1() {
console.log("F1 finished");
}
function F2(fun) {
setTimeout(fun,5000);
console.log("F2 finished");
}
F2(F1);
//F2 finished
...Five seconds later...
//F1 finished
在这个例子中,F1是在F2被调用5秒后被调用的,这貌似没什么毛病,但我们换个例子看看:
function F1() {
console.log("F1 finished");
}
function F2(fun) {
setTimeout(fun,5000);
console.log("F2 finished");
while(true) { }
}
F2(F1);
//F2 finished
你可能很疑惑,为什么F1()
没有被调用?
这是因为在这个例子中,我们调用了setTimeout(fun,5000)
,它将F1()
插入到了任务队列(task queue)中,而任务队列中的任务只有在主线程(stack)中的任务执行结束之后才会被执行,但因为while(true) { }
的存在,使得主线程中的任务永远都不会执行结束,这就导致了F1()
永远不会被执行。
现在将这个例子改为同步回调来看看会发生什么:
function F1() {
console.log("F1 finished");
}
function F2(fun) {
fun();
console.log("F2 finished");
while(true) { }
}
F2(F1);
//F1 finished
//F2 finished
不出意外,它输出了我们所期望的值。
这几个例子很好的向我们展示了回调的运行方式,同时也展示同步回调和异步回调的区别:
同步回调会将回调函数置于主线程之中,而异步回调会将回调函数置于任务队列之中。
三、回调函数的作用域
在谈这个问题之前,我们先看一下这个实例:
function count() {
for(var i = 3; i > 0; i --) {
setTimeout(function() {
console.log("The i is now " + String(i));
}, 1000);
}
}
count();
//The i is now 0
//The i is now 0
//The i is now 0
对于这样的结果,我想有些人可能会感觉到疑惑,为什么没有输出预想中的:
//The i is now 3
//The i is now 2
//The i is now 1
这是因为setTimeout()
将console.log("The i is now " + String(i))
添加到了任务队列中,而任务队列中的任务是在主线程中的任务执行结束之后才开始执行的,但因为count()
在主线程中,所以当console.log("The i is now " + String(i))
开始执行的时候,i
的值已经等于0
了,所以才会产生那样的结果。
不过这并非没有解决办法,在ES6之前,我们通常会采用闭包的方式来解决这个问题:
function count() {
for(var i = 3; i > 0; i --) {
(function(i) {
setTimeout(function() {
console.log("The i is now " + String(i));
}, 1000);
})(i);
}
}
count();
//The i is now 3
//The i is now 2
//The i is now 1
不过ES6中提出了一个更好的方案:
function count() {
for(let i = 3; i > 0; i --) {
setTimeout(function() {
console.log("The i is now " + String(i));
}, 1000);
}
}
count();
//The i is now 3
//The i is now 2
//The i is now 1
很显然第二章方法要比第一种更简单,而且现在绝大多数浏览器都已经基本支持ES6了,因此在没有特殊要求的情况下,我们应当使用let
而不是var
。