درک closureهای JavaScript - بخش 1

گردآوری و تالیف : عرفان کاکایی
تاریخ انتشار : 13 شهریور 1397
دسته بندی ها : جاوا اسکریپت

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

قبل از شروع...

قبل از درک closureها، درک برخی مفاهیم دیگر مهم است. یکی از آن‌ها، execution context (زمینه اجرایی) است. وقتی که کد در JavaScript اجرا می‌شود، محیطی که کد در آن اجرا شده است بسیار مهم بوده، و به صورت یکی از این موارد ارزیابی می‌شود:

Global code - محیط پیشفرضی که کد شما برای اولین بار در آن اجرا می‌شود.

Function - هرجایی که جریان اجرایی، وارد بدنه یک تابع می‌شود.

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

  1. JavaScript یک زمینه اجرایی جدید، یا به عبارتی یک زمینه اجرای local (داخلی) می‌سازد.
  2. این زمینه اجرایی داخلی، مجموعه متغیرهای به خصوص خود را دارا خواهد بود، که این متغیرها برای آن زمینه اجرایی، داخلی خواهند بود.
  3. زمینه اجرایی جدید به execution stack (پس‌زمینه اجرایی) ارسال می‌شود. پس‌زمینه اجرایی را به عنوان مکانیزمی برای رهگیری موقعیت برنامه در نظر بگیرید.

تابع چه زمانی به پایان می‌رسد؟ وقتی که به یک بیانیه return، یا یک براکت بسته ( } ) بر می‌خورد. وقتی که یک تابع به اتمام می‌رسد، این اتفاقات می‌افتند:

  1. زمینه اجرایی داخلی از پس‌زمینه اجرایی حذف می‌شود.
  2. تابع مقدار برگشتی را به calling context (زمینه فراخوان) می‌فرستد. زمینه فراخوان، زمینه‌ اجرایی‌ای است که این تابع را فراخوانی کرده است. این زمینه می‌تواند زمینه اجرایی global یا یک زمینه اجرایی داخلی دیگر باشد. در این نقطه، رسیدگی به بازگردانی مقدار به زمینه اجرای فراخوان بستگی دارد. مقدار بازگردانده شده می‌تواند یک آبجکت، آرایه، مقدار boolean، یا هر چیز دیگری باشد. اگر این تابع هیچ بیانیه returnای ندارد، مقدار undefined برگردانده می‌شود.
  3. زمینه اجرایی داخلی از بین نمی‌رود. این نکته مهم است. تمام متغیرهایی که در زمینه اجرایی داخلی تعریف شده بودند، از بین می‌روند. این متغیرها دیگر در دسترس نیستند و به همین علت است که متغیرهای داخلی نام دارند.

یک مثال پایه

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

1: let a = 3
2: function addTwo(x) {
3:   let ret = x + 2
4:   return ret
5: }
6: let b = addTwo(a)
7: console.log(b)

برای درک نحوه کار موتور JavaScript، بیایید این کد را بکشنیم:

  1. در خط ۱ ما یک متغیر جدید به نام «a» در زمینه اجرایی global تعریف کرده، و مقدار «۳» را به آن اختصاص دادیم.
  2. در اینجا مسئله کمی پیچیده می‌شود. خطوط ۲ تا ۵ در واقع با هم هستند. ما یک متغیر جدید به نام «addTwo» در زمینه اجرایی global تعریف کردیم. سپس یک تابع را به آن اختصاص دادیم. هر چیزی که میان براکت‌ها باشد، به آن اختصاص داده می‌شود. کد داخل تابع ارزیابی یا اجرا نشده است، بلکه فقط در یک متغیر برای استفاده در زمان آینده ذخیره شده است.
  3. پس حال به خط ۶ می‌رسیم. این خط ساده به نظر می‌آید، اما نکات بیشتری در آن وجود دارند. در ابتدا ما یک متغیر جدید در زمینه اجرایی داخلی تعریف کرده، و آن را «b» نام‌گذاری می‌کنیم. به محض این که یک متغیر تعریف شود، مقدار «undefined» را در خود ذخیره می‌کند.
  4. سپس، هنوز هم در خط ۶، یک عملگر اختصاص‌دهی را می‌بینیم. ما آماده می‌شویم تا یک مقدار جدید را به متغیر b اختصاص دهیم. سپس یک تابع را می‌بینیم که فراخوانی می‌شود. وقتی که می‌بینید یک متغیر به دنبال خود «...» را دارد، نشانگر این است که یک تابع فراخوانی شده است. به طور خلاصه، هر تابعی چیزی را بر می‌گرداند. (چه یک مقدار، یک آبجکت یا مقدار «undefined») هر چیزی که از این تابع برگردانده شود، به متغیر b اختصاص داده می‌شود.
  5. اما ابتدا باید تابعی که addTwo نام دارد را فراخوانی کنیم. JavaScript همینطور پیش رفته و در حافظه زمینه اجرایی global خود به دنبال متغیری به نام addTwo‌ می‌گردد، که در قدم‌ ۲ (خطوط ۲ تا ۵) تعریف شده بود. حال می‌بینید که متغیر addTwo شامل یک تابع است. دقت کنید که متغیر a به عنوان یک آرگومان به این تابع منتقل می‌شود. JavaScript در حافظه زمینه اجرایی global خود، به دنبال متغیری به نام a می‌گردد، آن را پیدا می‌کند، می‌بیند که مقدار ۳ را در خود دارد و عدد ۳ را به عنوان یک آرگومان به تابع مربوطه می‌فرستد. در نهایت، آماده اجرای تابع است.
  6. حال زمینه اجرایی تعویض می‌شود. یک زمینه اجرایی داخلی جدید ساخته شده است. بیایید آن را «addTwo execution context» نامگذاری کنیم. زمینه اجرایی به پس‌زمینه فراخوان منتقل می‌شود. اولین کاری که باید در زمینه اجرایی داخلی انجام دهیم چیست؟
  7. شاید بخواهید بگویید: «متغیر جدیدی به نام «ret» در زمینه اجرایی داخلی تعریف شده است.» این پاسخ صحیح نیست! پاسخ صحیح این است که ما باید اول به پارامترهای تابع نگاه کنیم. متغیر جدیدی به نام «x» در زمینه اجرایی داخلی تعریف شده است. و از آنجایی که ما عدد ۳ را به عنوان آرگومانش ارسال کردیم، متغیر x برابر با ۳ قرار داده خواهد شد.
  8. قدم بعدی این است که متغیر جدیدی به نام ret در زمینه اجرایی داخلی تعریف شده، و مقدار «undefined» به آن اختصاص داده شود. (خط ۳)
  9. همچنان در خط ۳، یک کار دیگر نیز باید انجام شود. در ابتدا به مقدار x نیاز داریم. JavaScript به دنبال متغیر x خواهد گشت. وقتی که آن را پیدا کرد، می‌بینید که مقدار آن ۳ است. حال مقدار بعدی، ۲ است. نتیجه جمع، یعنی ۵ به متغیر ret اختصاص داده خواهد شد.
  10. خط ۴. ما محتویات متغیر ret را بر می‌گردانیم، و یک گشت دیگر در زمینه اجرایی داخلی انجام می‌دهیم. ret شامل مقدار ۵ است. این تابع، مقدار ۵ را بر می‌گرداند، و سپس به اتمام می‌رسد.
  11. خطوط ۴ و ۵. تابع به پایان می‌رسد. زمینه اجرایی داخلی از بین رفته است. متغیر‌های x و ret حذف شده‌اند و دیگر وجود ندارند. زمینه از پس‌زمینه فراخوان پاک شده است و مقدار برگشتی در زمینه فراخوان قرار دارد. در این مورد، زمینه فراخوان، زمینه اجرایی global است؛ زیرا تابع addTwo از زمینه اجرایی global فراخوانی شده بود.
  12. حال کار خود در قدم ۴ را ادامه می‌دهیم. مقدار برگشتی، (عدد ۵) به متغیر b اختصاص داده می‌شود. ما همچنان در خط ۶ این برنامه کوچک هستیم.
  13. به جزئیات وارد نمی‌شوم، اما در خط ۷ محتویات متغیر b در کنسول چاپ می‌شود، که در مثال ما عدد ۵ است.

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

سطح لغوی (Lexical Scope)

ما باید برخی ابعاد سطح لغوی را درک کنیم. نگاهی به این مثال داشته باشید:

1: let val1 = 2
2: function multiplyThis(n) {
3:   let ret = n * val1
4:   return ret
5: }
6: let multiplied = multiplyThis(6)
7: console.log('example of scope:', multiplied)

در اینجا هدف این است که متغیرهایی را در زمینه اجرایی داخلی، و متغیرهایی را در زمینه اجرایی global داشته باشیم. یکی از پیچیگی‌های JavaScript، نحوه گشتن آن برای متغیرها است. اگر نتواند متغیری را در زمینه اجرایی داخلی خود پیدا کند، در زمینه فراخوان خود به دنبال آن خواهد گشت. حال اگر در زمینه فراخوان خود نیز آن را پیدا نکند، به زمینه اجرایی global می‌رسد. (البته اگر در آنجا نیز یافت نشود، برابر با undefined قرار خواهد گرفت) با مثال بالا پیش بروید. آن مثال در واضح سازی‌اش به شما کمک خواهد کرد. اگر می‌دانید که scope چگونه کار می‌کند، می‌توانید این بخش را رد کنید.

  1. یک متغیر جدید به نام «val1» در زمینه اجرایی global تعریف کن و مقدار ۲ را به آن اختصاص بده.
  2. خطوط ۲ تا ۵. متغیر جدیدی به نام multiplyThis تعریف کن و یک تابع را به آن اختصاص بده.
  3. خط ۶. متغیر جدیدی به نام multiplied در زمینه اجرایی global تعریف کن.
  4. متغیر multiplyThis را از حافظه زمینه اجرایی global بازیابی کن و آن را به عنوان یک تابع اجرا کن. عدد ۶ را به عنوان آرگومان آن انتقال بده.
  5. فراخوانی تابع جدید = زمینه اجرایی جدید. یک زمینه اجرایی داخلی جدید بساز.
  6. در زمینه اجرایی داخلی، متغیری به نام n‌ تعریف کرده، و مقدار ۶ را به آن اختصاص بده.
  7. خط ۳. در زمینه اجرایی داخلی، متغیری به نام ret تعریف کن.
  8. ادامه خط ۳. یک عملیات ضرب با دو مقدار اجرا کن؛ محتویات متغیرهای n و val1. به دنبال متغیر n در زمینه اجرایی داخلی بگرد. ما آن را در قدم ۶ تعریف کردیم. محتویات آن، عدد ۶ است. به دنبال متغیر val1 در زمینه اجرایی داخلی بگرد. زمینه اجرایی داخلی متغیری به نام val1 ندارد. زمینه فراخوان را بررسی کن. زمین فراخوان، زمینه اجرایی global است. به دنبال متغیر val1 در زمینه اجرایی global بگرد. این متغیر یافته شد و در مرحله ۱ تعریف شده بود. مقدار آن، برابر با ۲ است.
  9. ادامه خط ۳. دو مقدار موجود را ضرب کرده و آن را به متغیر ret اختصاص بده. 6 * 2 = 12. حال ret برابر با 12 است.
  10. متغیر ret را برگردان. زمینه اجرایی داخلی، به همراه متغیرهایش یعنی ret و n از بین رفته است. متغیر val1 از بین نرفته است؛ زیرا بخشی از زمینه اجرایی global بود.
  11. به خط ۶ بر می‌گردیم. در زمینه فراخوان، عدد ۱۲ به متغیر multiplied اختصاص داده می‌شود.
  12. در نهایت در خط ۷، مقدار متغیر multiplied را در کنسول نمایش می‌دهیم.

پس در این مثال، باید به یاد داشته باشیم که یک تابع به متغیرهایی که در زمینه فراخوانش تعریف شده‌اند، دسترسی دارد. نام معمولی این پدیده، «سطح لغوی» است.

تابعی که یک تابع را بر می‌گرداند

در مثال اول، addTwo یک عدد را بر می‌گرداند. به یاد داشته باشید که یک تابع می‌تواند هر چیزی را برگرداند. بیایید مثالی از یک تابع را ببینیم که یک تابع دیگر را بر می‌گرداند؛ زیرا این مسئله برای درک closureها ضروری است. در اینجا مثالی را می‌بینید که می‌خواهیم تجزیه و تحلیل کنیم:

1: let val = 7
 2: function createAdder() {
 3:   function addNumbers(a, b) {
 4:     let ret = a + b
 5:     return ret
 6:   }
 7:   return addNumbers
 8: }
 9: let adder = createAdder()
10: let sum = adder(val, 8)
11: console.log('example of function returning a function: ', sum)

حال بیایید قدم به قدم آن را بشکنیم.

  1. خط ۱. متغیری به نام val در زمینه اجرایی global تعریف کرده، و عدد ۷ را به آن اختصاص می‌دهیم.
  2. خطوط ۲ تا ۸. متغیری به نام createAdder در زمینه اجرایی global تعریف کرده، و یک تابع را به آن اختصاص می‌دهیم. خطوط ۳ تا ۷، تابع مورد نظر را تعریف می‌کنند. به مانند قبل، در این نقطه به تابع وارد نمی‌شویم؛ بلکه فقط تابع را در آن متغیر (createAdder) ذخیره می‌کنیم.
  3. خط ۹. متغیر جدیدی به نام adder در زمینه اجرایی global تعریف می‌کنیم. به صورت موقت، مقدار undefined را به آن اختصاص می‌دهیم.
  4. همچنان در خط ۹. ما می‌توانیم براکت‌ها ببینیم؛ حال باید یک تابع را اجرا کرده، یا فراخوانی کنیم. بیایید حافظه زمینه اجرایی global را بررسی کرده، و به دنبال متغیری به نام createAdder بگردیم. این متغیر در قدم ۲ ساخته شد، پس بیایید آن را فراخوانی کنیم.
  5. فراخوانی یک تابع. حال در خط ۲ هستیم. یک زمینه اجرایی داخلی جدید ساخته شده است. ما متغیرهای داخلی جدیدی در زمینه اجرایی جدید می‌سازیم. موتور JavaScript، زمینه اجرایی جدید را به پس‌زمینه فراخوانی اضافه می‌کند. این تابع هیچ آرگومانی ندارد، بیایید مستقیما به بدنه آن برویم.
  6. همچنان در خطوط ۳ تا ۶. ما یک تابع جدید داریم. یک متغیر جدید به نام addNumbers در زمینه اجرایی داخلی می‌سازیم. این بخش مهم است. AddNumbers فقط در زمینه اجرایی داخلی وجود دارد. ما یک تابع را در متغیر داخلی addNumbers اضافه می‌کنیم.
  7. حال در خط ۷ هستیم. محتویات متغیر addNumbers را بر می‌گردانیم. موتور JavaScript به دنبال متغیری به نام addNumbers گشته، و آن را پیدا می‌کند. یک متغیر، یک تابع را در خود دارد. یک تابع می‌تواند هر چیزی را برگرداند، که همه چیز شامل یک تابع دیگر نیز می‌شود. پس ما تعریفات تابع addNumbers را بر می‌گردانیم. هر چیزی که در میان براکت‌ها در خطوط ۴ و ۵ قرار دارد، تعریفات تابع را تشکیل می‌دهند. ما همچنین زمینه اجرایی داخلی را از زمینه فراخوانی حذف می‌کنیم.
  8. در هنگام برگرداندن، (return) زمینه اجرایی از بین می‌رود. متغیر addNumbers دیگر وجود ندارد. گرچه تعریفات تابع همچنان وجود دارد؛ زیرا از تابع برگردانده شده است و به متغیر adder اختصاص داده شده است. این متغیر را در قدم ۳ ساختیم.
  9. حال به خط ۱۰ رسیده‌ایم. متغیر جدیدی به نام sum در زمینه اجرایی global تعریف می‌کنیم. به طور موقت مقدار undefined را به آن اختصاص می‌دهیم.
  10. سپس باید یک تابع را اجرا کنیم. اما کدام تابع؟ تابعی که داخل متغیر adder‌ تعریف شده است. ما در زمینه اجرایی global به دنبال آن می‌گردیم، و آن را پیدا می‌کنیم. این تابع، تابعی است که دو پارامتر را می‌گیرد.
  11. بیایید این دو پارامتر را بازیابی کنیم، تا بتوانیم تابع را فراخوانی کرده، و آرگومان‌های صحیح را منتقل کنیم. اولین آرگومان، متغیری به نام val است، که در قدم ۱ تعریف کردیم و نمایانگر عدد ۷ است، و آرگومان دوم نیز عدد ۸ است.
  12. حال باید تابع را اجرا کنیم. تعریفات تابع در خطوط ۳ تا ۵ قرار دارد. یک زمینه اجرایی داخلی جدید ساخته می‌شود. داخل این زمینه اجرایی داخلی، دو متغیر جدید با نام‌های a و b‌ می‌سازیم. این دو متغیر به ترتیب اعداد ۷ و ۸ را دارند؛ درست به مانند آرگومان‌هایی که در تابع موجود در قدم قبلی منتقل کردیم.
  13. خط ۴. متغیر جدیدی به نام ret، در زمینه اجرایی داخلی تعریف شده است.
  14. خط ۴. یک عملیات جمع اجرا می‌شود، که در آن محتویات متغیرهای a و b را با هم جمع می‌کنیم. نتیجه جمع (15) به متغیر ret اختصاص داده می‌شود.
  15. متغیر ret از آن تابع برگردانده شده است. زمینه اجرایی داخلی از بین رفته، و از پس‌زمینه فراخوانی حذف شده است. متغیرهای a، b و ret دیگر وجود ندارند.
  16. مقدار برگردانده شده، به متغیر sum که در قدم ۹ تعریف کردیم، اختصاص داده شده است.
  17. مقدار sum را در کنسول چاپ می‌کنیم.

همانطور که انتظار می‌رود، کنسول مقدار ۱۵ را چاپ خواهد کرد. در اینجا از مراحل سختی می‌گذریم. سعی می‌کنم که به دو نکته مجددا اشاره کنم. اول این که یک تابع می‌تواند در یک متغیر ذخیره شود. یک تابع تا وقتی که فراخوانی شود، برای برنامه نامرئی است. دوم این که هر زمان که یک تابع فراخوانی می‌شود، یک زمینه اجرایی داخلی (به طور موقت) ساخته می‌شود. این زمینه اجرایی، وقتی که کار تابع تمام می‌شود از بین می‌رود. یک تابع وقتی که به یک return یا براکت بسته بر می‌خورد، به اتمام می‌رسد.

حال تمام پیش‌زمینه‌های مورد نیاز را گذراندیم و در بخش بعد، بالاخره با خود closure شروع خواهیم کرد.

منبع

مقالات پیشنهادی

درک closureهای JavaScript - بخش 2

همانطور که نام این مقاله اشاره می‌کند، درک closureهای JavaScript همیشه یک مسئله سخت هستند. من تعداد زیادی مقاله در این زمینه خوانده‌ام، از آن‌ها در کا...

وب سایت های الهام بخش برای طراحی - سری 10

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

15 کتابخانه جالب javascript و css

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

15 کتابخانه جالب javascript و css دی ۹۵

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