总听到这么一个词语:回调函数。
对于它的了解,只知道在微信的网页授权用到了回调,以及在Angular中可以用观察者模式进行.subscribe订阅,但对于它原理的理解,却是一团浆糊。直到昨天开会时,突然被问到回调函数的知识,我才意识到自己真的不理解。
我们先从最简单的写法入手,一步一步走向回调函数。
(如果熟悉语法,请跳到第二节)
最简单的方法就是,在Chrome控制台直接输入。
下文的Demo都在浏览器中演示。
(说明:这里的"函数"相当于面向对象的"方法")
/* 定义名为test的函数 传入的参数是a 功能:输出传入的变量 */ var test = function(a){ console.log(a); }; //调用方法,输出HelloWorld test('helloworld');
成功输出了结果。
很简单:
/* 定义函数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();
我们先看一个写死的函数:
var test = function( ){ console.log("HelloWorld"); };
这个函数事实上是没有意义的,因为它没有输入,不会变化,无论重复运行多少次,它的结果都是一样的。
一个函数里面,既有算法又有数据:"算法"指的是输出字符串的这个操作,"数据"这的是输出的内容'HelloWorld'。所以如果我们想让这个函数发挥作用,就要让它可以变化。
(备用)我们可以借助最原始时代的编程思想来理解:在早期的计算机思想中,数据和算法是分离的,算法被写成一段段代码,数据是用来被算法操作的。
程序等于数据加算法。借助这种想法,我们认为,在一个函数中,只要数据和算法其中一个是可以变化的,那么函数就是有意义的。
我们先想到的肯定是改变数据,把一个写死的函数加上参数,它就变“活了”。
//写死的函数 var test = function( ){ console.log("HelloWorld"); }; test(); //加上参数 var test = function(string){ console.log(string); }; test('HelloWorld');
这样,把一段永远不会变化的代码,变成了可以在调用时根据不同输入来获得不同输出的程序。
初次揭开回调函数的面纱:
把一个写死的函数变活,可以传入数据,用相同的算法对不同的数据进行处理;当然也可以传入一个算法,用不同的算法对相同的数据进行处理,而后者,正是回调函数。
用一句话概括:在直接调用函数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'这个字符串。
图片显示,这两种方式的结果一样。
普通函数,参数是数据,在调用test()时,传入HelloWorld字符串,那么abc就是这个字符串,test函数对传入的字符串执行了输出操作;
而回调函数,参数是函数,在调用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);} );
我们刚才已经知道,用传入的方法操作固定字符串,这个字符串就是传进来的函数的参数。
如果要用一个函数操作两个字符串呢?
——把传入的剪头函数定义两个参数。
上图中,传进去的剪头函数需要两个变量,那么回调的时候就得传进去两个变量,对应关系如上图。
如果要对同一个字符串执行两种不同的操作呢?
——传入两个箭头函数
上图中,传进去两个函数,对同一字符串操作,就实现了用两种不同方式操作同一字符串。
把上面两种结合一下:
// 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) } );
请结合之前的知识,自行理解上述代码。
回调嵌套在实际生产中使用的很少,但这并不妨碍它作为我们深刻理解回调函数的一种方式。
比较下面三个函数:
//普通函数 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')});
刚才说了,传进去的函数abc需要接收函数的能力,而再看接收的函数,正是{console.log()},也就是具备输出的功能,所以只需要在接收到函数aFunction之后,用这个aFunction函数处理'HelloWorld'字符串就可以了。
成功输出了结果:
怕上面没说清楚,最后用多图流,再说一下回调嵌套的步骤。
这是原始代码,定义了一个test,要求通过调用test来输出HelloWorld:
第一步,调用test(),传入函数,
传入之后,
abc = function(aFunction) {aFunction('HelloWorld')}
第二步,用传进来的abc处理另一个函数,需要把另一个函数作为参数aFunction,传到abc中,此时:
aFunction = Function(def){console.log(def);}
第三步,abc接收到aFunction后,用aFunction来操作'HelloWorld'字符串,把字符串传到aFunction中,此时:
def = 'HelloWorld';
第四步,执行console.log(def),输出'HelloWorld'
到此为止,如果弄懂了回调函数嵌套,我们对回调函数的理解就差不多了。
在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函数:
我们遇到什么回调函数,也不要怕,微笑着面对他,消除恐惧的最好办法,就是明白回调函数是在已经有数据的前提下,传入一个方法,然后用我们传入的方法去操作那个已经存在的数据,坚持就是胜利,加油,奥利给!!!!!!!