۲۵ ژوئیه ۲۰۲۲

مقداردهیِ تجزیه‌کننده‌ی ساختار

دو ساختار داده پر استفاده در جاوااسکریپت Object و Array هستند.

  • شیءها به ما این امکان را می‌دهند تا چیزی بسازیم که المان‌های داده را به واسطه کلید ذخیره کند.
  • آرایه‌ها به ما امکان جمع‌آوری المان‌های داده را در لیستی مرتب می‌دهند.

اگرچه، زمانی که ما آن‌ها را به تابع می‌دهیم، ممکن است که نیازی به کل یک شیء/آرایه نباشد. شاید تنها قطعه‌های تکی نیاز باشد.

مقداردهیِ تجزیه‌کننده‌ی ساختار (Destructuring assignment) یک سینتکس خاص است که به ما امکان می‌دهد تا آرایه‌ها یا شیءها را درون چند متغیر «پخش کنیم» چون بعضی اوقات این موضوع کار را راحت‌تر می‌کند.

تخریب ساختار همچنین با تابع‌های پیچیده که تعداد زیادی پارامتر، مقدارهای پیش‌فرض و… دارند هم به خوبی کار می‌کند. به زودی آن را خواهیم دید.

تجزیه ساختار آرایه

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

// ما یک آرایه شامل نام و نام خانوادگی داریم
let arr = ["John", "Smith"]

// مقداردهی تجزیه‌کننده‌ی ساختار
// را قرار می‌دهد firstName = arr[0]
// surname = arr[1] و
let [firstName, surname] = arr;

alert(firstName); // John
alert(surname);  // Smith

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

زمانی که با split یا متدهای دیگری که آرایه برمی‌گردانند عالی بنظر می‌رسد:

let [firstName, surname] = "John Smith".split(' ');
alert(firstName); // John
alert(surname);  // Smith

همانطور که می‌بینید، سینتکس ساده است. البته چند چیز ویژه در جزییات خود دارد. بیایید برای فهمیدن بهتر آن، مثال‌های بیشتری ببینیم.

عبارت «تجزیه‌کننده‌ی ساختار» به معنی «مخرب» نیست.

این سینتکس «مقداردهی تجزیه‌کننده‌ی ساختار» نامیده می‌شود چون با کپی کردن المان‌ها در چند متغیر «ساختار را تغییر می‌دهد». اما خود آرایه تغییر نمی‌کند.

فقط یک راه کوتاه‌تر برای نوشتن است:

// let [firstName, surname] = arr;
let firstName = arr[0];
let surname = arr[1];
از طریق استفاده از کاما المان‌ها را نادیده بگیرید

المان‌هایی که که نمی‌خواهیم را می‌توان با یک کامای اضافه دور انداخت:

// به المان دوم نیاز نداریم
let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];

alert( title ); // Consul

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

با هر حلقه‌پذیری در سمت راست کار می‌کند

…در واقع، ما می‌توانیم آن را با هر حلقه‌پذیری استفاده کنیم، نه فقط آرایه‌ها:

let [a, b, c] = "abc"; // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3]);

این کار می‌کند چون از درون، یک مقداردهی تجزیه‌کننده‌ی ساختار با حلقه زدن در مقدار سمت راست کار می‌کند. برای فراخوانی for..of در مقدار سمت راست = و تخصیص دادن مقدارها، به نوعی خوش سینتکس است.

در سمت چپ به هر چیزی تخصیص دهید

ما می‌توانیم از «قابل مقداردهی‌ها» در سمت چپ استفاده کنیم.

برای مثال، یک ویژگی شیء:

let user = {};
[user.name, user.surname] = "John Smith".split(' ');

alert(user.name); // John
alert(user.surname); // Smith
حلقه زدن با .entries()

در فصل قبل ما متد Object.entries(obj) را دیدیم.

می‌توانیم آن را با تجزیه‌کننده‌ی ساختار برای حلقه زدن در کلیدها و مقدارهای یک شیء استفاده کنیم:

let user = {
  name: "John",
  age: 30
};

// حلقه زدن در کلیدها و مقدارها
for (let [key, value] of Object.entries(user)) {
  alert(`${key}:${value}`); // name:John, then age:30
}

کد مشابه برای Map ساده‌تر است چون حلقه‌پذیر است:

let user = new Map();
user.set("name", "John");
user.set("age", "30");

// حلقه می‌سازد، برای تجزیه‌کننده‌ی ساختار بسیار مناسب است [key, value] با جفت‌های Map
for (let [key, value] of user) {
  alert(`${key}:${value}`); // age:30 سپس name:John
}
ترفند مبادله متغیرها

یک ترفند معروف برای مبادله مقدارهای دو متغیر با استفاده از مقداردهی تجزیه‌کننده‌ی ساختار وجود دارد:

let guest = "Jane";
let admin = "Pete";

// guest=Pete، admin=Jane بیایید مقدارها را مبادله کنیم: کاری کنیم که
[guest, admin] = [admin, guest];

alert(`${guest} ${admin}`); // Pete Jane (!با موفقیت مبادله شد)

اینجا ما یک آرایه موقتی از دو آرایه می‌سازیم و بلافاصله ساختار آن را نسبت به ترتیب مبادله تجزیه می‌کنیم.

می‌توانیم از این راه بیشتر از دو متغیر را مبادله کنیم.

رِست ‘…’

معمولا، اگر آرایه طولانی‌تر از لیست سمت چپ باشد، المان‌های «اضافی» از قلم می‌افتند.

برای مثال، اینجا فقط دو المان دریافت می‌شود و بقیه نادیده گرفته می‌شوند:

let [name1, name2] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];

alert(name1); // Julius
alert(name2); // Caesar
// المان‌های بعدی جایی ذخیره نمی‌شوند

اگر بخواهیم تمام المان‌های بعدی را دریافت کنیم – می‌توانیم یک پارامتر دیگر اضافه کنیم که با استفاده از "..." «بقیه المان‌ها» را دریافت کند:

let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];

// آرایه‌ای از المان‌ها است که از المان سوم شروع می‌شود rest
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2

مقدار rest آرایه‌ای از المان‌های باقی مانده است.

می‌توانیم از هر اسم دیگری به جای rest برای متغیر استفاده کنیم، فقط مطمئن شوید که قبل از آن سه نقطه وجود دارد و در انتهای مقداردهی تجزیه‌کننده‌ی ساختار قرار می‌گیرد.

let [name1, name2, ...titles] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
// titles = ["Consul", "of the Roman Republic"] حالا داریم

مقدارهای پیش‌فرض

اگر آرایه از لیست متغیرهای سمت چپ کوتاه‌تر باشد، هیچ اروری ایجاد نمی‌شود. مقدارهای ناموجود undefined در نظر گرفته می‌شوند:

let [firstName, surname] = [];

alert(firstName); // undefined
alert(surname); // undefined

ما یک مقدار «پیش‌فرض» بخواهیم که جایگزین مقدار ناموجود شود، می‌توانیم با استفاده از = آن را فراهم کنیم:

// مقدارهای پیش‌فرض
let [name = "Guest", surname = "Anonymous"] = ["Julius"];

alert(name);    // Julius (از آرایه)
alert(surname); // Anonymous (مقدار پیش‌فرض استفاده شد)

مقدارهای پیش‌فرض می‌توانند عبارات پیچیده‌تر یا حتی فراخوانی تابع باشند. آنها فقط زمانی که مقدار وجود نداشته باشد ارزیابی می‌شوند.

برای مثال، اینجا ما از تابع prompt برای دو مقدار پیش‌فرض استفاده کردیم:

// اجرا می‌شود prompt تابع suname فقط برای
let [name = prompt('name?'), surname = prompt('surname?')] = ["Julius"];

alert(name);    // Julius (از آرایه)
alert(surname); // دریافت کند prompt هر چیزی که

لطفا در نظر داشته باشید: prompt فقط برای مقدار ناموجود (surname) اجرا می‌شود.

تجزیه ساختار شیء

مقداردهی تجزیه‌کننده‌ی ساختار با شیءها هم کار می‌کند.

سینتکس ساده آن اینگونه است:

let {var1, var2} = {var1:…, var2:…}

ما باید یک شیء موجود که می‌خواهیم آن را در چند متغیر پخش کنیم در سمت راست داشته باشیم. سمت چپ شامل یک «الگوی» شیء مانند برای ویژگی‌های متناظر می‌شود. در ساده‌ترین حالت، یک لیست از اسم‌های متغیر در {...} است.

برای مثال:

let options = {
  title: "Menu",
  width: 100,
  height: 200
};

let {title, width, height} = options;

alert(title);  // Menu
alert(width);  // 100
alert(height); // 200

ویژگی‌های options.title، options.width و options.height به متغیرهای متناظر تخصیص داده شده‌اند.

ترتیب مهم نیست. اینگونه هم کار می‌کند:

// تغییر دادیم let {...} ترتیب را در
let {height, width, title} = { title: "Menu", height: 200, width: 100 }

الگوی سمت راست ممکن است پیچیده‌تر باشد و رابطه بین ویژگی‌ها و متغیرها را تعیین کند.

اگر ما بخواهیم که یک ویژگی را به یک متغیر با نامی دیگر تخصیص بدهیم، برای مثل، کاری کنیم که options.width در متغیری به نام w ذخیره شود، می‌توانیم اسم متغیر یا با استفاده از دو نقطه تنظیم کنیم:

let options = {
  title: "Menu",
  width: 100,
  height: 200
};

// { sourceProperty: targetVariable }
let {width: w, height: h, title} = options;

// width -> w
// height -> h
// title -> title

alert(title);  // Menu
alert(w);      // 100
alert(h);      // 200

دو نقطه نشان می‌دهد که «چه چیزی : کجا ذخیره می‌شود». در مثال بالا ویژگی width درون w، ویژگی height درون h ذخیره و title به اسمی مشابه با خودش تخصیص داده می‌شود.

برای ویژگی‌هایی که ممکن است موجود نباشند ما می‌توانیم با استفاده از "=" مقدار پیش‌فرض قرار دهیم، مثلا اینگونه:

let options = {
  title: "Menu"
};

let {width = 100, height = 200, title} = options;

alert(title);  // Menu
alert(width);  // 100
alert(height); // 200

درست مانند آرایه‌ها یا پارامترهای تابع، مقدارهای پیش‌فرض می‌توانند هر عبارتی یا حتی فراخوانی تابع باشند. اگر مقدار موجود نباشد، آنها ارزیابی می‌شوند.

در کد زیر prompt برای width درخواست می‌کند اما برای title نه:

let options = {
  title: "Menu"
};

let {width = prompt("width?"), title = prompt("title?")} = options;

alert(title);  // Menu
alert(width);  // (باشد prompt هر چیزی که نتیجه)

همچنین ما می‌توانیم دو نقطه و برابر قرار دادن را با هم ترکیب کنیم:

let options = {
  title: "Menu"
};

let {width: w = 100, height: h = 200, title} = options;

alert(title);  // Menu
alert(w);      // 100
alert(h);      // 200

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

let options = {
  title: "Menu",
  width: 100,
  height: 200
};

// را به عنوان متغیر استخراج کنید title فقط
let { title } = options;

alert(title); // Menu

الگوی رست “…”

اگر شیء بیشتر از تعدادی که ما متغیر داریم ویژگی داشته باشد چه اتفاقی می‌افتد؟ آیا می‌توانیم بعضی از آنها را دریافت کنیم و «بقیه» را جایی دیگر ذخیره کنیم؟

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

اینگونه بنظر می‌رسد:

let options = {
  title: "Menu",
  height: 200,
  width: 100
};

// title = title ویژگی‌ای به اسم
// rest = شیءای شامل بقیه ویژگی‌ها
let {title, ...rest} = options;

// title="Menu" ،rest={height: 200, width: 100} حالا داریم
alert(rest.height);  // 200
alert(rest.width);   // 100
اگر let وجود نداشته باشد گرفتار می‌شویم

در مثال‌های بالا متغیرها دقیقا درون مقداردهی تعریف شدند: let {…} = {…}. قطعا ما می‌توانستیم بدون let، از متغیرهای موجود هم استفاده کنیم. اما یک مشکل وجود دارد.

این کار نمی‌کند:

let title, width, height;

// در این خط ارور می‌گیریم
{title, width, height} = {title: "Menu", width: 200, height: 100};

مشکل اینجاست که جاوااسکریپت در کد اصلی (نه درون یک عبارت دیگر) با {...} به عنوان یک بلوک کد رفتار می‌کند. چنین بلوک‌های کدی می‌توانند برای گروه‌بندی دستورات استفاده شوند، مثلا اینگونه:

{
  // یک بلوک کد
  let message = "سلام";
  // ...
  alert( message );
}

پس اینجا جاوااسکریپت فرض می‌کند که ما یک بلوک کد داریم و به همین دلیل است که ارور ایجاد می‌شود. به جای آن ما تجزیه ساختار می‌خواهیم.

برای اینکه به جاوااسکریپت نشان دهیم که این یک بلوک کد نیست، می‌توانیم عبارت را درون پرانتز (...) قرار دهیم:

let title, width, height;

// الان مناسب است
({title, width, height} = {title: "Menu", width: 200, height: 100});

alert( title ); // Menu

تجزیه ساختار تودرتو

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

در کد زیر options یک شیء دیگری درون ویژگی size و یک آرایه درون ویژگی items دارد. الگوی سمت چپ مقداردهی ساختار یکسانی برای استخراج مقدار از آنها را دارد:

let options = {
  size: {
    width: 100,
    height: 200
  },
  items: ["Cake", "Donut"],
  extra: true
};

// مقداردهی تجزیه‌کننده‌ی ساختار برای واضح بودن در چند خط قرار گرفته است
let {
  size: { // را اینجا قرار دهید size
    width,
    height
  },
  items: [item1, item2], // را اینجا تخصیص دهید items
  title = "Menu" // در شیء وجود ندارد (مقدار پیش‌فرض استفاده می‌شود)
} = options;

alert(title);  // Menu
alert(width);  // 100
alert(height); // 200
alert(item1);  // Cake
alert(item2);  // Donut

تمام ویژگی‌های شیء options به جز extra که در سمت چپ وجود ندارد، به متغیرهای متناظر خود تخصیص داده شده‌اند:

سرانجام، ما width، height، item1، item2 و متغیر title را از مقدار پیش‌فرض داریم.

در نظر داشته باشید که هیچ متغیری برای size و items وجود ندارد چون به جای آنها، ما محتوایشان را می‌خواهیم.

پارامترهای هوشمند تابع

بعضی اوقات پیش می‌آید که یک تابع پارامترهای زیادی داشته باشد که اکثر آنها الزامی نیستند. خصوصا برای رابط کاربری این اتفاق می‌افتد. یک تابع را تصور کنید که یک منو ایجاد می‌کند. این منو ممکن است دارای طول(width)، ارتفاع(height)، عنوان(title)، لیستی از کالاها و غیره باشد.

این یک راه برای نوشتن چنین تابعی است:

function showMenu(title = "Untitled", width = 200, height = 100, items = []) {
  // ...
}

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

مثلا اینگونه؟

// می‌گذاریم undefined جایی که مقدارهای پیش‌فرض مشکلی ندارند
showMenu("My Menu", undefined, undefined, ["Item1", "Item2"])

این قشنگ نیست. و زمانی که با پارامترهای بیشتری سر و کار داشته باشیم خوانایی کمتری دارد.

تجزیه ساختار به نجات ما می‌آید!

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

// ما شیء را به تابع می‌دهیم
let options = {
  title: "My menu",
  items: ["Item1", "Item2"]
};

// و تابع بلافاصله آن را در متغیرها پراکنده می‌کند...
function showMenu({title = "Untitled", width = 200, height = 100, items = []}) {
  // ،از شیء گرفته شده‌اند title ،items
  // ،مقدارهای پیش‌فرض استفاده شدند width ،height
  alert( `${title} ${width} ${height}` ); // My Menu 200 100
  alert( items ); // Item1, Item2
}

showMenu(options);

همچنین می‌توانیم از تجزیه ساختار پیچیده‌تری همراه با شیءهای تودرتو و طراحی دو نقطه‌ای استفاده کنیم:

let options = {
  title: "My menu",
  items: ["Item1", "Item2"]
};

function showMenu({
  title = "Untitled",
  width: w = 100,  // می‌رود w درون width
  height: h = 200, // می‌رود h درون height
  items: [item1, item2] // می‌رود item2 و دومی درون item1 درون items اولین المان
}) {
  alert( `${title} ${w} ${h}` ); // My Menu 100 200
  alert( item1 ); // Item1
  alert( item2 ); // Item2
}

showMenu(options);

سینتکس کامل برای یک مقداردهی تجزیه‌کننده‌ی ساختار یکسان است:

function({
  incomingProperty: varName = defaultValue
  ...
})

سپس به صورت پیش‌فرض، برای یک شیء شامل پارامترها، یک متغیر varName (اسم متغیر) برای ویژگی incomingProperty (ویژگی ورودی)، همراه با defaultValue وجود خواهد داشت.

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

showMenu({}); // خب، تمام مقدارها پیش‌فرض هستند

showMenu(); // این ارور خواهد داد

ما می‌توانیم با قرار دادن {} به عنوان مقدار پیش‌فرض برای تمام شیء شامل پارامترها این مشکل را رفع کنیم:

function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
  alert( `${title} ${width} ${height}` );
}

showMenu(); // Menu 100 200

در کد بالا، تمام شیء آرگومان‌ها به صورت پیش‌فرض {} است پس همیشه چیزی برای تجزیه ساختار وجود دارد.

خلاصه

  • مقداردهی تجزیه‌کننده‌ی ساختار به ما اجازه می‌دهد تا بلافاصله یک شیء یا آرایه را روی بسیاری از متغیرها ترسیم کنیم.

  • سینتکس کامل شیء:

    let {prop : varName = default, ...rest} = object

    این به این معنی است که ویژگی prop باید درون متغیر varName برود و اگر چنین ویژگی‌ای وجود نداشت، سپس مقدار default باید استفاده شود.

    ویژگی‌های شیء که نقشی برای آنها وجود ندارد درون شیء rest کپی می‌شوند.

  • سینتکس کامل آرایه:

    let [item1 = default, item2, ...rest] = array

    المان اول درون item1 می‎رود؛ المان دوم درون item2 می‌شود، تمام المان‌های باقی مانده آرایه rest را تشکیل می‌دهند.

  • استخراج داده از آرایه/شیءهای تودرتو هم ممکن است، برای اینکار سمت چپ باید ساختار یکسانی با سمت راست داشته باشد.

تمارین

اهمیت: 5

ما یک شیء داریم:

let user = {
  name: "John",
  years: 30
};

مقداردهی تجزیه‌کننده‌ی ساختاری بنویسید که این‌ها را بخواند:

  • ویژگی name درون متغیر name.
  • ویژگی years درون متغیر age.
  • ویژگی isAdmin درون متغیر isAdmin (اگر این ویژگی ناموجود بود، مقدار false باشد)

یک مثال برای مقدارها بعد از مقداردهی شما:

let user = { name: "John", years: 30 };

// :کد شما در سمت چپ
// ... = user

alert( name ); // John
alert( age ); // 30
alert( isAdmin ); // false
let user = {
  name: "John",
  years: 30
};

let {name, years: age, isAdmin = false} = user;

alert( name ); // John
alert( age ); // 30
alert( isAdmin ); // false
اهمیت: 5

یک شیء salaries داریم:

let salaries = {
  "John": 100,
  "Pete": 300,
  "Mary": 250
};

تابع topSalary(salaries) بسازید که اسم شخصی با بیشترین حقوق را برگرداند.

  • اگر salaries خالی بود، باید null برگرداند.
  • اگر چند شخص با بیشترین حقوق وجود داشتند، یکی از آنها را برگرداند.

پی‌نوشت: از Object.entries و تجزیه ساختار برای حلقه زدن در جفت‌های کلید/مقدار استفاده کنید.

باز کردن یک sandbox همراه با تست‌ها.

function topSalary(salaries) {

  let maxSalary = 0;
  let maxName = null;

  for(const [name, salary] of Object.entries(salaries)) {
    if (maxSalary < salary) {
      maxSalary = salary;
      maxName = name;
    }
  }

  return maxName;
}

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

نقشه آموزش