Berbagai warisan / prototipe dalam JavaScript

132

Saya telah sampai pada titik di mana saya perlu memiliki beberapa pewarisan berganda yang belum sempurna yang terjadi di JavaScript. (Saya di sini bukan untuk membahas apakah ini ide yang bagus atau tidak, jadi tolong simpan komentar itu untuk Anda sendiri.)

Saya hanya ingin tahu apakah ada yang mencoba ini dengan sukses (atau tidak), dan bagaimana mereka melakukannya.

Untuk mendidihkannya, yang benar-benar saya butuhkan adalah untuk dapat memiliki objek yang mampu mewarisi properti dari lebih dari satu rantai prototipe (yaitu masing-masing prototipe dapat memiliki rantai yang tepat sendiri), tetapi dalam urutan prioritas tertentu (itu akan cari rantai untuk definisi pertama).

Untuk menunjukkan bagaimana ini secara teori dimungkinkan, itu dapat dicapai dengan menempelkan rantai sekunder pada akhir rantai primer, tetapi ini akan memengaruhi semua contoh prototipe sebelumnya dan bukan itu yang saya inginkan.

Pikiran?

devios1
sumber
1
Saya pikir dojo declare menangani beberapa warisan src juga aku punya perasaan mootools tidak juga, banyak dari ini adalah di luar saya tapi aku akan memiliki membaca cepat ini sebagai dojo menunjukkan
TI
Lihatlah TraitsJS ( tautan 1 , tautan 2 ) ini adalah alternatif yang sangat bagus untuk pewarisan berganda dan campuran ...
CMS
1
@Pointy karena itu tidak terlalu dinamis. Saya ingin dapat mengambil perubahan yang dilakukan ke rantai induk saat terjadi. Namun yang mengatakan, saya mungkin harus menggunakan ini jika itu tidak mungkin.
devios1
kemungkinan duplikat apakah javascript mendukung pewarisan berganda seperti C ++
Daniel Earwicker
1
Bacaan menarik tentang ini: webreflection.blogspot.co.uk/2009/06/...
Nobita

Jawaban:

49

Berbagai pewarisan dapat dicapai dalam ECMAScript 6 dengan menggunakan objek Proxy .

Penerapan

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

Penjelasan

Objek proxy terdiri dari objek target dan beberapa jebakan, yang menentukan perilaku khusus untuk operasi mendasar.

Saat membuat objek yang diwarisi dari yang lain, kami menggunakan Object.create(obj). Tetapi dalam hal ini kami ingin pewarisan berganda, jadi alih-alihobj saya menggunakan proxy yang akan mengarahkan operasi mendasar ke objek yang sesuai.

Saya menggunakan perangkap ini:

  • The hasperangkap adalah perangkap bagi inoperator yang . Saya menggunakan someuntuk memeriksa apakah setidaknya satu prototipe berisi properti.
  • The getperangkap adalah perangkap untuk mendapatkan nilai properti. Saya menggunakan finduntuk menemukan prototipe pertama yang berisi properti itu, dan saya mengembalikan nilainya, atau memanggil pengambil pada penerima yang sesuai. Ini ditangani oleh Reflect.get. Jika tidak ada prototipe yang mengandung properti, saya kembali undefined.
  • The setperangkap adalah perangkap untuk menetapkan nilai properti. Saya menggunakan finduntuk menemukan prototipe pertama yang berisi properti itu, dan saya menyebutnya setter pada penerima yang sesuai. Jika tidak ada setter atau prototipe tidak berisi properti, nilai ditentukan pada penerima yang sesuai. Ini ditangani olehReflect.set .
  • The enumerateperangkap adalah perangkap untuk for...inloop . Saya beralih properti enumerable dari prototipe pertama, lalu dari yang kedua, dan seterusnya. Setelah sebuah properti di iterasi, saya menyimpannya di tabel hash untuk menghindari iterasi lagi.
    Peringatan : Perangkap ini telah dihapus dalam konsep ES7 dan tidak digunakan lagi di browser.
  • The ownKeysperangkap adalah perangkap bagi Object.getOwnPropertyNames(). Sejak ES7, for...inloop terus memanggil [[GetPrototypeOf]] dan mendapatkan properti masing-masing. Jadi untuk membuatnya mengulangi properti dari semua prototipe, saya menggunakan perangkap ini untuk membuat semua properti yang diwarisi enumerable muncul seperti properti sendiri.
  • The getOwnPropertyDescriptorperangkap adalah perangkap bagi Object.getOwnPropertyDescriptor(). Membuat semua properti enumerable tampak seperti properti sendiri dalam ownKeysperangkap tidak cukup, for...inloop akan meminta deskriptor untuk memeriksa apakah mereka enumerable. Jadi saya gunakan finduntuk menemukan prototipe pertama yang berisi properti itu, dan saya mengulangi rantai prototipenya sampai saya menemukan pemilik properti, dan saya mengembalikan deskriptornya. Jika tidak ada prototipe yang mengandung properti, saya kembali undefined. Deskriptor dimodifikasi untuk membuatnya dapat dikonfigurasi, jika tidak kita dapat memecah beberapa invarian proxy.
  • Perangkap preventExtensionsdan definePropertyhanya disertakan untuk mencegah operasi ini dari memodifikasi target proxy. Kalau tidak, kita bisa akhirnya memecahkan beberapa invarian proxy.

Ada lebih banyak jebakan yang tersedia, yang tidak saya gunakan

  • The getPrototypeOfperangkap bisa ditambahkan, tetapi tidak ada cara yang tepat untuk mengembalikan beberapa prototipe. Ini berarti instanceoftidak akan berhasil. Oleh karena itu, saya membiarkannya mendapatkan prototipe target, yang awalnya adalah nol.
  • The setPrototypeOfperangkap dapat ditambahkan dan menerima array obyek, yang akan menggantikan prototipe. Ini dibiarkan sebagai latihan untuk pembaca. Di sini saya hanya membiarkannya memodifikasi prototipe target, yang tidak banyak berguna karena tidak ada perangkap yang menggunakan target.
  • The deletePropertyperangkap adalah perangkap bagi menghapus sifat sendiri. Proxy mewakili warisan, jadi ini tidak masuk akal. Saya membiarkannya mencoba penghapusan pada target, yang seharusnya tidak memiliki properti.
  • The isExtensibleperangkap adalah perangkap untuk mendapatkan diperpanjang tersebut. Tidak banyak berguna, mengingat bahwa seorang invarian memaksa untuk mengembalikan ekstensibilitas yang sama dengan target. Jadi saya biarkan saja mengarahkan operasi ke target, yang akan diperluas.
  • The applydan constructperangkap perangkap untuk menelepon atau instantiating. Mereka hanya berguna ketika targetnya adalah fungsi atau konstruktor.

Contoh

// 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"
Oriol
sumber
1
Apakah tidak ada beberapa masalah kinerja yang akan menjadi relevan bahkan pada aplikasi skala normal?
Tomáš Zato - Reinstate Monica
1
@ TomášZato Ini akan lebih lambat daripada properti data dalam objek normal, tapi saya tidak berpikir itu akan jauh lebih buruk daripada properti accessor.
Oriol
TIL:multiInherit(o1={a:1}, o2={b:2}, o3={a:3, b:3})
bloodyKnuckles
4
Saya akan mempertimbangkan mengganti "Multiple inheritance" dengan "Multiple delegation" untuk mendapatkan ide yang lebih baik tentang apa yang terjadi. Konsep kunci dalam implementasi Anda adalah bahwa proxy sebenarnya memilih objek yang tepat untuk mendelegasikan (atau meneruskan) pesan. Kekuatan solusi Anda adalah Anda dapat memperluas prototipe target secara dinamis. Jawaban lain menggunakan concatenation (ala Object.assign) atau mendapatkan grafik yang sangat berbeda, pada akhirnya semuanya mendapatkan rantai prototipe satu-satunya antara objek. Solusi proxy menawarkan percabangan runtime, dan ini mengguncang!
sminutoli
Tentang kinerja, jika Anda membuat objek yang mewarisi dari banyak objek, yang mewarisi dari banyak objek, dan seterusnya, maka itu akan menjadi eksponensial. Jadi ya, itu akan lebih lambat. Tetapi dalam kasus normal saya tidak berpikir itu akan seburuk itu.
Oriol
16

Pembaruan (2019): Posting asli semakin usang. Artikel ini (sekarang tautan arsip internet, karena domain hilang) dan pustaka GitHub yang terkait adalah pendekatan modern yang bagus.

Posting asli: Berbagai warisan [sunting, bukan jenis warisan yang tepat, tetapi properti; mixins] dalam Javascript cukup mudah jika Anda menggunakan prototipe buatan daripada yang generik-objek. Berikut adalah dua kelas induk untuk diwarisi dari:

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

Perhatikan bahwa saya telah menggunakan anggota "nama" yang sama dalam setiap kasus, yang bisa menjadi masalah jika orang tua tidak setuju tentang bagaimana "nama" harus ditangani. Tapi mereka kompatibel (berlebihan, sungguh) dalam hal ini.

Sekarang kita hanya perlu kelas yang mewarisi dari keduanya. Warisan dilakukan dengan memanggil fungsi konstruktor (tanpa menggunakan kata kunci baru) untuk prototipe dan konstruktor objek. Pertama, prototipe harus mewarisi dari prototipe induk

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

Dan konstruktor harus mewarisi dari konstruktor induk:

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

Sekarang Anda dapat menanam, makan, dan memanen berbagai contoh:

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

fp1.grow();
fp2.grow();
fp1.harvest();
fp1.eat();
fp2.harvest();
fp2.eat();
Roy J
sumber
Bisakah Anda melakukan ini dengan prototipe bawaan? (Array, String, Number)
Tomáš Zato - Reinstate Monica
Saya tidak berpikir prototipe built-in memiliki konstruktor yang dapat Anda hubungi.
Roy J
Yah, saya bisa melakukannya Array.call(...)tetapi sepertinya tidak mempengaruhi apa pun yang saya lewati this.
Tomáš Zato - Reinstate Monica
@ TomášZato Anda bisa melakukannyaArray.prototype.constructor.call()
Roy J
1
@AbhishekGupta Terima kasih telah memberi tahu saya. Saya telah mengganti tautan dengan tautan ke halaman web yang diarsipkan.
Roy J
7

Ini digunakan Object.createuntuk membuat rantai prototipe nyata:

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

Sebagai contoh:

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

akan kembali:

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

sehingga obj.a === 1, obj.b === 3, dll

pimvdb
sumber
Hanya pertanyaan hipotetis cepat: Saya ingin membuat kelas Vector dengan mencampur prototipe Number dan Array (untuk bersenang-senang). Ini akan memberi saya indeks array dan operator matematika. Tetapi apakah itu akan berhasil?
Tomáš Zato - Reinstate Monica
@ TomášZato, ada baiknya memeriksa artikel ini jika Anda melihat ke dalam array subkelas; itu bisa menyelamatkan Anda dari sakit kepala. semoga berhasil!
user3276552
5

Saya suka implementasi John Resig atas struktur kelas: http://ejohn.org/blog/simple-javascript-inheritance/

Ini dapat dengan mudah diperluas ke sesuatu seperti:

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

    // same code
}

yang akan memungkinkan Anda untuk melewati beberapa objek yang mewarisi. Anda akan kehilangan instanceOfkemampuan di sini, tapi itu diberikan jika Anda ingin banyak warisan.


contoh saya yang agak berbelit-belit di atas tersedia di https://github.com/cwolves/Fetch/blob/master/support/plugins/klass/klass.js

Perhatikan bahwa ada beberapa kode mati dalam file itu, tetapi memungkinkan beberapa pewarisan jika Anda ingin melihatnya.


Jika Anda ingin warisan yang dirantai (BUKAN multiple inheritance, tetapi bagi kebanyakan orang itu adalah hal yang sama), itu dapat diselesaikan dengan Kelas seperti:

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

yang akan mempertahankan rantai prototipe asli, tetapi Anda juga akan memiliki banyak kode tidak berguna yang berjalan.

Mark Kahn
sumber
7
Itu menciptakan klon dangkal yang digabungkan. Menambahkan properti baru ke objek "yang diwarisi" tidak akan menyebabkan properti baru muncul di objek yang diturunkan, seperti yang akan terjadi pada pewarisan prototipe yang sebenarnya.
Daniel Earwicker
@DanielEarwicker - Benar, tetapi jika Anda ingin "multiple inheritance" dalam satu kelas berasal dari dua kelas, sebenarnya tidak ada alternatif. Jawaban yang dimodifikasi untuk mencerminkan bahwa hanya merantai kelas bersama adalah hal yang sama dalam banyak kasus.
Mark Kahn
Sepertinya GitHUb Anda sudah hilang, apakah Anda masih memiliki github.com/cwolves/Fetch/blob/master/support/plugins/klass/... Saya tidak keberatan melihatnya jika Anda ingin berbagi?
JasonDavis
4

Jangan bingung dengan implementasi kerangka kerja JavaScript dari multiple inheritance.

Yang perlu Anda lakukan adalah menggunakan Object.create () untuk membuat objek baru setiap kali dengan objek dan properti prototipe yang ditentukan, kemudian pastikan untuk mengubah Object.prototype.constructor setiap langkah dari jalan jika Anda berencana untuk instantiating Bdi masa depan.

Untuk mewarisi properti instance thisAdan thisBkami menggunakan Function.prototype.call () di akhir setiap fungsi objek. Ini opsional jika Anda hanya peduli mewarisi prototipe.

Jalankan kode berikut di suatu tempat dan amati 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 mewarisi prototipe dari A
  • C mewarisi prototipe dari B
  • objC adalah contoh dari C

Ini adalah penjelasan yang baik dari langkah-langkah di atas:

OOP Dalam JavaScript: Yang Harus Anda Ketahui

Dave
sumber
Namun, bukankah ini menyalin semua properti ke objek baru? Jadi, jika Anda memiliki dua prototipe, A dan B, dan Anda membuat ulang keduanya di C, mengubah properti A tidak akan memengaruhi properti itu di C dan sebaliknya. Anda akan berakhir dengan salinan semua properti di A dan B yang disimpan dalam memori. Ini akan menjadi kinerja yang sama seperti jika Anda telah mengkodekan keras semua properti A dan B menjadi C. Ini bagus untuk dibaca, dan pencarian properti tidak harus melakukan perjalanan ke objek induk, tapi itu tidak benar-benar warisan - lebih seperti kloning. Mengubah properti di A tidak mengubah properti yang dikloning pada C.
Frank
2

Saya sama sekali tidak ahli dalam javascript OOP, tetapi jika saya mengerti Anda dengan benar Anda menginginkan sesuatu seperti (pseudo-code):

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

Cat inherit from (Earth, Animal);

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

Dalam hal ini, saya akan mencoba sesuatu seperti:

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]());
    }
}
David Hellsing
sumber
1
Bukankah ini hanya memilih prototipe pertama dan mengabaikan sisanya? Pengaturan c.prototypebeberapa kali tidak menghasilkan banyak prototipe. Misalnya, jika Anda punya Animal.isAlive = true, Cat.isAliveakan tetap tidak terdefinisi.
devios1
Ya, saya bermaksud untuk mencampur prototipe, dikoreksi ... (Saya menggunakan memperpanjang jQuery di sini, tetapi Anda mendapatkan gambar)
David Hellsing
2

Dimungkinkan untuk menerapkan banyak pewarisan dalam JavaScript, meskipun sangat sedikit perpustakaan yang melakukannya.

Saya bisa menunjuk Ring.js , satu-satunya contoh yang saya tahu.

nicolas-van
sumber
2

Saya banyak mengerjakan hal ini hari ini dan mencoba mencapai ini sendiri di ES6. Cara saya melakukannya menggunakan Browserify, Babel dan kemudian saya mengujinya dengan Wallaby dan sepertinya berhasil. Tujuan saya adalah untuk memperpanjang Array saat ini, termasuk ES6, ES7 dan menambahkan beberapa fitur kustom tambahan yang saya butuhkan dalam prototipe untuk menangani data audio.

Wallaby melewati 4 tes saya. File example.js dapat di-paste di konsol dan Anda dapat melihat bahwa properti 'include' ada dalam prototipe kelas. Saya masih ingin menguji ini lagi besok.

Inilah metode saya: (Saya kemungkinan besar akan refactor dan repackage sebagai modul setelah tidur!)

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

Daniel Ram
sumber
2

Saya pikir ini sangat sederhana. Masalahnya di sini adalah bahwa kelas anak hanya akan merujukinstanceof untuk kelas pertama yang Anda panggil

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
BarryBones41
sumber
1

Periksa kode di bawah ini yang menunjukkan dukungan untuk pewarisan berganda. Dilakukan dengan menggunakan Warisan PROTOTIPAL

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());
Shivang Gupta
sumber
1

Saya memiliki cukup fungsi untuk memungkinkan kelas didefinisikan dengan multiple inheritance. Ini memungkinkan untuk kode seperti berikut. Secara keseluruhan Anda akan mencatat keberangkatan lengkap dari teknik Klasifikasi asli dalam javascript (mis. Anda tidak akan pernah melihat classkata kunci):

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

untuk menghasilkan output seperti ini:

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

Berikut adalah definisi kelasnya:

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

Kita dapat melihat bahwa setiap definisi kelas menggunakan makeClassfungsi menerima Objectnama kelas induk yang dipetakan ke kelas induk. Ini juga menerima fungsi yang mengembalikan Objectproperti yang mengandung untuk kelas yang sedang didefinisikan. Fungsi ini memiliki parameterprotos , yang berisi informasi yang cukup untuk mengakses properti apa pun yang ditentukan oleh kelas induknya.

Bagian terakhir yang diperlukan adalah makeClassfungsi itu sendiri, yang melakukan sedikit pekerjaan. Ini dia, bersama dengan sisa kode. Saya sudah berkomentar makeClasscukup banyak:

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

The makeClassFungsi juga mendukung properti kelas; ini didefinisikan dengan awalan nama properti dengan $simbol (perhatikan bahwa nama properti akhir yang hasilnya akan $dihapus). Dengan mengingat hal ini, kita dapat menulis Dragonkelas khusus yang memodelkan "tipe" Naga, di mana daftar tipe Naga yang tersedia disimpan di Kelas itu sendiri, sebagai lawan dari contoh:

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

Tantangan Berbagai Warisan

Siapa pun yang mengikuti kode untuk makeClasserat akan mencatat fenomena yang tidak diinginkan yang agak signifikan terjadi secara diam-diam ketika kode di atas berjalan: instantiating RunningFlyingakan menghasilkan DUA panggilan ke Namedkonstruktor!

Ini karena grafik warisan terlihat seperti ini:

 (^^ More Specialized ^^)

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

  (vv More Abstract vv)

Ketika ada beberapa lintasan ke kelas induk yang sama dalam grafik warisan kelas-sub , instantiations dari sub-kelas akan memanggil konstruktor kelas induk beberapa kali.

Memerangi ini tidak sepele. Mari kita lihat beberapa contoh dengan nama kelas yang disederhanakan. Kami akan mempertimbangkan kelas A, kelas induk yang paling abstrak, kelas Bdan C, yang keduanya mewarisi dari A, dan kelas BCyang mewarisi dari Bdan C(dan karenanya secara konseptual "warisan ganda" dari 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');
  }
}));

Jika kita ingin mencegah permintaan BCganda, A.prototype.initkita mungkin perlu meninggalkan gaya memanggil langsung konstruktor yang diwariskan. Kita akan memerlukan beberapa tingkat tipuan untuk memeriksa apakah panggilan duplikat terjadi, dan korsleting sebelum terjadi.

Kami dapat mempertimbangkan untuk mengubah parameter yang disediakan ke fungsi properti: di samping protos, Objectdata mentah berisi yang menggambarkan properti yang diwariskan, kami juga dapat menyertakan fungsi utilitas untuk memanggil metode contoh sedemikian rupa sehingga metode induk juga dipanggil, tetapi panggilan duplikat terdeteksi dan dicegah. Mari kita lihat di mana kita menetapkan parameter untuk 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 ... */

};

Seluruh tujuan perubahan di atas makeClassadalah agar kami memiliki argumen tambahan yang diberikan kepada kami propertiesFnsaat kami memohon makeClass. Kita juga harus menyadari bahwa setiap fungsi yang didefinisikan dalam kelas apa pun sekarang dapat menerima parameter setelah semua yang lain, dinamai dup, yang merupakan Setyang menyimpan semua fungsi yang telah dipanggil sebagai akibat dari memanggil metode yang diwarisi:

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

Gaya baru ini sebenarnya berhasil memastikan "Construct A"hanya dicatat satu kali ketika sebuah instance BCdiinisialisasi. Tetapi ada tiga kelemahan, yang ketiga sangat kritis :

  1. Kode ini menjadi kurang mudah dibaca dan dipelihara. Banyak kerumitan bersembunyi di balik util.invokeNoDuplicatesfungsi, dan berpikir tentang bagaimana gaya ini menghindari multi-doa adalah non-intuitif dan merangsang sakit kepala. Kami juga memiliki dupsparameter sial itu , yang benar-benar perlu didefinisikan pada setiap fungsi tunggal di kelas . Aduh.
  2. Kode ini lebih lambat - cukup banyak tipuan dan perhitungan diperlukan untuk mencapai hasil yang diinginkan dengan pewarisan berganda. Sayangnya ini mungkin menjadi kasus dengan setiap solusi untuk masalah multiple-doa kita.
  3. Paling signifikan, struktur fungsi yang bergantung pada warisan menjadi sangat kaku . Jika sub-kelas NiftyClassmengabaikan fungsi niftyFunction, dan menggunakannya util.invokeNoDuplicates(this, 'niftyFunction', ...)untuk menjalankannya tanpa duplikasi-pemanggilan, NiftyClass.prototype.niftyFunctionakan memanggil fungsi yang dinamai niftyFunctionsetiap kelas induk yang mendefinisikannya, mengabaikan nilai pengembalian dari kelas-kelas itu, dan akhirnya melakukan logika khusus NiftyClass.prototype.niftyFunction. Ini adalah satu - satunya struktur yang mungkin . Jika NiftyClassmewarisi CoolClassdan GoodClass, dan kedua kelas induk ini memberikan niftyFunctiondefinisi sendiri, NiftyClass.prototype.niftyFunctiontidak akan pernah (tanpa mempertaruhkan banyak pemanggilan) dapat:
    • A. Jalankan logika khusus NiftyClasspertama, lalu logika khusus kelas induk
    • B. Jalankan logika khusus NiftyClasspada titik mana pun selain setelah semua logika induk khusus telah selesai
    • C. Berperilaku secara kondisional tergantung pada nilai balik logika khusus induknya
    • D. Hindari menjalankan orangtua tertentu yang khusus niftyFunctionsama sekali

Tentu saja, kami dapat menyelesaikan setiap masalah dengan huruf di atas dengan mendefinisikan fungsi khusus di bawah util:

  • A. definisikanutil.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
  • B. define util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)(Di mana parentNamenama induk yang logika khususnya akan segera diikuti oleh logika khusus kelas anak-anak)
  • C. define util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(Dalam hal ini testFnakan menerima hasil dari logika khusus untuk orang tua yang bernama parentName, dan akan mengembalikan true/falsenilai yang menunjukkan apakah korsleting harus terjadi)
  • D. define util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(Dalam hal ini blackListakan menjadi Arraynama induk yang logikanasinya harus dilewati sama sekali)

Semua solusi ini tersedia, tetapi ini adalah kekacauan total ! Untuk setiap struktur unik yang dapat diambil oleh panggilan fungsi yang diwarisi, kita membutuhkan metode khusus yang didefinisikan di bawah util. Benar-benar bencana yang absolut.

Dengan mengingat hal ini, kita dapat mulai melihat tantangan dalam menerapkan pewarisan berganda yang baik. Implementasi penuh dari yang makeClasssaya berikan dalam jawaban ini bahkan tidak mempertimbangkan masalah seruan berganda, atau banyak masalah lain yang timbul berkenaan dengan multiple inheritance.

Jawaban ini semakin panjang. Saya harap makeClassimplementasi yang saya masukkan masih bermanfaat, meskipun tidak sempurna. Saya juga berharap siapa pun yang tertarik dengan topik ini mendapatkan lebih banyak konteks untuk diingat ketika mereka membaca lebih lanjut!

Gershom
sumber
0

Lihatlah paket IeUnit .

Asimilasi konsep yang diterapkan di IeUnit tampaknya menawarkan apa yang Anda cari dengan cara yang cukup dinamis.

James
sumber
0

Berikut adalah contoh rantai prototipe menggunakan fungsi konstruktor :

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

Konsep ini menggunakan definisi Yehuda Katz tentang "kelas" untuk JavaScript:

... "kelas" JavaScript hanyalah objek Fungsi yang berfungsi sebagai konstruktor plus objek prototipe yang dilampirkan. ( Sumber: Guru Katz )

Berbeda dengan pendekatan Object.create , ketika kelas dibangun dengan cara ini dan kami ingin membuat contoh "kelas", kita tidak perlu tahu dari mana "kelas" masing-masing mewarisi dari. Kami hanya menggunakan 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

Urutan prioritas harus masuk akal. Pertama terlihat pada objek instan, lalu prototipe, lalu prototipe berikutnya, dll.

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

Kita juga dapat memodifikasi prototipe yang akan mempengaruhi semua objek yang dibangun di kelas.

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

Saya awalnya menulis ini dengan jawaban ini .

Luke
sumber
2
OP meminta beberapa rantai prototipe (mis. childMewarisi dari parent1dan parent2). Contoh Anda hanya berbicara tentang satu rantai.
poshest
0

Seorang latecomer dalam adegan ini adalah SimpleDeclare . Namun, ketika berhadapan dengan banyak pewarisan, Anda masih akan mendapatkan salinan konstruktor asli. Itu keharusan di Javascript ...

Merc.

Merc
sumber
Itu keharusan di Javascript ... sampai ES6 Proxy.
Jonathon
Proxy menarik! Saya pasti akan melihat ke dalam mengubah SimpleDeclare sehingga tidak perlu menyalin metode menggunakan proxy setelah mereka menjadi bagian dari standar. Kode SimpleDeclare benar-benar, sangat mudah dibaca dan diubah ...
Merc
0

Saya akan menggunakan ds.oop . Mirip dengan prototype.js dan lainnya. membuat multiple inheritance sangat mudah dan minimalis. (hanya 2 atau 3 kb) Juga mendukung beberapa fitur rapi lainnya seperti antarmuka dan injeksi ketergantungan

/*** 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();
dss
sumber
0

Bagaimana dengan ini, ini mengimplementasikan beberapa warisan dalam 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();

Dan inilah kode untuk fungsi utilitas spesialisasi_with ():

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

Ini adalah kode nyata yang berjalan. Anda dapat menyalin-menempelkannya di file html, dan coba sendiri. Itu berhasil.

Itulah upaya untuk mengimplementasikan MI dalam JavaScript. Tidak banyak kode, lebih banyak pengetahuan.

Silakan melihat artikel lengkap saya tentang ini, https://github.com/latitov/OOP_MI_Ct_oPlus_in_JS

Leonid Titov
sumber
0

Saya hanya biasa menetapkan kelas apa yang saya butuhkan di properti orang lain, dan menambahkan proxy ke titik-otomatis yang mereka sukai:

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
Shamaseen
sumber