本文深入解析JavaScript中回调函数的核心机制,阐述其作为第一类对象和闭包的原理。通过jQuery与原生JS示例,讲解匿名与命名回调的使用、this指向问题及Call/Apply修复方案,并探讨如何处理“回调地狱”与多重回调,助力写出更简洁、可维护的代码。
概述
在JavaScript中,函数被视为第一类对象(First-class objects),这意味着函数可以像普通对象一样进行管理:它们可以被赋值给变量,可以作为参数传递给其他函数,可以在函数内部创建,也可以作为返回值从函数中返回。
正是基于函数作为第一类对象的特性,JavaScript才得以支持回调函数。本文将全面解析回调函数的使用细节。尽管回调函数在字面上看起来只是一小段JavaScript或jQuery代码,且是函数式编程中最常用的技巧之一,但对于许多开发者而言,其内部机制仍是一个谜。阅读完本文后,你将掌握如何正确使用回调函数。
回调函数这一概念源于函数式编程范式。简而言之,函数式编程的核心在于将函数视为变量进行传递和处理。过去,函数式编程甚至至今仍未被广泛普及,常被视为少数资深程序员掌握的“秘传”技巧。
幸运的是,随着函数式编程理念的普及,如今包括初学者在内的普通开发者也能轻松掌握这一技巧。其中,回调函数是函数式编程中最核心的技巧之一。在后文你会看到,实现回调函数实际上与普通函数传参一样简单。由于其实现如此简单,我常感到疑惑:为何它常被归类为JavaScript的高级技巧章节?
什么是回调或者高阶函数
回调函数,有时也被称为高阶函数(Higher-order function),是指被作为参数传递给另一个函数(此处称为otherFunction)的函数,并在otherFunction内部被调用。本质上,回调函数是一种解决常见问题的编程模式,因此使用回调函数也被称为应用回调模式。
下面是一个在jQuery中使用回调函数的常见示例:
//注意到click方法中是一个函数而不是一个变量
//它就是回调函数
$("#btn_1").click(function() {
alert("Btn 1 Clicked");
});
如前例所示,我们将一个函数作为参数传递给了click方法。click方法随后会调用(或执行)我们传递进去的那个函数。这是JavaScript中回调函数的典型用法,在jQuery库中被广泛使用。
以下是另一个JavaScript中典型的回调函数示例:
var friends = ["Mike", "Stacy", "Andy", "Rick"];
friends.forEach(function (eachName, index){
console.log(index + 1 + ". " + eachName); // 1. Mike, 2. Stacy, 3. Andy, 4. Rick
});
同样地,注意我们将一个匿名函数(即没有名字的函数)作为参数传递给了forEach方法。
到目前为止,我们一直将匿名函数作为参数传递给另一个函数或方法。在查看更多实际例子和编写自定义回调函数之前,我们先来深入理解回调函数的工作原理。
回调函数是怎样运作的?
由于函数在JavaScript中是第一类对象,我们可以像处理对象一样处理函数:像传递变量一样传递函数,在函数中返回函数,或在其他函数内部使用函数。当我们将回调函数作为参数传递给另一个函数时,我们仅仅传递了函数的定义(引用),而并未在参数中立即执行该函数。我们并没有像平时调用函数那样加上执行小括号()。
需要特别注意的一点是,回调函数并不会立即执行。它会在包含它的函数内部的某个特定时间点被“回调”(正如它的名称所示)。因此,即使前面的jQuery示例如下所示:
//匿名函数不会再参数中被执行
//这是一个回调函数
$("#btn_1").click(function(){
alert("Btn 1 Clicked");
});
这个匿名函数稍后会在函数体内被调用。即使函数有名字,它依然通过arguments对象在包含它的函数内部被获取和执行。
回调函数是闭包
当我们将一个回调函数作为变量传递给另一个函数,并在包含它的函数内的某一点执行该回调函数时,其效果等同于在包含它的函数中直接定义该回调函数。这意味着,回调函数本质上是一个闭包(Closure)。
正如我们所知,闭包能够访问其包含函数的作用域。因此,回调函数可以获取包含它的函数中的局部变量,以及全局作用域中的变量。
实现回调函数的基本原理
回调函数并不复杂,但在开始创建和使用回调函数之前,我们需要熟悉几个实现回调函数的基本原理。
使用命名或匿名函数作为回调
在前面的jQuery示例以及forEach示例中,我们使用了在参数位置定义的匿名函数作为回调函数。这是使用回调函数的一种常见方式。另一种常见模式是定义一个命名函数,并将该函数的名称作为变量传递给目标函数。例如以下示例:
//全局变量
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(options);
}
//当我们调用getInput函数时,我们将logStuff作为一个参数传递给它
//因此logStuff将会在getInput函数内被回调(或者执行)
getInput({name:"Rich",speciality:"Javascript"}, logStuff);
//name:Rich
//speciality:Javascript
传递参数给回调函数
既然回调函数在执行时只是一个普通函数,我们就能给它传递参数。我们可以将包含它的函数的属性(或全局属性)作为参数传递给回调函数。在前面的示例中,我们将options作为一个参数传递给了回调函数。现在,我们传递一个全局变量和一个本地变量:
//全局变量
var generalLastName = "Cliton";
function getInput (options, callback){
allUserData.push (options);
//将全局变量generalLastName传递给回调函数
callback(generalLastName,options);
}
在执行之前确保回调函数是一个函数
在调用之前,检查作为参数传递的回调函数确实是一个函数,这是一种明智的做法。同时,这也是实现条件回调函数的最佳时机。
我们来重构上面例子中的getInput函数,以确保进行恰当的检查。
function getInput(options, callback){
allUserData.push(options);
//确保callback是一个函数
if(typeof callback === "function"){
//调用它,既然我们已经确定了它是可调用的
callback(options);
}
}
如果没有适当的检查,当getInput的参数中没有传递回调函数,或者传递的“回调”实际上并非函数时,我们的代码将会导致运行时错误。
使用this对象的方法作为回调函数时的问题
当回调函数是this对象的一个方法时,我们必须改变执行回调函数的方法,以保证this对象的上下文正确。否则,如果回调函数被传递给一个全局函数,this对象要么指向全局的window对象(在浏览器环境中),要么指向包含该方法的对象(取决于具体实现)。
我们在下面的代码中说明这一现象:
//定义一个拥有一些属性和一个方法的对象 //我们接着将会把方法作为回调函数传递给另一个函数
var clientData = {
id: 094545,
fullName "Not Set",
//setUsrName是一个在clientData对象中的方法
setUserName: fucntion (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
使用Call和Apply函数来保存this
我们可以使用Call或Apply函数来修复上述问题。到目前为止,我们知道JavaScript中的每个函数都有两个方法:Call和Apply。这些方法用于设置函数内部的this对象,并向该函数传递变量。
Call函数接收的第一个参数是用于在函数内部作为this的对象,传递给函数的其他参数则逐个传递(当然使用逗号分开)。Apply函数的第一个参数也是用于在函数内部作为this的对象,然而最后一个参数却是传递给函数的值的数组。
听起来很复杂,那么我们来看看使用Apply和Call有多么的简单。为了修复前面例子的问题,我在下面的示例中使用Apply函数:
//注意到我们增加了新的参数作为回调对象,叫做“callbackObj”
function getUserInput(firstName, lastName, callback. callbackObj){
//在这里做些什么来确认名字
callback.apply(callbackObj, [firstName, lastName]);
}
使用Apply函数正确设置了this对象,我们现在正确地执行了callback,并在clientData对象中正确设置了fullName属性:
//我们将clientData.setUserName方法和clientData对象作为参数,clientData对象会被Apply方法使用来设置this对象
getUserName("Barack", "Obama", clientData.setUserName, clientData);
//clientData中的fullName属性被正确的设置
console.log(clientUser.fullName); //Barack Obama
我们也可以使用Call函数,但是在这个例子中我们使用Apply函数。
允许多重回调函数
我们可以将不止一个的回调函数作为参数传递给一个函数,就像我们能够传递不止一个变量一样。这里有一个关于jQuery中AJAX的例子:
function successCallback(){
//在发送之前做点什么
}
function successCallback(){
//在信息被成功接收之后做点什么
}
function completeCallback(){
//在完成之后做点什么
}
function errorCallback(){
//当错误发生时做点什么
}
$.ajax({
url:"http://fiddle.jshell.net/favicon.png",
success:successCallback,
complete:completeCallback,
error:errorCallback
});
“回调地狱”问题以及解决方案
在执行异步代码时,无论以什么顺序简单地执行代码,经常情况会变成许多层级的回调函数堆积,以致代码变成下面的情形。这些杂乱无章的代码叫做“回调地狱”(Callback Hell),因为回调太多而使看懂代码变得非常困难。我从node-mongodb-native(一个适用于Node.js的MongoDB驱动)中拿来了一个例子。这段位于下方的代码将会充分说明回调地狱:
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();
});
});
});
});
});
});
你应该不想在你的代码中遇到这样的问题,当你遇到了
- 你将会时不时的遇到这种情况
- 这里有关于这个问题的两种解决方案。
- 给你的函数命名并传递它们的名字作为回调函数,而不是在主函数的参数中定义匿名函数。
- 模块化:将你的代码分隔到模块中,这样你就可以导出代码块来完成特定的工作。然后你可以在你的巨型应用中导入这些模块。
创建你自己的回调函数
既然你已经完全理解了关于JavaScript中回调函数的一切(我认为你已经理解了,如果没有那么快速的重读以便),你看到了使用回调函数是如此的简单而强大,你应该查看你的代码看看有没有能使用回调函数的地方。回调函数将在以下几个方面帮助你:
- 避免重复代码(DRY-不要重复你自己)
- 在你拥有更多多功能函数的地方实现更好的抽象(依然能保持所有功能)
- 让代码具有更好的可维护性
- 使代码更容易阅读
- 编写更多特定功能的函数
创建你的回调函数非常简单。在下面的例子中,我将创建一个函数完成以下工作:读取用户信息,用数据创建一首通用的诗,并且欢迎用户。这本来是个非常复杂的函数因为它包含很多if/else语句并且,它将在调用那些用户数据需要的功能方面有诸多限制和不兼容性。
相反,我用回调函数实现了添加功能,这样一来获取用户信息的主函数便可以通过简单的将用户全名和性别作为参数传递给回调函数并执行来完成任何任务。
简单来讲,getUserInput函数是多功能的:它能执行具有各种功能的回调函数。
//首先,创建通用诗的生成函数;它将作为下面的getUserInput函数的回调函数
function genericPoemMaker(name, gender) {
console.log(name + " is finer than fine wine.");
console.log("Altruistic and noble for the modern time.");
console.log("Always admirably adorned with the latest style.");
console.log("A " + gender + " of unfortunate tragedies who still manages a perpetual smile");
}
//callback,参数的最后一项,将会是我们在上面定义的genericPoemMaker函数
function getUserInput(firstName, lastName, gender, callback) {
var fullName = firstName + " " + lastName;
// Make sure the callback is a function
if (typeof callback === "function") {
// Execute the callback function and pass the parameters to it
callback(fullName, gender);
}
}
调用getUserInput函数并将genericPoemMaker函数作为回调函数:
getUserInput("Michael", "Fassbender", "Man", genericPoemMaker);
// 输出
/* Michael Fassbender is finer than fine wine.
Altruistic and noble for the modern time.
Always admirably adorned with the latest style.
A Man of unfortunate tragedies who still manages a perpetual smile.
*/
因为getUserInput函数仅仅只负责提取数据,我们可以把任意回调函数传递给它。例如,我们可以传递一个greetUser函数:
unction greetUser(customerName, sex) {
var salutation = sex && sex === "Man" ? "Mr." : "Ms.";
console.log("Hello, " + salutation + " " + customerName);
}
// 将greetUser作为一个回调函数
getUserInput("Bill", "Gates", "Man", greetUser);
// 这里是输出
Hello, Mr. Bill Gates
我们调用了完全相同的getUserInput函数,但是这次完成了一个完全不同的任务。
正如你所见,回调函数很神奇。即使前面的例子相对简单,想象一下能节省多少工作量,你的代码将会变得更加的抽象,这一切只需要你开始使用回调函数。大胆的去使用吧。
在JavaScript编程中回调函数经常以几种方式被使用,尤其是在现代Web应用开发以及库和框架中:
- 异步调用(例如读取文件,进行HTTP请求,等等)
- 时间监听器/处理器
- setTimeout和setInterval方法
- 一般情况:精简代码
结束语
JavaScript回调函数非常美妙且功能强大,它们为你的Web应用和代码提供了诸多好处。你应该在有需求时使用它;或者为了代码的抽象性,可维护性以及可读性而使用回调函数来重构你的代码。
以上就是理解与使用JavaScript中的回调函数的详细内容,更多关于JavaScript的资料请关注本站其它相关文章!