اذهب إلى المحتوى

توابع المصفوفات (Array methods) في جافاسكربت


صفا الفليج

تقدّم المصفوفات توابِع عديدة تُسهِّل التعامل معها. ولتبسيطها سنقسّمها إلى مجموعات بحسب الوظيفة في هذا الفصل ونشرح كل منها على حدة.

إضافة العناصر وإزالتها

عرفنا من الفصل الماضي بالتوابِع التي تُضيف العناصر وتُزيلها من بداية أو نهاية المصفوفة:

  • arr.push(...items)‎ -- يُضيف العناصر إلى النهاية،
  • arr.pop()‎ -- يستخرج عنصرًا من النهاية،
  • arr.shift()‎ -- يستخرج عنصرًا من البداية،
  • arr.unshift(...items)‎ -- يُضيف العناصر إلى البداية.

وهذه أخرى غيرها.

الوصل splice

يا ترى كيف نحذف أحد عناصر المصفوفة؟ المصفوفات كائنات، يمكننا تجربة delete وربما تنجح:

let arr = ["I", "go", "home"];

delete arr[1]; // ‫أزِل "go"

alert( arr[1] ); // undefined

// ‫صارت المصفوفة الآن arr = ["I",  , "home"];‎
alert( arr.length ); // 3

أُزيل العنصر صحيح، ولكنّ ما زال في المصفوفة ثلاثة عناصر، كما نرى في arr.length == 3.

هذا طبيعي، إذ يُزيل delete obj.key القيمة بمفتاحها key… وهذا فقط. ينفع للكائنات ربّما، لكنّا نريدها للمصفوفات أن تنتقل كل العناصر على اليمين وتأخذ الفراغ الجديد. أي أننا نتوقع أن تصغر المصفوفة الآن.

لهذا السبب علينا استعمال توابِع خاصّة لذلك.

يمكننا تشبيه التابِع arr.splice(start) بالتابِع «بتاع كُلّو» للمصفوفات (كما يُقال بالعامية). يمكنه أن يُجري ما تريد للعناصر: إدراج، إزالة، استبدال.

هذه صياغته:

arr.splice(index[, deleteCount, elem1, ..., elemN])

يبدأ التابِع من عند العنصر ذي الفهرس index، فيُزيل deleteCount من العناصر ويُدرج العناصر elem1, ..., elemN المُمرّرة إليه مكانها. أخيرًا يُعيد المصفوفة بالعناصر المُزالة.

فهم هذا التابِع بالأمثلة أبسط. فلنبدأ أولًا بالحذف:

let arr = ["I", "study", "JavaScript"];

// أزِل من العنصر ذا الفهرس 1 عنصرًا واحدًا (1)‏
arr.splice(1, 1); 

alert( arr ); // ["I", "JavaScript"]

رأيت؟ سهلة. نبدأ من العنصر ذي الفهرس 1 ونُزيل عنصرًا واحدًا (1).

الآن، نُزيل ثلاثة عناصر ونستبدلها بعنصرين آخرين:

let arr = ["I", "study", "JavaScript", "right", "now"];

// أزِل الثلاث عناصر الأولى وعوّضها بتلك الأخرى
arr.splice(0, 3, "Let's", "dance");

alert( arr ) // ‫["Let's", "dance", "right", "now"]

أمّا هنا فكيف يُعيد splice مصفوفةً بالعناصر المُزالة.

let arr = ["I", "study", "JavaScript", "right", "now"];

// أزِل أوّل عنصرين
let removed = arr.splice(0, 2);

alert( removed ); // "I", "study" <-- قائمة بالعناصر المُزالة

يمكن أن يُدرج تابِع splice العناصر دون إزالة أيّ شيء أيضًا. كيف؟ نضع deleteCount يساوي الصفر 0:

let arr = ["I", "study", "JavaScript"];

arr.splice(2, 0, "complex", "language");

alert( arr ); // "I", "study", "complex", "language", "JavaScript"

الفهارس السالبة ممكنة أيضًا يمكننا هنا وفي توابِع المصفوفات الأخرى استعمال الفهارس السالبة. وظيفتها تحديد المكان بدءًا من نهاية المصفوفة، هكذا:

let arr = [1, 2, 5];

arr.splice(-1, 0, 3, 4);

alert( arr ); // 1,2,3,4,5

القطع slice

التابِع arr.slice أبسط بكثير من شبيهه arr.splice.

صياغته هي:

arr.slice([start], [end])

وهو يُعيد مصفوفة جديدةً بنسخ العناصر من الفهرس start إلى end (باستثناء end). يمكن أن تكون start وحتّى end سالبتان، بهذا يُعدّ المحرّك القيمتان أماكن بدءًا من نهاية المصفوفة.

هذا التابِع يشبه تابِع السلاسل النصية str.slice، ولكن بدل السلاسل النصية الفرعية، يُعيد المصفوفات الفرعية. إليك المثال الآتي:

let arr = ["t", "e", "s", "t"];

// (نسخة تبدأ من 1 وتنتهي عند 3)
alert( arr.slice(1, 3) ); // e,s 

//  ‫(نسخة تبدأ من ‎-2 وتنتهي في النهاية)
alert( arr.slice(-2) ); // s,t

يمكننا أيضًا استدعائها بلا وُسطاء: يُنشئ arr.slice()‎ نسخة عن arr. نستعمل هذا غالبًا لأخذ نسخة وإجراء تعديلات عليها دون تعديل المصفوفة الأصلية، وتركها كما هي.

الربط concat

يُنشئ التابِع arr.concat مصفوفةً جديدة فيها القيم الموجودة في المصفوفات والعناصر الأخرى.

صياغته هي:

arr.concat(arg1, arg2...)

وهو يقبل أيّ عدد من الوُسطاء، أكانت مصفوفات أو قيم. أمّا ناتجه هو مصفوفة جديدة تحوي العناصر من arr، ثم arg1 فَـ arg2 وهكذا دواليك. لو كان الوسيط argN نفسه مصفوفة، فستُنسخ كل عناصره، وإلّا فسيُنسخ الوسيط نفسه. لاحِظ هذا المثال:

let arr = [1, 2];

// ‫اصنع مصفوفة فيها العنصرين: arr و [3,4]
alert( arr.concat([3, 4]) ); // 1,2,3,4

// ‫اصنع مصفوفة فيها العناصر: arr و[3,4] و[5,6]
alert( arr.concat([3, 4], [5, 6]) ); // 1,2,3,4,5,6

// ‫اصنع مصفوفة فيها العنصرين: arr و[3,4]، بعدها أضِف القيمتين 5 و 6
alert( arr.concat([3, 4], 5, 6) ); // 1,2,3,4,5,6

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

let arr = [1, 2];

let arrayLike = {
  0: "something",
  length: 1
};

alert( arr.concat(arrayLike) ); // 1,2,[object Object]

… ولكن لو كان للكائن الشبيه بالمصفوفات خاصية Symbol.isConcatSpreadable، فستتعامل معه concat مثلما تتعامل مع المصفوفات: ستُضاف عناصره بدل كيانه:

let arr = [1, 2];

let arrayLike = {
  0: "something",
  1: "else",
  [Symbol.isConcatSpreadable]: true,
  length: 2
};

alert( arr.concat(arrayLike) ); // 1,2,something,else

التكرار: لكلّ forEach

يتيح لنا التابِع arr.forEach تشغيل إحدى الدوال على كلّ عنصر من عناصر المصفوفة.

الصياغة:

arr.forEach(function(item, index, array) {
  // ... استعملهما فيما تريد
});

مثال على عرض كلّ عنصر من عناصر المصفوفة:

// ‫لكلّ عنصر، استدعِ دالة التنبيه alert
["Bilbo", "Gandalf", "Nazgul"].forEach(alert);

بينما هذه الشيفرة تحبّ الكلام الزائد ومكانها في المصفوفة المحدّدة:

["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
  alert(`${item} is at index ${index} in ${array}`);
});

ناتج التابِع (لو أعادَ شيئًا أصلًا) يُهمل ويُرمى.

البحث في المصفوفات

أمّا الآن لنرى التوابع التي تبحث في المصفوفة.

التوابِع indexOf و lastIndexOf و includes

للتوابِع arr.indexOf و arr.lastIndexOf و arr.includes نفس الصياغة ووظيفتها هي ذات وظيفة تلك بنسخة النصوص النصية، الفرق أنها هنا تتعامل مع العناصر بدل المحارف:

  • arr.indexOf(item, from)‎ -- يبحث عن العنصر item بدءًا من الفهرس from، ويُعيد فهرسه حيث وجده. ولو لم يجده، يُعيد -1.
  • arr.lastIndexOf(item, from)‎ -- نفسه، ولكن البحث يبدأ من اليمين وينتهي في اليسار.
  • arr.includes(item, from)‎ -- يبحث عن العنصر item بدءًا من الفهرس from، ويُعيد true إن وجدته.

مثال:

let arr = [1, 0, false];

alert( arr.indexOf(0) ); // 1
alert( arr.indexOf(false) ); // 2
alert( arr.indexOf(null) ); // -1

alert( arr.includes(1) ); // true

لاحظ أنّ التوابِع تستعمل الموازنة بِـ ===. لذا لو كنّا نبحث عن false، فستبحث هي عن false نفسها وليس الصفر.

لو أردت معرفة فيما كانت تحتوي المصفوفة على عنصر معيّن، ولا تريد معرفة فهرسه، فدالة arr.includes مناسبة لك.

وهناك أيضًا أمر، تختلف includes عن سابقاتها indexOf/lastIndexOf بأنّها تتعامل مع NaN كما ينبغي:

const arr = [NaN];
alert( arr.indexOf(NaN) ); // ‫يُعيد ‎-1 (الصحيح هو 0 إلّا أنّ الموازنة === لا تعمل مع NaN)
alert( arr.includes(NaN) );// true (الآن صحيح)

البحث عبر find و findIndex

لنقل أنّ لدينا مصفوفة من الكائنات، كيف نجد الكائن حسب شرط معيّن؟

هنا يمكننا استغلال التابِع arr.find(fn).

صياغته هي:

let result = arr.find(function(item, index, array) {
  // ‫لو أُعيدت القيمة true، فيُعاد العنصر ويتوقّف التعداد
  // ‫لو لم نجد ما نريد نُعيد undefined
});

تُستدعى الدالة على كل عنصر من عناصر المصفوفة، واحدًا بعد الآخر:

  • item: العنصر.
  • index: الفهرس.
  • array: المصفوفة نفسها.

لو أعادت true، يتوقّف البحث ويُعاد العنصر item. إن لم يوجد شيء فيُعاد undefined.

نرى في هذا المثال مصفوفة من المستخدمين، لكلّ مستخدم حقلان id وname. نريد الذي يتوافق مع الشرط id == 1:

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

let user = users.find(item => item.id == 1);

alert(user.name); // John

في الحياة العملية، يكثُر استعمال الكائنات في المصفوفات، ولهذا فالتابِع find مفيد جدًا لنا.

يمكنك ملاحظة بأنّا في المثال مرّرنا للتابِع find الدالة item => item.id == 1 وفيها وسيط واحد. هذا طبيعي فنادرًا ما نستعمل الوُسطاء البقية في هذه الدالة.

يتشابه التابِع arr.findIndex كثيرًا مع هذا، عدا على أنّه يُعيد فهرس العنصر الذي وجده بدل العنصر نفسه، ويُعيد ‎-1 لو لم يجد شيئًا.

الترشيح filter

يبحث التابِع find عن أوّل عنصر (واحد فقط) يُحقّق للدالة شرطها فتُعيد true.

لو أردت إعادة أكثر من واحد فيمكن استعمال arr.filter(fn).

تشبه صياغة filter التابِع find، الفرق هو إعادته لمصفوفة بكلّ العناصر المتطابقة:

let results = arr.filter(function(item, index, array) {
  // ‫لو كانت true فتُضاف القائمة إلى مصفوفة النتائج ويتواصل التكرار
  // يُعيد مصفوفة فارغة إن لم يجد شيئًا
});

مثال:

let users = [
  {id: 1, name: "John"},
  {id: 2, name: "Pete"},
  {id: 3, name: "Mary"}
];

// يُعيد مصفوفة تحتوي على أوّل مستخدمَين اثنين
let someUsers = users.filter(item => item.id < 3);

alert(someUsers.length); // 2

التعديل على عناصر المصفوفات

لنرى الآن التوابِع التي تُعدّل المصفوفة وتُعيد ترتيبها.

الخارطة map

يُعدّ التابِع arr.map أكثرها استخدامًا وفائدةً أيضًا. ما يفعله هو استدعاء الدالة على كلّ عنصر من المصفوفة وإعادة مصفوفة بالنتائج.

صياغته هي:

let result = arr.map(function(item, index, array) {
  // يُعيد القيمة الجديدة عوض العنصر
});

مثلًا، هنا نعدّل كل عنصر فنحوّله إلى طوله:

let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length);
alert(lengths); // 5,7,6

sort(fn)

نُرتّب باستدعاء arr.sort()‎ المصفوفة كما هي دون نسخها فنغيّر ترتيب عناصرها. هي الأخرى تُعيد المصفوفة المُرتّبة، ولكن غالبًا ما نُهمل القيمة المُعادة فالمصفوفة arr هي التي تتغيّر.

مثال:

let arr = [ 1, 2, 15 ];

// يعيد التابع ترتيب محتوى المصفوفة
arr.sort();

alert( arr );  // 1, 15, 2

هل لاحظت بأنّ الناتج غريب؟ صار ‎1, 15, 2. ليس هذا ما نريد. ولكن، لماذا؟

مبدئيًا، تُرتّب العناصر وكأنها سلاسل نصية.

بالمعنى الحرفي للكلمة: تُحوّل كل العناصر إلى سلاسل نصية عند الموازنة. والترتيب المعجماتي هو المتّبع لترتيب السلاسل النصية، ‎"2" > "15"‎ صحيحة حقًا.

علينا لاستعمال الترتيب الذي نريده تمريرَ دالة تكون وسيطًا للتابِع arr.sort()‎.

على الدالة موازنة قيمتين اثنتين (أيًا كانتا) وإعادة الناتج:

function compare(a, b) {
  // لو كانت القيمة الأولى أكبر من الثانية
  if (a > b) return 1; 
  
  // لو تساوت القيمتين
  if (a == b) return 0; 
  
  // لو كانت القيمة الأولى أصغر من الثانية
  if (a < b) return -1; 
}

مثال عن الترتيب لو كانت القيم أعدادًا:

function compareNumeric(a, b) {
  if (a > b) return 1;
  if (a == b) return 0;
  if (a < b) return -1;
}

let arr = [ 1, 2, 15 ];

arr.sort(compareNumeric);

alert(arr);  // 1, 2, 15

الآن صارت تعمل كما نريد.

لنتوقف لحظة ونفكّر فيما يحدث تمامًا. أنتّفق بأنّ المصفوفة arr يمكن أن تحتوي أيّ شيء؟ أيّ شيء من الأعداد أو السلاسل النصية أو الكائنات أو غيرها. كلّ ما لدينا هو مجموعة من العناصر. لترتيبها نحتاج دالة ترتيب تعرف طرقة مقارنة عناصر المصفوفة. مبدئيًا، الترتيب يكون بالسلاسل النصية.

يُنفِّذ التابع arr.sort(fn)‎في طيّاته خوارزمية فرز عامّة. لسنا نكترث كيف تعمل هذه الخوارزمية خلف الكواليس (وهي غالبًا quicksort محسّنة)، بل نكترث بأنّها ستمرّ على المصفوفة، تُوازن عناصرها باستعمال الدالة المقدّمة أعلاه وتُعيد ترتيبها. نكترث بأن نقدّم دالة fn التي ستؤدّي الموازنة.

بالمناسبة، لو أردت معرفة العناصر التي تُوازنها الدالة حاليًا، فلا بأس. لن يقتلك أحد لو عرضتها:

[1, -2, 15, 2, 0, 8].sort(function(a, b) {
  alert( a + " <> " + b );
});

يمكن أن تقارن الخوارزمية العنصر مع غيره من العناصر، ولكنّها تحاول قدر الإمكان تقليص عدد الموازنات.

يمكن أن تُعيد دالة الموازنة أيّ عدد في الواقع، ليس على دالة الموازنة إلّا إعادة عدد موجب بدلالة «هذا أكبر من ذاك» وسالب بدلالة «هذا أصغر من ذاك».

يمكننا هكذا كتابة الدوال بأسطر أقل: That allows to write shorter functions:

let arr = [ 1, 2, 15 ];

arr.sort(function(a, b) { return a - b; });

alert(arr);  // 1, 2, 15

تحيا الدوال السهمية أتذكر الدوال السهمية من فصل تعابير الدوال والدوال السهمية؟ يمكننا استعمالها أيضًا لتبسيط كود الفرز:

arr.sort( (a, b) => a - b );

لا تفرق هذه عن تلك الطويلة بشيء، البتة.

العكس reverse

يعكس التابِع arr.reverse ترتيب العناصر في المصفوفة arr.

مثال:

let arr = [1, 2, 3, 4, 5];
arr.reverse();

alert( arr ); // 5,4,3,2,1

كما ويُعيد المصفوفة arr بعد عكسها.

التقسيم split والدمج join

إليك موقفًا من الحياة العملية. تحاول الآن برمجة تطبيق مراسلة، ويُدخل المستخدم قائمة المستلمين بفاصلة بين كلّ واحد: John, Pete, Mary. ولكن لنا نحن المبرمجين، فالمصفوفة التي تحتوي الأسماء أسهل بكثير من السلسلة النصية. كيف السبيل إذًا؟

هذا ما يفعله التابِع str.split(delim). يأخذ السلسلة النصية ويقسمها إلى مصفوفة حسب محرف القاسِم delim المقدّم.

في المثال أعلاه نقسم حسب «فاصلة بعدها مسافة»:

let names = 'Bilbo, Gandalf, Nazgul';

let arr = names.split(', ');

for (let name of arr) {
  alert( `A message to ${name}.` ); // A message to Bilbo  (والبقية)
}

للتابِع split وسيطًا عدديًا اختياريًا أيضًا، وهو يحدّ طول المصفوفة. لو قدّمته فستُهمل العناصر الأخرى. ولكن في الواقع العملي، نادرًا ما ستفيدك هذا:

let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2);

alert(arr); // Bilbo, Gandalf

التقسيم إلى أحرف لو ناديت split(s)‎ وتركت s فارغًا فستُسقم السلسلة النصية إلى مصفوفة من الأحرف:

let str = "test";

alert( str.split('') ); // t,e,s,t

على العكس من split فنداء arr.join(glue) يُنشئ سلسلة نصية من عناصر arr مجموعةً معًا «باللاصق».

مثال:

let arr = ['Bilbo', 'Gandalf', 'Nazgul'];

let str = arr.join(';');

alert( str ); // Bilbo;Gandalf;Nazgul

التابِعان reduce و reduceRight

متى ما أردنا أن نمرّ على عناصر المصفوفة، استعملنا forEach أو for أو for..of. ومتى ما أردنا أن نمرّ ونُعيد بيانات كلّ عنصر، استعملنا map.

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

هذه الصياغة:

let value = arr.reduce(function(previousValue, item, index, array) {
  // ...
}, [initial]);

تُطبّق الدالة على كل عناصر المصفوفة واحدًا بعد الآخر، و«تنقل» النتيجة إلى النداء التالي لها:

وُسطاء الدالة:

  • previousValue - نتيجة النداء السابق للدالة. يُساوي قيمة initial في أوّل نداء (لو قُدّمت أصلًا).
  • item -- العنصر الحالي في المصفوفة.
  • index -- مكان العنصر.
  • array -- المصفوفة نفسها.

حين تُطبّق الدالة، تُمرّر إليها نتيجة النداء السابق في أوّل وسيط. أجل، معقّد قليلًا، لكن ليس كما تتخيّل لو قلنا أنّ الوسيط الأول بمثابة «ذاكرة» تخزّن النتيجة النهائية من إجراءات التنفيذ التي سبقتها. وفي آخر نداء تصير نتيجة التابِع reduce.

ربّما نقدّم مثالًا لتسهيل المسألة. هنا نعرف مجموعة عناصر المصفوفة في سطر برمجي واحد:

let arr = [1, 2, 3, 4, 5];

let result = arr.reduce((sum, current) => sum + current, 0);

alert(result); // 15

الدالة المُمرّرة إلى reduce تستعمل وسيطين اثنين فقط، وهذا كافٍ عادةً.

لنرى تفاصيل النداءات.

  1. في أوّل مرّة، قيمة sum هي قيمة initial (آخر وسيط في reduce) وتساوي 0، وcurrent هي أوّل عنصر في المصفوفة وتساوي 1. إذًا فناتج الدالة هو 1.
  2. في النداء التالي، sum = 1 ونُضيف العنصر الثاني في المصفوفة (2) ونُعيد القيمة.
  3. في النداء الثالث، sum = 3، ونُضيف العنصر التالي في المصفوفة، وهكذا دواليك إلى آخر نداء…

هذا سير العملية الحسابية:

reduce.png

وهكذا نمثّلها في جدول (كلّ صف يساوي نداء واحد للدالة على العنصر التالي في المصفوفة):

  sum current الناتج
أوّل نداء 0 1 1
ثاني نداء 1 2 3
ثالث نداء 3 3 6
رابع نداء 6 4 10
خامس نداء 10 5 15

هكذا نرى بوضوح شديد كيف يصير ناتج النداء السابق أوّل وسيط في النداء الذي يلحقه.

يمكننا أيضًا حذف القيمة الأولية:

let arr = [1, 2, 3, 4, 5];

// ‫أزلنا القيمة الأولية من التابِع reduce (اختفت القيمة 0)
let result = arr.reduce((sum, current) => sum + current);

alert( result ); // 15

وستكون النتيجة متطابقة، إذ أنّ reduce تأخذ أول عنصر من المصفوفة على أنّه قيمة أولية (لو لم نقدّم نحن قيمة أولية) وتبدأ العملية من العنصر الثاني.

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

مثال على ذلك:

let arr = [];

arr.reduce((sum, current) => sum + current);

الشيفرة السابقة ستطلق خطأ، إذ لا يمكن استدعاء reduce مع مصفوفة فارغة دون قيمة أولية، وتحل المشكلة بتوفير قيمة أولية، وستعاد آنذاك. لذا خُذ هذه النصيحة وحدّد قيمة أولية دومًا.

لا يختلف التابِع arr.reduceRight عن هذا أعلاه إلا بأنّه يبدأ من اليمين وينتهي على اليسار.

Array.isArray

المصفوفات ليست نوعًا منفصلًا في اللغة، بل هي مبنيّة على الكائنات. لذا typeof لن تفيدك في التفريق بين الكائن العادي والمصفوفة:

alert(typeof {}); // ‫كائن object
alert(typeof []); // كائن أيضًا

…ولكن، المصفوفات تستعمل كثيرًا جدًا لدرجة تقديم تابِع خاص لهذا الغرض: Array.isArray(value). يُعيد هذا التابِع true لو كانت value مصفوفة حقًا، وfalse لو لم تكن.

alert(Array.isArray({})); // false

alert(Array.isArray([])); // true

تدعم أغلب التوابِع thisArg

تقبل أغلب توابِع المصفوفات تقريبًا، التوابع التي تستدعي دوالًا (مثل find وfilter وmap، عدا sort) - تقبل المُعامل الاختياري thisArg.

لم نشرح هذا المُعامل في الأقسام أعلاه إذ أنّه نادرًا ما يُستعمل. ولكن علينا الحديث عنه لألا يكون الشرح ناقصًا.

هذه الصياغة الكاملة لهذه التوابِع:

arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// ‫الوسيط thisArg هو آخر وسيط اختياري

تكون قيمة المُعامل thisArg للدالة func تساوي this. هنا مثلًا نستعمل تابِع كائن army على أنّه مرشّح، والوسيط thisArg يمرّر سياق التنفيذ وذلك لإيجاد المستخدمين الذين يعيد التابع army.canJoin القيمة true:

let army = {
  minAge: 18,
  maxAge: 27,
  canJoin(user) {
    return user.age >= this.minAge && user.age < this.maxAge;
  }
};

let users = [
  {age: 16},
  {age: 20},
  {age: 23},
  {age: 30}
];

*!*

let soldiers = users.filter(army.canJoin, army);
*/!*

alert(soldiers.length); // 2
alert(soldiers[0].age); // 20
alert(soldiers[1].age); // 23

لو استعملنا في المثال أعلاه users.filter(army.canJoin)‎ فسيُستدعى التابِع army.canJoin كدالة مستقلة بذاتها حيث this=undefined، ما سيؤدي إلى خطأ.

يمكن استبدال استدعاء users.filter(army.canJoin, army)‎ بالتعليمة التي تُؤدّي ذات الغرض users.filter(user => army.canJoin(user))‎. نستعمل الأولى أكثر من الثانية إذ أنّ الناس تفهمها أكثر من تلك.

ملخص

ورقة فيها كل توابِع الدوال (غُشّ منها):

  • لإضافة العناصر وإزالتها:

    • push(...items)‎ -- تُضيف العناصر items إلى النهاية،

    • pop()‎ -- تستخرج عنصرًا من النهاية،

    • shift()‎ -- تستخرج عنصرًا من البداية،

    • unshift(...items)‎ -- تُضيف العناصر items إلى البداية.

    • splice(pos, deleteCount, ...items)‎ -- بدءًا من العنصر ذي الفهرس pos، احذف deleteCount من العناصر وأدرِج مكانه العناصر items.

    • slice(start, end)‎ -- أنشِئ مصفوفة جديدة وانسخ عناصرها بدءًا من start وحتّىend(ولكن دونend).

    • concat(...items)‎ -- أعِد مصفوفة جديدة: انسخ كل عناصر المصفوفة الحالية وأضَِف إليها العناصر items. لو كانت واحدة من عناصر items مصفوفة أيضًا، فستُنسخ عناصرها بدل.

  • لتبحث عن العناصر:

    • indexOf/lastIndexOf(item, pos)‎ -- ابحث عن العنصر item بدءًا من العنصر ذي الفهرس pos وأعِد فهرسه أو أعِد ‎-1 لو لم تجده.

    • includes(value)‎ -- أعِد القيمة true لو كان العنصر value في المصفوفة، وإلا أعِد false.

    • find/filter(func)‎ -- رشّح العناصر عبر دالة وأعِد أوّل قيمة (أو كل القيم) التي تُعيد الدالة قيمة true لو مُرّر ذلك العنصر لها.

    • findIndex يشبه find، ولكن يُعيد الفهرس بدل القيمة.

  • للمرور على عناصر المصفوفة:

    • forEach(func)‎ -- يستدعي func لكلّ عنصر ولا يُعيد أيّ شيء.

  • لتعديل عناصر المصفوفة:

    • map(func)‎ -- أنشِئ مصفوفة جديدة من نتائج استدعاء func لكلّ من عناصر المصفوفة.

    • sort(func)‎ -- افرز المصفوفة كما هي وأعِد ناتج الفرز.

    • reverse()‎ -- اعكس عناصر المصفوفة كما هي وأعِد ناتج العكس.

    • split/join -- حوّل المصفوفة إلى سلسلة نصية، والعكس أيضًا.

    • reduce(func, initial)‎ -- احسب قيمة من المصفوفة باستدعاء func على كلّ عنصر فيها وتمرير الناتج بين كلّ استدعاء وآخر.

  • وأيضًا:

    • Array.isArray(arr)‎ يفحص لو كانت arr مصفوفة أم لا.

لاحظ أنّ التوابِع sort وreverse وsplice تُعدّل المصفوفة نفسها.

هذه التوابِع أعلاه هي أغلب ما تحتاج وما تريد أغلب الوقت (99.99%). ولكن هناك طبعًا غيرها:

  • arr.some(fn)‎/arr.every(fn) تفحص المصفوفة.

    تُنادى الدالة fn على كلّ عنصر من المصفوفة (مثل map). لو كانت أيًا من (أو كل) النتائج true، فيُعيد true، وإلًا يُعيد false.

  • arr.fill(value, start, end)‎ -- يملأ المصفوفة بالقيمة المتكرّرة value من الفهرس start إلى الفهرس end.

  • arr.copyWithin(target, start, end)‎ -- ينسخ العناصر من العنصر ذا الفهرس start إلى ذا الفهرس end ويلصقها داخلها عند الفهرس target (تعوّض ما هو موجود مكانها في المصفوفة).

طالِع الكتيّب لقائمة فيها كل شيء. من أول وهلة سترى بأنّ عدد التوابِع لا ينتهي ومهمة حفظها مستحيلة، ولكن الواقع هي أنّها بسيطة جدًا.

طالِع «ورقة الغشّ» لتعرف ما تفعل كلًا منها، ثمّ حُلّ مهام هذا الفصل لتتدرّب عليها وتكون خبيرًا كفاية بتوابِع الدوال. بعدها، لو احتجت التعامل مع المصفوفات ولا تدري ما تفعل، تعال هنا وابحث في ورقة الغشّ عن التابِع المناسب لحالتك. الأمثلة الموجودة ستفيدك فتكتبها كما ينبغي. وسريعًا ما ستتذكّر كل التوابِع من تلقاء نفسك ودون بذل أيّ جهد.

تمارين

حوّل «border-left-width» إلى «borderLeftWidth»

الأهمية: 5

اكتب دالة camelize(str)‎ تغيّر الكلمات المقسومة بِشَرطات مثل «my-short-string» إلى عبارات بتنسيق «سنام الجمل»: «myShortString».

بعبارة أخرى: أزِل كلّ الشرطات وحوّل أوّل حرف من كلّ كلمة بعدها إلى الحالة الكبيرة.

أمثلة:

camelize("background-color") == 'backgroundColor';
camelize("list-style-image") == 'listStyleImage';
camelize("-webkit-transition") == 'WebkitTransition';

تلميح: استعمل split لتقسيم السلسلة النصية إلى مصفوفة، ثمّ عدّل عناصرها وأعِد ربطها بتابِع join.

الحل

function camelize(str) {
  return str
    .split('-') // splits 'my-long-word' into array ['my', 'long', 'word']
    .map(
      // كبر الحروف الأولى لجميع عناصر المصفوفة باستثناء أول عنصر
      // ['my', 'long', 'word'] --> ['my', 'Long', 'Word']
      (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1)
    )
    .join(''); // joins ['my', 'Long', 'Word'] into 'myLongWord'
}

نطاق ترشيح

الأهمية: 4

اكتب دالة filterRange(arr, a, b)‎ تأخذ المصفوفة arr، وتبحث في عناصرها بين a وb وتُعيد مصفوفة بها. يجب ألّا تُعدّل الدالة المصفوفة، بل إعادة مصفوفة جديدة.

مثال:

let arr = [5, 3, 8, 1];

let filtered = filterRange(arr, 1, 4); 

alert( filtered ); // 3,1

alert( arr ); // 5,3,8,1

الحل

function filterRange(arr, a, b) {
  // أضفنا الأقواس حول التعبير لتسهيل القراءة
  return arr.filter(item => (a <= item && item <= b));
}

let arr = [5, 3, 8, 1];

let filtered = filterRange(arr, 1, 4);

alert( filtered ); // 3,1

alert( arr ); // 5,3,8,1

نطاق ترشيح «كما هو»

الأهمية: 4

اكتب دالة filterRangeInPlace(arr, a, b)‎ تأخذ المصفوفة arr وتُزيل منها كل القيم عدا تلك بين a وb. الشرط هو: ‎a ≤ arr ≤ b.

يجب أن تُعدّل الدالة المصفوفة، ولا تُعيد شيئًا.

مثال:

let arr = [5, 3, 8, 1];

// حذف جميع الأعداد باستثناء الواقعة بين 1 و 4
filterRangeInPlace(arr, 1, 4); 

alert( arr ); // [3, 1]

الحل

function filterRangeInPlace(arr, a, b) {

  for (let i = 0; i < arr.length; i++) {
    let val = arr[i];

    // إزالة إن كانت خارج النطاق
    if (val < a || val > b) {
      arr.splice(i, 1);
      i--;
    }
  }

}

let arr = [5, 3, 8, 1];

filterRangeInPlace(arr, 1, 4); // removed the numbers except from 1 to 4

alert( arr ); // [3, 1]

الفرز بالترتيب التنازلي

الأهمية: 4

let arr = [5, 2, 1, -10, 8];

// ... شيفرة ترتيب العناصر تنازليًا

alert( arr ); // 8, 5, 2, 1, -10

الحل

let arr = [5, 2, 1, -10, 8];

arr.sort((a, b) => b - a);

alert( arr );

نسخ المصفوفة وفرزها

الأهمية: 5

في يدنا مصفوفة من السلاسل النصية arr. نريد نسخة مرتّبة عنها وترك arr بلا تعديل.

أنشِئ دالة copySorted(arr)‎ تُعيد هذه النسخة.

let arr = ["HTML", "JavaScript", "CSS"];

let sorted = copySorted(arr);

alert( sorted ); // CSS, HTML, JavaScript
alert( arr ); // HTML, JavaScript, CSS

الحل

يمكن أن نستعمل slice()‎ لأخذ نسخة ونفرز المصفوفة:

function copySorted(arr) {
  return arr.slice().sort();
}

let arr = ["HTML", "JavaScript", "CSS"];

*!*
let sorted = copySorted(arr);
*/!*

alert( sorted );
alert( arr );

خارطة بالأسماء

الأهمية: 5

لدينا مصفوفة من كائنات user، لكلّ منها صفة user.name. اكتب كودًا يحوّلها إلى مصفوفة من الأسماء.

مثال:

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let users = [ john, pete, mary ];

let names = /* شيفرتك هنا */

alert( names ); // John, Pete, Mary

الحل

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let users = [ john, pete, mary ];

let names = users.map(item => item.name);

alert( names ); // John, Pete, Mary

أنشِئ آلة حاسبة يمكن توسعتها لاحقًا

الأهمية: 5

أنشِئ دالة إنشاء باني «constructor»‏ Calculator تُنشئ كائنات من نوع «آلة حاسبة» يمكن لنا «توسعتها».

تنقسم هذه المهمة إلى جزئين اثنين:

  1. أولًا، نفّذ تابِع calculate(str)‎ يأخذ سلسلة نصية (مثل "1 + 2") بالتنسيق «عدد مُعامل عدد» (أي مقسومة بمسافات) ويُعيد الناتج. يجب أن يفهم التابِع الجمع + والطرح -.

    مثال عن الاستعمال:

    let calc = new Calculator;
    
    alert( calc.calculate("3 + 7") ); // 10

     

  2. بعدها أضِف تابِع addMethod(name, func)‎ يُعلّم الآلة الحاسبة عمليّة جديدة. يأخذ التابِع المُعامل name ودالة func(a,b)‎ بوسيطين تُنفّذ هذه العملية.

    كمثال على ذلك سنُضيف عمليات الضرب * والقسمة / والأُسّ **:

    let powerCalc = new Calculator;
    powerCalc.addMethod("*", (a, b) => a * b);
    powerCalc.addMethod("/", (a, b) => a / b);
    powerCalc.addMethod("**", (a, b) => a ** b);
    
    let result = powerCalc.calculate("2 ** 3");
    alert( result ); // 8

     

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

الحل

  • لاحظ طريقة تخزين التوابِع، حيث تُضاف إلى صفة this.methods فقط.
  • كلّ الشروط والتحويلات العددية موجودة في التابِع calculate. يمكننا في المستقبل توسيعه ليدعم تعابير أكثر تعقيدًا.
function Calculator() {

  this.methods = {
    "-": (a, b) => a - b,
    "+": (a, b) => a + b
  };

  this.calculate = function(str) {

    let split = str.split(' '),
      a = +split[0],
      op = split[1],
      b = +split[2]

    if (!this.methods[op] || isNaN(a) || isNaN(b)) {
      return NaN;
    }

    return this.methods[op](a, b);
  }

  this.addMethod = function(name, func) {
    this.methods[name] = func;
  };
}

خارطة بالكائنات

فرز المستخدمين حسب أعمارهم

الأهمية: 5

اكتب دالة sortByAge(users)‎ تأخذ مصفوفة من الكائنات بالصفة age وتُرتبّها حسب أعمارهم age.

مثال:

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let arr = [ pete, john, mary ];

sortByAge(arr);

// [john, mary, pete]
alert(arr[0].name); // John
alert(arr[1].name); // Mary
alert(arr[2].name); // Pete

الحل

function sortByAge(arr) {
  arr.sort((a, b) => a.age > b.age ? 1 : -1);
}

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 28 };

let arr = [ pete, john, mary ];

sortByAge(arr);

// [john, mary, pete]
alert(arr[0].name); // John
alert(arr[1].name); // Mary
alert(arr[2].name); // Pete

خلط المصفوفات

الأهمية: 3

اكتب دالة shuffle(array)‎ تخلط عناصر المصفوفة (أي ترتّبها عشوائيًا).

يمكن بتكرار نداء shuffle إعادة العناصر بترتيب مختلف. مثال:

let arr = [1, 2, 3];

shuffle(arr);
// arr = [3, 2, 1]

shuffle(arr);
// arr = [2, 1, 3]

shuffle(arr);
// arr = [3, 1, 2]
// ...

يجب أن تكون جميع احتمالات ترتيب العناصر متساوية. فمثلًا يمكن إعادة ترتيب [1,2,3] لتكون [1,2,3] أو [1,3,2] أو [3,1,2] أو أو أو، واحتمال حدوث كلّ حالة متساوٍ.

الحل

هذا هو الحل البسيط:

function shuffle(array) {
  array.sort(() => Math.random() - 0.5);
}

let arr = [1, 2, 3];
shuffle(arr);
alert(arr);

تعمل هذه الشيفرة (نوعًا ما) إذ أنّ Math.random() - 0.5 عددٌ عشوائي ويمكن أن يكون موجبًا أم سالبًا، بذلك تُعيد دالة الفرز ترتيب العناصر عشوائيًا.

ولكن ليس هذه الطريقة التي تعمل فيها دوال الفرز، إذ ليس لكلّ حالات التبديل الاحتمال نفسه. فمثلًا في الشيفرة أعلاه، تُنفّذ shuffle ‏1000000 مرّة وتعدّ مرّات ظهور النتائج الممكنة كلّها:

function shuffle(array) {
  array.sort(() => Math.random() - 0.5);
}

// نعدّ مرّات ظهور كلّ عمليات التبديل الممكنة
let count = {
  '123': 0,
  '132': 0,
  '213': 0,
  '231': 0,
  '321': 0,
  '312': 0
};

for (let i = 0; i < 1000000; i++) {
  let array = [1, 2, 3];
  shuffle(array);
  count[array.join('')]++;
}

// نعرض عدد عمليات التبديل الممكنة
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

إليك عيّنة عن الناتج (إذ يعتمد على محرّك جافاسكربت):

123: 250706
132: 124425
213: 249618
231: 124880
312: 125148
321: 125223

نرى تحيّز الشيفرة بوضوح شديد، إذ تظهر 123 و213 أكثر بكثير من البقية. يختلف ناتج هذه الشيفرة حسب محرّكات جافاسكربت ولكن هذا يكفي لنقول بأنّ هذه الطريقة ليست موثوقة.

ولكن لمَ لا تعمل الشيفرة؟ بشكل عام فتابِع sort أشبه ”بالصندوق الأسود“: نرمي فيه مصفوفة ودالة موازنة وننتظر أن تفرز لنا المصفوفة. ولكن بسبب عشوائية الموازنة يختلّ ذكاء الصندوق الأسود، وهذا الاختلال يعتمد على طريقة كتابة كلّ محرّك للشيفرة الخاصة به.

ثمّة طرق أخرى أفضل لهذه المهمّة، مثل الخوارزمية خلّاط فِشر ييتس الرائعة. فكرتها هي المرور على عناصر المصفوفة بالعكس وتبديل كلّ واحد بآخر قبله عشوائيًا:

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1)); // ‫رقم عشوائي من 0 إلى i

    // let t = array[i]; array[i] = array[j]; array[j] = t
    [array[i], array[j]] = [array[j], array[i]];
  }
}

بدلنا بالمثال هذا العنصرين array‎ و array[j]‎ وذلك نستعمل صياغة ”الإسناد بالتفكيك" وستجد تفاصيل أكثر عن هذه الصياغة في فصول لاحقة.

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

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
}

// نعدّ مرّات ظهور كلّ عمليات التبديل الممكنة
let count = {
  '123': 0,
  '132': 0,
  '213': 0,
  '231': 0,
  '321': 0,
  '312': 0
};

for (let i = 0; i < 1000000; i++) {
  let array = [1, 2, 3];
  shuffle(array);
  count[array.join('')]++;
}

// نعرض عدد عمليات التبديل الممكنة
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

عيّنة عن الناتج:

123: 166693
132: 166647
213: 166628
231: 167517
312: 166199
321: 166316

الآن كل شيء سليم: لكلّ عمليات التبديل ذات الاحتمال. كما أنّ خوارزمية ”فِشر ييتس“ أفضل من ناحية الأداء إذ ليس علينا تخصيص الموارد لعملية ”الفرز“.

ما متوسّط الأعمار؟

الأهمية: 4

اكتب دالة getAverageAge(users)‎ تأخذ مصفوفة من كائنات لها الصفة age وتُعيد متوسّط الأعمار.

معادلة المتوسّط: ‎(age1 + age2 + ... + ageN) / N.

مثال:

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 29 };

let arr = [ john, pete, mary ];

alert( getAverageAge(arr) ); // (25 + 30 + 29) / 3 = 28

الحل

function getAverageAge(users) {
  return users.reduce((prev, user) => prev + user.age, 0) / users.length;
}

let john = { name: "John", age: 25 };
let pete = { name: "Pete", age: 30 };
let mary = { name: "Mary", age: 29 };

let arr = [ john, pete, mary ];

alert( getAverageAge(arr) ); // 28

ترشيح العناصر الفريدة في المصفوفة

الأهمية: 4

لمّا أنّ arr مصفوفة، أنشِئ دالة unique(arr)‎ تُعيد مصفوفة فيها عناصر arr غير مكرّرة.

مثال:

function unique(arr) {
  /* شيفرة هنا */
}

let strings = ["Hare", "Krishna", "Hare", "Krishna",
  "Krishna", "Krishna", "Hare", "Hare", ":-O"
];

alert( unique(strings) ); // Hare, Krishna, :-O

الحل

ما سنفعل هو المرور على عناصر المصفوفة:

  • سنفحص كلّ عنصر ونرى إن كان في المصفوفة الناتجة.
  • إن كان كذلك… نُهمله، وإن لم يكن، نُضيفه إلى المصفوفة.
function unique(arr) {
  let result = [];

  for (let str of arr) {
    if (!result.includes(str)) {
      result.push(str);
    }
  }

  return result;
}

let strings = ["Hare", "Krishna", "Hare", "Krishna",
  "Krishna", "Krishna", "Hare", "Hare", ":-O"
];

alert( unique(strings) ); // Hare, Krishna, :-O

صحيح أنّ الكود يعمل، إلّا أنّ فيه مشكلة أداء محتملة. خلف الكواليس، يمرّ التابِع result.includes(str)‎ على المصفوفة result ويقارن كلّ عنصر مع str ليجد المطابقة المنشودة. لذا لو كان في result مئة 100 عنصر وما من أيّ مطابقة مع str، فعليها المرور على جُلّ result وإجراء 100 حالة مقارنة كاملة. ولو كانت result كبيرة مثل 10000 فيعني ذلك 10000 حالة مقارنة.

إلى هنا لا مشكلة، لأنّ محرّكات جافاسكربت سريعة جدًا، والمرور على 1000 عنصر في المصفوفة يحدث في بضعة ميكروثوان. ولكنّا هنا في حلقة for نُجري هذه الشروط لكلّ عنصر من arr. فإن كانت arr.length تساوي 10000 فيعني أنّا سنُجري 10000*10000 = مئة مليون حالة مقارنة. كثير جدًا.

إذًا، فهذا الحل ينفع للمصفوفات الصغيرة فقط. سنرى لاحقًا في الفصل كيف نحسّن هذا الكود.

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

اقرأ أيضًا


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

أفضل التعليقات

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



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...