Wayne Wang Stay Curious

ECMA-262-3,面向对象编程的基本理论(译)

Jan. 22, 2015 - Shang Hai

译者前言: 最近学习Javascript,便想深入了解其面向对象编程的诸多概念,于是便发现了Dmitry Soshnikov的关于ECMA-262 的系列文章,写的非常的深入,看的我是昏天地暗,神魂颠倒,哈。第一次尝试翻译这样的技术文章,主要目的如下,

  • 为了更好的理解文章讲述的内容。
  • 为了记录初试学习的成果,便于回顾。
  • 为了不再懒惰,多写点东西。

对于本文翻译,有些写在前头的话,

  • 在有些句子或段落很难逐字去翻译的,我就按照自己的理解来意译。
  • 在某些地方我觉得需要扩展或者强调的,我会加些自己的理解作为译注。
  • 阅读本文,需要些对Javascript有基本的了解。
  • 原文地址为,ECMA-262-3 in detail. Chapter 7.1. OOP: The general theory
  • 如若转载,请注明出处。

简介

在本文中我们将考察ECMAScript中关于面向对象编程的主要概念。不过,本文将会有别于已有的很多Javascript的讨论文章,我会把更多的讨论放在从理论角度由里而外的理解OOP。特别的,我们会看到对象的创建算法,对象之间的关联关系(包括基本的继承关系),还有某些概念的精准定义(我希望可以消除那些在其他介绍OOP文章中经常让人弄混的术语上的疑惑)。

译注:ECMAScript是应用在网页客户端的脚本语言的国际标准,Javascript是其中应用最广泛的实现之一。详细介绍见 wiki

总则,范式及观念

在分析 ECMAScript的面向对象编程(OOP)的技术细节之前,有必要列出那些基本特性,并澄清相关的核心概念。ECMAScript支持多种编程范式,包含结构化编程,面向对象编程,函数式编程,指令式编程,和某种程度上的AOP。不过,这篇文章专注在面向对象编程上。让我们看看ECMAScript给出的定义:

ECMAScript是一种基于原型(prototype)实现的面向对象的编程语言。

基于原型的面向对象编程相对于基于静态类的有很多不同之处,在下面的章节中我们会仔细讨论。

基于类(class)和基于原型(prototype)的模型的特征

注意,前面提到的有一点很重要,静态类。加上“静态”这个术语,我们便可以理解为静态对象、静态类,还有强类型(但最后一项并不是必须的)。

译注: 对于“静态“这个修饰,有几点需要说明, 1. “静态“类模型,指的是类一旦被定义,就不能被修改。比如C++里定义好的类在运行时是不能被修改的,同样的由类实例化出的对象,它的属性、方法等特征也是不能被修改的。 2. 类型,可以理解为一种元数据,它描述了由这个类型创建的对象在内存中的存储。比如,基本类型,或用户自定义类型-类。强类型(Strong Typing)则限定了所有对象在参与运算时,必须通过指定的类型检测,否则就会编译错误。或者显示的提供一种类型到另一种类型的转换,如果要混合不同类型间的运算。 3. 在静态类模型中,强类型并不是必须的。这是指,可以指定一个对象是dynamic的,在运行时可以被赋值为任意一个对象,所以也就无所谓那个类型的检测。比如,C#里可以这样使用,dynamic foo = "bar"; foo = 2; foo可以被赋值为字符串,也可以被赋值为整数。

我们强调这一点,是因为在各种文章和论坛里,大家经常以一个鲜明的对比“类 vs 原型”作为主要原因来区别Javascript,尽管这并不是最根本的(比如,基于动态类的编程语言python 和 ruby,就不能以此作为对比来区别Javascript)。所以更基本的对比应该描述成“静态+类 vs 动态+原型”。更精确的说,从一个基于静态类的编程模型以及它如何解析其属性和方法的机制上,可以清晰的看到区别与基于原型的编程模型实现的不同。下面,让我们一个接一个的讨论,首先是基本原理和这些范式的关键概念。

静态类模型

在基于类的模型中,class是基本概念,instance是类的一个实例。Instance也经常被命名为对象或者样例。

类和对象

类代表了一个抽象的集合,一个具有共同特征的实例的集合(也可以认为是对这类实例的知识的表示)。集合是一个数学概念,不过这里也可以称它为类型或分类。比如,(以下例子都用伪代码给出)

C=Class {a, b, c} // 类C,具有特征 a, b, c

然后,该类对应的实例所具有的特征包括,属性(对象的描述)和方法(对象的行为)。特征本身也可被认为是对象,比如一个属性是可写的,可配置的或者是可访问的等。

译注:这里所提到的类的特征本身作为对象并可配置的特性,应该是动态类的特征。而且原文中这句描述感觉有些突兀。

类的一个实例可以认为是类的一个状态(所有属性被赋予了一个值),类一旦被定义,其结构(特征)就不能被修改(比如,类的属性A,不能删除也不能改成另一个属性B),同样的类的所有实例的结构也不能被改动。

C = Class {a, b, c, method1, method2} 
c1 = {a: 10, b: 20, c: 30} // 类C的实例或对象 c1
c2 = {a: 50, b: 60, c: 70} // 类C的另一个实例,但却由不同的属性(状态)值
有层次结构的继承

为了提高代码的利用率,一个类可以通过另一个类扩展而来,然后再加入其特定的属性。这种机制叫做(有层次的)继承。

D = Class extends C = {d, e} // {a, b, c, d, e}
d1 = {a: 10, b: 20, c: 30, d: 40, e: 50}

当调用实例的方法时,方法的查找是由一种静态,不变且按顺序的方式来实现的。如果方法没有在当前的类中找到,那么就在其父类里接着查找,还没有就接着在父类的父类里查找,并以此类推,按继承的层次结构查找下去。如果到了继承的最顶端还是没有找到对应的方法,那么就说这个实例没有这个方法,因此也得不到任何结果。

d1.method1() // D.method1 (no) -> C.method1 (yes)
d1.method5() // D.method1 (no) -> C.method1 (no) -> no result

译注:以上给出的只是一般意义上的查找调用方法的方式,不同语言的具体实现都会有出入。比如C++,对于虚函数的查找则是直接访问一个虚函数表。

在继承关系中,类的方法并不会被拷贝到其子类里,但类的属性确实被拷贝的。

译注:我猜作者这里想要表达的意思是,当实例化每一个子类的对象时,其内存中都会有一份父类属性的拷贝,但有各自的属性值。这和下面将要讲到的基于原型的OOP不同,每个对象引用的prototype都是同一份。

我们可以从上面的例子可以看到,类D的属性a,b和c是从其父类C直接拿来的。但是,方法method1,method2是在使用时直接引用的。因此,实例化的对象的内存使用是直接和继承的深度成正比的。这里的一个“缺点”是,即使并不是所有的属性值都会被实例对象用到,实例化都会产生出所有的属性。

译注:上面这个作者提到的“缺点“,我只能说,这是使用者设计上的问题,并非这种语言模型的问题。

关键概念

基于原型的模型

第一个基本概念是,对象都是动态可变的。 可变性(不仅仅是对象的属性值,而是属性本身都可以被修改)直接和语言的动态特性相关联。这样的对象可以独立的描述它所有的特征(属性,方法)而不需要对应的类。

object = {a: 10, b: 20, c: 30, method: fn};
object.a; // 10
object.c; // 30
object.method();

更进一步的,因为动态特性,对象可以轻易的修改它的特征(增加,删除或者修改)

object.method5 = function () {...}; // 直接添加一个新的方法给这个对象!
object.d = 40; // 直接添加了一个新的属性 "d"
delete object.c; // 直接删除了属性 "с"
object.a = 100; // 修改属性的值 "а"
// 通过上面的修改后,对象编程了 {a: 100, b: 20, d: 40, method: fn, method5: fn};

这里的规则是,在给对象的一个属性或方法赋值时,如果它不存在,那么它会被创建出来并赋值;如果存在,那么更新为新值。 (另一个重要的概念)代码的重用,并不是通过类的扩展实现的(注意,这里类并非一些不变的特征的集合,事实上更本没有前面介绍的所谓的类的概念),而是通过引用所谓的原型实现的。

原型就是一个对象,它要么为其他对象提供拷贝,要么作为一个辅助的对象,让其他对象来引用那些他们没有但是原型对象有的特征(属性或方法)。

基于委托的原型实现

任何对象都可以做为另一个对象的原型,并且由于对象的可变性,它可以很容易的在运行时改变其原型。注意,目前我们只考虑一般意义的理论,并不涉及具体的实现。当我们讨论具体实现时候,我们将会看到一些不同的特性。比如,

x = {a: 10, b: 20};
y = {a: 40, c: 50};
y.[[Prototype]] = x; // x 是 y的一个原型

y.a; // 40, a 是y自己的属性
y.c; // 50, c 也是y自己的属性
y.b; // 20 – b 是通过原型得来的: y.b (no) -> y.[[Prototype]].b (yes): 20

delete y.a; // 删除y自己的属性 a
y.a; // 10 – 这时候,依旧可以访问属性a,不过是从原型中得来的。

z = {a: 100, e: 50}
y.[[Prototype]] = z; // 运行时,可以直接修改y的原型,这里改为z
y.a; // 100 – a 属性就从z中得来
y.e // 50, 这是也具有属性e,通用原型z中得来

z.q = 200 // 原型z添加新属性q
y.q // 对原型z的改动,对y对象是透明的。

译注:大部分Javascript实现中,要访问一个对象的原型可以通过如下的方式,object.__proto__,这是解析器自动生成的,且所有的对象都有的属性。

上面这个例子展示出了和原型相关的基本原理和机制,它可以让任何一个对象引用另一个称为原型的对象,以此“继承“来所有原型的特征。这种机制被称为委托,于是这种模型就称为delegation based prototyping。 在这种情况下,引用对象的一个特征(属性或方法)可以被认为是向对象发送一个消息。比如,当对象自身不能响应一个消息时,它便会委托其原型(委托原型去响应这个消息)。通过这种机制就实现了代码重用,它被称为基于委托的继承或者基于原型的继承。 既然,任何对象都可以被用来做原型,那么它意味着原型本身也会有原型。这样,链接在一起的原型就是所谓的原型链。这个原型链类似于静态类的层次继承结构,不过因为对象的易变性,原型链可以轻易的被改变。

x = {a: 10}

y = {b: 20}
y.[[Prototype]] = x

z = {c: 30}
z.[[Prototype]] = y

z.a // 10

// z.a 是通过如下的原型链查找的
// z.a (no) ->
// z.[[Prototype]].a (no) ->
// z.[[Prototype]].[[Prototype]].a (yes): 10

如果一个对象自身和它的原型链都不能响应一个消息,那么它便会触发一个相应的系统信号来进一步处理,继续分发消息或者委托给另一个原型链。 这种通过系统信号的处理在很多OOP的实现中都有,包括基于动态类的实现系统:SmallTalk 的#doesNotUnderstand#,Ruby的method_missing, Python的_getattr__, PHP的_call, ECMAScript其中之一实现的__noSuchMethod__ 。比如(SpiderMonkey的ECMAScript实现)

var object = {
  // 实现对应系统信号的方法,当对象不能相应消息
  __noSuchMethod__: function (name, args) {
    alert([name, args]);
    if (name == 'test') {
      return '.test() method is handled';
    }
    return delegate[name].apply(this, args);
  }

};

var delegate = {
  square: function (a) {
    return a * a;
  }
};

alert(object.square(10)); // 100
alert(object.test()); // .test() method is handled

那么,和基于静态类的OOP实现不同的,如果对象不能响应一个消息,那么它只是这个时候没有所要求的响应,但是并不代表以后也不能。这种可能性来自于委托给另一个原型链去查找,或者直接改变对象以让它具备这个消息的响应能力。 这里我们正在讨论的基于委托的原型的模型,正是ECMAScript所描述的。不过,根据不同厂商的实现,还都会有各自的特点,下面我们会看到。

Concatenative模型

为了描述的完整性,有必要讲一下为对象提供引用的原型的其他实现方式(虽然并没有被ECMAScript使用)。在这种模型里,代码重用并不是用委托的方式,而是在对象被创建出来时直接拷贝原型的内容。因此,这样的原型实现方式就被称为Concatenative 原型。 通过直接拷贝对应的原型,对象便完全拥有所有的属性和方法并可以随意修改(这样对其的改变便不会影响到其他已经创建好的对象,而基于委托的模型却不是这样——原型的改变会直接影响到由其创建的对象)。这种拷贝的方式固然有其优势,但其劣势是内存的使用将会随着对象的增多而变的很高。

鸭子(duck)类型

与基于静态类的模型相比,这里讨论的是动态“类”,弱类型和易变的对象,所以当说一个对象具有某种行为时,并不是考察它属于那个类别,而是看它是否能响应或处理传来的消息(通过某种方式来测试对象是否具有这个能力),比如下面的例子,

// 对静态类来说,要先判断对象是不是对应的类,以此来调用对应的方法
if (object instanceof SomeClass) {
  // some actions are allowed
}

// 在动态类实现中
// 对象当前是什么类型的并不重要, 因为对象的易变性,
// 对象的特征可以随时的改变。所以,更基本的检验方式是,
// 对象是否具备相应对应消息的能力(方法)

if (isFunction(object.test)) // ECMAScript

if object.respond_to?(:test) // Ruby

if hasattr(object, 'test'): // Python

这就是“Duck”类型。就是说,对象并不是由一个具体的类型来定义的,而是由它当前所具有的特征(属性和方法)来定义的。

译注,简单来说,如果当前时刻你走起来像鸭子,叫起来像鸭子,游起泳来像鸭子,那你就被认为是个鸭子(好吧这句话容易产生误解,不过我真的只是在做个拟物的比喻,嘎嘎。。。)

关键概念
  1. 对象是最基本概念。
  2. 对象是完全动态可变的(理论上来时,可以变成另一个完全不一样的对象。)
  3. 对象不需要一个精确的类来定义它的结构和行为,对象完全不需要类。
  4. 对象是通过委托给它的原型来响应某个方法或属性的调用,如果对象本身无法应答。
  5. 对象的原型可以在运行时被任意的修改。
  6. 在基于委托的原型实现中,对象原型的修改将会影响所有与其相关的对象。
  7. 在基于concatenative的原型实现中,对象会拷贝一份原型的数据到自身,由此和原型完全独立开。修改原型的特征便不会影响已创建好的对象。
  8. 如果对象不能响应或处理一个消息,那么可以通知调用者,并可能做进一步的处理(比如,直接分配消息的处理或者委托给另一个原型链)
  9. 判定对象的类型,并不是通过一个具体类的归属,而是通过被检测的类型(也是个对象)是否出现在对象的原型链中。

基于动态类的模型

在文章刚开头拿“类和原型”做对比时,已经提及过这个动态类模型(这里再强调一下的是,与原型动态特性形成鲜明对比的是静态类的定义)。对于动态类模型,可以举python或ruby作为例子。它们都是基于动态类的编程范式。但在某种意义上,原型的有些特性也会在动态类编程中看到。 在下面的例子中,我们可以看到正如基于委托的原型模型一样,我们也可以动态的修改一个类,然后影响所有这个类的对象。甚至,我们可以在运行时改变一个对象的类(类似与原型实现中,强行改变一个对象的原型)

# Python

class A(object):

    def __init__(self, a):
        self.a = a

    def square(self):
        return self.a * self.a

a = A(10) # 创建一个实例
print(a.a) # 10

A.b = 20 # 类A的新属性
print(a.b) # 20 – 于是,实例a马上就“拥有“了属性b

a.b = 30 # 这会创建a自己的同名属性b
print(a.b) # 30

del a.b # 删除a自己特有的属性b
print(a.b) # 20 - 然后b的属性又从类A中取得。

# 就像原型一样,可以动态的改变对象的类。

class B(object): # 空类B
    pass

b = B() # B的实例b

b.__class__ = A # 改变b的类为A

b.a = 10 # 创建新的属性a
print(b.square()) # 100 - 调用A的方法

# 显示的删除类A和B
del A
del B

# 但是对象还在隐式的引用类,所以下面的方法还是可用的
print(b.square()) # 100

# 不过改变对象的类为一个系统定义的类是不允许的,起码在当前版本是允许的。    
b.__class__ = dict # error

译注:python 2.7 上面的调用是错误的。顺便试了下 type(B): 'classobj' type(b): 'instance' type(dict): 'type'

Ruby也是类似的,基于动态类的语言(顺便说一下,python不同于Ruby和ECMAScript的地方是,不可以增强一个内置的类)。我们可以彻底的改变一个类或者对象的特性(比如改变类的属性和方法,并且这种改变也会影响已经创建的对象)。好了,这篇文章不是关于python或ruby的,所以让我们还是回来继续讨论ECMAScript吧。

But before, we still need to take a look on additional “syntactic and ideological sugar”, available in some OOP implementations, because such questions often appear in some articles about JavaScript. 译注:这段不知咋翻译了,上下不太搭调,又有些突兀。留着先了。。。

本文主要是在讨论Javascript作为另外一个基于原型的OOP语言的基本概念,且澄清并不能简单的以“class vs prototype“来区分的误解,接下来我们会看到OOP的基本语言特性是如何在Javascript基于原型的实现中体现的.

OOP语言特性

这一章节,我们会讨论OOP的语言特征以及各种OOP实现中的代码重用,以便和ECMAScript的OOP作对比.

多态(Polymorphism)

ECMAScript里说对象是多态的,有着不同的含义。 例如,一个函数可以被应用在不同的对象上,就像是对象上的固有的特性一样。

function test() {
  alert([this.a, this.b]);
}

test.call({a: 10, b: 20}); // 10, 20
test.call({a: 100, b: 200}); // 100, 200

var a = 1;
var b = 2;

test(); // 1, 2

上述例子展示了一个方法应用在不同对象时的动态行为。不过,也有例外的情况,比如内置方法Data.prototype.getTime(), 就要求调用的对象必须是date类型的对象,否则就会抛出异常。

alert(Date.prototype.getTime.call(new Date())); // time
alert(Date.prototype.getTime.call(new String(''))); // TypeError

这种适合所有类型的方法被称为parametric polymorphism,并且可以接受另外一个方法作为参数(比如,array的sort方法就可以接受一个方法作为参数)。 另一种多态是在原型中定义空的方法,然后通过原型创建出来的对象重新实现该方法(这种方式类似于“一个接口,但不同的实现”)。

译注:这里有一点需要注意的,对象不能直接重写原型对应的空方法,否则它将影响原型创建的所有其他对象,如果这个实现只和特性的对象有关。更一般的做法不是由对象重写,而是定义一个继承的原型(引用“接口“原型的一个拷贝)并实现对应的方法,然后再由这个原型实例化出对象。

还有多态特性和之前讨论的duck类型也有联系,比如,对象的类型还有它的层次结构其实并不重要,重要的是如果对象拥有所有期望的特性,那么它就是当前调用所需要的。

封装(Encapsulation)

这是一个经常被人误解的概念。我们先讨论它的一个常用的知识点,大家非常熟悉的修饰符:private,protected和public,也称为对象的访问权限。 我想强调的一个要点是,封装是为了更好的抽象,而不是臆想的去防止不法分子恶意的直接访问对象的数据。这是一个传播了许久的错误认识,为了隐藏而封装。

译注: 不过隐藏数据被不用户直接探查到,也算是封装的一个功效,虽然算不上主要目的。

访问权限,在各种OOP的实现中都是为了让编程者可以更好的去抽象,去构建系统。在前面提到的python和ruby里都可以看到这样的使用。在python里,形如_private 和 protected的属性(通过属性名字前加下划线的约定来区分访问权限),在对象外面是无法直接访问的。另外,python会通过一定的规则自动重命名这样的属性(_ClassName_field_name), 然后通过这个名字可以直接引用对应的属性。

class A(object):

    def __init__(self):
      self.public = 10
      self.__private = 20

    def get_private(self):
        return self.__private

# outside:

a = A() # A的实例

print(a.public) # OK, 30
print(a.get_private()) # OK, 20
print(a.__private) # fail, 只能再A内部访问

# 不过python为私有属性都提供了别名
# _ClassName__property_name
# 便可通过这个名字来直接访问

print(a._A__private) # OK, 20

在ruby里,也有类似的定义private和protected属性的机制,同样的通过一些特别的方法(比如,instance_variable_get, instance_variable_set, send),照样可以直接访问这些封装了的数据。

class A

  def initialize
    @a = 10
  end

  def public_method
    private_method(20)
  end

private

  def private_method(b)
    return @a + b
  end

end

a = A.new # new instance

a.public_method # OK, 30

a.a # fail, @a - a是a的私有变量

# fail "private_method" 是私有方法,仅再类A内使用
a.private_method # Error

# 但是,通过一些特殊的方法,我们便可以访问这些私有的属性:

a.send(:private_method, 20) # OK, 30
a.instance_variable_get(:@a) # OK, 10

提供各种这样或那样方式来访问那些private和protected数据的主要原因是编程者自己需要访问那些封装了的数据。如果,这些数据因此被错误的修改了(并非那种粗心的错误),那么责任完全在编程者自身。如果这样的错误经常发生,这就是一种坏的编程方式,因此通常最好还是通过统一的公共API来访问对象的数据。 再重复一遍,封装的本质目的是为了抽象的描述数据,而不是保护数据不被恶意访问。而且private修饰符的使用也不是软件更安全的判定标准 好的抽象封装,可以让程序像积木一样插在一起,让程序有更好的扩展性,让程序可以自由的配置以适应不同的应用场景,等等。这才是封装的真正目的。 通常类里的setter方法是为了把复杂的计算封装成一个函数。比如,html中的element.innerHTML setter,可以被简单的描述为“这个元素的html将被设置为。。。”,然后在实现里隐藏了所有的处理。 封装本身并不是OOP特有的特性。一个实现各种计算的简单方法也是一种封装,它让调用变的更直接。(比如,对用户来说,他并不需要知道Math.round(…)是如何实现的,他只需按要求调用即可)。 ECMAScript的当前版本里,并没有定义private,protected修饰符。但是在实际应用中,可以来模拟这种修饰符所起的作用。不过,一定要注意不能滥用(重申一下,封装本身并非隐藏数据,然后写一堆的setter和getter)。

function A() {

  var _a; // "private" a,通过变量的作用域,模拟私有属性

  this.getA = function _getA() {
    return _a;
  };

  this.setA = function _setA(a) {
    _a = a;
  };

}

var a = new A();

a.setA(10);
alert(a._a); // undefined, "private"
alert(a.getA()); // 10

这将会使内存的使用和创建对象的多少成正比(相对应的如果这些方法在对象原型中,则没有这个问题)。不过,在实际中,这些方法都可以被优化成一组。 在很多关于Javascript的文章中,对这样的方法有一个名字“privileged methods”。不过在ECMAScript里并没有这样的的定义。 当然在构造函数中定义方法是很常见的,比如上面的例子。根据定义,对象是完全可变的也可以有自己独特的性质(比如在构造函数中,通过某种条件来指定被创建的对象拥有或者不拥有某些方法。)

译注: 又是一小段突兀的表达,和前后都不搭调。

其实,上面例子演示的“隐藏”或“私有”的变量也是可以被直接访问的。比如,使用eval方法,通过传入特定的调用上下文,就可以访问到scope chain中的变量(通过当前当前上下文的variable object),

eval('_a = 100', a.getA); // or a.setA, as "_a" is in [[Scope]] of both methods
a.getA(); // 100

译注:关于scope chain的使用和原理,原文作者再另一篇文章中有着想尽的描述。

或者,在某些Javascript引擎的实现中可以直接访问方法的“activation object”(比如,Mozilla的Rhino)。可以通过直接访问“activation object”来进一步访问对应的内部变量。

// Rhino
var foo = (function () {
  var x = 10; // "private"
  return function () {
    print(x);
  };
})();
foo(); // 10
foo.__parent__.x = 20;
foo(); // 20

有些时候,在Javascript中,这种模拟private或protected访问时,变量名前通常会加上下划线(注意,和python不同的,这里只是一种命名惯例,没有任何语义的不同)。

var _myPrivateData = 'testString';

通常的,对象的初始化操作可以封装成一个独立的外部函数,通过“surrounding execution context”,在定义时即被调用。如下形式所示,

(function () {

  // initializing context

})();
多重继承

多重继承是一个提升代码重用率的好方式(如果我们可以继承一个类,为什么不一次继承10个,如果那些都是所需要的)。但是,多重继承有它的弊端,所以在使用中并非很受欢迎.

译注: 好吧,应该更全面的看待多重继承,以C++为例,可以参考FAQ

Mixins

Mixins也是代码重用的一种方式,是多重继承的另一种替代方案。多个不同的对象可以混合在一起,从而扩展了其中任何一个的特性(一个对象可以和任何其他对象混合在一起)。ECMA-262-3标准中并没有“mixins”的概念,但是根据mixins的定义,ECMAScript可以通过改变或扩展对象来模拟mixins的效果。如下例所示,

// helper for augmentation
Object.extend = function (destination, source) {
  for (property in source) if (source.hasOwnProperty(property)) {
    destination[property] = source[property];
  }
  return destination;
};

var X = {a: 10, b: 20};
var Y = {c: 30, d: 40};

Object.extend(X, Y); // mix Y into X
alert([X.a, X.b, X.c, X.d]); 10, 20, 30, 40

再次重申下,上面提到的“mixin”加了引号,是因为ECMA-262-3标准中并没有对应的概念,而且上面例子所展示的只是通过扩展对象的特性来模拟mixin(在Ruby中,mixin就是一个正式的概念,通过创建引用而非直接拷贝所有的属性到对象里,来完成mixin。事实上,相当于创建另一个“原型”对象来委托)。

Traits

Traits类似于mixins,不过最基本的区别是,Traits只提供方法而没有状态(可以理解为成员变量)并且需要手动去解决方法名冲突。在ECMAScript中也没有“traits”的概念,不过同样可以根据对象的可变性来模拟。

译注:mixins方式会隐式的解决命名冲突,更多对比参考mixins vs traits

接口

接口类似于mixins和traits,在很多OOP语言中都有这个概念。与mixins和traits不同的是,接口只提供方法的签名,而实现需要继承对象去实现。 接口可以被认为是完全抽象的类。对于只能单一继承的类来说,是可以实现多个接口的。所以,在这个意义上,接口(以及上面提到的mixins)可以认为是多重继承的另一种替代方案。 ECMA-262-3标准没有定义接口,或者抽象类的概念。不过,可以通过一下方式来模拟,比如对象继承一个原型,其对应方法都为空(可认为是接口),并强制继承对象来实现所有接口(如果不实现便抛出异常),来模拟接口继承方式。

对象组合

对象组合也是一个实现代码重用的技术。对象组合相对于继承,有着更好的灵活性。它可以被认为是基于委托的原型实现的基础

译注: 对象的原型本身就可以被认为是原型对象和当前对象的一个组合,并且可以灵活的修改对象的原型来重新组合。

除了原型可以被修改外,对象还可以聚合其他对象,从而委托该聚合的对象去响应消息。可以聚合多个对象,并在运行时做改变。

var _delegate = {
  foo: function () {
    alert('_delegate.foo');
  }
};

var agregate = {

  delegate: _delegate,

  foo: function () {
    return this.delegate.foo.call(this);
  }

};

agregate.foo(); // delegate.foo

agregate.delegate = {
  foo: function () {
    alert('foo from new delegate');
  }
};

agregate.foo(); // foo from new delegate

通常,组合在一起的对象间关系可以被称为“有一个”(has-a),而继承对象间的关系被描述为“是一个”(is-a)。 相对于对象组合的灵活性,显示的把对象组合在一起需要一定的额外代码才能实现

译注: 显而易见的,一个对象如果想要聚合另一个对象,那么它必然要增加一个成员变量来引用需要聚合的对象。

面向方面编程(AOP,aspect-oriented programming)
function checkDecorator(originalFunction) {
  return function () {
    if (fooBar != 'test') {
      alert('wrong parameter');
      return false;
    }
    return originalFunction();
  };
}

function test() {
  alert('test function');
}

var testWithCheck = checkDecorator(test);
var fooBar = false;

test(); // 'test function'
testWithCheck(); // 'wrong parameter'

fooBar = 'test';
test(); // 'test function'
testWithCheck(); // 'test function'

译注: 对这个概念没有研究过,着实不晓得怎么翻译这个例子所描述的精神。所以这段只是贴出了上面的例子。更多这个概念的介绍可以参考wiki 或者 Quora上的一个问答。感觉上,这是一种从模块化方面考虑程序设计的模式。

结论

到目前为止,我们便结束了EMACScript面向对象编程的基本理论的讨论,并且我希望这篇文章对大家有所帮助。下一篇文章将讨论ECMAScript面向对象的内部实现原理。

译注:下一篇更深入的关于ECMAScript OOP的内部实现原理的原文地址再ECMA-262-3 in detail. Chapter 7.2. OOP: ECMAScript implementation,等有时间再来翻译。

附录

Fork me on GitHub