اجبار نوع یا تایپ کورژن (Type Coercion) در جاوا اسکریپت

قبل از هر چیزی بهتر است بدانیم که ترجمه ی کلمه Type Coercion به فارسی اجبار نوع است که می‎توان گفت یک مقدار را به تغییر نوع خود مجبور می‎کند. در این مقاله ما از همان اصطلاح اصلی یعنی تایپ کورژن استفاده می‎کنیم چون برای اصطلاحات بهتر است از خود کلمه‎ها استفاده کنیم نه معادل فارسی آن.تایپ کورژن (Type Coercion) یک فرآیند تغییر مقدار، از یک نوع به نوع دیگر است ( مانند استرینگ به عدد، شی یا object به boolean و ... ). هر نوع از داده‎های اصلی یا یک شی، برای فرآیند تایپ کورژن معتبر است. جهت یادآوری، داده‎های اصلی عبارتند از: undefined, null, boolean, string, number به علاوه‎ی Symbol که در ES6 اضافه شده اند.به عنوان نمونه‎ای از تایپ کورژن در عمل، به جدول مقایسه جاوا اسکریپت نگاه کنید، که نحوه ی رفتار عملگر تساوی == در دو نوع مختلف a و b را نشان می‎دهد. این ماتریس به دلیل تایپ کورژنی که عملگر == انجام می‎دهد، به نظر ترسناک می‎رسد و به خاطر سپردن همه ی آن ترکیب‎ها سخت است. شما لازم نیست این کار را انجام دهید، فقط قواعد تایپ کورژن را یاد بگیرید.این مقاله چگونگی عملکرد تایپ کورژن در جاوا اسکریپت را به تفضیل توضیح می‎دهد.. بنابراین شما با خیال راحت می‎توانید بعد از خواندن این مقاله توضیح دهید که اصطلاحاتی که در زیر آمده چگونه محاسبه می‎شود. مثال‎های پایین در پایین مقاله به طور مفصل توضیح داده خواهد شد.true + false 12 / &quot6&quot &quotnumber&quot + 15 + 3 15 + 3 + &quotnumber&quot [1] > null &quotfoo&quot + + &quotbar&quot 'true' == true false == 'false' null == '' !!&quotfalse&quot == !!&quottrue&quot [‘x’] == ‘x’ [] + null + 1 [1,2,3] == [1,2,3] {}+[]+{}+[1] !+[]+[]+![] new Date(0) - 0 new Date(0) + 0بله، این لیست پر از چیزهای عجیب است که شما می‎توانید به عنوان برنامه‌نویس انجام دهید. در ۹۰٪ مواقع، بهتر است که از تایپ کورژن پرهیز شود. این لیست را به عنوان یک تمرین برای یادگیری در نظر بگیرید تا دانش خود را در مورد چگونگی عملکرد تایپ کورژن تست کنید. می‎توانید نمونه‌های بیشتری در wtfjs.com پیدا کنید.به هر حال، ممکن است گاهی اوقات با این چنین سوالاتی برای یک موقعیت شغلی به عنوان توسعه دهنده جاوا ‌اسکریپت مواجه شوید. پس به خواندن ادامه دهید.کورژن ضمنی و کورژن صریح (Implicit vs. explicit coercion)تایپ کورژن می‎تواند دو نوع صریح و ضمنی باشد:معمولا وقتی یه برنامه‌نویس می‎خواهد نوع مقدار را با نوشتن کد واضحی تغییردهد، از تایپ کورژن صریح (explicit type coercion یا type casting) استفاده میکند که نمونه آن در زیر آمده است:Number(value)از آنجایی که جاوا اسکریپت یک زبان weakly-typed است، نوع یک مقدار می‎تواند به طور خودکار از یک نوع به نوع دیگر تبدیل شود که به این نوع تبدیل، تایپ کورژن ضمنی (implicit type coercion) گفته می‌شود. این اتفاق معمولا زمانی رخ می‌دهد که شما عملگر‌ها را روی مقادیری از نوع‌های مختلف اعمال می‌کنید. مانند: 1 == null 2/’5' null + new Date()یا اینکه می‌تواند توسط محتوایی که اطراف آن را پوشش داده، ایجاد شود. برای مثال: if (value) {…}در اینجا، value به نوع بولی تبدیل می‌شود (coerced). یکی از عملگر‌هایی که در آن تایپ کورژن ضمنی رخ نمی‌دهد === است که به آن عملگر strict equality (تساوی سخت) گفته می‌شود. از طرف دیگر، == که به آن عملگر loose equality (تساوی سست) گفته‌ می‌شود، در صورت لزوم، تایپ کورژن را انجام می‌دهد و مقایسه می‌کند.تایپ کورژن ضمنی بدی‌هایی دارد اما یک روش خوب برای کمتر کد زدن بدون از دست دادن خوانایی است.سه نوع تبدیلاولین چیزی که باید بدانید این است که، فقط سه نوع تبدیل در جاوا اسکریپت وجود دارد: تبدیل به Stringتبدیل به Booleanتبدیل به Numberدومین چیز این است که، منطق تبدیل برای نوع‌های اصلی (number,string,...) و اشیا متفاوت عمل می‌کند، اما هر دو می‌توانند به آن سه نوع تبدیل شوند.در ابتدا از نوع‌های اصلی شروع می‌کنیم.تبدیل Stringبرای تبدیل صریح یک مقدار به یک رشته از تابع ()String استفاده می‌شود. کورژن ضمنی توسط عملگر باینری + انجام می‌شود، البته در صورتی که یکی از عملوند‌ها string باشدString(123) // explicit 123 + ' ' // implicitهمانطور که انتظار می‌رود، تمام مقادیر به string تبدیل می‌شوند:String(123) // '123' String(-12.3) // '-12.3' String(null) // 'null' String(undefined) // 'undefined' String(true) // 'true' String(false) // 'false'تبدیل symbol کمی فرق دارد، چون فقط می‌توان آن را به صورت صریح تبدیل کرد و امکان آن به صورت ضمنی وجود ندارد. در مورد قوانین کورژن symbol بیشتر بخوانید.String(Symbol('my symbol')) // 'Symbol(my symbol)' ' ' + Symbol('my symbol') // TypeError is thrownتبدیل به Booleanبرای تبدیل صریح یک مقدار به boolean از تابع ()Boolean استفاده می‌شود. تبدیل ضمنی یا در محتوا منطقی (logical context) یا توسط عملگر‎های منطقی ( || && !) انجام می‎شود.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 نباشد.let x = 'hello' && 123; // x === 123در بالا مقدار true برگردانده نمی‎شود و به جای آن عدد 123 بر می‎گردد. مقدار 'hello' و عدد 123 در داخل، به boolean تبدیل می‎شوند تا عبارت را محاسبه کنند.از آنجایی که تنها دو نتیجه برای تبدیل boolean ممکن است (true یا false) به خاطر سپردن لیست مقادیر false ساده تر است.Boolean(' ') // false Boolean(0) // false Boolean(-0) // false Boolean(NaN) // false Boolean(null) // false Boolean(undefined) // false Boolean(false) // falseهر مقداری که در لیست بالا نیست، به true تبدیل می‎شود، از جمله: object, fuction, آرایه, ِDate, نوع‎های تعریف شده توسط کاربر و ... . symbol ها، آرایه‎های خالی و object های خالی هم به true تبدیل می‎شوند:Boolean({ }) // true Boolean([ ]) // true Boolean(Symbol()) // true !!Symbol() // true Boolean(function() { }) // trueتبدیل عددی (Numeric)برای تبدیل صریح از تابع ()Number استفاده می‌شود، همانگونه که از ()Boolean برای تبدیل بولی و ()String برای تبدیل رشته ای استفاده شد.در اینجا تبدیل ضمنی کمی دشوارتر است، چون در موارد بیشتری نسبت به دیگر تبدیل‎ها انجام می‎شود:عملگرهای مقایسه (>, <, <=,>=)عملگرهای بیتی یا bitwise ( | & ^ ~)عملگرهای حسابی (- + * / % ). توجه: عملگر + باینری، زمانی که یکی از متغیرها string باشد، تبدیل عددی انجام نمی‎دهد.عملگر تساوی سست یا loose equality ( == و =!). توجه: عملگر == ، زمانی که هر دو عملگر رشته باشند، تبدیل عددی انجام نمی‎دهد. 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(&quot 12 &quot) // 12 Number(&quot-12.34&quot) // -12.34 Number(&quot\n&quot) // 0 Number(&quot 12s &quot) // NaN Number(123) // 123هنگام تبدیل یک رشته به یک عدد،موتور در ابتدا فضاهای خالی، کاراکتر t\ و کاراکتر n\ را حذف می‎کند (trim). اگر string بدون فضای خالی یک عدد معتبر را ارائه نکند، NaN بر می‎گردد. اگر string خالی باشد، مقدار 0 بر می‎گردد.از طرف دیگر null و undefined به طور متفاوت اداره می‎شوند: null به 0 و undefined به NaN تبدیل می‎شوند.سمبل‎ها (symbols)، نه به طور ضمنی و نه به طور صریح نمی‎توانند به عدد تبدیل شوند. شاید فکر کنید که مانند undefined تبدیل به NaN می‎شود اما به جای آن، یک خطای TypeError می‎فرستد.در مورد قوانین تبدیل symbol ها، در MDN بیشتر بخوانید.Number(Symbol('my symbol')) // TypeError is thrown +Symbol('123') // TypeError is thrownدو قانون ویژه وجود دارد:1. زمانی که عملگر == روی null یا undefined اعمال شود، تبدیل عددی رخ نمی‎دهد. null فقط با null یاundefined برابری می‎کند و هیچ چیز دیگری برابری نمی‎کند.2. مقدار NaN با هیچ چیزی حتی خودش هم برابری نمی‎کند.if (value !== value) { console.log(&quotwe're dealing with NaN here&quot) }زمانی که به این دستور بر خوردیم باید بدانیم که ما در اینجا ما با NaN سر و کار خواهیم داشت.تایپ کورژن برای Object هاتا کنون ما به تایپ کورژن‎ها برای مقادیر اصلی نگاه انداختیم که خیلی هیجان انگیز نبود. زمانی که صحبت از اشیا (objects) و برخورد موتور با عباراتی مانند [1] + [2,3]می‎شود، ابتدا نیاز است تا یک شی را به یک مقدار اصلی تبدیل کرد، که بعد از آن به نوع نهایی تبدیل می‎شود. البته همچنان سه نوع تبدیل داریم: عددی، string و boolean.ساده ترین مورد، تبدیل boolean است: هر مقدار غیر اصلی، چه شی یا آرایه خالی باشد، چه نباشد همیشه به true تبدیل خواهد شد. اشیا، توسط متد داخلی [[ToPrimitive]] به مقادیر اصلی تبدیل می‎شوند، که هم وظیفه تبدیل عددی و هم وظیفه تبدیل رشته را به عهده دارد.در اینجا می‎توانید یک شبه پیاده سازی از متد [[ToPrimitive]] را ببینید:متد [[ToPrimitive]] ، به همراه یک مقدار ورودی و نوع مورد نظر (preferredType) از تبدیل، پاس داده می‎شود: Number یا String، نوع مورد نظر اختیاری است.هر دو تبدیل عددی و رشته ای از دو متد valueOf و toString برای شیء ورودی استفاده می‎کنند.هر دو متد در Object.prototype اعلان شده اند، بنابراین برای هر نوع مشتق شده مانند Date، آرایه و ... در دسترس هستند.به طور کلی الگوریتم به شرح زیر است.اگر مقدار ورودی یکی از نوع‎های اصلی بود، کاری انجام نده و آن را برگردان.متد ()input.toString را صدا بزن، اگر نتیجه نوع داده ی اصلی بود، آن را برگردان.متد ()input.valueOf را صدا بزن، اگر نتیجه نوع داده ی اصلی بود، آن را برگردان.4. اگر نه ()input.toString و نه ()input.valueOf نوع داده اصلی برنگرداند، خطای TypeErrorرا بفرست.تبدیل عددی ابتدا متد valueOf را صدا می‎زند و پلن دوم آن متد toString است. در تبدیل رشته ای این اتفاق بر عکس است، ابتدا متد toString صدا زده می‎شود که پلن دوم آن متد valueOf است.اکثر نوع‎های درون ساخته (built-in types) یا متد valueOf ندارند یا اینکه متد valueOf خود آن شی را بر می‎گرداند که در این صورت نادیده گرفته می‎شود، چون یک نوع اصلی نیست. به همین علت است که تبدیل عددی و رشته ای ممکن است مثل هم عمل کنند و هر دو در پایان متد ()toStringرا صدا بزنند.عملگرهای مختلف می‎توانند با کمک پارامتر preferredType ،هم تبدیل عددی و هم تبدیل رشته ای انجام دهند. ولی در اینجا دو استثنا وجود دارد: تساوی سست (loose equality) == و عملگر + باینری حالت‎های پیشفرض تبدیل را انجام می‎دهند (preferredType یا مشخص نشده است یا اینکه برابر با default است). در این حالت، اکثر نوع‎های درون ساخته (built-in types) تبدیل عددی را به عنوان یک حالت پیشفرض، فرض می‎کنند. به غیر از Date که تبدیل رشته ای انجام می‎دهد.در اینجا مثالی از رفتار تبدیل Date آورده شده است:شما میتوانید حالات پیشفرض متدهای ()toString و()valueOf را تغییر دهید تا به منطق تبدیل شی به داده اصلی (object-to-primitive) تبدیل شود. توجه کنید که چطور عبارت '' + [101] رشته '101' را برمی‎گرداند. عملگر + حالت تبدیل پیشفرض را انجام می‎دهد، و همانطور که قبلا اشاره کردیم Object تبدیل عددی را به عنوان حالت پیشفرض، فرض می‎کند. بنابراین در ابتدا از متد ()valueOf به جای ()toString استفاده می‎شود.متد Symbol.toPrimitive در ES6در ES5 شما می‎توانید با تغییر متدهای toString و valueOf از منطق تبدیل شی به داده اصلی (object-to-primitive) استفاده کنید.در ES6 شما می‎توانید پیش تر بروید و پیاده سازی متد [Symbol.toPrimtive] روی یک شی را جایگزین روال [[ToPrimitive]] داخلی کنید.مثال هابعد از آموزش تئوری، حال زمان برگشتن به مثال هاست:true + false // 1 12 / &quot6&quot // 2 &quotnumber&quot + 15 + 3 // 'number153' 15 + 3 + &quotnumber&quot // '18number' [1] > null // true &quotfoo&quot + + &quotbar&quot // 'fooNaN' 'true' == true // false false == 'false' // false null == '' // false !!&quotfalse&quot == !!&quottrue&quot // 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' توضیحات هر عبارت را در زیر آمده است:عملگر + باینری، تبدیل عددی را برای true و false انجام می‎دهد.true + false ==> 1 + 0 ==> 1عملگر تقسیم حسابی (/) تبدیل عددی را برای رشته '6' انجام می‎دهد:// divide: 12 / '6' ==> 12 / 6 ==>> 2عملگر + اولویت چپ به راست دارد، بنابراین ابتدا عبارت number" + 15" اجرا می‎شود. از آنجایی که یکی از عملوند‎ها رشته است، عملگر + تبدیل رشته را برای عدد 15اجرا می‎کند. در گام دوم، عبارت number15" + 3" به طور مشابه عمل می‎کند.“number” + 15 + 3 ==> &quotnumber15&quot + 3 ==> &quotnumber153&quotعبارت 15 + 3 در گام اول اجرا می‎شود. از آنجایی که هر دو عملوند عدد هستند، نیازی به کورژن نیست. در گام دوم عبارت 18 + "number" اجرا می‎شود و از آنجایی که یکی از عملوند‎ها رشته است، تبدیل رشته ای انجام می‎شود.15 + 3 + &quotnumber&quot ==> 18 + &quotnumber&quot ==> &quot18number&quotعملگر مقایسه < ، تبدیل عددی را برای [1] و null انجام می‎دهد.[1] > null ==> '1' > 0 ==> 1 > 0 ==> trueعملگر یگانی (unary) + ، اولویت بالاتری نسبت به عملگر باینری + دارد. بنابراین عبارت +"bar" ابتدا اجرا می‎شود. عملگر جمع یگانی تبدیل عددی را برای رشته "ba". از آنجایی که رشته یک عدد معتبر را ارائه نمی‎کند، نتیجه NaNاست. در گام دوم، عبارت foo" + NaN" اجرا می‎شود.&quotfoo&quot + + &quotbar&quot ==> &quotfoo&quot + (+&quotbar&quot) ==> &quotfoo&quot + NaN ==> &quotfooNaN&quotعملگر ==، تبدیل عددی را اجرا می‎کند. رشته 'true' به NaN تبدیل می‎شود. مقدار بولی true به 1 تبدیل می‎شود.'true' == true ==> NaN == 1 ==> false false == 'false' ==> 0 == NaN ==> falseمعمولا عملگر ==، تبدیل عددی انجام می‎دهد. اما در مورد null اینطور نیست. null فقط با null یا undefined برابری می‎کند، و با چیزهای دیگر برابری نمی‎کند.عملگر !!، هر دو رشته "true" و "false" را از آنجایی که رشته خالی نیستند، تبدیل به مقدار بولی true می‎کند. سپس، عملگر == بدون هیچ کورژنی، فقط برابری دو مقدار بولی را چک می‎کند.!!&quotfalse&quot == !!&quottrue&quot ==> true == true ==> trueعملگر ==، تبدیل عددی را برای یک آرایه انجام می‎دهد. متد ()valueOf آرایه، خود آرایه را بر می‎گرداند و چون داده اصلی (primitive) نیست، نادیده گرفته می‎شود. متد ()toString آرایه، ['x'] را تبدیل به رشته 'x' می‎کند.['x'] == 'x' ==> 'x' == 'x' ==> trueعملگر + تبدیل عددی را برای []انجام می‎دهد. متد ()valueOf آرایه نادیده گرفته می‎شود، چون خود آرایه را بر می‎گرداند که آن هم داده اصلی نیست. متد ()toString آرایه، یک رشته خالی را بر می‎گرداند. در گام دوم عبارت null + 1 + '' اجرا می‎شود.[ ] + null + 1 ==> ' ' + null + 1 ==> 'null' + 1 ==> 'null1'عملگر‎های منطقی || و && ، عملوند‎ها را به boolean کورس می‎کند، اما مقدار اصلی عملوند را بر می‎گرداند (نه مقدار بولی). 0 false است، در حالی که '0' true است، چون رشته خالی نیست. شی خالی {} هم که true است.0 || &quot0&quot &==> (0 || &quot0&quot) &==> (false || true) && true // internally ==> &quot0&quot &==> true && true // internally ==> { }هیچ کورژنی نیاز نیست، چون هر دو عملوند یک نوع هستند. ازآن جایی که عملگر == هویت شی را بررسی می‎کند (نه برابری شی) و دو آرایه دو نمونه متفاوت هستند، نتیجه falseاست.[1,2,3] == [1,2,3] ==> falseهمه ی عملوند‎ها نوع داده غیر اصلی (non-primitive) هستند، بنابراین عملگر + از سمت چپ ترین، شروع به تبدیل عددی می‎کند. متد ()valueOf آرایه و شی، خود شی را بر می‎گرداند، بنابراین نادیده گرفته می‎شود. ()toString برای پلن بعدی مورد استفاده قرار می‎گیرد. مورد جالب اینجا این است که اولین {}، شی محسوب نمی‎شود و به عنوان تعریف بلاک کد محسوب می‎شود، بنابراین نادیده گرفته می‎شود. پروسه با عبارت بعدی []+ شروع می‎کند، که به وسیله ی متد ()toString به یک رشته خالی تبدیل می‎شود و سپس به 0 .{ }+[ ]+{ }+[1] ==> +[ ]+{ }+[1] ==> 0 + { } + [1] ==> 0 + '[object Object]' + [1] ==> '0[object Object]' + [1] ==> '0[object Object]' + '1' ==> '0[object Object]1'این مورد با توجه به اولویت عملگر، قدم به قدم بهتر توضیح داده شده است. از سمت چپ ابتدا عبارت [ ]+ تبدیل به عدد صفر می‎شود و عملگر ! صفر را تبدیل به true می‎کند. همچنین در عبارت [ ]! در سمت راست، همانطور که می‎دانید تبدیل بولی آرایه‎ها و شی‎ها true است که عملگر ! آن را تبدیل به false می‎کند.!+[ ]+[ ]+![ ] ==> (!+[ ]) + [ ] + (![ ]) ==> !0 + [ ] + false ==> true + [ ] + false ==> true + ' ' + false ==> 'truefalse'عملگر - تبدیل عددی را برای Dateانجام می‎دهد. متد ()Date.valueOf تعداد میلی ثانیه از دوره یونیکس را برمی‎گرداند.new Date(0) - 0 ==> 0 - 0 ==> 0عملگر + تبدیل پیشفرض را انجام می‎دهد. Date، تبدیل رشته ای را به عنوان تبدیل پیش فرض در نظر می‎گیرد. بنابراین از متد ()toString استفاده می‎شود.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' منبعاین مقاله برگرفته از مقاله ی JavaScript type coercion explained است که توسط Alexey Samoshkin منتشر شده است.برای اطلاعات بیشتر به لینک پایین مراجعه کنید: https://www.freecodecamp.org/news/js-type-coercion-explained-27ba3d9a2839/