Nota degli editori: Questo articolo contiene delle lezioni esempio tratte da Learn JavaScript, un corso che vi aiuta a imparare JavaScript per realizzare da zero dei component per il mondo reale.
Mutare significa cambiare forma o natura. Qualcosa che è mutabile può essere cambiato mentre qualcosa che è immutabile non può cambiare. Per comprendere la mutazione, pensate agli X-Men. In X-Men, le persone possono improvvisamente assumere dei poteri. Il problema è che non sapete quando emergeranno questi poteri. Immaginatevi il vostro amico che diventa blu e che tutto a un tratto ha la pelliccia: può essere spaventoso, vero?
In JavaScript, c’è questo stesso problema della mutazione. Se il vostro codice è mutabile, potreste cambiare (e rompere) qualcosa senza saperlo.
In JavaScript, gli oggetti sono mutabili#section1
In JavaScript, potete aggiungere delle proprietà a un oggetto. Quando lo fate dopo averlo istanziato, l’oggetto è cambiato per sempre. Muta, come muta un membro degli X-Men quando guadagna un potere.
Nell’esempio seguente, la variabile egg
muta quando gli si aggiunge la proprietà isBroken
. Diciamo che gli oggetti (come egg
) sono mutabili (hanno la capacità di mutare).
const egg = { name: "Humpty Dumpty" };
egg.isBroken = false;
console.log(egg);
// {
// name: "Humpty Dumpty",
// isBroken: false
// }
La mutazione è piuttosto normale in JavaScript. La usate sempre.
Ecco quando la mutazione diventa spaventosa.
Supponiamo di creare una constant variable chiamata newEgg
e le assegnamo egg
. Poi vogliamo cambiare il nome di newEgg
con qualcosa d’altro.
const egg = { name: "Humpty Dumpty" };
const newEgg = egg;
newEgg.name = "Errr ... Not Humpty Dumpty";
Quando cambiate (mutate) newEgg
, sapevate che egg
viene mutato automaticamente?
console.log(egg);
// {
// name: "Errr ... Not Humpty Dumpty"
// }
L’esempio precedente illustra perché la mutazione può fare paura: quando cambiate un pezzo del vostro codice, un altro pezzo può cambiare da un’altra parte senza che lo sappiate. Come risultato, otterrete dei bug difficili da tracciare e sistemare.
Questo strano comportamento avviene perché in JavaScrip gli oggetti vengono passati per riferimento.
In JavaScript gli oggetti sono passati per riferimento#section2
Per capire cosa significa “passato per riferimento”, per prima cosa dovete capire che ogni oggetto ha un’identità unica in JavaScript. Quando assegnate un oggetto a una variabile, collegate la variabile all’identità dell’oggetto (ossia, la passate per riferimento) piuttosto che assegnare la variabile direttamente al valore dell’oggetto. Questo è il motivo per cui quando confrontate due oggetti diversi, ottenete false
anche se gli oggetti hanno lo stesso valore.
console.log({} === {}); // false
Quando assegnate egg
a newEgg
, newEgg
punta allo stesso oggetto di egg
. Dal momento che egg
e newEgg
sono la stessa cosa, quando cambiate newEgg
, egg
viene cambiato automaticamente.
console.log(egg === newEgg); // true
Sfortunatamente, la maggior parte delle volte, non vorrete che egg
cambi con newEgg
, dal momento che causa la rottura del vostro codice quanto meno ve lo aspettereste. Quindi, come evitiamo che gli oggetti mutino? Prima di comprendere come prevenire la mutazione degli oggetti, dovete sapere cosa è immutabile in JavaScript.
I primitivi sono immutabili in JavaScript#section3
In JavaScript, i primitivi (String, Number, Boolean, Null, Undefined e Symbol) sono immutabili: non potete cambiare la struttura (aggiungere proprietà o metodi) di un primitivo. Non succede nulla anche se cercate di aggiungere proprietà a un primitivo.
const egg = "Humpty Dumpty";
egg.isBroken = false;
console.log(egg); // Humpty Dumpty
console.log(egg.isBroken); // undefined
const
non garantisce l’immutabilità#section4
Molte persone pensano che le variabili dichiarate con const
siano immutabili. Questa è una supposizione incorretta.
Dichiarare una variabile con const
non la rende immutabile: impedisce che gli assegniate un altro valore.
const myName = "Zell";
myName = "Triceratops";
// ERROR
Quando dichiarate un oggetto con const
, vi è ancora permesso far mutare l’oggetto. Nell’esempio egg
precedente, anche se egg
è creato con const
, const
non impedisce che egg
muti.
const egg = { name: "Humpty Dumpty" };
egg.isBroken = false;
console.log(egg);
// {
// name: "Humpty Dumpty",
// isBroken: false
// }
Impedire che gli oggetti mutino#section5
Potete usare Object.assign
e assignment per impedire che gli oggetti mutino.
Object.assign
#section6
Object.assign
vi permette di combinare due (o più) oggetti insieme in uno singolo. Ha la sintassi seguente:
const newObject = Object.assign(object1, object2, object3, object4);
newObject
conterrà delle proprietà da tutti gli oggetti che avete passato in Object.assign
.
const papayaBlender = { canBlendPapaya: true };
const mangoBlender = { canBlendMango: true };
const fruitBlender = Object.assign(papayaBlender, mangoBlender);
console.log(fruitBlender);
// {
// canBlendPapaya: true,
// canBlendMango: true
// }
Se si trovano due proprietà in contrasto, la proprietà in un oggetto seguente sovrascrive la proprietà in un oggetto precedente (nei parametri di Object.assign
parameters).
const smallCupWithEar = {
volume: 300,
hasEar: true
};
const largeCup = { volume: 500 };
// In this case, volume gets overwritten from 300 to 500
const myIdealCup = Object.assign(smallCupWithEar, largeCup);
console.log(myIdealCup);
// {
// volume: 500,
// hasEar: true
// }
Ma attenzione! Quando combinate due oggetti con Object.assign
, il primo oggetto viene mutato. Altri oggetti non vengono mutati.
console.log(smallCupWithEar);
// {
// volume: 500,
// hasEar: true
// }
console.log(largeCup);
// {
// volume: 500
// }
Risolvere il problema di mutazione di Object.assign
#section7
Potete passare un nuovo oggetto come vostro primo oggetto per impedire che gli oggetti esistenti mutino. Muterete ancora il primo oggetto però (l’oggetto vuoto), ma va bene perché questa mutazione non interessa null’altro.
const smallCupWithEar = {
volume: 300,
hasEar: true
};
const largeCup = {
volume: 500
};
// Using a new object as the first argument
const myIdealCup = Object.assign({}, smallCupWithEar, largeCup);
Potete mutare il vostro nuovo oggetto in qualunque modo vogliate d’ora in poi. Non interesserà nessuno dei vostri oggetti precedenti.
myIdealCup.picture = "Mickey Mouse";
console.log(myIdealCup);
// {
// volume: 500,
// hasEar: true,
// picture: "Mickey Mouse"
// }
// smallCupWithEar doesn't get mutated
console.log(smallCupWithEar); // { volume: 300, hasEar: true }
// largeCup doesn't get mutated
console.log(largeCup); // { volume: 500 }
Ma Object.assign
copia i riferimenti agli oggetti#section8
Il problema di Object.assign
è che fa uno shallow merge: copia le proprietà direttamente da un oggetto all’altro. Quando lo fa, copia anche i riferimenti a qualsiasi oggetto.
Spieghiamo questa affermazione con un esempio.
Supponiamo che compriate un nuovo sound system. Il sistema vi permette di dichiarare se l’alimentazione è accesa. Vi permette anche di impostare il volume, la quantità di bassi e altre opzioni.
const defaultSettings = {
power: true,
soundSettings: {
volume: 50,
bass: 20,
// other options
}
};
A qualche vostro amico piace la musica rumorosa, quindi decidete di creare un preset che di sicuro sveglierà i vostri vicini quando dormono.
const loudPreset = {
soundSettings: {
volume: 100
}
};
Poi invitate i vostri amici per una festa. Per preservare i vostri preset esistenti, cercate di combinare il preset rumoroso con quello di default.
const partyPreset = Object.assign({}, defaultSettings, loudPreset);
Ma partyPreset
suona strano: il volume è sufficientemente alto ma i bassi sono inesistenti. Quando ispezionate partyPreset
, siete sorpresi di non trovarci alcun basso!
console.log(partyPreset);
// {
// power: true,
// soundSettings: {
// volume: 100
// }
// }
Questo succede perché JavaScript copia il riferimento all’oggetto soundSettings
. Dal momento che sia defaultSettings
sia loudPreset
hanno un oggetto soundSettings
, quello che viene dopo viene copiato nel nuovo oggetto.
Se cambiate partyPreset
, loudPreset
muterà di conseguenza: prova del fatto che il riferimento a soundSettings
viene copiato.
partyPreset.soundSettings.bass = 50;
console.log(loudPreset);
// {
// soundSettings: {
// volume: 100,
// bass: 50
// }
// }
Dal momento che Object.assign
fa uno shallow merge, dovete usare un altro metodo per fare il merge degli oggetti che contengono delle proprietà annidate (ossia, oggetti all’interno di oggetti).
Entra in scena assignment.
assignment#section9
assignment è una piccola libreria creata da Nicolás Bevacqua di Pony Foo, un’ottima fonte di conoscenza JavaScript. Vi aiuta a fare un deep merge senza dovervi preoccupare della mutazione. A parte il nome del metodo, la sintassi è la stessa di Object.assign
.
// Perform a deep merge with assignment
const partyPreset = assignment({}, defaultSettings, loudPreset);
console.log(partyPreset);
// {
// power: true,
// soundSettings: {
// volume: 100,
// bass: 20
// }
// }
assignment copia i valori di tutti gli oggetti annidati, il che impedisce agli oggetti esistenti di venir mutati.
Se cercate di cambiare una qualsiasi proprietà in partyPreset.soundSettings
adesso, vedrete che loudPreset
rimarrà com’era.
partyPreset.soundSettings.bass = 50;
// loudPreset doesn't get mutated
console.log(loudPreset);
// {
// soundSettings {
// volume: 100
// }
// }
assignment è solo una della molte librerie che vi aiutano a fare un deep merge. Anche altre librerie, incluse lodash.merge e merge-options, possono aiutarvi a farlo. Sentitevi liberi di scegliere una di queste librerie.
Dovete sempre usare assignment invece di Object.assign
?#section10
Fintanto che sapete come impedire che i vostri oggetti mutino, potete usare Object.assign
. Non ci sono conseguenze nell’usarlo fintanto che sapete come usarlo in maniera appropriata.
Tuttavia, se dovete assegnare oggetti con proprietà annidate, preferite sempre un deep merge a Object.assign
.
Assicurarsi che gli oggetti non mutino#section11
Sebbene i metodi citati possano aiutarvi a prevenire le mutazioni degli oggetti, non garantiscono che gli oggetti non mutino. Se avete fatto un errore e avete usato Object.assign
per un oggetto annidato, avrete dei problemi in seguito.
Per tutelarvi, potreste voler garantire che gli oggetti non subiscano alcuna mutazione. Per farlo, potete usare librerie come ImmutableJS. Questa libreria dà un errore ogni volta che cercate di mutare un oggetto.
In alternativa, potete usare Object.freeze
e deep-freeze. Questi due metodi fanno un fail silenzioso (nessun throw error, ma non mutano nemmeno gli oggetti).
Object.freeze
e deep-freeze#section12
Object.freeze
impedisce che le proprietà dirette di un oggetto cambino.
const egg = {
name: "Humpty Dumpty",
isBroken: false
};
// Freezes the egg
Object.freeze(egg);
// Attempting to change properties will silently fail
egg.isBroken = true;
console.log(egg); // { name: "Humpty Dumpty", isBroken: false }
Ma non aiuta quando mutate una proprietà più profonda come defaultSettings.soundSettings.base
.
const defaultSettings = {
power: true,
soundSettings: {
volume: 50,
bass: 20
}
};
Object.freeze(defaultSettings);
defaultSettings.soundSettings.bass = 100;
// soundSettings gets mutated nevertheless
console.log(defaultSettings);
// {
// power: true,
// soundSettings: {
// volume: 50,
// bass: 100
// }
// }
Per impedire una mutazione profonda, potete usare una libreria chiamata deep-freeze, che chiama ricorsivamente Object.freeze
su tutti gli oggetti.
const defaultSettings = {
power: true,
soundSettings: {
volume: 50,
bass: 20
}
};
// Performing a deep freeze (after including deep-freeze in your code per instructions on npm)
deepFreeze(defaultSettings);
// Attempting to change deep properties will fail silently
defaultSettings.soundSettings.bass = 100;
// soundSettings doesn't get mutated anymore
console.log(defaultSettings);
// {
// power: true,
// soundSettings: {
// volume: 50,
// bass: 20
// }
// }
Non confondete il riassegnamento con la mutazione#section13
Quando riassegnate una variabile, cambiate quello a cui punta. Nell’esempio seguente, a
è cambiato da 11
a 100
.
let a = 11;
a = 100;
Quando mutate un oggetto, esso viene cambiato. Il riferimento all’oggetto rimane lo stesso.
const egg = { name: "Humpty Dumpty" };
egg.isBroken = false;
Conclusioni#section14
La mutazione fa paura perché può causare la rottura del vostro codice senza che lo sappiate. Anche se sospettate che la causa della rottura sia una mutazione, può essere difficile per voi definire con precisione quale parte del codice ha creato la mutazione. Quindi, il modo migliore per impedire che il codice smetta di funzionare senza saperlo è assicurarsi che gli oggetti non mutino fin dall’inizio.
Per impedire che gli oggetti mutino, potete usare delle librerie quali ImmutableJS e Mori.js, o usare Object.assign
e Object.freeze
.
Prendete nota che Object.assign
e Object.freeze
possono solo impedire che le proprietà dirette mutino. Se dovete impedire che livelli multipli di oggetti mutino, avrete bisogno di librerie come assignment e deep-freeze.
Nessun commento
Altro da ALA
Webwaste
Uno strumento essenziale per catturare i vostri progressi lavorativi
Andiamo al cuore dell’accessibilità digitale
JavaScript Responsabile, Parte II
JavaScript Responsabile: parte prima