تفاوت های تعریف متغیر با let ،var و const در جاوااسکریپت

برای تعریف متغیر در جاوااسکریپت از سه کلمه کلیدی let ،var و const استفاده می شود. استفاده از var برای تعریف متغیر به شروع زبان جاوااسکریپت برمی گردد، اما دو کلمه کلیدی let و const در ES6 معرفی شده اند. حالت استثنایی نیز وجود دارد که از هیچ کلمه کلیدی برای تعریف متغیر استفاده نمی کند.

در این پست می خواهیم به طور کامل خصوصیات متغیرهای تعریف شده در هر سه حالت را بررسی کنیم. بررسی را در 4 موضوع Scope ،Reassignment ،Redeclaration و Hoisting انجام می دهیم.


تعریف متغیر با var

بررسی Scope

برای var دو Scope تعریف شده است:

  • Global Scope
  • Function-level Scope

تعریف Global Scope

اگر متغیری را در خارج از هر بلاکی تعریف کنیم، Scope آن را عمومی یا Global تعریف کرده ایم. مثال زیر را در نظر بگیرید.

var name = "Sara"
console.log(name); // Sara

در این مثال متغیری با نام name تعریف کرده ایم، در خط بعدی می توان با استفاده از شناسه تعریف شده به محتوای آن دسترسی داشت.

با توجه به اینکه متغیر name را خارج از هر بلاکی تعریف کرده ایم (Global Scope) این متغیر به عنوان یک پراپرتی به شی سراسری اضافه می شود.

نکته: شی سراسری در محیط Browser شی window است، مگر آنگه در Strict mode باشیم.

console.log(window.name); // Sara

از هر نقطه ای از کد می توان به متغیرهای عمومی دسترسی داشت.

var name = "Sara"
const changeName = () => {
    name = "Ali"
};
changeName();
console.log(name); // Ali

در این مثال متغیر name در حوزه عمومی تعریف شده است، پس در تمام بخش های کد می توان آن را استفاده کرد. همانطور که می بینید تابع changeName مقدار name را تغییر داده است، بعد از فراخوانی تابع changeName مقدار متغیر name به Ali تغییر می کند.

تعریف Function-level Scope

اگر متغیری را در یک تابع معرفی کنیم، محدوده دسترسی به آن محدود به همان تابع می شود. مثال زیر را درنظر بگیرید:

const sayHello = () => {
    const helloMsg = "Hi there!"
    console.log(helloMsg);
};
sayHello(); // Hi there!

در این مثال متغیری با نام helloMsg در تابع sayHello تعریف شده است، بنابراین دارای اسکوپ محلی Function-level می باشد. درنتیجه اگر آن را خارج از تابع استفاده کنیم، با خطای ReferenceError مواجه می شویم.

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

بررسی Reassignment

ویژگی Reassignment به این موضوع اشاره می کند که آیا می توان مقدار جدیدی برای متغیر از قبل تعریف شده، درنظر گرفت یا خیر.

اگر متغیر را با کلمه کلیدی var تعریف کرده باشیم، می توانیم آن بارها و بارها مقداردهی کنیم.

var username = "hossein_p"
console.log(username); // hossein_p
username = "reza_ahmadi"
console.log(username); // reza_ahmadi

بررسی Redeclaration

خصوصیت Redeclaration به این موضوع اشاره می کند که آیا می توان متغیر تعریف شده را مجددا در همان اسکوپ تعریف کرد یا خیر. تعریف متغیر با var این ویژگی را به همراه دارد.

var userAge = 28;
if (true) {
    var userAge = 25;
}
console.log(userAge); // 25

در این مثال متغیر userAge را در یک بلاک شرطی همیشه true مجددا تعریف کرده ایم. در هنگام فراخوانی متغیر، جاوااسکریپت به آخرین تعریف از متغیر رفته و مقدار آن را برمی گرداند.

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

var userAge = 23;
const changeUserAge = () => {
    var userAge = 25;
    console.log(userAge); // 25
};
changeUserAge();
console.log(userAge); // 23

در این مثال تابعی با نام changeUserAge داریم که متغیر userAge تعریف می کند. با توجه به این نکته که متغیرهای تعریف شده در یک تابع دارای Function-level Scope هستند، این متغیر (userAge) ربطی به متغیر عمومی userAge ندارد، بلکه یک متغیر جدید است که تنها در تابع changeUserAge قابل دسترس است و پس از بی اعتبار می شود.

بررسی Hoisting

تکه کد زیر را درنظر بگیرید:

console.log(userAge);  //undefined
var userAge = 25;

می دانیم که ماشین زبان جاوااسکریپت از بالا به پایین کدها را تفسیر و اجرا می کند، پس در اجرای تکه کد بالا انتظار داریم مرورگر با خطای ReferenceError به ما اخطار دهد. اما چرا خروجی، undefined است؟

دلیل آن مفهومی به نام Hoisting است. جاواسکریپت قبل از اجرای کد، تمامی تعاریف متغیرهایی که با var هستند را به بالای کد انتقال می دهد. درواقع می توان گفت کد بالا به صورت زیر اجرا می شود.

var userAge;
console.log(userAge); //undefined
userAge = 25;

بخش پایانی var

برای آنکه از دانش خود از کلمه کلیدی var مطمئن شویم، سعی کنید دو سوال زیر را پاسخ دهید.

1. خروجی تکه کد زیر چیست؟

var age = 23;
const logAge = () => {
    console.log(age);
    var age = 25;
};
logAge();
console.log(age);

در خروجی ابتدا undefined چاپ می شود و بعد مقدار 25.

چرا undefined؟

در تابع logAge ابتدا از متغیر age لاگ گرفته شده و در خط بعد متغیر age مجددا تعریف شده است. با توجه به اینکه متغیر age در تابع Hoist می شود، درنتیجه تابع به شکل زیر تغییر یافته و اجرا می شود.

const logAge = () => {
    var age;
    console.log(age);
    age = 25;
};

2. خروجی تکه کد زیر چیست؟

(function() {
    var a = b = 3;
})();
console.log("a defined? " + (typeof a !== "undefined"));
console.log("b defined? " + (typeof b !== "undefined"));

با توجه به اینکه دو متغیر a و b در تابع تعریف شده اند، باید دارای اسکوپ تابعی باشند، یعنی خارج از تابع غیرقابل دسترس هستند. پس هر دو تابع typeof باید false باشد. اما خروجی تابع به صورت زیر خواهد بود.

a defined? false
b defined? true

نکته ای که در این مسئله درنظر گرفته نمی شود این است که تکه کد var a = b = 3 کوتاه شده خطوط زیر است.

b = 3;
var a = b;

همانطور که می بینید متغیر b بدون کلمه کلیدی تعریف شده است، پس دارای اسکوپ عمومی است و خارج از تابع نیز در دسترس است. اما متغیر a با کلمه var در تابع تعریف شده است که دارای اسکوپ تابعی است و خارج از تابع اعتباری ندارد.


تعریف متغیر بدون هیج کلمه کلیدی

بررسی Scope

اگر متغیر بدون استفاده از کلمه کلیدی تعریف شود، حوزه آن عمومی درنظر گرفته می شود و به شی عمومی window اضافه می شود. بنابراین برای این گونه متغیرها تنها Global Scope در نظر گرفته می شود. مثال زیر را در نظر بگیرید:

function test() {
    age = 25;
}
test();
console.log(window.age); // 25
console.log(age); // 25

در تکه کد بالا متغیر age در تابع test تعریف شده است، پس انتظار داریم که دارای Function-level Scope باشد، اما چون برای تعریف آن از هیچ کلمه کلیدی استفاده نشده است به عنوان متغیر عمومی درنظر گرفته می شود، به همین دلیل می توانیم خارج از تابع نیز از آن استفاده کنیم.

بررسی Reassignment

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

function setUsername() {
    username = "hossein_p"
}
function changeUsername() {
    username = "ali_aref"
}
setUsername();
console.log(username); // hossein_p
changeUsername();
console.log(username); // ali_aref

در تابع setUsername متغیری با نام username تعریف کرده ایم، با توجه به اینکه در هنگام تعریف متغیر از هیچ کدام از کلمات var ،let و const استفاده نشده است، آن را نه یک مغیر محلی، بلکه متغیر عمومی معرفی کرده ایم. در نتیجه در بخش های دیگری از کد می توانیم به آن دسترسی داشته باشیم.

بررسی Redeclaration

این نوع تعریف متغیر مرحله Declaration ندارد، بنابراین تنها می توان مقدار جدیدی به آن اختصاص داد. توجه داشته باشید که Declaration توسط سه دستور var ،let و const انجام می شود.

بررسی Hoisting

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

console.log(userAge);
userAge = 25; // Uncaught ReferenceError: userAge is not defined

همانطور که می بینید اجرای کد بالا منجر به تولید ReferenceError خواهد شد.

نکته مهم این است که اگر در Strict mode هستید، اجازه ندارید متغیری را بدون کلمه کلیدی تعریف کنید.


تعریف متغیر با let

بررسی Scope

برای let سه Scope تعریف شده است:

  • Global Scope
  • Function-level Scope
  • Block Scope

تعریف Global Scope

حوزه عمومی برای دو کلمه کلیدی var و let حدودا یکسان است. تنها تفاوت آن ها در این است که متغیر عمومی که با var تعریف شود به عنوان پراپرتی از شی سراسری window در نظر گرفته می شود، اما let این گونه نیست.

let age = 23;
console.log(window.age); //undefined

تعریف Function-level Scope

این دامنه هم کاملا شبیه به تعریف متغیر با var است، به این معنی که اگر متغیری را با استفاده از کلمه کلیدی let درون یک تابع تعریف کنیم، تنها در همان تابع اعتبار دارد.

تعریف Block Scope

به کدهای نوشته شده در میان {} یک بلاک کد گفته می شود. اگر متغیری را با let درون یک بلاک تعریف کنیم، تنها و تنها در همان بلاک قابل دسترس است. مثال زیر را درنظر بگیرید:

if (true) {
    let message = "This is valid!"
}
console.log(message); //  Uncaught ReferenceError: message is not defined

در این مثال درون یک بلاک شرطی همواره درست متغیری با نام message تعریف کرده ایم. خارج از بلاک شرطی سعی داشته ایم آن را لاگ بگیریم، اما همانطور که می بینید با خطای ReferenceError مواجه شده ایم. چرا که متغیر message دارای Block Scope است و تنها درون بلاک تعریف شده قابل دسترس است.

نکته: اگر بخواهیم متغیری را درون یک بلاک تعریف کنیم ولی خارج از بلاک نیز به آن دسترسی داشته باشیم باید آن را با استفاده از کلمه کلیدی var تعریف کنیم.

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

let age = 20;
if (true) {
    let age = 23;
    console.log(age);
}
console.log(age);

در اجرای کد بالا ابتدا مقدار 23 و بعد 20 چاپ می شود.

اما چرا 20؟

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

بررسی Reassignment

متغیر تعریف شده با let قابلیت مقداردهی مجدد را دارد. تنها توجه داشته باشید که ابتدا باید حوزه دسترسی متغیر را بررسی کرده و سپس اقدام به تغییر آن کنید.

let username = "hossein_p"
function changeUsername() {
    username = "reza_ahmadi"
}
console.log(username); // hossein_p
changeUsername();
console.log(username); // reza_ahmadi

در تکه کد بالا متغیر username در حوزه عمومی تعریف شده است، پس در تابع changeUsername می توان به آن دسترسی داشت و آن را ویرایش کرد.

بررسی Redeclaration

امکان Redeclaration در تعریف متغیر با کلمه کلیدی let ممکن نیست.

let name = "Ali"
let name = "Reza"

با توجه به اینکه دو متغیر name دارای اسکوپ یکسانی هستند (هر دو عمومی هستند) اجرای تکه کد بالا منجر به ارور زیر خواهد شد.

Uncaught SyntaxError: Identifier 'name' has already been declared

نکته: مجددا این نکته را یادآوری می کنیم که Redeclaration تنها در Scope یکسان اتفاق می افتد. به مثال زیر توجه کنید:

let name = "Sara"
if (true) {
    let name = "Ahmad"
    console.log(name); // Ahmad
}
console.log(name); // Sara

در ابتدا متغیر name در اسکوپ عمومی تعریف شده است، سپس در یک بلاک، متغیر جدیدی با همان نام تعریف شده است.

توجه داشته باشید متغیر name با مقدار Ahmad دارای Block Scope است و پس از بلاک شرط بی اعتبار می شود، اما name با مقدار Sara در اسکوپ Global است و تا انتهای کد دردسترس است. پس این دو متغیر کاملا مستقل و در دو اسکوپ متفاوت هستند. (ربطی به مفهوم Redeclaration ندارند.)

بررسی Hoisting

متغیرهایی که با let تعریف می شوند، Hoisted نمی شوند. بنابراین نمی توانید قبل از تعریف، از آن ها استفاده کنید. مثال زیر را مشاهده کنید:

console.log(name);
let name = "Sara"

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

Uncaught ReferenceError: Cannot access 'name' before initialization

تفاوت های var و let

  • متغیر تعریف شده با var می تواند دارای یکی از دو اسکوپ Global و Function-level باشد. اما کلمه کلیدی let علاوه بر این دو Block Scope را نیز پشتیبانی می کند.
  • با استفاده از کلمه کلیدی var می توانید یک متغیر در یک اسکوپ مجددا تعریف کنید، اما let اجازه Redeclaration در اسکوپ یکسان را به شما نمی دهد.
  • متغیرهایی که با var تعریف شده اند، در هنگام اجرا Hoist می شوند اما متغیرهای تعریف شده با let را باید قبل از استفاده، تعریف کنید.

تعریف متغیر با const

قبل از بررسی ویژگی های این کلیدواژه کمی درخصوص تعریف const صحبت کنیم.

برخی ممکن است const را این گونه تعریف کنند که const ثابتی است که مقدار آن تغییر نمی کند. اما این تعریف اشتباه است.

پس تعریف درست چیست؟ const متغیری است که نمی توان آن را Reassign کرد.

const APP_PORT = 5000;
APP_PORT = 1000;

طبق تعریفی که از const گفته شد، اجرای کد بالا با خطا مواجه می شود.

Uncaught TypeError: Assignment to constant variable.

با توجه به اینکه نمی توان یک ثابت را مجددا Assign کنیم، باید در هنگام تعریف آن، مقدارش را نیز مشخص کنیم، عدم تعریف مقدار ثابت منجر به خطای زیر خواهد شد.

Uncaught SyntaxError: Missing initializer in const declaration

آرایه و ثابت

const numbers = [2, 4, 6, 8];
numbers.push(10);

ممکن است فکر کنید که اجرای کد بالا باید خطا دهد، چرا که متغیر numbers از نوع ثابت تعریف شده است، اما در خط بعدی مقدار جدیدی به آن اضافه کرده ایم.

این موضوع دقیقا همان جایی است که اشتباه بودن تعریف اول const را اثبات می کند. طبق تعریف درست، const متغیری است که قابلیت Reassigned شدن ندارد. در تکه کد بالا هم متغیر numbers مجددا مقداردهی نشده است، بلکه مقداری به خانه های آن اضافه شده است.

آبجکت و ثابت ها

const user = {
    name: "Sara",
    family: "Mohammadi"
};
user.age = 25;

در این مثال نیز متغیر user مجددا مقداردهی نشده است، بلکه پراپرتی جدیدی به آن اضافه شده است.

بررسی Scope، Redeclaration و Hoisting

بررسی این سه مشخصه در تعریف متغیر با const کاملا مشابه let می باشد، بنابراین نیازی به تکرار تمامی نکات نیست.

بررسی Reassignment

تنها تفاوت تعریف متغیر با let و const در این مشخصه است. متغیری که با کلمه کلیدی const تعریف شده است را نمی توان مجددا Assign کرد.


به عنوان بخش پایانی مسئله مرتبطی را که ممکن است در یک مصاحبه جاوااسکریپت از شما پرسیده شود، بررسی می کنیم.

خروجی تکه کد زیر چیست؟

for (var i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    }, 1000);
}

اجرای کد بالا منجر به پنج بار چاپ شدن عدد 5 می شود.

دلیل این است که متغیر i با استفاده از var تعریف شده است که اسکوپ Block را پشتیبانی نمی کند، بنابراین بعد از حلقه for متغیر i با مقدار 5 دردسترس است. درنتیجه درهنگام اجرا شدن کال بک پاس داده شده به setTimeout مقدار i برابر با 5 است.

اگر می خواهید کد را به گونه ای تغییر دهید که اعداد 1 تا 4 چاپ شوند می توانید از دو روش استفاده کنید:

1. استفاده از let به جای var

for (let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i);
    }, 2000);
}

2. استفاده از Anonymous Function

for (var i = 0; i < 5; i++) {
    (function(i) {
        setTimeout(() => {
            console.log(i);
        }, 1000);
    })(i);
}