Rozdział 8. Programowanie obiektowe

> Dodaj do ulubionych

Na pocz─ůtku lat 90. w bran┼╝y programistycznej powsta┼éo zamieszanie spowodowane programowaniem obiektowym. Wi─Ökszo┼Ť─ç zasad tej techniki nie by┼éa ┼╝adn─ů nowo┼Ťci─ů, ale osi─ůgn─Ö┼éa ona na tyle du┼╝─ů popularno┼Ť─ç, ┼╝e w ko┼äcu nabra┼éa p─Ödu i zacz─Ö┼éa robi─ç si─Ö modna. Pisano ksi─ů┼╝ki na jej temat, przeprowadzano kursy oraz tworzono nowe obiektowe j─Özyki programowania. Nagle wszyscy zacz─Öli wychwala─ç pod niebiosa zalety obiektowo┼Ťci i z entuzjazmem u┼╝ywa─ç jej do rozwi─ůzywania wszystkich mo┼╝liwych problem├│w. Wygl─ůda┼éo to tak, jakby niekt├│rzy byli przekonani, ┼╝e w ko┼äcu odkryli prawid┼éowy spos├│b pisania program├│w.

Jest to do┼Ť─ç typowe.. Gdy co┼Ť jest bardzo skomplikowane, ludzie zawsze szukaj─ů jakiego┼Ť magicznego rozwi─ůzania. I gdy pojawi si─Ö cos, co tak wygl─ůda, od razu zyskuje pot─Ö┼╝n─ů rzesz─Ö oddanych wielbicieli. Do dzi┼Ť dla wielu programist├│w obiektowo┼Ť─ç (albo ich wyobra┼╝enie obiektowo┼Ťci) jest ┼Ťwi─Öto┼Ťci─ů. Dla nich program, kt├│ry nie jest „prawdziwie obiektowy” (cokolwiek to znaczy), to s┼éaby program.

Niemniej jednak niewiele jest przej┼Ťciowych m├│d, kt├│re przetrwa┼éyby tak d┼éugo. Sukces obiektowo┼Ťci mo┼╝na w du┼╝ym stopniu t┼éumaczy─ç tym, ┼╝e jest oparta na solidnych podstawach. W tym rozdziale znajduje si─Ö opis w┼éa┼Ťnie tych podwalin obiektowo┼Ťci oraz ich adaptacja w JavaScripcie. Chc─Ö te┼╝ podkre┼Ťli─ç, ┼╝e w poprzednich akapitach nie chcia┼éem zdyskredytowa─ç obiektowo┼Ťci jako takiej. Moj─ů intencj─ů by┼éo tylko ostrzec Ci─Ö przed nadmiernym przywi─ůzywaniem si─Ö do tej metodologii programowania.


Czym jest programowanie obiektowe

Jak nazwa wskazuje, programowanie obiektowe polega na u┼╝ywaniu obiekt├│w. Do tej pory u┼╝ywali┼Ťmy ich do lu┼║nego grupowania warto┼Ťci, dodaj─ůc i usuwaj─ůc z nich dane, gdy tak si─Ö nam podoba┼éo. W programowaniu obiektowym obiekty s─ů traktowane jak ma┼ée samodzielne ┼Ťwiaty, a ┼Ťwiat zewn─Ötrzny mo┼╝e si─Ö z nimi kontaktowa─ç tylko poprzez niewielki ┼Ťci┼Ťle zdefiniowany interfejs, b─Öd─ůcy zestawem metod i w┼éasno┼Ťci. Przyk┼éadem tego jest lista osi─ůgni─Ötych cel├│w, kt├│rej u┼╝ywali┼Ťmy na ko┼äcu rozdzia┼éu 7. Do pos┼éugiwania si─Ö ni─ů u┼╝ywali┼Ťmy tylko trzech funkcji: makeReachedList, storeReached oraz findReached. Te trzy funkcje stanowi─ů interfejs tego typu obiekt├│w.

To samo dotyczy obiekt├│w Date, Error i BinaryHeap. Zamiast zwyk┼éych funkcji do pracy z obiektami, do dyspozycji mamy s┼éowo kluczowe new do tworzenia obiekt├│w, kt├│re wraz z pewn─ů liczb─ů metod i w┼éasno┼Ťci stanowi ich interfejs.


Metody

Jednym ze sposob├│w na dodanie metod do obiekt├│w jest po prostu do┼é─ůczenie do nich funkcji.

var rabbit = {};
rabbit.speak = function(line) {
  print("Kr├│lik powiedzia┼é „", line, "”");
};

rabbit.speak("Teraz Ty pytasz mnie.");

W wi─Ökszo┼Ťci przypadk├│w metoda musi wiedzie─ç, na czym ma dzia┼éa─ç. Na przyk┼éad, je┼Ťli by┼éoby kilka kr├│lik├│w, metoda speak musia┼éaby wskazywa─ç, kt├│ry kr├│lik ma m├│wi─ç. Do tego s┼éu┼╝y specjalna zmienna o nazwie this, kt├│ra jest zawsze dost─Öpna w wywo┼éaniu funkcji, a je┼Ťli funkcja jest wywo┼éywana jako metoda, wskazuje odpowiedni obiekt. Funkcja nazywa si─Ö metod─ů, gdy nale┼╝y do obiektu i jest z niego wywo┼éywana, np. object.method().

function speak(line) {
  print("Pewien ", this.adjective, " kr├│lik m├│wi „", line, "”");
}
var whiteRabbit = {adjective: "biały", speak: speak};
var fatRabbit = {adjective: "gruby", speak: speak};

whiteRabbit.speak("Na moje uszy i w─ůsy, kt├│ra to ju┼╝ godzina!");
fatRabbit.speak("Nie pogardzi┼ébym jak─ů┼Ť ma┼é─ů marchewk─ů.");

Teraz mog─Ö wyja┼Ťni─ç do czego s┼éu┼╝y ten tajemniczy pierwszy argument metody apply, kt├│remu zawsze przypisywali┼Ťmy warto┼Ť─ç null w rozdziale 6. Przy jego u┼╝yciu mo┼╝na okre┼Ťli─ç obiekt, do kt├│rego ma zosta─ç zastosowana funkcja. W przypadku funkcji nieb─Öd─ůcych metodami argument ten jest niepotrzebny i dlatego nadawali┼Ťmy mu warto┼Ť─ç null.

speak.apply(fatRabbit, ["Pycha."]);

Funkcje r├│wnie┼╝ maj─ů metod─Ö call, kt├│ra jest podobna do apply, ale przyjmuje argumenty dla funkcji oddzielnie, zamiast w tablicy:

speak.call(fatRabbit, "Burp.");

Tworzenie obiekt├│w ÔÇô new

S┼éowo kluczowe new umo┼╝liwia tworzenie obiekt├│w w wygodny spos├│b. Gdy przed wywo┼éaniem funkcji wstawi si─Ö s┼éowo kluczowe new, jej zmienna this wska┼╝e na nowy obiekt, kt├│ry funkcja automatycznie zwr├│ci (chyba ┼╝e celowo ma ustawione, aby zwraca─ç co┼Ť innego). Funkcje s┼éu┼╝─ůce do tworzenia nowych obiekt├│w nazywaj─ů si─Ö konstruktorami. Poni┼╝ej znajduje si─Ö konstruktor kr├│lik├│w:

function Rabbit(adjective) {
  this.adjective = adjective;
  this.speak = function(line) {
    print("Pewien ", this.adjective, " kr├│lik m├│wi „", line, "”");
  };
}

var killerRabbit = new Rabbit("zab├│jczy");
killerRabbit.speak("KRAAAAAAAAACH!");

W programowaniu JavaScript istnieje konwencja, zgodnie z kt├│r─ů nazwy konstruktor├│w rozpoczyna si─Ö wielk─ů liter─ů. Dzi─Öki temu ┼éatwo si─Ö je odr├│┼╝nia od innych funkcji.

Czy słowa kluczowe new jest tak naprawdę potrzebne? Przecież równie dobrze można by było pisać tak:

function makeRabbit(adjective) {
  return {
    adjective: adjective,
    speak: function(line) {/*itd.*/}
  };
}

var blackRabbit = makeRabbit("czarny");

To nie jest dok┼éadnie to samo. S┼éowo kluczowe new wykonuje jeszcze kilka dodatkowych dzia┼éa┼ä, tylko tego nie wida─ç. Nasz obiekt killerRabbit ma w┼éasno┼Ť─ç o nazwie constructor wskazuj─ůc─ů funkcj─Ö Rabbit, kt├│ra go utworzy┼éa. Obiekt blackRabbit r├│wnie┼╝ ma tak─ů w┼éasno┼Ť─ç, ale wskazuj─ůc─ů funkcj─Ö Object.

show(killerRabbit.constructor);
show(blackRabbit.constructor);

Prototypy

Sk─ůd si─Ö wzi─Ö┼éa w┼éasno┼Ť─ç constructor? Jest ona cz─Ö┼Ťci─ů prototypu kr├│lika. Prototypy s─ů pot─Ö┼╝n─ů, cho─ç troch─Ö zawi┼é─ů, cz─Ö┼Ťci─ů systemu obiektowego j─Özyka JavaScript. Ka┼╝dy obiekt bazuje na jakim┼Ť prototypie, z kt├│rego dziedziczy r├│┼╝ne w┼éasno┼Ťci. Proste obiekty, kt├│rych u┼╝ywali┼Ťmy do tej pory bazuj─ů na podstawowym prototypie, kt├│ry jest zwi─ůzany z konstruktorem Object. W istocie wyra┼╝enie {} jest r├│wnowa┼╝ne z new Object().

var simpleObject = {};
show(simpleObject.constructor);
show(simpleObject.toString);

Metoda toString nale┼╝y do prototypu Object. Oznacza to, ┼╝e wszystkie proste obiekty maj─ů metod─Ö toString, kt├│ra konwertuje je na ┼éa┼äcuch. Nasze obiekty kr├│lik├│w s─ů utworzone na bazie prototypu zwi─ůzanego z konstruktorem Rabbit. Za pomoc─ů w┼éasno┼Ťci prototype konstruktora mo┼╝na nawet uzyska─ç dost─Öp do ich prototypu:

show(Rabbit.prototype);
show(Rabbit.prototype.constructor);

Ka┼╝dej funkcji automatycznie przypisywana jest w┼éasno┼Ť─ç prototype, kt├│rej w┼éasno┼Ť─ç constructor wskazuje na t─Ö funkcj─Ö. Poniewa┼╝ prototyp kr├│lika sam jest obiektem, bazuje na prototypie Object i ma jego metod─Ö toString.

show(killerRabbit.toString == simpleObject.toString);

Obiekty dziedzicz─ů w┼éasno┼Ťci swoich prototyp├│w, ale dziedziczenie to jest tylko jednostronne. W┼éasno┼Ťci prototypu maj─ů wp┼éyw na obiekt utworzony na bazie tego prototypu, ale w┼éasno┼Ťci tego obiektu nie maj─ů wp┼éywu na prototyp.

┼Üci┼Ťle rzecz bior─ůc regu┼éa ta brzmi nast─Öpuj─ůco: szukaj─ůc w┼éasno┼Ťci JavaScript najpierw przeszukuje zestaw w┼éasno┼Ťci samego obiektu. Je┼Ťli w┼éasno┼Ť─ç o szukanej nazwie zostanie znaleziona, to zostanie u┼╝yta. Je┼Ťli w┼éasno┼Ť─ç nie zostanie znaleziona, przeszukiwany jest prototyp obiektu, nast─Öpnie prototyp prototypu itd. Je┼Ťli nic nie zostanie znalezione, zostaje zwr├│cona warto┼Ť─ç undefined. Z drugiej strony, gdy ustawiana jest warto┼Ť─ç w┼éasno┼Ťci, JavaScript nigdy nie przechodzi do prototypu, lecz zawsze ustawia w┼éasno┼Ť─ç w samym obiekcie.

Rabbit.prototype.teeth = "małe";
show(killerRabbit.teeth);
killerRabbit.teeth = "długie, ostre i zakrwawione";
show(killerRabbit.teeth);
show(Rabbit.prototype.teeth);

Oznacza to, ┼╝e za pomoc─ů prototypu mo┼╝na w dowolnej chwili doda─ç nowe w┼éasno┼Ťci i metody do wszystkich bazuj─ůcych na nim obiekt├│w. Na przyk┼éad w trakcie pracy mo┼╝e si─Ö okaza─ç, ┼╝e nasze kr├│liki musz─ů umie─ç ta┼äczy─ç.

Rabbit.prototype.dance = function() {
  print("Pewien ", this.adjective, " królik tańczy gigę.");
};

killerRabbit.dance();

Jak si─Ö pewnie domy┼Ťlasz, prototyp kr├│lika jest doskona┼éym miejscem na dodawanie warto┼Ťci wsp├│lnych dla wszystkich kr├│lik├│w, takich jak metoda speak. Oto nowa wersja konstruktora Rabbit:

function Rabbit(adjective) {
  this.adjective = adjective;
}
Rabbit.prototype.speak = function(line) {
  print("Pewien ", this.adjective, " kr├│lik m├│wi „", line, "”");
};

var hazelRabbit = new Rabbit("br─ůzowy");
hazelRabbit.speak("Dobry Frith!");

Fakt, ┼╝e wszystkie obiekty maj─ů prototypy i mog─ů po nich dziedziczy─ç r├│┼╝ne w┼éasno┼Ťci mo┼╝e sprawia─ç problemy. Oznacza to, ┼╝e u┼╝ycie obiektu do przechowywania zbioru warto┼Ťci, jak w przypadku kot├│w w rozdziale 4, mo┼╝e si─Ö nie uda─ç. Gdyby┼Ťmy np. chcieli sprawdzi─ç czy istnieje kot o imieniu constructor, napisaliby┼Ťmy taki kod:

var noCatsAtAll = {};
if ("constructor" in noCatsAtAll)
  print("Tak, niew─ůtpliwie istnieje kot o imieniu „constructor”.");

Mamy problem. Dodatkowe trudno┼Ťci mo┼╝e sprawia─ç fakt, ┼╝e standardowe prototypy, takie jak Object i Array, cz─Östo rozszerza si─Ö o nowe przydatne funkcje. Na przyk┼éad mogliby┼Ťmy wszystkim obiektom doda─ç metod─Ö o nazwie properties zwracaj─ůc─ů tablic─Ö nazw wszystkich nieukrytych w┼éasno┼Ťci obiekt├│w:

Object.prototype.properties = function() {
  var result = [];
  for (var property in this)
    result.push(property);
  return result;
};

var test = {x: 10, y: 3};
show(test.properties());

Od razu wida─ç, w czym tkwi problem. Od tej chwili prototyp Object ma w┼éasno┼Ť─ç o nazwie properties, w zwi─ůzku z czym w wyniku iteracji przy u┼╝yciu p─Ötli for i in po w┼éasno┼Ťciach jakiegokolwiek obiektu otrzymamy tak┼╝e t─Ö wsp├│ln─ů w┼éasno┼Ť─ç, czego normalnie by┼Ťmy nie chcieli. Interesuj─ů nas jedynie w┼éasno┼Ťci nale┼╝─ůce tylko do tego obiektu.

Na szcz─Ö┼Ťcie mo┼╝na sprawdzi─ç, czy wybrana w┼éasno┼Ť─ç nale┼╝y do obiektu, czy do jednego z jego prototyp├│w. Niestety dodatek tego testu sprawia, ┼╝e kod p─Ötli staje si─Ö nieco niezgrabny. Ka┼╝dy obiekt ma metod─Ö o nazwie hasOwnProperty, kt├│ra informuje, czy obiekt ma w┼éasno┼Ť─ç o okre┼Ťlonej nazwie. Przy jej u┼╝yciu nasz─ů metod─Ö properties mogliby┼Ťmy przepisa─ç nast─Öpuj─ůco:

Object.prototype.properties = function() {
  var result = [];
  for (var property in this) {
    if (this.hasOwnProperty(property))
      result.push(property);
  }
  return result;
};

var test = {"Fat Igor": true, "Fireball": true};
show(test.properties());

I oczywi┼Ťcie mo┼╝emy j─ů przepisa─ç abstrakcyjnie jako funkcj─Ö wy┼╝szego rz─Ödu. Zwr├│─ç uwag─Ö, ┼╝e w wywo┼éaniu funkcji action przekazywana jest zar├│wno nazwa w┼éasno┼Ťci jak i jej warto┼Ť─ç w obiekcie.

function forEachIn(object, action) {
  for (var property in object) {
    if (object.hasOwnProperty(property))
      action(property, object[property]);
  }
}

var chimera = {głowa: "lwa", ciało: "kozła", ogon: "węża"};
forEachIn(chimera, function(name, value) {
  print(name, " ", value, ".");
});

Ale co b─Ödzie, gdy znajdziemy kota o imieniu hasOwnProperty? (Kto wie, jakie imiona ludzie mog─ů nadawa─ç swoim kotom). Zostanie ono zapisane w obiekcie i gdy spr├│bujemy potem przejrze─ç kolekcj─Ö kot├│w, wywo┼éanie metody object.hasOwnProperty nie uda si─Ö, poniewa┼╝ warto┼Ť─ç ta nie b─Ödzie ju┼╝ wskazywa┼éa warto┼Ťci funkcyjnej. Mo┼╝na to rozwi─ůza─ç stosuj─ůc jeszcze mniej eleganckie rozwi─ůzanie:

function forEachIn(object, action) {
  for (var property in object) {
    if (Object.prototype.hasOwnProperty.call(object, property))
      action(property, object[property]);
  }
}

var test = {name: "Mordechai", hasOwnProperty: "Uh-oh"};
forEachIn(test, function(name, value) {
  print("Property ", name, " = ", value);
});

(Uwaga: ten przyk┼éad nie dzia┼éa w przegl─ůdarce Internet Explorer 8, kt├│ra najwyra┼║niej ma trudno┼Ťci z przes┼éanianiem wbudowanych w┼éasno┼Ťci prototyp├│w).

W tym kodzie zamiast u┼╝ywa─ç metody z obiektu pos┼éugujemy si─Ö metod─ů pobran─ů z prototypu Object, a nast─Öpnie stosujemy j─ů do odpowiedniego obiektu za pomoc─ů funkcji call. Je┼Ťli nikt nie naba┼éagani w tej metodzie w Object.prototype (a nie nale┼╝y tego robi─ç), to program powinien dzia┼éa─ç prawid┼éowo.


Metody hasOwnProperty mo┼╝na tak┼╝e u┼╝ywa─ç w tych sytuacjach, w kt├│rych u┼╝ywali┼Ťmy operatora in, aby dowiedzie─ç si─Ö czy wybrany obiekt ma okre┼Ťlon─ů w┼éasno┼Ť─ç. Jest jednak pewien haczyk. W rozdziale 4 dowiedzia┼ée┼Ť si─Ö, ┼╝e niekt├│re w┼éasno┼Ťci, np. toString, s─ů ukryte i p─Ötle forin ich nie wykrywaj─ů. Przegl─ůdarki z rodziny Gecko (przede wszystkim Firefox) ka┼╝demu obiektowi przypisuj─ů ukryt─ů w┼éasno┼Ť─ç o nazwie __proto__ wskazuj─ůc─ů prototyp tego obiektu. Dla niej metoda hasOwnProperty r├│wnie┼╝ zwr├│ci true, mimo ┼╝e nie zosta┼éa dodana bezpo┼Ťrednio przez program. Dost─Öp do prototypu obiektu bywa przydatny, ale realizowanie tego w postaci w┼éasno┼Ťci nie by┼éo najlepszym pomys┼éem. Niemniej jednak Firefox to bardzo popularna przegl─ůdarka, a wi─Öc pisz─ůc aplikacj─Ö sieciow─ů trzeba o tym pami─Öta─ç. Istnieje te┼╝ metoda o nazwie propertyIsEnumerable, kt├│ra zwraca false dla ukrytych w┼éasno┼Ťci i za pomoc─ů kt├│rej mo┼╝na odfiltrowa─ç takie dziwad┼éa, jak __proto__. Poni┼╝sze wyra┼╝enie jest dobrym sposobem na obej┼Ťcie omawianego problemu:

var object = {foo: "bar"};
show(Object.prototype.hasOwnProperty.call(object, "foo") &&
     Object.prototype.propertyIsEnumerable.call(object, "foo"));

Proste i eleganckie, prawda? Jest to jedna z tych s┼éabszych stron projektu JavaScriptu. Obiekty pe┼éni─ů zar├│wno rol─Ö „warto┼Ťci z metodami”, dla kt├│rych prototypy s─ů po┼╝yteczne jak i „zbior├│w w┼éasno┼Ťci”, kt├│rym prototypy tylko przeszkadzaj─ů.


Wpisywanie powy┼╝szego wyra┼╝enia, za ka┼╝dym razem gdy trzeba sprawdzi─ç, czy obiekt zawiera jak─ů┼Ť w┼éasno┼Ť─ç jest niewykonalne. Mogliby┼Ťmy zdefiniowa─ç funkcj─Ö, ale jeszcze lepszym rozwi─ůzaniem jest napisanie konstruktora i prototypu specjalnie na okazje, gdy obiekt chcemy traktowa─ç jako zestaw w┼éasno┼Ťci. Poniewa┼╝ mo┼╝na w nim wyszukiwa─ç warto┼Ťci po nazwach, nazwiemy go Dictionary (s┼éownik).

function Dictionary(startValues) {
  this.values = startValues || {};
}
Dictionary.prototype.store = function(name, value) {
  this.values[name] = value;
};
Dictionary.prototype.lookup = function(name) {
  return this.values[name];
};
Dictionary.prototype.contains = function(name) {
  return Object.prototype.hasOwnProperty.call(this.values, name) &&
    Object.prototype.propertyIsEnumerable.call(this.values, name);
};
Dictionary.prototype.each = function(action) {
  forEachIn(this.values, action);
};

var colours = new Dictionary({Grover: "niebieski",
                              Elmo: "pomarańczowy",
                              Bert: "żółty"});
show(colours.contains("Grover"));
show(colours.contains("constructor"));
colours.each(function(name, colour) {
  print(name, " jest ", colour);
});

Ca┼éy mechanizm wykorzystania obiekt├│w jako zbior├│w w┼éasno┼Ťci zosta┼é zamkni─Öty w wygodnym interfejsie: jeden konstruktor i cztery metody. Zauwa┼╝, ┼╝e w┼éasno┼Ť─ç values obiektu Dictionary nie nale┼╝y do tego interfejsu, tylko jest wewn─Ötrznym szczeg├│┼éem, kt├│rego nie u┼╝ywa si─Ö bezpo┼Ťrednio podczas korzystania z obiekt├│w typu Dictionary.

Do ka┼╝dego tworzonego przez siebie interfejsu powinno doda─ç si─Ö kr├│tki komentarz opisuj─ůcy spos├│b jego dzia┼éania i u┼╝ycia. Dzi─Öki temu, gdy za kilka miesi─Öcy kto┼Ť (mo┼╝e Ty sam) zechce go u┼╝y─ç, b─Ödzie m├│g┼é szybko przeczyta─ç instrukcj─Ö obs┼éugi zamiast studiowa─ç kod.

Zazwyczaj kr├│tko po zaprojektowaniu interfejsu odkrywa si─Ö jego ograniczenia i usterki, kt├│re nale┼╝y zmieni─ç. Dlatego dla oszcz─Ödno┼Ťci czasu zaleca si─Ö dokumentowanie interfejs├│w dopiero po pewnym czasie ich u┼╝ytkowania, gdy zostanie udowodnione, ┼╝e s─ů praktyczne. Oczywi┼Ťcie to mo┼╝e by─ç pokus─ů, aby w og├│le zapomnie─ç o pisaniu dokumentacji. Sam robienie tego traktuj─Ö jako czynno┼Ť─ç wyko┼äczeniow─ů podczas prac nad systemem. Gdy interfejs jest gotowy, po prostu stwierdzam, ┼╝e czas co┼Ť o nim napisa─ç, aby przekona─ç si─Ö, ┼╝e jego opis w j─Özyku ludzkim brzmi r├│wnie dobrze, jak w j─Özyku JavaScript (lub jakimkolwiek innym j─Özyku programowania, jakiego u┼╝ywamy).


Interfejs i implementacja

Rozr├│┼╝nienie zewn─Ötrznego interfejsu i wewn─Ötrznych szczeg├│┼é├│w obiektu jest wa┼╝ne z dw├│ch powod├│w. Po pierwsze dzi─Öki niewielkiemu i ┼Ťci┼Ťle zdefiniowane interfejsowi obiekt jest ┼éatwy w u┼╝yciu. Trzeba tylko zna─ç ten interfejs, a reszt─Ö kodu obiektu nie musimy si─Ö interesowa─ç, chyba ┼╝e chcemy co┼Ť w nim zmieni─ç.

Po drugie cz─Östo zdarza si─Ö, ┼╝e trzeba co┼Ť zmieni─ç w wewn─Ötrznej implementacji obiektu, aby by┼é bardziej praktyczny, lepiej dzia┼éa┼é albo ┼╝eby usun─ů─ç usterk─Ö. Gdyby w innych cz─Ö┼Ťciach programu u┼╝ywane by┼éy wszystkie w┼éasno┼Ťci i elementy budowy obiektu, nie mo┼╝na by by┼éo w nim nic zmieni─ç bez dodatkowego modyfikowania du┼╝ych partii kodu w innych miejscach. Je┼Ťli na zewn─Ötrz obiektu u┼╝ywany jest tylko jego niewielki interfejs, mo┼╝na wprowadza─ç dowolne zmiany w implementacji, pod warunkiem, ┼╝e nie rusza si─Ö tego interfejsu.

Niekt├│rzy traktuj─ů to niezwykle powa┼╝nie. Osoby takie np. w interfejsach obiekt├│w nigdy nie umieszczaj─ů w┼éasno┼Ťci, a jedynie metody — je┼Ťli ich typ obiektowy ma d┼éugo┼Ť─ç, to jest ona dost─Öpna poprzez metod─Ö getLength, a nie w┼éasno┼Ť─ç length. Dzi─Öki temu, je┼Ťli kiedy┼Ť zechc─ů ze swojego obiektu usun─ů─ç w┼éasno┼Ť─ç length, bo np. od tej pory zawiera on wewn─Ötrzn─ů tablic─Ö, kt├│rej d┼éugo┼Ť─ç musi zwraca─ç, mog─ů zmodyfikowa─ç funkcj─Ö bez zmieniania interfejsu.

Jednak moim zdaniem nie jest to warte zachodu. Dodanie metody o nazwie getLength, kt├│ra zawiera tylko instrukcj─Ö return this.length; jest niepotrzebnym mno┼╝eniem kodu. Dla mnie w wi─Ökszo┼Ťci przypadk├│w taki bezsensowny kod jest wi─Ökszym problemem ni┼╝ konieczno┼Ť─ç zmiany interfejsu raz na gody.


Bardzo przydatne jest dodawanie nowych metod do istniej─ůcych prototyp├│w. W j─Özyku JavaScript dodatkowe metody przyda┼éyby si─Ö prototypom Array i String. Mogliby┼Ťmy np. zamieni─ç funkcje forEach i map metodami tablic, a funkcj─Ö startsWith napisan─ů w rozdziale 4 zamieni─ç w metod─Ö ┼éa┼äcuch├│w.

Je┼Ťli jednak Tw├│j program b─Ödzie dzia┼éa┼é na jednej stronie internetowej z innym programem, w kt├│rym programista u┼╝ywa konstrukcji forin naiwnie ÔÇĽ czyli tak, jak my do tej pory ÔÇĽ to dodanie metod do prototyp├│w, zw┼éaszcza Object i Array, na pewno spowoduje problemy, poniewa┼╝ p─Ötle te nagle zaczn─ů znajdowa─ç nowe w┼éasno┼Ťci. Dlatego niekt├│rzy wol─ů w og├│le nie rusza─ç tych prototyp├│w. Oczywi┼Ťcie je┼Ťli jeste┼Ť ostro┼╝ny i nie spodziewasz si─Ö, ┼╝e Tw├│j kod b─Ödzie pracowa┼é obok jakiego┼Ť ┼║le napisanego programu, dodawanie metod do standardowych prototyp├│w jest jak najbardziej warto┼Ťciow─ů technik─ů.


Budowa wirtualnego terrarium

W tym rozdziale zbudujemy wirtualne terrarium, czyli pojemnik zawieraj─ůcy fruwaj─ůce owady. W programie tym b─Ödziemy u┼╝ywa─ç obiekt├│w, co chyba Ci─Ö nie dziwi, skoro temat tego rozdzia┼éu to programowanie obiektowe w JavaScript. Nie b─Ödziemy tworzy─ç niczego skomplikowanego. Nasze terrarium b─Ödzie dwuwymiarow─ů siatk─ů, jak druga mapa w rozdziale 7. Na siatce rozmieszczone s─ů owady. Gdy terrarium jest aktywne, ka┼╝dy owad mo┼╝e wykona─ç jak─ů┼Ť czynno┼Ť─ç, np poruszy─ç si─Ö co p├│┼é sekundy.

W zwi─ůzku z tym podzielimy przestrze┼ä i czas na jednostki o sta┼éym rozmiarze ÔÇĽ kwadraty dla przestrzeni i po┼é├│wki sekund dla czasu. Zalet─ů tego jest uproszczenie modelowania w programie, a wad─ů niska precyzja. Na szcz─Ö┼Ťcie w tym symulatorze terrarium nic nie musi by─ç precyzyjne, a wi─Öc nie ma problemu.


Terrarium mo┼╝na zdefiniowa─ç przy u┼╝yciu szablonu b─Öd─ůcego tablic─ů ┼éa┼äcuch├│w. Mogliby┼Ťmy u┼╝y─ç pojedynczego ┼éa┼äcucha, ale poniewa┼╝ w JavaScripcie ┼éa┼äcuchy musz─ů w ca┼éo┼Ťci mie┼Ťci─ç si─Ö w jednym wierszu, by┼éoby to trudne do zrealizowania.

var thePlan =
  ["############################",
   "#      #    #      o      ##",
   "#                          #",
   "#          #####           #",
   "##         #   #    ##     #",
   "###           ##     #     #",
   "#           ###      #     #",
   "#   ####                   #",
   "#   ##       o             #",
   "# o  #         o       ### #",
   "#    #                     #",
   "############################"];

Znaki # reprezentuj─ů ┼Ťciany terrarium (i znajduj─ůce si─Ö w nim ozdobne kamienie), znaki o reprezentuj─ů owady, a spacje, jak si─Ö pewnie domy┼Ťlasz oznaczaj─ů puste miejsce.

Z takiej tablicy mo┼╝na utworzy─ç obiekt terrarium. W obiekcie tym przechowywane b─Öd─ů kszta┼ét i zawarto┼Ť─ç terrarium oraz b─Ödzie on umo┼╝liwia┼é poruszanie si─Ö owadom. Obiekt ma cztery metody: Pierwsza to toString, kt├│ra konwertuje terrarium na ┼éa┼äcuch podobny do bazowego planu, dzi─Öki czemu mo┼╝na zobaczy─ç, co si─Ö dzieje wewn─ůtrz. Metoda step pozwala owadom wykona─ç pojedynczy ruch, je┼Ťli sobie tego ┼╝ycz─ů. Natomiast metody start i stop s┼éu┼╝─ů do „w┼é─ůczania” i wy┼é─ůczania” terrarium. Gdy terrarium jest uruchomione, metoda step jest wywo┼éywana automatycznie co p├│┼é sekundy powoduj─ůc ruch owad├│w.


─ćwiczenie 8.1

Punkty na siatce tak┼╝e b─Öd─ů reprezentowane jako obiekty. W rozdziale 7 do pracy z punktami u┼╝ywane by┼éy trzy funkcje: point, addPoints oraz samePoint. Tym razem u┼╝yjemy konstruktora i dw├│ch metod. Napisz konstruktor Point pobieraj─ůcy dwa argumenty b─Öd─ůce wsp├│┼érz─Ödnymi x i y punktu i tworz─ůcy obiekt zawieraj─ůcy w┼éasno┼Ťci x i y. Prototypowi tego konstruktora dodaj metod─Ö add pobieraj─ůc─ů punkt jako argument i zwracaj─ůc─ů nowy punkt, kt├│rego wsp├│┼érz─Ödne x i y s─ů sum─ů wsp├│┼érz─Ödnych x i y dw├│ch podanych punkt├│w. Dodatkowo napisz metod─Ö isEqualTo pobieraj─ůc─ů punkt i zwracaj─ůc─ů warto┼Ť─ç logiczn─ů oznaczaj─ůc─ů, czy ten (this) punkt ma takie same wsp├│┼érz─Ödne, jak podany punkt.

Opr├│cz wymienionych dw├│ch metod w sk┼éad interfejsu tego typu obiekt├│w wchodz─ů r├│wnie┼╝ w┼éasno┼Ťci x i y: Kod u┼╝ywaj─ůcy obiekt├│w punkt├│w mo┼╝e dowolnie pobiera─ç i modyfikowa─ç w┼éasno┼Ťci x i y.

function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype.add = function(other) {
  return new Point(this.x + other.x, this.y + other.y);
};
Point.prototype.isEqualTo = function(other) {
  return this.x == other.x && this.y == other.y;
};

show((new Point(3, 1)).add(new Point(2, 4)));

Pami─Ötaj, aby Twoja wersja metody add pozostawia┼éa punkt this nietkni─Öty i tworzy┼éa nowy obiekt. Metoda zmieniaj─ůca bie┼╝─ůcy obiekt dzia┼éa┼éaby podobnie do operatora +=, kt├│ry z kolei dzia┼éa jak operator +.


Podczas pisania obiekt├│w do implementacji programu nie zawsze jest jasne, gdzie powinny by─ç zaimplementowane r├│┼╝ne funkcje. Niekt├│re rzeczy najlepiej jest zrealizowa─ç jako metody obiekt├│w, inne lepiej wyrazi─ç jako osobne funkcje, a jeszcze inne najlepiej jest zaimplementowa─ç poprzez dodanie nowego typu obiektowego. Aby kod by┼é klarowny i dobrze zorganizowany, nale┼╝y stara─ç si─Ö liczb─Ö metod i obowi─ůzk├│w obiekt├│w sprowadzi─ç do minimum. Gdy obiekt wykonuje zbyt wiele zada┼ä, robi si─Ö w nim ba┼éagan, kt├│ry bardzo trudno zrozumie─ç.

Wcze┼Ťniej napisa┼éem, ┼╝e obiekt terrarium b─Ödzie odpowiedzialny za przechowywanie zawarto┼Ťci terrarium i mo┼╝liwo┼Ť─ç ruchu owad├│w. Nale┼╝y podkre┼Ťli─ç s┼éowo mo┼╝liwo┼Ť─ç, kt├│re nie oznacza przymusu. Same owady te┼╝ b─Öd─ů obiektami i w ich gestii b─Ödzie le┼╝e─ç podejmowanie decyzji, co w danym momencie zrobi─ç. Terrarium umo┼╝liwia zaledwie pytanie owad├│w, czy chc─ů co┼Ť zrobi─ç co p├│┼é sekundy i je┼Ťli owad zechce si─Ö poruszy─ç, terrarium zadba o to, aby tak si─Ö sta┼éo.

Przechowywanie siatki, na kt├│rej rozmieszczona jest zawarto┼Ť─ç terrarium mo┼╝e by─ç skomplikowane. Trzeba zdefiniowa─ç jak─ů┼Ť reprezentacj─Ö, sposoby dost─Öpu do tej reprezentacji, spos├│b inicjacji siatki z tablicowego planu, spos├│b zapisania zawarto┼Ťci siatki w ┼éa┼äcuchu za pomoc─ů metody toString oraz ruch owad├│w na siatce. Dobrze by by┼éo przynajmniej cz─Ö┼Ť─ç tych obowi─ůzk├│w przenie┼Ť─ç na inny obiekt, aby obiekt terrarium nie sta┼é si─Ö zbyt rozbudowany.


Zawsze gdy natkniesz si─Ö na problem pomieszania reprezentacji danych i kodu implementacyjnego w jednym obiekcie, dobrym pomys┼éem jest wydzielenie kodu dotycz─ůcego reprezentacji danych do osobnego typu obiektu. W tym przypadku potrzebujemy reprezentacji siatki warto┼Ťci, a wi─Öc napisa┼éem typ o nazwie Grid obs┼éuguj─ůcy operacje wymagane przez terrarium.

Warto┼Ťci na siatce mo┼╝na zapisywa─ç na dwa sposoby: Mo┼╝na u┼╝y─ç tablicy tablic:

var grid = [["0,0", "1,0", "2,0"],
            ["0,1", "1,1", "2,1"]];
show(grid[1][2]);

Ale mo┼╝na te┼╝ wszystkie warto┼Ťci umie┼Ťci─ç w jednej tablicy. W tym przypadku element o wsp├│┼érz─Ödnych x,y mo┼╝na znale┼║─ç pobieraj─ůc element znajduj─ůcy si─Ö w tablicy na pozycji x + y * width, gdzie width to szeroko┼Ť─ç siatki.

var grid = ["0,0", "1,0", "2,0",
            "0,1", "1,1", "2,1"];
show(grid[2 + 1 * 3]);

Zdecydowa┼éem si─Ö na drugie z przedstawionych rozwi─ůza┼ä, poniewa┼╝ o wiele ┼éatwiej jest w nim zainicjowa─ç tablic─Ö. Instrukcja new Array(x) tworzy now─ů tablic─Ö o d┼éugo┼Ťci x, wype┼énion─ů warto┼Ťciami undefined.

function Grid(width, height) {
  this.width = width;
  this.height = height;
  this.cells = new Array(width * height);
}
Grid.prototype.valueAt = function(point) {
  return this.cells[point.y * this.width + point.x];
};
Grid.prototype.setValueAt = function(point, value) {
  this.cells[point.y * this.width + point.x] = value;
};
Grid.prototype.isInside = function(point) {
  return point.x >= 0 && point.y >= 0 &&
         point.x < this.width && point.y < this.height;
};
Grid.prototype.moveValue = function(from, to) {
  this.setValueAt(to, this.valueAt(from));
  this.setValueAt(from, undefined);
};

─ćwiczenie 8.2

B─Ödziemy te┼╝ potrzebowa─ç sposobu na przegl─ůdanie wszystkich element├│w siatki, aby znale┼║─ç owady, kt├│re maj─ů si─Ö poruszy─ç i przekonwertowa─ç ca┼éo┼Ť─ç na ┼éa┼äcuch. Naj┼éatwiej b─Ödzie napisa─ç funkcj─Ö wy┼╝szego rz─Ödu pobieraj─ůc─ů jako argument akcj─Ö. Dodaj metod─Ö each do prototypu Grid, kt├│ra jako argument b─Ödzie pobiera─ç funkcj─Ö dw├│ch argument├│w. Metoda b─Ödzie wywo┼éywa─ç t─Ö funkcj─Ö dla ka┼╝dego punktu na siatce przekazuj─ůc jej jako pierwszy argument obiekt tego punktu, a jako drugi argument — warto┼Ť─ç znajduj─ůc─ů si─Ö w tym punkcie na siatce.

Przegl─ůdanie rozpocznij w punkcie 0,0 i przegl─ůdaj po jednym wierszu, tzn. tak, aby punkt 1,0 zosta┼é odwiedzony wcze┼Ťniej ni┼╝ 0,1. To u┼éatwi p├│┼║niejsze napisanie funkcji toString terrarium. (Podpowied┼║: p─Ötl─Ö for dla wsp├│┼érz─Ödnej x umie┼Ť─ç wewn─ůtrz p─Ötli dla wsp├│┼érz─Ödnej y.)

Lepiej jest nie kombinowa─ç bezpo┼Ťrednio z w┼éasno┼Ťci─ů cells obiektu siatki, tylko zamiast tego do warto┼Ťci dosta─ç si─Ö u┼╝ywaj─ůc valueAt. Dzi─Öki temu, je┼Ťli postanowimy do zapisywania warto┼Ťci u┼╝y─ç innej metody, b─Ödziemy musieli przepisa─ç tylko valueAt i setValueAt, a pozosta┼ée metody pozostawi─ç bez zmian.

Grid.prototype.each = function(action) {
  for (var y = 0; y < this.height; y++) {
    for (var x = 0; x < this.width; x++) {
      var point = new Point(x, y);
      action(point, this.valueAt(point));
    }
  }
};

Przetestujemy siatk─Ö:

var testGrid = new Grid(3, 2);
testGrid.setValueAt(new Point(1, 0), "#");
testGrid.setValueAt(new Point(1, 1), "o");
testGrid.each(function(point, value) {
  print(point.x, ",", point.y, ": ", value);
});

Zanim napiszemy konstruktor Terrarium, musimy skonkretyzowa─ç obiekty owad├│w, kt├│re maj─ů w nim ┼╝y─ç. Wcze┼Ťniej napisa┼éem, ┼╝e terrarium b─Ödzie pyta─ç owady, jak─ů czynno┼Ť─ç chc─ů wykona─ç. B─Ödzie si─Ö to odbywa─ç nast─Öpuj─ůco: ka┼╝dy obiekt owada b─Ödzie mia┼é metod─Ö act zwracaj─ůc─ů „akcj─Ö”. Akcja to obiekt zawieraj─ůcy w┼éasno┼Ť─ç type okre┼Ťlaj─ůc─ů nazw─Ö typu czynno┼Ťci, jak─ů owad chce wykona─ç, np. move (ruch). Wi─Ökszo┼Ť─ç akcji zawiera dodatkowe informacje, takie jak kierunek, w jakim owad chce si─Ö poruszy─ç.

Owady s─ů niezwykle kr├│tkowzroczne, przez co widz─ů tylko kwadraty znajduj─ůce si─Ö w ich bezpo┼Ťrednim s─ůsiedztwie. Ale to wystarczy, aby wykona─ç ruch. Przy wywo┼éywaniu metodzie act b─Ödzie przekazywany obiekt zawieraj─ůcy informacje o otoczeniu okre┼Ťlonego owada. W obiekcie tym b─Ödzie znajdowa─ç si─Ö w┼éasno┼Ť─ç dla ka┼╝dego z o┼Ťmiu kierunk├│w. W┼éasno┼Ť─ç wskazuj─ůca, co znajduje si─Ö powy┼╝ej b─Ödzie mia┼éa nazw─Ö n (od North — p├│┼énoc), w┼éasno┼Ť─ç kierunku w g├│r─Ö i na prawo b─Ödzie si─Ö nazywa┼éa ne (od North-East itd.). Kierunki, do kt├│rych odnosz─ů si─Ö poszczeg├│lne nazwy mo┼╝na znale┼║─ç w poni┼╝szym obiekcie s┼éownikowym:

var directions = new Dictionary(
  {"n":  new Point( 0, -1),
   "ne": new Point( 1, -1),
   "e":  new Point( 1,  0),
   "se": new Point( 1,  1),
   "s":  new Point( 0,  1),
   "sw": new Point(-1,  1),
   "w":  new Point(-1,  0),
   "nw": new Point(-1, -1)});

show(new Point(4, 4).add(directions.lookup("se")));

Gdy owad postanowi si─Ö poruszy─ç, wska┼╝e interesuj─ůcy go kierunek nadaj─ůc powsta┼éemu w wyniku tej decyzji obiektowi akcji w┼éasno┼Ť─ç direction zawieraj─ůc─ů nazw─Ö jednego z kierunk├│w. Mo┼╝emy te┼╝ zrobi─ç g┼éupiego owada, kt├│ry zawsze porusza si─Ö w jednym kierunku — do ┼Ťwiat┼éa:

function StupidBug() {};
StupidBug.prototype.act = function(surroundings) {
  return {type: "move", direction: "s"};
};

Teraz mo┼╝e rozpocz─ů─ç prac─Ö nad obiektem Terrarium. Zaczniemy od konstruktora, kt├│ry b─Ödzie przyjmowa┼é plan (b─Öd─ůcy tablic─ů ┼éa┼äcuch├│w) jako argument i inicjowa┼é jego siatk─Ö.

var wall = {};

function Terrarium(plan) {
  var grid = new Grid(plan[0].length, plan.length);
  for (var y = 0; y < plan.length; y++) {
    var line = plan[y];
    for (var x = 0; x < line.length; x++) {
      grid.setValueAt(new Point(x, y),
                      elementFromCharacter(line.charAt(x)));
    }
  }
  this.grid = grid;
}

function elementFromCharacter(character) {
  if (character == " ")
    return undefined;
  else if (character == "#")
    return wall;
  else if (character == "o")
    return new StupidBug();
}

wall to obiekt s┼éu┼╝─ůcy do oznaczania ┼Ťcian siatki. Jak na ┼Ťcian─Ö przysta┼éo, nic nie robi, tylko stoi w jednym miejscu i nie pozwala przej┼Ť─ç.


Najprostsz─ů metod─ů obiektu jest toString, kt├│ra zamienia terrarium w ┼éa┼äcuch. Aby sobie u┼éatwi─ç, zaznaczymy wall i prototyp owada StupidBug w┼éasno┼Ťci─ů character zawieraj─ůc─ů znak reprezentuj─ůcy owady.

wall.character = "#";
StupidBug.prototype.character = "o";

function characterFromElement(element) {
  if (element == undefined)
    return " ";
  else
    return element.character;
}

show(characterFromElement(wall));

─ćwiczenie 8.3

Teraz do utworzenia łańcucha możemy użyć metody each obiektu Grid. Jednak aby wynik był czytelny, przydałoby się na końcu każdego wiersza dodać znak nowego wiersza. Końce rzędów można znaleźć po współrzędnej x pozycji na siatce. Dodaj do prototypu Terrarium metodę toString, która nie pobiera żadnych argumentów i zwraca łańcuch, który po przekazaniu do funkcji print prezentuje się jako dwuwymiarowy widok terrarium.

Terrarium.prototype.toString = function() {
  var characters = [];
  var endOfLine = this.grid.width - 1;
  this.grid.each(function(point, value) {
    characters.push(characterFromElement(value));
    if (point.x == endOfLine)
      characters.push("n");
  });
  return characters.join("");
};

Wypr├│buj ten kod…

var terrarium = new Terrarium(thePlan);
print(terrarium.toString());

Niewykluczone, ┼╝e pr├│buj─ůc rozwi─ůza─ç powy┼╝sze zadanie pr├│bowa┼ée┼Ť uzyska─ç dost─Öp do this.grid wewn─ůtrz funkcji przekazywanej jako argument do metody each siatki. To si─Ö nie uda. Wywo┼éanie funkcji zawsze powoduje powstanie nowej zmiennej this wewn─ůtrz tej funkcji, nawet je┼Ťli nie jest ona u┼╝ywana jako metoda. Przez to ┼╝adna zmienna this z poza funkcji nie b─Ödzie widoczna.

czasami problem ten mo┼╝na ┼éatwo obej┼Ť─ç zapisuj─ůc potrzebne informacje w zmiennej, np. endOfLine, kt├│ra jest widoczna w funkcji wewn─Ötrznej. Je┼Ťli potrzebujesz dost─Öpu do ca┼éego obiektu this, to jego r├│wnie┼╝ mo┼╝esz zapisa─ç w zmiennej. Zmiennej takiej cz─Östo nadaje si─Ö nazw─Ö self (albo that).

Jednak w dzia┼éaniach tych mo┼╝na si─Ö w ko┼äcu pogubi─ç. Innym dobrym rozwi─ůzaniem jest u┼╝ycie funkcji podobnej do partial z rozdzia┼éu 6. Zamiast dodawa─ç argumenty do funkcji, ta dodaje obiekt this u┼╝ywaj─ůc pierwszego argumentu metody apply funkcji:

function bind(func, object) {
  return function(){
    return func.apply(object, arguments);
  };
}

var testArray = [];
var pushTest = bind(testArray.push, testArray);
pushTest("A");
pushTest("B");
show(testArray);

W ten spos├│b mo┼╝na powi─ůza─ç (bind) wewn─Ötrzn─ů funkcj─Ö z this i b─Ödzie ona mia┼éa t─Ö sam─ů zmienn─ů this, co funkcja zewn─Ötrzna.


─ćwiczenie 8.4

W wyra┼╝eniu bind(testArray.push, testArray) nazwa testArray wyst─Öpuje dwa razy. Potrafisz zaprojektowa─ç funkcj─Ö o nazwie method pozwalaj─ůc─ů powi─ůza─ç obiekt z jedn─ů z jego metod bez podawania nazwy obiektu dwa razy?

Nazw─Ö metody mo┼╝na przekaza─ç jako ┼éa┼äcuch. Dzi─Öki temu funkcja method mo┼╝e sama znale┼║─ç odpowiedni─ů warto┼Ť─ç funkcyjn─ů.

function method(object, name) {
  return function() {
    object[name].apply(object, arguments);
  };
}

var pushTest = method(testArray, "push");

Funkcji bind (lub method) b─Ödziemy potrzebowa─ç przy implementowaniu metody step naszego terrarium. Metoda ta musi przejrze─ç wszystkie owady na siatce, spyta─ç je o zamierzone dzia┼éanie i wykona─ç to dzia┼éanie. Mo┼╝e Ci─Ö kusi─ç, aby przejrze─ç siatk─Ö za pomoc─ů instrukcji each i zrobi─ç, co trzeba z ka┼╝dym napotkanym owadem. Ale w├│wczas, je┼Ťli owad przemie┼Ťci si─Ö na po┼éudnie albo wsch├│d, napotkamy go ponownie i znowu pozwolimy mu wykona─ç ruch.

Dlatego najpierw zbierzemy wszystkie owady do tablicy, a potem je przetworzymy. Poni┼╝sza metoda zbiera owady lub inne byty maj─ůce metod─Ö act i zapisuje je w obiektach zawieraj─ůcych dodatkowe informacje o ich bie┼╝─ůcym po┼éo┼╝eniu:

Terrarium.prototype.listActingCreatures = function() {
  var found = [];
  this.grid.each(function(point, value) {
    if (value != undefined && value.act)
      found.push({object: value, point: point});
  });
  return found;
};

─ćwiczenie 8.5

Prosz─ůc owada, aby wykona┼é jak─ů┼Ť czynno┼Ť─ç musimy mu przekaza─ç obiekt zawieraj─ůcy informacje o jego aktualnym otoczeniu. W obiekcie tym b─Öd─ů znajdowa─ç si─Ö w┼éasno┼Ťci o nazwach odpowiadaj─ůcych nazwom kierunk├│w, o kt├│rych by┼éa mowa wcze┼Ťniej (n, ne itd.). Ka┼╝da w┼éasno┼Ť─ç zawiera ┼éa┼äcuch sk┼éadaj─ůcy si─Ö z jednego znaku, zwr├│cony przez characterFromElement, wskazuj─ůcy co owad widzi w danym kierunku.

Dodaj metod─Ö listSurroundings do prototypu Terrarium. Metoda ta powinna przyjmowa─ç jeden argument b─Öd─ůcy punktem, w kt├│rym aktualnie znajduje si─Ö owad i zwraca─ç obiekt z informacj─ů o otoczeniu tego punktu. Gdy punkt znajduje si─Ö przy kraw─Ödzi siatki, kierunki wykraczaj─ůce poza siatk─Ö oznaczaj znakiem #, aby owad nie pr├│bowa┼é tam si─Ö przemie┼Ťci─ç.

Podpowiedź: Nie wypisuj wszystkich kierunków, tylko zastosuj metodę each na słowniku directions.

Terrarium.prototype.listSurroundings = function(center) {
  var result = {};
  var grid = this.grid;
  directions.each(function(name, direction) {
    var place = center.add(direction);
    if (grid.isInside(place))
      result[name] = characterFromElement(grid.valueAt(place));
    else
      result[name] = "#";
  });
  return result;
};

Zwr├│─ç uwag─Ö na u┼╝ycie zmiennej grid w celu obej┼Ťcia problemu z this.


┼╗adna z powy┼╝szych metod nie wchodzi w sk┼éad zewn─Ötrznego interfejsu obiektu Terrarium — obie s─ů wewn─Ötrznymi szczeg├│┼éami. W niekt├│rych j─Özykach istnieje mo┼╝liwo┼Ť─ç jawnego oznaczenia wybranych metod i w┼éasno┼Ťci jako „prywatnych” i spowodowanie, ┼╝e pr├│ba ich u┼╝ycia poza obiektem zako┼äczy si─Ö b┼é─Ödem. W j─Özyku JavaScript nie jest to mo┼╝liwe, przez co trzeba opisa─ç interfejs za pomoc─ů komentarzy. Czasami pomocne mo┼╝e by─ç zastosowanie jakiego┼Ť specyficznego nazewnictwa, aby odr├│┼╝ni─ç w┼éasno┼Ťci zewn─Ötrzne od wewn─Ötrznych. Mo┼╝na np. nazwom wszystkich metod wewn─Ötrznych doda─ç przedrostek w postaci znaku podkre┼Ťlenia („_”). Dzi─Öki temu ┼éatwiej b─Ödzie zauwa┼╝y─ç wszystkie przypadkowe u┼╝ycia w┼éasno┼Ťci nie nale┼╝─ůcych do interfejsu obiektu.


W nast─Öpnej kolejno┼Ťci zajmiemy si─Ö kolejn─ů metod─ů wewn─Ötrzn─ů, t─ů kt├│ra pyta owada o czynno┼Ť─ç i j─ů wykonuje. Metoda ta przyjmuje jako argument obiekt z w┼éasno┼Ťciami object i point zwr├│cony przez listActingCreatures. Na razie znana jest jej tylko czynno┼Ť─ç move:

Terrarium.prototype.processCreature = function(creature) {
  var surroundings = this.listSurroundings(creature.point);
  var action = creature.object.act(surroundings);
  if (action.type == "move" && directions.contains(action.direction)) {
    var to = creature.point.add(directions.lookup(action.direction));
    if (this.grid.isInside(to) && this.grid.valueAt(to) == undefined)
      this.grid.moveValue(creature.point, to);
  }
  else {
    throw new Error("Nieobs┼éugiwana czynno┼Ť─ç: " + action.type);
  }
};

Zauwa┼╝, ┼╝e metoda ta sprawdza czy wybrany kierunek prowadzi do miejsca w obr─Öbie siatki i czy to miejsce jest wolne. Je┼Ťli nie jest, to je ignoruje. Dzi─Öki temu owad mo┼╝e prosi─ç o dowoln─ů czynno┼Ť─ç. Je┼Ťli jej wykonanie jest niemo┼╝liwe, to po prostu nic si─Ö nie dzieje. Jest to co┼Ť w rodzaju warstwy odizolowuj─ůcej owady od terrarium, kt├│ra pozwala nam troch─Ö zaniedba─ç precyzj─Ö przy pisaniu metod act owad├│w ÔÇĽ np. owad StupidBug zawsze zmierza na po┼éudnie, niezale┼╝nie od tego, czy na jego drodze stoj─ů jakie┼Ť ┼Ťciany.


Te trzy wewn─Ötrzne metody umo┼╝liwi┼éy nam napisanie w ko┼äcu metody step, kt├│ra wszystkim owadom daje szans─Ö na wykonanie jakiej┼Ť czynno┼Ťci (dotyczy to wszystkich element├│w maj─ůcych metod─Ö act ÔÇĽ mogliby┼Ťmy te┼╝ tak─ů metod─Ö zdefiniowa─ç dla obiektu wall, gdyby┼Ťmy chcieli mie─ç ruchome ┼Ťciany).

Terrarium.prototype.step = function() {
  forEach(this.listActingCreatures(),
          bind(this.processCreature, this));
};

Teraz mo┼╝emy utworzy─ç terrarium, aby zobaczy─ç czy owady b─Öd─ů si─Ö w nim porusza─ç…

var terrarium = new Terrarium(thePlan);
print(terrarium);
terrarium.step();
print(terrarium);

Chwileczk─Ö, jak to mo┼╝liwe, ┼╝e powy┼╝sze wywo┼éania print(terrarium) powoduj─ů wy┼Ťwietlenie wyniku naszej metody toString? Funkcja print zamienia swoje argumenty na ┼éa┼äcuchy za pomoc─ů funkcji String. Obiekty zamienia si─Ö w ┼éa┼äcuchy wywo┼éuj─ůc ich metod─Ö toString, a wi─Öc zdefiniowanie metody toString dla w┼éasnych typ├│w obiektowych jest dobrym sposobem na sprawienie, aby by┼éy czytelne po wydrukowaniu.

Point.prototype.toString = function() {
  return "(" + this.x + "," + this.y + ")";
};
print(new Point(5, 5));

Zgodnie z obietnic─ů obiekty Terrarium otrzymaj─ů tak┼╝e metody start i stop do uruchamiania i wy┼é─ůczania symulacji. Do ich budowy u┼╝yjemy dw├│ch funkcji dostarczanych przez przegl─ůdark─Ö: setInterval i clearInterval. Pierwsza z nich przyjmuje dwa argumenty. Pierwszy z nich okre┼Ťla kod (funkcj─Ö albo ┼éa┼äcuch zawieraj─ůcy kod JavaScript), kt├│ry ma by─ç przez t─Ö metod─Ö cyklicznie wywo┼éywany. Natomiast drugi okre┼Ťla liczb─Ö milisekund (1/1000 sekundy) mi─Ödzy wywo┼éaniami. Zwracana jest warto┼Ť─ç, kt├│r─ů mo┼╝ne przekaza─ç do metody clearInterval, aby zatrzyma─ç wykonywanie.

var annoy = setInterval(function() {print("Co?");}, 400);

I…

clearInterval(annoy);

Istniej─ů te┼╝ podobne metody do jednorazowych czynno┼Ťci. Metoda setTimeout powoduje wykonanie funkcji lub ┼éa┼äcucha po up┼éywie okre┼Ťlonej liczby milisekund, a clearTimeout anuluje t─Ö czynno┼Ť─ç.


Terrarium.prototype.start = function() {
  if (!this.running)
    this.running = setInterval(bind(this.step, this), 500);
};

Terrarium.prototype.stop = function() {
  if (this.running) {
    clearInterval(this.running);
    this.running = null;
  }
};

Mamy ju┼╝ terrarium z kilkoma ┼Ťrednio bystrymi owadami i mo┼╝emy je nawet uruchomi─ç. Ale ┼╝eby zobaczy─ç, co si─Ö dzieje, musimy ci─ůgle wywo┼éywa─ç funkcj─Ö print(terrarium). Nie jest to praktyczne. Lepiej by by┼éo, gdyby terrarium by┼éo drukowane automatycznie. Ponadto lepszy efekt uzyskamy, je┼Ťli zamiast drukowa─ç tysi─ůce terrari├│w jedno pod drugim b─Ödziemy aktualizowa─ç jeden wydruk. Je┼Ťli chodzi o drugi z opisanych problem├│w, to w tej ksi─ů┼╝ce dost─Öpna jest pomocnicza funkcja o nazwie inPlacePrinter. Zwraca funkcj─Ö podobn─ů do print, kt├│ra zamiast dodawa─ç wynik do aktualnego wydruku, zast─Öpuje go.

var printHere = inPlacePrinter();
printHere("Teraz widzisz.");
setTimeout(partial(printHere, "A teraz nie."), 1000);

Aby terrarium było ponownie drukowane po każdej zmianie, możemy zmodyfikować metodę step:

Terrarium.prototype.step = function() {
  forEach(this.listActingCreatures(),
          bind(this.processCreature, this));
  if (this.onStep)
    this.onStep();
};

Do terrarium zosta┼éa dodana w┼éasno┼Ť─ç onStep, kt├│ra b─Ödzie wywo┼éywana w ka┼╝dym kroku.

var terrarium = new Terrarium(thePlan);
terrarium.onStep = partial(inPlacePrinter(), terrarium);
terrarium.start();

Zwr├│─ç uwag─Ö na u┼╝ycie funkcji partial ÔÇĽ tworzy miejscow─ů drukark─Ö stosowan─ů do terrarium. Drukarka taka przyjmuje tylko jeden argument, a wi─Öc po jej cz─Ö┼Ťciowym zastosowaniu nie pozostaj─ů ┼╝adne argumenty i staje si─Ö funkcj─ů zera argument├│w. Dok┼éadnie tego potrzeba nam dla w┼éasno┼Ťci onStep.

Pami─Ötaj, ┼╝e terrarium nale┼╝y wy┼é─ůczy─ç gdy nie jest ju┼╝ interesuj─ůce (co powinno nast─ůpi─ç dosy─ç szybko), aby nie zu┼╝ywa┼éo zasob├│w komputera:

terrarium.stop();

Ale komu potrzebne jest terrarium z tylko jednym owadem i to g┼éupim? Na pewno nie mnie. Fajnie by by┼éo, gdyby┼Ťmy mogli doda─ç jeszcze inne rodzaje owad├│w. Na szcz─Ö┼Ťcie jedyne, co w tym celu musimy zrobi─ç, to uog├│lni─ç funkcj─Ö elementFromCharacter. Obecnie zawiera ona trzy przypadki, kt├│re s─ů w niej bezpo┼Ťrednio zakodowane:

function elementFromCharacter(character) {
  if (character == " ")
    return undefined;
  else if (character == "#")
    return wall;
  else if (character == "o")
    return new StupidBug();
}

Dwa pierwszy przypadki mo┼╝e pozostawi─ç, ale trzeci jest o wiele za bardzo specyficzny. Lepszym rozwi─ůzaniem by┼éoby zapisanie znak├│w i odpowiadaj─ůcych im konstruktor├│w owad├│w w s┼éowniku i pobieranie ich stamt─ůd:

var creatureTypes = new Dictionary();
creatureTypes.register = function(constructor) {
  this.store(constructor.prototype.character, constructor);
};

function elementFromCharacter(character) {
  if (character == " ")
    return undefined;
  else if (character == "#")
    return wall;
  else if (creatureTypes.contains(character))
    return new (creatureTypes.lookup(character))();
  else
    throw new Error("Nieznany znak: " + character);
}

Zwr├│─ç uwag─Ö na spos├│b dodania metody register do obiektu creatureTypes ÔÇĽ to, ┼╝e jest to obiekt s┼éownikowy nie znaczy, ┼╝e nie mo┼╝e on obs┼éugiwa─ç dodatkowej metody. Metoda ta znajduje znak zwi─ůzany z konstruktorem i zapisuje go w s┼éowniku. Powinna by─ç wywo┼éywana wy┼é─ůcznie na konstruktorach, kt├│rych prototypy zawieraj─ů w┼éasno┼Ť─ç character.

Teraz metoda elementFromCharacter szuka znaku podanego jej w creatureTypes i zg┼éasza wyj─ůtek je┼Ťli otrzyma nieznany znak.


Poni┼╝ej znajduje si─Ö definicja nowego typu owada i wywo┼éanie rejestruj─ůce jego znak w creatureTypes:

function BouncingBug() {
  this.direction = "ne";
}
BouncingBug.prototype.act = function(surroundings) {
  if (surroundings[this.direction] != " ")
    this.direction = (this.direction == "ne" ? "sw" : "ne");
  return {type: "move", direction: this.direction};
};
BouncingBug.prototype.character = "%";

creatureTypes.register(BouncingBug);

Rozumiesz jak to działa?


─ćwiczenie 8.6

Utw├│rz typ owada o nazwie DrunkBug, kt├│ry w ka┼╝dej kolejce pr├│buje wykona─ç ruch w losowym kierunku nie zwa┼╝aj─ůc na ┼Ťciany. Przypomnij sobie sztuczk─Ö z Math.random z rozdzia┼éu 7.

Aby wybra─ç losowy kierunek, potrzebna nam jest tablica nazw kierunk├│w. Oczywi┼Ťcie mogliby┼Ťmy po prostu napisa─ç ["n", "ne", ...], ale to oznacza┼éoby duplikowanie informacji, a powielanie danych mnie z┼éo┼Ťci. Mogliby┼Ťmy te┼╝ do budowy tej tablicy u┼╝y─ç instrukcji each w obiekcie directions, co by┼éoby ju┼╝ lepszym rozwi─ůzaniem.

Jednak w tym przypadku jest mo┼╝liwo┼Ť─ç zastosowania uog├│lnienia. Mo┼╝liwo┼Ť─ç utworzenia listy nazw w┼éasno┼Ťci znajduj─ůcych si─Ö w s┼éowniku wydaje si─Ö bardzo przydatna, a wi─Öc dodamy takie narz─Ödzie do prototypu Dictionary.

Dictionary.prototype.names = function() {
  var names = [];
  this.each(function(name, value) {names.push(name);});
  return names;
};

show(directions.names());

Neurotyk od razu doda┼éby jeszcze dla r├│wnowagi metod─Ö values zwracaj─ůc─ů list─Ö warto┼Ťci zapisanych w s┼éowniku. My┼Ťl─Ö jednak, ┼╝e to mo┼╝e poczeka─ç, a┼╝ b─Ödzie potrzebne.

Oto spos├│b pobrania losowego elementu z tablicy:

function randomElement(array) {
  if (array.length == 0)
    throw new Error("Tablica jest pusta.");
  return array[Math.floor(Math.random() * array.length)];
}

show(randomElement(["heads", "tails"]));

A to jest owad we własnej osobie:

function DrunkBug() {};
DrunkBug.prototype.act = function(surroundings) {
  return {type: "move",
          direction: randomElement(directions.names())};
};
DrunkBug.prototype.character = "~";

creatureTypes.register(DrunkBug);

Przetestujmy nasze nowe owady:

var newPlan =
  ["############################",
   "#                      #####",
   "#    ##                 ####",
   "#   ####     ~ ~          ##",
   "#    ##       ~            #",
   "#                          #",
   "#                ###       #",
   "#               #####      #",
   "#                ###       #",
   "# %        ###        %    #",
   "#        #######           #",
   "############################"];

var terrarium = new Terrarium(newPlan);
terrarium.onStep = partial(inPlacePrinter(), terrarium);
terrarium.start();

Widzisz, jak teraz pijane owady obijaj─ů si─Ö po ca┼éej scenie? Czysta komedia. Gdy nacieszysz ju┼╝ oko tym fascynuj─ůcym przedstawieniem, wy┼é─ůcz je:

terrarium.stop();

Mamy ju┼╝ dwa rodzaje obiekt├│w zawieraj─ůcych metod─Ö act i w┼éasno┼Ť─ç character. Dzi─Öki temu, ┼╝e maj─ů wsp├│lne te cechy, terrarium mo┼╝e z nimi post─Öpowa─ç w taki sam spos├│b. A to oznacza, ┼╝e mo┼╝emy utworzy─ç dowoln─ů liczb─Ö owad├│w nie zmieniaj─ůc niczego w kodzie terrarium. Technika ta to polimorfizm. Jest to chyba najpot─Ö┼╝niejsze narz─Ödzie programowania obiektowego.

M├│wi─ůc najpro┼Ťciej w polimorfizmie chodzi o to, ┼╝e gdy zostanie napisany modu┼é kodu przystosowany do wsp├│┼épracy z obiektami maj─ůcymi okre┼Ťlony interfejs, to mo┼╝na do niego pod┼é─ůczy─ç obiekt dowolnego typu, kt├│ry ten interfejs obs┼éuguje. Widzieli┼Ťmy ju┼╝ proste przyk┼éady zastosowania tego, np. metod─Ö toString obiekt├│w. Wszystkie obiekty maj─ůce zdefiniowan─ů w sensowny spos├│b metod─Ö toString mo┼╝na przekaza─ç do funkcji print oraz innych funkcji konwertuj─ůcych warto┼Ťci na ┼éa┼äcuchy i zostanie utworzony prawid┼éowy ┼éa┼äcuch bez wzgl─Ödu na to, jak ich metoda toString go zbuduje.

Analogicznie funkcja forEach dzia┼éa zar├│wno na prawdziwych tablicach, jak i pseudotablicach znajduj─ůcych si─Ö w zmiennej arguments, poniewa┼╝ potrzebna jest jej tylko w┼éasno┼Ť─ç length oraz w┼éasno┼Ťci o nazwach 0, 1 itd. element├│w tablicy.


Aby troch─Ö urozmaici─ç ┼╝ycie w terrarium, dodamy do niego poj─Öcia po┼╝ywienia i rozmna┼╝ania. Ka┼╝demu stworzeniu w terrarium dodamy now─ů w┼éasno┼Ť─ç o nazwie energy, kt├│rej warto┼Ť─ç b─Ödzie si─Ö zmniejsza┼éa w wyniku wykonywanych czynno┼Ťci i zwi─Öksza┼éa w wyniku zjadania po┼╝ywienia. Gdy ┼╝yj─ůtko b─Ödzie mia┼éo wystarczaj─ůco du┼╝o energii, b─Ödzie mog┼éo si─Ö rozmno┼╝y─ç2, czyli wygenerowa─ç nowe stworzenie tego samego gatunku.

Je┼Ťli w terrarium b─Öd─ů tylko owady marnuj─ůce energi─Ö na poruszanie si─Ö i zjadanie si─Ö nawzajem, szybko pogr─ů┼╝y si─Ö ono w entropii, sko┼äczy si─Ö energia i zostanie tylko martwa pustynia. Aby temu zapobiec (a przynajmniej, ┼╝eby nie nast─ůpi┼éo to zbyt szybko), dodamy do terrarium porosty. Porosty nie ruszaj─ů si─Ö, a jedynie gromadz─ů energi─Ö dzi─Öki fotosyntezie i rozmna┼╝aj─ů si─Ö.

Aby to dzia┼éa┼éo, potrzebujemy terrarium z inn─ů metod─ů processCreature. Mogliby┼Ťmy zmieni─ç metod─Ö prototypu Terrarium, ale zbytnio przywi─ůzali┼Ťmy si─Ö do symulacji pijanych owad├│w i nie chcieliby┼Ťmy niszczy─ç starego terrarium.

W zwi─ůzku z tym mo┼╝emy utworzy─ç nowy konstruktor, np. o nazwie LifeLikeTerrarium, kt├│rego prototyp b─Ödzie oparty na prototypie Terrarium, ale kt├│ry b─Ödzie mia┼é inn─ů metod─Ö processCreature.


Pomys┼é ten mo┼╝na zrealizowa─ç na kilka sposob├│w. Mo┼╝na przejrze─ç w┼éasno┼Ťci prototypu Terrarium.prototype i doda─ç je jedna po drugiej do prototypu LifeLikeTerrarium.prototype. Wykonanie tego jest ┼éatwe i w niekt├│rych sytuacjach jest to najlepsze rozwi─ůzanie, ale w tym przypadku jest lepszy spos├│b. Je┼Ťli stary obiekt prototypowy uczynimy prototypem nowego obiektu prototypowego (mo┼╝liwe, ┼╝e b─Ödziesz musia┼é kilka razy przeczyta─ç t─Ö cz─Ö┼Ť─ç zdania), to ten nowy obiekt automatycznie otrzyma wszystkie w┼éasno┼Ťci starego.

Niestety w j─Özyku JavaScript nie da si─Ö w ┼éatwy spos├│b utworzy─ç obiektu, kt├│rego prototypem jest wybrany inny obiekt. Mo┼╝na jednak napisa─ç funkcj─Ö, kt├│ra to zrobi. Trzeba tylko zastosowa─ç nast─Öpuj─ůc─ů sztuczk─Ö:

function clone(object) {
  function OneShotConstructor(){}
  OneShotConstructor.prototype = object;
  return new OneShotConstructor();
}

W funkcji tej u┼╝yty jest pusty jednorazowy konstruktor, kt├│rego prototypem jest podany obiekt. Je┼Ťli do tego konstruktora zastosuje si─Ö operator new, utworzy on nowy obiekt na bazie podanego obiektu.

function LifeLikeTerrarium(plan) {
  Terrarium.call(this, plan);
}
LifeLikeTerrarium.prototype = clone(Terrarium.prototype);
LifeLikeTerrarium.prototype.constructor = LifeLikeTerrarium;

Nowy konstruktor nie musi robi─ç czegokolwiek innego ni┼╝ stary, a wi─Öc tylko wywo┼éuje stary konstruktor na obiekcie this. Musimy te┼╝ odtworzy─ç w┼éasno┼Ť─ç constructor w nowym prototypie, bo je┼Ťli tego nie zrobimy, b─Ödzie „twierdzi┼é”, ┼╝e jego konstruktorem jest Terrarium (to oczywi┼Ťcie sprawia┼éoby problem, gdyby┼Ťmy u┼╝ywali tej w┼éasno┼Ťci, a tutaj tego nie robimy).


Teraz mo┼╝na wymieni─ç niekt├│re metody obiektu LifeLikeTerrarium albo doda─ç nowe. Utworzyli┼Ťmy nowy typ obiektu na bazie innego, dzi─Öki czemu unikn─Öli┼Ťmy przepisywania wszystkich metod, kt├│re w Terrarium i LifeLikeTerrarium s─ů takie same. Technika ta nazywa si─Ö dziedziczenie. Nowy typ dziedziczy w┼éasno┼Ťci po starym typie. W wi─Ökszo┼Ťci przypadk├│w nowy typ obs┼éuguje tak┼╝e interfejs starego typu, ale mo┼╝e mie─ç dodatkowo inne metody nie obs┼éugiwane przez stary typ. Dzi─Öki temu obiekt├│w nowego typu mo┼╝na u┼╝ywa─ç wsz─Ödzie tam, gdzie mo┼╝na u┼╝ywa─ç obiekt├│w starego typu. To si─Ö nazywa polimorfizm.

W wi─Ökszo┼Ťci „typowo” obiektowych j─Özyk├│w programowania dziedziczenie jest jednym z fundament├│w i korzystanie z niego jest bardzo ┼éatwe. W JavaScripcie jednak nie ma specjalnego mechanizmu, kt├│ry by to umo┼╝liwia┼é. Z tego te┼╝ powodu programi┼Ťci u┼╝ywaj─ůcy JavaScriptu opracowali wiele w┼éasnych technik realizacji dziedziczenia. Niestety ka┼╝da z nich ma jakie┼Ť wady. Z drugiej strony jest ich tak du┼╝o, ┼╝e zawsze da si─Ö znale┼║─ç odpowiedni─ů, a ponadto mo┼╝na stosowa─ç sztuczki, kt├│re w innych j─Özykach s─ů niemo┼╝liwe.

Na zakończenie rozdziału pokażę Ci kilka innych technik realizacji dziedziczenia oraz opiszę ich wady.


Poni┼╝ej znajduje si─Ö kod nowej metody o nazwie processCreature. Metoda ta jest do┼Ť─ç du┼╝a.

LifeLikeTerrarium.prototype.processCreature = function(creature) {
  var surroundings = this.listSurroundings(creature.point);
  var action = creature.object.act(surroundings);

  var target = undefined;
  var valueAtTarget = undefined;
  if (action.direction && directions.contains(action.direction)) {
    var direction = directions.lookup(action.direction);
    var maybe = creature.point.add(direction);
    if (this.grid.isInside(maybe)) {
      target = maybe;
      valueAtTarget = this.grid.valueAt(target);
    }
  }

  if (action.type == "move") {
    if (target && !valueAtTarget) {
      this.grid.moveValue(creature.point, target);
      creature.point = target;
      creature.object.energy -= 1;
    }
  }
  else if (action.type == "eat") {
    if (valueAtTarget && valueAtTarget.energy) {
      this.grid.setValueAt(target, undefined);
      creature.object.energy += valueAtTarget.energy;
    }
  }
  else if (action.type == "photosynthese") {
    creature.object.energy += 1;
  }
  else if (action.type == "reproduce") {
    if (target && !valueAtTarget) {
      var species = characterFromElement(creature.object);
      var baby = elementFromCharacter(species);
      creature.object.energy -= baby.energy * 2;
      if (creature.object.energy > 0)
        this.grid.setValueAt(target, baby);
    }
  }
  else if (action.type == "wait") {
    creature.object.energy -= 0.2;
  }
  else {
    throw new Error("Nieobs┼éugiwana czynno┼Ť─ç: " + action.type);
  }

  if (creature.object.energy <= 0)
    this.grid.setValueAt(creature.point, undefined);
};

Funkcja nadal rozpoczyna dzia┼éanie od spytania stworzenia, co chce zrobi─ç. Je┼Ťli wybrana czynno┼Ť─ç ma w┼éasno┼Ť─ç direction (kierunek), oblicza kt├│ry punkt na siatce ten kierunek wskazuje i jaka warto┼Ť─ç aktualnie si─Ö w nim znajduje. Informacja ta jest potrzebna trzem z pi─Öcie obs┼éugiwanych akcji i gdyby ka┼╝da z nich obliczenia wykonywa┼éa osobno, kod by┼éby jeszcze bardziej niezgrabny. Je┼Ťli nie ma w┼éasno┼Ťci direction albo jest niepoprawna, zmienne target i valueAtTarget pozostaj─ů niezdefiniowane.

Nast─Öpnie funkcja przechodzi przez akcje. Niekt├│re akcje przed wykonaniem wymagaj─ů dodatkowych test├│w, kt├│re s─ů realizowane w osobnej instrukcji if, dzi─Öki czemu je┼Ťli jakie┼Ť stworzenie spr├│buje np. przej┼Ť─ç przez ┼Ťcian─Ö nie generujemy wyj─ůtku "Nieobs┼éugiwana czynno┼Ť─ç".

Zwr├│─ç uwag─Ö, ┼╝e w akcji reproduce stworzenie b─Öd─ůce rodzicem traci dwa razy tyle energii, co otrzymuje nowonarodzone stworzenie (rodzenie dzieci nie jest ┼éatwe) i potomek pojawia si─Ö na siatce tylko, je┼Ťli rodzic ma wystarczaj─ůco du┼╝o energii.

Gdy czynno┼Ť─ç zostanie wykonana sprawdzamy, czy stworzeniu nie wyczerpa┼éa si─Ö energia. Je┼Ťli tak, stworzenie umiera i usuwamy je.


Porosty nie s─ů skomplikowanymi organizmami. Na planszy b─Öd─ů reprezentowane przez znak *. Upewnij si─Ö, ┼╝e masz zdefiniowan─ů funkcj─Ö randomElement z ─çwiczenia 8.6, poniewa┼╝ b─Ödzie nam tu potrzebna.

function Lichen() {
  this.energy = 5;
}
Lichen.prototype.act = function(surroundings) {
  var emptySpace = findDirections(surroundings, " ");
  if (this.energy >= 13 && emptySpace.length > 0)
    return {type: "reproduce", direction: randomElement(emptySpace)};
  else if (this.energy < 20)
    return {type: "photosynthese"};
  else
    return {type: "wait"};
};
Lichen.prototype.character = "*";

creatureTypes.register(Lichen);

function findDirections(surroundings, wanted) {
  var found = [];
  directions.each(function(name) {
    if (surroundings[name] == wanted)
      found.push(name);
  });
  return found;
}

Maksymalny poziom energii porost├│w wynosi 20. Gdyby mog┼éy rosn─ů─ç wi─Öksze, to tworzy┼éyby gigantyczne skupiska i nie by┼éoby miejsca na rozmna┼╝anie.


─ćwiczenie 8.7

Utw├│rz stworzenie LichenEater (zjadacz porost├│w). Pocz─ůtkowo niech ma 10 jednostek energii i niech zachowuje si─Ö nast─Öpuj─ůco:

  • gdy ma nie mniej ni┼╝ 30 jednostek energii i jest wystarczaj─ůco du┼╝o miejsca, rozmna┼╝a si─Ö.
  • W przeciwnym przypadku, je┼Ťli w pobli┼╝u s─ů jakie┼Ť porosty, niech je losowo zjada.
  • Je┼Ťli nie ma porost├│w w pobli┼╝u, ale jest puste miejsce, niech si─Ö przemieszcza w losowo wybranym kierunku.
  • Je┼Ťli nie ma wolnych miejsc, niech czeka.

Do sprawdzania otoczenia i wybierania kierunku u┼╝yj metod findDirections i randomElement. Stworom tym przypisz liter─Ö c na planszy (jak pac-man).

function LichenEater() {
  this.energy = 10;
}
LichenEater.prototype.act = function(surroundings) {
  var emptySpace = findDirections(surroundings, " ");
  var lichen = findDirections(surroundings, "*");

  if (this.energy >= 30 && emptySpace.length > 0)
    return {type: "reproduce", direction: randomElement(emptySpace)};
  else if (lichen.length > 0)
    return {type: "eat", direction: randomElement(lichen)};
  else if (emptySpace.length > 0)
    return {type: "move", direction: randomElement(emptySpace)};
  else
    return {type: "wait"};
};
LichenEater.prototype.character = "c";

creatureTypes.register(LichenEater);

Wypr├│buj to.

var lichenPlan =
  ["############################",
   "#                     ######",
   "#    ***                **##",
   "#   *##**         **  c  *##",
   "#    ***     c    ##**    *#",
   "#       c         ##***   *#",
   "#                 ##**    *#",
   "#   c       #*            *#",
   "#*          #**       c   *#",
   "#***        ##**    c    **#",
   "#*****     ###***       *###",
   "############################"];

var terrarium = new LifeLikeTerrarium(lichenPlan);
terrarium.onStep = partial(inPlacePrinter(), terrarium);
terrarium.start();

Najprawdopodobniej najpierw porosty szybko rozrosn─ů si─Ö i zajm─ů du┼╝─ů cz─Ö┼Ť─ç terrarium, po czym du┼╝a ilo┼Ť─ç po┼╝ywienia sprawi, ┼╝e zaczn─ů mno┼╝y─ç si─Ö w du┼╝ych ilo┼Ťciach zjadacze porost├│w, kt├│re wyt─Öpi─ů porosty i przy okazji samych siebie. C├│┼╝, taka ju┼╝ jest natura.

terrarium.stop();

┼Ümier─ç wszystkich mieszka┼äc├│w naszego terrarium w ci─ůgu kilku minut nie jest dla nas mi┼éa. Aby temu zapobiec, musimy nauczy─ç zjadaczy porost├│w d┼éugofalowego zarz─ůdzania po┼╝ywieniem. Je┼Ťli b─Öd─ů zjada─ç porosty tylko wtedy, gdy w pobli┼╝u widz─ů przynajmniej dwa krzaki (bez wzgl─Ödu na to jak s─ů g┼éodne), to nigdy nie wyt─Öpi─ů wszystkich porost├│w. Do tego potrzebna jest dyscyplina, ale w ten spos├│b powstanie biotop, kt├│ry nie b─Ödzie niszczy┼é samego siebie. Poni┼╝ej znajduje si─Ö nowy kod metody act ÔÇĽ jedyna zmiana polega na tym, ┼╝e jedzenie jest wykonywane tylko wtedy, gdy w┼éasno┼Ť─ç lichen.length ma warto┼Ť─ç nie mniejsz─ů od dw├│ch.

LichenEater.prototype.act = function(surroundings) {
  var emptySpace = findDirections(surroundings, " ");
  var lichen = findDirections(surroundings, "*");

  if (this.energy >= 30 && emptySpace.length > 0)
    return {type: "reproduce", direction: randomElement(emptySpace)};
  else if (lichen.length > 1)
    return {type: "eat", direction: randomElement(lichen)};
  else if (emptySpace.length > 0)
    return {type: "move", direction: randomElement(emptySpace)};
  else
    return {type: "wait"};
};

Uruchom ponownie terrarium lichenPlan i zobacz, co si─Ö dzieje. Po pewnym czasie zjadacze porost├│w prawdopodobnie wygin─ů, poniewa┼╝ podczas masowego g┼éodu b─Öd─ů porusza─ç si─Ö bezcelowo w prz├│d i w ty┼é, zamiast znajdowa─ç porosty znajduj─ůce si─Ö tu┼╝ obok.


─ćwiczenie 8.8

Zmodyfikuj obiekt LichenEater, aby mia┼é wi─Öksz─ů szans─Ö na przetrwanie. Nie oszukuj, tzn. this.energy += 100 jest niedozwolone. Je┼Ťli od nowa napiszesz konstruktor, nie zapomnij go zarejestrowa─ç w s┼éowniku creatureTypes albo terrarium nadal b─Ödzie u┼╝ywa─ç starego.

Jednym z rozwi─ůza┼ä mo┼╝e by─ç rezygnacja z losowego wybierania kierunk├│w ruchu. gdy kierunki s─ů wybierane losowo, stwory cz─Östo poruszaj─ů si─Ö w t─Ö i z powrotem ostatecznie nigdzie nie docieraj─ůc. Je┼Ťli stworzenie b─Ödzie pami─Öta─ç kierunek ostatniego ruchu i preferowa─ç jego kontynuacj─Ö, zmarnuje mniej czasu i szybciej dotrze do po┼╝ywienia.

function CleverLichenEater() {
  this.energy = 10;
  this.direction = "ne";
}
CleverLichenEater.prototype.act = function(surroundings) {
  var emptySpace = findDirections(surroundings, " ");
  var lichen = findDirections(surroundings, "*");

  if (this.energy >= 30 && emptySpace.length > 0) {
    return {type: "reproduce",
            direction: randomElement(emptySpace)};
  }
  else if (lichen.length > 1) {
    return {type: "eat",
            direction: randomElement(lichen)};
  }
  else if (emptySpace.length > 0) {
    if (surroundings[this.direction] != " ")
      this.direction = randomElement(emptySpace);
    return {type: "move",
            direction: this.direction};
  }
  else {
    return {type: "wait"};
  }
};
CleverLichenEater.prototype.character = "c";

creatureTypes.register(CleverLichenEater);

Wypr├│buj to na poprzedniej planszy terrarium.


─ćwiczenie 8.9

┼üa┼äcuch pokarmowy zawieraj─ůcy tylko jedno ogniwo to wci─ů┼╝ uboga opcja. Czy potrafisz napisa─ç nowego stwora, LichenEaterEater (znak @), kt├│ry aby ┼╝y─ç musi zjada─ç zjadaczy porost├│w? Spr├│buj tak go dopasowa─ç do ekosystemu, aby zbyt szybko nie wymar┼é. Dodaj kilka takich stworze┼ä do tablicy lichenPlan i wypr├│buj je.

Rozwi─ůzanie tego problemu musisz znale┼║─ç sam. Mnie nie uda┼éo si─Ö znale┼║─ç dobrego sposobu na to, aby zapobiec wygini─Öciu tych stworze┼ä natychmiast albo zaraz po wyt─Öpieniu wszystkich zjadaczy porost├│w. Sztuczka ze zjadaniem osobnik├│w tylko wtedy, gdy dwa z nich znajduj─ů si─Ö obok siebie tutaj nie dzia┼éa, poniewa┼╝ osobniki te si─Ö ruszaj─ů i trudno napotka─ç je, gdy s─ů obok siebie. Obiecuj─ůcym rozwi─ůzaniem jest danie zjadaczom zjadaczy du┼╝o energii, dzi─Öki kt├│rej mog─ů przetrwa─ç czas, gdy jest ma┼éo zjadaczy porost├│w oraz powolne rozmna┼╝anie, dzi─Öki czemu zasoby po┼╝ywienia nie s─ů zbyt szybko zu┼╝ywane.

┼╗yciem porost├│w i zjadaczy rz─ůdzi pewien cykl — raz jest du┼╝o porost├│w, co powoduje, ┼╝e rodzi si─Ö du┼╝o zjadaczy, co z kolei powoduje, ┼╝e robi si─Ö ma┼éo porost├│w, z kt├│rego to powodu zjadacze zaczynaj─ů umiera─ç z g┼éodu, co sprawia ┼╝e porosty znowu si─Ö rozrastaj─ů itd. Mo┼╝na te┼╝ spr├│bowa─ç hibernacji zjadaczy zjadaczy porost├│w (przy u┼╝yciu akcji wait) na pewien czas, gdy nie uda im si─Ö znale┼║─ç nic do jedzenia przez kilka kolejek. Dobre efekty mo┼╝e przynie┼Ť─ç wybudzanie stwor├│w z hibernacji po odpowiedniej liczbie kolejek albo gdy wyczuj─ů w pobli┼╝u jedzenie.


Na tym zako┼äczymy prac─Ö nad naszym terrarium. W dalszej cz─Ö┼Ťci rozdzia┼éu bardziej dog┼é─Öbnie zajmiemy si─Ö kwesti─ů dziedziczenia i zwi─ůzanymi z tym problemami w JavaScripcie.


Zaczniemy od odrobiny teorii. Studenci ucz─ůcy si─Ö programowania obiektowego cz─Östo dyskutuj─ů na temat prawid┼éowych i nieprawid┼éowych sposob├│w wykorzystania technik dziedziczenia. Dlatego wa┼╝ne jest, aby zdawa─ç sobie spraw─Ö, ┼╝e dziedziczenie to tak naprawd─Ö tylko sztuczka pozwalaj─ůca leniwym3 programistom unikn─ů─ç pisania cz─Ö┼Ťci kodu. W zwi─ůzku z tym wszelkie dyskusje dotycz─ůce poprawno┼Ťci stosowania dziedziczenia sprowadzaj─ů si─Ö do rozstrzygni─Öcia, czy otrzymany kod dzia┼éa poprawnie i nie zawiera niepotrzebnych powt├│rze┼ä. Z drugiej strony zasady, o kt├│rych toczone s─ů wspomniane dyskusje mog─ů by─ç dobrym wst─Öpem do dziedziczenia.

Dziedziczenie to technika tworzenia nowych typ├│w obiekt├│w, tzw. podtyp├│w, na bazie istniej─ůcych typ├│w, tzw. nadtyp├│w. Podtyp dziedziczy po nadtypie wszystkie w┼éasno┼Ťci i metody, a nast─Öpnie mo┼╝e je modyfikowa─ç i ewentualnie dodawa─ç nowe. Dziedziczenie najlepiej jest stosowa─ç wtedy, gdy obiekt, kt├│rego modelem jest podtyp mo┼╝e by─ç okre┼Ťlony, jako obiekt nadtypu.

Na przyk┼éad typ Fortepian mo┼╝e by─ç podtypem typu Instrument, poniewa┼╝ fortepian jest instrumentem. Poniewa┼╝ fortepian ma szereg klawiszy, niekt├│rych mo┼╝e kusi─ç uczynienie typu Fortepian podtypem typu Array, ale fortepian nie jest rodzajem tablicy i jego implementowanie w ten spos├│b na pewno spowoduje powstanie wielu nonsens├│w. Fortepian ma te┼╝ peda┼éy. Mo┼╝na spyta─ç dlaczego element piano[0] reprezentuje pierwszy klawisz, a nie peda┼é? W tej sytuacji, jako ┼╝e ka┼╝dy fortepian ma klawisze, o wiele lepiej by┼éoby utworzy─ç obiekt maj─ůcy w┼éasno┼Ťci klawisze i pedaly zawieraj─ůce tablice.

Ka┼╝dy podtyp mo┼╝e by─ç nadtypem innego podtypu. Niekt├│re problemy nawet najlepiej si─Ö rozwi─ůzuje poprzez budow─Ö skomplikowanych drzew rodzinnych typ├│w. Nale┼╝y tylko uwa┼╝a─ç, ┼╝eby z tym nie przesadzi─ç. Nadu┼╝ywanie dziedziczenia jest prost─ů drog─ů do zamienienia programu w jeden wielki ba┼éagan.


Spos├│b dzia┼éania s┼éowa kluczowego new i w┼éasno┼Ťci prototype konstruktor├│w narzucaj─ů okre┼Ťlony spos├│b u┼╝ywania obiekt├│w. W przypadku prostych obiekt├│w, jak stworzenia w terrarium jest to wystarczaj─ůce. Je┼Ťli jednak chcemy w programie intensywnie wykorzystywa─ç dziedziczenie, taki spos├│b obs┼éugi obiekt├│w szybko stanie si─Ö niezgrabny. Mo┼╝na sobie u┼éatwi─ç prac─Ö pisz─ůc funkcje do wykonywania niekt├│rych cz─Östo wykonywanych zada┼ä. Na przyk┼éad wielu programist├│w definiuje obiektom metody inherit i method.

Object.prototype.inherit = function(baseConstructor) {
  this.prototype = clone(baseConstructor.prototype);
  this.prototype.constructor = this;
};
Object.prototype.method = function(name, func) {
  this.prototype[name] = func;
};

function StrangeArray(){}
StrangeArray.inherit(Array);
StrangeArray.method("push", function(value) {
  Array.prototype.push.call(this, value);
  Array.prototype.push.call(this, value);
});

var strange = new StrangeArray();
strange.push(4);
show(strange);

Je┼Ťli poszukasz w internecie informacji na tematy „JavaScript” i dziedziczenie (ang. inheritance), to znajdziesz wiele r├│┼╝nych zda┼ä na ten temat, z kt├│rych cz─Ö┼Ť─ç jest o wiele bardziej skomplikowana i sprytna od przedstawionego przeze mnie.

Zwr├│─ç uwag─Ö na spos├│b, w jaki napisana tu metoda push wykorzystuje metod─Ö push z prototypu swojego typu nadrz─Ödnego. Jest to cz─Östo spotykany spos├│b dzia┼éania, je┼Ťli chodzi o dziedziczenie — metoda w podtypie wewn─Ötrznie u┼╝ywa metody nadtypu, ale jako┼Ť go rozszerza.


Najwi─Ökszym problemem z tym prostym podej┼Ťciem jest dualizm mi─Ödzy konstruktorami i prototypami. Konstruktory graj─ů centraln─ů rol─Ö, poniewa┼╝ od nich typ obiektowy wywodzi swoj─ů nazw─Ö, a gdy potrzebny jest prototyp, trzeba pobra─ç w┼éasno┼Ť─ç prototype konstruktora.

To nie tylko wymaga du┼╝o pisania (s┼éowo prototype sk┼éada si─Ö z 9 liter), ale i jest myl─ůce. We wcze┼Ťniejszym przyk┼éadzie musieli┼Ťmy napisa─ç pusty i bezu┼╝yteczny konstruktor dla typu StrangeArray. Sam nie raz omy┼ékowo doda┼éem metody do konstruktora zamiast jego prototypu albo pr├│bowa┼éem wywo┼éa─ç Array.slice, gdy w rzeczywisto┼Ťci chcia┼éem Array.prototype.slice. Moim zdaniem prototyp jest najwa┼╝niejszym aspektem typu obiektowego, a konstruktor jest tylko rozszerzeniem, specjaln─ů metod─ů.


Dodaj─ůc kilka prostych metod pomocniczych do prototypu Object.prototype mo┼╝na utworzy─ç alternatywne podej┼Ťcie do obiekt├│w i dziedziczenia. W podej┼Ťciu tym typ jest reprezentowany przez sw├│j prototyp i do przechowywania prototyp├│w u┼╝ywa si─Ö zmiennych o nazwach pisanych wielkimi literami. Gdy trzeba co┼Ť „skonstruowa─ç”, nale┼╝y u┼╝y─ç metody o nazwie construct. Dodamy metod─Ö o nazwie create do prototypu Object, kt├│ra b─Ödzie u┼╝ywana w miejsce s┼éowa kluczowego new. Metoda ta b─Ödzie klonowa─ç obiekt i wywo┼éywa─ç jego metod─Ö construct, je┼Ťli taka istnieje, przekazuj─ůc jej argumenty, kt├│re zosta┼éy do niej (create) przekazane.

Object.prototype.create = function() {
  var object = clone(this);
  if (typeof object.construct == "function")
    object.construct.apply(object, arguments);
  return object;
};

Dziedziczenie mo┼╝na zrealizowa─ç poprzez sklonowanie obiektu prototypowego i dodanie lub zmodyfikowanie w┼éasno┼Ťci. Do tego r├│wnie┼╝ napiszemy metod─Ö pomocnicz─ů, o nazwie extend, kt├│ra b─Ödzie klonowa─ç obiekt, do kt├│rego zostanie zastosowana i dodawa─ç do klonu w┼éasno┼Ťci obiektu, kt├│ry otrzyma┼éa w argumencie.

Object.prototype.extend = function(properties) {
  var result = clone(this);
  forEachIn(properties, function(name, value) {
    result[name] = value;
  });
  return result;
};

W przypadkach gdy kombinowanie z prototypem Object nie jest bezpieczne zamiast metod można utworzyć zwykłe funkcje.


Na przyk┼éad, je┼Ťli jeste┼Ť do┼Ť─ç stary, to mo┼╝liwe, ┼╝e kiedy┼Ť gra┼ée┼Ť w gr─Ö typu „tekstowa przygoda”, w kt├│rej chodzi si─Ö po ┼Ťwiecie u┼╝ywaj─ůc specjalnych polece┼ä i otrzymuje si─Ö opisy znajduj─ůcych si─Ö w otoczeniu rzeczy oraz wykonywanych dzia┼éa┼ä. Kiedy┼Ť to by┼éy gry!

Poniżej znajduje się przykładowy prototyp przedmiotu w takiej.

var Item = {
  construct: function(name) {
    this.name = name;
  },
  inspect: function() {
    print("To jest ", this.name, ".");
  },
  kick: function() {
    print("klunk!");
  },
  take: function() {
    print("Nie mo┼╝esz podnie┼Ť─ç ", this.name, ".");
  }
};

var lantern = Item.create("br─ůzowa latarnia");
lantern.kick();

A oto spos├│b dziedziczenia po nim…

var DetailedItem = Item.extend({
  construct: function(name, details) {
    Item.construct.call(this, name);
    this.details = details;
  },
  inspect: function() {
    print("Widzisz ", this.name, ", ", this.details, ".");
  }
});

var giantSloth = DetailedItem.create(
  "wielkiego leniwca",
  "wisi sobie na drzewie i ┼╝uje li┼Ťcie");
giantSloth.inspect();

Pozbycie si─Ö obowi─ůzkowej cz─Ö┼Ťci prototype sprawia, ┼╝e wywo┼éania typu Item.construct z konstruktora DetailedItem s─ů nieco prostsze. Zwr├│─ç uwag─Ö, ┼╝e this.name = name w DetailedItem.construct by┼éoby z┼éym pomys┼éem. By┼éoby to zduplikowanie wiersza. Oczywi┼Ťcie powielenie jednego wiersza jest lepsze ni┼╝ wywo┼éanie funkcji Item.construct, ale je┼Ťli p├│┼║niej zechcemy cos doda─ç do tego konstruktora, to b─Ödziemy musieli zrobi─ç to w dw├│ch miejscach.


W wi─Ökszo┼Ťci przypadk├│w konstruktor podtypu powinien zaczyna─ç dzia┼éanie od wywo┼éania konstruktora nadtypu. Dzi─Öki temu prac─Ö rozpoczyna od poprawnego obiektu nadtypu, kt├│ry nast─Öpnie rozszerza. W tym podej┼Ťciu do prototyp├│w typy nie wymagaj─ůce konstruktora mog─ů go opu┼Ťci─ç. Odziedzicz─ů go automatycznie po nadtypie.

var SmallItem = Item.extend({
  kick: function() {
    print(this.name, " fruwa po pokoju.");
  },
  take: function() {
    // (wyobra┼║ sobie tutaj kod wk┼éadaj─ůcy przedmiot do Twojej kieszeni)
    print("Bierzesz ", this.name, ".");
  }
});

var pencil = SmallItem.create("czerwony ołówek");
pencil.take();

Mimo że typ SmallItem nie definiuje własnego konstruktora, można go tworzyć przy użyciu argumentu name, ponieważ dziedziczył konstruktor po prototypie Item.


W j─Özyku JavaScript znajduje si─Ö operator o nazwie instanceof, za pomoc─ů kt├│rego mo┼╝na sprawdzi─ç czy obiekt jest utworzony na bazie okre┼Ťlonego prototypu. Po lewej stronie podaje si─Ö obiekt, a po prawej konstruktor. Zwracana jest warto┼Ť─ç logiczna: true je┼Ťli w┼éasno┼Ť─ç prototypekonstruktora jest bezpo┼Ťrednim lub po┼Ťrednim prototypem obiektu lub false w przeciwnym przypadku.

Je┼Ťli nie s─ů u┼╝ywane zwyk┼ée konstruktory, u┼╝ywanie tego operatora jest troch─Ö niepor─Öczne — jego drugim argumentem powinna by─ç funkcja konstrukcyjna, a my mamy tylko prototypy. Problem ten mo┼╝na rozwi─ůza─ç stosuj─ůc sztuczk─Ö podobn─ů do tej z funkcj─ů clone: Operatorowi instanceof przekazujemy „fa┼észywy” konstruktor.

Object.prototype.hasPrototype = function(prototype) {
  function DummyConstructor() {}
  DummyConstructor.prototype = prototype;
  return this instanceof DummyConstructor;
};

show(pencil.hasPrototype(Item));
show(pencil.hasPrototype(DetailedItem));

Nast─Öpnie chcemy utworzy─ç ma┼éy przedmiot, kt├│ry ma szczeg├│┼éowy opis. Wydaje si─Ö ┼╝e przedmiot ten powinien dziedziczy─ç zar├│wno po DetailedItem jak i SmallItem. W JavaScripcie obiekt nie mo┼╝e mie─ç kilku prototyp├│w, a nawet gdyby m├│g┼é, problem i tak nie by┼éby ┼éatwy do rozwi─ůzania. Na przyk┼éad, gdyby SmallItem z jakiego┼Ť powodu zawiera┼é definicj─Ö metody inspect, kt├│rej metody inspect u┼╝ywa┼éby nowy prototyp?

Derywacja typu obiektu z wi─Öcej ni┼╝ jednego typu nadrz─Ödnego nazywa si─Ö wielodziedziczeniem. W niekt├│rych j─Özykach jest to ca┼ékowicie zabronione, a w innych opracowano skomplikowane zasady, aby to dzia┼éa┼éo i by┼éo praktyczne. W j─Özyku JavaScript mo┼╝na zaimplementowa─ç porz─ůdny schemat wielodziedziczenia. Oczywi┼Ťcie, jak to zwykle bywa, mo┼╝na to zrobi─ç na kilka sposob├│w. Jest to jednak zbyt skomplikowane, aby to tutaj omawia─ç. Dlatego przedstawiam tylko proste rozwi─ůzanie, kt├│re powinno wystarczy─ç w wi─Ökszo┼Ťci przypadk├│w.


Domieszka (ang. mix-in) to specjalny rodzaj prototypu, kt├│ry mo┼╝na „wmiesza─ç” w inne prototypy. W ten spos├│b mo┼╝na potraktowa─ç prototyp SmallItem. Kopiuj─ůc jego metody kick i take do innego prototypu dodamy do niego domieszk─Ö.

function mixInto(object, mixIn) {
  forEachIn(mixIn, function(name, value) {
    object[name] = value;
  });
};

var SmallDetailedItem = clone(DetailedItem);
mixInto(SmallDetailedItem, SmallItem);

var deadMouse = SmallDetailedItem.create(
  "Mysz Fred",
  "on jest martwy");
deadMouse.inspect();
deadMouse.kick();

Pami─Ötaj, ┼╝e forEachIn przegl─ůda tylko w┼éasno┼Ťci nale┼╝─ůce do obiektu, a wi─Öc skopiuje metody kick i take, ale nie skopiuje konstruktora odziedziczonego przez SmallItem po Item.


Mieszanie prototyp├│w staje si─Ö o wiele bardziej skomplikowane, gdy domieszka ma konstruktor lub gdy niekt├│re jej metody koliduj─ů nazwami z metodami prototypu, do kt├│rego s─ů dodawane. Czasami da si─Ö wykona─ç „domieszkowanie r─Öczne”. Powiedzmy, ┼╝e mamy prototyp Monster, kt├│ry ma sw├│j w┼éasny konstruktor, i chcemy go zmiesza─ç z DetailedItem.

var Monster = Item.extend({
  construct: function(name, dangerous) {
    Item.construct.call(this, name);
    this.dangerous = dangerous;
  },
  kick: function() {
    if (this.dangerous)
      print(this.name, " odgryza Ci głowę.");
    else
      print(this.name, " ucieka, szlochaj─ůc.");
  }
});

var DetailedMonster = DetailedItem.extend({
  construct: function(name, description, dangerous) {
    DetailedItem.construct.call(this, name, description);
    Monster.construct.call(this, name, dangerous);
  },
  kick: Monster.kick
});

var giantSloth = DetailedMonster.create(

  "Wielki leniwiec",
  "wisi sobie na drzewie i ┼╝uje li┼Ťcie",
  true);
giantSloth.kick();

Zauwa┼╝ jednak, ┼╝e konstruktor Item przy tworzeniu DetailedMonster jest wywo┼éywany dwukrotnie ÔÇĽ raz poprzez konstruktor DetailedItem, a drugi raz poprzez konstruktor Monster. W tym przypadku nie powoduje to wielkich szk├│d, ale w innych mo┼╝e by─ç powa┼╝nym problemem.


Mimo tych komplikacji nie zniech─Öcaj si─Ö do dziedziczenia. Wielodziedziczenie, mimo ┼╝e czasami bardzo przydatne, w wi─Ökszo┼Ťci przypadk├│w mo┼╝na sobie darowa─ç. Dlatego w┼éa┼Ťnie w takich j─Özykach jak Java jest ono zabronione. A je┼Ťli kiedy┼Ť b─Ödziesz go naprawd─Ö potrzebowa─ç, mo┼╝esz poszuka─ç informacji w internecie, zrobi─ç rozeznanie i znale┼║─ç rozwi─ůzanie idealne dla siebie.

Tak mi teraz przysz┼éo do g┼éowy, ┼╝e JavaScript by┼éby doskona┼éym j─Özykiem do napisania tekstowej gry przygodowej. Bardzo w tym pomaga mo┼╝liwo┼Ť─ç zmieniania zachowa┼ä obiekt├│w, kt├│r─ů mamy dzi─Öki prototypowemu dziedziczeniu. Je┼Ťli masz obiekt hedgehog, kt├│ry ma niezwyk┼é─ů zdolno┼Ť─ç toczenia si─Ö, gdy zostanie kopni─Öty, mo┼╝esz tylko zmieni─ç jego metod─Ö kick.

Niestety tekstowe przygod├│wki podzieli┼éy losy p┼éyt winylowych i mimo ┼╝e kiedy┼Ť by┼éy bardzo popularne, dzi┼Ť gra w nie tylko garstka zapale┼äc├│w.

Przypisy

  1. Takie typy w innych j─Özykach zazwyczaj nazywaj─ů si─Ö klasami.
  2. Dla uproszczenia stworzenia w naszym terrarium rozmna┼╝aj─ů si─Ö bezp┼éciowo, same z siebie.
  3. Lenistwo w przypadku programist├│w niekoniecznie oznacza co┼Ť z┼éego. Osoby, kt├│re lubi─ů wielokrotnie powtarza─ç te same czynno┼Ťci s─ů dobrymi robotnikami przy ta┼Ťmach monta┼╝owych i s┼éabymi programistami.

Autor: Marijn Haverbeke

Źródło: http://eloquentjavascript.net/1st_edition/chapter8.html

Tłumaczenie: Łukasz Piwko

Tre┼Ť─ç tej strony jest dost─Öpna na zasadach licencji CC BY 3.0

1 komentarz do “Rozdzia┼é 8. Programowanie obiektowe”

  1. Obszerny materiał o_O
    chyba z tydzień zajmie mi przerabianie tego rozdziału

    ale motywacja jest, wi─Öc do boju… ­čśÇ

Mo┼╝liwo┼Ť─ç komentowania zosta┼éa wy┼é─ůczona.