JavaScript'te çoklu miras / prototipler


132

JavaScript'te bir tür temel çoklu kalıtıma sahip olmam gereken bir noktaya geldim. (Bunun iyi bir fikir olup olmadığını tartışmak için burada değilim, bu yüzden lütfen bu yorumları kendinize saklayın.)

Sadece herhangi birinin bunu herhangi bir başarı ile denediğini (veya denemediğini) ve bunu nasıl yaptıklarını bilmek istiyorum.

Özetlemek gerekirse, gerçekten ihtiyacım olan şey, bir özelliği birden fazla prototip zincirinden miras alabilen bir nesneye sahip olabilmektir (yani her prototip kendi uygun zincirine sahip olabilir), ancak belirli bir öncelik sırasına göre ilk tanım için zincirleri arayın).

Bunun teorik olarak nasıl mümkün olduğunu göstermek için, ikincil zinciri birincil zincirin ucuna bağlayarak başarılabilir, ancak bu önceki prototiplerin herhangi birinin tüm örneklerini etkileyecektir ve istediğim bu değil.

Düşünceler?


1
Bence Dojo beyan kolları birden miras src da çok bu beni aşıyor mu da ben bir his mootools var ama hızlı bir okuma alacağım bu dojoda olarak anlaşılacağı
TI

TraitsJS'ye bir göz atın ( bağlantı 1 , bağlantı 2 ), çoklu kalıtım ve karışımlara gerçekten iyi bir alternatif ...
CMS

1
@ Sivri çünkü bu çok dinamik değil. Her iki ana zincirde de yapılan değişiklikleri meydana geldikçe alabilmek istiyorum. Bununla birlikte, eğer mümkün değilse buna başvurmak zorunda kalabilirim.
devios1


Yanıtlar:


49

ECMAScript 6'da Proxy nesneleri kullanılarak çoklu kalıtım elde edilebilir .

uygulama

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

açıklama

Bir proxy nesnesi, bir hedef nesneden ve temel işlemler için özel davranışı tanımlayan bazı tuzaklardan oluşur.

Bir başkasından miras alan bir nesne oluştururken kullanırız Object.create(obj). Ancak bu durumda çoklu miras istiyoruz, bu nedenle objtemel işlemleri uygun nesneye yeniden yönlendirecek bir proxy kullanıyorum.

Bu tuzakları kullanıyorum:

  • hasTuzak için bir tuzak inoperatörü . someEn az bir prototipin özelliği içerip içermediğini kontrol etmek için kullanıyorum .
  • getTuzak özellik değerlerini almak için bir tuzak. findBu özelliği içeren ilk prototipi bulmak için kullanırım ve değeri döndürürüm veya alıcıyı uygun alıcıda çağırırım. Bu ele alınır Reflect.get. Mülkiyet prototipi içermiyorsa geri dönüyorum undefined.
  • setTuzak özellik değerlerini ayarlamak için bir tuzak. findBu özelliği içeren ilk prototipi bulmak için kullanıyorum ve uygun alıcıda onun ayarlayıcısını çağırıyorum. Ayarlayıcı yoksa veya özelliği içeren prototip yoksa, değer uygun alıcıda tanımlanır. Bu ele alınır Reflect.set.
  • enumerateTuzak için bir tuzak for...indöngüler . Numaralandırılabilir özellikleri ilk prototipten, sonra ikinciden vb. Yineliyorum. Bir özellik yinelendikten sonra, tekrar yinelemekten kaçınmak için onu bir hash tablosunda saklarım.
    Uyarı : Bu tuzak ES7 taslağında kaldırılmıştır ve tarayıcılarda kullanımdan kaldırılmıştır.
  • ownKeysTuzak için bir tuzak Object.getOwnPropertyNames(). ES7'den beri, for...indöngüler [[GetPrototypeOf]] 'u çağırmaya ve her birinin kendi özelliklerini almaya devam ediyor. Bu nedenle, tüm prototiplerin özelliklerini yinelemesini sağlamak için, bu tuzağı, tüm numaralandırılabilir miras alınan özelliklerin kendi özellikleri gibi görünmesini sağlamak için kullanıyorum.
  • getOwnPropertyDescriptorTuzak için bir tuzak Object.getOwnPropertyDescriptor(). Tüm numaralandırılabilir özelliklerin tuzaktaki kendi özellikleri gibi görünmesini sağlamak ownKeysyeterli değildir, for...indöngüler tanımlayıcıya numaralandırılabilir olup olmadıklarını kontrol edecektir. Bu yüzden find, bu özelliği içeren ilk prototipi bulmak için kullanıyorum ve mülk sahibini bulana kadar prototip zincirini yineliyorum ve tanımlayıcısını geri veriyorum. Mülkiyet prototipi içermiyorsa geri dönüyorum undefined. Tanımlayıcı, yapılandırılabilir olması için değiştirilir, aksi takdirde bazı vekil değişmezlerini kırabiliriz.
  • preventExtensionsVe definePropertytuzaklar sadece vekil hedef değiştirmesini bu işlemleri önlemek için dahildir. Aksi takdirde, bazı vekil değişmezleri bozabiliriz.

Kullanmadığım daha fazla tuzak var

  • getPrototypeOfTuzak eklenebilir, ancak birden prototipler dönmek için hiç uygun bir yolu yoktur. Bu da instanceofişe yaramayacağı anlamına gelir . Bu nedenle, başlangıçta boş olan hedefin prototipini almasına izin verdim.
  • setPrototypeOfTuzak ilave edildi ve prototip yerini aldığında, nesnelerin bir dizi kabul edilebilir. Bu okuyucu için bir alıştırma olarak bırakılmıştır. Burada hedefin prototipini değiştirmesine izin veriyorum, bu pek kullanışlı değil çünkü hiçbir tuzak hedefi kullanmıyor.
  • deletePropertyTuzak kendi özellikleri silmek için bir tuzak. Vekil mirası temsil eder, bu yüzden bu pek mantıklı olmaz. Zaten hiçbir mülkü olmayan hedefte silme girişiminde bulunmasına izin verdim.
  • isExtensibleTuzak genişleme almak için bir tuzak. Bir değişmezin onu hedefle aynı genişletilebilirliği döndürmeye zorladığı göz önüne alındığında pek kullanışlı değil. Bu yüzden operasyonu genişletilebilir olan hedefe yönlendirmesine izin verdim.
  • applyVe constructtuzaklar arayarak veya başlatmasını için tuzaklar vardır. Yalnızca hedef bir işlev veya yapıcı olduğunda yararlıdırlar.

Misal

// 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
Normal ölçekli uygulamalarda bile önemli hale gelebilecek bazı performans sorunları yok mu?
Tomáš Zato - Monica'yı

1
@ TomášZato Normal bir nesnedeki veri özelliklerinden daha yavaş olacaktır, ancak erişimci özelliklerinden çok daha kötü olacağını düşünmüyorum.
Oriol

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

4
Neler olup bittiğine dair daha iyi bir fikir edinmek için "Çoklu miras" ı "Çoklu delegasyon" ile değiştirmeyi düşünürdüm. Uygulamanızdaki temel kavram, proxy'nin aslında mesajı yetkilendirmek (veya iletmek) için doğru nesneyi seçmesidir . Çözümünüzün gücü, hedef prototipleri dinamik olarak genişletebilmenizdir. Diğer cevaplar birleştirme (ala Object.assign) kullanmak veya oldukça farklı bir grafik elde etmektir, sonunda hepsi nesneler arasında yalnızca bire bir prototip zinciri elde ediyor. Proxy çözümü, bir çalışma zamanı dallanması sunar ve bu harika!
sminutoli

Performans hakkında, birden çok nesneden miras alan, birden çok nesneden miras alan vb. Bir nesne oluşturursanız, üstel hale gelecektir. Yani evet, daha yavaş olacak. Ama normal durumlarda bunun o kadar da kötü olacağını düşünmüyorum.
Oriol

16

Güncelleme (2019): Orijinal gönderi oldukça güncelliğini yitiriyor. Bu makale (artık alan adı ortadan kalktığından beri internet arşiv bağlantısı) ve ilgili GitHub kitaplığı iyi bir modern yaklaşımdır.

Orijinal gönderi: Çoklu miras [düzenleme, türün uygun mirası değil, özelliklerin mirası; javascript'teki mixins], jenerik nesneler yerine inşa edilmiş prototipler kullanırsanız oldukça basittir. İşte miras alınacak iki üst sınıf:

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

Her durumda aynı "isim" üyesini kullandığımı unutmayın; bu, ebeveynler "adın" nasıl ele alınacağı konusunda hemfikir değilse sorun olabilir. Ancak bu durumda uyumludurlar (gerçekten gereksiz).

Şimdi sadece her ikisinden de miras alan bir sınıfa ihtiyacımız var. Kalıtım tarafından yapılır çağrı prototipler ve nesne inşa edenler için (yeni bir anahtar kelime kullanmadan) yapıcı işlevini ing. İlk olarak, prototipin ana prototiplerden miras alması gerekir

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

Ve kurucu, üst kuruculardan miras almalıdır:

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

Artık farklı örnekleri büyütebilir, yiyebilir ve hasat edebilirsiniz:

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

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

Bunu yerleşik prototiplerle yapabilir misiniz? (Dizi, Dize, Sayı)
Tomáš Zato - Yeniden Etkinleştir Monica

Yerleşik prototiplerde arayabileceğiniz kurucular olduğunu sanmıyorum.
Roy J

Yapabilirim Array.call(...)ama geçtiklerimi etkilemiyor gibi görünüyor this.
Tomáš Zato - Monica'yı eski durumuna getir

@ TomášZato YapabilirsinArray.prototype.constructor.call()
Roy J

1
@AbhishekGupta Bana haber verdiğiniz için teşekkürler. Bağlantıyı, arşivlenmiş web sayfasına giden bir bağlantıyla değiştirdim.
Roy J

7

Bu Object.create, gerçek bir prototip zinciri yapmak için kullanılır:

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

Örneğin:

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

dönecek:

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

Böylece obj.a === 1, obj.b === 3vb


Hızlı bir varsayımsal soru: Number ve Array prototiplerini karıştırarak (eğlence için) Vector sınıfı yapmak istedim. Bu bana hem dizi indekslerini hem de matematik operatörlerini verecektir. Ama işe yarar mı?
Tomáš Zato - Monica'yı eski durumuna getir

@ TomášZato, dizileri alt sınıflara ayırmak istiyorsanız bu makaleyi incelemeye değer ; Size biraz baş ağrısından kurtulabilir. iyi şanslar!
user3276552

5

John Resig'in sınıf yapısı uygulamasını seviyorum: http://ejohn.org/blog/simple-javascript-inheritance/

Bu, aşağıdaki gibi bir şeye genişletilebilir:

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

    // same code
}

bu, devralınacak birden çok nesneyi geçirmenize izin verir. instanceOfBurada yeteneğinizi kaybedeceksiniz , ancak birden fazla miras istiyorsanız bu bir verili.


Yukarıdakilere ilişkin oldukça karmaşık örneğim https://github.com/cwolves/Fetch/blob/master/support/plugins/klass/klass.js adresinde mevcuttur.

Bu dosyada bazı ölü kodlar olduğunu unutmayın, ancak bir göz atmak isterseniz çoklu kalıtıma izin verir.


Zincirlenmiş miras istiyorsanız (çoklu miras DEĞİL, ancak çoğu insan için aynı şeydir), aşağıdaki gibi Sınıf ile gerçekleştirilebilir:

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

Bu, orijinal prototip zincirini koruyacak, ancak aynı zamanda birçok anlamsız kod çalıştıracaksınız.


7
Bu, birleştirilmiş sığ bir klon oluşturur. "Devralınan" nesnelere yeni bir özellik eklemek, gerçek prototip mirasında olduğu gibi yeni özelliğin türetilmiş nesnede görünmesine neden olmaz.
Daniel Earwicker

@DanielEarwicker - Doğru, ancak bir sınıfın iki sınıftan türetildiği "çoklu kalıtım" istiyorsanız, gerçekten bir alternatif yoktur. Çoğu durumda sınıfları birbirine zincirlemenin aynı şey olduğunu yansıtmak için değiştirilmiş yanıt.
Mark Kahn

Görünüşe göre GitHUb'unuz gitmiş gibi hala github.com/cwolves/Fetch/blob/master/support/plugins/klass/… hesabınız var mı? Paylaşmak isterseniz ona bakmam sorun olmaz mı?
JasonDavis

4

Çoklu kalıtımın JavaScript çerçeve uygulamalarıyla karıştırmayın.

Yapmanız gereken tek şey, belirtilen prototip nesnesi ve özellikleriyle her seferinde yeni bir nesne oluşturmak için Object.create () kullanmaktır , ardından nesnede örnek oluşturmayı planlıyorsanız yolun her adımında Object.prototype.constructor öğesini değiştirdiğinizden emin olun B. geleceği.

Kalıt örneği özellikleri için thisAve thisBkullandığımız Function.prototype.call () her nesne işlevi sonunda. Yalnızca prototipi miras almayı önemsiyorsanız, bu isteğe bağlıdır.

Aşağıdaki kodu bir yerde çalıştırın ve gözlemleyin 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 prototipi miras alır A
  • C prototipi miras alır B
  • objC bir örneği C

Bu, yukarıdaki adımların iyi bir açıklamasıdır:

OOP JavaScript'te: Bilmeniz Gerekenler


Yine de bu, tüm özellikleri yeni nesneye kopyalamıyor mu? Dolayısıyla, A ve B olmak üzere iki prototipiniz varsa ve ikisini de C üzerinde yeniden yaratırsanız, A'nın bir özelliğini değiştirmek bu özelliği C üzerinde etkilemeyecektir ve bunun tersi de geçerlidir. Bellekte depolanan A ve B'deki tüm özelliklerin bir kopyasını elde edeceksiniz. A ve B'nin tüm özelliklerini C'ye sabit kodlamanızla aynı performans olacaktır. Okunabilirlik için güzel ve özellik aramasının ana nesnelere gitmesi gerekmiyor, ancak bu gerçekten kalıtım değil - daha çok klonlama gibi. A'da bir özelliği değiştirmek, C'de klonlanan özelliği değiştirmez
Frank

2

Hiçbir şekilde javascript OOP konusunda uzman değilim, ancak sizi doğru anlarsam (sözde kod) gibi bir şey istiyorsunuz:

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

Cat inherit from (Earth, Animal);

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

Bu durumda, şöyle bir şey denerim:

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
Bu sadece ilk prototipi seçmek ve gerisini görmezden gelmek değil mi? c.prototypeBirden çok kez ayarlamak birden fazla prototip üretmez. Örneğin, olsaydı Animal.isAlive = true, Cat.isAliveyine de tanımsız olurdu.
devios1

Evet, prototipleri karıştırmak istiyordum, düzeltildi ... (Burada
jQuery'nin uzantısını


2

Bugün bunun üzerinde çok çalışıyordum ve bunu ES6'da kendim başarmaya çalışıyordum. Bunu yaptığım yol Browserify, Babel kullanıyordu ve ardından Wallaby ile test ettim ve işe yarıyor gibiydi. Amacım, mevcut Diziyi genişletmek, ES6, ES7'yi dahil etmek ve prototipte ses verileriyle uğraşmak için ihtiyaç duyduğum bazı ek özel özellikler eklemek.

Wallaby 4 testimi geçti. Example.js dosyası konsola yapıştırılabilir ve 'include' özelliğinin sınıfın prototipinde olduğunu görebilirsiniz. Bunu yarın daha fazla test etmek istiyorum.

Yöntemim şu: (Büyük ihtimalle biraz uyuduktan sonra modül olarak yeniden düzenleme ve yeniden paketleyeceğim!)

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

Bunun gülünç derecede basit olduğunu düşünüyorum. Buradaki sorun, çocuk sınıfının sadece instanceofaradığınız ilk sınıfa atıfta bulunacağıdır.

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

Çoklu miras için destek gösteren aşağıdaki kodu kontrol edin. Kullanılarak yapılan Kalıtım

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

Sınıfların çoklu kalıtımla tanımlanmasına izin verecek kadar işleve sahibim. Aşağıdaki gibi kodlara izin verir. Genel olarak, javascript'te yerel Sınıflandırma tekniklerinden tamamen ayrıldığını göreceksiniz (örneğin, classanahtar kelimeyi asla görmeyeceksiniz ):

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

bunun gibi çıktı üretmek için:

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

Sınıf tanımları şöyle görünür:

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

makeClassİşlevi kullanan her bir sınıf tanımının Object, ebeveyn sınıflarına eşlenen bir ebeveyn sınıfı adlarını kabul ettiğini görebiliriz . Ayrıca Object, tanımlanmakta olan sınıf için bir kapsayıcı özellikler döndüren bir işlevi de kabul eder . Bu işlevin bir parametresi vardırprotos üst sınıflardan herhangi biri tarafından tanımlanan herhangi bir özelliğe erişmek için yeterli bilgiyi içeren .

Gereken son parça makeClass, oldukça fazla iş yapan işlevin kendisidir. İşte kodun geri kalanıyla birlikte. makeClassOldukça ağır yorumladım :

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

makeClassFonksiyon aynı zamanda sınıf özelliklerini destekler; bunlar, özellik adlarının önüne $sembol eklenmesiyle tanımlanır (sonuçta ortaya çıkan son özellik adının $kaldırılmış olacağını unutmayın ). Bunu aklımızda tutarak, DragonEjderhanın "türünü" modelleyen özel bir sınıf yazabiliriz , burada mevcut Dragon türlerinin listesi, örnekler yerine Sınıfın kendisinde saklanır:

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

Çoklu Kalıtımın Zorlukları

Kodu makeClassyakından takip eden herhangi biri , yukarıdaki kod çalıştığında sessizce meydana gelen oldukça önemli bir istenmeyen fenomeni fark edecektir : bir örnekleme kurucuya RunningFlyingİKİ çağrı ile sonuçlanacaktır Named!

Bunun nedeni, kalıtım grafiğinin aşağıdaki gibi görünmesidir:

 (^^ More Specialized ^^)

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

  (vv More Abstract vv)

Bir alt sınıfın kalıtım grafiğinde aynı ebeveyn-sınıfa giden birden çok yol olduğunda, alt sınıfın somutlaştırmaları o üst sınıfın yapıcısını birden çok kez çağıracaktır.

Bununla mücadele etmek önemsiz değil. Basitleştirilmiş sınıf adlarıyla bazı örneklere bakalım. Dersimiz düşünün gerekir A, en soyut ebeveyn-sınıf, sınıfları Bve C, hem devralma Ave sınıf BChangi devralır gelen Bve C(ve dolayısıyla kavramsal "çift-devralır" dan 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');
  }
}));

BCÇift çağrılmayı önlemek istiyorsak, A.prototype.initmiras alınan kurucuları doğrudan çağırma tarzını terk etmemiz gerekebilir. Yinelenen aramaların olup olmadığını ve gerçekleşmeden önce kısa devre olup olmadığını kontrol etmek için bir miktar dolaylı yönlendirmeye ihtiyacımız olacak.

Özellikler işlevine sağlanan parametreleri değiştirmeyi düşünebiliriz: miras alınan özellikleri açıklayan ham verilerin yanı sıra protos, Objectbir örnek yöntemini ana yöntemlerin de çağrılacağı ancak yinelenen çağrıların algılanacağı şekilde çağırmak için bir yardımcı program işlevi de dahil edebiliriz. ve engellendi. Aşağıdakiler için parametreleri nerede oluşturduğumuza bir göz atalım 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 ... */

};

Yukarıdaki şeklindeki değişikliğin tüm amacı, çağırdığımızda makeClassbizim için ek bir argüman sağladığımızdır . Ayrıca, herhangi bir sınıfta tanımlanan her işlevin , miras alınan yöntemin çağrılmasının bir sonucu olarak zaten çağrılmış olan tüm işlevleri içeren bir ad olan diğerlerinden sonra bir parametre alabileceğinin de farkında olmalıyız :propertiesFnmakeClassdupSet

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

Bu yeni stil "Construct A", bir örneği BCbaşlatıldığında yalnızca bir kez günlüğe kaydedilmesini sağlamayı başarır . Ancak üçüncüsü çok kritik olan üç dezavantaj var :

  1. Bu kod daha az okunabilir ve bakımı yapılabilir hale geldi. util.invokeNoDuplicatesİşlevin arkasında pek çok karmaşıklık var ve bu tarzın çoklu çağrıyı nasıl önlediğini düşünmek sezgisel değil ve baş ağrısına neden oluyor. Ayrıca , sınıftaki her işlevdedups gerçekten tanımlanması gereken sinir bozucu parametreye sahibiz . Ahh.
  2. Bu kod daha yavaştır - çok sayıda kalıtımla istenen sonuçları elde etmek için biraz daha fazla dolaylı ve hesaplama gerekir. Maalesef bu, çoklu çağrı sorunumuzun herhangi bir çözümünde geçerli olacaktır.
  3. En önemlisi, kalıtıma dayanan işlevlerin yapısı çok katı hale geldi . Bir alt sınıf NiftyClassbir işlevi geçersiz kılarsaniftyFunction ve util.invokeNoDuplicates(this, 'niftyFunction', ...)onu yinelenen çağrı olmadan çalıştırmak için kullanırsa , onu tanımlayan her ana sınıfın NiftyClass.prototype.niftyFunctionadını taşıyan işlevi çağırır, niftyFunctionbu sınıflardan herhangi bir dönüş değerini yok sayar ve son olarak özel mantığını gerçekleştirir NiftyClass.prototype.niftyFunction. Mümkün olan tek yapı budur . Ve NiftyClassmiras alırsa CoolClassve GoodClassbu üst sınıfların her ikisi de niftyFunctionkendi tanımlarını sağlıyorsa NiftyClass.prototype.niftyFunction, asla (çoklu çağrı yapma riski olmadan):
    • A.NiftyClass Önce özel mantığını , ardından ebeveyn sınıflarının özel mantığını çalıştırın.
    • B. Tüm özel üst düzey mantık tamamlandıktan sonra özel mantığı NiftyClassherhangi bir noktada çalıştırın.
    • C. Üstünün özel mantığının dönüş değerlerine bağlı olarak koşullu davranır.
    • D. Belirli bir ebeveynin uzmanlığını niftyFunctiontamamen yürütmekten kaçının

Tabii ki, yukarıdaki harflerle yazılmış problemleri, aşağıdaki özel fonksiyonları tanımlayarak çözebiliriz util:

  • A. tanımlautil.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
  • B. tanımla util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)( parentNameÖzel mantığının hemen ardından alt sınıfların özel mantığı gelecek olan ebeveynin adı nerede )
  • C. tanımlautil.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...) (Bu durumdatestFn , adı geçen ebeveyn için özelleşmiş mantığın sonucunu alır ve kısa devrenin olup olmayacağını gösteren parentNamebir true/falsedeğer döndürür )
  • D. tanımla util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(Bu durumdablackListArray , özel mantığı tamamen atlanması gereken bir ana isim olabilir)

Bu çözümlerin tümü mevcuttur, ancak bu tam bir kargaşa ! Miras alınan bir işlev çağrısının alabileceği her benzersiz yapı için, altında tanımlanan özel bir yönteme ihtiyacımız olacaktır.util . Ne mutlak bir felaket.

Bunu akılda tutarak, iyi çoklu kalıtım uygulamasının zorluklarını görmeye başlayabiliriz. Tam uygulamamakeClassBu yanıtta sağladığım , çoklu çağrı problemini veya çoklu kalıtımla ilgili ortaya çıkan diğer birçok sorunu dikkate almaz.

Bu cevap çok uzuyor. Umarım makeClassdahil ettiğim uygulama, mükemmel olmasa bile yine de yararlıdır. Ayrıca bu konuyla ilgilenen herkesin daha fazla okurken akılda tutacak daha fazla bağlam kazanmasını umuyorum!


0

IeUnit paketine bir göz atın .

IeUnit'te uygulanan konsept asimilasyon, aradığınız şeyi oldukça dinamik bir şekilde sunuyor gibi görünüyor.


0

Yapıcı işlevlerini kullanan bir prototip zincirleme örneği :

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

Bu kavram, Yehuda Katz'ın JavaScript için "sınıf" tanımını kullanır :

... bir JavaScript "sınıfı" yalnızca bir yapıcı ve ekli bir prototip nesnesi olarak hizmet eden bir Function nesnesidir. ( Kaynak: Guru Katz )

Object.create yaklaşımının aksine, sınıflar bu şekilde oluşturulduğunda ve bir "sınıf" ın örneklerini yaratmak istediğimizde, her "sınıfın" neyi miras aldığını bilmemize gerek yoktur. Sadece kullanıyoruz 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

Öncülük sırası mantıklı olmalıdır. Önce örnek nesnesine bakar, sonra prototipe, ardından sonraki prototipe vb. Bakar.

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

Ayrıca, sınıfta oluşturulan tüm nesneleri etkileyecek prototipleri de değiştirebiliriz.

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

Aslında bunun bir kısmını bu cevapla yazdım .


2
OP (örn çoklu prototip zincirleri istiyor childdevralır gelen parent1ve parent2). Örneğiniz sadece bir zincirden bahsediyor.
havalı

0

Sahneye geç gelen biri SimpleDeclare . Ancak, çoklu kalıtımla uğraşırken, yine de orijinal kurucuların kopyalarıyla karşılaşacaksınız. Javascript'te bu bir zorunluluktur ...

Merc.


Javascript'te bu bir zorunluluktur ... ES6 Proxy'lerine kadar.
Jonathon

Vekiller ilginç! SimpleDeclare'yi değiştirmeye kesinlikle bakacağım, böylece standartların bir parçası haline geldiklerinde proxy'leri kullanarak yöntemleri kopyalamaya gerek kalmayacak. SimpleDeclare'nin kodunu okumak ve değiştirmek gerçekten çok kolay ...
Merc

0

Ds.oop kullanırdım . Prototype.js ve diğerlerine benzer. çoklu kalıtımı çok kolay ve minimalist hale getirir. (yalnızca 2 veya 3 kb) Ayrıca arabirimler ve bağımlılık ekleme gibi diğer bazı düzgün özellikleri de destekler

/*** 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

Buna ne dersiniz, JavaScript'te çoklu miras uygular:

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

Ve işte specialize_with () yardımcı program işlevinin kodu:

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

Bu çalışan gerçek koddur. Html dosyasına kopyalayıp yapıştırabilir ve kendiniz deneyebilirsiniz. Çalışıyor.

Bu, JavaScript'te MI uygulama çabasıdır. Fazla kod yok, daha çok teknik bilgi.

Lütfen bu konudaki makalemin tamamına bakmaktan çekinmeyin, https://github.com/latitov/OOP_MI_Ct_oPlus_in_JS


0

Sadece başkalarının özelliklerinde ihtiyacım olan sınıfları atar ve sevdiğim onlara otomatik olarak işaret etmek için bir proxy eklerdim:

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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.