Closure در جاوا اسکریپت نوعی مفهوم اساسی است که هر برنامهنویسی باید به طور کامل آن را درک کند. درک عملکرد «Closure» به توسعهدهندگان این امکان را میدهد تا تسلط بیشتری بر ابزارهای خود داشته باشند. در این مطلب از «مجله تم آف» به زبان ساده این پرسش را پاسخ میدهیم که Closure در جاوا اسکریپت چیست و برای درک بهتر این مفهوم، مثالهایی نیز به همراه کدهای مربوطه ارائه شدهاند.
Closure در جاوا اسکریپت چیست؟
«بستار» (Closure) نوعی ویژگی بسیار تاثیرگذار است که در جاوا اسکریپت و همچنین بسیاری از زبانهای برنامه نویسی دیگر یافت میشود. طبق تعریف ارائه شده به وسیله «MDN»، کلوژرها توابعی هستند که به متغیرهای مستقل ارجاع میدهند. به این متغیرها، متغیرهای آزاد نیز میگویند. به عبارت دیگر در جاوا اسکریپت، Closureها به عنوان شکلی از «تعیین محدوده واژگانی» (Lexical scoping) برای حفظ متغیرها از محدوده بیرونی تابع در محدوده درونی آن استفاده میشوند. محدوده واژگانی، محدوده متغیر را بر اساس موقعیت آن در کد منبع تعیین میکند.
وقتی تابعی تعریف میشود، هر متغیری در آن تابع فقط در خود تابع قابل دسترسی است. تلاش برای دسترسی به این متغیرها از خارج از تابع منجر به خطای دامنه یا محدوده میشود. اینجا است که Closureها ارزشمند هستند و به کمک کاربر میآیند.
- نکته: توجه به این نکته مهم است که متغیرهای آزاد متغیرهایی هستند که نه به صورت محلی در تابع اعلان میشوند و نه به عنوان پارامتر ارسال خواهند شد.
مثال Closure در جاوا اسکریپت
برای درک بهتر مفهوم Closure در زبان برنامه نویسی جاوا اسکریپت در ادامه ۲ مثال از این مبحث ارائه خواهد شد. قطعه کد مثال اول به صورت زیر است.
function numberGenerator() {
// Local “free” variable that ends up within the closure
var num = 1;
function checkNumber() {
console.log(num);
}
num++;
return checkNumber;
}
var number = numberGenerator();
number(); // 2
در مثال داده شده تابعی به نام numberGenerator
وجود دارد. در این تابع، متغیر محلی به نام num
به عنوان نوعی متغیر آزاد تعریف میشود. در کنار آن، تابع دیگری به نام checkNumber
اعلان شده است که مقدار num
را در کنسول چاپ میکند.
اگرچه checkNumber
هیچ متغیر محلی برای خود ندارد، اما بنا بر مفهوم Closure در جاوا اسکریپت، میتواند به متغیرهای تابع بیرونی خود، یعنی numberGenerator
دسترسی داشته باشد. در نتیجه، checkNumber
میتواند حتی پس از اتمام اجرای numberGenerator
، به طور موثر از متغیر num
استفاده کند که در numberGenerator
اعلان شده است. این امر هنگام فراخوانی num
مشهود خواهد بود که ارجاع به تابع checkNumber
را نگه میدارد و در نتیجه مقدار 2
در کنسول ثبت میشود. حال قطعه کد مثال دوم در ادامه آمده است.
function sayHello() {
var say = function() { console.log(hello); }
// Local variable that ends up within the closure
var hello = 'Hello, world!';
return say;
}
var sayHelloClosure = sayHello();
sayHelloClosure(); // ‘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 در جاوا اسکریپت در ادامه مثالی ارائه شده است.
var x = 10;
function foo(a) {
var b = 20;
function bar(c) {
var d = 30;
return boop(x + a + b + c + d);
}
function boop(e) {
return e * -1;
}
return bar;
}
var moar = foo(5); // Closure
/*
The function below executes the function bar which was returned
when we executed the function foo in the line above. The function bar
invokes boop, at which point bar gets suspended and boop gets push
onto the top of the call stack (see the screenshot below)
*/
moar(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) تنها محیط واژگانی است که فاقد محیط بیرونی خواهد بود. برای تجسم این موضوع، میتوان محیطهای واژگانی را لایههایی از پیاز در نظر گرفت که محیط جهانی بیرونیترین لایه است. هر لایه بعدی نشان دهنده نوعی محیط تودرتو در داخل خواهد بود.
به طور خلاصه، محیط واژگانی نقشی حیاتی در ایجاد ارتباط و معنای شناسهها در کد دارد. این شامل نوعی رکورد محیطی برای ثبت پیوندهای شناسه و نوعی ساختار تو در توی سلسله مراتبی است که امکان زنجیرهبندی محیطهای واژگانی را فراهم میکند.
مثالی از مفهوم محیط واژگانی
مثال زیر برای درک مفهوم محیط واژگانی مهم است. محیط واژگانی به صورت زیر ساخته خواهد شد:
LexicalEnvironment = {
EnvironmentRecord: {
// Identifier bindings go here
},
// Reference to the outer environment
outer:
};
توجه به این نکته، مهم است که هر بار کد مربوطه ارزیابی میشود، نوعی محیط واژگانی جدید ایجاد خواهد شد. این نه تنها در مورد توابع، بلکه در مورد سایر ساختارهای کد مانند دستورات بلوک یا عبارات catch
نیز صدق میکند. با این حال، برای سادگی، در طول این بحث بر روی محیطهایی که به وسیله توابع ایجاد میشوند تمرکز خواهیم کرد.
هر زمینه اجرایی دارای نوعی محیط واژگانی است. این محیط متغیرها و مقادیر مربوط به آنها را نگه میدارد و در عین حال، ارجاع به محیط بیرونی خود را نیز حفظ میکند. انواع مختلفی از محیطهای واژگانی وجود دارد، از جمله محیط سراسری، که اعلانهای سطح بالا را در کل برنامه در بر میگیرد. علاوه بر این، محیطهایی برای ماژولها وجود دارند که شامل پیوندهای خاص ماژول هستند و محیطهای تابعی که هنگام فراخوانی تابع تولید میشوند.
زنجیره محدوده
در جاوا اسکریپت، مفهوم «دامنه یا محدوده» (Scope) ارتباط نزدیکی با تودرتویی سلسله مراتبی محیطهای واژگانی دارد و از پیشنیازهای درک و یادگیری مفهوم Closure یا بستار در جاوا اسکریپت است.
هر محیطی به محیط والد خود دسترسی داشته که آن هم به نوبه خود به محیط والد خود دسترسی دارد و زنجیرهای از محیطها را تشکیل میدهد که به «زنجیره محدوده» (Scope Chain) معروف است. مثال زیر این موضوع را نشان میدهد:
var x = 10;
function foo() {
var y = 20; // free variable
function bar() {
var z = 15; // free variable
return x + y + z;
}
return bar;
}
تصویر زیر درک بهتری را از مفهوم مثال مربوطه انتقال میدهد.
در نمودار داده شده بالا، تابع bar
درون تابع foo
تودرتو قرار دارد و رابطه سلسله مراتبی آنها را نشان میدهد. زنجیره محدوده، همچنین به عنوان زنجیرهای از محیطهای مرتبط با تابع شناخته میشود، زمانی که شی تابع ایجاد شد، زنجیره محدوده به وجود میآید و به آن متصل خواهد شد. این زنجیره محدوده نشان دهنده محدوده ایستا است که به وسیله مکان توابع در کد منبع تعیین میشود.
محدوده ایستا و محدوده پویا
زبانهای دارای دامنه پویا، مانند زبانهایی که پیادهسازیهای مبتنی بر پشته دارند، متغیرهای محلی و آرگومانهای تابع را در پشته ذخیره میکنند. وضعیت فعلی پشته برنامه تعیین میکند که کدام متغیر در زمانی معین ارجاع داده میشود.
در مقابل، دامنه استاتیک بر اساس متغیرهایی است که در زمان ایجاد به آنها ارجاع داده شده که به وسیله ساختار «کد منبع» (Source Code) برنامه تعیین میشود. متغیرهایی که مراجع به آنها اشاره میکنند بر اساس سلسله مراتب واژگانی کدها ثبت میشوند. برای نشان دادن تفاوت بین دامنه پویا و استاتیک، مثالهای زیر را در نظر بگیرید:
var x = 10;
function foo() {
var y = x + 5;
return y;
}
function bar() {
var x = 2;
return foo();
}
function main() {
foo(); // Static scope: 15; Dynamic scope: 15
bar(); // Static scope: 15; Dynamic scope: 7
return 0;
}
در مثال فوق، مشاهده میشود که دامنه استاتیک و داینامیک نتایج متفاوتی را هنگام فراخوانی تابع bar
به دست میدهند. با دامنه ایستا، مقدار بازگشتی bar
بر اساس مقدار x
در زمان ایجاد foo
است. این به دلیل ساختار ایستا و واژگانی کد منبع است که برای x
در آغاز 10
و در نتیجه 15
خواهد بود.
از سوی دیگر، دامنه داینامیک یا پویا با مجموعهای از تعاریف متغیر عمل میکند که در زمان اجرا ردیابی میشوند. تعیین اینکه از کدام x
استفاده شود بستگی به متغیرهای تعریف شده به صورت داینامیک در محدوده فعلی در زمان اجرا دارد. هنگام اجرای تابع مربوطه، x = 2
به بالای پشته منتقل میشود که منجر به بازیابی خروجی 7
خواهد شد. حال مثال زیر هم برای درک این مفهوم ضرورت دارد و قطعه کد آن در ادامه آمده است.
var myVar = 100;
function foo() {
console.log(myVar);
}
foo(); // Static scope: 100; Dynamic scope: 100
(function () {
var myVar = 50;
foo(); // Static scope: 100; Dynamic scope: 50
})();
// Higher-order function
(function (arg) {
var myVar = 1500;
arg(); // Static scope: 100; Dynamic scope: 1500
})(foo);
در مثال فوق و در دامنه داینامیک یا پویا، متغیر myVar
بر اساس مقدار آن در مکانی که تابع فراخوانی میشود، بازیابی خواهد شد. از طرف دیگر در دامنه استاتیک myVar
را به متغیری که در محدوده دو تابع «IIFE» در هنگام ایجاد ذخیره شده بود، بازیابی میکند. محدوه داینامیک اغلب ابهام ایجاد خواهد کرد، زیرا مشخص نیست که متغیر آزاد از کدام محدوده بازیابی میشود. در ادامه این مطلب در رابطه با IIFE توضیحاتی ارائه خواهد شد.
آموزش Closure در جاوا اسکریپت
Closure در جاوا اسکریپت ارتباط نزدیکی با درک کاربر از زمینه اجرا و محیط اجرا دارد. هر تابع زمینه اجرای خود را دارد که شامل محیطی است که به متغیرهای درون تابع معنا میدهد و ارجاعی به محیط والد خود دارد. این مرجع به توابع داخلی اجازه میدهد تا از محدوده والد خود به متغیرها دسترسی داشته باشند، صرف نظر از اینکه تابع داخلی در داخل یا خارج از محدودهای فراخوانی میشود که در آن تعریف شده است.
بینش کلیدی در این رابطه این است که تابع ارجاعی به محیط (یا محدوده) خود دارد که این ویژگی آن را قادر میسازد تا آن محیط و متغیرهای تعریف شده در آن را به خاطر بسپارد. مثال زیر برای درک این مفهوم بیان شده مهم است:
var x = 10;
function foo() {
var y = 20; // free variable
function bar() {
var z = 15; // free variable
return x + y + z;
}
return bar;
}
var test = foo();
test(); // 45
بر اساس درک کاربر از محیطها، میتوان تعریفهای محیط را برای این مثال به صورت زیر نمایش داد:
GlobalEnvironment = {
EnvironmentRecord: {
// built-in identifiers
Array: '',
Object: '',
// etc..
// custom identifiers
x: 10
},
outer: null
};
fooEnvironment = {
EnvironmentRecord: {
y: 20,
bar: ''
}
outer: GlobalEnvironment
};
barEnvironment = {
EnvironmentRecord: {
z: 15
}
outer: fooEnvironment
};
در قطعه کد بالا وقتی تابع test
فراخوانی میشود، مقدار بازگشتی 45
را بازیابی میکند. این به این دلیل است که تابع test
تابع bar
را فراخوانی خواهد کرد که حتی پس از بازگشت تابع foo
به متغیر آزاد y دسترسی دارد. تابع bar
دسترسی به y را به وسیله محیط بیرونی خود که محیط foo است حفظ میکند. علاوه بر این، تابع bar
میتواند به متغیر جهانی x دسترسی داشته باشد، زیرا محیط foo
به محیط جهانی دسترسی دارد. این ساز و کار به عنوان «بازرسی زنجیرهای محدوده» (Scope-Chain Lookup) شناخته میشود.
در خصوص دامنه داینامیک در مقابل دامنه استاتیک، Closure را نمیتوان با استفاده از محدوده داینامیک به وسیله پشته داینامیک برای ذخیره متغیرها پیادهسازی کرد. اگر از محدوده داینامیک استفاده میشد، متغیرها از پشته خارج میشدند و با بازگشت تابعی دیگر قابل دسترسی نبودند که این امر در تضاد با تعریف اولیه Closure در جاوا اسکریپت است.
مثال هایی برای Closure در جاوا اسکریپت
در ادامه ٣ مثال از مفهوم Closure در جاوا اسکریپت برای درک بهتر مفاهیم بیان شده رائه میشود.
مثال ١: ایجاد زمینه محصور کننده برای توابع
یکی از نمونههای رایجی که در آن خطا اتفاق میافتد، زمانی است که کاربر سعی میکند متغیر شمارنده را در حلقه For در جاوا اسکریپت با تابعی در داخل حلقه for
مرتبط کند که کد زیر این مفهوم را نشان میدهد:
var result = [];
for (var i = 0; i
درک مفاهیمی که تاکنون ذکر شد، تشخیص اشتباه را در اینجا آسانتر میکند. به طور انتزاعی، محیط در انتهای حلقه for
را میتوان به صورت زیر نشان داد:
environment: {
EnvironmentRecord: {
result: [...],
i: 5
},
outer: null,
}
اشتباه در این است که فرض میشود هر تابع در آرایه result
دارای محدوده جداگانه است. در واقع، هر پنج تابع دارای محیط یا محدوده یکسان هستند. بنابراین، هر زمان که متغیر i
افزایش یابد، دامنه اشتراکگذاری شده را بهروزرسانی میکند و در نتیجه تمام توابع به مقدار نهایی i
دسترسی دارند که با خروج از حلقه for
برابر با 5
است. یکی از راههای رفع این مشکل، ایجاد نوعی زمینه محصور کننده اضافی برای هر تابع است تا اطمینان حاصل شود که آنها زمینه و محدوده اجرای جداگانه خود را دارند که قطعه کد زیر این مفهوم را بیان میکند:
var result = [];
for (var i = 0; i
با تغییر بالا، مشکل برطرف خواهد شد. روش هوشمندانه دیگر، استفاده از let
به جای var
در تعریف متغیرهای جاوا اسکریپت است، زیرا let
دارای محدوده بلوکی است و برای هر بار پیمایش با حلقه for
نوعی شناسه جدید اتصال ایجاد خواهد کرد. کدهای زیر مربوط به این مسئله است.
var result = [];
for (let i = 0; i
مثال ٢: ایجاد Closure جداگانه
در مثال زیر، بررسی خواهد شد که چگونه هر فراخوانی تابع نوعی Closure در جاوا اسکریپت به طور جداگانه ایجاد میکند که قطعه کد آن به صورت زیر است.
function iCantThinkOfAName(num, obj) {
// This array variable, along with the 2 parameters passed in,
// are 'captured' by the nested function 'doSomething'
var array = [1, 2, 3];
function doSomething(i) {
num += i;
array.push(num);
console.log('num: ' + num);
console.log('array: ' + array);
console.log('obj.value: ' + obj.value);
}
return doSomething;
}
var referenceObject = { value: 10 };
var foo = iCantThinkOfAName(2, referenceObject); // closure #1
var bar = iCantThinkOfAName(6, referenceObject); // closure #2
foo(2);
/*
num: 4
array: 1,2,3,4
obj.value: 10
*/
bar(2);
/*
num: 8
array: 1,2,3,8
obj.value: 10
*/
referenceObject.value++;
foo(4);
/*
num: 8
array: 1,2,3,4,8
obj.value: 11
*/
bar(4);
/*
num: 12
array: 1,2,3,8,12
obj.value: 11
*/
در مثال فوق، میتوان مشاهده کرد که هر فراخوانی به تابع iCantThinkOfAName
نوعی Closure
مجزا ایجاد میکند که با foo
و bar
نشان داده میشود. وقتی این توابع Closure در جاوا اسکریپت متعاقباَ فراخوانی میشوند، آنها متغیرها را در Closureهای مربوطه خود بهروزرسانی میکنند. این نشان میدهد که متغیرهای هر Closure باقی میمانند و حتی پس از بازگشت iCantThinkOfAName
به تابع doSomething iCantThinkOfAName
در دسترس خواهند بود.
مثال ۳: دسترسی به متغیرهای محیطی
قطعه کد زیر را برای دسترسی به متغیرهای محیطی در نظر میگیریم:
function mysteriousCalculator(a, b) {
var mysteriousVariable = 3;
return {
add: function() {
var result = a + b + mysteriousVariable;
return toFixedTwoPlaces(result);
},
subtract: function() {
var result = a - b - mysteriousVariable;
return toFixedTwoPlaces(result);
}
};
}
function toFixedTwoPlaces(value) {
return value.toFixed(2);
}
var myCalculator = mysteriousCalculator(10.01, 2.01);
myCalculator.add(); // 15.02
myCalculator.subtract(); // 5.00
در مثال فوق، تابع MysteriousCalculator
در محدوده سراسری تعریف شده است و شیئی را با ۲ متد add
و subtract
برمیگرداند. به طور انتزاعی، محیطهای این مثال را میتوان به صورت زیر نشان داد:
GlobalEnvironment = {
EnvironmentRecord: {
// built-in identifiers
Array: '',
Object: '',
// etc...
// custom identifiers
mysteriousCalculator: '',
toFixedTwoPlaces: ''
},
outer: null
};
mysteriousCalculatorEnvironment = {
EnvironmentRecord: {
a: 10.01,
b: 2.01,
mysteriousVariable: 3
},
outer: GlobalEnvironment
};
addEnvironment = {
EnvironmentRecord: {
result: 15.02
},
outer: mysteriousCalculatorEnvironment
};
subtractEnvironment = {
EnvironmentRecord: {
result: 5.00
},
outer: mysteriousCalculatorEnvironment
};
با داشتن ارجاع به محیط تابع MysteriousCalculator
، متدهای add
و subtract
میتوانند به متغیرهای موجود در آن محیط (a ,b ,mysteriousVariable)
برای انجام محاسبات خود دسترسی داشته باشند.
مثال ٤: ارجاع خصوصی به متغیر در محدوده بیرونی
مثال آخر برای نشان دادن کاربرد مهم Closure در جاوا اسکریپت، حفظ ارجاع خصوصی به متغیر در محدوده بیرونی است که برای آن مثالی در ادامه آمده است.
function secretPassword() {
var password = 'xh38sk';
return {
guessPassword: function(guess) {
if (guess === password) {
return true;
} else {
return false;
}
}
}
}
var passwordGame = secretPassword();
passwordGame.guessPassword('heyisthisit?'); // false
passwordGame.guessPassword('xh38sk'); // true
در مثال فوق، تابع secretPassword
شیئی را با متد guessPassword
برمیگرداند. متغیر password
در محدوده تابع secretPassword
تعریف شده است و مستقیماً از خارج قابل دسترسی نیست. تابع guessPassword
به متغیر password
دسترسی دارد و به آن اجازه میدهد حدس ارائه شده را با رمز عبور مخفی مقایسه کند. این تضمین میکند که رمز عبور خصوصی باقی میماند و نمیتوان از خارج از Closure در جاوا اسکریپت به آن دسترسی داشت.
Closures و حلقه ها در جاوا اسکریپت
هنگام کار با حلقهها، ایجاد Closure در جاوا اسکریپت میتواند منجر به رفتار غیرمنتظره شود.
برای مثال فرض میشود قطعه کد زیر از setTimeout
در حلقه استفاده میکند:
for (var id = 0; id
در مثال فوق، حلقه سه بار اجرا میشود و تابع 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
، میتوان نوعی محدوده بلوک جدید برای هر پیمایش حلقه ایجاد و از رفتار مورد انتظار اطمینان حاصل کرد.
مثال زیر نحوه انجام این کار را بیشتر شرح میدهد.
for (let id = 0; id
در کد بالا، کلمه کلیدی 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» مهم است:
for (var id = 1; id
در کد بالا، تابع (function(id) { … })(id)
نشان دهنده IIFE است. این رویکرد فوراً تابع را با پارامتر id
فراخوانی کرده و محدوده تابع جدید را برای هر پیمایش حلقه ایجاد میکند. سپس تابع setTimeout
در داخل IIFE مقدار صحیح id
را در closure
خود میگیرد.
در حالی که رویکرد IIFE میتواند در سناریوهای خاصی موثر باشد، شایان ذکر است که راهحل «ES6» با استفاده از let
راهحل تمیزتر و مختصرتری برای مشکل ذکر شده ارائه میدهد. کلمه کلیدی let به طور خودکار محدوده بلوک را ایجاد میکند و در بیشتر موارد نیاز به IIFE را از بین میبرد. با این حال، ممکن است شرایطی وجود داشته باشد که رویکرد IIFE بهتر باشد که بحث در مورد آن بسیار تخصصی است.
Closure در جاوا اسکریپت چگونه ایجاد می شود؟
Closure در جاوا اسکریپت زمانی ایجاد میشود که تابعی درونی حتی پس از اتمام اجرای تابع بیرونی به متغیرها و محدوده عملکرد بیرونی خود دسترسی داشته باشد. تابع درونی ارجاع به محیط واژگانی خود را حفظ میکند و به آن اجازه میدهد تا متغیرها را از تابع بیرونی به خاطر بسپارد و به آن دسترسی داشته باشد.
کاربرد Closure در Javascript چیست؟
Closureها دارای دامنه واژگانی هستند، به این معنی که با موقعیت آنها در کد منبع، کاربرد آنها تعیین میشود. از آنها میتوان برای نگهداری دادههای خصوصی، ایجاد ماژولهای محصور شده و مدیریت عملیات ناهمزمان استفاده کرد.
چگونه Closure را عمیق یاد بگیریم؟
برای پیشرفت و عمیق شدن در مفهوم Closure در جاوا اسکریپت، مهم است که کاربران به طور فعال آنچه را که آموختهاند به کار ببرند و تمرین کنند. Closure میتواند مفهومی چالش برانگیز باشد، بنابراین اختصاص زمان برای تحقیق و تمرین Closure در سناریوهای مختلف، درک کاربر را بسیار افزایش میدهد. کاربران با به دست آوردن تجربه عملی، درک عمیقتری از نحوه عملکرد Closure در جاوا اسکریپت و کاربردهای عملی آن خواهند داشت.
سخن پایانی
در این مطلب از «مجله تم آف» در رابطه با Closure در جاوا اسکریپت و ابعاد مختلف پیرامون این مفهوم اطلاعاتی مطلوب به همراه مثالهای عملی ارائه شد.
در این مطلب، Closureها در جاوا اسکریپت در سناریوهای مختلفی به کار گرفته شدند و در کنار آن، مفاهیم زمینههای اجرا، محیطها، مفاهیم سطح بالا و غیره نیز ارائه شد.