تعریف متغیر یکی از پایهایترین جنبهها در هر زبان برنامهنویسی است. با این حال، نحوۀ تعریف متغیر و تابع در زبان جاوااسکریپت کمی متفاوت است. در واقع، مفهومی تحت عنوان Hoisting در این زبان وجود دارد که استفادۀ نادرست از این قابلیت، میتواند تعریف یک متغیر ساده را به یک باگ عجیب و غریب تبدیل کند! در این مقاله به توضیح مفهوم Hoisting خواهیم پرداخت و بررسی میکنیم که چگونه میتوان از این قابلیت به درستی استفاده کرد.
جاوااسکریپت یک زبان بسیار انعطافپذیر است و این امکان را برای دولوپرها فراهم میکند تا بتوانند یک متغیر را تقریباً در هر قسمتی از سورسکد که میخواهند، تعریف کنند. به عنوان مثال، در تابع IIFE زیر سه متغیر تعریف شده است؛ پس از اجرای برنامه این سه متغیر در یک آلرتباکس (Alert Box) نمایش داده خواهند شد (لازم به ذکر است که استفاده از آلرتباکس برای نمایش متغیرها هرگز توصیه نمیشود اما در اینجا به منظور درک بهتر مطلب، از آن استفاده شده است.)
آشنایی با مفهوم IIFE
IIFE مخفف عبارت Immediately Invoked Function Expression (اجرای تابع به محض فراخوانی آن) بوده و یک اصطلاح در زبان برنامهنویسی جاوااسکریپت است که در صورت استفاده از آن، توابع به محض تعریف، اجرا میشوند. همچنین به عنوان یک تابع بینام نیز شناخته شده است که موجب اجرای خودکار توابع میشود.
این تابع شامل دو قسمت اصلی است؛ قسمت اول آن تابع بینام که با اسکوپ () تعریف شده است (در این قسمت از تابع، دسترسی به متغیرهای داخل بدنۀ تابع IIFE از خارج از اسکوپ امکانپذیر نیست که این قابلیت منجر بدین خواهد شد تا اسکوپ گلوبال اصطلاحاً شلوغ نشود!) بخش دوم، ایجاد عبارت ;() است که از طریق آن موتور مفسر جاوااسکریپت به طور مستقیم تابع را تفسیر و بلافاصله اجرا میکند:
(function() {
var foo = 1;
var bar = 2;
var baz = 3;
alert(foo + " " + bar + " " + baz);
})();
مثال فوق، یک کد کاملاً بیاشکال به نظر میرسد و همانطور که انتظار میرود، پس از اجرای آن استرینگ 1 2 3 نمایش داده خواهد شد. اکنون فرض کنید که خط ششم (alert) را به خط سوم منتقل کنیم:
(function() {
var foo = 1;
alert(foo + " " + bar + " " + baz);
var bar = 2;
var baz = 3;
})();
این کد احتمالاً شما را به اشتباه بیندازد. واضح است که قبل از اینکه متغیرهای bar و baz تعریف شوند، خط سوم از کد اجرا شده و اعلام هشدار انجام میشود. لازم به ذکر است که این مثال یک کد کاملاً معتبر بوده و منجر به اکسپشنی نخواهد شد! در عوض، اجرای آن منجر به نمایش هشدار 1undefined undefined خواهد شد.
بر اساس سینتکس زبان جاوااسکریپت، این قابلیت برای دولوپرها فراهم شده است تا بتوانند متغیرهایی که هنوز وجود خارجی ندارند (یا هنوز تعریف نشدهاند) را در برنامه مورد استفاده قرار دهند. برای درک بهتر این موضوع، حال همان کد تابع IIFE را در نظر بگیرید با این تفاوت که این بار تعریف متغیر baz را به طور کامل حذف میکنیم. از این پس، با اجرای این کد، با یک ReferenceError مواجه خواهیم شد بدین دلیل که متغیر baz تعریف نشده است. در واقع، در جاوااسکریپت ReferenceError در نتیجۀ تلاش برای دسترسی به یک متغیر تعریف نشده صورت میپذیرد:
(function() {
var foo = 1;
alert(foo + " " + bar + " " + baz);
var bar = 2;
})();
این رفتار در زبان جاوااسکریپت واقعاً جالب است. برای درک بیشتر آنچه اتفاق افتاده است، بایستی مفهوم Hoisting را درک کنید.
Hoisting چیست؟
Hoisting یک عمل در مفسر جاوااسکریپت است که همهٔ تعاریف مربوط به متغیرها و توابع را قبل از اجرای کد، به ابتدای اسکوپ جاری انتقال میدهد (البته این در حالی است که فقط Declaration (تعریف) واقعی متغیرها یا توابع انتقال مییابند.) در حقیقت، تمام تخصیص مقادیر اولیه به متغیرها و همچنین بدنۀ توابع در همان خطی که از ابتدا نوشته شدهاند، باقی خواهند ماند؛ بنابراین، معادل مثال دوم ما برای تابع IIFE، کد زیر خواهد بود:
(function() {
var foo;
var bar;
var baz;
foo = 1;
alert(foo + " " + bar + " " + baz);
bar = 2;
baz = 3;
})();
اکنون این سؤال پیش میآید که چرا مثال دوم منجر به یک اکسپشن نشده است. در پاسخ به این سؤال بایستی گفت که با انتقال تعاریف واقعی متغیرها به ابتدای اسکوپ جاری، متغیرهای bar و baz قبل از اجرای خط مربوط به انجام هشدار، تعریف و اجرا میشوند؛ هرچند هنوز مقداری به آنها تخصیص نیافته است. در مثال سوم، متغیر baz را به طور کامل حذف کردهایم؛ بنابراین، هیچ چیزی برای انتقال به ابتدای اسکوپ وجود ندارد که در نهایت اجرای خط مربوط به هشدار به یک اکسپشن منجر خواهد شد.
انتقال تعریف تابع به ابتدای اسکوپ جاری
همانطور که در ابتدا نیز اشاره کردیم، در زبان جاوااسکریپت تعریف توابع را نیز میتوان به ابتدای اسکوپ جاری منتقل کرد اما این در حالی است که توابعی را که به متغیرها اختصاص داده میشوند (در یک متغیر نگهداری میشوند)، نمیتوان جابهجا کرد. به عنوان مثال، کد زیر که در آن تعریف تابع به ابتدای اسکوپ منتقل شده است، بسیار خوب کار خواهد کرد و پس از اجرا نیز نتیجۀ مورد انتظار را در خروجی خواهد داشت:
foo();
function foo() {
alert("Hello!");
}
اما مثال زیر به درستی کار نکرده و منجر به ارور خواهد شد چرا که متغیر تعریف شده برای تابع foo (متغیری که تابع به آن تخصیص مییابد یا به عبارت دیگر این تابع در آن متغیر نگاهداری میشود) به ابتدای اسکوپ منتقل شده و قبل از فراخوانی تابع مذکور اجرا خواهد شد اما این در حالی است که قابلیت Hoisting برای مواردی که توابع به یک متغیر تخصیص داده شدهاند، صدق نمیکند. در نتیجه، تلاش برای فراخوانی یک متغیر غیرتابع منجر به یک اکسپشن خواهد شد:
foo();
var foo = function() {
alert("Hello!");
};
نتیجهگیری
درک مفهوم Hoisting آسان است، اما اغلب موجب نادیده گرفته شدن نکات ریز در زبان جاوااسکریپت میشود و بدون داشتن درک درستی از Hoisting، برنامههای شما ممکن است مبتلا به یکسری باگهای کوچک شوند. در همین راستا، برای اجتناب از این باگها، بسیاری از توسعهدهندگان و ابزارهای Linting متغیرها را در ابتدای هر اسکوپ تعریف میکنند؛ چرا که با این کار اساساً مفسر جاوااسکریپت کد شما را به این شکل تفسیر خواهد کرد؛ در نتیجه، کدنویسی در زبان جاوااسکریپت بدین شکل، کاملاً معتبر خواهد بود؛ حتی اگر دولوپرهایی باشند که نخواهند طبق این قانون کدنویسی کنند (Linting ابزارهایی هستند که به منظور آنالیز یک برنامه و برای شناسایی ارورهای بالقوۀ موجود در آن در اختیار دولوپرها قرار میگیرند.)