Ví dụ hay về sự kế thừa dựa trên nguyên mẫu của JavaScript


89

Tôi đã lập trình với các ngôn ngữ OOP hơn 10 năm nhưng tôi đang học JavaScript và đây là lần đầu tiên tôi gặp phải sự kế thừa dựa trên nguyên mẫu. Tôi có xu hướng học nhanh nhất bằng cách nghiên cứu mã tốt. Ví dụ được viết tốt về một ứng dụng JavaScript (hoặc thư viện) sử dụng đúng cách kế thừa nguyên mẫu là gì? Và bạn có thể mô tả (ngắn gọn) cách / nơi kế thừa nguyên mẫu được sử dụng, để tôi biết bắt đầu đọc từ đâu?


1
Bạn có cơ hội kiểm tra thư viện Cơ sở đó không? Nó thực sự là tốt đẹp, và khá nhỏ. Nếu bạn thích nó, hãy xem xét đánh dấu câu trả lời của tôi là câu trả lời. TIA, roland.
Roland Bouman,

Tôi đoán tôi đang ở cùng thuyền với bạn. Tôi cũng muốn tìm hiểu một chút về ngôn ngữ nguyên mẫu này, không bị giới hạn chỉ trong các khuôn khổ phổ biến hoặc tương tự, thậm chí chúng tuyệt vời và tất cả, chúng ta cần phải học, phải không? Không chỉ một số khuôn khổ làm điều đó cho tôi, ngay cả khi tôi định sử dụng nó. Nhưng hãy học cách tạo ra những thứ mới bằng ngôn ngữ mới với những cách thức mới, hãy nghĩ xa hơn. Tôi thích phong cách của bạn. Tôi sẽ cố gắng giúp tôi và có lẽ sẽ giúp bạn. Ngay sau khi tôi tìm thấy thứ gì đó, tôi sẽ cho bạn biết.
marcelo-ferraz

Câu trả lời:


48

Douglas Crockford có một trang hay về Kế thừa Nguyên mẫu JavaScript :

Năm năm trước, tôi đã viết Kế thừa cổ điển bằng JavaScript. Nó chỉ ra rằng JavaScript là một ngôn ngữ nguyên mẫu, không có lớp học và nó có đủ sức mạnh biểu đạt để mô phỏng một hệ thống cổ điển. Phong cách lập trình của tôi đã phát triển kể từ đó, như bất kỳ lập trình viên giỏi nào cũng vậy. Tôi đã học cách tiếp nhận hoàn toàn chủ nghĩa nguyên mẫu, và đã giải phóng bản thân khỏi những giới hạn của mô hình cổ điển.

Các tác phẩm của Dean Edward's Base.js , Mootools's Class hoặc John Resig's Simple Inheritance là những cách để thực hiện kế thừa cổ điển trong JavaScript.


Tại sao không chỉ đơn giản newObj = Object.create(oldObj);nếu bạn muốn nó không có lớp học? Nếu không, thay thế bằng oldObjđối tượng nguyên mẫu của hàm tạo có hoạt động không?
Cyker

76

Như đã đề cập, các bộ phim của Douglas Crockford đưa ra lời giải thích tốt về lý do tại sao và nó bao gồm cách thức. Nhưng để đặt nó trong một vài dòng JavaScript:

// Declaring our Animal object
var Animal = function () {

    this.name = 'unknown';

    this.getName = function () {
        return this.name;
    }

    return this;
};

// Declaring our Dog object
var Dog = function () {

    // A private variable here        
    var private = 42;

    // overriding the name
    this.name = "Bello";

    // Implementing ".bark()"
    this.bark = function () {
        return 'MEOW';
    }  

    return this;
};


// Dog extends animal
Dog.prototype = new Animal();

// -- Done declaring --

// Creating an instance of Dog.
var dog = new Dog();

// Proving our case
console.log(
    "Is dog an instance of Dog? ", dog instanceof Dog, "\n",
    "Is dog an instance of Animal? ", dog instanceof Animal, "\n",
    dog.bark() +"\n", // Should be: "MEOW"
    dog.getName() +"\n", // Should be: "Bello"
    dog.private +"\n" // Should be: 'undefined'
);

Tuy nhiên, vấn đề với cách tiếp cận này là nó sẽ tạo lại đối tượng mỗi khi bạn tạo một đối tượng. Một cách tiếp cận khác là khai báo các đối tượng của bạn trên ngăn xếp nguyên mẫu, như sau:

// Defining test one, prototypal
var testOne = function () {};
testOne.prototype = (function () {
    var me = {}, privateVariable = 42;
    me.someMethod = function () {
        return privateVariable;
    };

    me.publicVariable = "foo bar";
    me.anotherMethod = function () {
        return this.publicVariable;
    };

    return me;

}());


// Defining test two, function
var testTwo = function() {
    var me = {}, privateVariable = 42;
    me.someMethod = function () {
        return privateVariable;
    };

    me.publicVariable = "foo bar";
    me.anotherMethod = function () {
        return this.publicVariable;
    };

    return me;
};


// Proving that both techniques are functionally identical
var resultTestOne = new testOne(),
    resultTestTwo = new testTwo();

console.log(
    resultTestOne.someMethod(), // Should print 42
    resultTestOne.publicVariable // Should print "foo bar"
);

console.log(
    resultTestTwo.someMethod(), // Should print 42
    resultTestTwo.publicVariable // Should print "foo bar"
);



// Performance benchmark start
var stop, start, loopCount = 1000000;

// Running testOne
start = (new Date()).getTime(); 
for (var i = loopCount; i>0; i--) {
    new testOne();
}
stop = (new Date()).getTime();

console.log('Test one took: '+ Math.round(((stop/1000) - (start/1000))*1000) +' milliseconds');



// Running testTwo
start = (new Date()).getTime(); 
for (var i = loopCount; i>0; i--) {
    new testTwo();
}
stop = (new Date()).getTime();

console.log('Test two took: '+ Math.round(((stop/1000) - (start/1000))*1000) +' milliseconds');

Có một nhược điểm nhỏ khi nói đến nội tâm. Bán phá giá testOne, sẽ dẫn đến ít thông tin hữu ích hơn. Ngoài ra, tài sản riêng "privateVariable" trong "testOne" được chia sẻ trong mọi trường hợp, als được đề cập hữu ích trong các câu trả lời của shesek.


3
Lưu ý rằng trong testOne privateVariablechỉ đơn giản là một biến trong phạm vi của IIFE và nó được chia sẻ trên tất cả các trường hợp, vì vậy bạn không nên lưu trữ dữ liệu dành riêng cho trường hợp trên đó. (trên testTwo nó là ví dụ cụ thể, như mỗi cuộc gọi đến testTwo () tạo ra một mới, mỗi sơ thẩm, phạm vi)
shesek

Tôi ủng hộ vì bạn đã chỉ ra cách tiếp cận khác và tại sao không sử dụng nó vì nó tạo ra các bản sao
Murphy316

Vấn đề tạo lại đối tượng mỗi lần chủ yếu là do các phương pháp được tạo lại cho mỗi đối tượng mới. Tuy nhiên, chúng ta có thể giảm thiểu vấn đề bằng cách xác định phương pháp trên Dog.prototype. Vì vậy, thay vì sử dụng this.bark = function () {...}, chúng ta có thể làm Dot.prototype.bark = function () {...}bên ngoài Dogchức năng. (Xem thêm chi tiết trong câu trả lời này )
Huang Chao

26
function Shape(x, y) {
    this.x = x;
    this.y = y;
}

// 1. Explicitly call base (Shape) constructor from subclass (Circle) constructor passing this as the explicit receiver
function Circle(x, y, r) {
    Shape.call(this, x, y);
    this.r = r;
}

// 2. Use Object.create to construct the subclass prototype object to avoid calling the base constructor
Circle.prototype = Object.create(Shape.prototype);

3
Có lẽ việc thêm liên kết này với câu trả lời của bạn có thể hoàn thiện bức tranh hơn nữa: developer.mozilla.org/en/docs/Web/JavaScript/Reference/…
Dynom

14

Tôi sẽ xem qua YUI và tại Basethư viện của Dean Edward : http://dean.edwards.name/weblog/2006/03/base/

Đối với YUI, bạn có thể xem nhanh mô-đun lang , đặc biệt. các YAHOO.lang.extend phương pháp. Và sau đó, bạn có thể duyệt qua nguồn của một số vật dụng hoặc tiện ích và xem cách họ sử dụng phương pháp đó.


YUI 2 đã không được chấp nhận kể từ năm 2011, do đó, liên kết tới langbị đứt đoạn. Có ai muốn sửa nó cho YUI 3 không?
ack

lang trong yui 3 dường như không có phương thức mở rộng. nhưng vì câu trả lời có ý định sử dụng việc triển khai làm ví dụ nên phiên bản không quan trọng.
eMBee


4

Đây là ví dụ rõ ràng nhất mà tôi tìm thấy, từ cuốn sách Node của Mixu ( http://book.mixu.net/node/ch6.html ):

Tôi ủng hộ thành phần hơn là thừa kế:

Thành phần - Chức năng của một đối tượng được tạo thành từ tập hợp các lớp khác nhau bằng cách chứa các thể hiện của các đối tượng khác. Tính kế thừa - Chức năng của một đối tượng được tạo thành từ chức năng riêng của nó cộng với chức năng từ các lớp cha của nó. Nếu bạn phải có quyền thừa kế, hãy sử dụng JS cũ thuần túy

Nếu bạn phải triển khai kế thừa, ít nhất hãy tránh sử dụng thêm một hàm thực thi / ma thuật không chuẩn khác. Dưới đây là cách bạn có thể triển khai bản sao kế thừa hợp lý trong ES3 thuần túy (miễn là bạn tuân theo quy tắc không bao giờ xác định thuộc tính trên nguyên mẫu):

function Animal(name) {
  this.name = name;
};
Animal.prototype.move = function(meters) {
  console.log(this.name+" moved "+meters+"m.");
};

function Snake() {
  Animal.apply(this, Array.prototype.slice.call(arguments));
};
Snake.prototype = new Animal();
Snake.prototype.move = function() {
  console.log("Slithering...");
  Animal.prototype.move.call(this, 5);
};

var sam = new Snake("Sammy the Python");
sam.move();

Đây không phải là điều giống với kế thừa cổ điển - nhưng nó là Javascript chuẩn, dễ hiểu và có chức năng mà mọi người chủ yếu tìm kiếm: các hàm tạo có thể thay thế và khả năng gọi các phương thức của lớp cha.


4

ES6 classextends

ES6 classextends chỉ là đường cú pháp cho thao tác chuỗi nguyên mẫu có thể có trước đây, và do đó được cho là thiết lập chuẩn nhất.

Trước tiên, hãy tìm hiểu thêm về chuỗi nguyên mẫu và .tra cứu thuộc tính tại: https://stackoverflow.com/a/23877420/895245

Bây giờ hãy giải mã những gì sẽ xảy ra:

class C {
    constructor(i) {
        this.i = i
    }
    inc() {
        return this.i + 1
    }
}

class D extends C {
    constructor(i) {
        super(i)
    }
    inc2() {
        return this.i + 2
    }
}
// Inheritance syntax works as expected.
(new C(1)).inc() === 2
(new D(1)).inc() === 2
(new D(1)).inc2() === 3
// "Classes" are just function objects.
C.constructor === Function
C.__proto__ === Function.prototype
D.constructor === Function
// D is a function "indirectly" through the chain.
D.__proto__ === C
D.__proto__.__proto__ === Function.prototype
// "extends" sets up the prototype chain so that base class
// lookups will work as expected
var d = new D(1)
d.__proto__ === D.prototype
D.prototype.__proto__ === C.prototype
// This is what `d.inc` actually does.
d.__proto__.__proto__.inc === C.prototype.inc
// Class variables
// No ES6 syntax sugar apparently:
// /programming/22528967/es6-class-variable-alternatives
C.c = 1
C.c === 1
// Because `D.__proto__ === C`.
D.c === 1
// Nothing makes this work.
d.c === undefined

Sơ đồ đơn giản hóa không có tất cả các đối tượng được xác định trước:

      __proto__
(C)<---------------(D)         (d)
| |                |           |
| |                |           |
| |prototype       |prototype  |__proto__
| |                |           |
| |                |           |
| |                | +---------+
| |                | |
| |                | |
| |                v v
|__proto__        (D.prototype)
| |                |
| |                |
| |                |__proto__
| |                |
| |                |
| | +--------------+
| | |
| | |
| v v
| (C.prototype)--->(inc)
|
v
Function.prototype


1

Những ví dụ tốt nhất mà tôi đã thấy là trong JavaScript của Douglas Crockford : The Good Parts . Nó chắc chắn đáng mua để giúp bạn có cái nhìn cân bằng về ngôn ngữ.

Douglas Crockford chịu trách nhiệm về định dạng JSON và làm việc tại Yahoo với tư cách là một chuyên gia về JavaScript.


7
chịu trách nhiệm? mà âm thanh gần giống như "tội" :)
Roland Bouman

@Roland Tôi nghĩ JSON là một định dạng không dài dòng khá đẹp để lưu trữ dữ liệu. Ông chắc chắn đã không phát minh ra nó, mặc dù định dạng là có cho các thiết lập cấu hình trong hơi trở lại vào năm 2002
Chris S

Chris S, tôi cũng nghĩ vậy - Ngày càng thường xuyên hơn, tôi ước chúng ta có thể bỏ qua định dạng trao đổi XML và chuyển sang JSON ngay lập tức.
Roland Bouman

3
Không có nhiều phát minh: JSON là một tập hợp con của đối tượng cú pháp JavaScript của riêng theo nghĩa đen, mà đã được trong ngôn ngữ kể từ khoảng năm 1997.
Tim Xuống

@Time điểm tốt - Tôi đã không nhận ra nó đã ở đó kể từ khi bắt đầu
Chris S

0

Có một đoạn mã JavaScript dựa trên Nguyên mẫu với các triển khai cụ thể của phiên bản ECMAScript. Nó sẽ tự động chọn cái nào để sử dụng giữa các triển khai ES6, ES5 và ES3 theo thời gian chạy hiện tại.


0

Thêm một ví dụ về kế thừa dựa trên Nguyên mẫu trong Javascript.

// Animal Class
function Animal (name, energy) {
  this.name = name;
  this.energy = energy;
}

Animal.prototype.eat = function (amount) {
  console.log(this.name, "eating. Energy level: ", this.energy);
  this.energy += amount;
  console.log(this.name, "completed eating. Energy level: ", this.energy);
}

Animal.prototype.sleep = function (length) {
  console.log(this.name, "sleeping. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "completed sleeping. Energy level: ", this.energy);
}

Animal.prototype.play = function (length) {
  console.log(this.name, " playing. Energy level: ", this.energy);
  this.energy -= length;
  console.log(this.name, "completed playing. Energy level: ", this.energy);
}

// Dog Class
function Dog (name, energy, breed) {
  Animal.call(this, name, energy);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function () {
  console.log(this.name, "barking. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "done barking. Energy level: ", this.energy);
}

Dog.prototype.showBreed = function () {
  console.log(this.name,"'s breed is ", this.breed);
}

// Cat Class
function Cat (name, energy, male) {
  Animal.call(this, name, energy);
  this.male = male;
}

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

Cat.prototype.meow = function () {
  console.log(this.name, "meowing. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "done meowing. Energy level: ", this.energy);
}

Cat.prototype.showGender = function () {
  if (this.male) {
    console.log(this.name, "is male.");
  } else {
    console.log(this.name, "is female.");
  }
}

// Instances
const charlie = new Dog("Charlie", 10, "Labrador");
charlie.bark();
charlie.showBreed();

const penny = new Cat("Penny", 8, false);
penny.meow();
penny.showGender();

ES6 sử dụng việc triển khai kế thừa dễ dàng hơn nhiều khi sử dụng hàm tạo và từ khóa siêu.

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.