原型继承相对于经典继承的好处?真正的原型遗传JavaScript 中的原型继承1. 原型继承很简单2. 原型继承功能强大3. 原型继承的冗余更少4. 原型遗传是动态的结论

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


答案 1

我知道这个答案晚了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中做了原型继承。这不是很简单吗?你拿一个对象,克隆它,改变你需要的任何东西,嘿,presto - 你给自己买了一个全新的对象。

现在你可能会问,“这怎么简单?每次我想创建一个新圆圈时,我都需要克隆并手动为其分配一个半径”。好吧,解决方案是使用一个函数来为您完成繁重的工作: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 中的原型继承

如果您在上面的程序中注意到该函数创建了 一个 克隆 ,会为其分配一个新的克隆,然后返回它。这正是构造函数在 JavaScript 中所做的:createcircleradius

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 中的构造函数模式是反转的原型模式。不是创建对象,而是创建构造函数。关键字将构造函数内的指针绑定到构造函数的克隆。newthisprototype

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

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

还有很多其他原因可以解释为什么应该避免JavaScript中的构造函数模式。你可以在我的博客文章中阅读它们:构造函数与原型


那么,与经典遗传相比,原型遗传有什么好处呢?让我们再次回顾最常见的论点,并解释原因

1. 原型继承很简单

CMS在他的回答中指出:

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

让我们考虑一下我们刚刚做了什么。我们创建了一个半径为 .然后我们克隆了它,并给克隆的半径一个半径。circle510

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

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

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

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

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

正如Steve Yegge在他的经典博客文章“Portrait of a N00b”中所说的那样:

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

从同样的角度来看,类只是元数据。继承不是严格要求的类。然而,有些人(通常是n00bs)发现使用类更舒适。这给了他们一种虚假的安全感。

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

正如我之前所说,课程给人一种虚假的安全感。例如,即使您的代码完全清晰易读,在Java中您也得到了太多的s。我发现经典继承通常会妨碍编程,但也许这只是Java。Python有一个惊人的经典继承系统。NullPointerException

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,这意味着向新程序员展示如何使用原型模式而不是构造函数模式编写代码。

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

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


答案 2

请允许我以内联方式实际回答这个问题。

原型继承具有以下优点:

  1. 它更适合动态语言,因为继承与它所处的环境一样动态(对JavaScript的适用性在这里应该很明显。这允许您在动态中快速执行操作,例如自定义类,而无需大量基础结构代码。
  2. 实现原型对象方案比经典类/对象二分法方案更容易。
  3. 它消除了对对象模型周围复杂锋利边缘的需求,例如“元类”(我从来都不喜欢元类......对不起!或“特征值”或类似的东西。

但是,它具有以下缺点:

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

我认为你可以在上面的行之间阅读,并提出传统类/对象方案的相应优缺点。当然,每个领域都有更多,所以我将把剩下的留给其他人来回答。