在复习js基础的时候,闭包是个回避不了的知识点。在MDN里面对闭包对解释是一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑到一起,这样的组合就是闭包(closure)
。也就是说闭包的作用其实是在一个内层函数中可以访问其外层函数的作用域。在js的里面每创建一个函数,同时闭包也就被创建出来。所以js里面每个函数都是一个闭包。
为什么js支持闭包
我们可以从执行上下文中去解释为什么js支持闭包。比如下面的这个函数:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope()();
这个函数的执行顺序我们可以在深入基础:js的上下文是什么中找到。当执行到f函数的时候外层的checkscopeContext已经被销毁了,但是在f的作用域链中维护了一个作用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
实际上scope的值仍然可以访问到,为"local scope"。所以js实现闭包的根本在于维护了这个作用域链。
经典的闭包面试题
我们可以用这个执行上下文来分析这个经典的面试题
let data = []
for(var i =0;i<3;i++){
data[i] = function (){
console.log(i)
}
}
data[0]()
data[1]()
data[2]()
这段代码大家都知道结果3,3,3
,我们来分析一下这段代码执行的过程:
<1>. 在执行完循环函数的时候,全局上下文为:
globalContext = {
VO:{
data:[...],
i:3
}
}
<2>. 当执行到data[0]
的时候,这个函数的作用域链为:
context = [AO,globalContext.VO]
这个时候的i值只能找到globaContext里面,所以很明显得到结果为3,后面类似。
怎么去解决问题呢?有几种方法如下:
- 经典解法,使用匿名闭包:
let data = []
for(var i =0;i<3;i++){
(data[i] = function (){
console.log(i)
})(i)
}
data[0]()
data[1]()
data[2]()
这个时候为什么能解决问题呢?答案就在我们添加的匿名函数上面,通过添加匿名函数,我们把i值绑定到匿名函数的变量对象上面,然后通过作用域链传给data[0]/data[1]/data[3]
。
匿名函数context={
AO={
arguments:{
0:0,
length:1
}
i:0
}
}
data[0]Context = [AO,匿名函数context.AO,globalContext.VO]
- 使用es6的let语法。
let data = []
for(let i =0;i<3;i++){
data[i] = function (){
console.log(i)
}
}
data[0]()
data[1]()
data[2]()
这样做的原理是let拥有块级作用域,在每次循环的时候产生一个块级作用域,每次循环都相当于存储链一个新的变量,所以在执行到对应的函数的时候,其上下文如下:
data[0]Context = [AO,iContext{i:0}(第1次循环时候产生的作用域),globalContext.VO]
用闭包模拟私有方法
js中不支持将方法定义为私有,但是利用js的闭包,也就是保存上下文的能力,我们可以模拟出私有方法。
//MDN中闭包模拟私有方法的示例
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
在上面这个例子中,通过闭包,将定义在匿名函数的私有方法通过返回的公共方法暴露出来。同时还有个好处,每次执行makeCounter的时候都是一个单独的闭包,这样生产的计数器相互之间互不影响。这样的特性有利于实现数据隐藏和封装。
闭包的性能问题
经常性的我们会听到js闭包会引发内存泄漏,但是实际上这句话是不准确的,js里面每个函数都是闭包,不可能每个函数都会内存泄漏。我们说的闭包性能问题实际上是每次创建闭包的时候,都会保存额外的变量,所以不合理的使用闭包的话会引发性能问题。
我们看下面的这段代码:
function setFirstName(firstName){
return function(lastName){
return firstName+" "+lastName;
}
}
var setLastName = setFirstName("kuitos");
var name = setLastName("lau");
使用setLastName的时候会有一个匿名函数,我们使用这个匿名函数获得了name变量的值,但是在执行完代码之后,setlastName的执行上下文已经销毁了,但是匿名函数的中存在的变量对象一直保存着,因为垃圾回收机制因为变量对象被匿名函数保存着无法回收,这个时候就会内存泄漏。我们想解决这个问题的话就必须手动释放内存,比如setLastName = null
,匿名函数的引用没有了,自然保存的变量对象也被销毁了。
关于闭包引起的内存泄露,这个其实是浏览器的gc(Garbage Collection,垃圾回收)机制问题,在老式浏览器中(IE8以下)存在,跟js本身的闭包机制没有关系
参考文档:
- JavaScript深入之闭包 #9
- 一道js面试题引发的思考 #18
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!