Nie ruszymy dalej z materiałem dekoratorów TS, jeżeli nie zrozumiemy dobrze, jak działają object properties w JavaScript. Lekcja krótka, ciekawa, a zarazem konieczna, aby dalej rozwijać swoje umiejętności TSa.

Ok, najpierw czym jest prevent extensions:

const object1 = {};

Object.preventExtensions(object1);

Taki kod sprawia, że nie można dodawać nowych properties:

const object1 = {};

Object.preventExtensions(object1);

try {
  Object.defineProperty(object1, 'property1', {
    value: 42,
  });
} catch (e) {
  console.log(e);
  // Expected output: TypeError: Cannot define property property1, object is not extensible
}

Dodam, że w przypadku metod value to funkcja, natomiast warto też pamiętać, że JS jest dziwny i się nie zrażać, bo takie coś „niby” nam przejdzie:

object1.newprop = 42;
console.log(object1.newprop);

To jest błędu nie będzie (w strict mode chyba będzie, ale TS to jeden wielki strict mode, moim zdaniem pomysł na strict mode był fajny, ale za mało on daje) ale koniec końców tam będzie undefined.

W JS do tego niedbalstwa, braku konsekwencji i tak dalej trzeba się po prostu przyzwyczaić, albo na TSa przełączyć.

Mamy też dość ciekawą metodę do sprawdzania czy coś jest extensible:

const object1 = {};

console.log(Object.isExtensible(object1));
// Expected output: true

Object.preventExtensions(object1);

console.log(Object.isExtensible(object1));
// Expected output: false

No ona akurat działa bez zarzutu, ale w JS musimy się przyzwyczaić do różnych dziwnych rzeczy. Bo na przykład jest metoda object.prototype.propertyIsEnumerable:

const object1 = {};
const array1 = [];
object1.property1 = 42;
array1[0] = 42;

console.log(object1.propertyIsEnumerable('property1'));
// Expected output: true

console.log(array1.propertyIsEnumerable(0));
// Expected output: true

console.log(array1.propertyIsEnumerable('length'));
// Expected output: false

I tego nie idzie na metodach używać. Metoda może być enumerable, ale nie idzie tego na niej użyć. Jedyny sposób aby sprawdzić, czy coś jest enumerable to zrobić for…in:

let stupidObj = new StupidClass("jim", 30);

for (const property in stupidObj) {
  console.log(`${property}: ${stupidObj[property]}`);
}

Ok, fajnie. Teraz jeszcze wyjaśnimy seal i freeze a potem defineProperty. Najpierw seal:

const object1 = {
  property1: 42,
};

Object.seal(object1);
object1.property1 = 33;
console.log(object1.property1);
// Expected output: 33

delete object1.property1; // Cannot delete when sealed
console.log(object1.property1);
// Expected output: 33

Seal oznacza, że:

  • Nie można dodawać nowych properties
  • Nie można usuwać istniejących properties
  • Nie można konfigurować istniejących properties (robić im define property na już istniejących polach)

Ok, definiowanie zaraz sobie wyjaśnimy. Na razie pamiętajmy, że istnieje też Object.isSealed i to sprawdza, czy obiekt jest sealed czy nie czyli czy spełnia wymagania powyżej.

Teraz freeze:

const obj = {
  prop: 42,
};

Object.freeze(obj);

obj.prop = 33;
// Throws an error in strict mode

console.log(obj.prop);
// Expected output: 42

Oto cała kwintesencja freeze, czyli oprócz tego, co robi już seal (nie wolno dodawać nowych properties, nie wolno usuwać/konfigurować istniejących properties) dochodzi jeszcze zasada, że po prostu nie można zmieniać wartości (value w property descriptorze) tych properties.

Taki obiekt jest już zamrożony, nic z nim robić nie można, tylko odczytywać. Ok, to teraz defineProperty:

const object1 = {};

Object.defineProperty(object1, 'property1', {
  value: 42,
  writable: false,
});

object1.property1 = 77;
// Throws an error in strict mode

console.log(object1.property1);
// Expected output: 42

Ok, to teraz sobie wbijmy do głowy:

  • value – wartość, w przypadku metod to będzie definicja funkcji
  • writable – czy można nadpisywać wartość czy tylko do odczytu
  • enumerable – czy się wyświetla w for…in
  • configurable – czy będzie można zrobić defineProperty na tej property jeszcze raz (domyślnie false)

Ok, i to nazywa się data descriptor:

Object.defineProperty(obj, 'key', {
  enumerable: false,
  configurable: false,
  writable: true,
  value: 'some value'
});

Bo mamy jeszcze accessor descriptor, czyli mamy get i set, ale nie możemy mieć value ani writable:

Object.defineProperty(obj, 'key', {
  enumerable: false,
  configurable: false,
  get() {
    return this.some_value;
  },
  set(data) {
    this.some_value = 'some value';
  }
});

Ok, mamy jeszcze taką zabawkę do zbierania informacji o properties obiektu:

const object1 = {
  property1: 42,
};

const descriptor1 = Object.getOwnPropertyDescriptor(object1, 'property1');

console.log(descriptor1.configurable);
// Expected output: true

console.log(descriptor1.value);
// Expected output: 42

W czystym JS może to i jest przesada się tego uczyć i nie widać jakichś profitów od razu, ale w TS już niedługo użyjemy sobie tej wiedzy w praktyce razem z dekoratorami.