聊聊JavaScript闭包

158 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

我的博客网站,学习react后写的,开源了管理系统及后台WebApi,欢迎查看交流 👉 www.weison-zhong.cn

1 What,闭包是什么

  • MDN文档的定义分析,闭包是一个由函数(a function)及其捆绑引用的状态(references to its state)的组合(the combination); image.png
  • 对这个定义我的理解是闭包是一个函数和这个函数的活动作用域对象组成的整体;

2 How,闭包怎么形成

  • 我们知道,在js中有全局作用域Global,函数作用域Local和块级作用域Block;(关于作用域和作用域链,变量函数声明提升的我有时间单独写一篇文章)
  • 在函数执行前,预编译阶段,就已经创建此函数的本地作用域Local对象,并做变量和函数声明提升,一般情况下当函数执行完后,会断开对这个这个作用域Local对象的引用,随后会自动被浏览器垃圾回收销毁掉;
  • 现在考虑如下情况,函数aout内部定义了一个函数ainner,并且ainner访问了外部函数aout的作用域中的变量a;
// debugger;
var atest = aout();
atest();
function aout() {
  var a = "a";
  function ainner() {
    console.log(2, a);
  }
  console.log(1, a);
  return ainner;
}
  • 此时我们调试发现当执行到var a = "a" 这一行时,ainner已经作为函数声明在aout的作用域中并做了预编译,创建了自己的作用域,可以看到此时ainner函数的词法环境第0位就已经有了闭包对象Closure,它引用了外部函数aout的本地作用域Local对象;
  • 按F9继续调试,可以看到外部函数aout作用域中的变量a完成赋值字符'a'时,内部函数ainner的闭包对象也同步变了;
  • 最后内部函数ainner被return到了全局作用域中的变量atest接收了,此时我们可以看到全局作用域Global对象中的变量atest就是函数ainner,并且其作用域链[[Scopes]]中第0位就是上面创建的闭包对象Closure,其保存了函数aout执行时的本地作用域Local对象,所以即使上一步函数aout执行完毕后,它的作用域Local变量对象仍然被闭包ainner引用,那么浏览器就不会回收它,这个变量对象就会一直存在于全局作用域Global中,这会导致其一直占用着内存直到整个js主进程结束,所以说过度使用或者不正确使用(使用完毕后未赋值null消除引用)闭包会造成内存泄漏;
  • 那如果我们不使用函数aout作用域中的变量呢,还会产生闭包吗?
    • 这里理论上如果从js的执行流程(内部函数ainner直接引用外部函数aout的作用域)来看我觉得是会产生闭包的;
    • 但我试着修改下内部函数ainner,让其直接打印2而不访问变量a,通过Chrome调试发现函数ainner的作用域链中只有一个全局作用域,并没产生闭包,这里我个人猜测是Chrome浏览器为了性能考虑对其做了优化工作,有知道的小伙伴可以在评论区和我分享一下; image.png
  • 然后我试着再修改下aout,再定义一个变量b,在内部函数ainner中只打印变量b,看下会怎样;
    • 发现如期产生了闭包对象,但它并没有直接去引用函数aout的作用域,而是只保留了自身用到的变量b; image.png

3 Why,为什么需要闭包,有什么用

  • 在ES6的块级作用域出现之前,需要闭包来模拟块级作用域才能访问期望的值,例如下面这个例子,在for循环内部我们结合立即执行函数和闭包实现了在绑定事件处理函数时能达到我们期望的效果; image.png
  • 防抖节流函数封装;
  • 函数柯里化;
  • react的useCallback和useMemo,由于react每次触发更新都会调用markUpdateLaneFromFiberToRoot方法回到根节点重新构建fiber树;有兴趣了解的可以查看➡️react源码系列之四 、状态更新;所以这两个API可以利用闭包的特性缓存函数和变量,避免重复创建,结合memo方法可实现避免组件重复render的问题;

小结

  • 利用闭包我们可以实现在一个函数中访问其他函数作用域,但应该谨慎使用,在使用完毕后记得赋值null消除引用以便垃圾回收;
  • 如果发现文章内容有误或者有疑问请点击前往纠错按钮反馈交流,大家互相学习.