1 概念的提出
如果推断如下的代码的输出结果:
function createCounter() {
let counter = 0
const myFunction = function() {
counter = counter + 1
return counter
}
return myFunction
}
const increment = createCounter()
const c1 = increment()
const c2 = increment()
const c3 = increment()
console.log('example increment', c1, c2, c3)
调用 createCounter()
返回一个函数,因此 increment
是一个函数,调用 increment()
时,按照一般的理解,由于函数体内没有声明 counter
变量,counter = counter + 1
就相当于 counter = undefined + 1
相当于 counter = 1
,按照以上推断,输出也许是:
// 错误 !!
1
1
1
但实际上正确的输出是:
1
2
3
输出之所以是后者,是因为 JavaScript 的闭包机制。
(一个可视化JavaScript 执行过程的网站: JavaScript coding tutor - Learn JavaScript by visualizing code execution )
2 什么是闭包
2.1 词法环境 (Lexical Environment)
在 JavaScript 中,每个正在运行的函数、代码块 {...}
和整个脚本都有一个内部(隐藏的)关联对象,称为词法环境。
词法环境对象由两部分组成:
-
Environment Record – an object that stores all local variables as its properties (and some other information like the value of
this
).
环境记录——一个将所有局部变量 (以及一些其他信息,如this
的值) 存储为其属性的对象。 -
A reference to the outer lexical environment, the one associated with the outer code.
一个指向外部词法环境的引用,它与外部代码相关联。
2.2 内部和外部词法环境(Inner and outer Lexical Environment)
函数调用期间具有有两个词法环境:内部此法环境和外部词法环境。
外部词法环境具有变量以及函数自身,内部词法环境具有一个指向外部词法环境的引用。
当代码想要访问一个变量时——首先搜索内部词法环境,然后是外部词法环境,然后是更外部的词法环境,依此类推,直到全局词法环境。
当函数开始运行时,会自动创建一个新的词法环境来存储调用的局部变量和参数。
所有函数都会记住它们被创建时的所在的词法环境,所有函数都有一个名为 [[Environment]]
的隐藏属性,持有对创建该函数的词法环境的引用。 [[Environment]]
引用在函数创建时被一次性设置,之后永不会再变。
2.3 闭包
闭包是一个函数,此函数记住了它所有的外部变量,并且能访问这些变量。每一个 JavaScript 函数都是一个闭包。
Closure is the concept that allows a returned inner function to access the lexical scope in which it was created in.
闭包是允许返回的内部函数访问创建它的词法范围的概念。
A closure is the ability of a function to remember the environment it was defined in so when it’s called in another scope, it can access the variables that were in the environment.
闭包是函数能够记住定义它的环境的能力,因此当在另一个范围内调用它时,它可以访问之前的环境中的变量。
比喻: 闭包就好比背着小背包的函数,小背包内装的是创建该函数时作用域内的所有变量。每一个 JavaScript 函数都有一个这样的小背包,每一个JavaScript 函数都是一个闭包。
原文:
The key to remember is that when a function gets declared, it contains a function definition and a closure. The closure is a collection of all the variables in scope at the time of creation of the function.
When a function gets created and passed around or returned from another function, it carries a backpack with it. And in the backpack are all the variables that were in scope when the function was declared.
以下面的例子为例:
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
所有的函数被创建时都会获得一个隐藏属性 [[Environment]]
指向它们被创建时所在的词法环境。
在调用函数 makeCounter()
时,创建了一个小的嵌套函数。
函数不管是使用函数声明还是函数表达式创建的,都将获得 [[Environment]]
属性,该属性是一个指向创建它们的词法环境的引用。 所以新的小嵌套函数也得到了它。
对于新嵌套函数,[[Environment]]
的值是 makeCounter()
的当前词法环境(它诞生的地方)
本文一开始的例子:
function createCounter() {
let counter = 0
const myFunction = function() {
counter = counter + 1
return counter
}
return myFunction
}
const increment = createCounter()
const c1 = increment() // increment 能够访问 counter,其值为 0, 然后 counter 被更新成 1
const c2 = increment() // increment 能够访问 counter,其值为 1, 然后 counter 被更新成 2
const c3 = increment() // increment 能够访问 counter,其值为 2, 然后 counter 被更新成 3
console.log('example increment', c1, c2, c3) // example increment 1 2 3
3 闭包 use cases
-
信息隐藏
-
绑定 event handler 到元素
4 闭包的若干例子
4.1 对象数组排序
let users = [
{ name: "John", age: 20, surname: "Johnson" },
{ name: "Pete", age: 18, surname: "Peterson" },
{ name: "Ann", age: 19, surname: "Hathaway" }
];
function byField(field) {
return function(a, b) {
return a[field] > b[field] ? 1 : -1
}
}
users.sort(byField('name'));
console.log(users)
users.sort(byField('age'));
console.log(users)
4.2 函数作为 filter
参数
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
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
4.3 一个 Counter
函数
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
4.4 一组函数
function makeArmy() {
let shooters = [];
// 每次循环都将创建一个新的词法环境
for (let i = 0; i < 10; i++) {
let shooter = function() { // create a shooter function,
console.log(i); // that should show its number
};
shooters.push(shooter); // and add it to the array
}
// ...and return the array of shooters
return shooters;
}
let army = makeArmy();
// all shooters show 10 instead of their numbers 0, 1, 2, 3...
army[0](); // 10 from the shooter number 0
army[1](); // 10 from the shooter number 1
army[2](); // 10 ...and so on.
4.5 油鹳上的例子
// Simple Closure
function f1(a) {
let b = 2;
setTimeout(function() {
console.log(a, b)
}, 1000)
// Closure with a problem
// 将 var 改成 let 就 ok,因为 var 作用域是整个 function
// 错误输出是: 3, 3, 3, 正确输出是 0, 1, 2
function f2() {
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i)
}
}
}
第2篇和第3篇最详细: