Unobtrusive Prototyping?
globe
- javascript
0 Jörg Peschke0 donp0 globe
n'abend,
HIIILLFEEE!!11fünfundzwanzigzwölf ;)
Ich bastle derzeit an (m)einem kleinen Javascript Framework. (Bemerkungen a la "Warum nutzt du nicht $insertRandomJsFramework?" darf man sich sparen. Würde ich diese Dinger nutzen wollen, würde ich es tun.)
Natürlich soll das System resistent gegen fremde ebenfalls geladene Scripts / Bibliotheken / Frameworks sein und diese in ihrer Funktionsweise auch nicht beeinflussen. Es soll also nicht möglich sein, dass eigene Komponenten von etwas anderem überschrieben werden, oder dass eine eigene Komponente etwas anderes überschreibt. So viel Anspruch darf sein. Das Gestaltet sich bei Utiltites und Widgets ja recht einfach, stellt mich beim "Erweitern bestehender Objekte" vor eine dicke Mauer.
Beispielhaft betrachten wir im Folgenden Nodes und eine hinzuzufügende Methode dummy().
Möglichkeit 1 - das Offensichtliche
Node.prototype.dummy = function(){ alert( this.className ); };
document.body.dummy();
Prototyping dieser Form hat den Nachteil, dass ein anderes dahergelaufenes Script ebenfalls Node prototypisch um dummy() erweitern könnte, allerdings mit einer komplett anderen funktionsweise, als ich diese erdacht habe. Das wäre für mein Framwork eher schlecht, fast so schlecht, als würde mein Framework auf die selbe Art und Weise Prototypen anderer Systeme überschreiben.
Möglichkeit 2 - das Unbrauchbare
fw.utils.Node.dummy = function( nodeObject ){ alert( nodeObject.className ); };
fw.utils.Node.dummy( document.body );
Nun, das erinnert irgendwie an komisch strukturierte, nicht-OOP Konzeption. Kann es nicht sein.
Möglichkeit 3 - die Augenwischerei
fw.utils.Node.dummy = function(){ alert( this.className ); };
document.myGetElementById = function( id )
{
var el = document.getElementById(id);
el.dummy = fw.utils.Node.dummy;
return el;
};
document.myGetElementById('demo').dummy()
Dieses Verfahren wird gerne von anderen Frameworks angewendet. Man nutzt eigene Methoden um Elemente zu erstellen Nodes im DOM zu finden. Man erweitert die Objekte in diesen Funktionen um die Methoden, die man eben nicht prototypisch hinzugefügt hat und gibt die erweiterte Node wieder zurück. Das ist ja schön, bewirkt aber für die gefundenen Nodes "das gleiche" wie die prototypische Erweiterung: ich könnte eine Methode eines anderen Systems überschreiben.
Möglichkeit 4 - die Verkapselei
function MyNode( base )
{
/* durchpipen der Attribute von base */
this.dummy = function(){ alert(base.className) };
}
document.myGetElementById = function( id )
{
var el = document.getElementById(id);
return new MyNode( el );
}
Tja, auf den ersten Blick sieht es ganz angenehm aus das original Objekt unangetastet zu lassen, es in einem anderen Objekt zu kapseln und durch die Kapsel alle erweiternden Methoden zur Verfügung zu stellen. Irgendwie müssen dann aber die Attribute der Node in MyNode verfügbar gemacht werden. Wie will man das denn bitte sinnvoll machen? Es soll ja Methoden und Attribute geben, die man nicht in einem for(..in..) wiederfindet, weil sie "versteckt" sind. Dann stellt sich die Frage ob es nicht sinnvoller ist, "statische" Kapseln zu bauen, also die bekannten Attribute in die Definition packen, statt darüber zu iterieren?
Möglichkeit 5 - die ProtoypeNamespaces
document.body.FrameworkNamespace.dummy()
Sowas in dieser Art wäre mir glaube ich am liebsten. Allerdings fehlen mir (funktionierende) Ansätze das umzusetzen.
weiterhin schönen abend...
Hallo,
Hmm, möglicherweise stehe ich auf dem Schlauch, aber geht nicht irgendwie sowas:
fw.utils.Node.dummy = function () {alert(this.className);}
fw.utils.Node.prototype = new Node();
(ohne Gewähr)
(vgl. Wikipedia-Artikel über Prototyp-basierte Vererbung in JavaScript)
Du lässt das Orginal-Node-Objekt unangetastet (was praktisch ist, für den Fall, dass ein Programmierer für eine Anwendung eben genau Deine Methoden/Eigenschaften nicht im Node-Objekt haben will), vererbst aber alle Node-Eigenschaften und Methoden an Deine eigene Klasse, sodass diese als Vollwertiges Node-Objekt (inkl. Deiner neuen Funktionalitäten) genutzt werden kann.
Viele Grüße,
Jörg
Hi,
Ich bastle derzeit an (m)einem kleinen Javascript Framework.
Ich auch, mehr oder weniger. Eigentlich nur an einer Bibliothek nützlicher Funktionen. Deren gibt es schon einige, aber die einen können mir zu viel, andere zu wenig...
Natürlich soll das System resistent gegen fremde ebenfalls geladene Scripts / Bibliotheken / Frameworks sein und diese in ihrer Funktionsweise auch nicht beeinflussen. Es soll also nicht möglich sein, dass eigene Komponenten von etwas anderem überschrieben werden, oder dass eine eigene Komponente etwas anderes überschreibt. So viel Anspruch darf sein.
...ist aber in letzter Konsequenz unmöglich. *Einen* Tod muss man sterben.
Möglichkeit 5 - die ProtoypeNamespaces
document.body.FrameworkNamespace.dummy()
Sowas in dieser Art wäre mir glaube ich am liebsten. Allerdings fehlen mir (funktionierende) Ansätze das umzusetzen.
Das ähnelt doch irgendwie deinen Varianten 1 und 2.
Ein Ansatz wäre vielleicht:
Object.prototype.begetObject = function () {
function F() {}
F.prototype = this;
return new F();
};
document.body.FrameworkNamespace = document.body.begetObject();
document.body.FrameworkNamespace.dummy = function(){ alert(this.className) };
Mit der neuen Methode .begetObject() (Dank an Douglas Crockford) kann sich jedes Objekt vervielfältigen, d.h. es ergibt sich ein neues, leeres Objekt, das *alle* Eigenschaften u. Methoden des alten Objekts prototypisch erbt. Anschließend kann man es beliebig erweitern, das alte Objekt wird davon nicht berührt.
Ich selbst habe begetObject() noch so erweitert, dass es ein anderes Objekt als Parameter nimmt und dessen eigene (nicht vererbte) Eigenschaften u. Methoden als eigene Eigensch./Methoden des neuen Objekts setzt. Diese Neue Methode bgetObj() kann also ein Objekt richtiggehend klonen indem man z.B. sagt
newObject = oldObject.bgetObj(oldObject);
newObject hat jetzt alle Eigensch./Methoden von OldObject selbst, d.h. die nachträgliche Änderung einer Eigenschaft/Methode von oldObject wirkt sich auf newObject nicht mehr aus. Den Quelltext dieser neuen Methode habe ich im Moment aber nicht zur Verfügung. Kann ich bei Interesse aber nachliefern.
Gruß, Don P
n'abend,
document.body.FrameworkNamespace = document.body.begetObject();
document.body.FrameworkNamespace.dummy = function(){ alert(this.className) };
[/code]
Wenn ich das richtig sehe, würde ich so aber mit einer Kopie von document.body arbeiten, nicht mit einer Referenz auf document.body. Ich bekomme eingangs zwar die Daten aus document.body zu gesicht, verändere ich aber etwas in der Kopie, ändert sich das nicht in der original-Instanz.
// Objekt-Ableiter nach http://javascript.crockford.com/prototypal.html
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
// Test Objekt
function MyOne()
{
this.message = 'initial message from constructor';
this.alert = function(){ alert(this.message) };
}
// original-Instanz
var s = new MyOne();
// abgeleitete Instanz
var e = object(s);
// Attribut in original-Instanz manipulieren
s.message = 'new message from original instance';
e.alert(); // Ausgabe: »new message from original instance«
// Attribut in abgeleiteter Instanz ändern
e.message = 'altered message from extended instance';
s.alert(); // Ausgabe: »new message from original instance«
e.alert(); // Ausgabe: altered message from extended instance«
// Attribut in original-Instanz manipulieren
s.message = 'altered message from original instance';
e.alert(); // Ausgabe: altered message from original instance«
Wie du siehst arbeitet man also definitiv mit einem neuen (vom original abgeleiteten) Objekt, welches als Initialwerte die Werte des originalen Objekts gesetzt hat.
Mal ganz davon abgesehen bin ich über ein Phänomen gestolpert, was den Anwendungsbereich dieser Ableitungsgeschichte etwas reduziert:
var s = "hello world";
var e = object( s );
alert( s.length ); // Ausgabe: 11
alert( e.length ); // Ausgabe: undefined
Da wird also irgendwas nicht durchgereicht?
var s = new String( "hello world" );
var e = object( s );
alert( s.length ); // Ausgabe: 11
alert( e.length ); // Fehler: String.prototype.toString called on incompatible Object
ahja? darf ich String also nicht erweitern, oder wie sehe ich das?
var s = [1,2,3];
var e = object( s );
alert( s.length ); // Ausgabe: 3
alert( e.length ); // AUsgabe: 3
oha, Arrays darf ich also vergewaltigen, interessant.
var s = new Number(123.45);
var e = object( s );
alert( s.toFixed() ); // Ausgabe: 123
alert( e.toFixed() ); // Fehler: Number.prototype.toFixed called on incompatible Object
Dafür Number auch nicht (ähnliche Probleme wie bei String)
scheint wohl nett zu sein, wenn man eigene Objekte erweitern/vererben will, aber eher unbrauchbar, wenn man an Javascript-eigenem Kram spielen möchte.
(Beobachtet unter Firefox 2.0.0.7 running on Mac OS X 10.4.10)
weiterhin schönen abend...
Tach auch,
Habe mich extra vorsichtig ausgedrückt mit "Ein Ansatz wäre vielleicht". Ein bisschen eigenen Hirnschmalz musst du schon noch dazugeben...
Wenn ich das richtig sehe, würde ich so aber mit einer Kopie von document.body arbeiten, nicht mit einer Referenz auf document.body. [...] verändere ich aber etwas in der Kopie, ändert sich das nicht in der original-Instanz.
Das siehst du völlig richtig. Ich schrub ja: es wird ein *neues* Objekt erzeugt. Dieses ist immerhin billig zu haben, denn wenn es eine Eigenschaft oder Methode nicht selbst hat, zeigt es einfach diejenige vom Original-Objekt vor. Es ist also nicht wirklich eine Kopie. Das ist der Clou der prototypischen Vererbung.
Man kann das neue Objekt nur deshalb gefahrlos erweitern, weil es eben gerade nicht das alte ist. Wäre es nur eine Referenz auf das alte, dann könnte man ja wieder dessen Methoden überschreiben, was du aber gerade verhindern willst.
Das ginge dann z.B. so...
// Test Objekt
var MyOne =
{
message: 'initial message from constructor',
alert: function(){ alert(this.message) }
}
var s = MyOne; // original-Instanz
var t = s; // abgeleitete Instanz
t.alert = function(){ alert('Überschriebene Methode!') };
s.alert(); // Ausgabe: Überschriebene Methode!
...und wäre also nicht das, was man will.
Man *muss* also zwangsläufig mit einer "Kopie" arbeiten. Wenn man damit trotzdem direkt auf die Eigenschaften oder Methoden des Original-Objekts zugreifen will, muss man halt jeweils das prototype-Objekt ansprechen:
// Objekt-Ableiter nach http://javascript.crockford.com/prototypal.html
function object(o) {
function F() {}
F.prototype = o;
return new F();
}// Test Objekt
function MyOne()
{
this.message = 'initial message from constructor';
this.alert = function(){ alert(this.message) };
}// original-Instanz
var s = new MyOne();// abgeleitete Instanz
var e = object(s);// Attribut in original-Instanz manipulieren
s.message = 'new message from original instance';
e.alert(); // Ausgabe: »new message from original instance«// Attribut in abgeleiteter Instanz ändern
e.proto.message = 'altered message from extended instance';
s.alert(); // Ausgabe: »altered message from original instance«
e.alert(); // Ausgabe: »altered message from extended instance«// Attribut in original-Instanz manipulieren
s.message = 'newly altered message from original instance';
e.alert(); // Ausgabe: »newly altered message from original instance«
Wie du siehst, arbeitet man jetzt definitiv mit dem originalen Objekt.
>
> Mal ganz davon abgesehen bin ich über ein Phänomen gestolpert, was den Anwendungsbereich dieser Ableitungsgeschichte etwas reduziert:
In der Tat werden die nicht iterablen Eigenschaften offenbar nicht weiterverebt. Auch dieses Problem lässt sich aber durch direktes Ansprechen des prototype-Objekts lösen:
> ~~~javascript
var s = "hello world";
> var e = object( s );
> alert( s.length ); // Ausgabe: 11
alert( e.__proto__.length ); // Ausgabe: undefined
Hier klappt's nicht mal mit dem prototype-Objekt, aber mit...
String.prototype.obj=function(){function F(){}F.prototype=this;return new F();};
// Allgemeiner: Object.prototype.newobj=function(){function F(){}F.prototype=this;return new F();};
var e = s.newobj();
alert( e.__proto__.length ); // Ausgabe: 11
...hat man auch endlich Zugriff auf die nicht iterable length-Eigenschaft :).
var s = new String( "hello world" );
var e = object( s );
alert( s.length ); // Ausgabe: 11
alert( e.proto.length ); // Ausgabe: 11
Du darfst also String erweitern, gelle?
> ~~~javascript
var s = [1,2,3];
> var e = object( s );
> alert( s.length ); // Ausgabe: 3
> alert( e.length ); // Ausgabe: 3
oha, Arrays darf ich also vergewaltigen, interessant.
"vergewaltigen" ist aber ein hartes Wort für eine so schöne Sache...
var s = new Number(123.45);
var e = object( s );
alert( s.toFixed() ); // Ausgabe: 123
alert( e.proto.toFixed() ); // Ausgabe: 123
Number funktioniert somit auch korrekt.
> scheint wohl nett zu sein, wenn man eigene Objekte erweitern/vererben will, aber eher unbrauchbar, wenn man an Javascript-eigenem Kram spielen möchte.
Tja, ob das alles besonders brauchbar ist, frage ich mich auch noch, deshalb meine vorsichtige Ausfrucksweise am Anfang. Habe ja auch nur mal einen Ansatz für deine Lieblingsvariante ausprobiert.
Funktionieren würde es jedenfalls, aber man müsste dann halt dem Framework noch ein paar schlaue Spezialfunktionen mitgeben, die dafür sorgen, das man immer das das richtige prototype-Objekt erwischt. Diese habe ich dir hier noch nicht vorgekaut, habe bis jetzt selber nur einen recht blassen Schimmer davon, wie sie aussehen müssten...
Gruß, Don P
Hallo,
Hier klappt's nicht mal mit dem prototype-Objekt, aber mit...
String.prototype.obj=function(){function F(){}F.prototype=this;return new F();};
// Allgemeiner: Object.prototype.newobj=function(){function F(){}F.prototype=this;return new F();};
var e = s.newobj();
alert( e.proto.length ); // Ausgabe: 11
> ...hat man auch endlich Zugriff auf die nicht iterable length-Eigenschaft :).
Natürlich ist length immer iterable, aber nur bei String-Objects, weil bei String-Primitives gar nix iterable ist.
Warum es hier funktioniert? Weil »this« in der Methode immer schon ein Object ist.
String.prototype.m = function () { alert(typeof this); };
s = "";
alert(typeof s);
s.m();
alert(typeof s);
s kann ich nie als Prototyp nehmen, solange es ein Primitive ist, aber es findet bei primitiveValue.methode() eine interne Umwandlung in Object statt, um die Methode in der Prototype-Chain zu finden. Die Umwandlung ist aber nur »virtuell« und gilt nur für den Kontext des Methodenaufrufs.
Mathias
Hallo Mathias,
Natürlich ist length immer iterable, aber nur bei String-Objects, weil bei String-Primitives gar nix iterable ist.
"String-Primitives" nennt man sowas also, interessant. Man lernt ja nie aus. Bis jetzt ist mir der Unterschied zwischen einem veritablen String-Objekt und so einem Primitivling noch nicht richtig klar geworden, aber ich arbeite daran, mit deiner Hilfe.
s kann ich nie als Prototyp nehmen, solange es ein Primitive ist, aber es findet bei primitiveValue.methode() eine interne Umwandlung in Object statt, um die Methode in der Prototype-Chain zu finden.
Das klingt plausibel.
Die Umwandlung ist aber nur »virtuell« und gilt nur für den Kontext des Methodenaufrufs.
Hmmm, was bedeutet das denn jetzt konkret?
Don P
P.S. Würde gerne dein Posting als "fachlich hilfreich" bewerten, aber obwohl registriert, geht's jetzt nicht, weil ich mein Passwort vergessen hab'. Schande über mich, aber mein übliches Allround-Passwort ist hier halt nicht akzeptiert worden u. ich hab' zu wenig RAM für 2000 versch. Passwörter im Kopf...
n'abend,
Die Umwandlung ist aber nur »virtuell« und gilt nur für den Kontext des Methodenaufrufs.
Hmmm, was bedeutet das denn jetzt konkret?
Das bedeutet, dass primitives (z.B. var string = "hallo welt"; var number = 123; var boolean = true;
) keine "vollwertigen" Objekte vom Typ String, Number oder Boolean sind und dementsprechend auch nicht über die Methoden / Attribute dieser Objekte verfügen. Damit man aber dennoch sinnvoll mit primitives arbeiten kann, werden diese für den (lokalen) Kontext des Methodenaufrufs/Attributgebrauchs kurz in ein "vollwertiges" Objekt verwandelt. Das "vollwertige" Objekt wird nachdem der Kontext verlassen wurde einfach verschrottet, man arbeitet also mit dem Primitive weiter.
.oO( schön, dass ich da bei meinen vorigen Tests auch nicht dran gedacht habe, obwohl kurz zuvor eben diesen Umstand in den Mozilla Docs las. )
weiterhin schönen abend...
Hallo Mathias,
Natürlich ist length immer iterable, aber nur bei String-Objects, weil bei String-Primitives gar nix iterable ist.
Zuerst habe ich das einfach geglaubt, aber hartnäckig wie ich bin, dann doch noch einmal überprüft, und siehe: Obige Aussage ist anscheinend völlig falsch, Beweis:
var myString = new String('abc');
for (var prop in myString) {
alert( 'property ' + prop + ' (Constructor)' );
}
//=> Beim "richtigen" Objekt kann man über fast alle Eigenschaften iterieren, aber "length" ist nicht dabei.
myString = 'abc';
for (prop in myString) {
alert( 'property ' + prop + ' (Primitive)' );
}
//=> Beim Primitivling gilt dasselbe.
Sag bloß, die for-Schleife bewirkt ebenfalls eine temporäre Umwandlung des Primitives in ein Objekt? Dann bleibt zumindest noch der erste Teil deiner Aussage falsch, da die length-Eigenschaft bewiesermaßen doch nicht iterable ist.
Gruß, Don P
Hallo Don,
Sag bloß, die for-Schleife bewirkt ebenfalls eine temporäre Umwandlung des Primitives in ein Objekt?
ECMAScript The for-in Statement:
»The production IterationStatement : for ( LeftHandSideExpression in Expression ) Statement is evaluated as follows:
1. Evaluate the Expression.
2. Call GetValue(Result(1)).
3. Call ToObject(Result(2)).
...«
»The operator ToObject converts its argument to a value of type Object according to the following table:
...
String – Create a new String object whose [[value]] property is set to the value of the string. See 15.5 for a description of String objects.«
Dann bleibt zumindest noch der erste Teil deiner Aussage falsch, da die length-Eigenschaft bewiesermaßen doch nicht iterable ist.
Ich hab hier keine Ahnung, was Ihr zwei genau mit iterable meint. Dass die Eigenschaft nicht enumiert wird? Klar:
»In every case, the length property of a built-in Function object described in this section has the attributes { ReadOnly, DontDelete, DontEnum }«
ECMAScript The for-in Statement (fortgeführt):
»5. Get the name of the next property of Result(3) that doesn't have the DontEnum attribute. If there is no such property, go to step 14.«
Tim
Hallo,
- Call ToObject(Result(2)).
Klar, der Punkt ist wohl eher, dass ein for-in bei einem String aber nie die wirklichen Eigenschaften hervorbringt, sondern den String wie einen Array aus einzelnen Zeichen wiedergibt. Für Arrays gilt dasselbe. Wo das jetzt genau steht, weiß ich nicht.
»In every case, the length property of a built-in Function object described in this section has the attributes { ReadOnly, DontDelete, DontEnum }«
Wir reden zwar nicht von function.length, aber recht hat du auch in Bezug auf string.length.
alert((s = new String("str")).hasOwnProperty("length") + " " + s.propertyIsEnumerable("length")); // true false
Warum ist jetzt z.B. string.substring() nicht enumerable? Wahrscheinlich ist das so gelöst, dass der String-Prototyp nicht enumerable ist und somit die Regel »Enumerating the properties of an object includes enumerating properties of its prototype« nicht gilt.
Mathias
Hallo,
var s = "hello world";
var e = object( s );
alert( s.length ); // Ausgabe: 11
alert( e.length ); // Ausgabe: undefined
>
> Da wird also irgendwas nicht durchgereicht?
Da wird kein einziger Member kopiert, weil primitive values keine Member haben. Ein primitive value kann kein Prototyp sein, weil es kein Object ist.
So würds gehn:
F.prototype = typeof(o) == "function" || typeof(o) == "object" ? o : Object(o); // mach zur Not ein Object draus
oder s = new String("hello world"); // mach gleich ein Object draus
> oha, Arrays darf ich also vergewaltigen, interessant.
Arrays sind auch Objects.
> Dafür Number auch nicht (ähnliche Probleme wie bei String)
Du hast es hier mit Number-Primitives zu tun. Sorg dafür, dass es ein Number-Object ist.
Mathias