JavaScript Closures چیست؟ راهنمای جامع درک Scope، حافظه و کاربردهای واقعی

JavaScript Closures چیست؟ راهنمای جامع درک Scope، حافظه و کاربردهای واقعی

  • سطح : متوسط
  • مطالعه : 30 دقیقه
  • 180

اگر چند ماه است با JavaScript کار می‌کنید، احتمالاً بارها واژه Closure را شنیده‌اید؛ مفهومی که تقریباً در تمام مصاحبه‌های فنی، کتاب‌های مرجع و حتی کدهای روزمره پروژه‌های واقعی حضور دارد. بسیاری از توسعه‌دهندگان در ابتدا تصور می‌کنند Closure یک قابلیت پیچیده یا جادویی در JavaScript است، در حالی که در واقع نتیجه طبیعی نحوه کار Lexical Scope و مدیریت حافظه در موتور جاوااسکریپت است.

جالب است بدانید بسیاری از قابلیت‌هایی که هر روز از آن‌ها استفاده می‌کنیم، بدون وجود javascript closures عملاً امکان‌پذیر نبودند. از Callbackها و Event Listenerها گرفته تا Promise، توابع Factory، ماژول‌ها، Hookهای React و حتی بسیاری از الگوهای طراحی، همگی به نوعی بر پایه Closure ساخته شده‌اند.

اما نکته مهم اینجاست که اکثر آموزش‌ها فقط تعریف ساده‌ای مانند «تابعی که به متغیرهای محیط بیرونی خود دسترسی دارد» ارائه می‌کنند؛ تعریفی که اگرچه صحیح است، اما هیچ دید عمیقی از اتفاقاتی که در پشت صحنه رخ می‌دهد به شما نمی‌دهد. در نتیجه توسعه‌دهنده مفهوم را حفظ می‌کند، اما هنگام طراحی معماری نرم‌افزار یا رفع مشکلات حافظه، نمی‌تواند از آن به‌درستی استفاده کند.

در این مقاله قصد داریم Closure را از دید موتور JavaScript بررسی کنیم. ابتدا با مفهوم Scope و Lexical Environment آشنا می‌شویم، سپس نحوه ایجاد Closure، ارتباط آن با Heap Memory و Garbage Collector را بررسی خواهیم کرد و در ادامه با مثال‌های واقعی پروژه‌های حرفه‌ای یاد می‌گیریم چگونه از آن برای تولید کدهای تمیز، ایمن و قابل نگهداری استفاده کنیم.

در این مقاله چه چیزهایی یاد می‌گیرید؟

  • Closure در JavaScript دقیقاً چیست و چگونه ایجاد می‌شود.
  • ارتباط Closures با Lexical Scope و Scope Chain.
  • نحوه نگهداری متغیرها در حافظه پس از پایان اجرای تابع.
  • دلیل استفاده Frameworkهایی مانند React از Closure.
  • پیاده‌سازی متغیرهای Private بدون استفاده از Class.
  • تفاوت Closure با Scope و اشتباهات رایج توسعه‌دهندگان.
  • تأثیر Closures بر مصرف حافظه و عملکرد برنامه.
  • بهترین روش‌های استفاده از Closure در پروژه‌های واقعی.

JavaScript Closures چیست؟

اگر بخواهیم Closure را تنها در یک جمله تعریف کنیم، می‌توان گفت:

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

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

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

همین ویژگی باعث شده Closure به یکی از ستون‌های اصلی طراحی نرم‌افزار در JavaScript تبدیل شود. بدون وجود آن، بسیاری از قابلیت‌هایی که امروزه بدیهی به نظر می‌رسند، مانند Callbackها، Event Listenerها، Promiseها، Hookهای React، ماژول‌ها و حتی بسیاری از کتابخانه‌های محبوب، قابل پیاده‌سازی نبودند.

نکته

NOTE
Closure یک ویژگی جداگانه یا دستور جدید در JavaScript نیست؛ بلکه نتیجه طبیعی نحوه کار Lexical Scope و موتور اجرای JavaScript است. یعنی هر زمان یک تابع به متغیرهای Scope بیرونی خود دسترسی داشته باشد، در واقع یک Closure ایجاد شده است.

JavaScript Closures و ارتباط آن با Lexical Scope

چرا JavaScript Closures اهمیت زیادی دارند؟

ممکن است این سؤال برایتان پیش بیاید که چرا مفهومی که ظاهراً فقط امکان دسترسی به چند متغیر را فراهم می‌کند، تا این اندازه در دنیای JavaScript مهم است؟

پاسخ ساده است؛ Closure یکی از پایه‌های اصلی معماری این زبان محسوب می‌شود. بسیاری از قابلیت‌هایی که روزانه از آن‌ها استفاده می‌کنیم، مستقیماً بر اساس همین مکانیزم ساخته شده‌اند.

    • ایجاد متغیرهای Private بدون استفاده از کلاس‌ها
    • پیاده‌سازی Factory Functionها
  • نوشتن Module Pattern
  • مدیریت Callbackها
  • کار با Event Listenerها
  • استفاده از Promise و Async/Await
  • پیاده‌سازی Hookهای React
  • ذخیره وضعیت (State) بین فراخوانی‌های مختلف یک تابع

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

Lexical Scope چگونه باعث ایجاد JavaScript Closures می‌شود؟

برای درک واقعی Closure ابتدا باید با مفهومی به نام Lexical Scope آشنا شوید. در حقیقت Closure چیزی مستقل از Scope نیست؛ بلکه نتیجه مستقیم نحوه تعریف Scope در JavaScript است.

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

به بیان ساده، هر تابع هنگام ایجاد، محیط اطراف خود را نیز «به خاطر می‌سپارد». این محیط شامل تمام متغیرهایی است که در همان لحظه در دسترس تابع قرار دارند. بعدها اگر همان تابع در هر نقطه دیگری از برنامه اجرا شود، همچنان می‌تواند به همان متغیرها دسترسی داشته باشد.
Lexical Scope در JavaScript

نکته

NOTE
JavaScript بر اساس محل تعریف توابع تصمیم می‌گیرد یک متغیر قابل دسترس باشد یا خیر؛ نه بر اساس محل فراخوانی آن‌ها. همین ویژگی، پایه اصلی ایجاد Closure محسوب می‌شود.

یک مثال ساده از Lexical Scope

در مثال زیر، تابع innerFunction به متغیری دسترسی دارد که داخل خودش تعریف نشده است. دلیل این موضوع، Lexical Scope است.

function outerFunction() {
  const message = "سلام از CafeAmoozesh";

  function innerFunction() {
    console.log(message);
  }

  innerFunction();
}

outerFunction();

خروجی

سلام از CafeAmoozesh

تحلیل مثال

زمانی که تابع innerFunction ایجاد می‌شود، موتور JavaScript بررسی می‌کند که متغیر message در Scope فعلی وجود ندارد. سپس به Scope والد مراجعه می‌کند و مقدار آن را پیدا می‌کند. این فرآیند از طریق Scope Chain انجام می‌شود.

تا اینجا هنوز Closure اتفاق خاصی نیست؛ زیرا تابع داخلی بلافاصله اجرا شده است. اما نکته جالب زمانی رخ می‌دهد که تابع داخلی را از تابع والد خارج کنیم و بعداً اجرا کنیم. در آن حالت مشاهده خواهیم کرد که با وجود پایان یافتن اجرای تابع والد، متغیر message همچنان در دسترس باقی مانده است و دقیقاً همان چیزی است که آن را JavaScript Closure می‌نامیم.

نکته

NOTE
اشتباه رایج بسیاری از برنامه‌نویسان این است که تصور می‌کنند متغیرهای یک تابع بلافاصله پس از پایان اجرای آن از حافظه حذف می‌شوند. این موضوع همیشه صحیح نیست. اگر تابع دیگری هنوز به آن متغیرها نیاز داشته باشد، موتور JavaScript آن‌ها را در حافظه نگه می‌دارد و این دقیقاً رفتار Closure است.

اولین JavaScript Closure واقعی را بسازیم

تا اینجا با مفهوم Lexical Scope آشنا شدیم، اما هنوز Closure واقعی را مشاهده نکرده‌ایم. در مثال قبلی، تابع داخلی بلافاصله اجرا می‌شد و سپس هر دو تابع به پایان می‌رسیدند. بنابراین هنوز مشخص نبود چرا همه درباره Closures صحبت می‌کنند.

قدرت واقعی Closure زمانی نمایان می‌شود که یک تابع، از داخل تابع دیگری برگردانده (Return) شود و بعداً در نقطه‌ای دیگر از برنامه اجرا گردد. در این حالت، سؤال مهمی مطرح می‌شود:

اگر تابع والد تمام شده باشد، متغیرهای آن چگونه هنوز در دسترس هستند؟

بیایید این موضوع را با یک مثال ساده بررسی کنیم.

function createGreeting() {
  const message = "سلام، به CafeAmoozesh خوش آمدید.";

  return function () {
    console.log(message);
  };
}

const greet = createGreeting();

greet();

خروجی

سلام، به CafeAmoozesh خوش آمدید.

تحلیل مثال

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

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

به همین دلیل، حتی پس از پایان اجرای تابع والد، فراخوانی greet() همچنان مقدار متغیر message را نمایش می‌دهد.

تعریف واقعی ClosureClosure در حقیقت ترکیبی از یک تابع و محیط لغوی (Lexical Environment) آن تابع است. یعنی علاوه بر خود کد تابع، تمام متغیرهایی که هنگام ایجاد در دسترس آن بوده‌اند نیز همراه آن باقی می‌مانند.

نحوه نگهداری متغیرها در JavaScript Closures

در پشت صحنه چه اتفاقی رخ می‌دهد؟

برای پاسخ به این سؤال باید کمی با نحوه کار موتور JavaScript آشنا شویم. هر بار که یک تابع فراخوانی می‌شود، موتور JavaScript یک Execution Context جدید ایجاد می‌کند. این محیط شامل اطلاعاتی مانند متغیرها، آرگومان‌ها و Scope مربوط به همان تابع است.

در شرایط عادی، زمانی که اجرای تابع به پایان می‌رسد و هیچ مرجعی به متغیرهای آن وجود ندارد، موتور JavaScript حافظه اشغال‌شده را آزاد می‌کند. این کار توسط Garbage Collector انجام می‌شود.

اما اگر تابع دیگری همچنان به آن متغیرها وابسته باشد، وضعیت کاملاً متفاوت خواهد بود. موتور JavaScript تشخیص می‌دهد که این داده‌ها هنوز موردنیاز هستند؛ بنابراین آن‌ها را از حافظه حذف نمی‌کند.

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

مراحل ایجاد یک Closure

  1. تابع والد اجرا می‌شود.
  2. متغیرهای محلی ایجاد می‌شوند.
  3. تابع داخلی ساخته می‌شود.
  4. تابع داخلی به متغیرهای والد دسترسی پیدا می‌کند.
  5. تابع داخلی توسط return بازگردانده می‌شود.
  6. اجرای تابع والد پایان می‌یابد.
  7. به دلیل وجود تابع داخلی، متغیرهای موردنیاز از حافظه حذف نمی‌شوند.
  8. هر زمان تابع داخلی اجرا شود، همان متغیرها دوباره قابل استفاده خواهند بود.

نکته

NOTE
 بسیاری از توسعه‌دهندگان تصور می‌کنند Closure باعث «کپی شدن» متغیرها می‌شود. این تصور اشتباه است. در واقع موتور JavaScript داده‌ها را کپی نمی‌کند؛ بلکه مرجعی (Reference) به محیط لغوی تابع را نگه می‌دارد. به همین دلیل اگر مقدار یک متغیر تغییر کند، Closure نیز مقدار جدید را مشاهده خواهد کرد.

آیا Closure مقدار متغیرها را ذخیره می‌کند یا Reference آن‌ها را؟

این سؤال یکی از رایج‌ترین پرسش‌های مصاحبه‌های استخدامی JavaScript است. بسیاری از افراد تصور می‌کنند هنگام ایجاد Closure، یک نسخه از متغیرها ذخیره می‌شود؛ اما واقعیت چیز دیگری است.

Closure معمولاً خودِ مقدار را ذخیره نمی‌کند؛ بلکه ارتباط خود را با محیطی که آن متغیر در آن قرار دارد حفظ می‌کند. به همین دلیل اگر مقدار متغیر بعداً تغییر کند، تابع داخلی نیز مقدار جدید را مشاهده خواهد کرد.

function counter() {
  let count = 0;

  return function () {
    count++;
    console.log(count);
  };
}

const increment = counter();

increment();
increment();
increment();

خروجی

1
2
3

تحلیل مثال

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

Best Practice

BEST PRACTICE
از این ویژگی برای نگهداری وضعیت (State) استفاده کنید، اما مراقب باشید داده‌های بسیار بزرگ یا اشیای بدون استفاده را به‌صورت ناخواسته داخل Closure نگه ندارید؛ زیرا ممکن است باعث افزایش مصرف حافظه و ایجاد Memory Leak شوند.

کاربرد JavaScript Closures در پروژه‌های واقعی

تا اینجا متوجه شدیم که Closure فقط یک مفهوم تئوری نیست؛ بلکه مکانیزمی است که امکان نگهداری داده‌ها پس از پایان اجرای یک تابع را فراهم می‌کند. حالا وقت آن رسیده است که ببینیم این قابلیت در پروژه‌های واقعی چگونه مورد استفاده قرار می‌گیرد.

تقریباً در هر پروژه JavaScript، حتی اگر مستقیماً Closure ننویسید، احتمال زیادی وجود دارد که کتابخانه یا فریم‌ورکی که از آن استفاده می‌کنید، در پشت صحنه بر پایه همین مفهوم ساخته شده باشد.
کاربردهای JavaScript Closures در پروژه_های واقعی

مثال اول: ساخت یک شمارنده (Counter)

یکی از معروف‌ترین مثال‌های آموزش Closure، ساخت یک شمارنده است. دلیل محبوبیت این مثال این است که به‌خوبی نشان می‌دهد چگونه یک متغیر می‌تواند بین چندین بار اجرای تابع حفظ شود، بدون اینکه از متغیرهای سراسری (Global Variables) استفاده کنیم.

function createCounter() {
  let count = 0;

  return function () {
    count++;
    return count;
  };
}

const counter = createCounter();

console.log(counter());
console.log(counter());
console.log(counter());

خروجی

1
2
3

تحلیل مثال

متغیر count فقط یک بار ایجاد می‌شود؛ یعنی هنگام اجرای تابع createCounter. پس از پایان اجرای این تابع، انتظار داریم متغیر از بین برود، اما تابعی که بازگردانده شده همچنان به آن دسترسی دارد.

به همین دلیل در هر بار فراخوانی counter() مقدار قبلی حفظ شده و یک واحد افزایش پیدا می‌کند.

چرا این روش بهتر است؟

اگر از یک متغیر Global استفاده می‌کردیم، هر بخش از برنامه می‌توانست مقدار آن را تغییر دهد. اما با استفاده از Closure فقط همان تابع داخلی اجازه تغییر مقدار count را دارد و این موضوع امنیت و قابلیت نگهداری کد را افزایش می‌دهد.


مثال دوم: ساخت متغیرهای Private

قبل از اضافه شدن فیلدهای Private در کلاس‌های JavaScript، رایج‌ترین روش برای مخفی کردن داده‌ها استفاده از Closure بود. این تکنیک هنوز هم در بسیاری از کتابخانه‌ها و پروژه‌ها استفاده می‌شود.

function createUser(name) {
  let password = "123456";

  return {
    getName() {
      return name;
    },

    checkPassword(input) {
      return input === password;
    }
  };
}

const user = createUser("Vahid");

console.log(user.getName());

console.log(user.checkPassword("123456"));

console.log(user.password);

خروجی

Vahid
true
undefined

تحلیل مثال

شیء بازگردانده شده فقط به دو متد دسترسی دارد. متغیر password خارج از تابع قابل مشاهده نیست، اما متد checkPassword() همچنان می‌تواند به آن دسترسی داشته باشد، زیرا از طریق Closure به محیط لغوی تابع متصل است.

به همین دلیل هیچ بخشی از برنامه نمی‌تواند مستقیماً مقدار رمز عبور را بخواند یا تغییر دهد.

Best Practice

BEST PRACTICE
هر زمان لازم بود اطلاعات حساس را فقط از طریق چند متد کنترل‌شده در اختیار سایر بخش‌های برنامه قرار دهید، Closure یکی از بهترین گزینه‌هاست. این روش باعث کاهش وابستگی بین بخش‌های مختلف برنامه و افزایش امنیت کد می‌شود.

مثال سوم: Factory Function

Factory Functionها توابعی هستند که هر بار یک شیء جدید ایجاد می‌کنند. یکی از مهم‌ترین دلایل محبوبیت این الگو، استفاده از Closure برای نگهداری داده‌های داخلی است.

function createBankAccount(initialBalance) {
  let balance = initialBalance;

  return {

    deposit(amount) {
      balance += amount;
    },

    withdraw(amount) {
      balance -= amount;
    },

    getBalance() {
      return balance;
    }

  };
}

const account = createBankAccount(1000);

account.deposit(500);

account.withdraw(200);

console.log(account.getBalance());

خروجی

1300

تحلیل مثال

هیچ بخشی از برنامه به متغیر balance دسترسی مستقیم ندارد. تنها راه تغییر موجودی، استفاده از متدهای تعریف‌شده داخل شیء است. این دقیقاً همان مفهومی است که در برنامه‌نویسی شیءگرا با عنوان Encapsulation شناخته می‌شود و Closure بدون نیاز به Class این قابلیت را در اختیار ما قرار می‌دهد.

JavaScript Closures در Callbackها و توابع ناهمگام (Asynchronous)

اگر مقاله‌های قبلی مربوط به Promise، Async/Await و Event Loop را مطالعه کرده باشید، احتمالاً این سؤال برایتان پیش آمده که چگونه Callbackها بعد از چند ثانیه یا حتی چند دقیقه، همچنان به متغیرهای قبلی خود دسترسی دارند؟

پاسخ این سؤال دقیقاً در یک کلمه خلاصه می‌شود:
Closure.

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


ارتباط JavaScript Closures با Callback

مثال: Closure در setTimeout

در مثال زیر، تابع Callback دو ثانیه بعد اجرا می‌شود. با این حال همچنان به متغیر message دسترسی دارد.

function showMessage() {
  const message = "سلام از CafeAmoozesh";

  setTimeout(function () {
    console.log(message);
  }, 2000);
}

showMessage();

خروجی

سلام از CafeAmoozesh

تحلیل مثال

تابع showMessage() تقریباً بلافاصله به پایان می‌رسد، اما Callback هنوز اجرا نشده است. زمانی که تایمر به پایان می‌رسد، Event Loop تابع Callback را دوباره وارد Call Stack می‌کند.

در همین لحظه Closure وارد عمل می‌شود و اجازه می‌دهد Callback همچنان به متغیر message دسترسی داشته باشد.

نکته

NOTE
اگر Closure وجود نداشت، تقریباً تمام Callbackهای JavaScript بی‌استفاده می‌شدند؛ زیرا پس از پایان اجرای تابع والد، تمام داده‌های موردنیاز آن‌ها از بین می‌رفت.

مثال: Closure در Event Listener

یکی دیگر از کاربردهای مهم Closure، مدیریت رویدادها (Event Listener) است. ممکن است یک Event Listener ساعت‌ها بعد از بارگذاری صفحه اجرا شود، اما همچنان به داده‌های اولیه دسترسی داشته باشد.

function registerButton() {
  let clickCount = 0;

  const button = document.querySelector("#btn");

  button.addEventListener("click", function () {
    clickCount++;
    console.log(`تعداد کلیک: ${clickCount}`);
  });
}

registerButton();

تحلیل مثال

تابع registerButton() فقط یک بار اجرا می‌شود، اما Event Listener ممکن است هزاران بار فراخوانی شود. با این وجود، متغیر clickCount همچنان مقدار خود را حفظ می‌کند، زیرا Callback به کمک Closure به آن دسترسی دارد.

کاربرد واقعی

تقریباً تمام کتابخانه‌های رابط کاربری مانند React، Vue و Angular هنگام مدیریت Eventها از همین مکانیزم استفاده می‌کنند.

اشتباهات رایج هنگام استفاده از JavaScript Closures

هرچند Closure یکی از قدرتمندترین قابلیت‌های JavaScript محسوب می‌شود، اما استفاده نادرست از آن می‌تواند باعث بروز باگ‌های پیچیده و حتی افزایش مصرف حافظه شود.

اشتباه اول: استفاده از var داخل حلقه

این احتمالاً معروف‌ترین خطایی است که تقریباً تمام توسعه‌دهندگان JavaScript حداقل یک بار با آن مواجه شده‌اند.

for (var i = 1; i <= 3; i++) {

  setTimeout(function () {
    console.log(i);
  }, 1000);

}

خروجی

4
4
4

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

متغیر i با استفاده از var فقط یک بار ایجاد می‌شود و تمام Callbackها به همان متغیر مشترک اشاره می‌کنند. زمانی که تایمرها اجرا می‌شوند، حلقه قبلاً پایان یافته و مقدار i برابر با ۴ شده است.

بنابراین هر سه Callback همان مقدار نهایی را نمایش می‌دهند.

راه‌حل اول: استفاده از let

for (let i = 1; i <= 3; i++) {

  setTimeout(function () {
    console.log(i);
  }, 1000);

}

خروجی

1
2
3

تحلیل مثال

در هر تکرار حلقه، let یک Scope جدید ایجاد می‌کند. بنابراین هر Callback به نسخه مستقل خودش از متغیر i دسترسی خواهد داشت.

Best Practice

BEST PRACTICE
در پروژه‌های مدرن تقریباً هیچ دلیلی برای استفاده از var وجود ندارد. استفاده از let و const علاوه بر خوانایی بیشتر، بسیاری از خطاهای مربوط به Closure را نیز برطرف می‌کند.

آیا JavaScript Closures باعث Memory Leak می‌شوند؟

یکی از رایج‌ترین باورهای اشتباه درباره Closure این است که گفته می‌شود «Closure باعث نشت حافظه (Memory Leak) می‌شود». این جمله به‌تنهایی صحیح نیست. Closure ذاتاً مشکلی ایجاد نمی‌کند؛ بلکه استفاده نادرست از آن می‌تواند باعث شود اشیایی که دیگر نیازی به آن‌ها نداریم، همچنان در حافظه باقی بمانند.

موتور JavaScript تا زمانی که مرجعی (Reference) به یک شیء وجود داشته باشد، آن شیء را از حافظه حذف نمی‌کند. اگر یک Closure همچنان به داده‌ای اشاره کند، Garbage Collector نیز آن داده را قابل حذف نمی‌داند.
Memory Leak در JavaScript Closures

مثال نادرست

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

function createCache() {

  const largeData = new Array(100000).fill("JavaScript");

  return function () {
    return largeData.length;
  };

}

const cache = createCache();

تحلیل مثال

اگرچه فقط طول آرایه استفاده می‌شود، اما خود آرایه نیز همچنان در حافظه باقی می‌ماند، زیرا Closure به آن Reference دارد. اگر چنین داده‌هایی بدون نیاز نگهداری شوند، مصرف حافظه برنامه به‌مرور افزایش پیدا می‌کند.

نکته

NOTE
Memory Leak به این معنا نیست که JavaScript حافظه را آزاد نمی‌کند؛ بلکه یعنی برنامه شما ناخواسته مرجع اشیایی را نگه داشته است که دیگر نیازی به آن‌ها ندارد.

نمونه بهینه‌تر

function createCounter() {

  let count = 0;

  return function () {
    return ++count;
  };

}

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

Best Practices هنگام استفاده از JavaScript Closures

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

  • فقط داده‌هایی را داخل Closure نگه دارید که واقعاً موردنیاز هستند.
  • از نگهداری آرایه‌ها و اشیای بسیار بزرگ بدون دلیل خودداری کنید.
  • در حلقه‌ها از let و const استفاده کنید.
  • برای ایجاد Private State از Closure استفاده کنید.
  • اگر از Closure استفاده نمی‌کنید، اجازه دهید Referenceها حذف شوند تا Garbage Collector حافظه را آزاد کند.
  • در پروژه‌های بزرگ از نام‌های واضح برای Factory Functionها و Callbackها استفاده کنید.
  • هر جا استفاده از Class خواناتر است، فقط به خاطر استفاده از Closure معماری پروژه را پیچیده نکنید.

نکته مدرس

TEACHER TIP
هدف استفاده از Closure صرفاً مخفی کردن متغیرها نیست. مهم‌ترین کاربرد آن مدیریت State، کاهش وابستگی بین بخش‌های مختلف برنامه و ایجاد توابع مستقل و قابل استفاده مجدد است.
مقایسه Scope و Closure
بسیاری از برنامه‌نویسان این دو مفهوم را با یکدیگر اشتباه می‌گیرند. در حالی که Scope و Closure ارتباط نزدیکی با هم دارند، اما یکسان نیستند.

ویژگی Scope Closure
تعریف محدوده دسترسی به متغیرها حفظ دسترسی به Scope پس از پایان اجرای تابع
زمان ایجاد هنگام تعریف کد هنگام ایجاد تابع
ارتباط با حافظه به‌تنهایی داده‌ای را نگه نمی‌دارد در صورت نیاز متغیرها را در حافظه حفظ می‌کند
کاربرد اصلی مدیریت محدوده متغیرها نگهداری State و داده بین اجرای توابع
نمونه کاربرد Block Scope، Function Scope Callback، Factory Function، React Hook

خلاصه تفاوت

Scope مشخص می‌کند چه متغیرهایی قابل دسترسی هستند، اما Closure باعث می‌شود این دسترسی حتی پس از پایان اجرای تابع نیز حفظ شود.

سؤالات متداول (FAQ)

آیا هر تابع در JavaScript یک Closure ایجاد می‌کند؟

از نظر فنی، هر تابع هنگام ایجاد، محیط لغوی (Lexical Environment) خود را نیز در اختیار دارد. اما تنها زمانی این ویژگی به‌صورت عملی اهمیت پیدا می‌کند که تابع پس از پایان اجرای Scope والد همچنان به متغیرهای آن نیاز داشته باشد.

آیا Closure باعث کند شدن برنامه می‌شود؟

خیر. استفاده صحیح از Closure تأثیر محسوسی بر عملکرد برنامه ندارد. تنها زمانی ممکن است مشکل ایجاد شود که اشیای بسیار بزرگ یا داده‌های بدون استفاده برای مدت طولانی داخل Closure نگهداری شوند.

تفاوت Closure و Scope چیست؟

Scope محدوده دسترسی متغیرها را مشخص می‌کند، اما Closure باعث می‌شود یک تابع حتی پس از پایان اجرای Scope والد نیز به آن متغیرها دسترسی داشته باشد.

آیا React از Closure استفاده می‌کند؟

بله. بسیاری از Hookهای React مانند useState، useEffect و Event Handlerها بر پایه مکانیزم Closure کار می‌کنند. به همین دلیل درک صحیح Closure برای یادگیری React اهمیت زیادی دارد.

آیا Closure فقط در توابع Anonymous ایجاد می‌شود؟

خیر. نوع تابع اهمیتی ندارد. هر تابع Named یا Anonymous در صورت دسترسی به متغیرهای Scope بیرونی می‌تواند یک Closure ایجاد کند.

چه زمانی نباید از Closure استفاده کنیم؟

اگر تنها هدف شما نگهداری داده‌های بزرگ برای مدت طولانی باشد یا معماری پروژه با استفاده از Class یا Module خواناتر می‌شود، استفاده از Closure بهترین انتخاب نیست.


سؤالات متداول مصاحبه JavaScript درباره Closures

اگر برای مصاحبه استخدامی JavaScript آماده می‌شوید، احتمال زیادی وجود دارد که حداقل یکی از سؤالات زیر از شما پرسیده شود.

۱. Closure را بدون حفظ کردن تعریف کنید.

سعی کنید به جای تعریف کتابی، نحوه کار آن را با مثال توضیح دهید. مصاحبه‌کننده معمولاً به میزان درک شما از مفهوم توجه می‌کند، نه حفظ بودن تعریف.

۲. چرا Callbackها بعد از پایان اجرای تابع والد همچنان به متغیرهای آن دسترسی دارند؟

پاسخ این سؤال مستقیماً به مفهوم Closure و نگهداری Lexical Environment مربوط می‌شود.

۳. خروجی کد زیر چیست؟

function createCounter() {

  let count = 0;

  return function () {
    return ++count;
  };

}

const counterA = createCounter();
const counterB = createCounter();

console.log(counterA());
console.log(counterA());
console.log(counterB());

پاسخ

1
2
1

هر بار اجرای createCounter() یک Closure مستقل ایجاد می‌کند؛ بنابراین counterA و counterB وضعیت (State) جداگانه‌ای دارند.

۴. چرا استفاده از var داخل حلقه می‌تواند باعث ایجاد باگ شود؟

زیرا تمام Closureها به یک متغیر مشترک اشاره می‌کنند، در حالی که let در هر تکرار Scope جدیدی ایجاد می‌کند.

۵. آیا Closure می‌تواند باعث Memory Leak شود؟

خود Closure مشکلی ایجاد نمی‌کند، اما نگه داشتن Reference اشیای بزرگ و بدون استفاده می‌تواند مانع آزاد شدن حافظه توسط Garbage Collector شود.


سؤالات رایج مصاحبه درباره JavaScript Closures

مفهوم JavaScript Closures یکی از موضوعاتی است که تقریباً در تمام مصاحبه‌های Front-End، Node.js و حتی React مطرح می‌شود. دلیل آن هم ساده است؛ Closure یکی از پایه‌ای‌ترین مفاهیم JavaScript است و بسیاری از قابلیت‌های این زبان بر اساس آن پیاده‌سازی شده‌اند.

۱. JavaScript Closure چیست؟

همانطور که می دانیم Closure ترکیبی از یک تابع و محیط لغوی (Lexical Environment) آن است. به زبان ساده، زمانی که یک تابع به متغیرهای Scope بیرونی خود دسترسی داشته باشد و این دسترسی حتی پس از پایان اجرای تابع والد حفظ شود، یک Closure ایجاد شده است.

۲. تفاوت Scope و Closure چیست؟

Scope مشخص می‌کند هر متغیر در چه محدوده‌ای قابل دسترس است، اما Closure باعث می‌شود یک تابع حتی پس از پایان اجرای Scope والد نیز بتواند به همان متغیرها دسترسی داشته باشد.

۳. آیا هر تابع در JavaScript یک Closure ایجاد می‌کند؟

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

۴. Closure چگونه با Garbage Collector ارتباط دارد؟

تا زمانی که یک Closure به داده‌ای Reference داشته باشد، موتور JavaScript آن داده را قابل استفاده فرض می‌کند و Garbage Collector آن را از حافظه حذف نخواهد کرد. به همین دلیل نگهداری داده‌های بزرگ داخل Closure می‌تواند باعث افزایش مصرف حافظه شود.

مفهوم نقش در Closure
Lexical Scope مشخص می‌کند تابع به چه متغیرهایی دسترسی دارد.
Execution Context هنگام اجرای تابع ایجاد می‌شود.
Reference ارتباط تابع با محیط لغوی را حفظ می‌کند.
Garbage Collector در صورت وجود Reference داده‌ها را حذف نمی‌کند.
Heap Memory محلی که موتور JavaScript معمولاً داده‌های زنده را نگهداری می‌کند.

۵. تفاوت Closure و Arrow Function چیست؟

Arrow Function فقط یک Syntax برای تعریف تابع است، اما Closure یک رفتار (Behavior) محسوب می‌شود. هم Functionهای معمولی و هم Arrow Functionها می‌توانند Closure ایجاد کنند.

۶. چرا Callbackها پس از پایان اجرای تابع والد همچنان به متغیرهای آن دسترسی دارند؟

زیرا هنگام ایجاد Callback، محیط لغوی آن نیز ذخیره می‌شود و Callback از طریق Closure به همان محیط متصل باقی می‌ماند.

۷. چرا استفاده از var داخل حلقه می‌تواند باعث ایجاد باگ شود؟

زیرا تمام Closureها به یک متغیر مشترک اشاره می‌کنند. استفاده از let باعث ایجاد Scope مستقل در هر تکرار حلقه می‌شود و این مشکل را برطرف می‌کند.

۸. آیا Closure باعث Memory Leak می‌شود؟

خیر. خود Closure مشکلی ایجاد نمی‌کند؛ اما اگر همچنان به داده‌هایی Reference داشته باشد که دیگر موردنیاز نیستند، Garbage Collector قادر به آزاد کردن آن حافظه نخواهد بود.

۹. در React چرا درک Closure اهمیت زیادی دارد؟

زیرا Hookهایی مانند useState، useEffect و Event Handlerها بر پایه همین مفهوم کار می‌کنند. بسیاری از باگ‌های مربوط به State در React نیز به درک نادرست Closure بازمی‌گردند.

۱۰. مهم‌ترین کاربرد JavaScript Closures در پروژه‌های واقعی چیست؟

مدیریت State، ایجاد Private Variables، پیاده‌سازی Factory Functionها، Callbackها، Event Listenerها، Hookهای React و بسیاری از الگوهای طراحی مدرن JavaScript.


چالش کافه آموزش

قبل از مشاهده پاسخ‌ها، سعی کنید به سؤالات زیر پاسخ دهید.

سؤال ۱

Closure در JavaScript چه چیزی را حفظ می‌کند؟

  1. یک کپی از تمام متغیرها
  2. Reference به محیط لغوی (Lexical Environment)
  3. فقط متغیرهای Global
  4. Call Stack
پاسخ

گزینه ۲ صحیح است. Closure معمولاً مقادیر را کپی نمی‌کند، بلکه ارتباط خود را با محیط لغوی تابع حفظ می‌کند.


سؤال ۲

خروجی کد زیر چیست؟

function counter() {

  let count = 0;

  return function () {
    return ++count;
  };

}

const c = counter();

console.log(c());
console.log(c());
  1. ۱ و ۱
  2. ۰ و ۱
  3. ۱ و ۲
  4. ۲ و ۲
پاسخ

گزینه ۳ صحیح است. متغیر count داخل Closure نگهداری می‌شود و مقدار آن بین فراخوانی‌های مختلف حفظ خواهد شد.


سؤال ۳

کدام گزینه یکی از کاربردهای رایج JavaScript Closures است؟

  1. مدیریت DOM
  2. ایجاد Private Variables
  3. کامپایل کردن کد JavaScript
  4. مدیریت CSS
پاسخ

گزینه ۲ صحیح است. یکی از مهم‌ترین کاربردهای Closure، مخفی کردن داده‌ها و ایجاد Private State است.


سؤال ۴

اگر یک تابع داخلی به هیچ متغیری از Scope والد دسترسی نداشته باشد، کدام عبارت صحیح‌تر است؟

  1. حتماً یک Closure ایجاد شده است.
  2. تابع هیچ ارتباطی با Scope ندارد.
  3. از نظر عملی Closure کاربردی ایجاد نشده است.
  4. تابع فقط داخل Heap اجرا می‌شود.
پاسخ

گزینه ۳ صحیح است. هر تابع محیط لغوی خود را دارد، اما زمانی Closure اهمیت پیدا می‌کند که تابع واقعاً به متغیرهای Scope بیرونی نیاز داشته باشد.


سؤال ۵

خروجی کد زیر چیست؟

for (var i = 1; i <= 3; i++) {

  setTimeout(function () {
    console.log(i);
  }, 0);

}
  1. ۱، ۲، ۳
  2. ۳، ۳، ۳
  3. ۴، ۴، ۴
  4. خطای Syntax
پاسخ

گزینه ۳ صحیح است. تمام Callbackها به یک متغیر مشترک i اشاره می‌کنند و پس از پایان حلقه مقدار آن برابر با ۴ خواهد بود.


سؤال ۶

کدام گزینه درباره Memory Leak و Closure صحیح است؟

  1. هر Closure باعث Memory Leak می‌شود.
  2. Closure هیچ ارتباطی با حافظه ندارد.
  3. نگهداری Reference به داده‌های بزرگ می‌تواند باعث افزایش مصرف حافظه شود.
  4. Garbage Collector همیشه تمام داده‌های Closure را حذف می‌کند.
پاسخ

گزینه ۳ صحیح است. خود Closure مشکل‌ساز نیست؛ اما اگر همچنان به اشیای بزرگ Reference داشته باشد، Garbage Collector نمی‌تواند آن‌ها را آزاد کند.


سؤال ۷

هر بار اجرای تابع createCounter() چه اتفاقی رخ می‌دهد؟

  1. همه نمونه‌ها از یک متغیر مشترک استفاده می‌کنند.
  2. یک Closure و Lexical Environment جدید ایجاد می‌شود.
  3. متغیرها به صورت Global ذخیره می‌شوند.
  4. Heap Memory پاک می‌شود.
پاسخ

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


سؤال ۸ (سطح مصاحبه)

کدام عبارت دقیق‌تر است؟

  1. Arrow Function همان Closure است.
  2. Closure فقط با Arrow Function ایجاد می‌شود.
  3. هم Function معمولی و هم Arrow Function می‌توانند Closure ایجاد کنند.
  4. Closure فقط در Callbackها وجود دارد.
پاسخ

گزینه ۳ صحیح است. Closure یک رفتار (Behavior) است، نه نوعی از تابع. هر تابعی که به متغیرهای Scope بیرونی دسترسی داشته باشد، می‌تواند Closure ایجاد کند.


مهم

IMPORTANT
نکته‌ای که معمولاً در مصاحبه‌ها پرسیده می‌شود
Arrow Function یک Closure نیست.
اما هر Arrow Function می‌تواند یک Closure ایجاد کند.
در مقابل، Functionهای معمولی نیز دقیقاً همین قابلیت را دارند.

اشتباهات رایج هنگام کار با JavaScript Closures (Common Mistakes)

بسیاری از مشکلاتی که برنامه‌نویسان هنگام کار با JavaScript تجربه می‌کنند، مستقیماً به درک نادرست Closure مربوط می‌شود. در این بخش چند مورد از رایج‌ترین اشتباهات را بررسی می‌کنیم و راه‌حل صحیح هر کدام را نیز خواهیم دید.

اشتباه اول: تصور اینکه Closure از متغیرها کپی تهیه می‌کند

یکی از رایج‌ترین باورهای اشتباه این است که هنگام ایجاد Closure، یک نسخه از تمام متغیرها ذخیره می‌شود.

در واقع چنین اتفاقی رخ نمی‌دهد. Closure معمولاً Reference محیط لغوی (Lexical Environment) را نگه می‌دارد، نه یک کپی از مقادیر.

function createUser() {

  let name = "Ali";

  return {
    getName() {
      return name;
    },

    setName(newName) {
      name = newName;
    }
  };

}

const user = createUser();

console.log(user.getName());

user.setName("Sara");

console.log(user.getName());

خروجی

Ali
Sara

اگر Closure از متغیرها کپی تهیه می‌کرد، خروجی دوم همچنان Ali بود؛ اما همان‌طور که مشاهده می‌کنید، مقدار متغیر اصلی تغییر کرده است.


اشتباه دوم: نگهداری اشیای بسیار بزرگ داخل Closure

گاهی اوقات بدون اینکه متوجه باشیم، یک آرایه یا شیء چند مگابایتی را داخل Closure نگه می‌داریم. در نتیجه Garbage Collector نمی‌تواند آن را آزاد کند.

function createReport() {

  const hugeArray = new Array(500000).fill("Report");

  return function () {
    return hugeArray.length;
  };

}

اشتباه چیست؟

اگر فقط طول آرایه موردنیاز باشد، نگه داشتن کل آرایه در حافظه کاملاً غیرضروری است.


اشتباه سوم: استفاده بی‌دلیل از Closure

برخی توسعه‌دهندگان تصور می‌کنند هرچه تعداد Closureها بیشتر باشد، کد حرفه‌ای‌تر است؛ در حالی که همیشه این‌طور نیست.

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

function sum(a, b) {
  return a + b;
}

در چنین مثال ساده‌ای استفاده از Closure هیچ مزیتی ایجاد نمی‌کند.


اشتباه چهارم: فراموش کردن اینکه هر Closure مستقل است

function counter() {

  let count = 0;

  return function () {
    return ++count;
  };

}

const counterA = counter();
const counterB = counter();

console.log(counterA());
console.log(counterA());

console.log(counterB());

خروجی

1
2
1

هر بار اجرای تابع counter() یک محیط لغوی جدید ایجاد می‌کند. بنابراین هر Closure وضعیت مخصوص به خودش را دارد.


اشتباه پنجم: استفاده از Closure برای هر نوع Private State

Closure ابزار بسیار قدرتمندی است، اما همیشه بهترین انتخاب نیست. در پروژه‌های بزرگ ممکن است استفاده از Class یا ماژول‌های ES Modules خواناتر و قابل نگهداری‌تر باشد.

توصیه حرفه‌ای

از Closure زمانی استفاده کنید که واقعاً به حفظ State یا مخفی کردن داده‌ها نیاز دارید؛ نه صرفاً به این دلیل که می‌توانید از آن استفاده کنید.

اشتباهات رایج JavaScript Closures

نکته‌ای که توسعه‌دهندگان حرفه‌ای همیشه به خاطر دارند

اگر بعد از خواندن این مقاله فقط یک جمله را به خاطر بسپارید، کافی است این باشد:

Closure متغیرها را ذخیره نمی‌کند؛ بلکه ارتباط خود را با محیطی که آن متغیرها در آن قرار دارند حفظ می‌کند.

درک همین جمله باعث می‌شود بسیاری از مفاهیم پیشرفته JavaScript مانند Callbackها، Promiseها، React Hooks، Event Listenerها و حتی برخی الگوهای طراحی برایتان کاملاً قابل درک شوند.

Best Practices هنگام استفاده از JavaScript Closures

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

۱. فقط داده‌های موردنیاز را داخل Closure نگه دارید

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

مناسب ✅

function createCounter() {

  let count = 0;

  return () => ++count;

}

نامناسب ❌

function createCounter() {

  const hugeArray = new Array(500000).fill("data");

  let count = 0;

  return () => {
    count++;
    return count;
  };

}

۲. از Closure برای مخفی کردن State استفاده کنید، نه برای پیچیده کردن کد

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

Closure زمانی ارزشمند است که بخواهید وضعیت (State) یا داده‌های خصوصی (Private Data) را مدیریت کنید.


۳. تا حد امکان از const و let استفاده کنید

یکی از مهم‌ترین دلایل ایجاد باگ‌های مربوط به Closure، استفاده از var است. در پروژه‌های مدرن JavaScript تقریباً همیشه استفاده از let و const انتخاب بهتری محسوب می‌شود.


۴. Closure را جایگزین Class ندانید

گاهی اوقات توسعه‌دهندگان سعی می‌کنند کل معماری برنامه را با Closure پیاده‌سازی کنند. این کار معمولاً باعث پیچیدگی بیش از حد کد می‌شود.

اگر Class یا ES Modules ساختار واضح‌تر و خواناتری ایجاد می‌کنند، همان گزینه را انتخاب کنید.


۵. از ایجاد Closureهای غیرضروری در حلقه‌های بزرگ خودداری کنید

اگر هزاران بار داخل یک حلقه Closure جدید ایجاد شود، علاوه بر مصرف بیشتر حافظه، فشار بیشتری نیز به Garbage Collector وارد خواهد شد.

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


۶. از Factory Functionها برای مدیریت State استفاده کنید

یکی از بهترین کاربردهای Closure، ساخت Factory Functionها است. در این روش هر نمونه، وضعیت (State) مستقل خودش را دارد و وابستگی به متغیرهای سراسری از بین می‌رود.

function createTimer() {

  let seconds = 0;

  return {

    tick() {
      seconds++;
    },

    getTime() {
      return seconds;
    }

  };

}

Best Practice

BEST PRACTICE

اگر بتوانید در یک جمله تصمیم بگیرید که چه زمانی باید از Closure استفاده کنید، این جمله بهترین راهنماست:

هر زمان لازم بود یک تابع، بدون استفاده از متغیرهای Global، وضعیت (State) یا داده‌ای را بین چندین بار اجرا حفظ کند، Closure یکی از بهترین انتخاب‌های JavaScript است.

داخل موتور JavaScript هنگام ایجاد Closure چه اتفاقی می‌افتد؟

تا اینجا چندین بار از واژه‌هایی مانند Lexical Environment، Execution Context و Heap Memory استفاده کردیم. حالا زمان آن رسیده است که ببینیم موتور JavaScript دقیقاً چه مراحلی را طی می‌کند تا یک Closure ایجاد شود.

فرض کنید کد زیر را اجرا می‌کنیم.

function createCounter() {

  let count = 0;

  return function () {
    return ++count;
  };

}

const counter = createCounter();

counter();

نحوه ایجاد JavaScript Closure در موتور V8

مرحله اول: ایجاد Global Execution Context

با شروع اجرای فایل، موتور JavaScript یک Global Execution Context ایجاد می‌کند. تمام توابع و متغیرهای سراسری در این مرحله ثبت می‌شوند، اما هنوز هیچ تابعی اجرا نشده است.

مرحله دوم: فراخوانی createCounter()

با اجرای تابع، یک Execution Context جدید روی Call Stack قرار می‌گیرد. در این محیط، متغیر count مقداردهی اولیه شده و تابع داخلی نیز ایجاد می‌شود.

مرحله سوم: ایجاد Lexical Environment

همزمان با ایجاد تابع داخلی، موتور JavaScript محیط لغوی آن را نیز ثبت می‌کند. این محیط شامل متغیر count است که تابع داخلی در آینده به آن نیاز خواهد داشت.

مرحله چهارم: بازگرداندن تابع داخلی

تابع داخلی توسط دستور return بازگردانده می‌شود. در این لحظه، اگرچه اجرای createCounter() رو به پایان است، اما هنوز یک Reference به محیط لغوی آن وجود دارد.

مرحله پنجم: خروج Execution Context از Call Stack

پس از پایان اجرای تابع، Execution Context از Call Stack حذف می‌شود، اما این به معنای حذف تمام داده‌های آن نیست.

مرحله ششم: نگهداری Lexical Environment در Heap Memory

از آنجا که تابع بازگشتی همچنان به متغیر count نیاز دارد، موتور JavaScript محیط لغوی را در Heap Memory نگه می‌دارد. این همان چیزی است که آن را Closure می‌نامیم.

خلاصه اتفاقات

Global Execution Context
        │
        ▼
createCounter()
        │
        ▼
Execution Context
        │
        ▼
Lexical Environment
        │
        ▼
Return Inner Function
        │
        ▼
Call Stack Empty
        │
        ▼
Heap Memory
        │
        ▼
Closure

نکته

NOTE
یکی از بزرگ‌ترین اشتباهات این است که تصور کنیم Closure روی Call Stack باقی می‌ماند. در واقع پس از پایان اجرای تابع، Call Stack کاملاً آزاد می‌شود و تنها محیط لغوی موردنیاز در Heap Memory حفظ خواهد شد.

تفاوت Closure و Arrow Function

یکی از سؤالات پرتکرار مصاحبه‌های JavaScript این است که آیا Arrow Function همان Closure است؟

پاسخ کوتاه:
خیر.

این دو مفهوم کاملاً متفاوت هستند و فقط معمولاً در کنار یکدیگر دیده می‌شوند.

Closure Arrow Function
یک رفتار (Behavior) است. یک نوع Syntax برای تعریف تابع است.
به محیط لغوی وابسته است. به نحوه تعریف تابع مربوط می‌شود.
در Functionهای معمولی و Arrow Functionها ایجاد می‌شود. ممکن است Closure داشته باشد یا نداشته باشد.
State را حفظ می‌کند. State را مدیریت نمی‌کند.
به Scope وابسته است. دارای this لغوی (Lexical this) است.

در حقیقت Arrow Function فقط یک روش جدید برای تعریف تابع است. اگر این تابع به متغیرهای محیط بیرونی دسترسی داشته باشد، یک Closure نیز ایجاد می‌شود؛ در غیر این صورت خیر.

مثال

function createUser(name) {

  return () => {

    console.log(name);

  };

}

const showUser = createUser("Vahid");

showUser();

خروجی

Vahid

تحلیل مثال

در این مثال، Arrow Function باعث ایجاد Closure نشده است؛ بلکه به دلیل دسترسی به متغیر name که در Scope والد قرار دارد، یک Closure ایجاد شده است. اگر همین کد را با یک Function معمولی نیز بنویسیم، نتیجه کاملاً یکسان خواهد بود.

جمع‌بندی

در نگاه اول، JavaScript Closures ممکن است مفهومی پیچیده به نظر برسد، اما در واقع نتیجه طبیعی نحوه تعریف Scope در این زبان است. هر تابع هنگام ایجاد، محیط لغوی خود را نیز به خاطر می‌سپارد و همین ویژگی باعث می‌شود حتی پس از پایان اجرای تابع والد نیز بتواند به متغیرهای موردنیاز دسترسی داشته باشد.

همین مکانیزم ساده، پایه بسیاری از قابلیت‌های مهم JavaScript مانند Callbackها، Event Listenerها، Factory Functionها، Module Pattern و حتی Hookهای React را تشکیل می‌دهد. به همین دلیل درک صحیح Closure نه‌تنها برای موفقیت در مصاحبه‌های فنی ضروری است، بلکه هنگام طراحی نرم‌افزارهای بزرگ نیز نقش مهمی در تولید کدهای تمیز، ماژولار و قابل نگهداری ایفا می‌کند.

چک لیست

CHECKLIST
Closure یک قابلیت جداگانه نیست؛ نتیجه طبیعی Lexical Scope است.
Closure داده‌ها را کپی نمی‌کند؛ بلکه Reference محیط لغوی را نگه می‌دارد.
هر بار اجرای یک تابع، می‌تواند یک Closure مستقل ایجاد کند.
نگهداری بی‌دلیل داده‌های بزرگ داخل Closure ممکن است باعث افزایش مصرف حافظه شود.
درک Closure، یادگیری Promise، Async/Await، React Hooks و بسیاری از مفاهیم پیشرفته JavaScript را بسیار ساده‌تر می‌کند.

منابع بیشتر

READ MORE

 

پرسش و پاسخ

اولین دیدگاه رو شما برای ما ارسال و کنید

کاربر گرامی : برای ارسال دیدگاه خود ابتدا باید در سایت لاگین نمایید.