原型继承优于经典的好处?

时间:2010-05-10 07:22:05

标签: javascript oop inheritance language-design prototype-programming

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

5 个答案:

答案 0 :(得分:519)

我知道这个答案迟了3年但我真的认为目前的答案没有提供有关how prototypal inheritance is better than classical inheritance的足够信息。

首先让我们看看JavaScript程序员在保护原型继承时所说的最常见的参数(我从当前的答案池中提取这些参数):

  1. 很简单。
  2. 它很强大。
  3. 它会导致更小,更少冗余的代码。
  4. 它是动态的,因此对动态语言来说更好。
  5. 现在这些论点都是有效的,但是没有人为解释原因而烦恼。这就像告诉孩子学习数学很重要。当然可以,但孩子当然不在乎;你不能说像数学这样的孩子,说它很重要。

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

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

        

      我们把它当作Java的小兄弟推动它,因为像Visual Basic这样的补充语言当时是微软语言家族的C ++。

      这很糟糕,因为当人们在JavaScript中使用构造函数时,他们会想到从其他构造函数继承的构造函数。这是错的。在原型继承中,对象继承自其他对象。构造函数永远不会出现。这让大多数人感到困惑。

      具有经典继承的Java等语言的人们更加困惑,因为虽然构造函数看起来像类,但它们不像类那样表现。正如Douglas Crockford所述:

        

      这种间接性旨在使语言对于经过专业训练的程序员来说更为熟悉,但却未能做到这一点,正如我们从Java程序员对JavaScript的非常低级的看法中看到的那样。 JavaScript的构造函数模式并没有吸引经典人群。它还掩盖了JavaScript真正的原型性质。因此,很少有程序员知道如何有效地使用该语言。

      你有它。直接离开马的嘴。

      True Prototypal Inheritance

      原型继承完全是关于对象的。对象从其他对象继承属性。这就是它的全部内容。有两种使用原型继承创建对象的方法:

      1. 创建一个全新的对象。
      2. 克隆现有对象并对其进行扩展。
      3. 注意: JavaScript提供了两种克隆对象的方法 - delegationconcatenation。从此我将使用" clone"通过授权专门提到继承,并且#34; copy"通过连接专门引用继承。

        足够的谈话。让我们看一些例子。假设我有一个半径为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提供了一种更好的方法 - delegation。 Crockford的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中的构造函数模式。您可以在我的博客文章中阅读相关内容:Constructors vs Prototypes


        原型继承优于经典继承有什么好处?让我们再次讨论最常见的论点,并解释为什么

        1。 Prototypal Inheritance很简单

        CMS在他的回答中指出:

          

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

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

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

        1. 创建新对象的方法(例如,对象文字)。
        2. 扩展现有对象的方法(例如Object.create)。
        3. 相比之下,经典继承要复杂得多。在经典继承中,你有:

          1. 类。
          2. 对象。
          3. 接口。
          4. 抽象类。
          5. 最后的课程。
          6. 虚拟基类。
          7. 构造
          8. 析构函数。
          9. 你明白了。关键是原型继承更容易理解,更容易实现,更容易推理。

            史蒂夫·叶格(Steve Yegge)将其置于他的经典博客文章" Portrait of a N00b":

              

            元数据是任何其他类型的描述或模型。代码中的注释只是计算的自然语言描述。使元数据元数据的原因在于它并非绝对必要。如果我的狗有一些谱系文书工作,而且我丢失了文书工作,我仍然有一只完全有效的狗。

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

              

            嗯,我们也知道静态类型只是元数据。它们是针对两种读者的专门评论:程序员和编译器。静态类型讲述了有关计算的故事,可能是为了帮助两个读者组理解程序的意图。但是静态类型可以在运行时丢弃,因为最终它们只是风格化的注释。他们喜欢谱系文书工作:它可能使某种不安全的性格类型对他们的狗更开心,但狗肯定不在乎。

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

            2。原型继承是强大的

            大多数来自古典背景的程序员认为经典继承比原型继承更强大,因为它具有:

            1. 私人变量。
            2. 多重继承。
            3. 这种说法是错误的。我们已经知道JavaScript支持private variables via closures,但是多重继承呢? JavaScript中的对象只有一个原型。

              事实是原型继承支持从多个原型继承。原型继承只是意味着一个对象继承自另一个对象。实际上有two ways to implement prototypal inheritance

              1. 委派或差异继承
              2. 克隆或连锁继承
              3. 是JavaScript只允许对象委托给另一个对象。但是,它允许您复制任意数量的对象的属性。例如,_.extend就是这样做的。

                当然,许多程序员并不认为这是真正的继承,因为instanceofisPrototypeOf不这样说。但是,通过在每个通过串联继承原型的对象上存储原型数组,可以很容易地解决这个问题:

                function copyOf(object, prototype) {
                    var prototypes = object.prototypes;
                    var prototypeOf = Object.isPrototypeOf;
                    return prototypes.indexOf(prototype) >= 0 ||
                        prototypes.some(prototypeOf, prototype);
                }
                

                因此,原型继承与经典继承一样强大。事实上,它比经典继承更强大,因为在原型继承中,您可以手动选择要复制的属性以及从不同原型中省略哪些属性。

                在经典继承中,选择要继承的属性是不可能的(或者至少是非常困难的)。他们使用虚拟基类和接口来解决the diamond problem

                在JavaScript中,您很可能从未听说过钻石问题,因为您可以精确控制您希望继承的属性以及原型。

                3。原型继承不太冗余

                这一点有点难以解释,因为经典继承并不一定会导致更多的冗余代码。实际上,继承(无论是经典还是原型)用于减少代码中的冗余。

                一个论点可能是大多数具有经典继承的编程语言是静态类型的,并且需要用户显式声明类型(与具有隐式静态类型的Haskell不同)。因此,这会导致更详细的代码。

                Java因此行为而臭名昭着。我清楚地记得Bob Nystrom在他关于Pratt Parsers的博文中提到了以下轶事:

                  

                你必须喜欢Java"请一式四份签署#34;这里的官僚水平。

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

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

                由于原型继承允许多重继承,因此如果使用原型继承而不是使用具有经典继承但没有多重继承的语言编写,那么需要多继承的代码就不那么多了。

                4。原型继承是动态的

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

                这在经典继承中是不可能的,因为一旦创建了类,您就无法在运行时修改它。这可能是原型继承优于经典继承的最大优势,它应该是最重要的。但是我喜欢在最后保存最好的东西。

                结论

                原型继承很重要。教育JavaScript程序员解释为什么放弃原型继承的构造函数模式以支持原型继承的原型模式是非常重要的。

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

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

                如果您喜欢这个答案,那么您还应该阅读我的博客文章" Why Prototypal Inheritance Matters"。相信我,你不会失望。

答案 1 :(得分:35)

请允许我实际回答内联问题。

原型继承具有以下优点:

  1. 它更适合动态语言,因为继承与它所处的环境一样动态。(这里对JavaScript的适用性应该很明显。)这允许你快速做事,就像定制类没有大量的数据一样基础设施代码。
  2. 与经典的类/对象二分法相比,实现原型对象方案更容易。
  3. 它消除了对象模型周围复杂尖锐边缘的需求,如“元类”(我从未喜欢过类别......对不起!)或“特征值”等。
  4. 但它有以下缺点:

    1. 检查原型语言的类型并非不可能,但这非常非常困难。大多数原型语言的“类型检查”是纯粹的运行时“鸭子打字”式检查。这并不适合所有环境。
    2. 同样难以通过静态(或通常甚至动态!)分析来优化方法调度。 可以(我强调:可以)很容易非常低效。
    3. 类似地,对象创建在原型语言中可以(并且通常是)比在更传统的类/对象二分法中更慢。
    4. 我认为您可以在上面的行之间进行阅读,并提出传统类/对象方案的相应优缺点。当然,每个区域都有更多,所以我会将其余部分留给其他人回答。

答案 2 :(得分:26)

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

该语言的原型性质可能会使经过经典训练的人感到困惑,但事实证明这实际上是真正简单而强大的概念,{{3} }。

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

如果您认为原型,您很快就会注意到您不需要课程......

原型继承在不久的将来会更受欢迎,differential inheritance规范引入了ECMAScript 5th Edition方法,它允许你以一种非常简单的方式生成一个从另一个继承的新对象实例:

var obj = Object.create(baseInstance);

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

答案 3 :(得分:10)

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

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

答案 4 :(得分:2)

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

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

经典Vs原型继承 - Stack Overflow

Classical Vs prototypal inheritance