web-dev-qa-db-fra.com

Emulate Super en javascript

Existe-t-il un bon mécanisme élégant pour émuler super avec une syntaxe aussi simple que celle décrite ci-dessous?

  • this.$super.prop() 
  • this.$super.prop.apply(this, arguments);

Les critères à respecter sont:

  1. this.$super doit être une référence au prototype. c'est-à-dire que si je change le super prototype au moment de l'exécution, ce changement sera pris en compte. Cela signifie fondamentalement que si le parent a une nouvelle propriété, celle-ci devrait être affichée au moment de l'exécution sur tous les enfants jusqu'à super, tout comme une référence codée en dur au parent refléterait les changements.
  2. this.$super.f.apply(this, arguments); doit fonctionner pour les appels récursifs. Pour tout ensemble d'héritage en chaîne dans lequel plusieurs super appels sont effectués au fur et à mesure que vous montez dans la chaîne d'héritage, vous ne devez pas résoudre le problème récursif.
  3. Vous ne devez pas coder en dur les références aux super objets de vos enfants. C'est à dire. Base.prototype.f.apply(this, arguments); défait le point.
  4. Vous ne devez pas utiliser de compilateur X to JavaScript ni de préprocesseur JavaScript.
  5. Doit être conforme à ES5

La mise en œuvre naïve serait quelque chose comme ça.

var injectSuper = function (parent, child) {
  child.prototype.$super = parent.prototype;
};

Mais ceci casse la condition 2 .

Le mécanisme le plus élégant que j'ai vu à ce jour est eval hack d'IvoWetzel, /, qui est à peu près un préprocesseur JavaScript et ne répond donc pas au critère 4.

24
Raynos

Je ne pense pas qu'il existe un moyen "gratuit" de résoudre le problème du "super récursif" que vous avez mentionné.

Nous ne pouvons pas jouer avec la variable this car cela nous obligerait soit à modifier les prototypes de manière non standard, soit à nous faire remonter dans la chaîne de proto en perdant des variables d'instance. Par conséquent, la "classe actuelle" et la "super classe" doivent être connues lorsque nous faisons le super-ing, sans que cette responsabilité revienne à this ou à l'une de ses propriétés.

Nous pourrions essayer beaucoup de choses, mais tout ce que je peux penser a des conséquences indésirables:

  • Ajoutez des super infos aux fonctions au moment de la création, accédez-y en utilisant arguments.calee ou une perversité similaire.
  • Ajouter des informations supplémentaires lors de l'appel de la super méthode

    $super(CurrentClass).method.call(this, 1,2,3)
    

    Cela nous oblige à dupliquer le nom actuel de la classe (afin que nous puissions rechercher sa super-classe dans un super dictionnaire), mais au moins ce n’est pas aussi grave que de devoir dupliquer le nom de la super-classe (depuis le couplage contre les relations couplage interne avec le nom de la classe)

    //Normal Javascript needs the superclass name
    SuperClass.prototype.method.call(this, 1,2,3);
    

    Bien que ce soit loin d'être idéal, il existe au moins un précédent historique de 2.x Python . (Ils ont "corrigé" super pour 3.0 afin qu'il ne nécessite plus d'arguments, mais je ne suis pas sûr de la quantité de magie que cela implique et de son portabilité pour JS)


Edit: Travailler fiddle

var superPairs = [];
// An association list of baseClass -> parentClass

var injectSuper = function (parent, child) {
    superPairs.Push({
        parent: parent,
        child: child
    });
};

function $super(baseClass, obj){
    for(var i=0; i < superPairs.length; i++){
        var p = superPairs[i];
        if(p.child === baseClass){
            return p.parent;
        }
    }
}
10
hugomg

John Resig a publié un mécanisme d'inhérence avec un support super simple mais efficace. La seule différence est que super pointe vers la méthode de base à partir de laquelle vous l'appelez.

Jetez un coup d'œil à http://ejohn.org/blog/simple-javascript-inheritance/ .

5
ngryman

La principale difficulté de super est que vous devez trouver ce que j'appelle here: l'objet qui contient la méthode qui fait la super référence. Cela est absolument nécessaire pour bien comprendre la sémantique. Bien évidemment, avoir le prototype de here est tout aussi bon, mais cela ne fait pas une grande différence. Ce qui suit est une solution statique:

// Simulated static super references (as proposed by Allen Wirfs-Brock)
// http://wiki.ecmascript.org/doku.php?id=harmony:object_initialiser_super

//------------------ Library

function addSuperReferencesTo(obj) {
    Object.getOwnPropertyNames(obj).forEach(function(key) {
        var value = obj[key];
        if (typeof value === "function" && value.name === "me") {
            value.super = Object.getPrototypeOf(obj);
        }
    });
}

function copyOwnFrom(target, source) {
    Object.getOwnPropertyNames(source).forEach(function(propName) {
        Object.defineProperty(target, propName,
            Object.getOwnPropertyDescriptor(source, propName));
    });
    return target;
};

function extends(subC, superC) {
    var subProto = Object.create(superC.prototype);
    // At the very least, we keep the "constructor" property
    // At most, we preserve additions that have already been made
    copyOwnFrom(subProto, subC.prototype);
    addSuperReferencesTo(subProto);
    subC.prototype = subProto;
};

//------------------ Example

function A(name) {
    this.name = name;
}
A.prototype.method = function () {
    return "A:"+this.name;
}

function B(name) {
    A.call(this, name);
}
// A named function expression allows a function to refer to itself
B.prototype.method = function me() {
    return "B"+me.super.method.call(this);
}
extends(B, A);

var b = new B("hello");
console.log(b.method()); // BA:hello
2
Axel Rauschmayer

Notez que pour l'implémentation suivante, lorsque vous êtes dans une méthode appelée via $super, l'accès aux propriétés lorsque vous travaillez dans la classe parent ne se résout jamais en méthodes ou variables de la classe enfant, sauf si vous accédez à un membre stocké directement sur l'objet. lui-même (par opposition à attaché au prototype). Cela évite beaucoup de confusion (lu comme des bugs subtils).

Mise à jour: Voici une implémentation qui fonctionne sans __proto__. Le problème est que l'utilisation de $super est linéaire dans le nombre de propriétés de l'objet parent.

function extend (Child, prototype, /*optional*/Parent) {
    if (!Parent) {
        Parent = Object;
    }
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
    for (var x in prototype) {
        if (prototype.hasOwnProperty(x)) {
            Child.prototype[x] = prototype[x];
        }
    }
    Child.prototype.$super = function (propName) {
        var prop = Parent.prototype[propName];
        if (typeof prop !== "function") {
            return prop;
        }
        var self = this;
        return function () {
            var selfPrototype = self.constructor.prototype;
            var pp = Parent.prototype;
            for (var x in pp) {
                self[x] = pp[x];
            }
            try {
                return prop.apply(self, arguments);
            }
            finally {
                for (var x in selfPrototype) {
                    self[x] = selfPrototype[x];
                }
            }
        };
    };
}

L'implémentation suivante est destinée aux navigateurs prenant en charge la propriété __proto__:

function extend (Child, prototype, /*optional*/Parent) {
    if (!Parent) {
        Parent = Object;
    }
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
    for (var x in prototype) {
        if (prototype.hasOwnProperty(x)) {
            Child.prototype[x] = prototype[x];
        }
    }
    Child.prototype.$super = function (propName) {
        var prop = Parent.prototype[propName];
        if (typeof prop !== "function") {
            return prop;
        }
        var self = this;
        return function (/*arg1, arg2, ...*/) {
            var selfProto = self.__proto__;
            self.__proto__ = Parent.prototype;
            try {
                return prop.apply(self, arguments);
            }
            finally {
                self.__proto__ = selfProto;
            }
        };
    };
}

Exemple:

function A () {}
extend(A, {
    foo: function () {
        return "A1";
    }
});

function B () {}
extend(B, {
    foo: function () {
        return this.$super("foo")() + "_B1";
    }
}, A);

function C () {}
extend(C, {
    foo: function () {
        return this.$super("foo")() + "_C1";
    }
}, B);


var c = new C();
var res1 = c.foo();
B.prototype.foo = function () {
    return this.$super("foo")() + "_B2";
};
var res2 = c.foo();

alert(res1 + "\n" + res2);
2
Thomas Eding

Dans un esprit de complétude (merci également à tous pour ce fil, c’est un excellent point de référence!), J’ai voulu lancer cette implémentation.

Si nous admettons qu’il n’ya pas de bonne façon de satisfaire à tous les critères ci-dessus, je pense que c’est un vaillant effort de l’équipe Salsify (je viens de le trouver) trouvé ici . C’est la seule implémentation que j’ai vue qui évite le problème de la récursivité, mais permet également à .super de faire référence au prototype correct, sans pré-compilation.

Au lieu de casser le critère 1, nous cassons le 5.

la technique repose sur l'utilisation de Function.caller (non conforme à es5, bien qu'elle soit largement prise en charge par les navigateurs et supprime les besoins futurs), mais donne une solution vraiment élégante à tous les autres problèmes (je pense). .caller nous permet d'obtenir la référence à la méthode, ce qui nous permet de localiser notre position dans la chaîne de prototypes et utilise une variable getter pour renvoyer le prototype correct. Ce n’est pas parfait mais c’est une solution très différente de ce que j’ai vu dans cet espace

var Base = function() {};

Base.extend = function(props) {
  var parent = this, Subclass = function(){ parent.apply(this, arguments) };

    Subclass.prototype = Object.create(parent.prototype);

    for(var k in props) {
        if( props.hasOwnProperty(k) ){
            Subclass.prototype[k] = props[k]
            if(typeof props[k] === 'function')
                Subclass.prototype[k]._name = k
        }
    }

    for(var k in parent) 
        if( parent.hasOwnProperty(k)) Subclass[k] = parent[k]        

    Subclass.prototype.constructor = Subclass
    return Subclass;
};

Object.defineProperty(Base.prototype, "super", {
  get: function get() {
    var impl = get.caller,
        name = impl._name,
        foundImpl = this[name] === impl,
        proto = this;

    while (proto = Object.getPrototypeOf(proto)) {
      if (!proto[name]) break;
      else if (proto[name] === impl) foundImpl = true;
      else if (foundImpl)            return proto;
    }

    if (!foundImpl) throw "`super` may not be called outside a method implementation";
  }
});

var Parent = Base.extend({
  greet: function(x) {
    return x + " 2";
  }
})

var Child = Parent.extend({
  greet: function(x) {
    return this.super.greet.call(this, x + " 1" );
  }
});

var c = new Child
c.greet('start ') // => 'start 1 2'

vous pouvez également l'ajuster pour renvoyer la méthode correcte (comme dans l'article d'origine) ou vous pouvez supprimer la nécessité d'annoter chaque méthode avec le nom en transmettant le nom à une super fonction (au lieu d'utiliser un getter)

voici un violon en marche démontrant la technique: jsfiddle

1
monastic-panic

JsFiddle :

Quel est le probleme avec ca?

'use strict';

function Class() {}
Class.extend = function (constructor, definition) {
    var key, hasOwn = {}.hasOwnProperty, proto = this.prototype, temp, Extended;

    if (typeof constructor !== 'function') {
        temp = constructor;
        constructor = definition || function () {};
        definition = temp;
    }
    definition = definition || {};

    Extended = constructor;
    Extended.prototype = new this();

    for (key in definition) {
        if (hasOwn.call(definition, key)) {
            Extended.prototype[key] = definition[key];
        }
    }

    Extended.prototype.constructor = Extended;

    for (key in this) {
        if (hasOwn.call(this, key)) {
            Extended[key] = this[key];
        }
    }

    Extended.$super = proto;
    return Extended;
};

Usage:

var A = Class.extend(function A () {}, {
    foo: function (n) { return n;}
});
var B = A.extend(function B () {}, {
    foo: function (n) {
        if (n > 100) return -1;
        return B.$super.foo.call(this, n+1);
    }
});
var C = B.extend(function C () {}, {
    foo: function (n) {
        return C.$super.foo.call(this, n+2);
    }
});

var c = new C();
document.write(c.foo(0) + '<br>'); //3
A.prototype.foo = function(n) { return -n; };
document.write(c.foo(0)); //-3

Exemple d'utilisation avec des méthodes privilégiées au lieu des méthodes publiques.

var A2 = Class.extend(function A2 () {
    this.foo = function (n) {
        return n;
    };
});
var B2 = A2.extend(function B2 () {
    B2.$super.constructor();
    this.foo = function (n) {
        if (n > 100) return -1;
        return B2.$super.foo.call(this, n+1);
    };
});
var C2 = B2.extend(function C2 () {
    C2.$super.constructor();
    this.foo = function (n) {
        return C2.$super.foo.call(this, n+2);
    };
});

//you must remember to constructor chain
//if you don't then C2.$super.foo === A2.prototype.foo

var c = new C2();
document.write(c.foo(0) + '<br>'); //3
1
Bill Barry

J'ai mis au point un moyen qui vous permettra d'utiliser un pseudo-mot-clé Super en modifiant le contexte d'exécution (une manière que je n'ai pas encore vue doit être présentée ici.) L'inconvénient que j'ai trouvé qui ne me satisfait pas du tout est qu'il ne peut pas ajouter la variable "Super" au contexte d'exécution de la méthode, mais le remplace à la place du contexte d'exécution complet, ce qui signifie que toutes les méthodes privées définies avec la méthode deviennent indisponibles ...

Cette méthode est très similaire à l'OP "eval hack" présenté, cependant elle ne traite pas la chaîne source de la fonction, elle redéclare simplement la fonction en utilisant eval dans le contexte d'exécution actuel. Ce qui le rend un peu meilleur car les deux méthodes ont le même inconvénient mentionné précédemment.

Méthode très simple:

function extend(child, parent){

    var superify = function(/* Super */){
        // Make MakeClass scope unavailable.
        var child = undefined,
            parent = undefined,
            superify = null,
            parentSuper = undefined,
            oldProto = undefined,
            keys = undefined,
            i = undefined,
            len = undefined;

        // Make Super available to returned func.
        var Super = arguments[0];
        return function(/* func */){
            /* This redefines the function with the current execution context.
             * Meaning that when the returned function is called it will have all of the current scopes variables available to it, which right here is just "Super"
             * This has the unfortunate side effect of ripping the old execution context away from the method meaning that no private methods that may have been defined in the original scope are available to it.
             */
            return eval("("+ arguments[0] +")");
        };
    };

    var parentSuper = superify(parent.prototype);

    var oldProto = child.prototype;
    var keys = Object.getOwnPropertyNames(oldProto);
    child.prototype = Object.create(parent.prototype);
    Object.defineProperty(child.prototype, "constructor", {enumerable: false, value: child});

    for(var i = 0, len = keys.length; i<len; i++)
        if("function" === typeof oldProto[keys[i]])
            child.prototype[keys[i]] = parentSuper(oldProto[keys[i]]);
}

Un exemple de faire un cours

function P(){}
P.prototype.logSomething = function(){console.log("Bro.");};

function C(){}
C.prototype.logSomething = function(){console.log("Cool story"); Super.logSomething.call(this);}

extend(C, P);

var test = new C();
test.logSomething(); // "Cool story" "Bro."

Un exemple de l'inconvénient mentionné plus haut.

(function(){
    function privateMethod(){console.log("In a private method");}

    function P(){};

    window.C = function C(){};
    C.prototype.privilagedMethod = function(){
        // This throws an error because when we call extend on this class this function gets redefined in a new scope where privateMethod is not available.
        privateMethod();
    }

    extend(C, P);
})()

var test = new C();
test.privilagedMethod(); // throws error

Notez également que cette méthode ne "superifie" pas le constructeur enfant, ce qui signifie que Super n'est pas disponible. Je voulais juste expliquer le concept, pas créer une bibliothèque de travail :)

En outre, je viens de me rendre compte que je remplissais toutes les conditions d'OP! (Bien qu'il devrait y avoir une condition sur le contexte d'exécution)

0
BAM5

Voici ma version: lowclass

Et voici l'exemple super spaghetti soup du fichier test.js (EDIT: transformé en exemple courant):

var SomeClass = Class((public, protected, private) => ({

    // default access is public, like C++ structs
    publicMethod() {
        console.log('base class publicMethod')
        protected(this).protectedMethod()
    },

    checkPrivateProp() {
        console.assert( private(this).lorem === 'foo' )
    },

    protected: {
        protectedMethod() {
            console.log('base class protectedMethod:', private(this).lorem)
            private(this).lorem = 'foo'
        },
    },

    private: {
        lorem: 'blah',
    },
}))

var SubClass = SomeClass.subclass((public, protected, private, _super) => ({

    publicMethod() {
        _super(this).publicMethod()
        console.log('extended a public method')
        private(this).lorem = 'baaaaz'
        this.checkPrivateProp()
    },

    checkPrivateProp() {
        _super(this).checkPrivateProp()
        console.assert( private(this).lorem === 'baaaaz' )
    },

    protected: {

        protectedMethod() {
            _super(this).protectedMethod()
            console.log('extended a protected method')
        },

    },

    private: {
        lorem: 'bar',
    },
}))

var GrandChildClass = SubClass.subclass((public, protected, private, _super) => ({

    test() {
        private(this).begin()
    },

    reallyBegin() {
        protected(this).reallyReallyBegin()
    },

    protected: {
        reallyReallyBegin() {
            _super(public(this)).publicMethod()
        },
    },

    private: {
        begin() {
            public(this).reallyBegin()
        },
    },
}))

var o = new GrandChildClass
o.test()

console.assert( typeof o.test === 'function' )
console.assert( o.reallyReallyBegin === undefined )
console.assert( o.begin === undefined )
<script> var module = { exports: {} } </script>
<script src="https://unpkg.com/[email protected]/index.js"></script>
<script> var Class = module.exports // get the export </script>

La tentative d'accès non valide ou l'utilisation non valide de _super provoquera une erreur.

A propos des exigences:

  1. this. $ super doit être une référence au prototype. c'est-à-dire que si je change le super prototype au moment de l'exécution, ce changement sera pris en compte. Cela signifie fondamentalement que si le parent a une nouvelle propriété, celle-ci devrait être affichée au moment de l'exécution sur tous les enfants par le biais de super, tout comme une référence codée en dur au parent refléterait les changements.

    Non, l'assistant _super ne renvoie pas le prototype, mais uniquement un objet avec des descripteurs copiés afin d'éviter toute modification des prototypes protégés et privés. De plus, le prototype à partir duquel les descripteurs sont copiés est contenu dans l'appel Class/subclass. Ce serait bien d'avoir cela. FWIW, classes natif se comportent de la même manière.

  2. this. $ super.f.apply (this, arguments); doit fonctionner pour les appels récursifs. Pour tout ensemble d'héritage en chaîne dans lequel plusieurs super appels sont effectués au fur et à mesure que vous montez dans la chaîne d'héritage, vous ne devez pas résoudre le problème récursif.

    oui, pas de problème.

  3. Vous ne devez pas coder en dur les références aux super objets de vos enfants. C'est à dire. Base.prototype.f.apply (this, arguments); défait le point.

    oui

  4. Vous ne devez pas utiliser de compilateur X to JavaScript ni de préprocesseur JavaScript.

    oui, tout runtime

  5. Doit être conforme à ES5

    Oui, il inclut une étape de construction basée sur Babel (par exemple, lowclass utilise WeakMap, qui est compilé dans un formulaire ES5 non perméable). Je ne pense pas que cela vainc l'exigence 4, cela me permet simplement d'écrire ES6 +, mais cela devrait quand même fonctionner dans ES5. Certes, je n'ai pas beaucoup testé cela dans ES5, mais si vous souhaitez l'essayer, nous pouvons certainement résoudre tous les problèmes de construction de mon côté, et de votre côté, vous devriez pouvoir le consommer sans aucune étape de construction. .

La seule condition non remplie est 1. Ce serait bien. Mais c’est peut-être une mauvaise pratique d’échanger des prototypes. Mais en fait, il y a des utilisations où j'aimerais échanger des prototypes afin de réaliser des méta-trucs. 'Il serait agréable d'avoir cette fonctionnalité avec super en natif (qui est statique :(), et encore moins dans cette implémentation.

Pour revérifier l'exigence 2, j'ai ajouté le test récursif de base à mon test.js, ce qui fonctionne (EDIT: transformé en exemple en cours d'exécution):

const A = Class((public, protected, private) => ({
    foo: function (n) { return n }
}))

const B = A.subclass((public, protected, private, _super) => ({
    foo: function (n) {
        if (n > 100) return -1;
        return _super(this).foo(n+1);
    }
}))

const C = B.subclass((public, protected, private, _super) => ({
    foo: function (n) {
        return _super(this).foo(n+2);
    }
}))

var c = new C();
console.log( c.foo(0) === 3 )
<script> var module = { exports: {} } </script>
<script src="https://unpkg.com/[email protected]/index.js"></script>
<script> var Class = module.exports // get the export </script>

(l'en-tête de classe est un peu long pour ces petites classes. J'ai quelques idées pour permettre de réduire cela si tous les assistants n'étaient pas nécessaires au départ)

0
trusktr

Pour ceux qui ne comprennent pas le problème de récursivité présenté par le PO, voici un exemple:

function A () {}
A.prototype.foo = function (n) {
    return n;
};

function B () {}
B.prototype = new A();
B.prototype.constructor = B;
B.prototype.$super = A.prototype;
B.prototype.foo = function (n) {
    if (n > 100) return -1;
    return this.$super.foo.call(this, n+1);
};

function C () {}
C.prototype = new B();
C.prototype.constructor = C;
C.prototype.$super = B.prototype;
C.prototype.foo = function (n) {
    return this.$super.foo.call(this, n+2);
};


alert(new C().foo(0)); // alerts -1, not 3

La raison: this en Javascript est lié dynamiquement. 

0
Thomas Eding

Regardez la bibliothèque Classy ; il fournit des classes, l'héritage et l'accès à une méthode remplacée à l'aide de this.$super

0
ThiefMaster