de Alexey Samoshkin

S-a explicat constrângerea de tip JavaScript

Cunoaște-ți motoarele

S a explicat constrangerea de tip JavaScript
Lucruri ciudate se pot întâmpla în JavaScript

[Edit 2/5/2018]: Această postare este acum disponibil în limba rusă. O aplaudă lui Serj Bulavyk pentru eforturile sale.

Tastați constrângerea este procesul de conversie a valorii de la un tip la altul (cum ar fi șir la număr, obiect la boolean și așa mai departe). Orice tip, fie el primitiv sau obiect, este un subiect valid pentru constrângerea tipului. Pentru a ne aminti, primitivele sunt: ​​număr, șir, boolean, nul, nedefinit + Simbol (adăugat în ES6).

Ca exemplu de constrângere de tip în practică, uitați-vă la Tabel comparativ JavaScript, care arată modul în care egalitatea liberă == operatorul se comportă diferit a și b tipuri. Această matrice pare înfricoșătoare din cauza coerciției implicite de tip care == operatorul o face și este greu de reținut toate aceste combinații. Și nu trebuie să faceți acest lucru – învățați doar principiile de constrângere de bază.

Acest articol analizează în detaliu modul în care funcționează constrângerea de tip în JavaScript și vă va înarma cunoștințele esențiale, astfel încât să vă puteți simți încrezători explicați la ce calculează următoarele expresii. Până la sfârșitul articolului voi arăta răspunsuri și le voi explica.

true + false
12 / "6"
"number" + 15 + 3
15 + 3 + "number"
[1] > null
"foo" + + "bar"
'true' == true
false == 'false'
null == ''
!!"false" == !!"true"
[‘x’] == ‘x’
[] + null + 1
[1,2,3] == [1,2,3]
{}+[]+{}+[1]
!+[]+[]+![]
new Date(0) - 0
new Date(0) + 0

Da, această listă este plină de lucruri destul de stupide pe care le puteți face ca dezvoltator. În 90% din cazurile de utilizare, este mai bine să evitați coerciția implicită de tip. Luați în considerare această listă ca un exercițiu de învățare pentru a vă testa cunoștințele despre modul în care funcționează constrângerea de tip. Dacă vă plictisiți, puteți găsi mai multe exemple pe wtfjs.com.

Apropo, uneori s-ar putea să vă confruntați cu astfel de întrebări la interviu pentru o poziție de dezvoltator JavaScript. Deci, continuă să citești?

Coerciție implicită vs.

Coerciția de tip poate fi explicită și implicită.

Când un dezvoltator își exprimă intenția de a converti între tipuri, scriind codul corespunzător, cum ar fi Number(value), se numeste constrângere de tip explicit (sau turnare tip).

Deoarece JavaScript este un limbaj slab tastat, valorile pot fi convertite automat între diferite tipuri și se numește constrângere de tip implicită. De obicei, se întâmplă atunci când aplicați operatori la valori de diferite tipuri, cum ar fi
1 == null, 2/’5', null + new Date(), sau poate fi declanșat de contextul înconjurător, cum ar fi cu if (value) {…}, Unde value este constrâns la boolean.

Un operator care nu declanșează constrângerea implicită de tip este ===, care este numit operatorul strict al egalității. Operatorul de egalitate liberă == pe de altă parte, face atât comparația, cât și constrângerea de tip, dacă este necesar.

Coerciția de tip implicită este o sabie cu margine dublă: este o mare sursă de frustrare și defecte, dar și un mecanism util care ne permite să scriem mai puțin cod fără a pierde lizibilitatea.

Trei tipuri de conversie

Prima regulă de știut este că există doar trei tipuri de conversie în JavaScript:

  • a strânge
  • a boolean
  • la număr

În al doilea rând, logica de conversie pentru primitive și obiecte funcționează diferit, dar atât primitive cât și obiecte pot fi convertite numai în aceste trei moduri.

Să începem mai întâi cu primitive.

Conversia șirului

Pentru a converti explicit valorile într-un șir, aplicați String() funcţie. Coerciția implicită este declanșată de binar + operator, când orice operand este un șir:

String(123) // explicit
123 + ''    // implicit

Toate valorile primitive sunt convertite în șiruri în mod natural așa cum v-ați putea aștepta:

String(123)                   // '123'
String(-12.3)                 // '-12.3'
String(null)                  // 'null'
String(undefined)             // "https://www.freecodecamp.org/news/js-type-coercion-explained-27ba3d9a2839/undefined"
String(true)                  // 'true'
String(false)                 // 'false'

Conversia simbolurilor este puțin complicată, deoarece poate fi convertită doar în mod explicit, dar nu implicit. Citeste mai mult pe Symbol reguli de constrângere.

String(Symbol('my symbol'))   // 'Symbol(my symbol)'
'' + Symbol('my symbol')      // TypeError is thrown

Conversia booleană

Pentru a converti în mod explicit o valoare într-o valoare booleană, aplicați Boolean() funcţie.
Conversia implicită are loc în context logic sau este declanșată de operatori logici ( || && !).

Boolean(2)          // explicit
if (2) { ... }      // implicit due to logical context
!!2                 // implicit due to logical operator
2 || 'hello'        // implicit due to logical operator

Notă: Operatori logici precum || și && faceți conversii booleene pe plan intern, dar returnează de fapt valoarea operanzilor originali, chiar dacă nu sunt booleeni.

// returns number 123, instead of returning true
// 'hello' and 123 are still coerced to boolean internally to calculate the expression
let x = 'hello' && 123;   // x === 123

De îndată ce există doar 2 rezultate posibile ale conversiei booleene: true sau false, este doar mai ușor să vă amintiți lista valorilor false.

Boolean('')           // false
Boolean(0)            // false     
Boolean(-0)           // false
Boolean(NaN)          // false
Boolean(null)         // false
Boolean(undefined)    // false
Boolean(false)        // false

Orice valoare care nu este în listă este convertită la true, inclusiv obiect, funcție, Array, Date, tip definit de utilizator și așa mai departe. Simbolurile sunt valori adevărate. Obiectul și matricile goale sunt, de asemenea, valori reale:

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

Conversia numerică

Pentru o conversie explicită, aplicați doar Number() funcție, la fel cum ați făcut cu Boolean() și String() .

Conversia implicită este dificilă, deoarece se declanșează în mai multe cazuri:

  • operatorii de comparație (>, <, <=,>=)
  • operatori bitwise ( | & ^ ~)
  • operatori aritmetici (- + * / % ). Rețineți că acel binar+ nu declanșează conversia numerică, atunci când orice operand este un șir.
  • unar + operator
  • operator de egalitate liberă == (incl. !=).
    Rețineți că == nu declanșează conversia numerică atunci când ambii operanzi sunt șiruri.
Number('123')   // explicit
+'123'          // implicit
123 != '456'    // implicit
4 > '5'         // implicit
5/null          // implicit
true | 0        // implicit

Iată cum valorile primitive sunt convertite în numere:

Number(null)                   // 0
Number(undefined)              // NaN
Number(true)                   // 1
Number(false)                  // 0
Number(" 12 ")                 // 12
Number("-12.34")               // -12.34
Number("n")                   // 0
Number(" 12s ")                // NaN
Number(123)                    // 123

Când convertește un șir într-un număr, motorul tunde mai întâi spațiul alb care conduce și trece, n, t personaje, întorcându-se NaN dacă șirul tăiat nu reprezintă un număr valid. Dacă șirul este gol, acesta revine 0.

null și undefined sunt tratate diferit: null devine 0, întrucât undefined devine NaN.

Simbolurile nu pot fi convertite într-un număr nici explicit, nici implicit. În plus, TypeError este aruncat, în loc să se convertească în tăcere la NaN, așa cum se întâmplă pentru undefined. Vedeți mai multe despre regulile de conversie a simbolurilor MDN.

Number(Symbol('my symbol'))    // TypeError is thrown
+Symbol('123')                 // TypeError is thrown

Sunt două reguli speciale a ține minte:

  1. Când aplicați == la null sau undefined, conversia numerică nu se întâmplă. null este egal doar cu null sau undefined, și nu este egal cu nimic altceva.
null == 0               // false, null is not converted to 0
null == null            // true
undefined == undefined  // true
null == undefined       // true

2. NaN nu este egal cu nimic chiar și el însuși:

if (value !== value) { console.log("we're dealing with NaN here") }

Tastați constrângerea pentru obiecte

Până în prezent, am analizat coerciția de tip pentru valorile primitive. Nu este foarte interesant.

Când vine vorba de obiecte și motorul întâlnește expresii de genul [1] + [2,3], mai întâi trebuie să convertească un obiect într-o valoare primitivă, care este apoi convertită la tipul final. Și totuși există doar trei tipuri de conversie: numerică, șir și boolean.

Cel mai simplu caz este conversia booleană: orice valoare neprimitivă este întotdeauna
constrâns la true, indiferent dacă un obiect sau o matrice sunt goale sau nu.

Obiectele sunt convertite în primitive prin intern [[ToPrimitive]] , care este responsabilă atât pentru conversia numerică, cât și pentru conversia șirurilor.

Iată o pseudo implementare a [[ToPrimitive]] metodă:

[[ToPrimitive]] este transmis cu o valoare de intrare și tipul preferat de conversie: Number sau String. preferredType este opțional.

Atât conversia numerică, cât și cea a șirurilor utilizează două metode ale obiectului de intrare: valueOf și toString . Ambele metode sunt declarate Object.prototype și astfel disponibil pentru orice tipuri derivate, cum ar fi Date, Array, etc.

În general, algoritmul este după cum urmează:

  1. Dacă intrarea este deja o primitivă, nu faceți nimic și returnați-o.

2. Sunați input.toString(), dacă rezultatul este primitiv, întoarceți-l.

3. Sunați input.valueOf(), dacă rezultatul este primitiv, întoarceți-l.

4. Dacă nici una, nici alta input.toString() nici input.valueOf() cedează primitiv, aruncă TypeError.

Primele apeluri de conversie numerică valueOf (3) cu o rezervă la toString (2). Conversia șirului face opusul: toString (2) urmat de valueOf (3).

Majoritatea tipurilor încorporate nu au valueOf, sau au valueOf întorcându-se this obiect în sine, deci este ignorat pentru că nu este un primitiv. De aceea, conversia numerică și șirul ar putea funcționa la fel – ambele ajung să apeleze toString().

Diferiti operatori pot declanșa conversia numerică sau a șirurilor cu ajutorul preferredType parametru. Dar există două excepții: egalitatea slabă == și binar + operatorii declanșează moduri de conversie implicite (preferredType nu este specificat sau este egal cu default). În acest caz, majoritatea tipurilor încorporate presupun conversia numerică ca implicit, cu excepția Date care face conversia șirului.

Iată un exemplu de Date comportament de conversie:

Puteți înlocui valoarea implicită toString() și valueOf() metode de conectare la logica de conversie obiect-la-primitiv.

Observați cum obj + ‘’ se intoarce ‘101’ ca un șir. + operatorul declanșează un mod de conversie implicit și așa cum am spus mai înainte Object presupune conversia numerică ca implicit, folosind astfel valueOf() prima metodă în loc de toString().

ES6 Symbol.toPrimitive method

În ES5 vă puteți conecta la logica de conversie obiect-primitiv prin suprascriere toString și valueOf metode.

În ES6 puteți merge mai departe și puteți înlocui complet internul[[ToPrimitive]] rutină prin implementarea[Symbol.toPrimtive] metoda pe un obiect.

Exemple

Înarmat cu teoria, să revenim acum la exemplele noastre:

true + false             // 1
12 / "6"                 // 2
"number" + 15 + 3        // 'number153'
15 + 3 + "number"        // '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[object Object]1'
!+[]+[]+![]              // 'truefalse'
new Date(0) - 0          // 0
new Date(0) + 0          // 'Thu Jan 01 1970 02:00:00(EET)0'

Mai jos puteți găsi explicații pentru fiecare expresie.

Binar + operatorul declanșează conversia numerică pentru true și false

true + false
==> 1 + 0
==> 1

Operator de diviziune aritmetică / declanșează conversia numerică pentru șir '6' :

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

Operator + are asociativitate de la stânga la dreapta, deci expresie "number" + 15 aleargă primul. Deoarece un operand este un șir, + operatorul declanșează conversia șirului pentru numărul 15. La expresia pasului doi "number15" + 3 este evaluat în mod similar.

“number” + 15 + 3 
==> "number15" + 3 
==> "number153"

Expresie 15 + 3 este evaluat mai întâi. Nu este deloc nevoie de constrângere, deoarece ambii operanzi sunt numere. La al doilea pas, expresie 18 + 'number' este evaluat și, deoarece un operand este un șir, declanșează o conversie de șir.

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

Operator de comparație &gt; declanșează conversia numerică for [1] And null.

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

Unar + operatorul are prioritate mai mare față de binar + operator. Asa de +'bar' expresia evaluează mai întâi. Unary plus declanșează conversia numerică pentru șir 'bar'. Deoarece șirul nu reprezintă un număr valid, rezultatul este NaN. La al doilea pas, expresie 'foo' + NaN este evaluat.

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

== operatorul declanșează conversia numerică, șirul 'true' este convertit în NaN, boolean true este convertit la 1.

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

false == 'false'   
==> 0 == NaN
==> false

== declanșează de obicei conversia numerică, dar nu este cazul null . null egal cu null sau undefined numai și nu este egal cu nimic altceva.

null == ''
==> false

!! operatorul convertește ambele 'true' și 'false' siruri de caractere pentru a boolean true, deoarece sunt șiruri ne-goale. Atunci, == doar verifică egalitatea a două booleeni true's fără nici o constrângere.

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

== operatorul declanșează o conversie numerică pentru o matrice. Array’s valueOf() metoda returnează matricea în sine și este ignorată deoarece nu este o primitivă. Array’s toString() converti ['x'] Doar pentru 'x' şir.

['x'] == 'x'  
==> 'x' == 'x'
==>  true

+ operatorul declanșează conversia numerică pentru []. Array’s valueOf() metoda este ignorată, deoarece returnează matricea însăși, care este neprimitivă. Array’s toString returnează un șir gol.

Pe cea de-a doua etapă expresie '' + null + 1 este evaluat.

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

Logic || și && operatorii constrâng operanzi la boolean, dar returnează operanzi originali (nu booleeni). 0 este fals, în timp ce '0' este adevărat, deoarece este un șir non-gol. {} obiectul gol este, de asemenea, adevărat.

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

Nu este nevoie de constrângere deoarece ambii operanzi au același tip. De cand == verifică identitatea obiectului (și nu egalitatea obiectului) și cele două tablouri sunt două instanțe diferite, rezultatul este false.

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

Toți operanzii sunt valori neprimitive, deci + începe cu cea mai stângă declanșare a conversiei numerice. Ambii Object’s și Array’s valueOf metoda returnează obiectul în sine, deci este ignorat. toString() este folosit ca rezervă. Trucul de aici este primul {} nu este considerat ca un obiect literal, ci mai degrabă ca o declarație de declarație de bloc, deci este ignorată. Evaluarea începe cu următoarea +[] expresie, care este convertită într-un șir gol prin intermediul toString() metoda și apoi la 0 .

{}+[]+{}+[1]
==> +[]+{}+[1]
==> 0 + {} + [1]
==> 0 + '[object Object]' + [1]
==> '0[object Object]' + [1]
==> '0[object Object]' + '1'
==> '0[object Object]1'

Acesta este mai bine explicat pas cu pas în funcție de prioritatea operatorului.

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

- operatorul declanșează conversia numerică pentru Date. Date.valueOf() returnează numărul de milisecunde de la epoca Unix.

new Date(0) - 0
==> 0 - 0
==> 0

+ operatorul declanșează conversia implicită. Data presupune conversia șirului ca fiind implicită, deci toString() se folosește mai degrabă metoda decât valueOf().

new Date(0) + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)' + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)0'

Resurse

Chiar vreau să recomand cartea excelentă „Înțelegerea ES6”Scris de Nicholas C. Zakas. Este o resursă de învățare ES6 excelentă, nu prea la nivel înalt și nu se sapă prea mult în interior.

Și iată o carte bună doar pentru ES5 – VorbindJS scris de Axel Rauschmayer.

(Rusă) Современный учебник Javascript – https://learn.javascript.ru/. In mod deosebit aceste Două pagini despre constrângerea de tip.

Tabel comparativ JavaScript – https://dorey.github.io/JavaScript-Equality-Table/

wtfjs – un mic blog cu cod despre limba pe care o iubim, în ciuda faptului că ne dă atât de mult de urât – https://wtfjs.com/