Sintassi Estesa per la creazione di oggetti in Javascript

Le versioni recenti di JavaScript hanno esteso la sintassi per creare oggetti letterali in modo da semplificare questa operazione. In questa lezione, vedremo alcune di queste estensioni.

Concetti Chiave
  • Le versioni recenti di JavaScript hanno esteso la sintassi per creare oggetti letterali, rendendo il codice più conciso e leggibile.
  • Le proprietà abbreviate consentono di omettere i due punti e il nome della variabile quando il nome della proprietà corrisponde al nome della variabile.
  • I nomi di proprietà calcolati permettono di utilizzare espressioni JavaScript come nomi di proprietà all'interno di un oggetto letterale.
  • I simboli possono essere utilizzati come nomi di proprietà per garantire l'unicità e prevenire conflitti tra proprietà.
  • L'operatore spread ... consente di copiare le proprietà di un oggetto esistente in un nuovo oggetto, semplificando la creazione di oggetti combinati.

Proprietà Abbreviate

Supponiamo di avere dei valori memorizzati nelle variabili x e y e di voler creare un oggetto con proprietà denominate x e y che contengano quei valori. Con la sintassi di base per i letterali di oggetto, finiremmo per ripetere ogni identificatore due volte.

Infatti, dovremmo scrivere qualcosa del genere:

let x = 1;
let y = 2;

// Creazione di un oggetto con proprietà x e y
let o = {
    x: x,
    y: y
};

In ES6 e versioni successive, possiamo omettere i due punti e una copia dell'identificatore e ottenere un codice molto più semplice e compatto

let x = 1;
let y = 2;

// Creazione di un oggetto con proprietà x e y
let o = {
    x,
    y
};

Nomi di Proprietà Calcolati

A volte abbiamo bisogno di creare un oggetto con una proprietà specifica, ma il nome di quella proprietà non è una costante al momento della compilazione che possiamo scrivere letteralmente nel nostro codice sorgente. Invece, il nome della proprietà di cui abbiamo bisogno è memorizzato in una variabile o è il valore di ritorno di una funzione che invochiamo. Non possiamo usare un oggetto letterale di base per questo tipo di proprietà. Invece, dobbiamo creare un oggetto e poi aggiungere le proprietà desiderate come passaggio aggiuntivo:

const NOME_PROPRIETA = "p1";
function calcolaNomeProprieta() {
    return "p" + 2;
}

let o = {};
o[NOME_PROPRIETA] = 1;
o[calcolaNomeProprieta()] = 2;

Questo codice risulta in un oggetto o con due proprietà, p1 e p2, che hanno i valori 1 e 2 rispettivamente. Possiamo accedere a queste proprietà come al solito:

o.p1 + o.p2 // => 3

Il problema è che questo codice è poco leggibile se non criptico. Non è immediatamente chiaro che stiamo creando un oggetto con due proprietà, e non è chiaro quali siano i nomi delle proprietà. Inoltre, se vogliamo creare un oggetto con molte proprietà, questo codice diventa rapidamente ingombrante e difficile da leggere.

È molto più semplice impostare un oggetto come questo con una funzionalità di ES6 conosciuta come proprietà calcolate che ci permette di prendere le parentesi quadre dal codice precedente e spostarle direttamente nell'oggetto letterale:

const NOME_PROPRIETA = "p1";
function calcolaNomeProprieta() { return "p" + 2; }

let p = {
    [NOME_PROPRIETA]: 1,
    [calcolaNomeProprieta()]: 2
};

p.p1 + p.p2 // => 3

Con questa nuova sintassi, le parentesi quadre delimitano un'espressione JavaScript arbitraria. Quell'espressione viene valutata, e il valore risultante (convertito in stringa, se necessario) viene usato come nome della proprietà.

Una situazione in cui potremmo voler usare le proprietà calcolate è quando abbiamo una libreria di codice JavaScript che si aspetta di ricevere oggetti con un particolare insieme di proprietà, e i nomi di quelle proprietà sono definiti come costanti in quella libreria. Se stiamo scrivendo codice per creare gli oggetti che verranno passati a quella libreria, potremmo codificare direttamente i nomi delle proprietà, ma rischieremmo di introdurre dei bug se digitassimo il nome della proprietà sbagliando in qualche punto, e rischieremmo problemi di incompatibilità di versione se una nuova versione della libreria cambiasse i nomi delle proprietà richiesti. Invece, potremmo trovare che usare la sintassi delle proprietà calcolate con le costanti dei nomi delle proprietà definite dalla libreria rende il nostro codice più robusto.

Simboli come Nomi di Proprietà

La sintassi delle proprietà calcolate abilita un'altra caratteristica molto importante dei letterali di oggetto.

In ES6 e versioni successive, i nomi delle proprietà possono essere stringhe o simboli. Se assegniamo un simbolo a una variabile o costante, possiamo utilizzare quel simbolo come nome di proprietà usando la sintassi delle proprietà calcolate:

const estensione = Symbol("il mio simbolo di estensione");

let o = {
    [estensione]: {
        /* dati di estensione memorizzati in questo oggetto */
    }
};

// Questo non andrà in conflitto con altre proprietà di o
o[estensione].x = 0;

Come spiegato in precedenza i Simboli sono valori opachi. Non possiamo fare nulla con essi se non utilizzarli come nomi di proprietà. Ogni Simbolo è diverso da ogni altro Simbolo. Ciò significa che i Simboli sono adatti per creare nomi di proprietà unici. Creiamo un nuovo Simbolo chiamando la funzione factory Symbol(). I Simboli sono valori primitivi, non oggetti, quindi Symbol() non è una funzione costruttore che invochiamo con new.

Il valore restituito da Symbol() non è uguale a nessun altro Simbolo o altro valore. Possiamo passare una stringa a Symbol(), e questa stringa viene utilizzata quando il nostro Simbolo viene convertito in stringa. Ma questo è solo un aiuto per il debug: due Simboli creati con lo stesso argomento stringa sono ancora diversi l'uno dall'altro.

Il punto dei Simboli non è la sicurezza, ma definire un meccanismo di estensione sicuro per gli oggetti JavaScript. Se otteniamo un oggetto da codice di terze parti che non controlliamo e dobbiamo aggiungere alcune delle nostre proprietà a quell'oggetto ma vogliamo essere sicuri che le nostre proprietà non andranno in conflitto con eventuali proprietà che potrebbero già esistere sull'oggetto, possiamo utilizzare in sicurezza i Simboli come nomi delle nostre proprietà. Se facciamo questo, possiamo anche essere certi che il codice di terze parti non altererà accidentalmente le nostre proprietà denominate simbolicamente. Quel codice di terze parti potrebbe, naturalmente, utilizzare Object.getOwnPropertySymbols() per scoprire i Simboli che stiamo utilizzando e potrebbe quindi alterare o eliminare le nostre proprietà. Questo è il motivo per cui i Simboli non sono un meccanismo di sicurezza.

Operatore Spread

Da ES2018 e versioni successive, possiamo copiare le proprietà di un oggetto esistente in un nuovo oggetto utilizzando l'operatore spread ... all'interno di un letterale oggetto:

let posizione = {
    x: 0,
    y: 0
};

let dimensioni = {
    larghezza: 100,
    altezza: 75
};

let rettangolo = {
    ...posizione,
    ...dimensioni
};

// Adesso rettangolo ha le proprietà x, y, larghezza e altezza
console.log(rettangolo); // { x: 0, y: 0, larghezza: 100, altezza: 75 }

// Calcola l'area del rettangolo
let area = rettangolo.larghezza * rettangolo.altezza;
console.log(area); // 7500

// Trasla il rettangolo
rettangolo.x += 10;
rettangolo.y += 20;

In questo codice, le proprietà degli oggetti posizione e dimensioni vengono sparse nel letterale dell'oggetto rettangolo come se fossero state scritte letteralmente all'interno di quelle parentesi graffe. Notiamo che questa sintassi ... viene spesso chiamata operatore spread ma non è un vero operatore JavaScript in nessun senso. Invece, è una sintassi di caso speciale disponibile solo all'interno dei letterali oggetto. I tre punti vengono utilizzati per altri scopi in altri contesti JavaScript, ma i letterali oggetto sono l'unico contesto dove i tre punti causano questo tipo di interpolazione di un oggetto in un altro.

Se l'oggetto che viene sparso e l'oggetto in cui viene sparso hanno entrambi una proprietà con lo stesso nome, allora il valore di quella proprietà sarà quello che viene per ultimo:

let o = {
    x: 1
};

let p = {
    x: 0,
    ...o
};

p.x   // => 1: il valore dall'oggetto o sovrascrive il valore iniziale

let q = {
    ...o,
    x: 2
};

q.x   // => 2: il valore 2 sovrascrive il valore precedente da o.

Notiamo anche che l'operatore spread sparge solo le proprietà proprie di un oggetto, non quelle ereditate:

let o = Object.create({x: 1}); // o eredita la proprietà x
let p = { ...o };
p.x                            // => undefined

Infine, vale la pena notare che, anche se l'operatore spread può sembrare innocuo, può rappresentare una quantità sostanziale di lavoro per l'interprete JavaScript. Se un oggetto ha n proprietà, il processo di spargere quelle proprietà in un altro oggetto è probabilmente un'operazione O(n). Questo significa che se ci troviamo a utilizzare ... all'interno di un ciclo o funzione ricorsiva come modo per accumulare dati in un grande oggetto, potremmo scrivere un algoritmo inefficiente O(n^2) che non scala bene quando n diventa più grande.