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 a
og b
typer. 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 value
der 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 Symbol
tvangsregler.
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: true
eller 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
, \t
tegn, vender tilbage NaN
, hvis den trimmede strengen ikke repræsenterer et gyldigt tal. Hvis strengen er tom, vender den tilbage 0
.
null
og undefined
håndteres forskelligt: null
bliver 0
, hvorimod undefined
bliver NaN
.
Symboler kan ikke konverteres til et tal hverken eksplicit eller implicit. Desuden TypeError
kastes 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:
- Ved anvendelse
==
tilnull
ellerundefined
sker der ikke numerisk konvertering.null
svarer kun tilnull
ellerundefined
, 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: Number
eller String
. preferredType
er valgfri.
Både numerisk og strengkonvertering bruger to metoder til inputobjektet: valueOf
og toString
. Begge metoder er erklæret på Object.prototype
og dermed til rådighed for eventuelle afledte typer, såsom Date
, Array
etc.
Generelt er algoritmen som følger:
- 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 n
ull .
[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’s
valueOf
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/