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

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

Hoisting چیست؟

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
console.log(foo);
var foo = 'foo';
console.log(foo); var foo = 'foo';
console.log(foo);
var foo = 'foo';

نتیجه ممکن است شگفت‌انگیز باشد زیرا با این که

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

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

foo
foo قبل از تعریف آن، استفاده کنیم.

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

همانطور که می‌دانیم یک متغیر را با استفاده از دستورات

var
var،
let
letو
const
constتعریف می‌کنیم. مثلا:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
var foo;
let bar;
var foo; let bar;
var foo;
let bar;

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Declaration
var foo;
let bar;
// Assignment
foo = 'foo';
bar = 'bar';
// Declaration var foo; let bar; // Assignment foo = 'foo'; bar = 'bar';
// Declaration
var foo;
let bar;

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
var foo = 'foo';
let bar = 'bar';
const baz = 'baz';
var foo = 'foo'; let bar = 'bar'; const baz = 'baz';
var foo = 'foo';
let bar = 'bar';
const baz = 'baz';

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

Hoisting متغیر با var

هنگامی که مفسر یک متغیر تعریف شده با

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
console.log(foo); // undefined
var foo = 'bar';
console.log(foo); // "bar"
console.log(foo); // undefined var foo = 'bar'; console.log(foo); // "bar"
console.log(foo); // undefined

var foo = 'bar';

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
var foo;
console.log(foo); // undefined
foo = 'foo';
console.log(foo); // "foo"
var foo; console.log(foo); // undefined foo = 'foo'; console.log(foo); // "foo"
var foo;

console.log(foo); // undefined

foo = 'foo';

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

باید به یاد داشته باشیم که مقدار خروجی اولین

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
console.log(foo); // Uncaught ReferenceError: foo is not defined
console.log(foo); // Uncaught ReferenceError: foo is not defined
console.log(foo); // Uncaught ReferenceError: foo is not defined

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

ReferenceError
ReferenceError ایجاد می‌کند زیرا هیچ مقداری وجود ندارد که hoist شود.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
console.log(foo); // Uncaught ReferenceError: foo is not defined
foo = 'foo'; // Assigning a variable that's not declared is valid
console.log(foo); // Uncaught ReferenceError: foo is not defined foo = 'foo'; // Assigning a variable that's not declared is valid
console.log(foo); // Uncaught ReferenceError: foo is not defined
foo = 'foo';      // Assigning a variable that's not declared is valid

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

let
letو
const
constکه در ECMAScript 2015 معرفی شدند رفتار متفاوتی دارند.

Hoisting متغیر با let و const

متغیرهای تعریف شده با

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
console.log(foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization
let foo = 'bar'; // Same behavior for variables declared with const
console.log(foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization let foo = 'bar'; // Same behavior for variables declared with const
console.log(foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization

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

باید به این موضوع توجه داشته باشیم که مفسر همچنان

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

The temporal dead zone

دلیل این اتفاق که وقتی می‌خواهیم به متغیر

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

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

foo
foo را نشان می‌دهد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
{
// 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
}
{ // 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 }
{
 	// 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
barتنظیم شود در TDZ قرار دارد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function foobar(foo = bar, bar = 'bar') {
console.log(foo);
}
foobar(); // Uncaught ReferenceError: Cannot access 'bar' before initialization
function foobar(foo = bar, bar = 'bar') { console.log(foo); } foobar(); // Uncaught ReferenceError: Cannot access 'bar' before initialization
function foobar(foo = bar, bar = 'bar') {
  console.log(foo);
}
foobar(); // Uncaught ReferenceError: Cannot access 'bar' before initialization

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

foo
foo دسترسی داشته باشیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function foobar(foo = 'foo', bar = foo) {
console.log(bar);
}
foobar(); // "foo"
function foobar(foo = 'foo', bar = foo) { console.log(bar); } foobar(); // "foo"
function foobar(foo = 'foo', bar = foo) {
  console.log(bar);
}
foobar(); // "foo"

typeof در TDZ

استفاده از متغیر

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
console.log(typeof foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization
let foo = 'foo';
console.log(typeof foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization let foo = 'foo';
console.log(typeof foo); // Uncaught ReferenceError: Cannot access 'foo' before initialization
let foo = 'foo';

این رفتار با سایر موارد

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

با این حال این مورد هنگام استفاده از متغیر

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
console.log(typeof foo); // "undefined"
var foo = 'foo';
console.log(typeof foo); // "undefined" var foo = 'foo';
console.log(typeof foo); // "undefined"
var foo = 'foo';

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

typeof
typeofیک رشته را برمی‌گرداند:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
console.log(typeof foo); // "undefined"
console.log(typeof foo); // "undefined"
console.log(typeof foo); // "undefined"

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

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

"foo"
"foo"را در خروجی نشان می‌دهد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
foo(); // "foo"
function foo() {
console.log('foo');
}
foo(); // "foo" function foo() { console.log('foo'); }
foo(); // "foo"

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

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

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

TypeError
TypeErrorیا
ReferenceError
ReferenceErrorدریافت خواهیم کرد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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 () { }
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 () { }
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
ReferenceErrorمتفاوت ایجاد می‌کند:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
foo(); // Uncaught ReferenceError: baz is not defined
foo(); // Uncaught ReferenceError: baz is not defined
foo(); // Uncaught ReferenceError: baz is not defined

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

Hoisting متغیر

به دلیل سردرگمی که

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

همچنین می‌توانیم از قانون

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

Hoisting تابع

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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");
}
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"); }
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 کنند.

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

دیدگاه‌ها:

افزودن دیدگاه جدید