Aufzählbarkeit ist ein Attribut von Objekteigenschaften. In diesem Kapitel betrachten wir genauer, wie es verwendet wird und wie es Operationen wie Object.keys() und Object.assign() beeinflusst.
Erforderliches Wissen: Eigenschaftsattribute
Für dieses Kapitel sollten Sie mit Eigenschaftsattributen vertraut sein. Wenn nicht, lesen Sie §9 „Eigenschaftsattribute: eine Einführung“.
Um zu demonstrieren, wie verschiedene Operationen von der Aufzählbarkeit beeinflusst werden, verwenden wir das folgende Objekt obj, dessen Prototyp proto ist.
const protoEnumSymbolKey = Symbol('protoEnumSymbolKey');
const protoNonEnumSymbolKey = Symbol('protoNonEnumSymbolKey');
const proto = Object.defineProperties({}, {
protoEnumStringKey: {
value: 'protoEnumStringKeyValue',
enumerable: true,
},
[protoEnumSymbolKey]: {
value: 'protoEnumSymbolKeyValue',
enumerable: true,
},
protoNonEnumStringKey: {
value: 'protoNonEnumStringKeyValue',
enumerable: false,
},
[protoNonEnumSymbolKey]: {
value: 'protoNonEnumSymbolKeyValue',
enumerable: false,
},
});
const objEnumSymbolKey = Symbol('objEnumSymbolKey');
const objNonEnumSymbolKey = Symbol('objNonEnumSymbolKey');
const obj = Object.create(proto, {
objEnumStringKey: {
value: 'objEnumStringKeyValue',
enumerable: true,
},
[objEnumSymbolKey]: {
value: 'objEnumSymbolKeyValue',
enumerable: true,
},
objNonEnumStringKey: {
value: 'objNonEnumStringKeyValue',
enumerable: false,
},
[objNonEnumSymbolKey]: {
value: 'objNonEnumSymbolKeyValue',
enumerable: false,
},
});| Operation | Zeichenketten-Schlüssel | Symbol-Schlüssel | Vererbt | |
|---|---|---|---|---|
Object.keys() |
ES5 | ✔ |
✘ |
✘ |
Object.values() |
ES2017 | ✔ |
✘ |
✘ |
Object.entries() |
ES2017 | ✔ |
✘ |
✘ |
Spreading {...x} |
ES2018 | ✔ |
✔ |
✘ |
Object.assign() |
ES6 | ✔ |
✔ |
✘ |
JSON.stringify() |
ES5 | ✔ |
✘ |
✘ |
for-in |
ES1 | ✔ |
✘ |
✔ |
Die folgenden Operationen (zusammengefasst in Tab. 2) berücksichtigen nur aufzählbare Eigenschaften
Object.keys() [ES5] gibt die Schlüssel von aufzählbaren eigenen Eigenschaften mit Zeichenketten-Schlüsseln zurück.
Object.values() [ES2017] gibt die Werte von aufzählbaren eigenen Eigenschaften mit Zeichenketten-Schlüsseln zurück.
Object.entries() [ES2017] gibt Schlüssel-Wert-Paare für aufzählbare eigene Eigenschaften mit Zeichenketten-Schlüsseln zurück. (Beachten Sie, dass Object.fromEntries() Symbole als Schlüssel akzeptiert, aber nur aufzählbare Eigenschaften erstellt.)
Das Spreading in Objekt-Literale [ES2018] berücksichtigt nur eigene aufzählbare Eigenschaften (mit Zeichenketten-Schlüsseln oder Symbol-Schlüsseln).
Object.assign() [ES6] kopiert nur aufzählbare eigene Eigenschaften (mit Zeichenketten-Schlüsseln oder Symbol-Schlüsseln).
JSON.stringify() [ES5] serialisiert nur aufzählbare eigene Eigenschaften mit Zeichenketten-Schlüsseln.
for-in Schleife [ES1] durchläuft die Schlüssel von eigenen und geerbten aufzählbaren Eigenschaften mit Zeichenketten-Schlüsseln.
for-in ist die einzige eingebaute Operation, bei der die Aufzählbarkeit für geerbte Eigenschaften eine Rolle spielt. Alle anderen Operationen arbeiten nur mit eigenen Eigenschaften.
| Operation | Str. Schlüssel | Sym. Schlüssel | Vererbt | |
|---|---|---|---|---|
Object.getOwnPropertyNames() |
ES5 | ✔ |
✘ |
✘ |
Object.getOwnPropertySymbols() |
ES6 | ✘ |
✔ |
✘ |
Reflect.ownKeys() |
ES6 | ✔ |
✔ |
✘ |
Object.getOwnPropertyDescriptors() |
ES2017 | ✔ |
✔ |
✘ |
Die folgenden Operationen (zusammengefasst in Tab. 3) berücksichtigen sowohl aufzählbare als auch nicht aufzählbare Eigenschaften
Object.getOwnPropertyNames() [ES5] listet die Schlüssel aller eigenen Eigenschaften mit Zeichenketten-Schlüsseln auf.
Object.getOwnPropertySymbols() [ES6] listet die Schlüssel aller eigenen Eigenschaften mit Symbol-Schlüsseln auf.
Reflect.ownKeys() [ES6] listet die Schlüssel aller eigenen Eigenschaften auf.
Object.getOwnPropertyDescriptors() [ES2017] listet die Eigenschaftsdeskriptoren aller eigenen Eigenschaften auf.
> Object.getOwnPropertyDescriptors(obj)
{
objEnumStringKey: {
value: 'objEnumStringKeyValue',
writable: false,
enumerable: true,
configurable: false
},
objNonEnumStringKey: {
value: 'objNonEnumStringKeyValue',
writable: false,
enumerable: false,
configurable: false
},
[objEnumSymbolKey]: {
value: 'objEnumSymbolKeyValue',
writable: false,
enumerable: true,
configurable: false
},
[objNonEnumSymbolKey]: {
value: 'objNonEnumSymbolKeyValue',
writable: false,
enumerable: false,
configurable: false
}
}Introspektion ermöglicht es einem Programm, die Struktur von Werten zur Laufzeit zu untersuchen. Es ist Metaprogrammierung: Normale Programmierung beschäftigt sich mit dem Schreiben von Programmen; Metaprogrammierung beschäftigt sich mit der Untersuchung und/oder Änderung von Programmen.
In JavaScript haben gängige introspektive Operationen kurze Namen, während selten verwendete Operationen lange Namen haben. Das Ignorieren von nicht aufzählbaren Eigenschaften ist die Norm, weshalb Operationen, die dies tun, kurze Namen und Operationen, die dies nicht tun, lange Namen haben
Object.keys() ignoriert nicht aufzählbare Eigenschaften.Object.getOwnPropertyNames() listet die Zeichenketten-Schlüssel aller eigenen Eigenschaften auf.Die Methoden von Reflect (wie Reflect.ownKeys()) weichen jedoch von dieser Regel ab, da Reflect Operationen bereitstellt, die „metaer“ sind und sich auf Proxies beziehen.
Zusätzlich wird folgende Unterscheidung getroffen (seit ES6, das Symbole eingeführt hat)
Daher wäre ein besserer Name für Object.keys() jetzt Object.names().
In diesem Abschnitt kürzen wir Object.getOwnPropertyDescriptor() wie folgt ab
Die meisten Dateneigenschaften werden mit folgenden Attributen erstellt
Dies schließt ein
Object.fromEntries()Die wichtigsten nicht aufzählbaren Eigenschaften sind
Prototyp-Eigenschaften von eingebauten Klassen
Per benutzerdefinierten Klassen erstellte Prototyp-Eigenschaften
Eigenschaft .length von Arrays
Eigenschaft .length von Zeichenketten (beachten Sie, dass alle Eigenschaften von primitiven Werten schreibgeschützt sind)
Wir werden uns als Nächstes die Anwendungsfälle für die Aufzählbarkeit ansehen, die uns sagen werden, warum einige Eigenschaften aufzählbar sind und andere nicht.
Aufzählbarkeit ist ein inkonsistentes Merkmal. Sie hat zwar Anwendungsfälle, aber es gibt immer eine Art von Vorbehalt. In diesem Abschnitt betrachten wir die Anwendungsfälle und die Vorbehalte.
for-in SchleifeDie for-in Schleife durchläuft alle aufzählbaren Eigenschaften eines Objekts mit Zeichenketten-Schlüsseln, sowohl eigene als auch geerbte. Daher wird das Attribut enumerable verwendet, um Eigenschaften zu verstecken, die nicht durchlaufen werden sollen. Dies war der Grund für die Einführung der Aufzählbarkeit in ECMAScript 1.
Im Allgemeinen ist es am besten, for-in zu vermeiden. Die nächsten beiden Unterabschnitte erklären warum. Die folgende Funktion hilft uns zu demonstrieren, wie for-in funktioniert.
function listPropertiesViaForIn(obj) {
const result = [];
for (const key in obj) {
result.push(key);
}
return result;
}for-in für Objektefor-in iteriert über alle Eigenschaften, einschließlich geerbter
const proto = {enumerableProtoProp: 1};
const obj = {
__proto__: proto,
enumerableObjProp: 2,
};
assert.deepEqual(
listPropertiesViaForIn(obj),
['enumerableObjProp', 'enumerableProtoProp']);Bei normalen einfachen Objekten sieht for-in keine geerbten Methoden wie Object.prototype.toString(), da diese alle nicht aufzählbar sind
In benutzerdefinierten Klassen sind alle geerbten Eigenschaften ebenfalls nicht aufzählbar und werden daher ignoriert
class Person {
constructor(first, last) {
this.first = first;
this.last = last;
}
getName() {
return this.first + ' ' + this.last;
}
}
const jane = new Person('Jane', 'Doe');
assert.deepEqual(
listPropertiesViaForIn(jane),
['first', 'last']);Fazit: Bei Objekten berücksichtigt for-in geerbte Eigenschaften, und wir möchten diese normalerweise ignorieren. Dann ist es besser, eine for-of Schleife mit Object.keys(), Object.entries() usw. zu kombinieren.
for-in für ArraysDie eigene Eigenschaft .length ist bei Arrays und Zeichenketten nicht aufzählbar und wird daher von for-in ignoriert
Es ist jedoch im Allgemeinen nicht sicher, for-in zum Iterieren über die Indizes eines Arrays zu verwenden, da es sowohl geerbte als auch eigene Eigenschaften berücksichtigt, die keine Indizes sind. Das folgende Beispiel zeigt, was passiert, wenn ein Array eine eigene Nicht-Index-Eigenschaft hat
const arr1 = ['a', 'b'];
assert.deepEqual(
listPropertiesViaForIn(arr1),
['0', '1']);
const arr2 = ['a', 'b'];
arr2.nonIndexProp = 'yes';
assert.deepEqual(
listPropertiesViaForIn(arr2),
['0', '1', 'nonIndexProp']);Fazit: for-in sollte nicht zum Iterieren über die Indizes eines Arrays verwendet werden, da es sowohl Index-Eigenschaften als auch Nicht-Index-Eigenschaften berücksichtigt
Wenn Sie an den Schlüsseln eines Arrays interessiert sind, verwenden Sie die Array-Methode .keys()
Wenn Sie über die Elemente eines Arrays iterieren möchten, verwenden Sie eine for-of Schleife, die den zusätzlichen Vorteil hat, auch mit anderen iterierbaren Datenstrukturen zu funktionieren.
Indem wir Eigenschaften nicht aufzählbar machen, können wir sie vor einigen Kopiervorgängen verstecken. Betrachten wir zunächst zwei historische Kopiervorgänge, bevor wir uns moderneren Kopiervorgängen zuwenden.
Object.extend()Prototype ist ein JavaScript-Framework, das Sam Stephenson im Februar 2005 als Grundlage für die Ajax-Unterstützung in Ruby on Rails erstellt hat.
Object.extend(destination, source) von Prototype kopiert alle aufzählbaren eigenen und geerbten Eigenschaften von source in eigene Eigenschaften von destination. Es wird wie folgt implementiert
function extend(destination, source) {
for (var property in source)
destination[property] = source[property];
return destination;
}Wenn wir Object.extend() mit einem Objekt verwenden, sehen wir, dass es geerbte Eigenschaften in eigene Eigenschaften kopiert und nicht aufzählbare Eigenschaften ignoriert (es ignoriert auch Symbol-Schlüssel-Eigenschaften). All dies liegt an der Funktionsweise von for-in.
const proto = Object.defineProperties({}, {
enumProtoProp: {
value: 1,
enumerable: true,
},
nonEnumProtoProp: {
value: 2,
enumerable: false,
},
});
const obj = Object.create(proto, {
enumObjProp: {
value: 3,
enumerable: true,
},
nonEnumObjProp: {
value: 4,
enumerable: false,
},
});
assert.deepEqual(
extend({}, obj),
{enumObjProp: 3, enumProtoProp: 1});$.extend()$.extend(target, source1, source2, ···) von jQuery funktioniert ähnlich wie Object.extend()
source1 in eigene Eigenschaften von target.source2.Die Basis der Kopie auf der Aufzählbarkeit hat mehrere Nachteile
Während die Aufzählbarkeit nützlich ist, um geerbte Eigenschaften zu verstecken, wird sie hauptsächlich auf diese Weise verwendet, da wir normalerweise nur eigene Eigenschaften in eigene Eigenschaften kopieren möchten. Der gleiche Effekt kann besser erzielt werden, indem geerbte Eigenschaften ignoriert werden.
Welche Eigenschaften kopiert werden sollen, hängt oft von der Aufgabe ab; es macht selten Sinn, ein einzelnes Flag für alle Anwendungsfälle zu haben. Eine bessere Wahl ist es, eine Kopiervariante mit einem Prädikat (einer Callback-Funktion, die einen booleschen Wert zurückgibt) bereitzustellen, das angibt, wann Eigenschaften ignoriert werden sollen.
Aufzählbarkeit versteckt praktisch die eigene Eigenschaft .length von Arrays beim Kopieren. Aber das ist ein unglaublich seltener Ausnahmefall: eine magische Eigenschaft, die sowohl Geschwister-Eigenschaften beeinflusst als auch von ihnen beeinflusst wird. Wenn wir diese Art von Magie selbst implementieren, werden wir (geerbte) Getter und/oder Setter verwenden, keine (eigenen) Dateneigenschaften.
Object.assign() [ES5]In ES6 kann Object.assign(target, source_1, source_2, ···) verwendet werden, um die Quellen in das Ziel zu verschmelzen. Alle eigenen aufzählbaren Eigenschaften der Quellen werden berücksichtigt (mit Zeichenketten-Schlüsseln oder Symbol-Schlüsseln). Object.assign() verwendet eine „get“-Operation, um einen Wert aus einer Quelle zu lesen, und eine „set“-Operation, um einen Wert in das Ziel zu schreiben.
In Bezug auf die Aufzählbarkeit setzt Object.assign() die Tradition von Object.extend() und $.extend() fort. Zitat von Yehuda Katz
Object.assignwürde den gleichen Weg wie alle bereits im Umlauf befindlichenextend()APIs ebnen. Wir dachten, dass der Präzedenzfall des Nicht-Kopierens von aufzählbaren Methoden in diesen Fällen Grund genug dafür sei, dassObject.assigndieses Verhalten aufweist.
Mit anderen Worten: Object.assign() wurde mit einem Upgrade-Pfad von $.extend() (und ähnlichen) im Sinn erstellt. Sein Ansatz ist sauberer als der von $.extend, da es geerbte Eigenschaften ignoriert.
Fälle, in denen Nicht-Aufzählbarkeit hilft, sind selten. Ein seltenes Beispiel ist ein aktuelles Problem, das die Bibliothek fs-extra hatte
Das integrierte Node.js-Modul fs hat eine Eigenschaft .promises, die ein Objekt mit einer Promise-basierten Version der fs API enthält. Zum Zeitpunkt des Problems führte das Lesen von .promise zu folgender Warnung, die in der Konsole protokolliert wurde
ExperimentalWarning: The fs.promises API is experimentalZusätzlich zur Bereitstellung eigener Funktionalität exportiert fs-extra auch alles, was in fs enthalten ist. Für CommonJS-Module bedeutet dies, dass alle Eigenschaften von fs in module.exports von fs-extra kopiert werden (mittels Object.assign()). Und wenn fs-extra dies tat, löste es die Warnung aus. Das war verwirrend, da es jedes Mal auftrat, wenn fs-extra geladen wurde.
Eine schnelle Korrektur bestand darin, die Eigenschaft fs.promises nicht aufzählbar zu machen. Danach ignorierte fs-extra sie.
Wenn wir eine Eigenschaft nicht aufzählbar machen, kann sie von Object.keys(), der for-in Schleife usw. nicht mehr gesehen werden. In Bezug auf diese Mechanismen ist die Eigenschaft privat.
Es gibt jedoch mehrere Probleme mit diesem Ansatz
JSON.stringify()JSON.stringify() schließt nicht aufzählbare Eigenschaften von seiner Ausgabe aus. Wir können daher die Aufzählbarkeit verwenden, um zu bestimmen, welche eigenen Eigenschaften nach JSON exportiert werden sollen. Dieser Anwendungsfall ähnelt dem vorherigen, dem Markieren von Eigenschaften als privat. Aber er ist auch anders, da es hier mehr um den Export geht und leicht unterschiedliche Überlegungen gelten. Zum Beispiel: Kann ein Objekt vollständig aus JSON rekonstruiert werden?
Als Alternative zur Aufzählbarkeit kann ein Objekt die Methode .toJSON() implementieren, und JSON.stringify() serialisiert, was immer diese Methode zurückgibt, anstatt das Objekt selbst. Das nächste Beispiel zeigt, wie das funktioniert.
class Point {
static fromJSON(json) {
return new Point(json[0], json[1]);
}
constructor(x, y) {
this.x = x;
this.y = y;
}
toJSON() {
return [this.x, this.y];
}
}
assert.equal(
JSON.stringify(new Point(8, -3)),
'[8,-3]'
);Ich finde toJSON() sauberer als Aufzählbarkeit. Es gibt uns auch mehr Freiheit bezüglich des Aussehens des Speicherformats.
Wir haben gesehen, dass fast alle Anwendungen für Nicht-Aufzählbarkeit Workarounds sind, für die es inzwischen andere und bessere Lösungen gibt.
Für unseren eigenen Code können wir normalerweise so tun, als ob Aufzählbarkeit nicht existiert
Das heißt, wir folgen automatisch bewährten Praktiken.