什么是 - 闭包 closures

126 阅读5分钟

闭包 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 函数中寻找闭包:

截屏2021-07-09 下午8.40.53.png

我们来一步一步解析这个函数。首先在顶部我们声明一个 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 的立方

“闭包”真正的作用:

我这里还有一幅图: 截屏2021-07-09 下午9.45.00.png

  • 打包上下文:函数会尽可能的利用它的访问变量的能力,收集一系列的环境内的变量,将它们统统打包到一个函数体里面。
  • 延迟调用:你可以把这个打包的函数体作为一个函数的返回值,return 出去。这个函数返回值是一个包,它携带着它打包好的上下文变量,随时等待你的调用。

最后我想说

闭包这个概念很宽泛,我们大可不必揪着它不放。就如有些人认为 JS 的函数嵌套就是闭包,而有些人则认为 JS 函数的这种能访问外部变量的能力就是闭包。事实上,管他呢。个人认为只是因为“闭包”这个名词,相对于“全局作用域”,“本地作用域”更加神秘隐晦罢了,是刻意考它和刻意学它的人让它变得更加模糊不清。