9. února 2025

Gettery a settery vlastností

Vlastnosti objektů se dělí do dvou druhů.

První druh jsou datové vlastnosti. S nimi už pracovat umíme. Všechny vlastnosti, které jsme až doposud používali, byly datové vlastnosti.

Druhý druh vlastností je něco nového. Jsou to přístupové vlastnosti. Jsou to v zásadě funkce, které se spustí při načítání a nastavování hodnoty, ale v externím kódu vypadají jako běžné vlastnosti.

Gettery a settery

Přístupové vlastnosti jsou představovány metodami nazývanými „getter“ a „setter“. V objektovém literálu jsou označeny get a set:

let obj = {
  get názevVlastnosti() {
    // getter, kód spuštěný při načítání obj.názevVlastnosti
  },

  set názevVlastnosti(hodnota) {
    // setter, kód spuštěný při nastavování obj.názevVlastnosti = hodnota
  }
};

Getter se spustí, když je vlastnost obj.názevVlastnosti načítána, setter se spustí, když je měněna.

Například máme objekt uživatel s vlastnostmi jméno a příjmení:

let uživatel = {
  jméno: "Jan",
  příjmení: "Novák"
};

Nyní chceme přidat vlastnost celéJméno, která by měla být "Jan Novák". Samozřejmě nechceme kopírovat a vkládat existující informaci, a tak ji můžeme implementovat jako přístupovou:

let uživatel = {
  jméno: "Jan",
  příjmení: "Novák",

  get celéJméno() {
    return `${this.jméno} ${this.příjmení}`;
  }
};

alert(uživatel.celéJméno); // Jan Novák

Zvenčí se přístupová vlastnost jeví jako obyčejná. To je myšlenka přístupových vlastností. Nevoláme uživatel.celéJméno jako funkci, ale normálně ji přečteme: getter se spustí za scénou.

Dosud má celéJméno jen getter. Jestliže se pokusíme přiřadit uživatel.celéJméno=, nastane chyba:

let uživatel = {
  get celéJméno() {
    return `...`;
  }
};

uživatel.celéJméno = "Test"; // Chyba (vlastnost má jen getter)

Opravme ji přidáním setteru pro uživatel.celéJméno:

let uživatel = {
  jméno: "Jan",
  příjmení: "Novák",

  get celéJméno() {
    return `${this.jméno} ${this.příjmení}`;
  },

  set celéJméno(hodnota) {
    [this.jméno, this.příjmení] = hodnota.split(" ");
  }
};

// nastavení vlastnosti celéJméno se spustí se zadanou hodnotou.
uživatel.celéJméno = "Alice Cooper";

alert(uživatel.jméno); // Alice
alert(uživatel.příjmení); // Cooper

Výsledkem je, že máme „virtuální“ vlastnost celéJméno. Lze ji číst i do ní zapisovat.

Deskriptory přístupových vlastností

Deskriptory přístupových vlastností se liší od deskriptorů datových vlastností.

U přístupových vlastností v nich nejsou value nebo writable, ale místo nich tam jsou funkce get a set.

To znamená, že přístupový deskriptor může mít:

  • get – funkce bez argumentů, která se spustí při čtení vlastnosti,
  • set – funkce s jedním argumentem, která je volána, když se vlastnost nastavuje,
  • enumerable – totéž jako u datových vlastností,
  • configurable – totéž jako u datových vlastností.

Například chceme-li vytvořit přístupovou vlastnost celéJméno pomocí metody defineProperty, můžeme předat deskriptor s get a set:

let uživatel = {
  jméno: "Jan",
  příjmení: "Novák"
};

Object.defineProperty(uživatel, 'celéJméno', {
  get() {
    return `${this.jméno} ${this.příjmení}`;
  },

  set(hodnota) {
    [this.jméno, this.příjmení] = hodnota.split(" ");
  }
});

alert(uživatel.celéJméno); // Jan Novák

for(let klíč in uživatel) alert(klíč); // jméno, příjmení

Prosíme všimněte si, že vlastnost může být buď přístupová (má metody get/set), nebo datová (má value), ale ne obojí.

Pokud se pokusíme ve stejném deskriptoru poskytnout get i value, nastane chyba:

// Chyba: Neplatný deskriptor vlastnosti.
Object.defineProperty({}, 'vlastnost', {
  get() {
    return 1
  },

  value: 2
});

Chytřejší gettery a settery

Gettery a settery můžeme používat jako obaly nad „skutečnými“ hodnotami vlastností, abychom nad operacemi s nimi získali větší kontrolu.

Například jestliže chceme v objektu uživatel zakázat příliš krátká jména, můžeme mít setter jméno a udržovat hodnotu v oddělené vlastnosti _jméno:

let uživatel = {
  get jméno() {
    return this._jméno;
  },

  set jméno(hodnota) {
    if (hodnota.length < 4) {
      alert("Jméno je příliš krátké, musí mít alespoň 4 znaky");
      return;
    }
    this._jméno = hodnota;
  }
};

uživatel.jméno = "Petr";
alert(uživatel.jméno); // Petr

uživatel.jméno = ""; // Jméno je příliš krátké...

Jméno je tedy uloženo ve vlastnosti _jméno a přístup k němu se děje přes getter a setter.

Technicky může externí kód přistupovat přímo ke jménu pomocí uživatel._jméno. Podle široce rozšířené konvence však vlastnosti začínající podtržítkem "_" jsou interní a nemělo by se na ně sahat zvnějšku objektu.

Používání pro kompatibilitu

Jedním z vynikajících využití přístupových vlastností je, že umožňují kdykoli převzít kontrolu nad „obyčejnou“ datovou vlastností tím, že ji nahradíme getterem a setterem a vylepšíme její chování.

Představme si, že jsme začali implementovat objekty uživatelů s použitím datových vlastností jméno a věk:

function Uživatel(jméno, věk) {
  this.jméno = jméno;
  this.věk = věk;
}

let jan = new Uživatel("Jan", 25);

alert( jan.věk ); // 25

…Dříve nebo později se to však může změnit. Místo vlastnosti věk se můžeme rozhodnout ukládat datumNarození, protože je to přesnější a vhodnější:

function Uživatel(jméno, datumNarození) {
  this.jméno = jméno;
  this.datumNarození = datumNarození;
}

let jan = new Uživatel("Jan", new Date(1992, 6, 1));

Co teď uděláme se starým kódem, který stále používá vlastnost věk?

Můžeme se pokusit všechna taková místa najít a opravit, ale to nám zabere čas a může to být těžko proveditelné, jestliže tento kód používá mnoho dalších lidí. A kromě toho je hezké mít věk v objektu uživatel, ne?

Necháme si ho.

Problém vyřeší přidání getteru pro věk:

function Uživatel(jméno, datumNarození) {
  this.jméno = jméno;
  this.datumNarození = datumNarození;

  // věk se vypočítá z dnešního data a z data narození
  Object.defineProperty(this, "věk", {
    get() {
      let dnešníRok = new Date().getFullYear();
      return dnešníRok - this.datumNarození.getFullYear();
    }
  });
}

let jan = new Uživatel("Jan", new Date(1992, 6, 1));

alert( jan.datumNarození ); // datumNarození je k dispozici
alert( jan.věk );      // ...stejně jako věk

Nyní bude fungovat i starý kód a navíc jsme získali další pěknou vlastnost.

Mapa tutoriálu

Komentáře

přečtěte si před komentováním…
  • Máte-li návrhy na zlepšení, vytvořte prosím issue na GitHubu nebo pull request místo komentáře.
  • Pokud v článku něčemu nerozumíte, napište prosím, čemu přesně a na kterém místě.
  • Pro vložení několika slov kódu použijte značku <code>, pro několik řádků je obalte značkou <pre>, pro více než 10 řádků vložte odkaz na pískoviště (plnkr, jsbin, codepen…)