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

الكائنات المكرَّرة (Iterables) في جافاسكربت


صفا الفليج

الكائنات المُكرَّرة (Iterables) هي مفهوم أعمّ من المصفوفات. تتيح لنا هذه الكائنات تحويل أيّ كائن إلى ”كائن يمكن تكراره“ فيمكننا استعماله في حلقة for..of.

بالطبع فالمصفوفات يمكن تكرارها، ولكن هناك كائنات أخرى (مضمّنة في أصل اللغة) يمكن تكرارها أيضًا، مثل السلاسل النصية.

لو لم يكن الكائن مصفوفة تقنيًا، ولكن يمكننا تمثيله على أنّه تجميعة من العناصر (النوع list، والنوع set)، فصياغة for..of ممتازة لنمرّ على عناصره. لذا دعنا نرى كيف يمكن توظيف هذه ”المُكرَّرات“.

Symbol.iterator

يمكن لنا أن نُدرك هذا المفهوم -مفهوم المُكرَّرات أعني- بأن نصنع واحدًا بنفسنا. لنقل أنّ لدينا كائن وهو ليس بمصفوفة بأيّ شكل، ولكن يمكن توظيفه لحلقة for..of. مثلًا كائن المدى هذا range يُمثّل مجموعة متتالية من الأعداد.

let range = {
  from: 1,
  to: 5
};

// ‫‏نُريد أن تعمل for..of هكذا:
// for(let num of range) ... num=1,2,3,4,5

لنُضيف خاصية التكرار إلى range (فتعمل بهذا for..of)، علينا إضافة تابِع إلى الكائن بالاسم Symbol.iterator (وهو رمز خاصّ في اللغة يتيح لنا هذه الميزة).

  1. حين تبدأ for..of، تنادي ذلك التابِع مرة واحدة (أو تعرض الأخطاء المعروفة لو لم تجدها). على هذا التابِع إعادة مُكرِّر/iterator، أي كائنًا له التابِع next.
  2. بعدها، تعمل for..of مع ذلك الكائن المُعاد فقط لا غير.
  3. حين تحتاج for..of القيمة التالية، تستدعي next()‎ لذاك الكائن.
  4. يجب أن يكون ناتج next()‎ بالشكل هذا {done: Boolean, value: any}، حيث لو كانت done=true فيعني أن التكرار اكتمل، وإلّا فقيمة value هي التالية.

إليك النص الكامل لتنفيذ كائن range (مع الملاحظات):

let range = {
  from: 1,
  to: 5
};

// ‫‏1. حين ننادي for..of فهي تنادي هذه
range[Symbol.iterator] = function() {

  // ‫‏...وتُعيد الكائن المُكرِّر:
  // ‫‏2. بعد ذلك تعمل for..of مع هذا المُكرِّر، طالبةً منه القيم التالية
  return {
    current: this.from,
    last: this.to,      

    // ‫‏3. يُستدعى next()‎ في كلّ كرّة في حلقة for..of
    next() {

      // ‫‏4. يجب أن يُعيد القيمة كائنًا كهذا {done:.., value :...‎}
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// ‫‏والآن تعمل!
for (let num of range) {
  alert(num); // ‫‏1، ثمّ 2 فَ‍ 3 فَ‍ 4 فَ‍ 5
}

لاحظ الميزة الأساس للمُكرَّرات: فصل الاهتمامات.

  • عنصر range ذاته ليس له التابِع next()‎.
  • بدل ذلك، يُنشأ كائن آخر (أي "المُكرَّر") عند استدعاء range[Symbol.iterator]()‎، وتابِعه next()‎ يُولّد قيم التكرار.

الخلاصة هي أنّ كائن المُكرَّر منفصل عن الكائن الذي يُكرِّره هذا المُكرَّر.

نظريًا، يمكننا دمجهما معًا واستعمال كائن range نفسه مُتعدَّدًا لتبسيط الكود أكثر. هكذا تمامًا:

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    this.current = this.from;
    return this;
  },

  next() {
    if (this.current <= this.to) {
      return { done: false, value: this.current++ };
    } else {
      return { done: true };
    }
  }
};

for (let num of range) {
  alert(num); // ‏1، ثمّ 2 فَ‍ 3 فَ‍ 4 فَ‍ 5

الآن صار يُعيد range[Symbol.iterator]()‎ كائن range نفسه: وهذا الكائن فيه تابِع next()‎ اللازم كما ويتذكّر حالة التعداد الحالية في this.current. الشيفرة أبسط، أجل. وأحيانًا لا بأس به هكذا.

المشكلة هنا هي استحالة وجود حلقتي for..of تعملان على الكائن في آن واحد، إذ سيتشاركان حالة التعداد؛ فليس هناك إلا مُتعدَّد واحد: الكائن نفسه. لكن أصلًا وجود حلقتي for..of نادر، حتى في العمليات غير المتزامَنة.

مُكرَّرات لا تنتهي يمكن أيضًا ألا تنتهي المُكرَّرات أبدًا. فمثلًا لا ينتهي المدى range لو صار range.to = Infinity. يمكن أيضًا أن نصنع كائن مُتعدَّد يولّد أعدادًا شبه عشوائية (pseudorandom) لانهائية، ستفيد في حالات حرجة.

ما من حدود مفروضة على ناتج next. يمكنه إعادة القيم مهما أراد وبالكم الذي أراد، لا مشكلة. طبعًا، المرور على هذا المُكرَّر بحلقة for..of لن ينتهي أبد الدهر، ولكن يمكننا إيقافها باستعمال break.

السلاسل النصية مُكرَّرة

تُعدّ المصفوفات والسلاسل النصية أكثر المُكرَّرات المقدَّمة من اللغة استعمالًا. بالنسبة إلى السلاسل النصية، فحلقة for..of تمرّ على محارفها:

for (let char of "test") {
  // ‫تتنفّذ أربع مرات: مرة لكلّ محرف
  alert( char ); // ‫t فَ‍ e فَ‍ s فَ‍ t
}

كما وتعمل -كما يجب!- مع الأزواج النائبة أو البديلة (Surrogate Pairs)!

let str = '??';
for (let char of str) {
    alert( char ); // ‫? وثمّ ?
}

نداء المُكرَّر جهارة

لنعرف المُكرَّرات معرفةً أعمق، لنرى كيف يمكن استعمالها جهارةً.

سنمرّ على سلسلة نصية بنفس الطريقة التي يمرّ بها for..of، ولكن هذه المرة ستكون النداءات مباشرة. تُنشِئ هذه الشيفرة مُكرَّرًا لسلسلة نصية وتأخذ القيم منه "يدويًا":

let str = "Hello";

// تنفّذ ما تنفّذه
// for (let char of str) alert(char);

*!*
let iterator = str[Symbol.iterator]();
*/!*

while (true) {
  let result = iterator.next();
  if (result.done) break;
  alert(result.value); // تطبع المحارف واحدًا تلو الآخر
}

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

المُكرَّرات والشبيهات بالمصفوفات

هذان المصطلحان الرسميان يبدوان متشابهين إلى حدّ ما، ولكنّهما مختلفين تمام الاختلاف. حاوِل إدراكهما إدراكًا صحيحًا لتتجنّب هذا الاختلاط لاحقًا.

  • المُكرَّرات كائنات تُنفّذ التابِع Symbol.iterator، كما شرحنا أعلاه.
  • الشبيهات بالمصفوفات كائنات لها فهارس وصفة طول length، وبهذا ”تشبه المصفوفات“… المصطلح يشرح نفسه.

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

السلاسل النصية مثلًا مُكرَّرة (يمكن استعمال for..of عليها)، وشبيهة بالمصفوفات أيضًا (لها فهارس عددية وصفة length). ولكن ليس من الضروري أن يكون المُكرَّر شبيه بالمصفوفة، والعكس صحيح (لا يكون الشبيه بالمصفوفة مُكرَّر). فالمدى range في المثال أعلاه مُكرَّر، ولكنه ليس شبيه بالمصفوفة إذ ليس فيه صفات فهارس وlength.

إليك كائنًا شبيهًا بالمصفوفات وليس مُكرَّرًا:

let arrayLike = { // ‫فيه فهارس وطول => شبيه بالمصفوفات
  0: "Hello",
  1: "World",
  length: 2
};

// ‫خطأ (ما من Symbol.iterator)
for (let item of arrayLike) {}

عادةً، لا تكون لا المُكرَّرات ولا الشبيهات بالمصفوفات مصفوفات حقًا، فليس لها push أو pop وغيرها. لكن هذا غير منطقي. ماذا لو كان لدينا كائن من هذا النوع وأردنا التعامل معه بأنه مصفوفة؟ لنقل أنّا سنعمل على range باستعمال توابِع المصفوفات، كيف السبيل؟

Array.from

التابِع العام Array.from يأخذ مُكرَّرًا أو شبيهًا بالمصفوفات ويحوّله إلى مصفوفة "فعلية". بعدها ننادي توابِع المصفوفات التي نعرفها عليها.

هكذا مثلًا:

let arrayLike = {
  0: "Hello",
  1: "World",
  length: 2
};

let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // ‫تكتب World (أيّ أنّ التابِع عمل)

يأخذ التابِع Array.from في سطر (*) الكائن، ويفحصه أكان مُكرَّرًا أو شبيهًا بالمصفوفات، ويصنع مصفوفة جديدة ينسخ قيم ذلك الكائن فيها.

ذات الأمر للمُكرَّرات:

// ‫نأخذ range من المثال أعلاه
let arr = Array.from(range);
alert(arr); // ‫تكتب 1,2,3,4,5 (تحويل toString للمصفوفة يعمل)

والصياغة الكاملة للتابِع Array.from تتيح لنا تقديم دالة ”خريطة“ اختيارية:

Array.from(obj[, mapFn, thisArg])

يمكن أن يكون الوسيط الاختياري الثاني mapFn دالةً تُطبّق على كلّ عنصر قبل إضافته للمصفوفة، ويتيح thisArg ضبط ماهيّة this للتابِع.

مثال:

// ‫نأخذ range من المثال أعلاه

// ‫نُربّع كلّ عدد
let arr = Array.from(range, num => num * num);

alert(arr); // 1,4,9,16,25

هنا نستعمل Array.from لتحويل سلسلة نصية إلى مصفوفة من المحارف:

let str = '??';

// يقسم ‫str إلى مصفوفة من المحارف
let chars = Array.from(str);

alert(chars[0]); // ?
alert(chars[1]); // ?
alert(chars.length); // 2

على العكس من str.split، فهي هنا تعتمد على طبيعة تكراريّة السلسلة النصية، ولهذا تعمل كما ينبغي (كما تعمل for..of) مع الأزواج النائبة.

هنا أيضًا تقوم بذات الفعل، نظريًا:

let str = '??';

let chars = []; // ‫داخليًا، تُنفّذ Array.from ذات الحلقة
for (let char of str) {
  chars.push(char);
}

alert(chars);

…ولكن تلك أقصر.

يمكننا أيضًا صناعة تابِع slice مبني عليها يحترم الأزواج النائبة.

function slice(str, start, end) {
  return Array.from(str).slice(start, end).join('');
}

let str = '???';

alert( slice(str, 1, 3) ); // ??

// ‫التابِع الأصيل/native في اللغة لا يدعم الأزواج النائبة
alert( str.slice(1, 3) ); // ‫يُولّد نصّ ”قمامة“ (قطعتين من أزواج نائبة مختلفة)

خلاصة

تُدعى الكائنات التي يمكن استعمالها في for..of بالمُكرَّرات (Iterables).

  • على المُكرَّرات (تقنيًا) تنفيذ التابِع بالاسم Symbol.iterator.
    • يُدعى ناتج obj[System.iterator]‎ بالمُكرَّر. يتعامل المُكرَّر بعملية التكرار.
    • يجب أن يحتوي المُكرَّر التابِع بالاسم next()‎ حيث يُعيد كائن {done: Boolean, value: any}… تُشير done:true هنا بأنّ التكرار اكتمل، وإلّا فَـ value هي القيمة التالية.
  • تُنادي الحلقة for..of التابِع Symbol.iterator تلقائيًا عند تنفيذها، ولكن يمكننا أيضًا فعل ذلك يدويًا.
  • تُنفّذ المُكرَّرات المضمّنة في اللغة Symbol.iterator (مثل السلاسل النصية والمصفوفات).
  • مُكرَّر السلاسل النصية يفهم الأزواج البديلة.

تُدعى الكائنات التي فيها صفات فهارس وصفة طول length بالشبيهات بالمصفوفات. يمكن أيضًا أن تكون لها صفات وتوابِع أخرى، إلّا أنّ ليس فيها توابِع المصفوفات المضمّنة في بنية اللغة.

لو نظرنا ورأينا مواصفات اللغة، فسنرى بأنّ أغلب التوابِع المضمّنة فيها تتعامل مع المصفوفات على أنّها مُكرَّرات أو شبيهات بالمصفوفات بدل أن تكون مصفوفات ”حقيقية“؛ هكذا تصير أكثر تجرّديّة (abstract).

تصنع Array.from(obj[, mapFn, thisArg])‎ مصفوفةً Array حقيقية من المُكرَّر أو الشبيه بالمصفوفات obj، بهذا يمكن استعمال توابِع المصفوفات عليها. يتيح لنا الوسيطين mapFn وthisArg تقديم دالة لكلّ عنصر من عناصرها.

ترجمة -وبتصرف- للفصل Iterables من كتاب 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.


×
×
  • أضف...