[闭包] 怎么和面试官聊闭包,以及闭包在 useEffect 中的体现
WTF is 闭包?
对已经理解了闭包的同学,可以直接看文章底部的实践环节。
闭包这一概念并不是 JS 专有的,早在 1960 年,一位名叫 Peter Landin 的大爷就提出了闭包的概念。
我们先抛开 JS 不谈,看看维基百科对闭包的定义:
a closure is a record
storing a function
together with an environment
.
翻译一下,就是闭包是把一个函数和一个 environment 给连接在了一起。
这里的 environment 在 JS 中,指的是词法环境 (Lexical Environment)
什么是 词法环境 (Lexical Environment)?
函数在被创建的时候,会生成一个对应的词法环境,类似一个小本本,用来记录本地变量和上一层语法环境的引用。 举个例子,下面这段代码会生成一个对应的 Lexical Environment,里面保存了本地变量 phrase,和上层语法环境的引用。
let phrase = "Hello";
alert(phrase);
如果把 Lexical Environment 想象成一个对象,它应该长这样:
{
phrase: "Hello",
outer: null //因为这里是全局环境,所以上层语法环境指向了 null
// alert 不需要记录,因为它是函数调用,不是变量
}
现在,来看一个稍微复杂些的代码:
let phrase;
phrase = "Hello";
phrase = "Bye";
在执行代码之前,这段代码的 Lexical Environment 会记录所有将要
用到的变量,并将它们记录为 <uninitialized>
,当代码一行行开始执行时,变量值会更新,如下图:
我们可以把词法环境想象成一个小本本,每个函数都有自己的小本本,用来记录本地变量和上层函数的小本本的引用。
词法环境和闭包有什么关系?
理解了什么是词法环境后,闭包就很好理解了:
函数 + 词法环境 = 闭包
所以严格来说,只要函数被创建了,就有了闭包。也就是说,闭包的产生不需要内部函数返回函数引用,我们只是通过内部函数来操纵它和它上层的词法环境罢了。
JS 怎么实现闭包的?
JS 通过函数的 [[Scopes]]
来保存上层的词法环境。
实践出真知
例子 1.
基本的闭包
// 这里是 global 词法环境
function makeWorker() {
// 这里是 makeWorker 的词法环境,它持有 global 语法环境的引用
// makeWorker 词法环境变量
let name = "Pete";
return function () {
// 这个函数也有自己的词法环境
console.log(name);
};
}
// global 词法环境变量
let name = "John";
// function 也是 global 词法环境的变量
let work = makeWorker();
work(); // 结果: Pete
调用 work()
→ 调用 makeWorker()
→ 在 work
的词法环境中没有找到 name
→ 找上一层的词法环境 → 在 makeWorker
的词法环境中找到 name
→ 打印 Pete
例子 2.
和 OOP 中实例化 class 类似,同一个函数多次调用会创建不同的词法环境。
// 这里是 global 词法环境
function makeCounter() {
// 这里是 makeCounter 词法环境,每次调用 makeCounter 会创建一个新的词法环境
let count = 0;
return function () {
// 返回的函数也有自己的词法环境
return count++;
};
}
let counter = makeCounter();
let counter2 = makeCounter();
// 调用 counter(),会在 counter 的词法环境中找变量
console.log(counter()); // 0
console.log(counter()); // 1
// 调用 counter2(),会在 counter2 的词法环境中找变量
console.log(counter2()); // 0
console.log(counter2()); // 1
例子 3.
// global 词法环境
function Counter() {
// Counter 的词法环境
let count = 0;
this.up = function () {
// up 的词法环境
return ++count;
};
this.down = function () {
// down 的词法环境
return --count;
};
}
// new 关键字会创建一个对象,并把 this 指向这个对象
let counter = new Counter();
// 1. counter.up 获取 up 函数
// 2. 调用 up 函数,up 的词法环境中没有 count,去 Counter 的词法环境找,找到 count = 0,然后 count++ 并返回,此时 Counter 的词法环境中的 count = 1
console.log(counter.up()); // 1
// 1. counter.up 获取 up 函数
// 2. 调用 up 函数,up 的词法环境中没有 count,去 Counter 的词法环境找,注意,此时的 Counter 词法环境是同一个,找到 count = 1,然后 count++ 并返回,此时 count = 2
console.log(counter.up()); // 2
// 同上
console.log(counter.down()); // 1
例子 4.
"use strict";
let phrase = "Hello";
if (true) {
let user = "John";
// sayHi 只在当前的 scope 中可见
function sayHi() {
console.log(`${phrase}, ${user}`);
}
}
// 在当前 scope 中找不到 sayHi
sayHi(); // sayHi not defined
例子 5.
let x = 1;
function func() {
console.log(x); // func 的词法环境中有 x,目前 x 为 uninitialized 状态
let x = 2;
}
func(); // error, access 'x' before init
面试官:简单解释一下闭包是什么?
一句话说,闭包是一个函数和它的词法环境的组合,所有函数都是闭包。
从原理角度来说, 函数内有个 [[Scopes]]
属性,它用来保存闭包中的值和上层词法环境的引用。操作闭包,就是通过 [[Scopes]]
属性操作词法环境。
useEffect 中的闭包
在 React 中实现计时器。一个常见的错误代码实现如下:
// global 词法环境
export default function App() {
// App 词法环境
let [count, setCount] = useState(0);
useEffect(() => {
// useEffect 回调函数的词法环境
let id = setInterval(() => {
// setInterval 回调函数的词法环境
setCount(count + 1); // count is always 0
}, 1000);
return () => {
// return 函数的词法环境
clearInterval(id);
};
}, []);
return <div>{countRef.current}</div>;
}
这段代码中的 count
会卡在 0,网上都只说是因为 useEffect
中的闭包机制导致的,没有再往下深挖了,那么具体是什么原因导致的呢?
我们来从词法环境的角度分析一下这段代码:
App
生成词法环境useEffect
中的回调生成词法环境,持有上层词法环境的引用,也就是App
的词法环境setInterval
中的回调生成词法环境,持有上层词法环境的引用,也就是useEffect
的回调函数的词法环境- 组件准备挂载 → 调用
App
→ 调用useEffect
的回调函数 →setInterval
开始每秒调用一次 - 调用
setCount(0 + 1)
→ 调用App
,App 生成新的词法环境,此时,setInterval
的回调函数持有的还是之前的 App() 的词法环境,也就是count = 0
的那个词法环境,导致setCount
拿到的count
还是 0
简单总结一下,App
函数每运行一次就会产生一个新的词法环境,useEffect
内的回调函数持有了 App
旧的词法环境,导致 count
始终是 0。
References
转载自:https://juejin.cn/post/7228526570993778746