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

9 Eigenschaftenattribute: Eine Einführung



In diesem Kapitel betrachten wir genauer, wie die ECMAScript-Spezifikation JavaScript-Objekte betrachtet. Insbesondere sind Eigenschaften in der Spezifikation nicht atomar, sondern bestehen aus mehreren Attributen (stellen Sie sich Felder in einem Record vor). Selbst der Wert einer Dateneigenschaft wird in einem Attribut gespeichert!

9.1 Die Struktur von Objekten

In der ECMAScript-Spezifikation besteht ein Objekt aus

9.1.1 Interne Slots

Die Spezifikation beschreibt interne Slots wie folgt. Ich habe Aufzählungspunkte hinzugefügt und einen Teil hervorgehoben

Es gibt zwei Arten von internen Slots

Normale Objekte haben die folgenden Datenslots

9.1.2 Eigenschaftsschlüssel

Der Schlüssel einer Eigenschaft ist entweder

9.1.3 Eigenschaftenattribute

Es gibt zwei Arten von Eigenschaften, die sich durch ihre Attribute unterscheiden

Zusätzlich gibt es Attribute, die beide Arten von Eigenschaften haben. Die folgende Tabelle listet alle Attribute und ihre Standardwerte auf.

Art der Eigenschaft Name und Typ des Attributs Standardwert
Dateneigenschaft value: any undefined
writable: boolean false
Zugriffseigenschaft get: (this: any) => any undefined
set: (this: any, v: any) => void undefined
Alle Eigenschaften configurable: boolean false
enumerable: boolean false

Wir sind bereits auf die Attribute value, get und set gestoßen. Die anderen Attribute funktionieren wie folgt

9.1.3.1 Fallstrick: Vererbte nicht beschreibbare Eigenschaften verhindern die Erstellung eigener Eigenschaften per Zuweisung

Wenn eine vererbte Eigenschaft nicht beschreibbar ist, können wir keine eigene Eigenschaft mit demselben Schlüssel per Zuweisung erstellen

const proto = {
  prop: 1,
};
// Make proto.prop non-writable:
Object.defineProperty(
  proto, 'prop', {writable: false});

const obj = Object.create(proto);

assert.throws(
  () => obj.prop = 2,
  /^TypeError: Cannot assign to read only property 'prop'/);

Weitere Informationen finden Sie unter §11.3.4 „Vererbte schreibgeschützte Eigenschaften verhindern die Erstellung eigener Eigenschaften per Zuweisung“.

9.2 Eigenschaftsdeskriptoren

Ein Eigenschaftsdeskriptor kodiert die Attribute einer Eigenschaft als JavaScript-Objekt. Ihre TypeScript-Interfaces sehen wie folgt aus.

interface DataPropertyDescriptor {
  value?: any;
  writable?: boolean;
  configurable?: boolean;
  enumerable?: boolean;
}
interface AccessorPropertyDescriptor {
  get?: (this: any) => any;
  set?: (this: any, v: any) => void;
  configurable?: boolean;
  enumerable?: boolean;
}
type PropertyDescriptor = DataPropertyDescriptor | AccessorPropertyDescriptor;

Die Fragezeichen zeigen an, dass alle Eigenschaften optional sind. §9.7 „Weglassen von Deskriptoreigenschaften“ beschreibt, was passiert, wenn sie weggelassen werden.

9.3 Deskriptoren für Eigenschaften abrufen

9.3.1 Object.getOwnPropertyDescriptor(): Deskriptor für eine einzelne Eigenschaft abrufen

Betrachten Sie das folgende Objekt

const legoBrick = {
  kind: 'Plate 1x3',
  color: 'yellow',
  get description() {
    return `${this.kind} (${this.color})`;
  },
};

Holen wir uns zuerst einen Deskriptor für die Dateneigenschaft .color

assert.deepEqual(
  Object.getOwnPropertyDescriptor(legoBrick, 'color'),
  {
    value: 'yellow',
    writable: true,
    enumerable: true,
    configurable: true,
  });

So sieht der Deskriptor für die Zugriffseigenschaft .description aus

const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
  Object.getOwnPropertyDescriptor(legoBrick, 'description'),
  {
    get: desc(legoBrick, 'description').get, // (A)
    set: undefined,
    enumerable: true,
    configurable: true
  });

Die Verwendung der Hilfsfunktion desc() in Zeile A stellt sicher, dass .deepEqual() funktioniert.

9.3.2 Object.getOwnPropertyDescriptors(): Deskriptoren für alle Eigenschaften eines Objekts abrufen

const legoBrick = {
  kind: 'Plate 1x3',
  color: 'yellow',
  get description() {
    return `${this.kind} (${this.color})`;
  },
};

const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
  Object.getOwnPropertyDescriptors(legoBrick),
  {
    kind: {
      value: 'Plate 1x3',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    color: {
      value: 'yellow',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    description: {
      get: desc(legoBrick, 'description').get, // (A)
      set: undefined,
      enumerable: true,
      configurable: true,
    },
  });

Die Verwendung der Hilfsfunktion desc() in Zeile A stellt sicher, dass .deepEqual() funktioniert.

9.4 Eigenschaften über Deskriptoren definieren

Wenn wir eine Eigenschaft mit dem Schlüssel k über einen Eigenschaftsdeskriptor propDesc definieren, hängt das Geschehen

9.4.1 Object.defineProperty(): Einzelne Eigenschaften über Deskriptoren definieren

Zuerst erstellen wir eine neue Eigenschaft über einen Deskriptor

const car = {};

Object.defineProperty(car, 'color', {
  value: 'blue',
  writable: true,
  enumerable: true,
  configurable: true,
});

assert.deepEqual(
  car,
  {
    color: 'blue',
  });

Als Nächstes ändern wir die Art einer Eigenschaft über einen Deskriptor; wir wandeln eine Dateneigenschaft in einen Getter um

const car = {
  color: 'blue',
};

let readCount = 0;
Object.defineProperty(car, 'color', {
  get() {
    readCount++;
    return 'red';
  },
});

assert.equal(car.color, 'red');
assert.equal(readCount, 1);

Schließlich ändern wir den Wert einer Dateneigenschaft über einen Deskriptor

const car = {
  color: 'blue',
};

// Use the same attributes as assignment:
Object.defineProperty(
  car, 'color', {
    value: 'green',
    writable: true,
    enumerable: true,
    configurable: true,
  });

assert.deepEqual(
  car,
  {
    color: 'green',
  });

Wir haben dieselben Eigenschaftenattribute wie bei der Zuweisung verwendet.

9.4.2 Object.defineProperties(): Mehrere Eigenschaften über Deskriptoren definieren

Object.defineProperties() ist die Multi-Property-Version von `Object.defineProperty()

const legoBrick1 = {};
Object.defineProperties(
  legoBrick1,
  {
    kind: {
      value: 'Plate 1x3',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    color: {
      value: 'yellow',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    description: {
      get: function () {
        return `${this.kind} (${this.color})`;
      },
      enumerable: true,
      configurable: true,
    },
  });

assert.deepEqual(
  legoBrick1,
  {
    kind: 'Plate 1x3',
    color: 'yellow',
    get description() {
      return `${this.kind} (${this.color})`;
    },
  });

9.5 Object.create(): Objekte über Deskriptoren erstellen

Object.create() erstellt ein neues Objekt. Sein erstes Argument gibt den Prototyp dieses Objekts an. Sein optionales zweites Argument gibt Deskriptoren für die Eigenschaften dieses Objekts an. Im nächsten Beispiel erstellen wir dasselbe Objekt wie im vorherigen Beispiel.

const legoBrick2 = Object.create(
  Object.prototype,
  {
    kind: {
      value: 'Plate 1x3',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    color: {
      value: 'yellow',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    description: {
      get: function () {
        return `${this.kind} (${this.color})`;
      },
      enumerable: true,
      configurable: true,
    },
  });

// Did we really create the same object?
assert.deepEqual(legoBrick1, legoBrick2); // Yes!

9.6 Anwendungsfälle für Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptors() hilft uns bei zwei Anwendungsfällen, wenn wir es mit Object.defineProperties() oder Object.create() kombinieren.

9.6.1 Anwendungsfall: Eigenschaften in ein Objekt kopieren

Seit ES6 verfügt JavaScript bereits über eine Tool-Methode zum Kopieren von Eigenschaften: Object.assign(). Diese Methode verwendet jedoch einfache Lese- und Schreibvorgänge, um eine Eigenschaft mit dem Schlüssel key zu kopieren

target[key] = source[key];

Das bedeutet, dass sie nur eine exakte Kopie einer Eigenschaft erstellt, wenn

Das folgende Beispiel verdeutlicht diese Einschränkung. Objekt source hat einen Setter mit dem Schlüssel data.

const source = {
  set data(value) {
    this._data = value;
  }
};

// Property `data` exists because there is only a setter
// but has the value `undefined`.
assert.equal('data' in source, true);
assert.equal(source.data, undefined);

Wenn wir Object.assign() verwenden, um die Eigenschaft data zu kopieren, wird die Zugriffseigenschaft data in eine Dateneigenschaft umgewandelt

const target1 = {};
Object.assign(target1, source);

assert.deepEqual(
  Object.getOwnPropertyDescriptor(target1, 'data'),
  {
    value: undefined,
    writable: true,
    enumerable: true,
    configurable: true,
  });

// For comparison, the original:
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
  Object.getOwnPropertyDescriptor(source, 'data'),
  {
    get: undefined,
    set: desc(source, 'data').set,
    enumerable: true,
    configurable: true,
  });

Glücklicherweise kopiert die Verwendung von Object.getOwnPropertyDescriptors() zusammen mit Object.defineProperties() die Eigenschaft data exakt

const target2 = {};
Object.defineProperties(
  target2, Object.getOwnPropertyDescriptors(source));

assert.deepEqual(
  Object.getOwnPropertyDescriptor(target2, 'data'),
  {
    get: undefined,
    set: desc(source, 'data').set,
    enumerable: true,
    configurable: true,
  });
9.6.1.1 Fallstrick: Kopieren von Methoden, die super verwenden

Eine Methode, die super verwendet, ist fest mit ihrem Home-Objekt (dem Objekt, in dem sie gespeichert ist) verbunden. Derzeit gibt es keine Möglichkeit, eine solche Methode in ein anderes Objekt zu kopieren oder zu verschieben.

9.6.2 Anwendungsfall für Object.getOwnPropertyDescriptors(): Objekte klonen

Shallow Cloning ähnelt dem Kopieren von Eigenschaften, weshalb Object.getOwnPropertyDescriptors() auch hier eine gute Wahl ist.

Um den Klon zu erstellen, verwenden wir Object.create()

const original = {
  set data(value) {
    this._data = value;
  }
};

const clone = Object.create(
  Object.getPrototypeOf(original),
  Object.getOwnPropertyDescriptors(original));

assert.deepEqual(original, clone);

Weitere Informationen zu diesem Thema finden Sie unter §6 „Kopieren von Objekten und Arrays“.

9.7 Weglassen von Deskriptoreigenschaften

Alle Eigenschaften von Deskriptoren sind optional. Was passiert, wenn Sie eine Eigenschaft weglassen, hängt von der Operation ab.

9.7.1 Weglassen von Deskriptoreigenschaften beim Erstellen von Eigenschaften

Wenn wir eine neue Eigenschaft über einen Deskriptor erstellen, bedeutet das Weglassen von Attributen, dass ihre Standardwerte verwendet werden

const car = {};
Object.defineProperty(
  car, 'color', {
    value: 'red',
  });
assert.deepEqual(
  Object.getOwnPropertyDescriptor(car, 'color'),
  {
    value: 'red',
    writable: false,
    enumerable: false,
    configurable: false,
  });

9.7.2 Weglassen von Deskriptoreigenschaften beim Ändern von Eigenschaften

Wenn wir stattdessen eine bestehende Eigenschaft ändern, bedeutet das Weglassen von Deskriptoreigenschaften, dass die entsprechenden Attribute nicht berührt werden

const car = {
  color: 'yellow',
};
assert.deepEqual(
  Object.getOwnPropertyDescriptor(car, 'color'),
  {
    value: 'yellow',
    writable: true,
    enumerable: true,
    configurable: true,
  });
Object.defineProperty(
  car, 'color', {
    value: 'pink',
  });
assert.deepEqual(
  Object.getOwnPropertyDescriptor(car, 'color'),
  {
    value: 'pink',
    writable: true,
    enumerable: true,
    configurable: true,
  });

9.8 Welche Eigenschaftenattribute verwenden eingebaute Konstrukte?

Die allgemeine Regel (mit wenigen Ausnahmen) für Eigenschaftenattribute ist

9.8.1 Eigene Eigenschaften, die per Zuweisung erstellt wurden

const obj = {};
obj.prop = 3;

assert.deepEqual(
  Object.getOwnPropertyDescriptors(obj),
  {
    prop: {
      value: 3,
      writable: true,
      enumerable: true,
      configurable: true,
    }
  });

9.8.2 Eigene Eigenschaften, die über Objektliterale erstellt wurden

const obj = { prop: 'yes' };

assert.deepEqual(
  Object.getOwnPropertyDescriptors(obj),
  {
    prop: {
      value: 'yes',
      writable: true,
      enumerable: true,
      configurable: true
    }
  });

9.8.3 Die eigene Eigenschaft .length von Arrays

Die eigene Eigenschaft .length von Arrays ist nicht aufzählbar, damit sie nicht von Object.assign(), Spread-Syntax und ähnlichen Operationen kopiert wird. Sie ist auch nicht konfigurierbar

> Object.getOwnPropertyDescriptor([], 'length')
{ value: 0, writable: true, enumerable: false, configurable: false }
> Object.getOwnPropertyDescriptor('abc', 'length')
{ value: 3, writable: false, enumerable: false, configurable: false }

.length ist eine spezielle Dateneigenschaft, insofern als sie von anderen eigenen Eigenschaften (insbesondere Index-Eigenschaften) beeinflusst wird (und diese beeinflusst).

9.8.4 Prototyp-Eigenschaften von eingebauten Klassen

assert.deepEqual(
  Object.getOwnPropertyDescriptor(Array.prototype, 'map'),
  {
    value: Array.prototype.map,
    writable: true,
    enumerable: false,
    configurable: true
  });

9.8.5 Prototyp-Eigenschaften und Instanz-Eigenschaften von benutzerdefinierten Klassen

class DataContainer {
  accessCount = 0;
  constructor(data) {
    this.data = data;
  }
  getData() {
    this.accessCount++;
    return this.data;
  }
}
assert.deepEqual(
  Object.getOwnPropertyDescriptors(DataContainer.prototype),
  {
    constructor: {
      value: DataContainer,
      writable: true,
      enumerable: false,
      configurable: true,
    },
    getData: {
      value: DataContainer.prototype.getData,
      writable: true,
      enumerable: false,
      configurable: true,
    }
  });

Beachten Sie, dass alle eigenen Eigenschaften von Instanzen von DataContainer beschreibbar, aufzählbar und konfigurierbar sind

const dc = new DataContainer('abc')
assert.deepEqual(
  Object.getOwnPropertyDescriptors(dc),
  {
    accessCount: {
      value: 0,
      writable: true,
      enumerable: true,
      configurable: true,
    },
    data: {
      value: 'abc',
      writable: true,
      enumerable: true,
      configurable: true,
    }
  });

9.9 API: Eigenschaftsdeskriptoren

Die folgenden Tool-Methoden verwenden Eigenschaftsdeskriptoren

9.10 Weiterführende Lektüre

Die nächsten drei Kapitel geben weitere Details zu Eigenschaftenattributen