写在前面
闭包是JavaScript中一个重要的概念,本文使用3w(what,why,how)原则总结一下闭包这个概念。
what,什么是闭包?
网上关于闭包的定义有很多,但大多过于繁杂。一种简洁的说法:闭包是一个有状态的函数(a stateful function)---- 首先,闭包是一个函数,它一定存在于外层函数内;其次,闭包“记住了”其外层函数拥有的变量,它能够访问外层函数的作用域。
下面的例子就形成了一个闭包:
function foo() {
var data = "hello";
function bar() {
console.log(data); // 打印 hello
}
return bar;
}
foo()();
函数bar就是一个闭包,它能够访问外层函数foo的变量data。当调用foo()()时,也就是调用了bar函数,将打印foo函数内部变量data的值。
why,为什么要用闭包,作用是什么?
闭包多用来提供“模块化”,起到“封装”代码的目的。
由于外部执行环境无法访问函数内部的变量,但可以通过将闭包暴露出来的方式,使外部获取对函数内部变量的访问权。
闭包的这个用法,类似于C++、Java等语言的公有函数(public),提供外界访问类的私有成员变量(private)的能力。
下面是一个闭包提供的例子:
function Person() {
var name = "Joe";
return {
getName: function() {
return name;
},
setName: function(newName) {
name = newName;
}
}
}
var p = Person();
console.log(p.name); // 打印undefined
console.log(p.getName()); // 打印Joe
p.setName('Mike');
console.log(p.getName()); // 打印Mike
Person函数的内部变量name只存在于Person的函数作用域内,无法被外界访问到,所以打印undefined。
通过闭包getName和setName,可以达到访问变量name的目的。
在es6中终于有了类的概念,上面的代码可以用类的方式改写:
class Person {
constructor() {
this.name = 'Joe';
}
getName() {
return this.name;
}
setName(newName) {
this.name = newName;
}
}
var p = new Person();
console.log(p.name); // 打印Joe
console.log(p.getName()); // 打印Joe
p.setName('Mike');
console.log(p.getName()); // 打印Mike
es6由于没有private概念,可以看到这里的name属性其实是可以被外界直接调用到的。
另外在回调函数(如callback,setTimeout)、异步执行的函数(如ajax请求)中,经常看到闭包的身影。闭包一定程度上简化了代码的写法。
how,闭包是如何形成的?
说起闭包的底层机制,就首先要搞懂js语言中的执行环境和作用域链的概念,这篇文章介绍了这些概念。
闭包的原理其实就是形成闭包的这个内层函数,在其自身的作用域链上,头结点(活动对象)是自身的变量对象,记录了自身内部的变量信息;而作用域链上的第二个节点是其外层函数的变量对象,记录了外层函数的变量信息----很显然,这个节点上保存的信息能够被闭包函数访问的到,闭包定义所说的“周围环境”,其实就是这个数据。
一般的,当外层函数执行完成后,js引擎会释放掉它的执行环境,作用域链等信息。但如果其内部含有闭包函数,由于闭包函数的作用域链上仍然引用着外层函数的变量对象(第二个节点),垃圾回收器判定这块内存仍然有在被使用(被闭包引用),就不会释放掉这块内存,所以即使外层函数执行完,我们依然能通过闭包函数,访问其外层函数的变量信息。经常有说闭包容易引起内存泄漏,也就是因为内存一直被闭包引用者,无法被垃圾回收导致的结果。
闭包的这种底层实现,有时候会引起看上去有点预想不到的结果,比如在for循环中。思考下面的例子,为什么打印出来的都是5呢?
function arrayFun() {
var arr = [];
for (var i = 0; i < 5; i++) {
arr.push(function () {
console.log(i);
});
}
return arr;
}
var retArr = arrayFun();
for (var i = 0; i < 5; i++) {
retArr[i](); // 打印 5,5,5,5,5
}
由于在es6之前,只有全局作用域和函数作用域,i变量在函数arrayFun中,只有一份副本。那么闭包(此处是个匿名函数)在作用域链上引用的是同一个i值(i在执行完for循环后值变为5),当调用闭包时,访问作用域链上的i值,就都是5了。
在es6中引入了let和块作用域,i的值在块作用域中是不同的副本,所以就不会出现上面的情况了。仍然是这篇文章有代码实例,不再赘述。