函数作为对象

2017年2月13日

在编程中,对象的根本概念是将数据和行为捆绑在一起。这在编写一组相关函数时提供了通用的数据上下文。它还提供了一个用于操作数据的接口,允许对象控制对该数据的访问,从而易于支持派生数据并防止对数据的无效修改。许多语言提供明确的语法来定义类,这些类充当对象的定义。但是,如果您使用的是具有头等函数和闭包的语言,则可以使用这些结构通过函数作为对象模式(最初由 Eugene Wallingford 描述)来创建对象。

以下是用 JavaScript 使用函数作为对象风格实现的简单 person 对象的示例。 [1]

function createPerson(name) {
  let birthday;
  return {
    name: () => name,
    setName: (aString) => name = aString,
    birthday: () => birthday,
    setBirthday: (aLocalDate) => birthday = aLocalDate,
    age: age,
    canTrust: canTrust,
  };
  function age() {
    return birthday.until(clock.today(), ChronoUnit.YEARS);
  }
  function canTrust() {
    return age() <= 30;
  }
}

函数作为对象的外部形式是一个函数,它被调用为构造函数。调用的结果本质上是一个函数的哈希映射 [2],它充当方法选择器。此映射捕获函数中任何变量的状态,使其在闭包中持久存在,允许数据在单个函数调用之外持续存在。此结果哈希映射可以像经典对象一样对待。

const kent = createPerson("kent");
kent.setBirthday(LocalDate.parse("1961-03-31"));
const youngEnoughToTrust = kent.canTrust();

从经典 OO 的角度来看函数作为对象

  • 对象的字段由构造函数的参数 (name) 以及局部变量 (birthday) 表示。
  • 对象的方法是嵌套在构造函数中的函数。与对象方法一样,它们可以自由地相互调用并操作这些局部作用域变量(字段)中的数据
  • 构造函数外部的任何内容都无法访问这些变量,从而保留数据封装。
  • 对象的公共方法是结果哈希映射中存在的那些函数。
  • 任何嵌套在构造函数中但不在结果哈希映射中的函数都是私有方法
  • 公共方法的名称是结果哈希映射的键,而不是构造函数中函数的名称。我更喜欢保持键和函数名称相同,以避免混淆(尽管在需要时为函数创建别名可能很方便)。 [3]

此模式的常见替代实现是返回一个函数作为方法选择器,而不是哈希映射,哈希映射是 JavaScript 中的自然方法选择器。要使用函数作为方法选择器,我将返回一个函数,该函数的第一个参数是要调用的方法的名称。函数体然后根据该值进行切换(有关更多信息,请参阅 Wallingford)。

函数作为对象的方法已经存在很长时间了,我多次在 lisp 中看到过它的描述,并且它在 JavaScript 中被广泛使用(在 ES6 之前,JavaScript 对类的概念非常有限)。它通常被用作特定类语法不必要的论据,这相当于对象爱好者争论说,当您可以使用单个“call”方法编写类时,您不需要头等函数。因此,JavaScript 世界中的许多人反对使用 ES6 类语法。就我个人而言,我喜欢同时拥有头等函数和头等类,并且更喜欢 ES6 的类语法。

进一步阅读

Eugene Wallingford 在他的 1999 年模式语言“Envoy” 中创造了“函数作为对象”这个名称。他的论文值得阅读,以了解更多关于此方面的细节,包括使用函数作为方法选择器和委托来支持某种继承概念。论文中的示例使用 Scheme。

致谢

Chris Ford、Fred George、James Shore、Kevin Yeung、Lucas Lego、Matteo Vaccari、Rob Miles 和 Eugene Wallingford 对本文草稿发表了评论

笔记

1: 对于日期处理,我使用的是 js-joda,它是 Joda-Time 库的移植版本,它清理了 Java 日期和时间处理的糟糕混乱。我很高兴 joda-js 正在重复为日期和时间处理带来理智的服务。

2: 在 JavaScript 术语中,它被称为对象,尽管它是一个 JavaScript 对象,而不是我们试图创建的经典对象。因此,我将称之为哈希映射,以尽量减少混淆。

3: 在 ES6 中,我可以使用简写属性名称来消除重复,将“age: age,”替换为“age,”。