本文作者:lss

1.回调函数

简单了解回调及递归的相关知识

首先看一段代码
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
29
30
31
32
33
34
35
36
37
38
/**
* 回调函数1
* 生成一个2n的函数
*/
function double(n) {
return n * 2;
}
/**
* 回调函数2
* 生成一个4n的函数
*/
function quadruple(n) {
return n * 4;
}

/**
* 中间函数
* 接收一个生成偶数的函数作为参数
* 返回一个奇数
*/
function getOddNumber(k, getEvenNumber) {
return 1 + getEvenNumber(k);
}

/**
* 起始函数
* 程序主函数
*/
function main() {
k = 1;
//当需要生成一个2n+1形式的奇数时
i = getOddNumber(k, double);
console.log(i);
//当需要生成一个4n+1形式的奇数时
i = getOddNumber(k, quadruple);
console.log(i);

}

在上面的代码段中,main()是主函数;
当主函数中执行getOddNumber()方法的时候,传入了两个参数kdouble
于是在getOddNumber()中,double作为函数名被传入使用,接着又触发了double()方法。

如果你把函数的指针(函数名/地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
所以double()quadruple()就是回调函数。

1
2
3
4
//代码运行结果
main();
3
5

(按F12,点击console查看结果)

使用回调函数可以将js的操作同步化,适用于需要按步骤进行的情况中。

在下个例子中,我们使用jQuery中的slideToggle()方法进行操作,jQuery中已经为其写好了回调函数,我们只需要添加参数即可。

slideToggle(speed,callback)

| 参数 | 描述 |
| ——– | :— |
| speed | 可选。规定元素从隐藏到可见的速度(或者相反)。默认为 “normal”。|
| callback | 可选。toggle 函数执行完之后,要执行的函数。 |

1
2
3
4
5
6
7
8
9

//不使用回调
$("#p1").slideToggle("slow");
alert("不使用回调,先弹出对话框再隐藏/显示!");

//使用回调
$("#p2").slideToggle("slow", function () {
alert("使用回调,先隐藏/显示再弹出对话框!")
)};


当我们不使用回调的时候,隐藏/显示和弹框同时出现。如图

不使用回调

尝试一下:

点击下面的按钮,这句话隐藏/显示和弹出对话框哪个先发生?


而当我们使用回调的时候,先隐藏/显示再弹出对话框。如下图
使用回调函数

尝试一下:

点击下面的按钮,这句话隐藏/显示和弹出对话框哪个先发生?

jQuery中类似的还有很多,比如hide()show()等,可以点击查看介绍。

2.异步

在浏览网页的时候,很多操作都需要登录才能使用。有时候登录需要返回到登录界面,我们看了一半的网页不想关掉,这时就需要异步技术来达到。

异步操作
某网站的异步登录,可以看到,登录成功时页面并没有刷新

1
2
3
4
5
6
7
8
//一段简单的ajax基本代码
$.ajax({
url: url,
type: "post",
data: dataPara,

success: fn
});

异步执行可以用回调函数实现

3.promise

先看一段程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义函数
var s = () => new Promise((res, rej) => { //给s赋值一个匿名函数,先实例化了promise对象
setTimeout(() => {
console.log('我是回调函数')
if (1) {
res("suc")
} else {
rej("err")
}
}, 3000);
})
s().then((data) => { //紧接着s才触发
console.log(data)
}, (err) => {
console.log(data)
})

运行结果

1
2
我是回调函数
suc

上面即是js中promise对象的用法。比如,有若干个异步任务,需要先做任务1,如果成功后再做任务2,任何任务失败则不再继续并执行错误处理函数。
在例子中可以看到,写在then中的事件会因为setTimeout()的影响而被影响,3秒之后才紧跟着输出参数data中的内容。

尝试一下:


(按F12,点击console查看结果)


promise有更简单的写法

1
2
3
4
5
6
7
8
9
10
11
12
....
....

(async () => {
await s()

setTimeout(() => {
console.log(333)
}, 2000);

console.log(111)
})()


await后面的函数s()会在自己执行完所有操作以后才继续执行下面的代码。

## 4.递归
程序调用自身的编程技巧称为递归(recursion)。如:

1
2
3
4
5
6
7
function fun(){

// 自己调用自己,称为递归调用
fun();
console.log("递归");

}


下面通过简单的例子【体会】一下递归。


我们任说1个整数x,猜它的平方根为y,如果不对或精度不够准确,那我令

y = (y+x/y)/2


如此循环反复下去,y就会无限逼近x的平方根。这就是经典的牛顿迭代法求平方根,用代码表示即:
1
2
3
4
5
6
7
8
9
10
11
12
13
function mysqrt(x, y0) {

y1 = (y0 + x / y0) / 2.0;

//一般认为当两次计算的y0、y1相差小于 0.0000000000001时,说明已经无限接近正确答案
if (Math.abs(y1 - y0) > 0.0000000000001)

return mysqrt(x, y1);

else

return y1;
}


这里的x1即为最终答案。
由于需要初始参数:需要求平方根的任意整数x第一次计算时需要的任意整数y,我们给出一个驱动的函数
1
2
3
var x = 2;//需要求平方根的任意整数x
var y0 = 1;//第一次计算时需要的任意整数y
mysqrt(x, y0);


运行后输出的结果为
1
1.414213562373095


尝试一下:
输入一个数字:


我们再看一个例子。
我们知道斐波那契数的递推公式为:F(0)=F(1)=1,F(n)=F(n-1)+F(n-2) n>=2;
这个明显地给出了递归边界n=0或1的时候F(n)的值,和递归逻辑F(n)=F(n-1)+F(n-2),即递推公式.所以这个递归函数不难写出来
1
2
3
4
5
6
7
function Fib(n) {  
if(n<=1){
return n;
}
else
return Fib(n-1)+Fib(n-2);
}


尝试一下:

n=(不要超过40)
结果:

这段代码简单明了,就是执行速度太慢了(经过测试,当n>40时浏览器已明显出现卡顿的现象),因为编译器是以如下方式进行计算的(例如计算Fib(6)):

1
2
3
4
5
Fib(6) = Fib(5) + Fib(4);
= Fib(4) + Fib(3) + Fib(3) + Fib(2);
= Fib(3) + Fib(2) + Fib(2) + Fib(1) + Fib(2) + Fib(1) + Fib(2);
= Fib(2) + Fib(1) + Fib(2) + Fib(2) + Fib(1) + Fib(2) + Fib(1) + Fib(2);
= 8

从上面的递归展开式可以看出,只要参数大于2,函数就会被再调用2次,如此重复循环。
作图如下表。

可以看到从13开始,位数越高斜率越高,也就是每多一位增加的次数就越多。
所以当计算到40时这个计算量就会变得非常大,也就变得非常慢。

所以为了优化算法,我们使用尾递归的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//尾递归
function Fib_Tail(n)
{
if (n == 1 || n == 2)
return 1;
else
return Tail(n, 1, 1, 3);
}
function Tail(n, b1, b2, begin)
{
if (n == begin)
{
return b1 + b2;
}
else
return Tail(n, b2, b1 + b2, begin + 1);
}

n=
结果:

经过测试可以发现,n>40时的卡顿的现象完全没有了。
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。
当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。
可以看到,这样的计算过程都是在每次进入递归函数时计算的(尾部),所以是一个线性增长,如下图。


对比一下,发现尾递归的计算次数相比刚才少多了,只要编译器允许我们可以计算Fib_tail(100)都非常迅速.