Познайте двигателите си

[Редактиране на 5/8/2018] : Тази публикация вече е достъпна на руски език. Пляска на Серж Булавик за усилията му.
Принудата на типа е процес на преобразуване на стойност от един тип в друг (като низ в число, обект в булева и т.н.). Всеки тип, било то примитивен или обект, е валиден предмет за принуда на типа. За да си припомним, примитивите са: число, низ, булев, нулев, недефиниран + символ (добавен в ES6).
Като пример за принуда на типа на практика, погледнете таблицата за сравнение на JavaScript, която показва как се ==
държи операторът на свободно равенство за различни a
и b
типове. Тази матрица изглежда плашеща поради принудителна принуда от типа, която ==
операторът прави, и едва ли е възможно да запомним всички тези комбинации. И не е нужно да правите това - просто научете основните принципи на принуда от типа.
Тази статия е задълбочена за това как работи принудата на типа в JavaScript и ще ви предостави основните знания, така че можете да се чувствате уверени, обяснявайки на какво се изчисляват следните изрази. В края на статията ще покажа отговори и ще ги обясня.
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
Да, този списък е пълен с доста глупави неща, които можете да направите като разработчик. В 90% от случаите на използване е по-добре да се избягва принуда от неявен тип. Помислете за този списък като учебно упражнение, за да проверите знанията си за това как работи принудата тип. Ако ви е скучно, можете да намерите още примери на wtfjs.com.
Между другото, понякога може да се сблъскате с такива въпроси в интервюто за позиция на разработчика на JavaScript. И така, продължете да четете?
Имплицитна срещу изрична принуда
Принудата на типа може да бъде явна и имплицитна.
Когато разработчикът изразява намерението да конвертира между типове, като напише съответния код, например Number(value)
, това се нарича изрично принуда на типа (или преливане на тип).
Тъй като JavaScript е слабо типизиран език, стойностите също могат да се преобразуват автоматично между различни типове и това се нарича принуда за неявен тип . Обикновено това се случва, когато прилагате оператори към стойности от различен тип, като
1 == null
, 2/’5'
, null + new Date()
, Или може да бъде предизвикана от околния контекст, като с if (value) {…}
, където value
е принуден да Булева.
Един оператор, който не задейства принуда от неявен тип, е ===
, който се нарича оператор за строго равенство. Операторът за свободно равенство, ==
от друга страна, прави както сравнение, така и принуда за тип, ако е необходимо.
Принудата от неявен тип е меч с двоен ръб: това е чудесен източник на разочарование и дефекти, но също така и полезен механизъм, който ни позволява да пишем по-малко код, без да губим четливостта.
Три вида преобразуване
Първото правило, което трябва да знаете е, че в JavaScript има само три типа преобразуване:
- да низ
- до булево
- да се брои
На второ място, логиката на преобразуване за примитиви и обекти работи по различен начин, но както примитивите, така и обектите могат да бъдат преобразувани само по тези три начина.
Нека първо започнем с примитивите.
Преобразуване на низове
За да конвертирате изрично стойности в низ, приложете String()
функцията. Имплицитната принуда се задейства от двоичния +
оператор, когато всеки операнд е низ:
String(123) // explicit 123 + '' // implicit
Всички примитивни стойности се преобразуват в низове естествено, както може да се очаква:
String(123) // '123' String(-12.3) // '-12.3' String(null) // 'null' String(undefined) // 'undefined' String(true) // 'true' String(false) // 'false'
Преобразуването на символи е малко сложно, защото може да се преобразува само изрично, но не и неявно. Прочетете повече за Symbol
правилата за принуда.
String(Symbol('my symbol')) // 'Symbol(my symbol)' '' + Symbol('my symbol') // TypeError is thrown
Булево преобразуване
За да преобразувате изрично стойност в булева стойност, приложете Boolean()
функцията.
Неявното преобразуване се случва в логически контекст или се задейства от логически оператори ( ||
&&
!
).
Boolean(2) // explicit if (2) { ... } // implicit due to logical context !!2 // implicit due to logical operator 2 || 'hello' // implicit due to logical operator
Забележка : Логическите оператори като ||
и &&
правят логически преобразувания вътрешно, но всъщност връщат стойността на оригиналните операнди, дори ако те не са булеви.
// 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
Щом има само 2 възможни резултата от логическо преобразуване: true
или false
просто е по-лесно да запомните списъка с фалшиви стойности.
Boolean('') // false Boolean(0) // false Boolean(-0) // false Boolean(NaN) // false Boolean(null) // false Boolean(undefined) // false Boolean(false) // false
Всяка стойност, която не е в списъка, се превръща true
, включително обект, функция, Array
, Date
, дефинирани от потребителя тип, и така нататък. Символите са истински стойности. Празни обекти и масиви също са истински стойности:
Boolean({}) // true Boolean([]) // true Boolean(Symbol()) // true !!Symbol() // true Boolean(function() {}) // true
Числово преобразуване
За изрично преобразуване просто приложете Number()
функцията, същата като тази с Boolean()
и String()
.
Неявното преобразуване е сложно, защото се задейства в повече случаи:
- оператори за сравнение (
>
,<
,<=
,>=
) - битови оператори (
|
&
^
~
) - аритметични оператори (
-
+
*
/
%
). Обърнете внимание, че двоичното+
не задейства числово преобразуване, когато който и да е операнд е низ. - единен
+
оператор - свободен оператор на равенство
==
(вкл.!=
).Имайте предвид, че
==
не задейства числово преобразуване, когато и двата операнда са низове.
Number('123') // explicit +'123' // implicit 123 != '456' // implicit 4 > '5' // implicit 5/null // implicit true | 0 // implicit
Ето как примитивните стойности се преобразуват в числа:
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
,, \t
символи, връщайки се, NaN
ако отрязаният низ не представлява валидно число. Ако низът е празен, той се връща 0
.
null
и undefined
се обработват по различен начин: null
става 0
, докато undefined
става NaN
.
Символите не могат да бъдат преобразувани в число нито изрично, нито неявно. Освен това TypeError
се хвърля, вместо да се преобразува безшумно NaN
, както се случва за undefined
. Вижте повече за правилата за преобразуване на символи в MDN.
Number(Symbol('my symbol')) // TypeError is thrown +Symbol('123') // TypeError is thrown
Има две специални правила, които трябва да запомните:
- Когато кандидатствате
==
заnull
илиundefined
, числовото преобразуване не се случва.null
е равно само наnull
илиundefined
и не е равно на нищо друго.
null == 0 // false, null is not converted to 0 null == null // true undefined == undefined // true null == undefined // true
2. NaN не се равнява на нищо дори на себе си:
if (value !== value) { console.log("we're dealing with NaN here") }
Тип принуда за обекти
Досега разгледахме принудата за тип за примитивни стойности. Това не е много вълнуващо.
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/