علی صادقی نیا
علی صادقی نیا
خواندن ۷ دقیقه·۳ سال پیش

مفهوم function closures در جاوا اسکریپت

در این نوشتار سعی دارم یک مفهوم جالب و "آدم سر ذوق بیار" رو برای شما تشریح کنم. البته بسته به اینکه شما چقدر با جاوا اسکریپت آشنا هستید، شاید توضیحات اولیه براتون جدید نباشه؛ اما انرژی تون رو نگه دارید و همراه من بمونید:

شمایی از همپوشانی محدوده های بوجود آورنده کلاسور
شمایی از همپوشانی محدوده های بوجود آورنده کلاسور


اول: Scope چیه؟

"محدوده دسترسی" یکی از اون مفاهیم مشترک بین زبان های برنامه نویسی به حساب میاد. توضیحش یک مقدار انتزاعی به نظر میرسه؛ پس سعی میکنم به جای تعریف scope در قالب یه دسته کد مفهومش رو برای شما شرح بدم:

var v1 = 1; var v2 = 2; function myFunc1(){ //statements } function myFunc2(){ //statements } var v3 = 3; function myFunc3(){ //statements } var v4 = 4;

داخل بدنه اصلی کد بالا، 4 متغیر تعریف و مقدار دهی شده؛ دوتا در ابتدای کد، یکی در میانه کد مابین تعریف تابع myFunc2 و تابع myFunc3 و دیگری در انتهای کد. از طرفی با سه بلوک مربوط به محدوده داخلی توابع مواجهیم که هر یک با {} مشخص شده اند. بیان ساده و سر راست ماجرا اینه که "محدوده" اصلی کد در خارج از بلوک هر تابع به عنوان "محدوده دسترسی جهانی" یا Global Scope شناخته میشه و متغیرهایی که داخل این محدوده تعریف شدند، متغیرهای جهانی هستند. از طرفی محدوده های داخل بلوک هر تابع، به عنوان "محدوده دسترسی محلی" مختص همان تابع یا Local Scope شناخته می شه و به هر متغیری که داخل اون تعریف بشه، متغیر محلی اون تابع میگن. بر اساس این دو تعریف "جهانی/محلی" باید نکات زیر رو به یاد داشته باشیم:

  • عمر متغیرهای جهانی تا پایان اجرای کد می باشد.
  • عمر متغیرهای محلی تا انتهای اجرای تابع حاوی آن است. مثلا یک متغیر محلی داخل تابع myFunc2 از زمانی که این تابع فراخوان می شود (invoice) تا زمانی که اجرای آن تمام می گردد، معنادار است و بعد از آن حذف شده و قابل دسترسی نیست.
  • کدهای نوشته شده در یک محدوده محلی، مثلا داخل تابع myFunc1، به تمامی متغیرهای تعریف شده در محدوده جهانی دسترسی دارند. مثلا داخل بلوک تابع myFunc1 می توان مقدار متغیر var1 را تغییر داد یا از آن برای یک عملیات ریاضی استفاده کرد.
  • کدهای نوشته شده در محدوده جهانی، به متغیرهای محلی دسترسی ندارند. مثلا نمی توانیم مقدار پیشفرض یک متغیر داخل بلوک تابعی رو از بیرون از اون تغییر بدیم.
  • از داخل یک محدوده محلی، نمی توان به محدوده محلی دیگری دسترسی داشت! مثلا از داخل تابع اول، نمی توان مقدار یک متغیر تعریف شده داخل تابع دوم را خواند.

خلاصه که:

متغیرهای داخل هر تابع مختص همان تابع بوده و بدون اجرای تابع نمی توان به آن دسترسی داشت. اما متغیرهای جهانی در تمامی محدوده های محلی قابل دسترسی هستند.

حالا با چیزهایی که اینجا یاد گرفتیم، به کد زیر نگاه کنید؛ به نظر شما نتیجه ای که داخل کنسول می بینیم چیه؟ قبل از ادامه مطالعه بهتره، این کد رو امتحان کنید و نتیجه رو ببینید.

function outter(){ let a = 3; function inner(b = 2){ return a+b; } return inner; } var func = outter(); console.log(func(4));

دوم: مفهوم کلاسور


کد بالا رو اجرا کردید؟ تابع func آرگومانش رو با عدد 3 جمع می کنه و حاصل رو بر می گردونه.

در این کد طبق چیزهایی که تا الان می دونیم، متغیر a، محلی تابع outter و متغیر b محلی تابع inner به حساب میاد. پس وقتی تابع inner رو به متغیر جهانی func نسبت دادیم، متغیرهاش رو هم به همراهش بردیم و ضمن اجرای ()func متغیر b هم معنادار میشه. تابع outter تنها یک بار حین تخصیص به func اجرا شده؛ اما هر بار ()func فرخوان بشه، تابع outter که اجرا نمی شه! یعنی توقع داریم متغیر a برای کد ما بی معنی باشه و بنابراین توقع داریم داخل کنسول یه خطای قرمز ببینیم که : "بزرگوار! a رو که declare نکرده استفاده کردی." اما در کمال تعجب می بینیم که کد ما a=3 رو در نظر گرفته و در اجرای ()func ازش استفاده کرده!!!!

یه لحظه به خودمون وقت بدیم؛ افکارمون رو منظم کنیم. پیشنهاد می کنم یه بار دیگه پاراگراف بالایی رو بخونید تا مطمئن بشیم ماجرا رو دقیقا متوجه شدید.

حالا میخوام یه نکته ای رو راجع به scope واضح کنم که احتمالا بنا به استقرا خودتون اون رو فرض کردید:

گفتیم هر تابع به scope جهانی دسترسی داره؛ دقیق تر این هست که بگیم هر تابع به "محدوده بالادست" خودش دسترسی داره؛ خواه محدوده جهانی باشه خواه محدوده یک تابع بیرونی تابع ما.

حالا اگر ما تابعی داشته باشیم که داخل محدوده تابع دیگه نوشته شده باشه، و تابع بیرونی اون رو به عنوان خروجی return کرده باشه (دقت کنید که باید خود شی تابع رو برگردونده باشه و نه اون رو اجرا کنه؛ دقیقا عین سینتکس کد بالا!)، بعد از آنکه نتیجه اجرای تابع بیرونی رو به یک متغیر نسبت دادیم (و محتوای متغیر شد همون تابع داخلی)، کاری کردیم که تابع داخلی، مقادیر متغیرهای محدوده بالادست خودش رو حین اجرا یادآوری کنه؛ بدون اینکه متغیر خارجی اجرا بشه!

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

خب حالا که چی؟ "این لقمه رو دور سر گردوندن" به چه کار میاد؟

برای اینکه کاربردش رو بهتر متوجه بشید، به مساله زیر دقت کنید.


سوم: معمای شمارنده


صورت مسأله معمای شمارنده یا "counter dilemma" اینه: یک تابع بنویسید که هربار فراخوانی بشه، یه بار بشماره و یکی به شمارنده اضافه بشه؛ سپس مقدار شمارنده رو بهمون بده. یعنی مثلا مقدار اولیه شمارنده صفره، اما با هر بار نوشتن ()add یکی بهش اضافه شده و مقدار جدید برگردانده بشه.

یا علی! دست به کار بشید ببینم چطوری این مساله رو حل می کنید؟

راهکار اولیه:

بعضی از شما ممکنه برای حل معما، راهکاری شبیه این داشته باشید:

// Initiate counter let counter = 0; // Function to increment counter function add() { counter += 1; } // Call add() 3 times add(); add(); add();

با هر بار فرخوانی ()add یکی به counter اضافه می شه. این کد کار میکنه؛ اما مشکل بزرگش اینه که متغیر شمارنده (counter) جهانیه، یعنی میشه به راحتی بدون اینکه ()add در جریان قرار بگیره اون رو تغییر داد. بنابراین میشه شمارنده رو با مشکل مواجه کرد.

اصلاحیه اول:

برای بهتر کردن این راهکار، میشه کد رو به شکل زیر تغییر داد:

// Initiate counter let counter = 0; // Function to increment counter function add() { let counter = 0; counter += 1; } // Call add() 3 times add(); add(); add();

حالا نه تنها کد بهتر نشد، بلکه حالا شمارنده اصلا نمی شماره! در کد بالا با هر چند 3 بار ()add اجرا شده اما counter هم چنان صفره!

بذارید روند حل مسأله رو واضح کنم: ما نیاز داریم تنها یک بار شمارنده صفر بشه تا مقدار اولیه شمارش معلوم باشه. اما بنا به چیزایی که گفتیم این تعریف و مقداردهی اولیه نباید گلوبال باشه (باید محلی باشه). از طرفی تابع ()add باید بتونه به متغیر محلی شمارنده دسترسی پیدا کنه و اون رو تغییر بده. تازه باید این تغییر ذخیره بشه تا اجرای بعدی ()add نتیجه قبلی قابل دسترسی باشه.

اصلاحیه دوم:

// Function to increment counter function add() { let counter = 0; counter += 1; return counter; } // Call add() 3 times add(); add(); add();

این بار ضمن اینکه ()add سه بار فرخوان شده اما هر بار تنها عدد 1 را بر میگردونه. چون متغیر counter متغیر محلیه و عمرش تا پایان اجرای add هست و بعدش مقدار قبلی رو به یاد نمیاره!

تغییر روش!:

بیاید سیاق کد رو عوض کنیم. کد زیر رو ببینید:

// Function to increment counter function add() { let counter = 0; // Anonymous function for counting function plus() {counter += 1;} plus(); return counter; }

حلقه for تو در تو شنیدید؟ این تابع تو در تو هست :)

اینجا با کمک تابع تو در تو و تعریف تابع plus، شمارنده رو محلی کردیم و تابعی که قراره بشماره حالا در این محدوده محلی با هر بار فراخوان، یکی به شمارنده اضافه می کنه.

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

فن استاد:

کدمون رو اینجوری تغییر می دیم:

// Function to increment counter function countCreator(){ let counter = 0; // Inner functoin function plus(){ counter += 1; return counter; } return plus; } // Assign outter function to a variable let add = countCreator(); // Call add() 3 times add(); add(); add();

با این راهکار، ضمن اینکه شمارنده درست کار می کنه، دیگه امکان اخلال در اجرا و تغییر مقدار شمارنده وجود نداره. تابع add ضمن هر بار اجرا، مقدار قبلی خودش رو به یاد میاره و یکی به اون اضافه کرده و نتیجه رو به ما نشون میده.


به نظرم تا همین جا توضیح و تفصیل کافیه. حالا نوبت شماست؛ چه موارد استفاده ای برای کلاسور به ذهنتون می رسه؟

ممنون می شم برای بهتر شدن مطالبم نظرتون رو باهام به اشتراک بذارید.

پ ن: در این مقاله از مطلبی آموزشی در سایت W3Schools بهره گرفته شده است.

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