所以这些年来我终于不再拖泥带水,决定“正确地”学习 JavaScript。语言设计中最令人头疼的元素之一是它的继承实现。有 Ruby 方面的经验,我很高兴看到闭包和动态类型;但是对于我的生活来说,我可以弄清楚使用其他实例进行继承的对象实例有什么好处。
我知道这个答案晚了 3 年,但我真的认为当前的答案没有提供足够的信息来说明原型继承如何优于经典继承。
首先让我们看看 JavaScript 程序员为保护原型继承而陈述的最常见的论点(我从当前的答案池中获取这些论点):
现在这些论点都是有效的,但没有人费心解释原因。这就像告诉孩子学习数学很重要。当然是,但孩子肯定不在乎;你不能说数学很重要就让孩子喜欢数学。
我认为原型继承的问题在于它是从 JavaScript 的角度来解释的。我喜欢 JavaScript,但 JavaScript 中的原型继承是错误的。与经典继承不同,原型继承有两种模式:
不幸的是,JavaScript 使用原型继承的构造函数模式。这是因为在创建 JavaScript 时,Brendan Eich(JS 的创建者)希望它看起来像 Java(具有经典继承):
我们把它作为 Java 的小兄弟来推动它,因为像 Visual Basic 这样的互补语言是当时微软语言家族中的 C++ 的补充。
这很糟糕,因为当人们在 JavaScript 中使用构造函数时,他们认为构造函数继承自其他构造函数。这是错误的。在原型继承中,对象继承自其他对象。构造函数永远不会出现。这是让大多数人感到困惑的地方。
来自像 Java 这样具有经典继承的语言的人会更加困惑,因为尽管构造函数看起来像类,但它们的行为并不像类。正如道格拉斯·克罗克福德所说:
这种间接性旨在使受过经典训练的程序员看起来更熟悉该语言,但未能做到这一点,正如我们从 Java 程序员对 JavaScript 的极低评价中可以看出的那样。JavaScript 的构造函数模式并没有吸引经典人群。它还掩盖了 JavaScript 真正的原型性质。结果,很少有程序员知道如何有效地使用该语言。
你有它。直接从马嘴里。
原型继承是关于对象的。对象从其他对象继承属性。这里的所有都是它的。使用原型继承创建对象有两种方法:
注意: JavaScript 提供了两种克隆对象的方法——委托和连接。以后我将使用“克隆”这个词专门指代通过委托进行的继承,而“复制”这个词专门指代通过串联的继承。
说够了。让我们看一些例子。假设我有一个半径圆5:
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。一种方法是:
10
var circle2 = { radius: 10, area: circle.area, circumference: circle.circumference };
然而 JavaScript 提供了一种更好的方式——委托。该Object.create函数用于执行此操作:
Object.create
var circle2 = Object.create(circle); circle2.radius = 10;
就这样。您刚刚在 JavaScript 中进行了原型继承。那不是很简单吗?你拿一个对象,克隆它,改变你需要的任何东西,嘿,你得到了一个全新的对象。
现在您可能会问,“这怎么简单?每次我想创建一个新圆时,我都需要克隆circle并手动为其分配一个半径”。那么解决方案是使用一个函数为您完成繁重的工作:
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);
如果您在上面的程序中注意到该create函数创建 的克隆circle,为其分配一个新radius的,然后返回它。这正是构造函数在 JavaScript 中所做的:
create
radius
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。
new
this
prototype
听起来很混乱?这是因为 JavaScript 中的构造函数模式不必要地使事情复杂化。这是大多数程序员难以理解的。
他们没有想到从其他对象继承的对象,而是想到了从其他构造函数继承的构造函数,然后变得完全混乱。
应该避免 JavaScript 中的构造函数模式还有很多其他原因。您可以在我的博客文章中阅读它们:构造函数与原型
那么原型继承相对于经典继承有什么好处呢?让我们再次回顾一下最常见的论点,并解释 原因 。
CMS “用户 CMS - 代码日志”)在他的回答中说:
在我看来,原型继承的主要好处是它的简单性。
让我们考虑一下我们刚刚做了什么。我们创建了circle一个半径为5. 然后我们克隆它并给克隆一个半径10。
因此,我们只需要两件事来使原型继承工作:
相比之下,经典继承要复杂得多。在经典继承中,您有:
你明白了。关键是原型继承更容易理解,更容易实现,也更容易推理。
正如 Steve Yegge 在他的经典博文“ N00b 的肖像”中所说:
元数据是对其他事物的任何类型的描述或模型。代码中的注释只是计算的自然语言描述。使元数据成为元数据的原因在于它不是绝对必要的。如果我的狗有一些血统文件,但我丢失了文件,我仍然有一只完全有效的狗。
在同样的意义上,类只是元数据。继承并不严格要求类。然而,有些人(通常是 n00bs)觉得上课更舒服。这给了他们一种虚假的安全感。
好吧,我们也知道静态类型只是元数据。它们是一种专门针对两种读者的评论:程序员和编译器。静态类型讲述了一个关于计算的故事,大概是为了帮助两个读者群体理解程序的意图。但是静态类型可以在运行时被丢弃,因为最终它们只是程式化的注释。它们就像家谱文件:它可能会让某种不安全的性格类型更喜欢他们的狗,但狗肯定不在乎。
正如我之前所说,课程给人一种虚假的安全感。NullPointerException例如,即使您的代码完全清晰易读,Java 中的 s也会太多。我发现经典继承通常会妨碍编程,但也许这只是 Java。Python 有一个惊人的经典继承系统。
NullPointerException
大多数来自经典背景的程序员认为,经典继承比原型继承更强大,因为它具有:
这种说法是错误的。我们已经知道 JavaScript通过闭包支持私有变量,但是多重继承呢?JavaScript 中的对象只有一个原型。
事实上,原型继承支持从多个原型继承。原型继承仅仅意味着一个对象从另一个对象继承。实际上有两种实现原型继承的方法:
是的,JavaScript 只允许对象委托给另一个对象。但是,它允许您复制任意数量的对象的属性。例如_.extend就是这样做的。
_.extend
当然,许多程序员不认为这是真正的继承,因为instanceof并isPrototypeOf另有说法。然而,这可以通过在每个通过连接继承自原型的对象上存储一个原型数组来轻松解决:
instanceof
isPrototypeOf
function copyOf(object, prototype) { var prototypes = object.prototypes; var prototypeOf = Object.isPrototypeOf; return prototypes.indexOf(prototype) >= 0 || prototypes.some(prototypeOf, prototype); }
因此,原型继承与经典继承一样强大。事实上,它比经典继承更强大,因为在原型继承中,您可以从不同的原型中手动选择要复制的属性以及要省略的属性。
在经典继承中,不可能(或至少非常困难)选择要继承的属性。他们使用虚拟基类和接口来解决菱形问题。
然而,在 JavaScript 中,您很可能永远不会听说过菱形问题,因为您可以准确控制希望继承哪些属性以及从哪些原型继承。
这一点有点难以解释,因为经典继承并不一定会导致更多的冗余代码。事实上,继承,无论是经典的还是原型的,都用于减少代码中的冗余。
一个论点可能是大多数具有经典继承的编程语言都是静态类型的,并且需要用户显式声明类型(与具有隐式静态类型的 Haskell 不同)。因此,这会导致更冗长的代码。
Java 因这种行为而臭名昭著。我清楚地记得Bob Nystrom在他关于Pratt Parsers的博客文章中提到了以下轶事:
您一定会喜欢 Java 的“请一式四份签名”的官僚作风。
再说一次,我认为那只是因为 Java 太烂了。
一个有效的论点是并非所有具有经典继承的语言都支持多重继承。再次想到 Java。是的,Java 有接口,但这还不够。有时你真的需要多重继承。
由于原型继承允许多重继承,因此如果使用原型继承而不是使用具有经典继承但没有多重继承的语言编写需要多重继承的代码,则冗余较少。
原型继承最重要的优点之一是您可以在创建原型后向原型添加新属性。这允许您向原型添加新方法,这些方法将自动提供给委托给该原型的所有对象。
这在经典继承中是不可能的,因为一旦创建了一个类,您就不能在运行时修改它。这可能是原型继承相对于经典继承的最大优势,它应该是最重要的。但是我喜欢把最好的留到最后。
原型继承很重要。教育 JavaScript 程序员为什么要放弃原型继承的构造函数模式转而支持原型继承的原型模式是很重要的。
我们需要开始正确地教授 JavaScript,这意味着向新程序员展示如何使用原型模式而不是构造函数模式编写代码。
使用原型模式不仅更容易解释原型继承,而且还可以培养出更好的程序员。
如果您喜欢这个答案,那么您还应该阅读我关于“为什么原型继承很重要”的博文。相信我,你不会失望的。