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

برای تعریف متغیر در جاوااسکریپت از سه کلمه کلیدی let ،var و const استفاده می شود. استفاده از var برای تعریف متغیر به شروع زبان جاوااسکریپت برمی گردد، اما دو کلمه کلیدی let و const در ES6 معرفی شده اند. حالت استثنایی نیز وجود دارد که از هیچ کلمه کلیدی برای تعریف متغیر استفاده نمی کند.در این پست می خواهیم به طور کامل خصوصیات متغیرهای تعریف شده در هر سه حالت را بررسی کنیم. بررسی را در 4 موضوع Scope ،Reassignment ،Redeclaration و Hoisting انجام می دهیم.تعریف متغیر با varبررسی Scopeبرای var دو Scope تعریف شده است:Global ScopeFunction-level Scopeتعریف Global Scopeاگر متغیری را در خارج از هر بلاکی تعریف کنیم، Scope آن را عمومی یا Global تعریف کرده ایم. مثال زیر را در نظر بگیرید.var name = &quotSara&quot console.log(name); // Saraدر این مثال متغیری با نام name تعریف کرده ایم، در خط بعدی می توان با استفاده از شناسه تعریف شده به محتوای آن دسترسی داشت.با توجه به اینکه متغیر name را خارج از هر بلاکی تعریف کرده ایم (Global Scope) این متغیر به عنوان یک پراپرتی به شی سراسری اضافه می شود.نکته: شی سراسری در محیط Browser شی window است، مگر آنگه در Strict mode باشیم.console.log(window.name); // Saraاز هر نقطه ای از کد می توان به متغیرهای عمومی دسترسی داشت.var name = &quotSara&quot const changeName = () => { name = &quotAli&quot }; changeName(); console.log(name); // Aliدر این مثال متغیر name در حوزه عمومی تعریف شده است، پس در تمام بخش های کد می توان آن را استفاده کرد. همانطور که می بینید تابع changeName مقدار name را تغییر داده است، بعد از فراخوانی تابع changeName مقدار متغیر name به Ali تغییر می کند.تعریف Function-level Scopeاگر متغیری را در یک تابع معرفی کنیم، محدوده دسترسی به آن محدود به همان تابع می شود. مثال زیر را درنظر بگیرید:const sayHello = () => { const helloMsg = &quotHi there!&quot 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 = &quothossein_p&quot console.log(username); // hossein_p username = &quotreza_ahmadi&quot 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 چاپ می شود و بعد مقدار 23.چرا undefined؟در تابع logAge ابتدا از متغیر age لاگ گرفته شده و در خط بعد متغیر age مجددا تعریف شده است. با توجه به اینکه متغیر age در تابع Hoist می شود، درنتیجه تابع به شکل زیر تغییر یافته و اجرا می شود.const logAge = () => { var age; console.log(age); age = 25; };2. خروجی تکه کد زیر چیست؟(function() { var a = b = 3; })(); console.log(&quota defined? &quot + (typeof a !== &quotundefined&quot)); console.log(&quotb defined? &quot + (typeof b !== &quotundefined&quot));با توجه به اینکه دو متغیر 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 = &quothossein_p&quot } function changeUsername() { username = &quotali_aref&quot } 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 ScopeFunction-level ScopeBlock 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 = &quotThis is valid!&quot } 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 = &quothossein_p&quot function changeUsername() { username = &quotreza_ahmadi&quot } console.log(username); // hossein_p changeUsername(); console.log(username); // reza_ahmadiدر تکه کد بالا متغیر username در حوزه عمومی تعریف شده است، پس در تابع changeUsername می توان به آن دسترسی داشت و آن را ویرایش کرد.بررسی Redeclarationامکان Redeclaration در تعریف متغیر با کلمه کلیدی let ممکن نیست.let name = &quotAli&quot let name = &quotReza&quotبا توجه به اینکه دو متغیر name دارای اسکوپ یکسانی هستند (هر دو عمومی هستند) اجرای تکه کد بالا منجر به ارور زیر خواهد شد.Uncaught SyntaxError: Identifier 'name' has already been declaredنکته: مجددا این نکته را یادآوری می کنیم که Redeclaration تنها در Scope یکسان اتفاق می افتد. به مثال زیر توجه کنید:let name = &quotSara&quot if (true) { let name = &quotAhmad&quot 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 = &quotSara&quotاجرای کد بالا منجر به خطای 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: &quotSara&quot, family: &quotMohammadi&quot }; 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 به جای varfor (let i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }, 2000); }2. استفاده از Anonymous Functionfor (var i = 0; i < 5; i++) { (function(i) { setTimeout(() => { console.log(i); }, 1000); })(i); }