Un Symbol è un valore unico garantito
Symbol è uno dei tipi primitivi di JavaScript, insieme a string, number, boolean, null, undefined e bigint. La sua caratteristica principale è semplice: ogni symbol che crei è diverso da qualsiasi altro symbol, per sempre.
Due symbol, entrambi creati senza argomenti, eppure non sono uguali. Non è un caso: è proprio lo scopo. Non puoi falsificare un symbol, ricrearne uno per sbaglio o entrare in conflitto con quello di qualcun altro.
Puoi aggiungere una descrizione per facilitare il debug. Non cambia l'identità:
Stessa descrizione, symbol diversi. La descrizione è solo un'etichetta per chi legge i log.
Perché esistono: chiavi senza conflitti
Il motivo principale per cui esistono i symbol è permetterti di aggiungere proprietà a un oggetto senza preoccuparti che la tua chiave si scontri con quella di qualcun altro. Le chiavi stringa competono in un unico spazio dei nomi piatto: se due librerie decidono entrambe di salvare metadati in obj.meta, si pestano i piedi a vicenda. Le chiavi symbol non competono, perché nessun altro ha il tuo symbol.
Le parentesi quadre in [ID]: 42 sono la sintassi delle proprietà calcolate: dicono "usa il valore di ID come chiave". Solo il codice che possiede lo stesso symbol ID può leggere o sovrascrivere quella proprietà. Qualsiasi altro modulo che usa la stringa "id" o il proprio Symbol("id") sta parlando di uno spazio completamente diverso.
Le chiavi symbol sono (quasi) nascoste
Le proprietà con chiave symbol non compaiono in for...in, Object.keys o JSON.stringify. Non sono segrete, perché un codice determinato può comunque trovarle, ma restano fuori dai piedi durante la normale iterazione.
Object.keys e JSON.stringify ignorano del tutto la chiave symbol. Se vuoi trovare di proposito le proprietà symbol, Object.getOwnPropertySymbols e Reflect.ownKeys te le mostrano. È esattamente il comportamento che vuoi per i metadati: visibili quando li cerchi, invisibili al codice che scorre solo le chiavi stringa.
Condividere symbol con Symbol.for
Symbol() ti dà un symbol locale e usa e getta. A volte ti serve il contrario: un symbol che sia lo stesso in tutto il programma, tra moduli diversi, così che parti diverse del codice possano accordarsi su una chiave. È a questo che serve Symbol.for.
Symbol.for(key) controlla un registro globale: se esiste già un symbol per quella chiave, ti restituisce quello; altrimenti ne crea uno e lo salva. Symbol.keyFor fa il percorso inverso: restituisce la chiave con cui è stato creato un symbol registrato (oppure undefined per quelli non registrati).
Nella maggior parte dei casi ti basta il semplice Symbol(). Usa Symbol.for quando un symbol deve davvero essere condiviso oltre i confini di un modulo.
Well-known symbol: agganci al linguaggio
Ecco dove i symbol si guadagnano il posto. JavaScript espone una serie di symbol predefiniti, i cosiddetti well-known symbol, che il linguaggio stesso cerca nei tuoi oggetti. Implementa un metodo a una di queste chiavi e il tuo oggetto si collega direttamente a una funzionalità integrata.
Il più utile è Symbol.iterator. Qualsiasi oggetto che abbia un metodo a questa chiave è iterabile: funziona con for...of, lo spread, il destructuring e Array.from.
range non è un array. È un semplice oggetto con un metodo dalla chiave speciale. Ma dato che quel metodo restituisce un iteratore, sia for...of sia lo spread funzionano su di esso, esattamente come funzionano su array, stringhe e Map. Il contratto è questo: implementa Symbol.iterator e il linguaggio ti tratta come iterabile.
Altri well-known symbol che vale la pena conoscere
Ce ne sono diversi altri, ognuno un aggancio a una parte diversa del linguaggio:
Symbol.iterator: rende un oggetto iterabile.Symbol.asyncIterator: lo stesso, ma perfor await...of.Symbol.toPrimitive: controlla come un oggetto viene convertito in primitivo (numero, stringa o default) nei contesti di coercizione.Symbol.hasInstance: personalizza cosa restituisceinstanceofper la tua classe.Symbol.toStringTag: imposta l'etichetta mostrata daObject.prototype.toString.call(obj).
Non serve impararli a memoria. Basta sapere che esistono: quando ti capita di volere che il tuo oggetto si comporti come un tipo integrato, di solito c'è un well-known symbol apposta.
I symbol non sono stringhe
Un errore comune: i symbol non si convertono automaticamente in stringhe. Concatenarne uno lancia un errore, e lo stesso succede se lo passi a un'API che si aspetta una stringa:
Anche i template literal lanciano lo stesso TypeError. Usa String(symbol) o symbol.toString() quando vuoi davvero del testo. Questi spigoli sono voluti: il linguaggio ti protegge dal trattare per sbaglio un valore di identità unica come un qualsiasi dato testuale.
Quando usare i symbol (e quando no)
Usa i symbol quando:
- Aggiungi metadati a oggetti che non controlli e ti serve una chiave che non entri in conflitto.
- Stai progettando un protocollo: "gli oggetti che vogliono funzionare con la mia libreria devono implementare un metodo a questo symbol."
- Vuoi una proprietà che non finisca in
JSON.stringifyo infor...in. - Stai implementando
Symbol.iteratoro un altro well-known symbol.
Lascia perdere i symbol quando:
- Ti serve solo una chiave di oggetto e non c'è rischio di conflitti. Una stringa è più semplice e nei log appare così com'è.
- Vuoi campi "privati". Le proprietà con chiave symbol non sono davvero private:
Object.getOwnPropertySymbolsle trova. Per una vera privacy usa i campi di classe#private. - Stai salvando dati che devono sopravvivere a
JSON.stringify. Non sopravvivono.
La maggior parte del codice JavaScript se la cava benissimo senza mai scrivere Symbol(...) direttamente. Ma nel momento in cui vuoi che un tuo oggetto funzioni con for...of o si comporti in modo naturale in un contesto di coercizione, i symbol sono la porta d'ingresso a quei meccanismi.
Prossimo passo: la dichiarazione di funzioni
Symbol, iteratori e generatori si appoggiano molto sulle funzioni: i metodi salvati in Symbol.iterator, le factory che restituiscono iteratori, i generatori la cui forma function* nasconde gran parte del codice ripetitivo. Le funzioni sono il prossimo capitolo, a partire dai diversi modi di dichiararle e da cosa cambia tra una forma e l'altra.
Domande frequenti
Cos'è un symbol in JavaScript?
Un symbol è un tipo primitivo i cui valori sono garantiti unici. Lo crei con Symbol() o Symbol('descrizione'), e due chiamate non producono mai symbol uguali. Si usano soprattutto come chiavi di oggetto che non entrano in conflitto con altre chiavi, e come agganci alle funzionalità del linguaggio tramite i well-known symbol come Symbol.iterator.
Quando conviene usare un symbol invece di una chiave stringa?
Usa un symbol quando vuoi aggiungere una proprietà a un oggetto senza rischiare un conflitto di nomi con altro codice: librerie che aggiungono metadati, framework che etichettano oggetti, o chiavi che di proposito non fanno parte dell'API pubblica. Per le chiavi di tutti i giorni, dove conta la leggibilità e i conflitti non sono un problema, le stringhe restano la scelta giusta.
A cosa serve Symbol.iterator?
Symbol.iterator è un well-known symbol che dice a for...of, allo spread e al destructuring come iterare un oggetto. Se definisci alla chiave Symbol.iterator un metodo che restituisce un iteratore, il tuo oggetto diventa iterabile. È così che funzionano dietro le quinte array, stringhe, Map e Set.
Qual è la differenza tra Symbol() e Symbol.for()?
Symbol('x') crea ogni volta un symbol nuovo: due chiamate con la stessa descrizione restano diverse. Symbol.for('x') consulta invece un registro globale e restituisce lo stesso symbol per la stessa chiave in tutto il programma. Usa Symbol.for quando devi condividere i symbol tra moduli o realm, e Symbol() quando ti basta l'unicità locale.