本文转载于 SegmentFault 社区
作者:LYX6666
前言
总听到这么一个词语: 回调函数 。对于它的了解,只知道在微信的网页授权用到了回调,以及在 Angular 中可以用观察者模式进行 .subscribe 订阅,但对于它原理的理解,却是一团浆糊。直到昨天开会时,突然被问到回调函数的知识,我才意识到自己真的不理解。
一
基础知识:JavaScript 标准写法
我们先从最简单的写法入手,一步一步走向回调函数。 (如果熟悉语法,请跳到第二节)1. 如何测试 JS 代码
最简单的方法就是,在 Chrome 控制台直接输入。![7c318c402162c27eb82cb590e5a68a96.png](https://i-blog.csdnimg.cn/blog_migrate/4cb579703322de38ffdb462ba969c91c.png)
2. JS 定义函数
说明:这里的"函数"相当于面向对象的"方法"
/* 定义名为test的函数
传入的参数是a
功能:输出传入的变量 */
var test = function(a){
console.log(a);
};
//调用方法,输出HelloWorld
test('helloworld');
![f8216161d761914cc6399e608848f353.png](https://i-blog.csdnimg.cn/blog_migrate/9ffea95ab825a6705366e49e17defeda.png)
3. 函数调用函数
很简单:
/* 定义函数sum
传入两个参数a,b
作用:求a,b的和 */
var sum = function(a,b)
{
return a+b;
};
/* 定义函数test
调用sum
传入参数1,2 */
var test = function()
{
result = sum(1,2);
console.log(result);
}
//调用test,启动程序
test();
![734f89ab50708b9e60aba52b17310bbd.png](https://i-blog.csdnimg.cn/blog_migrate/73c21ff7d355b67eab9919b4fc33db38.png)
二
数据和算法
我们先看一个写死的函数:
var test = function( ){
console.log("HelloWorld");
};
这个函数事实上是没有意义的,因为它没有输入,不会变化,无论重复运行多少次,它的结果都是一样的。
一个函数里面,既有算法又有数据:"算法"指的是输出字符串的这个操作,"数据"这的是输出的内容 'HelloWorld' 。所以如果我们想让这个函数发挥作用,就要让它可以变化。 (备用) 我们可以借助最原始时代的编程思想来理解:在早期的计算机思想中,数据和算法是分离的,算法被写成一段段代码,数据是用来被算法操作的。 程序等于数据加算法。借助这种想法,我们认为,在一个函数中,只要数据和算法其中一个是可以变化的,那么函数就是有意义的。
1. 改变数据——普通的函数
我们先想到的肯定是改变数据,把一个写死的函数加上参数,它就变“活了”。
//写死的函数
var test = function( ){
console.log("HelloWorld");
};
test();
//加上参数
var test = function(string){
console.log(string);
};
test('HelloWorld');
这样,把一段永远不会变化的代码,变成了可以在调用时根据不同输入来获得不同输出的程序。
2. 改变算法——回调函数
初次揭开回调函数的面纱:把一个写死的函数变活,可以传入数据,用相同的算法对不同的数据进行处理;当然也可以传入一个算法,用不同的算法对相同的数据进行处理,而后者,正是回调函数。 用一句话概括:在直接调用函数 A() 时,把另一个函数 B() 作为参数,传入函数 A() 里面,以此来通过函数 A() 间接调用函数 B() 。 比较下面两个函数的异同:
//函数1
var test = function(abc){
console.log(abc);
};
//函数1的调用
test('HelloWorld');
//函数2
var test = function(abc){
abc('Helloworld');
};
//函数2的调用
test( function(words) {console.log(words);} );
这两个函数有着同样的参数,都是 abc,只不过,函数 1 是普通函数,参数 abc 是作为数据传入的,然后用函数 1 的语句来操作 abc;
而函数 2 是回调函数,参数是作为算法传入的,然后用传进来的这个函数 abc 来操作 'HelloWorld' 这个字符串。
![b7cf7abfd4fd567ed99f0e70836e738d.png](https://i-blog.csdnimg.cn/blog_migrate/163e31c069c7d5112ab080482d40fa27.png)
![76642d5512b0476d191dad937aa01012.png](https://i-blog.csdnimg.cn/blog_migrate/8c14b6e0d1cc1e668842434a7466fd45.jpeg)
而回调函数,参数是函数,在调用 test() 时,传入输出字符串的方法,那么 abc 就是输出字符串的方法,test 函数将会用传进来的输出字符串的方法对一个固定字符串 HelloWorld 执行操作。与此同时,这个 HelloWorld 字符串,又成了传到 test 函数的这个 function(words) {console.log(words);} 函数的参数,如果传入的参数有名字的话,在调用 test 时,在函数内部,等价于发生了如下操作:
{
//调用test传进来一个函数之后,abc就是那个函数
abc = function(words) {console.log(words);};
//用abc操作字符串,'HelloWorld'变成了传进来的函数的参数
abc('HelloWorld');
}
三
深入回调函数
我们已经理解了初级阶段的回调,但目前,传入的函数还处于 function(){} 的形式,这种写法是原始写法,相当于定义了一个新函数然后传进去,不仅脱离生产环境,而且有很多局限 (比如 this. 作用域问题) ,下一步,要把这种形式改为剪头函数。 一个函数只用一次,并且不需要直到它的名字时,可以用匿名函数来简化。 在解决 this. 作用域时,又将 匿名函数 转化为 箭头函数 。
//以下两种写法等价
function (words) {console.log(words);}
(words) => {console.log(words);}
可以看出,箭头函数省略了
function
标识,只留下了参数
words
和函数体
console.log(
)
, 二者用箭头连接。
这种省略写法更贴近生产环境:
//函数2
var test = function(abc){
abc('Helloworld');
};
//函数2的调用
test( (words) => {console.log(words);} );
1. 了解参数的对应关系
![c1112e46a25401841f6654ed66f9f556.png](https://i-blog.csdnimg.cn/blog_migrate/b40870bcb6c82459dddf4f4e7a0fcf8c.png)
![3df0abd8e4f3dc466ae349c383e5fb18.png](https://i-blog.csdnimg.cn/blog_migrate/699d1ce602bee093bb74a0afe8e7306c.jpeg)
![036ef262873a2a24e63110ff6f84f855.png](https://i-blog.csdnimg.cn/blog_migrate/7ec8bc44cbe9100d0ba8e6f1ecca64eb.png)
![d3be1824fb21349341eded5740e8484d.png](https://i-blog.csdnimg.cn/blog_migrate/d640fe5ae211e978811feaff89e5f583.png)
![0fa190dd0c9827bd3491be97e0201741.png](https://i-blog.csdnimg.cn/blog_migrate/b250d1a859c6800ca88d8f04d174e501.png)
//
var test = function(abc, def){
abc('HelloWorld1', 'HelloWorld2');
def('HelloWorld1', 'HelloWorld2');
};
//
test((words1,words2) => {
console.log('我是箭头1,我输出'+words1);
console.log('我是箭头1,我输出'+words2);
},
(words1,words2)=> {
console.log('我是箭头2,我输出'+words1);
console.log('我是箭头2,我输出'+words2)
}
);
![5d682c68db081636c54af4b88ce1a7f9.png](https://i-blog.csdnimg.cn/blog_migrate/474bc8f529a48c78487f3a6cd0087d60.png)
四
回调函数嵌套
回调嵌套在实际生产中使用的很少,但这并不妨碍它作为我们深刻理解回调函数的一种方式。 比较下面三个函数:
//普通函数
var test = function(abc){
console.log(abc);
}
//回调函数
var test = function(abc){
abc('HelloWorld');
};
//回调函数嵌套
var test = function(abc){
abc( (def) => {console.log(def);} );
}
练习题:问,以上三种情况下,如果分别调用三个函数,输出 HelloWorld 字符串?第一种早就学会了,直接调用就可以:
//普通函数
test('HelloWorld');
第二种也已经会了,有了
HelloWorld
的数据,我们需要传进去的是操作这个数据的方法,所以:
//回调函数
test( (words) => {console.log(words);});
主要说的是第三种,回调函数是传入一个函数,用传入的函数 abc 去操作一个数据
'HelloWorld'
。这个被操作的数据,作为传进来的函数 abc 的参数。
而再看回调函数嵌套,它也是传进去一个函数 abc,但不同的是,它是用这个传进来的函数去操作另一个函数。
此时,我们传入的 abc 函数需要一种可以接收函数的能力,而不再是接收变量的能力。
所以怎么办?——在传进去的这个函数 abc 中再使用一次回调,使得 abc 接收的参数是一个函数,而不是一个变量:
//回调函数嵌套
test( (aFunction) => {aFunction('HelloWorld')} );
//如果看不明白,把箭头函数复原,如下
test( function(aFunction) {aFunction('HelloWorld')});
![be82bbfb87fbe15018b675fef09b6caa.png](https://i-blog.csdnimg.cn/blog_migrate/7e6dad47494d1944790b4781f76483ec.png)
成功输出了结果:
![64738d8193d06c491890190b24533db5.png](https://i-blog.csdnimg.cn/blog_migrate/9f9e1abdfd8b0173fe7fb3fb8006d9d2.png)
五
图解具体步骤
怕上面没说清楚,最后用多图流,再说一下回调嵌套的步骤。 这是原始代码,定义了一个 test,要求通过调用 test 来输出 HelloWorld :![b15c6079088b471c176f21dfc7ece759.png](https://i-blog.csdnimg.cn/blog_migrate/9b3cd97f4241250ef69e38ff2fbcd26a.png)
![e2c81ae9543c922b569cf7ad7835c6ae.png](https://i-blog.csdnimg.cn/blog_migrate/13893e512e04d1f36d725316bafbc217.png)
abc = function(aFunction) {aFunction('HelloWorld')}
第二步,用传进来的 abc 处理另一个函数,需要把另一个函数作为参数
aFunction
,传到 abc 中,此时:
![642bd2198a54154c2d21f344555031f1.png](https://i-blog.csdnimg.cn/blog_migrate/91eb61cdd912c9d5926ddf73085308e8.png)
aFunction = Function(def){console.log(def);}
第三步,abc 接收到
aFunction
后,用
aFunction
来操作
'HelloWorld'
字符串,把字符串传到
aFunction
中,此时:
![c0e5eea3438a8da7d5d2c8380ebc1dc8.png](https://i-blog.csdnimg.cn/blog_migrate/fda6909949ea0f500a4d367e692987e4.png)
def = 'HelloWorld';
第四步,执行
console.log(def)
,输出
'HelloWorld'
![66c2eb00c577c0d5ddfd26ec19dc66cd.png](https://i-blog.csdnimg.cn/blog_migrate/4606847cf32610660bba33e9be1aca30.png)
六
生产环境中的观察者
在 Angular 中,有一个 .subcribe 订阅,这个就是回调,以前这知道这么用,但现在我们可以解释一下它的原理了! 先上代码:
//向8080端口的helloWorld路径发起请求
httpClient.get('http://localhost:8080/helloWorld')
.subscribe(
function success(data) {
console.log('请求成功');
console.log(data);
},
function error(data) {
console.log('请求失败');
console.log(data);
});
·
httpClient
的作用是发起 HTTP 请求,并且记录请求状态。·
.get
设定请求地址。·
.subscript
是设定请求完成的操作。
我们的目的是,输出请求之后的信息,以便让我们知道请求是否成功。并且我们已经知道
httpClient
会记录请求的状态,这个“状态”就是个变量。
既然变量就在那里放着不动,我们只需要想办法,去操作这个状态的变量就可以了,——传入两个函数, success 和 error ,在请求成功之后调用 success ,如果失败调用 error 。再次强调,它已经有数据了,我们传进去的是函数,请求完毕后,就会用我们传入的函数去操作这个请求状态。我们传入的“输出”,他就“输出”这个状态!
//向8080端口的helloWorld路径发起请求
httpClient.get('http://localhost:8080/helloWorld')
.subscribe(
function success(data) {
console.log('请求成功');
console.log(data);
},
function error(data) {
console.log('请求失败');
console.log(data);
});
//系统中的subscirbe函数
function subscribe(success,error)
{
request_success;//请求是否成功
request_data;//请求数据
if (request_success == 1) {
success(request_data);
}
else{
error(request_data);
}
}
为了便于理解,我写了一个假的
subscribe
函数:
![3019f82465205d784ddeae786e5619ad.png](https://i-blog.csdnimg.cn/blog_migrate/a3b0be775e54ac82ed437d340942041b.jpeg)
七
总结
我们遇到什么回调函数,也不要怕,微笑着面对他,消除恐惧的最好办法,就是明白回调函数是在已经有数据的前提下,传入一个方法,然后用我们传入的方法去操作那个已经存在的数据,坚持就是胜利,加油,奥利给!!!!!!!- END -
![112e46ef7a56aa38524a8f611fab116a.png](https://i-blog.csdnimg.cn/blog_migrate/7d997e2ad1b947836be70dc4cd58bf46.jpeg)