闭包
一、概念
1.1 定义
在 JavaScript 中,根据词法作用域的规则,内部函数可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束,内部函数引用的外部变量依然会保存在内存中。这些变量的集合被称为闭包。
如下示例:
function foo() {
var myName = '极客时间'
let test1 = 1
const test2 = 2
var innerBar = {
getName: function () {
console.log(test1)
return myName
},
setName: function (newName) {
myName = newName
},
}
return innerBar
}
var bar = foo()
bar.setName('极客邦')
console.log(bar.getName())
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
当执行到 foo 函数内部的 return innerBar
这行代码时,调用栈的情况如下图所示:
innerBar
是一个包含了 getName
和 setName
的两个方法的对象(注:课程原图有误:innerBar
应该是一个对象,而不是 function
)。
根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量,所以当 innerBar 对象返回给全局变量 bar 时,虽然 foo 函数已经执行结束,但是 getName 和 setName 函数依然可以使用 foo 函数中的变量 myName
和 test1
。所以当 foo 函数执行完成之后,其整个调用栈的状态如下图所示: 从上图可以看出,foo 函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的 setName 和 getName 方法中使用了 foo 函数内部的变量 myName 和 test1,所以这两个变量依然保存在内存中。
1.2 使用
当执行到 bar.setName
方法中的 myName = "极客邦"
这句代码时,JavaScript 引擎会沿着 "当前执行上下文 -> foo 函数闭包 -> 全局执行上下文" 的顺序来查找 myName 变量:
如图,setName 的执行上下文中没有 myName 变量,foo 函数的闭包中包含了变量 myName,所以调用 setName 时,会修改 foo 闭包中的 myName 变量的值。
同样的流程,当调用 bar.getName 的时候,所访问的变量 myName 也是位于 foo 函数闭包中的。
通过 “开发者工具” 也可以看看闭包的情况,打开 chrome 的 “开发者工具”,在 bar 函数任意地方打上断点,然后刷新页面,可以看到如下内容:
从图中可以看出来,当调用 bar.getName 的时候,右边 Scope 项就体现出了作用域链的情况: Local 就是当前的 getName 函数的作用域,Closure(foo)
是指 foo 函数的闭包,最下面的 Global 就是指全局作用域,"Local -> Closure(foo) -> Global" 就是一个完整的作用域链。
注:闭包是并不包含整个变量环境和词法环境,而是只是包含用到的变量。
1.2 回收
闭包的回收机制与普通函数不同。普通函数执行完毕后,其执行上下文会被销毁,但闭包引用的变量不会立即被销毁。
如果引⽤闭包的函数是个局部变量,等函数销毁后,在下次JavaScript引擎执⾏垃圾回收时,判断闭包这块内容如果已经不再被使⽤了,那么JavaScript引擎的垃圾回收器就会回收这块内存。
二、应用
2.1 模块化
闭包可以用于实现模块模式,隐藏一些不必要的细节,同时暴露必要的接口。例如:
function myModule() {
var name = "coolModule";
var version = "1.0.0";
function getInfo() {
return "name: " + name + ", ver: " + version;
}
function update() {
version += 1;
console.log("updated to " + version + " successfully!");
}
return {
getInfo: getInfo,
update: update,
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在这个例子中,myModule
函数封装了两个私有变量 name
和 version
,以及两个私有函数 getInfo
和 update
。外界无法直接访问这些私有变量,但可以通过返回的对象中的方法来操作内部数据。返回的对象中的方法持有了 myModule
函数作用域的闭包。
2.2 单例模式
单例模式是一种常见的设计模式,它保证了一个类只有一个实例。实现方法通常是先判断实例是否存在,如果存在则直接返回,否则创建并返回实例。例如:
function Singleton() {
this.data = "singleton";
}
Singleton.getInstance = (function () {
var instance;
return function () {
if (instance) {
return instance;
} else {
instance = new Singleton();
return instance;
}
};
})();
var sa = Singleton.getInstance();
var sb = Singleton.getInstance();
console.log(sa === sb); // true
console.log(sa.data); // 'singleton'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在这个例子中,Singleton
函数是一个构造函数,getInstance
是一个自执行函数返回的函数。它通过闭包保存了 instance
变量,确保每次调用 getInstance
时返回的都是同一个实例。
2.3 柯里化
柯里化是一种将接受多个参数的函数变换成接受一个单一参数的函数的技术。它返回一个新函数,该新函数接受剩余的参数并返回结果。例如,可以用柯里化的方法实现 bind
方法:
Function.prototype.myBind = function (context = window) {
if (typeof this !== "function") throw new Error("Error");
let selfFunc = this;
let args = [...arguments].slice(1);
return function F() {
if (this instanceof F) {
return new selfFunc(...args, ...arguments);
} else {
return selfFunc.apply(context, args.concat(arguments));
}
};
};
2
3
4
5
6
7
8
9
10
11
12
13
14
myBind 方法通过闭包保存了 selfFunc
和 args
,并返回一个新的函数 F
。当调用 F
时,它会根据是否通过 new
调用来决定如何执行 selfFunc
。