JavaScript型強制の説明

あなたのエンジンを知っている

[2018年2月5編集]:この投稿はロシア語で利用できるようになりました。SerjBulavykの努力に拍手喝采。

型強制は、値をある型から別の型に変換するプロセスです(文字列から数値、オブジェクトからブール値など)。プリミティブであろうとオブジェクトであろうと、どの型も型強制の有効な対象です。思い出してください。プリミティブは、数値、文字列、ブール値、null、未定義+シンボル(ES6で追加)です。

実際に型強制の例として、JavaScriptの比較表を見ている様子を示して緩い等号==異なるためのオペレータに振舞うabタイプ。この行列は、==演算子が行う暗黙の型強制のために恐ろしく見え、これらすべての組み合わせを覚えることはほとんど不可能です。そして、それを行う必要はありません。基礎となる型強制の原則を学ぶだけです。

この記事では、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 == null2/’5'null + new Date()、またはそれと同じように、周囲のコンテキストによってトリガすることができるif (value) {…}場合、valueブール値に強制されます。

暗黙の型強制をトリガーしない演算子の1つは===、であり、これは厳密な等式演算子と呼ばれます。==一方、ルーズイコリティ演算子は、必要に応じて比較と型強制の両方を実行します。

暗黙的な型強制は両刃の剣です。これはフラストレーションや欠陥の大きな原因ですが、読みやすさを失うことなくコードを少なくすることができる便利なメカニズムでもあります。

3種類の変換

知っておくべき最初のルールは、JavaScriptには3種類の変換しかないということです。

  • 文字列に
  • ブール値に
  • 番号に

次に、プリミティブとオブジェクトの変換ロジックの動作は異なりますが、プリミティブとオブジェクトの両方は、これら3つの方法でのみ変換できます。

まず、プリミティブから始めましょう。

文字列変換

値を文字列に明示的に変換するには、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オブジェクト、関数、など、ArrayDate、ユーザ定義型など。記号は真の値です。空のオブジェクトと配列も真の値です。

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

  1. When applying == to null or undefined, numeric conversion does not happen. null equals only to null or undefined, 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:

  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/