理解与使用Javascript中的回调函数

理解与使用Javascript中的回调函数

有点小感慨

在这不得不提一下Python,在人工智能火起来之前就在断断续续的看教程。现在因为人工智能的原因,更要好好学习一下,,在学习的过程中了,第一次好像明白『函数式编程』到底是怎么一回事儿了。放张图,各位体会下,,👇
编程语言的抽象层次
因为最近工作的原因,先后遇到了几个关于js的问题,没想到需要阅读的Tab页越来越多,需要总结的页是越来越多。这个过程中,没想到朝夕相处的js中也有类似的特性,,比如闭包,之前特地去了解,可还是半知半解,,如今从Python反观js,一下子通透了~哈哈,下面进入正文


Javascript中,函数是第一类对象,这意味着函数可以像对象一样按照第一类管理被使用。既然函数实际上是对象:它们能被“存储”在变量中,能作为函数参数被传递,能在函数中被创建,能从函数中返回。

因为函数是第一类对象,我们可以在Javascript使用回调函数。在下面的文章中,我们将学到关于回调函数的方方面面。回调函数可能是在Javascript中使用最多的函数式编程技巧,虽然在字面上看起来它们一直一小段Javascript或者jQuery代码,但是对于许多开发者来说它任然是一个谜。在阅读本文之后你能了解怎样使用回调函数。

回调函数是从一个叫函数式编程的编程范式中衍生出来的概念。简单来说,函数式编程就是使用函数作为变量。函数式编程过去 - 甚至是现在,依旧没有被广泛使用 - 它过去常被看做是那些受过特许训练的,大师级别的程序员的秘传技巧。

幸运的是,函数是编程的技巧现在已经被充分阐明因此像我和你这样的普通人也能去轻松使用它。函数式编程中的一个主要技巧就是回调函数。在后面内容中你会发现实现回调函数其实就和普通函数传参一样简单。这个技巧是如此的简单以致于我常常感到很奇怪为什么它经常被包含在讲述Javascript高级技巧的章节中。

什么是回调或者高阶函数

一个回调函数,也被称为高阶函数,是一个被作为参数传递给另一个函数(在这里我们把另一个函数叫做“otherFunction”)的函数,回调函数在otherFunction中被调用。一个回调函数本质上是一种编程模式(为一个常见问题创建的解决方案),因此,使用回调函数也叫做回调模式。

  • 变量可以指向函数
  • 函数的参数可以接收变量
  • 一个函数可以接收另一个函数做参数
  • 能接收函数做参数的函数就是高阶函数

下面是一个在jQuery中使用回调函数简单普遍的例子:👇

1
2
3
4
5
//注意到click方法中是一个函数而不是一个变量
//它就是回调函数
$("#btn_1").click(function() {
alert("Btn 1 Clicked");
});

正如你在例子中看到的,我们将一个函数作为参数传递给了click方法。click方法会调用(或者执行)我们传递给它的函数。这是Javascript中回调函数的典型用法,它在jQuery中广泛被使用。

关于回调函数的运作方式,详见异步机制

除了在参数位置定义匿名函数作为回调函数之外,另一种常见的模式是定义一个命名函数并将函数名作为变量传递给函数。比如下面的例子:👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//全局变量
var allUserData = [];

//普通的logStuff函数,将内容打印到控制台
function logStuff (userData){
if ( typeof userData === "string"){
console.log(userData);
}else if ( typeof userData === "object"){
for(var item in userData){
console.log(item + ": " + userData[item]);
}
}
}

//一个接收两个参数的函数,后面一个是回调函数
function getInput (options, callback){
allUserData.push(options);
//确保callback是一个函数
if(typeof callback === "function"){
callback(options);
}
}

//当我们调用getInput函数时,我们将logStuff作为一个参数传递给它
//因此logStuff将会在getInput函数内被回调(或者执行)
getInput({name:"Rich",speciality:"Javascript"}, logStuff);
//name:Rich
//speciality:Javascript

使用this对象的方法作为回调函数时的问题

当回调函数是一个this对象的方法时,我们必须改变执行回调函数的方法来保证this对象的上下文。否则如果回调函数被传递给一个全局函数,this对象要么指向全局window对象(在浏览器中),要么指向包含方法的对象。 我们在下面的代码中说明:👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var clientData = {
id: 094545,
fullName: "Not Set",
//setUsrName是一个在clientData对象中的方法
setUserName: function (firstName, lastName){
//这指向了对象中的fullName属性
this.fullName = firstName + " " + lastName;
}
}

function getUserInput(firstName, lastName, callback){
//在这做些什么来确认firstName/lastName

//现在存储names
callback(firstName, lastName);
}

//当clientData.setUsername被执行时,this.fullName并没有设置clientData对象中的fullName属性。
//相反,它将设置window对象中的fullName属性,因为getUserInput是一个全局函数。这是因为全局函数中的this对象指向window对象。
getUserInput("Barack","Obama",clientData.setUserName);

console.log(clientData,fullName); //Not Set
//fullName属性将在window对象中被初始化
console.log(window.fullName); //Barack Obama

使用CallApply函数来保存this

我们知道每个Javascript中的函数都有两个方法:CallApply。这些方法被用来设置函数内部的this对象以及给此函数传递变量。

call接收的第一个参数为被用来在函数内部当做this的对象,传递给函数的参数被挨个传递(当然使用逗号分开)。Apply函数的第一个参数也是在函数内部作为this的对象,然而最后一个参数确是传递给函数的值的数组。

听起来很复杂,那么我们来看看使用ApplyCall有多么的简单。为了修复前面例子的问题,我将在👇下面你的例子中使用Apply函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//注意到我们增加了新的参数作为回调对象,叫做“callbackObj”
function getUserInput(firstName, lastName, callback, callbackObj){
//在这里做些什么来确认名字

//apply
callback.apply(callbackObj, [firstName, lastName]);
//等价call
//callback.call(callbackObj, firstName, lastName);
}

//我们将clientData.setUserName方法和clientData对象作为参数,clientData对象会被Apply方法使用来设置this对象
getUserInput("Barack", "Obama", clientData.setUserName, clientData);

//clientData中的fullName属性被正确的设置
console.log(clientUser.fullName); //Barack Obama

回调地狱问题以及解决方案

在执行异步代码时,无论以什么顺序简单的执行代码,经常情况会变成许多层级的回调函数堆积以致代码变成下面的情形。这些杂乱无章的代码叫做回调地狱因为回调太多而使看懂代码变得非常困难。我从node-mongodb-native,一个适用于Node.js的MongoDB驱动中拿来了一个例子。这段位于下方的代码将会充分说明回调地狱:👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var p_client = new Db('integration_tests_20', new Server("127.0.0.1", 27017, {}), {'pk':CustomPKFactory});
p_client.open(function(err, p_client) {
p_client.dropDatabase(function(err, done) {
p_client.createCollection('test_custom_key', function(err, collection) {
collection.insert({'a':1}, function(err, docs) {
collection.find({'_id':new ObjectID("aaaaaaaaaaaa")}, function(err, cursor) {
cursor.toArray(function(err, items) {
test.assertEquals(1, items.length);

// Let's close the db
p_client.close();
});
});
});
});
});
});

你应该不想在你的代码中遇到这样的问题,当你遇到了–你将会时不时的遇到这种情况–这里有关于这个问题的两种解决方案。

  1. 给你的函数命名并传递它们的名字作为回调函数,而不是主函数的参数中定义匿名函数。
  2. 模块化L将你的代码分隔到模块中,这样你就可以到处一块代码来完成特定的工作。然后你可以在你的巨型应用中导入模块。

Javascript回调函数非常美妙且功能强大,它为web应用和代码提供了诸多好处。我们应该在有需求时使用它,或者为了代码的抽象性、可维护性以及可读性而使用回调函数来重构你的代码。

本文译自understand Javascript callback functions and use them,有删改。


节选自:前端乱炖-http://www.html-js.com/article/1592 有删改