一尘不染

原型继承相对于经典的好处?

javascript

因此,这些年来,我终于停止拖延脚步,决定“适当”学习JavaScript。语言设计中最令人头疼的元素之一是继承的实现。拥有Ruby的经验,我很高兴看到闭包和动态类型。但是对于我一生来说,无法弄清楚使用其他实例进行继承的对象实例将带来什么好处。


阅读 370

收藏
2020-05-01

共1个答案

一尘不染

首先,让我们看一下JavaScript程序员在捍卫原型继承中所陈述的最常见的参数(我从当前的答案池中获取这些参数):

  1. 这很简单。
  2. 功能强大。
  3. 它导致更小的,更少的冗余代码。
  4. 它是动态的,因此对于动态语言来说更好。

现在这些论点都是有效的,但是没有人愿意去解释原因。这就像告诉孩子学习数学很重要。当然可以,但是孩子当然不在乎;并不能通过说这很重要来使孩子喜欢数学。

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

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

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

我们将它作为Java的弟弟推销,因为像Visual Basic这样的补充语言当时是Microsoft语言家族中的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中进行了原型继承。那不是那么简单吗?您拿了一个对象,对其进行克隆,更改所需的内容,然后嘿,您便拥有了一个全新的对象。

现在您可能会问:“这有多简单?每次我要创建一个新圆时,都需要克隆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中的构造函数模式不必要地使事情复杂化。这是大多数程序员难以理解的。

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


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

1.原型继承很简单

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

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

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

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

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

  1. Classes.
  2. Object.
  3. Interfaces.
  4. Abstract Classes.
  5. Final Classes.
  6. Virtual Base Classes.
  7. Constructors.
  8. Destructors.

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

元数据是其他任何形式的描述或模型。您代码中的注释只是对计算的自然语言描述。使元数据成为元数据的原因在于它不是绝对必要的。如果我的狗有一些血统书,而我却丢失了文件,那么我仍然有一只非常有效的狗。

从同样的意义上讲,类仅仅是元数据。继承不是严格要求的类。但是,有些人(通常为n00bs)发现使用这些类更舒适。这给了他们一种错误的安全感。

好吧,我们也知道静态类型只是元数据。它们是针对两种读者的一种特殊的注释:程序员和编译器。静态类型讲述了一个有关计算的故事,大概是为了帮助两个读者群体了解程序的意图。但是静态类型可以在运行时扔掉,因为最后它们只是风格化的注释。他们就像家谱的文书工作:这可能会使某种不安全的性格类型更快乐地对待他们的狗,但狗当然不在乎。

如前所述,类给人一种错误的安全感。例如NullPointerException,即使您的代码清晰易读,您在Java中也会收到太多。我发现经典继承通常会妨碍编程,但也许那只是Java。Python有一个了不起的经典继承系统。

2.原型继承是强大的

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

  1. 私有变量。
  2. 多重继承。

该说法是错误的。我们已经知道JavaScript通过闭包”JavaScript中的私有成员”)支持私有变量”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的官僚机构的“请一式四份签名”级别。

再说一次,我认为那只是因为Java很烂。

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

由于原型继承允许多重继承,因此,如果使用原型继承而不是具有经典继承但没有多重继承的语言编写,则需要多重继承的代码将减少冗余。

4.原型继承是动态的

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

在经典继承中这是不可能的,因为一旦创建了一个类,您将无法在运行时对其进行修改。与经典继承相比,这可能是原型继承的最大优点,应该放在首位。但是,我喜欢为最终目的尽力而为。

结论

原型继承很重要。对于JavaScript程序员,为什么要放弃原型继承的构造函数模式而转而使用原型继承的原型模式,这一点很重要。

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

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

2020-05-01