RSS

Klasy w JavaScript ES5 i ES6 — bitwa

Liczba odsłon: 174

Język Java­Script zna­ny jest z te­go, że nie po­ja­wia się w nim, przy­naj­mniej wprost, po­ję­cie kla­sy. Obiekty two­rzy się przez pro­to­ty­po­wa­nie, a każ­dy z nich mo­że być roz­bu­do­wy­wa­ny o do­dat­ko­we po­la, wła­ści­wo­ści i me­to­dy. Mimo to, pro­gra­miś­ci lu­bią ope­ro­wać po­ję­ciem kla­sy i wie­le osnów pro­gra­mis­tycz­nych Java­Script ba­zu­je na włas­nych me­cha­niz­mach sy­mu­lu­ją­cych ist­nie­nie klas. Nawet naj­now­sze wcie­le­nia ję­zy­ka, ta­kie jak ECMAScript 2015, wpro­wa­dza­ją sło­wo klu­czo­we class i po­zwa­la­ją pro­gra­mis­tom za­pi­sy­wać kla­sy tak sa­mo, jak w ję­zy­kach Java lub C++.

Nie ozna­cza to jed­nak, że w „czys­tym” dia­lek­cie Java­Script nie moż­na wy­ko­rzys­ty­wać klas. W po­niż­szym tekś­cie znaj­dzie­cie li­stę wszyst­kich przy­dat­nych wy­ra­żeń poz­wa­la­ją­cych two­rzyć pro­to­ty­py obiek­tów za­cho­wu­ją­ce się jak kla­sy, a tak­że ich po­rów­na­nie z no­wy­mi ele­men­ta­mi skład­ni ES6.

Język Java­Script (ES5) nie na­le­ży do naj­prost­szych, a pro­gra­mo­wa­nie obiek­to­we za je­go po­mo­cą mo­że być do­brym ćwi­cze­niem ner­wów. Jeżeli jed­nak two­rzo­ne opro­gra­mo­wa­nie ma być po­praw­nie rea­li­zo­wa­ne przez wszyst­kie obec­ne na ryn­ku prze­glą­dar­ki WWW i mu­si się cha­rak­te­ry­zo­wać wy­so­ką wy­daj­noś­cią, uży­cie obec­ne­go od lat dia­lek­tu mo­że być naj­lep­szym roz­wią­za­niem. Szczególnie, że wraz ze wzros­tem świa­do­moś­ci użyt­kow­ni­ków w za­kre­sie wy­daj­noś­ci apli­kac­ji sie­cio­wych, a tak­że pow­szech­no­ści urzą­dzeń mo­bil­nych dys­po­nu­ją­cych ogra­ni­czo­ny­mi za­so­ba­mi sprzę­to­wy­mi i po­wol­ny­mi łą­cza­mi sie­cio­wy­mi, ro­śnie zna­cze­nie naj­prost­szych osnów pro­gra­mis­tycz­nych, poz­wa­la­ją­cych two­rzyć naj­mniej­sze i naj­szyb­sze apli­kac­je. Unaocznie­niem te­go, w dość hu­mo­ry­stycz­ny zresz­tą spo­sób, jest osno­wa pro­gra­mi­stycz­na Vanilla JS.

Nawet je­że­li po­dej­mie się de­cy­zję o uży­ciu ję­zy­ka ECMAScript 2015 (ES6), zaw­sze moż­na użyć trans­la­to­ra ta­kie­go jak Babel, by prze­kształ­cić tekst źród­ło­wy do po­praw­nej po­sta­ci Java­Script. Powoduje to jed­nak ko­niecz­ność do­da­nia ko­lej­ne­go kro­ku bu­do­wa­nia pro­jek­tu, uza­leż­nia pro­jekt od po­praw­noś­ci dzia­ła­nia na­rzę­dzia i po­wo­du­je po­wsta­wa­nie ko­du, któ­ry mo­że być trud­ny w diag­no­zo­wa­niu i po­pra­wia­niu.

Skoro za­tem ES5 ofe­ru­je te sa­me moż­li­woś­ci, tyl­ko za po­mo­cą w nie­co bar­dziej za­gmat­wa­nej skład­ni, mo­że war­to na­uczyć się tej skład­ni i pi­sać bez­po­śred­nio w prost­szym, lecz po­wszech­nie uży­wa­nym ję­zy­ku? Decyzję po­zo­sta­wiam Tobie, na­to­miast by ją ułat­wić, po­ni­żej pre­zen­tu­ję po­rów­na­nie naj­waż­niej­szych ele­men­tów oby­dwu dia­lek­tów zwią­za­nych z de­fi­nio­wa­niem klas oraz two­rze­niem i wy­ko­rzys­ty­wa­niem obiek­tów.

Definio­wa­nie kla­sy

Zarówno w ES5, jak i w ES6, funk­cja jest jed­no­cześ­nie obiek­tem. Aby za­tem zde­fi­nio­wać kla­sę w ES5, de­fi­niu­je­my funk­cję. W ES6 słu­ży do te­go wy­dzie­lo­ne sło­wo klu­czo­we class, jest ono jed­nak je­dy­nie sło­dzi­kiem skład­nio­wym.

JavaScript ECMAScript 2015
function Circle() {
}
class Circle {
}

O rów­no­waż­noś­ci po­wyż­szych za­pi­sów moż­na się prze­ko­nać za­pi­su­jąc po­wyż­szą kla­sę Circle, two­rząc jej in­stan­cję za po­mo­cą ope­ra­to­ra new i oglą­da­jąc za­war­tość stwo­rzo­ne­go obiek­tu. Pole con­struc­tor pro­to­ty­pu obiek­tu bę­dzie wska­zy­wa­ło na funk­cję Circle().

Korzysta­nie ze sło­wa klu­czo­we­go class ma jed­nak do­dat­ko­we skut­ki ubocz­ne. Pierwszą róż­ni­cą jest uwzględ­nia­nie ko­lej­noś­ci w ko­dzie. Teoretycz­nie w ko­dzie Java­Script wy­wo­ła­nia funk­cji mo­gą na­stę­po­wać przed jej zde­fi­nio­wa­niem (choć nie jest to do­bra prak­ty­ka). W przy­pad­ku klas ES6 de­fi­ni­cja kla­sy mu­si wy­stą­pić przed pierw­szym jej uży­ciem. Drugą róż­ni­cą jest wy­mu­sza­nie try­bu strict w treś­ci kla­sy, co mo­że skut­ko­wać wyż­szą wy­daj­noś­cią i mniej­szą licz­bą błę­dów (try­bu te­go moż­na jed­nak użyć rów­nież w przy­pad­ku ko­du ES5).

Definio­wa­nie kon­struk­to­ra

W dia­lek­cie ES5 funk­cja two­rzą­ca no­we obiek­ty (sta­no­wią­ca w efek­cie de fac­to de­fi­ni­cję kla­sy) peł­ni funk­cję kon­struk­to­ra i mo­że przyj­mo­wać do­wol­ne ar­gu­men­ty, któ­re na­stęp­nie moż­na wy­ko­rzys­tać przy kon­fi­gu­ro­wa­niu no­we­go obiek­tu. W ES6 kon­struk­tor zo­stał wy­dzie­lo­ny, jed­nak efekt jest zde­fi­nio­wa­nia jest do­kład­nie ta­ki sam.

JavaScript ECMAScript 2015
function Circle(r) {
   this.r = r;
}
class Circle {

   constructor(r) {
      this.r = r
   }

}

Definio­wa­nie me­tod

Aby w dia­lek­cie ES5 zde­fi­nio­wać me­to­dę, któ­ra bę­dzie auto­ma­tycz­nie do­stęp­na we wszyst­kich obiek­tach two­rzo­nych przez funk­cję sta­no­wią­cą kon­struk­tor „kla­sy”, na­le­ży do­dać ją ja­ko po­le pro­to­ty­pu tej funk­cji. W przy­pad­ku, gdy obiekt nie za­wie­ra da­ne­go ele­men­tu, na­stę­pu­je od­wo­ła­nie do pro­to­ty­pu bez zmia­ny re­fe­ren­cji this.

W przy­pad­ku ES6 ten sam efekt uzys­ku­je się do­da­jąc treść me­to­dy do de­fi­ni­cji kla­sy. Taki za­pis jest zde­cy­do­wa­nie krót­szy i mniej po­dat­ny na błę­dy.

JavaScript ECMAScript 2015
Circle.prototype.toString = function() {
   return 'r = ' + this.r;
}
class Circle {
   …
   toString() {
      return 'r = ' + this.r
   }
   …
}

Definio­wa­nie me­tod sta­tycz­nych

Metody sta­tycz­ne mo­gą być wy­wo­ły­wa­ne bez ko­niecz­noś­ci two­rze­nia in­stan­cji obiek­tów i sta­no­wią spo­sób na udos­tęp­nia­nie funkcjo­nal­noś­ci nie­za­leż­nej od sta­nu obiek­tu lub bar­dziej elas­tycz­ne bu­do­wa­nie obiek­tów da­nej kla­sy.

W przy­pad­ku dia­lek­tu ES5 me­to­dę sta­tycz­ną two­rzy się przez za­pi­sa­nie jej ja­ko po­la obiek­tu funk­cji peł­nią­cej ro­lę kon­struk­to­ra kla­sy. W dia­lek­cie ES6 za­pi­su­je się ją bez­po­śred­nio w kla­sie, do­da­jąc sło­wo klu­czo­we sta­tic.

JavaScript ECMAScript 2015
Circle.unit = function() {
   return new Circle(1);
}
class Circle {
   …
   static unit() {
      return new Circle(1)
   }
   …
}

Definio­wa­nie pól

W przy­pad­ku oby­dwu dia­lek­tów nie ma mo­wy o de­fi­nio­wa­niu li­sty pól obiek­tu. Każdy obiekt po­cząt­ko­wo po­sia­da po­la stwo­rzo­ne przez kon­struk­tor kla­sy. Lista pól mo­że być na­stęp­nie mo­dy­fi­ko­wa­na po­przez do­da­wa­nie no­wych pól oraz usu­wa­nie ist­nie­ją­cych za po­mo­cą ope­ra­to­ra de­lete (o ile po­le nie zo­sta­ło zde­fi­nio­wa­ne ja­ko nie­usu­wal­ne).

Przyszłe wer­sje stan­dar­du ECMAScript wpro­wa­dzą moż­li­wość de­kla­ro­wa­nia pól we­wnątrz de­fi­ni­cji klas oraz po­da­wa­nia ich do­myśl­nych war­toś­ci. Pozwoli to uproś­cić kon­struk­to­ry nie­któ­rych klas (lub usu­nąć po­trze­bę ich sto­so­wa­nia) oraz zwięk­szy czy­tel­ność klas (sa­mo­do­ku­men­to­wa­nie się ko­du).

Definio­wa­nie wła­ści­wo­ści

Właściwoś­ci (ang. pro­per­ties) to w pro­gra­mo­wa­niu obiek­to­wym po­la, któ­rych war­tość jest do­stęp­na za po­śred­nic­twem ak­ce­so­ra od­czy­tu­ją­ce­go (ang. getter), a zmie­nia­na za po­mo­cą ak­ce­so­ra za­pi­su­ją­ce­go (ang. setter). Dzięki te­mu moż­na za­bez­pie­czyć po­le przed wpro­wa­dza­niem war­toś­ci spo­za do­me­ny, a tak­że stwo­rzyć po­la tyl­ko do od­czy­tu lub wręcz wy­li­cza­ne na pod­sta­wie in­nych in­for­mac­ji.

W dia­lek­cie ES5 ta­kie po­la (wła­ści­wo­ści) de­fi­niu­je się za po­mo­cą wy­wo­ła­nia Object.defineProperty(). W przy­pad­ku ES6 skład­nia jest prost­sza, gdyż od­po­wied­nie akce­so­ry za­pi­su­je się bez­po­śred­nio w de­fi­ni­cji kla­sy, na po­zio­mie zwyk­łych me­tod. W oby­dwu przy­pad­kach moż­li­we jest też two­rze­nie wła­ści­wo­ści sta­tycz­nych, to zna­czy po­wią­za­nych z sa­mą kla­są, a nie jej in­stan­cją (obiek­tem). W ES5 ro­bi się to de­fi­niu­jąc po­le przy­pi­sa­ne do obiek­tu funk­cji (a nie jej pro­to­ty­pu), w ES6 — do­da­jąc sło­wo klu­czo­we sta­tic do de­fi­ni­cji da­ne­go ak­ce­so­ra.

JavaScript ECMAScript 2015
Object.defineProperty(Circle.prototype, 'radius', {
   get() {
      return this.r;
   },
   set(newRadius) {
      if (newRadius < 0)
         throw new RangeError('Negative radius');
      this.r = newRadius;
   }
});

Object.defineProperty(Circle.prototype, 'circumference', {
   get() {
      return 2 * Math.PI * this.r;
   }
});
class Circle {
   …
   get radius() {
      return this.r
   }

   set radius(newRadius) {
      if (newRadius < 0)
         throw new RangeError('Negative radius')
      this.r = newRadius
   }

   get circumference() {
      return 2 * Math.PI * this.r
   }
   …
}

Dziedzicze­nie

W oby­dwu przy­pad­kach moż­li­we jest stwo­rze­nie funk­cji two­rzą­cej obiekt kla­sy po­chod­nej wo­bec in­nej, to zna­czy za­cho­wu­ją­cej wszyst­kie po­la i me­to­dy pro­to­ty­pu. W przy­pad­ku dia­lek­tu ES5 wy­ma­ga to po­wie­le­nia pro­to­ty­pu funk­cji oraz ręcz­ne­go wy­wo­ła­nia kon­struk­to­ra kla­sy ba­zo­wej z właś­ci­wym kon­tek­stem. W przy­pad­ku ES6 wy­star­czy użyć sło­wa klu­czo­we­go ex­tends. Wywołania kon­struk­to­ra kla­sy ba­zo­wej moż­na po­mi­nąć (zo­sta­nie wy­ko­na­ne przez do­mysł), je­że­li jed­nak kon­struk­tor ma wy­ko­nać ja­kieś do­dat­ko­we dzia­ła­nia, moż­na w nim od­wo­łać się po­przez su­per do wer­sji z kla­sy ba­zo­wej.

JavaScript ECMAScript 2015
function Wheel(r) {
   Circle.call(this, r);
}

Wheel.prototype = Object.create(Circle.prototype);

Object.defineProperty(Wheel.prototype, 'area', {
   get() {
      return Math.PI * this.r * this.r;
   }
});

Wheel.prototype.toString = function() {
   return 'Wheel(' + Circle.prototype
         .toString.call(this) + ')';
}
class Wheel extends Circle {

   get area() {
      return Math.PI * this.r * this.r
   }

   toString() {
      return 'Wheel(' + super.toString() + ')'
   }

}

Pod­su­mo­wa­nie

Dialekty ES5 i ES6 ma­ją w za­kre­sie de­fi­nio­wa­nia klas i two­rze­nia obiek­tów ta­kie sa­me moż­li­woś­ci i róż­nią się je­dy­nie skład­nią. Zapis ES6 jest zde­cy­do­wa­nie bar­dziej zwięz­ły, za­wie­ra mniej po­wtó­rzeń, le­piej za­cho­wu­je się w cza­sie re­fak­to­ry­za­cji i po­zo­sta­wia mniej miej­sca na błę­dy. Kod za­pi­sa­ny z je­go wy­ko­rzys­ta­niem mu­si być jed­nak uru­cha­mia­ny przez prze­glą­dar­kę WWW zgod­ną z ECMAScript 2015 lub wcześ­niej prze­two­rzo­ny za po­mo­cą trans­la­to­ra. Sprawia to, że mu­si­my al­bo zre­zyg­no­wać ze zgod­no­ści ze star­szy­mi prze­glą­dar­ka­mi (na co mo­że nie zgo­dzić się klient), al­bo skom­pli­ko­wać pro­ces bu­do­wa­nia apli­kac­ji.

Mam na­dzie­ję, że po­wyż­szy tekst po­słu­ży pro­gra­mis­tom Java­Script za „ścią­gaw­kę” przy­po­mi­na­ją­cą w ja­ki spo­sób moż­na ko­rzy­stać z obiek­to­wych moż­li­woś­ci te­go ję­zy­ka, a tym, któ­rzy chcą – lub zo­sta­li zmu­sze­ni przez wy­ma­ga­nia pro­jek­tu – ko­rzy­stać nie z ele­ganc­kie­go dia­lek­tu ES6, lecz tra­dy­cyj­ne­go ES5, po­ka­że, że róż­ni­ce nie są du­że, a po odro­bi­nie prak­ty­ki moż­na uży­wać ES5 w spo­sób płyn­ny i na­tu­ral­ny.