likes
comments
collection
share

[闭包] 怎么和面试官聊闭包,以及闭包在 useEffect 中的体现

作者站长头像
站长
· 阅读数 51

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>,当代码一行行开始执行时,变量值会更新,如下图:

[闭包] 怎么和面试官聊闭包,以及闭包在 useEffect 中的体现

我们可以把词法环境想象成一个小本本,每个函数都有自己的小本本,用来记录本地变量和上层函数的小本本的引用。

词法环境和闭包有什么关系?

理解了什么是词法环境后,闭包就很好理解了: 函数 + 词法环境 = 闭包

所以严格来说,只要函数被创建了,就有了闭包。也就是说,闭包的产生不需要内部函数返回函数引用,我们只是通过内部函数来操纵它和它上层的词法环境罢了。

[闭包] 怎么和面试官聊闭包,以及闭包在 useEffect 中的体现

JS 怎么实现闭包的?

JS 通过函数的 [[Scopes]] 来保存上层的词法环境。

[闭包] 怎么和面试官聊闭包,以及闭包在 useEffect 中的体现

实践出真知

例子 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 中的闭包机制导致的,没有再往下深挖了,那么具体是什么原因导致的呢?

我们来从词法环境的角度分析一下这段代码:

  1. App 生成词法环境
  2. useEffect 中的回调生成词法环境,持有上层词法环境的引用,也就是 App 的词法环境
  3. setInterval 中的回调生成词法环境,持有上层词法环境的引用,也就是 useEffect 的回调函数的词法环境
  4. 组件准备挂载 → 调用 App → 调用 useEffect 的回调函数 → setInterval 开始每秒调用一次
  5. 调用 setCount(0 + 1) → 调用 AppApp 生成新的词法环境,此时, setInterval 的回调函数持有的还是之前的 App() 的词法环境,也就是 count = 0 的那个词法环境,导致 setCount 拿到的 count 还是 0

简单总结一下,App 函数每运行一次就会产生一个新的词法环境,useEffect 内的回调函数持有了 App 旧的词法环境,导致 count 始终是 0。

References

javascript.info/closure#nes… en.wikipedia.org/wiki/Closur…

转载自:https://juejin.cn/post/7228526570993778746
评论
请登录