Tvangs JavaScript-type forklaret

Kend dine motorer

[Rediger 2/5/2018] : Dette indlæg er nu tilgængeligt på russisk. Klapper sammen med Serj Bulavyk for hans indsats.

Type tvang er processen med at konvertere værdi fra en type til en anden (såsom streng til nummer, objekt til boolsk osv.). Enhver type, hvad enten det er primitiv eller et objekt, er et gyldigt emne for typetvingelse. For at huske er primitive: tal, streng, boolsk, null, udefineret + symbol (tilføjet i ES6).

Som et eksempel på type tvang i praksis skal du se på JavaScript-sammenligningstabellen, der viser, hvordan den løse ligningsoperator ==opfører sig for forskellige aog btyper. Denne matrix ser skræmmende ud på grund af implicit type tvang, som ==operatøren gør, og det er næppe muligt at huske alle disse kombinationer. Og du behøver ikke gøre det - bare lær de underliggende tvangsprincipper.

Denne artikel går i dybden med, hvordan type tvang fungerer i JavaScript, og vil bevæbne dig med den vigtige viden, så du kan føle dig sikker på at forklare, hvad følgende udtryk beregner til. I slutningen af ​​artiklen viser jeg svarene og forklarer dem.

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

Ja, denne liste er fuld af temmelig fjollede ting, du kan gøre som udvikler. I 90% af brugssagerne er det bedre at undgå implicit type tvang. Overvej denne liste som en læringsøvelse for at teste din viden om, hvordan type tvang fungerer. Hvis du keder dig, kan du finde flere eksempler på wtfjs.com.

Forresten kan du nogle gange stå over for sådanne spørgsmål i interviewet til en JavaScript-udviklerposition. Så fortsæt med at læse?

Implicit vs. eksplicit tvang

Type tvang kan være eksplicit og implicit.

Når en udvikler udtrykker intentionen om at konvertere mellem typer ved at skrive den relevante kode, Number(value)kaldes det eksplicit type tvang (eller type casting).

Da JavaScript er et svagt skrevet sprog, kan værdier også automatisk konverteres mellem forskellige typer, og det kaldes implicit type tvang . Det sker normalt, når du anvender operatorer på værdier af forskellige typer, f.eks

1 == null, 2/’5', null + new Date(), Eller det kan være udløst af den omgivende kontekst, ligesom med if (value) {…}, hvor valueder er tvunget til boolean.

En operatør, der ikke udløser implicit type tvang, er ===, der kaldes den strenge ligestillingsoperatør. Den løse ligestillingsoperatør gør ==på den anden side både sammenligning og type tvang, hvis det er nødvendigt.

Implicit type tvang er et dobbeltkant sværd: det er en stor kilde til frustration og mangler, men også en nyttig mekanisme, der giver os mulighed for at skrive mindre kode uden at miste læsbarheden.

Tre typer konvertering

Den første regel at vide er, at der kun er tre typer konvertering i JavaScript:

  • til streng
  • til boolsk
  • at nummerere

For det andet fungerer konverteringslogik for primitiver og objekter forskelligt, men både primitiver og objekter kan kun konverteres på disse tre måder.

Lad os starte med primitiver først.

Strengkonvertering

For at eksplicit konvertere værdier til en streng skal du anvende String()funktionen. Implicit tvang udløses af den binære +operatør, når en hvilken som helst operand er en streng:

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

Alle primitive værdier konverteres til strenge naturligt, som du kunne forvente:

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

Symbolkonvertering er lidt vanskelig, fordi den kun kan konverteres eksplicit, men ikke implicit. Læs mere om Symboltvangsregler.

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

Boolsk konvertering

Brug funktionen til at eksplicit konvertere en værdi til en boolsk Boolean().

Implicit konvertering sker i logisk sammenhæng eller udløses af logiske operatorer ( ||&&!).

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

Bemærk : Logiske operatorer som f.eks. ||Og &&foretager boolske konverteringer internt, men returnerer faktisk værdien af ​​originale operander, selvom de ikke er boolske.

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

Så snart der kun er 2 mulige resultater af boolsk konvertering: trueeller false, er det bare lettere at huske listen over falske værdier.

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

Enhver værdi, der ikke er i listen konverteres til true, herunder objekt, funktion, Array, Date, brugerdefineret type, og så videre. Symboler er sandhedsværdier. Tomt objekt og arrays er også sandhedsværdier:

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

Numerisk konvertering

For en eksplicit konvertering skal du bare bruge Number()funktionen, som du gjorde med Boolean()og String().

Implicit konvertering er vanskelig, fordi den udløses i flere tilfælde:

  • sammenligningsoperatorer ( >, <, <=, >=)
  • bitvise operatører ( |&^~)
  • aritmetiske operatorer ( -+*/%). Bemærk, at binær +ikke udløser numerisk konvertering, når en hvilken som helst operand er en streng.
  • unary +operatør
  • løs ligestillingsoperatør ==(inkl. !=).

    Bemærk, at ==der ikke udløser numerisk konvertering, når begge operander er strenge.

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

Sådan konverteres primitive værdier til tal:

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

Når du konverterer en streng til et tal, at motoren først beskærer indledende og afsluttende mellemrum, \n, \ttegn, vender tilbage NaN, hvis den trimmede strengen ikke repræsenterer et gyldigt tal. Hvis strengen er tom, vender den tilbage 0.

nullog undefinedhåndteres forskelligt: nullbliver 0, hvorimod undefinedbliver NaN.

Symboler kan ikke konverteres til et tal hverken eksplicit eller implicit. Desuden TypeErrorkastes i stedet for lydløst at konvertere til NaN, som det sker for undefined. Se mere om symbolkonverteringsregler på MDN.

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

Der er to specielle regler at huske:

  1. Ved anvendelse ==til nulleller undefinedsker der ikke numerisk konvertering. nullsvarer kun til nulleller undefined, og svarer ikke til noget andet.
null == 0 // false, null is not converted to 0 null == null // true undefined == undefined // true null == undefined // true

2. NaN svarer ikke til noget selv:

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

Skriv tvang til genstande

Indtil videre har vi kigget på tvang af typen for primitive værdier. Det er ikke særlig spændende.

Når det kommer til objekter, og motoren møder udtryk som [1] + [2,3], skal den først konvertere et objekt til en primitiv værdi, som derefter konverteres til den endelige type. Og der er stadig kun tre typer konvertering: numerisk, streng og boolsk.

Det enkleste tilfælde er boolsk konvertering: enhver ikke-primitiv værdi er altid

tvunget til true, uanset om et objekt eller et array er tomt eller ej.

Objekter konverteres til primitive via den interne [[ToPrimitive]]metode, som er ansvarlig for både numerisk og strengkonvertering.

Her er en pseudo-implementering af [[ToPrimitive]]metoden:

[[ToPrimitive]]sendes med en inputværdi og foretrukken type konvertering: Numbereller String. preferredTypeer valgfri.

Både numerisk og strengkonvertering bruger to metoder til inputobjektet: valueOfog toString. Begge metoder er erklæret på Object.prototypeog dermed til rådighed for eventuelle afledte typer, såsom Date, Arrayetc.

Generelt er algoritmen som følger:

  1. Hvis input allerede er primitivt, skal du ikke gøre noget og returnere det.

2. Ring input.toString(), hvis resultatet er primitivt, skal du returnere det.

3. Ring input.valueOf(), hvis resultatet er primitivt, skal du returnere det.

4. Hvis hverken input.toString()eller input.valueOf()giver primitiv, kast TypeError.

Numerisk konvertering kalder først valueOf(3) med et tilbagefald til toString(2). Strengkonvertering gør det modsatte: toString(2) efterfulgt af 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 - en lille kodeblog om det sprog, vi elsker, på trods af at vi giver os så meget at hade - //wtfjs.com/