زنجیرهی اختیاری .? روشی بدون خطا برای دستیابی به ویژگیهای(properties) داخلی شیء است حتی در زمانی که ویژگی میانی وجود نداشته باشد
مشکل “ویژگی ناموجود”
اگر به تازگی شروع به خواندن آموزش و یادگیری جاوااسکریپت کردهاید ، شاید این مشکل را هنوز لمس نکردهاید، اما این یک مشکل کاملاً رایج است.
به عنوان مثال، بیایید بگوییم شیءهای user را داریم که اطلاعاتی درباره کاربرهای ما در خود دارد.
اکثر کاربران ما در ویژگی user.address آدرسهایی دارند و خیابان را در user.address.street دارند ولی بعضی از آنان این اطلاعات را ارائه نکردهاند.
در چنین موردی، اگر ما تلاش کنیم مقدار user.address.street را دریافت کنیم، و کاربر آدرس نداشته باشد، با خطا مواجه میشویم:
let user = {}; // "address" یک کاربر بدون ویژگی
alert(user.address.street); // !خطا
این یک خروجی قابل حدس است٬ جاوااسکریپت اینگونه کار میکند. تا زمانی که user.address برابر با undefined است تلاش برای گرفتن user.address.street با خطا مواجه میشود.
ولی در بسیاری از موارد عملی، ما ترجیح میدهیم به جای خطا، undefined را دریافت کنیم (به معنای “بدون خیابان”).
…و مثالی دیگر. در توسعه وب، ما میتوانیم یک شیء که با یک المان در صفحه مطابقت دارد را با استفاده از یک متد خاص، مانند document.querySelector('.elem') بگیریم و این متد هنگامی که چنین المانی وجود نداشته باشد null را برمیگرداند:
// document.querySelector('.elem') خواهد شد اگر المنت وجود نداشته باشد null برابر با
let html = document.querySelector('.elem').innerHTML; // باشد خطا خواهد داد null اگر
باری دیگر، اگر اِلمان وجود نداشته باشد ما هنگام دسترسی به ویژگی .innerHTML از null ارور دریافت میکنیم. و در بعضی موارد، وقتی که نبودِ اِلمان طبیعی است، ما میخواهیم از خطا جلوگیری کنیم و فقط html = null را به عنوان نتیجه قبول کنیم.
چگونه میتوانیم این کار را انجام دهیم؟
راهحل واضح این است که قبل از اینکه به ویژگی آن دسترسی پیدا کنیم، مقدار آن را با if یا عمگر شرطی ? بررسی کنیم، به این صورت:
let user = {};
alert(user.address ? user.address.street : undefined);
الان بدون خطا کار میکند… ولی اصلا زیبا نیست. همانطور که میبینید "user.address" دوبار در کد تکرار شده است.
اینجا میبینیم که این موضوع چگونه برای document.querySelector بنظر میرسد:
let html = document.querySelector('.elem') ? document.querySelector('.elem').innerHTML : null;
میتوانیم ببینیم که جستجوی المان document.querySelector('.elem') اینجا دوبار فراخوانی شده است. این خوب نیست.
برای ویژگیهایی با تو در تویی عمیقتر، حتی زشتتر میشود چون تکرارهای بیشتری نیاز است.
برای مثال بیاید مقدار user.address.street.name را با روشی مشابه بگیریم.
let user = {}; // کاربر آدرسی ندارد
alert(user.address ? user.address.street ? user.address.street.name : null : null);
این افتضاح است، یک نفر ممکن است حتی با درک این کد مشکل داشته باشد.
راه بهتری برای نوشتن آن وجود دارد، با استفاده از عملگر &&:
let user = {}; // کاربر آدرسی ندارد
alert( user.address && user.address.street && user.address.street.name ); // undefined (بدون خطا)
استفاده از AND در کل مسیر رسیدن به ویژگی، وجود همه اجزا را تضمین میکند (اگر این چنین نباشد، ارزیابی متوقف میشود)، اما آن هم ایدهآل نیست.
همانطور که میبینید نام ویژگیها همچنان در کد تکرار میشوند. به طور مثال در قطعه کد بالا user.address سه بار تکرار شده است.
به همین دلیل زنجیرهی اختیاری .? به زبان اضافه شد. تا این مشکل را برای همیشه برطرف کند!
زنجیرهی اختیاری
زنجیرهی اختیاری .? اگر مقدار قبل از قسمت ?. برابر با undefined یا null باشد ارزیابی را متوقف میکند و مقدار undefined را برمیگرداند.
در ادامه این مقاله، برای اختصار، خواهیم گفت که اگر چیزی null و undefined نباشد، “وجود دارد”.
یا به عبارت دیگر value?.prop:
- اگر
valueوجود داشته باشد، مثلvalue.propکار میکند، - در غیر اینصورت (زمانی که
valueبرابر باundefined/nullاست) مقدارundefinedرا برمیگرداند.
کد پایین راهی مطمئن برای دسترسی به user.address.street با استفاده از .? است:
let user = {}; // کاربر آدرسی ندارد
alert( user?.address?.street ); // undefined (بدون خطا)
حالا کد کوتاه و تمیز است، بدون هیچ تکرار اضافهای.
اینجا مثالی با استفاده از document.querySelector داریم:
let html = document.querySelector('.elem')?.innerHTML; // خواهد بود undefined اگر المانی وجود نداشته باشد
خواندن آدرس با استفاده از user?.address حتی اگر شیء user وجود نداشته باشد هم کار میکند:
let user = null;
alert( user?.address ); // undefined
alert( user?.address.street ); // undefined
لطفا توجه داشته باشید: سینتکس .? مقدارهای قبلی را اختیاری میکند نه مقدارهای جلوی آن را.
مثلا در user?.address.street.name عبارت .? اجازه میدهد که user برابر با null/undefined باشد (و در این صورت undefined را برمیگرداند)، اما این موضوع فقط برای user صادق است. به ویژگیهای جلویی با سبک معمولی دسترسی پیدا میشود. اگر ما میخواهیم بعضی از ویژگیها را اختیاری کنیم میتوانیم تعداد بیشتری از . را با .? جایگزین کنیم.
ما باید از .? فقط زمانی استفاده کنیم که عدم وجود چیزی اشکالی ندارد.
برای مثال، اگر طبق منطق کد ما، شیء user باید وجود داشته باشد ولی address اختیاری باشد، پس ما باید اینگونه بنویسیم user.address?.street نه user?.address?.street.
بنابراین، اگر تصادفاً user برابر با undefined باشد، شاهد یک خطای برنامهنویسی در مورد آن خواهیم بود و آن را برطرف خواهیم کرد. در غیر این صورت، اگر از .? استفاده کنیم، خطاهای کد را می توان در مواردی که مناسب نیست ساکت کرد، و این کار اشکالزدایی را دشوارتر میکند.
.? باید تعریف شده باشداگر متغیر user کلا وجود نداشته باشد user?.anything خطا میدهد:
// ReferenceError: user is not defined
user?.address;
باید متغیر تعریف شده باشد (برای مثال: let/const/var user یا به عنوان یک پارامتر تابع). زنجیرهی اختیاری فقط برای متغیرهای تعریف شده کار میکند.
کوتاه کردن اتصال
همانطور که قبلا گفته شد عبارت .? اگر عبارت سمت چپ آن وجود نداشته باشد، فوراً ارزیابی را متوقف میکند (اتصال را کوتاه میکند).
بنابراین، اگر فراخوانی تابعی یا عملیات دیگری در سمت راست .? وجود داشته باشند، اتفاق نمیافتند.
برای نمونه:
```js run let user = null; let x = 0;
user?.sayHi(x++); // نمیرسد ++x و sayHi وجود ندارد، پس اجرای کد به فراخوانی “user”
alert(x); // 0 :مقدار افزایش نیافته پس ```
انواع دیگر: ?.()، ?.[]
زنجیرهی اختیاری .? یک عمگر نیست بلکه یک ساختار سینتکسی خاص است که با توابع و براکتها نیز کار میکند.
برای مثال ().? برای صدا زدن تابعی که ممکن است وجود نداشته باشد هم کاربرد دارد.
در کد زیر، برخی از کاربران ما متد admin را دارند و برخی خیر:
```js run let userAdmin = { admin() { alert(“من ادمین هستم”); } };
let userGuest = {};
! userAdmin.admin?.(); // من ادمین هستم /!
! userGuest.admin?.(); // چیزی اتفاق نمیافتد (چنین متدی وجود ندارد) /! ```
اینجا، در هر دو خط، ما ابتدا از نقطه (userAdmin.admin) برای گرفتن ویژگی admin استفاده میکنیم به خاطر اینکه فرض میکنیم که شیء user حتما وجود دارد پس خواندن از آن مطمئن است.
سپس ().? عبارت سمت چپ را بررسی میکند: اگر تابع admin وجود داشته باشد اجرا میشود (برای userAdmin صدق میکند). در غیر اینصورت (برای userGuest) ارزیابی بدون خطا متوقف میشود.
سینتکس [].? نیز کار میکند، اگر ما میخواهیم از براکت به جای نقطه . برای دستیابی به ویژگیها استفاده کنیم. مشابه موارد قبلی، این سینتکس اجازه می دهد تا با خیال راحت یک ویژگی از شیءای که ممکن است وجود نداشته باشد را بخوانیم.
let key = "firstName";
let user1 = {
firstName: "John"
};
let user2 = null;
alert( user1?.[key] ); // John
alert( user2?.[key] ); // undefined
ما میتوانیم از .? با delete هم استفاده کنیم:
delete user?.name; // را حذف کن user.name وجود داشت user اگر
.? برای پاک کردن و خواندن مطمئن استفاده کنیم ولی مقداردهی نه.زنجیرهی اختیاری .? هیچ کاربردی برای سمت چپ مساوی ندارد.
برای مثال:
let user = null;
user?.name = "John"; // ارور، کار نمیکند
// undefined = "John" :چون اینگونه ارزیابی میشود
خلاصه
زنجیرهی اختیاری ?. سه شکل دارد:
obj?.prop– مقدار obj.propرا اگرobjوجود داشته باشد برمیگرداند در غیر اینصورت مقدارundefinedرا برمیگرداند.obj?.[prop]– مقدار obj.[prop]را اگرobjوجود داشته باشد برمیگرداند در غیر اینصورت مقدارundefinedرا برمیگرداند.obj.method()– obj?.methodرا اگرobjوجود داشته باشد صدا میزند در غیر این صورت مقدارundefinedرا برمیگرداند.
همانطور که میبینیم، همه آنها برای استفاده ساده و آسان هستند. .? سمت چپ را از نظر null/undefined بودن بررسی میکند و اگر برابر با null/undefined نباشد اجازه میدهد تا ارزیابی ادامه یابد.
زنجیرهای از .? امکان دسترسی به ویژگیهای تودرتو را هم فراهم میکند.
با این حال هنوز ما باید .? را با دقت اعمال کنیم، فقط درصورتی که با توجه به منطق کد ما وجود نداشتن قسمت سمت چپ قابل قبول باشد. تا اگر ارورهای برنامهنویسی رخ دادند، از ما پنهان نباشند.
نظرات
<code>استفاده کنید، برای چندین خط – کد را درون تگ<pre>قرار دهید، برای بیش از ده خط کد – از یک جعبهٔ شنی استفاده کنید. (plnkr، jsbin، codepen…)