所以这些年来我终于不再拖泥带水,决定“好好”学习JavaScript。语言设计中最令人头疼的元素之一是它的继承实现。有了 Ruby 的经验,我真的很高兴看到闭包和动态类型;但是对于我的一生来说,无法弄清楚使用其他实例进行继承的对象实例有什么好处。
原文由 Pierreten 发布,翻译遵循 CC BY-SA 4.0 许可协议
所以这些年来我终于不再拖泥带水,决定“好好”学习JavaScript。语言设计中最令人头疼的元素之一是它的继承实现。有了 Ruby 的经验,我真的很高兴看到闭包和动态类型;但是对于我的一生来说,无法弄清楚使用其他实例进行继承的对象实例有什么好处。
原文由 Pierreten 发布,翻译遵循 CC BY-SA 4.0 许可协议
请允许我实际回答在线问题。
原型继承具有以下优点:
但是它有以下缺点:
我想您可以从上面的字里行间中读出,并得出传统类/对象方案的相应优点和缺点。当然,每个区域都有更多,所以我会把剩下的留给其他人回答。
原文由 JUST MY correct OPINION 发布,翻译遵循 CC BY-SA 2.5 许可协议
13 回答13.1k 阅读
7 回答2.2k 阅读
3 回答1.4k 阅读✓ 已解决
6 回答1.4k 阅读✓ 已解决
2 回答1.5k 阅读✓ 已解决
3 回答1.4k 阅读✓ 已解决
6 回答1.2k 阅读
我知道这个答案晚了 3 年,但我真的认为当前的答案没有提供足够的信息 来说明原型继承如何优于经典继承。
首先让我们看看 JavaScript 程序员为捍卫原型继承而提出的最常见的论点(我从当前的答案池中提取这些论点):
现在这些论点都是有效的,但没有人费心去解释原因。这就像告诉孩子学习数学很重要。当然是,但是孩子肯定不在乎;你不能通过说数学很重要来让孩子喜欢数学。
我认为原型继承的问题在于它是从 JavaScript 的角度来解释的。我喜欢 JavaScript,但 JavaScript 中的原型继承是错误的。与经典继承不同,原型继承有两种模式:
不幸的是,JavaScript 使用原型继承的构造函数模式。这是因为在创建 JavaScript 时, Brendan Eich (JS 的创建者)希望它看起来像 Java(具有经典继承):
这很糟糕,因为当人们在 JavaScript 中使用构造函数时,他们会想到构造函数继承自其他构造函数。这是错误的。在原型继承中,对象继承自其他对象。构造函数永远不会出现。这是大多数人的困惑。
来自 Java 等具有经典继承的语言的人会更加困惑,因为尽管构造函数看起来像类,但它们的行为却不像类。正如 道格拉斯克罗克福德 所说:
你有它。直接从马嘴里说出来。
真正的原型继承
原型继承都是关于对象的。对象从其他对象继承属性。这里的所有都是它的。有两种使用原型继承创建对象的方法:
注意: JavaScript 提供了两种克隆对象的方法—— 委托 和 连接。从今以后,我将使用“克隆”一词来专门指代通过委托进行的继承,而使用“复制”一词来专门指代通过连接进行的继承。
说够了。让我们看一些例子。假设我有一个半径为
5
的圆:我们可以根据圆的半径计算出圆的面积和周长:
现在我想创建另一个半径为
10
的圆。一种方法是:不过 JavaScript 提供了一种更好的方式—— 委托。
Object.create
函数用于执行此操作:就这样。您刚刚在 JavaScript 中进行了原型继承。是不是很简单?你拿一个对象,克隆它,改变你需要的任何东西,嘿,转眼间——你给自己一个全新的对象。
现在您可能会问,“这怎么这么简单?每次我想创建一个新圆时,我都需要克隆
circle
并手动为其分配半径”。那么解决方案是使用一个函数来为您完成繁重的工作:事实上,您可以将所有这些组合成一个对象文字,如下所示:
JavaScript 中的原型继承
如果您在上面的程序中注意到
create
函数创建了circle
的克隆,分配一个新的radius
并返回给它。这正是构造函数在 JavaScript 中所做的:JavaScript 中的构造函数模式是原型模式的倒置。您创建构造函数而不是创建对象。
new
关键字将构造函数内部的this
指针绑定到构造函数的prototype
的克隆。听起来很混乱?这是因为 JavaScript 中的构造函数模式不必要地使事情复杂化。这是大多数程序员难以理解的。
他们没有考虑从其他对象继承的对象,而是考虑从其他构造函数继承的构造函数,然后变得完全混乱。
有很多其他原因可以避免 JavaScript 中的构造函数模式。您可以在我的博客文章中阅读它们: Constructors vs Prototypes
那么原型继承比经典继承有什么好处呢?让我们再次回顾最常见的论点,并解释 _原因_。
1.原型继承很简单
CMS 在他的回答中指出:
让我们考虑一下我们刚刚做了什么。我们创建了一个对象
circle
其半径为5
。然后我们克隆它并给克隆半径10
。因此,我们只需要两件事就可以使原型继承起作用:
Object.create
)。相比之下,经典继承要复杂得多。在经典继承中,你有:
你明白了。关键是原型继承更容易理解,更容易实现,也更容易推理。
正如 Steve Yegge 在他的经典博文“ Portrait of a N00b ”中所说:
在同样的意义上,类只是元数据。类不是继承所必需的。然而,有些人(通常是 n00bs)发现类更适合工作。这给了他们一种虚假的安全感。
正如我之前所说,课程会给人们一种虚假的安全感。例如,即使您的代码完全清晰易读,您在 Java 中得到了太多
NullPointerException
。我发现经典继承通常会妨碍编程,但也许这只是 Java。 Python 有一个惊人的经典继承系统。2. 原型继承很强大
大多数来自经典背景的程序员认为经典继承比原型继承更强大,因为它具有:
这种说法是错误的。我们已经知道 JavaScript 通过闭包支持私有变量,但是多重继承呢? JavaScript 中的对象只有一个原型。
事实上,原型继承支持从多个原型继承。原型继承仅仅意味着一个对象继承自另一个对象。 实现原型继承其实有两种方式:
是的 JavaScript 只允许对象委托给另一个对象。但是,它允许您复制任意数量的对象的属性。例如
_.extend
就是这样做的。当然,许多程序员并不认为这是真正的继承,因为
instanceof
和isPrototypeOf
另有说法。然而,这可以很容易地通过在每个对象上存储一个原型数组来解决,这些对象通过连接从原型继承:因此,原型继承与经典继承一样强大。事实上,它比经典继承更强大,因为在原型继承中,您可以手动选择要复制的属性以及要从不同原型中省略的属性。
在经典继承中,不可能(或者至少很难)选择要继承的属性。他们使用虚拟基类和接口来解决 钻石问题。
然而,在 JavaScript 中,您很可能永远不会听说过菱形问题,因为您可以准确控制希望继承哪些属性以及从哪些原型继承。
3.原型继承不那么冗余
这一点有点难以解释,因为经典继承并不一定会导致更多的冗余代码。事实上,无论是经典继承还是原型继承,都是用来减少代码冗余的。
一个论点可能是大多数具有经典继承的编程语言都是静态类型的,并且需要用户显式声明类型(不像 Haskell 具有隐式静态类型)。因此,这会导致更冗长的代码。
Java 因这种行为而臭名昭著。我清楚地记得 Bob Nystrom 在他关于 Pratt Parsers 的博客文章中提到了以下轶事:
同样,我认为这只是因为 Java 太烂了。
一个有效的论点是,并非所有具有经典继承的语言都支持多重继承。再次想到Java。是的,Java 有接口,但这还不够。有时你真的需要多重继承。
由于原型继承允许多重继承,如果使用原型继承而不是使用具有经典继承但没有多重继承的语言编写,则需要多重继承的代码会减少冗余。
4. 原型继承是动态的
原型继承最重要的优点之一是您可以在创建原型后向其添加新属性。这允许您向原型添加新方法,这些方法将自动提供给委托给该原型的所有对象。
这在经典继承中是不可能的,因为一旦创建了一个类,您就无法在运行时修改它。这可能是原型继承相对于经典继承的最大优势,它应该排在最前面。但是我喜欢把最好的留到最后。
结论
原型继承很重要。重要的是要教育 JavaScript 程序员为什么要放弃原型继承的构造函数模式而支持原型继承的原型模式。
我们需要开始正确地教授 JavaScript,这意味着向新程序员展示如何使用原型模式而不是构造函数模式来编写代码。
使用原型模式不仅可以更容易地解释原型继承,而且还可以培养更好的程序员。
如果您喜欢这个答案,那么您还应该阅读我的博客文章“ 为什么原型继承很重要”。相信我,你不会失望的。