EcmaScript è la specifica del linguaggio che comunemente chiamiamo Javascript, esistono diverse implementazioni di EcmaScript come quelle usate dai browser e Node.js.
Babel
EcmaScript 6 non è ancora supportato dai principali engine ma può essere utilizzato tramite un transpiler come Babel.
L’installazione di Babel è semplice tramite l’utilizzo di npm
. Nella root del progetto creiamo il file package.json
con il seguente contenuto
{ "name": "my-project", "version": "1.0.0", "scripts": { "build": "babel src -d lib" }, "devDependencies": { "babel": "6.5.2", "babel-preset-es2015": "6.3.13" } }
Installiamo babel tramite il comando npm install
e siamo pronti per lavorare. Eseguendo il comando npm run build
babel cercherà qualsiasi file nella cartella src
e tradurrà i file salvandoli nella cartella lib
, per cui assicuriamoci di creare la cartella src
.
Babel di default non effettua alcuna trasformazione, infatti nel package.json
abbiamo anche aggiunto lo specifico transpiler per la specifica richiesta cioè es2015
.
L’ultimo step è di creare il file .babelrc
e inserire il seguente contenuto, necessario per indicare a Babel di effettuare la transpilazione dei sorgenti.
{ "presets": ["es2015"] }
Per cui eseguendo ora npm run build
i sorgenti verranno correttamente tradotti.
Come esempio si può utilizzare
class Dog { constructor(name) { this.name = name; } bark() { console.log(`${this.name} barks`); } } new Dog('Fido').bark();
Eseguendo i comandi in successione:
npm run build node lib/Dog.js
Si otterrà l’output
Fido barks
Già in questo esempio sono stati utilizzati due nuovi costrutti: le classi e i template string che affronteremo insieme durante il proseguo dell’articolo.
Classi
Al contrario di quanto il nome possa suggerire ad uno sviluppatore Java, le classi in ES6 non traducono il paradigma orientato agli oggetti basato su classi tipico di Java.
Javascript rimane sempre un linguaggio orientato agli oggetti basato su prototipi, la keyword class
introduce zucchero sintattico che ci permette di ragionare in termini di classi semplificando il modo di ragionare per chi proviene da questo mondo.
class Coder { constructor(name, language) { this.name = name; this.language = language; } hello() { console.log(`Hello I'm ${this.name} and I like ${this.language}`); } } new Coder('Nicola', 'ES6').hello();
La sintassi è familiare ad uno sviluppatore Java ma comunque bisogna notare che il costruttore si indica con il metodo constructor
, convenzione della specifica.
E’ possibile specificare anche metodi statici.
class Coder { constructor(name, language) { this.name = name; this.language = language; } hello() { console.log(`Hello I'm ${this.name} and I like ${this.language}`); } static unnikked() { return new Coder('Nicola', 'ES6'); } } Coder.unnikked().hello();
ES6 supporta getter e setter per gli oggetti
class Coder { constructor(name, language) { this.name = name; this.language = language; } get foo() { return this.name; } hello() { console.log(`Hello I'm ${this.name} and I like ${this.language}`); } static unnikked() { return new Coder('Nicola', 'ES6'); } } console.log(Coder.unnikked().foo); // Nicola
Come le funzioni che possono essere passate nei parametri, anche le classi possono essere passate come parametri a funzioni o altre classi stesse.
Ecco un esempio che mostra il pattern interpreter applicato alla risoluzione di una espressione booleana (esempio semplice ma che fa capire il concetto).
class And { constructor(left, right) { this.left = left; this.right = right; } interpret() { return this.left.interpret() && this.right.interpret(); } } class Or { constructor(left, right) { this.left = left; this.right = right; } interpret() { return this.left.interpret() || this.right.interpret(); } } class Not { constructor(child) { this.child = child } interpret() { return !this.child.interpret(); } } class Value { constructor(value) { this.value = value; } interpret() { return this.value; } } let t = new Value(true); let f = new Value(false); let expression = new And(new Not(f), new Or(f, t)); console.log(expression.interpret()); // true
Moduli
ES6 introduce il concetto di moduli, un concetto simile ma non del tutto identico ai package di Java.
Nel file Coder.js
:
export class Coder { constructor(name, language) { this.name = name; this.language = language; } hello() { console.log(`Hello, I'm ${this.name} and I like ${this.language}`); } }
Da notare la keyword export
che indica il componente da esportare e rendere disponibile quando si richiama il file.
import {Coder} from './Coder.js' new Coder('Nicola', 'ES6').hello();
La keyword import
indica il componente da importare dal file indicato dopo from
.
Dichiarazioni di variabili
ES6 introduce due nuove keyword per la dichiarazione di variabili let
e const
.
let
consente di dichiarare variabili a livello di blocco come succede in Java, in modo tale che esse non vengano spostate tramite hoisting.
Un blocco è delimitato dalle parentesi graffe { ... }
function example(bool) { if(bool) { let foo = 'bar'; console.log(foo); } else { console.log(foo); } }
Pertanto l’esecuzione di example(false)
produrrà foo is not defined
poiché tale variabile non è presente nello scope del blocco else
.
const
invece permette di rendere non assegnabile una variabile una volta assegnata, ciò non rende la variabile stessa immutabile.
const names = ['Gianpaolo', 'Francesco']; names = ['Andrea']; console.log(names);
Risulterà in "names" is read-only
. Invece:
const names = ['Gianpaolo', 'Francesco']; names.push('Andrea'); console.log(names);
Produrrà [ 'Gianpaolo', 'Francesco', 'Andrea' ]
Questo accade perché è stato modificato l’array che è un oggetto mutabile e non il riferimento della variabile. const non garantisce l’immutabilità.
Il concetto che sta dietro a const
è il medesimo che si trova nel mondo Java nell’utilizzo della keyword final
per le variabili.
Anche const
come let
crea variabili a livello di blocco.
Per scoprire come funzionano gli scopes di Javascript in generale consiglio la lettura del post di Andrea.
Arrow function
E’ una nuova sintassi che permette di definire funzioni anonime a la Java (le funzioni lambda) tramite una semplice sintassi del tipo (params) => (body)
.
Prendiamo come esempio l’esercizio di voler elevare al quadrato un array di interi, un modo classico potrebbe essere.
let numbers = [1, 2, 3]; numbers = numbers.map(function (element) { return element * element; }); console.log(numbers);
Otterremo certamente il risultato [ 1, 4, 9]
. Con la nuova sintassi invece diventerà:
let numbers = [1, 2, 3]; numbers = numbers.map(element => element * element); console.log(numbers);
Semplice zucchero sintattico che ci permette di rendere il codice meno noioso agli occhi.
Non è necessario utilizzare la keyword return
nel caso in cui nel corpo della funzione è presente un solo statement.
E’ possibile utilizzare questa sintassi per assegnare funzioni anonime a variabili e passarle ai parametri come di consueto.
let numbers = [1, 2, 3]; let square = x => x * x; numbers = numbers.map(square); console.log(numbers);
Nel caso in cui la funzione prende più parametri è necessario porre le parentesi intorno.
let sum = (a, b) => a + b; console.log(sum(3, 2));
Oppure definire un blocco nel caso bisogna scrivere più di uno statement nel corpo della funzione.
let f = (a, b) => { a = a * 2; return a + b; }; console.log(f(3,2));
Come qualcuno di voi avrà notato questa sintassi ha molto in comune con le Lambda di Java 8.
Parametri di default
Questa nuova feature non ha un riscontro diretto nel mondo Java. Nel passato si è scritto codice del tipo:
var sum = function(a, b) { var a = a || 3; var b = b || 2 return a + b; } console.log(sum()); console.log(sum(8)); console.log(sum(10, 5)); console.log(sum(0, 0)); // 5 invece di 0
Sfruttando le caratteristiche del linguaggio e dell operatore ||
. Sembra una soluzione ottimale ma può portare a comportamenti inattesi come mostrato nell’esempio.
Invece in ES6 è possibile specificare valori di default ai parametri di ingresso di una funzione, chi ha usato PHP o Python sa di cosa stiamo parlando.
function sum (a = 3, b = 2) { return a + b; } console.log(sum()); console.log(sum(8)); console.log(sum(10, 5)); console.log(sum(0, 0)); // 0 corretto
Una caratteristica particolare di questo costrutto è che il valore di default non deve essere necessariamente un valore primitivo ma può anche essere un oggetto.
let f = () => 5 * 2; function sum (a = 3, b = f()) { return a + b; } console.log(sum(3)); // 13
Rest e Spread
L’operatore rest ...
traduce il concetto il funzione variadica presente in Java. In pratica permette di far accettare ad una funzione un numero qualsiasi di parametri.
Cosi come in Java la variabile variadica viene trasformata in un array allo stesso modo avviene la trasformazione in ES6.
function f(...numbers) { return numbers.map(n => Math.sin(n)); } console.log(f(45, 90, 135, 180));
Cosi come in Java la variabile variadica deve essere dichiarata per ultima nella firma del metodo allo stesso modo bisogna dichiararla in ES6.
L’operatore spread ...
funziona in modo duale rispetto a rest. Se rest compatta una serie di valori in un array spread scompatta un array in una serie di valori.
function f(a, b) { return a + b; } let numbers = [3, 2]; console.log(f(...numbers)); // 5
Template String
Tramite i backtick ``
è possibile definire stringhe che possono essere sfruttare come template. Le stringhe possono essere multilinee e le variabili vengono richiamate tramite la sintassi ${varName}
.
let name = "Nicola"; let template = "Hello ${name}"; console.log(template); // "Hello Nicola"
Abbreviazione per gli oggetti
Siamo nella situazione in cui vogliamo ritornare un oggetto le cui proprietà corrispondono a delle variabili già definite.
function author() { let name = "Nicola"; let age = 23; // assunzione: valori presi da fonti esterne // altrimenti l'esempio è triviale return { name: name, age: age } }
Come si può notare le proprietà name
e age
sono già definite nel corpo della funzione, con ES6 si può semplificare in questo modo.
function author() { let name = "Nicola"; let age = 23; return {name, age}; } console.log(author().name); // Nicola
Le abbreviazioni valgono anche per i metodi, invece di scrivere:
function author() { let name = "Nicola"; let age = 23; return { name: name, age: age, hello: function () { return "Hello I'm " + this.name; } } } console.log(author().hello()); // Hello I'm Nicola
E’ possibile scrivere in modo equivalente:
function author() { let name = "Nicola"; let age = 23; return { name, age, hello() { return `Hello I'm ${this.name}`; } }; } console.log(author().hello()); // Hello I'm Nicola
Destrutturare gli oggetti
E’ possibile estrarre le proprietà di un oggetto e assegnarle a variabili direttamente utilizzando la sintassi di ES6:
let unnikked = { name: 'Nicola', age: 23 }; let { name, age } = unnikked; console.log(name); // Nicola
E’ importante sottolineare che questo costrutto ci consente di destrutturare un oggetto secondo le nostre esigenze.
let data = { a: 1, b() { return 2 }, c: "3" }; let {a, c} = data; console.log(c); // 3
E’ possibile usare questo costrutto anche per i parametri delle funzioni.
function printPerson({name, age}) { console.log(`Hello ${name} you are ${age} years old`); } printPerson({ name: 'Nicola', age: 23, profession: 'Student' });
Sicuramente risparmieremo un bel po’ di tempo.
Conclusioni
Personalmente non conosco Javascript così bene da poter esporre una confronto rispetto alle versioni precedenti, ma da sviluppatore Java posso affermare che queste novità mi hanno aiutato a comprendere meglio il linguaggio sfruttando conoscenze pregresse senza perdere molto tempo nell’apprendimento.
Per tanto se non ti eri mai avvicinato al mondo Javascript e provieni da un mondo Java, ES6 fornisce i mezzi necessari per rendere meno “dolorosa” la transizione.
Pingback: Cıvata()