javascript学习笔记(12)--闭包

之前我们学习了高阶函数,高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回

引入

通常情况下,针对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计算的
这点需要好好理解

猜你喜欢

转载自blog.csdn.net/azraelxuemo/article/details/106886229
今日推荐