رفتارهای عجیب زبان جاوا اسکریپت (چالش ذهنی)

!Strange behaviors of the JavaScript

05 اسفند 1399
رفتار های عجیب زبان جاوا اسکریپت (چالش ذهنی)

جاوا اسکریپت، زبان زیبایی ها!

زبان جاوا اسکریپت یکی از بزرگترین و محبوب ترین زبان های برنامه نویسی در هر پلتفرمی و در کل دنیا است. تمام تحقیقات و نظرسنجی های بزرگ نشان می دهند که اکثر توسعه دهندگان دنیا با زبان جاوا اسکریپت آشنا هستند و حداقل چندبار در حرفه خود از آن استفاده کرده اند. با این همه این زبان دارای نکات مخفی و رفتارهای عجیب و غریبی است که شاید به چشم بسیاری از توسعه دهندگان «باگ» به حساب بیاید. ما در این مقاله مثال هایی از این دست را برایتان آماده کرده ایم؛ رفتارهایی که به نظر «باگ» به حساب می آیند یا به نوعی جالب هستند. اگر از توسعه دهندگان تازه کار باشید می توانید از این لیست برای درک بهتر زبان جاوا اسکریپت و نگاهی عمیق تر به آن استفاده کنید. اگر از توسعه دهندگان حرفه ای هستید نیز می توانید از این لیست برای رفع اشکالات و خطاهای احتمالی در پروژه هایتان استفاده کنید و به توسعه دهنده با تجربه تری تبدیل شوید.

نکته: این مقاله صرفا جهت تمرکز روی نکات جالب جاوا اسکریپت و به چالش کشیدن دانش شما از آن است بنابراین مسائلی که در این مقاله مطرح می شوند معمولا کاربرد عملی ندارند بلکه بیشتر بازی با تئوری به حساب می آیند (مانند هر چالشی در علوم دیگر).

پیش نیازها

از آنجایی که این مقاله وارد جزئیات و مثال های عجیب و غریب زبان جاوا اسکریپت می شود، برای مطالعه آن باید تا حد خوبی با کلیت زبان جاوا اسکریپت آشنایی داشته باشید. مطالعه این مقاله به افرادی که در حال یادگیری جاوا اسکریپت هستند توصیه نمی شود چرا که ممکن است در روند یادگیری شما اخلال ایجاد کند.

همچنین از این به بعد قرار می گذاریم که از علامت های <- برای نمایش مقدار expression ها استفاده می کنیم بنابراین هر جا که <- را دیدید بدانید منظورمان مقدار یک expression  خاص در آن لحظه است. از طرفی برای نمایش خروجی دستورات console.log نیز از علامت های < // استفاده می کنیم. کامنت های عادی نیز فقط جهت ارائه توضیحات هستند.

برابری آرایه های خالی

به کد زیر توجه کنید:

[] == ![]; // -> true

چرا آرایه خالی با برعکس آرایه خالی برابر است؟ ما می دانیم که اپراتور تساوی (==) هر دو طرف مقایسه را به عدد تبدیل می کند تا بتواند آن ها را با هم مقایسه کند.

ابتدا به طرف راست ([]!) دقت کنید: در طرف راست این مقایسه، یک آرایه خالی را داریم که با اپراتور ! برعکس شده است. ما می دانیم آرایه ها مقادیر truthy هستند (آرایه ها در اصل شیء هستند) بنابراین در حالت عادی True خواهند بود اما به دلیل وجود اپراتور ! به false تبدیل می شوند و نهایتا برای مقایسه به عدد صفر تبدیل خواهند شد. یعنی آرایه ابتدا به boolean تبدیل شده و سپس به عدد صفر تبدیل می شود، این ترتیب برای ما مهم است. چرا؟ آرایه هنوز به عدد تبدیل نشده است بلکه هنوز یک آرایه است و آرایه به خودی خود (فارغ از خالی بودنش) truthy می باشد و حالت برعکس آن falsy می باشد و falsy در هنگام تبدیل به عدد صفر می شود. اگر این ترتیب را اشتباه در نظر بگیرید به جواب نمی رسید.

حالا به طرف چپ مقایسه توجه کنید: در طرف چپ فقط یک آرایه خالی را داریم که برای مقایسه به عدد صفر تبدیل می شود (این طبیعت جاوا اسکریپت است).

نتیجه: صفر و صفر برابر هستند!

در صورتی که بخواهم این کد را به شکل خلاصه برایتان بنویسم به چنین چیزی می رسیم:

+[] == +![];

0 == +false;

0 == 0;

true;

در ضمن برای یاد‌آوری می گویم که مقادیر زیر همیشه falsy (شبه false) هستند:

  • false
  • 0 (عدد صفر)
  • "" (رشته خالی)
  • null
  • undefined
  • NaN

به غیر از این موارد تمامی مقادیر دیگر truthy هستند (اشیاء خالی، آرایه های خالی، توابع و غیره).

مقایسه true و آرایه ها

یک آرایه خالی ([]) با true برابر نیست اما با برعکس آن ([]!) نیز برابر نیست! به کد زیر توجه کنید:

// بخش اول

true == []; // -> false

true == ![]; // -> false




// بخش دوم

false == []; // -> true

false == ![]; // -> true

به نظر شما چگونه می توان این رویداد را توضیح داد؟ بگذارید قسمت اول از کد بالا را به شکل زیر بنویسم:

true == []; // -> false

true == ![]; // -> false




// بر اساس توضیحات بخش قبل




true == []; // -> false




toNumber(true); // -> 1

toNumber([]); // -> 0




1 == 0; // -> false




true == ![]; // -> false




![]; // -> false




true == false; // -> false

بر اساس توضیحی که در بخش قبلی دادم (اولویت اپراتور ! برای برعکس کردن truthy به falsy نسبت به اپراتور مقایسه) متوجه می شویم که ابتدا آرایه خالی به حالت برعکس خودش یعنی یک مقدار falsy تبدیل می شود و سپس به عدد تبدیل خواهد شد (عدد صفر). از طرفی true همیشه به عدد ۱ تبدیل می شود و صفر هیچگاه با آن یکی نخواهد بود.

اما قسمت دوم کد چطور؟ می توانیم آن را نیز خلاصه نویسی کنیم:

false == []; // -> true

false == ![]; // -> true




// بر اساس توضیحات ارائه شده




false == []; // -> true




toNumber(false); // -> 0

toNumber([]); // -> 0




0 == 0; // -> true




false == ![]; // -> true




![]; // -> false




false == false; // -> true

این بخش دقیقا برعکس بخش اول است بنابراین بر اساس توضیحاتی که داده ام باید برایتان واضح باشد.

true با false برابر است؟

به کد زیر توجه کنید:

!!"false" == !!"true"; // -> true

!!"false" === !!"true"; // -> true

در این کد یک حقه ساده به کار رفته است. آیا می توانید آن را توضیح بدهید؟ من این کد را با توضیحات بیشتر برایتان خلاصه نویسی می کنم:

// true is 'truthy' and represented by value 1 (number), 'true' in string form is NaN.

true == "true"; // -> false

false == "false"; // -> false




// 'false' is not the empty string, so it's a truthy value

!!"false"; // -> true

!!"true"; // -> true

true یک مقدار truthy است اما رشته true فقط و فقط یک string است و ربطی به مقادیر boolean ندارد. از طرفی می دانیم رشته ها در صورتی که خالی نباشند true هستند. با این حساب شاید تصور کنید که رشته true با true برابر است چرا که هر دو truthy هستند اما اینطور نیست. چرا؟ قبلا هم توضیح دادم که اپراتور مقایسه (==) آن ها را به عدد تبدیل می کند؛ true همیشه به عدد ۱ تبدیل می شود اما رشته هایی که کاراکتر الفبایی دارند نمی توانند به عدد تبدیل شوند و NaN می شوند (مخفف Not a Number). طبیعتا NaN با عدد ۱ برابر نیستند. این مسئله برای false نیز صادق است چرا که false همیشه به صفر تبدیل می شود اما صفر نیز هیچ گاه برابر با NaN نمی باشد.

در نهایت به رشته false می رسیم. رشته false یک مقدار truthy است (رشته هایی که خالی نباشند truthy هستند) بنابراین حالت برعکس آن با اپراتور ! برابر با false خواهد بود اما دوباره اپراتور ! را داریم که این false را به true تبدیل می کند. این مسئله برای رشته true نیز صادق است؛ یک رشته truthy که به false تبدیل شده و سپس با دومین اپراتور ! به true برمی گردد.

کلمه baNaNa

چطور ممکن است کلمه baNaNa از کد زیر تولید شود؟

"b" + "a" + +"a" + "a"; // -> 'baNaNa'

ما می توانیم این کد را به شکل زیر خلاصه کنیم:

"foo" + +"bar"; // -> 'fooNaN'

آیا هنوز متوجه نشه اید؟ به کد زیر نگاه کنید:

'foo' + (+'bar')

در جاوا اسکرپیت قرار دادن علامت + در کنار یک رشته به معنی تبدیل آن به عدد است و bar نمی تواند به عدد تبدیل شود بنابراین NaN را به جایش دریافت می کنیم. حالا به رشته اول برگردید:

"b" + "a" + +"a" + "a"; // -> 'baNaNa'

ابتدا b و a با هم در یک رشته قرار می گیرند و سپس "a" + + به NaN تبدیل می شود و نهایتا a نیز به آن اضافه می شود و کلمه baNaNa را دریافت می کنیم.

NaN با NaN برابر نیست!

چطور ممکن است که دو مقدار دقیقا یکسان (NaN) با یکدیگر برابر نباشند؟ به کد زیر توجه کنید:

NaN === NaN; // -> false

نکته ای که باید به آن توجه کنید توضیحات موسسه IEEE است:

Four mutually exclusive relations are possible: less than, equal, greater than, and unordered. The last case arises when at least one operand is NaN. Every NaN shall compare unordered with everything, including itself.

بنابراین IEEE تصمیم گرفته است که در هر مقایسه ای که یکی از طرفین NaN باشد، مقایسه از نوع  unordered و false شود. به نمونه های زیر نگاهی بیندازید:

console.log(NaN === "A")

console.log(NaN === false)

console.log(NaN === true)

console.log(NaN === 0)

console.log(NaN === 1)

console.log(NaN === "")

console.log(NaN === [])

console.log(NaN === {site: 'roxo.ir/plus'})

console.log(NaN === ['data'])

console.log(NaN === +"A")

console.log(NaN === NaN)

console.log(NaN === +NaN)

من انواع و اقسام مقایسه ها را در کد بالا انجام داده ام؛ مقایسه با رشته، با boolean، با عدد، با رشته خالی، با آرایه خالی، با شیء غیر خالی، با آرایه غیر خالی، با رشته ای که تبدیل به عدد می شود با NaN و تبدیل NaN به عدد! با اجرای کد بالا نتیجه زیر را دریافت می کنیم:

false

false

false

false

false

false

false

false

false

false

false

false

احتمالا می پرسید چرا IEEE چنین تصمیمی گرفته است؟ NaN فقط زمانی تولید می شود که شما مقدار خاصی را که قابلیت تبدیل شدن به عدد ندارد، به عدد تبدیل کنید. با این حساب هر جایی از کد ما که به NaN برسد، مشکل بزرگی خواهیم داشت. اگر قرار باشد NaN مانند بقیه مقادیر رفتار کند (مثلا تقسیم NaN بر NaN برابر ۱ باشد یا در مقایسه با خودش برابر باشد) ما متوجه این مشکل نخواهیم شد بلکه بدون دریافت خطا، نتیجه عجیب و غریبی در کد هایمان دریافت می کنیم که ممکن است رخنه های امنیتی بزرگی را در پروژه ما ایجاد کند. به این دلیل است که IEEE تصمیم گرفته است تا NaN مقدار خاصی باشد و توانایی انجام عملیات های عادی را نداشته باشد.

جمع زدن آرایه های خالی

به نظر شما نتیجه اجرای کد زیر چه مقداری است؟

(![] + [])[+[]] +

  (![] + [])[+!+[]] +

  ([![]] + [][[]])[+!+[] + [+[]]] +

  (![] + [])[!+[] + !+[]];

// -> 'fail'

همانطور که در خود کد مشخص کرده ام نتیجه fail است. منظور من از fail چیست؟ بیایید کد بالا را به شکل زیر بنویسیم:

const data = (![] + [])[+[]] +

  (![] + [])[+!+[]] +

  ([![]] + [][[]])[+!+[] + [+[]]] +

  (![] + [])[!+[] + !+[]];

// -> 'fail'




console.log(data)

نتیجه اجرای کد بالا عبارت fail است! چطور؟ اگر خوب به کد بالا دقت کنید، متوجه تکرار شدن یک الگوی خاص می شوید:

![] + []; // -> 'false'

![]; // -> false

در ابتدا مقدار []! را داریم که false است (در اولین چالش، توضیحات آن را ارائه کردیم) و جمع زدن آن با یک آرایه خالی باز هم false را به ما می دهد. چرا؟ در سورس کد جاوا اسکریپت توابع خاصی اجرا می شوند (binary + Operator -> ToPrimitive -> [[DefaultValue]]) که باعث می شوند آرایه خالی به رشته تبدیل شود:

![] + [].toString(); // 'false'

با انجام این کار مقدار boolean ما (false) به رشته false تبدیل می شود. به کد اصلی نگاه کنید؛ قسمت []+ یعنی false به عدد تبدیل شود که همان صفر می باشد بنابراین به عنوان یک ایندکس حساب می شود. ایندکس صفر از رشته false چیست؟ حرف f!

"false"[0]; // -> 'f'

بنابراین می توانیم کد اصلی را به قسمت های مختلفی تقسیم کنیم تا به کلمه fail برسیم:

const data = (![] + [])[+[]] +

  (![] + [])[+!+[]] +

  ([![]] + [][[]])[+!+[] + [+[]]] +

  (![] + [])[!+[] + !+[]];

// -> 'fail'




console.log("The letter F:", (![] + [])[+[]])

console.log("The letter A:", (![] + [])[+!+[]])

console.log("The letter I:", ([![]] + [][[]])[+!+[] + [+[]]])

console.log("The letter L:", (![] + [])[!+[] + !+[]])

من هر خط از کد اصلی را به یک دستور log جداگانه داده ام تا تک تک کلمات را دریافت کنیم. با اجرای کد بالا نتیجه زیر را می گیریم:

The letter F: f

The letter A: a

The letter I: i

The letter L: l

همانطور که می بینید این کد به ما رشته fail را می دهد. حروف f و a و l آسان به دست می آیند اما i از کجا می آید؟ حرف i یک نکته جالب دارد! در خط سوم از کد اصلی که مسئول تولید حرف i است، مقدار falseundefined را دریافت می کنیم که رشته ای واحد از کلمات false و undefined است و سپس ایندکس دهم را گرفته ایم که همان حرف i است. با خواندن کد به راحتی می توانید این مسئله را درک کنید اما اگر هنوز هم به جواب نمی رسید بهتر است کد مربوط به حرف i را به قسمت های مختلف بشکنیم:

const data = (![] + [])[+[]] +

  (![] + [])[+!+[]] +

  ([![]] + [][[]])[+!+[] + [+[]]] +

  (![] + [])[!+[] + !+[]];

// -> 'fail'




console.log([![]])

console.log([][[]])

console.log(+!+[])

console.log([+[]])

با اجرای کد بالا نتیجه زیر را دریافت می کنید:

[ false ]

undefined

1

[ 0 ]

حتما حالا مسئله برایتان روشن شده است.

آرایه های خالی truthy هستند اما true نیستند

همانطور که می دانید مقادیر truthy مقادیری هستند که شبه true هستند یا به عبارتی به true تبدیل می شوند. آرایه ها نیز truthy هستند اما true نیستند!

!![]       // -> true

[] == true // -> false

آیا می توانید کد بالا را توضیح بدهید؟ این کد دقیقا همان چالشی است که در بخش اول به شما توضیح دادم و هدف من از طرح آن، این است که حافظه خودتان را بسنجید. آیا پاسخ را یادتان مانده است؟  نکته مهم در پاسخ به این سوال اولویت اپراتور ! بود. با این حساب دو راه حل داریم.

روش اشتباه حل سوال: آرایه خالی false است بنابراین اپراتور ! آن را به true تبدیل می کند و سپس دومین اپراتور ! آن را به false برمی گرداند.

روش صحیح حل سوال: آرایه خالی همیشه truthy است بنابراین با اپراتور ! به false و سپس به true تبدیل می شود.

بنابراین آرایه خالی truthy است اما با خود true برابر نخواهد بود.

null مقداری falsy است اما false نیست

بر اساس آنچه در بخش قبل توضیح دادم می توان گفت null نیز falsy است اما false نیست:

!!null; // -> false

null == false; // -> false

توضیح این کد دقیقا همان توضیح بخش قبل است بنابراین آن را تکرار نمی کنم اما نکته مهمی وجود دارد: نباید این بحث را به مقادیر دیگر تفصیل بدهید! یعنی چه؟ یعنی این بخش فقط مخصوص null است و هیچ ارتباطی با دیگر مقادیر falsy ندارد. به مثال زیر توجه کنید:

0 == false; // -> true

"" == false; // -> true

رشته های خالی و عدد صفر مقادیری falsy هستند اما با false هم برابر می باشند.

document.all و عجایب آن!

document.all بخشی از Browser API در جاوا اسکریپت است بنابراین در Node.js کار نمی کند. با اینکه document.all یک شیء آرایه مانند است و به ما اجازه دسترسی به عناصر DOM را می دهد،  در صورتی که به typeof داده شود مقدار undefined را برمی گرداند!

document.all instanceof Object; // -> true

typeof document.all; // -> 'undefined'

در عین حال document.all با undefined یا null برابری عینی (===) ندارد:

document.all === undefined; // -> false

document.all === null; // -> false

اما برابری ضمنی دارد:

document.all == undefined; // -> true

document.all == null; // -> true

این مسئله طبیعت جاوا اسکریپت است و باید آن را بدانید. دلیل این رفتار در این است که document.all روشی قدیمی برای ارتباط با عناصر DOM در مرورگر internet explorer بود اما دیگر از این روش برای دریافت عناصر DOM استفاده نمی کنیم (روش های جایگزین مانند document.getElementById را داریم). از آنجایی که این دستور استفاده زیادی داشت کمیته ecmascript تصمیم به حفظ آن گرفت اما طبیعتا از دوره خارج شد بنابراین چنین رفتاری را نشان می دهد (اطلاعات بیشتر در این لینک).

آیا کوچک ترین عدد از صفر بزرگ تر است؟

احتمالا شنیده اید که خصوصیت MIN_VALUE روی شیء Number در جاوا اسکریپت، کوچک ترین عدد ممکن در این زبان را به ما می دهد و ظاهرا این عدد از صفر بزرگ تر است!

Number.MIN_VALUE > 0; // -> true

چنین چیزی چطور ممکن است؟ در ابتدا باید توضیح بدهم که MIN_VALUE برابر با 5e-324 می باشد. یعنی چقدر؟ قبل از اینکه ادامه بدهیم باید در مورد حرف e صحبت کنیم. حرف e در ریاضیات و اعداد دو معنی دارد:

  • در محاسبات ریاضی به معنای «لگاریتم طبیعی» بوده و تقریبا برابر با 2.718281828459 می باشد.
  • در هنگام کار با ماشین حساب ها چیزی به نام «نماد علمی» است. نماد علمی روشی برای نوشتن و خواندن اعداد بسیار بزرگ کاربرد دارند. به طور مثال به جای نوشتن ۴۰۰۰۰۰۰۰۰۰۰ می توان ۴ را ضربدر ۱۰ به توان ۱۰ کرد که خلاصه تر نوشته و خوانده می شود. به زبان ساده تر: 4e10

بنابراین 5e-324 به معنی ۵ ضربدر ۱۰ به توانِ منفیِ ۳۲۴ است که عدد بسیار بسیار کوچکی خواهد بود. باید توجه داشته باشید که این عدد در عین کوچک بودن، هنوز مثبت است! حتما از ریاضیات دوران دبیرستان به یاد دارید که توان منفی به معنای منفی بودن عدد نیست بلکه به معنای اعشاری شدن عدد است. به طور مثال 0.0000000000001 هنوز هم از صفر بزرگ تر است. دو دلیل وجود دارد که باعث می شود توسعه دهندگان با نتیجه کد بالا متعجب شوند:

  • عدم درک صحیح از ریاضیات پایه (توان منفی به معنای منفی شدن عدد نیست)
  • عدم درک صحیح از Number.MIN_VALUE (این کوچکترین عدد ممکن در جاوا اسکریپت نیست بلکه کوچکترین عدد مثبت در جاوا اسکریپت است).

اگر Number.MIN_VALUE کوچکترین عدد ممکن در جاوا اسکریپت نیست بنابراین کوچکترین مقدار ممکن چیست؟ از نظر فنی Number.NEGATIVE_INFINITY کوچکترین مقدار ممکن است اما از نظر ریاضی یک عدد نیست بلکه نشان دهنده «بی نهایتِ منفی» می باشد! این مقدار از مباحث فنی و جزئی جاوا اسکریپت است که من نمی خواهم وارد آن شوم اما شما می توانید اطلاعات بیشتری را در صفحه توسعه دهندگان موزیلا مطالعه کنید.

function برابر با function نیست!

یکی از خطا های آزاردهنده در زبان جاوا اسکریپت خطای زیر است:

Uncaught TypeError: undefined is not a function

یعنی undefined یک تابع نیست اما خطای زیر از آن نیز آزاردهنده تر است:

// Declare a class which extends null

class Foo extends null {}

// -> [Function: Foo]




new Foo() instanceof null;

// > TypeError: function is not a function

// >     at … … …

همانطور که می بینید ما ابتدا کلاسی را تعریف کرده ایم که مقدار null در جاوا اسکریپت را extend می کند. این کار به ما تابعی به نام همین کلاس می دهد اما این تابع نمونه یا instance ای از null نیست! بلکه خطای function is not a function (تابع یک تابع نیست) را به ما می دهد! این یک باگ در Node.js است که تا نسخه 7 حضور داشت اما از نسخه ۸ و بعد از آن تصحیح شد (خطای متناسب را پرتاب می کند) بنابراین تا زمانی که از نسخه های قدیمی Node.js استفاده نمی کنید هیچ مشکلی نخواهید داشت.

جمع زدن آرایه ها

به نظر شما اگر دو آرایه را با هم جمع بزنیم چه اتفاقی می افتد؟

[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'

به نظر شما چرا نتیجه بالا را دریافت کرده ایم؟ بسیاری از توسعه دهندگان تصور می کنند که جمع زدن دو آرایه با یکدیگر باعث خطا می شود اما اینطور نیست. ما می توانیم کد بالا را به شکل زیر بنویسیم:

[1, 2, 3] +

  [4, 5, 6][

    // call toString()

    (1, 2, 3)

  ].toString() +

  [4, 5, 6].toString();

// concatenation

"1,2,3" + "4,5,6";

// ->

("1,2,34,5,6");

به عبارت ساده تر آرایه ها ابتدا به رشته تبدیل می شوند و سپس با هم جمع زده خواهند شد بنابراین نتیجه رشته "1,2,34,5,6" خواهد بود.

Trailing comma در آرایه ها

trailing comma علامت های ویرگولی هستند که در انتهای لیست اعضای آرایه می آیند:

var arr = [

  1,

  2,

  3,

];




arr; // [1, 2, 3]

arr.length; // 3

همانطور که می بینید این ویرگول انتهایی تغییری در تعداد اعضای آرایه ایجاد نمی کند (استفاده از آن دلایل خاص خودش را دارد مثل diff count صحیح و جلوگیری از crash کردن کد) اما اگر از چندین trailing comma استفاده کنیم چطور؟

let a = [, , ,];

a.length; // -> 3

a.toString(); // -> ',,'

ما در این مثال آرایه ای با چهار عضو خالی نوشته ایم اما یکی از آن ها trailing comma محسوب می شود بنابراین این آرایه یک آرایه ۳ عضوی خواهد بود. شاید مثال ساده تری از این رفتار را بتوان در کد زیر مشاهده کرد:

var arr = [1, 2, 3,,,];

arr.length; // 5

یکی از ویرگول ها trailing comma محسوب می شود بنابراین تعداد اعضای آرایه ۵ عدد خواهد بود.

غول بزرگ تساوی در آرایه ها

مبحث تساوی آرایه ها و مقایسه آن ها با هم در زبان جاوا اسکرپیت مبحثی بسیار پیچیده است و برای خودش غولی حساب می شود! به مثال زیر توجه کنید:

[] == ''   // -> true

[] == 0    // -> true

[''] == '' // -> true

[0] == 0   // -> true

[0] == ''  // -> false

[''] == 0  // -> true




[null] == ''      // true

[null] == 0       // true

[undefined] == '' // true

[undefined] == 0  // true




[[]] == 0  // true

[[]] == '' // true




[[[[[[]]]]]] == '' // true

[[[[[[]]]]]] == 0  // true




[[[[[[ null ]]]]]] == 0  // true

[[[[[[ null ]]]]]] == '' // true




[[[[[[ undefined ]]]]]] == 0  // true

[[[[[[ undefined ]]]]]] == '' // true

همانطور که می بینید اپراتور == اپراتور بسیار عجیبی است. شما می توانید لیست کاملی از رفتار این اپراتور را در documentation رسمی ECMASCRIPT (انتهای صفحه ۱۳۳) مطالعه نمایید. من چند مورد از مثال های بالا را توضیح می دهم:

  • در خط اول یک آرایه خالی با یک رشته خالی مقایسه شده است. در این مقایسه اپراتور == باعث تبدیل شدن آرایه به رشته می شود و یک رشته خالی با یک رشته خالی برابر است بنابراین نتیجه true می باشد.
  • در خط دوم آرایه خالی با عدد صفر مقایسه شده است. عدد صفر یک مقدار falsy است و آرایه نیز به رشته خالی تبدیل می شود که falsy است بنابراین هر دو مقدار falsy بوده و نتیجه true خواهد بود.
  • در خط چهارم آرایه ای تک عضوی (عدد صفر) با عدد صفر مقایسه شده است. برای مقایسه ابتدا آرایه ما به رشته صفر تبدیل می شود بنابراین صفر با صفر یکی خواهد بود و true می گیریم.

مطالعه دیگر موارد را بر عهده خودتان می گذارم.

undefined و Number

به کد زیر توجه کنید:

Number(); // -> 0

Number(undefined); // -> NaN

در صورتی که هیچ آرگومانی را به تابع Number پاس ندهیم، عدد صفر را دریافت می کنیم. از طرفی در جاوا اسکریپت زمانی که مقداری برای یک آرگومان خاص نداریم، معمولا undefined را به آن می دهیم. به همین دلیل ممکن است تصور کنید که پاس دادن undefined دقیقا مانند پاس ندادن هیچ آرگومانی باشد اما اینطور نیست و اگر undefined را پاس بدهید NaN می گیرید. چرا؟ توضیحات آن بدین شکل است:

  • اگر هیچ آرگومانی پاس داده نشده باشد، n (عدد پاس داده شده) برابر با 0+ (عدد صفر) خواهد بود.
  • در غیر این صورت n باید ToNumber(value) شود.
  • در نهایت اگر n برابر با undefined باشد ToNumber(undefined) باید NaN را برگرداند.

عجایب parseInt

تابع parseInt از توابع معروف جاوا اسکریپت است چرا که رفتارهای عجیب و غریبی از خودش نشان می دهد:

parseInt("r*xo"); // -> NaN

parseInt("r*xo", 16); // -> 15

چرا در این مثال به NaN رسیده ایم؟ تابع parseInt کاراکتر به کاراکتر جلو می رود تا زمانی که به کاراکتری برسد که آن را نمی شناسد بنابراین زمانی که به * می رسد باعث تولید NaN می شود. parseInt دو پارامتر دارد: پارامتر اول همان مقداری است که باید به عدد تبدیل شود و پارامتر دوم مبنای عدد مورد نظر است. با این حساب چطور می توانیم رشته r*xo را در مبنای ۱۶ حساب کنیم در حالی که در کد اول نمی توانستیم عددی را محاسبه کنیم؟! حرف f در رشته r*xo مقدار ۱۵ را در مبنای hexadecimal دارد.

برای درک پیچیدگی این تابع و رفتارهای عجیب آن کافی است به مثال های زیر نگاهی بیندازید:

//

parseInt("Infinity", 10); // -> NaN

// ...

parseInt("Infinity", 18); // -> NaN...

parseInt("Infinity", 19); // -> 18

// ...

parseInt("Infinity", 23); // -> 18...

parseInt("Infinity", 24); // -> 151176378

// ...

parseInt("Infinity", 29); // -> 385849803

parseInt("Infinity", 30); // -> 13693557269

// ...

parseInt("Infinity", 34); // -> 28872273981

parseInt("Infinity", 35); // -> 1201203301724

parseInt("Infinity", 36); // -> 1461559270678...

parseInt("Infinity", 37); // -> NaN

عملیات های محاسباتی پشت پرده باعث تولید چنین نتایجی می شود. این مباحث بسیار پیچیده بوده و از حوزه بحث این مقاله خارج هستند. هدف ذکر این مبحث در این قسمت، آشنایی شما با رفتار parseInt می باشد. این رفتارها فقط به Infinity خلاصه نمی شوند، به طور مثال:

parseInt(null, 24); // -> 23

در واقع parseInt سعی می کند null را به رشته "null" تبدیل کند. پس از انجام این کار در مبنای صفر تا ۲۳ هیچ راهی برای تبدیل این رشته به عدد ندارد بنابراین NaN را برمی گرداند اما در مبنای ۲۴ می تواند عدد ۲۳ را بدهد. چطور؟ در مبنای ۲۴ حرف n از رشته null برابر با ۱۴ اُمین حرف الفبای انگلیسی است، همچنین در مبنای ۳۱ حرف u برابر ۲۱ اُمین حرف الفبا خواهد بود و نهایتا کل رشته decode می شود.

همچنین مسئله در octal ها پیچیده تر می شود:

parseInt("06"); // 6

parseInt("08"); // اگر از نسخه پنجم اکمااسکریپت پشتیبانی شود عدد هشت را می گیریم

parseInt("08"); // اگر از نسخه پنجم اکمااسکریپت پشتیبانی نشود عدد صفر را می گیریم

چطور؟ اگر رشته پاس داده شده به parseInt با صفر شروع شود، مبنا عدد هشت (octal) یا ۱۰ (decimal) خواهد بود. حالا اگر runtime ای که کد ما را اجرا می کند از ECMAScript 5 پشتیبانی کند از مبنای ۱۰ استفاده خواهد کرد، در غیر این صورت از مبنای ۸ استفاده می کند. از آنجایی که برخی از مرورگرها از ECMAScript 5 پشتیبانی نمی کنند بهتر است همیشه مبنا را به صورت دستی مشخص کنید. همچنین در هنگام کار با اعداد اعشاری باید انتظار نتایجی مانند نتایج زیر را داشته باشید:

parseInt(0.000001); // -> 0

parseInt(0.0000001); // -> 1

parseInt(1 / 1999999); // -> 5

چطور چنین چیزی ممکن است؟ تابع parseInt همیشه یک رشته را گرفته و عدد آن را در مبنای مشخص شده برمی گرداند. از طرف دیگر زمانی که parseInt به اولین کاراکتر غیر عددی در رشته پاس داده شده برسد، ادامه رشته را حذف می کند. با این حساب 0.000001 به "0.000001" تبدیل می شود و سپس parseInt عدد صفر را برمی گرداند (همه چیز بعد از اعشار یا همان علامت . حذف می شود). حالا خط دوم از کد بالا را در نظر بگیرید؛ زمانی که 0.0000001 به رشته پاس داده شود، با نماد علمی "1e-7" نوشته می شود بنابراین parseInt به حرف e می رسد و همه چیز پس از آن را حذف می کند و تنها 1 را برمی گرداند. برای خط سوم نیز می گوییم که 1/1999999 (۱ تقسیم بر ۱۹۹۹۹۹۹) برابر با 5.00000250000125e-7 خواهد بود بنابراین parseInt فقط عدد ۵ را برمی گرداند.

ریاضی با مقادیر boolean

آیا می توانید نتایج تولید شده در کد زیر را توضیح بدهید؟

true -

  true + (

    // -> 2

    true + true

  ) *

    (true + true) -

  true; // -> 3

ما در اینجا از عملیات ریاضی برای جمع زدن true استفاده کرده ایم. از طرفی می دانیم که true در عملیات های ریاضی به عدد ۱ تبدیل می شود:

Number(true); // -> 1

این کار به شکل زیر نیز انجام می شود:

+true; // -> 1

با این حساب اگر به جای true در عملیات ریاضی بالا عدد ۱ را بگذاریم نتیجه را به دست می آوریم:

1 -

  1 + (

    1 + 1

  ) *

    (1 + 1) -

  1; // -> 3

کامنت های HTML در جاوا اسکریپت!

آیا می دانستید استفاده از کامنت های HTML در زبان جاوا اسکریپت نیز مجاز است؟

// valid comment

<!-- valid comment too

اکثر توسعه دهندگان از وجود چنین قابلیتی بی خبر هستند! کامنت های HTML در ابتدا برای مرورگرهایی قدیمی (امثال Netscape 1) ساخته شده بودند که هنوز با تگ <script> آشنایی نداشتند تا بتوانند بدون خراب شدن ناگهانی کد ها و به مرور زمان خودشان را با استاندارد های جدید وفق بدهند. این مرورگرها دیگر در دنیای مدرن استفاده ای ندارند بنابراین هیچ نیازی به قرار دادن کامنت های HTML درون تگ های <script> نداریم. از آنجایی که Node.js بر اساس موتور V8 کروم ساخته شده است، از این رفتار پیروی کرده و به شما اجازه می دهد از کامنت های HTML استفاده کنید.

توجه داشته باشید که این مورد نیز مانند اکثر موارد این صفحه جنبه معمایی و چالشی دارند و صرفا به این دلیل که «می توانید» از کامنت های HTML استفاده کنید نباید به سراغ چنین کاری برویم.

NaN یک عدد است!

همانطور که می دانید NaN مخفف Not a Number (یک عدد نیست) مقدار خاصی برای نمایش مقادیر غیر عددی است اما اگر از اپراتور typeof روی آن استفاده کنیم number را دریافت می کنیم:

typeof NaN; // -> 'number'

دلیل این اتفاق این است که NaN در مقداری عددی ذخیره می شود (هویت نگهدارنده اش عددی است) بنابراین زمانی که typeof را روی آن اجرا می کنیم number را دریافت می کنیم.

آرایه های خالی و null شیء هستند

یکی دیگر از مسائل عجیب و غریب در جاوا اسکریپت این است که آرایه های خالی و null هر دو شیء محسوب می شوند:

typeof []; // -> 'object'

typeof null; // -> 'object'




// در عین حال

null instanceof Object; // false

این از رفتارهای عجیب جاوا اسکریپت است و به تفاوت های بنیادین و پشت صحنه ای سورس کد جاوا اسکریپت برمی گردد. به طور خلاصه اپراتور typeof یک رشته را بر اساس جدول ۳۵ از documentation اکمااسکریپت برمی گرداند. null در این جدول جزو مقادیری است که خصوصیت [[Call]] را پیاده سازی نمی کند بنابراین رشته object برایش برگردانده می شود. گرچه شما می توانید با استفاده از toString نوع دقیق تر آن شیء را به صورت دستی چک کنید:

Object.prototype.toString.call([]);

// -> '[object Array]'




Object.prototype.toString.call(new Date());

// -> '[object Date]'




Object.prototype.toString.call(null);

// -> '[object Null]'

باز هم در نظر داشته باشید که این مسئله از مباحث بسیار پیشرفته جاوا اسکریپت است و تنها برای به چالش کشیدن ذهن شما مطرح شده اند.


منبع: وب‌سایت github

نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.