原型继承比经典继承的好处?

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

82713 次浏览

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

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

经典Vs原型继承-堆栈溢出

经典Vs原型继承

请允许我直接回答这个问题。

原型继承具有以下优点:

  1. 它更适合动态语言,因为继承和它所处的环境一样是动态的。(这里对JavaScript的适用性应该是显而易见的。)这使得您可以在不需要大量基础设施代码的情况下快速地完成一些事情,比如定制类。
  2. 实现原型对象方案比实现经典的类/对象二分方案更容易。
  3. 它消除了对象模型周围复杂的尖锐边缘的需要,比如“元类”(我从来没有喜欢元类……抱歉!)或者“特征值”之类的。

但它有以下缺点:

  1. 对原型语言进行类型检查并非不可能,但是非常非常困难。大多数原型语言的“类型检查”都是纯粹的运行时“duck typing”风格的检查。这并不适用于所有环境。
  2. 同样,通过静态(或者经常是动态!)分析来优化方法分派也很困难。它可以(我强调:可以)非常低效,非常容易。
  3. 类似地,在原型语言中创建对象可能(通常)比在更传统的类/对象二分模式中慢得多。

我认为您可以从上面的字里行间看出传统类/对象方案的相应优点和缺点。当然,每个领域都有更多的问题,所以我把剩下的留给其他人来回答。

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

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

你不需要创建分类,你的代码更小,更少冗余,对象继承自其他更一般的对象。

如果你认为图片,你很快就会发现你不需要类…

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

var obj = Object.create(baseInstance);

所有浏览器厂商都在实现这个新版本的标准,我认为我们将开始看到更多纯粹的原型继承……

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

正如您所看到的,由于可以操作以经典风格声明的“类”的原型,因此使用原型继承实际上没有任何好处。它是经典方法的一个子集。

我知道这个答案晚了3年,但我真的认为目前的答案没有提供关于原型继承比经典继承好在哪里的足够信息。

首先让我们看看JavaScript程序员在捍卫原型继承时最常见的参数(我从当前的答案池中选取了这些参数):

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

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

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

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

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

我们把它作为Java的小兄弟,作为一种补充语言,就像Visual Basic是当时微软语系中c++的补充语言一样。

这很糟糕,因为当人们在JavaScript中使用构造函数时,他们认为构造函数继承自其他构造函数。这是错误的。在原型继承中,对象从其他对象继承。构造函数从来没有出现过。这是大多数人困惑的地方。

来自Java等具有经典继承的语言的人会更加困惑,因为尽管构造函数看起来像类,但它们的行为却不像类。如道格拉斯Crockford所述:

这种间接的方式是为了让受过传统训练的程序员对这种语言更加熟悉,但我们从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中的构造函数模式。你可以在我的博客文章中阅读:构造函数vs原型


那么,与经典继承相比,原型继承的好处是什么呢?让我们再过一遍最常见的参数,并解释为什么

1. 原型继承很简单

CMS在他的回答中说:

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

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

因此,我们只需要两个东西来使原型继承工作:

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

相比之下,经典的继承要复杂得多。在经典遗传中有:

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

你懂的。关键是原型继承更容易理解,更容易实现,也更容易推理。

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

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

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

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

如前所述,课程给人一种虚假的安全感。例如,你会在Java中得到太多的__abc,即使你的代码是完全易读的。我发现经典的继承通常会阻碍编程,但这可能只是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因这种行为而臭名昭著。我清楚地记得鲍勃Nystrom在他的博客文章中提到了以下关于普拉特解析器的轶事:

你一定会喜欢Java的“请签署一式四份”级别的官僚主义。

同样,我认为这只是因为Java太糟糕了。

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

由于原型继承允许多重继承,使用原型继承编写需要多重继承的代码比使用具有经典继承但没有多重继承的语言编写的代码更少冗余。

4. 原型继承是动态的

原型继承最重要的优点之一是,您可以在创建原型后向原型添加新属性。这允许你向原型中添加新方法,这些方法将自动对委托给该原型的所有对象可用。

这在经典继承中是不可能的,因为一旦创建了一个类,就不能在运行时修改它。这可能是原型继承相对于经典继承的最大优势,而且它应该是最重要的。然而,我喜欢把最好的留到最后。

结论

原型继承很重要。教育JavaScript程序员为什么要放弃原型继承的构造函数模式而选择原型继承的原型模式是很重要的。

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

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

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