في جافاسكربت، الكائنات والمصفوفات هي أكثر بنى البيانات المستعملة. تُتيح لنا الكائنات إنشاء كيان واحد يُخزّن عناصر البيانات حسب مفاتيحها، وتُتيح لنا المصفوفات بجمع مختلف عناصر البيانات في تجميعة مرتّبة (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
الناقصة يسار عبارة الإسناد) إلى المتغيرات المقابلة لها:
وفي النهاية يكون لدينا المتغيّرات 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
اقرأ أيضًا
- المقال التالي: النوع Date: تمثيل التاريخ والوقت
- المقال السابق: مفاتيح الكائنات وقيمها ومدخلاتها
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.