Javascript pakk No. 1 – ECMAScript 6
11 min readA frontend fejlesztők élete nem csak játék és mese. Nem elég hogy a javascript prototype object modelje sokakban a hányingerre kisértetiesen emlékeztető érzéseket kelt, mindezt megfejelik aszinkron funkcionalitással és callback hegyekkel, a dinamikus típusosságról nem is beszélve.
Persze a nyelv fejlesztői mindezzel tökéletesen tisztában vannak, ezért kifejlesztették egymás közt a csuklás legjobb gyógymódját, az ECMAScript 6-os szabványt!
Ez sok újdonságot hoz a nyelvbe, viszont a böngészők egy része még nem támogatja vagy nem teljesen, viszont van rá mód, hogy azok számára is emészthetővé tegyük. A későbbiekben erről is írok.
Menjünk hát végig, hogy miben változik a szabvány az eddigiekhez képest!
Konstansok
A nyelv eddig nem támogatta a konstansokat, vagyis olyan "változókat", amiknek nem lehet megváltoztatni a tartalmát a definiálást követően.
Megjegyzés: A változó maga nem változhat, viszont az ahhoz rendelt tartalom igen. Tehát egy objectre mutató pointer ugyanarra az objectre fog mutatni, viszont az objektum maga változhat.
// ECMAScript 6 const PI = 3.141593
// ES5-ben az object helperekkel lehetett megvalósítani // és azt is csak global scope-ba Object.defineProperty(typeof global === "object" ? global : window, "PI", { value: 3.141593, enumerable: true, writable: false, configurable: false });
Scope-ok
A nyelvben eddig nem volt lehetőség ún. block-scoped változók deklarálására. Két opció volt eddig, mikor globális változót hoztunk létre, sima értékadással:
pi = 3.14; // globális, a definiálás helyétől függetlenül
A másik opció, mikor a var kulccsó használatával lokális változót hozunk létre.
function scoping() { var teszt = 5; // a létrehozó function-ön belül elérhető console.log(teszt); // 5 } function scoping2() { for (var x = 0; x < 10; x++) { var teszt2 = 36; } console.log(teszt2); // 36 még itt is elérhető, hiszen a létrehozó function ugyanaz } console.log(teszt); // undefined console.log(teszt2); // undefined
Let it be!
Na és akkor jöjjön az újdonság az ES6 oldalról. A let kulcsszó segítségével ún. block scoped változókat tudunk létrehozni, amik nem lesznek az egész tartalmazó function-ön belül elérhetőek (ahogy azt minden normális nyelvben is lehet):
function teszt() { let teszt = 36 for (var x = 0; x < 10; x++) { let teszt = 5; // csak az adott blokkon belül (jelen esetben a for ciklus) érhető el } console.log(teszt); // 36 }
Na most akkor ismét idézném a kedves orosz kollégát.. How cool is that?
.NET betyárok, szevasztok!
Aki foglalkozott már valaha C#-al, az már bizonyára belefutott az ún. lambda kifejezésekbe. Hasonló (de működését tekintve más) szintax érkezett most az ES6-al. Akinek új: röviden egy egyszerűbb és átláthatóbb szintaxis a closure-ök létrehozására:
// ES5 módi valamilyen.metodus(function(x) { return x + 1; }); // ez eddig is ment, nincs ebben semmi új, igaz? valamilyen.metodus(function(x,y) { return x + y;}); // ez se új
// ES6 valamilyen.metodus(x => x + 1); // he? várjunk csak.. ez annyira nem bonyolult.. sőt, egész jó, nem? // akkor most bonyolítsuk kicsit valamilyen.metodus((x,y) => x + y); // hoppá, megy ez több paraméterrel is? Hol volt az az orosz idézet?
Nézzük meg mindezt pl egy forEach-nek átadott functionben!
// (Good) Plain old ES5 tomb.forEach(function(v) { if (v % 5 === 0) { fives.push(v); } }); // ES6 style tomb.forEach(v => { if (v % 5 === 0) { fives.push(v); } });
THIS, you are here!
Emlékeztek még arra, amikor javascriptben nem kellett újraassign-olni az aktuális objektumra mutató pointer (this) értékét egy lokális változóba, vagy éppen bindolni azt (ES 5.1 után) a meghívott függvényben? Nem? Én se. Viszont ezeknek az időknek vége! Mostantól a this, az ahogy nevéből is adódik. Ez lesz. Nem pedig valami más.
Maradjunk az előbbi forEach példánál:
// ES5 style var self = this; this.nums.forEach(function(v) { if (v % 5 === 0) { self.fives.push(v); } }) // ES5.1+ style this.nums.forEach(function(v) { if (v % 5 === 0) { this.fives.push(v); } }.bind(this)); // itt bekötjük a this context-et a functionbe // ES6 style this.nums.forEach(function(v) { if (v % 5 === 0) { this.fives.push(v); // se this újraassign-olás, se bind, csak gyönyörű haj! } });
Default parameter value
Akik PHP-vel foglalatoskodnak, azoknak nem lesz újdonság, hogy ún. alapértelmezett értékekkel adjuk át a függvényeinknek a paramétereket. Tehát ha az adott paraméter nem kerül átadásra, akkor is hozzárendel valami értéket. Persze jól szituált hákolással ez is megvalósítható volt eddig, nézzük hogy is zajlott mindez:
function f (x, y, z) { if (y === undefined) y = 7; if (z === undefined) z = 42; return x + y + z; }; f(1) === 50; // jóféle hákolás, mi?
Akkor nézzük meg mennyivel egyszerűsödik le az életünk most az ES6-al:
function f (x, y = 7, z = 42) { return x + y + z } f(1) === 50
Hát komolyan, szóhoz se lehet jutni, már kezd olyan lenni az egész, mintha valami programnyelv lenne, nem? De a java még hátravan!
Rest parameter
A napfényes polimorfizmus egyik formája az ún. method overload. Sajnos ezen nyelvben erre nincs lehetőség olyan formában, mint pl. Javaban vagy C#-ben, viszont amit pluszban odapasszolunk a függvényünknek, azt be tudjuk csomagolni egy tömbbe:
// ES5 módi function f (x, y) { var a = Array.prototype.slice.call(arguments, 2); // fogjuk és levágjuk az első két elemét az átadott paraméterek alkotta tömbnek return (x + y) * a.length; }; f(1, 2, "hello", true, 7) === 9;
Akkor nézzük mennyivel közelebb áll ez a világunkhoz az ES6:
function f (x, y, ...a) { // a ...a jelenti az összes többi argumentumot tömbbé alakítva, amiket esetleg megkap a függvényünk return (x + y) * a.length } f(1, 2, "hello", true, 7) === 9
Ha már ennyire szétbontunk mindent tömbökre, akkor nézzük hol lehet még a spreading syntaxot használni?
// ES5 style var params = [ "hello", true, 7 ]; // alap tömbünk var other = [ 1, 2 ].concat(params); // [ 1, 2, "hello", true, 7 ] // régen ezt csak concattal lehetett beleoktrojálni a másikba f.apply(undefined, [ 1, 2 ].concat(params)) === 9; // az előző függvényünket használva kipróbáljuk azt var str = "foo"; var chars = str.split(""); // [ "f", "o", "o" ] // stringet csak splittel tudunk az egyes karakterek alkotta tömbbé alakítani // ES6 var params = [ "hello", true, 7 ] var other = [ 1, 2, ...params ] // [ 1, 2, "hello", true, 7 ] // spreadelve adjuk át az elemeket, mintha concat lenne f(1, 2, ...params) === 9 var str = "foo" var chars = [ ...str ] // [ "f", "o", "o" ] spread a stringet is :O
Template literals (template strings)
PHP-ben már korábban is jelen volt (és egyes esetekben okozhatott meglepetéseket) a következő feature. Javascriptben eddig, ha változókat akartunk behelyettesíteni stringbe, akkor a string replace-el vagy épp egyesével összerakosgatva tudtuk megtenni azt. PHP-ben a ""-ök közötti stringekben elhelyezett változók értékét automatikusan behelyettesítette a rendszer, hasonló került most be az ES6-al, de nézzük az eddigi hákolásos megoldásokat:
// ES5 : Based on a true story var customer = { name: "Foo" }; var card = { amount: 7, product: "Bar", unitprice: 42 }; message = "Hello " + customer.name + ",\n" + // a jó öreg összeollózott karakterliterál "want to buy " + card.amount + " " + card.product + " for\n" + "a total of " + (card.amount * card.unitprice) + " bucks?";
ES6-ban is jelölnünk kell, hogy a következő string bizony template, amibe változókat szeretnénk behelyettesíteni. Ehhez a szokásos " helyett ` karakterek közé kell azt tennünk, az alábbi módon:
var customer = { name: "Foo" } var card = { amount: 7, product: "Bar", unitprice: 42 } message = `Hello ${customer.name}, want to buy ${card.amount} ${card.product} for a total of ${card.amount * card.unitprice} bucks?` // és bumm, így lett az XSS!
OO újítások
Gondolom akárkit kérdeznék, aki foglalkozik más komolyabb objektumorientált paradigmákat alkalmazó nyelvvel, az nem igen szívleli a prototype object modeljét a javascriptnek. Körülményes, a szemnek idegen szavak és kód. Na, ennek vége!
Nézzük csak az osztálydefiníciót:
// ES5 - From hell var Shape = function (id, x, y) { // őő.. igen, ez egy konstruktor, a Shape meg egy osztály, fúj. this.id = id; this.move(x, y); }; Shape.prototype.move = function (x, y) { // Ennek nem az osztálydefiníción belül kéne lennie? Meg miért kell a prototype, miért? this.x = x; this.y = y; };
Akkor most vegyünk egy mély lélegzetet, számoljunk el tízig és nézzük meg a következőt:
class Shape { // osztálydefiníció? constructor (id, x, y) { // konstruktor this.id = id this.move(x, y) } move (x, y) { // instance method? this.x = x this.y = y } } // és mindez JS? Bizony, nem szellemeket látsz!
Öröklődés
Ha ez nem lett volna elég, hogy instant nekiess a specifikációnak, akkor jöjjön a következő lépés. Mi a helyzet, ha öröklődést akarsz megvalósítani?
// Kérem felkészülni, felkavaró ES5 öröklődés következik: var Rectangle = function (id, x, y, width, height) { // still szép konstruktor Shape.call(this, id, x, y); // az a bizonyos "super" this.width = width; this.height = height; }; Rectangle.prototype = Object.create(Shape.prototype); // átpasszoljuk a prototípust Rectangle.prototype.constructor = Rectangle; // assignoljuk a konstruktort var Circle = function (id, x, y, radius) { // megint egy "konstruktor" Shape.call(this, id, x, y); // super this.radius = radius; }; Circle.prototype = Object.create(Shape.prototype); // és így tovább Circle.prototype.constructor = Circle;
Akkor jöjjön mindez ES6-ban:
class Rectangle extends Shape { // extends? :O constructor (id, x, y, width, height) { super(id, x, y) // super?? this.width = width this.height = height } } class Circle extends Shape { constructor (id, x, y, radius) { super(id, x, y) // ez nem csak konstruktorra működik, bizony.. base class elérés a super kulcsszóval.. F-yeah! this.radius = radius } }
Kérem tegye fel a kezét, aki szerint ez utóbbi sokkal inkább OO-style!
Static members
Akár hiszitek, akár nem, ezzel még mindig nincs vége. Lassan kukát fejelek, ahogy írom, mert inkább JS-eznék (na jó, ez hazugság, inkább valami erősen típusos nyelv, de psszt! ):
// ES5 var Rectangle = function (id, x, y, width, height) { // "konstruktor" }; Rectangle.defaultRectangle = function () { // ez lenne a statikus metódus, jelen esetben egy factory method return new Rectangle("default", 0, 0, 100, 100); }; var Circle = function (id, x, y, width, height) { … }; Circle.defaultCircle = function () { return new Circle("default", 0, 0, 100); }; var defRectangle = Rectangle.defaultRectangle(); var defCircle = Circle.defaultCircle();
Ezt mondjuk kitaláltuk volna, de nézzük már meg, hogy mi a változás, hé!
class Rectangle extends Shape { … static defaultRectangle () { // na ne.. tényleg képesek voltak beletenni végre egy static kulcsszót? return new Rectangle("default", 0, 0, 100, 100) } } class Circle extends Shape { … static defaultCircle () { // bizonyám! return new Circle("default", 0, 0, 100) } } var defRectangle = Rectangle.defaultRectangle() var defCircle = Circle.defaultCircle()
Set the world on fire!
Újabb adatszerkezetek érkeznek a nyelvbe, hogy a gyakran használt struktúrák helyét átvegyék és mindeközben frissebb-lágyabb-jobb érzéssel töltsenek el minden Coccolino macit. Ezek egyike lett a set ojjektum.
// ES5 var s = {}; // sima ojjektum s["hello"] = true; s["goodbye"] = true; s["hello"] = true; // feltöltjük elemekkel, a hello lévén ismétlődik, felülcsapja az előzőt Object.keys(s).length === 2; // 2 elem van benne s["hello"] === true; // bizony, still true for (var key in s) // arbitrary order if (s.hasOwnProperty(key)) // fincsi, mi? console.log(s[key]); // ES6 let s = new Set() // hmm, Set ojjektum? s.add("hello").add("goodbye").add("hello") // az add felülcsapja, ha már van ilyen kulcs s.size === 2 // size property length helyett s.has("hello") === true for (let key of s.values()) // values-al szedjük ki a cuccot console.log(key)
Ez gondolom még senkit sem vág a falhoz, szóval akkor jöjjön a következő...
Goole Maps! Javascriptben, eddig ha ún. HashMap vagy PHP-s körökben asszociatív tömb kellett, akkor a nyelv ezt egy sima object kulcsaiban tárolta, ez szép és jó, csak semmiféle plusz, Mapre jellemző funkcionalitással nem bírtak a plain objecten felül:
var m = {}; // sima object m["hello"] = 42; // beleoktrojáljuk "kulcsként" for (key in m) { if (m.hasOwnProperty(key)) { var val = m[key]; console.log(key + " = " + val); // majd a kulcs -> érték párokat kiszedjük belőle } } // ES6 let m = new Map() // Map ojjektum m.set("hello", 42) // beletesszük a kulcsot m.size === 1 for (let [ key, val ] of m.entries()) // az entries()-el tudjuk kinyerni a benne elhelyezett párokat console.log(key + " = " + val)
Fasza, ugye? Akkor jöjjön az amitől a Java fejlesztők elalélnak majd!
WeakSet / WeakMap
Ha valaki belefutott már egy autentikus memory leakbe, akkor annak nem kell mondanom mennyire kardinális kérdés ez. Amikor csak 1 lekérés erejéig él az alkalmazás, akkor még annyira nem kardinális a dolog, viszont ha hosszú időn át fut, akkor jön elő mennyire durva a helyzet. Frontendnél még annyira nem szoktak ilyenek előjönni, ahhoz nagyon nagy baklövés kell, de backenden egy kellően szarul megírt node tud finomságokat produkálni. Persze a rendszer nem úgy működik, mintha C-ben írnánk, azért tesz értünk és fut az a bizonyos GC, de ha referenciák beragadnak, akkor bizony az óhatatlanul ottmarad és csámcsog a heap tetején. Hogy megkönnyítsék az életünket, itt is megjelentek az ún. weak reference-ek, illetve azoknak két konkrét "megvalósítása". Ez esetünkben nem a konkrét Mapra és Setre, hanem a benne tárolt kulcsokra vonatkozik, tehát ha valahol az adott kulcson csücsülő ojjektum eredeti referenciáját kitakarítjuk, akkor nem marad benne ezekben az adatszerkezetekben. Lévén ilyet nem lehetett ES5-ben csinálni, ezért csak az ES6 példa jöjjön:
let isMarked = new WeakSet() // az a bizonyos weak referenciákkal vértezett set let attachedData = new WeakMap() // és map export class Node { // csinálunk egy ojjektumot constructor (id) { this.id = id } mark () { isMarked.add(this) } // betesszük a set-be unmark () { isMarked.delete(this) } // kiszedjük a set-ből marked () { return isMarked.has(this) } // megnézzük, hogy a set-ben van-e az adott elem set data (data) { attachedData.set(this, data) } // betesszük a map-be get data () { return attachedData.get(this) } // és kikapjuk onnan } let foo = new Node("foo") // példányosítjuk az objektumot JSON.stringify(foo) === '{"id":"foo"}' foo.mark() // betesszük a set-be foo.data = "bar" // a setteren keresztül betesszük a map-be foo.data === "bar" JSON.stringify(foo) === '{"id":"foo"}' isMarked.has(foo) === true // megnézzük, hogy bent van-e a set-ben attachedData.has(foo) === true // megnézzük, hogy az objektum bent van-e a mapben foo = null /* kitakarítjuk a referenciát */ attachedData.has(foo) === false // hopp.. már nincs bent isMarked.has(foo) === false // a set-be se
Nos, remélem ez a kis adag kedvet hozott arra, hogy Ti magatok is beleássátok magatokat a specifikáció részleteibe vagy elkezdjétek próbálgatni. Persze még nem érdemes csak úgy ES6 kódot hányni, legalábbis kliensoldalon, lévén még a kompatibilitás hagy némi kívánnivalót maga után, viszont akadnak fordítók, amik ES5-öt varázsolnak belőle. Ilyen pl. a babel, amiről majd szintén írok az ünnepek alatt!
Apropó ünnepek...
Minden kedves olvasómnak Boldog Karácsonyt és kellemes húsvéti ünnepeket kívánok!
Ez a this-es dolog nem csak akkor működik, amikor => -al csinálsz függvényt?
A preading syntax-nál az es6-os példa ugyanaz, mint az es5-ös.
Köszi, javítva!
Az export/import kimaradt, viszont egy kicsit jobban értem az egészet. Köszönöm!
Elég sokminden kimaradt, elég nagy lépés ez a nyelvben, hogy csak egy cikkben összefoglaljam 🙂