理解 JavaScript 回调函数并使用
JavaScript中,函数是一等(first-class)对象;也就是说,函数是 Object 类型并且可以像其他一等对象(String,Array,Number等)一样使用。它们可以“保存在变量中,作为参数传递给函数,在函数内创建,以及被函数返回”。
由于函数是一等对象,我们可以把一个函数作为参数传递给另一个函数,然后在那个函数内执行,甚至也可以被那个函数返回,然后再执行。这就是 JavaScript 中回调函数(callback functions)的本质。在本文的剩余部分,我们将学习到关于 JavaScript 回调函数的所有知识。回调函数可能是 JavaScript 中使用最广泛的函数式编程技术了,你可以在任何一段 JavaScript或jQuery 代码发现它,但是,它对很多 JavaScript 开发者来说依然是神秘的。直到你阅读了本文,就再也不会对它感到神秘了。
回调函数 是来源于函数式编程的一种技术。从底层来说,函数式编程把函数用作参数。函数式编程过去是 —— 现在仍然是,(尽管如今不太流行)被有经验的、高级开发者视作难懂的技术。
幸运的是,函数式编程已经被阐明到像我们这样的的普通人都能容易理解的地步。其主要技术之一就是回调函数。下面你就会看到,实现回调函数很容易,就像传递一个普通变量参数一样。这个技术如此简单以至于我很奇怪为什么大多数教程都把它归类为高级主题里面。
回调是什么?
回调函数,也叫高阶(higher-order)函数,是一个作为参数传递到其他函数的函数,然后回调函数在其他函数中被调用。回调函数本质上是一个模式,因此使用回调函数被称为回调模式。
请看下面的代码,这是 jQuery 中常见的回调函数的使用:
$("#btn_1").click(function(){
alert("Btn 1 Clicked");
});
这里,传递了一个匿名函数给 click 方法。click 方法将调用或执行这个回调函数。
再看一个例子:
var friend = ["Mike", "Stacy", "Andy", "Rick"];
friend.forEach(function(eachName, index){
console.log(index + 1 + "." + eachName); // 1.Mike,2.Stacy,3.Andy,4.Rick
});
这里,传递了一个匿名函数给 forEach 方法。
回调函数如何工作?
当我们传递一个回调函数给其他函数时,我们只是传递了函数定义。我们并没有在参数中执行函数。换句话说,传递函数时不能在函数名后面加括号“()”,而执行函数时那样需要。
由于其他函数在参数中有该回调函数的定义,所以它可以在任何时候执行该函数。
注意到回调函数不是立即执行的,所谓“回调”就是指在其他函数中某个特定的时候被回头调用。所以再看第一个例子,click 函数中的匿名函数将在click函数体内被调用。即使该函数匿名,也可以通过 arguments 对象访问到。
回调函数都是闭包
当传递一个回调函数给其他函数时,回调函数在其他函数函数体内某处执行,就好像回调函数是在其他函数中定义的。这说明回调函数是一个闭包(closure)。众所周知,闭包可以访问外层包含函数的作用域,所以回调函数也能访问其他函数的变量,甚至全局作用域中的变量。
实现回调函数所遵循的基本准则
尽管不复杂,但仍然有一些值得注意的地方。
使用命名的或匿名的函数作为回调函数
第二个例子中我们使用了匿名函数作为回调函数,这是一种常见的做法。另一种常见做法是定义一个有名字的函数,然后传递给另一个函数。
// 全局变量
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, calllback){
allUserData.push(options);
calllback(options);
}
// 调用 getInput 函数时传递了 logStuff,所以,logStuff 将在 getInput 函数中被调用(或执行)
getInput({name:"Rich",speciality:"JavaScript"},logStuff);
// name:Rich
// speciality:JavaScript
传递参数给回调函数
由于回调函数在执行时也只是一个普通函数,所以我们可以传递参数给它。可以传递外层函数的属性或者全局变量。上例中,传递了 options 作为回调函数的参数。然后,让我们传递一个全局变量和局部变量。
// 全局变量
var generalLastName = "Clinton";
function getInput(options, calllback){
allUserData.push(options);
// 传递全局变量
calllback(generalLastName, options);
}
执行前确保回调是一个函数
在调用之前检查所传入的回调函数是否真的是一个函数是一个好习惯。让我们重构一下上例中getInput函数:
function getInput(options, calllback){
allUserData.push(options);
// 确保 calllback 是一个函数
if(typeof calllback === "function"){
// 调用之,因为已经确保它是函数了
calllback(options);
}
}
如果不检测其类型,当传入的参数不是函数时,就会导致运行时错误。
回调函数与this相关的问题
当回调函数中用到了 this 对象时,我们不得不改变执行回调函数的方式来保持原有的 this 对象。否则,this 对象可能指向全局的 window 对象,如果回调函数被传入了全局函数中的话。或者指向外层包含方法的对象。
下面在代码中演示:
var clientData = {
id: 012334,
fullName: "Not Set",
// setUserName 是 clientData 对象的方法
setUserName: function(firstName, lastName){
this.fullName = firstName + " " + lastName;
}
}
function getUserInput(firstName, lastName, calllback){
calllback(firstName,lastName);
}
下面的代码中,当 clientData.setUserName 执行时,this.fullName 将不会设置 clientData 对象的 fullName 属性,而是设置为 window 对象的 fullName 属性。这是因为全局函数中的 this 对象指向 window 对象
getUserInput("Barack", "Obama", clientData.setUserName);
console.log(clientData.fullName); // Not Set
console.log(window.fullName); // Barack Obama
使用 Call 或 Apply 函数来保持 this
我们可以通过 Call 或 Apply 函数来解决上面的问题。目前来说,JavaScript 中的每个函数都有两个方法:Call 和 Apply。这两个方法用来设置函数内的 this 对象。
Call 把第一个参数作为函数内的 this 对象,其他的参数分别传递给函数。Apply 也是把第一个参数作为函数内的 this 对象,而第二个参数是一个数组(或者argument对象)。
我们在下面的代码中使用 Apply 来解决这个问题:
function getUserInput(firstName, lastName, calllback, calllbackObj){
calllback.apply(calllbackObj, [firstName,lastName]);
}
apply 正确设置了 this 对象,现在可以正确执行回调,并设置 clientData 上的 fullName 属性了。
getUserInput("Barack", "Obama", clientData.setUserName, clientData);
console.log(clientData.fullName); // Barack Obama
允许使用多个回调函数
我们传递多个回调函数到另外函数,就像传递多个变量一样,下面时一个经典的jQuery AJAX函数的例子:
function successCallBack(){
}
function failCallBack(){
}
function completeCallback(){
}
function errorCallback(){
}
$.ajax({
url:"http://www.91ymb.com/favicon.png",
success: successCallBack,
complete: completeCallback,
error: errorCallback
});
“回调地狱”问题和解决
在异步代码执行中,代码可能以任何顺序执行,有时会看到有很多层回调函数,比如下例:
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 函数是通用的:它执行所有回调函数来实现具体的功能。
// 首先,建立一个诗句生成函数,它将作为回调函数传递
function genericPoemMaker(name, gender){
console.log(name + " is finer than fine wine.");
console.log("ALltrustic 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");
}
//
function getUserInput(firstName, lastName, gender, callback){
var fullName = firstName + " " + lastName;
// 确保 callback 是函数
if(typeof callback === "function"){
callback(fullName, callback);
}
}
// 调用 getUserInput,并传递回调函数
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 函数:
function greetUser(customerName, sex){
var salutation = sex & sex == "Man" ? "Mr." : "Ms.";
console.log("Hello, " + salutation + " " + customerName);
}
getUserInput("Bill", "Gates", "Man", greetUser);
// 输出
// Hello, Mr. Bill Gates
我们同样调用 getUserInput 函数两次,但执行了不同的任务。
注意到,下列场景是我们频繁使用到回调函数的地方,尤其是现代 web 应用开发,库和框架开发:
- 异步执行(如读取文件,HTTP请求)
- 事件监听器/处理器
- setTimeout 函数和 setInterval 函数
- 通用原则:代码简洁性
最后
JavaScript 回调函数很好用,有很多好处。现在就开始使用回调函数来重构代码以提高抽象、可维护性、可读性吧。
原文链接: http://javascriptissexy.com/understand-javascript-callback-functions-and-use-them/
理解 JavaScript 回调函数并使用的更多相关文章
- 理解javascript 回调函数
##回调函数定义 百度百科:回调函数 回调函数就是一个通过函数指针调用的函数.如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数.回调函数不 ...
- 重新理解javascript回调函数
把函数作为参数传入到另一个函数中.这个函数就是所谓的回调函数 经常遇到这样一种情况,某个项目的A层和B层是由不同的人员协同完成.A层负责功能funA,B层负责funcB.当B层要用到某个模块的数据,于 ...
- JavaScript回调函数的实现
https://github.com/forsigner/blog/blob/master/source/_posts/javascript-callback.md 在JavaScript中,回调函数 ...
- 【JavaScript】JavaScript回调函数
什么是Javascript 回调函数? 函数和其他数据一样可以被赋值,删除,拷贝等,所以也可以把函数作为参数传入到另一个函数中. 这个函数就是所谓的回调函数 举例: //不带参数的case fun ...
- JavaScript回调函数及数组方法测试
JavaScript回调函数及数组方法测试 具体代码如下: <!DOCTYPE html> <html lang="en"> <head> &l ...
- 简单理解js回调函数
前言 其实回调函数简单通俗点就是当有a和b两个函数,当a作为参数传给b,并在b中执行,这时a就是一个回调(callback)函数,如果a是一个匿名函数,则为匿名回调函数那下面们来通过一个实例来具体解释 ...
- 如何定义 Java 的回调函数,与 JavaScript 回调函数的区别
JavaScript 中的回调函数 在 JavaScript 中经常使用回调函数,比如:get 请求.post 请求等异步任务.在我们请求之前以及请求之后,都需要完成一些固定的操作,比如:请求之前先从 ...
- JavaScript回调函数的理解
这里是个人对回调函数的一段理解 <!DOCTYPE html> <html> <head> <title>回调函数</title> < ...
- Javascript 回调函数理解---二娃子买肾机6
在Javascript中什么是回调函数,我认为简单来说就是把一个函数B作为参数传递给另一个函数A,在A函数中的一定时机调用函数B. 这里可以看出回调函数形成了一个闭包,它可以访问函数A中的活动对象. ...
随机推荐
- Apworks框架实战(五):EasyMemo的领域模型设计
在上一讲中,我们已经新建了一个聚合根对象Account,并已经可以开始设计领域模型了.在这一讲中,我们会着重介绍EasyMemo领域模型的分析和设计,并引入Visual Studio Ultimate ...
- 在线课程笔记—.NET基础
关于学习北京理工大学金旭亮老师在线课程的笔记. 介绍: 在线课程网址:http://mooc.study.163.com/university/BIT#/c 老师个人网站:http://jinxuli ...
- 【无私分享:ASP.NET CORE 项目实战(第十二章)】添加对SqlServer、MySql、Oracle的支持
目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 增加对多数据库的支持,并不是意味着同时对多种数据库操作,当然,后面,我们会尝试同时对多种数据库操作,这可能需要多个上下文,暂且 ...
- Xamarin Android 之起始篇
序言: 在博客园注册了已经有2年多了,快三年了.从开始学习这一行开始就在博客园注册了这个账号.至今也还没有写过一篇随笔,大多时候都是在园子里头潜水,看大牛写的文章,学习. 写博客不为啥,就是自己对自己 ...
- 使用 GitHub 和 Hexo 搭建个人独立博客
Wordpress这类博客系统功能强大,可对与我只想划拉的写点东西的人,感觉大材小用了.而且wp需要部署,网站的服务器也会带来问题,国内的服务器首先需要备案,费用不低:国外服务器访问速度受影响.近 ...
- javaScript中的小细节-局部作用域中的var
javaScript中var是很神奇的,在局部作用域中,var a = b = c = 1;是不一样的,a为使用var声明的变量,而b和c则是全局下的,此类变量被称为隐式全局变量:var a = 1; ...
- jQuery选择什么版本 1.x? 2.x? 3.x?
类似标题:jQuery选择什么版本?jquery一般用什么版本?jquery ie8兼容版本.jquery什么版本稳定? 目前jQuery有三个大版本:1.x:兼容ie678,使用最为广泛的,官方只做 ...
- Android开发者的Kotlin:书
原文标题:Kotlin for Android Developers: The book 原文链接:http://antonioleiva.com/kotlin-android-developers/ ...
- unable to boot the simulator,无法启动模拟器已解决
突然模拟器报错:unable to boot the simulator(无法启动模拟器) 试了好几种解决办法,删除所有的模拟器重启以后再添加,删除钥匙串登陆中的证书,重新安装Xcode都不行 最后通 ...
- iOS从零开始学习直播之音频2.后台播放和在线播放
本篇主要讲音频的后台播放和在线播放. 后台播放 上一篇写的工程运行之后程序退至后台,发现运行不了,歌停止了,这显然不行,音乐后台播放是标配啊.今天就来讲一下后台播放. 1.在plist文件里,告诉 ...