[TOC]

105.JavaScript回调函数

1.JavaScript函数

1.1 函数也是对象

想弄明白回调函数,首先的清楚地明白函数的规则。
在javascript中,函数是比较奇怪的,但它确确实实是对象。
确切地说,函数是用Function()构造函数创建的Function对象。Function对象包含一个字符串,字符串包含函数的javascript代码
假如你是从C语言或者java语言转过来的,这也许看起来很奇怪,代码怎么可能是字符串?但是对于javascript来说,这很平常。数据和代码之间的区别是很模糊的。
//可以这样创建函数
var fn = new Function(“arg1”, “arg2”, “return arg1 * arg2;”);
fn(2, 3); //6
  这样做的一个好处,可以传递代码给其他函数,也可以传递正则变量或者对象(因为代码字面上只是对象而已)。

1.2 JavaScript中函数定义的三种方式

105.JavaScript回调函数 - 图1
<!DOCTYPE html>













可以有返回值,可以用变量来接受其返回值;如果没有return,则返回undefined.
其中用“function语句”和使用“函数直接量”来定义函数的方法似乎比较常见,也比较好理解,在此不多说。
针对使用Function()构造函数克隆函数,一般很少用,因为一个函数通常有多条语句组成,如果将他们以字符串的形式作为参数传递,难免会使得代码的可读性很差**
在这里再顺便提一下构造函数吧,其实从字面上理解,
构造函数似乎也是函数,其实它并不是函数,而只是一种函数模型。
举个不恰当的例子,
构造函数相当于一部刚组装好的车子,无论远看还是近看,它都是一部车子,但是还没有加油(代表在使用前的一个必要步骤),所以它并不能启动。**如果想要这部车子正常行驶,就必须给它加上油,其实这个过程就等同于构造函数的实例化,否则它并不能正常运行!
看下面这个例子:
function Fn(){ //定义构造函数  
  this.elem =”这里是使用function()构造函数定义函数,呵呵”;  
  this.fn = function(){    
    alert(“这是使用function()构造函数定义函数,嘿嘿”);  
  }
}
var f = new Fn(); //实例化
alert(f.elem);
f.fn();

1.3 函数调用

105.JavaScript回调函数 - 图2
105.JavaScript回调函数 - 图3
105.JavaScript回调函数 - 图4

1.4 JavaScript函数-带返回值

1)不带返回值函数

105.JavaScript回调函数 - 图5

2)带有返回值的函数

有时,我们会希望函数将值返回调用它的地方。
通过使用 return 语句就可以实现。
在使用 return 语句时,函数会停止执行,并返回指定的值。
105.JavaScript回调函数 - 图6
在您仅仅希望退出函数时 ,也可使用 return 语句。返回值是可选的:
105.JavaScript回调函数 - 图7

2.JavaScript回调函数

2.1 什么是回调

JavaScript 是单线程工作,这意味着两段脚本不能同时运行,而是必须一个接一个地运行。
JavaScript由于单线程限制,防止阻塞,只能通过异步函数的调用方式,把需要延迟处理的事件放入事件循环队列。
回调是编写和处理JavaScript程序异步逻辑的最常用方式。
简单的定义:回调就是一个在另外一个函数执行完后要执行的函数
复杂的定义:在JavaScript中,函数是对象。因此函数可以将函数作为参数,并且可以由其他函数进行返回。执行此操作的函数称为高阶函数任何作为参数传递的函数都称为回调函数。

2.2 回调函数

1)定义

(1)定义

回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)代码。
回调函数可以简单理解为:(执行完)回(来)调(用)的函数。
也就是说,回调函数不仅可以用于异步调用,一般同步的场景也可以用回调。在同步调用下,回调函数一般是最后执行的。
而异步调用下,可能一段时间后执行或不执行(未达到执行的条件)。
在JavaScript中函数也是对象的一种,同样对象可以作为参数传递给函数,因此函数也可以作为参数传递给另外一个函数,这个作为参数的函数就是回调函数。
维基百科是这么解释回调函数的:回调函数就是一个通过函数指针调用的函数。
如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

(2)通俗理解

回调是一个函数被作为一个参数传递到另一个函数里,在那个函数执行完后再执行。
举例:B函数被作为参数传递到A函数里,在A函数执行完后再执行B。

2)分类

(1)同步回调

/**同步回调**/
var fun1 = function (callback) {
//do something
console.log(“before callback”);
(callback && typeof (callback) === ‘function’) && callback();
console.log(“after callback”);
}
var fun2 = function (param) {
//do something
var start = new Date();
while ((new Date() - start) < 3000) { //delay 3s
}
console.log(“I’m callback”);
}
fun1(fun2);
// ———- output————
// before callback
// //after 3s
// I’m callback
// after callback
由于是同步回调,会阻塞后面的代码,如果fun2是个死循环,后面的代码就不执行了。
function myCallBack(fn) {
const str = ‘I love China’;
fn(str);
}
myCallBack((data) => {
console.log(data);
});
console.log(‘我后执行’);
});

(2)异步回调(回调函数)

function myCallBack(fn) {
const str = ‘I love China’;
setTimeout(() => {
fn(str)
}, 1000);
}
myCallBack((data) => {
console.log(data);
});
console.log(‘我先执行’);
setTimeout就是常见的异步回调,另外常见的异步回调即ajax请求。
/**异步回调**/
function request(url, param, successFun, errorFun) {
$.ajax({
type: ‘GET’,
url: url,
param: param,
async: true, //默认为true,即异步请求;false为同步请求
success: successFun,
error: errorFun
});
}
request(‘test.html’, ‘’, function (data) {
//请求成功后的回调函数,通常是对请求回来的数据进行处理
console.log(‘请求成功啦, 这是返回的数据:’, data);
}, function (error) {
console.log(‘sorry, 请求失败了, 这是失败信息:’, error);
让其拥有了“多线程”的能力,其实并不其然,异步回调是怎么解决并发问题,阻塞问题,不知道大家思考过背后的运行机制吗?

3)回调函数特点?

(1)不会立刻执行

回调函数作为参数传递给一个函数的时候,传递的只是函数的定义并不会立即执行。和普通的函数一样,回调函数在调用函数中也要通过()运算符调用才会执行。

(2)是个闭包

回调函数是一个闭包,也就是说它能访问到其外层定义的变量。

(3)执行前类型判断

在执行回调函数前最好确认其是一个函数。
function add(num1, num2, callback){
var sum = num1 + num2;
if(typeof callback === ‘function’){
callback(sum);
}
}
this的使用 注意在回调函数调用时this的执行上下文并不是回调函数定义时的那个上下文,而是调用它的函数所在的上下文。
var obj = {
sum: 0,
add: function(num1, num2){
this.sum = num1 + num2;
}
};
function add(num1, num2, callback){
callback(num1, num2);
};
add(1,2, obj.add);
console.log(obj.sum); //=>0
console.log(window.sum); //=>3
上述代码调用回调函数的时候是在全局环境下,因此this指向的是window,所以sum的值是赋值给windows的。
关于this执行上下文的问题可以通过apply方法解决。
var obj = {
sum: 0,
add: function(num1, num2){
this.sum = num1 + num2;
}
};
function add(num1, num2, callbackObj, callback){
callback.apply(callbackObj, [ num1, num2 ]);
};
add(1,2, obj, obj.add);
console.log(obj.sum); //=>3
console.log(window.sum); //=>undefined

(4)允许传递多个回调函数

一个函数中可以传递多个回调函数,典型的例子如jQuery
function successCallback() {
// Do stuff before send
}
function successCallback() {
// Do stuff if success message received
}
function completeCallback() {
// Do stuff upon completion
}
function errorCallback() {
// Do stuff if error received
}
$.ajax({
url: “http://fiddle.jshell.net/favicon.png“,
success: successCallback,
complete: completeCallback,
error: errorCallback
});

(5)回调函数嵌套

一个回调函数中可以嵌入另一个回调函数,对于这种情况出现多层嵌套时,代码会难以阅读和维护,这个时候可以采用命名回调函数的方式调用,或者采用模块化管理函数,也可以用promise模式编程。

4)任务队列

(1)JavaScript问题

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

(2)任务分类
对比 同步任务(synchronous) 异步任务(asynchronous)
在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。 不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

(3)JavaScript运行机制

只要主线程空了,就会去读取”任务队列”,这就是JavaScript的运行机制

(4)异步执行的运行机制

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
(3)一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

5)JavaScript如何进行异步回调的

了解完异步回调的概念后,我们来看看JavaScript是如何运行的?首先我们一起来看看下面的图:
105.JavaScript回调函数 - 图8

(1)Event Loop(事件循环)

1.JavaScript运行内容

实际上在js运行中,起码有三个东西:
task队列
Microtask队列

主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈(stack)中的代码调用各种外部API,它们在”任务队列”中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取”任务队列”,依次执行那些事件所对应的回调函数。
“任务队列”是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,”任务队列”上第一位的事件就自动进入主线程。但是,由于存在后文提到的”定时器”功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
105.JavaScript回调函数 - 图9
在理解Event Loop时,要理解两句话:

  • 理解哪些语句会放入异步任务队列
  • 理解语句放入异步任务队列的时机
    2.图解释
    文字介绍是不是特枯燥,让我们看看下组的图,将JavaScript的运行机制可视化,是否更容易理解呢?
    105.JavaScript回调函数 - 图10
    105.JavaScript回调函数 - 图11
    105.JavaScript回调函数 - 图12
    105.JavaScript回调函数 - 图13
    105.JavaScript回调函数 - 图14
    105.JavaScript回调函数 - 图15
    105.JavaScript回调函数 - 图16
    105.JavaScript回调函数 - 图17
    (2)从Promise来看JavaScript中的Event Loop、Tasks和Microtasks
    1.面试题
    前几天面试的时候,碰到了这样一个题:
    说出下列代码的执行结果:
    setTimeout(function () {
    console.log(1)
    },0);
    new Promise(function executor(resolve) {
    resolve();
    }).then(function () {
    console.log(2);
    }); //2,1
    (function test() {
    setTimeout(function () { console.log(4) }, 0);
    new Promise(function executor(resolve) {
    console.log(1);
    for (var i = 0; i < 10000; i++) {
    i == 9999 && resolve();
    }
    console.log(2);
    }).then(function () {
    console.log(5);
    });
    console.log(3);
    })() //1,2,3,5,4
    为了弄清楚运行结果的原因,我们要从JS的Event Loop说起,参考了网上的一些文档,按照我自己的理解整理如下:
    2.promise是怎么做到异步
    | 对比 | task | promise | | —- | —- | —- | | 区别 | | promise并不是task,它属于Microtask,可以叫它微任务~ 跟在task末尾执行,像个小跟班。 | | 所属队列 | setTimeout属于task队列。 | promise属于Microtask队列,这俩的异步队列不一样, | | 队里优先级 | | Promise所在的那个异步队列优先级要高一些。 |

2.3 创建一个简单的回调

1)在函数调用中定义回调函数

function _doHomework(subject) {
alert(Starting my ${subject} homework.);
}
上面我们创建了doHomeWork的函数,我们接受一个变量,通过控制台调用,将得到下面的提示:
doHomework(‘math’);
// Alerts: Starting my math homework.
接着,我们开始添加回调,在doHomework函数中添加一个参数callback,然后在第二个参数中回调我们定义的函数。代码如下:
_function _doHomework(subject, callback) {
alert(Starting my ${subject} homework.);
callback();
}
doHomework(‘math’, _function
() {
alert(‘Finished my homework’);
});
正如你希望的,我们在控制台里运行上述代码,将会受到两个连续的alert,Starting my math homework,然后弹出 Finished my homework。

2)单独定义回调函数

但是回调函数并不是非得在调用函数中定义,我们可以单独定义,修改后的代码如下:
_function _doHomework(subject, callback) {
alert(Starting my ${subject} homework.);
callback();
}
_function _alertFinished(){
alert(‘Finished my homework’);
}
doHomework(‘math’, alertFinished);
此示例的输出结果和上段代码的结果一致,我们实现了在doHomework函数中调用alertFinished,实现了函数作为参数进行传递,实现了回调函数的创建。

3)传递函数作为回调

  很容易把一个函数作为参数传递。
function fn(arg1, arg2, callback){
var num = Math.ceil(Math.random() * (arg1 - arg2) + arg2);
callback(num);  //传递结果
}
fn(10, 20, function(num){
console.log(“Callback called! Num: “ + num);
});    //结果为10和20之间的随机数

4)回调函数使用案例

示例1:回调函数

function add(num1, num2, callback) {
var sum = num1 + num2;
callback(sum);
}
function print(num) {
console.log(num);
}
add(1, 2, print); //=>3

示例2:匿名回调函数

function add(num1, num2, callback) {
var sum = num1 + num2;
callback(sum);
}
add(1, 2, function (sum) {
console.log(sum); //=>3
});

示例3:jQuery中大量的使用了回调函数

$(“#btn”).click(function () {
alert(“button clicked”);
});

示例4:

  下面有个更加全面的使用AJAX加载XML文件的示例,并且使用了call()函数,在请求对象(requested object)上下文中调用回调函数
function fn(url, callback) {
var httpRequest; //创建XHR
httpRequest = window.XMLHttpRequest
? new XMLHttpRequest() //针对IE进行功能性检测
: window.ActiveXObject
? new ActiveXObject(“Microsoft.XMLHTTP”)
: undefined;
httpRequest.onreadystatechange = function () {
if (httpRequest.readystate === 4 && httpRequest.status === 200) {
//状态判断
callback.call(httpRequest.responseXML);
}
};
httpRequest.open(“GET”, url);
httpRequest.send();
}
fn(“text.xml”, function () {
//调用函数
console.log(this); //此语句后输出
});
console.log(“this will run before the above callback.”); //此语句先输出
  我们请求异步处理,意味着我们开始请求时,就告诉它们完成之时调用我们的函数。在实际情况中,onreadystatechange事件处理程序还得考虑请求失败的情况,这里我们是假设xml文件存在并且能被浏览器成功加载。这个例子中,异步函数分配给了onreadystatechange事件,因此不会立刻执行。
  最终,第二个console.log语句先执行,因为回调函数直到请求完成才执行。
  上述例子不太易于理解,那看看下面的示例:
function foo() {
var a = 10;
return function () {
a = 2;
return a;
};
}
var f = foo();
f(); //return 20.
f(); //return 40.
函数在外部调用,依然可以访问变量a。这都是因为javascript中的作用域是词法性的。*函数式运行在定义它们的作用域中(上述例子中的foo内部的作用域),而不是运行此函数的作用域中。
只要f被定义在foo中,它就可以访问foo中定义的所有的变量,即便是foo的执行已经结束。因为它的作用域会被保存下来,但也只有返回的那个函数才可以访问这个保存下来的作用域。返回一个内嵌匿名函数是创建闭包最常用的手段。

5)个人使用

(1)在定义处

105.JavaScript回调函数 - 图18

(2)在实际调用处写回调函数

105.JavaScript回调函数 - 图19

(3)JavaScript回调函数

1.写回调函数

105.JavaScript回调函数 - 图20

2.调用-回调函数

105.JavaScript回调函数 - 图21

2.4 使用场景

1)异步编程(解决阻塞问题)

由于JavaScript要解决这个问题,必须要突破单线程的瓶颈,“异步回调”就成为JavaScript的秘密武器,完美的解决了此问题。

2)事件监听、处理。

3)setTimeout、setInterval方法。

4)通用功能,简化逻辑。

2.5 优点和缺点-回调地狱

优点

1)DRY,避免重复代码。

2)可以将通用的逻辑抽象。

3)加强代码可维护性。

4)加强代码可读性。

5)分离专职的函数。

缺点:回调地狱

回调主要用于解决异步编程问题,是处理异步任务的一种解决方案,异步编程是我们的代码中使用的一种方法,用于推迟事件以便以后执行。
如果我们有多个任务依赖于前几个任务的结果,那我们就要使用多个嵌套回调,但是就会引发“回调地狱”(过多的回调嵌套会使得代码变得难以理解与维护),还好Promise解决了“回调地狱”的问题。