Ismerje motorjait

[Szerkesztés 2018.02.05.] : Ez a bejegyzés már elérhető orosz nyelven. Tapogat Serj Bulavykért erőfeszítéseiért.
A típus kényszerítése az érték egyik típusból a másikba konvertálásának folyamata (például karakterlánc számra, objektum logikai értékre stb.). Bármely típus, legyen az primitív vagy objektum, érvényes alany a típus kényszerítésének. Emlékeztetni kell arra, hogy a primitívek a következők: szám, karakterlánc, logikai érték, null, undefined + Szimbólum (hozzáadva ES6-ban).
A gyakorlati típusú kényszer példaként tekintse meg a JavaScript összehasonlító táblázatot, amely megmutatja, hogy a laza egyenlőség ==
operátor hogyan viselkedik a különböző típusok a
és b
típusok esetében. Ez a mátrix ijesztőnek tűnik az ==
operátor általi implicit típusú kényszer miatt , és aligha lehet emlékezni ezekre a kombinációkra. És ezt nem kell tennie - csak meg kell tanulnia a mögöttes típusú kényszerítés alapelveit.
Ez a cikk elmélyülten elmagyarázza, hogy a típusú kényszer hogyan működik a JavaScript-ben, és felkarolja Önt az alapvető tudással, így nyugodtan elmagyarázhatja, hogy az alábbi kifejezések mire számítanak. A cikk végére megmutatom a válaszokat és elmagyarázom azokat.
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
Igen, ez a lista tele van elég ostoba dolgokkal, amelyeket fejlesztőként megtehet. A használati esetek 90% -ában jobb elkerülni az implicit típusú kényszert. Tekintse ezt a listát tanulási gyakorlatként, hogy tesztelje tudását a típusú kényszer működéséről. Ha unatkozol, további példákat találhatsz a wtfjs.com oldalon.
Egyébként előfordulhat, hogy ilyen interjúkkal szembe kell néznie egy JavaScript-fejlesztői pozícióval kapcsolatban. Szóval, olvass tovább?
Implicit vs explicit kényszer
A típusú kényszer lehet explicit és implicit.
Amikor a fejlesztő kifejezi a típusok közötti konverzió szándékát a megfelelő kód megírásával Number(value)
, ezt kifejezett típusú kényszernek (vagy típus castingnak) hívják .
Mivel a JavaScript gyengén tipizált nyelv, az értékeket a különböző típusok között automatikusan is átalakíthatjuk, és ezt implicit típusú kényszernek nevezzük . Ez általában akkor történik, amikor az operátorokat különböző típusú értékekre, például
1 == null
, 2/’5'
, null + new Date()
, Vagy lehet váltja ki a környező összefüggésben, mint a if (value) {…}
, ahol value
a kényszer, hogy logikai.
Az egyik olyan operátor, amely nem vált ki implicit típusú kényszert, az az ===
, amelyet szigorú egyenlőség operátornak neveznek. A laza egyenlőség-operátor ==
viszont szükség esetén összehasonlítást és típusú kényszert is végrehajt.
Az implicit típusú kényszer kettős élű kard: nagyszerű frusztráció és hibák forrása, ugyanakkor hasznos mechanizmus, amely lehetővé teszi számunkra, hogy kevesebb kódot írjunk az olvashatóság elvesztése nélkül.
Három típusú konverzió
Az első tudnivaló, hogy a JavaScriptben csak három típusú konverzió létezik:
- fűzni
- logikai
- számozáshoz
Másodszor, a primitívek és az objektumok konverziós logikája másképpen működik, de mind a primitívek, mind az objektumok csak ezen a három módon konvertálhatók.
Kezdjük először a primitívekkel.
Karakterlánc-átalakítás
Az értékek kifejezett karakterláncokká történő átalakításához alkalmazza a String()
függvényt. Az implicit kényszert a bináris +
operátor váltja ki , ha bármely operandus karakterlánc:
String(123) // explicit 123 + '' // implicit
Minden primitív értéket természetesen karakterláncokká alakítanak át, amire számíthat:
String(123) // '123' String(-12.3) // '-12.3' String(null) // 'null' String(undefined) // 'undefined' String(true) // 'true' String(false) // 'false'
A szimbólumkonvertálás kissé trükkös, mert csak kifejezetten, de implicit módon nem konvertálható. További információ a Symbol
kényszerítési szabályokról.
String(Symbol('my symbol')) // 'Symbol(my symbol)' '' + Symbol('my symbol') // TypeError is thrown
Logikai konverzió
Az érték kifejezett logikai konvertálásához alkalmazza a Boolean()
függvényt.
Az implicit átalakítás logikai kontextusban történik, vagy logikai operátorok váltják ki ( ||
&&
!
).
Boolean(2) // explicit if (2) { ... } // implicit due to logical context !!2 // implicit due to logical operator 2 || 'hello' // implicit due to logical operator
Megjegyzés : A logikai operátorok boolean konverziókat hajtanak végre ||
és &&
belsőleg hajtanak végre, de valójában visszaadják az eredeti operandusok értékét, még akkor is, ha azok nem logikai értékek.
// 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
Amint csak két lehetséges eredménye van a logikai konvertálásnak :, true
vagy false
könnyebb megjegyezni a hamis értékek listáját.
Boolean('') // false Boolean(0) // false Boolean(-0) // false Boolean(NaN) // false Boolean(null) // false Boolean(undefined) // false Boolean(false) // false
Bármely érték, amely nincs a listán, konvertálásra kerül true
, ideértve az objektumot, a függvényt Array
, a Date
, a felhasználó által definiált típust stb. A szimbólumok valóságos értékek. Az üres objektum és tömbök valósághű értékek is:
Boolean({}) // true Boolean([]) // true Boolean(Symbol()) // true !!Symbol() // true Boolean(function() {}) // true
Numerikus átalakítás
Egy explicit konverzióhoz csak alkalmazza a Number()
függvényt, ugyanúgy, mint a Boolean()
és a billentyűkkel String()
.
Az implicit konverzió bonyolult, mert több esetben váltja ki:
- összehasonlító operátorokat (
>
,<
,<=
,>=
) - bitenkénti operátorok (
|
&
^
~
) - számtani operátorok (
-
+
*
/
%
). Ne feledje, hogy a bináris+
nem vált ki numerikus átalakítást, ha bármely operandus karakterlánc. - unáris
+
operátor - laza egyenlőség operátor
==
(beleértve!=
).Vegye figyelembe, hogy
==
ez nem váltja ki a numerikus átalakítást, ha mindkét operandus karakterlánc.
Number('123') // explicit +'123' // implicit 123 != '456' // implicit 4 > '5' // implicit 5/null // implicit true | 0 // implicit
Így alakítják a primitív értékeket számokká:
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
Ha egy karakterláncot számgá konvertál, akkor a motor először levágja és elhagyja a szóközöket \n
,, \t
karaktereket, visszatérve, NaN
ha a levágott karakterlánc nem érvényes számot képvisel. Ha a karakterlánc üres, akkor visszatér 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:
- When applying
==
tonull
orundefined
, numeric conversion does not happen.null
equals only tonull
orundefined
, 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:
- 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 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 — a little code blog about that language we love despite giving us so much to hate — //wtfjs.com/