闭包
闭包的概念
严格来讲,闭包(Closure)是一种自然发生的现象,表现为内部函数访问了外部函数的变量。
代码示例:
var foo = 'foo';
function bar () {
console.log(foo);
}
bar();按照上面介绍闭包的说法,上述代码中,bar函数内部使用了外部的foo变量,那么在执行函数时,是会形成闭包的。
我们将第三行代码打一个断点进行调试,来验证这段代码中是否形成了闭包。

可以看到左侧的变量对象列表中多了一个Closure,并且foo变量就在Closure中,证实了前面的说法是正确的。
关于闭包,我们可以拆词来理解它的意思。「闭」可以理解为「封闭、闭环」,「包」可以理解为「一个类似包裹的空间」,因此「闭包」可以理解为「一个封闭的空间」,而这个空间就是用来存储变量的。
需要注意的是,没有被内部函数访问的外部变量,是不会被放入到闭包中的。

上述代码中,baz函数访问了外部函数的x、k、j、i变量,这些变量分别存在于bar、foo函数以及全局作用域中,因此分别创建了三个闭包,bar闭包存储了变量x,foo闭包存储了变量j、k,全局闭包存储了变量i。
可以看出这三个闭包中并没有变量y,这就是因为y变量没有被访问,因此不会形成闭包。
那么形成这么多的闭包,是否会占用大量的内存空间呢?
这就要看闭包是如何形成的了,闭包能够自动形成,也能够手动形成。
而上述代码中,都是自动形成的闭包,它们在函数执行完毕后,会自动销毁(即垃圾回收)。

可以看到,执行最后一条输出语句后,左侧的变量对象列表中已经没有Closure了,说明闭包已经被垃圾回收。
那么如何手动形成闭包呢?
function foo () {
var bar = 'bar';
// 返回一个内部函数
return function () {
console.log(bar);
}
}
var sayBar = foo();
sayBar(); // bar上述代码中,foo函数返回了一个函数,并且在这个内部函数中访问了bar变量。执行foo函数并将结果赋值给sayBar变量,此时sayBar变量指向的是foo函数中返回的这个内部函数,执行sayBar,最终输出bar变量的值。
为什么能够访问到bar变量,原因很简单,就是因为这个内部函数还有访问bar变量,因此在执行完foo函数时,bar变量并没有被垃圾回收。
当我们不再使用bar变量时,只需要将sayBar变量设置为null,解除对bar变量的引用,就可以手动清除闭包了。这一点需要千万记住,否则容易造成内存泄漏。
闭包的经典问题
这是闭包的一道经典问题:
for (var i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}上述代码中,我们期望的结果是1秒后依次输出1, 2, 3,但运行结果是4, 4, 4。
导致这个问题的原因是闭包和 JavaScript 的执行机制:
- 由于 JavaScript 的执行机制,setTimeout 会在循环结束后才执行
- 由于 setTimeout 的回调函数中访问了外部变量
i,形成了闭包,因此在循环结束后i变量并没有被垃圾回收,但此时的i变量已经变为4了
我们没法改变 JavaScript 的执行机制,当然只能从闭包入手了,要想解决这个问题,只需要阻止闭包的形成即可。
for (var i = 1; i <= 3; i++) {
(function (index) {
setTimeout(function () {
console.log(index);
}, 1000);
})(i);
}这里我们通过立即执行函数将i变量以参数的形式传入,那么在这个立即执行函数中,setTimeout 都能拿到每次循环的i变量,就不会再形成闭包了。
还有一个更加简单的解决办法,就是使用ES6新增的块级作用域。
for (let i = 1; i <= 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}由let关键字声明的变量有块级作用域,如果将它放入循环中,那么每次循环都有一个新的变量i,这样即使形成闭包也没问题,因此形成了多个闭包,每个闭包保存的都是不同的i变量。
闭包的应用
防抖
最后一次触发的delay毫秒后调用handler函数。
/**
* 防抖
* @param {function} handler 回调函数
* @param {number} delay 延迟时间,单位ms
* @returns 返回新的防抖函数
*/
function debounce (handler, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
handler.apply(this, args);
}, delay);
}
}节流
在wait毫秒内只执行一次handler函数。
/**
* 节流
* @param {function} handler 回调函数
* @param {number} wait 需要节流的毫秒数
* @returns 返回新的节流函数
*/
function throttle (handler, wait) {
let lastTime = 0;
return function (...args) {
let nowTime = Date.now();
if (nowTime - lastTime >= wait) {
handler.apply(this, args);
lastTime = nowTime;
}
}
}柯里化
创建一个柯里化的函数,若func所需参数已满足,则直接返回func函数的执行结果,否则返回一个接收剩余参数的函数。
/**
* 柯里化
* @param {function} func 用来柯里化的函数
* @param {...any} args 提供给func的参数
* @returns 返回新的柯里化函数
*/
function curry(func, ...args) {
let _self = this;
return function (...curArgs) {
let totalArgs = args.concat(curArgs);
if (totalArgs.length >= func.length) {
return func.apply(null, totalArgs);
} else {
totalArgs.unshift(func);
return curry.apply(_self, totalArgs);
}
}
}单例模式
单例模式,即一个类只有一个实例。
class User {
constructor (name) {
this.name = name;
}
getName () {
return this.name;
}
}
let proxySingleton = (function () {
let instance = null;
return function (name) {
if (!instance) {
instance = new User(name);
}
return instance;
}
})();
let foo = proxySingleton('foo');
let bar = proxySingleton('bar');
console.log(foo === bar); // true原型链导致的闭包漏洞
闭包能够保护变量不被垃圾回收,并且内部函数没有返回变量的话,我们也无法对变量进行修改,但这并不代表闭包中的变量就是安全的。
代码示例:
var o = (function() {
var obj = {
foo: 'foo',
bar: 'bar'
}
return {
get(key) {
return obj[key];
}
}
})();
// 如何修改变量obj?通过上述代码,我们可以通过抛出的 get 方法来获取闭包中 obj 变量中的属性。除了 obj 变量中自带的属性外,我们也能访问到 obj 原型链上的属性,比如 valueOf 属性。
o.get('valueOf'); // [Function: valueOf]但我们无法通过 valueOf 来获取 obj 变量,因为 get 方法中不管读取的是什么属性都是直接返回,这就导致读取一个方法的时候,this 指向的不是 obj 本身。
o.get('valueOf')(); // Uncaught TypeError: Cannot convert undefined or null to object
// 相当于
var valueOf = Object.prototype.valueOf; // 此时 this 指向全局作用域
valueOf();那如果有一种属性在读取的时候就是函数调用,就可以解决这个问题了,答案就是访问器。
Object.defineProperty(Object.prototype, "_this", {
get() {
return this;
}
});
var newobj = o.get('_this'); // { foo: 'foo', bar: 'bar' }
newobj.bar = 'baz';
newobj.foo = 'f';
o.get('bar'); // baz
p.get('foo'); // f通过 Object.definProperty 方法在 Object 的原型上设置一个访问器属性,在 getter 函数中直接将调用者本身也就是 this 返回,这样当读取 _this 属性时,执行对应的 getter 函数,就能够将闭包中的变量完整获取到了。
那么要如何避免这样的情况发生呢?
我们只需要通过 obj.hasOwnProperty 方法来判断读取的属性是否属性 obj 变量本身就行了,因为源头在于原型链,只要避免读取到原型链中的属性就不会出现这样的问题。
完整代码:
var o = (function () {
var obj = {
a: 1,
b: 2,
};
return {
get(key) {
if (obj.hasOwnProperty(key)) {
return obj[key];
}
},
};
})();
Object.defineProperty(Object.prototype, "_this", {
get() {
return this;
},
});总结
闭包(Closure)是一种自然发生的现象,表现为内部函数访问了外部函数的变量。
自动形成的闭包,在函数执行完毕后,会自动销毁,因此不存在内存泄漏的问题。
手动形成闭包的方式是,让一个外部函数返回一个内部函数,内部函数访问了外部函数中的变量。
手动形成的闭包有以下特点:
- 外部环境可以访问到函数内部的变量
- 能够让局部变量保存下来,而不被垃圾回收
闭包的应用场景如下:
- 防抖
- 节流
- 柯里化
- 单例模式
- ...