Features von übermorgen: ES7 Observer

Tales from the Web side  –  3 Kommentare

Ein Feature, welches es nicht mehr in den ES6-Standard geschafft, aber mittlerweile den Status eines Draft inne hat und somit für eine Aufnahme in ES7 ganz gut positioniert scheint, ist die Methode Object.observe().

Mit Hilfe dieser Methode ist es möglich, sogenannte Observer an Objekten zu registrieren, um anschließend über Änderungen an dem jeweiligen Objekt informiert zu werden. Wie das funktioniert, zeigt dieser Artikel.

Einführung

Die Methode Obect.observe() erwartet zwei Parameter: das zu "beobachtende" Objekt sowie eine Callback-Funktion, die aufgerufen wird, wenn sich etwas am Objekt ändert (sozusagen die Observer-Komponente). Vom Prinzip her sieht das wie folgt aus:

Object.observe(person, function(changes) { /* Zugriff auf die Änderungen */});

Unter Verwendung einer Arrow-Funktion lautet die Schreibweise dagegen:

Object.observe(person, changes => { /* Zugriff auf die Änderungen */});

Oder man lagert die Funktion wie in folgendem Beispiel (und in allen weiteren) zu sehen komplett aus:

function observer(changes) { /* Zugriff auf die Änderungen */}
Object.observe(person, observer);

Innerhalb der Callback-Funktion hat man dann Zugriff auf die Änderungen, die am Objekt vorgenommen wurden (changes). Dabei handelt es sich um ein Array von Objekten, welche jeweils eine Änderung repräsentieren. Jede Änderung wiederum hat vier Eigenschaften: Die Eigenschaft type bezeichnet den Typ der Änderung. Hierbei unterscheidet man zwischen "update" für das Aktualisieren einer Eigenschaft, "add" für das Hinzufügen, "delete" für das Löschen sowie "reconfigure" und "preventExtensions", falls Eigenschaften des Property-Deskriptors verändert werden, z.B. beim Aufruf von Object.freeze(). Die Eigenschaft name dagegen enthält den Namen der betroffenen Eigenschaft, oldValue ihren alten Wert und object schließlich das Objekt, das beobachtet wird.

Anhand folgenden Listings können Sie das Beschriebene in Aktion sehen. Sie sehen beispielsweise: Ändert man den Wert der Eigenschaft firstName, führt dies zu einer Änderung vom Typ "update", fügt man eine neue Eigenschaft (lastName) hinzu, führt dies zu einer Änderung vom Typ "add" und so weiter. Insgesamt werden auf diese Weise sechs Veränderungen am Objekt registriert. Erwähnenswert: Die object-Eigenschaft des change-Objekts enthält immer den Wert des aktuellen Objekts (und nicht etwa den zum Zeitpunkt der entsprechenden Änderung). Im Beispiel hat das Objekt daher immer den Wert {firstName: 'Moritz'}, sprich den Zustand nach allen Änderungen.

'use strict'
let person = {
firstName: 'Max'
};
function observer(changes) {
console.log(changes.length); // Ausgabe: 6 (siehe unten)
changes.forEach(function(change) {
console.log(
change.type, // Typ der Änderung
change.name, // Name der geänderten Eigenschaft
change.oldValue, // Alter Wert
change.object // Objekt
)
});
}

Object.observe(person, observer);

person.firstName = 'Moritz;' // Ausgabe: "update firstName Max {
// firstName: 'Moritz;' }"
person.lastName = 'Patternman'; // Ausgabe: "add lastName undefined {
// firstName: 'Moritz;' }"
person.lastName = 'Mustermann'; // Ausgabe: "update lastName
// Patternman { firstName: 'Moritz;'
// }"
delete person.lastName; // Ausgabe: "delete lastName
// Mustermann { firstName: 'Moritz;'
// }"
Object.freeze(person); // Ausgabe: "reconfigure firstName
// undefined { firstName: 'Moritz;' }
// preventExtensions undefined
// undefined { firstName: 'Moritz;'
// }"

Selektives Beobachten

Möchte man nicht auf alle Typen von Änderungen lauschen, kann man entweder innerhalb des Observers eine entsprechend selektierende Funktionalität implementieren. Alternativ kann der Methode Object.observe() aber auch als dritter Parameter ein Array übergeben werden, das die Typen der Änderungen enthält, auf die man reagieren möchte ("update", "add", "delete", "reconfigure" und "preventExtensions"). Ist man beispielsweise nur an Änderungen vom Typ "update" interessiert, reicht ein Aufruf von Object.observe() wie folgt:

'use strict'
let person = {
firstName: 'Max'
};
function observer(changes) {
console.log(changes.length);
changes.forEach(change => {
console.log(
change.type, // Typ der Änderung
change.name, // Name der geänderten Eigenschaft
change.oldValue, // Alter Wert
change.object // Objekt
)
});
}
Object.observe(person, observer, ['update']);

person.firstName = 'Moritz;' // Ausgabe: "update firstName Max {
// firstName: 'Moritz;' }"
person.lastName = 'Patternman'; // Keine Ausgabe
person.lastName = 'Mustermann'; // Ausgabe: "update lastName
// Patternman { firstName: 'Moritz;'
// }"
delete person.lastName; // Keine Ausgabe
Object.freeze(person); // Keine Ausgabe

Beobachten beenden

Analog zu der Methode Object.observe() steht die Methode Object.unobserve() zur Verfügung, über die sich ein Observer wieder von einem Objekt lösen lässt. Folgendes Codebeispiel zeigt ihre Verwendung. Man sieht, dass anschließende Änderungen am Objekt nicht mehr vom Observer registriert werden (und daher im Beispiel dann auch nicht auf die Konsole ausgegeben werden).

'use strict'
let person = {
firstName: 'Max'
};
function observer(changes) {
changes.forEach(function(change) {
console.log(
change.type, // Typ der Änderung
change.name, // Name der geänderten Eigenschaft
change.oldValue, // Alter Wert
change.object // Objekt
)
});
}
Object.observe(person, observer);

person.firstName = 'Moritz;' // Ausgabe: "update firstName Max {
// firstName: 'Moritz;' }"
person.lastName = 'Patternman'; // Ausgabe: "add lastName undefined {
// firstName: 'Moritz;' }"
person.lastName = 'Mustermann'; // Ausgabe: "update lastName
// Patternman { firstName: 'Moritz;'
// }"
delete person.lastName; // Ausgabe: "delete lastName
// Mustermann { firstName: 'Moritz;'
// }"

Object.unobserve(person, observer);

person.firstName = 'Moritz;' // Keine Ausgabe
person.lastName = 'Patternman'; // Keine Ausgabe
person.lastName = 'Mustermann'; // Keine Ausgabe
delete person.lastName; // Keine Ausgabe

Events auslösen

Möchte man eines der vorgenannte Events selber auslösen, erreicht man dies wie in folgendem Listing mit Hilfe des Codes Object.getNotifier(object).notify() (wobei object das entsprechend zu beobachtende Objekt ist, im Beispiel eben this). Ein typischer Anwendungsfall wäre das manuelle Überschreiben des Getters (get) oder Setters (set). Dann wird standardmäßig nämlich keines der Events erzeugt und man ist innerhalb seiner Implementierung selbst verantwortlich dafür, das entsprechende Event auszulösen.

'use strict'
let person = {
firstName: 'Max'
};

let _lastName = 4711;
Object.defineProperty(person, 'lastName', {
get: function() {
return _lastName;
},
set: function(lastName) {
Object.getNotifier(this).notify({
type: 'update',
name: 'lastName',
oldValue: _lastName
});
_lastName = lastName;
}
});

function observer(changes) {
changes.forEach(function(change) {
console.log(
change.type, // Typ der Änderung
change.name, // Name der geänderten Eigenschaft
change.oldValue, // Alter Wert
change.object // Objekt
)
});
}
Object.observe(person, observer);

person.lastName = 'Patternman'; // Ausgabe: update lastName 4711 {
// firstName: 'Max' }
person.lastName = 'Mustermann'; // Ausgabe: update lastName Patternman
// { firstName: 'Max' }

Arrays beobachten

Was für Objekte funktioniert, funktioniert auch für Arrays. Dementsprechend stehen die Methoden Array.observe() und Array.unobserve() bereit. Vom Prinzip her funktioniert das genauso wie eben für Objekte gezeigt. Auch hier gilt aber: change.object enthält immer das aktuelle Array, also das Array im Zustand, nachdem alle Änderungen ausgeführt wurden. Im Listing sieht man das deutlich, da das Array immer den Wert [ 'Max', 'Moritz', 'Peter' ] hat.

'use strict'
let names = ['Moritz', 'Max'];
function observer(changes) {
changes.forEach(function(change) {
console.log(
change.type, // Typ der Änderung
change.name, // Name der geänderten Eigenschaft
change.oldValue, // Alter Wert
change.object // Array
)
});
}
Array.observe(names, observer);
names[0] = 'Max'; // Ausgabe: update 0 Moritz [ 'Max', 'Moritz',
// 'Peter' ]
names[1] = 'Moritz'; // Ausgabe: update 1 Max [ 'Max', 'Moritz',
// 'Peter' ]
names.push('Peter'); // splice undefined undefined [ 'Max', 'Moritz',
// 'Peter' ]

Fazit

Object.observe() hat wie erwähnt Draft-Status und wird es aller Voraussicht nach in den ES7-Standard schaffen. Momentan werden Object.observe() und die anderen relevanten Methoden von Chrome, Node.js und io.js implementiert, die in dieser Hinsicht sogar momentan den Transpilern überlegen sind: Traceur und Babel implementieren das neue Feature beispielsweise noch nicht. Ein Bereich, in dem Object.observe() wahrscheinlich großen Einfluss haben könnte, ist das Data Binding. Dieses könnte sich grundlegend (Stichwort: Dirty Checking) vereinfachen. Die Zukunft wirds zeigen.