JavaScript-Zwang erklärt

Kennen Sie Ihre Motoren

[Edit 2/5/2018] : Dieser Beitrag ist jetzt in russischer Sprache verfügbar. Klatscht an Serj Bulavyk für seine Bemühungen.

Beim Typzwang wird der Wert von einem Typ in einen anderen konvertiert (z. B. Zeichenfolge in Zahl, Objekt in Boolescher Wert usw.). Jeder Typ, sei es primitiv oder ein Objekt, ist ein gültiges Subjekt für Typenzwang. Um sich zu erinnern, sind die Grundelemente: Zahl, Zeichenfolge, Boolescher Wert, Null, undefiniert + Symbol (in ES6 hinzugefügt).

Schauen Sie sich als Beispiel für Typenzwang in der Praxis die JavaScript-Vergleichstabelle an, die zeigt, wie sich der ==Operator für lose Gleichheit für verschiedene Typen aund bTypen verhält . Diese Matrix sieht aufgrund des impliziten Zwangs des ==Operators beängstigend aus , und es ist kaum möglich, sich an all diese Kombinationen zu erinnern. Und das müssen Sie nicht tun - lernen Sie einfach die zugrunde liegenden Prinzipien des Typzwangs.

Dieser Artikel befasst sich ausführlich mit der Funktionsweise von Typenzwang in JavaScript und vermittelt Ihnen das notwendige Wissen, damit Sie sicher erklären können, wie die folgenden Ausdrücke berechnet werden. Am Ende des Artikels werde ich Antworten zeigen und sie erklären.

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, diese Liste ist voll von ziemlich dummen Dingen, die Sie als Entwickler tun können. In 90% der Anwendungsfälle ist es besser, impliziten Zwang zu vermeiden. Betrachten Sie diese Liste als Lernübung, um Ihr Wissen darüber zu testen, wie Typenzwang funktioniert. Wenn Sie sich langweilen, finden Sie weitere Beispiele auf wtfjs.com.

Übrigens kann es vorkommen, dass Sie im Interview für eine JavaScript-Entwicklerposition auf solche Fragen stoßen. Also weiterlesen?

Impliziter vs. expliziter Zwang

Typenzwang kann explizit und implizit sein.

Wenn ein Entwickler die Absicht zum Ausdruck bringt, zwischen Typen zu konvertieren, indem er den entsprechenden Code schreibt, spricht Number(value)man von explizitem Typenzwang (oder Typumwandlung).

Da JavaScript eine schwach typisierte Sprache ist, können Werte auch automatisch zwischen verschiedenen Typen konvertiert werden. Dies wird als impliziter Typzwang bezeichnet . Dies geschieht normalerweise, wenn Sie Operatoren auf Werte unterschiedlichen Typs anwenden, z

1 == null, 2/’5', null + new Date(), Oder sie kann durch den umgebenden Kontext ausgelöst werden, wie mit if (value) {…}, wo valuezu boolean dazu gezwungen wird.

Ein Operator, der keinen impliziten Typzwang auslöst ===, ist der Operator für strikte Gleichheit. Der lose Gleichheitsoperator ==hingegen führt bei Bedarf sowohl Vergleiche als auch Typzwang durch.

Impliziter Typenzwang ist ein zweischneidiges Schwert: Es ist eine große Quelle für Frustration und Fehler, aber auch ein nützlicher Mechanismus, mit dem wir weniger Code schreiben können, ohne die Lesbarkeit zu verlieren.

Drei Arten der Konvertierung

Die erste Regel ist, dass es in JavaScript nur drei Arten der Konvertierung gibt:

  • zu bespannen
  • zu boolesch
  • zu nummerieren

Zweitens funktioniert die Konvertierungslogik für Grundelemente und Objekte unterschiedlich, aber sowohl Grundelemente als auch Objekte können nur auf diese drei Arten konvertiert werden.

Beginnen wir zuerst mit Primitiven.

String-Konvertierung

Um Werte explizit in eine Zeichenfolge zu konvertieren, wenden Sie die String()Funktion an. Impliziter Zwang wird vom binären +Operator ausgelöst , wenn ein Operand eine Zeichenfolge ist:

String(123) // explicit 123 + '' // implicit

Alle primitiven Werte werden natürlich wie erwartet in Zeichenfolgen konvertiert:

String(123) // '123' String(-12.3) // '-12.3' String(null) // 'null' String(undefined) // 'undefined' String(true) // 'true' String(false) // 'false'

Die Symbolkonvertierung ist etwas schwierig, da sie nur explizit, aber nicht implizit konvertiert werden kann. Lesen Sie mehr über SymbolZwangsregeln.

String(Symbol('my symbol')) // 'Symbol(my symbol)' '' + Symbol('my symbol') // TypeError is thrown

Boolesche Konvertierung

Um einen Wert explizit in einen Booleschen Wert zu konvertieren, wenden Sie die Boolean()Funktion an.

Die implizite Konvertierung erfolgt im logischen Kontext oder wird von logischen Operatoren ( ||&&!) ausgelöst .

Boolean(2) // explicit if (2) { ... } // implicit due to logical context !!2 // implicit due to logical operator 2 || 'hello' // implicit due to logical operator

Hinweis : Logische Operatoren wie ||und &&führen intern boolesche Konvertierungen durch, geben jedoch den Wert der ursprünglichen Operanden zurück, auch wenn sie nicht boolesch sind.

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

Sobald es nur zwei mögliche Ergebnisse der Booleschen Konvertierung gibt: trueoder falsees ist einfacher, sich die Liste der falschen Werte zu merken.

Boolean('') // false Boolean(0) // false Boolean(-0) // false Boolean(NaN) // false Boolean(null) // false Boolean(undefined) // false Boolean(false) // false

Jeder Wert, der nicht in der Liste ist , wird umgewandelt true, einschließlich Objekt, eine Funktion Array, Date, benutzerdefinierten Typ, und so weiter. Symbole sind wahrheitsgemäße Werte. Leere Objekte und Arrays sind ebenfalls wahrheitsgemäße Werte:

Boolean({}) // true Boolean([]) // true Boolean(Symbol()) // true !!Symbol() // true Boolean(function() {}) // true

Numerische Konvertierung

Wenden Sie für eine explizite Konvertierung einfach die Number()Funktion an, genau wie Sie es mit Boolean()und getan haben String().

Die implizite Konvertierung ist schwierig, da sie in mehreren Fällen ausgelöst wird:

  • Vergleichsoperatoren ( >, <, <=, >=)
  • bitweise Operatoren ( |&^~)
  • arithmetische Operatoren ( -+*/%). Beachten Sie, dass Binär +keine numerische Konvertierung auslöst, wenn ein Operand eine Zeichenfolge ist.
  • unärer +Operator
  • loser Gleichheitsoperator ==(inkl. !=).

    Beachten Sie, dass ==keine numerische Konvertierung ausgelöst wird, wenn beide Operanden Zeichenfolgen sind.

Number('123') // explicit +'123' // implicit 123 != '456' // implicit 4 > '5' // implicit 5/null // implicit true | 0 // implicit

So werden primitive Werte in Zahlen umgewandelt:

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

Beim Konvertieren einer Zeichenfolge in eine Zahl schneidet die Engine zuerst führende und nachfolgende Leerzeichen \nund \tZeichen zurück und gibt zurück, NaNwenn die zugeschnittene Zeichenfolge keine gültige Zahl darstellt. Wenn die Zeichenfolge leer ist, wird sie zurückgegeben 0.

nullund undefinedwerden anders gehandhabt: nullwird 0, während undefinedwird NaN.

Symbole können weder explizit noch implizit in eine Zahl konvertiert werden. Darüber hinaus TypeErrorwird geworfen, anstatt stillschweigend zu konvertieren NaN, wie es passiert undefined. Weitere Informationen zu Symbolkonvertierungsregeln für MDN.

Number(Symbol('my symbol')) // TypeError is thrown +Symbol('123') // TypeError is thrown

Es sind zwei spezielle Regeln zu beachten:

  1. Bei der Anwendung ==auf nulloder undefinedfindet keine numerische Konvertierung statt. nullist nur gleich nulloder undefinedund ist gleich nichts anderem.
null == 0 // false, null is not converted to 0 null == null // true undefined == undefined // true null == undefined // true

2. NaN ist selbst nichts gleich:

if (value !== value) { console.log("we're dealing with NaN here") }

Geben Sie Zwang für Objekte ein

Bisher haben wir uns mit Typzwang für primitive Werte befasst. Das ist nicht sehr aufregend.

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/