kế thừa cổ điển so với kế thừa nguyên mẫu trong javascript


118

Tôi đã truy cập rất nhiều liên kết và không thể hiểu rõ về sự khác biệt giữa kế thừa cổ điển và kế thừa nguyên mẫu?

Tôi đã học được một số điều từ những điều này nhưng tôi vẫn còn nhầm lẫn về các khái niệm.

Kế thừa cổ điển

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

Kế thừa cổ điển có sử dụng kế thừa nguyên mẫu bên trong không?

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

Từ liên kết trên, tôi đã biết rằng chúng ta không thể thêm các phương thức mới tại thời điểm chạy trong kế thừa cổ điển . Điều này có chính xác? Nhưng bạn có thể kiểm tra đoạn mã trên Tôi có thể thêm phương thức "di chuyển" và bất kỳ phương pháp nào tại thời điểm chạy thông qua nguyên mẫu . Vì vậy, đây là kế thừa cổ điển dựa trên nguyên mẫu? Nếu vậy kế thừa cổ điển thực tế và kế thừa nguyên mẫu là gì? Tôi bối rối về điều đó.

Kế thừa nguyên mẫu.

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

Điều này có giống với kế thừa cổ điển không? Tôi hoàn toàn bối rối về thừa kế nguyên mẫu là gì? Kế thừa cổ điển là gì? Tại sao kế thừa cổ điển là xấu?

Bạn có thể cho tôi một ví dụ đơn giản để hiểu rõ hơn về những điều này một cách đơn giản.

Cảm ơn,

Siva


Duplicate, hãy kiểm tra này: stackoverflow.com/questions/1595611/...
Silviu Burcea

5
không chắc bạn đang nói gì ở đây - khối mã đầu tiên kế thừa nguyên mẫu, không phải cổ điển. Khối mã thứ hai của bạn không có sự kế thừa nào cả!
Alnitak

Điều này có thể giải thích: blog.stephenwyattbush.com/2012/05/01/…
HasanAboShally

@alnitak developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… liên kết này cho biết rằng một liên kết là kế thừa cổ điển. đó là lý do tại sao tôi bối rối.
SivaRajini

Để biết thêm về lý do tại sao bạn có thể muốn tránh kế thừa cổ điển, hãy xem bài nói chuyện của tôi, "Kế thừa cổ điển đã lỗi thời: Cách suy nghĩ trong nguyên mẫu OO" vimeo.com/69255635
Eric Elliott

Câu trả lời:


248

Cả hai mẫu mã mà bạn đã trình bày trong câu hỏi của mình đều sử dụng kế thừa nguyên mẫu. Trên thực tế, bất kỳ mã hướng đối tượng nào bạn viết bằng JavaScript đều là mô hình kế thừa nguyên mẫu. JavaScript đơn giản là không có tính kế thừa cổ điển. Điều này sẽ làm rõ ràng mọi thứ một chút:

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

Như bạn có thể thấy kế thừa nguyên mẫu và kế thừa cổ điển là hai mô hình kế thừa khác nhau. Một số ngôn ngữ như Self, Lua và JavaScript hỗ trợ kế thừa nguyên mẫu. Tuy nhiên hầu hết các ngôn ngữ như C ++, Java và C # đều hỗ trợ kế thừa cổ điển.


Tổng quan nhanh về lập trình hướng đối tượng

Cả kế thừa nguyên mẫu và kế thừa cổ điển đều là mô hình lập trình hướng đối tượng (tức là chúng xử lý các đối tượng). Đối tượng chỉ đơn giản là những trừu tượng đóng gói các thuộc tính của một thực thể trong thế giới thực (tức là chúng đại diện cho những thứ từ thực trong chương trình). Điều này được gọi là trừu tượng.

Tính trừu tượng: Sự thể hiện của những thứ trong thế giới thực trong các chương trình máy tính.

Về mặt lý thuyết, một sự trừu tượng được định nghĩa là "một khái niệm chung được hình thành bằng cách trích xuất các đặc điểm chung từ các ví dụ cụ thể". Tuy nhiên, vì lợi ích của lời giải thích này, chúng tôi sẽ sử dụng định nghĩa nói trên để thay thế.

Bây giờ một số đối tượng có rất nhiều điểm chung. Ví dụ như một chiếc xe đạp bùn và một chiếc Harley Davidson có rất nhiều điểm chung.

Một chiếc xe đạp bùn:

Một chiếc xe đạp bùn.

Một chiếc Harley Davidson:

Một chiếc Harley Davidson

Một chiếc xe đạp bùn và một chiếc Harley Davidson đều là xe đạp. Do đó, xe đạp là sự tổng hòa của cả xe đạp bùn và xe Harley Davidson.

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

Trong ví dụ trên, xe đạp, xe đạp bùn và Harley Davidson đều là những bản trừu tượng. Tuy nhiên, xe đạp là một sự trừu tượng chung hơn của xe đạp bùn và Harley Davidson (tức là cả xe đạp bùn và Harley Davidson đều là những loại xe đạp cụ thể).

Khái quát hóa: Một sự trừu tượng của một sự trừu tượng cụ thể hơn.

Trong lập trình hướng đối tượng, chúng tôi tạo ra các đối tượng (là những trừu tượng của các thực thể trong thế giới thực) và chúng tôi sử dụng các lớp hoặc nguyên mẫu để tạo ra các khái quát của các đối tượng này. Khái quát hóa được tạo ra thông qua kế thừa. Một chiếc xe đạp là một khái quát của một chiếc xe đạp bùn. Do đó xe đạp bùn kế thừa từ xe đạp thồ.


Lập trình hướng đối tượng cổ điển

Trong lập trình hướng đối tượng cổ điển, chúng ta có hai loại trừu tượng: lớp và đối tượng. Một đối tượng, như đã đề cập trước đây, là một sự trừu tượng của một thực thể thế giới thực. Mặt khác, một lớp là sự trừu tượng của một đối tượng hoặc một lớp khác (tức là nó là một sự tổng quát hóa). Ví dụ, hãy xem xét:

+----------------------+----------------+---------------------------------------+
| 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.              |
+----------------------+----------------+---------------------------------------+

Như bạn có thể thấy trong ngôn ngữ lập trình hướng đối tượng cổ điển, các đối tượng chỉ là trừu tượng (tức là tất cả các đối tượng có mức trừu tượng là 1) và các lớp chỉ là khái quát hóa (tức là tất cả các lớp có mức trừu tượng lớn hơn 1).

Các đối tượng trong ngôn ngữ lập trình hướng đối tượng cổ điển chỉ có thể được tạo bằng các lớp khởi tạo:

class Human {
    // ...
}

class Man extends Human {
    // ...
}

Man johnDoe = new Man();

Tóm lại trong ngôn ngữ lập trình hướng đối tượng cổ điển, các đối tượng là sự trừu tượng hóa của các thực thể trong thế giới thực và các lớp là sự tổng quát hóa (tức là sự trừu tượng hóa của một trong hai đối tượng hoặc các lớp khác).

Do đó, khi mức độ trừu tượng tăng lên, các thực thể trở nên tổng quát hơn và khi mức độ trừu tượng giảm thì các thực thể trở nên cụ thể hơn. Theo nghĩa này, mức độ trừu tượng tương tự như một thang đo từ các thực thể cụ thể hơn đến các thực thể tổng quát hơn.


Lập trình hướng đối tượng nguyên mẫu

Ngôn ngữ lập trình hướng đối tượng nguyên mẫu đơn giản hơn nhiều so với ngôn ngữ lập trình hướng đối tượng cổ điển vì trong lập trình hướng đối tượng nguyên mẫu chúng ta chỉ có một kiểu trừu tượng (tức là các đối tượng). Ví dụ, hãy xem xét:

+----------------------+----------------+---------------------------------------+
| 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.              |
+----------------------+----------------+---------------------------------------+

Như bạn có thể thấy trong các ngôn ngữ lập trình hướng đối tượng nguyên mẫu, các đối tượng là sự trừu tượng hóa của các thực thể trong thế giới thực (trong trường hợp đó chúng được gọi đơn giản là các đối tượng) hoặc các đối tượng khác (trong trường hợp đó chúng được gọi là nguyên mẫu của các đối tượng mà chúng trừu tượng hóa). Do đó, một nguyên mẫu là một sự tổng quát hóa.

Các đối tượng trong ngôn ngữ lập trình hướng đối tượng nguyên mẫu có thể được tạo ra từ ex-nihilo (tức là không có gì) hoặc từ một đối tượng khác (trở thành nguyên mẫu của đối tượng mới được tạo):

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

Theo quan điểm khiêm tốn của tôi, ngôn ngữ lập trình hướng đối tượng nguyên mẫu mạnh hơn ngôn ngữ lập trình hướng đối tượng cổ điển vì:

  1. Chỉ có một kiểu trừu tượng.
  2. Khái quát hóa đơn giản là đối tượng.

Đến đây chắc hẳn bạn đã nhận ra sự khác biệt giữa kế thừa cổ điển và kế thừa nguyên mẫu. Kế thừa cổ điển được giới hạn cho các lớp kế thừa từ các lớp khác. Tuy nhiên, kế thừa nguyên mẫu không chỉ bao gồm các nguyên mẫu kế thừa từ các nguyên mẫu khác mà còn bao gồm các đối tượng kế thừa từ nguyên mẫu.


Tính phân lập lớp nguyên mẫu

Bạn phải nhận thấy rằng các nguyên mẫu và các lớp rất giống nhau. Đúng. Họ đang. Trên thực tế, chúng giống nhau đến mức bạn thực sự có thể sử dụng các nguyên mẫu để mô hình hóa các lớp:

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

Sử dụng CLASShàm trên, bạn có thể tạo các nguyên mẫu trông giống như các lớp:

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

Tuy nhiên, điều ngược lại là không đúng (tức là bạn không thể sử dụng các lớp để lập mô hình nguyên mẫu). Điều này là do nguyên mẫu là đối tượng nhưng các lớp không phải là đối tượng. Chúng là một kiểu trừu tượng hoàn toàn khác.


Phần kết luận

Tóm lại, chúng ta đã học được rằng trừu tượng là một "khái niệm chung được hình thành bằng cách trích xuất các đặc điểm chung từ các ví dụ cụ thể" và khái quát đó là "một trừu tượng của một trừu tượng cụ thể hơn" . Chúng tôi cũng đã tìm hiểu về sự khác biệt giữa kế thừa nguyên mẫu và cổ điển và cách cả hai đều là hai mặt của cùng một đồng tiền.

Trên một lưu ý chia tay, tôi muốn lưu ý rằng có hai mẫu kế thừa nguyên mẫu: mẫu nguyên mẫu và mẫu phương thức khởi tạo. Mẫu nguyên mẫu là mẫu chính tắc của kế thừa nguyên mẫu trong khi mẫu phương thức khởi tạo được sử dụng để làm cho kế thừa nguyên mẫu trông giống kế thừa cổ điển hơn. Cá nhân tôi thích mô hình nguyên mẫu hơn.

Tái bút Tôi là người đã viết bài đăng trên blog " Tại sao các vấn đề thừa kế nguyên mẫu " và trả lời câu hỏi " Lợi ích của việc kế thừa nguyên mẫu so với cổ điển? ". Câu trả lời của tôi là câu trả lời được chấp nhận.


2
cảm ơn vì câu trả lời tuyệt vời của bạn. tôi cần hiểu mẫu nguyên mẫu tốt hơn như thế nào so với mẫu phương thức khởi tạo. ví dụ như thế nào?
SivaRajini

1
Tôi đã viết một bài phê bình so sánh về constructors vs nguyên mẫu trong blog của tôi: aaditmshah.github.io/why-prototypal-inheritance-matters/...
Aadit M Shah

Vì vậy, sẽ chính xác khi nói rằng khi chúng ta sử dụng các hàm trong javascript để đạt được kế thừa, chúng ta phần nào sử dụng mô hình kế thừa cổ điển và khi chúng ta sử dụng các đối tượng thuần túy thì nó kế thừa nguyên mẫu (cả nội bộ đều tuân theo kế thừa nguyên mẫu)?
Swanidhi

1
@Swanidhi Không. Nếu bạn đang sử dụng JavaScript thì bạn đang sử dụng mô hình kế thừa nguyên mẫu. Tuy nhiên, JavaScript có hai cách kế thừa nguyên mẫu: sử dụng các hàm (tức là mẫu phương thức khởi tạo) và sử dụng các đối tượng (tức là mẫu nguyên mẫu).
Aadit M Shah

5
@Swanidhi Không. Nó vẫn còn nguyên mẫu. JavaScript không có "lớp" và do đó hoàn toàn không có gì trong JavaScript cổ điển, kể cả các hàm tạo. Nó vẫn là kế thừa nguyên mẫu. Chỉ là một dạng thừa kế nguyên mẫu kỳ lạ mà mọi người nhầm lẫn với kế thừa cổ điển. Tóm lại, programming with classes = classical inheritance, programming with prototypes = prototypal inheritance, programming with constructors = weird form of prototypal inheritance that looks a lot like classical inheritance. Hy vọng rằng điều đó làm sáng tỏ.
Aadit M Shah

8

Trước khi chuyển sang phần kế thừa, chúng ta sẽ xem xét hai mô hình chính để tạo các thể hiện (đối tượng) trong javascript:

Mô hình cổ điển: Đối tượng được tạo từ một bản thiết kế (lớp)

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

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

Mô hình nguyên mẫu: Đối tượng được tạo trực tiếp từ đối tượng khác.

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

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

Trong cả hai trường hợp, Tính kế thừa * đạt được bằng cách liên kết các đối tượng bằng cách sử dụng đối tượng nguyên mẫu.

(* các phương thức của lớp cơ sở có thể truy cập thông qua. lớp dẫn xuất thông qua đối tượng nguyên mẫu và không bắt buộc phải có trong lớp dẫn xuất.)

Đây là một lời giải thích tốt để hiểu rõ hơn ( http://www.objectplayground.com/ )


0

Một con chó là một con vật. Suzanna là một con chó. Trong kế thừa cổ điển, Animallà một lớp, Doglà một lớp con của Animal, và suzannalà một thể hiện của a Dog.

Trong kế thừa nguyên mẫu, không có lớp. Bạn có một animal, là một đối tượng. A doglà một đối tượng khác, nó sao chép và mở rộng animal(đối tượng nguyên mẫu). suzannalà một đối tượng thứ ba, sao chép và mở rộng 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();

Nếu bạn viết Dogthay vì dog, đặc biệt nếu bạn tạo Dogmột số loại hàm "phương thức khởi tạo", thì bạn không thực hiện kế thừa nguyên mẫu; bạn đang làm (giả) kế thừa cổ điển . Thực tế là bạn đang sử dụng Object.create()để đạt được điều này không có nghĩa là bạn đang thực hiện kế thừa nguyên mẫu.

Trên thực tế, JavaScript chỉ hỗ trợ kế thừa nguyên mẫu. newToán tử và .prototypethuộc tính khó hiểu ở đó để làm cho kế thừa nguyên mẫu trông giống như kế thừa cổ điển (giả).

Douglas Crockford khám phá điều này trong cuốn sách của ông, "JavaScript: Những phần tốt đẹp".

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.