blog
blog copied to clipboard
理解 JS 中的闭包
概念
闭包 是指一个函数可以记住其外部变量并可以访问这些变量。闭包是函数和声明该函数的词法环境的组合。
通过函数嵌套函数的形式延长内部函数的词法环境,获取到外部环境的 [[Environment]] 属性
。这一条词法链将会保留在内存中,不会被垃圾回收机制销毁。
词法环境
在 JavaScript 中,每个运行的函数,代码块 {...}
以及整个脚本,都有一个被称为 词法环境(Lexical Environment) 的内部(隐藏)的关联对象。
词法环境对象由两部分组成:
环境记录(Environment Record) —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如 this 的值)的对象。 对 外部词法环境 的引用,与外部代码相关联。
当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
let value = "Surprise!";
function f() {
let value = "the closest value";
function g() {
return value;
}
return g;
}
let g = f();
console.dir(g); // 这里的词法优先顺序是 Closure > Script > Global
// ƒ g()
// arguments: null
// caller: null
// length: 0
// name: "g"
// prototype: {constructor: ƒ}
// [[FunctionLocation]]: aa.html:16
// [[Prototype]]: ƒ ()
// [[Scopes]]: Scopes[3]
// 0: Closure (f) {value: 'the closest value'}
// 1: Script {value: 'Surprise!', g: ƒ}
// 2: Global {window: Window, self: Window, document: document, name: '', location: Location, …}
console.log(g()); // the closest value
垃圾回收
通常,函数调用完成后,会将词法环境和其中的所有变量从内存中删除。因为现在没有任何对它们的引用了。与 JavaScript 中的任何其他对象一样,词法环境仅在可达时才会被保留在内存中。
但是,如果有一个嵌套的函数在函数结束后仍可达,则它将具有引用词法环境的 [[Environment]] 属性。
在下面这个例子中,即使在(外部)函数执行完成后,它的词法环境仍然可达。因此,此词法环境仍然有效。
例如:
function f() {
let value = 123;
return function () {
alert(value);
};
}
let g = f(); // g.[[Environment]] 存储了对相应 f() 调用的词法环境的引用
请注意,如果多次调用 f(),并且返回的函数被保存,那么所有相应的词法环境对象也会保留在内存中。下面代码中有三个这样的函数:
function f() {
let value = Math.random();
return function () {
alert(value);
};
}
// 数组中的 3 个函数,每个都与来自对应的 f() 的词法环境相关联
let arr = [f(), f(), f()];
当词法环境对象变得不可达时,它就会死去(就像其他任何对象一样)。换句话说,它仅在至少有一个嵌套函数引用它时才存在。
在下面的代码中,嵌套函数被删除后,其封闭的词法环境(以及其中的 value)也会被从内存中删除:
function f() {
let value = 123;
return function () {
alert(value);
};
}
let g = f(); // 当 g 函数存在时,该值会被保留在内存中
g = null; // ……现在内存被清理了
常见场景
- 封装私有变量
用相同的 makeCounter 函数创建了两个计数器(counters):counter 和 counter2。
构造具有独立性,闭包维护自身私有变量,相互不会产生影响
function makeCounter() {
let count = 0;
return function () {
return count++;
};
}
let counter = makeCounter();
let counter2 = makeCounter();
alert(counter()); // 0
alert(counter()); // 1
alert(counter2()); // 0
alert(counter2()); // 1
function Counter() {
let count = 0;
this.up = function () {
return ++count;
};
this.down = function () {
return --count;
};
}
let counter = new Counter();
alert(counter.up()); // 1
alert(counter.up()); // 2
alert(counter.down()); // 1
- 在循环中创建闭包
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function () {
console.log(i);
};
})(i);
}
data[0]();
data[1]();
data[2]();
每隔一秒依次打印 1,2,3,4,5
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
- 节流函数
function throttle(fn, gapTime = 1500) {
let _lastTime = null;
// 返回新的函数
return function () {
let _nowTime = +new Date();
if (_nowTime - _lastTime > gapTime || !_lastTime) {
fn.apply(this, arguments); //将this和参数传给原函数
_lastTime = _nowTime;
}
};
}
- 闭包 sum
编写一个像 sum(a)(b) = a+b 这样工作的 sum 函数。
是的,就是这种通过双括号的方式(并不是错误)。
举个例子:
sum(1)(2) = 3
sum(5)(-1) = 4
为了使第二个括号有效,第一个(括号)必须返回一个函数。
就像这样:
function sum(a) {
return function (b) {
return a + b; // 从外部词法环境获得 "a"
};
}
alert(sum(1)(2)); // 3
alert(sum(5)(-1)); // 4
- 通过函数筛选
我们有一个内建的数组方法 arr.filter(f)。它通过函数 f 过滤元素。如果它返回 true,那么该元素会被返回到结果数组中。
制造一系列“即用型”过滤器:
-
inBetween(a, b) —— 在 a 和 b 之间或与它们相等(包括)。
-
inArray([...]) —— 包含在给定的数组中。 用法如下所示:
-
arr.filter(inBetween(3,6)) —— 只挑选范围在 3 到 6 的值。
-
arr.filter(inArray([1,2,3])) —— 只挑选与 [1,2,3] 中的元素匹配的元素。
例如:
/* .. inBetween 和 inArray 的代码 */
let arr = [1, 2, 3, 4, 5, 6, 7];
alert(arr.filter(inBetween(3, 6))); // 3,4,5,6
alert(arr.filter(inArray([1, 2, 10]))); // 1,2
// inBetween 筛选器
function inBetween(a, b) {
return function (x) {
return x >= a && x <= b;
};
}
let arr = [1, 2, 3, 4, 5, 6, 7];
alert(arr.filter(inBetween(3, 6))); // 3,4,5,6
// inArray 筛选器
function inArray(arr) {
return function (x) {
return arr.includes(x);
};
}
let arr = [1, 2, 3, 4, 5, 6, 7];
alert(arr.filter(inArray([1, 2, 10]))); // 1,2
- 按字段排序 我们有一组要排序的对象:
let users = [
{ name: "John", age: 20, surname: "Johnson" },
{ name: "Pete", age: 18, surname: "Peterson" },
{ name: "Ann", age: 19, surname: "Hathaway" },
];
通常的做法应该是这样的:
// 通过 name (Ann, John, Pete)
users.sort((a, b) => (a.name > b.name ? 1 : -1));
// 通过 age (Pete, Ann, John)
users.sort((a, b) => (a.age > b.age ? 1 : -1));
我们可以让它更加简洁吗,比如这样?
users.sort(byField("name"));
users.sort(byField("age"));
function byField(fieldName) {
return (a, b) => (a[fieldName] > b[fieldName] ? 1 : -1);
}
参考
- 学习 Javascript 闭包(Closure)- 阮一峰
- 闭包 - MDN
- JavaScript 深入之闭包
- https://zh.javascript.info/closure