Es gibt mehrere Ebenen der objektorientierten Programmierung (OOP) in JavaScript:
Grob gesagt sind alle Objekte in JavaScript Maps (Wörterbücher) von Strings zu Werten. Ein (Schlüssel, Wert)-Eintrag in einem Objekt wird als Eigenschaft bezeichnet. Der Schlüssel einer Eigenschaft ist immer ein Textstring. Der Wert einer Eigenschaft kann jeder JavaScript-Wert sein, einschließlich einer Funktion. Methoden sind Eigenschaften, deren Werte Funktionen sind.
Es gibt drei Arten von Eigenschaften
[[Prototype]] den Prototyp eines Objekts und ist über Object.getPrototypeOf() lesbar. JavaScript- Objektliterale erlauben Ihnen, direkt einfache Objekte (direkte Instanzen von Object) zu erstellen. Der folgende Code verwendet ein Objektliteral, um ein Objekt der Variablen jane zuzuweisen. Das Objekt hat die beiden Eigenschaften: name und describe. describe ist eine Methode:
varjane={name:'Jane',describe:function(){return'Person named '+this.name;// (1)},// (2)};
this in Methoden, um auf das aktuelle Objekt zu verweisen (auch als Empfänger eines Methodenaufrufs bezeichnet). Sie könnten den Eindruck bekommen, dass Objekte nur Maps von Strings zu Werten sind. Aber sie sind mehr als das: sie sind echte Allzweckobjekte. Zum Beispiel können Sie Vererbung zwischen Objekten verwenden (siehe Ebene 2: Die Prototyp-Beziehung zwischen Objekten) und Sie können Objekte vor Änderungen schützen. Die Möglichkeit, Objekte direkt zu erstellen, ist eine der herausragenden Eigenschaften von JavaScript: Sie können mit konkreten Objekten beginnen (keine Klassen benötigt!) und Abstraktionen später einführen. Zum Beispiel sind Konstruktoren, die Fabriken für Objekte sind (wie in Ebene 3: Konstruktoren – Fabriken für Instanzen besprochen), grob ähnlich wie Klassen in anderen Sprachen.
Der Punktoperator bietet eine kompakte Syntax für den Zugriff auf Eigenschaften. Die Eigenschaftsschlüssel müssen Bezeichner sein (siehe Gültige Bezeichner). Wenn Sie Eigenschaften mit beliebigen Namen lesen oder schreiben möchten, müssen Sie den Klammeroperator verwenden (siehe Klammeroperator ([]): Zugriff auf Eigenschaften über berechnete Schlüssel).
Die Beispiele in diesem Abschnitt arbeiten mit dem folgenden Objekt
varjane={name:'Jane',describe:function(){return'Person named '+this.name;}};
Der Punktoperator ermöglicht es Ihnen, eine Eigenschaft abzurufen (ihren Wert zu lesen). Hier sind einige Beispiele
> jane.name // get property `name` 'Jane' > jane.describe // get property `describe` [Function]
Das Abrufen einer nicht existierenden Eigenschaft gibt undefined zurück
> jane.unknownProperty undefined
Der Punktoperator wird auch verwendet, um Methoden aufzurufen:
> jane.describe() // call method `describe` 'Person named Jane'
Sie können den Zuweisungsoperator (=) verwenden, um den Wert einer Eigenschaft festzulegen, auf die über die Punktnotation verwiesen wird. Zum Beispiel:
> jane.name = 'John'; // set property `name` > jane.describe() 'Person named John'
Wenn eine Eigenschaft noch nicht existiert, wird sie beim Festlegen automatisch erstellt. Wenn eine Eigenschaft bereits existiert, ändert das Festlegen ihren Wert.
Der Operator delete ermöglicht es Ihnen, eine Eigenschaft (das gesamte Schlüssel-Wert-Paar) vollständig aus einem Objekt zu entfernen. Zum Beispiel:
> var obj = { hello: 'world' };
> delete obj.hello
true
> obj.hello
undefinedWenn Sie eine Eigenschaft lediglich auf undefined setzen, existiert die Eigenschaft weiterhin und das Objekt enthält immer noch ihren Schlüssel:
> var obj = { foo: 'a', bar: 'b' };
> obj.foo = undefined;
> Object.keys(obj)
[ 'foo', 'bar' ]Wenn Sie die Eigenschaft löschen, ist auch ihr Schlüssel weg
> delete obj.foo true > Object.keys(obj) [ 'bar' ]
delete wirkt sich nur auf die direkten („eigenen“, nicht vererbten) Eigenschaften eines Objekts aus. Seine Prototypen werden nicht berührt (siehe Löschen einer geerbten Eigenschaft).
Verwenden Sie den Operator delete sparsam. Die meisten modernen JavaScript-Engines optimieren die Leistung von Instanzen, die von Konstruktoren erstellt wurden, wenn ihre „Form“ sich nicht ändert (ungefähr: keine Eigenschaften werden entfernt oder hinzugefügt). Das Löschen einer Eigenschaft verhindert diese Optimierung.
delete gibt false zurück, wenn die Eigenschaft eine eigene Eigenschaft ist, aber nicht gelöscht werden kann. In allen anderen Fällen gibt es true zurück. Hier sind einige Beispiele.
Zur Vorbereitung erstellen wir eine löschbare Eigenschaft und eine weitere, die nicht gelöscht werden kann (Abrufen und Definieren von Eigenschaften über Deskriptoren erklärt Object.defineProperty())
varobj={};Object.defineProperty(obj,'canBeDeleted',{value:123,configurable:true});Object.defineProperty(obj,'cannotBeDeleted',{value:456,configurable:false});
delete gibt false zurück für eigene Eigenschaften, die nicht gelöscht werden können
> delete obj.cannotBeDeleted false
delete gibt true in allen anderen Fällen zurück
> delete obj.doesNotExist true > delete obj.canBeDeleted true
delete gibt true zurück, auch wenn es nichts ändert (geerbte Eigenschaften werden nie entfernt)
> delete obj.toString true > obj.toString // still there [Function: toString]
Während Sie reservierte Wörter (wie var und function) nicht als Variablennamen verwenden können, können Sie sie als Eigenschaftsschlüssel verwenden:
> var obj = { var: 'a', function: 'b' };
> obj.var
'a'
> obj.function
'b'Zahlen können als Eigenschaftsschlüssel in Objektliteralen verwendet werden, werden aber als Strings interpretiert. Der Punktoperator kann nur auf Eigenschaften zugreifen, deren Schlüssel Bezeichner sind. Daher benötigen Sie den Klammeroperator (im folgenden Beispiel gezeigt), um auf Eigenschaften zuzugreifen, deren Schlüssel Zahlen sind:
> var obj = { 0.7: 'abc' };
> Object.keys(obj)
[ '0.7' ]
> obj['0.7']
'abc'Objektliterale erlauben Ihnen auch, beliebige Strings (die weder Bezeichner noch Zahlen sind) als Eigenschaftsschlüssel zu verwenden, aber Sie müssen sie in Anführungszeichen setzen. Auch hier benötigen Sie den Klammeroperator, um auf die Eigenschaftswerte zuzugreifen
> var obj = { 'not an identifier': 123 };
> Object.keys(obj)
[ 'not an identifier' ]
> obj['not an identifier']
123Während der Punktoperator mit festen Eigenschaftsschlüsseln funktioniert, ermöglicht Ihnen der Klammeroperator, eine Eigenschaft über einen Ausdruck anzusprechen.
Der Klammeroperator lässt Sie den Schlüssel einer Eigenschaft über einen Ausdruck berechnen:
> var obj = { someProperty: 'abc' };
> obj['some' + 'Property']
'abc'
> var propKey = 'someProperty';
> obj[propKey]
'abc'Das ermöglicht auch den Zugriff auf Eigenschaften, deren Schlüssel keine Bezeichner sind
> var obj = { 'not an identifier': 123 };
> obj['not an identifier']
123Beachten Sie, dass der Klammeroperator seinen Inhalt zu einem String koerziert. Zum Beispiel
> var obj = { '6': 'bar' };
> obj[3+3] // key: the string '6'
'bar'Das Aufrufen von Methoden funktioniert wie erwartet:
> var obj = { myMethod: function () { return true } };
> obj['myMethod']()
trueDas Festlegen von Eigenschaften funktioniert analog zum Punktoperator:
> var obj = {};
> obj['anotherProperty'] = 'def';
> obj.anotherProperty
'def'Das Löschen von Eigenschaften funktioniert ebenfalls ähnlich wie beim Punktoperator:
> var obj = { 'not an identifier': 1, prop: 2 };
> Object.keys(obj)
[ 'not an identifier', 'prop' ]
> delete obj['not an identifier']
true
> Object.keys(obj)
[ 'prop' ]Es ist kein häufiger Anwendungsfall, aber manchmal müssen Sie einen beliebigen Wert in ein Objekt konvertieren. Object(), als Funktion verwendet (nicht als Konstruktor), bietet diesen Dienst. Es liefert die folgenden Ergebnisse:
| Wert | Ergebnis |
(Aufgerufen ohne Parameter) |
|
|
|
|
|
Ein boolescher Wert |
|
Eine Zahl |
|
Ein String |
|
Ein Objekt |
|
Hier sind einige Beispiele
> Object(null) instanceof Object
true
> Object(false) instanceof Boolean
true
> var obj = {};
> Object(obj) === obj
trueDie folgende Funktion prüft ob value ein Objekt ist:
functionisObject(value){returnvalue===Object(value);}
Beachten Sie, dass die vorherige Funktion ein Objekt erstellt, wenn value kein Objekt ist. Sie können dieselbe Funktion implementieren, ohne dies zu tun, über typeof (siehe Fallstrick: typeof null).
Sie können Object auch als Konstruktor aufrufen, was die gleichen Ergebnisse liefert wie der Aufruf als Funktion:
> var obj = {};
> new Object(obj) === obj
true
> new Object(123) instanceof Number
trueVermeiden Sie den Konstruktor; ein leeres Objektliteral ist fast immer die bessere Wahl:
varobj=newObject();// avoidvarobj={};// prefer
Wenn Sie eine Funktion aufrufen, ist this immer ein (impliziter) Parameter:
Auch wenn normale Funktionen keinen Nutzen für this haben, existiert es dennoch als spezielle Variable, deren Wert immer das globale Objekt ist (window in Browsern; siehe Das globale Objekt)
> function returnThisSloppy() { return this }
> returnThisSloppy() === window
true
this ist immer undefined
> function returnThisStrict() { 'use strict'; return this }
> returnThisStrict() === undefined
true
this bezieht sich auf das Objekt, auf dem die Methode aufgerufen wurde
> var obj = { method: returnThisStrict };
> obj.method() === obj
trueBei Methoden wird der Wert von this als Empfänger des Methodenaufrufs bezeichnet.
Denken Sie daran, dass Funktionen auch Objekte sind. Daher hat jede Funktion eigene Methoden. Drei davon werden in diesem Abschnitt vorgestellt und helfen beim Aufrufen von Funktionen. Diese drei Methoden werden in den folgenden Abschnitten verwendet, um einige der Fallstricke beim Aufrufen von Funktionen zu umgehen. Die kommenden Beispiele beziehen sich alle auf das folgende Objekt, jane:
varjane={name:'Jane',sayHelloTo:function(otherName){'use strict';console.log(this.name+' says hello to '+otherName);}};
Der erste Parameter ist der Wert, den this innerhalb der aufgerufenen Funktion haben wird; die restlichen Parameter werden als Argumente an die aufgerufene Funktion übergeben. Die folgenden drei Aufrufe sind äquivalent:
jane.sayHelloTo('Tarzan');jane.sayHelloTo.call(jane,'Tarzan');varfunc=jane.sayHelloTo;func.call(jane,'Tarzan');
Für den zweiten Aufruf müssen Sie jane wiederholen, da call() nicht weiß, wie Sie die Funktion erhalten haben, auf der es aufgerufen wird.
jane.sayHelloTo('Tarzan');jane.sayHelloTo.apply(jane,['Tarzan']);varfunc=jane.sayHelloTo;func.apply(jane,['Tarzan']);
Für den zweiten Aufruf müssen Sie jane wiederholen, da apply() nicht weiß, wie Sie die Funktion erhalten haben, auf der es aufgerufen wird.
apply() für Konstruktoren erklärt, wie apply() mit Konstruktoren verwendet wird.
Diese Methode führt eine partielle Funktionsanwendung durch – das bedeutet, sie erstellt eine neue Funktion, die den Empfänger von bind() wie folgt aufruft: der Wert von this ist thisValue und die Argumente beginnen mit arg1 bis argN, gefolgt von den Argumenten der neuen Funktion. Mit anderen Worten, die neue Funktion hängt ihre Argumente an arg1, ..., argN an, wenn sie die ursprüngliche Funktion aufruft. Betrachten wir ein Beispiel:
functionfunc(){console.log('this: '+this);console.log('arguments: '+Array.prototype.slice.call(arguments));}varbound=func.bind('abc',1,2);
Die Array-Methode slice wird verwendet, um arguments in ein Array zu konvertieren, was für die Protokollierung notwendig ist (diese Operation wird in Array-ähnliche Objekte und generische Methoden erklärt). bound ist eine neue Funktion. Hier ist die Interaktion
> bound(3) this: abc arguments: 1,2,3
Die folgenden drei Aufrufe von sayHelloTo sind alle äquivalent
jane.sayHelloTo('Tarzan');varfunc1=jane.sayHelloTo.bind(jane);func1('Tarzan');varfunc2=jane.sayHelloTo.bind(jane,'Tarzan');func2();
Nehmen wir an, JavaScript hätte einen Dreipunktoperator (...), der Arrays in tatsächliche Parameter umwandelt. Ein solcher Operator würde es Ihnen ermöglichen, Math.max() (siehe Andere Funktionen) mit Arrays zu verwenden. In diesem Fall wären die folgenden beiden Ausdrücke äquivalent
Math.max(...[13,7,30])Math.max(13,7,30)
Für Funktionen können Sie den Effekt des Dreipunktoperators über apply() erzielen
> Math.max.apply(null, [13, 7, 30]) 30
Der Dreipunktoperator wäre auch für Konstruktoren sinnvoll
newDate(...[2011,11,24])// Christmas Eve 2011
Leider funktioniert apply() hier nicht, da es nur bei Funktions- oder Methodenaufrufen hilft, nicht bei Konstruktoraufrufen.
Wir können apply() in zwei Schritten simulieren.
Übergabe der Argumente an Date über einen Methodenaufruf (sie sind noch nicht in einem Array)
new(Date.bind(null,2011,11,24))
Der vorherige Code verwendet bind(), um einen Konstruktor ohne Parameter zu erstellen und ihn über new aufzurufen.
Verwenden Sie apply(), um ein Array an bind() zu übergeben. Da bind() ein Methodenaufruf ist, können wir apply() verwenden
new(Function.prototype.bind.apply(Date,[null,2011,11,24]))
Das vorherige Array enthält null, gefolgt von den Elementen von arr. Wir können concat() verwenden, um es zu erstellen, indem wir null voranstellen zu arr
vararr=[2011,11,24];new(Function.prototype.bind.apply(Date,[null].concat(arr)))
Die vorherige manuelle Umgehung ist von einer Bibliotheksmethode von Mozilla inspiriert. Das Folgende ist eine leicht bearbeitete Version davon
if(!Function.prototype.construct){Function.prototype.construct=function(argArray){if(!Array.isArray(argArray)){thrownewTypeError("Argument must be an array");}varconstr=this;varnullaryFunc=Function.prototype.bind.apply(constr,[null].concat(argArray));returnnewnullaryFunc();};}
Hier ist die Methode in Gebrauch
> Date.construct([2011, 11, 24]) Sat Dec 24 2011 00:00:00 GMT+0100 (CET)
Eine Alternative zum vorherigen Ansatz ist es, eine nicht initialisierte Instanz über Object.create() zu erstellen und dann den Konstruktor (als Funktion) über apply() aufzurufen. Das bedeutet, dass Sie effektiv den new-Operator neu implementieren (einige Prüfungen werden weggelassen)
Function.prototype.construct=function(argArray){varconstr=this;varinst=Object.create(constr.prototype);varresult=constr.apply(inst,argArray);// (1)// Check: did the constructor return an object// and prevent `this` from being the result?returnresult?result:inst;};
Der vorherige Code funktioniert nicht für die meisten eingebauten Konstruktoren, die immer neue Instanzen erzeugen, wenn sie als Funktionen aufgerufen werden. Das heißt, Schritt (1) richtet inst nicht wie gewünscht ein.
Wenn Sie eine Methode aus einem Objekt extrahieren, wird sie wieder zu einer echten Funktion. Ihre Verbindung zum Objekt ist getrennt, und sie funktioniert normalerweise nicht mehr richtig. Nehmen wir zum Beispiel das folgende Objekt, counter:
varcounter={count:0,inc:function(){this.count++;}}
Das Extrahieren von inc und dessen Aufruf (als Funktion!) schlägt fehl
> var func = counter.inc; > func() > counter.count // didn’t work 0
Hier ist die Erklärung: Wir haben den Wert von counter.inc als Funktion aufgerufen. Daher ist this das globale Objekt und wir haben window.count++ ausgeführt. window.count existiert nicht und ist undefined. Das Anwenden des Operators ++ darauf setzt ihn auf NaN
> count // global variable NaN
Wenn die Methode inc() im strict mode ist, erhalten Sie eine Warnung
> counter.inc = function () { 'use strict'; this.count++ };
> var func2 = counter.inc;
> func2()
TypeError: Cannot read property 'count' of undefinedDer Grund ist, dass wenn wir die Strict-Mode-Funktion func2 aufrufen, this undefined ist, was zu einem Fehler führt.
Dank bind() können wir sicherstellen, dass inc die Verbindung zu counter nicht verliert
> var func3 = counter.inc.bind(counter); > func3() > counter.count // it worked! 1
In JavaScript gibt es viele Funktionen und Methoden, die Callbacks akzeptieren. Beispiele in Browsern sind setTimeout() und Ereignisbehandlung. Wenn wir counter.inc als Callback übergeben, wird es ebenfalls als Funktion aufgerufen, was zum gleichen Problem führt, das gerade beschrieben wurde. Um dieses Phänomen zu veranschaulichen, verwenden wir eine einfache Callback-aufrufende Funktion:
functioncallIt(callback){callback();}
Das Ausführen von counter.count über callIt löst eine Warnung aus (wegen strict mode)
> callIt(counter.inc) TypeError: Cannot read property 'count' of undefined
Wie zuvor beheben wir die Dinge mit bind()
> callIt(counter.inc.bind(counter)) > counter.count // one more than before 2
Jeder Aufruf von bind() erstellt eine neue Funktion. Das hat Konsequenzen, wenn Sie Callbacks registrieren und abmelden (z. B. für die Ereignisbehandlung). Sie müssen den Wert, den Sie registriert haben, irgendwo speichern und auch zum Abmelden verwenden.
Sie verschachteln häufig Funktionsdefinitionen in JavaScript, da Funktionen Parameter sein können (z. B. Callbacks) und da sie an Ort und Stelle erstellt werden können, über Funktionsausdrücke. Dies birgt ein Problem, wenn eine Methode eine normale Funktion enthält und Sie das this der ersteren innerhalb der letzteren zugreifen möchten, da das this der Methode von this der normalen Funktion überschattet wird (die nicht einmal einen eigenen this benötigt). Im folgenden Beispiel versucht die Funktion bei (1), das this der Methode bei (2) zu erreichen:
varobj={name:'Jane',friends:['Tarzan','Cheeta'],loop:function(){'use strict';this.friends.forEach(function(friend){// (1)console.log(this.name+' knows '+friend);// (2)});}};
Dies schlägt fehl, da die Funktion bei (1) ihr eigenes this hat, das hier undefined ist
> obj.loop(); TypeError: Cannot read property 'name' of undefined
Es gibt drei Möglichkeiten, dieses Problem zu umgehen.
Wir weisen this einer Variablen zu, die innerhalb der verschachtelten Funktion nicht überschattet wird
loop:function(){'use strict';varthat=this;this.friends.forEach(function(friend){console.log(that.name+' knows '+friend);});}
Hier ist die Interaktion
> obj.loop(); Jane knows Tarzan Jane knows Cheeta
Wir können bind() verwenden, um dem Callback einen festen Wert für this zu geben – nämlich das this der Methode (Zeile (1)):
loop:function(){'use strict';this.friends.forEach(function(friend){console.log(this.name+' knows '+friend);}.bind(this));// (1)}
Eine Umgehung, die spezifisch für forEach() ist (siehe Untersuchungsmethoden), ist, nach dem Callback einen zweiten Parameter anzugeben, der zu dem this des Callbacks wird:
loop:function(){'use strict';this.friends.forEach(function(friend){console.log(this.name+' knows '+friend);},this);}
Die Prototyp-Beziehung zwischen zwei Objekten betrifft die Vererbung: jedes Objekt kann ein anderes Objekt als seinen Prototyp haben. Dann erbt das erstere Objekt alle Eigenschaften seines Prototyps. Ein Objekt gibt seinen Prototyp über die interne Eigenschaft [[Prototype]] an. Jedes Objekt hat diese Eigenschaft, aber sie kann null sein. Die Kette von Objekten, die durch die [[Prototype]]-Eigenschaft verbunden sind, wird als Prototyp-Kette bezeichnet (Abbildung 17-1).
Um zu sehen, wie die Prototyp-basierte (oder prototypische) Vererbung funktioniert, betrachten wir ein Beispiel (mit erfundener Syntax zur Angabe der [[Prototype]]-Eigenschaft)
varproto={describe:function(){return'name: '+this.name;}};varobj={[[Prototype]]:proto,name:'obj'};
Das Objekt obj erbt die Eigenschaft describe von proto. Es hat auch eine sogenannte eigene (nicht vererbte, direkte) Eigenschaft, name.
obj erbt die Eigenschaft describe; Sie können darauf zugreifen, als ob das Objekt selbst diese Eigenschaft hätte:
> obj.describe [Function]
Immer wenn Sie über obj auf eine Eigenschaft zugreifen, beginnt JavaScript die Suche danach in diesem Objekt und fährt mit seinem Prototyp, dem Prototyp seines Prototyps usw. fort. Deshalb können wir proto.describe über obj.describe abrufen. Die Prototyp-Kette verhält sich, als wäre sie ein einzelnes Objekt. Diese Illusion wird beim Aufruf einer Methode aufrechterhalten: der Wert von this ist immer das Objekt, bei dem die Suche nach der Methode begann, nicht dort, wo die Methode gefunden wurde. Das ermöglicht der Methode den Zugriff auf alle Eigenschaften der Prototyp-Kette. Zum Beispiel
> obj.describe() 'name: obj'
Innerhalb von describe() ist this obj, was der Methode ermöglicht, auf obj.name zuzugreifen.
In einer Prototyp-Kette überschreibt eine Eigenschaft in einem Objekt eine Eigenschaft mit demselben Schlüssel in einem „späteren“ Objekt: die erstere Eigenschaft wird zuerst gefunden. Sie verbirgt die letztere Eigenschaft, auf die nicht mehr zugegriffen werden kann. Als Beispiel überschreiben wir die Methode proto.describe() in obj:
> obj.describe = function () { return 'overridden' };
> obj.describe()
'overridden'Das ist ähnlich wie das Überschreiben von Methoden in klassenbasierten Sprachen funktioniert.
Prototypen sind hervorragend zum Teilen von Daten zwischen Objekten geeignet: mehrere Objekte erhalten denselben Prototyp, der alle gemeinsamen Eigenschaften enthält. Betrachten wir ein Beispiel. Die Objekte jane und tarzan enthalten beide dieselbe Methode, describe(). Das ist etwas, das wir durch Teilen vermeiden möchten:
varjane={name:'Jane',describe:function(){return'Person named '+this.name;}};vartarzan={name:'Tarzan',describe:function(){return'Person named '+this.name;}};
Beide Objekte sind Personen. Ihre name-Eigenschaft ist unterschiedlich, aber wir könnten sie die Methode describe teilen lassen. Das tun wir, indem wir einen gemeinsamen Prototyp namens PersonProto erstellen und describe darin platzieren (Abbildung 17-2).
Der folgende Code erstellt die Objekte jane und tarzan, die sich den Prototyp PersonProto teilen
varPersonProto={describe:function(){return'Person named '+this.name;}};varjane={[[Prototype]]:PersonProto,name:'Jane'};vartarzan={[[Prototype]]:PersonProto,name:'Tarzan'};
Und hier ist die Interaktion
> jane.describe() Person named Jane > tarzan.describe() Person named Tarzan
Dies ist ein übliches Muster: die Daten befinden sich im ersten Objekt einer Prototyp-Kette, während Methoden sich in späteren Objekten befinden. JavaScripts Art der prototypischen Vererbung ist darauf ausgelegt, dieses Muster zu unterstützen: das Festlegen einer Eigenschaft beeinflusst nur das erste Objekt in einer Prototyp-Kette, während das Abrufen einer Eigenschaft die gesamte Kette berücksichtigt (siehe Festlegen und Löschen beeinflusst nur eigene Eigenschaften).
Bisher haben wir so getan, als könnten wir auf die interne Eigenschaft [[Prototype]] aus JavaScript zugreifen. Aber die Sprache erlaubt Ihnen das nicht. Stattdessen gibt es Funktionen zum Lesen des Prototyps und zum Erstellen eines neuen Objekts mit einem gegebenen Prototyp.
Object.create(proto,propDescObj?)
erstellt ein Objekt, dessen Prototyp proto ist. Optional können Eigenschaften über Deskriptoren hinzugefügt werden (die in Eigenschaftsdeskriptoren erklärt werden). Im folgenden Beispiel erhält das Objekt jane den Prototyp PersonProto und eine veränderbare Eigenschaft name, deren Wert 'Jane' ist (wie über einen Eigenschaftsdeskriptor angegeben)
varPersonProto={describe:function(){return'Person named '+this.name;}};varjane=Object.create(PersonProto,{name:{value:'Jane',writable:true}});
Hier ist die Interaktion
> jane.describe() 'Person named Jane'
Aber Sie erstellen häufig nur ein leeres Objekt und fügen dann manuell Eigenschaften hinzu, da Deskriptoren umständlich sind
varjane=Object.create(PersonProto);jane.name='Jane';
Object.getPrototypeOf(obj)
gibt den Prototyp von obj zurück. Fortsetzung des vorherigen Beispiels
> Object.getPrototypeOf(jane) === PersonProto true
Object.prototype.isPrototypeOf(obj)
prüft, ob der Empfänger der Methode ein (direkter oder indirekter) Prototyp von obj ist. Mit anderen Worten: befinden sich der Empfänger und obj in derselben Prototyp-Kette, und kommt obj vor dem Empfänger? Zum Beispiel
> var A = {};
> var B = Object.create(A);
> var C = Object.create(B);
> A.isPrototypeOf(C)
true
> C.isPrototypeOf(A)
falseDie folgende Funktion iteriert über die Eigenschaftskette eines Objekts obj. Sie gibt das erste Objekt zurück, das eine eigene Eigenschaft mit dem Schlüssel propKey hat, oder null, wenn kein solches Objekt existiert:
functiongetDefiningObject(obj,propKey){obj=Object(obj);// make sure it’s an objectwhile(obj&&!{}.hasOwnProperty.call(obj,propKey)){obj=Object.getPrototypeOf(obj);// obj is null if we have reached the end}returnobj;}
Im vorherigen Code haben wir die Methode Object.prototype.hasOwnProperty generisch aufgerufen (siehe Generische Methoden: Methoden von Prototypen ausleihen).
Einige JavaScript-Engines haben eine spezielle Eigenschaft zum Abrufen und Festlegen des Prototyps eines Objekts: __proto__. Sie bringt direkten Zugriff auf [[Prototype]] in die Sprache:
> var obj = {};
> obj.__proto__ === Object.prototype
true
> obj.__proto__ = Array.prototype
> Object.getPrototypeOf(obj) === Array.prototype
trueEs gibt mehrere Dinge, die Sie über __proto__ wissen müssen
__proto__ wird „dunder proto“ ausgesprochen, eine Abkürzung für „double underscore proto“. Diese Aussprache wurde aus der Programmiersprache Python übernommen (wie von Ned Batchelder im Jahr 2006 vorgeschlagen). Spezielle Variablen mit doppelten Unterstrichen sind in Python recht häufig.__proto__ ist kein Teil des ECMAScript 5-Standards. Daher dürfen Sie es nicht verwenden, wenn Ihr Code diesem Standard entsprechen und zuverlässig über aktuelle JavaScript-Engines laufen soll.__proto__ und es wird Teil von ECMAScript 6 sein.Der folgende Ausdruck prüft, ob eine Engine __proto__ als spezielle Eigenschaft unterstützt
Object.getPrototypeOf({__proto__:null})===null
Nur das Abrufen einer Eigenschaft berücksichtigt die vollständige Prototyp-Kette eines Objekts. Das Festlegen und Löschen ignoriert Vererbung und betrifft nur eigene Eigenschaften.
Das Festlegen einer Eigenschaft erstellt eine eigene Eigenschaft, auch wenn eine geerbte Eigenschaft mit diesem Schlüssel existiert. Zum Beispiel, bei folgendem Quellcode:
varproto={foo:'a'};varobj=Object.create(proto);
obj erbt foo von proto
> obj.foo
'a'
> obj.hasOwnProperty('foo')
falseDas Festlegen von foo hat das gewünschte Ergebnis
> obj.foo = 'b'; > obj.foo 'b'
Wir haben jedoch eine eigene Eigenschaft erstellt und proto.foo nicht geändert
> obj.hasOwnProperty('foo')
true
> proto.foo
'a'Der Grund dafür ist, dass Prototyp-Eigenschaften dazu bestimmt sind, von mehreren Objekten geteilt zu werden. Dieser Ansatz erlaubt es uns, sie nicht-destruktiv zu „ändern“ – nur das aktuelle Objekt ist betroffen.
Sie können nur eigene Eigenschaften löschen. Richten wir wieder ein Objekt, obj, mit einem Prototyp, proto, ein:
varproto={foo:'a'};varobj=Object.create(proto);
Das Löschen der geerbten Eigenschaft foo hat keine Auswirkung
> delete obj.foo true > obj.foo 'a'
Für weitere Informationen über den Operator delete konsultieren Sie Löschen von Eigenschaften.
Wenn Sie eine geerbte Eigenschaft ändern möchten, müssen Sie zuerst das Objekt finden, das sie besitzt (siehe Finden des Objekts, in dem eine Eigenschaft definiert ist) und dann die Änderung an diesem Objekt vornehmen. Zum Beispiel löschen wir die Eigenschaft foo aus dem vorherigen Beispiel
> delete getDefiningObject(obj, 'foo').foo; true > obj.foo undefined
Operationen zum Iterieren über und Erkennen von Eigenschaften werden beeinflusst durch:
true oder false sein kann. Aufzählbarkeit ist selten wichtig und kann normalerweise ignoriert werden (siehe Aufzählbarkeit: Best Practices).Sie können eigene Eigenschaftsschlüssel auflisten, alle aufzählbaren Eigenschaftsschlüssel auflisten und prüfen, ob eine Eigenschaft existiert. Die folgenden Unterabschnitte zeigen, wie.
Sie können entweder alle eigenen Eigenschaftsschlüssel auflisten oder nur die aufzählbaren:
Object.getOwnPropertyNames(obj) gibt die Schlüssel aller eigenen Eigenschaften von obj zurück.Object.keys(obj) gibt die Schlüssel aller aufzählbaren eigenen Eigenschaften von obj zurück. Beachten Sie, dass Eigenschaften normalerweise aufzählbar sind (siehe Aufzählbarkeit: Best Practices), sodass Sie Object.keys() insbesondere für Objekte verwenden können, die Sie selbst erstellt haben.
Wenn Sie alle Eigenschaften (sowohl eigene als auch geerbte) eines Objekts auflisten möchten, haben Sie zwei Möglichkeiten.
Option 1 ist die Verwendung der Schleife
for(«variable»in«object»)«statement»
zum Iterieren über die Schlüssel aller aufzählbaren Eigenschaften von object. Siehe for-in für eine ausführlichere Beschreibung.
Option 2 ist die Implementierung einer eigenen Funktion, die über alle Eigenschaften (nicht nur aufzählbare) iteriert. Zum Beispiel
functiongetAllPropertyNames(obj){varresult=[];while(obj){// Add the own property names of `obj` to `result`result=result.concat(Object.getOwnPropertyNames(obj));obj=Object.getPrototypeOf(obj);}returnresult;}
Sie können prüfen, ob ein Objekt eine Eigenschaft hat, oder ob eine Eigenschaft direkt innerhalb eines Objekts existiert:
propKey in obj
true zurück, wenn obj eine Eigenschaft mit dem Schlüssel propKey hat. Vererbte Eigenschaften sind in dieser Prüfung enthalten.Object.prototype.hasOwnProperty(propKey)
true zurück, wenn der Empfänger (this) eine eigene (nicht vererbte) Eigenschaft mit dem Schlüssel propKey hat. Vermeiden Sie den direkten Aufruf von hasOwnProperty() auf einem Objekt, da er überschrieben werden kann (z. B. durch eine eigene Eigenschaft mit dem Schlüssel hasOwnProperty)
> var obj = { hasOwnProperty: 1, foo: 2 };
> obj.hasOwnProperty('foo') // unsafe
TypeError: Property 'hasOwnProperty' is not a functionEs ist besser, ihn generisch aufzurufen (siehe Generische Methoden: Methoden von Prototypen ausleihen)
> Object.prototype.hasOwnProperty.call(obj, 'foo') // safe
true
> {}.hasOwnProperty.call(obj, 'foo') // shorter
trueDie folgenden Beispiele basieren auf diesen Definitionen:
varproto=Object.defineProperties({},{protoEnumTrue:{value:1,enumerable:true},protoEnumFalse:{value:2,enumerable:false}});varobj=Object.create(proto,{objEnumTrue:{value:1,enumerable:true},objEnumFalse:{value:2,enumerable:false}});
Object.defineProperties() wird in Properties über Deskriptoren abrufen und definieren erklärt, aber es sollte ziemlich offensichtlich sein, wie es funktioniert: proto hat die eigenen Eigenschaften protoEnumTrue und protoEnumFalse und obj hat die eigenen Eigenschaften objEnumTrue und objEnumFalse (und erbt alle Eigenschaften von proto).
Beachten Sie, dass Objekte (wie proto im vorherigen Beispiel) normalerweise mindestens den Prototyp Object.prototype haben (wo Standardmethoden wie toString() und hasOwnProperty() definiert sind)
> Object.getPrototypeOf({}) === Object.prototype
trueUnter den property-bezogenen Operationen beeinflusst die Enumerabilität nur die for-in-Schleife und Object.keys() (sie beeinflusst auch JSON.stringify(), siehe JSON.stringify(value, replacer?, space?)).
Die for-in-Schleife iteriert über die Schlüssel aller aufzählbaren Eigenschaften, einschließlich der vererbten (beachten Sie, dass keine der nicht aufzählbaren Eigenschaften von Object.prototype angezeigt wird)
> for (var x in obj) console.log(x); objEnumTrue protoEnumTrue
Object.keys() gibt die Schlüssel aller eigenen (nicht vererbten) aufzählbaren Eigenschaften zurück
> Object.keys(obj) [ 'objEnumTrue' ]
Wenn Sie die Schlüssel aller eigenen Eigenschaften wünschen, müssen Sie Object.getOwnPropertyNames() verwenden
> Object.getOwnPropertyNames(obj) [ 'objEnumTrue', 'objEnumFalse' ]
Nur die for-in-Schleife (siehe vorheriges Beispiel) und der in-Operator berücksichtigen die Vererbung:
> 'toString' in obj
true
> obj.hasOwnProperty('toString')
false
> obj.hasOwnProperty('objEnumFalse')
trueObjekte haben keine Methode wie length oder size, daher müssen Sie den folgenden Workaround verwenden:
Object.keys(obj).length
Um über Property-Schlüssel zu iterieren:
Kombinieren Sie for-in mit hasOwnProperty(), wie in for-in beschrieben. Dies funktioniert auch auf älteren JavaScript-Engines. Zum Beispiel
for(varkeyinobj){if(Object.prototype.hasOwnProperty.call(obj,key)){console.log(key);}}
Kombinieren Sie Object.keys() oder Object.getOwnPropertyNames() mit der Array-Iteration forEach()
varobj={first:'John',last:'Doe'};// Visit non-inherited enumerable keysObject.keys(obj).forEach(function(key){console.log(key);});
Um über Property-Werte oder über (Schlüssel, Wert)-Paare zu iterieren
ECMAScript 5 ermöglicht es Ihnen, Methoden zu schreiben, deren Aufrufe so aussehen, als würden Sie eine Eigenschaft abrufen oder setzen. Das bedeutet, dass eine Eigenschaft virtuell ist und kein Speicherplatz. Sie könnten zum Beispiel das Setzen einer Eigenschaft verbieten und immer den Wert berechnen, der beim Lesen zurückgegeben wird.
Das folgende Beispiel verwendet ein Objekt-Literal, um einen Setter und einen Getter für die Eigenschaft foo zu definieren:
varobj={getfoo(){return'getter';},setfoo(value){console.log('setter: '+value);}};
Hier ist die Interaktion
> obj.foo = 'bla'; setter: bla > obj.foo 'getter'
Eine alternative Möglichkeit, Getter und Setter zu spezifizieren, sind Property-Deskriptoren (siehe Property Deskriptoren). Der folgende Code definiert dasselbe Objekt wie das vorherige Literal
varobj=Object.create(Object.prototype,{// object with property descriptorsfoo:{// property descriptorget:function(){return'getter';},set:function(value){console.log('setter: '+value);}}});
Getter und Setter werden von Prototypen geerbt:
> var proto = { get foo() { return 'hello' } };
> var obj = Object.create(proto);
> obj.foo
'hello'Property-Attribute und Property-Deskriptoren sind ein fortgeschrittenes Thema. Sie müssen normalerweise nicht wissen, wie sie funktionieren.
In diesem Abschnitt betrachten wir die interne Struktur von Properties:
Der gesamte Zustand eines Properties, sowohl seine Daten als auch seine Metadaten, wird in Attributen gespeichert. Dies sind Felder, die ein Property hat, ähnlich wie ein Objekt Properties hat. Attributschlüssel werden oft in doppelten Klammern geschrieben. Attribute sind für normale Properties und für Accessors (Getter und Setter) wichtig.
Die folgenden Attribute sind spezifisch für normale Properties
[[Value]] enthält den Wert des Properties, seine Daten.[[Writable]] enthält einen booleschen Wert, der angibt, ob der Wert eines Properties geändert werden kann.Die folgenden Attribute sind spezifisch für Accessors:
[[Get]] enthält den Getter, eine Funktion, die aufgerufen wird, wenn ein Property gelesen wird. Die Funktion berechnet das Ergebnis des Lesezugriffs.[[Set]] enthält den Setter, eine Funktion, die aufgerufen wird, wenn einem Property ein Wert zugewiesen wird. Die Funktion empfängt diesen Wert als Parameter.Alle Properties haben die folgenden Attribute:
[[Enumerable]] enthält einen booleschen Wert. Wenn ein Property als nicht aufzählbar markiert wird, wird es von einigen Operationen ausgeblendet (siehe Iteration und Erkennung von Properties).[[Configurable]] enthält einen booleschen Wert. Wenn er false ist, können Sie ein Property nicht löschen, seine Attribute (außer [[Value]]) nicht ändern oder es von einem Datenproperty in ein Accessor-Property umwandeln oder umgekehrt. Mit anderen Worten, [[Configurable]] steuert die Schreibbarkeit der Metadaten eines Properties. Es gibt eine Ausnahme von dieser Regel – JavaScript erlaubt es, ein nicht konfigurierbares Property von beschreibbar auf schreibgeschützt zu ändern, aus historischen Gründen; das Property length von Arrays ist immer beschreibbar und nicht konfigurierbar gewesen. Ohne diese Ausnahme könnten Sie Arrays nicht einfrieren (siehe Einfrieren).Wenn Sie keine Attribute angeben, werden die folgenden Standardwerte verwendet:
| Attributschlüssel | Standardwert |
|
|
|
|
|
|
|
|
|
|
|
|
Diese Standardwerte sind wichtig, wenn Sie Properties über Property-Deskriptoren erstellen (siehe nächster Abschnitt).
{value:123,writable:false,enumerable:true,configurable:false}
Sie können dasselbe Ziel, Unveränderlichkeit, über Accessors erreichen. Dann sieht der Deskriptor wie folgt aus
{get:function(){return123},enumerable:true,configurable:false}
Property-Deskriptoren werden für zwei Arten von Operationen verwendet:
Das Definieren eines Properties bedeutet je nachdem, ob ein Property bereits existiert, etwas anderes
Wenn ein Property nicht existiert, erstellen Sie ein neues Property, dessen Attribute durch den Deskriptor spezifiziert sind. Wenn ein Attribut keine entsprechende Eigenschaft im Deskriptor hat, verwenden Sie den Standardwert. Die Standardwerte ergeben sich aus der Bedeutung der Attributnamen. Sie sind das Gegenteil der Werte, die beim Erstellen eines Properties durch Zuweisung verwendet werden (dann ist das Property beschreibbar, aufzählbar und konfigurierbar). Zum Beispiel:
> var obj = {};
> Object.defineProperty(obj, 'foo', { configurable: true });
> Object.getOwnPropertyDescriptor(obj, 'foo')
{ value: undefined,
writable: false,
enumerable: false,
configurable: true }Ich verlasse mich normalerweise nicht auf die Standardwerte und gebe alle Attribute explizit an, um vollständig klar zu sein.
Wenn ein Property bereits existiert, aktualisieren Sie die Attribute des Properties gemäß dem Deskriptor. Wenn ein Attribut keine entsprechende Eigenschaft im Deskriptor hat, ändern Sie es nicht. Hier ist ein Beispiel (Fortsetzung des vorherigen)
> Object.defineProperty(obj, 'foo', { writable: true });
> Object.getOwnPropertyDescriptor(obj, 'foo')
{ value: undefined,
writable: true,
enumerable: false,
configurable: true }Die folgenden Operationen ermöglichen es Ihnen, die Attribute eines Properties über Property-Deskriptoren abzurufen und festzulegen:
Object.getOwnPropertyDescriptor(obj, propKey)
Gibt den Deskriptor des eigenen (nicht vererbten) Properties von obj zurück, dessen Schlüssel propKey ist. Wenn kein solches Property existiert, wird undefined zurückgegeben.
> Object.getOwnPropertyDescriptor(Object.prototype, 'toString')
{ value: [Function: toString],
writable: true,
enumerable: false,
configurable: true }
> Object.getOwnPropertyDescriptor({}, 'toString')
undefinedObject.defineProperty(obj, propKey, propDesc)
Erstellt oder ändert ein Property von obj, dessen Schlüssel propKey ist und dessen Attribute über propDesc spezifiziert sind. Gibt das geänderte Objekt zurück. Zum Beispiel
varobj=Object.defineProperty({},'foo',{value:123,enumerable:true// writable: false (default value)// configurable: false (default value)});
Object.defineProperties(obj, propDescObj)
varobj=Object.defineProperties({},{foo:{value:123,enumerable:true},bar:{value:'abc',enumerable:true}});
Object.create(proto, propDescObj?)
Erstellen Sie zuerst ein Objekt, dessen Prototyp proto ist. Fügen Sie dann, falls der optionale Parameter propDescObj angegeben wurde, diesem Properties hinzu – auf die gleiche Weise wie Object.defineProperties. Geben Sie schließlich das Ergebnis zurück. Das folgende Code-Snippet erzeugt zum Beispiel dasselbe Ergebnis wie das vorherige Snippet
varobj=Object.create(Object.prototype,{foo:{value:123,enumerable:true},bar:{value:'abc',enumerable:true}});
Um eine identische Kopie eines Objekts zu erstellen, müssen Sie zwei Dinge richtig machen:
Die folgende Funktion führt eine solche Kopie durch
functioncopyObject(orig){// 1. copy has same prototype as origvarcopy=Object.create(Object.getPrototypeOf(orig));// 2. copy has all of orig’s propertiescopyOwnPropertiesFrom(copy,orig);returncopy;}
Die Eigenschaften werden mit dieser Funktion von orig nach copy kopiert
functioncopyOwnPropertiesFrom(target,source){Object.getOwnPropertyNames(source)// (1).forEach(function(propKey){// (2)vardesc=Object.getOwnPropertyDescriptor(source,propKey);// (3)Object.defineProperty(target,propKey,desc);// (4)});returntarget;};
Dies sind die beteiligten Schritte
source.target zu erstellen.Beachten Sie, dass diese Funktion der Funktion _.extend() in der Underscore.js-Bibliothek sehr ähnlich ist.
Die folgenden beiden Operationen sind sehr ähnlich:
defineProperty() und defineProperties() (siehe Properties über Deskriptoren abrufen und definieren).=.Es gibt jedoch einige subtile Unterschiede
Das Zuweisen zu einem Property prop bedeutet, ein bestehendes Property zu ändern. Der Prozess ist wie folgt:
prop ein Setter ist (eigen oder geerbt), rufen Sie diesen Setter auf.prop schreibgeschützt ist (eigen oder geerbt), lösen Sie eine Ausnahme aus (im strikten Modus) oder tun Sie nichts (im schludrigen Modus). Der nächste Abschnitt erklärt dieses (etwas unerwartete) Phänomen ausführlicher.prop ein eigenes (und beschreibbares) Property ist, ändern Sie den Wert dieses Properties.prop, oder es ist geerbt und beschreibbar. In beiden Fällen wird ein eigenes Property prop definiert, das beschreibbar, konfigurierbar und aufzählbar ist. Im letzteren Fall haben wir gerade ein geerbtes Property überschrieben (nichtdestruktiv geändert). Im ersteren Fall wurde ein fehlendes Property automatisch definiert. Diese Art der automatischen Definition ist problematisch, da Tippfehler bei Zuweisungen schwer zu erkennen sein können.Wenn ein Objekt, obj, ein Property, foo, von einem Prototyp erbt und foo nicht beschreibbar ist, dann können Sie obj.foo nicht zuweisen:
varproto=Object.defineProperty({},'foo',{value:'a',writable:false});varobj=Object.create(proto);
obj erbt das schreibgeschützte Property foo von proto. Im schludrigen Modus hat das Setzen des Properties keine Auswirkung
> obj.foo = 'b'; > obj.foo 'a'
Im strikten Modus erhalten Sie eine Ausnahme
> (function () { 'use strict'; obj.foo = 'b' }());
TypeError: Cannot assign to read-only property 'foo'Dies passt zu der Vorstellung, dass Zuweisungen geerbte Properties ändern, aber nichtdestruktiv. Wenn ein geerbtes Property schreibgeschützt ist, möchten Sie alle Änderungen verbieten, auch die nichtdestruktiven.
Beachten Sie, dass Sie diesen Schutz umgehen können, indem Sie ein eigenes Property definieren (siehe vorheriger Unterabschnitt über den Unterschied zwischen Definition und Zuweisung)
> Object.defineProperty(obj, 'foo', { value: 'b' });
> obj.foo
'b'Die allgemeine Regel ist, dass von Systemen erstellte Properties nicht aufzählbar sind, während von Benutzern erstellte Properties aufzählbar sind:
> Object.keys([]) [] > Object.getOwnPropertyNames([]) [ 'length' ] > Object.keys(['a']) [ '0' ]
Dies gilt insbesondere für die Methoden der eingebauten Instanzprototypen
> Object.keys(Object.prototype) [] > Object.getOwnPropertyNames(Object.prototype) [ hasOwnProperty', 'valueOf', 'constructor', 'toLocaleString', 'isPrototypeOf', 'propertyIsEnumerable', 'toString' ]
Der Hauptzweck der Enumerabilität ist es, der for-in-Schleife mitzuteilen, welche Properties sie ignorieren soll. Wie wir gerade gesehen haben, als wir Instanzen von eingebauten Konstruktoren betrachtet haben, wird alles, was nicht vom Benutzer erstellt wurde, von for-in ausgeblendet.
Die einzigen Operationen, die von der Enumerabilität betroffen sind, sind
for-in-SchleifeObject.keys() (Eigene Property-Schlüssel auflisten)JSON.stringify() (JSON.stringify(value, replacer?, space?))Hier sind einige Best Practices, die Sie beachten sollten
for-in-Schleife vermeiden (Best Practices: Iterieren über Arrays).Es gibt drei Stufen des Schutzes eines Objekts, hier von der schwächsten zur stärksten aufgelistet:
Das Verhindern von Erweiterungen über:
Object.preventExtensions(obj)
macht es unmöglich, Properties zu obj hinzuzufügen. Zum Beispiel
varobj={foo:'a'};Object.preventExtensions(obj);
Nun schlägt das Hinzufügen eines Properties im schludrigen Modus stillschweigend fehl
> obj.bar = 'b'; > obj.bar undefined
und wirft im strikten Modus einen Fehler
> (function () { 'use strict'; obj.bar = 'b' }());
TypeError: Can't add property bar, object is not extensibleSie können Properties jedoch immer noch löschen
> delete obj.foo true > obj.foo undefined
Sie überprüfen, ob ein Objekt erweiterbar ist, über
Object.isExtensible(obj)
Versiegeln über
Object.seal(obj)
verhindert Erweiterungen und macht alle Properties „unkonfigurierbar“. Letzteres bedeutet, dass die Attribute (siehe Property-Attribute und Property-Deskriptoren) von Properties nicht mehr geändert werden können. Zum Beispiel bleiben schreibgeschützte Properties für immer schreibgeschützt.
Das folgende Beispiel zeigt, dass das Versiegeln alle Properties unkonfigurierbar macht
> var obj = { foo: 'a' };
> Object.getOwnPropertyDescriptor(obj, 'foo') // before sealing
{ value: 'a',
writable: true,
enumerable: true,
configurable: true }
> Object.seal(obj)
> Object.getOwnPropertyDescriptor(obj, 'foo') // after sealing
{ value: 'a',
writable: true,
enumerable: true,
configurable: false }Sie können das Property foo immer noch ändern
> obj.foo = 'b'; 'b' > obj.foo 'b'
aber Sie können seine Attribute nicht ändern
> Object.defineProperty(obj, 'foo', { enumerable: false });
TypeError: Cannot redefine property: fooSie überprüfen, ob ein Objekt versiegelt ist, über
Object.isSealed(obj)
Einfrieren wird durchgeführt über:
Object.freeze(obj)
varpoint={x:17,y:-5};Object.freeze(point);
Auch hier erhalten Sie stille Fehler im schludrigen Modus
> point.x = 2; // no effect, point.x is read-only
> point.x
17
> point.z = 123; // no effect, point is not extensible
> point
{ x: 17, y: -5 }Und Sie erhalten Fehler im strikten Modus
> (function () { 'use strict'; point.x = 2 }());
TypeError: Cannot assign to read-only property 'x'
> (function () { 'use strict'; point.z = 123 }());
TypeError: Can't add property z, object is not extensibleSie überprüfen, ob ein Objekt eingefroren ist, über
Object.isFrozen(obj)
Das Schützen eines Objekts ist flach: es betrifft die eigenen Properties, aber nicht die Werte dieser Properties. Betrachten Sie zum Beispiel das folgende Objekt:
varobj={foo:1,bar:['a','b']};Object.freeze(obj);
Obwohl Sie obj eingefroren haben, ist es nicht vollständig unveränderlich – Sie können den (veränderlichen) Wert des Properties bar ändern
> obj.foo = 2; // no effect
> obj.bar.push('c'); // changes obj.bar
> obj
{ foo: 1, bar: [ 'a', 'b', 'c' ] }Zusätzlich hat obj den Prototyp Object.prototype, der ebenfalls veränderlich ist.
Eine Konstruktorfunktion (kurz: Konstruktor) hilft bei der Erzeugung von Objekten, die in gewisser Weise ähnlich sind. Es ist eine normale Funktion, wird aber anders benannt, eingerichtet und aufgerufen. Dieser Abschnitt erklärt, wie Konstruktoren funktionieren. Sie entsprechen Klassen in anderen Sprachen.
Wir haben bereits ein Beispiel für zwei ähnliche Objekte gesehen (in Daten zwischen Objekten über einen Prototyp teilen)
varPersonProto={describe:function(){return'Person named '+this.name;}};varjane={[[Prototype]]:PersonProto,name:'Jane'};vartarzan={[[Prototype]]:PersonProto,name:'Tarzan'};
Die Objekte jane und tarzan werden beide als „Personen“ betrachtet und teilen sich das Prototypobjekt PersonProto. Wandeln wir diesen Prototyp in einen Konstruktor Person um, der Objekte wie jane und tarzan erstellt. Die Objekte, die ein Konstruktor erstellt, werden als seine Instanzen bezeichnet. Solche Instanzen haben dieselbe Struktur wie jane und tarzan und bestehen aus zwei Teilen
jane und tarzan im vorherigen Beispiel).PersonProto im vorherigen Beispiel).Ein Konstruktor ist eine Funktion, die über den new-Operator aufgerufen wird. Nach Konvention beginnen die Namen von Konstruktoren mit Großbuchstaben, während die Namen von normalen Funktionen und Methoden mit Kleinbuchstaben beginnen. Die Funktion selbst richtet Teil 1 ein
functionPerson(name){this.name=name;}
Das Objekt in Person.prototype wird zum Prototyp aller Instanzen von Person. Es trägt Teil 2 bei
Person.prototype.describe=function(){return'Person named '+this.name;};
Erstellen und verwenden wir eine Instanz von Person
> var jane = new Person('Jane');
> jane.describe()
'Person named Jane'Wir sehen, dass Person eine normale Funktion ist. Sie wird nur dann zu einem Konstruktor, wenn sie über new aufgerufen wird. Der new-Operator führt folgende Schritte aus:
Person. prototype ist.Person erhält dieses Objekt als impliziten Parameter this und fügt Instanzeigenschaften hinzu.Abbildung 17-3 zeigt, wie die Instanz jane aussieht. Die Eigenschaft constructor von Person.prototype zeigt zurück auf den Konstruktor und wird in Die constructor-Eigenschaft von Instanzen erläutert.
Der Operator instanceof ermöglicht uns zu überprüfen, ob ein Objekt eine Instanz eines bestimmten Konstruktors ist
> jane instanceof Person true > jane instanceof Date false
Wenn Sie den new-Operator manuell implementieren würden, sähe er ungefähr so aus:
functionnewOperator(Constr,args){varthisValue=Object.create(Constr.prototype);// (1)varresult=Constr.apply(thisValue,args);if(typeofresult==='object'&&result!==null){returnresult;// (2)}returnthisValue;}
In Zeile (1) sehen Sie, dass der Prototyp einer von einem Konstruktor Constr erstellten Instanz Constr.prototype ist.
Zeile (2) offenbart ein weiteres Merkmal des new-Operators: Sie können ein beliebiges Objekt aus einem Konstruktor zurückgeben, und es wird zum Ergebnis des new-Operators. Dies ist nützlich, wenn Sie möchten, dass ein Konstruktor eine Instanz eines Unterkonstruktors zurückgibt (ein Beispiel finden Sie in Beliebige Objekte von einem Konstruktor zurückgeben).
Leider wird der Begriff Prototyp in JavaScript mehrdeutig verwendet:
Ein Objekt kann der Prototyp eines anderen Objekts sein
> var proto = {};
> var obj = Object.create(proto);
> Object.getPrototypeOf(obj) === proto
trueIm vorherigen Beispiel ist proto der Prototyp von obj.
prototype-EigenschaftJeder Konstruktor C hat eine prototype-Eigenschaft, die auf ein Objekt verweist. Dieses Objekt wird zum Prototyp aller Instanzen von C:
> function C() {}
> Object.getPrototypeOf(new C()) === C.prototype
trueNormalerweise macht der Kontext klar, welcher der beiden Prototypen gemeint ist. Sollte eine Klärung notwendig sein, dann sind wir mit Prototyp zur Beschreibung der Beziehung zwischen Objekten festgefahren, da dieser Name über getPrototypeOf und isPrototypeOf in die Standardbibliothek gelangt ist. Wir müssen also einen anderen Namen für das von der prototype-Eigenschaft referenzierte Objekt finden. Eine Möglichkeit ist Konstruktor-Prototyp, aber das ist problematisch, da Konstruktoren auch Prototypen haben
> function Foo() {}
> Object.getPrototypeOf(Foo) === Function.prototype
trueDaher ist Instanz-Prototyp die beste Option.
Standardmäßig enthält jede Funktion C ein Instanz-Prototyp-Objekt C.prototype, dessen Eigenschaft constructor zurück auf C zeigt:
> function C() {}
> C.prototype.constructor === C
trueDa die constructor-Eigenschaft von jedem Instanz-Prototyp von jeder Instanz geerbt wird, können Sie sie verwenden, um den Konstruktor einer Instanz zu erhalten
> var o = new C(); > o.constructor [Function: C]
In der folgenden catch-Klausel treffen wir unterschiedliche Maßnahmen, abhängig vom Konstruktor der abgefangenen Ausnahme:
try{...}catch(e){switch(e.constructor){caseSyntaxError:...break;caseCustomError:...break;...}}
Dieser Ansatz erkennt nur direkte Instanzen eines bestimmten Konstruktors. Im Gegensatz dazu erkennt instanceof sowohl direkte Instanzen als auch Instanzen aller Unterkonstruktoren.
Zum Beispiel
> function Foo() {}
> var f = new Foo();
> f.constructor.name
'Foo'Nicht alle JavaScript-Engines unterstützen die Eigenschaft name für Funktionen.
So erstellen Sie ein neues Objekt, y, das denselben Konstruktor wie ein vorhandenes Objekt, x, hat
functionConstr(){}varx=newConstr();vary=newx.constructor();console.log(yinstanceofConstr);// true
Dieser Trick ist nützlich für eine Methode, die für Instanzen von Unterkonstruktoren funktionieren muss und eine neue Instanz erstellen möchte, die this ähnelt. Dann können Sie keinen festen Konstruktor verwenden
SuperConstr.prototype.createCopy=function(){returnnewthis.constructor(...);};
Einige Vererbungsbibliotheken weisen dem Subkonstruktor eine Super-Prototyp-Eigenschaft zu. Zum Beispiel bietet das YUI-Framework Unterklassenbildung über Y.extend
functionSuper(){}functionSub(){Sub.superclass.constructor.call(this);// (1)}Y.extend(Sub,Super);
Der Aufruf in Zeile (1) funktioniert, da extend Sub.superclass auf Super.prototype setzt. Dank der constructor-Eigenschaft können Sie den Superkonstruktor als Methode aufrufen.
Der Operator instanceof (siehe Der instanceof-Operator) ist nicht auf die constructor-Eigenschaft angewiesen.
Stellen Sie sicher, dass für jeden Konstruktor C die folgende Aussage gilt:
C.prototype.constructor===C
Standardmäßig hat jede Funktion f bereits eine prototype-Eigenschaft, die korrekt eingerichtet ist
> function f() {}
> f.prototype.constructor === f
trueSie sollten daher vermeiden, dieses Objekt zu ersetzen und nur Eigenschaften dazu hinzuzufügen
// Avoid:C.prototype={method1:function(...){...},...};// Prefer:C.prototype.method1=function(...){...};...
Wenn Sie es ersetzen, sollten Sie den korrekten Wert für constructor manuell zuweisen
C.prototype={constructor:C,method1:function(...){...},...};
Beachten Sie, dass nichts Entscheidendes in JavaScript von der constructor-Eigenschaft abhängt; aber es ist guter Stil, sie einzurichten, da sie die in diesem Abschnitt genannten Techniken ermöglicht.
Der Operator instanceof
valueinstanceofConstr
valueinstanceofConstrConstr.prototype.isPrototypeOf(value)
Hier sind einige Beispiele
> {} instanceof Object
true
> [] instanceof Array // constructor of []
true
> [] instanceof Object // super-constructor of []
true
> new Date() instanceof Date
true
> new Date() instanceof Object
trueWie erwartet, ist instanceof immer false für primitive Werte
> 'abc' instanceof Object false > 123 instanceof Number false
Schließlich löst instanceof eine Ausnahme aus, wenn seine rechte Seite keine Funktion ist
> [] instanceof 123 TypeError: Expecting a function in instanceof check
Fast alle Objekte sind Instanzen von Object, da Object.prototype in ihrer Prototypenkette liegt. Es gibt aber auch Objekte, bei denen das nicht der Fall ist. Hier sind zwei Beispiele:
> Object.create(null) instanceof Object false > Object.prototype instanceof Object false
Das erstere Objekt wird im Detail in Das dict-Muster: Objekte ohne Prototypen sind bessere Maps erläutert. Das letztere Objekt ist dort, wo die meisten Prototypketten enden (und sie müssen irgendwo enden). Beide Objekte haben keinen Prototyp
> Object.getPrototypeOf(Object.create(null)) null > Object.getPrototypeOf(Object.prototype) null
Aber typeof klassifiziert sie korrekt als Objekte
> typeof Object.create(null) 'object' > typeof Object.prototype 'object'
Dieser Fallstrick ist für die meisten Anwendungsfälle von instanceof kein Dealbreaker, aber man muss sich dessen bewusst sein.
In Webbrowsern hat jeder Frame und jedes Fenster seinen eigenen Realm mit separaten globalen Variablen. Das verhindert, dass instanceof für Objekte funktioniert, die Realms überqueren. Um zu sehen, warum, betrachten Sie den folgenden Code:
if(myvarinstanceofArray)...// Doesn’t always work
Wenn myvar ein Array aus einem anderen Realm ist, dann ist sein Prototyp das Array.prototype aus diesem Realm. Daher findet instanceof das Array.prototype des aktuellen Realms nicht in der Prototypenkette von myvar und gibt false zurück. ECMAScript 5 hat die Funktion Array.isArray(), die immer funktioniert:
<head><script>functiontest(arr){variframe=frames[0];console.log(arrinstanceofArray);// falseconsole.log(arrinstanceofiframe.Array);// trueconsole.log(Array.isArray(arr));// true}</script></head><body><iframesrcdoc="<script>window.parent.test([])</script>"></iframe></body>
Offensichtlich ist dies auch ein Problem mit Nicht-Built-in-Konstruktoren.
Abgesehen von der Verwendung von Array.isArray() gibt es mehrere Dinge, die Sie tun können, um dieses Problem zu umgehen
postMessage()-Methode, die ein Objekt in einen anderen Realm kopieren kann, anstatt eine Referenz zu übergeben.Überprüfen Sie den Namen des Konstruktors einer Instanz (funktioniert nur auf Engines, die die Eigenschaft name für Funktionen unterstützen)
someValue.constructor.name==='NameOfExpectedConstructor'
Verwenden Sie eine Prototyp-Eigenschaft, um Instanzen als zu einem Typ T gehörig zu markieren. Es gibt mehrere Möglichkeiten, dies zu tun. Die Prüfungen, ob value eine Instanz von T ist, sehen wie folgt aus
value.isT(): Der Prototyp von T-Instanzen muss von dieser Methode true zurückgeben; ein gemeinsamer Superkonstruktor sollte den Standardwert false zurückgeben.'T' in value: Sie müssen den Prototyp von T-Instanzen mit einer Eigenschaft versehen, deren Schlüssel 'T' (oder etwas Eindeutigeres) ist.value.TYPE_NAME === 'T': Jeder relevante Prototyp muss eine TYPE_NAME-Eigenschaft mit einem geeigneten Wert haben.Dieser Abschnitt gibt einige Tipps zur Implementierung von Konstruktoren.
Wenn Sie new vergessen, wenn Sie einen Konstruktor verwenden, rufen Sie ihn als Funktion und nicht als Konstruktor auf. Im "sloppy mode" erhalten Sie keine Instanz und globale Variablen werden erstellt. Leider geschieht all dies ohne Vorwarnung:
functionSloppyColor(name){this.name=name;}varc=SloppyColor('green');// no warning!// No instance is created:console.log(c);// undefined// A global variable is created:console.log(name);// green
Im strikten Modus erhalten Sie eine Ausnahme
functionStrictColor(name){'use strict';this.name=name;}varc=StrictColor('green');// TypeError: Cannot set property 'name' of undefined
In vielen objektorientierten Sprachen können Konstruktoren nur direkte Instanzen erzeugen. Betrachten Sie zum Beispiel Java: Nehmen wir an, Sie möchten eine Klasse Expression implementieren, die die Unterklassen Addition und Multiplication hat. Das Parsen erzeugt direkte Instanzen der beiden letzteren Klassen. Sie können dies nicht als Konstruktor von Expression implementieren, da dieser Konstruktor nur direkte Instanzen von Expression erzeugen kann. Als Workaround werden in Java statische Factory-Methoden verwendet:
classExpression{// Static factory method:publicstaticExpressionparse(Stringstr){if(...){returnnewAddition(...);}elseif(...){returnnewMultiplication(...);}else{thrownewExpressionException(...);}}}...Expressionexpr=Expression.parse(someStr);
In JavaScript können Sie einfach jedes benötigte Objekt von einem Konstruktor zurückgeben. Die JavaScript-Version des vorherigen Codes würde also wie folgt aussehen:
functionExpression(str){if(...){returnnewAddition(..);}elseif(...){returnnewMultiplication(...);}else{thrownewExpressionException(...);}}...varexpr=newExpression(someStr);
Das sind gute Nachrichten: JavaScript-Konstruktoren sind nicht starr, sodass Sie Ihre Meinung jederzeit ändern können, ob ein Konstruktor eine direkte Instanz oder etwas anderes zurückgeben soll.
Dieser Abschnitt erklärt, dass Sie in den meisten Fällen keine Daten in Prototyp-Eigenschaften speichern sollten. Es gibt jedoch einige Ausnahmen von dieser Regel.
Prototypen enthalten Eigenschaften, die von mehreren Objekten gemeinsam genutzt werden. Daher eignen sie sich gut für Methoden. Zusätzlich können Sie mit einer als nächstes beschriebenen Technik auch Anfangswerte für Instanz-Eigenschaften bereitstellen. Ich werde später erklären, warum dies nicht empfohlen wird.
Ein Konstruktor setzt normalerweise Instanz-Eigenschaften auf Anfangswerte. Wenn einer dieser Werte ein Standardwert ist, müssen Sie keine Instanz-Eigenschaft erstellen. Sie benötigen nur eine Prototyp-Eigenschaft mit demselben Schlüssel, deren Wert der Standardwert ist. Zum Beispiel:
/*** Anti-pattern: don’t do this** @param data an array with names*/functionNames(data){if(data){// There is a parameter// => create instance propertythis.data=data;}}Names.prototype.data=[];
Der Parameter data ist optional. Wenn er fehlt, erhält die Instanz keine data-Eigenschaft, sondern erbt stattdessen Names.prototype.data.
Dieser Ansatz funktioniert größtenteils: Sie können eine Instanz n von Names erstellen. Das Abrufen von n.data liest Names.prototype.data. Das Setzen von n.data erstellt eine neue eigene Eigenschaft in n und behält den gemeinsam genutzten Standardwert im Prototyp bei. Wir haben nur ein Problem, wenn wir den Standardwert ändern (anstatt ihn durch einen neuen Wert zu ersetzen).
> var n1 = new Names();
> var n2 = new Names();
> n1.data.push('jane'); // changes default value
> n1.data
[ 'jane' ]
> n2.data
[ 'jane' ]Im vorherigen Beispiel hat push() das Array in Names.prototype.data geändert. Da dieses Array von allen Instanzen ohne eigene data-Eigenschaft gemeinsam genutzt wird, hat sich auch der Anfangswert von n2.data geändert.
Angesichts dessen, was wir gerade besprochen haben, ist es besser, keine Standardwerte gemeinsam zu nutzen und immer neue zu erstellen.
functionNames(data){this.data=data||[];}
Offensichtlich tritt das Problem der Änderung eines gemeinsam genutzten Standardwerts nicht auf, wenn dieser Wert unveränderlich ist (wie alle Primitiven; siehe Primitive Values). Aber der Konsistenz halber ist es am besten, sich an eine einzige Methode zur Einrichtung von Eigenschaften zu halten. Ich bevorzuge es auch, die übliche Trennung der Zuständigkeiten beizubehalten (siehe Layer 3: Constructors—Factories for Instances): Der Konstruktor richtet die Instanzeigenschaften ein und der Prototyp enthält die Methoden.
ECMAScript 6 wird dies zu einer noch besseren Praxis machen, da Konstruktorparameter Standardwerte haben können und Sie Prototyp-Methoden über Klassen definieren können, aber keine Prototyp-Eigenschaften mit Daten.
Gelegentlich ist die Erstellung eines Eigenschaftswerts ein teurer Vorgang (rechnerisch oder speicherintensiv). In diesem Fall können Sie eine Instanzeigenschaft bei Bedarf erstellen:
functionNames(data){if(data)this.data=data;}Names.prototype={constructor:Names,// (1)getdata(){// Define, don’t assign// => avoid calling the (nonexistent) setterObject.defineProperty(this,'data',{value:[],enumerable:true,configurable:false,writable:false});returnthis.data;}};
Wir können die Eigenschaft data nicht über eine Zuweisung zur Instanz hinzufügen, da JavaScript über einen fehlenden Setter klagen würde (was es tut, wenn es nur einen Getter findet). Daher fügen wir sie über Object.defineProperty() hinzu. Konsultieren Sie Properties: Definition Versus Assignment, um die Unterschiede zwischen Definition und Zuweisung zu wiederholen. In Zeile (1) stellen wir sicher, dass die constructor-Eigenschaft korrekt eingerichtet ist (siehe The constructor Property of Instances).
Offensichtlich ist das viel Arbeit, daher müssen Sie sicher sein, dass es sich lohnt.
Wenn dieselbe Eigenschaft (gleicher Schlüssel, gleiche Semantik, im Allgemeinen unterschiedliche Werte) in mehreren Prototypen vorhanden ist, wird sie als polymorph bezeichnet. Dann wird das Ergebnis des Lesens der Eigenschaft über eine Instanz dynamisch über den Prototyp dieser Instanz bestimmt. Nicht-polymorph verwendete Prototyp-Eigenschaften können durch Variablen ersetzt werden (was ihre nicht-polymorphe Natur besser widerspiegelt).
Sie können beispielsweise eine Konstante in einer Prototyp-Eigenschaft speichern und über this darauf zugreifen.
functionFoo(){}Foo.prototype.FACTOR=42;Foo.prototype.compute=function(x){returnx*this.FACTOR;};
Diese Konstante ist nicht polymorph. Daher können Sie genauso gut über eine Variable darauf zugreifen.
// This code should be inside an IIFE or a modulefunctionFoo(){}varFACTOR=42;Foo.prototype.compute=function(x){returnx*FACTOR;};
Hier ist ein Beispiel für polymorphe Prototyp-Eigenschaften mit unveränderlichen Daten. Durch das Tagging von Instanzen eines Konstruktors über Prototyp-Eigenschaften können Sie diese von Instanzen eines anderen Konstruktors unterscheiden:
functionConstrA(){}ConstrA.prototype.TYPE_NAME='ConstrA';functionConstrB(){}ConstrB.prototype.TYPE_NAME='ConstrB';
Dank des polymorphen "Tags" TYPE_NAME können Sie die Instanzen von ConstrA und ConstrB unterscheiden, selbst wenn sie Realms überschreiten (dann funktioniert instanceof nicht; siehe Pitfall: crossing realms (frames or windows)).
JavaScript hat keine dedizierten Mittel zur Verwaltung privater Daten für ein Objekt. Dieser Abschnitt beschreibt drei Techniken, um diese Einschränkung zu umgehen:
Zusätzlich werde ich erklären, wie globale Daten über IIFEs privat gehalten werden.
Wenn ein Konstruktor aufgerufen wird, werden zwei Dinge erstellt: die Instanz des Konstruktors und eine Umgebung (siehe Environments: Managing Variables). Die Instanz soll vom Konstruktor initialisiert werden. Die Umgebung enthält die Parameter und lokalen Variablen des Konstruktors. Jede Funktion (einschließlich Methoden), die innerhalb des Konstruktors erstellt wird, behält eine Referenz auf die Umgebung – die Umgebung, in der sie erstellt wurde. Dank dieser Referenz hat sie immer Zugriff auf die Umgebung, auch nachdem der Konstruktor beendet ist. Diese Kombination aus Funktion und Umgebung wird als Closure bezeichnet (siehe Closures: Functions Stay Connected to Their Birth Scopes). Die Umgebung des Konstruktors ist somit ein Datenspeicher, der von der Instanz unabhängig ist und nur mit ihr in Beziehung steht, weil beide zur gleichen Zeit erstellt werden. Um sie richtig zu verbinden, müssen wir Funktionen haben, die in beiden Welten leben. Nach Douglas Crockfords Terminologie kann eine Instanz drei Arten von Werten zugeordnet sein (siehe Figure 17-4):
Die folgenden Abschnitte erklären jede Art von Wert genauer.
Denken Sie daran, dass es für einen Konstruktor Constr zwei Arten von Eigenschaften gibt, die öffentlich sind und für jeden zugänglich sind. Erstens sind Prototyp-Eigenschaften in Constr.prototype gespeichert und werden von allen Instanzen gemeinsam genutzt. Prototyp-Eigenschaften sind normalerweise Methoden:
Constr.prototype.publicMethod=...;
Zweitens sind Instanz-Eigenschaften für jede Instanz einzigartig. Sie werden im Konstruktor hinzugefügt und enthalten normalerweise Daten (keine Methoden):
functionConstr(...){this.publicData=...;...}
Die Umgebung des Konstruktors besteht aus den Parametern und lokalen Variablen. Sie sind nur von innerhalb des Konstruktors zugänglich und somit privat für die Instanz:
functionConstr(...){...varthat=this;// make accessible to private functionsvarprivateData=...;functionprivateFunction(...){// Access everythingprivateData=...;that.publicData=...;that.publicMethod(...);}...}
Private Daten sind so sicher vor externem Zugriff, dass Prototyp-Methoden nicht darauf zugreifen können. Aber wie sollte man sie sonst nach Verlassen des Konstruktors verwenden? Die Antwort sind privilegierte Methoden: Im Konstruktor erstellte Funktionen werden als Instanz-Methoden hinzugefügt. Das bedeutet, dass sie einerseits auf private Daten zugreifen können; andererseits sind sie öffentlich und werden daher von Prototyp-Methoden gesehen. Mit anderen Worten, sie dienen als Vermittler zwischen privaten Daten und der Öffentlichkeit (einschließlich Prototyp-Methoden):
functionConstr(...){...this.privilegedMethod=function(...){// Access everythingprivateData=...;privateFunction(...);this.publicData=...;this.publicMethod(...);};}
Das Folgende ist eine Implementierung eines StringBuilder unter Verwendung des Crockford Privacy Patterns:
functionStringBuilder(){varbuffer=[];this.add=function(str){buffer.push(str);};this.toString=function(){returnbuffer.join('');};}// Can’t put methods in the prototype!
Hier ist die Interaktion
> var sb = new StringBuilder();
> sb.add('Hello');
> sb.add(' world!');
> sb.toString()
’Hello world!’Hier sind einige Punkte, die Sie bei der Verwendung des Crockford Privacy Patterns berücksichtigen sollten:
Für die meisten nicht sicherheitskritischen Anwendungen ist Privatsphäre eher ein Hinweis für API-Kunden: „Das müssen Sie nicht sehen.“ Das ist der Hauptvorteil der Kapselung – Verbergen von Komplexität. Obwohl intern mehr passiert, müssen Sie nur den öffentlichen Teil einer API verstehen. Die Idee einer Namenskonvention besteht darin, Kunden über die Privatsphäre zu informieren, indem der Schlüssel einer Eigenschaft gekennzeichnet wird. Ein vorangestellter Unterstrich wird dafür oft verwendet.
Schreiben wir das vorherige StringBuilder-Beispiel neu, sodass der Puffer in einer Eigenschaft _buffer gespeichert wird, die aber nur konventionsgemäß privat ist.
functionStringBuilder(){this._buffer=[];}StringBuilder.prototype={constructor:StringBuilder,add:function(str){this._buffer.push(str);},toString:function(){returnthis._buffer.join('');}};
Hier sind einige Vor- und Nachteile der Privatsphäre über gekennzeichnete Eigenschaftsschlüssel:
Ein Problem bei einer Namenskonvention für private Eigenschaften ist, dass Schlüssel kollidieren können (z. B. ein Schlüssel von einem Konstruktor mit einem Schlüssel von einem Unterkonstruktor oder ein Schlüssel von einem Mixin mit einem Schlüssel von einem Konstruktor). Sie können solche Kollisionen unwahrscheinlicher machen, indem Sie längere Schlüssel verwenden, die zum Beispiel den Namen des Konstruktors enthalten. Dann würde im vorherigen Fall die private Eigenschaft _buffer _StringBuilder_buffer heißen. Wenn ein solcher Schlüssel zu lang für Ihren Geschmack ist, haben Sie die Möglichkeit, ihn zu reifizieren, indem Sie ihn in einer Variablen speichern:
varKEY_BUFFER='_StringBuilder_buffer';
Wir greifen nun über this[KEY_BUFFER] auf die privaten Daten zu.
varStringBuilder=function(){varKEY_BUFFER='_StringBuilder_buffer';functionStringBuilder(){this[KEY_BUFFER]=[];}StringBuilder.prototype={constructor:StringBuilder,add:function(str){this[KEY_BUFFER].push(str);},toString:function(){returnthis[KEY_BUFFER].join('');}};returnStringBuilder;}();
Wir haben eine IIFE um StringBuilder gewickelt, damit die Konstante KEY_BUFFER lokal bleibt und den globalen Namensraum nicht verschmutzt.
Reifizierte Eigenschaftsschlüssel ermöglichen die Verwendung von UUIDs (universally unique identifiers) in Schlüsseln. Zum Beispiel über Robert Kieffers node-uuid.
varKEY_BUFFER='_StringBuilder_buffer_'+uuid.v4();
KEY_BUFFER hat jedes Mal, wenn der Code ausgeführt wird, einen anderen Wert. Er kann zum Beispiel so aussehen:
_StringBuilder_buffer_110ec58a-a0f2-4ac4-8393-c866d813b8d1
Lange Schlüssel mit UUIDs machen Schlüsselkonflikte praktisch unmöglich.
Diese Unterabschnitt erklärt, wie globale Daten für Singleton-Objekte, Konstruktoren und Methoden privat gehalten werden können, über IIFEs (siehe Introducing a New Scope via an IIFE). Diese IIFEs erstellen neue Umgebungen (siehe Environments: Managing Variables), in denen Sie die privaten Daten platzieren.
Sie benötigen keinen Konstruktor, um ein Objekt mit privaten Daten in einer Umgebung zu verknüpfen. Das folgende Beispiel zeigt, wie Sie eine IIFE für denselben Zweck verwenden, indem Sie sie um ein Singleton-Objekt wickeln:
varobj=function(){// open IIFE// publicvarself={publicMethod:function(...){privateData=...;privateFunction(...);},publicData:...};// privatevarprivateData=...;functionprivateFunction(...){privateData=...;self.publicData=...;self.publicMethod(...);}returnself;}();// close IIFE
Einige globale Daten sind nur für einen Konstruktor und die Prototyp-Methoden relevant. Indem Sie eine IIFE um beides wickeln, können Sie sie vor der Öffentlichkeit verbergen. Private Data in Properties with Reified Keys gab ein Beispiel: Der Konstruktor StringBuilder und seine Prototyp-Methoden verwenden die Konstante KEY_BUFFER, die einen Eigenschaftsschlüssel enthält. Diese Konstante ist in der Umgebung einer IIFE gespeichert.
varStringBuilder=function(){// open IIFEvarKEY_BUFFER='_StringBuilder_buffer_'+uuid.v4();functionStringBuilder(){this[KEY_BUFFER]=[];}StringBuilder.prototype={// Omitted: methods accessing this[KEY_BUFFER]};returnStringBuilder;}();// close IIFE
Beachten Sie, dass Sie, wenn Sie ein Modulsystem verwenden (siehe Chapter 31), denselben Effekt mit saubererem Code erzielen können, indem Sie den Konstruktor plus Methoden in ein Modul legen.
Manchmal benötigen Sie globale Daten nur für eine einzige Methode. Sie können sie privat halten, indem Sie sie in der Umgebung einer IIFE platzieren, die Sie um die Methode wickeln. Zum Beispiel:
varobj={method:function(){// open IIFE// method-private datavarinvocCount=0;returnfunction(){invocCount++;console.log('Invocation #'+invocCount);return'result';};}()// close IIFE};
Hier ist die Interaktion
> obj.method() Invocation #1 'result' > obj.method() Invocation #2 'result'
In diesem Abschnitt untersuchen wir, wie Konstruktoren vererbt werden können: Gegeben einen Konstruktor Super, wie können wir einen neuen Konstruktor, Sub, schreiben, der alle Merkmale von Super plus einige eigene Merkmale hat? Leider hat JavaScript keinen eingebauten Mechanismus, um diese Aufgabe auszuführen. Daher müssen wir einige manuelle Arbeiten leisten.
Abbildung 17-5 veranschaulicht die Idee: Der Subkonstruktor Sub sollte alle Eigenschaften von Super (sowohl Prototyp-Eigenschaften als auch Instanz-Eigenschaften) zusätzlich zu seinen eigenen haben. Somit haben wir eine grobe Vorstellung davon, wie Sub aussehen sollte, wissen aber nicht, wie wir dorthin gelangen. Es gibt mehrere Dinge, die wir herausfinden müssen, die ich als nächstes erklären werde.
instanceof funktioniert: Wenn sub eine Instanz von Sub ist, wollen wir auch, dass sub instanceof Super wahr ist.Super-Methoden in Sub anzupassen.Super-Methoden überschrieben haben, müssen wir möglicherweise die ursprüngliche Methode aus Sub aufrufen.Instanzeigenschaften werden im Konstruktor selbst eingerichtet, daher beinhaltet das Vererben der Instanzeigenschaften des Superkonstruktors das Aufrufen dieses Konstruktors:
functionSub(prop1,prop2,prop3,prop4){Super.call(this,prop1,prop2);// (1)this.prop3=prop3;// (2)this.prop4=prop4;// (3)}
Wenn Sub über new aufgerufen wird, bezieht sich sein impliziter Parameter this auf eine frische Instanz. Sie übergibt diese Instanz zuerst an Super (1), das seine Instanzeigenschaften hinzufügt. Danach richtet Sub seine eigenen Instanzeigenschaften ein (2,3). Der Trick ist, Super nicht über new aufzurufen, da dies eine frische Superinstanz erzeugen würde. Stattdessen rufen wir Super als Funktion auf und übergeben die aktuelle (Sub-)Instanz als Wert von this.
Gemeinsam genutzte Eigenschaften wie Methoden werden im Instanzprototyp gespeichert. Daher müssen wir einen Weg finden, damit Sub.prototype alle Eigenschaften von Super.prototype erbt. Die Lösung besteht darin, Sub.prototype den Prototyp Super.prototype zu geben.
Ja, die JavaScript-Terminologie ist hier verwirrend. Wenn Sie sich verloren fühlen, konsultieren Sie Terminology: The Two Prototypes, das erklärt, wie sie sich unterscheiden.
Dies ist der Code, der dies erreicht.
Sub.prototype=Object.create(Super.prototype);Sub.prototype.constructor=Sub;Sub.prototype.methodB=...;Sub.prototype.methodC=...;
Object.create() erzeugt ein frisches Objekt, dessen Prototyp Super.prototype ist. Danach fügen wir Subs Methoden hinzu. Wie in The constructor Property of Instances erklärt, müssen wir auch die Eigenschaft constructor einrichten, da wir den ursprünglichen Instanzprototyp ersetzt haben, wo er den korrekten Wert hatte.
Abbildung 17-6 zeigt, wie Sub und Super nun zusammenhängen. Subs Struktur ähnelt tatsächlich dem, was ich in Abbildung 17-5 skizziert habe. Das Diagramm zeigt nicht die Instanzeigenschaften, die durch den im Diagramm erwähnten Funktionsaufruf eingerichtet werden.
„Sicherstellen, dass instanceof funktioniert“ bedeutet, dass jede Instanz von Sub auch eine Instanz von Super sein muss. Abbildung 17-7 zeigt, wie die Prototypenkette von subInstance, einer Instanz von Sub, aussieht: Ihr erster Prototyp ist Sub.prototype und ihr zweiter Prototyp ist Super.prototype.
Beginnen wir mit einer einfacheren Frage: Ist subInstance eine Instanz von Sub? Ja, das ist es, denn die folgenden beiden Aussagen sind äquivalent (die letztere kann als Definition der ersteren betrachtet werden).
subInstanceinstanceofSubSub.prototype.isPrototypeOf(subInstance)
Wie bereits erwähnt, ist Sub.prototype einer der Prototypen von subInstance, daher sind beide Aussagen wahr. Ebenso ist subInstance auch eine Instanz von Super, denn die folgenden beiden Aussagen gelten:
subInstanceinstanceofSuperSuper.prototype.isPrototypeOf(subInstance)
Wir überschreiben eine Methode in Super.prototype, indem wir eine Methode mit demselben Namen zu Sub.prototype hinzufügen. methodB ist ein Beispiel, und in Abbildung 17-7 sehen wir, warum es funktioniert: Die Suche nach methodB beginnt in subInstance und findet Sub.prototype.methodB vor Super.prototype.methodB.
Um Super-Aufrufe zu verstehen, müssen Sie den Begriff Home-Objekt kennen. Das Home-Objekt einer Methode ist das Objekt, das die Eigenschaft besitzt, deren Wert die Methode ist. Zum Beispiel ist das Home-Objekt von Sub.prototype.methodB Sub.prototype. Das Super-Aufrufen einer Methode foo beinhaltet drei Schritte:
foo ist.this auf. Die Begründung ist, dass die Supermethode mit derselben Instanz wie die aktuelle Methode arbeiten muss; sie muss auf dieselben Instanzeigenschaften zugreifen können.Daher sieht der Code der Submethode wie folgt aus. Sie ruft sich selbst per Super-Aufruf auf, sie ruft die Methode auf, die sie überschrieben hat.
Sub.prototype.methodB=function(x,y){varsuperResult=Super.prototype.methodB.call(this,x,y);// (1)returnthis.prop3+' '+superResult;}
Eine Möglichkeit, den Super-Aufruf unter (1) zu lesen, ist wie folgt: Beziehen Sie sich direkt auf die Supermethode und rufen Sie sie mit dem aktuellen this auf. Wenn wir sie jedoch in drei Teile aufteilen, finden wir die oben genannten Schritte:
Super.prototype: Beginnen Sie Ihre Suche in Super.prototype, dem Prototyp von Sub.prototype (dem Home-Objekt der aktuellen Methode Sub.prototype.methodB).methodB: Suchen Sie nach einer Methode mit dem Namen methodB.call(this, ...): Rufen Sie die im vorherigen Schritt gefundene Methode auf und behalten Sie das aktuelle this bei.Bis jetzt haben wir immer auf Supermethoden und Superkonstruktoren Bezug genommen, indem wir den Namen des Superkonstruktors genannt haben. Diese Art der Festkodierung macht Ihren Code weniger flexibel. Sie können sie vermeiden, indem Sie den Superprototyp einer Eigenschaft von Sub zuweisen:
Sub._super=Super.prototype;
Dann sehen der Aufruf des Superkonstruktors und einer Supermethode wie folgt aus:
functionSub(prop1,prop2,prop3,prop4){Sub._super.constructor.call(this,prop1,prop2);this.prop3=prop3;this.prop4=prop4;}Sub.prototype.methodB=function(x,y){varsuperResult=Sub._super.methodB.call(this,x,y);returnthis.prop3+' '+superResult;}
Das Einrichten von Sub._super wird normalerweise von einer Hilfsfunktion gehandhabt, die auch den Subprototyp mit dem Superprototyp verbindet. Zum Beispiel:
functionsubclasses(SubC,SuperC){varsubProto=Object.create(SuperC.prototype);// Save `constructor` and, possibly, other methodscopyOwnPropertiesFrom(subProto,SubC.prototype);SubC.prototype=subProto;SubC._super=SuperC.prototype;};
Dieser Code verwendet die Hilfsfunktion copyOwnPropertiesFrom(), die in Copying an Object gezeigt und erklärt wird.
Lesen Sie "subclasses" als Verb: SubC subclasses SuperC. Eine solche Hilfsfunktion kann einige der Mühen bei der Erstellung eines Subkonstruktors abnehmen: Es gibt weniger manuell zu tun, und der Name des Superkonstruktors wird nie redundant erwähnt. Das folgende Beispiel zeigt, wie es den Code vereinfacht.
Als konkretes Beispiel nehmen wir an, dass der Konstruktor Person bereits existiert:
functionPerson(name){this.name=name;}Person.prototype.describe=function(){return'Person called '+this.name;};
Wir möchten nun den Konstruktor Employee als Unterkonstruktor von Person erzeugen. Dies geschieht manuell und sieht wie folgt aus:
functionEmployee(name,title){Person.call(this,name);this.title=title;}Employee.prototype=Object.create(Person.prototype);Employee.prototype.constructor=Employee;Employee.prototype.describe=function(){returnPerson.prototype.describe.call(this)+' ('+this.title+')';};
Hier ist die Interaktion
> var jane = new Employee('Jane', 'CTO');
> jane.describe()
Person called Jane (CTO)
> jane instanceof Employee
true
> jane instanceof Person
trueDie Hilfsfunktion subclasses() aus dem vorherigen Abschnitt vereinfacht den Code von Employee etwas und vermeidet die harte Kodierung des Superkonstruktors Person.
functionEmployee(name,title){Employee._super.constructor.call(this,name);this.title=title;}Employee.prototype.describe=function(){returnEmployee._super.describe.call(this)+' ('+this.title+')';};subclasses(Employee,Person);
Integrierte Konstruktoren verwenden denselben Unterklassenansatz, der in diesem Abschnitt beschrieben wird. Beispielsweise ist Array ein Unterkonstruktor von Object. Daher sieht die Prototypenkette einer Instanz von Array wie folgt aus:
> var p = Object.getPrototypeOf > p([]) === Array.prototype true > p(p([])) === Object.prototype true > p(p(p([]))) === null true
Vor ECMAScript 5 und Object.create() war eine häufig verwendete Lösung, den Unterprototyp zu erstellen, indem der Superkonstruktor aufgerufen wurde:
Sub.prototype=newSuper();// Don’t do this
Dies wird unter ECMAScript 5 nicht empfohlen. Der Prototyp wird alle Instanzeigenschaften von Super haben, die er nicht benötigt. Daher ist es besser, das oben erwähnte Muster (mit Object.create()) zu verwenden.
Fast alle Objekte haben Object.prototype in ihrer Prototypenkette:
> Object.prototype.isPrototypeOf({})
true
> Object.prototype.isPrototypeOf([])
true
> Object.prototype.isPrototypeOf(/xyz/)
trueDie folgenden Unterabschnitte beschreiben die Methoden, die Object.prototype für seine Prototypen bereitstellt.
Die folgenden beiden Methoden werden verwendet, um ein Objekt in einen primitiven Wert umzuwandeln:
Object.prototype.toString()
Gibt eine Zeichenfolgendarstellung eines Objekts zurück:
> ({ first: 'John', last: 'Doe' }.toString())
'[object Object]'
> [ 'a', 'b', 'c' ].toString()
'a,b,c'Object.prototype.valueOf()
Dies ist der bevorzugte Weg, ein Objekt in eine Zahl umzuwandeln. Die Standardimplementierung gibt this zurück.
> var obj = {};
> obj.valueOf() === obj
truevalueOf wird von Wrapper-Konstruktoren überschrieben, um das gewrappte Primitiv zurückzugeben.
> new Number(7).valueOf() 7
Die Umwandlung in Zahl und Zeichenfolge (ob implizit oder explizit) baut auf der Umwandlung in ein Primitiv auf (Details siehe Algorithmus: ToPrimitive()-Umwandlung eines Wertes in ein Primitiv). Deshalb können Sie die oben genannten beiden Methoden verwenden, um diese Umwandlungen zu konfigurieren. valueOf() wird von der Umwandlung in eine Zahl bevorzugt.
> 3 * { valueOf: function () { return 5 } }
15toString() wird von der Umwandlung in eine Zeichenfolge bevorzugt.
> String({ toString: function () { return 'ME' } })
'Result: ME'Die Umwandlung in einen booleschen Wert ist nicht konfigurierbar; Objekte werden immer als true betrachtet (siehe Umwandlung in Boolesch).
Diese Methode gibt eine lokalitätsspezifische Zeichenfolgendarstellung eines Objekts zurück. Die Standardimplementierung ruft toString() auf. Die meisten Engines gehen nicht über diese Unterstützung für diese Methode hinaus. Die ECMAScript Internationalization API (siehe Die ECMAScript Internationalization API), die von vielen modernen Engines unterstützt wird, überschreibt sie jedoch für mehrere integrierte Konstruktoren.
Die folgenden Methoden helfen bei der prototypischen Vererbung und Eigenschaften:
Object.prototype.isPrototypeOf(obj)
Gibt true zurück, wenn der Empfänger Teil der Prototypenkette von obj ist.
> var proto = { };
> var obj = Object.create(proto);
> proto.isPrototypeOf(obj)
true
> obj.isPrototypeOf(obj)
falseObject.prototype.hasOwnProperty(key)
Gibt true zurück, wenn this eine Eigenschaft mit dem Schlüssel key besitzt. „Eigen“ bedeutet, dass die Eigenschaft im Objekt selbst und nicht in einem seiner Prototypen vorhanden ist.
Normalerweise sollten Sie diese Methode generisch aufrufen (nicht direkt), insbesondere für Objekte, deren Eigenschaften Sie nicht statisch kennen. Warum und wie, wird in Iteration und Erkennung von Eigenschaften erklärt.
> var proto = { foo: 'abc' };
> var obj = Object.create(proto);
> obj.bar = 'def';
> Object.prototype.hasOwnProperty.call(obj, 'foo')
false
> Object.prototype.hasOwnProperty.call(obj, 'bar')
trueObject.prototype.propertyIsEnumerable(propKey)
Gibt true zurück, wenn der Empfänger eine Eigenschaft mit dem Schlüssel propKey hat, die aufzählbar ist, und andernfalls false:
> var obj = { foo: 'abc' };
> obj.propertyIsEnumerable('foo')
true
> obj.propertyIsEnumerable('toString')
false
> obj.propertyIsEnumerable('unknown')
falseManchmal haben Instanzprototypen Methoden, die für mehr Objekte nützlich sind als die, die von ihnen erben. Dieser Abschnitt erklärt, wie die Methoden eines Prototyps verwendet werden können, ohne von ihm zu erben. Zum Beispiel hat der Instanzprototyp Wine.prototype die Methode incAge():
functionWine(age){this.age=age;}Wine.prototype.incAge=function(years){this.age+=years;}
Die Interaktion sieht wie folgt aus:
> var chablis = new Wine(3); > chablis.incAge(1); > chablis.age 4
Die Methode incAge() funktioniert für jedes Objekt, das die Eigenschaft age hat. Wie können wir sie für ein Objekt aufrufen, das keine Instanz von Wine ist? Schauen wir uns den vorherigen Methodenaufruf an:
chablis.incAge(1)
Es gibt tatsächlich zwei Argumente:
chablis ist der Empfänger des Methodenaufrufs, der über this an incAge übergeben wird.1 ist ein Argument, das über years an incAge übergeben wird.Wir können das Erste nicht durch ein beliebiges Objekt ersetzen – der Empfänger muss eine Instanz von Wine sein. Andernfalls wird die Methode incAge nicht gefunden. Aber der vorherige Methodenaufruf ist äquivalent zu (siehe auch Aufrufen von Funktionen unter Festlegung von this: call(), apply() und bind()):
Wine.prototype.incAge.call(chablis,1)
Mit dem vorherigen Muster können wir ein Objekt zum Empfänger machen (erstes Argument von call), das keine Instanz von Wine ist, da der Empfänger nicht zum Finden der Methode Wine.prototype.incAge verwendet wird. Im folgenden Beispiel wenden wir die Methode incAge() auf das Objekt john an:
> var john = { age: 51 };
> Wine.prototype.incAge.call(john, 3)
> john.age
54Eine Funktion, die auf diese Weise verwendet werden kann, wird als generische Methode bezeichnet; sie muss darauf vorbereitet sein, dass this keine Instanz „ihres“ Konstruktors ist. Daher sind nicht alle Methoden generisch; die ECMAScript-Sprachspezifikation gibt explizit an, welche dies sind (siehe Eine Liste aller generischen Methoden).
Das Aufrufen einer Methode generisch ist ziemlich umständlich:
Object.prototype.hasOwnProperty.call(obj,'propKey')
Sie können dies verkürzen, indem Sie auf hasOwnProperty über eine Instanz von Object zugreifen, wie sie durch ein leeres Objektliteral {} erstellt wird:
{}.hasOwnProperty.call(obj,'propKey')
Ähnlich sind die folgenden beiden Ausdrücke äquivalent:
Array.prototype.join.call(str,'-')[].join.call(str,'-')
Der Vorteil dieses Musters ist, dass es weniger umständlich ist. Aber es ist auch weniger selbsterklärend. Die Leistung sollte kein Problem sein (zumindest langfristig), da Engines statisch feststellen können, dass die Literale keine Objekte erstellen sollen.
Dies sind einige Beispiele für generische Methoden in Aktion:
Verwenden Sie apply() (siehe Function.prototype.apply(thisValue, argArray)), um ein Array zu pushen (anstelle einzelner Elemente; siehe Hinzufügen und Entfernen von Elementen (destruktiv)).
> var arr1 = [ 'a', 'b' ]; > var arr2 = [ 'c', 'd' ]; > [].push.apply(arr1, arr2) 4 > arr1 [ 'a', 'b', 'c', 'd' ]
Dieses Beispiel dreht sich darum, ein Array in Argumente zu verwandeln, nicht darum, eine Methode von einem anderen Konstruktor auszuleihen.
Wenden Sie die Array-Methode join() auf einen String an (der kein Array ist).
> Array.prototype.join.call('abc', '-')
'a-b-c'Wenden Sie die Array-Methode map() auf einen String an:[17]
> [].map.call('abc', function (x) { return x.toUpperCase() })
[ 'A', 'B', 'C' ]Die generische Verwendung von map() ist effizienter als die Verwendung von split(''), was ein Zwischenarray erstellt.
> 'abc'.split('').map(function (x) { return x.toUpperCase() })
[ 'A', 'B', 'C' ]Wenden Sie eine String-Methode auf Nicht-Strings an. toUpperCase() wandelt den Empfänger in eine Zeichenfolge um und wandelt das Ergebnis in Großbuchstaben um.
> String.prototype.toUpperCase.call(true) 'TRUE' > String.prototype.toUpperCase.call(['a','b','c']) 'A,B,C'
Die generische Verwendung von Array-Methoden auf einfachen Objekten gibt Einblick in deren Funktionsweise.
Rufen Sie eine Array-Methode für ein Array-ähnliches Objekt auf.
> var fakeArray = { 0: 'a', 1: 'b', length: 2 };
> Array.prototype.join.call(fakeArray, '-')
'a-b'Sehen Sie, wie eine Array-Methode ein Objekt transformiert, das sie wie ein Array behandelt.
> var obj = {};
> Array.prototype.push.call(obj, 'hello');
1
> obj
{ '0': 'hello', length: 1 }Es gibt einige Objekte in JavaScript, die sich wie ein Array anfühlen, aber tatsächlich keins sind. Das bedeutet, dass sie zwar indizierten Zugriff und eine length-Eigenschaft haben, aber keine der Array-Methoden (forEach(), push, concat() usw.). Das ist bedauerlich, aber wie wir sehen werden, ermöglichen generische Array-Methoden eine Lösung.
Die spezielle Variable arguments (siehe Alle Parameter nach Index: Die spezielle Variable arguments), die ein wichtiges Array-ähnliches Objekt ist, da sie ein grundlegender Bestandteil von JavaScript ist. arguments sieht aus wie ein Array:
> function args() { return arguments }
> var arrayLike = args('a', 'b');
> arrayLike[0]
'a'
> arrayLike.length
2Aber keine der Array-Methoden ist verfügbar.
> arrayLike.join('-')
TypeError: object has no method 'join'Das liegt daran, dass arrayLike keine Instanz von Array ist (und Array.prototype nicht in der Prototypenkette liegt).
> arrayLike instanceof Array false
Browser DOM-Knotenlisten, die von document.getElementsBy*() (z. B. getElementsByTagName()), document.forms usw. zurückgegeben werden.
> var elts = document.getElementsByTagName('h3');
> elts.length
3
> elts instanceof Array
falseStrings, die ebenfalls Array-ähnlich sind.
> 'abc'[1] 'b' > 'abc'.length 3
Der Begriff Array-ähnlich kann auch als Vertrag zwischen generischen Array-Methoden und Objekten betrachtet werden. Die Objekte müssen bestimmte Anforderungen erfüllen, andernfalls funktionieren die Methoden nicht für sie. Die Anforderungen sind:
Die Elemente eines Array-ähnlichen Objekts müssen über eckige Klammern und ganzzahlige Indizes ab 0 zugänglich sein. Alle Methoden benötigen Lesezugriff, und einige Methoden benötigen zusätzlich Schreibzugriff. Beachten Sie, dass alle Objekte diese Art von Indizierung unterstützen: Ein Index in Klammern wird in eine Zeichenfolge umgewandelt und als Schlüssel verwendet, um einen Eigenschaftswert nachzuschlagen.
> var obj = { '0': 'abc' };
> obj[0]
'abc'length-Eigenschaft haben, deren Wert die Anzahl seiner Elemente ist. Einige Methoden erfordern, dass length veränderbar ist (z. B. reverse()). Werte, deren Längen unveränderlich sind (z. B. Strings), können mit diesen Methoden nicht verwendet werden.Die folgenden Muster sind für die Arbeit mit Array-ähnlichen Objekten nützlich:
Ein Array-ähnliches Objekt in ein Array umwandeln
vararr=Array.prototype.slice.call(arguments);
Die Methode slice() (siehe Verketten, Slicen, Verbinden (nicht-destruktiv)) ohne Argumente erstellt eine Kopie eines Array-ähnlichen Empfängers.
varcopy=['a','b'].slice();
Um alle Elemente eines Array-ähnlichen Objekts zu durchlaufen, können Sie eine einfache for-Schleife verwenden.
functionlogArgs(){for(vari=0;i<arguments.length;i++){console.log(i+'. '+arguments[i]);}}
Aber Sie können auch Array.prototype.forEach() ausleihen.
functionlogArgs(){Array.prototype.forEach.call(arguments,function(elem,i){console.log(i+'. '+elem);});}
In beiden Fällen sieht die Interaktion wie folgt aus:
> logArgs('hello', 'world');
0. hello
1. worldDie folgende Liste enthält alle Methoden, die gemäß der ECMAScript-Sprachspezifikation generisch sind:
Array.prototype (siehe Array-Prototypmethoden)
concat
every
filter
forEach
indexOf
join
lastIndexOf
map
pop
push
reduce
reduceRight
reverse
shift
slice
some
sort
splice
toLocaleString
toString
unshift
Date.prototype (siehe Date-Prototypmethoden)
toJSON
Object.prototype (siehe Methoden aller Objekte)
Object-Methoden sind automatisch generisch – sie müssen für alle Objekte funktionieren.)
String.prototype (siehe String-Prototypmethoden)
charAt
charCodeAt
concat
indexOf
lastIndexOf
localeCompare
match
replace
search
slice
split
substring
toLocaleLowerCase
toLocaleUpperCase
toLowerCase
toUpperCase
trim
Da JavaScript keine integrierte Datenstruktur für Maps hat, werden Objekte oft als Maps von Zeichenfolgen zu Werten verwendet. Leider ist dies fehleranfälliger, als es scheint. Dieser Abschnitt erklärt drei Fallstricke, die bei dieser Aufgabe auftreten.
Die Operationen, die Eigenschaften lesen, können in zwei Arten unterteilt werden:
Sie müssen sorgfältig zwischen diesen Arten von Operationen wählen, wenn Sie die Einträge eines Objekt-als-Map lesen. Um zu verstehen, warum, betrachten wir das folgende Beispiel:
varproto={protoProp:'a'};varobj=Object.create(proto);obj.ownProp='b';
obj ist ein Objekt mit einer eigenen Eigenschaft, deren Prototyp proto ist, der ebenfalls eine eigene Eigenschaft hat. proto hat den Prototyp Object.prototype, wie alle Objekte, die durch Objektliterale erstellt werden. Daher erbt obj Eigenschaften sowohl von proto als auch von Object.prototype.
Wir möchten, dass obj als Map mit dem einzelnen Eintrag interpretiert wird:
ownProp: 'b'
Das heißt, wir möchten geerbte Eigenschaften ignorieren und nur eigene Eigenschaften berücksichtigen. Sehen wir uns an, welche Lesevorgänge obj auf diese Weise interpretieren und welche nicht. Beachten Sie, dass wir für Objekte-als-Maps normalerweise beliebige Eigenschaftsschlüssel verwenden möchten, die in Variablen gespeichert sind. Das schließt die Punktnotation aus.
Der in-Operator prüft, ob ein Objekt eine Eigenschaft mit einem bestimmten Schlüssel hat, berücksichtigt aber geerbte Eigenschaften:
> 'ownProp' in obj // ok true > 'unknown' in obj // ok false > 'toString' in obj // wrong, inherited from Object.prototype true > 'protoProp' in obj // wrong, inherited from proto true
Wir benötigen eine Prüfung, die geerbte Eigenschaften ignoriert. hasOwnProperty() tut, was wir wollen:
> obj.hasOwnProperty('ownProp') // ok
true
> obj.hasOwnProperty('unknown') // ok
false
> obj.hasOwnProperty('toString') // ok
false
> obj.hasOwnProperty('protoProp') // ok
falseWelche Operationen können wir verwenden, um alle Schlüssel von obj zu finden, während wir unsere Interpretation als Map beibehalten? for-in sieht so aus, als könnte es funktionieren. Aber leider tut es das nicht:
> for (propKey in obj) console.log(propKey) ownProp protoProp
Es berücksichtigt geerbte aufzählbare Eigenschaften. Der Grund, warum hier keine Eigenschaften von Object.prototype auftauchen, ist, dass sie alle nicht aufzählbar sind.
Im Gegensatz dazu listet Object.keys() nur eigene Eigenschaften auf:
> Object.keys(obj) [ 'ownProp' ]
Diese Methode gibt nur aufzählbare eigene Eigenschaften zurück; ownProp wurde per Zuweisung hinzugefügt und ist daher standardmäßig aufzählbar. Wenn Sie alle eigenen Eigenschaften auflisten möchten, müssen Sie Object.getOwnPropertyNames() verwenden.
Zum Lesen des Werts einer Eigenschaft können wir nur zwischen dem Punktoperator und dem Klammeroperator wählen. Wir können ersteren nicht verwenden, da wir beliebige Schlüssel haben, die in Variablen gespeichert sind. Das lässt uns mit dem Klammeroperator, der geerbte Eigenschaften berücksichtigt:
> obj['toString'] [Function: toString]
Das ist nicht das, was wir wollen. Es gibt keine integrierte Operation zum Lesen nur eigener Eigenschaften, aber Sie können leicht selbst eine implementieren:
functiongetOwnProperty(obj,propKey){// Using hasOwnProperty() in this manner is problematic// (explained and fixed later)return(obj.hasOwnProperty(propKey)?obj[propKey]:undefined);}
Mit dieser Funktion wird die geerbte Eigenschaft toString ignoriert.
> getOwnProperty(obj, 'toString') undefined
Die Funktion getOwnProperty() hat die Methode hasOwnProperty() auf obj aufgerufen. Normalerweise ist das in Ordnung:
> getOwnProperty({ foo: 123 }, 'foo')
123Wenn Sie jedoch eine Eigenschaft zu obj hinzufügen, deren Schlüssel hasOwnProperty ist, überschreibt diese Eigenschaft die Methode Object.prototype.hasOwnProperty() und getOwnProperty() funktioniert nicht mehr.
> getOwnProperty({ hasOwnProperty: 123 }, 'foo')
TypeError: Property 'hasOwnProperty' is not a functionSie können dieses Problem beheben, indem Sie direkt auf hasOwnProperty() verweisen. Dies vermeidet, dass sie über obj gefunden wird:
functiongetOwnProperty(obj,propKey){return(Object.prototype.hasOwnProperty.call(obj,propKey)?obj[propKey]:undefined);}
Wir haben hasOwnProperty() generisch aufgerufen (siehe Generische Methoden: Ausleihen von Methoden aus Prototypen).
In vielen JavaScript-Engines ist die Eigenschaft __proto__ (siehe Die spezielle Eigenschaft __proto__) besonders: Das Abrufen ermittelt den Prototyp eines Objekts, und das Setzen ändert den Prototyp eines Objekts. Deshalb kann das Objekt keine Kartendaten in einer Eigenschaft speichern, deren Schlüssel '__proto__' ist. Wenn Sie den Map-Schlüssel '__proto__' zulassen möchten, müssen Sie ihn maskieren, bevor Sie ihn als Eigenschaftsschlüssel verwenden:
functionget(obj,key){returnobj[escapeKey(key)];}functionset(obj,key,value){obj[escapeKey(key)]=value;}// Similar: checking if key exists, deleting an entryfunctionescapeKey(key){if(key.indexOf('__proto__')===0){// (1)returnkey+'%';}else{returnkey;}}
Wir müssen auch die maskierte Version von '__proto__' (usw.) maskieren, um Kollisionen zu vermeiden; das heißt, wenn wir den Schlüssel '__proto__' als '__proto__%' maskieren, müssen wir auch den Schlüssel '__proto__%' maskieren, damit er keinen '__proto__'-Eintrag ersetzt. Das ist es, was in Zeile (1) passiert.
Mark S. Miller erwähnt die realen Auswirkungen dieses Fallstricks in einer E-Mail.
Denken Sie, diese Übung ist akademisch und tritt nicht in realen Systemen auf? Wie in einem Support-Thread beobachtet, hing bis vor kurzem auf allen Nicht-IE-Browsern, wenn Sie „__proto__“ am Anfang eines neuen Google Docs tippten, Ihr Google Doc. Dies wurde auf eine solche fehlerhafte Verwendung eines Objekts als Zeichenfolgen-Map zurückgeführt.
Sie erstellen ein Objekt ohne Prototyp wie folgt:
vardict=Object.create(null);
Ein solches Objekt ist eine bessere Map (Wörterbuch) als ein normales Objekt, weshalb dieses Muster manchmal als Dict-Muster (dict für dictionary) bezeichnet wird. Untersuchen wir zunächst normale Objekte und finden dann heraus, warum Prototyp-lose Objekte bessere Maps sind.
Normalerweise hat jedes in JavaScript erstellte Objekt mindestens Object.prototype in seiner Prototypenkette. Der Prototyp von Object.prototype ist null, daher enden die meisten Prototypketten dort.
> Object.getPrototypeOf({}) === Object.prototype
true
> Object.getPrototypeOf(Object.prototype)
nullPrototyp-lose Objekte haben zwei Vorteile als Maps:
in-Operator frei verwenden, um zu erkennen, ob eine Eigenschaft existiert, und Klammern, um Eigenschaften zu lesen.__proto__ deaktiviert. In ECMAScript 6 wird die spezielle Eigenschaft __proto__ deaktiviert, wenn Object.prototype nicht in der Prototypenkette eines Objekts liegt. Sie können erwarten, dass JavaScript-Engines langsam zu diesem Verhalten übergehen, aber es ist noch nicht sehr verbreitet.Der einzige Nachteil ist, dass Sie die von Object.prototype bereitgestellten Dienste verlieren. Ein Dict-Objekt kann zum Beispiel nicht mehr automatisch in eine Zeichenfolge umgewandelt werden:
> console.log('Result: '+obj)
TypeError: Cannot convert object to primitive valueAber das ist kein wirklicher Nachteil, da es sowieso nicht sicher ist, Methoden direkt auf einem Dict-Objekt aufzurufen.
Verwenden Sie das Dict-Muster für schnelle Hacks und als Grundlage für Bibliotheken. In (nicht-Bibliotheks-)Produktionscode ist eine Bibliothek vorzuziehen, da Sie sicher sein können, alle Fallstricke zu vermeiden. Der nächste Abschnitt listet einige solcher Bibliotheken auf.
Es gibt viele Anwendungen für die Verwendung von Objekten als Maps. Wenn alle Eigenschaftsschlüssel statisch bekannt sind (zur Entwicklungszeit), müssen Sie nur sicherstellen, dass Sie die Vererbung ignorieren und nur eigene Eigenschaften betrachten. Wenn beliebige Schlüssel verwendet werden können, sollten Sie sich an eine Bibliothek wenden, um die in diesem Abschnitt genannten Fallstricke zu vermeiden. Hier sind zwei Beispiele:
Dieser Abschnitt ist eine schnelle Referenz mit Verweisen auf ausführlichere Erklärungen.
Objektliterale (siehe Objektliterale)
varjane={name:'Jane','not an identifier':123,describe:function(){// methodreturn'Person named '+this.name;},};// Call a method:console.log(jane.describe());// Person named Jane
Punktoperator (.) (siehe Punktoperator (.): Zugriff auf Eigenschaften über feste Schlüssel)
obj.propKeyobj.propKey=valuedeleteobj.propKey
Klammeroperator ([]) (siehe Klammeroperator ([]): Zugriff auf Eigenschaften über berechnete Schlüssel)
obj['propKey']obj['propKey']=valuedeleteobj['propKey']
Prototyp abrufen und festlegen (siehe Prototyp abrufen und festlegen)
Object.create(proto,propDescObj?)Object.getPrototypeOf(obj)
Iteration und Erkennung von Eigenschaften (siehe Iteration und Erkennung von Eigenschaften)
Object.keys(obj)Object.getOwnPropertyNames(obj)Object.prototype.hasOwnProperty.call(obj,propKey)propKeyinobj
Eigenschaften abrufen und definieren über Deskriptoren (siehe Eigenschaften abrufen und definieren über Deskriptoren)
Object.defineProperty(obj,propKey,propDesc)Object.defineProperties(obj,propDescObj)Object.getOwnPropertyDescriptor(obj,propKey)Object.create(proto,propDescObj?)
Objekte schützen (siehe Objekte schützen)
Object.preventExtensions(obj)Object.isExtensible(obj)Object.seal(obj)Object.isSealed(obj)Object.freeze(obj)Object.isFrozen(obj)
Methoden aller Objekte (siehe Methoden aller Objekte)
Object.prototype.toString()Object.prototype.valueOf()Object.prototype.toLocaleString()Object.prototype.isPrototypeOf(obj)Object.prototype.hasOwnProperty(key)Object.prototype.propertyIsEnumerable(propKey)