درک closureهای JavaScript - بخش 2
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 9 دقیقه

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

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

در نهایت، یک closure

به این کد نگاهی داشته، و سعی کنید درک کنید که چه اتفاقی می‌افتد.

1: function createCounter() {
 2:   let counter = 0
 3:   const myFunction = function() {
 4:     counter = counter + 1
 5:     return counter
 6:   }
 7:   return myFunction
 8: }
 9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)

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

  1. خطوط ۱ تا ۸. متغیر جدیدی به نام createCounter در زمینه اجرایی global تعریف می‌کنیم، و تعریف تابع مربوطه به آن اختصاص داده می‌شود.
  2. خط ۹. متغیر جدیدی به نام increment در زمینه اجرایی global تعریف می‌کنیم.
  3. باز هم در خط ۹. ما تابع createCounter را فراخوانی کرده، و مقدار برگشتی آن را به متغیر increment اختصاص می‌دهیم.
  4. خطوط ۱ تا ۸. فراخوانی تابع. ساخت زمینه اجرایی داخلی جدید.
  5. خط ۲. در زمینه اجرایی داخلی، متغیر جدیدی به نام counter تعریف کن. عدد «صفر» را به متغیر counter اختصاص بده.
  6. خطوط ۳ تا ۶. تعریف یک متغیر جدید به نام myFunction. این متغیر در زمینه اجرایی داخلی تعریف شده است. باز هم محتویات این متغیر، یک تابع دیگر است؛ همانطور که در خطوط ۴ و ۵ تعریف شد.
  7. خط ۷. بازگرداندن محتویات متغیر myFunction. زمینه اجرایی داخلی حذف می‌شود. myFunction و counter دیگر وجود ندارند. قدرت کنترل به زمینه فراخوان برگردانده شده است.
  8. خط ۹. در زمینه فراخوان، یا زمینه اجرایی global، مقدار برگردانده شده توسط createCounter به متغیر increment اختصاص داده می‌شود. حال متغیر increment شامل یک تابع است. تابعی که توسط createCounter برگردانده شده بود. این متغیر دیگر myFunction نام ندارد، اما همان تعریفات تابع را در خود دارد. در داخل زمینه اجرایی global، این متغیر increment نام دارد.
  9. خط ۱۰. یک متغیر جدید به نام c1 تعریف کن.
  10. ادامه خط ۱۰. به دنبال متغیر increment بگرد. این متغیر یک تابع است، آن را فراخونی کن. همانطور که در خطوط ۴ و ۵ تعریف شد، این متغیر شامل تابعی است که پیش‌تر برگردانده شده بود.
  11. یک زمینه اجرایی جدید بساز. هیچ پارامتری وجود ندارد. شروع به اجرای تابع کن.
  12. خط ۴. counter = counter + 1. ما زمینه اجرایی را ساختیم، ولی هیچ متغیر داخلی‌ای تعریف نکردیم. بیایید به داخل زمینه اجرایی global‌ نگاهی داشته باشیم. هیچ متغیری به نام counter در اینجا وجود ندارد. JavaScript این را به عنوان counter = undefined + 1 ارزیابی خواهد کرد. متغیر جدیدی به نام counter تعریف کرده، و عدد ۱ را به آن اختصاص بده؛ زیرا undefined برابر با 0 است.
  13. خط ۵. ما محتویات متغیر counter، یا عدد ۱ را بر می‌گردانیم. ما زمینه اجرایی داخلی، و متغیر counter را از بین می‌بریم.
  14. به خط ۱۰ بر می‌گردیم. مقدار برگردانده شده (1) به متغیر c1 اختصاص داده می‌شود.
  15. خط ۱۱. قدم‌های ۱۰ تا ۱۴ را تکرار می‌کنیم، و c2 نیز مقدار ۱ را می‌گیرد.
  16. خط ۱۲. قدم‌های ۱۰ تا ۱۴ را تکرار می‌کنیم، و c3 نیز مقدار ۱ را می‌گیرد.
  17. خط ۱۳. محتویات متغیرهای c1، c2 و c3 را log می‌کنیم.

این کار را خودتان امتحان کنید و ببینید که چه اتفاقی می‌افتد. متوجه خواهید شد که بر خلاف انتظار شما، مقادیر ۱، ۱ و ۱ لاگ نمی‌شوند. در عوض، مقادیر ۱، ۲ و ۳ لاگ خواهند شد. تابع increment به نوعی مقدار counter را به یاد نگه می‌دارد. اما چگونه؟

آیا counter بخشی از زمینه اجرایی global است؟ اگر دستور console.log(counter) را امتحان کنید، مقدار undefined را خواهید گرفت. پس این پاسخ ما نیست.

شاید وقتی که increment را فراخوانی می‌کنید، به نوعی به تابعی که در آن ساخته شده بود (createCounter) بر می‌گردد؟ متغیر increment شامل تابع است، نه جایی که ساخته شده بود. پس این هم پاسخ ما نیست.

پس باید مکانیزم دیگری در میان باشد: Closure.

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

پس توضیحات بالای ما کاملا اشتباه بود؛ بیایید مجددا به صورت صحیح آن را امتحان کنیم.

1: function createCounter() {
 2:   let counter = 0
 3:   const myFunction = function() {
 4:     counter = counter + 1
 5:     return counter
 6:   }
 7:   return myFunction
 8: }
 9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
  1. خطوط ۱ تا ۸. متغیر جدیدی به نام createCounter در زمینه اجرایی global می‌سازیم، و این متغیر تابع خود را دریافت می‌کند. درست به مانند بالا.
  2. خط ۹. متغیر جدیدی به نام increment در زمینه اجرایی global تعریف می‌کنند. درست به مانند بالا.
  3. با هم خط ۹. ما تابع createCounter را فراخوانی کرده، و مقدار برگشتی آن را به متغیر increment اختصاص می‌دهیم. درست به مانند بالا.
  4. خطوط ۱ تا ۸. فراخونی تابع. ساخت یک زمینه اجرایی داخلی جدید. درست به مانند بالا.
  5. خط ۲. در داخل زمینه اجرایی داخلی، متغیر جدیدی به نام counter تعریف کن. عدد «صفر» را به متغیر counter اختصاص بده. درست به مانند بالا.
  6. خطوط ۳ تا ۶. تعریف یک متغیر جدید به نام myFunction. این متغیر در زمینه اجرایی داخلی تعریف شده است. باز هم محتویات این متغیر، یک تابع دیگر است؛ همانطور که در خطوط ۴ و ۵ تعریف شد. ما همچنین یک closure‌ می‌سازیم و آن را به عنوان بخشی از تابع، include می‌کنیم. این closure متغیرهایی که در آن هنگام وجود دارند را شامل است؛ که در این مورد متغیر counter است. (با مقدار «صفر»)
  7. خط ۷. برگرداندن محتویات متغیر myFunction. زمینه اجرایی داخلی حذف شده است. متغیرهای myFunction و counter دیگر وجود ندارند. کنترل به زمینه فراخوان برگردانده شده است. پس ما تابع، closure آن، و متغیرهایی که پیش‌تر ساخته شده بودند را بر می‌گردانیم.
  8. خط ۹. در زمینه فراخوان، یا زمینه اجرایی global، مقدار برگردانده شده توسط createCounter به متغیر increment اختصاص داده می‌شود. حال متغیر increment شامل یک تابع (و closure) است. تابعی که توسط createCounter برگردانده شده بود. این متغیر دیگر myFunction نام ندارد، اما همان تعریفات تابع را در خود دارد. در داخل زمینه اجرایی global، این متغیر increment نام دارد.
  9. خط ۱۰. یک متغیر جدید به نام c1 تعریف کن.
  10. ادامه خط ۱۰. به دنبال متغیر increment بگرد. این متغیر یک تابع است، آن را فراخونی کن. همانطور که در خطوط ۴ و ۵ تعریف شد، این متغیر شامل تابعی است که پیش‌تر برگردانده شده بود. (همچنین متغیرها را نیز به همراه خود دارد)
  11. یک زمینه اجرایی جدید بساز. هیچ پارامتری وجود ندارد. شروع به اجرای تابع کن.
  12. خط ۴. counter = counter + 1. ما باید به دنبال متغیر counter‌ بگردیم. قبل از این که به زمینه‌های اجرایی داخلی و global نگاه داشته باشیم، بیایید به متغیرهایی که به همراه closure آوردیم نگاهی بیندازیم. Closure شامل متغیری به نام counter است، که مقدار «صفر» را در خود دارد. پس از عملیات‌های خط ۴، مقدار آن برابر با «۱» قرار داده شده، و مجددا به همراه متغیرهای ما ذخیره شده است. حال closure متغیر counter با مقدار «۱» را داراست.
  13. خط ۵. ما محتویات متغیر counter، یا عدد ۱ را بر می‌گردانیم. ما زمینه اجرایی داخلی، و متغیر counter را از بین می‌بریم.
  14. به خط ۱۰ بر می‌گردیم. مقدار برگردانده شده (1) به متغیر c1 اختصاص داده می‌شود.
  15. خط ۱۱. قدم‌های ۱۰ تا ۱۴ را تکرار می‌کنیم. این بار، وقتی که به closure خود نگاه می‌کنیم، می‌بینیم که مقدار «۱» را در خود دارد. این اتفاق، در قدم ۱۲ یا خط ۴ برنامه افتاد. مقدار آن افزایش می‌یابد و در closure تابع increment، با مقدار ۲ ذخیره می‌شود. به این صورت، c2 مقدار ۲ را می‌گیرد.
  16. خط ۱۲. قدم‌های ۱۰ تا ۱۴ را تکرار می‌کنیم، و c3 مقدار ۳ را می‌گیرد.
  17. خط ۱۳. محتویات متغیرهای c1، c2 و c3 را log می‌کنیم.

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

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

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

closure غیر بدیهی

گاهی اوقات closureها وقتی که اصلا متوجه آن‌ها نمی‌شوید، ظاهر می‌شوند. شاید مثالی از چیزی که به آن می‌گوییم «برنامه جزئی» (Partial Application) را دیده باشید. به مانند کد زیر:

let c = 4
const addX = x => n => n + x
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)

اگر تابع پیکانی باعث گیج شدن شما می‌شود، معادل آن را در زیر می‌بینید:

let c = 4
function addX(x) {
  return function(n) {
     return n + x
  }
}
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)

ما یک تابع اضافه کننده عمومی به نام addX تعریف می‌کنیم که یک پارامتر (x) را می‌گیرد و یک تابع دیگر را بر می‌گرداند.

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

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

در این مثال، کنسول عدد «۷» را چاپ خواهد کرد.

نتیجه گیری

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

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
در انتظار ثبت رای

/@er79ka

دیدگاه و پرسش

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

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

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