JavaScript是一个无class的面向对象语言,它使用原型继承而非类继承。这会让那些使用传统面向对象语言如C++和Java的程序员们感到困惑。正如我们所看到的,JavaScript的原型继承比类继承具有更强的表现力。

  但首先,要搞清楚我们为什么如此关注继承?主要有两个原因。首先是方便类型的转换。我们希望语言系统能够对那些相似类的引用进行自动转换。而对于一个要求对引用对象进行显示转换的类型系统来说只能获得很少的类型安全性。这对于强类型语言来说很重要,但是在像JavaScript这样的松散型语言中,永远不需要对对象引用进行强制转换。

  第二个原因是代码的复用。代码中存在大量拥有相同方法的对象是十分常见的。类可以通过一组定义来创建它们。另外存在很多相似的对象也很普遍,这些对象中只有少数有关添加和修改的方法存在区别。类的继承可以很有效地解决这些问题,但原型继承更有效。

  为了说明这一点,我们将介绍一点语法糖,它允许我们以类似于传统的class的语言来编写代码。然后我们将介绍一些有用的模式,这些模式不适用于传统的class语言。最后,我们将对语法糖进行解释。

类继承

  首先,我们添加了一个Parenizor类,包含set和get两个方法,分别用来设置和获取value,以及一个toString方法,用来对parens中的value进行包装。

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接受方法的名称和一个function,并将这个function作为公共方法添加到类中。

  然后我们可以这样写:

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

  正如你所期望的,myString的值为"(0)".

  现在我们创建另一个类继承Parenizor,除了toString方法中对于value为空或0的情况会输出"-0-"外其余都和Parenizor相同。

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-".

  JavaScript没有类,但是我们可以通过编程来实现它。

多重继承

  通过操作一个函数的原型对象,我们可以实现多重继承,从而使我们可以用多个类的方法来构建一个类。混合多重继承可能难以实现,并可能存在方法名称的冲突。我们可以在JavaScript中实现混合多重继承,但是在本例中我们将使用一个更严格的被称之为Swiss继承的形式。

  假设有一个NumberValue类,包含一个方法setValue,该方法检查value是否为某个特定范围内的数字,必要的时候会抛出异常。我们只需要ZParenizorsetValuesetRange方法,而不需要toString方法。那么我们可以这样写:

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

  这样只会将我们需要的方法添加到类中。

寄生继承

  ZParenizor还有另外一种写法。除了从Parenizor类继承,我们还可以在构造函数中调用Parenizor的构造函数,并传递返回的结果。通过这种方式,我们给构造函数添加特权方法,而不用再去为其添加公共方法。

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

  类的继承是is-a关系(公有继承),而寄生继承是was-a-but-now's-a关系(私有继承与公有继承)。构造函数在对象的构造中发挥了很大的作用。注意ubersuper方法仍然可用于特权方法。

类的扩充

  JavaScript的动态性允许我们添加或替换现有类的方法,method方法可以随时被调用,这样类的所有实例在现在和将来都会有这个方法。我们可以在任何时候对一个类进行扩展。继承具有追溯性,我们把这个叫做类的扩充(Class Augmentation),以避免与Java的extends产生混淆。

对象的扩充

  在静态面向对象语言中,如果你想要一个对象与另一个对象略微不同,就需要定义一个新的类。在JavaScript中,你可以将方法添加到单个的对象中,而不需要在定义额外的类。这个非常强大,因为你只需要写很少的类,并且类都可以很简单。回想一下,JavaScript对象就像哈希表,你可以随时添加新的值,如果值是function,那么它就成了一个方法。

  因此在上面的示例中,我根本不需要ZParenizor类。我可以简单地修改我的实例。

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

  我将toString方法添加到我的myParenizor实例中,而没有使用任何形式的继承。我们可以修改单个的实例,因为语言是无class的。

Sugar(语法糖)

  为了使上面的示例能正常工作,我写了四个sugar方法。首先是method方法,它将一个实例方法添加到类中。

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

  它在Function.prototype上添加了一个公共方法,因此所有的函数都通过Class Augmentation(类的扩充)获得了该方法。它接受一个名称和一个函数,并将它们添加到函数的原型对象中。

  它返回this. 当我编写一个不需要返回值的方法时,我通常都会返回this,这样就具有了一个级联式的编程风格。

  接下来是inherits方法,它用来表示一个类从另一个类继承。应该在两个类都被定义之后再调用这个方法,并且在继承类的方法之前添加该方法。

Function.method('inherits', function (parent) {
this.prototype = new parent();
var d = {},
p = this.prototype;
this.prototype.constructor = parent;
this.method('uber', function uber(name) {
if (!(name in d)) {
d[name] = 0;
}
var f, r, t = d[name], v = parent.prototype;
if (t) {
while (t) {
v = v.constructor.prototype;
t -= 1;
}
f = v[name];
} else {
f = p[name];
if (f == this[name]) {
f = v[name];
}
}
d[name] += 1;
r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
d[name] -= 1;
return r;
});
return this;
});

  我们继续对Function进行扩充。我们创建了一个父类的实例,并将其作为新的原型。我们还修改了构造函数的字段,并将uber方法添加到原型中。

  Uber方法在自己的原型中查找指定的方法。这是在寄生继承或对象扩充的情况下调用的函数。如果我们进行类的继承,那么我们就需要在父类的原型中找到这个函数。Return语句使用函数的apply方法来调用function,显示地设置this并传递一个数组参数。参数(如果有的话)从arguments数组中获取。可惜arguments数组不是一个真正的数组,所以我们不得不再次使用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进行遍历。对每一个name,它都从父类的原型中复制一个成员到新类的原型中。

结论

  JavaScript可以像class语言一样来使用,但它也具有相当独特的表现力。我们研究了类的继承,Swiss继承,寄生继承,类的扩充以及对象的扩充。这种大量代码的复用模式来自于一种被认为比Java更小,更简单的语言。

  类的对象非常严格,要将一个新成员添加到对象中,唯一的方法就是创建一个新类。而在JavaScript中,对象是松散的,可以通过简单的赋值操作将一个新成员添加到对象中。

  由于JavaScript中的对象非常灵活,所以你需要对类的层次结构进行不同的考虑。深层次的结构并不太适用,相反,浅层次的结构更高效,更具有表现力。

我从事编写JavaScript代码已经有14年了,而且我从来没有发现需要使用uber函数。Super在class模式中十分重要,但是在原型和函数式模式中不是必须的。现在看来我早期尝试在JavaScript中支持class模式是一个错误。

原文地址:Classical Inheritance in JavaScript

相关链接:http://www.cnblogs.com/sanshi/archive/2009/07/08/1519036.html

JavaScript中的类继承的更多相关文章

  1. 深入理解JavaScript中的类继承

    由于写本文时全部是在编辑器中边写代码边写感想的,所以,全部思想都写在代码注释里面了 // 类继承 //todo.1 extends 关键字 class Animal { constructor(nam ...

  2. 理解JavaScript中的原型继承(2)

    两年前在我学习JavaScript的时候我就写过两篇关于原型继承的博客: 理解JavaScript中原型继承 JavaScript中的原型继承 这两篇博客讲的都是原型的使用,其中一篇还有我学习时的错误 ...

  3. JavaScript中的类

          JavaScript类的相关知识 1.例子 /* 例1 */// 定义一个构造函数function Range(from, to){ this.from = from; this.to = ...

  4. JavaScript中一个对象如何继承另外一个对象

    如题,JavaScript中一个对象a如何继承另外一个对象b.即将b中的属性和方法复制到a中去. 面试中遇到了这个问题,当时脑子里的想法是: 1.除了循环遍历复制,还能怎样 2.javascript中 ...

  5. Javascript中的类实现

    Javascript本身并不支持面向对象,它没有访问控制符,它没有定义类的关键字class,它没有支持继承的extend或冒号,它也没有用来支持虚函数的virtual,不过,Javascript是一门 ...

  6. JavaScript中定义类的方式详解

    本文实例讲述了JavaScript中定义类的方式.分享给大家供大家参考,具体如下: Javascript本身并不支持面向对象,它没有访问控制符,它没有定义类的关键字class,它没有支持继承的exte ...

  7. JavaScript中创建类,赋值给ajax中的data参数

    缘由:因为要给根据是否选中checkbox来动态增加ajax中data的属性(ajax的data属性格式的几种方法,参考http://www.jb51.net/article/46676.htm) d ...

  8. javascript中的原型继承

    在Javascript面向对象编程中,原型继承不仅是一个重点也是一个不容易掌握的点.在本文中,我们将对Javascript中的原型继承进行一些探索. 基本形式 我们先来看下面一段代码: <cod ...

  9. C++中的类继承之单继承&多继承&菱形继承

     C++中的类继承之单继承&多继承&菱形继承 单继承是一般的单一继承,一个子类只 有一个直接父类时称这个继承关系为单继承.这种关系比较简单是一对一的关系: 多继承是指 一个子类有两个或 ...

随机推荐

  1. 读书笔记《CSS权威指南》

    阅读本书主要目的: 自从学会CSS以来,虽然熟练掌握了其使用方法和技巧,但对其底层的原理和实现并不清晰,阅读本书想进一步系统化的学习和深入研究其本质,对这门前端基础语言从熟练使用到真正理解. 第1章 ...

  2. Centos7 Zookeeper

    本文版权归博客园和作者吴双本人共同所有 转载和爬虫请注明原文地址 www.cnblogs.com/tdws 一.写在前面 ZK是一个高效的分布式协调服务,高可用的分布式管理协调框架. 朋友推荐一本书& ...

  3. [bzoj1783] [Usaco2010 Jan]Taking Turns

    题意: 一排数,两个人轮流取数,保证取的位置递增,每个人要使自己取的数的和尽量大,求两个人都在最优策略下取的和各是多少. 注:双方都知道对方也是按照最优策略取的... 傻逼推了半天dp......然后 ...

  4. hdu_5104 Primes Problem()

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5104 rimes Problem Time Limit: 2000/1000 MS (Java/Oth ...

  5. zlib1.2.11静态编译

    1.进入官网http://zlib.net/,下载且解压zlib1211.zip: 2. 打开已解压的zlib-1.2.11目录,找到win32文件夹 3.将Makefile.msc复制到上一层,也就 ...

  6. Node类型知识大全

    Node类型 1.节点关系 每个节点都有一个childNodes属性,其中保存着一个NodeList对象.NodeList是一种类数组对象,用于保存一组有序的节点,可以通过位置来访问这些节点.请注意, ...

  7. UE4 TSubclassOf VS Native Pointer

    最近看到了TSubclassOf ,所以想要弄清楚跟一般指针的区别~ NativePointer    VS     UClass*      VS     TSubclassOf AActor* p ...

  8. Linq 实例

    1.分页 ).Take(); 2.分组 1)一般分组 //根据顾客的国家分组,查询顾客数大于5的国家名和顾客数var 一般分组 = from c in ctx.Customers group c by ...

  9. virtuoso装载大的rdf文件的方法

    本文详细介绍了将一个比较大的rdf文件装载到virtuoso数据库的过程.参考virtuoso网站的文档说明,通过实践,将一个大约4.6G左右的nt文件装载到virtuoso数据库中,用了大概6个多小 ...

  10. Oracle创建、管理撤销表空间

    撤销管理模式: 用户通过设定撤销管理模式(undo mode)就可以灵活地选择使用手动撤销管理(manual undo management)或自动撤销管理(automatic undo manage ...