闭包

深入理解javascript的闭包

首先回顾一下作用域链

直接上来举个简单的栗子

1
2
3
4
5
6
7
8
//window
var a = 1
function aa(){
var b = 2
function bb(){
var c = 3
}
}

这里涉及三个执行环境:全局,函数aa,函数bb。
1、全局环境:可访问变量a与函数aa。全局变量对象为{a : 1, aa : (function)}
2、函数aa:可访问变量a,变量b,函数bb。aa变量对象为{b : 2, bb : (function), arguments} {a : 1, aa : (function)}
3、函数bb:可访问变量a,b,c。bb变量对象为{c : 3, arguments} {b : 2, bb : (function), arguments} {a : 1, aa : (function)}

简单来说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期

红宝书上的概念:当代码在一个环境中执行时,会创建变量对象的一个作用域链。他的作用是保证对执行环境有权访问的所有变量和函数的有序访问。在作用域链中只能向上访问而不能向下访问。
若环境是函数,则将其活动对象作为变量对象,活动对象一开始只包含一个arguments对象。其下一个变量对象来自包含环境,再下一个变量对象来自下一个包含环境,这样一直持续到全局环境,形成一个作用域链。

延长作用域链

由于有些语句可以在作用域链前端临时增加一个变量对象,该变量对象会在代码执行之后被移除
1、try-catch语句的catch块
2、with语句

1
2
3
4
5
6
function a(){
with(location){
var url = href + '?name=Tom'
}
return url
}

有了这个语句,函数a这个执行环境中的变量对象就包含了location对象及其所有属性,相当于把location对象挂在了作用域链的最前端。with语句中的href实际上是location中的href。而在with内部定义的url变量,也会曾为函数变量对象的一部分

闭包

创建闭包最常用的方式,就是在一个函数内部创建另一个函数。简单来说:函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。
首先直接来个栗子:

1
2
3
4
5
6
7
8
9
function A() {
let a = 1
function B() {
console.log(a)
}
return B
}
var C = A()
C() // 1

在上述例子中,函数A的活动对象为{arguments, a : 1, B : (function)},当A()执行之后函数A即被销毁,但是由于B使用到了A的活动对象内容,因此A的活动对象会被保存到内存。函数C执行可以返回正确的内容

但在这种模式下,就会产生js闭包的一个经典问题(闭包循环返回最后一个值的问题),红宝书中这样描述:闭包只能取得包含函数中任何变量的最后一个值。闭包所保存的是整个变量对象,而不是某个特殊的值。
红宝书上的两个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//例子1
function a(){
var res = [];
for(var i = 0; i < 10; i++){
res[i] = function(){
return i
}
}
return res
}
var c = a()
c[0]() //10
c[1]()//10
...

//例子2
function a(){
var res = [];
for(var i = 0; i < 10; i++){
res[i] = (function(num){
return function(){
return num//这里保存了不同的num副本
}
})(i)
}
return res;
}
var c = a()
c[0]//0
c[i]]//1
...

上述两个例子中,区别在于,res[i]=的后面,例子一是直接将闭包赋值给了res,例子二是又创建了一个闭包函数赋值给res。

前者不立即执行,而是当res返回之后手动调用res才执行,这个时候函数a中循环结束,变量对象中保存有i的值为10,所以在之后调用res函数时,无论调用res数组的哪个函数,只会返回10

后者由于是一个立即执行的匿名函数,因此生成的10个匿名函数都会正确得到 i 的值,然后再将这些值依次通过num值传递给内部的闭包函数,并将闭包函数返回。

总结来说,前者10个函数共享同一个变量对象,这个变量对象在res返回之后才被保存使用,因此值为10,;后者10个函数分别使用不同的父级环境的变量对象,这些变量对象是随着for循环依次保存给内部闭包使用,因此内部闭包保留有不同的num副本,可以返回正确的值

更多例子:闭包,看这一篇就够了——带你看透闭包的本质,百发百中
(转载,侵删)
其中,个人认为这个例子能最简单地看清闭包的本质。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function m1(){
var x = 1;
return function(){
console.log(++x);
}
}

m1()(); //2
m1()(); //2
m1()(); //2
//使用不同的变量对象
var m2 = m1();
m2(); //2
m2(); //3
m2(); //4
//引用同一个变量对象

总结:就是认清楚到底是什么时候保存的被销毁函数的变量对象
当然,还有一个小坑值得一提

1
2
3
4
5
6
7
8
9
10
11
12
function Foo() {
var i = 0;
return function() {
console.log(i++);
}
}

var f1 = Foo(),
f2 = Foo();
f1();
f1();
f2();

这段代码的输出是0,1, 0
我一开始认为f1和f2都=foo()是都指向了同一个function引用类型,所以顺理成章就会答错认为:0 1 2
但其实foo()返回的是一个匿名函数,所以f1,f2相当于指向了两个不同的函数对象,所以结果也就是0 1 0

闭包带来的问题:内存泄漏

合理使用闭包可以很好的实现封装与缓存
但是需要注意的是,闭包的特性使得其一些变量与参数不会被js的垃圾回收机制所回收,所以在老版本的IE中,如果使用闭包不当,会造成消耗内存和内存溢出的问题。
举个栗子:

1
2
3
4
5
6
7
function a(){
var element = document.getElementById('test')
element.onclick = function(){
console.log(element.id)
}
}
//以上函数由于保存了函数a的活动对象的引用,因此element占用的内存将永远不会被释放

这个问题可以用以下方法暂时解决

1
2
3
4
5
6
7
8
function a(){
var element = document.getElementById('test')
var id = element.id
element.onclick = function(){
console.log(id)
}
element = null
}

上述代码中,我们将element的id值保留着在一个副本中,防止闭包函数对element的直接引用,但一定要记住的是,由于闭包保存的是执行环境的整个活动对象,所以即使没有直接引用element,活动对象{element, id, …}中element依然存在,因此需要将element设置为null以回收内存,否则并不会解决内存泄漏的问题

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2015-2021 AURORA_ZXH
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信