什么是闭包
“闭包就是一个能够访问其他函数局部变量的函数”—–鲁迅(误
之前谈到过JS中的作用域,里面稍微涉及到了闭包的知识,其实,闭包与作用域关系密切:JS中函数function可以创建出一个独立的执行环境,在其中定义的局部变量(var)和函数定义在外部环境是无法被访问到的,例如:1
2
3
4
5
6
7
8
9function inner(){
var inner_var = 1;
/*在外部调用inner_fn函数也会抛出异常
function inner_fn(){
//do Something
}
*/
}
console.log(inner_var); //ReferenceError
但是,内部函数却可以访问到外部函数的局部变量和函数定义,这是因为作用域链(scope chain)的关系,而当这个内部函数被以某种方式传递到外部执行环境中时,它仍然能够访问到之前所在环境的变量,(换句话说,它的作用域链依然包含着外部函数的作用域),例如:1
2
3
4
5
6
7
8
9
10
11function outer(){
var inner_var = 3;
function inner_fn(){ //引用了外部函数中的inner_var
console.log("inner_var:" + inner_var + " can be found by closure");
}
return inner_fn; //将这个内部函数传递出去
}
var foo = outer(); //得到了对outer内部变量的引用
foo(); //inner_var:3 can be found by closure
//这就是闭包
闭包的原理
先丢一个案例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function foo(){
var num = 9;
function add(){
num++;
}
function show(){
console.log(num);
}
return { //返回一个对象,对象属性为内部函数的引用
add: add,
show: show
}
}
var exe = foo();
exe.show(); //9
exe.add();
exe.show(); //10
这是一个用闭包实现模块化(module)的例子,foo函数返回的对象类似于一个API,它的属性是两个内部函数,能够访问和修改foo的内部数据;
闭包的微观世界
我们已经知道:
- 在定义add函数,show函数时,会将当前作用域链链接到自的
[[scope]]
属性中,此时的作用域链应该包含foo函数的活动对象和全局变量对象 - 而foo函数的活动对象中含有局部变量num的同名属性和值
- foo函数返回一个对象并赋值给全局变量exe
- exe调用show函数时,会为show函数创建一个执行环境,复制之前
[[scope]]
属性中的对象构建起执行环境的作用域链,并产生一个活动对象,将其推入作用域链的顶端,并添加arguments
属性
问题来了:一般来说,当函数执行完毕后,局部活动对象就会被销毁,内存中仅仅保留全局作用域;回到这个例子,也就是说foo函数在执行完后,它的内存就会被回收,局部变量num也会消失;
但闭包的情况就是与众不同,因为show/add函数的作用域链中依然保存着对foo函数活动对象的引用,所以,当show/add函数被传递出去后,即使foo函数已经执行完毕,其活动对象也不会销毁;
show/add函数仍然能够访问到foo函数中的变量;
换句话说,foo函数执行完后,其执行环境的作用域链被销毁,但活动对象仍然保留在内存中,直到show/add函数被销毁,foo的活动对象才会被销毁,例如:
var exe = foo; //创建函数
exe = null; //解除对show/add函数的引用,释放内存
想起网上一段对’死亡’的定义:
一个人一生中会死三次,
第一次是脑死亡,意味着身体死了,
第二次是葬礼,意味着在社会中死了,
第三次是遗忘,这世上再也没有人记得你了,你就真正地死去了。
为什么突然有种凄凉的感觉( *・ω・)✄╰ひ╯
闭包的作用
其实在平时编码的过程中会不经意的用到闭包,比如刚才说的模块化的实现,还有几点网上总结的应用场景:
- 保护函数内部的变量安全
- 在内存中维持一个变量
- 通过保护变量安全实现JS私有属性和私有方法
私有变量
第三点很重要,是面向对象设计模式中”封装”的实现方法之一,严格来说,JavaScript中的对象并没有私有成员(private)的概念,所有对象属性都是公有的。但在函数中定义的局部变量,外部是访问不了的,除非使用闭包;因此JavaScript中的私有变量一般指函数的参数、局部变量、和内部定义的其他函数;1
2
3
4
5
6
7
8
9
10
11
12function Person(name){ //除非使用函数内部的特权函数,否则无法访问参数name
this.getName = function(){
return name;
}
this.setName = function(value){
name = value;
}
}
var person = new Person("Mmear");
console.log(person.getName()); //Mmear
person.setName("Mirr");
console.log(person.getName()); //Mirr
上述的name变量在每一个Person实例中都不同,因为每次调用构造函数都会重新创建这两个方法。在函数中定义特权函数的缺点是:针对每一个实例都会创建同样一组新方法,这在之后的面向对象设计模式会谈到。
静态私有变量
通过在私有作用域(块级作用域)中定义私有变量或函数,同样可以创建特权方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17(function(){
//私有变量和函数
var private_var = 10;
function private_fn(){
return private_var;
};
//构造函数
Con = function(){ //注意没有使用var关键字,说明Con被声明在全局环境中
};
//特权方法
Con.prototype.publicMethod = function()[
private_var++;
console.log(private_fn());
};
})()
这个模式通过立即执行函数(IIFE)
来创建了一个私有作用域,然后定义私有变量及函数,再定义构造函数和其公有方法,这个公有方法,作为闭包保有着对作用域的引用;在这种模式下,变量private_var成为了一个静态的,由所有对象共享的变量:1
2
3
4var con1 = new Con();
con1.publicMethod(); //11
var con2 = new Con():
con2.publicMethod(); //12
这种方式创建静态私有变量会因为使用了原型模式而增加代码复用,但每个实例都没有自己的私有变量;
使用闭包的注意事项
内存泄漏
因为闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页性能的问题;IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些问题:比如,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素无法被销毁;
解决方法是在函数执行完毕之前将不需要的局部变量删除(null);
闭包的副作用
闭包是动态的,即它永远保存着函数中变量最后一个值(或者说最新的值),因为闭包保存的是整个变量对象,而不是单个变量:1
2
3
4
5
6
7function createFn(){
for(var i = 0; i < 5; i++){
listener[i].onlick = function(){
alert("button " + i " was clicked!");
}
}
}
结果是无论点击哪个按钮,都会alert出"button 6 was clicked"
,这是因为每个onclick事件处理函数都保留着createFn函数的活动对象,所以它们引用着同一个变量i,当循环结束后,i的值为6,所以就喜闻乐见地出bug了;
解决方法是用一个匿名函数强制构建一个作用域:1
2
3
4
5
6
7
8
9function createFn(){
for(var i = 0; i < 5; i++){
(function(){ //在createFn中再创建出一个作用域
listener[i].onlick = function(){
alert("button " + i " was clicked!");
}
})(i);
}
}
原理比较粗暴,就是创建了6个不同的匿名函数,保存了6个不同的i值,产生了6个不同的闭包,通过创建新作用域的方法使闭包强行符合预期;ES6中可以使用let
关键字将{}变成一个块级作用域,并在这个作用域上声明一个变量:1
2
3
4
5
6
7function createFn(){ //每次迭代都会重新声明一遍i,并将上次迭代的值赋予i
for(let i = 0; i < 5; i++){ //结果和上个例子相同
listener[i].onlick = function(){
alert("button " + i " was clicked!");
}
}
}
总结
闭包是一个非常强大的功能,结合块作用域可以实现更加强大的功能~
想想还有什么要写:
- IIFE的介绍和作用
- 模块模式