Pentru aceia dintre voi care nu au auzit de ECMAScript, vă informăm că acesta este standardul care definește limbajul de programare pe care noi îl știm drept JavaScript. Ediția anterioară a standardului ECMAScript 5 a fost lansat la sfârșitul anului 2009, cu îmbunătățiri semnificative ale limbajului, dar cu câteva funcții importante lipsă. ECMAScript 6 (ES6) a introdus în sfârșit caracteristici pe care dezvoltatorii le așteptau de mulți ani.
De fapt, denumirea oficială este ECMAScript 2015, dar se pare că în cadrul comunității, ES6 este încă versiunea pe care cei mai mulți o recunosc.
Voi expune și analiza mai jos câteva dintre îmbunătățirile care mie personal îmi plac și care cred că mă vor ajuta cel mai mult. Bineînțeles, în funcție de tipul proiectului, s-ar putea ca voi să găsiți utile alte caracteristici.
Funcțiile Arrow (săgeată) sunt o nouă modalitate mai scurtă de a scrie funcții anonime. Dacă ești un programator care preferă programarea funcțională, cu siguranță îți vor plăcea. Iată un exemplu scurt:
// versiunea es6
[1, 2, 3].map((item, index) => {
return item * 2;
});
// versiunea es5
[1, 2, 3].map(function(item, index) {
return item * 2;
});
După cum puteți vedea, am folosit cuvântul cheie "funcție", care deja ne scutește de niște caractere, dar lucrurile devin și mai bune. Dacă funcția Arrow necesită numai un argument, puteți scăpa de asemenea și de parantezele de deschidere și de încheiere din lista de argumente:
[1, 2, 3].map(item => {
return item * 2;
});
Aceasta arată bine, dar încă nu am terminat! Mai există încă o îmbunătățire pe care o putem face aici: dacă corpul funcției Arrow este numai o afirmație de returnare, putem scăpa de paranteze și de asemenea și de cuvântul cheie "return" și vom obține ceva de genul:
[1, 2, 3].map(item => item * 2).filter(item => item % 3 === 0); // this will return [6]
Acum, să comparăm aceasta cu versiunea ES5:
[1, 2, 3].map(function (item) {
return item * 2;
}).filter(function (item) {
return item % 3 === 0;
});
Există o ameliorare evidentă nu numai în faptul că putem scrie același cod cu mai puține caractere, dar de asemenea în lizibilitatea codului. Dacă nu aveți argumente la funcția voastră, ar trebui să utilizați următoarea sintaxă:
const func = () => {
// do something
}
Există un singur impediment pe care ar trebui să îl luați în considerare: spre deosebire de funcțiile anonime normale care creează fiecare un context nou, funcțiile Arrow vor păstra contextul funcțiilor părinte. Dezvoltatorii au avut probleme adesea în identificarea contextului în care rula funcția lor; aceasta ar trebui să facă lucrurile puțin mai ușor de înțeles. Iată un exemplu:
document.body.addEventListener('click', function (event) {
console.log(this);
// 'this' va face referință la elementul body
});
document.body.addEventListener('click',
(event) => {
console.log(this);
// 'this' va face referință la obiectul Window
// (adică contextul părintelui)
});
Deci, nu puteți face pur și simplu o căutare rapidă și să înlocuiți toate funcțiile anonime cu funcții Arrow, pentru că va strica ceva, cu siguranță.
O adăugire mult așteptată la JavaScript a fost abilitatea de a crea și încărca module. Cu complexitatea în creștere a aplicațiilor web, era evident că această caracteristică trebuia să existe; de aceea, comunitatea JavaScript și-a asumat sarcina de a crea câteva soluții pentru aceasta. Cele mai populare sunt standardul CommonJS, utilizat de Node JS și Browserify, și Asynchronous Module Definition (AMD), utilizat de RequireJS. Soluția ES6 pentru module încorporează caracteristici de la ambele sisteme de module populare, dar adaugă și mai multă funcționalitate utilă.
O schimbare importantă care a fost făcută la modulele ES6 este că ele au o structură statică, adică importurile vor fi analizate static în timpul compilării. Aceasta are un avantaj major, deoarece instrumentele de linting vor putea analiza mult mai bine codul nostru.
Sintaxa modulelor ES6 ne oferă multiple moduri de a exporta valori: exporturi denumite (putem avea mai multe într-un modul) și exporturi default (numai unul pe fișier). Partea frumoasă este că putem avea ambele în același modul. Exporturile denumite arată așa:
// my_module.js
export function helloWorld () {
console.log('Hello world');
}
export const MY_CONSTANT = 5;
export let loremIpsum = 'dolor';
Aveți, de asemenea, opțiunea de a defini toate funcțiile/ variabilele pe care doriți să le exportați și apoi să le exportați separat, după cum urmează:
// my_module.js
const helloWorld = () => {
console.log('Hello world');
}
const MY_CONSTANT = 5;
let loremIpsum = 'dolor';
export {
helloWorld,
MY_CONSTANT,
loremIpsum
};
În acest fel este puțin mai vizibil ce exportați de fapt dintr-un modul. Ca observație, probabil ați observat că am utilizat "let" pentru a declara variabila "loremIpsum". "let" este, de asemenea, parte a specificației ES6, și este o modalitate nouă de a declara variabile. Diferența dintre "let" și "var" este aceea că "let" este limitat la bloc (block scoped) în loc să fie limitat la funcție (function scoped) (sau global, dacă nu se află într-o funcție), precum "var".
Acestea fiind spuse, importul din module arată așa:
// main.js
import { helloWorld, MY_CONSTANT } from 'my_module';
helloWorld(); // va afișa 'Hello world'
// sau importă toate exporturile denumite
import * as m from 'my_module';
m.helloWorld();
Exporturile default au o sintaxă scurtă:
// sum.js
export default function (a, b) {
return a + b;
};
// main.js
import sum from 'sum';
alert(sum(4, 5)); // va afișa '9'
Este posibil să avem ambele, exporturi denumite și un export default în același modul, și aceasta ar arăta așa:
// module.js
const func1 = () => {
...
};
const func2 = () => {
...
};
// Exportă valoarea default
export default function () {
...
};
// Exportă valorile denumite
export {
func1,
func2
};
// main.js
import defaultFunction,
{ func1, func2 } from 'module';
Există un dezavantaj al acestei sintaxe de importare: nu poate fi utilizată în interiorul unui element de tip script (deoarece este un element sincron) și nu puteți încărca un modul în mod condiționat. Există, totuși, o modalitate alternativă de a încărca module, utilizând un API. Sintaxa arată așa:
System.import('my_module').then(my_module => {
my_module.helloWorld();
}).catch(error => {
alert('Cannot load module');
});
Datorită naturii asincrone a JavaScript, ne găsim adesea în ceea ce este cunoscut drept "callback hell", adică multe funcții callback în alte funcții callback care reduc mult lizibilitatea:
doSomething(param1, param2, () => {
doSomethingElse((param3) => {
doMore((param4) => {
andMore((param5) => {
// Unde sunt?
});
});
});
});
Cred că se poate vedea cum lucrurile pot deveni complicate destul de rapid și cei mai mulți din dezvoltatorii JavaScript au întâlnit situații ca și asta. Promises sunt o alternativă la funcțiile callbacks pentru a tatona codul asincron. Ca și modulele, există câteva implementări open source pentru promises, precum Q, RSVP sau Bluebird. Popularul jQuery are un obiect Deferred, care oferă același tip de funcționalitate, dar sintaxa sa este diferită și nu este conformă cu ES6.
Promises sunt în fond obiecte care se pot afla într-una din următoarele trei etape:
în așteptare: așteptând ca operația să se finalizeze. Aceasta este etapa inițială atunci când creăm un obiect Promise.
îndeplinite: operația s-a finalizat cu succes.
Fiecare obiect Promise primește o funcție callback drept parametru. Această funcție callback primește doi parametri: o funcție fulfill (îndeplinire) pentru operația finalizată cu succes, iar o funcție reject (respingere), pentru operația eșuată.
Utilizarea de bază a promisiunilor arată cam așa:
// Definim o funcție asincronă (un apel de tip ajax,
// de exemplu) care returnează un obiect Promise
const myAsyncFunction = () => {
return new Promise((fulfill, reject) => {
// facem ceva cu datele
if (...) {
fulfill(param1, param2);
} else {
reject(error);
}
});
}
// Apelăm funcția asincronă
myAsyncFunction().then((param1, param2) => {
console.log('async function finished');
}).catch((error) => {
console.log('Something bad happened: ' + error);
});
Cea mai bună parte a promises este că ele se pot înlănțui, ceea ce aplatizează codul:
myAsyncFunction().then(() => {
// funcția myAsyncFunction a rulat cu succes
return anotherAsyncFunction();
}).then(() => {
// funcția anotherAsyncFunction a rulat cu succes
}).catch((error) => {
// funcția myAsyncFunction sau funcția
// anotherAsyncFunction a aruncat o eroare
});
Da, clasele au ajuns în sfârșit în JavaScript. Pe cât este de anticipată această caracteristică, pe atât este de controversată, deoarece există destul de mulți dezvoltatori care se opun introducerii claselor, deoarece javascript are deja o moștenire bazata pe prototip care poate de asemenea să ofere o moștenire clasică.
Clasele în ES6 sunt în mare parte syntactic sugar, deoarece același tip de funcționalitate ar fi putut fi creată utilizând ES5, dar introducerea lor va permite viitoarelor ediții ale ECMAScript să le adauge mai multă funcționalitate. Există deja câteva intenții de a extinde clasele în ES7 prin adăugarea variabilelor private și a altor caracteristici.
Sintaxa pentru clase arată așa:
class Vehicle {
constructor(type, color) {
this.type = type;
this.color = color;
}
getColor() {
return this.color;
}
}
Aceasta va crea o clasă "Vehicle" cu un constructor și o metodă "getColor". De asemenea, puteți extinde clasele:
// Extindem clasa Vehicle
class Car extends Vehicle {
constructor(color, maxSpeed) {
super('car', color);
this.maxSpeed = maxSpeed;
}
getMaxSpeedFormatted() {
return this.maxSpeed + 'km/h';
}
}
let car = new Car('blue', 200);
console.log('We have a ' + car.getColor() +
' car with a max speed of ' +
car.getMaxSpeedFormatted());
Veți observa că folosim "super(…)" aici pentru a apela un constructor părinte. Cele mai multe dintre limbajele orientate pe obiect își dau această posibilitate drept o opțiune, dar în JavaScript, acest lucru este obligatoriu dacă suprascriem constructorul. Trebuie să apelăm super-constructorul înainte de a încerca să accesăm "this", altfel, veți obține o eroare "Uncaught ReferenceError: this is not defined":
class Car extends Vehicle {
constructor(color, maxSpeed) {
maxSpeed *= 0.6214;
// transform to miles. You can do this before calling
// 'super'
this.maxSpeed = maxSpeed;
// nu putem face referință la "this". Aceasta va
// arunca o eroare de tip ReferenceError
super('car', color);
}
}
Puteți apela alte metode pe clasa de bază, nu numai constructorul, utilizând "super.methodName()".
Există multe caracteristici noi în ES6 despre care trebuie să știți, cum ar fi noua modalitate de a declara variabile utilizând "let" sau valori default pentru argumentele funcțiilor, multe metode noi pentru prototipurile String și Array, și mult mai multe. Aceasta a fost doar o privire destul de generală, dar sper că am reușit să vă fac să doriți să aflați mai multe.
Pentru o listă mai extinsă a noilor caracteristici din ES6, vedeți specificația de limbaj aici.
O altă resursă extraordinară dacă doriți să pătrundeți tainele ES6 este cartea Doctorului Axel Rauschmayer, "Exploring ES6", pe care o oferă gratis, cu generozitate, în versiunea online, aici sau puteți cumpăra cartea pentru a-i sprijini munca, aici.
Cum am putea utiliza și valorifica azi aceste caracteristici? ES6 a fost aprobat oficial din Iunie 2015, dar știm cu toții ca va dura ceva timp până când browser-ele vor implementa în întregime toate caracteristicile. Browser-ele evergreen recuperează din urmă destul de rapid, dar chiar dacă vor implementa ES6 complet, cei mai mulți dintre noi încă trebuie să oferim suport pentru versiunile mai vechi de browser- știm cu toții că mă refer aici la Internet Explorer. Deci, cum putem să le utilizăm pe toate sau cel puțin un set din aceste caracteristici astăzi?
Soluția? Transpilers! Există câteva transpilers de la ES6 la ES5 disponibile în acest moment, cu Babel JS și Traceur fiind cele mai populare. O listă actualizată cu cele mai utilizate compilatoare transpilers și suportul caracteristicilor lor (plus suport pentru browser) pot fi găsite în tabelul de compatibilitate al kangax. La momentul scrierii acestui articol, Babel JS (care a evoluat mult între timp și este mai mult o platformă decât un transpiler) are cel mai bun suport, cu peste 71% din caracteristici susținute, ceea ce e destul de bine pentru un transpiler!
Babel și cele mai multe alte transpilers au de asemenea plugin-uri gulp (și Grunt) disponibile, deci este ușor și rapid să le instalați în sistemul vostru. Priviți acest exemplu scurt și minimal de utilizare a Babel cu gulp:
var gulp = require('gulp');
var babel = require('gulp-babel');
gulp.task(build-js, function () {
return gulp.src('src/app.js')
.pipe(babel())
.pipe(gulp.dest('dist'));
});
ES6 ne-a adus cu siguranță multe îmbunătățiri, de la simple syntactic sugars (precum clasele) până la caracteristicile noi și complexe, cum sunt obiectele Promise și modulele. Unele dintre caracteristicile noi schimbă jocul (ex. module, obiectele Promise, etc.), unele ne vor face codul mai curat și mai lizibil (ex. funcțiile Arrow, template strings, etc.), altele sunt pur și simplu utile (ex. metode noi pentru mulțimi).
Această nouă lansare a ECMAScript nu numai că va schimba modul în care scriem JavaScript, dar de asemenea va modifica modul în care gândim și ne organizăm codul JavaScript. Cu alte cuvinte, ne va face viața mai ușoară.