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

Hoisting چیست؟

در ادامه یک مثال کد داریم:

console.log(foo);
var foo = 'foo';

نتیجه ممکن است شگفت‌انگیز باشد زیرا با این که fooپس از console.logتعریف شده است اما این کد در خروجی undefinedرا نشان داده و خطایی ایجاد نمی‌کند.

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

Hoisting متغیر در جاوااسکریپت

همانطور که می‌دانیم یک متغیر را با استفاده از دستورات var، letو constتعریف می‌کنیم. مثلا:

var foo;
let bar;

با استفاده از عملگر انتساب مقداری را به یک متغیر اختصاص می‌دهیم:

// Declaration
var foo;
let bar;

// Assignment
foo = 'foo';
bar = 'bar';

در بسیاری از موارد می‌توانیم تعریف و تخصیص را در یک مرحله انجام دهیم:

var foo = 'foo';
let bar = 'bar';
const baz = 'baz';

hoisting متغیر بسته به نحوه تعریف متغیرها رفتار متفاوتی دارد. در ادامه هر کدام از این‌ها را باهم بررسی می‌کنیم.

Hoisting متغیر با var

هنگامی که مفسر یک متغیر تعریف شده با var را hoist می‌کند، value آن را با undefinedمقداردهی اولیه می‌کند. خروجی اولین خط کد زیر به صورت undefined خواهد بود:

console.log(foo); // undefined

var foo = 'bar';

console.log(foo); // "bar"

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

var foo;

console.log(foo); // undefined

foo = 'foo';

console.log(foo); // "foo"

باید به یاد داشته باشیم که مقدار خروجی اولین console.log(foo)برابر با undefinedخواهد بود زیرا fooبالا می‌رود و یک مقدار پیش فرض می‌گیرد (نه به این دلیل که متغیر هرگز تعریف نمی‌شود). استفاده از یک متغیری که تعریف نشده است یک ReferenceErrorایجاد می‌کند:

console.log(foo); // Uncaught ReferenceError: foo is not defined

استفاده از یک متغیر تعریف نشده قبل از تخصیص نیز یک ReferenceError ایجاد می‌کند زیرا هیچ مقداری وجود ندارد که hoist شود.

console.log(foo); // Uncaught ReferenceError: foo is not defined
foo = 'foo';      // Assigning a variable that's not declared is valid

در حال حاضر ممکن است این‌طور به نظر بیاید که این رفتار جاوااسکریپت به ما اجازه می‌دهد تا به متغیرها قبل از تعریف شدن آن‌ها دسترسی داشته باشیم یک رفتار غیرعادی بوده و می‌تواند منجر به ایجاد خطا شود. بنابراین استفاده از یک متغیر قبل از تعریف شدن آن معمولاً مطلوب نیست. خوشبختانه متغیرهای letو constکه در ECMAScript 2015 معرفی شدند رفتار متفاوتی دارند.

Hoisting متغیر با let و const

متغیرهای تعریف شده با letو const hoist می‌شوند اما value آن‌ها به‌طور پیش‌فرض مقداردهی اولیه نمی‌شوند. دسترسی به متغیر let یا const قبل از تعریف منجر به ایجاد ReferenceErrorمی‌شود:

console.log(foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization

let foo = 'bar';  // Same behavior for variables declared with const

باید به این موضوع توجه داشته باشیم که مفسر همچنان fooرا hoist می‌کند. پیام خطا به ما می‌گوید که متغیر در جایی مقداردهی اولیه شده است.

The temporal dead zone

دلیل این اتفاق که وقتی می‌خواهیم به متغیر let یا const قبل از تعریف آن دسترسی پیدا کنیم خطای ReferenceError دریافت می‌کنیم، the temporal dead zone (TDZ) است.

TDZ از ابتدای scope متغیر شروع می‌شود و با تعریف آن به پایان می‌رسد. دسترسی به متغیر در این TDZ یک ReferenceError ایجاد می‌کند. در ادامه یک مثال با یک block مشخص داریم که شروع و پایان TDZ مربوط به foo را نشان می‌دهد:

{
 	// Start of foo's TDZ
  	let bar = 'bar';
  console.log(bar); // "bar"

  console.log(foo); // ReferenceError because we're in the TDZ

  let foo = 'foo';  // End of foo's TDZ
}

TDZ همچنین در پارامترهای تابع پیش‌فرض وجود دارد که از چپ به راست ارزیابی می‌شوند. به عنوان مثال در کد زیر تا زمانی که مقدار پیش‌فرض برای barتنظیم شود در TDZ قرار دارد:

function foobar(foo = bar, bar = 'bar') {
  console.log(foo);
}
foobar(); // Uncaught ReferenceError: Cannot access 'bar' before initialization

اما کد زیر کار می‌کند زیرا می‌توانیم خارج از TDZ به foo دسترسی داشته باشیم:

function foobar(foo = 'foo', bar = foo) {
  console.log(bar);
}
foobar(); // "foo"

typeof در TDZ

استفاده از متغیر letیا constبه عنوان عملوند عملگر typeofدر TDZ یک خطا ایجاد می‌کند:

console.log(typeof foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization
let foo = 'foo';

این رفتار با سایر موارد letو const در TDZ که دیده‌ایم سازگار است. اما دلیل اینکه اینجا یک ReferenceErrorدریافت می‌کنیم این است که fooتعریف شده است اما مقداردهی اولیه نشده است که طبق گفته Axel Rauschmayer باید توجه داشته باشیم قبل از مقداردهی اولیه از آن استفاده کنیم.

با این حال این مورد هنگام استفاده از متغیر varقبل از تعریف، صدق نمی‌کند. زیرا هنگامی که hoist می‌شود مقداردهی اولیه با undefined انجام می‌گردد:

console.log(typeof foo); // "undefined"
var foo = 'foo';

نکته بسیار جالبی که در این مورد وجود دارد این است که می‌توانیم نوع متغیری که وجود ندارد را بدون هیچ خطایی بررسی کنیم. در مثال پایین typeofیک رشته را برمی‌گرداند:

console.log(typeof foo); // "undefined"

Hoisting تابع در جاوااسکریپت

تعریف توابع نیز همانند متغیرها از hoisting پیروی می‌کند. hoisting تابع به ما این اجازه را می‌دهد تا بتوانیم یک تابع را قبل از تعریف فراخوانی کنیم. به عنوان مثال، کد زیر با موفقیت اجرا می‌شود و "foo"را در خروجی نشان می‌دهد:

foo(); // "foo"

function foo() {
  console.log('foo');
}

باید به این نکته توجه داشته باشیم که فقط تعریف تابع هست که از hoisting پیروی می‌کند، نه عبارت‌های تابع.

اگر بخواهیم متغیری که عبارت تابع به آن اختصاص داده شده است را فراخوانی کنیم، بسته به scope متغیر، یک TypeErrorیا ReferenceErrorدریافت خواهیم کرد:

foo(); // Uncaught TypeError: foo is not a function
var foo = function () { }

bar(); // Uncaught ReferenceError: Cannot access 'bar' before initialization
let bar = function () { }

baz(); // Uncaught ReferenceError: Cannot access 'baz' before initialization
const baz = function () { }

این موضوع با فراخوانی تابعی که هرگز تعریف نشده است متفاوت است و یک ReferenceErrorمتفاوت ایجاد می‌کند:

foo(); // Uncaught ReferenceError: baz is not defined

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

Hoisting متغیر

به دلیل سردرگمی که varدر رابطه با hoisting می‌تواند ایجاد کند، بهتر است در پروژه‌های خود برای تعریف متغیرها به جای آن از letو constاستفاده کنیم. اما اگر به هر دلیلی مجبور به استفاده از var هستیم، MDN توصیه می‌کند که تعریف‌ متغیرها را تا حد امکان نزدیک به بالای scope آن‌ها انجام دهیم. این کار باعث می‌شود تا scope متغیرها واضح‌تر شود.

همچنین می‌توانیم از قانون No-use-Before-DefineESLint استفاده کنیم که اطمینان می‌دهد متغیری قبل از اینکه تعریف شود مورد استفاده قرار نمی‌گیرد.

Hoisting تابع

hoisting تابع یک کار مفید است زیرا می‌توانیم اجرای تابع را در فایلی پنهان کنیم و به خواننده اجازه دهیم تا روی کاری که کد انجام می‌دهد تمرکز کند. به عبارت دیگر ما می‌توانیم بدون اینکه در ابتدا درک کنیم که کد چگونه پیاده‌سازی شده است، یک فایل را باز کنیم و ببینیم که آن کد چه کاری انجام می‌دهد. مثال زیر را در نظر بگیرید:

resetScore();
drawGameBoard();
populateGameBoard();
startGame();

function resetScore() {
  console.log("Resetting score");
}

function drawGameBoard() {
  console.log("Drawing board");
}

function populateGameBoard() {
  console.log("Populating board");
}

function startGame() {
  console.log("Starting game");
}

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

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

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