Εξήγησε ο εξαναγκασμός τύπου JavaScript

Γνωρίστε τους κινητήρες σας

[Επεξεργασία 2/5/2018] : Αυτή η ανάρτηση είναι πλέον διαθέσιμη στα ρωσικά. Χτυπάει τον Serj Bulavyk για τις προσπάθειές του.

Ο εξαναγκασμός τύπου είναι η διαδικασία μετατροπής της τιμής από έναν τύπο σε έναν άλλο (όπως συμβολοσειρά σε αριθμό, αντικείμενο σε boolean και ούτω καθεξής). Οποιοσδήποτε τύπος, είτε είναι πρωτόγονος είτε αντικείμενο, είναι έγκυρο θέμα για εξαναγκασμό τύπου Για να θυμηθώ, τα πρωτόγονα είναι: αριθμός, συμβολοσειρά, boolean, null, undefined + Symbol (προστίθεται στο 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εξαναγκάζεται να boolean.

Ένας χειριστής που δεν προκαλεί σιωπηρό εξαναγκασμό τύπου είναι ===, ο οποίος καλείται χειριστής αυστηρής ισότητας. Ο χειριστής χαλαρής ισότητας ==από την άλλη πλευρά κάνει σύγκριση και εξαναγκασμό τύπου, εάν χρειάζεται.

Ο σιωπηρός εξαναγκασμός τύπου είναι ένα σπαθί διπλού άκρου: είναι μια μεγάλη πηγή απογοήτευσης και ελαττωμάτων, αλλά και ένας χρήσιμος μηχανισμός που μας επιτρέπει να γράφουμε λιγότερο κώδικα χωρίς να χάσουμε την αναγνωσιμότητα.

Τρεις τύποι μετατροπής

Ο πρώτος κανόνας που πρέπει να γνωρίζετε είναι ότι υπάρχουν μόνο τρεις τύποι μετατροπών στο JavaScript:

  • στο έγχορδα
  • σε boolean
  • στον αριθμό

Δεύτερον, η λογική μετατροπής για πρωτόγονα και αντικείμενα λειτουργεί διαφορετικά, αλλά τόσο τα πρωτόγονα όσο και τα αντικείμενα μπορούν να μετατραπούν μόνο με αυτούς τους τρεις τρόπους.

Ας ξεκινήσουμε πρώτα με πρωτόγονους.

Μετατροπή συμβολοσειράς

Για να μετατρέψετε ρητά τιμές σε συμβολοσειρά εφαρμόστε τη 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 εφαρμόστε τη Boolean()συνάρτηση.

Η σιωπηρή μετατροπή συμβαίνει σε λογικό περιβάλλον ή ενεργοποιείται από λογικούς τελεστές ( ||&&!).

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

Σημείωση : Λογικοί τελεστές όπως ||και &&πραγματοποιούν boolean μετατροπές εσωτερικά, αλλά στην πραγματικότητα επιστρέφουν την αξία των αρχικών τελεστών, ακόμη και αν δεν είναι boolean.

// 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 πιθανά αποτελέσματα μετατροπής boolean: 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().

Η σιωπηρή μετατροπή είναι δύσκολη, επειδή ενεργοποιείται σε περισσότερες περιπτώσεις:

  • τελεστές σύγκρισης ( >, <, <=, >=)
  • τελεστές ( |&^~)
  • αριθμητικοί τελεστές ( -+*/%). Σημειώστε, ότι το δυαδικό +δεν προκαλεί αριθμητική μετατροπή, όταν οποιοσδήποτε τελεστής είναι μια συμβολοσειρά.
  • unary +χειριστής
  • τελεστής χαλαρής ισότητας ==(συμπ. !=)

    Σημειώστε ότι ==δεν προκαλεί αριθμητική μετατροπή όταν και οι δύο τελεστές είναι συμβολοσειρές.

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 - ένα μικρό ιστολογικό κώδικα για τη γλώσσα που αγαπάμε παρά το γεγονός ότι μας έδωσε τόσο πολύ να μισούμε - //wtfjs.com/