۱ فوریه ۲۰۲۲

حلقه‌پذیرها

شیءهای حلقه‌پذیر تعمیمی از آرایه‌ها هستند. این مفهومی است که به ما اجازه می‌دهد تا هر شیءای را در حلقه for..of قابل استفاده کنیم.

قطعا آرایه‌ها حلقه‌پذیر هستند. اما شیءهای درون‌ساخت دیگری هم هستند که حلقه‌پذیرند. برای مثال، رشته‌ها هم حلقه‌پذیرند.

اگر یک شیء به طور فنی آرایه نباشد، اما یک مجموعه (لیست یا دسته) از چیزها را نشان دهد، 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 شروع می‌شود، متد را یک بار صدا می‌زند (یا اگر پیدا نشود ارور می‌دهد). متد باید یک حلقه‌زننده را برگرداند – شیءای که متد 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. فراخوانی می‌شود for..of در هر دور حلقه توسط next()
    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, then 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, then 2, 3, 4, 5
}

حالا range[Symbol.iterator]() خود شیء range را برمی‌گرداند: این شیء دارای متد مورد نیاز next() است و فرایند کنونی حلقه‌زدن را در this.current به خاطر می‌سپارد. کوتاه‌تر است؟ بله. و گاهی اوقات این هم خوب است.

اما امتیازی منفی وجود دارد: حالا غیر ممکن است که دو حلقه for..of بتوانند به طور همزمان در شیء حلقه بزنند چون آنها وضعیت حلقه‌زدن را به اشتراک می‌گذارند و آن هم به دلیل اینکه تنها یک حلقه‌زننده وجود دارد – خود شیءها. اما دو for-of همزمان به ندرت پیش می‌آید، حتی در سناریوهای async (همگام‌سازی).

حلقه‌زننده‌های بی‌نهایت

حلقه‌زننده‌های بی‌نهایت هم ممکن است ایجاد شوند. برای مثل، range به ازای range.to = Infinity بی‌نهایت می‌شود. یا ما می‌توانیم یک شیء حلقه‌پذیر را که یک دنباله بی‌نهایت از شبه اعداد ایجاد می‌کند بسازیم. می‌تواند مفید هم باشد.

هیچ محدودیتی برای next وجود ندارد، این متد می‌تواند مقدارهای بیشتر و بیشتری برگرداند و این عادی است.

قطعا، حلقه for..of درون چنین حلقه‌پذیری پایان‌ناپذیر خواهد بود. اما می‌توانیم آن را همیشه با break متوقف کنیم.

رشته حلقه‌پذیر است

آرایه‌ها و رشته‌ها به عنوان حلقه‌پذیرهای درون ساخت بیشترین استفاده را دارند.

برای یک رشته، for..of در کاراکترهای آن حلقه می‌زند:

for (let char of "test") {
  // چهار بار اجرا می‌شود: یک بار برای هر کاراکتر
  alert( char ); // t, then e, then s, then t
}

و با جفت‌های جایگیر به درستی کار می‌کند!

let str = '𝒳😂';
for (let char of str) {
    alert( char ); // 𝒳, and then 😂
}

صدا زدن یک حلقه‌زننده به طور ضمنی

برای فهم عمیق‌تر، بیایید ببینیم چگونه از حلقه‌زننده به طور ضمنی استفاده کنیم.

ما در یک رشته دقیقا به همان روش 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 = { // است => شبه آرایه length دارای ایندکس و
  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 فرض می‌کنیم

// به توان 2 رساندن هر عدد
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) ); // 😂𩷶

// متد اصلی از جفت‌های جایگیر پشتیبانی نمی‌کند
alert( str.slice(1, 3) ); // چرت و پرت (دو قطعه از جفت‌های جایگیر متفاوت)

خلاصه

شیءهایی که بتوانند در for..of استفاده شوند، حلقه‌پذیر نامیده می‌شوند.

  • به طور فنی، حلقه‌پذیرها باید متدی به اسم Symbol.iterator را پیاده‌سازی کنند.
    • نتیجه فراخوانی obj[Symbol.iterator]() باید یک حلقه‌زننده باشد که فرایند حلقه‌زدن‌های بعدی را مدیریت می‌کند.
    • یک حلقه‌زننده باید متدی به نام next() داشته باشد که یک شیء به صورت {done: Boolean, value: any} را برمی‌گرداند، اینجا done:true نشان دهنده پایان فرایند حلقه‌زدن است، در غیر این صورت value مقدار بعدی است.
  • متد Symbol.iterator توسط for..of به صورت خودکار صدا زده می‌شود اما ما می‌توانیم به طور مستقیم این کار را انجام دهیم.
  • حلقه‌پذیرهای داخلی مانند رشته‌ها یا آرایه‌ها هم Symbol.iterator را پیاده‌سازی می‌کنند.
  • حلقه‌زننده رشته‌ای از جفت‌های جایگیر آگاه است.

شیءهایی که دارای ویژگی‌های ایندکسی و length هستند شبه آرایه نامیده می‌شوند. چنین شیءهایی ممکن است ویژگی‌ها و متدهای دیگری هم داشته باشند اما متدهای آرایه را ندارند.

اگر ما به خصوصیات زبان نگاهی بیاندازیم – خواهیم دید که بیشتر متدهای درون‌ساخت، فرض می‌کنند که به جای آرایه‌های «واقعی» با حلقه‌پذیرها یا شبه آرایه‌ها کار می‌کنند چون اینگونه کوتاه‌تر است.

Array.from(obj[, mapFn, thisArg]) یک آرایه واقعی از یک حلقه‌پذیر یا شبه آرایه‌ی obj می‌سازد و سپس می‌توانیم بر روی آن از متدهای آرایه‌استفاده کنیم. آرگومان اختیاری mapFn و thisArg به ما اجازه اعمال یک تابع بر روی هر یک از المان‌ها را می‌دهند.

نقشه آموزش