javascript, informatica, programmazione

Come non perdersi nell'ecosistema JavaScript

Che differenze vi sono tra JavaScript, TypeScript ed EcmaScript? Cos'è un transpiler? Come faccio ad eseguire il mio codice JS in tutti i browser? Cerchiamo di rispondere a queste domande.

Andrea Scianò Andrea Scianò 14 Ago 2018 · lettura da 20 min
Come non perdersi nell'ecosistema JavaScript

Come scritto altre volte all'interno di vari articoli, fino a poco tempo fa mi trovavo a destreggiarmi con il framework Ember per lo sviluppo dei nostri prodotti a Sysdig.

Circa un mese fa, dopo un lungo periodo fatto di pianificazione e apprendimento di nuove tecnologie, abbiamo iniziato la migrazione a React. Questo mi ha portato a confrontarmi con argomenti che non conoscevo o conoscevo in modo approssimativo, per cui, una volta appresi, ho pensato che un articolo facente luce su alcuni concetti, mi sarebbe stato molto utile in fase di apprendimento. Scrivo queste righe con lo stesso proposito, rivolgendomi a chiunque faccia lo stesso percorso.

Il problema delle versioni di JavaScript

Le specifiche del linguaggio JavaScript (JS è il suo diminutivo) sono regolate da un'organizzazione chiamata ECMA: European Computer Manufacturers Association. Lo stato attuale e futuro del linguaggio è guidato da un comitato tecnico chiamato TC39 (TC39-Royalty Free TG) e comprende una varietà di membri provenienti da differenti organizzazioni, sia coinvolte nello sviluppo come Mozilla, Google, Microsoft, ecc... ma anche membri rappresentanti entità non aziendali.

Essi si incontrano ogni tre mesi per analizzare le novità proposte per il linguaggio: decidono quali accettare, quali declinare e cosa includere nella prossima versione.
Tutto questo può essere esaminato nei repository di Ecma TC39 su GitHub.
Il nome ufficiale di JavaScript, anche se può suonare strano, è in realtà ECMAScript, per cui le varie versioni vengono denominate apponendo ES come prefisso, seguito dal numero della versione (ad esempio ES6, versione uscita nel 2015: ES sta appunto per EcmaScript).
TC39 si impegna a divulgare una nuova versione ogni anno, sebbene le funzionalità siano aggiunte in modo incrementale durante quest'ultimo.
È importante sottolineare che il comitato comunica solamente le specifiche del linguaggio, saranno poi le aziende che abbiamo elencato poche righe sopra, ad implementare e rilasciare il loro codice, seguendo le loro linee guida. Questo è il motivo per cui non tutte le implementazioni di JavaScript sono uguali: le nuove caratteristiche tendono ad arrivare in modo scaglionato a seconda dell'implementazione e questo rappresenta un problema, vedremo tra poco perchè.
Quali feature si trovino in una determinata versione è descritto in modo molto efficiente in Kangax: la tabella di compatibilità ECMAScript 2016+.

Standard esistenti

JavaScript ha una strana cronologia di nomi. Inizialmente chiamato LiveScript, come spiegato in questo articolo, venne rinominato in JavaScript un anno dopo, con l'intento di capitalizzare la popolarità di Java in quel periodo.
Nel 1996 Netscape consegna JavaScript ad ECMA International per la sua standardizzazione; questo porta ad avere un nuovo standard chiamato appunto ECMAScript. Il termine JavaScript rimase per ragioni storiche e di marketing, per cui nel mondo reale, ECMAScript viene comunemente usato per riferirsi allo standard mentre JavaScript viene usato in modo generico quando si fa riferimento al linguaggio.

ECMAScript non cambiò molto per i primi 15 anni della sua esistenza ma le implementazioni del mondo reale spesso differivano significativamente dallo standard. Dopo la prima versione, ne furono rapidamente rilasciate altre due, arrivando a ECMASCript 3 nel 1999.
Poi, incredibilmente, non sono state apportate modifiche per un decennio! Nonostante ciò, diversi fornitori di browser apportavano i propri cambiamenti senza curarsi delle linee guida dettate dallo standard e gli sviluppatori web dovevano lottare per riuscire a supportare API diverse tra loro. ECMAScript 5 (si, se ve lo state chiedendo la versione 4 manca) viene pubblicato nel 2009; ci sono voluti diversi anni per averne ampio supporto da parte dei browser e molti sviluppatori continuavano a scrivere codice in stile ECMAScript 3, senza necessariamente conoscere lo standard.

Intorno al 2012 le cose iniziano a cambiare: cresceva la volontà di smettere il supporto a Internet Explorer e di scrivere codice in stile ES5.
Allo stesso tempo, era in corso il lavoro su un nuovo standard che sarebbe diventato ES6. Proprio Nel 2015, TC39 si rende conto che il modello di aggiornamento delle versioni adottato sino ad allora non regge più e si passa a release annuali in cui le nuove funzionalità sarebbero state aggiunte non appena approvate, piuttosto che attendere di averle tutte complete per includerle in blocco. Di conseguenza, la sesta edizione di ECMAScript viene rinominata ECMAScript 2015 (ES2015) prima di essere pubblicata nel giugno dello stesso anno. Ecco un riassunto schematico:

Versione Data
ECMAScript Prima versione del linguaggio standardizzato da ECMA International e supervisionata dal comitato TC39. Questo termine è solitamente usato per riferirsi allo standard stesso. Giugno 1997
ECMAScript 2 Piccoli cambi editoriali per mantenere le specifiche concordanti con l'ISO/IEC 16262. Giugno 1998
ECMAScript 3 Aggiunte le espressioni regolari, miglior gestione delle stringhe, nuovi comandi di controllo, gestione del try/catch, formattazione per output numerici ed altri miglioramenti. Dicembre 1999
ECMAScript 4 Eedizione abbandonata per diversità di idee riguardo la complessità del linguaggio. Molte delle feature proposte sono state del tutto scartate, altre sono state riproposte nella versione 6. MAI
ECMAScript 5 (ES5) Aggiunge lo "strict mode" inteso a provvedere un più completo controllo di errori. Chiarifica molte ambiguità introdotte nella terza versione. Aggiunge alcune feature come getters e setter, la libreria per il supporto a JSON e la reflection sulle proprietà degli oggetti. Dicembre 2009
ECMAScript 2015 (ES6) Apporta modifiche sintattiche significative che aprono la porta ad applicazioni più complesse, includendo classi e moduli definendoli semanticamente come lo "strict mode" in ES5. Altre nuove feature sono diversi iteratori, for/of loop, Python-style generators e generator expression, arrow functions, binary data, typed arrays, collections (maps, sets e weak maps), promises, miglioramenti delle funzioni matematiche, reflection e proxies (metaprogrammazione per virtual objects). Il supporto da parte dei browser di ES6 rimane ancora incompleto per il momento. Giugno 2015
ECMAScript 2016 (ES7) La settima edizione è di fatto un assestamento dei cambiamenti introdotti nella precedente. Introduce anche due novità: l'operatore esponente (**) e Array.prototype.includes. Giugno 2016
ECMAScript 2017 (ES8) Le nuove funzionalità proposte includono concurrency ed atomics, zero copy binary transfer, altri miglioramenti nell'ambito matematico, integrazione sintattica con le promise, observable stream, SIMD, migliore metaprogrammazione con le classi, overload degli operatori e molto altro. Giugno 2017

Consultando la tabella Kangax possiamo notare quanto i fornitori di browser siano indietro nell'includere le funzionalità rispetto alla versione odierna dello standard. Ad oggi molte delle funzionalità di ES6 non sono ancora nativamente disponibili dato che ogni compagnia decide cosa implementare e quando divulgarla, figuratevi quelle successive. 😅
Questo perché ogni browser utilizza un motore JavaScript diverso: Chrome esegue V8, Firefox esegue SpiderMonkey e Interet Explorer, Chakra. Ognuno ha caratteristiche e prestazione diverse, ciascuno implementa un sottoinsieme diverso di funzionalità e le varie aziende si avvicinano alla piena conformità con le specifiche a diverse velocità.

I transpiler (o transpilatori)

A causa di quanto esposto nel paragrafo precedente, se ad oggi volessimo scrivere codice usando la versione ES6, per essere sicuri che il nostro codice funzioni, dovremmo controllare che tutti i browser supportino le funzionalità che andiamo ad usare: diventeremmo matti!
Ecco che entrano in gioco loro: i transpiler! Ma cosa sono?

I transpiler sono uno strumento in grado di prendere in input un codice sorgente scritto in un determinato linguaggio L1 generando un output di un codice sorgente scritto in un linguaggio L2 ≠ L1 (diverso da quello dato in input) la cui esecuzione produce gli stessi identici risultati del primo.

Data questa definizione, è legittimo chiedersi quale sia la differenza con i compilatori (per lo meno, io mi sono posto questa domanda; per coloro i quali non sapessero cosa siano, ecco il link a wikipedia).
La differenza è sottile ma c'è: il transpiling è un tipo specifico di compilazione:

Compilazione è il termine generale per indicare la trasformazione di un codice sorgente scritto in un linguaggio ad un altro.

Transpilazione è un termine specifico per indicare la trasformazione di un codice sorgente scritto in un linguaggio ad un altro che abbia un livello di astrazione simile.

Per fare un esempio concreto, semplificando un po' le cose per rendere l'idea, quando si compila Java si ottengono delle istruzioni scritte in Bytecode che verranno poi eseguite dalla Java Virtual Machine. Questa azione non può essere chiamata transpilazione perché i due linguaggi, Java e Bytecode, hanno livelli di astrazione molto diversi.

Uno strumento che invece è in grado di trasformare, ad esempio, C++ in C si può chiamare transpiler perchè il linguaggio ottenuto ha lo stesso livello di astrazione di quello di partenza.

Sia i compilatori che i transpilatori possono ottimizzare il codice come parte del processo.

Transpiler per JavaScript

Ora che il quadro è un po' più chiaro, possiamo finalmente intuire come mai i transpiler tornano utili: non è affatto semplice scrivere codice JavaScript che viene eseguito all'interno di browser diversi!
Programmi ES5 eseguiti nelle versioni più recenti di Chrome, Firefox o Safari, non funzioneranno se eseguiti in versioni precedenti. Né se eseguiti in Internet Explorer, qualsiasi versione essa sia... 😂

Le varie aziende lavorano sodo per portare le funzionalità supportate da ES6 sui rispettivi browser, vi sono progressi ma non è ancora il momento di scrivere codice interamente in quella versione. Possiamo dunque avvalerci di questo fantastico strumento quali sono i transpiler, scrivendo codice in ES6 lasciando che un traspilatore lo traduca in ES5 per noi.
In questo modo siamo sicuri che possa essere eseguito su tutti i browser; addirittura, se si ha necessità di supportare tecnologie obsolete, è possibile una transpilazione fino ad ES3.

I transpiler più famosi ed utilizzati sono Babel e Traceur (a Sysdig utilizziamo Babel).

I transpilatori svolgono anche un ruolo importante nel guidare le decisioni del comitato TC39. Possono contribuire direttamente all'inclusione di una caratteristica nello standard, ma la cosa più importante è che riescono a fornire un feedback proveniente direttamente dagli utilizzatori.
Grazie ai transpiler, TC39 può conoscere in anticipo come verrebbe accolta una certa funzionalità, dato che, come ormai abbiamo capito, essa sarebbe presente al loro interno prima dell'inclusionne nella release ufficiale e verrebbe utilizzata dai suoi utenti. Altro aspetto importante è che gli sviluppatori di questi strumenti, aiutano il comitato ad iterare sulle varie sintassi da adottare e sulle proposte in generale; questo succede spesso durante le riunioni del TC39 dove Babel viene citato spesso anche nelle note scritte.

Ora che sappiamo di più su cosa sono i transpiler, cerchiamo di capire come usarli. Questa porzione di codice è scritta in ES6:

"use strict";

class Automobile {
  constructor (modello, colore) {
    this.modello  = modello;
    this.colore = colore || 'nero';
  }

  dimmiColore () {
    console.log(`Sono di colore ${this.colore}.`)
  }
}

const alfaGiulia = new Automobile('Giulia', 'rosso');
alfaGiulia.dimmiColore();

Possiamo prenderla ed inserirla nel transpilatore live ufficiale di Babel che offre traduzioni in tempo reale.
Il seguente è l'output che ne riceviamo:

Se ci fate caso, nella parte di destra le costanti sono state convertite in var, la stringa all'interno del console.log() è creata con una banale concatenazione anzichè utilizzando la nuova sintassi e la classe, concetto non presente in ES5, viene creata affidandosi al costruttore di una funzione.

Non per forza si deve eseguire il transpile di tutte le funzionalità: arriverà un giorno in cui convertire let con var non sarà più necessario poichè verrà eseguito da tutti i browser; potremmo allora dire a Babel di non toccare quella parte. let è già accettato da Chrome dalla versione 49, se dovessimo supportare unicamente quel browser, potremmo impostarlo in modo da non eseguire quel transpile! Babel ha molte configurazioni disponibili, possiamo indicare con molta precisione cosa trasformare e cosa no.

Il transpilatore live è molto carino per queste dimostrazioni ma certamente non è utilizzabile in un progetto, dove si automatizza il processo con l'uso della versione CLI di Babel in combinazione con altri strumenti per la fase di building come webpack o simili. Non mi dilungherò nello spiegare tutta la sua installazione perchè le istruzioni al link sono abbastanza chiare, qualora mi sbagliassi, scrivetemi, potrebbe essere uno spunto per un nuovo post.

I "dialetti" di JavaScript

Conosciamo il motivo delle profonde differenze che vi sono tra una versione di JavaScirpt e l'altra, abbiamo toccato con mano la difficoltà nel poterle eseguire indifferentemente su più browser ed abbiamo trovato una soluzione a questo, non ci rimane altro che fare un passo in più andando ad affrontare, come se non bastasse il panorama presentato sino ad ora, il tema dei "dialetti" (in senso figurato) di JavaScript.

Si perchè TypeScript, CoffeeScript, Dart ed altri linguaggi più o meno famosi, vengono chiamati, in gergo tecnico, compile to JavaScript languages (linguaggi che, una volta compilati, restituiscono JavaScript).
Ma quale necessità ha dato il via alla loro creazione?

Le applicazioni moderne che girano nei vari browser hanno requisiti diversi da semplici siti Web, ma qualsiasi app che deve essere eseguita al loro interno necessita di essere implementata in quel linguaggio.

JavaScript ha molti difetti (che magari discuteremo in un altro articolo) e quando si tratta di costruire applicazioni complesse potrebbe non essere sufficiente.
Proprio per evitare questo problema, sono stati creati diversi nuovi linguaggi: tutti producono codice che, una volta compilato/transpilato, può funzionare nel browser senza che lo sviluppatore di turno abbia dovuto scrivere alcuna riga di JavaScript o si sia dovuto preoccupare dei limiti del medesimo!

I vantaggi che introduce TypeScript

Nell'azienda per cui lavoro, passando a React abbiamo iniziato ad utilizzare TypeScript (TS è il suo diminutivo) per cui di tutti i linguaggi compile-to-js, esso è quello riguardo al quale posso esprimere dei concetti.

JavaScript fu sviluppato con lo scopo di essere ovviamente un linguaggio lato client, allora non si sapeva che sarebbe diventato disponibile anche per la programmazione lato server e non si poteva prevedere il seguito che avrebbe ottenuto o come le applicazioni web sarebbero evolute divenendo sempre più esigienti. Con questo tipo di crescita, il codice di applicazioni non banali divenne sempre più complesso e pesante.

TypeScript è stato sviluppato per colmare diverse lacune: è noto come linguaggio di programmazione orientato agli oggetti (a differenza di JavaScript che è un linguaggio di scripting), prende come base ES6 ed introduce, tra le altre cose, il type checking statico (controllo dei tipi statico), i tipi generici, il supporto ai moduli e fornisce la possibilità di definire interfacce.
Qualsiasi cosa si possa scrivere in JS, la si può scrivere in TS.
Siccome TypeScript ha bisogno di essere transpilato per ottenere il codice JavaScript corrispondente, molti errori vengono catturati in fase di compilazione! Per questo motivo al momento della sua esecuzione, la possibilità di incorrere in errori è molto inferiore rispetto a puro JavaScript, che è un linguaggio interpretato.
Grazie al type checking statico, anche la correttezza dei tipi si può verificare in fase di compilazione, come nei più famosi linguaggi ad oggetti lato server:

// JavaScript
function getPassword(showPassword) {
    if (showPassword) {
        return 'password';
    }
    return '********';
}

let pw = getPassword('true'); // il valore di pw sarà "password"

Niente in JavaScript impedirà ad uno script di chiamare la funzione getPassword con parametri non validi (non booleani) che genereranno un errore a runtime. Questo può essere completamente evitato in fase di compilazione usando le type annotation (annotazioni di tipo) in TypeScript:

// TypeScript
function getPassword(showPassword: boolean) : string {
    if (showPassword) {
        return 'password';
    }
    return '********';
}

let pw = getPassword('true'); // lancia un errore dicendo che il valore '"true"' (stringa) non è assegnabile ad un parametro che richiede 'boolean'.

Questo esempio dimostra come possiamo impedire delle operazioni su oggetti di tipo inaspettato; storicamente una delle più grandi debolezze di JS che può causare risultati indesiderati ai meno esperti.

TypeScript non è altro che JavaScript con funzionalità aggiuntive.

Infatti TypeScript si dice essere un soprainsieme di JavaScript, la seguente figura serve per schiarire le idee:

Babel o TypeScript? 🤔

È comune sentir dire di aver scelto TypeScript per funzionalità come moduli e classi. Tuttavia, è importante comprendere che queste funzioni sono disponibili anche da ES6 in poi, ed è possibile utilizzare Babel per transpilare il tutto in ES5 per una maggiore compatibilità con tutti i browser.
A causa di questa confusione, ecco un rapido confronto della sintassi per alcune delle funzionalità introdotte in ES6.

Moduli

// TypeScript
export default class Automobile { }

// Output compilato di TypeScript
define(["require", "exports"], function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    var Automobile = /** @class */ (function () {
        function Automobile() {
        }
        return Automobile;
    }());
    exports.default = Automobile;
});


// JavaScript con Babel
export default class Automobile { }

// Output compilato di Babel
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Automobile = function Automobile() {
  _classCallCheck(this, Automobile);
};

exports.default = Automobile;


Classi

// TypeScript
class Automobile {
    modello: string;

    constructor(modello: string) {
        this.modello = modello;
    }
}

// Output compilato di TypeScript
var Automobile = /** @class */ (function () {
    function Automobile(modello) {
        this.modello = modello;
    }
    return Automobile;
}());


// JavaScript con Babel
class Automobile {
    constructor(modello) {
        this.modello = modello;
    }
}

// Output compilato di Babel
"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Automobile = function Automobile(modello) {
    _classCallCheck(this, Automobile);

    this.modello = modello;
};


Conclusioni

Babel e TypeScript non rappresentano lo stesso strumento, per cui la domanda posta nel titolo del paragrafo precedente non ha molto senso: Babel è un transpiler, TypeScript è un superset di JavaScript.

Alcuni concetti di Babel vengono effettivamente introdotti in TypeScript e se siamo costretti ad effettuare una scelta tra i due, nel caso in cui tutto ciò di cui si ha bisogno è compatibilità tra i vari browser utilizzando però le ultime funzionalità introdotte da ECMA allora la scelta è Babel.

Se ciò che si cerca è un linguaggio più strutturato, orientato agli oggetti, che garantisca un netto miglioramento in termini di error catching a compile time, allora la scelta è senz'altro TypeScript.

Infine, quello che mi preme sottolineare è che non necessariamente i due sono in mutua esclusione, potenzialmente è possibile avere codice TypeScript trasformato in ES6 dal suo compilatore e poi utilizzare Babel per transpilare nuovamente tale output in ES5: questo è esattamente quello che facciamo a Sysdig in questo momento di transizione, dove non ancora tutta la codebase è in TypeScript ed abbiamo moltissimi file scritti in ES6 prodotti durante l'utilizzo del framework precedente.

E tu, cosa hai scelto per il tuo progetto? 🤓