Spiegazione della coercizione del tipo JavaScript

Conosci i tuoi motori

Cose strane possono accadere in JavaScript

[Modifica 5/5/2018]: questo post è ora disponibile in russo. Applaude a Serj Bulavyk per i suoi sforzi.

La coercizione del tipo è il processo di conversione del valore da un tipo a un altro (come stringa in numero, oggetto in booleano e così via). Qualsiasi tipo, sia esso primitivo o un oggetto, è un argomento valido per la coercizione del tipo. Per ricordare, le primitive sono: numero, stringa, booleano, null, non definito + simbolo (aggiunto in ES6).

Come esempio di coercizione dei tipi nella pratica, guarda la tabella di confronto JavaScript, che mostra come l'operatore di uguaglianza libera == si comporta per diversi tipi aeb. Questa matrice sembra spaventosa a causa della coercizione implicita di tipo che fa l'operatore == ed è quasi impossibile ricordare tutte quelle combinazioni. E non devi farlo, basta imparare i principi di coercizione del tipo di base.

Questo articolo approfondisce come funziona la coercizione dei tipi in JavaScript e ti fornirà le conoscenze essenziali, così puoi sentirti sicuro di spiegare a cosa calcolano le seguenti espressioni. Entro la fine dell'articolo mostrerò le risposte e le spiegherò.

vero + falso
12 / "6"
"numero" + 15 + 3
15 + 3 + "numero"
[1]> null
"pippo" + + "bar"
'true' == true
false == 'false'
null == ''
!! "false" == !! "true"
["" X "] ==" x "
[] + null + 1
[1,2,3] == [1,2,3]
{} + [] + {} + [1]
! + [] + [] +! []
nuova data (0) - 0
nuova data (0) + 0

Sì, questo elenco è pieno di cose piuttosto stupide che puoi fare come sviluppatore. Nel 90% dei casi di utilizzo è meglio evitare la coercizione implicita del tipo. Considera questo elenco come un esercizio di apprendimento per testare le tue conoscenze su come funziona la coercizione di tipo. Se sei annoiato, puoi trovare altri esempi su wtfjs.com.

A proposito, a volte potresti dover affrontare tali domande nell'intervista per una posizione di sviluppatore JavaScript. Quindi, continua a leggere

Coercizione implicita vs. esplicita

La coercizione del tipo può essere esplicita e implicita.

Quando uno sviluppatore esprime l'intenzione di convertire tra i tipi scrivendo il codice appropriato, come Numero (valore), si chiama coercizione esplicita del tipo (o cast del tipo).

Poiché JavaScript è un linguaggio di tipo debole, i valori possono anche essere convertiti automaticamente tra tipi diversi e si chiama coercizione di tipo implicito. Di solito succede quando si applicano gli operatori a valori di diversi tipi, come
1 == null, 2 / ’5 ', null + new Date (), oppure può essere attivato dal contesto circostante, come con if (value) {…}, dove value è forzato in booleano.

Un operatore che non attiva la coercizione di tipo implicito è ===, chiamato operatore di uguaglianza rigorosa. L'operatore di uguaglianza libera == d'altra parte fa sia il confronto che la coercizione del tipo, se necessario.

La coercizione implicita di tipo è un'arma a doppio taglio: è una grande fonte di frustrazione e difetti, ma anche un meccanismo utile che ci consente di scrivere meno codice senza perdere la leggibilità.

Tre tipi di conversione

La prima regola da sapere è che ci sono solo tre tipi di conversione in JavaScript:

  • accordare
  • booleano
  • al numero

In secondo luogo, la logica di conversione per primitivi e oggetti funziona in modo diverso, ma sia i primitivi che gli oggetti possono essere convertiti solo in questi tre modi.

Cominciamo prima con le primitive.

Conversione di stringhe

Per convertire esplicitamente i valori in una stringa, applicare la funzione String (). La coercizione implicita viene attivata dall'operatore binario +, quando qualsiasi operando è una stringa:

String (123) // esplicito
123 + '' // implicito

Tutti i valori primitivi vengono convertiti in stringhe in modo naturale come ci si potrebbe aspettare:

String (123) // '123'
String (-12.3) // '-12.3'
String (null) // 'null'
String (undefined) // 'undefined'
String (true) // 'true'
String (false) // 'false'

La conversione dei simboli è un po 'complicata, perché può essere convertita solo esplicitamente, ma non implicitamente. Maggiori informazioni sulle regole di coercizione dei simboli.

String (Symbol ('my symbol')) // 'Symbol (my symbol)'
'' + Symbol ('my symbol') // TypeError viene lanciato

Conversione booleana

Per convertire esplicitamente un valore in un valore booleano, applicare la funzione Boolean ().
La conversione implicita avviene in un contesto logico o è innescata da operatori logici (|| &&!).

Booleano (2) // esplicito
if (2) {...} // implicito a causa del contesto logico
!! 2 // implicito a causa dell'operatore logico
2 || 'ciao' // implicito a causa dell'operatore logico

Nota: operatori logici come || e && eseguono conversioni booleane internamente, ma in realtà restituiscono il valore degli operandi originali, anche se non sono booleani.

// restituisce il numero 123, anziché restituire true
// 'ciao' e 123 sono ancora costretti a booleani internamente per calcolare l'espressione
lascia x = 'ciao' && 123; // x === 123

Non appena ci sono solo 2 possibili risultati della conversione booleana: vero o falso, è solo più facile ricordare l'elenco dei valori falsi.

Boolean ('') // false
Booleano (0) // false
Booleano (-0) // false
Booleano (NaN) // false
Booleano (null) // false
Booleano (non definito) // false
Booleano (falso) // falso

Qualsiasi valore non presente nell'elenco viene convertito in true, inclusi oggetto, funzione, matrice, data, tipo definito dall'utente e così via. I simboli sono valori veritieri. Anche gli oggetti vuoti e le matrici sono valori veritieri:

Booleano ({}) // true
Boolean ([]) // true
Boolean (Symbol ()) // true
!! Symbol () // true
Boolean (function () {}) // true

Conversione numerica

Per una conversione esplicita basta applicare la funzione Number (), come hai fatto con Boolean () e String ().

La conversione implicita è complicata, poiché viene attivata in più casi:

  • operatori di confronto (>, <, <=,> =)
  • operatori bit a bit (| & ^ ~)
  • operatori aritmetici (- + * /%). Nota che binario + non attiva la conversione numerica, quando un operando è una stringa.
  • unario + operatore
  • operatore di uguaglianza libera == (incl.! =).
    Si noti che == non attiva la conversione numerica quando entrambi gli operandi sono stringhe.
Numero ('123') // esplicito
+ "123" // implicito
123! = '456' // implicito
4> '5' // implicito
5 / null // implicito
vero | 0 // implicito

Ecco come i valori primitivi vengono convertiti in numeri:

Numero (null) // 0
Numero (non definito) // NaN
Numero (vero) // 1
Numero (falso) // 0
Numero ("12") // 12
Numero ("- 12.34") // -12.34
Numero ("\ n") // 0
Numero ("12s") // NaN
Numero (123) // 123

Quando si converte una stringa in un numero, il motore prima taglia gli spazi iniziali e finali, i caratteri \ n, \ t, restituendo NaN se la stringa tagliata non rappresenta un numero valido. Se la stringa è vuota, restituisce 0.

null e undefined sono gestiti in modo diverso: null diventa 0, mentre undefined diventa NaN.

I simboli non possono essere convertiti in un numero né esplicitamente né implicitamente. Inoltre, viene generato TypeError, invece di convertirlo silenziosamente in NaN, come accade per un indefinito. Ulteriori informazioni sulle regole di conversione dei simboli su MDN.

Number (Symbol ('my symbol')) // TypeError viene generato
+ Simbolo ('123') // Viene generato un errore di tipo

Ci sono due regole speciali da ricordare:

  1. Quando si applica == a null o indefinito, la conversione numerica non avviene. null è uguale solo a null o indefinito e non è uguale a nient'altro.
null == 0 // false, null non viene convertito in 0
null == null // true
undefined == undefined // true
null == undefined // true

2. NaN non è uguale a nulla nemmeno a se stesso:

if (value! == value) {console.log ("qui abbiamo a che fare con NaN")}

Digitare coercizione per gli oggetti

Finora abbiamo esaminato la coercizione dei tipi per i valori primitivi. Non è molto eccitante.

Quando si tratta di oggetti e il motore incontra espressioni come [1] + [2,3], per prima cosa deve convertire un oggetto in un valore primitivo, che viene quindi convertito nel tipo finale. E ancora ci sono solo tre tipi di conversione: numerico, stringa e booleano.

Il caso più semplice è la conversione booleana: ogni valore non primitivo è sempre
costretto a vero, non importa se un oggetto o un array è vuoto o meno.

Gli oggetti vengono convertiti in primitivi tramite il metodo interno [[ToPrimitive]], responsabile sia della conversione numerica che di stringa.

Ecco una pseudo implementazione del metodo [[ToPrimitive]]:

[[ToPrimitive]] viene passato con un valore di input e un tipo di conversione preferito: Number o String. favoriteType è facoltativo.

Sia la conversione numerica che quella stringa utilizzano due metodi dell'oggetto di input: valueOf e toString. Entrambi i metodi sono dichiarati su Object.prototype e quindi disponibili per qualsiasi tipo derivato, come Data, Matrice, ecc.

In generale l'algoritmo è il seguente:

  1. Se l'input è già una primitiva, non fare nulla e restituirlo.

2. Chiamare input.toString (), se il risultato è primitivo, restituirlo.

3. Chiamare input.valueOf (), se il risultato è primitivo, restituirlo.

4. Se né input.toString () né input.valueOf () restituisce primitivo, genera TypeError.

La conversione numerica prima chiama valueOf (3) con un fallback a toString (2). La conversione di stringhe fa il contrario: toString (2) seguito da valueOf (3).

La maggior parte dei tipi predefiniti non ha valueOf o value of Restituire questo oggetto stesso, quindi viene ignorato perché non è una primitiva. Ecco perché la conversione numerica e stringa potrebbe funzionare allo stesso modo - entrambi finiscono per chiamare toString ().

Operatori diversi possono attivare la conversione numerica o stringa con l'aiuto del parametro favoriteType. Ma ci sono due eccezioni: l'uguaglianza libera == e gli operatori binari + attivano le modalità di conversione predefinite (il tipo preferito non è specificato o è uguale al valore predefinito). In questo caso, la maggior parte dei tipi predefiniti presuppone la conversione numerica come predefinita, ad eccezione di Data che esegue la conversione di stringhe.

Ecco un esempio del comportamento di conversione della data:

È possibile sovrascrivere i metodi toString () e valueOf () predefiniti da agganciare alla logica di conversione da oggetto a primitiva.

Nota come obj + "" restituisce "101" come stringa. L'operatore + attiva una modalità di conversione predefinita e, come detto prima, Object assume la conversione numerica come predefinita, quindi utilizzando prima il metodo valueOf () anziché toString ().

Metodo ES6 Symbol.toPrimitive

In ES5 è possibile collegarsi alla logica di conversione da oggetto a primitivo sovrascrivendo i metodi toString e valueOf.

In ES6 puoi andare oltre e sostituire completamente la routine interna [[ToPrimitive]] implementando il metodo [Symbol.toPrimtive] su un oggetto.

Esempi

Armati della teoria, torniamo ora ai nostri esempi:

vero + falso // 1
12 / "6" // 2
"numero" + 15 + 3 // 'numero153'
15 + 3 + "numero" // '18number'
[1]> null // true
"foo" + + "bar" // 'fooNaN'
'true' == true // false
false == 'false' // false
null == '' // false
!! "false" == !! "true" // true
['x'] == 'x' // true
[] + null + 1 // 'null1'
[1,2,3] == [1,2,3] // false
{} + [] + {} + [1] // '0 [oggetto Oggetto] 1'
! + [] + [] +! [] // 'truefalse'
nuova data (0) - 0 // 0
nuova data (0) + 0 // 'gio 01 gen 1970 02:00:00 (EET) 0'

Di seguito puoi trovare una spiegazione per ciascuna espressione.

L'operatore binario + avvia la conversione numerica per vero e falso

vero + falso
==> 1 + 0
==> 1

Operatore divisione aritmetica / avvia la conversione numerica per la stringa '6':

12 / '6'
==> 12/6
== >> 2

L'operatore + ha associatività da sinistra a destra, quindi l'espressione "numero" + 15 viene eseguita per prima. Poiché un operando è una stringa, l'operatore + avvia la conversione di stringa per il numero 15. Al secondo passaggio l'espressione "numero15" + 3 viene valutata in modo simile.

"Numero" + 15 + 3
==> "numero15" + 3
==> "numero153"

L'espressione 15 + 3 viene valutata per prima. Nessuna necessità di coercizione, poiché entrambi gli operandi sono numeri. Nel secondo passaggio, viene valutata l'espressione 18 + "numero" e poiché un operando è una stringa, avvia una conversione di stringa.

15 + 3 + "numero"
==> 18 + "numero"
==> "18number"

Operatore di confronto> attiva la conversione numerica per [1] e null.

[1]> null
==> '1'> 0
==> 1> 0
==> vero

L'operatore Unario + ha una precedenza maggiore rispetto all'operatore binario +. Quindi l'espressione + 'bar' viene valutata per prima. Unary plus avvia la conversione numerica per la stringa "bar". Poiché la stringa non rappresenta un numero valido, il risultato è NaN. Nel secondo passaggio, viene valutata l'espressione 'pippo' + NaN.

"pippo" + + "bar"
==> "pippo" + (+ "bar")
==> "pippo" + NaN
==> "fooNaN"

== l'operatore avvia la conversione numerica, la stringa 'true' viene convertita in NaN, il valore booleano true viene convertito in 1.

'true' == true
==> NaN == 1
==> false
false == 'false'
==> 0 == NaN
==> false

== di solito attiva la conversione numerica, ma non è il caso di null. null è uguale a null o solo indefinito e non è uguale a nient'altro.

null == ''
==> false

!! L'operatore converte le stringhe "true" e "false" in true booleane, poiché sono stringhe non vuote. Quindi, == controlla solo l'uguaglianza di due veri booleani senza alcuna coercizione.

!! "false" == !! "true"
==> true == true
==> vero

== l'operatore avvia una conversione numerica per un array. Il metodo valueOf () dell'array restituisce l'array stesso e viene ignorato perché non è una primitiva. ToString () dell'array converte ['x'] in una stringa 'x'.

['x'] == 'x'
==> 'x' == 'x'
==> vero

+ l'operatore avvia la conversione numerica per []. Il metodo valueOf () dell'array viene ignorato, poiché restituisce l'array stesso, che non è primitivo. ToString dell'array restituisce una stringa vuota.

Al secondo passaggio viene valutata l'espressione '' + null + 1.

[] + null + 1
==> '' + null + 1
==> 'null' + 1
==> 'null1'

Logico || e gli operatori && obbligano gli operandi a booleani, ma restituiscono operandi originali (non booleani). 0 è falso, mentre "0" è vero, perché è una stringa non vuota. {} anche l'oggetto vuoto è vero.

0 || "0" && {}
==> (0 || "0") && {}
==> (false || true) && true // internamente
==> "0" && {}
==> true && true // internamente
==> {}

Non è necessaria alcuna coercizione perché entrambi gli operandi hanno lo stesso tipo. Poiché == verifica l'identità dell'oggetto (e non l'uguaglianza dell'oggetto) e le due matrici sono due istanze diverse, il risultato è falso.

[1,2,3] == [1,2,3]
==> false

Tutti gli operandi sono valori non primitivi, quindi + inizia con la conversione numerica che attiva all'estrema sinistra. Il valore Value of Of sia dell'Object sia dell'array restituisce l'oggetto stesso, quindi viene ignorato. toString () viene utilizzato come fallback. Il trucco qui è che il primo {} non è considerato come un oggetto letterale, ma piuttosto come un'istruzione di dichiarazione di blocco, quindi viene ignorato. La valutazione inizia con l'espressione successiva + [], che viene convertita in una stringa vuota tramite il metodo toString () e quindi in 0.

{} + [] + {} + [1]
==> + [] + {} + [1]
==> 0 + {} + [1]
==> 0 + '[oggetto oggetto]' + [1]
==> '0 [oggetto oggetto]' + [1]
==> '0 [oggetto oggetto]' + '1'
==> '0 [oggetto oggetto] 1'

Questo è meglio spiegato passo dopo passo in base alla precedenza dell'operatore.

! + [] + [] +! []
==> (! + []) + [] + (! [])
==>! 0 + [] + false
==> true + [] + false
==> true + '' + false
==> 'truefalse'

- l'operatore attiva la conversione numerica per la data. Date.valueOf () restituisce il numero di millisecondi dall'epoca di Unix.

nuova data (0) - 0
==> 0 - 0
==> 0

+ l'operatore avvia la conversione predefinita. La data presuppone la conversione della stringa come predefinita, quindi viene utilizzato il metodo toString () anziché valueOf ().

nuova data (0) + 0
==> "Giovedì 01 gennaio 1970 02:00:00 GMT + 0200 (EET)" + 0
==> "Giovedì 01 gennaio 1970 02:00:00 GMT + 0200 (EET) 0"

risorse

Voglio davvero raccomandare l'eccellente libro "Capire ES6" scritto da Nicholas C. Zakas. È una grande risorsa di apprendimento ES6, non troppo di alto livello e non scava troppo negli interni.

Ed ecco un buon libro solo su ES5 - SpeakingJS scritto da Axel Rauschmayer.

(Russo) Современный учебник Javascript - https://learn.javascript.ru/. Soprattutto queste due pagine sulla coercizione del tipo.

Tabella di confronto JavaScript - https://dorey.github.io/JavaScript-Equality-Table/

wtfjs - un piccolo blog in codice su quella lingua che amiamo nonostante ci dia così tanto da odiare - https://wtfjs.com/