Обяснена е принуда от тип JavaScript

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

[Редактиране на 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

Има две специални правила, които трябва да запомните:

  1. Когато кандидатствате ==за 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:

  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 — a little code blog about that language we love despite giving us so much to hate — //wtfjs.com/