Lekce 4 - Objektově orientované programování v CoffeeScript
V předchozí lekci, Funkce a výjimky v CoffeeScript, jsme se podívali na funkce.
Zdravím vás u dalšího dílu seriálu, který se zabývá syntaxí jazyka CoffeeScript. V tomto dílu probereme objektově orientované programování. Nejprve si připomeneme prototypy z JavaScriptu a poté se podíváme na OOP s třídami, jak jej znáte z většiny C-like jazyků.
Jak to známe z JavaScriptu
Víme, že JS využívá objektově orientované programování na principu prototypů. Namísto tříd a objektů, které jsou jejich instancí, stojí tento způsob na principu klonování již existujícího objektu, který slouží jako prototyp. Vytvořme si jednoduchý příklad s autem:
// konstruktor var Auto = function(znacka, model, barva) { this.znacka = znacka; this.model = model; this.barva = barva; this.palivo = 100; }; // přidání funkce do prototypu Auto.prototype.jed = function() { if(this.palivo >= 10) { this.palivo -= 10; alert('Jsem ' + this.znacka + ' ' + this.model + ' a jeduuuuuuuu...'); } else { alert('Došlo palivo.'); } }; var sportak = new Auto('Mitsubishi', 'Lancer Evolution IX', 'stříbrná'); sportak.jed();
Asi bychom z minulých dílů věděli, jak tento kód napsat v CoffeeScriptu. Vypadal by nějak takto:
Auto = (znacka, model, barva) -> @.znacka = znacka @model = model @.barva = barva @palivo = 100 Auto::jed = -> if @.palivo >= 10 @palivo -= 10 alert "Jsem #{@.znacka} #{@model} a jeduuuuuuuu..." else alert 'Došlo palivo.' sportak = new Auto 'Mitsubishi', 'Lancer Evolution IX', 'stříbrná' do sportak.jed
Dvě dvojtečky v Auto::jed jsou zkratkou pro
.prototype. a slovo this nahrazuje zavináč.
Bystřejší z vás si všimli, že někdy dávám mezi zavináč a název
vlastnosti tečku, a někdy ne. Důvod je jednoduchý - chci vám ukázat, že
je to úplně jedno.
Znak
@ nahrazuje samotné slovo this i this.
podle situace, můžete si vybrat, co je vám milejší, já preferuji verzi bez
tečky. Dvě tečky a více by vám samozřejmě způsobily chybu.
K prototypovému přístupu v CoffeeScriptu je to vlastně vše, právě teď vás však čeká...
Hlavní chod
Pro ty, co se ztrácejí v prototypech, nelíbí se jim tento způsob OOP,
nebo prostě jen preferují způsob známý z C++, C#, Javy a „asi milionu“
dalších jazyků, CoffeeScript nabízí třídy, psané (jak jinak než) slovem
class. Víme, že CoffeeScript je JavaScript, uvnitř tedy stále
bije prototypové srdce, je to pouze abstrakce, která nám však dovoluje
používat známé postupy a hlavně ulehčí práci lidem, pro které je
dědičnost v JS španělskou vesnicí. Samozřejmě není problém míchat tyto
dva přístupy dohromady.
Přepišme si náš dřívější příklad s použitím třídy:
class Auto constructor: (@znacka, @model, @barva) -> jed: -> if @palivo >= 10 @palivo -= 10 alert "Jsem #{@znacka} #{@model} a jeduuuuuuuu..." else alert 'Došlo palivo.'
Tento zápis třídy by neměl nikoho překvapit. Konstruktor označujeme
slovem constructor. Toto slovo však není klíčovým slovem
CoffeeScriptu a proto můžete mít proměnnou pojmenovanou „constructor“ (z
pochopitelných důvodů to nedoporučuji). Slovo změní své chování pouze
tehdy, když se nalézá ve třídě. Kam ale zmizelo tělo konstruktoru?
Všimněte si @ v parametrech funkce. Říkáme tím funkci, ať
parametry dosadí do vlastností třídy se stejným jménem, jak lze vidět v
JavaScriptu:
var Auto; Auto = (function() { // náš 'constructor' function Auto(znacka, model, barva) { this.znacka = znacka; this.model = model; this.barva = barva; } // přidání metody do 'třídy' Auto.prototype.jed = function() { if (this.palivo >= 10) { this.palivo -= 10; return alert("Jsem " + this.znacka + " " + this.model + " a jeduuuuuuuu..."); } else { return alert('Došlo palivo.'); } }; return Auto; })();
Zápis v CoffeeScriptu nás ušetří nutnosti psát stejný název 3x místo jednou a několik řádků navíc. Důvodem pro uzavření „třídy“ do závorek je, jak už to tak bývá, Internet Explorer.
Dědičnost
Víme, že bez dědičnosti by to nebylo ono, a i když ji JavaScript obsahuje, je obzvlášť pro začátečníky krkolomná. Díky CoffeeScriptu ji však můžeme zapisovat a využívat velmi snadno. Zde je příklad:
class Vozidlo constructor: (@pocetKol) -> class Motocykl extends Vozidlo constructor: -> super 2
Dědičnost se zapisuje pomocí slova extends, jako v PHP, Javě
atd. Když chceme volat funkci z předka se stejným jménem, použijeme slovo
super (v našem případě v konstruktoru). Podíváme se, co nám
bylo vygenerováno:
// 1. var Motocykl, Vozidlo, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { // 2. for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } // 3. function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); // 4. child.__super__ = parent.prototype; return child; }; Vozidlo = (function() { function Vozidlo(pocetKol) { this.pocetKol = pocetKol; } return Vozidlo; })(); // 5. Motocykl = (function(_super) { __extends(Motocykl, _super); function Motocykl() { Motocykl.__super__.constructor.call(this, 2); } return Motocykl; })(Vozidlo);
Konečně něco, co vypadá zajímavě. Rozeberme si celý kód:
- Explicitní hoisting, ten už známe. Druhý řádek deklaruje pouze zkratku
pro metodu
hasOwnProperty, která je použita v následující funkci (proč není tato metoda deklarována uvnitř, nebo není používána rovnou, je mi záhadou) - První část těla funkce
__extendsnakopíruje vlastnosti předka do potomka (i jejich hodnoty!) - Zajistí, že objekt
child.prototypemá jako prototyp instanci předka. Také se postará o to, že všechny instance potomka mají správný konstruktor. Tato část je ekvivalentem ECMAScript 5 kódu:
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
- Přidá vlastnost
__super__, do které uloží referenci na předkův prototyp. Toho se využívá právě ve volánísuperfunkce v CoffeeScriptu. Bez tohoto řešení by sesupermuselo řešit voláním předka jeho jménem, což může způsobit problémy. - V deklaraci 'třídy' je předán předek jako parametr - v tomto případě
Vozidlo. Na prvním řádku v těle funkce se zavolá dříve vytvořená funkce
na „propojení“. V konstruktoru je použita vlastnost
__super__společně s metodoucall(), která zavolá metodu předka ve svém kontextu.
Snad jste teď pochopili, co se „pod kapotou“ děje. Jestli ne, nevadí,
život je dlouhý, pochopíte to jindy.
To by bylo k dědičnosti vše, CoffeeScript neobsahuje mnohonásobnou
dědičnost (JS ji také nemá, je potřeba použít mixin), ani interface
(nemá kontrolu datových typů během kompilace, na rozdíl od
TypeScriptu).
Magie dvojité šipky
Jedna z věcí, která může být v JavaScriptu matoucí, je
this. Ti, co dělají s jQuery, se s tímto problémem často
setkávají. Mějme stránku, kde je tlačítko s id 'klik':
$('#klik').on 'click', -> do -> alert @
JavaScript:
$('#klik').on('click', function() { return (function() { return alert(this); })(); });
Při kliknutí se zobrazí [object Window]. Kdybychom IIFE nahradili pouze
funkcí alert(this), zobrazí se [object HTMLButtonElement]. Tuto
situaci, kdy nám 'toto' nahrazuje 'tamto', když to nechceme, řeší
CoffeeScript znakem =>
$('#klik').on 'click', -> do => alert @
JavaScript:
$('#klik').on('click', function() { return (function(_this) { return function() { return alert(_this); }; })(this)(); });
Krom jQuery je tato šipka také velmi užitečná právě pro třídy. Vytvoříme si třídu, ve které bude konstruktor, jedna „jednoduchá“ metoda a jedna „dvojitá“. Poté tyto metody pošleme jako callback do funkce f:
class Area constructor: -> @x = 51 jedna: -> alert @x dve: => alert @x a = new Area a.jedna() # 51 a.dve() # 51 f = (funkce) -> funkce() f(a.jedna) # undefined f(a.dve) # 51 f(-> a.jedna()) # 51
Ve výsledném JavaScriptu nás zajímají hlavně tyto dva řádky:
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; // v těle konstruktoru this.dve = __bind(this.dve, this);
Které „přišpendlí“ danou funkci k objektu, kde je definována, a pomocí ní se poté můžeme odkazovat na vlastnosti objektu i v jiném kontextu. Za krásný příklad výše děkuji stránce StackOverflow.
A co statika?
Ano, i s tou můžete pracovat. Jelikož třída je zde vlastně objekt,
this označuje v definici samotnou třídu (konstruktor). Statickou
vlastnost/metodu vytvoříme jednoduše:
class Trida notStaticM: -> alert 'nejsem static' @staticA: 0 @staticM: -> ++@staticA alert Trida.staticA # 0 Trida.staticM() alert Trida.staticA # 1 Trida.notStaticM() # TypeError: undefined is not a function t = new Trida alert t.staticA # undefined t.staticM() # TypeError: undefined is not a function
V JS můžeme krásně vidět, že statické metody a vlastnosti se
přiřazují přímo ke 'třídě', tedy o objektu v proměnné
Trida, zatímco ne-statické se přiřazují k prototypu:
// tělo definice Trida.prototype.notStaticM = function() { return alert('nejsem static'); }; Trida.staticA = 0; Trida.staticM = function() { return ++this.staticA; // ... };
Jestli jste se dostali až sem, gratuluji vám, jste u konce seriálu. Já doufám, že se vám líbil a že pro vás bude od teď CoffeeScript důležitý nástroj při realizování vašich nápadů na ovládnutí světa. Jestli ne, tak snad máte o trošku lepší vhled do JavaScriptu.
V následujícím kvízu, Kvíz - CoffeeScript, si vyzkoušíme nabyté zkušenosti z kurzu.


