原文链接:medium.com/dailyjs/i-n… ,笔者只是为方便查阅笔记而写,想要进一步了解可以查阅原文。
对闭包通俗的理解:闭包的方法是通过背包的类比。当一个函数被创建并传递或从另一个函数返回时,它会携带一个背包。背包中是函数声明时作用域内的所有变量。
看看下面的代码,并试着弄清楚会发生什么。
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
现在,我们已经从前两个示例中掌握了它的诀窍,让我们按照预期的方式快速执行它:
- 第
1-8行。我们在全局执行上下文中创建了一个新的变量createCounter,并赋值了一个的函数定义。 - 第
9行。我们在全局执行上下文中声明了一个名为increment的新变量。 - 第
9行。我们需要调用createCounter函数并将其返回值赋给increment变量。 - 第
1-8行。调用函数,创建新的本地执行上下文。 - 第
2行。在本地执行上下文中,声明一个名为counter的新变量并赋值为0; - 第
3-6行。声明一个名为myFunction的新变量,变量在本地执行上下文中声明,变量的内容是为第4行和第5行所定义。 - 第7行。返回
myFunction变量的内容,删除本地执行上下文。变量myFunction和counter不再存在。此时控制权回到了调用上下文。 - 第
9行。在调用上下文(全局执行上下文)中,createCounter返回的值赋给了increment,变量increment现在包含一个函数定义内容为createCounter返回的函数。它不再标记为myFunction````,但它的定义是相同的。在全局上下文中,它是的标记为labeledincrement```。 - 第
10行。声明一个新变量c1。 - 继续第
10行。查找increment变量,它是一个函数并调用它。它包含前面返回的函数定义,如第4-5行所定义的。 - 创建一个新的执行上下文。没有参数,开始执行函数。
- 第
4行。counter=counter + 1。在本地执行上下文中查找counter变量。我们只是创建了那个上下文,从来没有声明任何局部变量。让我们看看全局执行上下文。这里也没有counter变量。Javascript会将其计算为counter = undefined + 1,声明一个标记为counter的新局部变量,并将其赋值为number 1,因为undefined被当作值为 0。 - 第
5行。我们变量counter的值1,我们销毁本地执行上下文和counter变量。 - 回到第
10行。返回值1被赋给c1。 - 第
11行。重复步骤10-14,c2也被赋值为1。 - 第
12行。重复步骤10-14,c3也被赋值为1。 - 第
13行。我们打印变量c1 c2和c3的内容。
你自己试试,看看会发生什么。你会将注意到,它并不像从我上面的解释中所期望的那样记录1,1,1。而是记录1,2,3。这个是为什么?
不知怎么滴,increment函数记住了那个cunter的值。这是怎么回事?
counter是全局执行上下文的一部分吗?尝试 console.log(counter),得到undefined的结果,显然不是这样的。
也许,当你调用increment时,它会以某种方式返回它创建的函数(createCounter)?这怎么可能呢?变量increment包含函数定义,而不是函数的来源,显然也不是这样的。
所以一定有另一种机制。闭包,我们终于找到了,丢失的那块。
它是这样工作的,无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包。闭包包含在函数创建时作用域中的所有变量,它类似于背包。函数定义附带一个小背包,它的包中存储了函数定义创建时作用域中的所有变量。
所以我们上面的解释都是错的,让我们再试一次,但是这次是正确的。
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
- 同上,第
1-8行。我们在全局执行上下文中创建了一个新的变量createCounter,它得到了指定的函数定义。 - 同上,第
9行。我们在全局执行上下文中声明了一个名为increment的新变量。 - 同上,第
9行。我们需要调用createCounter函数并将其返回值赋给increment变量。 - 同上,第
1-8行。调用函数,创建新的本地执行上下文。 - 同上,第
2行。在本地执行上下文中,声明一个名为counter的新变量并赋值为0。 - 第
3-6行。声明一个名为myFunction的新变量,变量在本地执行上下文中声明,变量的内容是另一个函数定义。如第4行和第5行所定义,现在我们还创建了一个闭包,并将其作为函数定义的一部分。闭包包含作用域中的变量,在本例中是变量counter(值为0)。 - 第
7行。返回myFunction变量的内容,删除本地执行上下文。myFunction和counter不再存在。控制权交给了调用上下文,我们返回函数定义和它的闭包,闭包中包含了创建它时在作用域内的变量。 - 第
9行。在调用上下文(全局执行上下文)中,createCounter返回的值被指定为increment,变量increment现在包含一个函数定义(和闭包),由createCounter返回的函数定义,它不再标记为myFunction,但它的定义是相同的,在全局上下文中,称为increment。 - 第
10行。声明一个新变量c1。 - 继续第
10行。查找变量increment,它是一个函数,调用它。它包含前面返回的函数定义,如第4-5行所定义的。(它还有一个带有变量的闭包)。 - 创建一个新的执行上下文,没有参数,开始执行函数。
- 第
4行。counter = counter + 1,寻找变量counter,在查找本地或全局执行上下文之前,让我们检查一下闭包,瞧,闭包包含一个名为counter的变量,其值为0。在第4行表达式之后,它的值被设置为1。它再次被储存在闭包里,闭包现在包含值为1的变量counter。 - 第
5行。我们返回counter的值,销毁本地执行上下文。 - 回到第
10行。返回值1被赋给变量c1。 - 第
11行。我们重复步骤10-14。这一次,在闭包中此时变量counter的值是1。它在第12行设置的,它的值被递增并以2的形式存储在递增函数的闭包中,c2被赋值为2。 - 第
12行。重复步骤10-14行,c3被赋值为3。 - 第13行。我们打印变量
c1 c2和c3的值。
你可能会问,是否有任何函数具有闭包,甚至是在全局范围内创建的函数?答案是肯定的。在全局作用域中创建的函数创建闭包,但是由于这些函数是在全局作用域中创建的,所以它们可以访问全局作用域中的所有变量,闭包的概念并不重要。
当函数返回函数时,闭包的概念就变得更加重要了。返回的函数可以访问不属于全局作用域的变量,但它们仅存在于其闭包中。
闭包不是那么简单
有时候闭包在你甚至没有注意到它的时候就会出现,你可能已经看到了我们称为部分应用程序的示例,如下面的代码所示:
let c = 4
const addX = x => n => n + x
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)
如果箭头函数让你感到困惑,下面是同样效果:
let c = 4 function addX(x) {
return function(n) {
return n + x
}
}
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)
我们声明一个能用加法函数addX,它接受一个参数x并返回另一个函数。返回的函数还接受一个参数并将其添加到变量x中。
变量x是闭包的一部分,当变量addThree在本地上下文中声明时,它被分配一个函数定义和一个闭包,闭包包含变量x。
所以当addThree被调用并执行时,它可以从闭包中访问变量x以及为参数传递变量n并返回两者的和 7。