JavaScript’s eingebaute Konstruktoren sind schwierig zu unterklassifizieren. Dieses Kapitel erklärt warum und präsentiert Lösungen.
Wir verwenden den Ausdruck einen Built-in unterklassifizieren und vermeiden den Begriff erweitern, da dieser in JavaScript bereits vergeben ist.
AB eines gegebenen Built-in-Konstruktors A. Instanzen von B sind auch Instanzen von A.objEs gibt zwei Hindernisse für die Unterklassifizierung eines Built-ins: Instanzen mit internen Eigenschaften und ein Konstruktor, der nicht als Funktion aufgerufen werden kann.
Die meisten Built-in-Konstruktoren haben Instanzen mit sogenannten internen Eigenschaften (siehe Arten von Eigenschaften), deren Namen in doppelten eckigen Klammern geschrieben sind, wie folgt: [[PrimitiveValue]]. Interne Eigenschaften werden von der JavaScript-Engine verwaltet und sind normalerweise nicht direkt in JavaScript zugänglich. Die normale Unterklassifizierungstechnik in JavaScript besteht darin, einen Superkonstruktor als Funktion mit dem this des Unterkonstruktors aufzurufen (siehe Ebene 4: Vererbung zwischen Konstruktoren).
functionSuper(x,y){this.x=x;// (1)this.y=y;// (1)}functionSub(x,y,z){// Add superproperties to subinstanceSuper.call(this,x,y);// (2)// Add subpropertythis.z=z;}
Die meisten Built-ins ignorieren die als this übergebene Unterinstanz (2), ein Hindernis, das im nächsten Abschnitt beschrieben wird. Darüber hinaus ist das Hinzufügen interner Eigenschaften zu einer bestehenden Instanz (1) im Allgemeinen unmöglich, da sie die Natur der Instanz grundlegend verändern. Daher kann der Aufruf unter (2) nicht zum Hinzufügen interner Eigenschaften verwendet werden. Die folgenden Konstruktoren haben Instanzen mit internen Eigenschaften:
Instanzen von Boolean, Number und String umschließen primitive Werte. Sie alle haben die interne Eigenschaft [[PrimitiveValue]], deren Wert von valueOf() zurückgegeben wird; String hat zwei zusätzliche Instanzeigenschaften.
Boolean: Interne Instanzeigenschaft [[PrimitiveValue]].Number: Interne Instanzeigenschaft [[PrimitiveValue]].String: Interne Instanzeigenschaft [[PrimitiveValue]], benutzerdefinierte interne Instanzmethode [[GetOwnProperty]], normale Instanzeigenschaft length. [[GetOwnProperty]] ermöglicht den indizierten Zugriff auf Zeichen durch Lesen des umschlossenen Strings, wenn ein Array-Index verwendet wird.Array
[[DefineOwnProperty]] fängt das Setzen von Eigenschaften ab. Sie stellt sicher, dass die length-Eigenschaft korrekt funktioniert, indem sie length auf dem neuesten Stand hält, wenn Array-Elemente hinzugefügt werden, und überschüssige Elemente entfernt, wenn length verkleinert wird.Datum
[[PrimitiveValue]] speichert die von einer Datumsinstanz dargestellte Zeit (als Anzahl der Millisekunden seit dem 1. Januar 1970 00:00:00 UTC).Funktion
[[Call]] (der Code, der ausgeführt wird, wenn eine Instanz aufgerufen wird) und möglicherweise andere.RegExp
Die interne Instanzeigenschaft [[Match]], plus zwei nicht-interne Instanzeigenschaften. Gemäß der ECMAScript-Spezifikation:
Der Wert der internen Eigenschaft
[[Match]]ist eine implementierungsabhängige Darstellung des Musters desRegExp-Objekts.
Die einzigen Built-in-Konstruktoren, die keine internen Eigenschaften haben, sind Error und Object.
MyArray ist eine Unterklasse von Array. Sie hat einen Getter size, der die tatsächlichen Elemente in einem Array zurückgibt und Lücken ignoriert (wo length Lücken berücksichtigt). Der Trick, der zur Implementierung von MyArray verwendet wird, besteht darin, dass sie eine Array-Instanz erstellt und ihre Methoden hineinkopiert:[22]
functionMyArray(/*arguments*/){vararr=[];// Don’t use Array constructor to set up elements (doesn’t always work)Array.prototype.push.apply(arr,arguments);// (1)copyOwnPropertiesFrom(arr,MyArray.methods);returnarr;}MyArray.methods={getsize(){varsize=0;for(vari=0;i<this.length;i++){if(iinthis)size++;}returnsize;}}
Dieser Code verwendet die Hilfsfunktion copyOwnPropertiesFrom(), die in Kopieren eines Objekts gezeigt und erklärt wird.
Wir rufen den Array-Konstruktor in Zeile (1) nicht auf, wegen einer Eigenart: Wenn er mit einem einzelnen Parameter aufgerufen wird, der eine Zahl ist, wird die Zahl kein Element, sondern bestimmt die Länge eines leeren Arrays (siehe Initialisieren eines Arrays mit Elementen (vermeiden!)).
Hier ist die Interaktion
> var a = new MyArray('a', 'b')
> a.length = 4;
> a.length
4
> a.size
2Das Kopieren von Methoden auf eine Instanz führt zu Redundanzen, die mit einem Prototyp vermieden werden könnten (wenn wir die Option hätten, einen zu verwenden). Zusätzlich erstellt MyArray Objekte, die keine Instanzen davon sind.
> a instanceof MyArray false > a instanceof Array true
functionSuper(x,y){this.x=x;this.y=y;}functionSub(x,y,z){// Add superproperties to subinstanceSuper.call(this,x,y);// (1)// Add subpropertythis.z=z;}
Das Problem ist, dass Error immer eine neue Instanz erzeugt, auch wenn es als Funktion aufgerufen wird (1); das heißt, es ignoriert den Parameter this, der ihm über call() übergeben wird.
> var e = {};
> Object.getOwnPropertyNames(Error.call(e)) // new instance
[ 'stack', 'arguments', 'type' ]
> Object.getOwnPropertyNames(e) // unchanged
[]In der vorherigen Interaktion gibt Error eine Instanz mit eigenen Eigenschaften zurück, aber es ist eine neue Instanz, nicht e. Das Unterklassifizierungsmuster würde nur funktionieren, wenn Error die eigenen Eigenschaften zu this (e im vorherigen Fall) hinzufügen würde.
Erstellen Sie innerhalb des Unterkonstruktors eine neue Superinstanz und kopieren Sie deren eigene Eigenschaften auf die Unterinstanz.
functionMyError(){// Use Error as a functionvarsuperInstance=Error.apply(null,arguments);copyOwnPropertiesFrom(this,superInstance);}MyError.prototype=Object.create(Error.prototype);MyError.prototype.constructor=MyError;
Die Hilfsfunktion copyOwnPropertiesFrom() ist in Kopieren eines Objekts gezeigt. Ausprobieren von MyError
try{thrownewMyError('Something happened');}catch(e){console.log('Properties: '+Object.getOwnPropertyNames(e));}
hier ist die Ausgabe unter Node.js:
Properties: stack,arguments,message,type
Die instanceof-Beziehung ist wie sie sein sollte.
> new MyError() instanceof Error true > new MyError() instanceof MyError true
Delegation ist eine sehr saubere Alternative zur Unterklassifizierung. Um beispielsweise Ihren eigenen Array-Konstruktor zu erstellen, behalten Sie ein Array in einer Eigenschaft:
functionMyArray(/*arguments*/){this.array=[];Array.prototype.push.apply(this.array,arguments);}Object.defineProperties(MyArray.prototype,{size:{get:function(){varsize=0;for(vari=0;i<this.array.length;i++){if(iinthis.array)size++;}returnsize;}},length:{get:function(){returnthis.array.length;},set:function(value){returnthis.array.length=value;}}});
Die offensichtliche Einschränkung ist, dass Sie nicht über eckige Klammern auf Elemente von MyArray zugreifen können; Sie müssen Methoden dafür verwenden.
MyArray.prototype.get=function(index){returnthis.array[index];}MyArray.prototype.set=function(index,value){returnthis.array[index]=value;}
Normale Methoden von Array.prototype können über das folgende Stück Metaprogrammierung übertragen werden:
['toString','push','pop'].forEach(function(key){MyArray.prototype[key]=function(){returnArray.prototype[key].apply(this.array,arguments);}});
Wir leiten MyArray-Methoden von Array-Methoden ab, indem wir sie auf dem Array this.array aufrufen, das in Instanzen von MyArray gespeichert ist.
Verwendung von MyArray
> var a = new MyArray('a', 'b');
> a.length = 4;
> a.push('c')
5
> a.length
5
> a.size
3
> a.set(0, 'x');
> a.toString()
'x,b,,,c'