Dnes konečně pootevřeme Pandořinu skříňku a nahlédneme do tajů objektového programování. Objektové nebo objektové orientované programování (OOP) je takzvané programovací paradigma. Je to určitý specifický pohled na to, jak strukturovat programy tak, aby se dobře spravovaly, i když jejich velikost začne růst do obřích rozměrů. My si zde ukážeme pouze nejnutější základy. Svět OOP je ohromný a opravdu dobře se naučit tímto způsobem přemýšlet vyžaduje hodně studia a také praxe.

Metody

Začněme opravdu zvolna a jednoduše. Vytvoříme si objekt, představující jednoho člena jedné z nejznámějších seriálových rodinek.

const homer = {
  firstName: 'Homer',
  middleName: 'Jay',
  lastName: 'Simpson',
  age: 39,
};

Běžně je zvykem psát prostřední jméno pouze pomocí iniciály, tedy Homer J. Simpson. Napišme si funkci, která dělá přesně takovou věc.

const personName = (person) => {
  return `${person.firstName} ${person.middleName[0]}. ${person.lastName}`;
};

Můžeme pak psát

> personName(homer)
'Homer J. Simpson

Takováto funkce se nám bude často hodit. Vlastně by se nám hodilo, kdyby tato funkce byla svázána a naším objektem jako jeho metoda. Zde konečně odhalíme naše tajemství a použijeme naši funkci jako hodnotu přímo v našem objektu.

const homer = {
  firstName: 'Homer',
  middleName: 'Jay',
  lastName: 'Simpson',
  age: 39,
  name: () => {
    return `${homer.firstName} ${homer.middleName[0]}. ${homer.lastName}`;
  },
};

Všimněte si, že funkce nyní nemá žádné parametry, protože přímo pracuje s objektem homer. Můžeme pak psát

> homer.name()
'Homer J. Simpson

To vypadá daleko elegantněji. Co se týče funkcí jako hodnot v objektech, nečeká nás zatím žádné velké překvapení. Dokonce si možná někteří z vás hnání zvědavostí podobnou věc už zkusili na vlastní pěst. Je tu však jedna drobnost, která je trochu nešikovná. Uvnitř metody se musíme k našemu objektu odkazovat skrze proměnnou homer. Funkce je tak závislé na jedné konkrétní proměnné musíme ji tak pro každý další objekt vyrábět znova.

const homer = {
  firstName: 'Homer',
  middleName: 'Jay',
  lastName: 'Simpson',
  age: 39,
  name: () => {
    return `${homer.firstName} ${homer.middleName[0]}. ${homer.lastName}`;
  },
};

const marge = {
  firstName: 'Marge',
  middleName: 'Jacqueline',
  lastName: 'Simpson',
  age: 36,
  name: () => {
    return `${marge.firstName} ${marge.middleName[0]}. ${marge.lastName}`;
  },
};

Život by nám ulehčilo, kdybychom se mohli uvnitř metody nějak odkázat na objekt, jehož je tato metoda součástí. JavaScript runtime nám v tom rád pomůže. Musíme mu však vyjít naproti a k tomu budeme potřebovat malinko jiný způsob deklarace funkcí.

Staré dobré funkce

Způsob zápisu funkcí jako níže, který jsem doteď používali, je ve skutečnosti v JavaScriptu docela novinka.

const personEmail = (person, domain) => {
  const username = `${person.firstName}.${person.lastName}`;
  return username.toLowerCase() + domain;
};

Tomuto zápisu se říká arrow funkce a v JavaScriptu přibyl až ve verzi ES6. Dříve bylo zvykem psát funkce takto.

const personEmail = function (person, domain) {
  const username = `${person.firstName}.${person.lastName}`;
  return username.toLowerCase() + domain;
};

Takovýmto funkcím budeme říkat staré dobré funkce old-fashioned functions . Není zde velký rozdíl co se týče zápisu. Staré dobré funkce však mají jednu vlastnost, kterou arrow funkce nemají. Mají speciální tajný parametr, který lze do této funkce propašovat jakýmisi zadními vrátky. Tento parametr se jmenuje this. Jeho obsah závisí na tom, jakým způsobem naši funkci voláme. Pojďme si rovnou napsat jednoduchou testovací funkcí, abychom zjistili, jak this funguje.

'use strict';

const greet = function (name) {
  return `hello from ${this} and ${name}`;
};

Tato funkce jednoduše vrátí řetězec obsahující cokoliv, co je zrovna uloženo v tajném parametru this a v parametru name. Pokud takovou funkci zavoláme ve strict módu běžným způsobem, parametr this je undefined.

> greet('Martin')
"hello from undefined and Martin"

Pokud však funkci zavoláme pomocí metody call, můžeme si v prvním parametru určit, co bude uloženo v this.

> greet.call('Petr', 'Martin')
"hello from Petr and Martin"
> greet.call(5, 'Martin')
"hello from 5 and Martin"

K čemu nám takovéto podivné volání funkce je? Přímo nám programátorům vlastně skoro k ničemu. Tento trik totiž použije především JavaScript runtime při volání metod našich vlastních objektů. Pojďme výše uvedenou vítací metodu přidat do našeho oblíbeního objektu.

const homer = {
  firstName: 'Homer',
  middleName: 'Jay',
  lastName: 'Simpson',
  age: 39
  greet: function (name) {
    return `hello from ${this.firstName} and ${name}`;
  }
};

Ve chvíli, kdy metodu greet zavoláme takto

> homer.greet('Martin')
"hello from Homer and Martin"

JavaScript použije stejný trik jako my dříve s call a parametr this v naší metodě nastaví na objekt, na kterém tuto metodu zrovna voláme. Díky tajnému parametru this tak mohou všechny naše metody mít v době volání přístup ke svému objektu. Hurá!

Pozor na to, že takto fungují pouze staré dobré funkce. Arrow funkce žádný tajný parametr this nemají. Tento fakt nám pozdějí bude velmi ku prospěchu. Zaplatíme za to však tím, že arrow funkce nemůžeme použít jako metody.

Díky všem komplikovaným hrátkám s this jsme se tak posunuli o krok kupředu na naší cestě za elegantním objektovým programováním. Díky starým dobrým funkcím se naše objekty Simpsonovic rodinky o kus zpřehlední. Nemusíme už vytvářet nové metody pro každý objekt zvlášt. Stačí nám vytvořit je všehny pouze jednout a pak je k naším objektům jen připojit.

const name = function () {
  return `${this.firstName} ${this.middleName[0]}. ${this.lastName}`;
};

const email = function (domain) {
  const username = `${this.firstName}.${this.lastName}`;
  return username.toLowerCase() + domain;
};

const homer = {
  firstName: 'Homer',
  middleName: 'Jay',
  lastName: 'Simpson',
  age: 39,
  name: name,
  email: email,
};

const marge = {
  firstName: 'Marge',
  middleName: 'Jacqueline',
  lastName: 'Simpson',
  age: 36,
  name: name,
  email: email,
};

Můžeme pak psát jako obvykle

> marge.email('gmail.com')
"marge.simpson@gmail.com"

Cvičení - Metody a this

1

Pejsek

pohodička
  1. Vytvořte objekt pes. Pes má vlastnosti jméno, barva srsti a délka srsti. Dále má metody stekej, zavrc a trhej. Tyto metody jen vypisují do konzole vhodný text. (U metody trhej vymyslete nějakou vtipnou hlášku.) Pracujte v konzoli.
  2. Svého psa si v konzoli pořádně otestujte!
2

Profil na seznamce

pohodička

Pracujte v konzoli.

  1. Vytvořte objekt profil, který obsahuje data potřebná k seznamování na seznamce. V našem případě to bude jméno, barva vlasů, barva očí a oblíbený drink na prvním rande.
  2. Přidejte do objektu metodu, která do konzole vypíše text seznamovacího inzerátu. Text bude obsahovat všechna data z objektu, tedy jméno, obě barvy a oblíbený drink. Používejte klíčové slovo this. Udělejte to chytře, abyste se vyhnuli skloňování.
  3. Opět si to otestujte v konzoli!
Bonusová cvičení
Nepovinné úložky, které můžete řešit pokud máte chuť na větší výzvu nebo si chcete látku procvičit víc do hloubky.
3

Hledání na MDN

pohodička
  1. Na stránce Mozilla Developer Network najděte, co dělá metoda startsWith patřící všem řetězcům. Všimněte si klíčového slova prototype v titulku stránky (budeme o něm mluvit později).
  2. Stejným způsobem najděte, co dělá metoda indexOf patřící všem polím.

Prototypy

Příklad výše už je trochu hezčí než vytváření nové metody pro každý objekt, pořád však naše metody name a email musíme ručně vepisovat do každého objektu. To nás brzo přestane bavit. JavaScript runtime nám však opět přispěchá na pomoc, tentokrát s něčím, čemu se říká prototyp prototype .

Stejně jako mají staré dobré funkce tajný parametr this, maji i objekty jeden tajný klíč s trochu zlověstným názvem __proto__. Představte si, že máme e-shop a prodáváme v něm ledničky. Jednu ledničku bychom mohli reprezentovat takovýmto objektem.

const fridge1 = {
  volume: 275,
  freezer: false,
  color: 'white',

  __proto__: {
    name: 'SNAIGE CD290 1008',
    price: 7990,
  },
};

Všimněte si, že pod klíčem __proto__ je objekt s dalšími parametry naší ledničky. Tomuto objektu říkáme prototyp. Pokud se pokusíme přistoupit k nějaké vlastnosti naší ledničky například takto

> fridge1.price
7990

JavaScript runtime se podívá, jestli se klič price nachází v našem objektu. Pokud ne, podívá se do jeho prototypu uloženém v __proto__, zda se náhodou klíč nenachází tam. Pokud jej nenajde ani zde, zkouší prototyp prototypu a tak dál, dokud nenarazí na dno.

Prototyp tedy můžeme využít k tomu, abychom v něm skladovali věci, které jsou společné vícero objektům. Nejčastěji zde skladujeme metody, které chceme mít na všech objektech stejného typu. Můžeme tak například říct, že každý objekt představující osobu musí mít metody, která umí zobrazit celé jméno a email této osoby. Vytvoříme si tak následující prototyp.

const Person = {
  name: function() {
    return `${this.firstName} ${this.middleName[0]}. ${this.lastName}`;
  }
  email: function(domain) {
    const username = `${this.firstName}.${this.lastName}`;
    return username.toLowerCase() + domain;
  }
};

Proměnné obsahující prototypy je zvykem psát s velkým písmenem. Díky prototypu Person pak snadno vytvoříme naše Simpsonovic manžele.

const homer = {
  __proto__: Person,

  firstName: 'Homer',
  middleName: 'Jay',
  lastName: 'Simpson',
  age: 39,
};

const marge = {
  __proto__: Person,

  firstName: 'Marge',
  middleName: 'Jacqueline',
  lastName: 'Simpson',
  age: 36,
};

Nyní když provedeme takovéto volání

> homer.name()
"Homer J. Simpson"

JavaScript runtime zkouší najit metodu name v objektu homer. Pokud tam není, zkouší ji najít v jeho prototypu Person. Tam už metoda je. Runtime tedy nastaví její this na objekt homer a zavolá ji. Tím se nám všchno hezky propojí a běží jako po másle.

Cvičení - Objekty a prototypy

4

Pozemky

to dáš

Představte si, že programujeme aplikaci, která spravuje inzeráty k pronájmu pozemků. Každý pozemek budeme reprezentovat jak obdélník s určitou šírkou a výškou v metrech. Pro každý pozemek také budeme chtít určit jeho výměru v metrech čtverečních a obvod pozemku v metrech.

Založte JavaScriptový program a vytvořte objekt s názvem Estate podle následujícího vzoru

const Estate = {
  area: function () {
    // váš kód
  },
  border: function () {
    // váš kód
  },
};

Tento objekt bude představovat prototyp pro všechny naše pozemky.

  1. Doplňte kód metody area, která na základě hodnot this.width a this.height vrátí výměru pozemku v metrech čtverečních.
  2. Doplňte kód metody border, která spočítá délku hranice pozemku v metrech.
  3. Vytvořte objekt estate1 jako níže.
    const estate1 = {
      width: 120,
      height: 50,
    };
    
  4. Pomocí vlastnosti __proto__ nastavte tomuto pozemku prototyp Estate. V konzoli vyzkoušejte následujicí příkazy.
    > estate1.area()
    > estate1.border()
    
  5. Vytvořte pozemek estate2 s nějakými rozměry a správným prototypem a v konzli vyzoušejte, že i tento správně funguje.
5

Cena pozemku

to dáš

Začněte s hotovým řešením předchozího příkladu.

  1. Do všech pozemků přidejte novou vlastnost m2Price, která udává cenu pozemku za jeden metr čtvereční.
  2. Do prototypu Estate přidejte metodu price, která spočítá cenu celého pozemku podle jeho výměry.
  3. Vyzkoušejte v konzoli zavolat metodu price na všech vašich pozemcích.
  4. Vyzkoušejte, co se stane, když do některého pozemku zapomenete vloži vlastnost m2price a zkusíte zavolat metodu price.
6

Hodiny

to dáš

Vytvořte prototyp Clock, který bude přestavovat digitální hodiny zobrazující hodiny a minuty. Metody prototypu pracují s vlastnostmi this.hours a this.minutes.

  1. Přidejte do prototypu metodu hourUp, která zvýší čas na hodinách o jednu hodinu. Hodiny používají 24-hodinový formát, pod hodině 23 tedy následuje hodina 0.
  2. Podobným způsobem vytvořte metodu hourDown, která sníží čas o jednu hodinu.
  3. Přidejte metodu minuteUp, která zvýší čas na hodinách o jednu minutu. Dejte dobrý pozor na časy jako 9:59 a hlavně 23:59.
  4. Přidejte metodu minuteDown, která sníží čas na hodinách o jednu minutu. Pozor na časy jako 9:00 a hlavně 0:00.
  5. Přidejte metodu show, která vrátí řetězec s aktuláním časem na hodinách ve formátu H:MM, jedy alespoň jedna cifra pro hodinu a přesně dvě cifry pro minutu.
  6. Vytvořte několik objektů hodin s různými časy a vyzkoušejte v konzoli svoje metody.

Konstrukce objektů

Přechozí část této lekce sloužila k tomu, abychom si osahali jak technicky fungují prototypy objektů. Způsob, jakým jsme vytvářeli naše objekty je však pořád dost neohrabaný. V této části si ukážeme, jak tento postup zkrátit díky různým JavaScriptovým vychytávkám.

Na úvod je nutno zmínit, že vlastnost __proto__ není spolehlivá. Nejde totiž o standardizovanou část JavaScriptu. Na světě existuje vícero JavaScriptových runtimů a každý může prototypy objektů ukládat malinko jinak. Budeme proto potřebovat způsob, jak se vyhnout přímému nastavování vlasnoti __proto__.

Přesně k tomu slouží funkce s názvem Object.create. Tato funkce vytvoří prázdný objekt a jeho prototyp nastaví na hodnotu, kterou dostane v prvním parametru.

Pokud tedy chceme vytvořit objekt, který ve výsledku bude vypadat takto

const homer = {
  __proto__: Person,

  firstName: 'Homer',
  middleName: 'Jay',
  lastName: 'Simpson',
  age: 39,
};

stačí napsat

const homer = Object.create(Person);
homer.firstName = 'Homer';
homer.middleName = 'Jay';
homer.lastName = 'Simpson';
homer.age = 39;

const marge = Object.create(Person);
marge.firstName = 'Marge';
marge.middleName = 'Jacqueline';
marge.lastName = 'Simpson';
marge.age = 36;

Takto jsme se hezky zbavili nutnosti používat vlastnost __proto__ a nechali jsme nastavení prototypu na JavaScript runtimu. Chtěli bychom se však také zbavit neustálého opakování nastavování hodnot jednotlivých vlastností. K tomu si uvnitř prototypu vytvříme speciální metodu s názvem constructor. Tato metoda bude brát všechna potřebná data jako vstupy nastaví je rovnou jako vlastností prototypu. Náš prototyp Person pak bude vypadat takto.

const Person = {
  constructor: function (firstName, middleName, lastName, age) {
    this.firstName = 'Marge';
    this.middleName = 'Jacqueline';
    this.lastName = 'Simpson';
    this.age = 36;
  },
  name: function () {
    return `${this.firstName} ${this.middleName[0]}. ${this.lastName}`;
  },
  email: function (domain) {
    const username = `${this.firstName}.${this.lastName}`;
    return username.toLowerCase() + domain;
  },
};

Při vytváření objektů nám pak stačí zavolat konstruktor se správnými hodnotami.

const homer = Object.create(Person);
homer.constructor('Homer', 'Jay', 'Simpson', 39);

const marge = Object.create(Person);
marge.constructor('Marge', 'Jacqueline', 'Simpson', 36);

Vytváření objektů se tak významně zkrátilo díky tomu, že se všechna práce děje uvnitř prototypu.

Vzhledem k tomu, že objekty se v JavaScriptu vytváří velmi často a protože programátoři jsou pohodlní a nechce se jim psát ani písmenko navíc, existuje způsob jak zápis prototypu ještě o kousek zkrátit. Vždy, když v objektu vytváříme metodu, můžeme beztrestně vynechat dvojtečku a klíčové slovo function. Prototyp potom bude vypadat takto.

const Person = {
  constructor(firstName, middleName, lastName, age) {
    this.firstName = 'Marge';
    this.middleName = 'Jacqueline';
    this.lastName = 'Simpson';
    this.age = 36;
  },
  name() {
    return `${this.firstName} ${this.middleName[0]}. ${this.lastName}`;
  },
  email(domain) {
    const username = `${this.firstName}.${this.lastName}`;
    return username.toLowerCase() + domain;
  },
};

Jde pouze o kosmetickou změnu, která nám ušetří ťukání na klávesnici. Tento nový zápis znamená přesně totéž co předchozí zápis s funkcemi.

Tento způsob vytváření objektů už je na chlup blízko tomu, jak se objekty v praxi skutečně vytvářejí. Ještě nám chybí poslední drobnost, kterou si však necháme na některou z dalších lekcí.

Cvičení - konstrukce objektů

7

Pozemky 2

to dáš

Převeďte prototyp Estate z předchozího cvičení na nový formát zápisu prototypů pomocí konstrukturu. Pomocí Object.create a volání konstruktoru vytvořte několik pozemků a v konzoli vyzkoušejte, že fungují.

8

Hodiny 2

to dáš

Převeďte prototyp Clock z předchozího cvičení na nový formát zápisu prototypů pomocí konstrukturu. Pomocí Object.create a volání konstruktoru vytvořte několik digitálních hodin a v konzoli vyzkoušejte, že fungují.

9

Hodiny 3

zapni hlavu
  1. Přidejte do prototypu hodin metodu render, která vytvoří a vrátí DOM element hodin podle následujícího vzoru.
    <div class="clock">
      <span class="clock__hours">12</span>
      :
      <span class="clock__minutes">24</span>
    </div>
    
  2. Přidejte do prototypu metodu mount se vstupem parent, což bude nějaký DOM element. Pomocí metody render vytvořte v metodě mount DOM element hodin a pomocí appendChild jej zapojte na konec elementu parent.
  3. V koznoli vytvořte nějaké digitální hodiny a pomocí metody mount je zapojte do stránky. Zkuste do stránky zpojit více různých hodin za sebou.