总是担心写的不够完全、不够好,不管怎样,还是要把我理解的闭包和大家分享下,比较长,希望耐心看完。
定义
说实话,给闭包下一个定义是很困难的,原因在于javascript设计的时候并没有专门设计闭包这样一个规则,闭包是随着作用域链、函数可以作为一等公民这样的规则而诞生的。
尽管不能下一个很完美的定义,但是我们还是可以给闭包下一个尽量准确的定义。
闭包:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。闭包的创建和使用在你的代码中随处可见。
哪些是闭包?
来看下面这个例子1:
复制代码
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
复制代码
基于词法作用域的查找规则,函数bar() 可以访问外部作用域中的变量a(这个例子中的是一个RHS 引用查询)。
那么这个是闭包吗?很遗憾不是,因为bar函数执行在其定义的词法作用域处。
不过稍加修改后就是个闭包了,例子2:
复制代码
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。
复制代码
baz函数执行实际上只是通过不同的标识符引用调用了内部的函数bar()
bar()函数显然可以被正常执行,也就是在自己定义的词法作用域以外的地方执行。
根据作用域的规则,函数bar()函数能够访问foo()的内部作用域,因此foo()执行完后,其内部作用域并不会被回收,bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。
这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。
当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。
来看例子3:
复制代码
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 妈妈快看呀,这就是闭包!
}
复制代码
是的,这也是个闭包,这里将baz传递出去了在bar()函数中执行,而不是在自己定义的词法作用域中执行,但是它却保留这对定义时词法作用域的引用
再看例子4:
复制代码
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 将baz 分配给全局变量
}
function bar() {
fn(); // 妈妈快看呀,这就是闭包!
}
foo();
bar(); // 2
复制代码
是的没错,这还是个闭包,无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
那我们看一个难一点的例子5:
复制代码
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
复制代码
这是闭包吗?答案是的,在这里我们向setTimeOut传入timer()函数,并且timer函数可以访问wait的内部作用域,保持着对wait内部作用域的引用,比如里面的message变量。
这时候你肯定会心生疑惑:不对呀?这在哪执行呢?不是说要在定义的词法作用域以外执行吗?
传入的timer函数当然会被执行,只是内部引擎调用执行的。
深入到引擎的内部原理中,内置的工具函数setTimeout(..) 持有对一个参数的引用,这个参数也许叫作fn 或者func,或者其他类似的名字。引擎会调用这个函数,在例子中就是内部的timer 函数,而词法作用域在这个过程中保持完整,time函数保持着对wait内部作用域的引用。
IIFE(立即执行函数)是闭包吗?
例子6:
复制代码
var a = 2;
(function IIFE() {
console.log( a );
})();
复制代码
按照我们的定义来说,这不是闭包。
但是,尽管IIFE 本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。
因此IIFE 的确同闭包息息相关,即使本身并不会真的使用闭包。
这也是为什么很难给闭包下定义的地方,因为如果从内存或者作用来看,IIFE创建了闭包(也就是在内存中创建了一块区域,这块区域保存着作用域链上作用域的引用,稍后可见例子9),或者说效果等同于创建了闭包。
而如果从闭包的定义来看,这却不是闭包。
我们来看例子7:
复制代码
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
复制代码
大家都知道这段代码会输出五次6,为什么呢?
因为setTimeOut()是异步函数,也就是等循环结束后才去执行setTimeOut()中的回调函数,而在for循环中,并不存在着块级作用域,也就是这个i声明在全局作用域中,并且自始至终只有一个i(因为var声明会变量声明提升,也就是其实只声明了一次),而在for循环结束后,这个i的值是6。setTimeOut()中的回调函数timer()保持着对i的引用,但是5次timer()函数引用的只是同一个i,所以输出5次6。
例子8:
复制代码
for (var i=1; i<=5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}
复制代码
这样有效果么?答案是没有的,虽然通过IIFE每次都创建了一个作用域,但是这个作用域是空的(也就是创建了一个空作用域),所以还会沿着词法作用域链去上一层找i,结果找到的还是全局作用域中的i,也就是只有一个i,还是会输出五次6。
所以我们需要这样改,来看例子9:
复制代码
// 它需要有自己的变量,用来在每个迭代中储存i 的值:
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})();
}
// 行了!它能正常工作了!。
// 可以对这段代码进行一些改进:
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
//当然你也可以这样写
for (var i=1; i<=5; i++) {
(function(i) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})( i );
}
复制代码
在迭代内使用IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
好在ES6出来了let的解决方案,let并不会变量声明提升,并且具有块级作用域的效果,也就是这里会产生5个i的内存空间,被五个timer()函数引用着。
例子10:
复制代码
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
复制代码
关于闭包的垃圾回收
问题1:闭包会造成内存泄漏吗?
我们常说闭包会造成内存泄漏,这是真的吗?答案是不会的。
之所以之前一直说闭包会造成垃圾泄露是由于IE9 之前的版本对JavaScript 对象(标记清除)和COM 对象(引用计数)使用不同的垃圾收集方法。因此闭包在IE 的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML 元素,那么就意味着该元素将无法被销毁
例子11:
复制代码
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}
复制代码
以上代码创建了一个作为element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element 的引用数。只要匿名函数存在,element 的引用数至少也是1,因此它所占用的内存就永远不会被回收。
解决办法就是把element.id 的一个副本保存在一个变量中,从而消除闭包中该变量的循环引用同时将element变量设为null。
例子12:
复制代码
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
};
element = null;
}
复制代码
问题2:闭包中没有使用的变量会被回收吗?
答案是会的。
来看例子13:
复制代码
function foo() {
var x = {};
var y = "whatever";
return function bar() {
alert(y);
};
}
var z = foo();
复制代码
在这里x没有被使用,那么x会被回收吗?答案是的。
理论上来说,bar函数保存着foo作用域中的引用,那么x不应该会被回收。但是现代javascript引擎是非常智能的,对这里进行了优化。
javascript引擎经过逃逸分析(分析函数调用关系,以判断变量是否“逃逸”出当前作用域范围)后判断出来x没有在闭包中使用到,那么它就会把x从堆中的作用域中移除出去。
一般是如何分析呢?很简单,如果闭包中没有引用到这个变量,并且没有使用 eval 或者 new Function,那么javascript引擎可以知道闭包的内存中的作用域不需要这个变量x.
具体测试可以看之前司徒正美的一篇文章:JS闭包测试
或者可以看看stackoverflow上的一篇解答:JavaScript Closures Concerning Unreferenced Variables
问题3:闭包中函数里的变量是分配在堆中还是栈中?
在简单的解释器实现里,函数里的变量是分配在堆而不是在栈上的。现代 JS 引擎当然就比较牛逼了,通过逃逸分析是可以知道哪些可以分配在栈上,哪些需要分配在堆上的。
也就是闭包中使用到的变量会分配在堆中,没有使用到的会分配在栈中(针对简单类型而言),以方便回收。
比如例子13的x,没有被闭包使用,不过是一个复杂类型,所以它在内存中是变量x存储在栈中,同时栈中x的值是堆中的对象{}的地址,大概是下面这样
【栈x】---->(堆{})
例子13中的y,被闭包使用了,闭包的函数就基于原先的词法作用域单独在堆中分配了内存,也就是闭包保存在了堆,同时其使用的变量也随着闭包一起保存在堆,大概是下面这样。
(堆(闭包(y:“whatever”)))
好了,以上这就是我的个人理解了,如果有什么疑问或者建议欢迎讨论。
分类: 你不知道的JS
好文要顶 关注我 收藏该文
汕大小吴
关注 - 2
粉丝 - 19
+加关注
1
« 上一篇:JS刷剑指offer总结
posted @ 2018-09-10 02:23 汕大小吴 阅读(219) 评论(0) 编辑 收藏
刷新评论刷新页面返回顶部
注册用户登录后才能发表评论,请 登录 或 注册,访问网站首页。
【推荐】超50万VC++源码: 大型组态工控、电力仿真CAD与GIS源码库!
【免费】要想入门学习Linux系统技术,你应该先选择一本适合自己的书籍
【前端】SpreadJS表格控件,可嵌入应用开发的在线Excel
【推荐】企业SaaS应用开发实战,快速构建企业运营/运维系统
腾讯云0831
最新IT新闻:
· 京东红太阳
· 走近后厂村程序员的真实生活:拿命换钱
· 滴滴仍给无营运证的黑车司机派单 客服:无证被扣车
· 大脑如何产生自由意识?源于其原子核自旋的相互纠缠
· 西门子中国研究院院长:不做家电很多年西门子,早已转型做数字化
» 更多新闻...
华为云HC0905
最新知识库文章:
· 为什么说 Java 程序员必须掌握 Spring Boot ?
· 在学习中,有一个比掌握知识更重要的能力
· 如何招到一个靠谱的程序员
· 一个故事看懂“区块链”
· 被踢出去的用户
» 更多知识库文章...
公告
昵称:汕大小吴
园龄:1总是担心写的不够完全、不够好,不管怎样,还是要把我理解的闭包和大家分享下,比较长,希望耐心看完。
定义
说实话,给闭包下一个定义是很困难的,原因在于javascript设计的时候并没有专门设计闭包这样一个规则,闭包是随着作用域链、函数可以作为一等公民这样的规则而诞生的。
尽管不能下一个很完美的定义,但是我们还是可以给闭包下一个尽量准确的定义。
闭包:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。闭包的创建和使用在你的代码中随处可见。
哪些是闭包?
来看下面这个例子1:
复制代码
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
复制代码
基于词法作用域的查找规则,函数bar() 可以访问外部作用域中的变量a(这个例子中的是一个RHS 引用查询)。
那么这个是闭包吗?很遗憾不是,因为bar函数执行在其定义的词法作用域处。
不过稍加修改后就是个闭包了,例子2:
复制代码
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。
复制代码
baz函数执行实际上只是通过不同的标识符引用调用了内部的函数bar()
bar()函数显然可以被正常执行,也就是在自己定义的词法作用域以外的地方执行。
根据作用域的规则,函数bar()函数能够访问foo()的内部作用域,因此foo()执行完后,其内部作用域并不会被回收,bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。
这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。
当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。
来看例子3:
复制代码
function foo() {
var a = 2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 妈妈快看呀,这就是闭包!
}
复制代码
是的,这也是个闭包,这里将baz传递出去了在bar()函数中执行,而不是在自己定义的词法作用域中执行,但是它却保留这对定义时词法作用域的引用
再看例子4:
复制代码
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 将baz 分配给全局变量
}
function bar() {
fn(); // 妈妈快看呀,这就是闭包!
}
foo();
bar(); // 2
复制代码
是的没错,这还是个闭包,无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
那我们看一个难一点的例子5:
复制代码
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
复制代码
这是闭包吗?答案是的,在这里我们向setTimeOut传入timer()函数,并且timer函数可以访问wait的内部作用域,保持着对wait内部作用域的引用,比如里面的message变量。
这时候你肯定会心生疑惑:不对呀?这在哪执行呢?不是说要在定义的词法作用域以外执行吗?
传入的timer函数当然会被执行,只是内部引擎调用执行的。
深入到引擎的内部原理中,内置的工具函数setTimeout(..) 持有对一个参数的引用,这个参数也许叫作fn 或者func,或者其他类似的名字。引擎会调用这个函数,在例子中就是内部的timer 函数,而词法作用域在这个过程中保持完整,time函数保持着对wait内部作用域的引用。
IIFE(立即执行函数)是闭包吗?
例子6:
复制代码
var a = 2;
(function IIFE() {
console.log( a );
})();
复制代码
按照我们的定义来说,这不是闭包。
但是,尽管IIFE 本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。
因此IIFE 的确同闭包息息相关,即使本身并不会真的使用闭包。
这也是为什么很难给闭包下定义的地方,因为如果从内存或者作用来看,IIFE创建了闭包(也就是在内存中创建了一块区域,这块区域保存着作用域链上作用域的引用,稍后可见例子9),或者说效果等同于创建了闭包。
而如果从闭包的定义来看,这却不是闭包。
我们来看例子7:
复制代码
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
复制代码
大家都知道这段代码会输出五次6,为什么呢?
因为setTimeOut()是异步函数,也就是等循环结束后才去执行setTimeOut()中的回调函数,而在for循环中,并不存在着块级作用域,也就是这个i声明在全局作用域中,并且自始至终只有一个i(因为var声明会变量声明提升,也就是其实只声明了一次),而在for循环结束后,这个i的值是6。setTimeOut()中的回调函数timer()保持着对i的引用,但是5次timer()函数引用的只是同一个i,所以输出5次6。
例子8:
复制代码
for (var i=1; i<=5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}
复制代码
这样有效果么?答案是没有的,虽然通过IIFE每次都创建了一个作用域,但是这个作用域是空的(也就是创建了一个空作用域),所以还会沿着词法作用域链去上一层找i,结果找到的还是全局作用域中的i,也就是只有一个i,还是会输出五次6。
所以我们需要这样改,来看例子9:
复制代码
// 它需要有自己的变量,用来在每个迭代中储存i 的值:
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})();
}
// 行了!它能正常工作了!。
// 可以对这段代码进行一些改进:
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
//当然你也可以这样写
for (var i=1; i<=5; i++) {
(function(i) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})( i );
}
复制代码
在迭代内使用IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
好在ES6出来了let的解决方案,let并不会变量声明提升,并且具有块级作用域的效果,也就是这里会产生5个i的内存空间,被五个timer()函数引用着。
例子10:
复制代码
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
复制代码
关于闭包的垃圾回收
问题1:闭包会造成内存泄漏吗?
我们常说闭包会造成内存泄漏,这是真的吗?答案是不会的。
之所以之前一直说闭包会造成垃圾泄露是由于IE9 之前的版本对JavaScript 对象(标记清除)和COM 对象(引用计数)使用不同的垃圾收集方法。因此闭包在IE 的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML 元素,那么就意味着该元素将无法被销毁
例子11:
复制代码
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}
复制代码
以上代码创建了一个作为element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element 的引用数。只要匿名函数存在,element 的引用数至少也是1,因此它所占用的内存就永远不会被回收。
解决办法就是把element.id 的一个副本保存在一个变量中,从而消除闭包中该变量的循环引用同时将element变量设为null。
例子12:
复制代码
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
};
element = null;
}
复制代码
问题2:闭包中没有使用的变量会被回收吗?
答案是会的。
来看例子13:
复制代码
function foo() {
var x = {};
var y = "whatever";
return function bar() {
alert(y);
};
}
var z = foo();
复制代码
在这里x没有被使用,那么x会被回收吗?答案是的。
理论上来说,bar函数保存着foo作用域中的引用,那么x不应该会被回收。但是现代javascript引擎是非常智能的,对这里进行了优化。
javascript引擎经过逃逸分析(分析函数调用关系,以判断变量是否“逃逸”出当前作用域范围)后判断出来x没有在闭包中使用到,那么它就会把x从堆中的作用域中移除出去。
一般是如何分析呢?很简单,如果闭包中没有引用到这个变量,并且没有使用 eval 或者 new Function,那么javascript引擎可以知道闭包的内存中的作用域不需要这个变量x.
具体测试可以看之前司徒正美的一篇文章:JS闭包测试
或者可以看看stackoverflow上的一篇解答:JavaScript Closures Concerning Unreferenced Variables
问题3:闭包中函数里的变量是分配在堆中还是栈中?
在简单的解释器实现里,函数里的变量是分配在堆而不是在栈上的。现代 JS 引擎当然就比较牛逼了,通过逃逸分析是可以知道哪些可以分配在栈上,哪些需要分配在堆上的。
也就是闭包中使用到的变量会分配在堆中,没有使用到的会分配在栈中(针对简单类型而言),以方便回收。
比如例子13的x,没有被闭包使用,不过是一个复杂类型,所以它在内存中是变量x存储在栈中,同时栈中x的值是堆中的对象{}的地址,大概是下面这样
【栈x】---->(堆{})
例子13中的y,被闭包使用了,闭包的函数就基于原先的词法作用域单独在堆中分配了内存,也就是闭包保存在了堆,同时其使用的变量也随着闭包一起保存在堆,大概是下面这样。
(堆(闭包(y:“whatever”)))
https://www.cnblogs.com/wuguanglin/p/closure.html