Javascript Coercion [4] Loose Equality

J
Questo articolo fa parte di una serie di articoli:

  1. Javascript Coercion [1] Abstract Value Operations
  2. Javascript Coercion [2] Explicit Coercion
  3. Javascript Coercion [3] Implicit Coercion
  4. Javascript Coercion [4] Loose Equality

 

Loose equality vs Strict equality

Carissimi Javascripter, siamo giunti al dunque! In quest’utlimo articolo della serie Javascript Coercion analizzeremo nel dettaglio l’operazione meno compresa e più odiata nella storia della programmazione: la loose equality.

Innanzitutto, quale è la differenza tra la loose equality e la sua “controparte buona”, la strict equality?
In molti pensano che “==” controlli solo l’uguaglianza tra i valori, mentre “===” controlli l’uguaglianza sia tra i tipi degli operandi che tra i loro valori. Potrebbe sembrare una spiegazione in linea con la verità, ma purtroppo è errata.
La definizione correttà è la seguente: “==” consente la coercizione nell’operazione di controllo dell’uguaglianza tra gli operandi, mentre “===” non la permette.

L’implicazione più ovvia risiede nel fatto che entrambi gli operatori controllino i tipi degli operandi; la differenza risiede nel modo in cui si comportano se questi tipi non sono uguali. La domanda fulcro che dobbiamo porci nella scelta dell’operatore da utilizzare è perciò: nel caso in cui i tipi siano diversi, voglio abilitare la coercizione oppure no?

 

Abstract Equality

Il comportamento dell’operatore “==” è definito da una serie di operazioni che prendono il nome di The Abstract Equality Comparison Algorithm. Esso contiene delle specifiche istruzioni per le varie combinazioni di tipi; in particolar modo come e se la coercizione deve avvenire sugli operandi.

Nel caso in cui i tipi degli operandi siano i medesimi, l’algoritmo impone alla loose equality di comportarsi esattamente come la strict equality: verrà semplicemente confrontata l’identità degli operandi senza alcuna coercizione.
Vediamo quindi alcune delle principali conseguenze:

  • NaN è sempre diverso da se stesso
  • +0 è uguale a -0
  • il confronto tra due oggetti (funzioni ed array compresi) restituisce true solo se i valori confrontati sono entrambi riferimenti al medesimo oggetto

Se invece i tipi degli operandi sono differenti, la strict equality restituisce sempre false; mentre la loose equality permette che la coercizione avvenga in modo che i valori in gioco diventino del medesimo tipo. A questo punto ne verrà confrontata l’identità.

 

The algorithm

Analizziamo nel dettaglio lo Abstract Equality Comparison Algorithm nei vari casi dove i tipi dei valori non corrrispondono.

 

NULL – UNDEFINED

Il primo particolare della loose equality è l’interscambiabilità tra il valore null e il valore undefined. A differenza della strict equality dove:

null === undefined; //false
undefined === null; //false

a motivo del diverso tipo degli operandi, nella loose equality abbiamo che:

null == undefined; //true
undefined == null; //true

Non dobbiamo preoccuparci di eventuali coercizioni tra questi due tipi (null e undefined) e i restanti tipi del Javascript (numeri, stringhe, booleani, oggetti) poiché esse non sono contemplate nello Abstract Equality Comparison Algorithm; perciò il risultato del confronto sarà sempre pari a false:

undefined == 0; //false 
null == 0; //false

undefined == ""; //false 
null == ""; //false

undefined == false; //false 
null == false; //false

undefined == true; //false 
null == true; //false

undefined == []; //false
null == []; //false

undefined == {}; //false
null == {}; //false

Tale caratteristica della loose equality ci permette di ridurre il seguente codice (decisamente verboso e poco elegante):

if (foo === undefined || foo === null) {
    // ...
}

in questo:

if (foo == undefined) {
    // ... 
}

oppure in questo:

if (foo == null) {
    // ... 
}

 

Stringa – numero

Il secondo caso preso in considerazione dall’algoritmo è la situazione in cui uno dei due operandi è di tipo numerico, mentre l’altro è di dipo stringa.
Esso impone una coercizione del valore stringa in uno numerico, invocando la ToNumber abstract operation analizzata nel primo articolo di questa serie.
A questo punto la loose equality viene rivalutata.

Qualche esempio:

// "42" (stringa) viene coerciso in 42 (numero) e la loose equality viene rivalutata
"42" == 42; //true 
42 == "42"; //true

// "42.7" (stringa) viene coerciso in 42.7 (numero) e la loose equality viene rivalutata
"42.7" == 42.7; //true 
42.7 == "42.7"; //true

// "foo" viene coerciso in NaN, il quale è sempre diverso da se stesso
"foo" == NaN // false

 

* – booleano

Come terza possibilità abbiamo la comparazione tra un valore di tipo booleano e qualsiasi altro tipo. È un caso dove va prestata particolare attenzione, poiché è SEMPRE il valore booleano ad essere coerciso in un valore di tipo numerico invocando la ToNumber abstract operation, mentre il secondo operando viene lasciato inalterato.
A questo punto la loose equality viene rivalutata.

Qualche esempio:

42 == true; //false 
/* 
   true viene coerciso nel valore numerico corrispondente, ovvero 1 
   42 == 1 viene valutata
   42 == 1 è falso 
*/ 

false == 42; //false 
/* 
   false viene coerciso nel valore numerico corrispondente, ovvero 0 
   0 == 42 viene valutata 
   0 == 42 è falso
*/



"42" == true; //false
/* 
   true viene coerciso nel valore numerico corrispondente, ovvero 1
   "42" == 1 viene valutata
   "42" viene coerciso nel valore numerico corrispondente, ovvero 42
   42 == 1 è falso
*/

false == "42"; //false
/* 
   false viene coerciso nel valore numerico corrispondente, ovvero 0
   0 == "42" viene valutata
   "42" viene coerciso nel valore numerico corrispondente, ovvero 42
   0 == 42 è falso
*/

In altre parole, un valore di tipo primitivo come 42 oppure “42” non è == true== false. In effetti questa affermazione potrebbe risultare assurda. Come può un valore non essere né vero né falso?
Ecco il vero problema! Stai semplicemente ponendo la domanda sbagliata, perché la loose equality in questo caso non esegue una coercizione nel tipo booleano, ma è il tipo booleano ad essere coerciso in altro.
La ToBoolean abstract operation non è invocata, perciò non interessa a nessuno se il valore primitivo preso in considerazione (42 e“42” nell’esempio) è un valore truthy o un valore falsy.

Dato che false corrisponde al valore numerico 0 e true corrisponde al valore numerico 1, ci sono ovviamente dei casi in cui la loose equality risulta vera:

1 == true; // true perché true viene coerciso in 1 -> 1 == 1 è vero
0 == false; // true perché false viene coerciso in 0 -> 0 == 0 è vero

"1" == true; // true
/* 
   true viene coerciso in 1
   "1" == 1 viene valutata
   "1" viene coerciso in 1 -> 1 == 1 è vero
*/

"0" == false; // true
/* 
   false viene coerciso in 0
   "0" == 0 viene valutata
   "0" viene coerciso in 0 -> 0 == 0 è vero
*/

Se il valore di tipo booleano viene confrontato con un oggetto non ci sono eccezioni: il booleano viene coerciso in numero:

true == {}; // false
/* 
   true viene coerciso in 1 e la loose equality viene rivalutata
   1 == {}viene valutata e risulta false (sottotitolo successivo)
*/

false == {}; // false
/* 
   false viene coerciso in 0 e la loose equality viene rivalutata
   0 == {} viene valutata e risulta false (sottotitolo successivo)
*/

["1"] == true; // true
/*
   true viene coerciso in 1 e la loose equality viene rivalutata
   ["1"] == 1 viene valutata e risulta true (sottotitolo successivo)
*/

[0] == false; // true
/*
   false viene coerciso in 0 e la loose equality viene rivalutata
   [0] == 0 viene valutata e risulta true (sottotitolo successivo)
*/

e anche qua in alcuni casi il confronto risulta essere true.

Per riassumere: scrivere == true e == false NON EQUIVALE a un test booleano, ma a un confronto con i valori numerici 0 e 1; perciò è consigliabile non usare mai e poi mai una simile sintassi dato che al 99% non corrisponde a quello che abbiamo realmente intenzione di fare. E anche se fosse, è fuorviante per il restante 99% degli sviluppatori.
Sfruttiamo anzi la strict equality o la conversione implicita nel tipo booleano in un caso e scriviamo == 0 e == 1, per rendere evidenti le nostre intenzioni, nell’altro.

 

Stringa/numero – oggetto

L’ultimo caso preso in considerazione dall’algoritmo della loose equality è il confronto tra un oggetto e un valore di tipo numerico oppure di tipo stringa. Il confronto tra un oggetto e un valore di tipo booleano è implicitamente incluso nel confronto oggettonumero perché, ripetiamo, un valore di tipo booleano viene sempre coerciso nel corrispondente valore numerico.

Un oggetto confrontato con una stringa oppure con un numero viene trasformato nel corrispondente valore di tipo primitivo tramite la ToPrimitive abstract operation, analizzata nel primo articolo di questa serie.
A questo punto la loose equality viene rivalutata.

Vediamo qualche esempio (è necessario aver seguito l’intera serie per non trovare difficoltà):

[42] == 42; // true
/*
   [42] viene coerciso in "42" dalla ToPrimitive abstract operation
   la quale richiama il metodo toString() presente nella prototype chain
   "42" == 42 viene valutata
   "42" viene coerciso in 42 dall'algoritmo della loose equality
   42 == 42 viene valutatata e risulta true
*/

["foo"] == "foo"; // true
/*
   ["foo"] viene coerciso in "foo" dalla ToPrimitive abstract operation
    "foo" == "foo" viene valutata e risulta true
*/

["f", "o", "o"] == "foo"; // false
/*
   ["foo"] viene coerciso in "f,o,o" dalla ToPrimitive abstract operation
    "f,o,o" == "foo" viene valutata e risulta false
*/

let obj = {
   valueOf: () => "obj",
   toString: () => "foo"
} 

obj == "foo"; // false
/*
   obj viene coerciso in "obj" dalla ToPrimitive abstract operation,
   la quale dà priorità al metodo valueOf(), richiamando il metodo toString()
   solo se il precedente non restituisce un  valore di tipo primitivo
   "obj" == "foo" viene valutata e risulta false
*/

let obj2 = {
   toString: () => "foo",
} 

obj2 == "foo"; // true
/*
   obj2 viene coerciso in "foo" dalla ToPrimitive abstract operation,
   dato che il metodo valueOf() presente nella prototype chain, quando invocato,
   non restituisce un tipo primitivo
   la ToPrimitive abstract operation ripiega quindi sul metodo toString
   "foo" == "foo" viene valutata e risulta true
*/

let obj3 = {
   bar: "baz",
} 

obj3 == "baz"; // false
/*
   obj viene coerciso in "[object Object]" dalla ToPrimitive abstract operation,
   dato che il metodo valueOf() presente nella prototype chain, quando invocato,
   non restituisce un tipo primitivo
   la ToPrimitive abstract operation ripiega quindi sul metodo toString presente
   nella prototype chain
   "[object Object]" == "foo" viene valutata e risulta false
*/

true == {}; // false
/* 
   true viene coerciso in 1 e la loose equality viene rivalutata
   1 == {} viene valutata
   {} viene coerciso in "[object Object]" dalla ToPrimitive abstract operation
   1 == "[object Object]" viene valutata
   "[object Object]" viene coerciso in NaN dalla loose equality
   1 == NaN viene valutata e risulta true
*/

[0] == false; // true
/*
   false viene coerciso in 0 e la loose equality viene rivalutata
   [0] == 0 viene valutata
   [0] viene coerciso in "0" dalla ToPrimitive abstract operation
   "0" = 0 viene valutata
   "0" viene coerciso in 0 dalla loose equality
   0 == 0 viene valutata e risulta true
*/

 

AlTRE CASISTICHE

I restanti confronti che non sono presi in considerazione dall’algoritmo restituiscono sempre false.

Qualche esempio:

undefined == 1; // false;
undefined == 0; // false;
undefined == "0"; // false;
undefined == []; // false
undefined == {}; // false
undefined == false; // false
/* 
   false viene coerciso nel valore numerico 0 dalla loose equality ma
   undefined == 0 viene valutata e risulta false
*/
null == 1; // false
null == 0; // false
null == "0"; // false
null == []; // false
null == {}; // false
null == false; // false
/* 
   false viene coerciso nel valore numerico 0 dalla loose equality ma
   null == 0 viene valutata e risulta false
*/

 

 

COMPORTAMENTI INASPETTATI

Tenendo presente quanto detto fin’ora, analizziamo alcuni casi che potrebbero sorprenderci se non esaminati con cautela. Riusciremo ad estrapolare due regole che ci permetteranno di utilizzare la loose equality senza alcun timore.

 

[] == []; // false

Non facciamoci ingannare, poiché questo non è un confronto tra due array uguali, ma tra i due riferimenti collegati agli array. Ricordiamo le somiglianze con la strict equality? Il confronto tra due oggetti (funzioni ed array compresi) restituisce true solo se i valori confrontati sono entrambi riferimenti al medesimo oggetto. Anche se i due array in questione sono uguali, risiedono in zone di memoria differenti.

 

{} == {}; // SyntaxError

La grammatica del Javascript ci impone di considerare il primo {} non come un oggetto, ma come un blocco di codice. L’istruzione == {}; non è, perciò, corretta.

 

({}) == {}; // false

In questo caso, il primo {} viene considerato un oggetto, ma si applica il ragionamento del primo caso trattato.

 

[] == ![]; // true

È ovvio che un array è uguale alla negazione di se stesso.
Non capisco perché ti sei stupito.
Dilettante.

Offese a parte, vediamo di trovare una spiegazione:

  1. l’operatore unario ! esegue una coercizione esplicita del valore [] nel valore booleano false
  2. [] == false; viene valutata
  3. false viene convertito nel valore numerico 0 dalla loose equality
  4. [] == 0; viene valutata
  5. [] viene convertito nel valore stringa “” dalla ToPrimitive Abstract Operation
  6. "" == 0; viene valutata
  7. “” viene convertito nel valore numerico 0 dalla loose equality
  8. 0 == 0; viene valutata e risulta true

 

"" == [null]; // true

Ricordiamoci che i valori null e undefined, rappresentando uno slot vuoto, NON vengono convertiti nel corrispettivo valore stringa dalla ToPrimitive Abstract Operation, lasciando un posto vuoto nel risultato. In questo caso il risultato è pari al valore stringa “”.

 

false == "0"; // true
false == 0; // true
false == ""; // true
false == []; // true
[] == 0; // true
[] == ""; // true
"" == 0; // true

Non dimostreremo la validità di questi confronti, che viene lasciata come esercizio al lettore, ma ci limiteremo a ragionare su di essi. Il risultato è antintuitivo; senza una conoscenza profonda dei meccanismi della loose equality è logico aspettarsi che tutte queste operazioni producano false come le seguenti:

"0" == null; // false
"0" == undefined; // false
"0" == NaN; // false
"0" == ""; // false

false == null; // false
false == undefined; // false
false == NaN; // false
false == {}; // false

"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == {}; // false

0 == null; // false
0 == undefined;	// false
0 == NaN; // false
0 == {}; // false

Dobbiamo perciò definire alcuni limiti all’uso della loosy equality.
I primi quattro dei sette confronti li eviteremo già se abbiamo compreso i rischi nello scrivere == true oppure == false esposti nell’analisi della coercizione implicita del tipo booleano in un valore numerico esaminata nel precedente sottotitolo.
Il quinto e il sesto confronto con il valore [] sono inutili e pericolosi, perché il test == [] può risultare true solo quando l’altro operatore è uno tra i seguenti: false, “”, 0.
È invece più probabile ricadere nell’ultimo confronto, in ambo i sensi.
Un codice come il seguente:

function foo(bar) { 
   if(bar == "") { 
      // ... 
   } 
}

potrebbe risultare in un comportamento errato se invocato con foo(0) oppure con foo([]) perché il test interno di controllo non fallirebbe. Questo ci porta a fare attenzione ai possibili valori del parametro bar; se non abbiamo un controllo totale su di esso è preferibile una soluzione come questa (che sfrutta la strict comparison):

function foo(bar) {
   if(bar === "") { 
      // ... 
   } 
}

se vogliamo escludere totalmente anche i valori numerici.
Altrimenti potremmo optare per la seguente:

function foo(bar) {
   if((bar === "") || (bar === 0)) { 
      // ... 
   } 
}

se vogliamo includerli.

Possiamo quindi riassumere il ragionamento dentro queste due semplici regole:

  1. Se anche solo uno dei due operandi nell’operazione di confronto potrebbe diventare true oppure false, non usare MAI E POI MAI l’operatore ==
  2. Se anche solo uno dei due operandi nell’operazione di confronto potrebbe diventare [], “”, oppure 0 considera seriamente la possibilità di non utilizzare l’operatore ==

È quindi importante esaminare il nostro programma, ragionando sui possibili valori che potrebbero essere comparati. Sarà un’attenta valutazione che ci porterà a scegliere l’operatore === al posto dell’operatore ==, non la paura di quello che potrebbe succedere.

 

CONCLUSIONE

Eccoci giunti alla conclusione della serie: Javascript Coercion. In questo articolo abbiamo visto che la questione loose equality vs strict equality può riassumersi in una banale domanda: la coercizione è permessa nella comparazione oppure no?

Ci sono molti casi dove la suddetta coercizione è veramente utile, mentre i casi effettivi dove essa è realmente pericolosa sono pochi e possiamo facilmente arginarli utilizzando l’operatore ===. Perciò non limitarti ad inserire === dappertutto, sii uno sviluppatore responsabile e maturo, studia e impara ad utilizzare il potere della coercizione in modo sicuro ed efficace. E insegna agli altri a fare lo stesso.

 

BIBLIOGRAFIA

You Don’t Know JS: Types & Grammar

Standard ECMA-262 5.1 Edition

A proposito di me

Andrea Simone Costa

Classe 1997, Toscano DOCP, Asimov oriented.
Si è diplomato come perito elettronico, ma ha ben presto tradito le origini per immergersi nell'universo JavaScript. Ama condividere ciò che ha imparato in modo semplice e pragmatico, senza mai ricadere nel banale, coltivando segretamente il sogno di insegnare.

I nostri Partner

Gli articoli più letti

Articoli recenti

Commenti recenti