之前我们学习了高阶函数,高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回
引入
通常情况下,针对array求和的函数是这样定义的
function sum(arr) {
return arr.reduce(function (x, y) {
return x + y;
});
}
sum([1, 2, 3, 4, 5]); // 15
但是,如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?
那么我们也可以不返回求和的结果,而是返回求和的函数
function lazy_sum(arr) {
var sum = function () {
return arr.reduce(function (x, y) {
return x + y;
});
}
return sum;
}
var f=lazy_sum([1,2,3,4,5])
f;//function sum()
f();//15
当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数,调用函数f时,才真正计算求和的结果
注意一点,当我们调用lazy_sum()时,即使传入相同的参数,每次调用都会返回一个新的函数
var f1 = lazy_sum([1, 2, 3, 4, 5]);
var f2 = lazy_sum([1, 2, 3, 4, 5]);
f1 === f2; // false
看上去两个一摸一样,为什么不同呢?不着急,我们马上分析原因
总结:在这个例子中,我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局 部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这就称为闭包
为什么要返回函数
你可能会疑惑,为什么要这样设计,为什么不直接返回函数结果呢?
因为我们计算的时候一般只需要最终的结果,返回一个函数的话我们还要再调用函数才是最终的结果,这不是画蛇添足吗?
不,千万别这么想
我是这么理解的(如果有不对的地方,还希望大佬们指出,仅代表我个人的理解)
利用我们返回函数,可以实现类似c程static的效果,也就是保存值的效果
比如说我们要设计一个持续加1的函数
function create_counter(){
var x=0;
x=x+1;
return x;
};
create_counter();//1
create_counter();//1
大家有过语言基础的,应该都可以理解,因为每次调用函数的时候,var x=0;都会被重新执行一次,根本不会保存之前的记录,所以c有了static这个关键字
利用返回函数,我们可以实现类似static的效果
function create_counter(){
var x=0;
function add(){return x=x+1;}
return add;
};
var f=create_counter();
f();//1
f();//2
为什么会有这个效果(也是我的理解)
我们都知道,调用函数会把函数初始化部分再执行一遍,而我们这里函数初始化部分全部在create_counter里面,那么根据我们这里的情况,其实我们只调用了一次create_counter();就是var f赋值的语句,在这个地方x被定义了,并且赋值为0,之后就没有被调用了,被调用的只是里面的内部函数add,所以x不会再次被初始化,那么之后每次调用f的时候相当于就完成了x的累加,而由于x变量仍然还在使用(没有被回收),x值也没有被初始化,所以x可以获得类似static的效果
有了上面的基础,我们也可以订制一款累加器
function create_counter(initial){
var x=initial || 0;
function add(){return x=x+1;}
return add;
};
var f=create_counter();
typeof f;//"function"
f();//1
f();//2
var f=create_counter(11);
f();//12
f();//13
传入initial初始化,当然也可以不初始化,如果不传入参数,直接调用,那么initial就会对应undefined,因为undefined是false,所以x就会被赋值为 0,如果传入了数,因为0又对英的是false,然后就会被赋值成intial(具体的机理我不太清楚,.也希望有人可以分享一下)
当然还有下面这种写法
function create_counter(initial) {
var x = initial || 0;
return {
inc: function () { return x=x+1;; }
}
}
var f=create_counter()
typeof f;//"object"
f.inc();//1
f.inc();//2
这个除了用法上有细微的差别…别的地方都是一样的
s为什么会出现这样的区别呢,我对比了这两种用法,区分了一下两个create_counter的返回类型,第一个返回值是function,所以可以直接调用,(跟一般的函数没什么区别)
而这里return返回的是一个对象,因为我们的写法return {}
,标准的对象写法,然后这个对象里面唯一的方法就是inc,(也是对象里面的标准写法),所以我们拿到返回的对象f后,就可以用对象使用方法的格式,f.inc进行调用方法(行为)来得到结果
因为我们返回的结果是函数,那么这个函数也可以带参,利用这一点,我们可以大大提高代码可操作性
计算x的y次方可以用Math.pow(x, y)函数,不过考虑到经常计算 x2或x3,我们可以利用闭包创建新的函数pow2和pow3
function make_pow(n) {
return function (x) { return Math.pow(x, n); }
}
var pow2 = make_pow(2);
var pow3 = make_pow(3);
pow2(5); // 25
pow3(7); // 343
异步处理的问题
上面我也提到了,返回的函数并没有立刻执行,而是直到调用了f()才执行,那么这就会产生一个异步的问题
我们来看这样的一个函数
function count() {
var arr = [];
for (var i=1; i<=3; i++)
{
arr.push(function () { return i * i; });
}
return arr;
}
这个函数想要实现的就是往arr里追加1,4,9,然后返回arr
那我们看看实际效果如何
var results = count();
results
Array(3) [ count(), count(), count() ]
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
f1()
16
f2()
16
f3()
16
这部分其实不是很好理解,刚才我们上面的函数,最外层的函数return sum确实返回的是一个函数
但是这里我们可以看到,return arr,返回的是一个数组不是函数呀?那为什么results里面还有3个函数
emm这个地方就要细细看啦,我们可以看到arr.push(function),平常push都是往arr里追加数,但这里追加的是函数,所以results里面返回了3个函数也不奇怪啦
所以之后我们f1,f2,f3分别获取results里面的3个元素,获取到的是函数也不奇怪,想获得结果还需要调用函数
但另一个问题也出现了,我们希望的时候每一次循环获得11,22,33为什么这里直接返回了44
原因就在于返回的函数引用了变量i,但它并非立刻执行,所以当我们捕获results的时候,我们没有立即执行f1(),f2(),f3(),也就是i=1,i=2,i=3的时候没有执行内部的ii,当等到我们最后调用函数执行ii的时候,这个时候i已经变成了4,,因此最终结果为16
上面的部分可能比较难理解,我也是…弄了很长时间才弄懂,确实有点怪怪的,但我们还是要理解
通过上面这个例子我们要懂得的一点
返回闭包时牢记:返回函数不要引用任何循环变量,或者后续会发生变化的变量
这个也涉及到了javascript的单线程处理,这点比较详细的讲解会在jQuery里面事件处理上,那里会涉及到这种异步处理,这里我们大概理解一下就好
如果一定要引用循环变量怎么办?
方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变,这样写也叫匿名函数并立刻执行
“创建一个匿名函数并立刻执行”的语法
(function (x) { return x * x; })(3); // 9
括号里的3就是我们传入的x的值
理论上讲,创建一个匿名函数并立刻执行可以这么写
function (x) { return x * x } (3);
但是由于JavaScript语法解析的问题,会报SyntaxError错误,因此需要用括号把整个函数定义(不包括后面的3)括起来
通常,一个立即执行的匿名函数可以把函数体拆开,一般这么写
(function (x) {
return x * x;
})(3);
其实没啥差别,只是…好看一点…
s那么上面的函数我们就可以修改成下面这样啦
function count() {
var arr = [];
for (var i=1; i<=3; i++)
{
arr.push((function (n)
{
return function ()
{ return n * n; } })(i));
}
return arr;
}
上面这种写法可能看起来很奇怪…为什么不这样写呢
function count() {
var arr = [];
for (var i=1; i<=3; i++) {
arr.push(
(function (n)
{
return n * n;
})(i)
);
}
return arr;
}
那么我们先用下面这种做测试吧
var results = count();
results;//Array(3) [ 1, 4, 9 ]
怎么返回的是值,不是函数了呢
刚才也说过,这个叫匿名函数并立刻执行,你可以理解为直接就返回了函数值,直接的那种function() {return i*i;}
是普通函数,普通函数都需要调用才能执行,而这种匿名函数并立刻执行就不需要调用,就直接执行了,所以也不涉及异步的问题了
如果我们还是希望返回的是一个函数而不是结果,就只能用第一种写法啦
function count() {
var arr = [];
for (var i=1; i<=3; i++)
{
arr.push(
(function (n)
{
return function ()
{ return n * n; } }
)(i)
);
}
return arr;
}
var results=count();
var f1=results[0]
var f2=results[1]
var f3=results[2]
f1();//1
f2();//4
f3();//9
还有一个问题要注意,就是这个匿名函数并立刻执行的位置
因为注意到,我们刚才在写内部函数的时候,是这样的
(function (n)
{
return function ()
{ return n * n; } }
)(i)
当时我想,为什么不能这样写呢
function ()
{
return (function (n)
{ return n * n; })(i)
}
表面上看上去,这只是一个内外的差别,但我后来试了试,发现第二种结果全变成了16,这个该怎么解释呢
这里还是要分析我们为什么要设计这个匿名函数并立刻执行,我们需要锁定i的值,而不是需要立即算出结果
第一种写法,他相当于是锁定了i的值,我们调用的时候只是为了计算结果
第二种写法,它相当于只是起到了个立刻执行的效果,但没有锁定i,最后由于异步的原因,导致i还是用4计算的
这点需要好好理解