面试必问题目,但总觉得理解得不深入,索性写一篇文章慢慢梳理吧。
什么是闭包
红宝书上给出的定义是:闭包是指有权访问另一个函数作用域中的变量的函数,看到另外一个理解是:函数和函数内部能访问到的变量(或者环境)的总合,就是一个闭包。创建一个闭包最常见的方式就是在一个函数内部创建另一个函数。下面写一个例子:
function f1() {
var a = 1;
function closure() {
console.log(++a);
}
return closure;
}
上面例子中,f1
内部的匿名函数以及它能够访问到的外部函数的变量 a
合在一起,就形成了一个闭包。使用 return
将闭包返回的目的是让它可以被外部访问。下面看看它怎么使用:
var f2 = f1(); // 执行外部函数,返回闭包
f2(); // 2
f2(); // 3
f2(); // 4
第一句执行函数 f1()
后,闭包被返回并赋值给了一个全局变量 f2
,以后每次调用 f2()
,变量 a
的值就会加 1
。通常函数执行完毕后,其作用域链和活动对象都会被销毁,为什么这里 a
并没有被销毁并且每次执行 f2()
还会被递增?原因是闭包有权访问外部函数的变量,进一步说,闭包的作用域链会引用外部函数的活动对象,所以 f2()
在执行时,其作用域链实际上是:
- 自身的活动对象;
-
f1()
的活动对象; - 全局变量对象。
所以 f1()
执行完后,其执行环境的作用域链会被销毁,但活动对象仍然会留在内存中,因为闭包作用域链在引用这个活动对象(说白了就是闭包还需要使用外层函数的变量,不允许它们被销毁),直到闭包被销毁后,f1()
的活动对象才会被销毁。
上面例子中,是将返回的闭包赋值给了一个全局变量 f2
,var f2 = f1();
,f2
是不会被销毁的,每次执行完 f2()
,闭包的作用域链不会被销毁,所以就会出现每次执行 f2()
,a
递增。
但是换一种闭包的调用方式,情况会不同:
f1()(); // 2
f1()(); // 2
因为没有把闭包赋值给一个全局变量,闭包执行完后,其执行域链与活动对象都销毁了。
闭包的作用
创建用于访问私有变量的公有方法
其实构造函数中定义的实例方法,就是闭包:
function Person(){
var name = 'Leon';
function sayHi() {
alert('Hi!');
}
this.publicMethod = function() {
alert(name);
return sayHi();
}
}
构造函数 Person
中定义实例方法 publicMethod()
就是一个闭包,它可以访问外部函数的变量 name
和 函数 sayHi()
,为什么要这么做呢?因为我们想在构造函数中定义一些私有变量,让外部不能直接访问,只能通过定义好的公有方法访问,从而达到保护变量,收敛外部权限的目的。
而在普通函数中,把闭包 return
出去供外部使用,其实目的也就是:让函数内部的变量始终保持在内存中,同时保护这些变量,让它们不能被直接访问。
function person(){
var name = 'Leon';
function sayHi() {
alert('Hi!');
}
function publicMethod() {
alert(name);
return sayHi();
}
return publicMethod;
}
闭包用于创建单例
所谓单例,就是只有一个实例的对象。单例模式的好处在于:
-
保证一个类只有一个实例,避免了一个在全局范围内使用的实例频繁创建与销毁。
- 比如网页中的弹窗,点击 a 按钮弹出,点击 b 按钮隐藏,如果弹窗每一次弹出都需要新建一个对象,将会造成性能的浪费,更好的办法就是只实例化一个对象,一直使用。
-
划分了命名空间,避免了与全局命名空间的冲突。
- 比如在一个单例中可以定义很多方法,通过
单例.方法
来使用,避免了在全局环境中定义函数,造成函数名冲突。
- 比如在一个单例中可以定义很多方法,通过
下面逐步介绍下单例的创建方式,后两种方式将用到闭包。
1. 对象字面量创建单例
var singleton = {
attr1: 1,
attr2: 2,
method: function () {
return this.attr1 + this.attr2;
}
}
var s1 = singleton;
var s2 = singleton;
console.log(s1 == s2) // true
上面用字面量形式创建了一个单例,可以看到 s1
和 s2
是等同的。这种方式的问题在于外部可以直接访问单例的内部变量并加以修改,如果想让单例拥有私有变量,就需要使用模块模式,模块模式就是用了闭包。
2. 模块模式
JS 中的模块模式的作用是:为单例添加私有变量和公有方法。它使用立即执行函数和闭包来达到目的。
var singleton = (function(){
// 创建私有变量
var privateNum = 1;
// 创建私有函数
function privateFunc(){
console.log(++privateNum);
}
// 返回一个对象包含公有方法
return {
publicMethod: function(){
console.log(privateNum)
return privateFunc()
}
};
})();
singleton.publicMethod();
// 1
// 2
这里首先定义了一个立即执行函数,它返回一个对象,该对象中有一个闭包 publicMethod()
, 它可以访问外部函数的私有变量。从而这个被返回的对象就成为了单例的公共接口,外部可以通过它的公有方法访问私有变量而无权直接修改。总结一下就是两点:
- 立即执行函数可以创建一个块级作用域, 避免在全局环境中添加变量。
- 闭包可以访问外层函数中的变量。
3. 构造函数+闭包
上面提到的对象字面是用来创建单例的方法之一,既然单例只能被实例化一次,不难想到,在使用构造函数新建实例时,先判断实例是否已被新建,未被新建则新建实例,否则直接返回已被新建的实例。
var Singleton = function(name){
this.name = name;
};
// 获取实例对象
var getInstance = (function() {
var instance = null;
return function(name) {
if(!instance) {
instance = new Singleton(name);
}
return instance;
}
})();
var a = getInstance('1');
console.log(a); // {name: "1"}
var b = getInstance('2');
console.log(b); // {name: "1"}
这里将构造函数和实例化过程进行了分离, getInstance()
中存在一个闭包,它可以访问到外部变量 instance
,第一次 instance = null
,则通过 new Singleton(name)
新建实例,并将这个实例保存在instance
中,之后再想新建实例,因为闭包访问到的instance
已经有值了,就会直接返回之前实例化的对象。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。