۷ نکته در مورد جاوا اسکریپت که احتمالاً نمی‌دانید
۷ نکته در مورد جاوا اسکریپت که احتمالاً نمی‌دانید

کار کردن با جاوا اسکریپت جذابیت‌های زیادی دارد. این زبان به‌قدری گسترده است که حتی اگر هرروز از آن استفاده کنید، هرچند وقت یک‌بار با قسمت‌های ناشناخته‌ای مواجه می‌شوید. در این مقاله تلاش خواهیم کرد روی این بخش ناشناخته و نکاتی تمرکز کنیم که احتمالاً نمی‌دانید. با این ۷ نکته در مورد جاوا اسکریپت همراه باشید.

۱- دو صفر وجود دارد

ما معمولاً از حالت مثبت صفر استفاده می‌کنیم؛ اما یک صفر منفی هم وجود دارد! به خاطر طرز رشته بندی شدنشان، نمی‌شود این علامت‌ها را در کنسول دید:

+۰
→ ۰
 
-۰
→ ۰

یعنی فرمان (-۰).toString() و (+۰).toString()، هردو خروجی صفر می‌دهند. اگر کمی در این نکته عمیق‌تر شویم، می‌بینیم که این صفر مثبت و صفر منفی در واقع باهم برابرند.

+۰ === -۰
→ true
 
+۰ > -0
→ false
 
+۰ < -0
→ false

نکته این‌جاست که استفاده از فرمان indexOf روی خط‌ها هم کمک چندانی به ما نمی‌کند. چون این فرمان از مساوی سخت (===) استفاده می‌کند.

// We would expect 1, right?
[+۰, -۰, ۴۲].indexOf(-0)
→ ۰
 
[-۰, +۰, ۴۲].indexOf(+0)
→ ۰

اما یک‌راه برای تشخیص صفر منفی وجود دارد. برای این کار باید یک عدد مثبت و متناهی را بر صفری که داریم تقسیم کنیم.

۴۲ / ۰
→ Infinity
 
۴۲ / -۰
→ -Infinity

پس می‌توانیم به‌سادگی بگوییم:

// Two conditions:
//   ۱. The input parameter should be zero (negative or positive)
//   ۲. Dividing a number by that input, we should get a negative value (-Infinity)
let isNegativeZero = input => input === 0 && 1 / input < 0;
 
// Let's test it
isNegativeZero(0)
→ false
 
isNegativeZero(+0)
→ false
 
isNegativeZero(-0)
→ true

مقاله مرتبط: راهنمای غیر فعال کردن JavaScript در مرورگر های مختلف


۲- NaN یک عدد ویژه است

ازآنجایی‌که NaN خلاصه‌شده‌ی کلمه‌ی Not a Number (غیر عدد) است، اصولاً انتظار داریم هر چیزی باشد به‌جز عدد؛ اما در جاوا اسکریپت NaN یک عدد ویژه است:

typeof NaN
→ "number"

برای به دست آوردن NaN چندراه وجود دارد:

  • تقسیم کردن صفر بر صفر: فرمان ۰ / ۰
  • تبدیل کردن یک‌رشته‌ی غیر عدد به عدد: فرمان +’foo’
  • کم کردن بی‌نهایت از بی‌نهایت
  • و راه‌های دیگر…

واقعیت مهم این است که NaN یک عدد ویژه است. علاوه بر این، باید بدانید که NaN با خودش برابر نیست. پس اگر چیزی شبیه (x !== x) دیدید، باید بدانید که با چه پدیده‌ای مواجه شده‌اید. به‌طور خلاصه NaN با خودش برابر نیست.

NaN === NaN
→ false
 
// Even by storing the value in a variable
let x = NaN
x === x
→ false

یکی از تأثیرات جانبی این نکته این است که نمی‌توانید از فرمان indexOf برای فهمیدن ایندکس NaN در یک‌رشته استفاده کنید:

let values = [7, NaN, 42];
 
// Find the index of 42
values.indexOf(42);
→ ۲
 
// Find the index of NaN (WRONG!)
values.indexOf(NaN)
→ -۱
 
 
// If you really want to find the index of NaN, simply create your indexOf function
let myIndexOf = (arr, value, start) => {
  if (value !== value) {
    start = start || 0;
    for (let i = start; i < arr.length; ++i) {
       if (arr[i] !== arr[i]) {
          return i;
       }
    }
    return -1;
  }
  return arr.indexOf(value, start);
};
 
// Now, it will work!
myIndexOf(values, NaN)
→ ۱

توجه داشته باشید که به‌جای فرمان x !== x برای این‌که بفهمید NaN با x برابر است یا نه، می‌توانید از فرمان isNaN(x) استفاده کنید. البته حالت دوم، کمی کندتر است.

می‌پرسید چرا؟ پاسخ ساده این است که چنین اتفاقی به خاطر IEEE ۷۵۴ می‌افتد: تمام NaN ها باید با همه‌چیز -ازجمله خودشان- به شکل نامساوی مقایسه شوند.

توصیه: اگر از ES۲۰۱۵ استفاده می‌کنید، فرمان includes به این شکل عمل می‌کند:

[۴۲, NaN].includes(NaN)
→ true

NaN یک عدد متناهی نیست. البته معنی این جمله این نیست که NaN بی‌نهایت است. درواقع باید گفت که NaN نه متناهی است و نه بی‌نهایت.

// Check if it's finite
isFinite(NaN)
→ false
 
// Comparing with infinity, it will always give us false
Infinity > NaN
→ false
 
> Infinity < NaN
→ false
 
-Infinity < NaN
→ false
 
> -Infinity > NaN
→ false

NaN نه مثبت است و نه منفی. چیزی مثل +NaN یا –NaN نداریم. هردوی این‌ها NaN هستند.

NaN
→ NaN
 
-NaN
→ NaN
 
+NaN
→ NaN

۳- استفاده از عملگر بیتی

این مسئله مستقل از زبان مورداستفاده است؛ اما چون عملگرهای بیتی در جاوا اسکریپت هم پشتیبانی می‌شوند، در ادامه به چند نکته‌ی جالب در مورداستفاده از آن‌ها اشاره خواهیم کرد.

ضرب و تقسیم سریع اعداد صحیح

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


مقاله مرتبط: چگونه پیام هشدار واکنش گرا را جایگزین JavaScript کنیم؟


هروقت که بخواهید عددی را در ۲، ۴، ۸، ۱۶ یا هر توان دیگری از ۲ ضرب کنید یا آن را بر یکی از این اعداد تقسیم کنید، عملگرهای بیتی بسیار مفیدند. استفاده از آن‌ها، بیت‌های اعداد را به راست (برای تقسیم) یا چپ (برای ضرب) می‌برد.

// Same with 21 * 2, but faster
۲۱ << 1
→ ۴۲
  
// Same with 5 * 4
۵ << 2
→ ۲۰

عملگر << تمام بیت‌ها را یک جایگاه به سمت چپ می‌برد. اعداد به شکل باینری نمایش داده می‌شوند. در نتیجه یک عدد صفر به سمت راست اضافه می‌شود:

// ۵ in base 2:
۱۰۱
 
۱۰۱ << 1
→ ۱۰۱۰ (which is 10 in base 2)

استفاده از عملگر >> هم نتیجه‌ی مشابهی به همراه دارد.

۸۴ >> 1
→ ۴۲

بر اساس آزمایش‌های مختلف، استفاده از << به‌اندازه‌ی ۱٫۰۴ برابر از عملگر * سریع‌تر است. البته ممکن است نخواهید قابلیت خوانده شدن ساده‌ی کد توسط افراد دیگر را به خاطر این مقدار از افزایش سرعت از دست بدهید. ولی به‌هرحال دانستن این نکته می‌تواند به دردتان بخورد.

ارسال پیام‌های رمزگذاری شده

عملگر XOR (^) در رمزنگاری (کریپتوگرافی) استفاده می‌شود. شما می‌توانید با استفاده از این فرمان، پیام‌ها را رمزگذاری یا رمزگشایی کنید. نحوه‌ی عملکرد به این شکل است:

A   B   ^
=========
۰   ۰   ۰
۰   ۱   ۱
۱   ۰   ۱
۱   ۱   ۰

یک مثال ساده با رمزگذاری روی یک عدد و سپس رمزگشایی از آن، مسئله را کاملا نشان می‌دهد:

// Alice and Bob share the same secret key:
let key = 123;
 
// Alice wants to send Bob a number
let msg = 42;
 
// But before sending it, Alice encrypts it:
msg = msg ^ key // or directly: msg ^= key
→ ۸۱
 
// Bob receives 45, but knowing the key is 123 he knows to decrypt it:
۸۱ ^ key
→ ۴۲
 
// Now Bob can enjoy the message from Alice

فهمیدن این‌که یک عنصر خاص جزء یک خط است یا نه

استفاده از عملگر بیتی NOT (~) به ما اجازه می‌دهد چک کنیم که آیا یک عنصر خاص در یک خط وجود دارد یا نه.

let fruits = ["apple", "pear", "orange"];
 
// Usually they do it like this
if (fruits.indexOf("pear") !== -1) {
   ...
}
 
// That works, but using the `~` makes the code shorter
if (~fruits.indexOf("pear")) {
   ...
}

این عملگر، -۱ را به ۰ تبدیل می‌کند (به خاطر طبیعت باینری که ۰ را به ۱ و ۱ را به ۰ تبدیل می‌کند) اما برای اعداد دیگر یک خروجی غیر صفر نمایش می‌دهد. چون صفر یک مقدار نادرست است (!!۰ → false) وارد نتایج شرط تعیین‌شده‌ی شما نمی‌شود.


مقاله مرتبط: ١٩ تکنیک خلاصه نویسی JavaScript


توصیه: اگر از ES۲۰۱۵ استفاده می‌کنید، فرمان includes به این شکل عمل می‌کند:

["pear", "apple"].includes("apple")
→ true

۴- از کدهای هکس یا یونیکد به‌عنوان نماینده‌ی رشته‌ها استفاده کنید

اگر به دلیل خاصی می‌خواهید یک‌رشته از کد را بدون استفاده از توابع مخفی کنید، می‌توانید از جاوا اسکریپت استفاده کنید. از توالی‌های هکسادسیمال یا یونیکد استفاده کنید:

// Hex: '2a' (in base 16) is 42—representing the ASCII code of '*'
'\x2a'
→ '*'
 
// Unicode: it expects 4 hex digits
'\u002a'
→ '*'

بیایید یک تابع ساده بسازیم که بتواند پیام‌های قابل خوانده شدن توسط انسان را تبدیل کند:

let strToHex = input => input.split('').map(
    // Convert each character into its hex code
    c => `\x${c.charCodeAt(0).toString(16)}`
).join('');
 
// Let's convert something.
strToHex('hello world!')
→ '\\x68\\x65\\x6c\\x6c\\x6f\\x20\\x77\\x6f\\x72\\x6c\\x64\\x21'
 
// Once we have the above output, we just have to remove the escaping:
$ echo '\\x68\\x65\\x6c\\x6c\\x6f\\x20\\x77\\x6f\\x72\\x6c\\x64\\x21' | awk 'gsub("\\\\\\\\", "\\")'
→ \x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x21
 
// And finally, we have it!
\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x21
→ 'hello world!'

۵- دور زدن عملگرهای منطقی باینری

در زبان باینری اگر اپراند اول شرایط تعریف‌شده برای نتیجه را داشته باشد، اپراند دوم ارزیابی نمی‌شود. بیایید به چند مثال نگاه کنیم:

true && 'hello'
→ 'hello'
 
// We won't get 'hello', because 'false && anything else' will be always false
false && 'hello'
→ false
 
// Let's try a logical or
false || 'hello'
→ 'hello'
 
// When we have more than two, they are interpreted in the obvious way (from left to right)
۰ && false && 'hello'
→ ۰
 
۴۲ && false && 'hello'
→ false
 
۴۲ && true && 'hello'
→ 'hello'

نکته‌ی جالب این است که این قضیه در مورد توابع هم صادق است.

// Get some random number
let rand = () => Math.random() + 1;
 
// Let's create an object to collect some data
let data = {};
 
// A function to assign some random numbers to that specific key, only the first time
let add = key => !data[key] && (data[key] = rand()) || data[key];
 
// Assign a random number to 'a'
add('a')
→ ۱.۰۳۹۸۱۶۸۷۱۷۶۵۹۲۴۲
 
// Reassigning won't work, but we get back the existing value
add('a')
→ ۱.۰۳۹۸۱۶۸۷۱۷۶۵۹۲۴۲
 
// Do the same for 'b'
add('b')
→ ۱.۴۲۶۷۹۱۵۰۸۳۳۷۸۷۲۲
 
// And for 'c'
add('c')
→ ۱.۴۹۵۲۸۹۳۲۹۶۶۵۷۸۵
 
// Let's see how the data looks like internally:
// Seems we do have a clean key-value map
{ a: 1.0398168717659242,
  b: 1.4267915083378722,
  c: 1.495289329665785 }

اما جادوی اصلی در تابع جمع اتفاق می‌افتد! بیایید آن را بسط دهیم:

// Let's see what's going on here
let add = key => !data[key] && (data[key] = rand()) || data[key];
 
// Looks more human-readable, but it's longer 😀
add = key => {
 
   // If the value is not yet set...
   if (!data[key]) {
      // set it!
      data[key] = rand();
   }
 
   // Always, do return the value
   return data[key];
};

۶- اجرای فرمان Eval در حالت سخت آن‌قدرها هم بد نیست

بله فرمان Eval بد است! اما درعین‌حال فایده‌هایی هم دارد که می‌توانند به دردتان بخورند.

وقتی دارید این فرمان را در حالت سخت اجرا می‌کنید، اجازه‌ی ساختن متغیر در قلمروی مجاور را ندارید. Eval فقط کدی که وارد کرده‌اید را ترجمه و اجرا می‌کند:

let x = 35;
 
// Sum x + 7
let result = eval('x + 7');
→ ۴۲
 
// You can declare variables there (we are NOT in the strict mode):
eval('var y = x + 7');
console.log(y);
→ ۴۲

حالا وقتی حالت سخت را فعال کنیم، فرمان دوم Eval هم درهرصورت کار می‌کند؛ اما متغیر y را خارج از محدوده‌ی Eval نمی‌سازد:

"use strict";
 
let x = 35;
 
eval('var y = x + 7');
//        ^
// ReferenceError: y is not defined
console.log(y);

۷- توابع دینامیک بسازید

با استفاده از سازنده‌ی جدید توابع، می‌توانیم تابع دینامیک بسازیم.

let square = new Function('x', 'return x * x');
 
// Let's see how it looks like:
console.log(square.toString());
function anonymous(x
/**/) {
return x*x
}
 
square(4)
→ ۱۶

یکی از موارد استفاده‌ی این نکته برای الگوسازی در کتابخانه‌ها (مثلاً ejs یا ajs و…) است. این کار، یک‌بار الگو را تجزیه می‌کند و تابعی می‌سازد که داده‌های الگو را در خود می‌پذیرد.

نکته: از فرمان Eval یا ساختن تابع برای مواردی مثل تجزیه‌ی داده‌های JSON یا به دست آوردن ارزش یک کلید هدف دینامیک استفاده نکنید.

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

۷ نکته در مورد جاوا اسکریپت که احتمالاً نمی‌دانید

علی رجبی

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

7  +  1  =