Nhiều kế thừa / nguyên mẫu trong JavaScript


132

Tôi đã đến một điểm mà tôi cần phải có một số loại thừa kế thô sơ xảy ra trong JavaScript. (Tôi không ở đây để thảo luận liệu đây có phải là một ý tưởng tốt hay không, vì vậy xin vui lòng giữ những bình luận đó cho riêng bạn.)

Tôi chỉ muốn biết liệu có ai đã cố gắng làm điều này với bất kỳ (hoặc không) thành công nào không, và họ đã thực hiện nó như thế nào.

Để làm cho nó sôi sục, điều tôi thực sự cần là có thể có một đối tượng có khả năng thừa kế một tài sản từ nhiều hơn một chuỗi nguyên mẫu (tức là mỗi nguyên mẫu có thể có chuỗi phù hợp riêng), nhưng theo thứ tự ưu tiên nhất định (nó sẽ được ưu tiên tìm kiếm các chuỗi để định nghĩa đầu tiên).

Để chứng minh làm thế nào điều này có thể về mặt lý thuyết, có thể đạt được bằng cách gắn chuỗi thứ cấp vào cuối chuỗi chính, nhưng điều này sẽ ảnh hưởng đến tất cả các trường hợp của bất kỳ nguyên mẫu nào trước đó và đó không phải là điều tôi muốn.

Suy nghĩ?


1
Tôi nghĩ võ đường tuyên bố xử lý nhiều thừa kế src Tôi cũng có cảm giác mootools cũng vậy, phần lớn điều này nằm ngoài tôi nhưng tôi sẽ đọc nhanh về điều này như dojo gợi ý
TI

Hãy xem TraitsJS ( liên kết 1 , liên kết 2 ) đây là một sự thay thế thực sự tốt cho nhiều kế thừa và mixins ...
CMS

1
@Pointy vì điều đó không năng động lắm. Tôi muốn có thể nhận các thay đổi được thực hiện cho chuỗi cha mẹ khi chúng xảy ra. Tuy nhiên, điều đó nói rằng, tôi có thể phải dùng đến điều này nếu điều đó là không thể.
devios1


1
Một chi tiết thú vị về điều này: webreflection.blogspot.co.uk/2009/06/ Kẻ
Nobita

Câu trả lời:


49

Nhiều kế thừa có thể đạt được trong ECMAScript 6 bằng cách sử dụng các đối tượng Proxy .

Thực hiện

function getDesc (obj, prop) {
  var desc = Object.getOwnPropertyDescriptor(obj, prop);
  return desc || (obj=Object.getPrototypeOf(obj) ? getDesc(obj, prop) : void 0);
}
function multiInherit (...protos) {
  return Object.create(new Proxy(Object.create(null), {
    has: (target, prop) => protos.some(obj => prop in obj),
    get (target, prop, receiver) {
      var obj = protos.find(obj => prop in obj);
      return obj ? Reflect.get(obj, prop, receiver) : void 0;
    },
    set (target, prop, value, receiver) {
      var obj = protos.find(obj => prop in obj);
      return Reflect.set(obj || Object.create(null), prop, value, receiver);
    },
    *enumerate (target) { yield* this.ownKeys(target); },
    ownKeys(target) {
      var hash = Object.create(null);
      for(var obj of protos) for(var p in obj) if(!hash[p]) hash[p] = true;
      return Object.getOwnPropertyNames(hash);
    },
    getOwnPropertyDescriptor(target, prop) {
      var obj = protos.find(obj => prop in obj);
      var desc = obj ? getDesc(obj, prop) : void 0;
      if(desc) desc.configurable = true;
      return desc;
    },
    preventExtensions: (target) => false,
    defineProperty: (target, prop, desc) => false,
  }));
}

Giải trình

Một đối tượng proxy bao gồm một đối tượng đích và một số bẫy, xác định hành vi tùy chỉnh cho các hoạt động cơ bản.

Khi tạo một đối tượng kế thừa từ một đối tượng khác, chúng tôi sử dụng Object.create(obj). Nhưng trong trường hợp này, chúng tôi muốn nhiều kế thừa, vì vậy thay vìobj tôi sử dụng proxy sẽ chuyển hướng các hoạt động cơ bản đến đối tượng thích hợp.

Tôi sử dụng các bẫy này:

  • Cái hasbẫy là một cái bẫy cho người invận hành . Tôi sử dụng someđể kiểm tra nếu ít nhất một nguyên mẫu có chứa tài sản.
  • Cái getbẫy là một cái bẫy để có được giá trị tài sản. Tôi sử dụng findđể tìm nguyên mẫu đầu tiên có chứa thuộc tính đó và tôi trả về giá trị hoặc gọi hàm getter trên máy thu thích hợp. Điều này được xử lý bởi Reflect.get. Nếu không có nguyên mẫu chứa tài sản, tôi trở lại undefined.
  • Cái setbẫy là một cái bẫy để thiết lập các giá trị tài sản. Tôi sử dụng findđể tìm nguyên mẫu đầu tiên chứa thuộc tính đó và tôi gọi setter của nó trên máy thu thích hợp. Nếu không có setter hoặc không có nguyên mẫu nào chứa thuộc tính, giá trị được xác định trên máy thu thích hợp. Điều này được xử lý bởi Reflect.set.
  • Cái enumeratebẫy là một cái bẫy cho for...incác vòng lặp . Tôi lặp lại các thuộc tính vô số từ nguyên mẫu đầu tiên, sau đó từ thứ hai, v.v. Khi một thuộc tính đã được lặp đi lặp lại, tôi lưu trữ nó trong bảng băm để tránh lặp lại nó.
    Cảnh báo : Bẫy này đã bị xóa trong bản nháp ES7 và không dùng nữa trong các trình duyệt.
  • Cái ownKeysbẫy là một cái bẫy cho Object.getOwnPropertyNames(). Kể từ ES7,for...in các vòng lặp tiếp tục gọi [[GetPrototypeOf]] và nhận các thuộc tính riêng của từng cái. Vì vậy, để làm cho nó lặp lại các thuộc tính của tất cả các nguyên mẫu, tôi sử dụng cái bẫy này để làm cho tất cả các thuộc tính được thừa kế xuất hiện giống như các thuộc tính riêng.
  • Cái getOwnPropertyDescriptorbẫy là một cái bẫy cho Object.getOwnPropertyDescriptor(). Làm cho tất cả các thuộc tính vô số xuất hiện như các thuộc tính riêng trong ownKeysbẫy là không đủ, for...incác vòng lặp sẽ lấy bộ mô tả để kiểm tra xem chúng có đếm được không. Vì vậy, tôi sử dụng findđể tìm nguyên mẫu đầu tiên chứa thuộc tính đó và tôi lặp lại chuỗi nguyên mẫu của nó cho đến khi tôi tìm thấy chủ sở hữu tài sản và tôi trả lại mô tả của nó. Nếu không có nguyên mẫu chứa tài sản, tôi trở lại undefined. Bộ mô tả được sửa đổi để làm cho nó có thể cấu hình được, nếu không chúng ta có thể phá vỡ một số bất biến proxy.
  • Các bẫy preventExtensionsdefinePropertychỉ được bao gồm để ngăn các hoạt động này sửa đổi mục tiêu proxy. Nếu không, chúng ta có thể phá vỡ một số bất biến proxy.

Có nhiều bẫy hơn mà tôi không sử dụng

  • Cái getPrototypeOfbẫy có thể được thêm vào, nhưng không có cách nào thích hợp để trả về nhiều nguyên mẫu. Điều này ngụ ý instanceofsẽ không làm việc. Do đó, tôi để nó lấy nguyên mẫu của mục tiêu, ban đầu là null.
  • Cái setPrototypeOfbẫy có thể được thêm vào và chấp nhận một loạt các đối tượng, sẽ thay thế các nguyên mẫu. Điều này được để lại như một bài tập cho người đọc. Ở đây tôi chỉ để nó sửa đổi nguyên mẫu của mục tiêu, điều này không hữu ích lắm vì không có bẫy nào sử dụng mục tiêu.
  • Cái deletePropertybẫy là một cái bẫy để xóa các thuộc tính riêng. Proxy đại diện cho sự kế thừa, vì vậy điều này sẽ không có nhiều ý nghĩa. Tôi để nó cố gắng xóa mục tiêu, dù sao cũng không có tài sản.
  • Các isExtensible bẫy là một cái bẫy để có được khả năng mở rộng. Không có nhiều hữu ích, cho rằng một bất biến buộc nó phải trả lại khả năng mở rộng giống như mục tiêu. Vì vậy, tôi chỉ để nó chuyển hướng hoạt động đến mục tiêu, sẽ được mở rộng.
  • các applyconstruct bẫy bẫy là các bẫy để gọi hoặc khởi tạo. Chúng chỉ hữu ích khi mục tiêu là hàm hoặc hàm tạo.

Thí dụ

// Creating objects
var o1, o2, o3,
    obj = multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3});

// Checking property existences
'a' in obj; // true   (inherited from o1)
'b' in obj; // true   (inherited from o2)
'c' in obj; // false  (not found)

// Setting properties
obj.c = 3;

// Reading properties
obj.a; // 1           (inherited from o1)
obj.b; // 2           (inherited from o2)
obj.c; // 3           (own property)
obj.d; // undefined   (not found)

// The inheritance is "live"
obj.a; // 1           (inherited from o1)
delete o1.a;
obj.a; // 3           (inherited from o3)

// Property enumeration
for(var p in obj) p; // "c", "b", "a"

1
Không có một số vấn đề hiệu suất sẽ trở nên có liên quan ngay cả trên các ứng dụng quy mô thông thường?
Tomáš Zato - Phục hồi Monica

1
@ TomášZato Nó sẽ chậm hơn các thuộc tính dữ liệu trong một đối tượng bình thường, nhưng tôi không nghĩ nó sẽ tệ hơn nhiều so với các thuộc tính của trình truy cập.
Oriol

TIL:multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3})
bloodyKnuckles

4
Tôi sẽ xem xét thay thế "Nhiều kế thừa" bằng "Nhiều đoàn" để hiểu rõ hơn về những gì đang diễn ra. Khái niệm chính trong việc triển khai của bạn là proxy thực sự đang chọn đúng đối tượng để ủy quyền (hoặc chuyển tiếp) tin nhắn. Sức mạnh của giải pháp của bạn là bạn có thể mở rộng nguyên mẫu / s mục tiêu một cách linh hoạt. Các câu trả lời khác đang sử dụng phép nối (ala Object.assign) hoặc nhận một biểu đồ khá khác nhau, cuối cùng tất cả chúng đều nhận được chuỗi nguyên mẫu duy nhất giữa các đối tượng. Giải pháp proxy cung cấp một nhánh thời gian chạy và đá này!
sminutoli

Về hiệu suất, nếu bạn tạo một đối tượng kế thừa từ nhiều đối tượng, kế thừa từ nhiều đối tượng, v.v., thì nó sẽ trở thành số mũ. Vì vậy, có, nó sẽ chậm hơn. Nhưng trong những trường hợp bình thường tôi không nghĩ nó sẽ tệ đến thế.
Oriol

16

Cập nhật (2019): Bài viết gốc đang trở nên khá lỗi thời. Bài viết này (bây giờ là liên kết lưu trữ internet, vì tên miền đã biến mất) và thư viện GitHub liên quan của nó là một cách tiếp cận hiện đại tốt.

Bài gốc: Nhiều kế thừa [chỉnh sửa, không kế thừa đúng loại, nhưng thuộc tính; mixins] trong Javascript khá đơn giản nếu bạn sử dụng các nguyên mẫu được xây dựng thay vì các mẫu đối tượng chung. Đây là hai lớp cha mẹ để kế thừa từ:

function FoodPrototype() {
    this.eat = function () {
        console.log("Eating", this.name);
    };
}
function Food(name) {
    this.name = name;
}
Food.prototype = new FoodPrototype();


function PlantPrototype() {
    this.grow = function () {
        console.log("Growing", this.name);
    };
}
function Plant(name) {
    this.name = name;
}
Plant.prototype = new PlantPrototype();

Lưu ý rằng tôi đã sử dụng cùng một thành viên "tên" trong mỗi trường hợp, đây có thể là một vấn đề nếu cha mẹ không đồng ý về cách xử lý "tên". Nhưng chúng tương thích (dự phòng, thực sự) trong trường hợp này.

Bây giờ chúng ta chỉ cần một lớp kế thừa từ cả hai. Kế thừa được thực hiện bằng cách gọi hàm xây dựng (không sử dụng từ khóa mới) cho các nguyên mẫu và các hàm tạo đối tượng. Đầu tiên, nguyên mẫu phải kế thừa từ các nguyên mẫu mẹ

function FoodPlantPrototype() {
    FoodPrototype.call(this);
    PlantPrototype.call(this);
    // plus a function of its own
    this.harvest = function () {
        console.log("harvest at", this.maturity);
    };
}

Và hàm tạo phải kế thừa từ các hàm tạo mẹ:

function FoodPlant(name, maturity) {
    Food.call(this, name);
    Plant.call(this, name);
    // plus a property of its own
    this.maturity = maturity;
}

FoodPlant.prototype = new FoodPlantPrototype();

Bây giờ bạn có thể trồng, ăn và thu hoạch các trường hợp khác nhau:

var fp1 = new FoodPlant('Radish', 28);
var fp2 = new FoodPlant('Corn', 90);

fp1.grow();
fp2.grow();
fp1.harvest();
fp1.eat();
fp2.harvest();
fp2.eat();

Bạn có thể làm điều này với các nguyên mẫu được xây dựng? (Mảng, Chuỗi, Số)
Tomáš Zato - Tái lập Monica

Tôi không nghĩ các nguyên mẫu tích hợp có các hàm tạo mà bạn có thể gọi.
Roy J

Chà, tôi có thể làm Array.call(...)nhưng dường như nó không ảnh hưởng đến bất cứ điều gì tôi vượt qua this.
Tomáš Zato - Phục hồi Monica

@ TomášZato Bạn có thể làmArray.prototype.constructor.call()
Roy J

1
@AbhishekGupta Cảm ơn đã cho tôi biết. Tôi đã thay thế liên kết bằng một liên kết đến trang web lưu trữ.
Roy J

7

Cái này dùng Object.createđể tạo ra một chuỗi nguyên mẫu thực sự:

function makeChain(chains) {
  var c = Object.prototype;

  while(chains.length) {
    c = Object.create(c);
    $.extend(c, chains.pop()); // some function that does mixin
  }

  return c;
}

Ví dụ:

var obj = makeChain([{a:1}, {a: 2, b: 3}, {c: 4}]);

sẽ trở lại:

a: 1
  a: 2
  b: 3
    c: 4
      <Object.prototype stuff>

do đó obj.a === 1, obj.b === 3vv


Chỉ là một câu hỏi giả thuyết nhanh: Tôi muốn tạo lớp Vector bằng cách trộn các nguyên mẫu Number và Array (cho vui). Điều này sẽ cung cấp cho tôi cả chỉ mục mảng và toán tử toán học. Nhưng nó sẽ làm việc?
Tomáš Zato - Phục hồi Monica

@ TomášZato, đáng để kiểm tra bài viết này nếu bạn đang xem xét các mảng phân lớp; nó có thể giúp bạn đỡ đau đầu chúc may mắn!
dùng3276552

5

Tôi thích triển khai cấu trúc lớp của John Resig: http://ejohn.org/blog/simple-javascript-inherribution/

Điều này có thể được mở rộng đơn giản thành một cái gì đó như:

Class.extend = function(prop /*, prop, prop, prop */) {
    for( var i=1, l=arguments.length; i<l; i++ ){
        prop = $.extend( prop, arguments[i] );
    }

    // same code
}

sẽ cho phép bạn vượt qua trong nhiều đối tượng để kế thừa. Bạn sẽ mất instanceOfkhả năng ở đây, nhưng đó là điều được đưa ra nếu bạn muốn có nhiều quyền thừa kế.


ví dụ khá phức tạp của tôi về những điều trên có sẵn tại https://github.com/cwolves/Fetch/blob/master/support/plugins/klass/klass.js

Lưu ý rằng có một số mã chết trong tệp đó, nhưng nó cho phép nhiều kế thừa nếu bạn muốn xem qua.


Nếu bạn muốn kế thừa chuỗi (KHÔNG phải thừa kế nhiều lần, nhưng đối với hầu hết mọi người, đó là điều tương tự), có thể được thực hiện với Class như:

var newClass = Class.extend( cls1 ).extend( cls2 ).extend( cls3 )

sẽ bảo tồn chuỗi nguyên mẫu ban đầu, nhưng bạn cũng sẽ có rất nhiều mã vô nghĩa đang chạy.


7
Điều đó tạo ra một bản sao nông hợp nhất. Thêm một thuộc tính mới vào các đối tượng "được kế thừa" sẽ không làm cho thuộc tính mới xuất hiện trên đối tượng dẫn xuất, giống như trong kế thừa nguyên mẫu thực sự.
Daniel Earwicker

@DanielEarwicker - Đúng, nhưng nếu bạn muốn "nhiều kế thừa" trong đó một lớp xuất phát từ hai lớp, thì thực sự không có sự thay thế nào. Câu trả lời được sửa đổi để phản ánh rằng chỉ đơn giản là xâu chuỗi các lớp với nhau là điều tương tự trong hầu hết các trường hợp.
Mark Kahn

Có vẻ như GitHUb của bạn đã biến mất, bạn vẫn còn github.com/cwolves/Fetch/blob/master/support/plugins/klass/ nam Tôi không ngại nhìn vào nó nếu bạn quan tâm chia sẻ?
JasonDavis

4

Đừng nhầm lẫn với việc triển khai khung JavaScript của nhiều kế thừa.

Tất cả những gì bạn cần làm là sử dụng Object.create () để tạo một đối tượng mới mỗi lần với các đối tượng và thuộc tính nguyên mẫu được chỉ định, sau đó hãy chắc chắn thay đổi Object.prototype.constructor mỗi bước trong quá trình nếu bạn dự định khởi tạo Btrong Tương lai.

Để kế thừa các thuộc tính cá thể thisAthisBchúng tôi sử dụng Function.prototype.call () ở cuối mỗi hàm đối tượng. Đây là tùy chọn nếu bạn chỉ quan tâm đến việc kế thừa nguyên mẫu.

Chạy đoạn mã sau ở đâu đó và quan sát objC:

function A() {
  this.thisA = 4; // objC will contain this property
}

A.prototype.a = 2; // objC will contain this property

B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;

function B() {
  this.thisB = 55; // objC will contain this property

  A.call(this);
}

B.prototype.b = 3; // objC will contain this property

C.prototype = Object.create(B.prototype);
C.prototype.constructor = C;

function C() {
  this.thisC = 123; // objC will contain this property

  B.call(this);
}

C.prototype.c = 2; // objC will contain this property

var objC = new C();
  • B kế thừa nguyên mẫu từ A
  • C kế thừa nguyên mẫu từ B
  • objC là một ví dụ của C

Đây là một lời giải thích tốt về các bước trên:

OOP trong JavaScript: Những gì bạn cần biết


Điều này không sao chép tất cả các thuộc tính vào đối tượng mới, mặc dù? Vì vậy, nếu bạn có hai nguyên mẫu A và B và bạn tạo lại cả hai trên C, việc thay đổi một tài sản của A sẽ không ảnh hưởng đến tài sản đó trên C và ngược lại. Bạn sẽ kết thúc với một bản sao của tất cả các thuộc tính trong A và B được lưu trong bộ nhớ. Nó sẽ có hiệu suất tương tự như khi bạn đã mã hóa cứng tất cả các thuộc tính của A và B thành C. Thật dễ đọc và tìm kiếm tài sản không phải di chuyển đến các đối tượng cha mẹ, nhưng nó không thực sự là thừa kế - giống như nhân bản. Thay đổi thuộc tính trên A không thay đổi thuộc tính nhân bản trên C.
Frank

2

Tôi không phải là một chuyên gia về javascript OOP, nhưng nếu tôi hiểu bạn một cách chính xác, bạn muốn một cái gì đó như (mã giả):

Earth.shape = 'round';
Animal.shape = 'random';

Cat inherit from (Earth, Animal);

Cat.shape = 'random' or 'round' depending on inheritance order;

Trong trường hợp đó, tôi sẽ thử một cái gì đó như:

var Earth = function(){};
Earth.prototype.shape = 'round';

var Animal = function(){};
Animal.prototype.shape = 'random';
Animal.prototype.head = true;

var Cat = function(){};

MultiInherit(Cat, Earth, Animal);

console.log(new Cat().shape); // yields "round", since I reversed the inheritance order
console.log(new Cat().head); // true

function MultiInherit() {
    var c = [].shift.call(arguments),
        len = arguments.length
    while(len--) {
        $.extend(c.prototype, new arguments[len]());
    }
}

1
Đây không phải là chỉ chọn nguyên mẫu đầu tiên và bỏ qua phần còn lại? Đặt c.prototypenhiều lần không mang lại nhiều nguyên mẫu. Ví dụ, nếu bạn có Animal.isAlive = true, Cat.isAlivevẫn sẽ không được xác định.
devios1

Phải, tôi có ý định trộn các nguyên mẫu, đã sửa ... (Tôi đã sử dụng phần mở rộng của jQuery ở đây, nhưng bạn có được hình ảnh)
David Hellsing

2

Có thể triển khai nhiều kế thừa trong JavaScript, mặc dù rất ít thư viện thực hiện.

Tôi có thể chỉ ra Ring.js , ví dụ duy nhất tôi biết.


2

Tôi đã làm việc về điều này rất nhiều ngày hôm nay và cố gắng để đạt được điều này trong ES6. Cách tôi đã làm là sử dụng Browserify, Babel và sau đó tôi đã thử nghiệm nó với Wallaby và nó dường như hoạt động. Mục tiêu của tôi là mở rộng Mảng hiện tại, bao gồm ES6, ES7 và thêm một số tính năng tùy chỉnh bổ sung tôi cần trong nguyên mẫu để xử lý dữ liệu âm thanh.

Wallaby vượt qua 4 bài kiểm tra của tôi. Tệp example.js có thể được dán trong bảng điều khiển và bạn có thể thấy rằng thuộc tính 'bao gồm' nằm trong nguyên mẫu của lớp. Tôi vẫn muốn thử nghiệm điều này nhiều hơn vào ngày mai.

Đây là phương pháp của tôi: (Tôi rất có thể sẽ tái cấu trúc và đóng gói lại dưới dạng một mô-đun sau một số giấc ngủ!)

var includes = require('./polyfills/includes');
var keys =  Object.getOwnPropertyNames(includes.prototype);
keys.shift();

class ArrayIncludesPollyfills extends Array {}

function inherit (...keys) {
  keys.map(function(key){
      ArrayIncludesPollyfills.prototype[key]= includes.prototype[key];
  });
}

inherit(keys);

module.exports = ArrayIncludesPollyfills

Github Repo: https://github.com/danieldram/array-includes-polyfill


2

Tôi nghĩ nó đơn giản đến nực cười. Vấn đề ở đây là lớp con sẽ chỉ đề cập đến instanceoflớp đầu tiên bạn gọi

https://jsfiddle.net/1033xzyt/19/

function Foo() {
  this.bar = 'bar';
  return this;
}
Foo.prototype.test = function(){return 1;}

function Bar() {
  this.bro = 'bro';
  return this;
}
Bar.prototype.test2 = function(){return 2;}

function Cool() {
  Foo.call(this);
  Bar.call(this);

  return this;
}

var combine = Object.create(Foo.prototype);
$.extend(combine, Object.create(Bar.prototype));

Cool.prototype = Object.create(combine);
Cool.prototype.constructor = Cool;

var cool = new Cool();

console.log(cool.test()); // 1
console.log(cool.test2()); //2
console.log(cool.bro) //bro
console.log(cool.bar) //bar
console.log(cool instanceof Foo); //true
console.log(cool instanceof Bar); //false

1

Kiểm tra mã bên dưới mà IS hiển thị hỗ trợ cho nhiều kế thừa. Thực hiện bằng cách sử dụng BẢO VỆ PROTOTYPAL

function A(name) {
    this.name = name;
}
A.prototype.setName = function (name) {

    this.name = name;
}

function B(age) {
    this.age = age;
}
B.prototype.setAge = function (age) {
    this.age = age;
}

function AB(name, age) {
    A.prototype.setName.call(this, name);
    B.prototype.setAge.call(this, age);
}

AB.prototype = Object.assign({}, Object.create(A.prototype), Object.create(B.prototype));

AB.prototype.toString = function () {
    return `Name: ${this.name} has age: ${this.age}`
}

const a = new A("shivang");
const b = new B(32);
console.log(a.name);
console.log(b.age);
const ab = new AB("indu", 27);
console.log(ab.toString());

1

Tôi có khá nhiều chức năng để cho phép các lớp được định nghĩa với nhiều kế thừa. Nó cho phép mã như sau. Nhìn chung, bạn sẽ lưu ý một sự khởi đầu hoàn toàn từ các kỹ thuật Phân loại gốc trong javascript (ví dụ: bạn sẽ không bao giờ thấy classtừ khóa):

let human = new Running({ name: 'human', numLegs: 2 });
human.run();

let airplane = new Flying({ name: 'airplane', numWings: 2 });
airplane.fly();

let dragon = new RunningFlying({ name: 'dragon', numLegs: 4, numWings: 6 });
dragon.takeFlight();

để tạo ra sản lượng như thế này:

human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!

Dưới đây là các định nghĩa lớp trông như thế nào:

let Named = makeClass('Named', {}, () => ({
  init: function({ name }) {
    this.name = name;
  }
}));

let Running = makeClass('Running', { Named }, protos => ({
  init: function({ name, numLegs }) {
    protos.Named.init.call(this, { name });
    this.numLegs = numLegs;
  },
  run: function() {
    console.log(`${this.name} runs with ${this.numLegs} legs.`);
  }
}));

let Flying = makeClass('Flying', { Named }, protos => ({
  init: function({ name, numWings }) {
    protos.Named.init.call(this, { name });
    this.numWings = numWings;
  },
  fly: function( ){
    console.log(`${this.name} flies away with ${this.numWings} wings!`);
  }
}));

let RunningFlying = makeClass('RunningFlying', { Running, Flying }, protos => ({
  init: function({ name, numLegs, numWings }) {
    protos.Running.init.call(this, { name, numLegs });
    protos.Flying.init.call(this, { name, numWings });
  },
  takeFlight: function() {
    this.run();
    this.fly();
  }
}));

Chúng ta có thể thấy rằng mỗi định nghĩa lớp sử dụng makeClass hàm chấp nhận một Objecttên lớp cha được ánh xạ tới các lớp cha. Nó cũng chấp nhận một hàm trả về một Objectthuộc tính chứa cho lớp được định nghĩa. Hàm này có một tham số protos, chứa đủ thông tin để truy cập vào bất kỳ thuộc tính nào được xác định bởi bất kỳ lớp cha nào.

Phần cuối cùng được yêu cầu là makeClasschính hàm, nó thực hiện khá nhiều công việc. Đây là, cùng với phần còn lại của mã. Tôi đã nhận xét makeClasskhá nhiều:

let makeClass = (name, parents={}, propertiesFn=()=>({})) => {
  
  // The constructor just curries to a Function named "init"
  let Class = function(...args) { this.init(...args); };
  
  // This allows instances to be named properly in the terminal
  Object.defineProperty(Class, 'name', { value: name });
  
  // Tracking parents of `Class` allows for inheritance queries later
  Class.parents = parents;
  
  // Initialize prototype
  Class.prototype = Object.create(null);
  
  // Collect all parent-class prototypes. `Object.getOwnPropertyNames`
  // will get us the best results. Finally, we'll be able to reference
  // a property like "usefulMethod" of Class "ParentClass3" with:
  // `parProtos.ParentClass3.usefulMethod`
  let parProtos = {};
  for (let parName in parents) {
    let proto = parents[parName].prototype;
    parProtos[parName] = {};
    for (let k of Object.getOwnPropertyNames(proto)) {
      parProtos[parName][k] = proto[k];
    }
  }
  
  // Resolve `properties` as the result of calling `propertiesFn`. Pass
  // `parProtos`, so a child-class can access parent-class methods, and
  // pass `Class` so methods of the child-class have a reference to it
  let properties = propertiesFn(parProtos, Class);
  properties.constructor = Class; // Ensure "constructor" prop exists
  
  // If two parent-classes define a property under the same name, we
  // have a "collision". In cases of collisions, the child-class *must*
  // define a method (and within that method it can decide how to call
  // the parent-class methods of the same name). For every named
  // property of every parent-class, we'll track a `Set` containing all
  // the methods that fall under that name. Any `Set` of size greater
  // than one indicates a collision.
  let propsByName = {}; // Will map property names to `Set`s
  for (let parName in parProtos) {
    
    for (let propName in parProtos[parName]) {
      
      // Now track the property `parProtos[parName][propName]` under the
      // label of `propName`
      if (!propsByName.hasOwnProperty(propName))
        propsByName[propName] = new Set();
      propsByName[propName].add(parProtos[parName][propName]);
      
    }
    
  }
  
  // For all methods defined by the child-class, create or replace the
  // entry in `propsByName` with a Set containing a single item; the
  // child-class' property at that property name (this also guarantees
  // there is no collision at this property name). Note property names
  // prefixed with "$" will be considered class properties (and the "$"
  // will be removed).
  for (let propName in properties) {
    if (propName[0] === '$') {
      
      // The "$" indicates a class property; attach to `Class`:
      Class[propName.slice(1)] = properties[propName];
      
    } else {
      
      // No "$" indicates an instance property; attach to `propsByName`:
      propsByName[propName] = new Set([ properties[propName] ]);
      
    }
  }
  
  // Ensure that "init" is defined by a parent-class or by the child:
  if (!propsByName.hasOwnProperty('init'))
    throw Error(`Class "${name}" is missing an "init" method`);
  
  // For each property name in `propsByName`, ensure that there is no
  // collision at that property name, and if there isn't, attach it to
  // the prototype! `Object.defineProperty` can ensure that prototype
  // properties won't appear during iteration with `in` keyword:
  for (let propName in propsByName) {
    let propsAtName = propsByName[propName];
    if (propsAtName.size > 1)
      throw new Error(`Class "${name}" has conflict at "${propName}"`);
    
    Object.defineProperty(Class.prototype, propName, {
      enumerable: false,
      writable: true,
      value: propsAtName.values().next().value // Get 1st item in Set
    });
  }
  
  return Class;
};

let Named = makeClass('Named', {}, () => ({
  init: function({ name }) {
    this.name = name;
  }
}));

let Running = makeClass('Running', { Named }, protos => ({
  init: function({ name, numLegs }) {
    protos.Named.init.call(this, { name });
    this.numLegs = numLegs;
  },
  run: function() {
    console.log(`${this.name} runs with ${this.numLegs} legs.`);
  }
}));

let Flying = makeClass('Flying', { Named }, protos => ({
  init: function({ name, numWings }) {
    protos.Named.init.call(this, { name });
    this.numWings = numWings;
  },
  fly: function( ){
    console.log(`${this.name} flies away with ${this.numWings} wings!`);
  }
}));

let RunningFlying = makeClass('RunningFlying', { Running, Flying }, protos => ({
  init: function({ name, numLegs, numWings }) {
    protos.Running.init.call(this, { name, numLegs });
    protos.Flying.init.call(this, { name, numWings });
  },
  takeFlight: function() {
    this.run();
    this.fly();
  }
}));

let human = new Running({ name: 'human', numLegs: 2 });
human.run();

let airplane = new Flying({ name: 'airplane', numWings: 2 });
airplane.fly();

let dragon = new RunningFlying({ name: 'dragon', numLegs: 4, numWings: 6 });
dragon.takeFlight();

Các makeClasschức năng cũng hỗ trợ thuộc tính của lớp; chúng được xác định bằng tiền tố tên thuộc tính với $ký hiệu (lưu ý rằng tên thuộc tính cuối cùng mà kết quả sẽ bị $xóa). Với suy nghĩ này, chúng tôi có thể viết một chuyên ngànhDragon lớp mô hình hóa "loại" của Rồng, trong đó danh sách các loại Rồng có sẵn được lưu trữ trên chính Lớp đó, trái ngược với các trường hợp:

let Dragon = makeClass('Dragon', { RunningFlying }, protos => ({

  $types: {
    wyvern: 'wyvern',
    drake: 'drake',
    hydra: 'hydra'
  },

  init: function({ name, numLegs, numWings, type }) {
    protos.RunningFlying.init.call(this, { name, numLegs, numWings });
    this.type = type;
  },
  description: function() {
    return `A ${this.type}-type dragon with ${this.numLegs} legs and ${this.numWings} wings`;
  }
}));

let dragon1 = new Dragon({ name: 'dragon1', numLegs: 2, numWings: 4, type: Dragon.types.drake });
let dragon2 = new Dragon({ name: 'dragon2', numLegs: 4, numWings: 2, type: Dragon.types.hydra });

Những thách thức của đa kế thừa

Bất cứ ai theo dõi mã makeClasschặt chẽ sẽ lưu ý một hiện tượng không mong muốn khá quan trọng xảy ra âm thầm khi đoạn mã trên chạy: khởi tạo một RunningFlyingkết quả sẽ dẫn đến HAI cuộc gọi đến nhà Namedxây dựng!

Điều này là do biểu đồ thừa kế trông như thế này:

 (^^ More Specialized ^^)

      RunningFlying
         /     \
        /       \
    Running   Flying
         \     /
          \   /
          Named

  (vv More Abstract vv)

Khi có nhiều đường dẫn đến cùng một lớp cha trong biểu đồ thừa kế của lớp con , các cảnh báo của lớp con sẽ gọi hàm tạo của lớp cha mẹ đó nhiều lần.

Kết hợp điều này là không tầm thường. Chúng ta hãy xem xét một số ví dụ với các tên lớp đơn giản hóa. Chúng ta sẽ xem xét lớp A, lớp cha mẹ trừu tượng nhất, các lớp BC, cả hai đều kế thừa từ Avà lớp BCkế thừa từ BC(và do đó về mặt khái niệm là "thừa kế kép" từ A):

let A = makeClass('A', {}, () => ({
  init: function() {
    console.log('Construct A');
  }
}));
let B = makeClass('B', { A }, protos => ({
  init: function() {
    protos.A.init.call(this);
    console.log('Construct B');
  }
}));
let C = makeClass('C', { A }, protos => ({
  init: function() {
    protos.A.init.call(this);
    console.log('Construct C');
  }
}));
let BC = makeClass('BC', { B, C }, protos => ({
  init: function() {
    // Overall "Construct A" is logged twice:
    protos.B.init.call(this); // -> console.log('Construct A'); console.log('Construct B');
    protos.C.init.call(this); // -> console.log('Construct A'); console.log('Construct C');
    console.log('Construct BC');
  }
}));

Nếu chúng ta muốn ngăn chặn việc BCgọi hai lầnA.prototype.init chúng ta có thể cần phải từ bỏ kiểu gọi trực tiếp các hàm tạo. Chúng tôi sẽ cần một số mức độ gián tiếp để kiểm tra xem các cuộc gọi trùng lặp có xảy ra hay không và ngắn mạch trước khi chúng xảy ra.

Chúng ta có thể xem xét thay đổi các tham số được cung cấp cho hàm thuộc tính: bên cạnh protos, Objectchứa dữ liệu thô mô tả các thuộc tính được kế thừa, chúng ta cũng có thể bao gồm một hàm tiện ích để gọi một phương thức cá thể theo cách mà các phương thức cha mẹ cũng được gọi, nhưng các cuộc gọi trùng lặp được phát hiện và ngăn chặn. Chúng ta hãy xem nơi chúng ta thiết lập các tham số cho propertiesFn Function:

let makeClass = (name, parents, propertiesFn) => {

  /* ... a bunch of makeClass logic ... */

  // Allows referencing inherited functions; e.g. `parProtos.ParentClass3.usefulMethod`
  let parProtos = {};
  /* ... collect all parent methods in `parProtos` ... */

  // Utility functions for calling inherited methods:
  let util = {};
  util.invokeNoDuplicates = (instance, fnName, args, dups=new Set()) => {

    // Invoke every parent method of name `fnName` first...
    for (let parName of parProtos) {
      if (parProtos[parName].hasOwnProperty(fnName)) {
        // Our parent named `parName` defines the function named `fnName`
        let fn = parProtos[parName][fnName];

        // Check if this function has already been encountered.
        // This solves our duplicate-invocation problem!!
        if (dups.has(fn)) continue;
        dups.add(fn);

        // This is the first time this Function has been encountered.
        // Call it on `instance`, with the desired args. Make sure we
        // include `dups`, so that if the parent method invokes further
        // inherited methods we don't lose track of what functions have
        // have already been called.
        fn.call(instance, ...args, dups);
      }
    }

  };

  // Now we can call `propertiesFn` with an additional `util` param:
  // Resolve `properties` as the result of calling `propertiesFn`:
  let properties = propertiesFn(parProtos, util, Class);

  /* ... a bunch more makeClass logic ... */

};

Toàn bộ mục đích của thay đổi ở trên makeClasslà để chúng tôi có thêm một đối số được cung cấp cho chúng tôi propertiesFnkhi chúng tôi gọi makeClass. Chúng ta cũng nên lưu ý rằng mọi hàm được định nghĩa trong bất kỳ lớp nào bây giờ có thể nhận được một tham số sau tất cả các hàm khác, được đặt tên dup, đó là một Sethàm chứa tất cả các hàm đã được gọi là kết quả của việc gọi phương thức được kế thừa:

let A = makeClass('A', {}, () => ({
  init: function() {
    console.log('Construct A');
  }
}));
let B = makeClass('B', { A }, (protos, util) => ({
  init: function(dups) {
    util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
    console.log('Construct B');
  }
}));
let C = makeClass('C', { A }, (protos, util) => ({
  init: function(dups) {
    util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
    console.log('Construct C');
  }
}));
let BC = makeClass('BC', { B, C }, (protos, util) => ({
  init: function(dups) {
    util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
    console.log('Construct BC');
  }
}));

Phong cách mới này thực sự thành công trong việc đảm bảo "Construct A"chỉ được ghi lại một lần khi một phiên bản BCđược khởi tạo. Nhưng có ba nhược điểm, thứ ba là rất nghiêm trọng :

  1. Mã này đã trở nên ít đọc và có thể duy trì. Rất nhiều sự phức tạp ẩn đằng sau util.invokeNoDuplicateschức năng, và suy nghĩ về cách phong cách này tránh được việc gọi đa năng là không trực quan và gây đau đầu. Chúng ta cũng có dupstham số pesky đó, thực sự cần được xác định trên mỗi hàm duy nhất trong lớp . Ôi.
  2. Mã này chậm hơn - yêu cầu nhiều hơn một chút và tính toán để đạt được kết quả mong muốn với nhiều kế thừa. Thật không may, đây có thể là trường hợp với bất kỳ giải pháp nào cho vấn đề đa yêu cầu của chúng tôi.
  3. Đáng kể nhất, cấu trúc của các chức năng dựa trên sự kế thừa đã trở nên rất cứng nhắc . Nếu một lớp con NiftyClassghi đè một hàm niftyFunctionvà sử dụng util.invokeNoDuplicates(this, 'niftyFunction', ...)để chạy nó mà không cần gọi trùng lặp, NiftyClass.prototype.niftyFunctionsẽ gọi hàm có tên niftyFunctioncủa mọi lớp cha định nghĩa nó, bỏ qua mọi giá trị trả về từ các lớp đó và cuối cùng thực hiện logic chuyên biệt của NiftyClass.prototype.niftyFunction. Đây là cấu trúc duy nhất có thể . Nếu NiftyClasskế thừa CoolClassGoodClassvà cả hai lớp cha mẹ này cung cấp các niftyFunctionđịnh nghĩa của riêng chúng, NiftyClass.prototype.niftyFunctionsẽ không bao giờ (không có rủi ro nhiều lần gọi) có thể:
    • A. Chạy logic chuyên biệt NiftyClasstrước, sau đó là logic chuyên biệt của các lớp cha
    • B. Chạy logic chuyên biệt NiftyClasstại bất kỳ điểm nào khác ngoài sau khi tất cả logic cha chuyên biệt đã hoàn thành
    • C. Hành xử có điều kiện tùy thuộc vào giá trị trả về của logic chuyên biệt của cha mẹ
    • D. Tránh chạy niftyFunctionhoàn toàn chuyên biệt của một phụ huynh cụ thể

Tất nhiên, chúng ta có thể giải quyết từng vấn đề bằng chữ ở trên bằng cách xác định các hàm chuyên dụng trong util:

  • A. xác địnhutil.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
  • B. xác định util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)( parentNameTên của cha mẹ có logic chuyên biệt sẽ được theo sau bởi logic chuyên biệt của lớp con)
  • C. xác định util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(Trong trường hợp này testFnsẽ nhận được kết quả của logic chuyên biệt cho cha mẹ được đặt tên parentNamevà sẽ trả về một true/falsegiá trị cho biết liệu có xảy ra ngắn mạch hay không)
  • D. xác định util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(Trong trường hợp này blackListsẽ là một Arraytên cha mẹ có logic chuyên biệt nên được bỏ qua hoàn toàn)

Những giải pháp này đều có sẵn, nhưng đây là tình trạng hỗn loạn ! Đối với mọi cấu trúc duy nhất mà một lệnh gọi hàm được kế thừa có thể thực hiện, chúng ta sẽ cần một phương thức chuyên biệt được định nghĩa dưới đây util. Thật là một thảm họa tuyệt đối.

Với suy nghĩ này, chúng ta có thể bắt đầu thấy những thách thức trong việc thực hiện nhiều kế thừa tốt. Việc thực hiện đầy đủ của makeClasstôi được cung cấp trong câu trả lời này thậm chí không xem xét vấn đề đa yêu cầu, hoặc nhiều vấn đề khác phát sinh liên quan đến nhiều kế thừa.

Câu trả lời này đang trở nên rất dài. Tôi hy vọng việc makeClasstriển khai tôi đưa vào vẫn hữu ích, ngay cả khi nó không hoàn hảo. Tôi cũng hy vọng bất cứ ai quan tâm đến chủ đề này sẽ có được nhiều bối cảnh hơn để ghi nhớ khi họ đọc thêm!


0

Hãy xem gói IeUnit .

Việc đồng hóa khái niệm được triển khai trong IeUnit dường như cung cấp những gì bạn đang tìm kiếm một cách khá năng động.


0

Dưới đây là một ví dụ về chuỗi nguyên mẫu sử dụng các hàm xây dựng :

function Lifeform () {             // 1st Constructor function
    this.isLifeform = true;
}

function Animal () {               // 2nd Constructor function
    this.isAnimal = true;
}
Animal.prototype = new Lifeform(); // Animal is a lifeform

function Mammal () {               // 3rd Constructor function
    this.isMammal = true;
}
Mammal.prototype = new Animal();   // Mammal is an animal

function Cat (species) {           // 4th Constructor function
    this.isCat = true;
    this.species = species
}
Cat.prototype = new Mammal();     // Cat is a mammal

Khái niệm này sử dụng định nghĩa "lớp" của Yehuda Katz cho JavaScript:

... một "lớp" JavaScript chỉ là một đối tượng Hàm đóng vai trò là hàm tạo cộng với một đối tượng nguyên mẫu đính kèm. ( Nguồn: Giáo sư Katz )

Không giống như cách tiếp cận Object.create , khi các lớp được xây dựng theo cách này và chúng tôi muốn tạo các thể hiện của một "lớp", chúng tôi không cần biết mỗi "lớp" được thừa hưởng từ cái gì. Chúng tôi chỉ sử dụng new.

// Make an instance object of the Cat "Class"
var tiger = new Cat("tiger");

console.log(tiger.isCat, tiger.isMammal, tiger.isAnimal, tiger.isLifeform);
// Outputs: true true true true

Thứ tự ưu tiên sẽ có ý nghĩa. Đầu tiên, nó nhìn vào đối tượng, sau đó là nguyên mẫu, rồi nguyên mẫu tiếp theo, v.v.

// Let's say we have another instance, a special alien cat
var alienCat = new Cat("alien");
// We can define a property for the instance object and that will take 
// precendence over the value in the Mammal class (down the chain)
alienCat.isMammal = false;
// OR maybe all cats are mutated to be non-mammals
Cat.prototype.isMammal = false;
console.log(alienCat);

Chúng ta cũng có thể sửa đổi các nguyên mẫu sẽ ảnh hưởng đến tất cả các đối tượng được xây dựng trên lớp.

// All cats are mutated to be non-mammals
Cat.prototype.isMammal = false;
console.log(tiger, alienCat);

Ban đầu tôi đã viết một số điều này với câu trả lời này .


2
OP đang yêu cầu nhiều chuỗi nguyên mẫu (ví dụ: childkế thừa từ parent1parent2). Ví dụ của bạn chỉ nói về một chuỗi.
poshest

0

Một latecomer trong cảnh là SimpleDeclare . Tuy nhiên, khi xử lý nhiều kế thừa, bạn vẫn sẽ kết thúc với các bản sao của các hàm tạo ban đầu. Đó là một điều cần thiết trong Javascript ...

Merc.


Đó là một điều cần thiết trong Javascript ... cho đến khi ES6 Proxy.
Jonathon

Proxy thật thú vị! Tôi chắc chắn sẽ xem xét việc thay đổi SimpleDeclare để không cần sao chép các phương thức bằng cách sử dụng proxy khi chúng trở thành một phần của tiêu chuẩn. Mã của SimpleDeclare thực sự, rất dễ đọc và thay đổi ...
Merc

0

Tôi sẽ sử dụng ds.oop . Nó tương tự như mẫu.j.j và các loại khác. làm cho nhiều kế thừa rất dễ dàng và tối giản của nó. (chỉ 2 hoặc 3 kb) Cũng hỗ trợ một số tính năng gọn gàng khác như giao diện và nội dung phụ thuộc

/*** multiple inheritance example ***********************************/

var Runner = ds.class({
    run: function() { console.log('I am running...'); }
});

var Walker = ds.class({
    walk: function() { console.log('I am walking...'); }
});

var Person = ds.class({
    inherits: [Runner, Walker],
    eat: function() { console.log('I am eating...'); }
});

var person = new Person();

person.run();
person.walk();
person.eat();

0

Còn về điều này, nó thực hiện nhiều kế thừa trong JavaScript:

    class Car {
        constructor(brand) {
            this.carname = brand;
        }
        show() {
            return 'I have a ' + this.carname;
        }
    }

    class Asset {
        constructor(price) {
            this.price = price;
        }
        show() {
            return 'its estimated price is ' + this.price;
        }
    }

    class Model_i1 {        // extends Car and Asset (just a comment for ourselves)
        //
        constructor(brand, price, usefulness) {
            specialize_with(this, new Car(brand));
            specialize_with(this, new Asset(price));
            this.usefulness = usefulness;
        }
        show() {
            return Car.prototype.show.call(this) + ", " + Asset.prototype.show.call(this) + ", Model_i1";
        }
    }

    mycar = new Model_i1("Ford Mustang", "$100K", 16);
    document.getElementById("demo").innerHTML = mycar.show();

Và đây là mã cho hàm Utility_with ():

function specialize_with(o, S) { for (var prop in S) { o[prop] = S[prop]; } }

Đây là mã thực sự chạy. Bạn có thể sao chép-dán nó trong tệp html và tự mình thử. Nó không hoạt động.

Đó là nỗ lực để thực hiện MI trong JavaScript. Không có nhiều mã, nhiều bí quyết.

Xin vui lòng xem bài viết đầy đủ của tôi về điều này, https://github.com/latitov/OOP_MI_Ct_oPlus_in_JS


0

Tôi chỉ sử dụng để gán những lớp tôi cần trong các thuộc tính của người khác và thêm proxy để tự động trỏ đến chúng mà tôi thích:

class A {
    constructor()
    {
        this.test = "a test";
    }

    method()
    {
        console.log("in the method");
    }
}

class B {
    constructor()
    {
        this.extends = [new A()];

        return new Proxy(this, {
            get: function(obj, prop) {

                if(prop in obj)
                    return obj[prop];

                let response = obj.extends.find(function (extended) {
                if(prop in extended)
                    return extended[prop];
            });

            return response ? response[prop] : Reflect.get(...arguments);
            },

        })
    }
}

let b = new B();
b.test ;// "a test";
b.method(); // in the method
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.