١٥ ديسمبر ٢٠٢١

توابع المصفوفات (Array methods)

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

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

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

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

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

الوصل splice

كيف نحذف أحد عناصر المصفوفة؟

المصفوفات كائنات، يمكننا تجربة delete وربما تنجح:

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

delete arr[1]; // remove "go"

alert(arr[1]); // undefined

// now arr = ["I",  , "home"];
alert(arr.length); // 3

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

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

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

The arr.splice method is a swiss army knife for arrays. It can do everything: insert, remove and replace elements.

هذه صياغته:

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

It modifies arr starting from the index start: removes deleteCount elements and then inserts elem1, ..., elemN at their place. Returns the array of removed elements.

فهم هذا التابِع بالأمثلة أبسط.

فلنبدأ أولًا بالحذف:

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

arr.splice(1, 1); // from index 1 remove 1 element

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

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

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

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

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

alert( arr ) // now ["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"];

// from index 2
// delete 0
// then insert "complex" and "language"
arr.splice(2, 0, "complex", "language");

alert(arr); // "I", "study", "complex", "language", "JavaScript"
الفهارس السالبة ممكنة أيضًا يمكننا هنا وفي توابِع المصفوفات الأخرى استعمال الفهارس السالبة. وظيفتها تحديد المكان بدءًا من نهاية المصفوفة، هكذا:


```js run
let arr = [1, 2, 5];

// from index -1 (one step from the end)
// delete 0 elements,
// then insert 3 and 4
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، ولكن بدل السلاسل النصية الفرعية، يُعيد المصفوفات الفرعية. إليك المثال الآتي:

For instance:

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

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

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

يمكننا أيضًا استدعائها بلا وُسطاء: يُنشئ 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، ويُعيد فهرسه حيث وجده. ولو لم يجده، يُعيد&#8209;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، فيُعاد العنصر ويتوقّف التعداد
  // ‫لو لم نجد ما نريد نُعيد undefinedd
});

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

  • 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 فتُضاف القائمة إلى مصفوفة النتائج ويتواصل التكرار
  // يُعيد مصفوفة فارغة إن لم يجد شيئًا
});

For instance:

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 the first value is greater than the second
  if (a == b) return 0; // if values are equal
  if (a < b) return -1; // if the first value is less than the second
}

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

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 يمكن أن تحتوي أيّ شيء؟ أيّ شيء من الأعداد أو السلاسل النصية أو الكائنات أو غيرها. كلّ ما لدينا هو مجموعة من العناصر. لترتيبها نحتاج دالة ترتيب تعرف طرقة مقارنة عناصر المصفوفة. مبدئيًا، الترتيب يكون بالسلاسل النصية.

The arr.sort(fn) method implements a generic sorting algorithm. We don’t need to care how it internally works (an optimized quicksort or Timsort most of the time). It will walk the array, compare its elements using the provided function and reorder them, all we need is to provide the fn which does the comparison.

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

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

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

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


```js run
let arr = [ 1, 2, 15 ];

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

alert(arr);  // 1, 2, 15
```
```js
arr.sort( (a, b) => a - b );
```

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

تذكر الـ strings فى مقارنة الخوارزميات؟ انها تُقارن الحروب عن طريق الكود الخاص بها .

بالنسبة للعديد من الحروف الأبجدية ، من الأفضل استخدام str.localeCompare لترتيب الحروف بشكل صحيح على سبيل المثال : دعنا نرتب القليل من البلاد بالألمانية

let countries = ["Österreich", "Andorra", "Vietnam"];

alert(countries.sort((a, b) => (a > b ? 1 : -1))); // Andorra, Vietnam, Österreich (خطأ)

alert(countries.sort((a, b) => a.localeCompare(b))); // Andorra,Österreich,Vietnam (صحيح!)

العكس 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)](mdn:js/String/split‎. يأخذ السلسلة النصية ويقسمها إلى مصفوفة حسب محرف القاسِم 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

اذا ناديت arr.join(glue) فانها تقوم بعمل عكسى لـ `split ، أى أنها تعيد لصق عناصر المصفوفة كما لو أنها تلصقها بمادة لاصقة

مثال :

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

let str = arr.join(";"); // glue the array into a string using ;

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

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

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

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

هذه الصياغة:

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

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

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

  • accumulator – هو نتيجة للكل الدوال السابقة و يتعبر مساويا لـ 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، ونُضيف العنصر التالي في المصفوفة، وهكذا دواليك إلى آخر نداء…

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

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

sum current result
the first call 0 1 1
the second call 1 2 3
the third call 3 3 6
the fourth call 6 4 10
the fifth call 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 []); // same

…ولكن، المصفوفات تستعمل كثيرًا جدًا لدرجة تقديم تابِع خاص لهذا الغرض: 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}
];

// find users, for who army.canJoin returns true
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) بالتعليمة التي تُؤدّي ذات الغرض users.filter(user => army.canJoin(user)). نستعمل الأولى أكثر من الثانية إذ أنّ الناس تفهمها أكثر من تلك.

ملخص

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

A call to users.filter(army.canJoin, army) can be replaced with users.filter(user => army.canJoin(user)), that does the same. The latter is used more often, as it’s a bit easier to understand for most people.

  • 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 مصفوفة أيضًا، فستُنسخ عناصرها بدل…

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

  • To add/remove elements:
    • push(...items) – adds items to the end,
    • pop() – extracts an item from the end,
    • shift() – extracts an item from the beginning,
    • unshift(...items) – adds items to the beginning.
    • splice(pos, deleteCount, ...items) – at index pos deletes deleteCount elements and inserts items.
    • slice(start, end) – creates a new array, copies elements from index start till end (not inclusive) into it.
    • concat(...items) – returns a new array: copies all members of the current one and adds items to it. If any of items is an array, then its elements are taken.
  • للمرور على عناصر المصفوفة:
  • forEach(func) – يستدعي func لكلّ عنصر ولا يُعيد أيّ شيء.

  • To transform the array:

    • map(func) – creates a new array from results of calling func for every element.

    • sort(func) – sorts the array in-place, then returns it.

    • reverse() – reverses the array in-place, then returns it.

    • split/join – convert a string to array and back.

    • reduce/reduceRight(func, initial) – calculate a single value over the array by calling func for each element and passing an intermediate result between the calls.

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

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

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

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

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

  • Additionally:
    • Array.isArray(arr) ‎ يفحص لو كانت arr مصفوفة أم لا. لاحظ أنّ التوابِعsort, reverse و splice تُعدّل المصفوفة نفسها.

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

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

These methods behave sort of like || and && operators: if fn returns a truthy value, arr.some() immediately returns true and stops iterating over the rest of items; if fn returns a falsy value, arr.every() immediately returns false and stops iterating over the rest of items as well.

We can use every to compare arrays:

function arraysEqual(arr1, arr2) {
  return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
}

alert( arraysEqual([1, 2], [1, 2])); // true

For the full list, see the manual.

قد يبدو من النظرة الأولى أن هناك العديد من الطرق ، يصعب تذكرها. ولكن في الواقع هذا أسهل بكثير.

انظر من خلال ورقة الغش فقط لتكون على دراية بها. ثم حل مهام هذا الفصل للممارسة ، بحيث يكون لديك خبرة في أساليب الصفيف.

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

مهمه

الأهمية: 5

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

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

أمثلة:

:

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

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

افتح sandbox بالإختبارات.

function camelize(str) {
  return str
    .split('-') // splits 'my-long-word' into array ['my', 'long', 'word']
    .map(
      // capitalizes first letters of all array items except the first one
      // converts ['my', 'long', 'word'] into ['my', 'Long', 'Word']
      (word, index) => index == 0 ? word : word[0].toUpperCase() + word.slice(1)
    )
    .join(''); // joins ['my', 'Long', 'Word'] into 'myLongWord'
}

افتح الحل الإختبارات في sandbox.

الأهمية: 4

Write a function filterRange(arr, a, b) that gets an array arr, looks for elements with values higher or equal to a and lower or equal to b and return a result as an array.

مثال:

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

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

alert(filtered); // 3,1 (matching values)

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

افتح sandbox بالإختبارات.

function filterRange(arr, a, b) {
  // added brackets around the expression for better readability
  return arr.filter(item => (a <= item && item <= b));
}

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

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

alert( filtered ); // 3,1 (matching values)

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

افتح الحل الإختبارات في sandbox.

الأهمية: 4

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

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

مثال:

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

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

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

افتح sandbox بالإختبارات.

function filterRangeInPlace(arr, a, b) {

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

    // remove if outside of the interval
    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]

افتح الحل الإختبارات في sandbox.

الأهمية: 4
let arr = [5, 2, 1, -10, 8];

// ... your code to sort it in decreasing order

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 (no changes)

We can use slice() to make a copy and run the sort on it:

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

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

let sorted = copySorted(arr);

alert( sorted );
alert( arr );
الأهمية: 5

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

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

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

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

```js
let calc = new Calculator;

alert( calc.calculate("3 + 7") ); // 10
```
  1. بعدها أضِف تابِع 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
  • في هذه المهمة ليس هناك أقواس رياضية أو تعابير معقّدة.
  • تفصل الأعداد والمُعامل مسافة واحدة فقط.
  • يمكنك التعامل مع الأخطاء لو أردت.

افتح sandbox بالإختبارات.

  • يرجى ملاحظة كيفية تخزين الطرق. تتم إضافتها ببساطة إلى خاصية this.methods.
  • يتم إجراء جميع الاختبارات والتحويلات الرقمية بطريقة حساب '. يمكن توسيعه في المستقبل لدعم التعبيرات الأكثر تعقيدًا. في هذه المهمة نفترض أنid` فريد. قد لا يكون هناك عنصران للصفيف بنفس “المعرف”.

يُرجى استخدام طريقة الصفيف .reduce في الحل.

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

افتح الحل الإختبارات في sandbox.

الأهمية: 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 /* ... your code */ = 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

لديك مصفوفة مكونة من userو كل كائن من هذا يحتوى على name و surname و id

اكتب الكود اللازم لانتاج مصفوفة آخرى تحتوى على كائنات بها id و fullName و يكون الـ fullName ناتج من دمج name مع surname.

مثال :

let john = { name: "John", surname: "Smith", id: 1 };
let pete = { name: "Pete", surname: "Hunt", id: 2 };
let mary = { name: "Mary", surname: "Key", id: 3 };

let users = [ john, pete, mary ];

let usersMapped = /* ... your code ... */

/*
usersMapped = [
  { fullName: "John Smith", id: 1 },
  { fullName: "Pete Hunt", id: 2 },
  { fullName: "Mary Key", id: 3 }
]
*/

alert( usersMapped[0].id ) // 1
alert( usersMapped[0].fullName ) // John Smith

لذلك ، فانك بالتأكيد بحاجة لاستخدام الخرائط map فحاول استخدام => للتسهيل فى الاستخدام

let john = { name: "John", surname: "Smith", id: 1 };
let pete = { name: "Pete", surname: "Hunt", id: 2 };
let mary = { name: "Mary", surname: "Key", id: 3 };

let users = [ john, pete, mary ];

let usersMapped = users.map(user => ({
  fullName: `${user.name} ${user.surname}`,
  id: user.id
}));

/*
usersMapped = [
  { fullName: "John Smith", id: 1 },
  { fullName: "Pete Hunt", id: 2 },
  { fullName: "Mary Key", id: 3 }
]
*/

alert( usersMapped[0].id ); // 1
alert( usersMapped[0].fullName ); // John Smith

Please note that in the arrow functions we need to use additional brackets.

We can’t write like this:

let usersMapped = users.map(user => {
  fullName: `${user.name} ${user.surname}`,
  id: user.id
});

As we remember, there are two arrow functions: without body value => expr and with body value => {...}.

Here JavaScript would treat { as the start of function body, not the start of the object. The workaround is to wrap them in the “normal” brackets:

let usersMapped = users.map(user => ({
  fullName: `${user.name} ${user.surname}`,
  id: user.id
}));

Now fine.

الأهمية: 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);

// now: [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);
}

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

// now sorted is: [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] أو أو أو، واحتمال حدوث كلّ حالة متساوٍ.

The simple solution could be:

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

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

That somewhat works, because Math.random() - 0.5 is a random number that may be positive or negative, so the sorting function reorders elements randomly.

But because the sorting function is not meant to be used this way, not all permutations have the same probability.

For instance, consider the code below. It runs shuffle 1000000 times and counts appearances of all possible results:

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

// counts of appearances for all possible permutations
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('')]++;
}

// show counts of all possible permutations
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

An example result (depends on JS engine):

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

We can see the bias clearly: 123 and 213 appear much more often than others.

The result of the code may vary between JavaScript engines, but we can already see that the approach is unreliable.

Why it doesn’t work? Generally speaking, sort is a “black box”: we throw an array and a comparison function into it and expect the array to be sorted. But due to the utter randomness of the comparison the black box goes mad, and how exactly it goes mad depends on the concrete implementation that differs between engines.

There are other good ways to do the task. For instance, there’s a great algorithm called Fisher-Yates shuffle. The idea is to walk the array in the reverse order and swap each element with a random one before it:

function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    let j = Math.floor(Math.random() * (i + 1)); // random index from 0 to i

    // swap elements array[i] and array[j]
    // we use "destructuring assignment" syntax to achieve that
    // you'll find more details about that syntax in later chapters
    // same can be written as:
    // let t = array[i]; array[i] = array[j]; array[j] = t
    [array[i], array[j]] = [array[j], array[i]];
  }
}

Let’s test it the same way:

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

// counts of appearances for all possible permutations
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('')]++;
}

// show counts of all possible permutations
for (let key in count) {
  alert(`${key}: ${count[key]}`);
}

The example output:

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

Looks good now: all permutations appear with the same probability.

Also, performance-wise the Fisher-Yates algorithm is much better, there’s no “sorting” overhead.

الأهمية: 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) {
  /* your code */
}

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

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

افتح sandbox بالإختبارات.

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

  • سنفحص كلّ عنصر ونرى إن كان في المصفوفة الناتجة.
  • إن كان كذلك… نُهمله، وإن لم يكن، نُضيفه إلى المصفوفة.
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 = مئة مليون حالة مقارنة. كثير جدًا.

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

افتح الحل الإختبارات في sandbox.

الأهمية: 4

دعنا نقول أننا نستقبل مصفوفة خاصة بالمستخدمين داخل form مكونة {id:..., name:..., age... }

اكتب دالة groupById(arr) لانشاء كائن منها يحتوى على id كمفتاح و عناصر المصفوفة كقيم

مثال :

let users = [
  { id: "john", name: "John Smith", age: 20 },
  { id: "ann", name: "Ann Smith", age: 24 },
  { id: "pete", name: "Pete Peterson", age: 31 }
];

let usersById = groupById(users);

/*
// after the call we should have:

usersById = {
  john: {id: 'john', name: "John Smith", age: 20},
  ann: {id: 'ann', name: "Ann Smith", age: 24},
  pete: {id: 'pete', name: "Pete Peterson", age: 31},
}
*/

هذه الوظيفة مفيدة حقًا عند العمل مع بيانات الخادم.

في هذه المهمة نفترض أن id فريد. قد لا يكون هناك عنصران للصفيف بنفس “المعرف”.

يُرجى استخدام طريقة الصفيف .reduce في الحل.

افتح sandbox بالإختبارات.

function groupById(array) {
  return array.reduce((obj, value) => {
    obj[value.id] = value;
    return obj;
  }, {})
}

افتح الحل الإختبارات في sandbox.

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