Javascript Coercion [1] Abstract Value Operations

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

To coerce or not to coerce?– every JS Developer

Venghino Signori, venghino! È arrivato il circo!

 

COERCION

Una delle peggiori vars – tsk, spiritoso – che affliggono il mondo degli sviluppatori JS riguarda proprio una delle peculiarità meno comprese del linguaggio. Ma il vero problema è la coercizione o il programmatore che non sa utilizzarla?
(Questo articolo sarà ovviamente di parte, perciò sentitevi liberi di terminare seduta stante la sua lettura spostandovi direttamente sulla sezione commenti per insultarmi).

Per quanto molti libri e articoli la considerino il male assoluto, confusionaria, in qualche modo magica, l’errore peggiore di chi ha progettato il linguaggio, ecc., Noi – magnifico rettore dell’Università di ITC – riteniamo che ogni sviluppatore che si definisca tale possa imparare come utilizzare nel modo più appropriato e sicuro la coercizione, sia esplicita che implicita.

Se, per qualche oscuro motivo, tu non sappia di cosa stiamo parlando, apri la console e digita:

"2" - 1; 
"2" + 1;
[] == ![];

I risultati di queste tre semplici operazioni ti hanno rovinato la giornata? Benvenuto carissimo/a! Rovineranno pure il resto della tua vita, a meno che tu non legga l’intera serie di articoli.

 

Abstract Value Operations

Le abstract value operations sono una serie di operazioni che vengono eseguite ogni volta che la coercizione, implicita o esplicita, deve avvenire. Diamo assieme un’occhiata alle più comuni.

 

To string

Quando viene chiamata in causa la ToString abstract operation? Quando un valore non-stringa deve essere convertito in un valore stringa.

Su quale logica si basa?
I valori primitivi possiedono già un valore stringa associato:

  • null -> “null”
  • undefined -> “undefined”
  • true -> “true”
  • false -> “false”
  • 42 -> “42”
  • 42 * 10 ** 21 -> “4.2e+22
  • 42 * 10 ** -21 -> “4.2e-20

Nulla di eccezionale. L’unica differenza nasce per i valori numerici molto elevati o molto piccoli dato che viene preferita la notazione esponenziale.

Gli oggetti, invece, verranno prima trasformati dalla ToPrimitive abstract operation, della quale parleremo a breve, in un valore di tipo primitivo; nel caso in cui quest’ultima operazione astratta provveda come risultato un valore di tipo stringa, la ToString abstract operation non eseguirà nessun’altra operazione per quel specifico valore.

A meno che un oggetto non abbia ridefinito i metodi toString()/valueOf() solitamente il risultato è, per esempio, “[object Object]”. Nello specifico verrà utilizzata la proprietà interna [[Class]] per creare il risultato.
Gli array sono oggetti per i quali di default il metodo toString() è stato ridefinito: verrà restituita una stringa contenente la concatenazione di tutti i valori interni (ognuno dei quali verrà sottoposto alla ToString abstract operation), separati da delle virgole “,“.
È importante sapere che i valori null e undefined presentano un comportamento particolare se inseriti in un array: rappresentando uno slot vuoto NON vengono convertiti nel corrispettivo valore stringa, piuttosto lasciano un posto vuoto nel risultato.
Qualche esempio:

  • [] -> “”
  • [“string”, 42, null, undefined, {foo:3}, [0,1,2,3]] -> “string,42,,,[object Object],0,1,2,3”

Anche le funzioni sono oggetti per i quali di default il metodo toString() è stato ridefinito:

  • function foo () {} -> “function foo () {}”

 

To NUMBER

Quando viene chiamata in causa la ToNumber abstract operation? Quando un valore non-numerico deve essere convertito in un valore numerico.

Su quale logica si basa?
I valori primitivi possiedono già un valore numerico associato:

  • null -> 0
  • undefined -> NaN
  • true -> 1
  • false -> 0
  • “42” -> 42
  • “” oppure “\t\n ” -> 0 // stringa vuota oppure contenente solo whitespaces
  • “foo” -> NaN
  • “Infinity” -> Infinity
  • “-Infinity” -> -Infinity

Anche qua nulla di eccezionale. Bisogna solo prestare maggiore attenzione ad alcuni casi particolari riguardanti le stringhe. Infinity e -Infinity sono valori validi di tipo numerico.

Gli oggetti, invece, verranno prima trasformati dalla ToPrimitive abstract operation in un valore di tipo primitivo; nel caso in cui quest’ultima operazione astratta provveda come risultato un valore di tipo numerico, la ToNumber abstract operation non eseguirà nessun’altra operazione per quel specifico valore.

A meno che un oggetto non abbia ridefinito i metodi valueOf()/toString() il risultato è NaN, poiché sarà il risultato del metodo toString() (ad esempio “[object Object]”) ad essere convertito in un valore numerico.
Gli array sono oggetti per i quali di default il metodo toString() è stato ridefinito e solitamente sarà la stringa prodotta dall’invocazione di questo metodo ad essere convertita in un valore numerico:

  • [] -> “” -> 0
  • [42] -> “42” -> 42
  • [42, 43] -> “42, 43” -> NaN

Anche le funzioni sono oggetti per i quali di default il metodo toString() è stato ridefinito, perciò anche in questo caso solitamente sarà la stringa prodotta dall’invocazione di questo metodo ad essere convertita in un valore numerico:

  • function foo () {} -> “function foo () {}” -> NaN

Tranquilli, appena definiremo meglio la ToPrimitive abstract operation vi sarà tutto più chiaro.

 

To Boolean

Quando viene chiamata in causa la ToBoolean abstract operation? Quando un valore non-booleano deve essere convertito in un valore booleano.

Su quale logica si basa?
Ogni valore in Javascript ha una controparte booleana. La lista dei valori, definiti per l’appunto falsy values, che risultano nel valore booleano false è la seguente:

  • null
  • undefined
  • false
  • 0 (+0), -0 e NaN
  • “”

Tutti gli altri valori verranno considerati truthy values, che risultano nel valore booleano true, come ad esempio:

  • true
  • ” “
  • “0”
  • “false”
  • {}
  • []
  • function () {}

Non esiste una lista che comprende tutti i possibili truthy values poiché essa non avrebbe teoricamente fine. Perciò è sufficiente marchiare a fuoco nella nostra mente la lista dei falsy values e considerare tutti i valori non compresi in essa come truthy values.

 

To PRIMITIVE

Quando viene chiamata in causa la ToPrimitive abstract operation? Quando un valore non-primitivo deve essere convertito in un valore primitivo. La coercizione deve risultare sempre in un valore primitivo, perciò se la ToPrimitive abstract operation fallisce verrà sollevato un TypeError.

Questa operazione astratta si basa sui metodi valueOf() e toString() dell’oggetto questione.
Nel caso in cui sia stata invocata dalla ToString abstract operation verrà data priorità al metodo toString(). Solo se quest’ultimo non fornisce un valore primitivo verrà invocato il metodo valueOf() per un tentativo di ripiego.
Nel caso in cui sia stata invocata dalla ToNumber abstract operation verrà data priorità al metodo valueOf(). Solo se quest’ultimo non fornisce un valore primitivo verrà invocato il metodo toString() per un tentativo di ripiego.

Generalmente il metodo valueOf() presente di default negli oggetti (array e funzioni compresi) restituisce il this ovvero l’oggetto stesso, o più nello specifico un riferimento ad esso. Non essendo un valore primitivo, il tentativo della ToPrimitive abstract operation per la coercizione verso il tipo Number ricade spesso e volentieri nella chiamata del metodo toString(). Questo spiega lo strano comportamento della ToNumber abstract operation.

Prendiamo ad esempio il seguente caso:

  • [42, 43] -> “42, 43” -> NaN

La ToNumber abstract operation invoca la ToPrimitive abstract operation sull’array [42, 43]. Dato che la ToPrimitive è stata invocata dalla ToNumber viene data precedenza al metodovalueOf() , il quale purtroppo restituisce l’array stesso che non è un valore primitivo. La ToPrimitive ripiega quindi sul metodo toString(), il quale restituisce “42, 43“. Questo valore è un primitivo che può essere restituito alla ToNumber. A questo punto la ToNumber coercizza questo valore stringa nel valore numerico corrispondente, ottenendo NaN.

Vedremo, in un successivo articolo, che la ToPrimitive abstract operation potrebbe essere invocata anche su un valore di tipo primitivo. Il risultato sarà equivalente all’input poiché non verrà eseguito alcun tentativo di conversione.
Dato che né la ToString abstract operation né la ToNumber abstract operation implicano l’invocazione della ToPrimitive abstract operation su un valore di tipo primitivo, ciò significa che quest’ultima operazione astratta può essere chiamata in causa direttamente? Si.
In questi casi essa darà priorità al metodo valueOf() per tutti i valori di tipo non-primitivo, tranne il tipo Date per il quale la priorità l’avrà il metodo toString(). Ovviamente anche in questo caso se uno dei due dovesse fallire l’altro verrebbe utilizzato come ripiego per un secondo tentativo.

 

CONCLUSIONE

That’s it! Siete sorpresi? Vi sareste mai aspettati che il grosso del meccanismo che governa la coercizione si basa su queste semplici regole? Certo, l’implementazione scelta per le varie abstract operations potrebbe farvi storcere il naso su alcune decisioni che furono prese a monte, ma questo è il Javacript. È stato creato così. Le regole che governano questo aspetto del linguaggio non sono affatto impossibili da comprendere ne illogiche come spesso vengono presentate.

Nei prossimi articoli esamineremo più a fondo la coercizione esplicita e la coercizione implicita, dove spesso e volentieri è sufficiente aver capito l’ordine di chiamata delle varie abstract operations per riuscire a padroneggiare la coercizione nei vari casi nei quali essa è implicata. Ovviamente tutto ciò non significa che sia giusto utilizzarla sempre e ovunque, il JS è sempre pieno di soprese ed è bene definire delle regole oltre le quali non è sicuro spingersi.

Ad ogni modo non ritengo assolutamente corretto evitare di utilizzarla completamente – cosa che vedremo rasenta l’impossibile pure per la fazione opposta – solo perché può causare alcuni problemi in alcune circostanze. Anzi la maggior parte dei problemi derivano dal non essersi mai presi il tempo di esaminare questo lato del Javascript. Quindi, di nuovo, di chi è effettivamente la colpa?

A proposito di me

Andrea Simone Costa

Giovane appassionato di coding, frequento un corso di studi incentrato sul front-end web. Nel tempo libero amo sviscerare gli arcani misteri dell’informatica, con un’attenzione particolare al linguaggio C e C++.

Gli articoli più letti

Articoli recenti

Commenti recenti