[译]JavaScript继承实例
原文: JavaScript inheritance by example
作者: Axel Rauschmayer
原文发布时间:
2012.01.08
译文:
本文通过几个例子说明了JavaScript中的关于继承的几个主题: 第一个例子是JS中构造器(constructor) Point
以及它的二级构造器ColorPoint
的原生实现, 然后一步步对它们进行改进.
对象
JavaScript是少数可以直接创建对象的面向对象编程语言之一. 在大部分其他编程语言中, 需要通过class来创建对象. 现在我们用JavaScript创建一个point对象.
|
|
使用大括号创建对象的语法是通过对象初始化器/对象字面量. 对象point有4个属性: x
, y
, dist
以及toString
. 可以通过点操作符读取这些属性的值:
|
|
值为函数的属性叫做方法(methods), 调用方法的方式是:
|
|
构造器
以上例子中我们只创建一个point对象, 如果想要创建多个, 就需要为这些对象创建工厂. 在类继承编程语言中这种工厂叫做类 classes, 在JavaScript中叫做构造器 constructors. 以函数foo
为例: JS中调用函数foo
有两种方式:
- 函数调用:
foo(arg1, arg2)
- 构造器调用:
new foo(arg1, arg2)
以下是用以创建多个point对象的构造器Point:
|
|
我们执行new Point()
时, 构造器的任务是设置一个全新的对象, 然后通过隐式的参数this
传入对象的属性. 我们之前使用对象字面量来定义对象的属性, 现在则通过this
来定义. 构造器隐式返回全新的对象, 即对象的实例.
|
|
和之前一样, 可以调用方法, 获取属性值:
|
|
然而方法被某个实例所特有并不合适, 应该被所有实例共享以节省内存. 可以使用prototype
(原型)达到这个目的. 存储在Point.prototype
的对象成为Point
所产生的所有实例的原型. 对象(“prototypee”)及其原型的工作原理是这样的: prototypee继承所有其原型的属性. 一般来说, 原型只有一个, prototypee可以有多个, 所有prototypee共用原型的属性.
因此, 我们可以在Point.prototype
内定义方法:
|
|
上例中我们通过对象字面量将一个对象赋值给Point.prototype
, 这个对象含有两个方法: dist
和toString
. 现在各部分的任务有了明确的分工, 构造器设置实例专属的数据, 原型中包含共享的数据(如方法). 注意JavaScript引擎对原型实现了高度优化, 因此在原型中定义方法不会引起太大的性能问题, 调用方法的方式也与之前无异, 跟在哪里定义方法无关.
不过还是存在一个问题: 对于所有的函数f
, 以下声明必须成立(详见参考[1]):
|
|
上述声明是所有函数的默认行为, 但我们利用对象字面量替换了Point.prototype
的默认值, 这样上述声明就不再成立, 为了使上述声明成立, 有两种方法, 一是手动在对象字面量添加constructor
属性, 二是通过添加而非替换的方式定义原型中的方法, 这样就不会覆盖原本的默认值:
|
|
constructor属性不是特别重要, 主要用于检查特定的实例由哪个构造器创建:
|
|
扩展对象
在JavaScript中, 扩展一个对象意味着在对象中添加新属性, 但会带来不好的结果: 比如, 我们现在要扩展对象A, 扩展的内容为对象B, 仅仅是将B的属性浅拷贝到A中. JavaScript中有个较不常见的实现这种扩展的方法Object.extend()
, 该方法来自于框架”Prototype”. 以下是其实现方式:
|
|
上述代码的问题是它使用for-in
来遍历对象的所有属性, 包括继承自原型的属性. 见下例:
|
|
我们想要扩展的对象获得Point
的实例对象本身的属性x
和y
, 但是来自原型的继承属性dist
和toString
方法并不是我们想要的. 继承的属性也被拷贝到第一个对象参数中, 因为for-in会遍历包括继承的属性在内的所有属性. Point
继承了多个来自Object
对象的属性,
比如valueOf
:
|
|
但valueOf
这类属性没有被拷贝到对象实例中, 因为for-in只会遍历可枚举(Enumerable)(详见参考[2])属性, 而valueOf
这类属性不可枚举:
|
|
为了让extend()
方法达到我们预期的效果, 必须确保只有原对象自身的属性被拷贝到目标对象.
|
|
还有一个问题是: 如果source
对象本身含有一个名为”hasOwnProperty”的属性(详见参考[3]), 上述代码就无法成功执行:
|
|
失败的原因是source.hasOwnProperty
获取的是source对象自身的hasOwnProperty属性, 属性值是一个数字, 而非原型链中的同名属性. 可以通过从原型链中获取这个属性解决该问题:
|
|
在ECMAScript5或者更旧版本但装载了shim(详见参考[4])的引擎, 使用以下版本的extend()
方法更好, 因为这个版本在实现继承(扩展)的同时, 没有改变source对象属性的描述符, 例如可枚举性.
|
|
设置对象的原型
现在, 我们已经知道如何通过一种不太完美的方式为对象添加属性. 还了解了怎么通过原型添加属性以避免前一种方法带来的缺陷: 令属性只存在于构造器的所创建的实例中, 但不存在于对象本身. 如果这种方式的继承可以更直接地实现, 同时不需要通过构造器设置对象原型, 那就更好了. 由于对象原型是个十分重要, 高度优化的特性, 利用原型创建新对象是实现这种继承唯一标准的方式. 也就是说, 你只有一次机会设置对象的原型, 就是在创建它的时候. 以下代码使用了ECMAScript
5的Object.create()
方法创建了一个新对象, 该对象的原型是对象proto
.
|
|
新建的对象obj
同时包含继承和自身的属性:
|
|
ECMAScript 5 shim 则使用与以下类似的代码使Object.create
兼容旧浏览器.
|
|
以上代码使用了一个临时函数构造器来创建含有特定原型的对象实例. 直到现在, 我们使用Object.create()
时, 都没有考虑第二个参数, 在该参数中可以定义新建对象的属性:
|
|
这些属性是通过property descriptors(Object.defineProperty()
方法)定义的. 使用该方法,
不仅能定义属性和属性值, 还可以定义属性的描述符, 比如可枚举性. 现在让我们试着实现一个方法protoChain()
, 它是Object.create()
方法的简化版本. 不过该简化版本没有实现定义属性描述符的功能, 例:
|
|
泛化上述protoChain
方法:
|
|
我们需要先创建全新的对象才可以为其添加原型. 因此, protoChain()
返回obj_n
的浅拷贝, obj_n
的原型是obj_n-1
的浅拷贝, 以此类推. obj_0
是唯一一个未被拷贝的从chain返回的对象. 可以这样实现protoChain()
方法:
|
|
创建二级构造器
subtyping的意思是基于当前存在的构造器创建一个新的构造器. 新的构造器叫做二级构造器(sub-constructor), 当前存在的构造器是super-constructor. 下面的ColorPoint
是Point
的二级构造器:
|
|
以上代码为构造器ColorPoint
将要创建的实例设置了属性x
, y
和 color
. 这个功能是通过绑定this
(ColorPoint
的实例)到Point
对象里实现的: Point
作为函数被调用, 利用call()
方法调用Point
确保其在正确的执行环境下被调用.
这样的话, Point()
函数为我们添加了x
和y
属性, color
属性则是由我们自己添加. 然后需要添加方法: 有一部分的方法希望从Point
中继承, 有一些则想要自己定义, 可以通过extend()
方法实现:
|
|
首先将Point.prototype
上的方法拷贝到ColorPoint.prototype
, 然后添加自己定义的方法: 以上代码中, 我们修改Point
的toString()
方法, 在toString()
原来的结果前加上ColorPoint
的color属性值. 更多调用super-prototype方法的信息详见参考[5].
执行上述代码后, 调用ColorPoint
的toString()
方法就能得出我们所预期的结果:
|
|
对代码进一步优化, 我们可以通过将Point.prototype
设置为ColorPoint.prototype
的原型避免添加过多属性到ColorPoint.prototype
.
|
|
首行代码中, 我们替换了ColorPoint.prototype
的默认值, 因此需要在第二行代码中设置它的constructor
属性值. 设置单一的constructor
属性在概念上很容易理解, 但是手写代码的步骤较复杂, 因此可以通过辅助函数inherits()
简化步骤:
|
|
函数inherits()
借鉴了Node.js的util.inherits()
方法. 该方法能帮助我们创建二级类, 并且保持一般函数构造器的简洁. 使用inherits()
时需要注意以下几点:
- 不必在意添加方法到原型之前还是之后调用
inherits()
函数. inherits()
函数应该确保constructor
属性设置正确.
下面是inherits
方法的实现:
|
|
关联父级属性
还有一个方法可以进行优化, ColorPoint.prototype.toString()
实际上是调用以下函数:
|
|
然而这并不理想, 因为我们写死了ColorPoint的父级构造器. 以下是更好的方式:
|
|
为了使上述代码成立, inherits()
函数执行以下赋值语句即可:
|
|
从这部分开始, inherits()
方法与Node.js中的inherits()
开始有所差异, 在Node.js中, SubC.super_
指向SuperC
. 而这里的ColorPoint
构造器引用被写死, 指向Point
. 想要避免这样的情况, 可以按照以下方式调用:
|
|
代码不是很简洁, 但确实达到了目的, 最后的ColorPoint
是这样的:
|
|
总结
本文相关内容源码GitHub地址: inheritance-by-example
JavaScript中的继承 - 延伸阅读
- 为何我推荐使用构造器: In defense of JavaScript’s constructors
- 创建对象的几种模式: Exemplar patterns in JavaScript
- 保持对象中的数据私有: Private data for objects in JavaScript
- 深入理解属性(属性, 属性描述符等): Object properties in JavaScript