Kế thừa Javascript: gọi super-constructor hay sử dụng chuỗi nguyên mẫu?


82

Gần đây tôi đã đọc về cách sử dụng cuộc gọi JavaScript trong MDC

https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/call

một trong những ví dụ được hiển thị bên dưới, tôi vẫn không hiểu.

Tại sao họ sử dụng kế thừa ở đây như thế này

Prod_dept.prototype = new Product();

điều này có cần thiết không? Bởi vì có một lệnh gọi đến hàm tạo siêu trong

Prod_dept()

dù sao thì, như thế này

Product.call

Đây có phải chỉ là một hành vi phổ biến? Khi nào thì tốt hơn khi sử dụng lệnh gọi cho siêu hàm tạo hoặc sử dụng chuỗi nguyên mẫu?

function Product(name, value){
  this.name = name;
  if(value >= 1000)
    this.value = 999;
  else
    this.value = value;
}

function Prod_dept(name, value, dept){
  this.dept = dept;
  Product.call(this, name, value);
}

Prod_dept.prototype = new Product();

// since 5 is less than 1000, value is set
cheese = new Prod_dept("feta", 5, "food");

// since 5000 is above 1000, value will be 999
car = new Prod_dept("honda", 5000, "auto");

Cảm ơn vì đã làm cho mọi thứ rõ ràng hơn


Cách bạn sử dụng nó gần như đúng nhưng bạn có thể muốn sử dụng Object.create () thay vì khởi tạo cơ sở bằng cách sử dụng từ khóa mới (có thể gây ra sự cố nếu hàm tạo cơ sở cần đối số). Tôi có thông tin chi tiết trên blog của mình: ncombo.wordpress.com/2013/07/11/…
Jon

2
Cũng lưu ý rằng Sản phẩm () được gọi hiệu quả hai lần.
event_jr

Câu trả lời:


109

Câu trả lời cho câu hỏi thực sự là bạn cần phải làm cả hai:

  • Việc đặt nguyên mẫu thành một thể hiện gốc sẽ khởi tạo chuỗi nguyên mẫu (kế thừa), điều này chỉ được thực hiện một lần (vì đối tượng nguyên mẫu được chia sẻ).
  • Việc gọi hàm tạo của cha sẽ khởi tạo chính đối tượng, điều này được thực hiện với mọi khởi tạo (bạn có thể truyền các tham số khác nhau mỗi khi bạn xây dựng nó).

Do đó, bạn không nên gọi hàm tạo của cha khi thiết lập kế thừa. Chỉ khi khởi tạo một đối tượng kế thừa từ một đối tượng khác.

Câu trả lời của Chris Morgan gần như đầy đủ, thiếu một chi tiết nhỏ (thuộc tính của constructor). Hãy để tôi đề xuất một phương pháp để thiết lập kế thừa.

function extend(base, sub) {
  // Avoid instantiating the base class just to setup inheritance
  // Also, do a recursive merge of two prototypes, so we don't overwrite 
  // the existing prototype, but still maintain the inheritance chain
  // Thanks to @ccnokes
  var origProto = sub.prototype;
  sub.prototype = Object.create(base.prototype);
  for (var key in origProto)  {
     sub.prototype[key] = origProto[key];
  }
  // The constructor property was set wrong, let's fix it
  Object.defineProperty(sub.prototype, 'constructor', { 
    enumerable: false, 
    value: sub 
  });
}

// Let's try this
function Animal(name) {
  this.name = name;
}

Animal.prototype = {
  sayMyName: function() {
    console.log(this.getWordsToSay() + " " + this.name);
  },
  getWordsToSay: function() {
    // Abstract
  }
}

function Dog(name) {
  // Call the parent's constructor
  Animal.call(this, name);
}

Dog.prototype = {
    getWordsToSay: function(){
      return "Ruff Ruff";
    }
}    

// Setup the prototype chain the right way
extend(Animal, Dog);

// Here is where the Dog (and Animal) constructors are called
var dog = new Dog("Lassie");
dog.sayMyName(); // Outputs Ruff Ruff Lassie
console.log(dog instanceof Animal); // true
console.log(dog.constructor); // Dog

Xem bài đăng trên blog của tôi để biết thêm về cú pháp khi tạo lớp. http://js-bits.blogspot.com/2010/08/javascript-inheritance-done-right.html

Kỹ thuật được sao chép từ Ext-JS và http://www.uselesspickles.com/class_library/ và nhận xét từ https://stackoverflow.com/users/1397311/ccnokes


6
Trong EcmaScript5 + (tất cả các trình duyệt hiện đại), bạn có thể làm cho nó không thể liệt kê được nếu bạn định nghĩa nó như thế này Object.defineProperty(sub.protoype, 'constructor', { enumerable: false, value: sub }); Bằng cách này, bạn sẽ nhận được chính xác "hành vi" giống như khi javascript tạo một phiên bản mới của một hàm (hàm tạo được đặt là có thể liệt kê = false tự động)
Adaptabi

2
Bạn không thể đơn giản hóa phương pháp mở rộng thành hai dòng? Cụ thể: sub.prototype = Object.create (base.prototype); sub.prototype.constructor = sub;
Appetere

@Steve Có, bạn có thể, khi tôi viết lần đầu tiên, Object.createkhông được hỗ trợ tốt ... cập nhật nó. Lưu ý rằng hầu hết các Object.createpolyfills được thực hiện bằng kỹ thuật mà tôi đã chỉ ra ban đầu.
Juan Mendes

1
Vì vậy, nếu tôi muốn thêm các phương thức vào chỉ đối tượng con và các thể hiện của nó, đối tượng "Dog" trong trường hợp này, bạn có chỉ cần hợp nhất hai nguyên mẫu trong hàm mở rộng của mình như sau: jsfiddle.net/ccnokes/75f9P không?
ccnokes

1
@ elad.chen Cách tiếp cận mà tôi đã mô tả trong chuỗi câu trả lời là nguyên mẫu, các mixin thường sao chép tất cả các thuộc tính vào một thể hiện, không phải nguyên mẫu. Xem stackoverflow.com/questions/7506210/…
Juan Mendes,

30

Cách lý tưởng để làm điều đó là không làm Prod_dept.prototype = new Product();, bởi vì điều này gọi hàm Producttạo. Vì vậy, cách lý tưởng là sao chép nó ngoại trừ hàm tạo, giống như sau:

function Product(...) {
    ...
}
var tmp = function(){};
tmp.prototype = Product.prototype;

function Prod_dept(...) {
    Product.call(this, ...);
}
Prod_dept.prototype = new tmp();
Prod_dept.prototype.constructor = Prod_dept;

Sau đó, super constructor được gọi tại thời điểm xây dựng, đó là những gì bạn muốn, vì sau đó bạn cũng có thể truyền các tham số.

Nếu bạn nhìn vào những thứ như Thư viện đóng cửa của Google, bạn sẽ thấy đó là cách họ thực hiện.


Tôi gọi hàm tạo đó được sử dụng để thiết lập kế thừa là hàm tạo thay thế. Ví dụ của bạn vẫn quên để thiết lập lại tài sản constructor sau khi thiết lập thừa kế, do đó bạn đúng cách có thể phát hiện các nhà xây dựng
Juan Mendes

1
@Juan: OK, đã cập nhật để thêm Prod_dept.prototype.constructor = Prod_dept;.
Chris Morgan

@ChrisMorgan Tôi đang gặp rắc rối hiểu dòng cuối cùng trong mẫu của bạn: Prod_dept.prototype.constructor = Prod_dept;. Trước hết, tại sao nó lại cần và tại sao nó trỏ tới Prod_deptthay vì Product?
Lasse Christiansen

1
@ LasseChristiansen-sw_lasse: Prod_dept.prototypelà thứ sẽ được sử dụng làm nguyên mẫu đầu ra của new Prod_dept(). (Thông thường, nguyên mẫu đó có sẵn instance.__proto__, mặc dù đây là chi tiết triển khai.) Về lý do constructor— nó là một phần tiêu chuẩn của ngôn ngữ và vì vậy nên được cung cấp để có tính nhất quán; nó đúng theo mặc định, nhưng vì chúng tôi đang thay thế nguyên mẫu hoàn toàn, chúng tôi phải gán lại giá trị phù hợp, nếu không một số thứ sẽ không ổn (trong trường hợp này, điều đó có nghĩa là một Prod_deptphiên bản sẽ có this.constructor == Product, điều đó thật tệ).
Chris Morgan

6

Nếu bạn đã thực hiện Lập trình hướng đối tượng trong JavaScript, bạn sẽ biết rằng bạn có thể tạo một lớp như sau:

Person = function(id, name, age){
    this.id = id;
    this.name = name;
    this.age = age;
    alert('A new person has been accepted');
}

Cho đến nay class person của chúng ta chỉ có hai thuộc tính và chúng ta sẽ cung cấp cho nó một số phương thức. Một cách rõ ràng để làm điều này là sử dụng đối tượng 'nguyên mẫu' của nó. Bắt đầu từ JavaScript 1.1, đối tượng nguyên mẫu đã được giới thiệu trong JavaScript. Đây là một đối tượng được xây dựng sẵn giúp đơn giản hóa quá trình thêm các thuộc tính và phương thức tùy chỉnh vào tất cả các trường hợp của một đối tượng. Hãy thêm 2 phương thức vào lớp của chúng ta bằng cách sử dụng đối tượng 'nguyên mẫu' của nó như sau:

Person.prototype = {
    /** wake person up */
    wake_up: function() {
        alert('I am awake');
    },

    /** retrieve person's age */
    get_age: function() {
        return this.age;
    }
}

Bây giờ chúng ta đã định nghĩa lớp Person của chúng ta. Điều gì sẽ xảy ra nếu chúng ta muốn định nghĩa một lớp khác được gọi là Manager kế thừa một số thuộc tính từ Person. Không có ích gì khi xác định lại tất cả các thuộc tính này khi chúng ta định nghĩa lớp Manager của mình, chúng ta chỉ có thể đặt nó kế thừa từ lớp Person. JavaScript không được tích hợp sẵn tính kế thừa nhưng chúng ta có thể sử dụng một kỹ thuật để triển khai kế thừa như sau:

Inheritance_Manager = {};// Chúng tôi tạo một lớp quản lý kế thừa (tên là tùy ý)

Bây giờ chúng ta hãy cung cấp cho lớp kế thừa của chúng ta một phương thức có tên là extension lấy các đối số baseClass và subClassas. Trong phương thức mở rộng, chúng ta sẽ tạo một lớp bên trong được gọi là kế thừa hàm kế thừa () {}. Lý do tại sao chúng tôi sử dụng lớp bên trong này là để tránh nhầm lẫn giữa các nguyên mẫu baseClass và subClass. Tiếp theo, chúng ta làm cho nguyên mẫu của lớp kế thừa của chúng ta trỏ đến nguyên mẫu baseClass như với đoạn mã sau: inherit.prototype = baseClass. nguyên mẫu; Sau đó, chúng ta sao chép nguyên mẫu kế thừa vào nguyên mẫu subClass như sau: subClass.prototype = new inherit (); Điều tiếp theo là chỉ định phương thức khởi tạo cho SubClass của chúng ta như sau: subClass.prototype.constructor = subClass; Sau khi hoàn thành việc tạo mẫu lớp con, chúng ta có thể chỉ định hai dòng mã tiếp theo để đặt một số con trỏ lớp cơ sở.

subClass.baseConstructor = baseClass;
subClass.superClass = baseClass.prototype;

Đây là mã đầy đủ cho chức năng mở rộng của chúng tôi:

Inheritance_Manager.extend = function(subClass, baseClass) {
    function inheritance() { }
    inheritance.prototype = baseClass.prototype;
    subClass.prototype = new inheritance();
    subClass.prototype.constructor = subClass;
    subClass.baseConstructor = baseClass;
    subClass.superClass = baseClass.prototype;
}

Bây giờ chúng ta đã thực hiện kế thừa của mình, chúng ta có thể bắt đầu sử dụng nó để mở rộng các lớp của mình. Trong trường hợp này, chúng tôi sẽ mở rộng lớp Người của chúng ta thành lớp Người quản lý như sau:

Chúng tôi xác định lớp Người quản lý

Manager = function(id, name, age, salary) {
    Person.baseConstructor.call(this, id, name, age);
    this.salary = salary;
    alert('A manager has been registered.');
}

chúng tôi làm cho nó hình thức kế thừa Person

Inheritance_Manager.extend(Manager, Person);

Nếu bạn nhận thấy, chúng tôi vừa gọi phương thức mở rộng của lớp Inheritance_Manager và chuyển Trình quản lý lớp con trong trường hợp của chúng tôi và sau đó là Người thuộc lớp cơ sở. Lưu ý rằng thứ tự rất quan trọng ở đây. Nếu bạn hoán đổi chúng, tài sản thừa kế sẽ không hoạt động như bạn dự định. Cũng lưu ý rằng bạn sẽ cần chỉ định sự kế thừa này trước khi bạn thực sự có thể xác định Lớp con của chúng ta. Bây giờ chúng ta hãy xác định SubClass của chúng ta:

Chúng ta có thể thêm nhiều phương pháp như bên dưới. Lớp Manager của chúng ta sẽ luôn có các phương thức và thuộc tính được định nghĩa trong lớp Person vì nó kế thừa từ nó.

Manager.prototype.lead = function(){
   alert('I am a good leader');
}

Bây giờ để kiểm tra nó, chúng ta hãy tạo hai đối tượng, một từ lớp Person và một từ người quản lý lớp kế thừa:

var p = new Person(1, 'Joe Tester', 26);
var pm = new Manager(1, 'Joe Tester', 26, '20.000');

Vui lòng nhận toàn bộ mã và thêm nhận xét tại: http://www.cyberminds.co.uk/blog/articles/how-to-implement-javascript-inheritance.aspx

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.