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
__extends
nakopíruje vlastnosti předka do potomka (i jejich hodnoty!) - Zajistí, že objekt
child.prototype
má 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ísuper
funkce v CoffeeScriptu. Bez tohoto řešení by sesuper
muselo ř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.