4. díl - Objektově orientované programování v CoffeeScript

JavaScript CoffeeScript Objektově orientované programování v CoffeeScript

Zdravím vás u čtvrté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:

  1. 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)
  2. První část těla funkce __extends nakopíruje vlastnosti předka do potomka (i jejich hodnoty!)
  3. 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;
  1. 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 se super muselo řešit voláním předka jeho jménem, což může způsobit problémy.
  2. 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 metodou call(), 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.


 

  Aktivity (1)

Článek pro vás napsal Yahkem
Avatar

Jak se ti líbí článek?
Celkem (1 hlasů) :
55555


 


Miniatura
Předchozí článek
Funkce a výjimky v CoffeeScript
Miniatura
Všechny články v sekci
CoffeeScript

 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!