چرا یادگیری closure های جاوااسکریپت لازم است؟

ترجمه و تالیف : عرفان حشمتی
تاریخ انتشار : 24 مرداد 99
خواندن در 6 دقیقه
دسته بندی ها : جاوا اسکریپت

درک کامل از closure ها می‌تواند یک مجوز برای تبدیل شدن به یک توسعه دهنده جاوااسکریپت باشد.

دلیلی که ایجاد closure ها را دشوار می‌کند این است که اغلب رو به عقب آموزش داده می‌شوند. ممکن است به شما آموخته شود که closure چیست، اما ممکن است نفهمید که چگونه درون کد یک توسعه دهنده مفید هستند.

بنابراین چرا closure ها در کدهای جاوااسکریپت روزمره اهمیت دارند؟

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

به عنوان مثال می‌خواهیم یک برنامه کلون از سایت وبلاگ نویسی تولید کنیم و هر کاربر بتواند پست‌های مختلف را ببیند و لایک کند.

هر زمان که کاربر روی دکمه لایک کلیک کند، هر بار مقدار آن افزایش یابد.

چرا یادگیری closure های جاوااسکریپت لازم است؟

تابعی که هر بار با افزایش تعداد 1 عدد کنترل می‌شود، به نام handLikePost نامگذاری شده است و ما تعداد لایک‌ها را با متغیری به نام likeCount در نظر می‌گیریم:

// global scope
let likeCount = 0;

function handleLikePost() {
  // function scope
  likeCount = likeCount + 1;
}

handleLikePost();
console.log("like count:", likeCount); // like count: 1

هر زمان که کاربر یک پست را پسند کند، آن را handLikePost می‌نامیم و likeCount یک عدد اضافه می‌گردد و این کار می‌کند زیرا ما می‌دانیم که توابع می‌توانند به متغیرهای خارج از خود دسترسی پیدا کنند.

به عبارت دیگر، توابع می‌توانند به متغیرهای تعریف شده در هر جای والد دسترسی پیدا کنند.

با این وجود این کد مشکل دارد. از آنجا که likeCount در محدوده global است و در همه توابع نیست و یک متغیر global است. متغیرهای global توسط هر بیت کد یا تابع دیگر در برنامه ما قابل استفاده است و تغییر می‌یابد.

به عنوان مثال، اگر بعد از تابع خود اشتباها likeCount را بر روی صفر تنظیم کنیم چه می‌شود؟

let likeCount = 0;

function handleLikePost() {
  likeCount = likeCount + 1;
}

handleLikePost();
likeCount = 0;
console.log("like count:", likeCount); // like count: 0 

طبیعتا likeCount هرگز نمی‌تواند از "0" افزایش یابد.

هنگامی که یک تابع فقط به یک قطعه داده خاص نیاز دارد، باید به صورت محلی وجود داشته باشد (درون آن تابع).

حالا بیایید likeCount را در تابع خود قرار دهیم:

function handleLikePost() {
  // likeCount moved from global scope to function scope
  let likeCount = 0;
  likeCount = likeCount + 1;
}

توجه داشته باشید که یک راه کوتاه‌تر برای نوشتن کد در جایی که مانند Count افزایش می‌دهیم وجود دارد. به جای اینکه بگوییم likeCount برابر است با مقدار قبلی likeCount و اضافه کردن یکی مثل این، ما فقط می‌توانیم از عملگر + = مانند آنچه که در زیر آمده استفاده کنیم:

function handleLikePost() {
  let likeCount = 0;
  likeCount += 1;
}

و برای این که مانند گذشته کار کند و مقدار count را بگیرد، لازم است Console.log خود را نیز به این بخش اضافه کنیم.

function handleLikePost() {
  let likeCount = 0;
  likeCount += 1;
  console.log("like count:", likeCount);
}

handleLikePost(); // like count: 1

هنوز هم مانند گذشته به درستی کار می‌کند.

handleLikePost(); // like count: 1
handleLikePost(); // like count: 1
handleLikePost(); // like count: 1

اما وقتی این کد را اجرا می‌کنیم، مشکلی پیش می‌آید.

انتظار داریم شاهد افزایش likeCount باشیم، اما هر بار فقط "1" را می بینیم. چرا این اتفاق رخ می‌دهد؟

لحظه‌ای به کد ما نگاه کنید و سعی کنید بفهمید که چرا likeCount دیگر افزوده نمی‌شود.

بیایید نگاهی به تابع handleLikePost و نحوه عملکرد آن بیندازیم:

function handleLikePost() {
  let likeCount = 0;
  likeCount += 1;
  console.log("like count:", likeCount);
}

هر بار که از آن استفاده می‌کنیم، این متغیر likeCount را که به آن مقدار "0" داده می‌شود، دوباره می‌سازیم.

جای تعجب نیست که نمی‌توانیم تعداد دفعات فراخوانی را پیگیری کنیم. هر بار "0" را تنظیم می‌کند، سپس "1" را افزایش می‌دهد، پس از آن تابع در حال اجرا به پایان می‌رسد.

بنابراین اینجا گیر کرده‌ایم. متغیر ما باید در داخل تابع handLikePost باشد، اما نمی‌توانیم تعداد را حفظ کنیم.

ما به چیزی نیاز داریم که این امکان را می‌دهد مقدار likeCount را بین فراخوانی‌های تابعی حفظ یا به خاطر بسپاریم.

چه می‌شود اگر چیزی را امتحان کنیم که در ابتدا کمی عجیب به نظر می‌رسید. اگر سعی کنیم تابع دیگری را در تابع خود داشته باشیم چه می‌شود:

function handleLikePost() {
  let likeCount = 0;
  likeCount += 1;
  function() {

  }
}

handleLikePost();

در اینجا می‌خواهیم این تابع را addLike نامگذاری کنیم. دلیل آن چیست؟ زیرا اکنون مسئولیت افزایش متغیر likeCount را بر عهده دارد.

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

addLike اکنون مسئولیت افزایش likeCount ما را بر عهده خواهد داشت، بنابراین ما خطی را که در آن "1" را اضافه می‌کردیم، به تابع داخلی خود منتقل می‌کنیم.

function handleLikePost() {
  let likeCount = 0;
  function addLike() {
    likeCount += 1;
  }
}

اگر بخواهیم این تابع addLike را در handLikePost صدا کنیم، چه می‌شود؟

تمام آنچه که اتفاق می‌افتد این است که addLike مانند like اضافه می‌شود، اما دیگر متغیر likeCount از بین می‌رود. بنابراین دوباره ارزش خود را از دست می‌دهد و نتیجه صفر خواهد شد.

اما به جای اینکه addLike را درون تابع محصور خود فراخوانی کنیم، اگر آن را خارج از تابع صدا کنیم، چه می‌شود؟ این حتی عجیب به نظر می‌رسد، اما چگونه این کار را می‌کنیم؟

ما در این مرحله می‌دانیم که توابع مقادیر بازگشتی را نشان می‌دهند. به عنوان مثال، ما می‌توانیم مقدار likeCount خود را در انتهای LikePost برگردانیم تا آن را به قسمت‌های دیگر برنامه خود منتقل کنیم:

function handleLikePost() {
  let likeCount = 0;
  function addLike() {
    likeCount += 1;
  }
  addLike();
  return likeCount;
}

اما به جای انجام این کار، بیایید likeCount را در addLike برگردانیم و سپس تابع addLike را به خودش ارجاع دهیم:

function handleLikePost() {
  let likeCount = 0;
  return function addLike() {
    likeCount += 1;
    return likeCount;
  };
  // addLike();
}

handleLikePost();

حالا ممکن است عجیب به نظر برسد، اما این در جاوااسکریپت مجاز است. ما می‌توانیم از توابع درست مانند هر مقدار دیگر در جاوااسکریپت استفاده کنیم. این بدان معنی است که یک تابع را می‌توان از یک تابع دیگر برگرداند. با برگرداندن تابع داخلی، می‌توان آن را خارج از تابع محصور کننده آن فراخوانی کرد.

اما چگونه این کار را انجام دهیم؟ برای یک دقیقه در مورد این موضوع فکر کنید و ببینید که آیا می‌توانید آن را کشف کنید ...

ابتدا، برای اینکه بهتر ببینیم چه اتفاقی می‌افتد، وقتی فراخوانی کردیم، بیایید (console.log (handleLikePost را ببینیم که چه کار می‌کند:

function handleLikePost() {
  let likeCount = 0;
  return function addLike() {
    likeCount += 1;
    return likeCount;
  };
}

console.log(handleLikePost()); // ƒ addLike()

با کمال تعجب، تابع addLike را گرفته‌ایم. چرا؟ زیرا بعدا در حال برگرداندن آن هستیم.

برای اینکه آن را صدا کنیم، آیا نمی‌توانیم آن را فقط در متغیر دیگری قرار دهیم؟ همانطور که پیش از این گفتیم، توابع می‌توانند مانند سایر مقادیر موجود در جاوااسکریپت استفاده شوند. اگر بتوانیم آن را از یک تابع برگردانیم، می‌توان آن را نیز در یک متغیر قرار داد. بنابراین بگذارید آن را در یک متغیر جدید مانند زیر قرار دهیم:

function handleLikePost() {
  let likeCount = 0;
  return function addLike() {
    likeCount += 1;
    return likeCount;
  };
}

const like = handleLikePost();

و در آخر بیایید call را صدا کنیم. ما چندین بار آن‌را انجام خواهیم داد و هر بار console.log را نتیجه می‌گیریم:

function handleLikePost() {
  let likeCount = 0;
  return function addLike() {
    likeCount += 1;
    return likeCount;
  };
}

const like = handleLikePost();

console.log(like()); // 1
console.log(like()); // 2
console.log(like()); // 3

بالاخره likeCount ما حفظ می‌شود. هربار که like را صدا می‌کنیم، likeCount از مقدار قبلی خود افزایش می‌یابد.

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

Closure چگونه کار می‌کند، خط به خط بررسی می‌کنیم

مطمئنا اجرای ما این بود، اما چگونه مقدار likeCount را بین فراخوانی‌های تابعی حفظ کردیم؟

function handleLikePost() {
  let likeCount = 0;
  return function addLike() {
    likeCount += 1;
    return likeCount;
  };
}

const like = handleLikePost();

console.log(like()); // 1

۱. تابع بیرونی handleLikePost اجرا شده است و نمونه‌ای از تابع داخلی addLike ایجاد می‌کند. این تابع بعد از متغیر likeCount که بالاتر است، بسته می‌شود.

۲. ما تابع addLike را از خارج از محدوده‌ای که در آن اعلام شده است، صدا کردیم و این کار را با برگرداندن تابع داخلی از قسمت بیرونی و ذخیره کردن یک مرجع به آن، به نام like انجام دادیم تا آن‌را صدا کنیم.

۳. وقتی تابع مشابه به پایان برسد، به طور معمول انتظار داریم تمام متغیرهای آن در زباله جمع‌آوری شوند (از حافظه خارج می‌شوند، این یک فرآیند خودکار است که کامپایلر جاوااسکریپت انجام می‌دهد). انتظار داریم که هر تابعی مانند آن به محض انجام این کار از بین برود، اما این کار انجام نمی‌شود.

اما دلیل آن چیست؟ درست است closure این کار را می‌کند.

از آنجایی که نمونه‌های تابع داخلی هنوز برقرار هستند (به صورت اختصاص داده شده)، closure همچنان متغیرهای countLike را حفظ می‌کند.

شاید فکر کنید داشتن یک تابع نوشته شده در یک تابع دیگر، فقط مانند تابعی است که در محدوده global نوشته شده است، اما اینطور نیست.

به همین دلیل است که closure توابع را بسیار قدرتمند می‌کند، زیرا این یک ویژگی خاص است که در هر زبان دیگری وجود ندارد.

طول عمر یک متغیر

برای درک بهتر closure ها، باید بدانیم که جاوااسکریپت چگونه با متغیرهای ایجاد شده رفتار می‌کند. ممکن است برای شما این سوال پیش آمده باشد که هنگام بسته شدن صفحه یا رفتن به صفحه دیگری در یک برنامه، چه اتفاقی برای متغیرها رخ می‌دهد. متغیرها چه مدت قابل استفاده می‌مانند؟

متغیرهای global تا زمانی که برنامه حذف نشود، برقرار هستند. به عنوان مثال وقتی پنجره را می‌بندید، آن‌ها برای زنده ماندن برنامه هنوز پابرجا هستند.

با این حال، متغیرهای محلی عمر کوتاهی دارند. آن‌ها هنگام فراخوانی تابع ایجاد می‌شوند و پس از اتمام تابع حذف می‌گردند.

بنابراین قبل از آن، جایی که likeCount فقط یک متغیر محلی بود، وقتی این تابع اجرا شد، متغیر likeCount در ابتدای تابع ایجاد شد و پس از اتمام اجرای آن، از بین رفت.

Closure ها متغیرهای محلی را نگه می‌دارند

بعضی اوقات گفته می‌شود که closure های جاوااسکریپت شبیه به عکس‌های فوری هستند، تصویری از برنامه ما در زمانی معین. این یک تصور غلط است که می‌توانیم با اضافه کردن ویژگی دیگری به قابلیت دکمه مانند برنامه، آن را برطرف کنیم.

بهتر است بگوییم که در موارد مناسبی، به کاربران این امکان را می‌دهیم که یک پست را با "دو ضربه" لایک کنند و likeCount را در یک زمان دو بار افزایش دهند.

چگونه این قابلیت را اضافه کنیم؟

روشی دیگر برای انتقال مقادیر به یک تابع، البته از طریق آرگومان‌ها که درست مانند متغیرهای محلی عمل می‌کنند.

بیایید در یک آرگومان به نام گام از تابع عبور کنیم، که به ما این امکان را می‌دهد یک مقدار پویا و تغییر پذیر ارائه دهیم تا بتوانیم تعداد را به جای مقدار "1" افزایش دهیم.

function handleLikePost(step) {
  let likeCount = 0;
  return function addLike() {
    likeCount += step;
    // likeCount += 1;
    return likeCount;
  };
}

در مرحله بعد، بیایید یک تابع ویژه ایجاد کنیم که به ما امکان می‌دهد تا پست هایمان را DoubleLike کنیم. ما برای ساخت آن از "2" به عنوان مقدار گام خود استفاده می‌کنیم و سپس سعی خواهیم کرد که هر دو تابع like و DoubleLike را فراخوانی کنیم:

function handleLikePost(step) {
  let likeCount = 0;
  return function addLike() {
    likeCount += step;
    return likeCount;
  };
}

const like = handleLikePost(1);
const doubleLike = handleLikePost(2);

like(); // 1
like(); // 2

doubleLike(); // 2 (the count is still being preserved!)
doubleLike(); // 4

می‌بینیم که likeCount برای DoubleLike نیز حفظ می‌شود.

چه اتفاقی در اینجا می‌افتد؟

هر نمونه از تابع addLike داخلی زودتر از هر دو متغیر likeCount و step از محدوده تابع خارجی آن با تابع handleLikePost بسته می‌شود. step با گذشت زمان باقی می‌ماند، اما شمارش در هر فراخوانی از تابع داخلی به روز می‌شود. از آنجا که closure زودتر از متغیرها عمل می‌کند و فقط عکس‌های فوری از مقادیر نیست، این بروزرسانی‌ها بین فراخوانی‌های تابعی حفظ می‌شوند.

و اما این کد چه چیزی را به ما نشان می‌دهد. واقعیت این است که آیا می‌توانیم برای تغییر نتیجه تابع خود، در مقادیر دینامیکی قرار بگیریم؟ که آن‌ها هنوز پابرجا هستند! Closure ها، متغیرهای محلی را از توابعی که باید مدت‌ها پیش از بین می‌رفتند، زنده نگه می‌دارد.

به عبارت دیگر، آن‌ها ایستا و تغییر ناپذیر نیستند. Closure متغیرها را حفظ کرده و پیوندی فعال برای آن‌ها ایجاد می‌کند. در نتیجه می‌توانیم از closure استفاده کنیم و با مرور زمان به روزرسانی این متغیرها را انجام دهیم.

Closure ها دقیقا چه هستند؟

اکنون که می‌بینید closure چقدر مفید است، دو معیار برای چیزهایی که می‌توانند closure باشند وجود دارد که هر دوی آن‌ها را دیده‌اید:

  1. closure ها یک ویژگی توابع جاوااسکریپت هستند و فقط تابع اند. هیچ نوع داده دیگری ویژگی آن‌ها را ندارد.
  2. برای مشاهده یک closure، شما باید یک تابع را در یک دامنه متفاوت نسبت به جایی که در ابتدا این تابع تعریف شده بود، اجرا کنید.

چرا باید در مورد closure ها بدانیم؟

بیایید به سؤال اصلی که برای آن این مقاله را تهیه کردیم، پاسخ دهیم. براساس آنچه که ما دیده‌ایم، برای پاسخ به این سؤال مکث کرده و به عقب برگردید. چرا باید به عنوان توسعه دهندگان جاوااسکریپت از closure ها بهره بگیریم؟

closure برای شما و کد شما مهم است زیرا به شما این امکان را می‌دهد که مقادیر را به خاطر بسپارید، که یک ویژگی بسیار قدرتمند و منحصر به فرد در زبانی است که فقط توابع در آن وجود دارد.

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

منبع

گردآوری و تالیف عرفان حشمتی
آفلاین
user-avatar

عرفان حشمتی هستم، مهندس سخت افزار و برنامه نویس و طراح وب سایت، علاقه مند به دنیای آی تی و تکنولوژی، همچنین در حوزه ادیت فیلم و تصویر مطالعه و تمرین می کنم.

دیدگاه‌ها و پرسش‌ها

برای ارسال نظر لازم است ابتدا وارد سایت شوید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید