首页
Javascript
Html
Css
Node.js
Electron
移动开发
小程序
工具类
服务端
浏览器相关
前端收藏
其他
关于
公司注册

ECMA-262-3 in detail——第七章:OOP(第一部分:一般理论)

2015年05月06日 发布 阅读(1706) 作者:Jerman

原文: http://dmitrysoshnikov.com/ecmascript/chapter-7-1-oop-general-theory/

介绍 | Introduction

这一章我们讨论ECMAScript中面向对象编程(object-oriented programming)的几个主要方面。由于这一主题已经在许多文章中谈论过,本章并不打算“老调重弹”,而是试图更多地着眼于这些过程内在的理论方面。尤其是,我们将研究对象创建的算法,看看对象间的关系(包括最基本的关系——继承)是如何实现的,并且给出一些讨论中将用到的准确定义(我希望这样能够打消一些术语和思路上的疑惑以及一些关于Javascript文章中OOP部分的常见的混淆)。

一般规定,范式和思想 | General provisions, paradigms and ideology

在分析ES中OOP的技术部分之前,有必要提到一些一般特性,并澄清一般理论中的关键概念。
ECMAScript支持多种编程范式:结构的(structured),面向对象的(object-oriented, OO),函数式的(functional),指令式的(imperative),以及在某些情况下的面向侧面(aspect-oriented programming, AOP);这篇文章中主要着眼于OOP,让我们给出ECMAScript中关于这个本质的定义:
ECMAScript是基于原型(prototype)实现的,面向对象的编程语言。
基于原型的OOP模式和基于静态类(static class)的范式在一些方面有区别。让我们来看一下这些方面的细节。

基于类和基于原型的模式的特性 | Features of class based and prototype based models

注意,上一句中有一个重点:——基于静态(static)类。我们通过“静态”这个术语来理解静态对象和类,以及作为一种规则的强类型(strong typing),尽管,最后一种不涉及到。
人们已经注意到上面两者的区别,因为我们常常在各种文章和论坛中看到,Javascript被称为是(和一般的OOP相比)“另类”“不同的”,而主要原因就是“class vs. prototype”。然而,在一些实现中(比如,基于动态类的Python和Ruby)这对区别并不是那么根本的(而且除了一些情况外,Javascript也不是如此地“另类”,尽管在一定的思想特性上区别确实存在)。但是更基本的是下面这对区别“statics + classes vs. dynamics + prototypes”。确实,一个静态的(statics)和类的(classes)实现(比如C++,Java)以及它们相关的属性/方法解析的机制与基于原型的实现相比有着明显的不同。
但让我们一个一个来看。让我们来看一下这些范式的基本理论和关键概念。

基于静态类的模式 | Static class based model

在基于类的模式中,有一个类(class)的概念,以及一个属于这个类的实例(instance)的概念。一个类的实例也常常被称为对象(objects)或范例(exemplars)。

类和对象 | Classes and objects

类是实例(即对象)的一般性特征的形式的抽象的集合(formal abstract set)。
这里的术语”集合”(set)是更数学化的,然而也可以称它为类型(type)或者类别(classification)。
示例:(这里和下面的那些例子将使用伪代码)

  1. C = Class {a, b, c} // class C, 拥有a,b,c三个特征

实例的特征是:属性(properties, 对于对象的描述)和方法(methods, 对象的行为)。
这些特征本身也可以被看作是对象:比如,一个属性是否可写,是否可配置,是否可激活(getter / setter),等等。
也就是说,对象储存的是一个状态(一个类中描述的所有属性的具体的值),而类定义的是它的实例的严格不可改变的结构(即存在的是这些还是另一些属性)和严格不可改变的行为(即存在的是这些还是另一些方法)。

  1. C = Class {a, b, c, method1, method 2}
  2. c1 = {a: 10, b: 20, c: 30} // 类C的对象/实例 c1
  3. c2 = {a: 50, b: 60, c: 70} // 类C的另一个对象c2,拥有和c1不同的状态

分级的继承 | Hierarchical inheritance

为了提高代码重用率,一个类能扩展(extend)别的类,从而引入额外的(父类的)属性。这个机制被称为(分级的 hierarchical)继承(inheritance)。

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

当实例调用方法时,方法的解析是通过类的严格的不变的连续的测试处理以决定使用哪一个方法:如果方法在当前类中没有找到,那么在父类中查找,然后在父类的父类中,等等。也就是说,在严格的分级的链(strict hierarchical chain)中处理。如果在继承链的源头节点仍然没有解析到方法,那么结果是:这个对象没有(在它的集合中以及在它的分级链中都没有)这个名称的行为,因而不可能获得调用这个方法的预期的结果。

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

和方法(在继承中不会复制到子类中而是通过分级链的方式查找)不同,属性在继承中总是复制的。我们在上面类C的子类D的例子中能看到这一现象:类C的属性a,b,c被复制给D,而使得D的结构是{a, b, c, d, e}。然而方法{method1, method2}没有复制,而是继承了。因此,在这方面的内存的占用是和分级的级数成正比的。这里的主要缺陷是,即使当前对象不需要深层类中的某些属性,它仍然将全部拥有它们。

基于类的模式的主要概念 | Key concepts of class based model

要创建一个对象,首先需要定义它的类;
即是说,对象根据它自己的”形象和相似性“分类(结构和行为)而创建;
方法的解析是通过一条严格的直接的不可改变的继承链(chain of inheritance)来处理的;
子孙类(以及根据它们创建的对象)包含继承链中的所有属性(即使其中的一些属性对于它们而言是不必要的);
类在创建后不能改变(根据静态的模式)它的实例的任何特征(无论是属性还是方法);
实例(还是由于静态的模式)不能拥有与它的类结构和行为不同的,任何额外的行为或属性。
让我们来看一看另一种OOP模式:基于原型的模式。

基于原型的模式 | Prototype based model

这里的基本概念是动态易变的对象(dynamic mutable objects)。
变动性(mutation, 即完全可变性:不只是值,还包括所有的特性本身)是和语言的动态性直接相关的。
这些对象能够独立储存它们全部的特性(属性和方法),而不需要类。

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

而且,由于动态性,它们可以简单地改变它们的特性(增加,删除,修改):

  1. object.method5 = function() { ... }; // 增加新的方法
  2. object.d = 40; // 增加新的属性
  3. delete object.c; // 删除属性c
  4. object.a = 100; // 修改属性a

就是说,在赋值时,如果对象中不存在这个特性,那么特性被创建并初始化为传入值;如果特性已存在,则只是更新它的值。
这个情况下的代码重用并不是通过扩展类而实现的(注意,我们说的并不是作为不可改变的特性的集合的类;而是这里不存在任何类),而是通过引用原型(prototype)。
原型是一个对象,它或者作为其他对象的原始拷贝(original copy);或者作为辅助对象,以便于其他对象当它们没有所需要的特性而原型对象中已有时委托(delegate)原型中这些特性。

基于委托的模式 | Delegation based model

任何对象都可以作为其他对象的原型,并且由于变动性,对象可以在运行时动态地改变它的原型。
注意,现在我们考虑的是一般理论,而不涉及到具体的实现。当我们讨论到具体实现时(这里指ECMAScript),我们会看到实现的一些独有特征。
实例(伪代码):

  1. x = {a: 10, b: 20};
  2. y = {a: 40, c: 50};
  3. y.[[Prototype]] = x; // x是y的原型
  4. y.a; // 40 自身属性
  5. y.c; // 50 自身属性
  6. y.b; // 20 从原型中获得: y.b (no) -> y.[[Prototype]].b (yes) 20
  7. delete y.a; //删除自身属性a
  8. y.a; // 10 从原型中获得
  9. z = {a: 100, e: 50};
  10. y.[[Prototype]] = z; //改变y的原型为z
  11. y.a; // 100 没有自有属性a,从原型中获得
  12. y.e; // 50 同上
  13. z.q = 200; //原型增加属性
  14. y.q; // 200 同样能从原型中获得新属性

这个例子展示了和原型相关的重要特性和机制:当它作为辅助对象时,它的属性在其他对象中缺少相同属性时可以被委托使用。
这个机制称为一个委托(delegation),而基于它的原型模式就称为委托原型(delegating prototyping)或者基于委托的原型(delegation based prototyping)。
这个情况中的特性的引用被称为传递一个消息(sending a message)给一个对象。换句话说,当这个对象自身不能响应这个消息,它委托它的原型(请求它应答这个消息)。
这个情况中的代码重用被称为基于委托的继承(delegation based inheritance)或者基于原型的继承(prototype based inheritance)。
由于任何对象都可以作为原型,这意味着原型也可以有它们自己的原型。这种原型间的连接组合被称为原型链(prototype chain)。和静态类相似,原型链也是分级的(hierarchical),然而由于动态性,它可以很容易地重新排列,从而改变层级和结构。

  1. x = {a: 10}
  2. y = {b: 20}
  3. y.[[Prototype]] = x
  4. z = {c: 30}
  5. z.[[Prototype]] = y
  6. z.a // 10
  7. // z.a (no) ->
  8. // z.[[Prototype]].a (no) ->
  9. // z.[[Prototype]].[[Prototype]].a (yes): 10

如果一个对象和它的原型链不能响应发出的消息,对象能够激活一个相应的系统信号(system signal)来处理它是否能够继续调度和委派给另一条链。
许多实现中都有这个系统信号,包括基于动态类的系统:SmallTalk中的#doesNotUnderstand;Ruby中的methodmissing;Python中的getattr;PHP中的call;某种ECMAScript实现中的noSuchMethod_(译者按:Mozilla),等等。
示例(ECMAScript实现器SpiderMonkey中):

  1. var object = {
  2. __noSuchMethod__: function (name, args) {
  3. alert([name, args]);
  4. if (name == 'test') {
  5. return '.test() method is handled';
  6. }
  7. return delegate[name].apply(this, args);
  8. }
  9. };
  10. var delegate = {
  11. square: function (a) {
  12. return a * a;
  13. }
  14. };
  15. alert(object.square(10)); // 100
  16. alert(object.test()); // .test() method is handled

也就是说,和基于静态类的实现不同,当无法响应消息时,其结论是:对象没有所请求的特性,然而,如果通过分析可选的原型链仍然可能得到结果,或者,对象也可能在一些变动后获得所请求的特性。
关于ECMAScript,这个实现确实是使用了基于委托的原型(delegation based prototyping)。然而,我们将看到,涉及到规范和具体实现器时它也有一些自身的特性。

串联模式 | Concatenative model

为了公平起见,有必要说明几个符合定义的(即原型作为其他对象复制的原始对象)但没有在ECMAScript中使用的其他情况。
在这种情况下,当对象创建时,代码重用并不是通过委托(delegation),而是通过一个原型的实际拷贝(a clone)。
这种使用原型的方式称为串联式原型(concatenative prototyping)。
当一个对象复制了它原型的所有特性之后,它可以像原型一样完全更改它的属性和方法(并且不像基于委托的原型模式中的修改原型特性那样,它的修改不会影响到现存的对象)。这种模式的优点是减少了调度和委托的时间,而缺点则是更多的内存占用。

鸭子类型 | “Duck” typing

回到动态性上,和基于静态类的模式相反,在弱类型和对象变动性的模式中,某个对象是否具有完成某项工作的能力不是和它是哪个类型(类class)相关,而是和它是否能响应某条消息相关(通过测试来确定它是否具有某种能力)。
示例:

  1. // 基于静态类的模式
  2. if (object instanceof SomeClass) {
  3. // 可以执行某些行为
  4. }
  5. // 动态的实现中,不必知道对象是什么类型的,
  6. // 因为变动性,类型和特性可以重复转换,
  7. //因此是看对象是否能响应相关的消息
  8. if (isFunction(object.test)) // ECMAScript
  9. if object.respond_to?(:test) // Ruby
  10. if hasattr(object, 'test'): // Python

行话中,这被称为“鸭子类型(duck typing)”。就是说,我们可以通过检查对象在某一时刻所拥有的特性集合来识别它,而不是通过看它在层级中的位置或它属于哪种具体类型。(译者按:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”)。

基于原型的模式的主要概念 | Key concepts of prototype based model

那么,让我们来看下这个模式的主要特点:
基本概念是对象(而不用先定义它的类);
对象是完全动态和可变的(并且,理论上可以完全从一种类型变成另一种类型);
对象没有严格的类来描述它们的结构和行为,对象不需要类;
然而,虽然没有类,但是对象可以有原型(prototypes),以便于当它们自身不能应答消息时委托原型;
对象的原型可以在运行时的任何时刻改变;
在基于委托(delegation based)的模式中,改变原型的特性将会影响到和这个原型相关的所有对象;
在串联式原型(concatenative prototyping)模式中,原型是对象克隆的原始拷贝,因此变得完全独立,改变原型的特性不会影响到根据它克隆出的对象;
如果不能响应一个消息,可以发信号给调用者,以便于采取额外的措施(比如改变调度);
对象的识别可以不通过它们的层级位置或者它们属于哪个具体的类型,而是通过对当前拥有的特性。
然而,我们还需要提到另一个模式。

基于动态类的模式 | Dynamic class based model

我们通过一开始提到的一个例子来考虑这个模式——“a class vs. a prototype”之间的区别并不是那么重要(尤其当原型链不变的时候,为了更准确地区分,它就应当被看做是一种静态的(statics)类)。可以用Python或Ruby(或其他类似的语言)为例。这两种语言都使用了基于动态类的范式。然而在某些方面,又能看到基于原型的实现的特征。
在下面的例子中我们能够看到,就像在基于委托的原型中那样,我们能增加一个类(原型),然后它将影响到和它相关的对象,我们也能在运行时动态地改变对象的类(提供一个新的委托对象),等等。

  1. # Python
  2. class A(object):
  3. def __init__(self, a):
  4. self.a = a
  5. def square(self):
  6. return self.a * self.a
  7. a = A(10) # 创建实例
  8. print(a.a) # 10
  9. A.b = 20 # 类的新属性
  10. print(a.b) # 20 – 实例‘a’通过委托也能获得
  11. a.b = 30 # 创建自有属性
  12. print(a.b) # 30
  13. del a.b # 删除自有属性
  14. print(a.b) # 20 - 再次在类(原型)中获得
  15. # 就像基于原型的模式,可以在运行时改变一个对象的类(原型)
  16. class B(object): # "empty" class B
  17. pass
  18. b = B() # 类B的实例
  19. b.__class__ = A # 动态改变类(原型)
  20. b.a = 10 # 创建新属性
  21. print(b.square()) # 100 - 获得类A的方法
  22. 5
  23. 5# 可以直接删除对类的引用
  24. 5del A
  25. 5del B
  26. 5
  27. 5# 但对象仍然保留了引用,并可以调用相关方法
  28. 5print(b.square()) # 100
  29. 5
  30. 5# 但是不能将对象的类变为内建(至少根据当前的版本)
  31. 60. b.__class__ = dict # error

在Ruby中的情况也是类似的:使用的同样是完全动态的类(顺便说一句,和ECMAScript和Ruby不同,在当前版本的Python中,不能够增加内建的类/原型),我们能完全更改对象和类的特性(如果在类中增加属性或方法,这些改变将影响到现存的对象);然而,它不能够动态地改变一个对象的类。
当然,由于这篇文章不是讲述Python和Ruby的,因此我们结束这些对比,然后开始讨论ECMAScript。
但在这之前,我们仍需要来看一下一些OOP的实现中提供的附加的“语法和思路上的糖分”(syntactic and ideological sugar),因为相关的问题常常出现在Javascript的文章中。
这一节主要用来让我们注意到下面说法是不正确的:“Javascript和OOP是不同的,它没有类,而只有原型”。我们有必要理解,并不是所有的基于类的实现都完全不同。并且,即使我们说“Javascript不同”,也有必要考虑到除了“类”的概念之外的其他相关特性。

各种OOP实现中的附加特性 | Additional features of various OOP implementations

在这一节中我们将快速浏览一下各种OOP实现中的附加特性以及代码重用的方式,和ECMAScript的OOP实现作一个对比。其原因是,在目前的一些Javascript的文章中,OOP的概念被局限在了一些习惯的实现方式上,而忽略了其他可能的不同实现方式;而这里的目的就是为了从语法和思路上证明这些被忽略的部分。当在某种(习惯的)实现方式上没有找到类似的“语法糖分(syntactic sugar)”时就草率地认为Javascript“不是一个纯OOP语言”是不正确的。

多态性 | Polymorphism

ECMAScript中的对象在一些意义上是多态的(polymorphic)。
例如,一个函数可以被应用到不同对象上,就像它是对象的原始特性(因为函数中的this的值在进入执行上下文时确定):

  1. function test() {
  2. alert([this.a, this.b]);
  3. }
  4. test.call({a: 10, b: 20}); // 10, 20
  5. test.call({a: 100, b: 200}); // 100, 200
  6. var a = 1;
  7. var b = 2;
  8. test(); // 1, 2

然而,有一些例外:方法Date.prototype.getTime(),根据标准,它的this值总是一个date对象,否则将抛出异常:

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

或者,参数多态性(parametric polymorphism),即函数可以接受多态的函数式参数(polymorphic functional argument )(比如数组的.sort方法和它的参数——各种排序函数),顺便说一句,上面的例子也可以认为是一种参数多态性。
或者,在原型中方法可以定义为空,而由它创建的对象再重定义(实现)这个方法(也就是说,“一个接口(结构),多个实现”)。
同样,这里的多态性也可以和我们上面提到的“鸭子类型”相联系:即,对象的类型和它在层级中的位置不那么重要,如果它拥有所有必要特性,它就可以被简单接受(换句话说,一般接口(interface)是重要的,而实现可以是各不相同的)。

封装 | Encapsulation

对于这个概念,常常存在一些感觉上的混乱和错误。在这个方面我们来讨论一个在一些实现中提供的方便的“sugar”——修饰符(modifiers):public(公共), private(私有), and protected(保护),它们被称为对象特性的访问等级(access level)或者访问修饰符(access modifiers)。
这里,我希望注意和提醒一下封装这个概念的主要目的:封装(encapsulation),是一种抽象度的增加(increasing of abstraction),但不是对“恶意黑客”——那些想要在你的类中直接写点什么的人——的执着的隐藏。
这里有一个很大的(并且是广泛的)错误,那就是为了隐藏而隐藏(use hiding for the sack of hiding)。
一些OOP实现中提供的访问等级(private, protected, public),最主要的是为了方便编程者(并且确实很方便)去更抽象地描述和构建系统。
这一点在一些实现中(比如上面提到过的Python和Ruby)可以看到。一方面(在Python中),有private和_protected属性(通过下划线的命名约定)可以用来禁止外部访问。另一方面,Python又可以通过特殊的规则简单地重命名这些域(_ClassNamefield_name),并通过这样的命名使得外部可以访问到。(译者按:因此,封装的意义是为了抽象,而不是强制隐藏)

  1. class A(object):
  2. def __init__(self):
  3. self.public = 10
  4. self.__private = 20
  5. def get_private(self):
  6. return self.__private
  7. # outside:
  8. a = A() # instance of A
  9. print(a.public) # OK, 30
  10. print(a.get_private()) # OK, 20
  11. print(a.__private) # fail, available only within A description
  12. # but Python just renames such properties to
  13. # _ClassName__property_name
  14. # and by this name theses properties are
  15. 5# available outside
  16. 5
  17. 5print(a._A__private) # OK, 20

或者在Ruby中:一方面,可以定义私有和保护域的特性;另一方面,也有特殊的方法(比如instance_variable_get, instance_variable_set,等待)来允许访问封装的数据。

  1. class A
  2. def initialize
  3. @a = 10
  4. end
  5. def public_method
  6. private_method(20)
  7. end
  8. private
  9. def private_method(b)
  10. return @a + b
  11. end
  12. end
  13. a = A.new # new instance
  14. a.public_method # OK, 30
  15. a.a # fail, @a - is private instance variable without "a" getter
  16. # fail "private_method" is private and
  17. # available only within A class definition
  18. a.private_method # Error
  19. # But, using special meta methods - we have
  20. # access to that encapsulated data:
  21. a.send(:private_method, 20) # OK, 30
  22. a.instance_variable_get(:@a) # OK, 10

主要原因是:程序员本身希望访问到封装的(注意,这里我使用的词不是“隐藏的”)数据。并且如果这些数据被不正确地更改或者出现任何错误——那完全是程序员的责任,而不是一个简单的“输入错误”或“有人随意改变了某些域”。但如果这样的情况(即访问封装的数据)频繁发生,那么我们可能还是需要注意到,这是一种坏的编程习惯和风格,因为通常而言最好只通过公共的API来和对象“交谈”。
重申一下,封装的基本目的,是将辅助的数据从用户端抽象出,而不是一种“创建防黑客的安全对象的方式”。在软件安全方面使用的是远比“private”修饰符更严格的措施。
通过封装辅助(局部)对象,我们为之后公共接口的行为改变提供了最小开支(minimum of expenses)的可能性,将这些改变局部化并预测它们的位置。而这正是封装的主要目的。
同样,一个setter方法的主要目的是为了抽象那些复杂的计算。例如,element.innerHTML setter——我们简单地把语句抽象成——“现在,这个语句的html如下”,而在这个setter函数内部对innerHTML属性所做的将是复杂的运算和检查。这个情况下的问题主要是关于抽象(abstraction),但是作为抽象度增加的封装过程也发生了。
封装的概念不仅和OOP相关。例如,它也可以是指一个简单的函数封装了各种计算过程,而使得它能被抽象地使用(例如,对于用户来说不需要知道Math.round函数的内部实现,而只是简单地调用它)。这就是一个封装,并且注意,我并没有说到任何“私有、保护或者公共的”。
ECMAScript在目前版本的规范中并没有定义private, protected, public修饰符。
然而,在实际中我们可能会看到一些“在JS中模拟封装”的说法。通常,使用封闭上下文(作为规则,就是函数构造式本身)来实现这个目的。但不幸的是,在实现这些“模拟”时,程序员们常常会生产出完全不抽象的“getter/setter”(重复一下,这是不正确地理解封装):

  1. function A() {
  2. var _a; // "private" a
  3. this.getA = function _getA() {
  4. return _a;
  5. };
  6. this.setA = function _setA(a) {
  7. _a = a;
  8. };
  9. }
  10. var a = new A();
  11. a.setA(10);
  12. alert(a._a); // undefined, "private"
  13. alert(a.getA()); // 10

这里,我们很容易理解,每一个创建的对象都会创建一对“getA/setA”方法,从而使得内存占用的问题直接和创建的对象数量成正比(如果方法定义在原型中则相反)。虽然,理论上可以通过联合对象(joined objects)来优化。
同样关于上面的方法,在一些关于js的文章中称为“特权方法(privileged methods)”。为了澄清,注意,ECMAScript-262-3中并没有定义任何“特权方法”的概念。
然而,它可以作为在构造函数中创建方法的一般方式,因为它符合这门语言的思路——对象是完全可变的,并且拥有独立的特性(在构造函数中,可以通过条件语句来让一些对象获得某些方法而另一些对象没有,等等)。
此外,在Javascript中,这种“隐藏”、“私有”的变量并非那么隐蔽(如果封装还是被误解为防止“恶意黑客”直接在某些域中写入值,而不是使用一个setter方法)。在一些实现器中(SpiderMonkey 1.7之前的版本),可以通过在eval函数中传入调用上下文来访问所需要的作用域链(并从而访问到其中的变量对象):

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

或者,在某些实现器中允许直接访问活化对象(比如Rhino),就可以通过访问活化对象上的相应属性来改变内部变量的值:

  1. // Rhino
  2. var foo = (function () {
  3. var x = 10; // "private"
  4. return function () {
  5. print(x);
  6. };
  7. })();
  8. foo(); // 10
  9. foo.__parent__.x = 20;
  10. foo(); // 20

有时候,作为一种组织方式(也可以被视为一种封装),Javascript中“private”和“protected”的数据通过一个前置的下划线来标识(但和Python中不同,这里只是为了命名上的方便):

  1. var _myPrivateData = 'testString';

关于封闭执行上下文的作用,它被常常用到,但是是用于封装和对象不直接相关的辅助数据,它可以方便地将它们从外部API中抽象出来:

  1. (function () {
  2. // initializing context
  3. })();

多重继承 | Multiple inheritance

多重继承是提高代码重用率的一个方便的特性(如果我们能继承一个类,为什么不一次继承十个?)。然而,它有一些缺点,因此在实现中并不流行。
ECMAScript不支持多重继承(换句话说,只有一个对象可以用作直接原型),虽然它的祖先 Self编程语言是有这个特性的。不过,在一些实现器中,比如SpiderMonkey中,通过使用noSuchMethod,可以管理调度和委派到另一条可选的原型链中。

混入 | Mixins

混入(Mixins)也是代码重用的一种方便的方式。混入已经被建议作为多重继承的替代。独立的元素可以混入(mixed with)其他任何对象,从而扩展它们的功能(就是说对象可以混入多个mixins)。ECMAScript-262-3规范中没有定义“混入(mixin)”的概念,然而根据混入的定义,并且由于ECMAScript中的对象是动态可变的,因此没有任何东西阻止对象混入其他对象,而是简单地增加它的特性:

  1. //辅助添加方法:
  2. Object.extend = function(destination, source) {
  3. for (property in source) {
  4. if (source.hasOwnProperty(property)) {
  5. destination[property] = source[property];
  6. }
  7. }
  8. return destination;
  9. };
  10. var X = {a: 10, b: 20};
  11. var Y = {c: 30, d: 40};
  12. Object.extend(X, Y); //将Y“混入”X
  13. alert([X.a, X.b, X.c, X.d]); // 10, 20, 30, 40

注意,我们在定义(“mixin”)上加引号是因为我们提到过ECMA-262-3中没有这样一个概念,而且事实上并不是一个混入,而是为一个对象扩展了新的特性(相反,比如在Ruby中,存在正式的mixins概念,mixin创建了相关模块的引用(换句话说,事实上创建了用于委托的额外的对象(“原型”)),而不只是简单地将模块中的所有属性复制到对象上)。

性状 | Traits

性状(Traits)和mixins相似,然而有一些自身的特性(其中最根本的是,根据定义,traits和mixins不同,它不能有状态(state),而后者可能引起命名冲突)。而关于ECMAScript,traits也可以通过和mixins一样的方式模拟,而标准中并没有定义“traits”的概念。

接口 | Interfaces

和mixins以及traits一样,接口(Interface)也是在一些OOP实现中提供的。然而,和mixins以及traits不同的是,接口要求(实现它们的)类完全实现接口中方法签名的行为(completely implement behavior of signatures of their methods)。、
接口可以被看作是完全抽象类。但是,和抽象类(可以自己实现部分方法,然后把其他的定义为签名)不同,一个类只能单一继承,但是可以实现多个接口;由于这一点,接口(和mixins一样)可以作为多种继承的替代。
ECMAScript-262-3标准中既没有定义“接口(interface)”,也没有定义“抽象类(abstract class)”。然而,作为模拟,可以为对象添加“空”方法(或者在方法中抛出一个异常,以表示这个方法应该被实现)。

对象组合 | Object composition

对象组合(Object composition)也是动态代码重用的一种技术。和继承不同,对象组合拥有更多的灵活性,并且实现了对动态可变代表的委托。而这一点反过来又成为基于委托的原型的基础。在动态可变原型中,对象可以聚集(其结果是创建了一个组合(composition)或者说,一个聚合(aggregation))其他对象以便于委托,然后当传递消息给对象时,委托这些对象。可以有多个委托,并且由于可变性,可以在运行时改变它们。
上面已经提到过的noSuchMethod方法可以作为一个例子,但是这里我们用另一个示例来说明如何准确使用委托:

  1. var _delegate = {
  2. foo: function () {
  3. alert('_delegate.foo');
  4. }
  5. };
  6. var agregate = {
  7. delegate: _delegate,
  8. foo: function () {
  9. return this.delegate.foo.call(this);
  10. }
  11. };
  12. agregate.foo(); // _delegate.foo
  13. agregate.delegate = {
  14. foo: function () {
  15. alert('foo from new delegate');
  16. }
  17. };
  18. agregate.foo(); // foo from new delegate

对象间的这种关系被称为”has-a”,也就是说,是“内部包含”的关系而不是像继承那种“is-a”的关系。
明确组合的缺点(与它的优于继承的灵活性优点并存的)是中间代码的增加。

AOP的特性 | AOP features

面向侧面编程(aspect-oriented programming)的其中一个特性是函数修饰符(function decorators)。ECMA-262-3中没有明确定义“函数修饰符”的概念(相反的,Python中有正式的定义)。然而,由于有了函数式参数,函数可以通过某种方式来修饰和活化(通过一个成为劝告(“advice”)的检查结构):

  1. function checkDecorator(originalFunction) {
  2. return function () {
  3. if (fooBar != 'test') {
  4. alert('wrong parameter');
  5. return false;
  6. }
  7. return originalFunction();
  8. };
  9. }
  10. function test() {
  11. alert('test function');
  12. }
  13. var testWithCheck = checkDecorator(test);
  14. var fooBar = false;
  15. test(); // 'test function'
  16. testWithCheck(); // 'wrong parameter'
  17. fooBar = 'test';
  18. test(); // 'test function'
  19. testWithCheck(); // 'test function'

总结

在这篇文章中我们完成了对一般理论的回顾(希望这些材料能够有所帮助)。接下来是 ECMA-262-3 in detail——第七章:OOP(第二部分:ECMAScript实现)

版权声明:本站文章除特别声明外,均采用署名-非商业性使用-禁止演绎 4.0 国际 许可协议,如需转载,请注明出处
  • ECMA-262-3 in detail——第七章:OOP(第二部分:ECMAScript实现)

    这一章的第二部分是关于EMCAScript中的面向对象编程。在第一部分中我们讨论了OOP的基本理论并勾画出和ECMAScript的相似之处。在阅读第二部分之前,如果有必要,我还是建议首先阅读这一章的第一部分.基本理论,因为后面将会用到其中的一些术语。

    发布:2015-05-26 阅读(1949)

  • ECMA-262-3 in detail——第七章:OOP(第一部分:一般理论)

    这一章我们讨论ECMAScript中面向对象编程(object-oriented programming)的几个主要方面。由于这一主题已经在许多文章中谈论过,本章并不打算“老调重弹”,而是试图更多地着眼于这些过程内在的理论方面。尤其是,我们将研究对象创建的算法,看看对象间的关系(包括最基本的关系——继承)是如何实现的,并且给出一些讨论中将用到的准确定义(我希望这样能够打消一些术语和思路上的疑惑以及一些关于Javascript文章中OOP部分的常见的混淆)。

    发布:2015-05-06 阅读(1706)

  • ECMA-262-3 in detail——第六章:闭包

    在这一章中我们来谈谈Javascript中被讨论最多的话题之一——关于闭包(closures)。事实上这个主题并不是新鲜的。然而我们在这里将试着更多从理论的角度去分析和理解它,然后我们还会看一下ECMAScript内关于闭包的内容。

    发布:2015-04-28 阅读(1716)

  • ECMA-262-3 in detail——第五章:函数

    在这章里我们讨论ECMAScript中的一个基本对象——函数。我们将会看到不同类型的函数如何影响一个上下文中的变量对象,以及这些函数的作用域链中都包含什么。我们将会回答像下面这样经常被问到的问题:“下面这两种创建函数的方式有什么区别吗(如果有的话,区别是什么呢)?”

    发布:2015-04-17 阅读(1855)

  • ECMA-262-3 in detail——第四章:作用域链

    正如我们从第二章.变量对象中了解到的,执行上下文的数据(变量,函数声明,函数形参)以变量对象的属性的方式储存。

    发布:2015-04-07 阅读(1650)

  • ECMA-262-3 in detail——第三章:this关键字

    许多程序员习惯于认为在编程语言中,this关键字是与面向对象编程紧密相关的,而且引用的是由构造式最新创建的对象。在ECMAScript中,这个概念也被实现了,然而我们将看到,在这里它不仅仅指向已创建的对象。

    发布:2015-03-25 阅读(1859)

  • ECMA-262-3 in detail——第二章:变量对象

    在程序中我们总是声明变量和函数然后用它们来搭建我们的系统。但是解释器(interpreter)是在哪里和以什么方式来找到我们的数据(函数,变量)的呢?

    发布:2015-03-24 阅读(1963)

  • ECMA-262-3 in detail——第一章:执行上下文

    第1章:在这一章里,我们将会讨论ECMAScript中的执行上下文(execution context)以及与它们相关的可执行代码(executable code)的类型。

    发布:2015-01-10 阅读(1908)