حسان امینی لو
حسان امینی لو
خواندن ۱۱ دقیقه·۲ سال پیش

یکبار برای همیشه! فرق عمیق let و var و const در جاواسکریپت.

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

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

تا قبل از ES2015 تنها گزینه استفاده از var بود. بعد let و const به زبان اضافه شدن و باعث شدن روی متغیر ها و نحوه تعریف و دسترسی شون کنترل بیشتری داشته باشیم. تو این مطلب میخوایم فرق عمیق اون ها رو باهم ببینیم و از جوانب مختلف بررسی شون کنیم.

با چیز هایی مثل Hoisting و Temporal Dead Zone و Lexical Scope آشنا میشیم و دقیقا میبینیم که چطور رفتار میکنن و توی اون پروسه ها چه اتفاقی میوفته.


مفهوم Scope

برای اینکه بهتر متوجه بشیم اون سه تا کلمه کلیدی چه تفاوتی باهم دارن اول باید ببینیم مفهوم Scope توی زبان برنامه نویسی چیه؟

در واقع Scope اشاره داره به قسمتی از برنامه که یک entity با یک نام که تعریف میشه (مثل متغیر ها) توی اون قسمت (یا بلاک یا تیکه کد) تا زمانی وجود داشتنش valid و قابل قبول هست. مثلا اگر شما توی اتاق، روی میز تون یک دفتر و خودکار داشته باشید، scope اون دفتر و خودکار میشه محدوده میز شما. و اگر شما از دفتر و خودکار صحبت میکنید، میدونید که دارید تو سطح اون میز صحبت میکنید و اگر برید تو حموم اون خودکار و دفتر وجود نخواهند داشت! مگر اینکه یه خودکار و دفتر رو ببرید توی حموم (چرا باید اینکارو بکنید دیگه نمیدونم). پس در واقع entity من که میشه قلم و دفتر فقط در سطح میز و مجدد در سطح میزی که توی اتاق هست معنا پیدا میکنه.

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

این میشه مفهوم Scope. یک چیزی (entity) در یک فضایی تعریفش با یک اسمی (name) معنا دار باشه و قابل دسترس باشه که تا زمانی که داخل اون فضا (scope) هستیم بتونیم بهش اشاره کنیم، بخونیم و تغییرش بدیم و به طور کلی روش بتونیم کاری انجام بدیم.

جزییات بیشتر رو در مورد scope اینجا بخونید.


کلمه کلیدی Var

تا قبل از ES2015 برای تعریف متغیر ها از var استفاده میشد.

کاری که var انجام میده اینه که یه متغیر در Global Scope تعریف میکنه. منظور ما اینجا از global در واقع همون window هست. در کنار این global scope بودن در واقع var یک متغیر رو میتونه در scope یک تابع محدود کنه.

یک مقدار بخوایم تخصصی تر در موردش صحبت کنیم میتونیم اینطور بگیم:

در زمانی که از var برای تعریف متغیر استفاده میشه scope اون متغیر نسبت به Current Execution Context تعیین میشه، مثلا اگر اون context داخل فرایند اجرای یک تابع باشه، اون scope هم میشه همون تابع و اگر در بیرون تابع باشه میشه کل اون فایل یا ماژول جاواسکریپت یا تابع والد. پس سطح دسترس پذیر بودنش نسبت به این پارامتر ها متغیره.

همینطور که توی مثال میتونید ببینید، مقدار name که بیرون از تابع تعریف شده در یک scope دیگری هست، و name که داخل تابع تعریف شده scope خودش رو داره.

حالا اگر این مثال رو به این شکل تغییر بدیم:

میبینیم که name همون مقدار global رو تغییر داده. دقت کنید که متغیر ها از بیرون داخل تابع قابل دسترس هستند ولی از داخل تابع به بیرون (با var) نه! مثال بعدی رو ببینید:

مفهوم Hoisting

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

فرایند hoisting در زمان اجرای کد جاواسکریپت اتفاق میوفته در تو این فرایند مفسر (interpreter) میاد تعاریف تابع ها، متغیر ها و کلاس ها رو به بالاترین جا داخل scope شون انتقال میده. البته نسبت به ترتیب اجراشون (نه ترتیبی که توی کد نوشته شدن).

چیز هایی که قابلیت Hoisting دارن اینا هستن:

  • تابع ها (function)
  • توابع مولد یا همون Generator Function ها (*function)
  • توابع همروند یا Async Function ها (async function)
  • توابع مولد همروند (*async function)
من خیلی طرفدار ترجمه فارسی این عبارات نیستم، ولی به خاطر محدودیت پلتفرم ویرگول به خاطر نحوه چیدمان حروف فارسی و انگلیسی مجبورم جمله هام رو با یه کلمه فارسی شروع کنم!

در واقع متغیر ها هم جز این قاعده هستند ولی با کمی تفاوت اما به طور کلی به صورت عامیانه میتونیم به hoisting اینطور نگاه کنیم که:

  1. میتونیم قبل از تعریف متغیر، ازش استفاده کنیم.
  2. میتونیم قبل از اینکه بهش مقداردهی کنیم، بهش اشاره کنیم (ولی خب مقدارش احتمالا undefined هست)
  3. تعریف یه متغیر میتونه رفتار برنامه رو توی یه scope تا قبل از جایی که تعریف شده تغییر بده.

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

مثال از w3school
مثال از w3school


جلو تر بهتر دلیل این اتفاق رو و تفاوتش با let و const رو میبینیم.

رفتار var تو hoisting

خب همینطور که hoisting رو خیلی خلاصه توضیح دادم، فرایند hoisting باعث میشه تعریف متغیر ها و توابع به بالای اون scope انتقال پیدا کنه و همه جا داخل scope در دسترس باشه. دیدیم که توی مثال بالا مقدار y بعد از اینکه استفاده شده بود مقدار دهی شد و مشکلی هم نداشت اما چیزی که روی صفحه نمایش میداد این بود:

5 undefined

این اتفاقی هست که under the hood داره میوفته:

خب پس متوجه شدیم که var در فرایند hoisting مقدارش undefined ست میشه.

مشکل کار اینجا بوجود میاد که var قابلیت redclare شدن داره:

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


کلمه کلیدی Let

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

چرا اسمش شد let؟ میتونید اینجارو ببینید که کامل توضیح داده.

در خلاف var که به صورت funciton scope بود، let به صورت block scope هست.

حالا اصلا block چیه؟ بلاک رو میتونیم اینطور در نظر بگیریم که هر جا که یک آکولاد (آکلاد؟) هست یک بلاک از کد رو داریم مثلا همه این نمونه ها که میبینید یک block از کد هستند:

وقتی میگیم که let به صورت block scope هست یعنی وقتی که ما توی هر کدوم از این بلاک های کد متغیری رو با کمک let تعریف کنیم داخل همون بلاک از کد valid هست و معنا داره و بیرونش دیگه قابل دسترس نیست. دقیقا اتفاقی که توی var نمیوفتاد (فقط برای تابع اتفاق میوفتاد البته)

تو اینجا میتونیم ببینیم که name داخل بلاک if تعریف شده و کاری با name که بیرون تعریف شده نداره.

مشکلی که با var داشتیم قابلیت redeclare شدنش بود، بر خلاف var توی let این امکان وجود نداره:

میبینیم که تو این حالت خطا میگیریم. البته میتونیم مقدار داخلش رو تغییر بدیم ولی نمیشه مجدد redeclare بشه که قبل تر توضیحش رو دادم.


رفتار let تو hoisting

مثل var که میرفتن بالای اون scope برای let هم این اتفاق میوفته ولی برخلاف var که مقدار اولیه اش undefined ست میشد، متغیر let مقدار اولیه نمیگیره بنابراین اگر سعی کنید بهش دسترسی پیدا کنید به شما خطا میده.


کلمه کلیدی const

همه چیز const خیلی شبیه let هست از نظر scope، تنها تفاوتی که نسبت به let وجود داره اینه که، مقدارش قابل تغییر نیست. یعنی redeclare نمیشه همچنین reassign هم نمیتونه بشه:

البته این موضوع reassign شدن داخل const یکم عجیب ممکنه رفتار کنه. اگر مقداری که داخل متغیری که با const تعریف میشه جنسش object یا array باشه میتونیم مقدار داخلش رو تغییر بدیم ولی نمیتونیم reassign اش کنیم:

این اتفاق برای object ها هم میتونه بیوفته که خیلی شبیه همین حالته که من دیگه مثالش رو نمیزنم.


رفتار const تو hoisting

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


تعریف Temporal dead zone یا TDZ

وقتی که شما متغیر ها رو به کمک let یا const تعریف میکنید، اصطلاحا میگیم اون متغیر وارد یک TDZ میشه، از آغاز یک بلاک شروع میشه و تا جایی که اجرای کد وارد مرحله ی تعریف و مقدار دهی اون متغیر بشه.

پس یعنی میتونیم بهش اینطور نگاه کنیم:

نمونه از MDN
نمونه از MDN

از اول بلاک شروع میشه TDZ و تا انتهای بلاک که متغیر foo تعریف و مقدار دهی شده ادامه داره. این رو با انتهای کل بلاک اشتباه نگیرید.

دلیلی که اسمش Temporal هست اینه که اندازه این فضا به ترتیب اجرا شدن کد وابسته هست نه به ترتیبی که کد نوشته شده. یعنی وابستگیش زمانی هست تا مکانی.

برای نمونه:

این تیکه کد کار میکنه به خاطر اینکه اگر طبق زمان اجرا شدنشون بریم جلو، اول متغیر letVar تعریف و مقدار دهی شده و بعد func رو صدا زدیم. در زمان اجرای func هم خب میتونیم دسترسی داشته باشیم به مقدار letVar. ولی اگر نسبت به موقعیت مکانی میخواست حساب کنه باید همون اول که تابع func تعریف شده و لاگ گرفتیم از letVar به ما ارور میداد.


مفهوم Lexical Scoping

خب اینجا لازمه یکم در مورد این موضوع صحبت کنیم. در واقع Lexical Scope فضایی هست برای تعاریف! یعنی فرض کنید انگار جاواسکریپت که میخواد تعریف متغیر ها رو گروه بندی کنه یا تو conext های مختلفی اون ها رو نگهداری کنه از این مفهوم استفاده میشه. (البته نگهداری واژه درستی نیست) برای درک بهتر این موضوع این مثال رو ببینید:


چون متغیر i به صورت var تعریف شده، در تمام iteration های این حلقه مقدار قبلش رو تغییر میده و از اونجا که setTimeout یک تسک async هست بنابراین همیشه آخرین مقدار i رو داخلش داره که میشه ۴.

ولی کافیه اون i رو به صورت let تعریف کنیم، الان داستان متفاوت میشه، چون که توی هر بار اجرای این حلقه یک lexical context بوجود میاد،‌ حتی با وجود setTimeout مقدار i رو چیزی نشون میده که اون زمان که این کد اجرا شده بوده مقدار داشته. توی این حالت میشه 0 و 1 و 2 و 3.

سعی کردم توی این تصویر اینو بهتر توضیح بدم:

تو هر مرحله از اجرا یک scope جدید بوجود میاد و مقادیر داخلش تعریف میشن. امیدوارم مفهومش رو متوجه شده باشید.


ترکیب TDZ و Lexical Scope

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

منبع تصویر MDN
منبع تصویر MDN


چرا؟ به خاطر اینکه زمانی که اینجا شرط داخل تابع درسته چون foo تعریف و مقدار دهی شده و کد وارد بلاک if میشه.

خب وارد بلاک if شدیم، حالا یه متغیر تعریف شده به اسم foo که داخل همین بلاک if هم وجود داره، یعنی چی؟ یعنی الان داخل TDZ هم هست و هنوز مقدار دهی نشده، حالا اگه بیایم بخوایم مقدار foo رو داخلش بریزیم ارور میده چون هنوز TDZ کارش باهاش تموم نشده! مثل همون کاری میمونه که بیایم سعی کنیم به مقدار foo دسترسی داشته باشیم قبل از اینکه مقدار دهیش کرده باشیم.

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



چندتا نکته اضافه ولی مهم

زمانی که متغیر ها رو به صورت Array Destructure یا Object Destructure هم تعریف میکنید تمام اتفاقاتی که برای let یا const میوفته اونجا هم صادقه. مثلا:


میتونید چندتا متغیر رو باهم به صورت همزمان هم تعریف کنید:

میتونستید هم از let استفاده کنید هم const هم var
میتونستید هم از let استفاده کنید هم const هم var



اگر هنگام تعریف متغیر با const و let بلاک کد رو مشخص نکنید بهتون خطا میده:

دلیل این اتفاق اینه که let و const باید داخل یک بلاک از کد قرار بگیرن که TDZ بتونه کارش رو انجام بده. در غیر اینطورت نمیدونه scope این متغیر ها کجاست و خطا دادنش منطقی هست. اما از اونجا که var به صورت global تعریف میشه مشکلی براش پیش نمیاد. این اتفاق برای switch statement هم میوفته.


جمع بندی

  • هر سه این کلمات برای تعریف متغیر به کار میرن
  • فرایند hoisting فرایندی هست که متغیر ها و decleration ها با بالای scope خودشون انتقال داده میشن و از اونجا به بعد قابل دسترس هستند.
  • توی var متغیر به صورت global یا function scope تعریف میشه، میتونیم redeclare اش کنیم و در فرایند hoisting مقدارش undefined میشه.
  • مفهوم scope میشه فضایی که متغیری که تعریف شده توی اون فضا وجود داشته و معنادار باشه.
  • توی let و const متغیر به صورت block scope تعریف میشه که بیرون از اون block قابل دسترس نیست، همچنین هر دو این ها قابلیت redeclare شدن رو ندارن.
  • در const علاوه بر غیرممکن بودن redeclaration امکان reassignment هم وجود نداره و خطا میده، ولی اگر مقدار متغیر به صورت array یا object باشه میتونیم اطلاعات داخل اون ها رو تغییر بدیم.
  • مفهوم lexical scoping میشه چیزی شبیه یه context که توش تعاریف رو نگهداری میکنه.
  • دیدیم که Temporal Dead Zone یا TDZ از زمانی که یک متغیر بوجود میاد یا تعریف میشه تا زمانی که مقدار دهی میشه وجود داره.


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

valentinog تبار تصویر از بلاگ
valentinog تبار تصویر از بلاگ



امیدوارم از خوندن این مطلب لذت برده باشید و یه چیز جدید یاد گرفته باشید. برای من نوشتن این مطالب انگیزه ای هست برای ادامه دادن و یاد گرفتن . عمیق تر شدن. حتما دوست دارم نظراتتون رو بشنوم و اگه دوست داشتید میتونید از طریق لینکدین باهام تماس بگیرید.


برنامه نویسیجاواسکریپتآموزش
برنامه نویس از جلو
شاید از این پست‌ها خوشتان بیاید