Deep JavaScript
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

12 Aufzählbarkeit von Eigenschaften



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“.

12.1 Wie die Aufzählbarkeit Eigenschafts-iterierende Konstrukte beeinflusst

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,
  },
});

12.1.1 Operationen, die nur aufzählbare Eigenschaften berücksichtigen

Tabelle 2: Operationen, die nicht aufzählbare Eigenschaften ignorieren.
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

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.

12.1.2 Operationen, die sowohl aufzählbare als auch nicht aufzählbare Eigenschaften berücksichtigen

Tabelle 3: Operationen, die sowohl aufzählbare als auch nicht aufzählbare Eigenschaften berücksichtigen.
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

12.1.3 Namenskonventionen für introspektive Operationen

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

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().

12.2 Die Aufzählbarkeit von vordefinierten und erstellten Eigenschaften

In diesem Abschnitt kürzen wir Object.getOwnPropertyDescriptor() wie folgt ab

const desc = Object.getOwnPropertyDescriptor.bind(Object);

Die meisten Dateneigenschaften werden mit folgenden Attributen erstellt

{
  writable: true,
  enumerable: false,
  configurable: true,
}

Dies schließt ein

Die wichtigsten nicht aufzählbaren Eigenschaften 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.

12.3 Anwendungsfälle für die Aufzählbarkeit

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.

12.3.1 Anwendungsfall: Verstecken von Eigenschaften vor der for-in Schleife

Die 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;
}
12.3.1.1 Die Vorbehalte bei der Verwendung von for-in für Objekte

for-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

const obj = {};
assert.deepEqual(
  listPropertiesViaForIn(obj),
  []);

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.

12.3.1.2 Die Vorbehalte bei der Verwendung von for-in für Arrays

Die eigene Eigenschaft .length ist bei Arrays und Zeichenketten nicht aufzählbar und wird daher von for-in ignoriert

> listPropertiesViaForIn(['a', 'b'])
[ '0', '1' ]
> listPropertiesViaForIn('ab')
[ '0', '1' ]

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

12.3.2 Anwendungsfall: Markieren von Eigenschaften als nicht zu kopierend

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.

12.3.2.1 Historischer Kopiervorgang: Prototype’s 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});
12.3.2.2 Historischer Kopiervorgang: jQuery’s $.extend()

$.extend(target, source1, source2, ···) von jQuery funktioniert ähnlich wie Object.extend()

12.3.2.3 Die Nachteile der aufzählbarkeitsgesteuerten Kopie

Die Basis der Kopie auf der Aufzählbarkeit hat mehrere Nachteile

12.3.2.4 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.assign würde den gleichen Weg wie alle bereits im Umlauf befindlichen extend() APIs ebnen. Wir dachten, dass der Präzedenzfall des Nicht-Kopierens von aufzählbaren Methoden in diesen Fällen Grund genug dafür sei, dass Object.assign dieses 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.

12.3.2.5 Ein seltenes Beispiel dafür, dass Nicht-Aufzählbarkeit beim Kopieren nützlich ist

Fälle, in denen Nicht-Aufzählbarkeit hilft, sind selten. Ein seltenes Beispiel ist ein aktuelles Problem, das die Bibliothek fs-extra hatte

12.3.3 Markieren von Eigenschaften als privat

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

12.3.4 Verstecken eigener Eigenschaften vor 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.

12.4 Fazit

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.