۲۵ ژوئیه ۲۰۲۲

نوع سمبل (Symbol type)

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

  • نوع رشته، یا
  • نوع سمبل(symbol).

در غیر این صورت، اگر کسی از نوع دیگری استفاده کند، مثل عدد، به صورت خودکار به رشته تبدیل می‌شود. پس obj[1] با obj["1"] یکسان است و obj[true] با obj["true"].

تا اینجا ما فقط از رشته‌ها استفاده می‌کردیم.

حال بیایید سمبل‌ها و کاری که می‌توانند برای ما انجام دهند را ببینیم.

سمبل‌ها (Symbols)

یک «سمبل» نشان دهنده‌ی شناسه‌ای یکتا است.

یک مقدار از این نوع می‌تواند با استفاده از Symbol() ساخته شود:

let id = Symbol();

بعد از ساختن، می‌توانیم به سمبل یک سری توضیحات بدهیم (همچنین به آن اسم سمبل هم می‌گویند)، که اکثرا برای رفع خطا استفاده می‌شود:

// است "id" یک سمبل به همراه توضیحات id
let id = Symbol("id");

سمبل‌ها برای یکتا بودن تضمین‌شده هستند. حتی اگر ما چند سمبل را با توضیحات یکسان بسازیم، آنها مقدارهایی متفاوت هستند. توضیحات فقط یک برچسب است که روی چیزی تاثیر نمی‌گذارد.

برای مثال، اینجا دو سمبل با توضیحات یکسان داریم – آنها برابر نیستند:

let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

اگر شما با Ruby یا زبان دیگری که یک جورایی “سمبل” دارد آشنایی دارید – لطفا گمراه نشوید. سمبل‌های جاوااسکریپت متفاوت هستند.

پس، به طور خلاصه، یک سمبل یک «مقدار یکتای اصلی» همراه با یک توضیح اختیاری است. بیایید ببینیم کجا می‌توانیم از آنها استفاده کنیم.

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

اکثر مقدارهای در جاوااسکریپت تبدیل به رشته به صورت ضمنی را انجام می‌دهند. برای مثال، ما می‌توانیم هر مقداری را alert کنیم، و این کار خواهد کرد. سمبل‌ها خاص هستند. آنها به صورت خودکار تبدیل نمی‌شوند.

برای مثال، این alert یک ارور را نمایش می‌دهد:

let id = Symbol("id");
alert(id); // TypeError: Cannot convert a Symbol value to a string

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

اگر ما واقعا نیاز داریم که یک سمبل را نمایش دهیم، باید به طور ضمنی همراه آن .toString() را هم صدا بزنیم، مثل اینجا:

let id = Symbol("id");
alert(id.toString()); // Symbol(id)، حالا کار می‌کند

یا فقط ویژگی symbol.description را برای نمایش توضیحات دریافت کنیم:

let id = Symbol("id");
alert(id.description); // id

ویژگی‌های «مخفی» شیء

سمبل‌ها به ما این امکان را می‌دهند که در یک شیء ویژگی‌های “مخفی” بسازیم، که هیچ کجای کد نتواند به صورت تصادفی به آن دسترسی داشته باشد یا آن را تغییر دهد.

برای مثال، اگر ما در حال کار کردن با شیءهای user که متعلق به یک شخص ثالث است باشیم. ما می‌خواهیم شناسه‌هایی به آنها اضافه کنیم.

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

let user = { // به یک کد دیگر تعلق دارد
  name: "John"
};

let id = Symbol("id");

user[id] = 1;

alert( user[id] ); // می‌توانیم با استفاده سمبل به عنوان کلید به داده دسترسی داشته باشیم

مزیت استفاده از Symbol("id") به جای رشته "id" چیست؟

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

همچنین تصور کنید که یک اسکریپت دیگر بنا به دلایلی، بخواهد شناسه خودش را درون user داشته باشد.

سپس آن اسکریپت می‌تواند Symbol("id") خودش را بسازد، مثل این:

// ...
let id = Symbol("id");

user[id] = "آنها id مقدار";

هیچ تعارضی بین شناسه ما و آنها وجود نخواهد داشت، چون سمبل‌ها همیشه متفاوت هستند، حتی اگر اسم یکسانی داشته باشند.

…اما اگر ما از رشته "id" به جای سمبل برای مقصود مشابه استفاده می‌کردیم، آنگاه تعارضی وجود داشت:

let user = { name: "John" };

// استفاده می‌کند "id" اسکریپت ما از ویژگی
user.id = "ما id مقدار";

// ...می‌خواهد "id" یک اسکریپت دیگر هم بنا به دلیلی...

user.id = "آنها id مقدار"
// !بوم! توسط یک اسکریپت دیگر بازنویسی شد

سمبل‌ها در یک شیء لیترال

اگر ما بخواهیم از یک سمبل در شیء لیترال {...} استفاده کنیم، باید دور آن براکت قرار دهیم.

مثل این:

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // "id": 123 نه
};

به این دلیل که ما به مقدار متغیر id به عنوان کلید نیاز داریم، نه رشته‌ی “id”.

سمبل‌ها توسط for…in رد می‌شوند

ویژگی‌های سمبلی در حلقه for..in شرکت نمی‌کنند.

برای مثال:

let id = Symbol("id");
let user = {
  name: "John",
  age: 30,
  [id]: 123
};

for (let key in user) alert(key); // name, age (بدون سمبل)

// دسترسی مستقیم از طریق سمبل کار می‌کند
alert( "به طور مستقیم: " + user[id] );

همچنین Object.keys(user) هم آنها را نادیده می‌گیرد. این بخشی از اصل کلی «مخفی‌سازی ویژگی‌های سمبلی» است. اگر یک اسکریپت یا کتابخانه دیگر در شیء ما حلقه بزند، به تصادفی به ویژگی سمبلی ما دسترسی نخواهد داشت.

در مقابل، Object.assign هم ویژگی‌های رشته و هم سمبل را کپی می‌کند:

let id = Symbol("id");
let user = {
  [id]: 123
};

let clone = Object.assign({}, user);

alert( clone[id] ); // 123

هیچ تناقضی اینجا وجود ندارد. طراحی آن اینگونه است. زمانی که ما یک شیء را شبیه‌سازی می‌کنیم یا شیءها را ادغام می‌کنیم، معمولا می‌خواهیم که تمام ویژگی‌ها کپی شوند (شامل سمبل‌هایی مثل id).

سمبل‌های گلوبال (global)

همانطور که دیدیم، معمولا تمام سمبل‌ها متفاوت هستند، حتی اگر اسم یکسان داشته باشند. اما گاهی اوقات ما می‌خواهیم که سمبل‌های هم نام با یکدیگر برابر باشند. برای مثال، قسمت‌های مختلف برنامه ما می‌خواهند به سمبل "id" که ویژگی یکسانی است دسترسی داشته باشند.

برای بدست آوردن آن، یک رجیستری ثبت سمبل گلوبال وجود دارد. ما می‌توانیم سمبل‌ها را درون آن بسازیم و بعدا به آن دسترسی داشته باشیم، و تضمین می‌کند که دسترسی تکراری با استفاده از یک اسم به ما دقیقا سمبل یکسان را برمی‌گرداند.

برای خواندن (یا در صورت ناموجود بودن، ساختن) یک سمبل از رجیستری ثبت، از Symbol.for(key) استفاده کنید.

این صدازدن رجیستری ثبت گلوبال را بررسی می‌کند، و اگر یک سمبل توصیف شده به عنوان key موجود باشد، آن را برمی‌گرداند، در غیر این صورت یک سمبل جدید Symbol(key) می‌سازد و آن را در رجیستری ثبت با استفاده از key داده شده ذخیره می‌کند.

برای مثال:

// گلوبال خواندن از رجیستری ثبت
let id = Symbol.for("id"); // اگر سمبل وجود نداشته باشد، ساخته می‌شود

// دوباره آن را می‌خواند (شاید از یک جای دیگر کد باشد)
let idAgain = Symbol.for("id");

// سمبل یکسان
alert( id === idAgain ); // true

سمبل‌های درون رجیستری ثبت سمبل‌های گلوبال نامیده می‌شوند. اگر ما یک سمبل در تمام سطح برنامه بخواهیم که در همه جای کد قابل دسترس باشد – این سمبل‌ها مناسب هستند.

به نظر می‌رسد مثل Ruby باشد

در بعضی از زبان‌های برنامه‌نویسی، مثل Ruby، به ازای هر اسم فقط یک سمبل وجود دارد.

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

متد Symbol.keyFor

دیدیم که برای سمبل‌های گلوبال، Symbol.for(key) یک سمبل را بر اساس اسم آن برمی‌گراند. برای انجام کار برعکس یعنی برگرداندن اسم بر اساس یک سمبل گلوبال می‌توانیم از این استفاده کنیم: Symbol.keyFor(sym):

برای مثال:

// گرفتن سمبل بر اساس اسم
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// گرفتن اسم بر اساس سمبل
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id

متد Symbol.keyFor درون خود از رجیستری ثبت سمبل گلوبال برای پیدا کردن کلید (key) سمبل استفاده می‌کند. پس برای سمبل‌های غیر گلوبال کار نمی‌کند. اگر سمبل گلوبال نباشد، قادر به پیدا کردن آن نخواهد بود و undefined را برمی‌گرداند.

همانطور که گفته شد، هر سمبل ویژگی description را دارد.

برای مثال:

let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");

alert( Symbol.keyFor(globalSymbol) ); // سمبل گلوبال ،name
alert( Symbol.keyFor(localSymbol) ); // گلوبال نیست ،undefined

alert( localSymbol.description ); // name

سمبل‌های سیستمی

تعداد زیادی سمبل “سیستمی” وجود دارد که جاوااسکریپت درون خود از آنها استفاده می‌کند، و ما می‌توانیم از آنها برای ایجاد تغییرات کوچک در جنبه‌های متنوع شیءها استفاده کنیم.

آنها در مشخصات زبان در جدول سمبل‌های شناخته‌شده لیست شده‌اند:

  • Symbol.hasInstance
  • Symbol.isConcatSpreadable
  • Symbol.iterator
  • Symbol.toPrimitive
  • …و غیره.

برای مثال، Symbol.toPrimitive به ما امکان توصیف تبدیل شیء به مقدار اصلی (primitive) را می‌دهد. ما کاربرد آن را به زودی می‌بینیم.

شما با بقیه سمبل‌ها هم زمانی که ویژگی زبان مربوط به آنها را مطالعه کنیم آشنا می‌شوید.

خلاصه

Symbol یک نوع مقدار اصلی (primitive) برای شناسه‌های یکتا است.

سمبل‌ها با صدازدن Symbol() به همراه توضیحات (اسم) اختیاری ساخته می‌شوند.

سمبل‌ها همیشه مقدارهای متفاوت دارند، حتی اگر اسم یکسان داشته باشند. اگر بخواهیم که سمبل‌های همنام برابر باشند، باید از رجیستری ثبت گلوبال استفاده کنیم: Symbol.for(key) یک سمبل گلوبال را با استفاده از key به عنوان اسم برمی‌گرداند (اگر نیاز باشد آن را می‌سازد). چند مرتبه صدازدن Symbol.for با key یکسان دقیقا سمبل یکسان را برمی‌گرداند.

سمبل‌ها در دو مورد زیاد استفاده می‌شوند:

  1. ویژگی‌های «مخفی» شیء. اگر ما بخواهیم یک ویژگی را درون یک شیء که به اسکریپت یا کتابخانه دیگری “تعلق دارد” اضافه کنیم، می‌توانیم یک سمبل بسازیم و از آن به عنوان کلید ویژگی استفاده کنیم. یک ویژگی سمبلی در for..in نمایان نمی‌شود، پس با ویژگی‌های دیگر به طور تصادفی روی آن فرایندی انجام نمی‌گیرد. همچنین دسترسی مستقیم به آن وجود ندارد، چون اسکریپت دیگر سمبل ما را ندارد. پس ویژگی از استفاده یا بازنویسی تصادفی در امان می‌ماند.

    پس ما می‌توانیم با استفاده از ویژگی‌های سمبلی، به صورت “مخفیانه” چیزی را که نیاز داریم درون شیءها پنهان کنیم، اما بقیه آن را نباید ببینند.

  2. سمبل‌های سیستمی زیادی وجود دارند که توسط جاوااسکریپت استفاده می‌شوند و با Symbol.* قابل دسترس هستند. ما می‌توانیم از آنها برای تغییر بعضی از رفتارهای درون زبان استفاده کنیم. برای مثال، بعدا در همین آموزش ما از Symbol.iterator برای قابل تکرارها (iterables)، Symbol.toPrimitive برای ایجاد تبدیل شیء به مقدار اصلی و غیره استفاده خواهیم کرد.

از لحاظ فنی، سمبل‌ها 100% مخفی نیستند. یک متد درون‌ساخت Object.getOwnPropertySymbols(obj) وجود دارد که به ما امکان دریافت تمام سمبل‌ها را می‌دهد. همچنین یک متد به نام Reflect.ownKeys(obj) وجود دارد که تمام کلیدهای یک شیء که شامل کلیدهای سمبلی هم هست را برمی‌گرداند. پس آنها در واقع پنهان نیستند. اما اکثر کتابخانه‌ها، توابع درون‌ساخت و ساختارهای سینتکس از این متدها استفاده نمی‌کنند.

نقشه آموزش