٢٥ مارس ٢٠٢١

المُعاملات «البقية» ومُعامل التوزيع

تتوقّع العديد من دوال جافا سكريبت المضمّنة في اللغة عددًا من الوُسطاء لا ينتهي.

مثال:

  • ‎Math.max(arg1, arg2, ..., argN)‎ – يُعيد أكبر وسيط من الوُسطاء.
  • ‎Object.assign(dest, src1, ..., srcN)‎ – ينسخ الخصائص من ‎src1..N‎ إلى ‎dest‎.
  • …وهكذا.

سنتعلّم في هذا الفصل كيف نفعل ذلك أيضًا. كما وكيف نمرّر المصفوفات إلى هذه الدوال على أنّها مُعاملات.

المُعاملات «البقية» ...

يمكن أن ننادي الدالة بأيّ عدد من الوُسطاء كيفما كانت معرّفة الدالة.

هكذا:

function sum(a, b) {
  return a + b;
}

alert( sum(1, 2, 3, 4, 5) );

لن ترى أيّ خطأ بسبب تلك الوُسطاء «الزائدة». ولكن طبعًا فالنتيجة لن تأخذ بالحسبان إلا أوّل اثنين.

يمكن تضمين بقية المُعاملات في تعريف الدالة باستعمال الثلاث نقاط ‎...‎ ثمّ اسم المصفوفة التي ستحتويهم. تعني تلك النقط حرفيًا «اجمع المُعاملات الباقية في مصفوفة».

فمثلًا لجمع كلّ الوُسطاء في المصفوفة ‎args‎:

function sumAll(...args) { // ‫اسم المصفوفة هو args
  let sum = 0;

  for (let arg of args) sum += arg;

  return sum;
}

alert( sumAll(1) ); // 1
alert( sumAll(1, 2) ); // 3
alert( sumAll(1, 2, 3) ); // 6

يمكن لو أردنا أن نأخذ المُعاملات الأولى في متغيّرات ونجمع البقية فقط.

هنا نأخذ الوسيطين الأوليين في متغيرات والباقي نرميه في المصفوفة ‎titles‎:

function showName(firstName, lastName, ...titles) {
  alert( firstName + ' ' + lastName ); // Julius Caesar

  // ‫الباقي نضعه في مصفوفة الأسماء titles
  // ‫مثلًا titles = ["Consul", "Imperator"]‎
  alert( titles[0] ); // Consul
  alert( titles[1] ); // Imperator
  alert( titles.length ); // 2
}

showName("Julius", "Caesar", "Consul", "Imperator");
يجب أن تُترك المُعاملات البقية إلى النهاية

تجمع المُعاملات البقية كلّ الوُسطاء التي بقيت. وبهذا فالآتي ليس منطقيًا وسيتسبّب بخطأ:

function f(arg1, ...rest, arg2) { // ‫الوسيط arg2 بعد ...البقية؟!
  // خطأ
}

يجب أن يكون ‎...rest‎ الأخير دومًا.

متغيّر الوُسطاء arguments

هناك كائن آخر شبيه بالمصفوفات يُدعى ‎arguments‎ ويحتوي على كلّ الوُسطاء حسب ترتيب فهارسها.

مثال:

function showName() {
  alert( arguments.length );
  alert( arguments[0] );
  alert( arguments[1] );

  // المصفوفة مُتعدَّدة
  // for(let arg of arguments) alert(arg);
}

// ‫تعرض: 2, Julius, Caesar
showName("Julius", "Caesar");

// ‫تعرض: 1, Ilya, undefined (ما من مُعطى ثانٍ)
showName("Ilya");

قديمًا لم تكن المُعاملات البقية موجودة في اللغة ولم يكن لدينا سوى استعمال ‎arguments‎ لنجلب كلّ مُعاملات الدالة. وما زالت تعمل الطريقة إلى يومنا هذا ويمكن أن تراها في الشيفرات القديمة.

ولكن السلبية هنا هي أنّ ‎arguments‎ ليست مصفوفة (على الرغم من أنّها شبيهة بالمصفوفات ومُتعدّدة). بهذا لا تدعم توابِع المصفوفات فلا ينفع أن نستدعي عليها ‎arguments.map(...)‎ مثلًا.

كما وأنّها تحتوي على كل الوُسطاء دومًا. لا يمكن أن نأخذ منها ما نريد كما نفعل مع المُعاملات البقية.

لهذا متى ما احتجنا إلى ميزة كهذه، فالأفضل استعمال المُعاملات البقية بدلًا من ‎arguments‎.

ليس للدوال السهمية ‎"arguments"‎ لو حاولت الوصول إلى كائن الوُسطاء ‎arguments‎ من داخل الدالة السهمية، فستستلم الناتج من الدالة «الطبيعية» الخارجية. إليك مثالًا:

function f() {
  let showArg = () => alert(arguments[0]);
  showArg();
}

f(1); // 1

كما نذكر فليس للدوال السهمية قيمة ‎this‎ تخصّها، أمّا الآن صرنا نعلم بأنّ ليس لها كائن ‎arguments‎ أيضًا.

مُعامل التوزيع

رأينا كيف نأخذ مصفوفة من قائمة من المُعطيات.

ولكن ماذا لو أردنا العكس من ذلك؟

فمثلًا لنقل أردنا استعمال الدالة المبنية في اللغة Math.max والتي تُعيد أكبر عدد من القائمة:

alert( Math.max(3, 5, 1) ); // 5

لنقل أنّ لدينا المصفوفة ‎[3, 5, 1]‎. كيف نستدعي ‎Math.max‎ عليها؟

لا ينفع تمريرها «كما هي» لأنّ ‎Math.max‎ يتوقّع قائمةً بالوُسطاء العددية لا مصفوفة واحدة:

let arr = [3, 5, 1];

alert( Math.max(arr) ); // NaN

وطبعًا لا يمكن أن نفكّ عناصر القائمة يدويًا في الشيفرة ‎Math.max(arr[0], arr[1], arr[2])‎ لأنّنا في حالات لا نعرف كم من عنصر هناك أصلًا. وما إن يتنفّذ السكربت يمكن أن يكون فيه أكبر مما كتبناه أو حتّى لا شيء أصلًا، وسنحصد لاحقًا ما جنته هذه الشيفرة.

عاش مُنقذنا مُعامل التوزيع! عاش عاش عاش! من بعيد نراه مشابهًا تمامًا للمُعاملات البقية، كما ويستعمل ‎...‎، إلّا أنّ وظيفته هي العكس تمامًا.

فحين نستعمل ‎‎...arr‎ في استدعاء الدالة، «يتوسّع» الكائن المُتعدَّد ‎...arr‎ إلى قائمة من الوُسطاء.

فمثلًا نعود إلى ‎Math.max‎:

let arr = [3, 5, 1];

// ‫5 (يحوّل التوزيع المصفوفة إلى قائمة من الوُسطاء)
alert( Math.max(...arr) );

يمكن أيضًا أن نمرّر أكثر من مُتعدَّد واحد بهذه الطريقة:

let arr1 = [1, -2, 3, 4];
let arr2 = [8, 3, -8, 1];

alert( Math.max(...arr1, ...arr2) ); // 8

أو حتّى ندمج مُعامل التوزيع مع القيم العادية:

let arr1 = [1, -2, 3, 4];
let arr2 = [8, 3, -8, 1];

alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25

كما يمكن أن نستعمل مُعامل التوزيعة لدمج المصفوفات:

let arr = [3, 5, 1];
let arr2 = [8, 9, 15];

let merged = [0, ...arr, 2, ...arr2];

alert(merged); // ‫0,3,5,1,2,8,9,15 (0 ثمّ arr ثمّ 2 ثمّ arr2)

استعملنا في الأمثلة أعلاه مصفوفة لنشرح مُعامل التوزيع، إلّا أنّ المُتعدَّدات أيًا كانت تنفع أيضًا.

فمثلًا نستعمل هنا مُعامل التوزيع لنحوّل السلسلة النصية إلى مصفوفة محارف:

let str = "Hello";

alert( [...str] ); // H,e,l,l,o

يستعمل مُعامل التوزيع هذا داخليًا المُعدِّدات لجمع العناصر، كما تفعل حلقة ‎for..of‎.

لذا لو استلمت ‎for..of‎ سلسلةً نصيّة فتُعيد لنا المحارف وتصير ‎‎...str‎ بالقيمة ‎"H","e","l","l","o"‎. وهكذا تُمرّر قائمة المحارف إلى مُهيّئ المصفوفة ‎[...str]‎.

يمكننا أيضًا لهذه المهمة استعمال ‎Array.from‎ إذ أنّه يحوّل المُتعدَّد (مثل السلاسل النصية) إلى مصفوفة:

let str = "Hello";

// ‫يُحوّل Array.from المُتعدَّد إلى مصفوفة
alert( Array.from(str) ); // H,e,l,l,o

ناتجه هو ذات ناتج ‎[‎...str]‎.

ولكن… هناك فرق ضئيل بين ‎Array.from(obj)‎ و‎[...obj]‎:

  • يعمل ‎Array.from‎ على الشبيهات بالمصفوفات والمُتعدَّدات.
  • ويعمل مُعامل التوزيع على المُتعدَّدات فقط لا غير.

لذا لو أردت تحويل شيء إلى مصفوفة فالتابِع ‎Array.from‎ أكثر استعمالًا وشيوعًا.

الحصول علي نسخة من المصفوفة/الكائن

Copy an array/object

يمكن أن تقوم بالمثل عن طريق ....

Remember when we talked about Object.assign() in the past?

let arr = [1, 2, 3];

let arrCopy = [...arr]; // spread the array into a list of parameters
                        // then put the result into a new array

// هل متساويين؟
alert(arr === arrCopy); // خطأ (ليس نفس المرجع)

// تعديل مصفوفتنا الأولى لا يتم تعديله في النسخة:

arr.push(4);
alert(arr); // 1, 2, 3, 4
alert(arrCopy); // 1, 2, 3

لاحظ أنه من الممكن أن نقوم بنفس الشئ لصنع نسخة من الكائن أيضاً:

let obj = { a: 1, b: 2, c: 3 };

let objCopy = { ...obj }; // spread the object into a list of parameters
                          // then return the result in a new object

// هل الكائنات تمتلك نفس القيمة؟
alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // صحيح

// هل متساويين؟
alert(obj === objCopy); // خطأ (ليس نفس المرجع)

// تعديل الكائن الأول لا يعدل في النسخة
obj.d = 4;
alert(JSON.stringify(obj)); // {"a":1,"b":2,"c":3,"d":4}
alert(JSON.stringify(objCopy)); // {"a":1,"b":2,"c":3}

This way of copying an object is much shorter than let objCopy = Object.assign({}, obj) or for an array let arrCopy = Object.assign([], arr) so we prefer to use it whenever we can.

متى رأينا ‎"..."‎ في الشيفرة نعرف أنّه إمّا المُعاملات البقية وأمّا مُعامل التوزيع.

إليك طريقة بسيطة للتفريق بينهما:

  • حين ترى ‎...‎ موجودة في نهاية مُعاملات الدالة فهي «المُعاملات البقية» وستجمع بقية قائمة الوُسطاء في مصفوفة.
  • وحين ترى ‎...‎ في استدعاء دالة أو ما شابهه فهو «مُعامل توزيع» يوسّع المصفوفة إلى قائمة.

طُرق الاستعمال:

  • تُستعمل المُعاملات البقية لإنشاء دوال تقبل أيّ عدد كان من الوُسطاء.
  • يُستعمل مُعامل التوزيع لتمرير مصفوفة إلى دوال تطلب (عادةً) قائمة طويلة من الوُسطاء.

كلا الميزتين تساعدك في التنقل بين القائمة ومصفوفة المُعاملات بسهولة ويُسر.

يمكنك أيضًا أن ترى كل وُسطاء استدعاء الدالة «بالطريقة القديمة» ‎arguments‎ وهو كائن مُتعدَّد شبيه بالمصفوفات.

خريطة الدورة التعليمية