درک Hoist کردن (بالا بردن) در JavaScript

گردآوری و تالیف : عرفان کاکایی
تاریخ انتشار : 29 مهر 1397
دسته بندی ها : جاوا اسکریپت

در این آموزش، خواهیم دید که مکانیزم hoist کردن (بالا بردن) معروف، چگونه در JavaScript بروز می‌دهد. قبل از این که شروع کنیم، بیایید ببینیم که بالا بردن به طور کلی یعنی چه؟

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

این یعنی این که اهمیتی ندارد توابع و متغیرها در کجا تعریف شده‌اند؛ در هر حال آن‌ها به ناچار به بالای scope خود منتقل می‌شوند. چه scope آن‌ها global باشد،‌ یا چه local.

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

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

  1. Undefined علیه ReferenceError
  2. بالا بردن متغیرها
  3. ES5
  4. ES6
  5. بالا بردن توابع
  6. ترتیب اولویت‌ها
  7. بالا بردن کلاس‌ها
  8. هشدار
  9. نتیجه گیری

Undefined علیه ReferenceError:

قبل از این که به طور جدی شروع کنیم، بیایید چند چیز را بسنجیم.

console.log(typeof variable); // خروجی: undefined

این کار ما را به اولین نکته می‌رساند:

در JavaScript، یک متغیر تعریف نشده در هنگام اجرا مقدار «undefined» را می‌گیرد و همچنین نوع آن هم «undefined»‌ است.

نکته دوم این است که:

console.log(variable); // خروجی: ReferenceError: variable is not defined

در JavaScript، وقتی که تلاش می‌کنید که به یک متغیر از پیش تعریف شده دسترسی داشته باشید، یک خطای ReferenceError بروز می‌دهد.

رفتار JavaScript در زمان مدیریت متغیرها به علت بالا بردن، منحصر به فرد می‌شود. ما در بخش‌های بعدی به عمق این مطلب نگاه خواهیم داشت.

بالا بردن متغیرها:

تصویر زیر، lifecycle جاوا اسکریپت و نشان دهنده ترتیبی است که تعریف و راه‌اندازی متغیر بروز می‌دهد.

گرچه، از آنجایی که JavaScript ما را قادر می‌سازد تا هم تعریف و هم راه‌اندازی متغیر را به صورت همزمان انجام دهیم، این رایج‌ترین الگو است:

var a = 100;

گرچه مهم است به یاد داشته باشیم که در پس‌زمینه، JavaScript در ابتدا متغیر ما را تعریف کرده، و راه‌اندازی می‌کند.

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

گرچه در تضاد این مسئله، متغیرهای تعریف شده تا زمانی که کد اختصاص دهی آن‌ها اجرا نشده است، وجود ندارند. از این رو، اختصاص دهی یک مقدار به یک متغیر تعریف نشده، آن را به عنوان یک متغیر global‌ می‌سازد. این یعنی این که تمام متغیرهای تعریف نشده، متغیرهای global هستند.

برای نمایش این رفتار، به این کد نگاهی داشته باشید:

function hoist() {

  a = 20;

  var b = 100;

}

hoist();

console.log(a); 

Accessible as a global variable outside hoist() function

Output: 20

console.log(b); 

از آنجایی که این خارج از نحوه مدیریت متغیرها توسط JavaScript‌ است، پیشنهاد می‌شود که همیشه متغیرها را بدون توجه به این که آن‌ها در یک تابع بوده، یا در محدوده global هستند، بسازیم. این کار به وضوح نحوه مدیریت آن‌ها در هنگام اجرا را تعریف می‌کند.

ES5:

Var

Scope متغیرهایی که با کلمه کلیدی var تعریف شده‌اند، زمینه اجرایی فعلی آن‌ها است. این یا یک تابع محصور است، یا برای متغیرهایی که در خارج از هر نوع تابع تعریف شده‌اند، global است. بیایید به چند مثال نگاه داشته باشیم، تا مشخص کنیم که معنای این جمله چیست.

متغیرهای global

console.log(hoist); // Output: undefined

var hoist = 'The variable has been hoisted.';

ما انتظار داشتیم که نتیجه log این باشد: ReferenceError hoist is not defined. اما در عوض، خروجی ما «undefined» است.

چرا این اتفاق افتاد؟

این کشف، ما را به سرگردان کردن طعمه خود نزدیک‌تر می‌کند.

JavaScript تعریف متغیر را به بالا برده است. کد بالا در interpreter، چنین ظاهری خواهد داشت:

var hoist;

console.log(hoist); // خروجی: undefined

hoist = 'The variable has been hoisted.';

به همین دلیل، ما می‌توانیم از متغیرها قبل از تعریف آن‌ها استفاده کنیم. گرچه، باید مراقب باشیم؛ زیرا متغیر بالا برده شده، با مقدار «undefined» راه‌اندازی می‌شود. بهترین گزینه این است که متغیر خود را قبل از استفاده تعریف کرده، و راه‌اندازی کنیم.

متغیرهای در محدوده تابع

همانطور که در بالا دیدیم، متغیرهای داخل محدوده global به بالای آن محدوده برده شده‌اند. سپس، بیایید به نحوه بالا برده شدن متغیرهای در سطح تابع نگاهی داشته باشیم.

function hoist() {

  console.log(message);

  var message='Hoisting is all the rage!'

}

hoist();

حدث بزنید که خروجی ما چه خواهد بود.

اگر «undefined» را حدث زدید، ‌حدث شما درست است. اگر نه، نگران باشید؛ به زودی به انتهای این مسئله خواهیم رسید.

Interpreter کد بالا را به این صورت می‌بیند:

function hoist() {

  var message;

  console.log(message);

  message='Hoisting is all the rage!'

}

hoist(); // خروجی: undefined

تعریف متغیر یا var message، که scope آن تابع hoist() است، به بالای آن تابع منتقل شده است.

در جهت جلوگیری از افتادن در این تله، مطمئن می‌شویم که متغیر را قبل از استفاده تعریف کرده، و راه‌اندازی ‌می‌کنیم.

function hoist() {

  var message='Hoisting is all the rage!'

  return (message);

}

hoist(); // خروجی: Hoisting is all the rage!

حالت strict

با تشکر از ابزاری در نسخه ES5 جاوااسکریپت که با نام حالت strict شناخته می‌شود، ما می‌توانیم نسبت به تعریف متغیرهای خود بیشتر مراقب باشیم. با فعال کردن حالت strict، ما یک نوع محدود JavaScript را انتخاب می‌کنیم که استفاده از متغیرها قبل از تعریف شدن آن‌ها را تحمل نمی‌کند.

اجرای کد در حالت strict:

  1. برخی خطاهای JavaScript بی صدا را با تغییر آن‌ها به حالت خطای explicit که توسط interpreter بیرون ریخته می‌شوند، از بین می‌برد.
  2. اشتباهاتی که باعث می‌شوند اجرای بهینه‌سازی‌ها برای موتورهای JavaScript سخت شود را بر طرف می‌کند.
  3. برخی سینتکس‌هایی که احتمال دارد در نسخه‌های آینده JavaScript تعریف شوند را ممنوع می‌کند.

ما با نوشتن این کد در تابع یا فایل خود، حالت strict را فعال می‌کنیم:

'use strict';

// یا

"use strict";

بیایید آن را آزمایش کنیم.

'use strict';

console.log(hoist); // خروجی: ReferenceError: hoist is not defined

hoist = 'Hoisted'; 

ما می‌توانیم ببینیم که به جای فرض این که ما تعریف متغیر را فراموش کردیم، use strict ما را با بروز خطای Reference error متوقف کرده است. این کار را بدون استفاده از strict امتحان کنید، و ببینید که چه اتفاقی می‌افتد.

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

ES6:

ECMAScript 6 یا ECMAScript 2015 که با نام ES6 هم شناخته می‌شود، آخرین نسخه استاندارد ECMAScript است. این نسخه برخی تغییرات نسبت به نسخه ES5 را به همراه دارد.

علاقه ما در این است که تغییرات در استاندارد، چگونه تعریف و راه‌اندازی متغیرهای JavaScript را تحت تاثیر قرار می‌دهد.

let

قبل از این که شروع کنیم، باید توجه داشته باشید که متغیرهایی که با کلمه کلیدی let تعریف شده‌اند، در scope بلوک قرار دارند، نه بلوک تابع. این مسئله قابل توجه است، اما نباید در اینجا مزاحم ما شود. گرچه به طور خلاصه به این معنی است که scope متغیر به بلوکی بسته است که در آن تعریف شده است؛ نه تابعی که در آن تعریف شده است.

بیایید با نگاه به رفتار کلمه کلیدی let شروع کنیم.

console.log(hoist); // خروجی: ReferenceError: hoist is not defined ...

let hoist = 'The variable has been hoisted.';

به مانند قبل، برای کلمه کلیدی var، ما انتظار داریم که خروجی log برابر با undefined باشد. گرچه از آنجایی که ES6 let نسبت به استفاده از متغیرهای تعریف نشده خوب واکنش نشان نمی‌دهد، interpreter یک خطای Reference را بروز می‌دهد.

این تضمین می‌کند که ما همیشه اول متغیر خود را تعریف می‌کنیم.

گرچه، همچنان باید در اینجا مراقب باشیم. یک پیاده‌سازی به مانند مورد زیر، با نتیجه undefined به جای Reference error ختم خواهد شد:

let hoist;

console.log(hoist); // خروجی: undefined

hoist = 'Hoisted'

از این رو در جهت امنیت، بهتر است که قبل از استفاده از متغیر خود، آن را تعریف کرده و اختصاص دهیم.

const

کلمه کلیدی const در ES6 برای فراهم کردن متغیرهای غیر قابل جهش معرفی شد. منظور از متغیرهای غیر قابل جهش، متغیرهایی است که مقدارشان پس از اختصاص یابی، نمی‌تواند تغییر یابد.

با const درست به مانند let، متغیر به بالای بلوک فرستاده می‌شود.

بیایید ببینیم که اگر سعی کنیم مقدار متصل به متغیر const را مجددا اختصاص دهیم، چه اتفاقی می‌افتد.

const PI = 3.142;

PI = 22/7; // بیایید مقدار متغیر مورد نظر را اختصاص دهیم

console.log(PI); // خروجی: TypeError: Assignment to constant variable.

const چگونه تعریف متغیر را تغییر می‌دهد؟ بیایید نگاهی داشته باشیم.

console.log(hoist); // خروجی: ReferenceError: hoist is not defined

const hoist = 'The variable has been hoisted.';

بسیار شبیه به کلمه کلیدی let، به جای این که به طور بی صدا با یک خطای undefined خارج شویم، interpreter با بروز خطای Reference error ما را نجات می‌دهد.

همین مسئله در هنگام استفاده از const در داخل توابع هم اتفاق می‌افتد.

function getCircumference(radius) {

  console.log(circumference)

  circumference = PI*radius*2;

  const PI = 22/7;

}

getCircumference(2) // ReferenceError: circumference is not defined

ES6 با const بیشتر پیش‌ می‌رود. اگر قبل از تعریف و راه‌اندازی یک const از آن استفاده کنیم، interpreter یک خطا را بروز می‌دهد.

Linter ما هم سریعا با بروز این خطا، ما را از این اشتباه آگاه می‌سازد:

PI was used before it was declared, which is illegal for const variables.

معنی: «PI قبل از این که تعریف شود، استفاده شد، که برای متغیرهای const ممکن نیست.»

به صورت global:

const PI;

console.log(PI); // خروجی: SyntaxError: Missing initializer in const declaration

PI=3.142;

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

به عنوان مقدمه‌ای به این بخش، مهم است که دقت داشته باشید JavaScript در واقع متغیرهایی که در let و const در ES6 تعریف شده‌اند را به بالا منتقل می‌کند. در این مورد، تفاوت در نحوه راه‌اندازی آن‌ها است. متغیرهایی که با استفاده از let و const تعریف شده‌اند، در ابتدای اجرا راه‌اندازی نشده باقی می‌مانند، در حالیکه متغیرهای تعریف شده با استفاده از var، با مقدار undefined راه‌اندازی می‌شوند.

بالا بردن توابع:

توابع JavaScript می‌توانند آزادانه به این صورت طبقه‌بندی شوند:

  1. تعریفات تابع
  2. بیانیه‌های تابع

ما نحوه تحت تاثیر قرار گرفتن بالا بردن توسط هر دو نوع تابع را بررسی خواهیم کرد.

تعریفات تابع

این‌ قطعه کد در قالب مربوطه است و کاملا به بالا منتقل شده است. (hoist شده است) حال ما می‌توانیم درک کنیم که چرا JavaScript ما را قادر می‌سازد تا یک تابع را بدون تعریف کردن آن، فراخوانی کنیم.

hoisted(); // خروجی: "This function has been hoisted."

function hoisted() {

  console.log('This function has been hoisted.');

};

بیانیه‌های تابع

گرچه عبارات تابع، به بالا منتقل نمی‌شوند.

expression(); //خروجی: "TypeError: expression is not a function

var expression = function() {

  console.log('Will this work?');

};

بیایید ترکیب یک تعریف و بیانیه تابع را امتحان کنیم.

expression(); // خروجی: TypeError: expression is not a function

var expression = function hoisting() {

  console.log('Will this work?');

};

همانطور که در بالا می‌بینیم، تعریف متغیر var expression به بالا منتقل شده است، اما اختصاص‌دهی آن به یک تابع اینطور نیست. از این رو، با توجه به این که interpreter ما expression را نه به عنوان یک تابع، بلکه به عنوان یک متغیر می‌بیند، یک خطای TypeError را بروز می‌دهد.

ترتیب اولویت‌ها:

در هنگام تعریف کردن توابع و متغیرهای JavaScript، باید چند چیز را به یاد داشته باشید.

  1. اختصاص‌دهی متغیر نسبت به تعریف تابع اولویت دارد.
  2. تعریفات تابع نسبت به تعریفات متغیر اولویت دارند.

تعریفات تابع بالاتر از تعریفات متغیر منتقل می‌شوند، اما نه بالاتر از اختصاص‌دهی متغیر.

بیایید ببینیم که این رفتار چه پیامدهایی دارد.

اختصای دهی متغیر، فراتر از تعریف تابع

var double = 22;

function double(num) {

  return (num*2);

}

console.log(typeof double); // خروجی: number

تعریفات تابع، فراتر از تعریفات متغیر


var double;

function double(num) {

  return (num*2);

}

console.log(typeof double); // خروجی: function

حتی اگر موقعیت تعریفات را برعکس می‌کردیم، ترجمه کننده JavaScript همچنان double را به عنوان یک تابع در نظر می‌گرفت.

بالا بردن کلاس‌ها:

کلاس‌های JavaScript هم می‌توانند به عنوان این موارد بالا بردن شوند:

  1. تعریفات کلاس
  2. بیانیه‌های کلاس

تعریفات کلاس

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

var Frodo = new Hobbit();

Frodo.height = 100;

Frodo.weight = 300;

console.log(Frodo); // خروجی: ReferenceError: Hobbit is not defined

class Hobbit {

  constructor(height, weight) {

    this.height = height;

    this.weight = weight;

  }

}

مطمئن متوجه شده‌اید که به جای دریافت undefined، یک خطای Reference error را دریافت می‌کنیم. این شواهد، به موقعیت ما می‌گوید که تعریفات کلاس به بالا منتقل شده‌اند.

اگر به linter‌ دقت کنید، یک نکته پرکاربر را برای ما به نمایش می‌گذارد.

Hobbit was used before it is declared, which is illegal for class variables

معنی: «Hobbit قبل از این که تعریف شود، استفاده شد، که برای متغیرهای const ممکن نیست.»

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

class Hobbit {

  constructor(height, weight) {

    this.height = height;

    this.weight = weight;

  }

}

var Frodo = new Hobbit();

Frodo.height = 100;

Frodo.weight = 300;

console.log(Frodo); // خروجی: { height: 100, weight: 300 }

بیانیه‌های کلاس

بیانیه‌های کلاس JavaScript هم درست به مانند همتایان تابع آن‌ها، بالا برده نمی‌شوند.

در اینجا یک مثال از نوع «بدون نام» یا «ناشناخته»، از بیانیه کلاس را مشاهده می‌کنید:

var Square = new Polygon();

Square.height = 10;

Square.width = 10;

console.log(Square); // خروجی: TypeError: Polygon is not a constructor

var Polygon = class {

  constructor(height, width) {

    this.height = height;

    this.width = width;

  }

};

و این هم یک مثال با یک بیانیه کلاس نامگذاری شده:

var Square = new Polygon();

Square.height = 10;

Square.width = 10;

console.log(Square); // خروجی: TypeError: Polygon is not a constructor

var Polygon = class Polygon {

  constructor(height, width) {

    this.height = height;

    this.width = width;

  }

};

روش درست انجام آن هم به این صورت است:

var Polygon = class Polygon {

  constructor(height, width) {

    this.height = height;

    this.width = width;

  }

};

var Square = new Polygon();

Square.height = 10;

Square.width = 10;

console.log(Square);

هشدار:

درباره این که متغیرها و کلاس‌های let و const بالا برده می‌شوند، تقریبا بالا برده می‌شوند یا این که اصلا بالا برده نمی‌شوند، مقداری بحث و جدل وجود دارد. برخی هم معتقدند که آن‌ها در واقع بالا برده می‌شوند، اما راه‌اندازی نمی‌شوند؛ در حالیکه برخی می‌گویند اصلا بالا برده نمی‌شوند.

نتیجه گیری:

بیاید آنچه که تا به اینجا یاد گرفته‌ایم را خلاصه کنیم:

  1. در هنگام استفاده از ES5 var، تلاش برای استفاده از متغیرهای تعریف نشده به این ختم می‌شوند که در هنگام بالا بردن، مقدار undefined به متغیر مورد نظر اختصاص داده شود.
  2. در هنگام استفاده از ES6 let و const، استفاده از متغیرهای تعریف نشده به یک خطای Reference error ختم خواهد شد؛ زیرا متغیر مورد نظر در هنگام اجرا، راه‌اندازی نشده باقی می‌ماند.

از این رو،

  1. باید تعریف و راه‌اندازی متغیرها قبل از استفاده را برای خود تبدیل به یک عادت کنیم.
  2. استفاده از حالت strict در JavaScript ES5 می‌تواند در به معرض گذاشتن متغیرهای تعریف نشده کمک کند.

امیدوارم این مقاله راهنمای خوبی برای شما در مفهوم بالا بردن در JavaScript باشد.

منبع

مقالات پیشنهادی

کم کردن حجم فایل های CSS - Javascript

با سلام خدمت دوستان در این مقاله طریقه ی فشرده سازی فایل های Css - javascript رو خدمتتون آموزش خواهم داد . هنگامی که شما چندین خط کد برای وب سایتتون م...

15 کتابخانه جالب javascript و css

ماموریت ما در راکت این است که شما را بر اساس تکنولوژی روز طراحی وب به روز نگه داریم . به همین خاطر هم هست که ما تقریبا ماهانه یا چند هفته در میان پستی...

درک closureهای JavaScript - بخش 2

همانطور که نام این مقاله اشاره می‌کند، درک closureهای JavaScript همیشه یک مسئله سخت هستند. من تعداد زیادی مقاله در این زمینه خوانده‌ام، از آن‌ها در کا...

15 کتابخانه جالب javascript و css دی ۹۵

ماموریت ما در راکت این است که شما را بر اساس تکنولوژی روز طراحی وب به روز نگه داریم . به همین خاطر هم هست که ما تقریبا ماهانه یا چند هفته در میان پستی...