1.闭包
看本篇文章时可以先了解一下前面的一片文章<JavaScript 高级特性 作用域详解>
闭包是JavaScript的一大谜团,关于这个问题有很多文章进行讲述,然而依然有相当数量的程序员对这个概念理解不透彻。闭包的官方定义为:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
一句话概括就是:闭包就是一个函数,捕获作用域内的外部绑定。这些绑定是为之后使用而被绑定,即使作用域已经销毁。
自由变量
自由变量与闭包的关系是,自由变量闭合于闭包的创建。闭包背后的逻辑是,如果一个函数内部有其他函数,那么这些内部函数可以访问在这个外部函数中声明的变量(这些变量就称之为自由变量)。然而,这些变量可以被内部函数捕获,从高阶函数(返回另一个函数的函数称为高阶函数)中return语句实现“越狱”,以供以后使用。内部函数在没有任何局部声明之前(既不是被传入,也不是局部声明)使用的变量就是被捕获的变量。实例如下:
function makeAdder(captured) {
return function(free) {
var ret = free + captured;
console.log(ret);
}
}
var add10 = makeAdder(10);
add10(2); // 输出:12
从上例可知,外部函数中的变量captured被执行加法的返回函数捕获,内部函数从未声明过captured变量,却可以引用它。
如果我们再创建一个加法器将捕获到同名变量captured,但有不同的值,因为这个加法器是在调用makeAdder之后被创建:
var add16 = makeAdder(16); add16(18); // 输出:34 add10(10); // 输出:20
如上述代码所示,每一个新的加法器函数都保留了自己创建时捕获的captured实例。
2.变量遮蔽
在JavaScript中,当变量在一定作用域内声明,然后在另一个同名变量在一个较低的作用域声明,会发生变量的遮蔽。实例如下:
var name = "jeri";
var name = "tom";
function glbShadow() {
var name = "fun";
console.log(name); // 输出:fun
}
glbShadow();
console.log(name); // 输出:tom
当在一个变量同一作用域内声明了多次时,最后一次声明会生效,会遮蔽以前的声明。
变量声明的遮蔽很好理解,然而函数参数的遮蔽就略显复杂。例如:
var shadowed = 0;
function argShadow(shadowed) {
var str = ["Value is",shadowed].join(" ");
console.log(str);
}
argShadow(108); // output:Value is 108
argShadow(); // output:Value is
函数argShadow的参数shadowed覆盖了全局作用域内的同名变量。即使没有传递任何参数,仍然绑定的是shadowed,并没有访问到全局变量shadowed = 0
任何情况下,离得最近的变量绑定优先级最高。实例如下:
var shadowed = 0;
function varShadow(shadowed) {
var shadowed = 123;
var str = ["Value is",shadowed].join(" ");
console.log(str);
}
varShadow(108); // output:Value is 123
varShadow(); // output:Value is 123
//varShadow(108)打印出来的并不是108而是123,即使没有参数传入也是打印的123,先访问离得最近的变量绑定。
遮蔽变量同样发生在闭包内部,实例如下:
function captureShadow(shadowed) {
console.log(shadowed); // output:108
return function(shadowed) {
console.log(shadowed); // output:2
var ret = shadowed + 1;
console.log(ret); // output:3
}
}
var closureShadow = captureShadow(108);
closureShadow(2);
在编写JavaScript代码时,因为变量遮蔽会使很多变量绑定超出我们的控制,我们应尽量避免变量遮蔽,一定要注意变量命名。
3.典型误区
下面是一个非常典型的问题,曾经困扰了很多人,下面也来探讨下。
var test = function() {
var ret = [];
for(var i = 0; i < 5; i++) {
ret[i] = function() {
return i;
}
}
return ret;
};
var test0 = test()[0]();
console.log(test0); // 输出:5
var test1 = test()[1]();
console.log(test1); //输出:5
/*
从上面的例子可知,test这个函数执行之后返回一个函数数组,
表面上看数组内的每个函数都应该返回自己的索引值,然而并不是如此。
当外部函数执行完毕后,外部函数虽然其执行环境已经销毁,
但闭包依然保留着对其中变量绑定的引用,仍然驻留在内存之中。
当外部函数执行完毕之后,才会执行内部函数,
而这时内部函数捕获的变量绑定已经是外部函数执行之后的最终变量值了,
所以这些函数都引用的是同一个变量i=5。
下面有个更优雅的例子来表述这个问题:
*/
for(var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 每隔1秒输出一个5
/*按照我们的推断,上例应该输出1,2,3,4,5。
然而,事实上输出的是连续5个5。
为什么出现这种诡异的状况呢?其本质上还是由闭包特性造成的,闭包可以捕获外部作用域的变量绑定。
上面这个函数片段在执行时,其内部函数和外部函数并不是同步执行的,
因为当调用setTimeout时会有一个延时事件排入队列,
等所有同步代码执行完毕后,再依次执行队列中的延时事件,而这个时候 i 已经 是5了。
那怎么解决这个问题呢?我们是不是可以在每个循环执行时,
给内部函数传进一个变量的拷贝,使其在每次创建闭包时,都捕获一个变量绑定。
因为我们每次传参不同,那么每次捕获的变量绑定也是不同的,也就避免了最后输出5个5的状况。实例如下:*/
for(var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
// 输出:0,1,2,3,4
闭包具有非常强大的功能,函数内部可以引用外部的参数和变量,但其
参数和变量不会被垃圾回收机制回,常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。但,闭包也是
javascript
语言的一大特点,主要应用闭包场合为:设计私有的方法和变量。
4.模拟私有变量
从上文的叙述我们知道,变量的捕获发生在创建闭包的时候,那么我们可以把闭包捕获到的变量作为私有变量。实例如下:
var closureDemo = (function() {
var PRIVATE = 0;
return {
inc:function(n) {
return PRIVATE += n;
},
dec:function(n) {
return PRIVATE -= n;
}
};
})();
var testInc = closureDemo.inc(10);
//console.log(testInc);
// 输出:10
var testDec = closureDemo.dec(7);
//console.log(testDec);
// 输出:3
closureDemo.div = function(n) {
return PRIVATE / n;
};
var testDiv = closureDemo.div(3);
console.log(testDiv);
//输出:Uncaught ReferenceError: PRIVATE is not defined
自执行函数closureDemo执行完毕之后,自执行函数作用域和PRIVATE随之销毁,但PRIVATE仍滞留在内存中,也就是加入到closureDemo.inc和closureDemo.dec的作用域链中,闭包也就完成了变量的捕获。但之后新加入的closureDemo.div并不能在作用域中继续寻找到PRIVATE了。因为,函数只有被调用时才会执行函数里面的代码,变量的捕获也只发生在创建闭包时,所以之后新加入的div方法并不能捕获PRIVATE。
5.创建特权方法
通过闭包我们可以创建私有作用域,那么也就可以创建私有变量和私有函数。创建私有函数的方式和声明私有变量方法一致,只要在函数内部声明函数就可以了。当然,既然可以模拟私有变量和私有函数,我们也可以利用闭包这个特性,创建特权方法。实例如下:
(function() {
// 私有变量和私有函数
var privateVar = 10;
function privateFun() {
return false;
};
// 构造函数
MyObj = function() {
};
// 公有/特权方法
MyObj.prototype.publicMethod = function() {
privateVar ++;
return privateFun();
}
})();
上面这个实例创建了一个私有作用域,并封装了一个构造函数和对应的方法。需要注意的是在上面的实例中,在声明MyObj这个函数时,使用的是不带var的函数表达式,我们希望产生的是一个全局函数而不是局部的,不然我们依然在外部无法访问。所以,MyObj就成为了一个全局变量,能够在外部进行访问,我们在原型上定义的方法publicMethod也就可以使用,通过这个方法我们也就可以访问私有函数和私有变量了。
总的来说,因为闭包奇特的特性,可以通过它实现一些强大的功能。但,我们在日常编程中,也要正确的使用闭包,要时刻注意回收不用的变量,避免内存泄露。
本文详细探讨了闭包的生成机理和一些常见错误理解,最后对闭包进行了应用举例。但限于个人水平有限,但有疏漏之处还望见谅。欢迎交流!
数据冰冷的,但我们要让数据温暖起来,改变我们的生活!可以关注我的订阅号,或者加微信
微信订阅号
微信号