函数作为对象
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,
”。