# 对 JavaScript 闭包的回顾

闭包,老生常谈的话题之一了,面试中我们经常被问到,在平时写代码的时候我们或多或少也是会用到他。

# 什么是闭包

  • mdn (opens new window)的解释是:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
  • 实际例子
function init() {
  var name = 'closure'; // name 是一个被 init 创建的局部变量
  function displayName() {
    // displayName() 是内部函数,一个闭包
    alert(name); // 使用了父函数中声明的变量,但是name又不存在于displayName函数的局部变量中
  }
  return displayName;
}
const initFn = init();
initFn();

有的同学可能会辩论这不是一个闭包,但是你不相信我,mdn (opens new window)可以相信吧哈哈哈,其实我想说不认同这个例子的同学,可能只是局限于对闭包的偏见,为什么呢?

  • 在我的理解中,我更愿意把闭包归结为可以突破变量局部作用域的界限,拿到不属于本身作用域内的变量的函数
  • 举个栗子把:子函数访问父函数的变量,由于父函数的变量存在引用,即使创建它的上下文已经销毁,它仍然存在(内部函数从父函数中返回),从而形成了闭包。闭包就是指有权访问另一个函数作用域中的变量的函数(返回函数也是闭包函数,不能释放内存)
  • 让我们再来一个栗子把,依然是来权威的《JavaScript 权威指南》
const param = 'bigParam';
function getParam() {
  const param = 'smallParam';
  return function f() {
    return param;
  };
}

const documentFn = getParam();
documentFn(); //smallParam

在这段代码运行时,执行上下文栈和执行上下文的逻辑如下:

  1. 创建全局执行上下文,并将其入栈
  2. 执行 getParam 函数,创建 getParam 函数执行上下文并将其入栈
  3. getParam 执行上下文初始化,创建变量对象、作用域链、this 等
  4. getParam 函数执行完毕,getParam 执行上下文出栈
  5. 执行 documentFn 函数,创建 documentFn 函数执行上下文并将其入栈
  6. documentFn 执行上下文初始化,创建变量对象、作用域链、this 等
  7. documentFn 函数执行完毕,documentFn 执行上下文出栈

那么,当 getParam 上下文出栈即被销毁时,为何我们还能通过 documentFn 读取到 param 值为 smallParam 呢? 这就得涉及到我们的垃圾回收机制了。

  • 在垃圾回收机制中有一个策略是引用计数算法,它把对象是否不再需要 简化定义为 对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收,那么我们这里存在对 getParam 函数作用域内 param 的引用,自然这个变量就会被保留下来,从而实现了闭包这个概念。

# 闭包的优点

  • 延迟变量的生命周期,如果执行上下文不被销毁,这个变量就可以一直存在
  • 形成私有作用域,使得某些变量不受外部干扰
  • 突破函数独立作用域,可以从外部拿到里面的变量

# 缺点

  • 显而易见的是内存泄漏,因为绕过了垃圾回收机制,这个变量一直存在,过度使用闭包会导致内存占用过多

# 闭包的应用场景(这里仅个人理解)

  • return 一个引用父级变量的函数
  • 函数作为参数
  • 自调用函数
  • 循环赋值
  • 回调函数
  • 防抖节流
  • 函数柯里化

# 简单补充 JS 堆栈

  • 堆内存:存储引用类型值,对象类型就是键值对,函数就是代码字符串。
  • 栈内存:提供代码执行的环境和存储基本类型值。
  • 堆内存释放:将引用类型的空间地址变量赋值成 null,或没有变量占用堆内存了浏览器就会释放掉这个地址
  • 栈内存释放:一般当函数执行完后函数的私有作用域就会被释放掉。
Last Updated: 6/2/2022, 3:50:31 AM