DOVEROSA PREMESSA
Questo è uno di quegli articoli, e lo scrivo con una certa soddisfazione, dove si costruisce un ragionamento più sensato del previsto partendo da una colossale stupidaggine.
La soddisfazione deriva dal fatto che i miei articoli sono spesso basati su elaborate e faticose argomentazioni, dove spesso manca quel pizzico di “shalla” che rende tutto il discorso più umano. Mi riferisco a quella libertà lasciata al lettore di interrompere la lettura quando preferisce, senza la paura di aver scelto l’aldilà in una questione di vita o di morte.
L’articolo sottoforma di chiaccherata, non di manuale delle istruzioni.
Però, la soddisfazione non è piena. Questo perché la possibilità di non riuscire a lasciare in chi mi segue la sensazione di aver imparato qualcosa proprio non la riesco a contemplare. Anche a costo di essere meno comprensibile di un tutorial in inglese tenuto da un indiano su YouTube.
Diciamocelo, mi piace essere tremendamente preciso e puntiglioso. Fin troppo.
Perciò, in un’ottica di crescita personale, mi sono ripromesso di provare ad allegerire il carico sul lettore, forte del seguente ragionamento mancante di senso: “Se tratto una tematica sensata, finisco per scrivere la mia versione delle specifiche del linguaggio. Quindi se, per una volta, mi basassi su una idiozia, a rigor di logica dovrei riuscire a non far venire il mal di testa a chi prova a darmi retta!”.
Questo significa che, d’ora in poi, i miei appuntamenti sul JavaScript saranno basati sul nulla assoluto? In medio stat virtus, dicevano i latini. In altre parole, no 😂. O meglio, in una sola altra parola. Vedete come sono puntiglioso? Proprio non riesco a smettere.
Semplicemente punto ad essere più leggero, più scorrevole, più amico. Perché un amico è un tesoro, e un amico sviluppatore che conosce il JavaScript è un tesorone.
Introduzione
Di cosa parleremo in questo appuntamento? Se non si era capito dal titolo, e spero vivamente di no, proveremo a dare un senso alla seguente pratica da deviati:
class MyClass extends null { // }
per poi lasciarci cullare da alcune sconsiderate considerazioni sull’ottimizzazione del codice in JavaScript.
EXTENDS NULL
È di fondamentale importanza, per lo sfortunato lettore, ricordare che stiamo parlando del JavaScript, un linguaggio mistico dove typeof null == "object"
e null == undefined
, dove le dichiarazioni risalgono in superficie peggio della plastica negli oceani e dove bisogna mettere minimo quattro o cinque =
in fila per essere sicuri al 100% di quello che si sta confrontando nelle operazioni di uguaglianza, a meno di non voler diventare Coercion Master (link al corso dettagliato e puntiglioso).
A nulla sono serviti i tentativi del dottor Axel Rauschmayer di far cambiare idea al TC39: si può ereditare da null, punto e basta! La cosa ha infatti perfettamente senso, inoltre sono troppo occupati a far finta di prendere in considerazione le proposal sul linguaggio per pensare di modificare le specifiche già approvate.
Ovviamente non dico questo perché continuano ad ignorare tutti i miei tentativi di comunicazione; a pensarci bene inviare quella mail con tutti i membri del comitato in CC non è stata una buona idea 🤔.
Passiamo oltre.
In poche parole quello che veramente succede dietro le quinte, quando una classe estende null, è che il legame implicito che ogni oggetto in JavaScript ha con la superclasse Object viene troncato. Come promesso evito spiegazioni troppo dettagliate, che chiamerebbero in causa i famigerati prototype e avrebbero da ridire sull’aver osato definire Object una superclasse.
Gli antichi, che adoperavano una forma primitiva del linguaggio (per gli amici antichi ES5), erano soliti, in alcune circostanze particolari, creare oggetti nel seguente modo:
var obj = Object.create(null);
L’oggetto obj è un oggetto a tutti gli effetti, con l’unica differenza che risiede nel legame mancante con Object. Nel caso in cui l’oggetto venisse usato semplicemente come dizionario rendendo superflui metodi come toString,
Nessun legame con Object, nessun ripiego, codice più veloce.
Potete avere conferma dell’esatto contrario di quanto detto finora dando un’occhiata al seguente test, dove, IE a parte, sembra proprio che il ragionamento non abbia riscontro alcuno nella realtà. Sulla carta la rimozione del collegamento ad Object dovrebbe aumentare le performance dato che implica un lookup più veloce.
A dirla tutta il numero di test eseguiti con browser meno recenti andrebbe aumentato, ma anche se trovassimo prove evidenti del precedente ragionamento, dobbiamo arrenderci al fatto che nei nostri giorni non è più valido, sebbene le specifiche del JS non siano cambiate sotto questo aspetto.
Un secondo tentativo può essere fatto grazie alle classi regalateci malvolentieri da ES6, con le quali potremmo finire a creare una struttura più o meno simile alla seguente:
class Hash extends null { constructor() { return Object.create(Hash.prototype) } get length() { return Object.keys(this).length; } clear() { Object.keys(this).forEach(key => delete this[key]); } }
dove sono stati aggiunti un paio di metodi giusto per dare un senso all’esistenza della classe.
Sicuramente è il codice presente nel costruttore a risultarci abbastanza esotico, e pare proprio che abbia tutte le carte in regola per farlo: niente chiamata a super (non era obbligatoria?), restituisce qualcosa (ditemi l’ultima volta che avete forzato il costruttore di una classe a restituire qualcosa) e osa addirittura utilizzare il primitivo Object.create. Fossi in voi mi indignerei.
Invece le specifiche del JS indicano che super non è affatto obbligatorio se il costruttore di una classe derivata è forzato a restituire un oggetto. La cosa interessante è che siamo obbligati a fare uso di questa forzatura, perché se super venisse invocato, implicitamente o esplicitamente, otterremo un sonoro errore. Errore sul quale non mi dilungo per rispettare la mia promessa iniziale, ma che fa venire voglia di chiedere al TC39 cosa fumano e, soprattutto, se ne hanno un po’ per noi che siamo costretti ad avere a che fare con i loro capricci.
Ad ogni modo vogliamo che l’oggetto restituito dal costruttore mantenga un legame con la classe Hash, legame ottenuto tramite l’invocazione a Object.create avente per argomento proprio uno di quei spregevoli prototype. Avendo esteso null, la classe Hash non è più collegata ad Object, perciò gli oggetti collegati ad Hash saranno liberi dal legame indesiderato con la somma classe.
Dove voglio arrivare? A questi due test (1 e 2) dove col cavolo che la versione senza legame con Object è più veloce di quella che lo ha.
CONSIDERAZIONI FINALI
La considerazione, molto personale ma sulla scia di quella presentata dal mitico Aldo in questo articolo, è che l’ottimizzazione di una certa parte del codice è un’attività che ormai rasenta l’inutilità, specialmente nel caso di linguaggi pseudointerpretacompilati come il JavaScript. Ovviamente non mi riferisco alle ottimizzazioni degli algoritmi, che come ha già ben sottolineato il mio collega devono avere la priorità, almeno finché non inventiamo un processore a frequenza infinita.
Mi riferisco a film mentali come la differenza tra i++ o ++i, tra una map e una reduce, tra Number e parseInt, tra l’invocazione a Object.assign rispetto all’object spread, e numerosi altri presunti problemi di efficienza che compaiono e scompaiono periodicamente.
Non fraintendetemi, è giusto informarsi e mettere in pratica le best practice quando esse sono basate su valide motivazioni, ma a meno di necessità impellenti non è saggio farne una questione di vita. In particolar modo, non è saggio peggiorare la leggibilità del codice in nome di una presunta maggior efficenza, ne perderci tempo e risorse che dovremmo spendere anzi nel seguire gli eterni principi del codice pulito e mantenibile.
Se già in un linguaggio come il C è difficile riuscire a superare il compilatore in quanto abilità ed astuzia, figuriamoci quanto è difficile farlo con il JavaScript, dove dovremmo conoscere di volta in volta il comportamento dei JIT compilers e degli engine che eseguono il codice, i quali variano da browser a browser e da versione a versione del medesimo browser.
Da un interessante articolo come questo, dove Mathias Bynens ci illustra alcune particolarità in comune ai più recenti engine, a mio avviso possiamo portarci a casa due punti importanti:
- Gli engine che eseguono il JS fanno delle assunzioni che permettono di velocizzare, notevolmente, alcune tra le più comuni operazioni. Perciò del codice che sulla carta dovrebbe essere più performante, potrebbe non rivelarsi tale e viceversa.
- Il JavaScript si evolve, gli engine vengono aggiornati continuamente e nuove conquiste vengono raggiunte ogni giorno. Codice JavaScript che ieri era meno performante potrebbe essere diventato dieci volte più veloce e viceversa. Dato che nessuno ha la sfera di cristallo, non possiamo sapere con assoluta certezza quali modifiche potrebbero venire apportate in futuro, che inevitabilmente potrebbero intaccare gli sforzi che compiamo oggi.
CONCLUSIONE DI GRANDE EFFETTO
Qualsiasi sia il vostro pensiero, spero almeno di avervi convinto del fatto che estendere il null è una delle migliori idiozie che questo stupendo linguaggio ci lascia fare.