Closure در جاوا اسکریپت چیست؟ – توضیح کلوژر به زبان ساده + مثال و کد

۹۰۶ بازدید
آخرین به‌روزرسانی: ۰۷ تیر ۱۴۰۲
زمان مطالعه: ۱۸ دقیقه
Closure در جاوا اسکریپت چیست؟ – توضیح کلوژر به زبان ساده + مثال و کد

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

Closure در جاوا اسکریپت چیست؟

«بستار» (Closure) نوعی ویژگی بسیار تاثیرگذار است که در جاوا اسکریپت و همچنین بسیاری از زبان‌های برنامه نویسی دیگر یافت می‌شود. طبق تعریف ارائه شده به وسیله «MDN»، کلوژرها توابعی هستند که به متغیرهای مستقل ارجاع می‌دهند. به این متغیرها، متغیرهای آزاد نیز می‌گویند. به عبارت دیگر در جاوا اسکریپت، Closure‌ها به عنوان شکلی از «تعیین محدوده واژگانی» (Lexical scoping) برای حفظ متغیرها از محدوده بیرونی تابع در محدوده درونی آن استفاده می‌شوند. محدوده واژگانی، محدوده متغیر را بر اساس موقعیت آن در کد منبع تعیین می‌کند.

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

closure در جاوا اسکریپت چیست
  • نکته: توجه به این نکته مهم است که متغیرهای آزاد متغیرهایی هستند که نه به صورت محلی در تابع اعلان می‌شوند و نه به عنوان پارامتر ارسال خواهند شد.

مثال Closure در جاوا اسکریپت

برای درک بهتر مفهوم Closure در زبان برنامه نویسی جاوا اسکریپت در ادامه ۲ مثال از این مبحث ارائه خواهد شد. قطعه کد مثال اول به صورت زیر است.

1function numberGenerator() {
2  // Local “free” variable that ends up within the closure
3  var num = 1;
4  function checkNumber() { 
5    console.log(num);
6  }
7  num++;
8  return checkNumber;
9}
10
11var number = numberGenerator();
12number(); // 2

در مثال داده شده تابعی به نام numberGenerator  وجود دارد. در این تابع، متغیر محلی به نام num  به عنوان نوعی متغیر آزاد تعریف می‌شود. در کنار آن، تابع دیگری به نام checkNumber  اعلان شده است که مقدار num را در کنسول چاپ می‌کند.

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

1function sayHello() {
2  var say = function() { console.log(hello); }
3  // Local variable that ends up within the closure 
4  var hello = 'Hello, world!';
5  return say;
6}
7var sayHelloClosure = sayHello(); 
8sayHelloClosure(); // ‘Hello, world!’

در مثال فوق، هدف نشان دادن این است که Closure در جاوا اسکریپت تمام متغیرهای محلی اعلان شده در تابع محصور بیرونی خود را در بر می‌گیرد. قطعه کد بالا تابعی به نام sayHello  را تعریف می‌کند. در داخل این تابع، نوعی متغیر محلی به نام say  وجود دارد که «تابعی ناشناس» (Anonymous function) به آن اختصاص داده شده است. این تابع ناشناس مقدار متغیر hello  را در کنسول ثبت می‌کند.

خود متغیر hello بعد از تابع ناشناس در همان تابع محصور اعلان می‌شود. با وجود این، تابع ناشناس همچنان می‌تواند به متغیر hello دسترسی داشته باشد و از آن استفاده کند. دلیلش این است که در زمان ایجاد تابع ناشناس، متغیر hello قبلاً در «محدوده» (Scope) تابع تعریف شده بود که به آن اجازه می‌داد زمانی که تابع ناشناس در نهایت اجرا می‌شود، در دسترس باشد.

مفاهیم سطح بالا در مبحث Closure ها

برای به دست آوردن درک عمیق‌تر از مفهوم Closure در جاوا اسکریپت، یادگیری مفاهیم مرتبط که زمینه لازم برای درک مفهوم نام برده را فراهم می‌کند، بسیار مهم است. در این مطلب ابتدا از مفاهیم پیشرفته شروع می‌کنیم. برای این هدف ابتدا باید با مفهوم «زمینه اجرایی» (Execution Context) آشنا شد که به معنای محیطی است که تابع در آن اجرا می‌شود.

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

Execution Context چیست؟

مفهوم زمینه اجرا یا «Execution Context» نوعی مفهوم انتزاعی است که به وسیله مشخصات «ECMAScript» برای ردیابی ارزیابی زمان اجرای کدها مورد استفاده می‌گیرد. این مفهوم، محیطی را نشان می‌دهد که کدها در آن اجرا می‌شوند. توجه به تصویر زیر و توضیحات بعدی آن، برای درک «Execution Context» اهمیت دارد.

Execution context چیست

در جاوا اسکریپت، تنها یک زمینه اجرا می‌تواند در زمانی معین فعال باشد و این ویژگی آن را به نوعی زبان «تک‌رشته‌ای» (Single-Threaded) تبدیل می‌کند. این بدان معنا است که در هر لحظه فقط یک فرمان، قابل پردازش خواهد بود. مرورگرها معمولاً زمینه‌های اجرایی را با استفاده از ساختمان داده‌ای به نام «پشته» (Stack) حفظ می‌کنند. پشته بر اساس «ورودی آخر، خروجی اول» (Last In First Out) یا به اختصار «LIFO» عمل می‌کند که در آن آخرین موجودیتی که به پشته وارد شده است، اولین موردی خواهد بود که خارج می‌شود. دلیلش این است که عناصر را فقط می‌توان از بالای پشته وارد یا خارج کرد.

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

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

Execution context در برنامه نویسی

مثال Execution Context

برای درک بهتر مفهوم Execution Context در جاوا اسکریپت در ادامه مثالی ارائه شده است.

1var x = 10;
2function foo(a) {
3  var b = 20;
4
5  function bar(c) {
6    var d = 30;
7    return boop(x + a + b + c + d);
8  }
9
10  function boop(e) {
11    return e * -1;
12  }
13
14  return bar;
15}
16
17var moar = foo(5); // Closure  
18/* 
19  The function below executes the function bar which was returned 
20  when we executed the function foo in the line above. The function bar 
21  invokes boop, at which point bar gets suspended and boop gets push 
22  onto the top of the call stack (see the screenshot below)
23*/
24moar(15); 

تصویر زیر از صفحه مربوط به کدهای بالا در کنسول مرورگر در ادامه آمده است.

آموزش کلوزر در جاوا اسکریپت

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

آموزش جاوا اسکریپت پیشرفته فرادرس

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

  1. «وضعیت ارزیابی کد» (Code evaluation state): وضعیت لازم برای انجام، تعلیق و از سرگیری ارزیابی کد مرتبط در زمینه اجرا است.
  2. «تابع» (Function): شی تابعی که به وسیله زمینه اجرا ارزیابی می‌شود (یا اگر زمینه متعلق به اسکریپت یا ماژول باشد، null است).
  3.  «قلمرو» (Realm): مجموعه‌ای از اشیای داخلی، نوعی محیط سراسری ECMAScript، همه کدهای ECMAScript بارگذاری شده در محدوده آن محیط سراسری و سایر وضعیت‌ها و منابع مرتبط.
  4. «محیط واژگانی» (Lexical Environment): به منظور حل ارجاعات شناسه ساخته شده به وسیله کد در زمینه اجرا استفاده می‌شود.
  5. «محیط متغیر» (Variable Environment): نوعی محیط واژگانی که «رکورد محیطی» (Environment Record) آن حاوی پیوندهایی بوده که به وسیله «Variable Statements» در زمینه اجرا ایجاد شده است.

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

  • توجه: از نظر فنی، هم محیط متغیر و هم محیط واژگانی برای اجرای Closure در جاوا اسکریپت استفاده می‌شوند. با این حال، برای سادگی، به آن‌ها به طور جمعی به عنوان «محیط» (Environment) اشاره می‌کنیم.

محیط واژگانی

محیط واژگانی نوعی مفهوم است که برای ایجاد رابطه بین شناسه‌ها (متغیرها و توابع) و پیوندهای خاص آن‌ها در کد «ECMAScript» استفاده می‌شود. این محیط از ۲ جزء اصلی تشکیل شده است، یکی «رکورد محیطی» (Environment Record) و دیگری نوعی ارجاع بالقوه تهی به محیط واژگانی بیرونی است. هر زمان که ساختارهای کد خاصی مانند «FunctionDeclaration» ،«BlockStatement» یا «Catch clause» به عنوان یک «TryStatement» ارزیابی شوند، نوعی محیط واژگانی جدید برای مدیریت شناسه‌های مرتبط ایجاد می‌شود.

جنبه های کلیدی محیط واژگانی

از مهم‌ترین جنبه‌های کلیدی محیط واژگانی می‌توان به موارد زیر اشاره کرد:

  • برای تعریف ارتباط شناسه‌ها استفاده می‌شود: هدف اولیه محیط واژگانی، ایجاد معنی یا ارتباط شناسه‌ها در کدها است. این مولفه زمینه و اهمیت را برای متغیرها و توابع فراهم می‌کند. به عنوان مثال، در خط کد console.log(x/10)  ، متغیر (یا شناسه) x  بدون مکانیزمی برای تعریف معنای آن، بی‌معنی خواهد بود. محیط واژگانی این نقش را به کمک «Environment Record» خود انجام می‌دهد.
  • محیط واژگانی از نوعی رکورد محیطی تشکیل شده است: محیط مسئول نگهداری رکوردی از همه شناسه‌ها و پیوندهای آن‌ها در محیط واژگانی خاص است. هر محیط واژگانی دارای رکورد محیطی اختصاصی خودش است که اطلاعات لازم را برای وضوح شناسه در خود دارد.
  • «ساختار لانه‌سازی واژگانی» (Lexical nesting structure): این جنبه رابطه سلسله مراتبی بین محیط‌های واژگانی را برجسته می‌کند. یک محیط «درونی» (Inner) به محیط «بیرونی» (Outer) اشاره دارد که آن را در بر می‌گیرد و این محیط بیرونی به نوبه خود می‌تواند محیط بیرونی خاص خودش را داشته باشد. بنابراین، محیط می‌تواند به عنوان محیط بیرونی برای چندین محیط درونی عمل کند. محیط «سراسری یا جهانی» (Global) تنها محیط واژگانی است که فاقد محیط بیرونی خواهد بود. برای تجسم این موضوع، می‌توان محیط‌های واژگانی را لایه‌هایی از پیاز در نظر گرفت که محیط جهانی بیرونی‌ترین لایه است. هر لایه بعدی نشان دهنده نوعی محیط تودرتو در داخل خواهد بود.
آموزش کلوزر در جاوا اسکریپت

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

مثالی از مفهوم محیط واژگانی

مثال زیر برای درک مفهوم محیط واژگانی مهم است. محیط واژگانی به صورت زیر ساخته خواهد شد:

1LexicalEnvironment = {
2  EnvironmentRecord: {
3  // Identifier bindings go here
4  },
5  
6  // Reference to the outer environment
7  outer: < >
8};

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

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

زنجیره محدوده

در جاوا اسکریپت، مفهوم «دامنه یا محدوده» (Scope) ارتباط نزدیکی با تودرتویی سلسله مراتبی محیط‌های واژگانی دارد و از پیش‌نیازهای درک و یادگیری مفهوم Closure یا بستار در جاوا اسکریپت است.

هر محیطی به محیط والد خود دسترسی داشته که آن هم به نوبه خود به محیط والد خود دسترسی دارد و زنجیره‌ای از محیط‌ها را تشکیل می‌دهد که به «زنجیره محدوده» (Scope Chain) معروف است. مثال زیر این موضوع را نشان می‌دهد:

1var x = 10;
2
3function foo() {
4  var y = 20; // free variable
5  function bar() {
6    var z = 15; // free variable
7    return x + y + z;
8  }
9  return bar;
10}

تصویر زیر درک بهتری را از مفهوم مثال مربوطه انتقال می‌دهد.

زنجیره محدوده در جاوا اسکریپت

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

محدوده ایستا و محدوده پویا

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

در مقابل، دامنه استاتیک بر اساس متغیرهایی است که در زمان ایجاد به آن‌ها ارجاع داده شده که به وسیله ساختار «کد منبع» (Source Code) برنامه تعیین می‌شود. متغیرهایی که مراجع به آن‌ها اشاره می‌کنند بر اساس سلسله مراتب واژگانی کدها ثبت می‌شوند. برای نشان دادن تفاوت بین دامنه پویا و استاتیک، مثال‌های زیر را در نظر بگیرید:

1var x = 10;
2
3function foo() {
4  var y = x + 5;
5  return y;
6}
7 
8function bar() {
9  var x = 2;
10  return foo();
11}
12 
13function main() {
14  foo(); // Static scope: 15; Dynamic scope: 15
15  bar(); // Static scope: 15; Dynamic scope: 7
16  return 0;
17}

در مثال فوق، مشاهده می‌شود که دامنه استاتیک و داینامیک نتایج متفاوتی را هنگام فراخوانی تابع bar  به دست می‌دهند. با دامنه ایستا، مقدار بازگشتی bar بر اساس مقدار x  در زمان ایجاد foo  است. این به دلیل ساختار ایستا و واژگانی کد منبع است که برای x در آغاز 10  و در نتیجه 15  خواهد بود.

از سوی دیگر، دامنه داینامیک یا پویا با مجموعه‌ای از تعاریف متغیر عمل می‌کند که در زمان اجرا ردیابی می‌شوند. تعیین اینکه از کدام x استفاده شود بستگی به متغیرهای تعریف شده به صورت داینامیک در محدوده فعلی در زمان اجرا دارد. هنگام اجرای تابع مربوطه، x = 2  به بالای پشته منتقل می‌شود که منجر به بازیابی خروجی 7  خواهد شد. حال مثال زیر هم برای درک این مفهوم ضرورت دارد و قطعه کد آن در ادامه آمده است.

1var myVar = 100;
2 
3function foo() {
4  console.log(myVar);
5}
6 
7foo(); // Static scope: 100; Dynamic scope: 100
8 
9(function () {
10  var myVar = 50;
11  foo(); // Static scope: 100; Dynamic scope: 50
12})();
13
14// Higher-order function
15(function (arg) {
16  var myVar = 1500;
17  arg();  // Static scope: 100; Dynamic scope: 1500
18})(foo);

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

آموزش Closure در جاوا اسکریپت

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

بینش کلیدی در این رابطه این است که تابع ارجاعی به محیط (یا محدوده) خود دارد که این ویژگی آن را قادر می‌سازد تا آن محیط و متغیرهای تعریف شده در آن را به خاطر بسپارد. مثال زیر برای درک این مفهوم بیان شده مهم است:

1var x = 10;
2
3function foo() {
4  var y = 20; // free variable
5  function bar() {
6    var z = 15; // free variable
7    return x + y + z;
8  }
9  return bar;
10}
11
12var test = foo();
13
14test(); // 45

بر اساس درک کاربر از محیط‌ها، می‌توان تعریف‌های محیط را برای این مثال به صورت زیر نمایش داد:

1GlobalEnvironment = {
2  EnvironmentRecord: { 
3    // built-in identifiers
4    Array: '<func>',
5    Object: '<func>',
6    // etc..
7    
8    // custom identifiers
9    x: 10
10  },
11  outer: null
12};
13 
14fooEnvironment = {
15  EnvironmentRecord: {
16    y: 20,
17    bar: '<func>'
18  }
19  outer: GlobalEnvironment
20};
21
22barEnvironment = {
23  EnvironmentRecord: {
24    z: 15
25  }
26  outer: fooEnvironment
27};

در قطعه کد بالا وقتی تابع test  فراخوانی می‌شود، مقدار بازگشتی 45  را بازیابی می‌کند. این به این دلیل است که تابع test تابع bar  را فراخوانی خواهد کرد که حتی پس از بازگشت تابع foo  به متغیر آزاد y دسترسی دارد. تابع bar دسترسی به y را به وسیله محیط بیرونی خود که محیط foo است حفظ می‌کند. علاوه بر این، تابع bar می‌تواند به متغیر جهانی x دسترسی داشته باشد، زیرا محیط foo به محیط جهانی دسترسی دارد. این ساز و کار به عنوان «بازرسی زنجیره‌ای محدوده» (Scope-Chain Lookup) شناخته می‌شود.

مثال هایی برای  Closure در جاوا اسکریپت

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

مثال هایی برای Closure در جاوا اسکریپت

در ادامه ٣ مثال از مفهوم Closure در جاوا اسکریپت برای درک بهتر مفاهیم بیان شده رائه می‌شود.

مثال ١: ایجاد زمینه محصور کننده برای توابع

یکی از نمونه‌های رایجی که در آن خطا اتفاق می‌افتد، زمانی است که کاربر سعی می‌کند متغیر شمارنده را در حلقه For در جاوا اسکریپت با تابعی در داخل حلقه for مرتبط کند که کد زیر این مفهوم را نشان می‌دهد:

1var result = [];
2 
3for (var i = 0; i < 5; i++) {
4  result[i] = function () {
5    console.log(i);
6  };
7}
8
9result[0](); // 5, expected 0
10result[1](); // 5, expected 1
11result[2](); // 5, expected 2
12result[3](); // 5, expected 3
13result[4](); // 5, expected 4

درک مفاهیمی که تاکنون ذکر شد، تشخیص اشتباه را در اینجا آسان‌تر می‌کند. به طور انتزاعی، محیط در انتهای حلقه for را می‌توان به صورت زیر نشان داد:

1environment: {
2  EnvironmentRecord: {
3    result: [...],
4    i: 5
5  },
6  outer: null,
7}

اشتباه در این است که فرض می‌شود هر تابع در آرایه result  دارای محدوده جداگانه است. در واقع، هر پنج تابع دارای محیط یا محدوده یکسان هستند. بنابراین، هر زمان که متغیر i  افزایش یابد، دامنه اشتراک‌گذاری شده را به‌روزرسانی می‌کند و در نتیجه تمام توابع به مقدار نهایی i دسترسی دارند که با خروج از حلقه for برابر با 5  است. یکی از راه‌های رفع این مشکل، ایجاد نوعی زمینه محصور کننده اضافی برای هر تابع است تا اطمینان حاصل شود که آن‌ها زمینه و محدوده اجرای جداگانه خود را دارند که قطعه کد زیر این مفهوم را بیان می‌کند:

1var result = [];
2 
3for (var i = 0; i < 5; i++) {
4  result[i] = (function inner(x) {
5    // additional enclosing context
6    return function() {
7      console.log(x);
8    }
9  })(i);
10}
11
12result[0](); // 0, expected 0
13result[1](); // 1, expected 1
14result[2](); // 2, expected 2
15result[3](); // 3, expected 3
16result[4](); // 4, expected 4

با تغییر بالا، مشکل برطرف خواهد شد. روش هوشمندانه دیگر، استفاده از let  به جای var  در تعریف متغیرهای جاوا اسکریپت است، زیرا let دارای محدوده بلوکی است و برای هر بار پیمایش با حلقه for نوعی شناسه جدید اتصال ایجاد خواهد کرد. کدهای زیر مربوط به این مسئله است.

1var result = [];
2 
3for (let i = 0; i < 5; i++) {
4  result[i] = function () {
5    console.log(i);
6  };
7}
8
9result[0](); // 0, expected 0
10result[1](); // 1, expected 1
11result[2](); // 2, expected 2
12result[3](); // 3, expected 3
13result[4](); // 4, expected 4

مثال ٢: ایجاد Closure جداگانه

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

1function iCantThinkOfAName(num, obj) {
2  // This array variable, along with the 2 parameters passed in, 
3  // are 'captured' by the nested function 'doSomething'
4  var array = [1, 2, 3];
5  function doSomething(i) {
6    num += i;
7    array.push(num);
8    console.log('num: ' + num);
9    console.log('array: ' + array);
10    console.log('obj.value: ' + obj.value);
11  }
12  
13  return doSomething;
14}
15
16var referenceObject = { value: 10 };
17var foo = iCantThinkOfAName(2, referenceObject); // closure #1
18var bar = iCantThinkOfAName(6, referenceObject); // closure #2
19
20foo(2); 
21/*
22  num: 4
23  array: 1,2,3,4
24  obj.value: 10
25*/
26
27bar(2); 
28/*
29  num: 8
30  array: 1,2,3,8
31  obj.value: 10
32*/
33
34referenceObject.value++;
35
36foo(4);
37/*
38  num: 8
39  array: 1,2,3,4,8
40  obj.value: 11
41*/
42
43bar(4); 
44/*
45  num: 12
46  array: 1,2,3,8,12
47  obj.value: 11
48*/

در مثال فوق، می‌توان مشاهده کرد که هر فراخوانی به تابع iCantThinkOfAName  نوعی Closure  مجزا ایجاد می‌کند که با foo  و bar  نشان داده می‌شود. وقتی این توابع Closure در جاوا اسکریپت متعاقباَ فراخوانی می‌شوند، آن‌ها متغیرها را در Closureهای مربوطه خود به‌روزرسانی می‌کنند. این نشان می‌دهد که متغیرهای هر Closure باقی می‌مانند و حتی پس از بازگشت iCantThinkOfAName  به تابع doSomething iCantThinkOfAName  در دسترس خواهند بود.

مثال ۳: دسترسی به متغیرهای محیطی

قطعه کد زیر را برای دسترسی به متغیرهای محیطی در نظر می‌گیریم:

1function mysteriousCalculator(a, b) {
2  var mysteriousVariable = 3;
3  return {
4    add: function() {
5      var result = a + b + mysteriousVariable;
6      return toFixedTwoPlaces(result);
7    },
8    subtract: function() {
9      var result = a - b - mysteriousVariable;
10      return toFixedTwoPlaces(result);
11    }
12  };
13}
14
15function toFixedTwoPlaces(value) {
16  return value.toFixed(2);
17}
18
19var myCalculator = mysteriousCalculator(10.01, 2.01);
20myCalculator.add(); // 15.02
21myCalculator.subtract(); // 5.00

در مثال فوق، تابع MysteriousCalculator  در محدوده سراسری تعریف شده است و شیئی را با ۲ متد add  و subtract  برمی‌گرداند. به طور انتزاعی، محیط‌های این مثال را می‌توان به صورت زیر نشان داد:

1GlobalEnvironment = {
2  EnvironmentRecord: {
3    // built-in identifiers
4    Array: '<func>',
5    Object: '<func>',
6    // etc...
7
8    // custom identifiers
9    mysteriousCalculator: '<func>',
10    toFixedTwoPlaces: '<func>'
11  },
12  outer: null
13};
14
15mysteriousCalculatorEnvironment = {
16  EnvironmentRecord: {
17    a: 10.01,
18    b: 2.01,
19    mysteriousVariable: 3
20  },
21  outer: GlobalEnvironment
22};
23
24addEnvironment = {
25  EnvironmentRecord: {
26    result: 15.02
27  },
28  outer: mysteriousCalculatorEnvironment
29};
30
31subtractEnvironment = {
32  EnvironmentRecord: {
33    result: 5.00
34  },
35  outer: mysteriousCalculatorEnvironment
36};

با داشتن ارجاع به محیط تابع MysteriousCalculator ، متدهای add و subtract می‌توانند به متغیرهای موجود در آن محیط (a ,b ,mysteriousVariable)  برای انجام محاسبات خود دسترسی داشته باشند.

فیلم آموزش کلوزر در جاوا اسکریپت

مثال ٤: ارجاع خصوصی به متغیر در محدوده بیرونی

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

1function secretPassword() {
2  var password = 'xh38sk';
3  return {
4    guessPassword: function(guess) {
5      if (guess === password) {
6        return true;
7      } else {
8        return false;
9      }
10    }
11  }
12}
13
14var passwordGame = secretPassword();
15passwordGame.guessPassword('heyisthisit?'); // false
16passwordGame.guessPassword('xh38sk'); // true

در مثال فوق، تابع secretPassword  شیئی را با متد guessPassword  برمی‌گرداند. متغیر password  در محدوده تابع secretPassword تعریف شده است و مستقیماً از خارج قابل دسترسی نیست. تابع guessPassword به متغیر password دسترسی دارد و به آن اجازه می‌دهد حدس ارائه شده را با رمز عبور مخفی مقایسه کند. این تضمین می‌کند که رمز عبور خصوصی باقی می‌ماند و نمی‌توان از خارج از Closure در جاوا اسکریپت به آن دسترسی داشت.

Closures و حلقه ها در جاوا اسکریپت

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

برای مثال فرض می‌شود قطعه کد زیر از setTimeout  در حلقه استفاده می‌کند:

1for (var id = 0; id < 3; id++) {
2    setTimeout(function () {
3        console.log('seconds: ' + id);
4    }, id * 1000);
5}

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

"seconds: 0"
"seconds: 1"
"seconds: 2"
	

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

"seconds: 3"
"seconds: 3"
"seconds: 3"
خروجی ثبت شده در کنسول آن گونه‌ نیست که انتظار می‌رود. در عوض، مقدار id مقدار نهایی است که در حلقه وجود داشت.

کلمه کلیدی let و Closure در جاوا اسکریپت

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

مثال زیر نحوه انجام این کار را بیشتر شرح می‌دهد.

1for (let id = 0; id < 3; id++) {
2    setTimeout(function () {
3        console.log('seconds: ' + id);
4    },id * 1000);
5}
6	

در کد بالا، کلمه کلیدی let برای اعلان متغیر id در داخل حلقه for  استفاده می‌شود. این کلمه کلیدی نوعی محدوده بلوک جدید برای هر پیمایش ایجاد می‌کند و به تابع setTimeout  اجازه می‌دهد تا مقدار شناسه صحیح را در هر مرحله دریافت کند. در نتیجه، خروجی رفتار مورد انتظار را منعکس خواهد کرد که به صورت زیر است:

"seconds: 0"
"seconds: 1"
"seconds: 2"
با استفاده از کلمه کلیدی let ، اطمینان حاصل می‌شود که هر Closure در جاوا اسکریپت به وسیله تابع setTimeout کپی خود را از مقدار id دریافت می‌کند و اثرات نامطلوب اشتراک‌گذاری محدوده در همه پیمایش‌ها از بین می‌رود.
مقالات تخصصی جاوا اسکریپت

IIFE و Closure در جاوا اسکریپت

نوعی رویکرد جایگزین برای مدیریت Closure در جاوا اسکریپت در حلقه و اجتناب از مشکلی که در بالا ذکر شد، استفاده از سینتکس IIFE مخفف «Immediately Invoked Function Expression» به معنای «گزاره فراخوانی فوری تابع» است. با قرار دادن کد در تابع و فراخوانی فوری آن، می‌توان اطمینان حاصل کرد که هر پیمایش حلقه نوعی محدوده تابع مجزا با متغیرهای خاص خود را ایجاد می‌کند.

این کار به تابع setTimeout اجازه می‌دهد تا مقدار صحیح متغیر id را در هر پیمایش دریافت کند. مثال زیر برای درک راه‌حل «IIFE» مهم است:

1for (var id = 1; id <= 3; id++) {
2    (function(id) {
3        setTimeout(function() {
4            console.log('seconds: ' + id);
5        }, id * 1000);
6    })(id);
7}

در کد بالا، تابع (function(id) { ... })(id)  نشان دهنده IIFE است. این رویکرد فوراً تابع را با پارامتر id فراخوانی کرده و محدوده تابع جدید را برای هر پیمایش حلقه ایجاد می‌کند. سپس تابع setTimeout در داخل IIFE مقدار صحیح id را در closure  خود می‌گیرد.

در حالی که رویکرد IIFE می‌تواند در سناریوهای خاصی موثر باشد، شایان ذکر است که راه‌حل «ES6» با استفاده از let  راه‌حل تمیزتر و مختصرتری برای مشکل ذکر شده ارائه می‌دهد. کلمه کلیدی let به طور خودکار محدوده بلوک را ایجاد می‌کند و در بیشتر موارد نیاز به IIFE را از بین می‌برد. با این حال، ممکن است شرایطی وجود داشته باشد که رویکرد IIFE بهتر باشد که بحث در مورد آن بسیار تخصصی است.

Closure در جاوا اسکریپت چگونه ایجاد می شود؟

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

کاربرد Closure در Javascript چیست؟

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

Closure در Javascript

چگونه Closure را عمیق یاد بگیریم؟

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

سخن پایانی

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

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

بر اساس رای ۲ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
FreeCodeCampBlog.Hubspot
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *