学闭包(closure),应该注意什么?

2,070 阅读5分钟

双优8期|学闭包(closure),应该注意什么?

tips:每个技术点都值得优学优写:8期

好文推荐:

约2万字-Vue源码解读汇总篇(续更)

前端要会打组合拳,复盘30+技术点打出的功能

闭包简述

Mozilla 上这样解释闭包:一个函数和对其周围状态(lexical environment,词法环境) 的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中, 每当创建一个函数, 闭包就会在函数创建的同时被创建出来。 词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。

我对闭包的理解:闭包使得可以模拟私有项,可以使得内部函数可以访问外部函数的属性,非必要不用闭包。

1.闭包使得内部函数可以访问外部函数的属性(变量或方法)。

这有时会带来便利, 例如有时可以通过在外部函数声明变量,代替全局变量。 下面是一个设备视口大小改变时,重置 echarts 的例子。

// 设备视口大小改变时,重置 echarts
let timer = null
window.onresize = function () {
  // 简单的防抖动处理
  if (timer) clearTimeout(timer)
  timer = setTimeout(() => {
    console.log(timer)
    chart.resize()
  }, 500)
}

也可以考虑使用闭包的方式,而不必在声明全局变量(更大范围的变量) timer,例如这样

window.onresize = this.debounce(() => {
  chart.resize()
}, 500)

function debounce (fn, delay = 500) {
  let timer = null
  return (p) => {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn(p)
    }, delay)
  }
}

2.闭包的广阔应用场景。

闭包的广阔应用场景,体现在你使用只有一个方法的对象的地方,都可以使用闭包。

因为闭包允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。 在面向对象编程中,对象允许我们将某些数据(对象的属性)与一个或者多个方法相关联。

而在日常开发中,符合使用闭包的场景其实很常见,因为使用只有一个方法的对象的地方,都可以使用闭包, 而使用也并不太麻烦,加上闭包本身就是 javascript 的重要知识点,这些加起来使得闭包具备了实用的特征。

但如果你不熟练闭包,有更好的替代方案,也不必非要使用,因为实用好用的东西很多, 闭包只是选择之一,为了给自己多一种选择闭包又是要学的。

3.用闭包模拟私有方法。

JavaScript 没有类似 JAVA 那样的将方法声明为私有的原生支持,但我们可以使用闭包来模拟私有方法。 私有方法不仅仅有利于限制对代码的访问,还提供了管理全局命名空间的强大能力, 避免非核心的方法弄乱代码的公共接口部分。

下面的示例展现了如何使用闭包来定义公共函数,并令其可以访问私有函数和变量。这个方式也称为模块模式(module pattern)

window.onload = () => {
  let Counter1 = makeCounter(); // 创建实例1
  let Counter2 = makeCounter(); // 创建实例2

  console.log(Counter1.value()); // value:0
  Counter1.add(); // 调用增加函数,执行加一
  console.log(Counter1.value()); // value:1

  console.log(Counter2.value()); // value:0

  // 注意,实例2的 value 没有受到实例1的影响,也就是说 Counter1 和 Counter2 各自独立。
  // 每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。
  // 然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

  // undefined,Counter1 无法直接访问私有项 privateNumber
  console.log(Counter1.privateNumber);
  // Counter1.changeBy is not a function,Counter1 无法直接访问私有项 changeBy
  console.log(Counter1.changeBy(10));

  // 问私有项无法被访问,这提示我们应关注到以这种方式使用闭包,
  // 提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。
}

/// 声明一个模块:计数器,模块内部包含了两个模拟的私有项 privateNumber 和 changeBy,
// 并返回一个对象,对象内部包含三个属性,分别是 add(),reduce(),value()。
let makeCounter = function () {
  let privateNumber = 0;

  function changeBy (val) {
    privateNumber += val;
  }

  return {
    add: function () {
      changeBy(1);
    },
    reduce: function () {
      changeBy(-1);
    },
    value: function () {
      return privateNumber;
    }
  }
};

在这个例子中,包含两个私有项: 名为 privateCounter 的变量和名为 changeBy 的函数。 这两项都无法在函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。这就是模拟了私有特性。

4.从性能角度考虑,非必要不使用闭包。

关于闭包的性能,我无深入的理解,也无数据证明,但我认为这挺重要的。因此,这里引用一下 Mozilla 的说法:

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的, 因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。 原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。

英文原文:

It is unwise to unnecessarily create functions within other functions if closures are not needed for a particular task, as it will negatively affect script performance both in terms of processing speed and memory consumption.

For instance, when creating a new object/class, methods should normally be associated to the object's prototype rather than defined into the object constructor. The reason is that whenever the constructor is called, the methods would get reassigned (that is, for every object creation).

概括就是一句话,非必要不用闭包。好东西很多闭包只是之一, 当然闭包作为js的重要知识点,作为可能的解决方案之一,学习是必要的。