递归
递归函数:直接或者间接地调用自身的一种函数,它把一个问题分解为一组相似的子问题。
// 一个简单的阶乘函数
var f = function (x) {
if (x === 1) {
return 1;
} else {
return x * f(x - 1);
}
};
经典的递归问题汉诺塔:塔上有3根柱子和一套子直径各不相同的空心圆盘。
开始盘子从小到大的顺序堆叠在1号柱子上,目标是要经过2号柱子全部移动到3号柱子上,中间不允许较大的盘放在较小的盘上;
分解成子问题:
- 将1号柱子上的从上到下的n-1个盘利用3号柱子移动到2号柱子上。
- 将1号柱子上最下面的一个盘,直接移动到3号柱子上.
- 最后把2号柱子上n-1个盘利用递归调用方法,全部移动到3号柱子上。
var hanoi = function(disc,src,aux,dst) {
if(disc > 0) {
hanoi(disc -1,src,dst,aux);
document.writeln('Move disc '+disc + " from "+src+"to "+dst);
hanoi(disc-1,aux,src,dst)
}
};
hanoi(3,'Src','Aux',"Dst");
// Move disc 1 from Srcto Dst
// Move disc 2 from Srcto Aux
// Move disc 1 from Dstto Aux
// Move disc 3 from Srcto Dst
// Move disc 1 from Auxto Src
// Move disc 2 from Auxto Dst
如果一个函数返回自身递归调用的结果,那么调用的过程会被替换为一个循环,可以显著提高速度。
//构建一个带尾递归的函数。因为它会返回自身调用的结果
// 实现 factorial = n*(n-1)(n-2)... 1;
var factorial = function factorial(i,a) {
a = a || 1;
if(i<2) {
return a;
}
return factorial(i-1,a*i);
};
document.writeln(factorial(5)) // 120
作用域
作用域控制着变量与参数的可见性及生命周期。
-
函数作用域: 定义在函数中的参数和变量在函数外部是不可见的,而在一个函数内部任何位置定义的变量,在该函数内部任何地方都可见。
-
在JavaScript中缺少块计作用域,最好的做法是在函数体的顶部声明函数中可能用到的所有变量。
闭包
闭包:函数可以访问它被创建时所处的上下文环境。
即函数体内部的变量都可以保存在函数作用域内,看起来是函数将变量”包裹”起来了,所以这种特性称为”闭包”。
闭包特点:
- 让外部访问函数内部变量成为可能
- 局部变量会常驻在内存中
- 可以避免使用全局变量,防止全局变量污染
- 会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
看下面几个例子可以更好理解闭包:
// **闭包找到的是同一地址中父级函数中对应变量最终的值
var i = 0;
function outerFn(){
function innerFn(){
i++;
console.log(i);
}
return innerFn;
}
var inner = outerFn();
inner();
inner();
inner();
var inner2 = outerFn();
inner2();
inner2();
inner2(); //1 2 3 4 5 6
function outerFn(){
var i = 0;
function innerFn(){
i++;
console.log(i);
}
return innerFn;
}
var inner = outerFn(); //每次外部函数执行的时候,都会开辟一块内存空间,外部函数的地址不同,都会重新创建一个新的地址
inner();
inner();
inner();
var inner2 = outerFn();
inner2();
inner2();
inner2(); //1 2 3 1 2 3
另外内部函数可以访问外部函数的实际变量而无需复制。
回调
回调:
- 发起异步请求,提供一个服务器的响应到达时随即触发的回调函数,
- 异步函数立即返回。
request = prepare_the_request ();
send_request_asynchronously(request, function (response) {
display(response);
});
传递一个函数作为参数给 send_request_asynchronously 函数,一旦接收到响应,它就会被调用。
模块
使用函数和闭包来构造模块。 模块是一个提供接口却隐藏状态与实现的函数或对象。通过使用函数产生模块,我们几乎可以摈弃全局变量的使用,从而缓解其带来的影响。
模板的一般形式:
- 一个定义了私有变量和函数的函数
- 利用闭包创建可以访问私有变量和函数的特权函数
- 返回这个特权函数,或者把它们保存到一个可访问到的地方
var module = new Object({
m1 : function (){
//...
},
m2 : function (){
//...
}
});
module.m1();
模块写法可以把模块写成一个对象,所有的模块成员都放到这个对象里面。
上面的函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性。
级联
有一些方法没有返回值。例如,一些设置或修改对象的某个状态却不返回任何值的方法;
-
如果我们让这些方法返回 this 而不是 undefined, 就可以启用级联。
-
在一个级联中我们可以在单独一条语句中依次调用同一个对象的很多方法。
一个启用级联的Ajax 类库可能允许我们以这样的形式去编码:
getElement ('myBoxDiv')
.move(350, 150)
.width(100)
.height(100)
.color('red')
.border('10px outset')
.padding('4px')
.appendText("Please stand by")
.on('mousedown', function (m) {
this.startDrag(m, this.getNinth(m));
})
.on('mousemove', 'drag')
.on('mouseup', 'stopDrag')
.later(2000, function () {
this
.color('yellow')
.setHTML("what hath God wraught?")
.slide(400, 40, 200, 200);
})
.tip('This box is resizeable');
这里的getElement 函数产生一个对应于 id=“myBoxDiv” 的DOM元素并提供了其他功能的对象。该方法允许我们移动元素,修改它的尺寸和样式,并添加行为。这些方法每一个都返回该对象,所以调用返回的结果可以被下一次调用所用。
柯里化
柯里化允许我们把函数与传递给它的参数结合,产生出一个新的函数。也就是把一个多参数的函数转化为单参数函数的方法。
function add(a, b) {
return a + b;
};
console.log(add(1, 2)); // 3
柯里化后的函数:
function add(b) {
return function(a) {
return a + b;
};
};
console.log(add(1)(2)); // 3
curry 方法通过创建一个保存着原始函数和被要套用的参数的闭包来工作。它返回另一个函数,该函数被调用时,会返回调用原始函数的结果,并传递调用 curry 时的参数加上当前调用的参数。
记忆
记忆:函数可以将先前操作的结果记录在某个对象里,从而避免无谓的重复运算。
比如说,我们想要一个递归函数来计算 Fibonacci 数列。一个 Fibonacci 数字是之前两个 Fibonacci 数字之和。
var fibonacci = function (n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
};
for (var i = 0; i <= 10; i += 1) {
document.writeln('// ' + i + ': ' + fibonacci(i));
}
// 0: 0
// 1: 1
// 2: 1
// 3: 2
// 4: 3
// 5: 5
// 6: 8
// 7: 13
// 8: 21
// 9: 34
// 10: 55
这样的话,它做了很多无谓的工作。 Fibonacci 函数被调用了 453 次。我们调用了 11 次,而它自身调用了 442 次去计算可能已经被刚计算过的值。如果我们让该函数具备记忆功能,就可以显著地减少它的运算量。
我们在一个名为 memo 的数组里保存我们的储存结果,储存结果可以隐藏在闭包中。当我们的函数被调用时,这个函数首先看是否已经知道计算的结果,如果已经知道,就立即返回这个储存结果。
var fibonacci = function() {
var memo = [0, 1];
var fib = function (n) {
var result = memo[n];
if (typeof result !== 'number') {
result = fib(n - 1) + fib(n - 2);
memo[n] = result;
}
return result;
};
return fib;
}();
这个函数返回同样的结果,但是它只被调用了 29 次。我们调用了它 11 次,它自身调用了 18 次去取得之前储存的结果。
推而广之,编写一个函数来帮助我们构造带记忆功能的函数。
var memoizer = function(memo, formula) {
var recur = function(n) {
var result = memo[n];
if(typeof result !== "number") {
result = formula(recur, n);
memo[n] = result;
}
return result;
};
return recur;
};
var fibonacci = memoizer([0,1],function(recur,n) {
return recur(n-1) + recur(n-2);
});
fibonacci(9) // 34
记忆是一种编程技巧,本质上就是以空间换时间,以减少工作量。