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

IT技术 javascript oop inheritance language-design prototype-programming
2021-01-26 11:03:52

所以这些年来我终于停止了拖延,并决定“正确地”学习 JavaScript。语言设计中最令人头疼的元素之一是继承的实现。有 Ruby 经验,我很高兴看到闭包和动态类型;但是对于我的一生,我无法弄清楚使用其他实例进行继承的对象实例有什么好处。

5个回答

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

首先让我们看看 JavaScript 程序员为保护原型继承而声明的最常见的论点(我从当前的答案池中获取这些论点):

  1. 这很简单。
  2. 它很强大。
  3. 它导致更小、更少冗余的代码。
  4. 它是动态的,因此更适合动态语言。

现在这些论点都是有效的,但没有人费心解释原因。这就像告诉孩子学习数学很重要一样。当然是,但孩子肯定不在乎;你不能说数学很重要就让孩子喜欢数学。

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

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

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

我们将它作为 Java 的小兄弟来推动,因为 Visual Basic 是当时微软语言家族中 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. class。
  2. 目的。
  3. 接口。
  4. 抽象类。
  5. 期末班。
  6. 虚拟基类。
  7. 构造函数。
  8. 破坏者。

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

正如 Steve Yegge 在他的经典博客文章“ N00b 的肖像”中所说

元数据是其他事物的任何类型的描述或模型。代码中的注释只是计算的自然语言描述。元数据元数据之所以成为元数据,是因为它并不是绝对必要的。如果我有一只带有一些谱系文件的狗,而我丢失了文件,我仍然拥有一只完全有效的狗。

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

好吧,我们也知道静态类型只是元数据。它们是一种专门针对两类读者的注释:程序员和编译器。静态类型讲述了一个关于计算的故事,大概是为了帮助两个读者群体理解程序的意图。但是静态类型可以在运行时丢弃,因为最终它们只是风格化的注释。它们就像血统文件:它可能会让某种缺乏安全感的性格类型对他们的狗更满意,但狗肯定不在乎。

正如我之前所说,课程给人们一种错误的安全感。例如NullPointerException,即使您的代码非常清晰,您在 Java 中也会得到太多的s。我发现经典继承通常会妨碍编程,但也许这只是 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,这意味着向新程序员展示如何使用原型模式而不是构造函数模式编写代码。

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

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

你经常使用克隆这个,这是完全错误的。Object.create正在创建具有指定原型的新对象。你选择的词给人的印象是原型被克隆了。
2021-03-26 11:03:52
clone绝对是一个糟糕的词选择。克隆被定义为其他东西的精确副本,这不是constructors发生的事情Object.create没有进行复制。现在答案的措辞方式,你冒着制造更多混乱的风险。
2021-03-26 11:03:52
@Aadit:真的没有必要这么防御。您的回答非常详细,值得一票。我并不是建议“链接”应该是“克隆”的直接替代品,但它更恰当地描述了对象与其继承的原型之间的联系,无论您是否断言自己对术语“克隆”的定义“ 或不。改还是不改,完全是你的选择。
2021-04-02 11:03:52
虽然我很欣赏你的回答的严谨和热情,但我不认为一个人一定要欣赏专为静态分析设计的语言之美。也许解决这个问题的一种方法是将您与一只狗相关的文书工作(元数据)进行类比。假设文书工作提供了对狗进行的医疗清单。当然,这些信息对于您的狗来说不是必要的。但它让兽医在下次治疗你的狗时有一些工作。
2021-04-04 11:03:52
@AaditMShah 虽然我喜欢你的第四点。想想维护几千行的多个 javascript 文件。我们需要在非常文件中查找变量。变量名中的一个小错误会造成严重破坏。编译器不会指出那个错误。到目前为止,还无法通过自动建议或自动更正来编写好的 IDE。变量名不能重构。并且 IDE 也不能建议 3rd 方module的接口。Javascript 程序员必须查找其他人的代码才有意义。Java 有可以内省的 jar 文件。静态类型语言具有所有这些优点。
2021-04-08 11:03:52

请允许我在线回答问题。

原型继承具有以下优点:

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

但是它有以下缺点:

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

我想你可以从上面的字里行间读出来,并提出传统类/对象方案的相应优点和缺点。当然,每个领域都有更多的内容,所以我会将其余部分留给其他人来回答。

我们今天有动态即时编译器,可以在代码运行时编译代码,为每个部分创建不同的代码段。JavaScript 实际上比使用经典类的 Ruby 或 Python 更快,因此即使您使用原型也是如此,因为已经做了很多优化工作。
2021-04-07 11:03:52
嘿,看,一个简洁的答案,不是粉丝。真的希望这是这个问题的最佳答案。
2021-04-09 11:03:52
这肯定是最佳答案。
2021-04-09 11:03:52

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

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

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

如果您按原型思考,您很快就会注意到您不需要类...

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

var obj = Object.create(baseInstance);

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

“你的代码更小,冗余更少......”,为什么?我已经查看了“差异继承”的维基百科链接,但没有任何内容支持这些断言。为什么经典继承会导致更大、更冗余的代码?
2021-03-20 11:03:52
没错,我同意诺埃尔的观点。原型继承只是完成工作的一种方式,但这并不是正确的方式。对于不同的工作,不同的工具将以不同的方式执行。原型继承有其一席之地。这是一个非常强大和简单的概念。话虽如此,缺乏对真正封装和多态的支持使 JavaScript 处于明显的劣势。这些方法比 JavaScript 存在的时间要长得多,而且它们的原理是合理的。所以认为原型是“更好的”完全是错误的心态。
2021-04-01 11:03:52
您可以使用基于原型的继承来模拟基于类的继承,但反之则不然。这可能是一个很好的论据。此外,我将封装更多地视为一种约定而不是语言功能(通常您可以通过反射来破坏封装)。关于多态性 - 您所获得的只是在检查方法参数时不必编写一些简单的“if”条件(如果在编译期间解析目标方法,则速度会更快)。这里没有真正的 JavaScript 缺点。
2021-04-08 11:03:52

这两种方法之间真的没有太多选择。要掌握的基本思想是,当 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"

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

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

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

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

经典与原型继承 - VoidCC

经典与原型继承

我认为最好总结链接的内容而不是粘贴链接(我自己曾经做过的事情),除非它是另一个 SO 链接。这是因为链接/网站出现故障,您无法回答问题,这可能会影响搜索结果。
2021-03-28 11:03:52
第一个链接没有回答为什么原型继承的问题它只是简单地描述了它。
2021-04-07 11:03:52