JavaScript中的经典继承与原型继承

146

我已经谷歌了很多链接,但无法理解经典继承和原型继承之间的区别。

我从这些中学到了一些东西,但我仍然对这些概念感到困惑。

经典继承

// Shape - superclass
function Shape() {
  this.x = 0;
  this.y = 0;
}

//superclass method
Shape.prototype.move = function(x, y) {
    this.x += x;
    this.y += y;
    console.info("Shape moved.");
};

// Rectangle - subclass
function Rectangle() {
  Shape.call(this); //call super constructor.
}

//subclass extends superclass
Rectangle.prototype = Object.create(Shape.prototype);

经典继承是否在内部使用原型继承?

http://aaditmshah.github.io/why-prototypal-inheritance-matters/

从上面的链接中,我了解到在经典继承中无法在运行时添加新方法。这个说法正确吗?但是您可以检查上面的代码我可以通过原型在运行时添加“移动”方法和任何方法。所以这是基于原型的经典继承吗?如果是这样,那么实际的经典继承和原型继承是什么呢?我对此感到困惑。

原型继承。

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);

这与经典继承相似吗?我完全不明白什么是原型继承?什么是经典继承?为什么经典继承不好?

您能给我举一个简单的例子以更好地理解这些概念吗?

谢谢,

Siva


重复的,请看这里:https://dev59.com/9nI-5IYBdhLWcg3w9tdw#1598077 - Silviu Burcea
5
不确定你在说什么 - 第一个代码块确实是原型继承,而不是经典继承。你的第二个代码块根本没有继承! - Alnitak
这篇文章解释了原型继承和经典继承之间的区别:http://blog.stephenwyattbush.com/2012/05/01/comparing-prototypal-and-classical-inheritance - HasanAboShally
@alnitak 这个链接告诉我们关于经典继承的内容,这就是为什么我感到困惑。 - SivaRajini
2
想了解更多关于为什么要避免经典继承的原因,请参阅我的演讲,“经典继承已过时:如何思考原型面向对象”http://vimeo.com/69255635 - Eric Elliott
4个回答

307

你在问题中展示的两个代码示例都使用了原型继承。实际上,你在JavaScript中编写的任何面向对象的代码都是原型继承范式。JavaScript只是没有经典继承。这应该会让事情变得更清晰:

                                   Inheritance
                                        |
                         +-----------------------------+
                         |                             |
                         v                             v
                    Prototypal                     Classical
                         |
         +------------------------------+
         |                              |
         v                              v
Prototypal Pattern             Constructor Pattern

如您所见,原型继承和经典继承是两种不同的继承范式。一些语言,如Self、Lua和JavaScript支持原型继承。然而大多数语言,如C++、Java和C#支持经典继承。


面向对象编程简介

原型继承和经典继承都是面向对象编程范例(即它们处理对象)。对象只是封装实体属性的抽象(即它们在程序中表示真实世界事物)。这就是所谓的“抽象”。

抽象:在计算机程序中表示真实世界事物。

理论上,抽象被定义为“从具体示例中提取公共特征形成的一般概念”。但出于解释目的,我们将使用前述定义。

现在,一些对象具有许多共同点。例如,泥地自行车和哈雷戴维森有许多共同之处。

一个泥地自行车:

A mud bike.

哈雷戴维森:

A Harley Davidson

泥地自行车和哈雷戴维森都是自行车。因此,自行车是泥地自行车和哈雷戴维森的概括。

                   Bike
                     |
    +---------------------------------+
    |                                 |
    v                                 v
Mud Bike                       Harley Davidson

在上面的例子中,自行车、越野摩托车和哈雷戴维森都是抽象的。然而,自行车是越野摩托车和哈雷戴维森的更一般的抽象(即,越野摩托车和哈雷戴维森都是自行车的特定类型)。

泛化:更具体的抽象。

在面向对象编程中,我们创建对象(它们是真实世界实体的抽象),并使用类或原型来创建这些对象的泛化。泛化是通过继承创建的。自行车是越野摩托车的泛化。因此,越野摩托车从自行车继承。


经典的面向对象编程

在经典的面向对象编程中,我们有两种类型的抽象:类和对象。如前所述,对象是真实世界实体的抽象。而类则是一个对象或另一个类的抽象(即,它是一种泛化)。例如,请考虑:

+----------------------+----------------+---------------------------------------+
| Level of Abstraction | Name of Entity |                Comments               |
+----------------------+----------------+---------------------------------------+
| 0                    | John Doe       | Real World Entity.                    |
| 1                    | johnDoe        | Variable holding object.              |
| 2                    | Man            | Class of object johnDoe.              |
| 3                    | Human          | Superclass of class Man.              |
+----------------------+----------------+---------------------------------------+

如您所见,在传统面向对象编程语言中,对象只是抽象概念(即所有对象的抽象级别都为1),而类只是一般化概念(即所有类的抽象级别大于1)。

在传统面向对象编程语言中,只能通过实例化类来创建对象:

class Human {
    // ...
}

class Man extends Human {
    // ...
}

Man johnDoe = new Man();

在传统的面向对象编程语言中,对象是真实世界实体的抽象,而类是一般化的(即对象或其他类的抽象)。

因此,随着抽象层次的增加,实体变得更为一般化,抽象层次的降低会使实体更具体。从这个意义上说,抽象级别类似于一种从更具体实体到更一般实体的标度。


原型面向对象编程

原型面向对象编程语言比传统的面向对象编程语言简单得多,因为在原型面向对象编程中我们只有一种类型的抽象(即对象)。例如,请考虑:

+----------------------+----------------+---------------------------------------+
| Level of Abstraction | Name of Entity |                Comments               |
+----------------------+----------------+---------------------------------------+
| 0                    | John Doe       | Real World Entity.                    |
| 1                    | johnDoe        | Variable holding object.              |
| 2                    | man            | Prototype of object johnDoe.          |
| 3                    | human          | Prototype of object man.              |
+----------------------+----------------+---------------------------------------+

正如您在原型对象导向编程语言中所看到的,对象是真实世界实体或其他对象(在这种情况下它们被称为那些抽象对象的原型)的抽象描述。因此,原型是一种概括。

原型对象导向编程语言中的对象可以通过两种方式创建:从零开始创建(即从头开始创建),或者从另一个对象创建(该对象成为新创建的对象的原型):

var human = {};
var man = Object.create(human);
var johnDoe = Object.create(man);

在我看来,基于原型的面向对象编程语言比基于经典的面向对象编程语言更强大,因为:

  1. 只有一种类型的抽象。
  2. 概括仅是对象。

你现在应该意识到了经典继承和原型继承之间的区别。经典继承仅限于类从其他类继承。然而,原型继承不仅包括原型从其他原型继承,还包括对象从原型继承。


原型-类同构

你一定注意到原型和类非常相似。实际上它们很相似,以至于你可以使用原型来建模类:

function CLASS(base, body) {
    if (arguments.length < 2) body = base, base = Object.prototype;
    var prototype = Object.create(base, {new: {value: create}});
    return body.call(prototype, base), prototype;

    function create() {
        var self = Object.create(prototype);
        return prototype.hasOwnProperty("constructor") &&
            prototype.constructor.apply(self, arguments), self;
    }
}

通过以上的 CLASS 函数,您可以创建类似类的原型:

var Human = CLASS(function () {
    var milliseconds = 1
      , seconds      = 1000 * milliseconds
      , minutes      = 60 * seconds
      , hours        = 60 * minutes
      , days         = 24 * hours
      , years        = 365.2425 * days;

    this.constructor = function (name, sex, dob) {
        this.name = name;
        this.sex = sex;
        this.dob = dob;
    };

    this.age = function () {
        return Math.floor((new Date - this.dob) / years);
    };
});

var Man = CLASS(Human, function (Human) {
    this.constructor = function (name, dob) {
        Human.constructor.call(this, name, "male", dob);
        if (this.age() < 18) throw new Error(name + " is a boy, not a man!");
    };
});

var johnDoe = Man.new("John Doe", new Date(1970, 0, 1));

然而相反的情况并非如此(即您不能使用类来建模原型)。这是因为原型是对象,但类不是对象。它们是完全不同类型的抽象。


结论

总之,我们了解到抽象是“从具体示例中提取共同特征形成的一般概念”,泛化是“更具体的抽象”。我们还了解了原型继承和经典继承之间的差异,以及它们如何是同一个硬币的两个面。

最后我想说的是,原型继承有两种模式:原型模式和构造函数模式。原型模式是原型继承的范本模式,而构造函数模式用于使原型继承看起来更像经典继承。个人更喜欢原型模式。


2
谢谢您的精彩回答。我需要了解原型模式相对于构造函数模式的优势。有任何例子吗? - SivaRajini
1
我在我的博客上写了一篇关于构造函数和原型的比较评论:http://aaditmshah.github.io/why-prototypal-inheritance-matters/#constructors_vs_prototypes - Aadit M Shah
那么,我们是否可以这样说,在JavaScript中使用函数来实现继承时,我们有点使用了经典的继承模型,而当我们使用普通对象时,它遵循原型继承(在内部都是遵循原型继承)? - Swanidhi
1
@Swanidhi 不是的。如果你在使用JavaScript,那么你正在使用原型继承模型。然而,JavaScript有两种原型继承的方式:使用函数(即构造函数模式)和使用对象(即原型模式)。 - Aadit M Shah
6
不,它仍然是原型式的。JavaScript没有“类”,因此JavaScript中没有任何经典的东西,包括构造函数。它仍然是原型式继承。只是一种奇怪的原型式继承形式,让人们将其与经典继承混淆。简而言之,“使用类编程=经典继承”,“使用原型编程=原型继承”,“使用构造函数编程=一种看起来很像经典继承的奇怪的原型继承形式”。希望这澄清了问题。 - Aadit M Shah
显示剩余2条评论

16

在深入了解继承之前,我们先来看一下javascript中创建实例(对象)的两个主要模型:

经典模型:从蓝图(类)创建对象。

class Person {
  fn() {...}
} // or constructor function say, function Person() {}

// create instance
let person = new Person();

原型模型:对象直接从另一个对象创建。

// base object
let Person = { fn(){...} }

// instance
let person = Object.create(Person);

无论哪种情况,继承*都是通过使用原型对象链接对象来实现的。
(*基类方法可以通过原型对象在派生类中访问,并且不需要在派生类中显式存在。)
这里有一个很好的解释,以便更好地理解(http://www.objectplayground.com/)

10

狗是一种动物。Suzanna是一只狗。在经典继承中,Animal是一个类,DogAnimal的子类,suzannaDog的实例。

在原型继承中,没有类的概念。您有一个animal对象。一只dog是另一个对象,它克隆并扩展了animal(原型对象)。suzanna是第三个对象,它复制并扩展了dog

let animal = {hasChlorophyl: false};

let dog = Object.create(animal);
Object.assign(dog, {
  speak() {
    console.log("Woof!");
  }
});

let suzanna = Object.create(dog);
Object.assign(suzanna, {
  name: "Suzanna"
});

suzanna.speak();

如果你将Dog写成大写字母形式,特别是如果你将Dog作为某种"构造函数",那么你并没有在进行原型继承,而是在进行(伪)类继承。使用Object.create()实现这一点并不意味着你正在进行原型继承。
事实上,JavaScript仅支持原型继承。混淆的new运算符和.prototype属性存在的目的是为了让原型继承看起来像(伪)类继承。
道格拉斯·克罗克福德在他的书《JavaScript语言精粹》中对此进行了详细探讨。

-1

类与原型符号之间的区别示例:

类继承:

class Person {
    constructor(name) {
        this.name = name;
        this.say = function (text) {
            return `${this.name} said: ${text}`;
        };
    }

    greet() {
        return `Hello, my name is ${this.name}!`;
    }

    static random() {
        return Math.random();
    }
}

class Student extends Person {
    constructor(name) {
        super(name);
    }
}

原型继承:

function Person(name) {
    // own properties
    this.name = name;
    this.say = function (text) {
        return `${this.name} said: ${text}`;
    };
}
// extend Person prototype with method:
Person.prototype.greet = function () {
    return `Hello, my name is ${this.name}!`;
};
// add static method:
Person.random = function () {
    return Math.random();
};

function Student(name) {
    // Call the parent constructor
    Person.call(this, name);
}
// Student as a subclass of Person:
Object.setPrototypeOf(Student.prototype, Person.prototype);
Object.setPrototypeOf(Student, Person); // add static properties

// replace the parent method
Student.prototype.greet = function () {
    return `Hi, I'm ${this.name} and I am a student`;
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接