Paaiškinta „JavaScript“ tipo prievarta

Žinok savo variklius

[Redaguoti 2018-02-05] : Šis įrašas dabar pasiekiamas rusų kalba. Ploja Serjui Bulavykui už jo pastangas.

Tipo prievarta yra vertės pavertimo iš vieno tipo į kitą procesas (pvz., Eilutė į skaičių, objektas į loginę reikšmę ir pan.). Bet koks tipas - primityvus ar objektas - yra tinkamas tipo prievartos subjektas. Norint priminti, primityviai yra: skaičius, eilutė, loginė reikšmė, nulis, neapibrėžtas + simbolis (pridėtas ES6).

Kaip tipo prievartos pavyzdį praktikoje, pažvelkite į „JavaScript“ palyginimo lentelę, kurioje parodyta, kaip laisvosios lygybės ==operatorius elgiasi skirtingiems air btipams. Ši matrica atrodo baisi dėl numanomo tipo prievartos, kurią ==daro operatorius, ir vargu ar įmanoma prisiminti visus tuos derinius. Ir jūs neturite to daryti - tiesiog sužinokite pagrindinius tipo prievartos principus.

Šiame straipsnyje išsamiai aprašoma, kaip tipo prievarta veikia „JavaScript“, ir suteiks jums pagrindinių žinių, kad galėtumėte drąsiai paaiškinti, ką apskaičiuoja šios išraiškos. Straipsnio pabaigoje aš parodysiu atsakymus ir juos paaiškinsiu.

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

Taip, šiame sąraše gausu gana kvailų dalykų, kuriuos galite padaryti kaip kūrėjas. 90% vartojimo atvejų geriau vengti numanomos prievartos. Apsvarstykite šį sąrašą kaip mokymosi pratimą, kad patikrintumėte savo žinias, kaip veikia tipo prievarta. Jei jums nuobodu, galite rasti daugiau pavyzdžių wtfjs.com.

Beje, kartais galite susidurti su tokiais klausimais interviu dėl „JavaScript“ kūrėjo pozicijos. Taigi, toliau skaityk?

Numanoma ir atvira prievarta

Tipo prievarta gali būti aiški ir numanoma.

Kai kūrėjas išreiškia ketinimą konvertuoti tipus, parašydamas atitinkamą kodą, pvz. Number(value), Tai vadinama aiškia tipo prievarta (arba tipo liejimu).

Kadangi „JavaScript“ yra silpnai įvesta kalba, reikšmės taip pat gali būti automatiškai konvertuojamos tarp skirtingų tipų ir tai vadinama implicitine tipo prievarta . Paprastai tai atsitinka, kai pritaikote operatorius skirtingų tipų vertėms, pvz

1 == null, 2/’5', null + new Date()Arba jis gali būti sukeltas kontekstą, kaip su if (value) {…}, kur valueyra verčiamas Būlio.

Vienas operatorius, kuris nesukelia numanomos tipo prievartos, yra tas ===, kuris vadinamas griežtosios lygybės operatoriumi. ==Kita vertus, laisvosios lygybės operatorius , jei reikia, daro ir palyginimą, ir tipo prievartą.

Numanoma tipo prievarta yra dvigubo krašto kardas: tai puikus nusivylimo ir defektų šaltinis, tačiau taip pat naudingas mechanizmas, leidžiantis parašyti mažiau kodo, neprarandant skaitomumo.

Trys konversijos tipai

Pirmiausia reikia žinoti, kad „JavaScript“ yra tik trys konversijų tipai:

  • virvelei
  • į loginę
  • prie numerio

Antra, primityvių ir objektų konversijos logika veikia skirtingai, tačiau tiek primityvus, tiek objektus galima konvertuoti tik šiais trim būdais.

Pirmiausia pradėkime nuo primityvų.

Stygų konvertavimas

Norėdami aiškiai konvertuoti reikšmes į eilutę, pritaikykite String()funkciją. Numanomą prievartą sukelia dvejetainis +operatorius, kai bet kuris operandas yra eilutė:

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

Visos primityvios reikšmės natūraliai paverčiamos eilutėmis, kaip galite tikėtis:

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

Simbolių keitimas yra šiek tiek keblus, nes juos galima konvertuoti tik aiškiai, bet netiesiogiai. Skaitykite daugiau apie Symbolprievartos taisykles.

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

Būlo konvertavimas

Norėdami aiškiai konvertuoti vertę į loginę reikšmę, pritaikykite Boolean()funkciją.

Numanomas konvertavimas vyksta loginiame kontekste arba jį suaktyvina loginiai operatoriai ( ||&&!).

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

Pastaba : Tokie loginiai operatoriai, kaip loginės konversijos , ||ir jas &&atlieka viduje, tačiau iš tikrųjų grąžina originalių operandų vertę, net jei jie nėra loginiai.

// 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

Kai tik yra tik 2 galimi loginės konversijos rezultatai: truearba falsepaprasčiau prisiminti klaidingų verčių sąrašą.

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

Bet kokia vertė, kuri yra sąraše nėra konvertuojama į true, įskaitant objekto, funkcijos, Array, Date, vartotojo apibrėžtas tipo ir pan. Simboliai yra tikros vertybės. Tuščias objektas ir masyvai taip pat yra tikros vertės:

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

Skaitmeninis perskaičiavimas

Jei norite atlikti aiškų konversiją, tiesiog naudokite Number()funkciją, tą patį, ką darėte su Boolean()ir String().

Numanomas konvertavimas yra keblus, nes jis suaktyvinamas daugiau atvejų:

  • palyginimo operatoriai ( >, <, <=, >=)
  • bitų operatoriai ( |&^~)
  • aritmetiniai operatoriai ( -+*/%). Atkreipkite dėmesį, kad dvejetainis skaičius +nesukelia skaitinės konvertavimo, kai bet kuris operandas yra eilutė.
  • Vienetinis +operatorius
  • laisvas lygybės operatorius ==(įsk. !=).

    Atkreipkite dėmesį, kad ==nesuaktyvina skaitinės konvertavimo, kai abi operandai yra eilutės.

Number('123') // explicit +'123' // implicit 123 != '456' // implicit 4 > '5' // implicit 5/null // implicit true | 0 // implicit

Štai kaip pirminės reikšmės paverčiamos skaičiais:

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

Konvertuojant eilutę į skaičių, variklis pirmiausia apipjausto priekinius ir užbaigusius tarpus \n, \tsimbolius, grįždamas, NaNjei apkarpyta eilutė neatitinka galiojančio skaičiaus. Jei eilutė tuščia, ji grąžinama 0.

null and undefined are handled differently: null becomes 0, whereas undefined becomes NaN.

Symbols cannot be converted to a number neither explicitly nor implicitly. Moreover, TypeError is thrown, instead of silently converting to NaN, like it happens for undefined. See more on Symbol conversion rules on MDN.

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

There are two special rules to remember:

  1. When applying == to null or undefined, numeric conversion does not happen. null equals only to null or undefined, and does not equal to anything else.
null == 0 // false, null is not converted to 0 null == null // true undefined == undefined // true null == undefined // true

2. NaN does not equal to anything even itself:

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

Type coercion for objects

So far, we’ve looked at type coercion for primitive values. That’s not very exciting.

When it comes to objects and engine encounters expression like [1] + [2,3], first it needs to convert an object to a primitive value, which is then converted to the final type. And still there are only three types of conversion: numeric, string and boolean.

The simplest case is boolean conversion: any non-primitive value is always

coerced to true, no matter if an object or an array is empty or not.

Objects are converted to primitives via the internal [[ToPrimitive]] method, which is responsible for both numeric and string conversion.

Here is a pseudo implementation of [[ToPrimitive]] method:

[[ToPrimitive]] is passed with an input value and preferred type of conversion: Number or String. preferredType is optional.

Both numeric and string conversion make use of two methods of the input object: valueOf and toString . Both methods are declared on Object.prototype and thus available for any derived types, such as Date, Array, etc.

In general the algorithm is as follows:

  1. If input is already a primitive, do nothing and return it.

2. Call input.toString(), if the result is primitive, return it.

3. Call input.valueOf(), if the result is primitive, return it.

4. If neither input.toString() nor input.valueOf() yields primitive, throw TypeError.

Numeric conversion first calls valueOf (3) with a fallback to toString (2). String conversion does the opposite: toString (2) followed by valueOf (3).

Most built-in types do not have valueOf, or have valueOf returning this object itself, so it’s ignored because it’s not a primitive. That’s why numeric and string conversion might work the same — both end up calling toString().

Different operators can trigger either numeric or string conversion with a help of preferredType parameter. But there are two exceptions: loose equality == and binary + operators trigger default conversion modes (preferredType is not specified, or equals to default). In this case, most built-in types assume numeric conversion as a default, except Date that does string conversion.

Here is an example of Date conversion behavior:

You can override the default toString() and valueOf() methods to hook into object-to-primitive conversion logic.

Notice how obj + ‘’ returns ‘101’ as a string. + operator triggers a default conversion mode, and as said before Object assumes numeric conversion as a default, thus using the valueOf() method first instead of toString().

ES6 Symbol.toPrimitive method

In ES5 you can hook into object-to-primitive conversion logic by overriding toString and valueOf methods.

In ES6 you can go farther and completely replace internal[[ToPrimitive]] routine by implementing the[Symbol.toPrimtive] method on an object.

Examples

Armed with the theory, now let’s get back to our examples:

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'

Below you can find explanation for each the expression.

Binary + operator triggers numeric conversion for true and false

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

Arithmetic division operator / triggers numeric conversion for string '6' :

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

Operator + has left-to-right associativity, so expression "number" + 15 runs first. Since one operand is a string, + operator triggers string conversion for the number 15. On the second step expression "number15" + 3 is evaluated similarly.

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

Expression 15 + 3 is evaluated first. No need for coercion at all, since both operands are numbers. On the second step, expression 18 + 'number' is evaluated, and since one operand is a string, it triggers a string conversion.

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

Comparison operator &gt; triggers numeric conversion for [1] and null .

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

Unary + operator has higher precedence over binary + operator. So +'bar' expression evaluates first. Unary plus triggers numeric conversion for string 'bar'. Since the string does not represent a valid number, the result is NaN. On the second step, expression 'foo' + NaN is evaluated.

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

== operator triggers numeric conversion, string 'true' is converted to NaN, boolean true is converted to 1.

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

== usually triggers numeric conversion, but it’s not the case with null . null equals to null or undefined only, and does not equal to anything else.

null == '' ==> false

!! operator converts both 'true' and 'false' strings to boolean true, since they are non-empty strings. Then, == just checks equality of two boolean true's without any coercion.

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

== operator triggers a numeric conversion for an array. Array’s valueOf() method returns the array itself, and is ignored because it’s not a primitive. Array’s toString() converts ['x'] to just 'x' string.

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

+ operator triggers numeric conversion for []. Array’s valueOf() method is ignored, because it returns array itself, which is non-primitive. Array’s toString returns an empty string.

On the the second step expression '' + null + 1 is evaluated.

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

Logical || and && operators coerce operands to boolean, but return original operands (not booleans). 0 is falsy, whereas '0' is truthy, because it’s a non-empty string. {} empty object is truthy as well.

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

No coercion is needed because both operands have same type. Since == checks for object identity (and not for object equality) and the two arrays are two different instances, the result is false.

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

All operands are non-primitive values, so + starts with the leftmost triggering numeric conversion. Both Object’s and Array’svalueOf method returns the object itself, so it’s ignored. toString() is used as a fallback. The trick here is that first {} is not considered as an object literal, but rather as a block declaration statement, so it’s ignored. Evaluation starts with next +[] expression, which is converted to an empty string via toString() method and then to 0 .

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

This one is better explained step by step according to operator precedence.

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

- operator triggers numeric conversion for Date. Date.valueOf() returns number of milliseconds since Unix epoch.

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

+ operator triggers default conversion. Date assumes string conversion as a default one, so toString() method is used, rather than 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'

Resources

I really want to recommend the excellent book “Understanding ES6” written by Nicholas C. Zakas. It’s a great ES6 learning resource, not too high-level, and does not dig into internals too much.

And here is a good book on ES5 only - SpeakingJS written by Axel Rauschmayer.

(Russian) Современный учебник Javascript — //learn.javascript.ru/. Especially these two pages on type coercion.

JavaScript Comparison Table — //dorey.github.io/JavaScript-Equality-Table/

wtfjs - nedidelis kodinis tinklaraštis apie tą kalbą, kurią mes mėgstame, nepaisant to, kad suteikėme mums tiek daug neapykantos - //wtfjs.com/