JavaScript中的继承性

来源:互联网 发布:人工智能网络营销 编辑:程序博客网 时间:2024/04/30 22:34
你可以象使用类的语言一样来使用JavaScript,甚至可以进一步发现一些代码重用的特性。
by Douglas Crockford

JavaScript是一个class-free的、面向对象的语言,它使用原型继承(prototypal inheritance)取代了传统继承(classical inheritance)。这样一来可能会使学习C++ 和Java 等面向对象语言的程序员感到迷惑,但是你会发现用在JavaScript中的原型继承比以前的传统继承功能更强大。

然而为什么要如此关注继承性呢?有两个原因。首先是为了更方便地使用类型。你一定希望语言系统能够对那些相似类的引用进行自动转换,而对于一个要求对引用对象进行显式转换的类型系统(type system)来说只能获得很少的类型安全性。这是强类型语言中至关重要的一点,但对于象JavaScript这种宽松类型的语言就没那么重要了,因为在JavaScript中的对象引用不存在转换的问题。

关注继承性的另一个原因是考虑到代码的重用性。许多对象执行同一个方法的情况很常见,通过类的使用可以只用一套定义来生成所有的对象。同样,存在相类似的对象的情况也很普遍,它们之间的区别仅仅在于增加或修改了一些方法而已。使用传统继承能够解决这一问题,但原型继承对此更加擅长。

为了证明这一点,我将引进一些“sugar”,它允许你在一种类似于惯用传统语言的格式下编写代码;然后我会让你看到一些无法用到传统语言中的很有用的模式。最后,我会解释这些“sugar”的应用。

传统继承
首先,我会建一个Parenizor 类,它包括其value的set方法和get方法,以及一个将value封装起来的toString方法:

function Parenizor(value) {    this.setValue(value);}Parenizor.method('setValue', function (value) {    this.value = value;    return this;});Parenizor.method('getValue', function () {    return this.value;});Parenizor.method('toString', function () {    return '(' + this.getValue() + ')';});

这种语法并不太常见,但你还是可以很容易地发现传统模式的存在。"method" 方法接受一个名称和一个函数,并把它们当作一个公用的方法加入到类里。

你可以写下:

myParenizor = new Parenizor(0);myString = myParenizor.toString();

和你预期的一样,myString的值是“(0)”。

接下来建一个Parenizor的继承类,它和Parenizor基本相同,除了toString方法略有差异:当value的值为“0”或为空的话,其显示结果为"-0-":

function ZParenizor(value) {    this.setValue(value);}ZParenizor.inherits(Parenizor);ZParenizor.method('toString', function () {    if (this.getValue()) {        return this.uber('toString');    }    return "-0-";});

这个inherits方法和Java的extends方法类似;uber方法和Java里的super方法类似。它们允许一个方法调用其父类中的方法。(为了避免使用保留字因而名字有所变化。)

你可以写下:

myZParenizor = new ZParenizor(0);myString = myZParenizor.toString();

这次,myString的值是"-0-"。

虽然在JavaStript里不使用类,但是你可以把它当作使用类的语言来对它进行编程。

多重继承(Multiple Inheritance)
通过使用函数的prototype对象,你可以实现多重继承,它允许你使用多个类的方法来构建一个类。将多重继承相混杂使用可能会难以操作,而且会有潜在的方法名称相冲突的问题。你可以在JavaScript中实现这种多重继承,但在以下这个例子中你会用到一个更为规整的格式,叫做Swiss Inheritance

假设有一个NumberValue 类,它包含一个确保value在某个范围内是数字型的setValue方法,如果需要的话可以抛出一个异常。你只需要将setValue 和setRange方法用在Zparenizor里就可以了。当然你也许不想再用toString方法了,所以你可以这样写:

ZParenizor.swiss(NumberValue, 'setValue', 'setRange');

这样就只把需要的方法加入到类中了。

寄生继承(Parasitic Inheritance)
还有另一种方法可以编写Zparenizor。它不是从Parenizor函数中继承,而是编写一个调用Parenizor函数的构造器,再把它的结果传过来使用。而且,它也不是加入到公共方法里,而是在构造器中加入一些特定的方法:

function ZParenizor2(value) {    var self = new Parenizor(value);    self.toString = function () {        if (this.getValue()) {            return this.uber('toString');        }        return "-0-"    };    return self;}

传统继承的类之间是一个is-a关系,而在原型继承中是一个was-a关系。构造器有一个更广泛的对象构建范围。你可以看到uber née super 方法仍然能够在这些特定方法中使用。

类的扩充(Class Augmentation)
JavaScript的动态性允许你增加或取代一个已存类中的方法。你可以随时调用method方法,使得该类中所有当前和未来的实例中都包含它。你也可以随时对一个类进行扩充,其继承性会做出相应变化。这就被称为Class Augmentation ,它能够避免和Java中的extends相混淆。

对象的扩充(Object Augmentation)
在静态的面向对象的语言中,如果你想得到和另一个对象稍有差别的对象,你就得定义一个新的类。而在JavaScript中,你可以在一个单独对象中加入方法而无需重新定义一个类。这样一来你就会因为类的精简而实现更强大的功能,而且会使你的程序看起来更加简洁。在这一点上JavaScript和哈希表(hashtable)很相似。你可以随时添加新的值,如果该值是个函数的话,添加之后它会变成一种方法。

在前面的例子中,我没有用到Zparenizor类,我可以将实例做简单的修改:

myParenizor = new Parenizor(0);myParenizor.toString = function () {    if (this.getValue()) {        return this.uber('toString');    }    return "-0-";};myString = myParenizor.toString();

Sugar
为了实现上述例子,我写了四个“sugar” 方法。首先,有一个method方法,用它将一个实例方法添加到一个类中:

Function.prototype.method = function (name, func) {    this.prototype[name] = func;    return this;};

这样就把它作为一个公共的方法添加到Function.prototype里来了,所有的函数通过Class Augmentation使用它。它接受一个名称和一个函数,并将其加入到函数的prototype对象中。

它返回 “this”。在我写一个没有返回值的方法时,通常让它返回一个“this”,这能够实现一种层叠式样(cascade-style)的编程手法。

接下来用inherits方法指明一个类是从另一个类继承而来的。这个方法会在这两个类被定义之后以及在其子类中加入其他方法之前被调用:

Function.method('inherits', function (parent) {    this.prototype = new parent();    this.prototype.constructor = this;    this.method('uber', function (name) {        var func = this.constructor.prototype[name];        if (func == this[name]) {            func = parent.prototype[name];        }        return func.apply      (this, Array.prototype.slice.apply(arguments, [1]));    });    return this;});

这里你再次扩充了函数,使它继承了父类的一个实例并将它用于一个新的原型,还修改了constructor部分,并将uber方法一并添加到这个原型中来了。

这个uber方法要求在其自己的原型中使用一个指定的方法。这是为防止寄生继承性或者对象的扩充而调用的函数。如果你使用的是传统继承,你需要找到该父类prototype中的函数。Return语句使用函数的appl方法调用函数,比如设置this以及传入一组参数。该参数从arguments数组中得到。但实际上,这个argument数组不是一个真实的数组,所以你不得不再次使用apply以调用数组中的slice方法。

最后是swiss方法:

Function.method('swiss', function (parent) {    for (var i = 1; i < arguments.length; i += 1) {        var name = arguments[i];        this.prototype[name] = parent.prototype[name];    }    return this;});

在这里,swiss方法中循环访问arguments数组中的元素。对于每一个名称,它从父级prototype中复制一个member到它的新类的prototype中去。

你可以将JavaScript当作一个传统语言来使用,同时它还包含一个独特的表现类(level of expressiveness)。我们已经了解了什么是Classical Inheritance、Swiss Inheritance、Parasitic Inheritance、Class Augmentation以及Object Augmentation。这些大量的代码重用模式均来自这种比Java更容易更简洁的语言—JavaScript。


关于作者:
Douglas Crockford 是State Software 公司的创始人及CTO。他曾是Electric Communities的创立CEO,Paramount公司的新媒体主管以及Lucasfilm Ltd公司的技术主管。你可以通过crock@statesoftware.com 联系他。

原创粉丝点击