闭包的形成与变量的作用域以及变量的生存周期息息相关
变量的作用域
变量的作用域就是指变量的有效范围。我们最长谈到的是在函数中声明的变量作用域。
当在函数中声明一个变量的时候,如果该变量前面没有带上关键字var,这个变量就会成为全局变量,这当然是一个容易造成命名冲突的做法。
另外一种情况就是用var关键字在函数中声明变量,这时候的变量就是局部变量,只有在该函数内部才能访问到这个变量,在函数外是不能访问到的
var func = function(){
var a=1;
alert(a)//输出1
}
func();
alert(a)//输出a is not defined
在javascript中,函数可以用来创造函数作用域,此时的函数像一层半透明的玻璃,在函数里面可以看到外面的变量,而在函数外面则无法看到函数里面的变量,这是因为当在函数中搜索一个变量的时候,如果该函数内并没有声明这个变量,那么此次搜索的过程会随着代码执行环境创建的作用域链往外层逐层搜索,一直搜索到全局对象为止,变量的搜索是由内而外而非从外到内的
var a=1;
var func1=function(){
var b=2;
var fun2 = function(){
var c=3;
alert(b);//输出2
alert(a)//输出1
}
func2();
alert(c)//输出 c is not defined
}
func1();
变量的生存周期
除了变量的作用域之外,另外一个跟闭包有关的概念就是变量的生存周期
对于全局变量来说,全局变量的生存周期是永久的,除非我们主动销毁这个全局变量。
而对于在函数内用var关键字声明的局部变量来说,当退出函数的时候,这些局部变量几失去了他们的价值,他们会随着函数的调用结束而销毁
var func = function(){
var a=1; //退出函数后局部变量a将被销毁
alert(a)
}
现在来看看下面这段代码
var func = function(){
var a=1;
return function (){
a++;
alert(a)
}
}
var f=func();
f();//输出2
f();//输出3
f();//输出4
跟我们之前的推论相反,当退出函数后,局部变量a并没有消失,而是似乎一直在某个地方存活着,这是因为当执行var f = func();f返回了一个匿名函数的引用,它可以访问到func()被调用时产生的环境,而局部变量a一直处在这个环境里,既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由,在这里产生了一个闭包结构,局部变量的生命看起来被延续了
利用闭包我们可以完成许多奇妙的工作,下面介绍一个闭包的经典应用,假设页面上有5个div节点,我们通过循环来给每个div绑定click事件,按照索引顺序,点击第一个div时弹出0,点击第二个弹出1,以此类推
<html>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</body>
<script>
var nodes=document.getElementByTagName('div');
for(var i=0,len=node.length;i<len;i++){
nodes[i].οnclick=function(){
alert(i)
}
}
</script>
</html>
测试这段代码就会发现,无论点击哪个div 最后弹出的结果都是5。这是因为div节点的onclick事件是被异步触发的,当事件触发的时候,for循环早已经结束,此时变量i的值已经是5,所以在div 的onclick 事件函数中顺着作用域链从内到外查找变量i时,查找到的值总是5
解决办法是在闭包的帮助下,把每次循环的i值都封存起来,当事件函数中顺着作用域链中从内到外查找变量i时,会先找到封存在闭包环境中的i,如果有5个div,这里面的i就分别是0,1,2,3,4
for (var i=0,len=nodes.length;i<len;i++){
(function(i){
nodes[i].οnclick=function(){
console.log(i)
}
})(i)
}
根据同样的道理,我们还可以编写如下一段代码
var Type = {};
for (var i = 0, type; type = ['String', 'Array', 'Number'][i++];) {
console.log(type);
(function (type) {
Type['is' + type] = function (obj) { return Object.prototype.toString.call(obj) === '[object ' + type + ']'
}
})(type) }
console.log(Type.isArray([])) true
console.log(Type.isString('asas')) true
闭包的更多作用
1.封装变量
闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”,假如有一个计算乘积的简单函数
var mult = function(){
var a=1;
for(var i=0,l=arguments.length;i<l;i++){
a=a*arguments[i];
}
return a
}
mult 函数接受一些number类型的参数,并返回这些参数的乘积,现在我们觉得对于那些相同的参数来说,每次都进行计算是一种浪费,我们可以加入缓存机制来提高这个函数的性能
var cache ={};
var mult = function(){
var args = Array.prototype.join.call(arguments,',');
if(cache[args]){
return cache[args]
}
var a=1;
for(var i=0,l=arguments.length;i<l;i++){
a=a*arguments[i];
}
return cache[args]=a;
}
console.log(mult(1,2,3));//输出6
console.log(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<l;i++){
a=a*arguments[i]
}
return cache[ args]=a;
}
})()
提炼函数是代码重构中的一种常见技巧。如果在一个大函数中有一些代码块能够独立出来,我们常常把这些代码块封装在独立的小函数里面,独立出来的小函数有助于代码复用,如果这些小函数有一个良好的命名,他们本身也起到了注释的作用,如果这些小函数不需要在程序的其他地方使用,最好是把他们用闭包封存起来
var mult=(function(){
var cache={};
var calculate=function(){//封闭calculate 函数
var a=1;
for(var i=0,l=arguments.length;i<l;i++){
a=a*arguments[i];
}
return a
};
return function(){
var argus=Array.prototype.join.call(arguments,',');
if(args in cache){
return cache[args];
}
return cache[args]=calculate.apply(null,arguments);
}
})()
2、延续局部变量的寿命
img对象经常用于进行数据上报
var report = function(src){
var img =new Image();
img.src=src
}
report('http://www.baidu.com');
但是通过查询后台的记录我们得知,因为一些低版本浏览器的实现存在bug,在这些浏览器下使用report函数进行数据上报会丢失30%左右的数据,也就是说,report函数并不是每一次都成功发起了http请求,丢失数据的原因是img是report函数中的局部变量,当report函数调用结束后,img局部变量随即被销毁,而此时或许还没来得及发出http请求,所以此次请求就会丢失掉
现在我们把img变量用闭包封装起来,便能解决请求丢失的问题
var report = (function(){
var imgs = [];
return function(src){
var img = new Image();
imgs.push(img);
img.src=src
}
})()
闭包和面向对象设计
过程和数据的结合是形容面向对象中的“对象”时经常使用的表达,对象以方法的形式包含了过程,而闭包则是在过程中以环境的形成包含了数据,通常用面向对象思想能实现的功能,用闭包也能实现,反之亦然。
var extent = function(){
debugger
var value=0;
return {
call:function(){
value++;
console.log(value)
}
}
}
var extent =extent();
extent.call()//输出1
extent.call()//输出2
extent.call()//输出3
如果换成面向对象的写法
var extent ={
value:0,
call:function(){
this.value++;
console.log(this.value)
}
}
extent.call()//输出1
extent.call()//输出2
extent.call()//输出3
或者
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
3、用闭包实现命令模式
在JavaScript版本的各种设计模式实现中,闭包的运用非常广泛
在完成闭包实现的命令模式之前,我们先用面向对象的方式来编写一段命令模式代码,虽然还没有进入设计模式的学习,但是这个作为哦演示作用的命令模式结构非常简单
<html>
<body>
<button id="execute">点击我执行命令</button>
<button id="undo">点击我执行命令</button>
<script>
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){
docuemnt.getElementById('execute').οnclick=function(){
command.execute()//输出:打开电视机
}
document.getElementById('undo').οnclick=function(){
command.undo()//输出:关闭电视机
}
}
setCommand(new OpenTvCommand(Tv));
</script>
</body>
</html>
命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之间的耦合关系,在命令被执行之前,可以预先往命令对象中植入命令的接收者
但在javascript中,函数作为一等对象,本身就可以四处传递,用函数对象而不是普通对象来封装请求显得更加简单自然,如果需要往函数对象中预先植入命令的接收者,那么闭包可以完成这个工作,在面向对象版本的命令模式中,预先植入的命令接收者被当成对象的属性保存起来,而在闭包版本的命令模式中,命令接收者会被封存在闭包形成的环境中,
2、高阶函数
高阶函数是指至少满足下列条件之一的函数。
函数可以作为参数被传递
函数可以作为返回值输出
高阶函数实现AOP
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制,异常处理等。把这些功能抽离出来之后再通过动态织入的方式掺入业务逻辑模块中,这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次可以很方便的复用日志统计等功能模块
通常在js中实现AOP,都是把一个函数 动态织入到另一个函数之中,具体的实现技术有很多,本节我们通过扩展Function.prototype 来做到这一点
Function.prototype.before = function (beforefn){
var _self =this;//保留原函数的引用
return function(){ //返回包含了原函数和新函数的 代理 函数
beforefn.apply(this,arguments);//执行新函数,修正this
return _self.apply(this,arguments);//执行原函数
}
}
Function.prototype.after = function(afterfn){
var _self = this;
return function (){
var ret = _self.apply(this,arguments);
afterfn.apply(this,arguments);
return ret
}
}
var func =function(){
console.log(1)
}
func = func.before(function(){
console.log(1);
}).after(function(){
console.log(3)
})
func();
我们把负责打印数字1和打印数字3的两个函数通过AOP的方式动态植入func函数。通过执行上面的代码,输出1,2,3
这种使用AOP的方式来给函数添加职责,也是js语言中一种非常特别和巧妙的装饰者模式实现,这种装饰着模式在实际开发中非常有用