JavaScriptでの複数の継承/プロトタイプ


132

JavaScriptである種の初歩的な多重継承が必要になるところまで来ました。(これが良いアイデアであるかどうかについて話し合うためにここにいるわけではないので、それらのコメントはご自分で保管してください。)

私は誰かがこれを試みて成功したかどうかを知りたいのですが、彼らはどうやってそれをやりましたか?

それを要約すると、私が本当に必要なのは、複数のプロトタイプチェーンからプロパティを継承できるオブジェクトを持つことができることです(つまり、各プロトタイプは独自の適切なチェーンを持つことができます)。チェーンを検索して、最初の定義を探します。

これがどのように理論​​的に可能であるかを示すために、プライマリチェーンの最後にセカンダリチェーンを接続することで実現できますが、これは以前のプロトタイプのすべてのインスタンスに影響し、それは私が望むものではありません。

考え?


1
dojo declareは多重継承srcを処理すると思います。mootoolsもそうだと感じています。これの多くは私を超えていますが、dojoが示唆するように、これを簡単に読むつもりです
TI

TraitsJS(リンク1リンク2)を見てください。これは、多重継承とミックスインの非常に優れた代替手段です
CMS

1
@Pointyは動的ではないためです。いずれかの親チェーンに加えられた変更が発生したときにそれを取得できるようにしたいと思います。とはいえ、それが不可能な場合は、これに頼らざるを得ないかもしれません。
devios1


1
これについて興味深いの読み取り:webreflection.blogspot.co.uk/2009/06/...
のび太

回答:


49

ECMAScript 6では、次を使用して多重継承を実現できます。 プロキシオブジェクトます

実装

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

説明

プロキシオブジェクトは、ターゲットオブジェクトといくつかのトラップで構成され、基本的な操作のカスタム動作を定義します。

別のオブジェクトから継承するオブジェクトを作成するときは、を使用しますObject.create(obj)。しかし、この場合は多重継承が必要なので、代わりにobj基本的な操作を適切なオブジェクトにリダイレクトするプロキシを使用する

私はこれらのトラップを使用します:

  • hasトラップはのためのトラップであるinオペレータsome少なくとも1つのプロトタイプにプロパティが含まれているかどうかを確認するために使用します。
  • getトラップは、プロパティ値を取得するための罠です。findそのプロパティを含む最初のプロトタイプを見つけるために使用し、値を返すか、適切なレシーバーでゲッターを呼び出します。これはによって処理されReflect.getます。プロトタイプにプロパティが含まれていない場合は、を返しundefinedます。
  • setトラップは、プロパティ値を設定するための罠です。findそのプロパティを含む最初のプロトタイプを見つけるために使用し、適切なレシーバーでセッターを呼び出します。セッターがないか、プロトタイプにプロパティが含まれていない場合、値は適切なレシーバーで定義されます。これはによって処理されReflect.setます。
  • enumerateトラップはのためのトラップであるfor...inループ。最初のプロトタイプから、次に2番目のプロトタイプから、列挙可能なプロパティを繰り返します。プロパティが反復されたら、それをハッシュテーブルに格納して、再度反復されないようにします。
    警告:このトラップはES7ドラフトでは削除されており、ブラウザでは非推奨になっています。
  • ownKeysトラップはのトラップですObject.getOwnPropertyNames()。ES7以降、for...inループは[[GetPrototypeOf]]を呼び出し、それぞれの独自のプロパティを取得し続けます。したがって、すべてのプロトタイプのプロパティを反復処理するために、このトラップを使用して、列挙可能なすべての継承プロパティを独自のプロパティのように見せています。
  • getOwnPropertyDescriptorトラップはのトラップですObject.getOwnPropertyDescriptor()。列挙可能なすべてのプロパティをownKeysトラップ内の独自のプロパティのように表示するだけでは不十分です。for...inループは、列挙可能なかどうかをチェックする記述子を取得します。そのfindため、そのプロパティを含む最初のプロトタイプを見つけるために使用し、プロパティの所有者が見つかるまでそのプロトタイプチェーンを繰り返し、その記述子を返します。プロトタイプにプロパティが含まれていない場合は、を返しundefinedます。記述子は、構成可能になるように変更されます。それ以外の場合は、プロキシの不変式を壊す可能性があります。
  • preventExtensionsそしてdefinePropertyトラップが唯一のプロキシターゲットを変更することから、これらの操作を防ぐために含まれています。そうしないと、プロキシの不変式を壊してしまう可能性があります。

使用可能なトラップが他にもありますが、使用しません

  • getPrototypeOfトラップは追加することができますが、複数のプロトタイプを返すために何の適切な方法はありません。これinstanceofはどちらも機能しないことを意味します。したがって、最初はnullであるターゲットのプロトタイプを取得します。
  • setPrototypeOfトラップを添加し、プロトタイプを置き換えることになるオブジェクトの配列を受け入れることができます。これは読者のための練習問題として残されています。ここでは、ターゲットのプロトタイプを変更させるだけです。ターゲットを使用するトラップがないため、あまり役に立ちません。
  • deletePropertyトラップは、自分のプロパティを削除するためのトラップです。プロキシは継承を表すので、これはあまり意味がありません。とにかくプロパティを持たないはずのターゲットで削除を試みます。
  • isExtensibleトラップは拡張性を得るためのトラップです。不変条件ではターゲットと同じ拡張性を強制的に返すため、あまり役に立ちません。そのため、操作をターゲットにリダイレクトさせるだけで、拡張可能になります。
  • トラップは呼び出しやインスタンス化のためのトラップです。これらは、ターゲットが関数またはコンストラクターである場合にのみ役立ちます。applyconstruct

// 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
通常の規模のアプリケーションでも関連するパフォーマンスの問題はありませんか?
トマーシュZato -復活モニカ

1
@TomášZato通常のオブジェクトのデータプロパティよりも遅くなりますが、アクセサプロパティよりもずっと悪いとは思いません。
Oriol

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

4
何が起こっているのかをよりよく理解するために、「複数の継承」を「複数の委任」に置き換えることを検討します。実装における重要な概念は、プロキシが実際に適切なオブジェクトを選択することです。 にメッセージ委任(または転送)することです。ソリューションの力は、ターゲットプロトタイプを動的に拡張できることです。他の答えは、連結(ala Object.assign)を使用するか、まったく異なるグラフを取得することです。結局、それらのすべては、オブジェクト間で唯一のプロトタイプチェーンを取得します。プロキシソリューションは、ランタイムブランチを提供します。
sminutoli 2017

パフォーマンスについては、複数のオブジェクトから継承するオブジェクト、複数のオブジェクトから継承するオブジェクトなどを作成すると、指数関数になります。だからはい、それは遅くなります。しかし、通常の場合、それはそれほど悪くはないと思います。
Oriol

16

更新(2019):元の投稿はかなり古くなっています。この記事(ドメインが廃止されたため、現在はインターネットアーカイブリンクです)とそれに関連するGitHubライブラリは、優れた最新のアプローチです。

元の投稿: 多重継承[編集、型の適切な継承ではなく、プロパティの継承。ミックスイン]ジェネリックオブジェクトのプロトタイプではなく、構築されたプロトタイプを使用する場合、Javascriptでの使用はかなり簡単です。継承元の2つの親クラスは次のとおりです。

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

それぞれのケースで同じ「name」メンバーを使用していることに注意してください。これは、「name」の処理方法について親が同意しなかった場合に問題になる可能性があります。ただし、この場合は互換性があります(実際には冗長です)。

ここで、両方から継承するクラスが必要です。継承は、プロトタイプとオブジェクトコンストラクターのコンストラクター関数(newキーワードを使用せず)を呼び出すことによって行われます。まず、プロトタイプは親プロトタイプから継承する必要があります

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

そして、コンストラクターは親コンストラクターから継承する必要があります。

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

これで、さまざまなインスタンスを成長させ、食べて、収穫することができます。

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

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

組み込みのプロトタイプでこれを行うことができますか?(配列、文字列、数値)
トマシュ・ザト-モニカを復活させる'22

組み込みのプロトタイプには、呼び出すことができるコンストラクターはないと思います。
Roy J

まあ、私はできるがArray.call(...)、それは私が渡すものに影響を与えていないようですthis
トマーシュザト-モニカを復活

@TomášZatoあなたにできることArray.prototype.constructor.call()
Roy J

1
@AbhishekGupta教えてくれてありがとう。リンクをアーカイブされたWebページへのリンクに置き換えました。
Roy J

7

これはObject.create、実際のプロトタイプチェーンを作成するために使用します。

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

例えば:

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

戻ります:

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

そのようにobj.a === 1obj.b === 3など、


ちょっとした仮説的な質問です。私は、NumberとArrayのプロトタイプを組み合わせてVectorクラスを作りたかったのです(楽しみのために)。これにより、配列のインデックスと数学演算子の両方が得られます。しかし、うまくいくでしょうか?
トマーシュZato -復活モニカ

@TomášZato、配列のサブクラス化を検討している場合は、この記事をチェックする価値があります。それはあなたにいくつかの頭痛を救うことができます。幸運を!
user3276552

5

John Resigのクラス構造の実装が好きです:http : //ejohn.org/blog/simple-javascript-inheritance/

これは単に次のようなものに拡張できます。

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

    // same code
}

これにより、継承する複数のオブジェクトを渡すことができます。instanceOfここで機能を失うことになりますが、多重継承が必要な場合は当然です。


上記の複雑な例は、https://github.com/cwolves/Fetch/blob/master/support/plugins/klass/klass.jsから入手できます

そのファイルにはいくつかのデッドコードが含まれていますが、確認したい場合は多重継承が可能です。


連鎖継承が必要な場合(多重継承ではありませんが、ほとんどの人にとってそれは同じことです)、それは次のようなクラスで実現できます。

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

これにより、元のプロトタイプチェーンが保持されますが、無意味なコードも多数実行されます。


7
これにより、マージされた浅いクローンが作成されます。「継承された」オブジェクトに新しいプロパティを追加しても、真のプロトタイプ継承の場合のように、派生オブジェクトに新しいプロパティが表示されることはありません。
Daniel Earwicker

@DanielEarwicker-True、ただし、1つのクラスが2つのクラスから派生するという「多重継承」が必要な場合、実際には代替手段はありません。クラスを単純にチェーンするだけでもほとんどの場合同じであることを反映するように回答を修正しました。
Mark Kahn

あなたのGitHubはなくなったようです github.com/cwolves/Fetch/blob/master/support/plugins/klass/...あなたが共有に気にしている場合、私はそれを見て気にしないだろうか?
JasonDavis 2017年

4

多重継承のJavaScriptフレームワーク実装と混同しないでください。

Object.create()を使用して、指定されたプロトタイプオブジェクトとプロパティで毎回新しいオブジェクトを作成する必要があります。インスタンス化を計画している場合は、方法の各ステップでObject.prototype.constructorを必ず変更してください。Bで未来。

継承インスタンスのプロパティへthisAthisB我々が使用Function.prototype.call()は、各目的関数の終わりに。プロトタイプの継承のみに関心がある場合、これはオプションです。

次のコードをどこかで実行し、観察して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 からプロトタイプを継承します A
  • C からプロトタイプを継承します B
  • objC のインスタンスです C

これは、上記のステップの適切な説明です。

JavaScriptのOOP:知っておくべきこと


ただし、これはすべてのプロパティを新しいオブジェクトにコピーしませんか?したがって、2つのプロトタイプAとBがあり、それらをCで両方再作成する場合、Aのプロパティを変更しても、Cのそのプロパティには影響がなく、その逆も同様です。AとBのすべてのプロパティのコピーがメモリに保存されます。これは、AとBのすべてのプロパティをCにハードコーディングした場合と同じパフォーマンスになります。読みやすく、プロパティルックアップは親オブジェクトに移動する必要はありませんが、継承ではなく、クローンのようなものです。Aのプロパティを変更すると、Cにクローン化されたプロパティは変更されません
フランク・

2

私は決してJavaScript OOPの専門家ではありませんが、私が正しく理解していれば、(擬似コード)のようなものが必要です。

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

Cat inherit from (Earth, Animal);

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

その場合、私は次のようなことを試します:

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
これは最初のプロトタイプを選び、残りを無視するだけではないですか?c.prototype複数回設定しても、複数のプロトタイプは生成されません。たとえば、があったAnimal.isAlive = trueとしてCat.isAliveも、未定義になります。
devios1

ええ、私はプロトタイプを混ぜるつもりでした、修正しました...(ここではjQueryの拡張機能を使用しましたが、画像が表示されます)
David Hellsing

2

JavaScriptで多重継承を実装することは可能ですが、それを行うライブラリはほとんどありません。

私が知っている唯一の例であるRing.jsを指すことができます。


2

今日私はこれに多くの作業をしていて、ES6でこれを自分で達成しようと努めていました。私がやった方法は、Browserify、Babelを使用していて、それをWallabyでテストしたところ、動作するようでした。私の目標は、現在の配列を拡張し、ES6、ES7を含め、オーディオデータを処理するためにプロトタイプに必要な追加のカスタム機能を追加することです。

ワラビーは私のテストのうち4つに合格しています。example.jsファイルをコンソールに貼り付けると、 'includes'プロパティがクラスのプロトタイプにあることがわかります。明日はもっとテストしたい。

これが私の方法です:(私はほとんどの場合、ある程度の睡眠の後、モジュールとしてリファクタリングして再パッケージします!)

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レポ:https : //github.com/danieldram/array-includes-polyfill


2

とんでもないほど簡単だと思います。ここでの問題は、子クラスがinstanceof呼び出す最初のクラスのみを参照することです

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

多重継承のサポートを示している以下のコードを確認してください。PROTOTYPAL INHERITANCEを使用して実行

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

私は、クラスを多重継承で定義できるようにするかなりの機能を持っています。次のようなコードが可能です。全体的に、JavaScriptのネイティブの分類手法からの完全な逸脱に気付くでしょう(たとえば、classキーワードが表示されることはありません)。

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

このような出力を生成するには:

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

クラス定義は次のようになります。

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関数を使用する各クラス定義は、Object親クラスにマップされた親クラスの名前を受け入れることがわかります。またObject、定義されているクラスの包含プロパティを返す関数も受け入れます。この関数にはパラメータがありますprotos、親クラスのいずれかによって定義されたプロパティにアクセスするための十分な情報を含むがあります。

最後に必要なのは、makeClassかなりの作業を行う関数自体です。ここに、残りのコードがあります。私はmakeClassかなりたくさんコメントしました:

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

makeClassこの関数は、クラスのプロパティをサポートしています。これらは、プロパティ名の前に$記号を付けることによって定義されます(結果の最終的なプロパティ名は$削除されることに注意してください)。これを念頭に置いて、Dragonドラゴンの「タイプ」をモデル化する特殊なクラスを作成できます。使用可能なドラゴンタイプのリストは、インスタンスではなくクラス自体に格納されます。

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

多重継承の課題

makeClass上記のコードを実行すると、コードを注意深くフォローした人なら誰でも、サイレントに発生するかなり重大な望ましくない現象に気付くでしょうインスタンス化するRunningFlyingと、Namedコンストラクターが2回呼び出されます。

これは、継承グラフが次のようになるためです。

 (^^ More Specialized ^^)

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

  (vv More Abstract vv)

サブクラスの継承グラフ同じ親クラスへの複数のパスがある場合、サブクラスのインスタンス化により、その親クラスのコンストラクターが複数回呼び出されます。

これと戦うことは簡単ではありません。簡略化されたクラス名を使用した例をいくつか見てみましょう。私たちは、クラスを検討しましょうA、最も抽象親クラス、クラスBC、両方から継承Aし、クラスをBCどの継承からBC(したがって、概念的に「ダブル・継承」から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二重呼び出しA.prototype.initを防ぐには、継承されたコンストラクターを直接呼び出すスタイルを放棄する必要がある場合があります。重複した呼び出しが発生しているかどうかを確認し、発生する前に短絡するために、ある程度の間接参照が必要になります。

プロパティ関数に提供されるパラメーターを変更することを検討することができます:継承されたプロパティを説明する生データを含むとともにprotosObject親メソッドも呼び出されるようにインスタンスメソッドを呼び出すためのユーティリティ関数を含めることもできますが、重複した呼び出しが検出されますと防止。のパラメーターを設定する場所を見てみましょう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 ... */

};

上記の変更の目的は、を呼び出すときにmakeClass追加の引数が提供されるようpropertiesFnにすることmakeClassです。また、任意のクラスで定義されたすべての関数dupは、Setと名付けられた他のすべての後にパラメータを受け取る可能性があることにも注意してください。

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

この新しいスタイルは"Construct A"、のインスタンスBCが初期化されるときに一度だけログに記録されることを確実に成功させます。しかし、3つの欠点があります。3番目の欠点は非常に重要です。

  1. このコードは、可読性と保守性が低下しています。多くの複雑さがutil.invokeNoDuplicates関数の背後に隠れており、このスタイルがマルチ呼び出しを回避する方法について考えることは非直感的で頭痛の種になります。また、その厄介なdupsパラメータもあります。これは、クラス内のすべての単一の関数で実際に定義する必要があります。痛い。
  2. このコードは低速です。多重継承で望ましい結果を得るには、かなり多くの間接参照と計算が必要です。残念ながら、これはの場合である可能性が高い任意の私たちの複数の呼び出しの問題を解決。
  3. 最も重要なのは、継承に依存する関数の構造が非常に厳格になったことです。サブクラスNiftyClassが関数をオーバーライドする場合niftyFunctionutil.invokeNoDuplicates(this, 'niftyFunction', ...)重複呼び出しなしでそれを実行するために使用する場合、はそれを定義するすべての親クラスのNiftyClass.prototype.niftyFunction名前付き関数を呼び出し、niftyFunctionそれらのクラスからの戻り値を無視して、最後にの特殊なロジックを実行しNiftyClass.prototype.niftyFunctionます。これが唯一可能な構造です。とをNiftyClass継承CoolClassGoodClass、これらの親クラスの両方niftyFunctionが独自の定義を提供する場合、NiftyClass.prototype.niftyFunction(複数呼び出しのリスクがない限り)次のことはできません。
    • A.NiftyClass最初に専用ロジックを実行し、次に親クラスの専用ロジックを実行する
    • B.すべての特殊化された親ロジックが完了したNiftyClass以外の任意の時点で特殊化されたロジックを実行する
    • C.親の専用ロジックの戻り値に応じて条件付きで動作する
    • D.特定の親に特化したものをniftyFunction完全に実行しない

もちろん、以下の特殊な関数を定義することにより、上記の各文字の問題を解決できますutil

  • A.定義するutil.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
  • B.定義util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)(ここparentNameで、特殊化されたロジックの直後に子クラスの特殊化されたロジックが続く親の名前)
  • C. 定義するutil.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(この場合testFn、という名前の親の特殊なロジックの結果を受け取り、短絡が発生するかどうかを示す値をparentName返しtrue/falseます)
  • D.定義util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(この場合blackListArray、特別なロジックを完全にスキップする必要がある親の名前になります)

これらのソリューションはすべて利用可能です。 が、これは完全な騒動です!継承された関数呼び出しが取ることができるすべての一意の構造について、で定義された特殊なメソッドが必要になりますutil。なんと絶対的な災害でしょう。

これを念頭に置いて、適切な多重継承を実装する際の課題を見始めることができます。makeClassこの回答で提供した完全な実装では、複数呼び出しの問題や、多重継承に関して発生する他の多くの問題も考慮されていません。

この答えは非常に長くなっています。私がmakeClass含めた実装は、完全ではない場合でも、まだ有用であることを願っています。また、このトピックに関心のある方は、本を読んでいる間、心に留めておくためのより多くのコンテキストを得ていただければ幸いです。



0

次に、コンストラクター関数を使用したプロトタイプチェーンの例を示します

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

このコンセプトでは、Yehuda Katz によるJavaScriptの「クラス」の定義を使用しています。

... JavaScriptの「クラス」は、コンストラクタに加えて、接続されたプロトタイプオブジェクトとして機能する単なるFunctionオブジェクトです。(出典:Guru Katz

Object.createのアプローチとは異なり、クラスがこの方法で構築され、「クラス」のインスタンスを作成する場合、各「クラス」が何から継承されているかを知る必要はありません。ただ使っています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

優先順位は意味があります。最初にインスタンスオブジェクト、次にプロトタイプ、次に次のプロトタイプなどを調べます。

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

また、クラスで構築されたすべてのオブジェクトに影響を与えるプロトタイプを変更することもできます。

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

私はもともとこの答えでこれのいくつかを書きました。


2
OPは複数のプロトタイプチェーンを求めています(たとえばchildparent1およびから継承parent2)。あなたの例は1つのチェーンについてのみ話します。
最高級の

0

シーンの後発はSimpleDeclareです。です。ただし、多重継承を処理する場合でも、元のコンストラクタのコピーが残ります。それはJavascriptで必要です...

Merc。


ES6プロキシまでは、Javascriptでそれが必要です。
Jonathon 2015

プロキシは面白いです!SimpleDeclareを変更することは間違いなく検討するので、プロキシが標準の一部になったときに、プロキシを使用してメソッドをコピーする必要はありません。SimpleDeclareのコードは非常に読みやすく、変更も簡単です...
Merc

0

私はds.oopを使用します。これは、prototype.jsなどに似ています。多重継承を非常に簡単にし、そのミニマリストにします。(2または3 kbのみ)インターフェースや依存性注入など、他のいくつかのきちんとした機能もサポートします

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

これについては、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();

そして、これがspecialize_with()ユーティリティ関数のコードです:

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

これは実行される実際のコードです。あなたはそれをhtmlファイルにコピー&ペーストして、それを自分で試すことができます。それは動作します。

それがJavaScriptでMIを実装するための取り組みです。コードの多くではなく、ノウハウの多く。

これに関する私の完全な記事、https://github.com/latitov/OOP_MI_Ct_oPlus_in_JSを自由に見てください。


0

私は他の人のプロパティで必要なクラスを割り当てるために使用しました、そして私が好きなそれらに自動ポイントするプロキシを追加します:

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.