born83.com frontend development

Symbole in JavaScript

Der siebte Datentyp in JavaScript

Data

Mit der Einführung von ECMAScript 6 hat auch ein neuer primitiver Datentyp Symbol in JavaScript Einzug erhalten. Damit wurden die bisherigen Datentypen (Undefined, Null, Boolean, Number, String und Object) um einen weiteren Typ ergänzt: eben Symbol.

Symbole erzeugen

Symbole werden in JavaScript mit Hilfe der Methode Symbol() erzeugt. Dabei nimmt die Methode optional den Parameter Description entgegen: Symbol(Description). Dieser Parameter ist aber wirklich lediglich eine Beschreibung und dient nicht zur Identifikation des Symbols, wie man es z.B. vom Index eines Arrays oder dem Key eines Objekts her kennt.

Besonderheit von JavaScript Symbolen

Symbole haben eine grundlegende Besonderheit: sie sind stets einzigartig. Zwei Symbole sind niemals gleich, selbst wenn Sie ohne Description oder mit der selben Description instanziiert wurden:

Symbol() === Symbol()
Symbol("test") === Symbol("test")   

Beide Prüfungen liefern jeweils das Ergebnis false zurück!

Symbole statt Properties verwenden

Symbole eignen sich vor allem dazu, Objekte um Properties zu ergänzen und dabei Kollisionen zu vermeiden. Es soll also verhindert werden, dass eine bereits vorhandene Property ungewollt überschrieben wird.

Denkbar wäre beispielsweise eine Erweiterung eines Objektes um die Eigenschaft einer einzigartigen ID. Durch das setzen eines Wertes objekt.id = ... könnte dabei aber eine bereits vorhandene ID überschrieben werden.

Hier ist die Verwendung eines Symbols wesentlich sicherer, da die Einzigartigkeit garantiert, dass keine vorhandene Property überschrieben wird:

let objekt = {};
objekt.id = 'vorhandene ID';

let id = Symbol("id");
objekt[id] = 'neue ID';

console.log(objekt.id);   // vorhandene ID
console.log(objekt[id]);  // neue ID

Symbole und for..in Schleifen

Eine weitere Besonderheit von Symbolen ist, dass sie in for..in Schleifen übersprungen werden. Lassen wir über unser oben erstelltes Objekt objekt eine for..in Schleife laufen und alle Properties anzeigen, wird nur die id vorhandene ID ausgegeben, nicht aber unsere neue ID, welche wir mit Hilfe des Symbols id gesetzt haben:

for (let key in objekt) {
    console.log( key + ": " + objekt[key] );
}

Als Ausgabe erhalten wir lediglich zurück: id: vorhandene ID.

Symbole und die Object-Literal Notation

Auch bei einem Object-Literal lassen sich Symbole verwenden. Das könnte beispielsweise so aussehen:

  const id1 = Symbol("id");
  const id2 = Symbol("id");

  let objekt = {
    [id1] : "einzigartiger Wert",
    [id2] : "ein anderer Wert"
  }

Symbole in JavaScript Klassen verwenden

Auch bei der Verwendung von Klassen in JavaScript sind Symbole sehr nützlich, vor allem im Zusammenhang mit der Verwendung von Get und Set Methoden in JavaScript.

Benötigte Properties der Klasse werden außerhalb der Klasse als Symbol instanziiert und dann innerhalb der Methoden get() und set() als Property verwendet.

const dogName = Symbol();

class Dog {
    constructor(name) {
            this[dogName] = name;
    }
    get name() {
        return this[dogName];
    }
    set name(neuerName) {
      this[dogName] = neuerName;
    }
}

let snoopy = new Dog("Snoopy");
console.log(snoopy);
console.log(snoopy.name);

Im Gegensatz zur Unterstrich Konvention, bei der man sich einer Hilfs-Variablen mit vorangestelltem Unterstrich bedient, lässt sich die Property jetzt nur noch mit der Verwendung des entsprechenden Symbols ändern.

Allerdings sind diese Properties nicht vollständig versteckt. Mit der Methode Object.getOwnPropertySymbols(object) lassen sich die Symbole eines Objektes identifizieren, wodurch auch die set() Methode wieder umgangen werden kann:

snoopy[Object.getOwnPropertySymbols(snoopy)[0]] = "Lassie";
console.log(snoopy.name);               

Globale Symbole: die Symbol.for() Methode

Symbole bieten einen weiteren interessanten Aspekt: globale Symbole. Diese Symbole werden global registriert und sind dann nicht mehr einzigartig. Hinzu kommt, dass globale Symbole sich auch über Grenzen wie beispielsweise iFrames oder Serive Worker hinweg nutzen lassen. Zu diesem Thema wird noch ein weiterer Artikel folgen.

let symbol1 = Symbol.for('global');
let symbol2 = Symbol.for('global');
console.log( symbol1 === symbol2    );      // true

Wie lässt sich überprüfen, ob ein Symbol ein lokales oder globales Symbol ist? Ganz einfach: mit der Methode Symbol.keyFor().

let lokalesSymbol = Symbol('test');
let globalesSymbol = Symbol.for('test');

console.log( Symbol.keyFor(lokalesSymbol) );        // undefined
console.log( Symbol.keyFor(globalesSymbol) );       // test

Gibt die Methode Symbol.keyFor() einen Wert zurück, so liegt ein globales Symbol vor.

Well-known symbols = Standard Symbole oder auch System Symbole genannt

Neben der Möglichkeit eigene lokale und globale Symbole zu definieren, kommen mit ECMAScript 6 auch Standard Symbole mit verschiedensten Funktionalitäten, so genannte Well-known symbols.

Schleifen Symbole (Iteration symbols)

Symbol.iterator

Mit der Methode wird der Standard-Iterator für ein Objekt zurückgegeben, sofern er existiert. Die Methode findet unter anderen in der Implementierung der Methode for..of Anwendung. Mehr Informationen zum Iterator Protokoll hier.

const array = ['hallo', 'welt'];
const iterator = array[Symbol.iterator]();
let entry = iterator.next();
while(!entry.done) 
{
  console.log(entry.value);
  entry = iterator.next();
}

Bei diesem einfachen Beispiel wird das Array in eine Art einfache verkettete Liste zerlegt. Die Property done ist vom Typ Boolean und zeigt an, wann das Ende der Liste erreicht ist. Der aktuelle Wert wird in der Property value gespeichert. Mit der Methode next() lässt sich das nächste Element der Liste ansteuern.

Reguläre Ausdruck Symbole (Regular expression symbols)

Diese Methoden finden in der jeweiligen Implementierung von String.prototype Anwendung. Mit Hilfe der Standard Symbole lassen sich allerdings Alternativen zu Regulären Ausdrücken implementieren:

Symbol.match

Die Methode String.prototype.match() sucht in einem String das Vorkommen eines regulären Ausdrucks und gibt zurück, an welcher Position dieser gefunden wurde. Als einfaches praktisches Beispiel lässt sich so prüfen, ob eine angegebene Umsatzsteuer-ID gültig ist:

const gueltigeUmsatzsteuerID = "DE-123456789";
const ungueltigeUmsatzsteuerID = "DE-ABC123456";

const re = /DE-[0-9]{9}/i;

console.log( gueltigeUmsatzsteuerID.match(re) );
console.log( ungueltigeUmsatzsteuerID.match(re) );

Hier erfolgt die Überprüfung anhand des regulären Ausdrucks /DE-[0-9]{9}/. Mit Hilfe der Methode Symbol.match können wir nun eine Alternative implementieren, die sich dann genau wie ein regulärer Ausdruck in der Methode String.prototype.match() verwenden lässt:

const pruefeUmsatzsteuer = function() {
    return {
        [Symbol.match](eingabeID) {
            if (eingabeID.length === 12 && eingabeID.substr(0,2) === 'DE' && !isNaN(eingabeID.substr(3,12))) {
                return [eingabeID];
            }
            return null;
        }
    }
}

console.log( gueltigeUmsatzsteuerID.match(pruefeUmsatzsteuer()) );
console.log( ungueltigeUmsatzsteuerID.match(pruefeUmsatzsteuer()) );

Beim Aufruf der Methode match findet nun zuerst ein Abgleich nach einer Implementierung der Methode [Symbol.match] statt. Liegt diese vor, wird diese Methode anstelle von RegExp angewandt.

Symbol.replace

Analog zu Symbol.match lassen sich auch für String.prototype.replace() mit Symbol.replace Methoden definieren, die statt regulärer Ausdrücke verwendet werden können:

const tempF = "50°F";
const re = /(\d+)°F/;

console.log( tempF.replace( re, function (eingabe, fahrenheit) {
    return ((fahrenheit - 32) * 5/9) + "°C";
}));

In diesem Beispiel durchsuchen wir den Eingabe-String mit Hilfe des regulären Ausdrucks /(\d+)°F/ nach einem Vorkommen einer Temperaturangabe in Grad Fahrenheit und rechnen diesen dann direkt um.

Mit Hilfe von Symbol.replace könnte dies wie folgt implementiert werden:

const inCelsius = function() {
    return {
      [Symbol.replace](eingabe) {
        const wert = parseInt(eingabe.split("°F")[0]);
        return (wert - 32) * 5/9 + "°C"
      }
    }
}

console.log( tempF.replace(inCelsius()) );

Für die Methode für String.prototype.search() existiert das Standard Symbol Symbol.search. String.search() sucht anhand eines regulären Ausdrucks nach dessen Vorkommen und gibt den Index zurück:

const str = "Der flinke, braune Fuchs springt über den faulen Hund?";
const re = /Fuchs/g;
console.log( str.search( re ) );

Mit Hilfe von Symbol.search könnte dies wie folgt implementiert werden:

const findeWort = function(wort) {
    return {
            wort : wort,
        [Symbol.search](eingabe) {
                return eingabe.indexOf(wort);
      }
    }
}

console.log( str.search(findeWort("Fuchs")) );

Symbol.split

Und auch für die Methode String.prototype.split() gibt es die Möglichkeit über das Standard Symbol Symbol.split individuelle Implementierungen umzusetzen.

Angenommen wir wollen einen Text in seine einzelnen Worte zerlegen, könnten wir das mit der Methode String.prototype.split() einfach erreichen:

const text = "Der flinke, braune Fuchs springt über den faulen Hund?"
console.log( text.split(" ") );

Der String wird jeweils beim Auftauchen eines Leerzeichens zerschnitten, daher erhalten wir als Ergebnis ein Array mit den einzelnen Worten. Unschön für die Weiterverarbeitung ist allerdings, dass die Satzzeichen nicht entfernt wurden. Dies müssten wir nun in einem zweiten Schritt erledigen, indem wir die Einträge des Arrays prüfen und bereinigen.

Oder wir verwenden Symbol.split um einen eigenen Splitter zu implementieren, der direkt die Satzzeichen entfernt:

const wortSplitter = function() {
    return {
        [Symbol.split](eingabe) {
                return eingabe.replace(/[,.;!?]/g,'').split(" ");
      }
    }
}

console.log( text.split(wortSplitter()) );

Das Ergebnis ist jetzt ein Array, bei dem wir nur die Worte erhalten, die schon um die Satzzeichen bereinigt wurden.

Weitere Standard Symbole (well-known symbols)

Symbol.hasInstance

Mit Hilfe dieses Symbols ist die Methode instanceof ab sofort erweiterbar. Zum Hintergrund: mit Hilfe von instanceof lässt sich überprüfen, ob ein Objekt die Instanz einer Klasse ist:

class Dog {
        constructor(name) {
            this.name = name;
        }
}
var snoopy = new Dog("Snoopy");
console.log( snoopy instanceof Dog );

Mit dem Standard Symbol Symbol.hasInstance können wir hier nun die Methode erweitern. Aber Achtung: die erweiterte Methode überschreibt die Standard-Methode instanceof! Das bedeutet, es muss sicher gestellt sein, dass die charakteristischen Merkmale der Klasse überprüft werden. Für das Beispiel oben nehmen wir einfach an, ein Hund ist nur dann ein Hund, wenn er den Namen Snoopy hat:

class Dog {
        constructor(name) {
            this.name = name;
        }
        static [Symbol.hasInstance](object) {
        return object.name === "Snoopy";
    }
}

var snoopy = new Dog("Snoopy");
console.log( snoopy instanceof Dog );       // true
var lassie = new Dog("Lassie");
console.log(lassie instanceof Dog);     // false

Die offensichtliche Instanz der Klasse Dog lassie wird somit nicht mehr als Instanz der Klasse identifiziert.

Symbol.isConcatSpreadable

Die Methode Array.prototype.concat() dient dazu, zwei Arrays konkateniert.

let arrayA = [1,2,3];
let arrayB = [4,5,6];
console.log( arrayA.concat(arrayB) );       // [ 1,2,3,4,5,6 ]

Durch das Standard-Symbol Symbol.isConcatSpreadable können wir hier Einfluss nehmen und bestimmen, welche Arrays konkateniert werden sollen und welche nicht. Dazu setzen wir einfach für Symbol.isConcatSpreadable den Boolean Wert true oder false:

let arrayA = [1,2,3];
let arrayB = [4,5,6];
arrayB[Symbol.isConcatSpreadable] = false;
console.log( arrayA.concat(arrayB) );       // [ 1,2,3, [4,5,6] ]

Das lässt sich natürlich nicht nur für einzelne Arrays festlegen, sondern auch für ganze Klassen, die Arrays erweitern:

class FilmSammlung extends Array {
        get [Symbol.isConcatSpreadable]() {
        return false;
    }
}

let sammlung1 = new FilmSammlung( 'Die fabelhafte Welt der Amelie', 'Zurück in die Zukunft', 'Grand Budapest Hotel' );
let sammlung2 = new FilmSammlung( 'Terminator', 'Stirb Langsam', 'Con Air' );
console.log( sammlung1.concat(sammlung2) );

Je nachdem, wie die Rückgabe für die Methode get [Symbol.isConcatSpreadable]() aussieht (true oder false) werden die einzelnen Elemente der Klasse FilmSammlung nun zu einer einzigen FilmSammlung konkateniert, oder es entsteht eine Instanz der Klasse FilmSammlung, die zwei weitere Instanzen der Klasse FilmSammlung enthält.

Symbol.unscopables

Mit ECMAScript6 wurde die Methode Object.keys() eingeführt, die ein Array zurückgibt, welches alle aufzählbaren Eigenschaften des Objektes enthält:

var obj = { name: 'Das Objekt', farbe: 'Gelb' };
console.log( Object.keys(obj) );                // ["name", "farbe"]

Zusätzlich existiert die Methode with(), welche als Argument ein Objekt erwartet und dieses Objekt dann ganz vorne in den Scope setzt:

with(obj) {
    console.log(name);      // Das Objekt
    console.log(farbe);     // Gelb
}

Hier wird nun also zuerst bei der Insatz obj nach den Properties name und farbe geschaut. Mit Hilfe von Symbol.unscopables lassen sich nun Properties definieren, die dabei ausdrücklich ausgeschlossen werden sollen:

obj[Symbol.unscopables] = { name: true };

Die Property name ist nun nicht mehr innerhalb der Anweisung with() im Scope auffindbar und es kann dementsprechend auch kein Wert ausgegeben werden.

Damit keine Missverständnisse entstehen: die Property ist nach wie vor sichtbar und wird z.B. auch bei der Methode Object.keys(obj) mit ausgegeben. Darauf hat das Symbol Symbol.unscopables keinen Einfluss. Hier geht es darum, dass bestimmte Properties eines Objektes nicht in den aktuellen Scope übertragen werden.

Verdeutlicht werden kann das am Beispiel der Methode Array.prototype.keys(). Die Methode erzeugt als Rückgabewert einen Iterator, mit allen Indizes des Arrays:

let kinder = [{name:'pia'}, {name:'peter'}, {name:'paul'}];
let iterator = kinder.keys();   
for (let key of iterator) 
{
    console.log( key, kinder[key].name );
}      

Angenommen wir hätten vorher ein Array mit dem Namen keys definiert und würden zusätzlich mit einem with() Ausdruck arbeiten, dann könnte es ein ernsthaftes Problem geben:

let kinder = [{name:'pia'}, {name:'peter'}, {name:'paul'}];
let keys = ['key1','key2','key3'];
with(kinder)
    console.log(keys);  

Welche Ausgabe erwarten wir jetzt für die Anweisung console.log(keys);? Im aktuellen Scope haben wir für die Variable keys ein Array definiert. Es existiert aber für das Array kinder ebenfalls eine Methode keys(), die nun durch die Methode with(kinder) in den aktuellen Scope kopiert würde und unser Array keys überschreiben würde.

Damit genau das nicht passiert, gibt es diverse Standard-Properties, die nie in den aktuellen Scope übertragen werden. Diese lassen sich beispielsweise für Array.prototype wie folgt ausgeben:

console.log( Object.keys(Array.prototype[Symbol.unscopables]) );

// copyWithin
// entries
// fill
// find
// findIndex
// includes
// keys
// values

Symbol.species

Dieses Symbol findet immer dann Anwendung, wenn neue Objekte auf basis bestehender Objekte abgeleitet werden. Ein Beispiel hierfür wäre ein Aufruf der Methode map() auf ein Array. Das könnte beispielsweise so aussehen:

class FilmSammlung extends Array {
    static get [Symbol.species]() {
        return this;    
    }
}

let sammlung = new FilmSammlung( 
    { name : 'Die fabelhafte Welt der Amelie' }, 
    { name : 'Zurück in die Zukunft' }, 
    { name : 'Grand Budapest Hotel' } 
);

let map = sammlung.map( function(film) { return film.name } );
console.log(map);
console.log(map instanceof FilmSammlung);

Das Ergebnis ist eine neue Instanz der Klasse FilmSammlung, welche als Werte die Werte der Properties name der Instanz sammlung enthält: FilmSammlung(3) ["Die fabelhafte Welt der Amelie", "Zurück in die Zukunft", "Grand Budapest Hotel"]. Der Befehl map instanceof FilmSammlung gibt den Wert true zurück.

Mit Hilfe des Symbols Symbol.species können wir den Rückgabetypen unserer Klasse jetzt beeinflussen. Passen wir die Zeile return this; an in return Array;, erhalten wir als Ergebnis keine Instanz der Klasse FilmSammlung mehr, sondern eine Array-Instanz: ["Die fabelhafte Welt der Amelie", "Zurück in die Zukunft", "Grand Budapest Hotel"]. Analog dazu ist das Ergebnis von map instanceof FilmSammlung jetzt false.

Symbol.toStringTag

Das Symbol Symbol.toStringTag gibt vor, wie die Methode toString für ein Objekt implementiert wird:

class Programmierer {
    constructor(vorname, nachname) {
        this.vorname = vorname;
        this.nachname = nachname;
    }
    get [Symbol.toStringTag]() {
        return 'Programmierer';
    }
}

let hanno = new Programmierer("Hanno", "Drefke");
console.log( hanno.toString() );                // [object, Programmierer]

Mit Hilfe dieser Methode lässt sich nun auch die oben erwähnte Methode instanceof erweitern:

class Programmierer {
    constructor(vorname, nachname) {
        this.vorname = vorname;
        this.nachname = nachname;
    }
    get [Symbol.toStringTag]() {
        return 'Programmierer';
    }
    static [Symbol.hasInstance](object) {
        return object.toString() === "[object Programmierer]";
    }
}

let hanno = new Programmierer("Hanno", "Drefke");
console.log( hanno instanceof Programmierer);

Symbol.toPrimitive

Noch einen Schritt weiter geht das Symbol Symbol.toPrimitive. Analog zur Definition des Datentyps bei Ableitungen von Objekten, lässt sich auch definieren, welchem Datentyp die Umwandlung eines Objektes entsprechen soll. Dazu dient das Symbol Symbol.toPrimitive.

Dabei wird automatisch der Typ der Variable als Parameter mitgegeben. Dadurch lassen sich beispielsweise unterschiedliche Rückgaben für die Datentypen Number oder String umsetzen:

class Mensch {
    constructor(vorname, nachname, alter) {
        this.vorname = vorname;
        this.nachname = nachname;
        this.alter = alter;
    }
    [Symbol.toPrimitive](aufrufTyp) {
        return aufrufTyp == "number" ? this.alter : "" + this.nachname + ", " + this.vorname;
    }
}

let hanno = new Mensch("Hanno", "Drefke", 35);
let hannoName = new String(hanno);
let hannoAlter = new Number(hanno);
console.log( hannoName );
console.log( hannoAlter );

Mehr Informationen zum Datentyp Symbol

Mehr Informationen zum primitiven Datentyp Symbol in JavaScript hier:

https://developer.mozilla.org/de/docs/Glossary/Symbol

https://www.ecma-international.org/ecma-262/6.0/#sec-ecmascript-language-types-symbol-type

https://www.ecma-international.org/ecma-262/6.0/#sec-symbol-objects

https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Symbol

https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/RegExp

https://hacks.mozilla.org/2015/06/es6-in-depth-symbols/

https://www.keithcirkel.co.uk/metaprogramming-in-es6-symbols/