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:
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.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.Base.prototype.f.apply(this, arguments);
défait le point.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.
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:
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;
}
}
}
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/ .
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
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);
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
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
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)
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:
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, class
es natif se comportent de la même manière.
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.
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
Vous ne devez pas utiliser de compilateur X to JavaScript ni de préprocesseur JavaScript.
oui, tout runtime
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)
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.
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