JS 闭包
从 JS 中的变量说起#
JS 中的变量就像一个盒子,它里面可以装 true、"字符串"、666、[1, { object: {} }, null] 等各类数据。
console.log执行的时候,foo这个箱子装的是{}
执行
箱子 foo装的是{},在执行到 line:2 的时候,print 函数绑定了箱子 foo,但不会绑定箱子 foo中装的东西
所以,变量跟变量所代表的值是两个东西,代码只有执行到使用变量的语句时,才会看这个变量箱子里装了什么。
关于闭包#
让我们从一个简单的例子开始,假设我们有一个类,叫 Person,需要有姓名、性别,且性别不可更改
在 JavaScript 还没有类、属性访问控制器的时候,我们要如何做到某个属性的访问控制呢?
在上面的例子中,createPerson 函数在执行时,有它自己的 FunctionScope(函数作用域)。return 的对象中有 4 个 函数:getName、setName、getGender、setGender。这四个函数又分别产生了自己的 FunctionScope。在 JS 中,函数作用域的变量是无法在外部被访问的,所以 _name、_gender 外界无法直接读写,做到了访问控制。
当我们执行 getGender 方法时,由于 getGender 的 FunctionScope 自身没有 _gender 这个变量,所以会往父作用域查询是否有叫 _gender 的变量。
总结闭包的要点#
- 父作用域无法访问子作用域的变量,子作用域可以访问父作用域的变量。在函数声明时,有一个俗称绑定作用域的过程。可以简单理解为把所有父作用域的箱子都记住,之后在执行的时候,如果碰到名字叫 A 的箱子,就从离自己近的箱子开始找名称为 A 的箱子
- 每次函数执行时,都会创建一个全新的闭包对象,与上一次函数执行时的闭包对象完全独立
闭包作用域就是函数作用域+父作用域集合。闭包变量就是闭包作用域中的变量集合
对于上面函数 son 来讲,闭包变量有firstName=Son、firstName=Daddy、firstName=grandpa、lastName=A(从近到远排列,放入本函数作用域的参数表)。在访问 firstName 和 lastName 时,分别按照从近到远的方式查找。
继续阅读 可视化 v8 引擎管理内存 了解更多函数执行时的作用域、内存分配相关知识,加深对闭包的理解。
React 中常见的闭包场景#
React Hooks 的 API 设计存在大量闭包,追踪闭包变量是所有函数式编程的基础。
按 F12 打开 DevTools,查看 console 打印的 count1 是几
上面的代码由于 useEffect 函数的第二个传参是空数组,所以 useEffect 只会在首次渲染的时候执行。函数只执行一次,所以绑定的 count1 是首次渲染时的父函数作用域的 count1=0。所以,console 打印的一直是 0。
让我们稍微修改一下代码:
count2 能正确更新到点击数,思考一下为什么?
当我们把 count 的作用域放到组件的外面时,每次组件函数执行时,绑定的父作用域的 count 没有改变,所以在修改之后,会影响到 console 的打印结果