3.1 闭包
闭包的形成与变量的作用域以及变量的生存周期密切相关。
3.1.1 变量的作用域
变量的作用域,就是指变量的有效范围,我们最常谈到的是在函数中声明的变量作用域。
当在函数中声明一个变量的时候,如果该变量前面没有带上关键字var,这个变量就会成为全局变量。
另外一种情况是用var声明变量,这时候的变量是局部变量,只有在该函数内部才能访问到这个变量,在函数外部是访问不到的。
在javascript中,函数可以来创造函数作用域。此时的函数就像是一层半透明的玻璃,在函数里面可以看到外面的变量,而在函数外面不能看到函数内部的变量。
这是因为当在函数中搜索一个变量时,如果在函数外面则无法看到函数里面的变量。这是因为当在函数中搜索一个变量的时候,如果函数内部并没有声明这个变量,那么此次搜索的过程会随代码执行环境创建的作用域链往外层逐层搜索,一直搜索到全局对象为止。
变量的搜索过程是从内到外而非从外到内。
var a=1;
var func1=function(){
var b=2;
var func2=function(){
var c=3;
alert(b);//输出2
alert(a);//输出1
}
func2();
alert(c);//c is not defined
}
func1();
3.1.2 变量的生存周期
全局变量的生存周期是永久的,局部变量函数退出之后就被销毁了。
var func=function(){
var a=1;
return function(){
a++;
alert(a);
}
}
var f=func();
f();//2
f();//3
f();//4
f();//5
跟之前的结论相反,当退出函数后,局部变量a并没有消失,而是似乎一直在某个地方存活着,这是因为当执行var f=func();时,f返回了一个匿名函数的引用,它可以访问到func()被调用时产生的环境,而局部变量a一直处在这个环境里。既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由。在这里产生了一个闭包结构,局部变量的生命看起来被延续了。
<div>1div>
<div>2div>
<div>3div>
<div>4div>
<div>5div>
<script type="text/javascript">
var nodes=document.getElementByTagName("div");
for(var i=0;ifunction(){
alert(i);
}
}
输出:5,5,5,5,5
有时候由于异步原因,onclick被触发时候,for已经结束,此时输出就是5,5,5,5,5,
解决方法就是在闭包的帮助下,把每次循环的i值都封闭起来。当在事件函数中顺着作用域链中从内到外查找变量i时,会先找到被封闭在闭包环境中的i值,如果有5个div,这里的i就分别是1,2,3,4,5
for(var i=0.len=nodes.length;ifunction(i){
nodes[i].onclick=function(){
console.log(i);
}
})(i)
};
输出,是:0,1,2,3,4,
关于异步:http://www.jb51.net/article/90877.htm
3.1.3闭包更多的作用
1.封装变量
闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。
var mult=function(){
var a=1;
for(var i=0,l=arguments.length;i<1;i++){
a=a*arguments[i];
}
return a;
}
mul函数接受一些number类型的参数,并返回这些参数的乘机。现在我们觉得对于那些相同的参数来说,每次都进行计算是一种浪费,我们可以加入缓存机制来提高这个函数的性能:
var mult=function(){
var cache={};
var args=Array.prototype.join.call(arguments,',');
if(cache[args]){
return cache[args];
}
var a=1;
for(var i=0,l=arguments.length;i<1;i++){
a=a*arguments[i];
}
return cache[args]=a;
}
alert(mult(1,2,3));//输出6
alert(mult(1,2,3));//输出6
我们看到cache这个变量仅仅在mult函数中函数中被使用,与其让cache变量跟mult函数一起平行地暴露在全局作用域之下,不如把它封闭在Mult函数内部,这样可以减少页面中的全局变量,以避免这个变量在其他地方被不小心修改而引发错误。
var mult=(function(){
var cache={};
return function(){
var args=Array.prototype.join.call(arguments,',');
if(args in cache){
return cache[args];
}
var a=1;
for(var i=0,l=arguments.length;i<1;i++){
a=a*arguments[i];
}
return cache[args]=a;
}
})();
alert(mult(1,2,3));
提炼函数是代码重构中的一个常见技巧。如果在一个大函数中有一些代码块能够独立出来,我们常常把这些代码块封装在独立的小函数里面。独立出来的小函数有助于代码复用,如果这些小函数有一个良好的命名,它们本身也起到了注释作用。如果这些小函数不需要再程序的其他地方使用,最好把它们用闭包封闭起来。代码如下:
var mult=(function(){
var cache={};
var calculate=function(){//封装calculate函数
var a=1;
for(var i=0,l=arguments.length;i<1;i++){
a=a*arguments[i];
}
return a;
}
return function(){
var args=Array.prototype.join.call(arguments,',');
if(args in cache){
return cache[args];
}
return cache[args]=calculate.apply(null,arguments);
}
})()
2.延续局部变量的寿命
img经常用于进行数据上报,如下所示:
var report=function(){
var img=new Image();
img.src=src;
}
report('http://xxx.com/getUserInfo');
但是通过查询后台的记录我们得知,因为一些低版本浏览器的实现存在Bug,在这些浏览器下使用report函数进行数据上报会丢失30%左右的数据,也就是说,report函数并不是每一次都成功的发起HTTP请求。丢失数据的原因是Img是report函数中的局部变量,当report函数的调用结束后,Img局部变量随机被销毁,而此时或许还没来得及发出HTTP请求,所以此次请求就会丢失掉。
var report=(function(){
var imgs=[];
return function(src){
var img=new Image();
imgs.push(img);
img.src=src;
}
})();
3.14 闭包和面向对象设计
过程和数据的结合是形容面向对象中的“对象”经常使用的表达。对象是以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据。通常用面向对象思想能实现的功能,用闭包也能实现。
跟闭包相关的代码:
var extent=function(){
var value=0;
return{
call:function(){
value++;
console.log(value);
}
}
}
var extent=extent();
extent.call();//1
extent.call();//2
extent.call();//3
对象(extent)即是函数,以及对象里面的函数(call)。
换成面向对象的写法:
var extent=function(){
this.value=0;
}
extent.prototype.call=function(){
this.value++;
console.log(this.value);
}
var extent=new extent();
extent.call();//1
extent.call();//2
extent.call();//3
<button id="execute">点我执行命令button>
<button id="undo">点我执行命令button>
<script type="text/javascript">
var Tv={
open:function(){
console.log("打开电视");
},
close:function(){
console.log("关上电视");
}
}
var OpenTvCommand=function(receiver){
this.receiver=receiver;//执行命令,打开电视机
}
OpenTvCommand.prototype.execute=function(){
this.receiver.open();//撤销命令,关闭电视机
}
OpenTvCommand.prototype.undo=function(){
this.receiver.close();//撤销命令,关闭电视机
}
var setCommand=function(command){
document.getElementById("execute").onclick=function(){
command.execute();//输出:打开电视机
}
document.getElementById("undo").onclick=function(){
command.undo();//输出:关闭电视机
}
}
setCommand(new OpenTvCommand(Tv));
script>
命令模式的意图是把请求封装成对象,从而分离请求的发起者和请求的接受者(执行者)之间的耦合关系。在命令被执行之前,可以预先往命令对象中植入命令的接受者。
但是在javascript中,函数作为一等对象,本身就可以四处传递,用函数对象而不是普通对象来封装请求显得更加简单和自然。如果需要往函数对象中预先植入命令的接收者,那么闭包可以完成这个工作。
在面向对象版本的命令模式中,预先植入的命令接受者被当成对象的属性保存起来;而在闭包版本的命令模式中,命令接收者会被封闭在闭包形成的环境中,代码如下:
<button id="execute">点我执行命令button>
<button id="undo">点我执行命令button>
<script type="text/javascript">
var Tv={
open:function(){
console.log("打开电视");
},
close:function(){
console.log("关上电视");
}
}
var createCommand=function(receiver){
var execute=function(){
return receiver.open();//执行命令,打开电视机
}
var undo=function(){
return receiver.close();//执行命令,关闭电视机
}
return{
execute:execute,
undo:undo
}
}
var setCommand=function(command){
document.getElementById("execute").onclick=function(){
command.execute();//输出:打开电视机
}
document.getElementById("undo").onclick=function(){
command.undo();//输出:关闭电视机
}
}
setCommand(createCommand(Tv));
script>
3.1.6 闭包和内存管理
局部变量本来应该再函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量会一直生存下去,从这个意义上看,闭包确实会使一些数据无法被及时销毁。
使用闭包的原因是我们选择主动把一些变量封闭在闭包里,因为可能以后还需要使用这些变量,把这些变量放在闭包和全局作用域,对内存方面的影响是一致的,这里并不能说成内存泄漏,如果在将来需要回收这些变量,我们可以手动把这些变量设置为Null。
更闭包和内存泄漏有关的地方是,使用闭包的同时可能形成循环引用,如果闭包的作用链中保存着一些DOM节点,这时候就可能造成内存泄漏。同样,如果要解决循环带来的内存泄漏的问题,我们只要把循环引用中的变量设为null即可,将变量设置为Null意味着切断变量预与它之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
3.2 高阶函数
高阶函数是指至少满足下列条件之一的函数。
(1)函数可以作为参数被传递
(2)函数可以作为返回值输出
3.2.1 函数作为参数传递
把函数当做参数传递,这代表我们可以抽离出一部分容易变化的业务逻辑放在函数参数中,这样一来可以分离业务代码中变化与不变的部分。其中一个重要应用场景就是常见的回调函数。
1.回调函数
在ajax异步请求的应用中,回调函数的使用非常频繁。当我们想在ajax请求返回之后做一些事情,但又不知道请求返回的确切时间,最常见的方案就是把callback函数当做参数出入发出ajax请求的方法中,待请求完成之后执行callback函数。
var getUserInfo=function(userId,callback){
$.ajax('http://xxx.com/getUserInfo?'+userId,function(data){
if(typeof callback==='function'){
callback(data);
}
})
}
getUserInfo(13157,function(data){
alert(data.userName);
});
回调函数的应用不仅仅只在异步请求中,当一个函数不适合执行一些请求时,我们也可以把这些请求封装成一个函数,并把它作为参数传递给另外一个函数,“委托”给另外一个函数来执行。
比如,我们想在页面中创建100个div节点,然后把这些div节点都设置为隐藏。下面是一种编写代码的方式:
<script type="text/javascript">
var appendDiv=function(){
for(var i=0;i<100;i++){
var div=document.createElement('div');
div.innerHTML=i;
document.body.appendChild(div);
div.style.display='none';
}
}
appendDiv();
script>
这里的函数难以复用,并不是每个人创建了节点之后就希望它们立刻被隐藏。于是我们把div.style.display=’none’这行代码抽出来,用回调函数的形式传入appendDiv方法:
<script type="text/javascript">
var appendDiv=function(callback){
for(var i=0;i<100;i++){
var div=document.createElement('div');
div.innerHTML=i;
document.body.appendChild(div);
if(typeof callback==='function'){
callback(div);
}
}
}
appendDiv(function(node){
node.style.display='none';
})
script>
可以看到,隐藏节点的请求实际上是由客户发起的,但是客户并不知道节点什么时候会创建好,于是把隐藏节点的逻辑放在回调函数中,委托给appendDiv方法。appendDiv方法当然知道节点什么时候创建好,所以在节点创建好的时候,appendDiv会执行之前客户传入的回调函数。
2.Array.prototype.sort
Array.prototype.sort接受一个函数当做参数,这个函数里面封装了数组元素的排序规则。从Array.prototype.sort的使用可以看到,我们的目的是对数组进行排序,这是不变的部分;从而使用什么规则去排序,这是可变的部分。把可变的部分封装在函数参数中,动态传Array.prototype.sort,使Array.prototype.sort方法成为了一个非常灵活的方法,代码如下:
<script type="text/javascript">
[1,4,5].sort(function(a,b){
return a-b;
})
script>//输出1,4,5
3.2.2 函数作为返回值输出
相比把函数当做参数传递,函数当作返回值输出的场景更多。让函数返回一个可执行的函数,意味着运算过程是可延续的。
1.判断数据的类型
判断一个数据是否数组,在以往的实现中,可以基于鸭子类型的概念来看,比如这个数据有没有length属性,有没有sort方法或者slice方法等。
但是更好的方式使用Object.prototype.toString.call([1,2,3])来计算。Object.prototype.toString.call(obj)返回一个字符串,比如Object.prototype.toString.call(“str”)总是会返回“[object String]”
所以我们可以编写一系列的isType函数:
<script type="text/javascript">
var isString=function(obj){
return Object.prototype.toString.call(obj)==='[object string]';
}
var isArray=function(obj){
return Object.prototype.toString.call(obj)==='[object Array]';
}
var isNumber=function(obj){
return Object.prototype.toString.call(obj)==='[object Number]';
}
script>