原型继承相对于经典的好处?


271

因此,这些年来,我终于不再拖延脚步,决定“适当”学习JavaScript。语言设计最令人头疼的元素之一就是继承的实现。拥有Ruby的经验,我很高兴看到闭包和动态类型。但是对于我一生来说,无法弄清楚使用其他实例进行继承的对象实例将带来什么好处。



Answers:


560

我知道这个答案要晚3年了,但是我真的认为当前的答案不能提供足够的信息来说明原型继承比经典继承更好

首先,让我们看一下JavaScript程序员为捍卫原型继承而声明的最常见的参数(我从当前的答案库中获取这些参数):

  1. 这很简单。
  2. 功能强大。
  3. 它导致更小的,更少的冗余代码。
  4. 它是动态的,因此对于动态语言来说更好。

现在这些论点都是有效的,但是没有人愿意去解释原因。这就像告诉孩子学习数学很重要。当然可以,但是孩子当然不在乎;并不能通过说这很重要来使孩子喜欢数学。

我认为原型继承的问题在于它是从JavaScript的角度进行解释的。我喜欢JavaScript,但是JavaScript的原型继承是错误的。与经典继承不同,原型继承有两种模式:

  1. 原型继承的原型模式。
  2. 原型继承的构造函数模式。

不幸的是,JavaScript使用原型继承的构造函数模式。这是因为创建JavaScript时,Brendan Eich(JS的创建者)希望它看起来像Java(具有经典继承):

我们把它作为Java的弟弟推销,因为像Visual Basic这样的补充语言当时是Microsoft语言家族中的C ++。

这很糟糕,因为当人们在JavaScript中使用构造函数时,他们会想到从其他构造函数继承的构造函数。错了 在原型继承中,对象从其他对象继承。构造函数永远不会出现。这就是让大多数人困惑的地方。

来自Java之类的具有经典继承性的语言的人们变得更加困惑,因为尽管构造函数看起来像类,但它们的行为却不像类。正如道格拉斯·克罗克福德所说:

这种间接作用旨在使经过经典训练的程序员对这种语言更加熟悉,但是这样做却没有做到,正如我们从Java程序员对JavaScript的极低见解中可以看到的那样。JavaScript的构造器模式对古典人群没有吸引力。它还掩盖了JavaScript的真正原型性质。结果,很少有知道如何有效使用该语言的程序员。

你有它。直接从马的嘴巴。

真正的原型继承

原型继承是关于对象的。对象从其他对象继承属性。这里的所有都是它的。有两种使用原型继承创建对象的方法:

  1. 创建一个全新的对象。
  2. 克隆现有对象并对其进行扩展。

注意: JavaScript提供了两种克隆对象的方法- 委托串联。从今以后,我将使用“克隆”一词专门指代通过委派的继承,而使用“复制”一词专门指代通过级联的继承。

聊够了。让我们看一些例子。假设我有一个半径范围5

var circle = {
    radius: 5
};

我们可以从圆的半径计算出圆的面积和周长:

circle.area = function () {
    var radius = this.radius;
    return Math.PI * radius * radius;
};

circle.circumference = function () {
    return 2 * Math.PI * this.radius;
};

现在,我想创建另一个半径圆10。一种方法是:

var circle2 = {
    radius: 10,
    area: circle.area,
    circumference: circle.circumference
};

但是JavaScript提供了更好的委托方式。该Object.create函数用于执行以下操作:

var circle2 = Object.create(circle);
circle2.radius = 10;

就这样。您只是在JavaScript中进行了原型继承。那不是那么简单吗?您拿了一个对象,对其进行克隆,更改所需的内容,然后嘿,您便拥有了一个全新的对象。

现在您可能会问:“这有多简单?每次我要创建一个新圆时,都需要克隆circle并手动为其指定半径”。好了,解决方案是使用一个函数为您完成繁重的工作:

function createCircle(radius) {
    var newCircle = Object.create(circle);
    newCircle.radius = radius;
    return newCircle;
}

var circle2 = createCircle(10);

实际上,您可以将所有这些组合成一个对象文字,如下所示:

var circle = {
    radius: 5,
    create: function (radius) {
        var circle = Object.create(this);
        circle.radius = radius;
        return circle;
    },
    area: function () {
        var radius = this.radius;
        return Math.PI * radius * radius;
    },
    circumference: function () {
        return 2 * Math.PI * this.radius;
    }
};

var circle2 = circle.create(10);

JavaScript中的原型继承

如果您在上述程序中注意到该create函数创建一个的克隆circle,则为其分配一个新值radius,然后将其返回。这正是构造函数在JavaScript中所做的工作:

function Circle(radius) {
    this.radius = radius;
}

Circle.prototype.area = function () {
    var radius = this.radius;
    return Math.PI * radius * radius;
};

Circle.prototype.circumference = function () {         
    return 2 * Math.PI * this.radius;
};

var circle = new Circle(5);
var circle2 = new Circle(10);

JavaScript中的构造函数模式是倒置的原型模式。无需创建对象,而是创建构造函数。的new关键字结合this构造内部指针的一个克隆prototype的构造的。

听起来令人困惑?这是因为JavaScript中的构造函数模式不必要地使事情复杂化。这是大多数程序员难以理解的。

他们没有想到从其他对象继承的对象,而是想到了从其他构造函数继承的构造函数,然后变得完全困惑。

还有很多其他原因应避免使用JavaScript中的构造函数模式。您可以在我的博客文章中阅读有关它们的信息:构造函数与原型


那么,原型继承比经典继承有什么好处?让我们再次审视最常见的论点,并解释原因

1.原型继承很简单

CMS在他的回答中指出:

在我看来,原型继承的主要好处是它的简单性。

让我们考虑一下我们刚刚做了什么。我们创建了circle一个半径为的对象5。然后我们对其进行克隆,并将克隆的半径设置为10

因此,我们只需要两件事就可以使原型继承工作:

  1. 一种创建新对象的方法(例如,对象文字)。
  2. 扩展现有对象的方法(例如Object.create)。

相比之下,经典继承要复杂得多。在经典继承中,您可以:

  1. 类。
  2. 目的。
  3. 接口。
  4. 抽象类。
  5. 期末课程。
  6. 虚拟基类。
  7. 构造函数。
  8. 析构函数。

你明白了。关键是原型继承更容易理解,更容易实现和更容易推理。

正如史蒂夫·耶格(Steve Yegge)在他的经典博客文章“ N00b的肖像 ”中所说的那样:

元数据是其他任何形式的描述或模型。您代码中的注释只是对计算的自然语言描述。使元数据成为元数据的原因在于它不是绝对必要的。如果我的狗有一些家谱文件,而我却丢失了文件,那么我仍然有一只非常有效的狗。

从同样的意义上讲,类只是元数据。继承不是严格要求的类。但是,有些人(通常为n00bs)发现使用此类课程更舒适。这给他们一种虚假的安全感。

好吧,我们也知道静态类型只是元数据。它们是针对两种读者的一种特殊类型的注释:程序员和编译器。静态类型讲述了有关计算的故事,大概是为了帮助两个读者群体了解程序的意图。但是静态类型可以在运行时被丢弃,因为最后它们只是风格化的注释。他们就像家谱的文书工作:也许会使某种不安全的性格类型更快乐地对待他们的狗,但狗当然不在乎。

如前所述,类给人一种错误的安全感。例如NullPointerException,即使您的代码清晰易读,您在Java中也会收到太多。我发现经典继承通常会妨碍编程,但也许那只是Java。Python具有惊人的经典继承系统。

2.原型继承是强大的

来自古典背景的大多数程序员都认为古典继承比原型继承更强大,因为它具有:

  1. 私有变量。
  2. 多重继承。

该说法是错误的。我们已经知道JavaScript 通过闭包支持私有变量,但是多重继承又如何呢?JavaScript中的对象只有一个原型。

事实是,原型继承支持从多个原型继承。原型继承只是意味着一个对象从另一个对象继承。实际上,有两种方法可以实现原型继承

  1. 委托或差异继承
  2. 克隆或串联继承

是的,JavaScript仅允许对象委托给另一个对象。但是,它允许您复制任意数量的对象的属性。例如_.extend这样做。

当然,许多程序员不认为这是正确的,因为继承instanceofisPrototypeOf别的说法。但是,可以通过在通过串联从原型继承的每个对象上存储一系列原型,来轻松地纠正这种情况:

function copyOf(object, prototype) {
    var prototypes = object.prototypes;
    var prototypeOf = Object.isPrototypeOf;
    return prototypes.indexOf(prototype) >= 0 ||
        prototypes.some(prototypeOf, prototype);
}

因此,原型继承与经典继承一样强大。实际上,它比经典继承强大得多,因为在原型继承中,您可以从不同的原型中手动选择要复制的属性和要忽略的属性。

在经典继承中,不可能(或至少非常困难)选择要继承的属性。他们使用虚拟基类和接口来解决菱形问题

但是,在JavaScript中,您很可能永远不会听说钻石问题,因为您可以精确控制要继承的属性以及从哪些原型继承的属性。

3.原型继承较少冗余

这一点很难解释,因为经典继承不一定会导致更多的冗余代码。实际上,无论是经典继承还是原型继承,都可以用来减少代码中的冗余。

一种说法可能是,大多数具有经典继承性的编程语言都是静态类型的,并且要求用户显式声明类型(与具有隐式静态类型的Haskell不同)。因此,这导致了更冗长的代码。

Java因这种行为而臭名昭著。我清楚地记得Bob Nystrom在他关于Pratt Parsers的博客文章中提到了以下轶事:

您一定会喜欢Java的官僚机构的“请一式四份签名”级别。

同样,我认为那只是因为Java很烂。

一个有效的论据是,并非所有具有经典继承的语言都支持多重继承。再一次想到Java。是的,Java有接口,但这还不够。有时您确实需要多重继承。

由于原型继承允许多重继承,因此,如果使用原型继承而不是使用具有经典继承但没有多重继承的语言编写,则需要多重继承的代码将减少冗余。

4.原型继承是动态的

原型继承的最重要优点之一是,可以在原型创建后向其添加新属性。这使您可以向原型添加新方法,该方法将自动提供给委派给该原型的所有对象。

在经典继承中这是不可能的,因为一旦创建了类,就无法在运行时对其进行修改。与经典继承相比,这可能是原型继承的最大优点,应该放在首位。但是,我喜欢尽全力以赴。

结论

原型继承很重要。对于JavaScript程序员,为什么要放弃原型继承的构造函数模式而转而使用原型继承的原型模式,这一点很重要。

我们需要正确地开始讲授JavaScript,这意味着向新程序员展示如何使用原型模式而不是构造函数模式编写代码。

使用原型模式解释原型继承不仅更加容易,而且还将使更好的程序员成为可能。

如果您喜欢这个答案,那么您还应该阅读我的博客文章“ 为什么原型继承很重要 ”。相信我,您不会失望的。


33
虽然我了解您的来源,并且同意原型继承非常有用,但是我认为通过假设“原型继承比经典继承更好”,您注定会失败。为了给您一些视角,我的库jTypes是JavaScript的经典继承库。因此,作为一个花时间做到这一点的人,我仍然会坐在这里,说原型继承很棒而且非常有用。但这仅仅是程序员拥有的众多工具中的一种。原型继承也有许多缺点。
redline

7
我完全同意你所说的。我感到太多的程序员因为缺乏经典继承而拒绝JavaScript,或者谴责它为一种简单而愚蠢的语言。我同意,它实际上是一个非常强大的概念,许多程序员应该接受并学习它。话虽这么说,我也感到JavaScript开发人员中有很多反对任何形式的经典继承,而实际上他们根本没有论点的基础。两者本身都具有强大的功能,也同样有用。
redline

9
好的,这是您的意见,但是我将继续持不同意见,并且我认为诸如CoffeeScript和TypeScript之类的东西越来越受欢迎表明,有大量的开发人员希望在适当的时候使用此功能。如您所说,ES6添加了语法糖,但仍不提供jTypes的广泛性。顺便说一句,我不是负责您的否决票的人。尽管我不同意您的看法,但我认为这并不构成您的错误回答。你很彻底
redline 2013年

25
您经常使用单词克隆,这完全是错误的。Object.create正在使用指定的原型创建新对象。您选择的单词会给人以原型被克隆的印象。
Pavel Horal 2013年

7
@Aadit:真的没有必要这么防御。您的答案非常详细,值得投票。我并不是在建议“链接”代替“克隆”,而是建议您更恰当地描述对象与其继承的原型之间的联系,无论您是否断言“克隆”是您自己的定义。 “ 或不。更改或不更改,完全由您选择。
2014年

42

请允许我实际内联回答问题。

原型继承具有以下优点:

  1. 它更适合动态语言,因为继承与环境一样是动态的。(对JavaScript的适用性在这里应该很明显。)这使您可以快速进行操作,就像在不使用大量基础结构代码的情况下自定义类。
  2. 与经典的类/对象二分法相比,实现原型对象方案要容易得多。
  3. 它消除了对象模型周围复杂的尖锐边缘的需要,例如“元类”(我从来没有喜欢过的元类……对不起!)或“特征值”之类。

但是,它具有以下缺点:

  1. 对原型语言进行类型检查不是不可能的,但这是非常非常困难的。原型语言的大多数“类型检查”是纯运行时“鸭子类型”样式检查。这并不适合所有环境。
  2. 同样,通过静态(或通常甚至是动态!)分析来优化方法分配等操作也很困难。它可以(我强调:)是非常低效的变得非常容易。
  3. 类似地,在原型语言中,对象创建可能(并且通常比)在更常规的类/对象二分方案中要慢得多。

我认为您可以在上述两行之间进行阅读,并提出传统类/对象方案的相应优点和缺点。当然,每个区域都有更多,因此我将其余部分留给其他人回答。


1
嘿,看似简洁的答案 真的希望这是该问题的最佳答案。
Siliconrockstar

今天,我们有动态即时编译器,可以在代码运行时编译代码,从而为每个部分创建不同的代码段。实际上,JavaScript的速度比使用经典类的Ruby或Python快,因为即使使用原型也是如此,因为在优化它方面已经完成了大量工作。
aoeu256 '19

28

IMO原型继承的主要好处是它的简单性。

语言的原型性质可能会使受过经典训练的人们感到困惑,但是事实证明,这实际上是一个非常简单而强大的概念,即差异继承

您不需要进行分类,您的代码更小,更少冗余,对象从其他更通用的对象继承。

如果您以原型方式思考,您很快就会发现您不需要课程...

原型继承将在不久的将来变得更加流行,ECMAScript 5th Edition规范引入了该Object.create方法,该方法使您可以生成一个新的对象实例,该对象实例可以以一种非常简单的方式从另一个实例继承:

var obj = Object.create(baseInstance);

所有浏览器供应商都在实施该标准的新版本,我认为我们将开始看到更多的纯原型继承。


11
“为什么您的代码更小,更少冗余...”,为什么呢?我看过Wikipedia链接的“差异继承”,没有任何东西支持这些断言。为什么经典继承会导致更大,更多的冗余代码?
Noel Abrahams 2012年

4
的确,我同意诺埃尔(Noel)的观点。原型继承只是完成工作的一种方法,但这并不是正确的方法。不同的工具将以不同的方式执行不同的工作。原型继承有其地位。这是一个非常强大且简单的概念。话虽如此,缺乏对真正封装和多态性的支持,使JavaScript处于明显的劣势。这些方法的使用时间比JavaScript长得多,并且其原理很合理。因此,认为原型“更好”是完全错误的心态。
redline 2013年

1
您可以使用基于原型的继承来模拟基于类的继承,反之则不行。这可能是一个很好的论点。另外,我将封装更多地视为一种约定,而不是语言功能(通常,您可以通过反射来破坏封装)。关于多态性-您获得的所有好处就是,在检查方法参数时不必编写一些简单的“ if”条件(如果在编译过程中解决了目标方法,则速度会有所提高)。这里没有真正的JavaScript缺点。
Pavel Horal 2013年

原型很棒,IMO。我正在考虑创建一种类似Haskell的函数式语言...但是,我不会创建抽象,而是将所有内容都建立在原型上。而不是一般化总和,并且阶乘以“折叠”,您应该建立总和函数的原型,并将+替换为*,将0替换为1以生成乘积。我将把“ Monads”解释为原型,该原型将promise / callback中的“ then”替换为flatMap是“ then”的同义词。我认为原型可以帮助将功能编程带入大众。
aoeu256 '19

11

两种方法之间确实没有太多选择。要掌握的基本思想是,当为JavaScript引擎赋予要读取的对象的属性时,它首先检查实例,如果缺少该属性,它将检查原型链。这是显示原型和古典之间的区别的示例:

原型

var single = { status: "Single" },
    princeWilliam = Object.create(single),
    cliffRichard = Object.create(single);

console.log(Object.keys(princeWilliam).length); // 0
console.log(Object.keys(cliffRichard).length); // 0

// Marriage event occurs
princeWilliam.status = "Married";

console.log(Object.keys(princeWilliam).length); // 1 (New instance property)
console.log(Object.keys(cliffRichard).length); // 0 (Still refers to prototype)

经典的实例方法 (效率低下,因为每个实例都存储自己的属性)

function Single() {
    this.status = "Single";
}

var princeWilliam = new Single(),
    cliffRichard = new Single();

console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 1

高效古典

function Single() {
}

Single.prototype.status = "Single";

var princeWilliam = new Single(),
    cliffRichard = new Single();

princeWilliam.status = "Married";

console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 0
console.log(cliffRichard.status); // "Single"

如您所见,由于可以操纵以经典样式声明的“类”的原型,因此使用原型继承确实没有任何好处。它是经典方法的子集。


2
查看有关该主题的其他答案和资源,您的答案似乎像是在说:“原型继承是添加到JavaScript中的语法糖的子集,以允许出现经典继承。” OP似乎在询问JS中的原型继承相对于其他语言中的经典继承的好处,而不是比较JavaScript中的实例化技术。
暗淡的推子,2015年

2

Web开发:原型继承与经典继承

http://chamnapchhorn.blogspot.com/2009/05/prototypal-inheritance-vs-classical.html

经典vs原型继承-代码日志

古典与原型继承


20
我认为最好总结链接的内容,而不是粘贴链接(我自己曾经做过的事情),除非它是另一个SO链接。这是因为链接/站点断开了,您失去了对该问题的回答,并且可能影响搜索结果。
詹姆斯·韦斯特盖特

第一个链接没有回答为什么原型继承的问题?它只是描述它。
viebel 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.