函数即对象(FunctionAsObject)- Martin Fowler博客


/**
 * 献给我最尊敬的偶像Martin Fowler
 * 原文出处:https://martinfowler.com/bliki/FunctionAsObject.html
 * @author dogstar.huang <chanzonghuang@gmail.com> 2017-03-13
 */

本翻译已征得Martin Fowler同意,并链接在博客原文下方。

函数即对象

在编程中,对象的基本概念是绑定数据和行为。这为编写一系列相关函数时提供了一个公共的数据上下文。它还提供了一个用于操纵数据的接口,让对象可以控制对此数据的访问,使得支持衍生数据和预防无效的数据修改变得容易。很多语言提供了明确的语法来定义类,一如作为对象的定义。但如果你有一个支持一等函数(first-class functions)和闭包的语言,你可以通过函数即对象模式(最初由Eugene Wallingford提出)使用这些构造器创建对象。

这是一个简化的person对象例子,在JavaScript中通过使用函数即对象风格来完成。[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();  

从传统的面向对象视角来看函数即对象:

  • 用提供给构造函数的参数(name)和本地变量(birthday)一起表示person对象的字段。
  • person对象的方法嵌套在构造函数里。像对象方法一样,他们可以随意相互调用并且操作在这些本地作用域变量(字段)里的数据。
  • 构造函数以外没有任何东西可以访问这些变量,保持了数据的封装性。
  • person对象的公共方法就是那些在散列表结果中出现的函数。
  • 任何嵌套在构造函数但没出现在散列表结果里的函数,则是私有方法。
  • 公共方法的名字是散列表结果中的key,而不是在构造函数里的函数名称。我更喜欢保持哈希key和函数名称一致以免混淆(尽管如果需要可以用别名函数)。 [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,”,我可以使用速记属性名字以减少重复。

dogstar

一位喜欢翻译的开发人员,艾翻译创始人之一。

广州