JavaScript Restart – Neidentifikovatelný létající objekt

Objektově orientované programování jest naším denním chlebem, živitelem i učitelem, leč možnosti JavaScriptu v tomto směru bývaly ubohé a ne každý vývojář se zamiloval do kouzel prototypové dědičnosti a přístupu dle hesla “Urob si sám”. Vše se však k lepšímu obrací, a to budiž tématem tohoto článku.

Seriál: JavaScript Restart (4 díly)

  1. JavaScript Restart – QuerySelector 11.2.2015
  2. JavaScript Restart – Hurá na pole 23.2.2015
  3. JavaScript Restart – Neidentifikovatelný létající objekt 9.3.2015
  4. JavaScript Restart – Restartováno 27.3.2015

Object.defineProperty()

Pro nastavování členských proměnných Javascriptového objektu (prostě vlastností neboli “proprt”) jsem se setkal s těmito přístupy:

  • přímý přístup k proměnné
    no ale fuj, hanba, to si říkáme programátoři? No, ale funguje to docela dobře.
  • Java přístup
    používáme metody getProperty a setProperty – což funguje bez ohledu na jazyk, vlastně není žádná zvláštní syntaxe, pouze jmenná konvence pro názvy metod
  • jQuery přístup
    pokud má metoda parametr, pak se chová jako setter jinak se chová jako getter(případně s jedním parametrem jako getter, se dvěma jako setter a tak dále)

    $(el).css(‘color’); //getter
    $(el).css(‘color’, ‘red’); //setter

Nemůžeme popřít, že práce s vlastnostmi objektu je trochu problém, který se řeší, a řeší se různými způsoby, a tak je na místě standardizace, kterou za nás udělá někdo jiný, a nám tak (doufejme) zčitelní kód.

Pojďme se tedy podívat na definici vlastnosti objektu v Javascriptu (ES 5.1):

Object.defineProperty(MyObject, ‘answerToTheUltimateQuestion’, {
  value: 42
});

To nám může posloužit maximálně k definici konstanty. Pokud se totiž zkusíte do konstanty zapsat:

MyObject.answerToTheUltimateQuestion = 10;

nedostanete žádné varování ani výjimku (nejsme-li ve strict módu), pouze její hodnota bude pořád 42.

Pokud použijeme parametr writable:

Object.defineProperty(MyObject, ‘answerToTheUltimateQuestion’, {
  value: 42,
  writable: true
});

pak se nám hodnota úspěšně uloží.

Zůstává pro mne poněkud záhadou, proč přednastavená hodnota pro writable je false, jelikož ve většině případů je třeba ji změnit.

Aby byla vlastnost viditelná při čtení vlastností objektu např. přes konstrukci for..in nebo přes Object.keys(), musíme nastavit ještě parametr enumerable (otestovat jej můžeme pomocí propertyIsEnumerable):

Object.defineProperty(MyObject, ‘answerToTheUltimateQuestion’, {
  value: 42,
  writable: true,
  enumerable: true
});

Záhada s přednastavenou hodnotou na false pokračuje. Nějak se mi nepodařilo přijít na případ, kdy bych potřeboval proměnnou, která je viditelná, jen pokud ji čtu přímo. Možná něco na způsob privátní proměnné.

Poslední vám (mnou) utajenou možností je parametr configurable:

Object.defineProperty(MyObject, ‘AnswerToTheUltimateQuestion’, {
  value: 42,
  writable: true,
  enumerable: true,
  configurable: true,
});

Configurable zařídí pouze takovou drobnost, povolí možnost vlastnost předefinovat nastavení vlastnosti nebo ji smazat pomocí příkazu delete.

Málem bych zapomněl na get a set, kvůli kterým tohle povídání vlastně vzniklo:

var answer,
  MyObject = {};

Object.defineProperty(MyObject, ‘answerToTheUltimateQuestion’, {
  get: function() {
    return answer;
  },
  set: function(newAnswer) {
    answer = newAnswer;
  },
  enumerable: true,
  configurable: true
});

Zcela logicky se blokuje writable nastavením set, zároveň není možno nastavit parametr writable, pokud existuje getter nebo setter.

Pro úplnost – výše zmíněné parametry se nazývají deskriptory a dělíme je na datové (set,get) a přístupové (configurable, writable, enumerable).

Object.defineProperties()

Je nutné si přiznat, že zápis pomocí defineProperty je celkem “ukecaný”, a pokud zadefinujete jen pár vlastností, zabere to strašně moc řádek. A proto vám jistě udělá radost možnost si usnadnit práci a definovat všechno vlastnosti pomocí jednoho objektu:

Object.defineProperties(Scalear.Neck.prototype, {
    namesVisible: {
        value: false,
        writable: true
    },
    rootNote: {
        value: 0,
        writable: true
    },
    scale: {
        value: 0,
        writable: true
    }
});

Object.create()

Každý, kdo chtěl dědit nejen po babičce, ale i po jiných objektech, se asi už potkal s takovouto konstrukcí:

Scalear.Box.prototype = new Mvc.View();

která má tu protivnou vlastnost, že nám zavolá konstruktor předka, a pokud ten bude, nedejbože, očekávat nějaké parametry, tak je zle.
Nyní lze dědičnost řešit takto:

Scalear.Box.prototype = Object.create(Mvc.View.prototype);

což ovšem není žádný zázrak (na počet znaků je to dokonce delší), ale konstruktor se nevolá a pokud přidáme druhý parametr:

Scalear.Box.prototype = Object.create(Mvc.View.prototype, {
  namesVisible: {
    value: false,
    writable: true
  },
  rootNote: {
    value: 0,
    writable: true
  },
  scale: {
    value: 0,
    writable: true
  }
});

který je, jak vidno, opět definice vlastností, stane se Object.create naší docela oblíbenou funkcí.

Vynechám-li vyšší výkonnost, pak už možnost definice vlastností zároveň s definicí dědičnosti je velký přínos pro zpřehlednění kódu. A zápis se zase o trochu víc podobá definici třídy.

Object.keys()

Když už jsem se v povídání o vlastnostech otřel o parametr enumerable, je na místě zmínit další funkci, která s enumerací přímo souvisí. Pokud chceme získat jména vlastností objektu, můžeme zavolat:

Object.keys(myObject)

a výsledkem je pole názvů vlastností, čili pak můžeme udělat toto:

Object.keys(myObject).forEach(function(key) {
    //kód
});

a máme variantu ke konstrukci for..in.

Zajímavé je, pokud jako parametr použijeme řetězec:

Object.keys(‘text’)

vrátí

[“0”, “1”, “2”, “3”]

v Chrome a Firefoxu, zatímco v IE 11 končí vyjímkou TypeError. Je to způsobenu změnou v ES6 (řetězec je iterable) proti chování ES5 (nebo spíše tím jak ten který prohlížeč pokročil v implementaci těchto novot), a tak bych si na řetězce raději dával pozor.

Object.freeze()

Během zkoumání toho, co objekty umí nového, jsem nalezl metodu freeze, nad jejímž použítím jsem si dlouho lámal hlavu. Nakonec jsem ji rád přidal do právě rozepsané aplikace. V souvislosti s touto metodou jsem narazil na anglické slovo “immutable”, což, jak jsem zjistil, není od základu “mute” čili “ztlumit”, alebrž nýbrž od “mutation” čili “změna” a “immutable” znamená tedy “neměnitelný”. A neměnitelnost je to, co právě tato metoda zařídí.

Máme-li objekt s daty, která potřebuje aplikace a která se při běhu aplikace nemají měnit (konfigurace, atd.) je možné jejich neměnnost právě pojistit metodou freeze.

var appInfo = {
  appName: ‘IceCream app’,
  version: ‘1.2’
};
Object.freeze(appInfo);

Pokud se nyní pokusíme v appInfo změnit některou hodnotu (nebo další přidat), příkaz se provede bez varování a výjimek (pokud nejsme ve sctrict módu), ale ke změně obsahu nedojde.

Zmrzlost (neměnnost) objektu se pak dá otestovat pomocí metody isFrozen:

Object.isFrozen(appInfo)

Zajímavé je, že pokud se pokusíte zavolat push nad “zmraženým” polem dostanete vyjímku, oznamující, že není možno měnit readonly vlastnost length. To je dobré vědět a myslet na to. Druhou věcí, na kterou je třeba nezapomenout, je, že k freeze není opačná metoda něco jako unfreeze nebo defrost, čili kdo jednou zmrazil, už nerozmrazí.

Object.seal()

Funkce Object.seal() je méně striktní variantou Object.freeze(). Znemožní přídávání nových vlastností objektu, ale umožní jejich editaci. Zároveň změní deskriptor configurable na false.

Seal znamená “utěsnit” nebo “uzavřít” což docela hezky charakterizuje, co funkce dělá. To, že objekt “utěsněný” se pak dá otestovat funkcí.

Object.isSealed(appInfo)

Object.preventExtensions()

Metoda Object.preventExtensions je méně striktní variantou Object.seal, ale tentokrát pouze znemožní přidání nové vlastnosti, ale nemění nic v destriptorech. otestuje se takto:

Nejsem si jist, kde bych použil Object.seal() a Object.preventExtensions(), ale bez nich by tento článek nebyl úplný. Rozhodně se tu otvírá prostor pro diskuzi.

Object.isExtensible(appInfo)

Jak vidno, tvůrcům a standartizátorům ECMA skriptu došly jazykové prostředky, a tudíž zvolili název, který nijak nenapovídá, že je tu jistá souvislost s předchozími dvěma funkcemi.

Object.freeze vs. Object.seal() vs. Object.preventExtensions()

Akce freeze seal preventExtensions
změna hodnoty zakázáno povoleno povoleno
změna deskriptoru zakázáno zakázáno povoleno
přidání vlastnosti zakázáno zakázáno zakázáno
Object.isSealed() true true false
Object.isFrozen() true false false
Object.isExtensible() false false false

Object.observe()

Zatím všechny všechny funkce, o kterých tu byla řeč, patří do ES5. Object.observe() je součástí specifikace ES6. To znamená, že kromě Chrome, ho ještě žádný prohlížeč neimplementoval. Object.observe() je ovšem dle mého názoru naprosto bezvadná záležitost, kvůli které stojí za to použít polyfill.

Ve Firefoxu (Gecko) existuje ještě podobná funkce Object.watch(), která je ovšem nestandardní a není doporučeno ji využívat. Je třeba dávat pozor na jejich záměnu, často se to splete.

Představte si situaci – na straně klienta máme uložená data v modelu a několik view, která se musí aktualizovat, jakmile se změní model. Jak detekovat změnu modelu? Samozřejmě knihovny jako AngularJS, Ember.js a Ext JS (se kterými mám bližší zkušenost), tohle řeší a vlastně to byl jeden z motivů jejich vzniku. Nebo si to může vývojář napsat sám, jenže kdo chce pořád vymýšlet kolo, že?

Nuže, nyní máme funkci, které předáme náš model (objekt) a callback funkci. Callback funkce se zavolá při každé změně modelu, a dostane seznam změn jako parametr.

Object.observe(model, function(changes){
    changes.forEach(function(change) {
        //kód
    });
});

Co přesně přijde v seznamu změn? Pole takovýchto objektů:

{
  name: “scale”  //jméno proměnné
  object: Object //objekt po změně
  oldValue: 13   //stará hodnota
  type: “update” //type změny
}

Pak už jen zbývá dopsat kód, který bude řešit co se bude dít při změně té které proměnné modelu a máme hotovo.

Chrome Object.observe() již implementoval a pro ostatní prohlížeče lze použít polyfill.

V souvislostí s Object.observe() se začíná mluvit o revoluci v Javascriptu a vývojáři-vizionáři se začínají se slzami v očích loučit se svými oblíbenými frameworky a knihovnami.

Perlička na závěr

Na závěr bych se rád vrátil na začátek. Pokud chceme definovat vlastnost můžeme použít následující syntaxi (která je součástí ES 5.1):

var answer;

get answerToTheUltimateQuestion() {
      return answer;
}

set answerToTheUltimateQuestion(newAnswer) {
  answer = newAnswer;
}

což jistě potěší vývojáře používajících k obživě a radosti i jiné jazyky (zejména C#), jenže jak vidno, je to zpětně nekompatibilní syntaxe, čili ve starších prohlížečích skript prostě nepoběží, a žádný polyfill to nespraví (ani nemůže).

Pracuje jako vývojář webových aplikací ve společnosti TopMonks, s.r.o. a specializuje se na JavaScript, AngularJS a EmberJS, hlavně na vývoj uživatelských rozhraní. Je členem kapely Rezatý Rakety. Když nehraje ani neprogramuje, inhaluje výpary při lepení plastikových modelů.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 9

Přehled komentářů

Marek Sirkovský Uvozovky
Ondřej Žára Čeština
karfcz Object.observe
Pavel Lang Re: Object.observe
Jarda Re: Object.observe
Ondřej Žára Re: Object.observe
Martin Hassman Re: Object.observe
tacoberu Re: Object.observe
karfcz Re: Object.observe
Zdroj: https://www.zdrojak.cz/?p=14401