Mmear's 😘.

JavaScript闭包

字数统计: 2.1k阅读时长: 8 min
2018/03/29 Share

什么是闭包

“闭包就是一个能够访问其他函数局部变量的函数”—–鲁迅(误

之前谈到过JS中的作用域,里面稍微涉及到了闭包的知识,其实,闭包与作用域关系密切:JS中函数function可以创建出一个独立的执行环境,在其中定义的局部变量(var)和函数定义在外部环境是无法被访问到的,例如:

1
2
3
4
5
6
7
8
9
function 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
11
function 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
18
function 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
12
function 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
4
var 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
7
function 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
9
function 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
7
function createFn(){              //每次迭代都会重新声明一遍i,并将上次迭代的值赋予i
for(let i = 0; i < 5; i++){ //结果和上个例子相同
listener[i].onlick = function(){
alert("button " + i " was clicked!");
}
}
}

总结

闭包是一个非常强大的功能,结合块作用域可以实现更加强大的功能~
想想还有什么要写:

  • IIFE的介绍和作用
  • 模块模式
CATALOG
  1. 1. 什么是闭包
  2. 2. 闭包的原理
    1. 2.1. 闭包的微观世界
  3. 3. 闭包的作用
    1. 3.1. 私有变量
      1. 3.1.1. 静态私有变量
  4. 4. 使用闭包的注意事项
    1. 4.1. 内存泄漏
    2. 4.2. 闭包的副作用
  5. 5. 总结