Closure در جاوا اسکریپت چیست؟ – توضیح کلوژر به زبان ساده + مثال و کد
Closure در جاوا اسکریپت نوعی مفهوم اساسی است که هر برنامهنویسی باید به طور کامل آن را درک کند. درک عملکرد «Closure» به توسعهدهندگان این امکان را میدهد تا تسلط بیشتری بر ابزارهای خود داشته باشند. در این مطلب از «مجله فرادرس» به زبان ساده این پرسش را پاسخ میدهیم که Closure در جاوا اسکریپت چیست و برای درک بهتر این مفهوم، مثالهایی نیز به همراه کدهای مربوطه ارائه شدهاند.
Closure در جاوا اسکریپت چیست؟
«بستار» (Closure) نوعی ویژگی بسیار تاثیرگذار است که در جاوا اسکریپت و همچنین بسیاری از زبانهای برنامه نویسی دیگر یافت میشود. طبق تعریف ارائه شده به وسیله «MDN»، کلوژرها توابعی هستند که به متغیرهای مستقل ارجاع میدهند. به این متغیرها، متغیرهای آزاد نیز میگویند. به عبارت دیگر در جاوا اسکریپت، Closureها به عنوان شکلی از «تعیین محدوده واژگانی» (Lexical scoping) برای حفظ متغیرها از محدوده بیرونی تابع در محدوده درونی آن استفاده میشوند. محدوده واژگانی، محدوده متغیر را بر اساس موقعیت آن در کد منبع تعیین میکند.
وقتی تابعی تعریف میشود، هر متغیری در آن تابع فقط در خود تابع قابل دسترسی است. تلاش برای دسترسی به این متغیرها از خارج از تابع منجر به خطای دامنه یا محدوده میشود. اینجا است که 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» اهمیت دارد.
در جاوا اسکریپت، تنها یک زمینه اجرا میتواند در زمانی معین فعال باشد و این ویژگی آن را به نوعی زبان «تکرشتهای» (Single-Threaded) تبدیل میکند. این بدان معنا است که در هر لحظه فقط یک فرمان، قابل پردازش خواهد بود. مرورگرها معمولاً زمینههای اجرایی را با استفاده از ساختمان دادهای به نام «پشته» (Stack) حفظ میکنند. پشته بر اساس «ورودی آخر، خروجی اول» (Last In First Out) یا به اختصار «LIFO» عمل میکند که در آن آخرین موجودیتی که به پشته وارد شده است، اولین موردی خواهد بود که خارج میشود. دلیلش این است که عناصر را فقط میتوان از بالای پشته وارد یا خارج کرد.
زمینه اجرای فعلی همیشه در بالای پشته قرار دارد و تا زمانی که کدهای درون آن ارزیابی شود در این موقعیت باقی میماند. پس از ارزیابی، زمینه اجرایی فعلی از پشته خارج میشود و به آیتم بعدی اجازه میدهد تا به زمینه اجرای فعلی (در حال اجرا) تبدیل شود. همچنین قبل از اینکه زمینه اجرای دیگری تصاحب شود، نیازی به تکمیل زمینه اجرای در حال اجرا نیست. شرایطی وجود دارد که زمینه اجرایی در حال اجرا به طور موقت به حالت تعلیق در میآید و نوعی زمینه اجرایی متفاوت دیگر به زمینه در حال اجرای جدید تبدیل میشود.
زمینه اجرایی تعلیق شده امکان دارد بعداً از جایی که متوقف شده است، اجرای خود را از سر بگیرد. هر زمان که زمینه اجرایی فعلی با دیگری جایگزین شود، نوعی زمینه اجرایی جدید ایجاد و به پشته وارد خواهد شد. در این حالت، زمینه اجرایی وارد شده به زمینه اجرای فعلی تبدیل میشود. توجه به تصویر زیر برای درک توضیحات بالا مهم است:
مثال 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»، هر زمینه اجرا دارای مولفههای حالت مختلفی است که پیشرفت کدها را در آن زمینه اجرایی دنبال میکند. این مولفهها به صورت موارد زیر هستند:
- «وضعیت ارزیابی کد» (Code evaluation state): وضعیت لازم برای انجام، تعلیق و از سرگیری ارزیابی کد مرتبط در زمینه اجرا است.
- «تابع» (Function): شی تابعی که به وسیله زمینه اجرا ارزیابی میشود (یا اگر زمینه متعلق به اسکریپت یا ماژول باشد، null است).
- «قلمرو» (Realm): مجموعهای از اشیای داخلی، نوعی محیط سراسری ECMAScript، همه کدهای ECMAScript بارگذاری شده در محدوده آن محیط سراسری و سایر وضعیتها و منابع مرتبط.
- «محیط واژگانی» (Lexical Environment): به منظور حل ارجاعات شناسه ساخته شده به وسیله کد در زمینه اجرا استفاده میشود.
- «محیط متغیر» (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) تنها محیط واژگانی است که فاقد محیط بیرونی خواهد بود. برای تجسم این موضوع، میتوان محیطهای واژگانی را لایههایی از پیاز در نظر گرفت که محیط جهانی بیرونیترین لایه است. هر لایه بعدی نشان دهنده نوعی محیط تودرتو در داخل خواهد بود.