وأوضح قسر نوع جافا سكريبت
تعرف على محركاتك

[تحرير 2/5/2018] : هذا المنشور متاح الآن باللغة الروسية. يصفق لسيرج بولافيك لجهوده.
نوع الإكراه هو عملية تحويل القيمة من نوع إلى آخر (مثل سلسلة إلى رقم ، وكائن إلى منطقي ، وما إلى ذلك). أي نوع ، سواء كان بدائيًا أو كائنًا ، هو موضوع صالح لإكراه النوع. للتذكير ، الأساسيات هي: رقم ، سلسلة ، منطقية ، خالية ، غير معرف + رمز (مضاف في ES6).
كمثال على نوع الإكراه في الممارسة العملية ، انظر إلى جدول مقارنة جافا سكريبت ، والذي يوضح كيف ==
يتصرف عامل المساواة الفضفاض 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
بمجرد وجود نتيجتين محتملتين فقط للتحويل المنطقي: 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
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:
- When applying
==
tonull
orundefined
, numeric conversion does not happen.null
equals only tonull
orundefined
, 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:
- 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 - مدونة صغيرة حول تلك اللغة التي نحبها على الرغم من إعطائنا الكثير لنكرهه - //wtfjs.com/