web-dev-qa-db-fra.com

Classe ES6 Héritage multiple

J'ai effectué la plupart de mes recherches à ce sujet sur BabelJS et sur MDN (qui ne contient aucune information), mais n'hésitez pas à me dire si je n'ai pas été assez attentif pour plus d’informations sur la ES6 Spec.

Je me demande si ES6 prend en charge l'héritage multiple de la même manière que les autres langages typés. Par exemple, puis-je faire quelque chose comme:

class Example extends ClassOne, ClassTwo {
    constructor() {
    }
}

étendre plusieurs classes à la nouvelle classe? Si tel est le cas, l'interprète préférera-t-il les méthodes/propriétés de ClassTwo à ClassOne?

88
BTC

Un objet ne peut avoir qu'un seul prototype. Vous pouvez hériter de deux classes en créant un objet parent en combinant deux prototypes parent.

La syntaxe de sous-classement permet de le faire dans la déclaration, car le membre de droite de la clause extends peut être n’importe quelle expression. Ainsi, vous pouvez écrire une fonction combinant des prototypes selon les critères de votre choix et appeler cette fonction dans la déclaration de classe.

51
Pointy

Vérifiez mon exemple ci-dessous, la méthode super fonctionne comme prévu. Utiliser quelques astuces même instanceof fonctionne (la plupart du temps):

// base class
class A {  
  foo() {
    console.log(`from A -> inside instance of A: ${this instanceof A}`);
  }
}

// B mixin, will need a wrapper over it to be used
const B = (B) => class extends B {
  foo() {
    if (super.foo) super.foo(); // mixins don't know who is super, guard against not having the method
    console.log(`from B -> inside instance of B: ${this instanceof B}`);
  }
};

// C mixin, will need a wrapper over it to be used
const C = (C) => class extends C {
  foo() {
    if (super.foo) super.foo(); // mixins don't know who is super, guard against not having the method
    console.log(`from C -> inside instance of C: ${this instanceof C}`);
  }
};

// D class, extends A, B and C, preserving composition and super method
class D extends C(B(A)) {  
  foo() {
    super.foo();
    console.log(`from D -> inside instance of D: ${this instanceof D}`);
  }
}

// E class, extends A and C
class E extends C(A) {
  foo() {
    super.foo();
    console.log(`from E -> inside instance of E: ${this instanceof E}`);
  }
}

// F class, extends B only
class F extends B(Object) {
  foo() {
    super.foo();
    console.log(`from F -> inside instance of F: ${this instanceof F}`);
  }
}

// G class, C wrap to be used with new decorator, pretty format
class G extends C(Object) {}

const inst1 = new D(),
      inst2 = new E(),
      inst3 = new F(),
      inst4 = new G(),
      inst5 = new (B(Object)); // instance only B, ugly format

console.log(`Test D: extends A, B, C -> outside instance of D: ${inst1 instanceof D}`);
inst1.foo();
console.log('-');
console.log(`Test E: extends A, C -> outside instance of E: ${inst2 instanceof E}`);
inst2.foo();
console.log('-');
console.log(`Test F: extends B -> outside instance of F: ${inst3 instanceof F}`);
inst3.foo();
console.log('-');
console.log(`Test G: wraper to use C alone with "new" decorator, pretty format -> outside instance of G: ${inst4 instanceof G}`);
inst4.foo();
console.log('-');
console.log(`Test B alone, ugly format "new (B(Object))" -> outside instance of B: ${inst5 instanceof B}, this one fails`);
inst5.foo();

Va imprimer 

 Test D: étend A, B, C -> instance extérieure de D: true 
 De A -> instance interne de A: true 
 De B -> instance interne de B: true 
 De C -> instance interne de C: true 
 De D -> instance interne de D: true 
-
 Test E: étend A, C -> instance externe de E: true 
 De A - > instance interne de A: true 
 de C -> instance interne de C: true 
 de E -> instance interne de E: true 
-
 Test F: étend B -> instance externe de F: true 
 De B -> instance interne de B: true 
 De F -> instance interne de F: true 
-
 Tester G: wraper pour utiliser C seul avec le "nouveau" décorateur, joli format -> instance externe de G: true 
 de C -> instance interne de C: true 
-
 Testez B seul, format moche "nouveau (B (Object))" -> instance externe de B: false, celui-ci échoue 
 De B -> instance interne de B: true 

Lien vers le violon

62
Poelinca Dorin

L'implémentation de Sergio Carneiro et Jon vous oblige à définir une fonction d'initialisation pour toutes les classes sauf une. Voici une version modifiée de la fonction d'agrégation, qui utilise plutôt les paramètres par défaut dans les constructeurs. Inclus sont aussi quelques commentaires par moi.

var aggregation = (baseClass, ...mixins) => {
    class base extends baseClass {
        constructor (...args) {
            super(...args);
            mixins.forEach((mixin) => {
                copyProps(this,(new mixin));
            });
        }
    }
    let copyProps = (target, source) => {  // this function copies all properties and symbols, filtering out some special ones
        Object.getOwnPropertyNames(source)
              .concat(Object.getOwnPropertySymbols(source))
              .forEach((prop) => {
                 if (!prop.match(/^(?:constructor|prototype|arguments|caller|name|bind|call|apply|toString|length)$/))
                    Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop));
               })
    }
    mixins.forEach((mixin) => { // outside contructor() to allow aggregation(A,B,C).staticFunction() to be called etc.
        copyProps(base.prototype, mixin.prototype);
        copyProps(base, mixin);
    });
    return base;
}

Voici une petite démo:

class Person{
   constructor(n){
      this.name=n;
   }
}
class Male{
   constructor(s='male'){
      this.sex=s;
   }
}
class Child{
   constructor(a=12){
      this.age=a;
   }
   tellAge(){console.log(this.name+' is '+this.age+' years old.');}
}
class Boy extends aggregation(Person,Male,Child){}
var m = new Boy('Mike');
m.tellAge(); // Mike is 12 years old.

Cette fonction d'agrégation préférera les propriétés et les méthodes d'une classe qui apparaîtront plus tard dans la liste des classes.

15
Chong Lip Phang

Ce n'est pas vraiment possible avec la manière dont fonctionne l'héritage prototype. Voyons comment fonctionnent les accessoires hérités dans js

var parent = {a: function() { console.log('ay'); }};
var child = Object.create(parent);
child.a() // first look in child instance, nope let's go to it's prototype
          // then look in parent, found! return the method

voyons ce qui se passe lorsque vous accédez à un accessoire qui n'existe pas:

child.b; // first look in child instance, nope let's go to it's prototype
         // then look in parent, nope let's go to it's prototype
         // then look in Object.prototype, nope let's go to it's prototype
         // then look at null, give up and return undefined

Vous pouvez utiliser mixins pour obtenir certaines de ces fonctionnalités, mais vous n'obtiendrez pas de liaison tardive:

var a = {x: '1'};
var b = {y: '2'};
var c = createWithMixin([a, b]);
c.x; // 1
c.y; // 2
b.z = 3;
c.z; // undefined

contre

var a = {x: 1}
var o = Object.create(a);
o.x; // 1
a.y = 2;
o.y; // 2
8
qwertymk

Justin Fagnani décrit une manière très simple (imho) de composer plusieurs classes en une seule en utilisant le fait que dans ES2015, les classes peuvent être créées avec la classe expressions.

Expressions vs déclarations

En gros, comme vous pouvez créer une fonction avec une expression: 

function myFunction() {}      // function declaration
var myFunction = function(){} // function expression

vous pouvez faire la même chose avec les classes:

class MyClass {}             // class declaration
var MyClass = class {}       // class expression

L'expression est évaluée au moment de l'exécution, à l'exécution du code, tandis qu'une déclaration est exécutée au préalable.

Utiliser des expressions de classe pour créer des mixins

Vous pouvez utiliser ceci pour créer une fonction qui crée dynamiquement une classe uniquement lorsque la fonction est appelée:

function createClassExtending(superclass) {
  return class AwesomeClass extends superclass {
    // you class body here as usual
  }
}

La bonne chose à ce sujet est que vous pouvez définir la classe entière à l’avance et décider uniquement de la classe à étendre au moment où vous appelez la fonction:

class A {}
class B {}
var ExtendingA = createClassExtending(A)
var ExtendingB = createClassExtending(B)

Si vous souhaitez mélanger plusieurs classes, les classes ES6 ne prenant en charge que l'héritage unique, vous devez créer une chaîne de classes contenant toutes les classes que vous souhaitez mélanger. Alors disons que vous voulez créer une classe C qui étend A et B, vous pouvez faire ceci:

class A {}
class B extends A {}
class C extends B {}  // C extends both A and B

Le problème, c'est que c'est très statique. Si vous décidez plus tard de créer une classe D qui étend B mais pas A, vous avez un problème.

Mais avec quelques astuces intelligentes utilisant le fait que les classes peuvent être des expressions, vous pouvez résoudre ce problème en créant A et B pas directement en tant que classes, mais en tant que fabriques de classes (en utilisant des fonctions de flèche par souci de concision):

class Base {} // some base class to keep the arrow functions simple
var A = (superclass) => class A extends superclass
var B = (superclass) => class B extends superclass
var C = B(A(Base))
var D = B(Base)

Remarquez comme nous ne décidons qu'au dernier moment quelles classes inclure dans la hiérarchie.

Aidez-nous à construire l'avenir!

Bien sûr, si vous êtes comme moi, cela vous incite à construire la bibliothèque ultime pour l'héritage multiple en Javascript. Si vous le souhaitez, aidez-moi à faire exactement cela! Découvrez ce projet et aidez-vous si vous le pouvez!

micros

mics (prononcer: mix) est une bibliothèque qui simplifie l'héritage multiple en Javascript. Inspiré par l'excellent article de blog «Real» Mixins avec les classes Javascript de Justin Fagnani, mics tente de créer une bibliothèque minimale autour du concept d'utilisation d'expressions de classe (usines) en tant que mixines. mics étend les concepts présentés dans l'article de blog en faisant des mixins citoyens de première classe qui peuvent être utilisés directement pour instancier des objets et peuvent être mélangés avec d'autres mixins plutôt qu'avec des classes uniquement.

8
Stijn de Witt

Depuis la page es6-features.org/#ClassInheritanceFromExpressions , il est possible d'écrire une fonction d'agrégation pour autoriser l'héritage multiple

class Rectangle étend l'agrégation (Shape, Colored, ZCoord) {}

var aggregation = (baseClass, ...mixins) => {
    let base = class _Combined extends baseClass {
        constructor (...args) {
            super(...args)
            mixins.forEach((mixin) => {
                mixin.prototype.initializer.call(this)
            })
        }
    }
    let copyProps = (target, source) => {
        Object.getOwnPropertyNames(source)
            .concat(Object.getOwnPropertySymbols(source))
            .forEach((prop) => {
            if (prop.match(/^(?:constructor|prototype|arguments|caller|name|bind|call|apply|toString|length)$/))
                return
            Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop))
        })
    }
    mixins.forEach((mixin) => {
        copyProps(base.prototype, mixin.prototype)
        copyProps(base, mixin)
    })
    return base
}

Mais cela est déjà fourni dans des bibliothèques commeaggregation .

2
Sergio Carneiro

Je viens avec ces solutions:

'use strict';

const _         = require( 'lodash' );

module.exports  = function( ParentClass ) {

    if( ! ParentClass ) ParentClass = class {};

    class AbstractClass extends ParentClass {
        /**
         * Constructor
        **/
        constructor( configs, ...args ) {
            if ( new.target === AbstractClass )
                throw new TypeError( "Cannot construct Abstract instances directly" );

            super( args );

            if( this.defaults === undefined )
                throw new TypeError( new.target.name + " must contain 'defaults' getter" );

            this.configs = configs;
        }
        /**
         * Getters / Setters
        **/
        // Getting module configs
        get configs() {
            return this._configs;
        }
        // Setting module configs
        set configs( configs ) {
            if( ! this._configs ) this._configs = _.defaultsDeep( configs, this.defaults );
        }
    }

    return AbstractClass;
}

usage:

const EventEmitter  = require( 'events' );
const AbstractClass = require( './abstracts/class' )( EventEmitter );

class MyClass extends AbstractClass {
    get defaults() {
        return {
            works: true,
            minuses: [
                'u can have only 1 class as parent wich was\'t made by u',
                'every othere classes should be your\'s'
            ]
        };
    }
}

Tant que vous faites ce tour avec vos classes écrites sur mesure, vous pouvez les enchaîner. mais dès que vous voulez étendre une fonction/classe écrite pas comme ça - vous n’aurez plus aucune chance de continuer en boucle.

const EventEmitter  = require( 'events' );
const A = require( './abstracts/a' )(EventEmitter);
const B = require( './abstracts/b' )(A);
const C = require( './abstracts/b' )(B);

fonctionne pour moi dans le noeud v5.4.1 avec --harmony flag

2
Maikal

Bien Object.assign vous donne la possibilité de faire quelque chose de proche, mais un peu plus comme une composition avec des classes ES6. 

class Animal {
    constructor(){ 
     Object.assign(this, new Shark()) 
     Object.assign(this, new Clock()) 
  }
}

class Shark {
  // only what's in constructor will be on the object, ence the weird this.bite = this.bite.
  constructor(){ this.color = "black"; this.bite = this.bite }
  bite(){ console.log("bite") }
  eat(){ console.log('eat') }
}

class Clock{
  constructor(){ this.tick = this.tick; }
  tick(){ console.log("tick"); }
}

let animal = new Animal();
animal.bite();
console.log(animal.color);
animal.tick();

Je n'ai pas vu cela utilisé nulle part, mais c'est en fait très utile. Vous pouvez utiliser function shark(){} au lieu de la classe, mais utiliser la classe présente des avantages.

Je crois que la seule différence avec l'héritage avec le mot clé extend est que la fonction ne vit pas uniquement sur la variable prototype, mais également sur l'objet lui-même.

Donc maintenant, quand vous faites new Shark(), la shark créée a une méthode bite, alors que seul son prototype a une méthode eat

1
Ced

utilisez Mixins pour l'héritage multiple ES6.

let classTwo = Base => class extends Base{
    // ClassTwo Code
};

class Example extends classTwo(ClassOne) {
    constructor() {
    }
}
1
No8

Cette solution ES6 a fonctionné pour moi:

multiple-héritage.js

export function allOf(BaseClass, ...Mixins) {

  function copyProperties(target, source) {
    const allPropertyNames = Object.getOwnPropertyNames(source).concat(Object.getOwnPropertySymbols(source))

    allPropertyNames.forEach((propertyName) => {
      if (propertyName.match(/^(?:constructor|prototype|arguments|caller|name|bind|call|apply|toString|length)$/))
        return
      Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName))
    })
  }

  class Base extends BaseClass
  {
    constructor (...args) {
      super(...args)

      Mixins.forEach((Mixin) => {
        copyProperties(this, new Mixin(...args))
      })
    }
  }

  Mixins.forEach((mixin) => {
    copyProperties(Base.prototype, Mixin.prototype)
  })

  return Base
}

main.js

import { allOf } from "./multiple-inheritance.js"

class A
{
    constructor(name) {
        this.name = name
    }
    sayA() {
        return this.name
    }
}

class B
{
    constructor(name) {
        this.name = name
    }
    sayB() {
        return this.name
    }
}

class AB extends allOf(A, B)
{
    sayAB() {
        return this.name
    }
}

const ab = new AB("ab")
console.log("ab.sayA() = "+ab.sayA()+", ab.sayB() = "+ab.sayB()+", ab.sayAB() = "+ab.sayAB())

Rendements sur la console du navigateur:

ab.sayA() = ab, ab.sayB() = ab, ab.sayAB() = ab
1
user2006754

Il n'y a pas de moyen facile de faire l'héritage de plusieurs classes. Je suis l'association de l'héritage pour associer ce type de comportement.

    class Person {
        constructor(firstname, lastname, age){
            this.firstname = firstname,
            this.lastname = lastname
            this.Age = age
        }

        fullname(){
                return this.firstname +" " + this.lastname;
            } 
    }

    class Organization {
        constructor(orgname){
            this.orgname = orgname;
        }
    }

    class Employee extends Person{
        constructor(firstname, lastname, age,id) {
            super(firstname, lastname, age);
            this.id = id;
        }

    }
    var emp = new Employee("John", "Doe", 33,12345);
    Object.assign(emp, new Organization("Innovate"));
    console.log(emp.id);
    console.log(emp.orgname);
    console.log(emp.fullname());

J'espère que c'est utile.

1
AnandShanbhag

ça marche pour moi !! (2019)

class Example extends (ClassOne, ClassTwo)
{
    constructor() 
    {
    }
}
1
Iván Rodríguez

Je vais aussi ajouter ma solution - je l’ai trouvée la plus conviviale de ce que j’ai lu dans ce fil.

export const aggregate = (...mixins) => (Base) => {
  const copyProps = (target, source) => {
    Object.getOwnPropertyNames(source)
      .concat(Object.getOwnPropertySymbols(source))
      .forEach((prop) => {
        if (prop.match(/^(?:constructor|prototype|arguments|caller|name|bind|call|apply|toString|length)$/)) {
          return;
        }
        Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop));
      });
  };
  mixins.forEach((mixin) => {
    copyProps(Base, mixin);
    copyProps(Base.prototype, mixin.prototype);
  });
  return Base;
};

Vous pouvez l'utiliser ensuite comme ceci:

class _MyBaseClass {}
const MyBaseClass = aggregate(ExtensionOne, ExtensionTwo)(_MyBaseClass);
1
Ancinek

Pour prouver le concept, j'ai effectué la fonction suivante. Il prend une liste de classes et les compose dans une nouvelle classe (le dernier prototype gagne afin d'éviter tout conflit). Lors de la création d'une fonction composée, l'utilisateur peut choisir d'utiliser tous les constructeurs d'origine [sic!] ou de transmettre leurs propres. C'était le plus gros défi de cette expérience: trouver une description de ce que le constructeur devrait faire. La copie de méthodes dans un prototype n’est pas un problème, mais quelle est la logique voulue pour un objet nouvellement composé? Ou peut-être que cela devrait être sans constructeur? En Python, d'après ce que je sais, il trouve le constructeur matching, mais les fonctions de JS sont plus acceptables. Par conséquent, on peut passer à une fonction à peu près tout et la signature ne sera pas claire.

Je ne pense pas que ce soit optimisé, mais le but était d'explorer les possibilités. instanceof ne se comportera pas comme prévu, ce qui, je suppose, est une déception, car les développeurs orientés vers les classes aiment l'utiliser comme un outil.

Peut-être que JavaScript ne l'a tout simplement pas.

/*
    (c) Jon Krazov 2019

    Below is an experiment searching boundaries of JavaScript.
    It allows to compute one class out of many classes.

    Usage 1: Without own constructor

    If no constructor is passed then constructor of each class will be called
    with params passed in object. In case of missing params, constructor
    will be called without params.

    Example:

    const MyClass1 = computeClass([Class1, Class2, Class3]);
    const myClass1Instance = new MyClass1({
        'Class1': [1, 2],
        'Class2': ['test'],
        'Class3': [(value) => value],
    });

    Usage 2: With own constructor

    If constructor is passed in options object (second param) then it will
    be called in place of constructors of all classes.

    Example:

    const MyClass2 = computeClass([Class1, Class2, Class3], {
        ownConstructor(param1) {
            this.name = param1;
        }
    });
    const myClass2Instance = new MyClass2('Geoffrey');
*/

// actual function

var computeClass = (classes = [], { ownConstructor = null } = {}) => {
    const noConstructor = (value) => value != 'constructor';

    const ComputedClass = ownConstructor === null
        ? class ComputedClass {
            constructor(args) {
                classes.forEach((Current) => {
                    const params = args[Current.name];

                    if (params) {
                        Object.assign(this, new Current(...params));
                    } else {
                        Object.assign(this, new Current());
                    }
                })
            }
        }
        : class ComputedClass {
            constructor(...args) {
                if (typeof ownConstructor != 'function') {
                    throw Error('ownConstructor has to be a function!');
                }
                ownConstructor.call(this, ...args);
            } 
        };

    const prototype = classes.reduce(
        (composedPrototype, currentClass) => {
            const partialPrototype = Object.getOwnPropertyNames(currentClass.prototype)
                .reduce(
                    (result, propName) =>
                        noConstructor(propName)
                            ? Object.assign(
                                    result,
                                    { [propName]: currentClass.prototype[propName] }
                                )
                            : result,
                    {}
                );

            return Object.assign(composedPrototype, partialPrototype);
        },
        {}
    );

    Object.entries(prototype).forEach(([prop, value]) => {
        Object.defineProperty(ComputedClass.prototype, prop, { value });
    });
    
    return ComputedClass;
}

// demo part

var A = class A {
    constructor(a) {
        this.a = a;
    }
    sayA() { console.log('I am saying A'); }
}

var B = class B {
    constructor(b) {
        this.b = b;
    }
    sayB() { console.log('I am saying B'); }
}

console.log('class A', A);
console.log('class B', B);

var C = computeClass([A, B]);

console.log('Composed class');
console.log('var C = computeClass([A, B]);', C);
console.log('C.prototype', C.prototype);

var c = new C({ A: [2], B: [32] });

console.log('var c = new C({ A: [2], B: [32] })', c);
console.log('c instanceof A', c instanceof A);
console.log('c instanceof B', c instanceof B);

console.log('Now c will say:')
c.sayA();
c.sayB();

console.log('---');

var D = computeClass([A, B], {
    ownConstructor(c) {
        this.c = c;
    }
});

console.log(`var D = computeClass([A, B], {
    ownConstructor(c) {
        this.c = c;
    }
});`);

var d = new D(42);

console.log('var d = new D(42)', d);

console.log('Now d will say:')
d.sayA();
d.sayB();

console.log('---');

var E = computeClass();

console.log('var E = computeClass();', E);

var e = new E();

console.log('var e = new E()', e);

Initialement posté ici (Gist.github.com).

0
The Witness

utilisez Extent avec une fonction personnalisée pour gérer plusieurs héritages avec es6

var aggregation = (baseClass, ...mixins) => {
    let base = class _Combined extends baseClass {
        constructor (...args) {
            super(...args)
            mixins.forEach((mixin) => {
                mixin.prototype.initializer.call(this)
            })
        }
    }
    let copyProps = (target, source) => {
        Object.getOwnPropertyNames(source)
            .concat(Object.getOwnPropertySymbols(source))
            .forEach((prop) => {
            if (prop.match(/^(?:constructor|prototype|arguments|caller|name|bind|call|apply|toString|length)$/))
                return
            Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop))
        })
    }
    mixins.forEach((mixin) => {
        copyProps(base.prototype, mixin.prototype)
        copyProps(base, mixin)
    })
    return base
}

class Colored {
    initializer ()     { this._color = "white" }
    get color ()       { return this._color }
    set color (v)      { this._color = v }
}

class ZCoord {
    initializer ()     { this._z = 0 }
    get z ()           { return this._z }
    set z (v)          { this._z = v }
}

class Shape {
    constructor (x, y) { this._x = x; this._y = y }
    get x ()           { return this._x }
    set x (v)          { this._x = v }
    get y ()           { return this._y }
    set y (v)          { this._y = v }
}

class Rectangle extends aggregation(Shape, Colored, ZCoord) {}

var rect = new Rectangle(7, 42)
rect.z     = 1000
rect.color = "red"
console.log(rect.x, rect.y, rect.z, rect.color)

0
Jon