دليل تعلم جافاسكربت الإسناد بالتفكيك (Destructuring assignment) في جافاسكربت


صفا الفليج

في جافاسكربت، الكائنات والمصفوفات هي أكثر بنى البيانات المستعملة. تُتيح لنا الكائنات إنشاء كيان واحد يُخزّن عناصر البيانات حسب مفاتيحها، وتُتيح لنا المصفوفات بجمع مختلف عناصر البيانات في تجميعة مرتّبة (ordered collection).

ولكن حين نُمرّرها هذه الكائنات والمصفوفات إلى دالة، غالبًا ما لا نريد كامل الكائن/المصفوفة، بل بعضًا منها لا أكثر.

صياغة الإسناد بالتفكيك (Destructuring assignment) هي صياغة خاصّة تُتيح لنا ”فكّ“ المصفوفات أو الكائنات إلى مجموعة من المتغيرات إذ تكون أحيانًا أكثر منطقية. يفيدنا التفكيك أيضًا مع الدوال المعقّدة التي تحتوي على مُعاملات كثيرة وقيم مبدئية وغيرها وغيرها.

تفكيك المصفوفات

إليك مثال عن تفكيك مصفوفة إلى مجموعة من المتغيرات:

// معنا مصفوفة فيها اسم الشخص واسم عائلته
let arr = ["Ilya", "Kantor"]

// يضبط الإسناد بالتفكيك
// ‫هذه firstName = arr[0]‎
// ‫وهذه surname = arr[1]‎
let [firstName, surname] = arr;

alert(firstName); // Ilya
alert(surname);  // Kantor

يمكننا الآن العمل مع تلك المتغيرات عوض عناصر المصفوفة. وما إن تجمع تابِع split وغيرها من توابِع تُعيد مصفوفات، سترى بريق هذا التفكيك يتألق:

let [firstName, surname] = "Ilya Kantor".split(' ');

”التفكيك“ (Destructuring) لا يعني ”التكسير“ (destructive)

نُسمّيه "الإسناد بالتفكيك" (destructuring assignment) لأنّه "يفكّك" العناصر بنسخها إلى متغيرات. أمّا المصفوفة نفسها فتبقى دون تعديل.

كتابة هذه الشيفرة أسهل من تلك الطويلة (ندعها لك تتخيّلها):

// 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]);

أسنِدها إلى ما تريد على اليسار

يمكن أن نستعمل أيّ متغيّر يمكن إسناده على الجانب الأيسر من سطر الإسناد. لاحظ مثلًا إسناد خاصية لكائن:

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

alert(user.name); // Ilya

المرور على العناصر عبر ‎.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
}

…وذات الأمر للخارطة:

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

for (let [key, value] of user) {
  alert(`${key}:${value}`); // name:John, then age:30
}

الباقي ”…“

لو أردنا أخذ القيم الأولى إضافةً إلى كل ما يليها، فنُضيف مُعاملًا آخر يجلب ”الباقي“ باستعمال ثلاث نقاط "...":

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

alert(name1); // Julius
alert(name2); // Caesar

// ‫انتبه أنّ المتغير rest مصفوفة.
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2

ستكون قيمة المتغير rest مصفوفةً فيها عناصر الباقية في المصفوفة الأولى. يمكننا استعمال أيّ اسم آخر بدل rest، المهم أن يكون قبله ثلاث نقاط ويكون الأخير في جملة الإسناد بالتفكيك.

القيم المبدئية

لو كانت القيم في المصفوفة أقلّ من تلك في جملة الإسناد فلن يحدث أيّ خطأ. يُعدّ المحرّك القيم ”الغائبة“ غير معرّفة:

let [firstName, surname] = [];

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

لو أردنا قيمة مبدئية تعوّض تلك الناقصة فيمكننا تقديمها باستعمال =:

// القيم المبدئية
let [name = "Guest", surname = "Anonymous"] = ["Julius"];

alert(name);    // ‫Julius (من المصفوفة)
alert(surname); // ‫Anonymous (المبدئي)

يمكن أن تكون القيم المبدئية تعابيرَ معقّدة أو استدعاءات دوال حتّى. لن يقدّر ناتجها المحرّك إلّا لو لم تمرّر القيم تلك. فمثلًا يمكننا استعمال الدالة promot لأخذ قيمتين مبدئيتين. أمّا هنا فستسأل عن القيمة الناقصة فقط:

// ‫لا تطلب إلا اسم العائلة surname
let [name = prompt('name?'), surname = prompt('surname?')] = ["Julius"];

alert(name);    // ‫Julius (نأخذه من المصفوفة)
alert(surname); // ‫نحترم هنا ما يقول promot

تكفيك الكائنات

الإسناد بالتفكيك يدعم أيضًا الكائنات. هذه صياغته الأساس:

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 }

يمكن أن يكون النمط على اليسار معقّدًا أكثر ومُحدّدًا فيحدّد طريقة ترابط الخاصيات بالمتغيرات عبر الخارطة (mapping).

لو أردنا إسناد خاصية إلى متغير له اسم آخر فعلينا استعمال النقطتين الرأسيتين لذلك (مثلًا 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 كما هي title.

يمكننا هنا أيضًا وضع قيمة مبدئية للخاصيات الناقصة باستعمال "=" هكذا:

let options = {
  title: "Menu"
};

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

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

كما يمكن أن تكون هذه القيم المبدئية أيّة تعابير أو استدعاءات دوال كما مقابلاتها في المصفوفات ومُعاملات الدوال، ولن يقدّر المحرّك قيمتها إلّا لو لم تقدّم قيمة للدالة.

في الشيفرة أدناه، تطلب الدالة promot قيمة width ولا تطلب قيمة title:

let options = {
  title: "Menu"
};

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

alert(title);  // Menu
alert(width);  // (promot هنا نحترم أيضًا ما يقول)

يمكننا أيضًا جمع النقطتان الرأسيتان والقيم المبدئية:

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

نمط الباقي ”…“

ماذا لو كان للكائن خاصيات أكثر من المتغيرات التي لدينا؟ هل يمكننا أخذها وإسنادها في متغيّر ”rest“ أيضًا؟

أجل يمكننا استعمال نمط الباقي تمامًا مثل المصفوفات. بعض المتصفحات القديمة لا تدعمه (مثل إنترنت إكسبلورر، استعمل Babel لترقيعه polyfill، أي لتعويض نقص الدعم)، إلّا أن الحديثة تدعمه.

هكذا نفعلها:

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};

المشكلة هي أنّ جافاسكربت تتعامل مع {...} في سياق الشيفرة الأساس (أي ليس داخل تعبير آخر) على أنّها بنية شيفرة (Code Block). يمكن استعمال بنى الشيفرة هذه لجمع التعليمات البرمجية، هكذا:

{
  // بنية شيفرة
  let message = "Hello";
  // ...
  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: { // هنا يكون المقاس
    width,
    height
  },
  items: [item1, item2], // وهنا نضع العناصر
  title = "Menu" // ليست موجودة في الكائن (ستُستعمل القيمة المبدئية)‏
} = options;

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

هكذا تُسند كلّ خاصيات options (عدا extra الناقصة يسار عبارة الإسناد) إلى المتغيرات المقابلة لها:

destructuring-complex.png

وفي النهاية يكون لدينا المتغيّرات width وheight وitem1 وitem2 وtitle من تلك القيمة المبدئية. لاحظ ألّا وجود لمتغيّرات تنسخ size وitems إذ ما نريد هو محتواها لا هي.

مُعاملات الدوال الذكية

أحيانًا وأنت تعمل تجد نفسك تكتب دالة لها مُعاملات كثيرة وأغلبها اختيارية. يحدث هذا غالبًا مع دوال واجهات المستخدم. عُدّ أنّ لديك دالة تُنشئ قائمة، وللقائمة عَرض وارتفاع وعنوان وقائمة عناصر وغيرها.

هكذا تصنع تلك الدالة بالأسلوب الخطأ:

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

تكمن المشكلة (في الحياة الواقعية) في تذكّر ترتيب تلك الوُسطاء. صحيح أنّ بيئات التطوير تفيدنا هنا عادةً -خصوصًا لو كان المشروع موثّق توثيقًا ممتازًا- ولكن مع ذلك فالمشكلة الأخرى هي طريقة استدعاء الدالة لو كانت كلّ مُعاملاتها المبدئية مناسبة لنا.

نستدعيها هكذا؟

// نضع ‫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 – هذه من options
  // ‫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,  // نضع ‫width في w
  height: h = 200, // ‫ونضع height في h
  items: [item1, item2] 
  // ‫أوّل عنصر في items يصير item1، وثاني عنصر يصير item2
}) {

  alert( `${title} ${w} ${h}` ); // My Menu 100 200
  alert( item1 ); // Item1
  alert( item2 ); // Item2
}

showMenu(options);

صياغة الدالة الكاملة تتطابق مع صياغة الإسناد بالتفكيك:

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

وحينها متى ما تمرّر كائن على أساس أنّه مُعامل، نضع الخاصية incomingProperty في المتغير varName وقيمته المبدئية هي 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 والإسناد بالتفكيك للمرور على أزواج ”مفاتيح/قيم“.

الحل

function topSalary(salaries) {

  let max = 0;
  let maxName = null;

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

  return maxName;
}

ترجمة -وبتصرف- للفصل Destructuring assignment من كتاب The JavaScript language

اقرأ أيضًا





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن