Mobin Varnaseri
Mobin Varnaseri
خواندن ۷ دقیقه·۱ سال پیش

نگاه عمیق به مفهوم Closure در توابع جاوا اسکریپت

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

با این حال، بنظر من با توضیح درست، در واقع آنقدرها هم درک آن سخت نیست، اما بدون داشتن درک کافی از مباحثی مانند execution context، call stack، scope chain درک Closure ممکن نیست.

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

با ایجاد یک تابع جدید به نام secureBooking‍‍ شروع میکنیم.

چیزی که در مورد این تابع خاص است، این است که یک تابع جدید را ریترن میکند. و کاری که ما در این تابع جدید انجام می‌دهیم، به روز رسانی متغیر "passengerCount" است که در تابع والد تعریف شده است. (دقت به این پاراگراف مهم است.)

حالا بیایید تابع "secureBooking" را فراخوانی کنیم و سپس نتیجه را در متغیری به نام "booker" ذخیره کنیم.

تا اینجا، ما یک تابع داریم (secureBooking) که آن را فراخوانی می‌کنیم و این تابع، تابع داخلی‌اش را return میکند. که ما آن را در متغیر "booker" ذخیره می‌کنیم. خب حالا، "booker" نیز یک تابع است، درسته؟

دقت داشته باشید که تابع booker دقیقا حاوی کد زیر است و اینکه تابع booker دقیقا همان تابع ریترن شده از داخل تابع والد است نه یک کپی از آن! در واقع خود آن تابع الان در booker ذخیره شده است.

خب حالا بیایید چندبار تابع booker را فراخوانی کنیم.

حالا نتیجه‌ی آن را در کنسول ببینیم:

ما چندین بار تابع booker را فرخوانی کردیم و هر دفعه مقدار pasengerCount افزایش پیدا کرد. اما نکته این است که وقتی تابع booker متغیر pasengerCount را در خودش ندارد، چطور همچین چیزی (دسترسی به متغیر PasengerCount و افزایش آن) امکان دارد؟

بیایید با جزئیات تجزیه و تحلیل کنیم که وقتی خط کد
()const booker = secureBooking
اجرا می شود چه اتفاقی می‌افتد:

هنگامی که "SecureBooking" توسط خط کد
()const booker = secureBooking
فراخوانی و اجرا می‌شود، متغیر "pasengerCount" روی صفر ست شده است. و اسکوپ آن متغیر به صورت لوکال است یعنی فقط در همان بلاک تابع secureBooking قابل دسترسی است.

در مرحله ی بعد، تابع "secureBooking" یک تابع جدید ریترن میکند که در متغیر "booker" ذخیره می شود. و سپس خود تابع secureBooking از کال استک خارج میشود. (باید بدانیم وقتی یک تابع از کال استک خارج میشود، دیگر امکان دسترسی به متغیر های لوکال درون آن وجود ندارد. به یاد داشتن این نکته خیلی مهم است)

خب حالا ما یک تابع ریترن شده داریم (در booker ذخیره شده است) که دیگر دسترسی به متغیر pasengerCount ندارد درست است؟

حالا وقتی booker را فراخوانی می‌کنیم چطور ممکن است که هنوز به متغیر pasengerCount و مقدار آن دسترسی داشته باشد تا آن را ++ و سپس آپدیت کند و در کنسول نمایش دهد؟

دوباره این نکته را یادآوری میکنم که تابع secureBooking از کال استک خارج شده است و طبیعتا دسترسی به متغیر های درون آن به صورت کلی نباید وجود داشته باشد.

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

اما ببینیم که Closure واقعاً چگونه کار می‌کند. و راز کار آن چیست؟

راز آن در یک جمله، این است: «هر تابعی همیشه به متغیرهای موجود در زنجیره اسکوپ محلی که در آن ایجاد شده است، دسترسی دارد.»

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

در واقع محل ساخته شدن تابع booker (یا همان تابع ریترن شده)، تابع secureBooking است. و در لحظه ای که تابع secureBooking هنوز درون کال استک است یک زنجیره اسکوپ مشخص دارد و تابع درون آن هم به متغییر های درون آن اسکوپ ها دسترسی دارد.
در مرحله بعد تابع والد از کال استک خارج و نابود میشود اما تابع booker همچنان به همان متغییر هایی که قبلا دسترسی داشت، الان هم دسترسی دارد. و یک چیزی این ارتباط ناممکن(!) را حفظ کرده است که ما به این ارتباط و این مفهوم Closure می‌گوییم.

دوباره تکرار می‌کنیم تا موضوع روشن شود.
به عبارت دقیق تر یک تابع همیشه به variable environment درون execution context ای که در آن ایجاد شده است، دسترسی دارد، حتی پس از بین رفتن execution context. (دقت به این قسمت آخر واقعا مهم است.)

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

اما قبل از آن،
چیزی که در اینجا بسیار مهم است این است که تابع booker به لطف Closure به متغیر PasengerCount دسترسی دارد زیرا اساساً در محدوده ای تعریف شده است که تابع بوکر واقعاً در آن ایجاد شده بود. پس، زنجیره scope در واقع از طریق Closure حفظ می شود، حتی زمانی که یک execution context از بین رفته است (و طبیعتا variable environment آن هم همینطور) اما در واقع حتی اگر execution context از بین رفته باشد، variable environment هنوز هم به نحوی در جایی در موتور زندگی می کند!

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


  1. یک Closure، در واقع یک variable environment پک شده از طرف execution context است که تابع در آن ایجاد شده است ، حتی پس از آن که execution context از بین می رود.
  2. تعریف ساده تر شده از تعریف قبل: Closure به یک تابع، دسترسی تمام متغیرهای تابع والد اش را میدهد. این تابع یک رفرنس از اسکوپ خارجی اش نگه میدارد که باعث میشود زنجیره اسکوپ را در طول زمان حفظ شود.
  3. تعریف ساده تر شده از تعریف قبل: Closure اطمینان می دهد که یک تابع هرگز ارتباطش را با متغیرهای موجود در زادگاهش، از دست نمی دهد. و این متغیرها را حتی پس از از بین رفتن محل تولد به یاد می آورد. این مثل شخصی است که ارتباط خود را با زادگاه خود از دست نمی دهد.
  4. برخی از افراد دوست دارند به این variable environment متصل، به عنوان کوله پشتی فکر کنند. پس، در این تشبیه، یک تابع دارای کوله پشتی ای است که هر کجا که می رود، آن را حمل می کند. این کوله پشتی شامل تمام متغیرهایی است که در محیطی که تابع در آن ایجاد شده است وجود دارد. هر زمان که یک متغیر در اسکوپ تابع پیدا نشود ، موتور JavaScript به کوله پشتی نگاه می کند و متغیر گمشده را از آنجا می گیرد.

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

با این حال ، ما می توانیم از Console.dir استفاده کنیم تا متغییر های Closure را ببینیم.

نتیجه ی کنسول:

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


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

شاید تاپیک یکم تخصصی بود اما هنوز دوست دارم چیزای بیشتری در مورد Js یاد بگیرم و عمیق تر اون رو درک کنم، پس اگر نکته ای بود و یا نظری داشتید خیلی خوشحال میشم که حتما حتما حتمااااااا کامنت کنید! (;


جاوا اسکریپتClosureتوابع جاوا اسکریپتفرانت اندبرنامه نویسی
Front-end Web developer
شاید از این پست‌ها خوشتان بیاید