闭包 closures 之所以难以理解,是因为它是一个隐形概念。它本身就是一个晦涩的名词,但很多文章却喜欢用同样晦涩的名词来解释它。(函数一等公民、执行上下文、词法作用域、作用域链等等)
闭包的定义
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。 — 红宝书第四版
你要是愿意,大可读到这里为止
接下来的部分并不会再去解释什么是闭包,而是引导你完成发现闭包的过程 —— 就像 1960 年代第一批程序员所做的那样,我们假设那时候发明的第一个语言就是 JS。
🥚函数的诞生🐥
// 当我们有一串代码, 表示吃 🍞
let food = '🍞';
console.log(`${food} is good.`);
// > 🍞 is good
当我们有这样的一串代码,一旦我们想让它不止运行一次,怎么办呢?
一种方式就是复制粘贴;
第二种方式就是使用循环体;
for (let i = 0; i < 2; i++) {
let food = '🍞';
console.log(`${food} is good.`);
}
// > 🍞 is good
// > 🍞 is good
第三种方式是我们现在更中意的,就是使用函数体,把代码包装起来
function eat() {
let food = '🍞';
console.log(`${food} is good.`);
}
eat();
eat();
// > 🍞 is good
// > 🍞 is good
使用函数 function eat()
给我们的代码带来了终极的灵活性,因为我们可以在任意时间,在我们程序的任意地方,调用函数任意多次。
也就是说,程序中函数 function
的存在,使得我们的代码块可以更加高效且灵活,然而,过不了多久,你就会发现这种灵活度的似乎还不够。
🔭 函数想要点变数 📺
- 函数能访问它函数作用域内的变量
function eat() {
let food = '🍞';
console.log(`${food} is good`);
}
eat();
eat();
eat();
// > 🍞 is good
// > 🍞 is good
// > 🍞 is good
- 通过入参,函数将变得灵活了一些
function eat(food) {
console.log(`${food} is good`);
}
eat('🥛');
eat('🍎');
eat('🍲');
// > 🥛 is good
// > 🍎 is good
// > 🍲 is good
- 我们试着让**函数能访问到它声明处旁边的变量,**好像一切都理所当然
let food = '🍞';
function eat() {
console.log(food + ' is good');
}
eat();
food = '🥛';
eat();
food = '🍎';
eat();
food = '🍲';
eat();
// > 🍞 is good
// > 🥛 is good
// > 🍎 is good
// > 🍲 is good
这样,如果我们想 eat
不同的 food
,只需要在 eat
之前,改变 food
变量的值就可以。
**函数能访问外部变量,**只要求你理解了这一点,我们就开始讲下一步。
🕵️发现闭包👜
我们已经有两条线索
- 如果我们想复用我们的代码,可以将这串代码包裹在
function
当中 function
函数可以访问定义在它外面的变量
那么把两者进行碰撞,会发生什么?没错,我说的是把 function
定义的这串代码块,再用一个 function
给包起来。
依旧使用这个 eat
函数
let food = '🍞';
function eat() {
console.log(food + ' is good');
}
eat();
// > 🍞 is good
现在我将上面这个代码块,统统用一个 function
包起来。
function liveADay() {
let food = '🍞';
function eat() {
console.log(food + ' is good');
}
eat();
}
liveADay();
// > 🍞 is good
这两种代码生效了,而且执行一次后的效果都一样。
这种方式执行函数能生效,需要得益于 js 的语法是支持函数嵌套的。很多语言这种函数嵌套的写法直接非法。就比如 C 中上述这样的代码就是不合法的(C 也就没有闭包的概念),也就是说,在一些像 C 这样的语言中,我们不能随随便便的把一串代码用 function
包起来,而在 javascript 就没有这种限制。
我们再去尝试在 eat
函数中寻找闭包:
我们来一步一步解析这个函数。首先在顶部我们声明一个 liveADay
函数,随即就调用了它。这个函数有一个本地变量 food
,还有一个 eat
函数,这个函数在 liveADay
内部被执行了。因为 eat
函数在 liveADay
里面,所以它能访问到 food
变量,所以函数运行成功了。这就是一个典型的闭包。
为何要有“闭包”
我觉得是给闭包下定义的书籍,曾经给人们,至少给我带来了迷惑,即说“闭包”就是为了函数能访问另一个函数的内部的作用域。
是的,“闭包”似乎做到了能访问另一个函数内部作用域,但是本质上,这还是 JS 函数有能力引用其外部声明的变量的另一个说法而已,你说它访问到了“另一个函数内部”,难道不仅仅是因为它也被声明在了那个函数的内部吗?
隔壁老牌优等生 C 语言都馋哭了,它根本就不允许嵌套函数。因此,这门语言的函数就只能访问它们的本地变量和全局变量,这种限制可谓是痛苦的。
闭包不是 JS 所独有的,其他语言如 golang、python 和 php 等也都实现了闭包,有的语言如 Java 管“闭包”叫 lambda
表达式。
package main
import "fmt"
func fib() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
func main() {
f := fib()
// Function calls are evaluated left-to-right.
fmt.Println(f(), f(), f(), f(), f())
}
#闭包函数,其中 exponent 称为自由变量
def nth_power(exponent):
def exponent_of(base):
return base ** exponent
return exponent_of # 返回值是 exponent_of 函数
square = nth_power(2) # 计算一个数的平方
cube = nth_power(3) # 计算一个数的立方
print(square(2)) # 计算 2 的平方
print(cube(2)) # 计算 2 的立方
“闭包”真正的作用:
我这里还有一幅图:
- 打包上下文:函数会尽可能的利用它的访问变量的能力,收集一系列的环境内的变量,将它们统统打包到一个函数体里面。
- 延迟调用:你可以把这个打包的函数体作为一个函数的返回值,
return
出去。这个函数返回值是一个包,它携带着它打包好的上下文变量,随时等待你的调用。
最后我想说
闭包这个概念很宽泛,我们大可不必揪着它不放。就如有些人认为 JS 的函数嵌套就是闭包,而有些人则认为 JS 函数的这种能访问外部变量的能力就是闭包。事实上,管他呢。个人认为只是因为“闭包”这个名词,相对于“全局作用域”,“本地作用域”更加神秘隐晦罢了,是刻意考它和刻意学它的人让它变得更加模糊不清。