پر تکرار ترین سوال مصاحبه های جاواسکریپت بی شک همین سواله. تعریف خیلی ساده میشه براش داد، تعریف های پیچیده تر هم میشه براش داد، من دنبال اینم که از همه زوایا بهش نگاه کنم و فرق عمیق این ها رو با هم ببینیم که چی هست.
جاواسکریپت برای تعریف کردن یک متغیر به ما گزینه های مختلفی میده، یه متغیر میتونه بدون هیچکدوم از این کلمات کلیدی قبلش هم تعریف بشه یا میتونه با یکی از این کلمات باشه.
تا قبل از ES2015 تنها گزینه استفاده از var بود. بعد let و const به زبان اضافه شدن و باعث شدن روی متغیر ها و نحوه تعریف و دسترسی شون کنترل بیشتری داشته باشیم. تو این مطلب میخوایم فرق عمیق اون ها رو باهم ببینیم و از جوانب مختلف بررسی شون کنیم.
با چیز هایی مثل Hoisting و Temporal Dead Zone و Lexical Scope آشنا میشیم و دقیقا میبینیم که چطور رفتار میکنن و توی اون پروسه ها چه اتفاقی میوفته.
برای اینکه بهتر متوجه بشیم اون سه تا کلمه کلیدی چه تفاوتی باهم دارن اول باید ببینیم مفهوم Scope توی زبان برنامه نویسی چیه؟
در واقع Scope اشاره داره به قسمتی از برنامه که یک entity با یک نام که تعریف میشه (مثل متغیر ها) توی اون قسمت (یا بلاک یا تیکه کد) تا زمانی وجود داشتنش valid و قابل قبول هست. مثلا اگر شما توی اتاق، روی میز تون یک دفتر و خودکار داشته باشید، scope اون دفتر و خودکار میشه محدوده میز شما. و اگر شما از دفتر و خودکار صحبت میکنید، میدونید که دارید تو سطح اون میز صحبت میکنید و اگر برید تو حموم اون خودکار و دفتر وجود نخواهند داشت! مگر اینکه یه خودکار و دفتر رو ببرید توی حموم (چرا باید اینکارو بکنید دیگه نمیدونم). پس در واقع entity من که میشه قلم و دفتر فقط در سطح میز و مجدد در سطح میزی که توی اتاق هست معنا پیدا میکنه.
حالا اگر این scope رو بزرگتر کنیم، مثلا بیاید بگیم که یه خودکار و دفتری هست که توی Scope خونه تعریف میشه، چه این دفتر و قلم روی میز توی اتاق باشه چه هرجای دیگری از خونه ما میدونیم که داریم راجع به چه قلم و خودکاری صحبت میکنیم. پس در حقیقت scope رو بزرگتر کردیم و تعریف مون از قلم و دفتر هم تو سطح کل خونه معنا دار هست.
این میشه مفهوم Scope. یک چیزی (entity) در یک فضایی تعریفش با یک اسمی (name) معنا دار باشه و قابل دسترس باشه که تا زمانی که داخل اون فضا (scope) هستیم بتونیم بهش اشاره کنیم، بخونیم و تغییرش بدیم و به طور کلی روش بتونیم کاری انجام بدیم.
جزییات بیشتر رو در مورد scope اینجا بخونید.
تا قبل از 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 در زمان اجرای کد جاواسکریپت اتفاق میوفته در تو این فرایند مفسر (interpreter) میاد تعاریف تابع ها، متغیر ها و کلاس ها رو به بالاترین جا داخل scope شون انتقال میده. البته نسبت به ترتیب اجراشون (نه ترتیبی که توی کد نوشته شدن).
چیز هایی که قابلیت Hoisting دارن اینا هستن:
من خیلی طرفدار ترجمه فارسی این عبارات نیستم، ولی به خاطر محدودیت پلتفرم ویرگول به خاطر نحوه چیدمان حروف فارسی و انگلیسی مجبورم جمله هام رو با یه کلمه فارسی شروع کنم!
در واقع متغیر ها هم جز این قاعده هستند ولی با کمی تفاوت اما به طور کلی به صورت عامیانه میتونیم به hoisting اینطور نگاه کنیم که:
تا همینجا کافیه و فقط من یه مثال میزنم برای اینکه موضوع قشنگ جا بیوفته و بریم سراغ ادامه داستان مون.
جلو تر بهتر دلیل این اتفاق رو و تفاوتش با let و const رو میبینیم.
رفتار var تو hoisting
خب همینطور که hoisting رو خیلی خلاصه توضیح دادم، فرایند hoisting باعث میشه تعریف متغیر ها و توابع به بالای اون scope انتقال پیدا کنه و همه جا داخل scope در دسترس باشه. دیدیم که توی مثال بالا مقدار y بعد از اینکه استفاده شده بود مقدار دهی شد و مشکلی هم نداشت اما چیزی که روی صفحه نمایش میداد این بود:
5 undefined
این اتفاقی هست که under the hood داره میوفته:
خب پس متوجه شدیم که var در فرایند hoisting مقدارش undefined ست میشه.
مشکل کار اینجا بوجود میاد که var قابلیت redclare شدن داره:
اینجا با وجودی که name یه بار قبلا تعریف شده، ما دوباره داریم تعریفش میکنیم و خروجی ما احتمالا چیزی نیست که ما میخوایم.
خب دیگه امروزه بیشتر از 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 خیلی شبیه let هست از نظر scope، تنها تفاوتی که نسبت به let وجود داره اینه که، مقدارش قابل تغییر نیست. یعنی redeclare نمیشه همچنین reassign هم نمیتونه بشه:
البته این موضوع reassign شدن داخل const یکم عجیب ممکنه رفتار کنه. اگر مقداری که داخل متغیری که با const تعریف میشه جنسش object یا array باشه میتونیم مقدار داخلش رو تغییر بدیم ولی نمیتونیم reassign اش کنیم:
این اتفاق برای object ها هم میتونه بیوفته که خیلی شبیه همین حالته که من دیگه مثالش رو نمیزنم.
رفتار const تو hoisting
توی این موضوع هم کاملا رفتار مشابه let رو داره.
وقتی که شما متغیر ها رو به کمک let یا const تعریف میکنید، اصطلاحا میگیم اون متغیر وارد یک TDZ میشه، از آغاز یک بلاک شروع میشه و تا جایی که اجرای کد وارد مرحله ی تعریف و مقدار دهی اون متغیر بشه.
پس یعنی میتونیم بهش اینطور نگاه کنیم:
از اول بلاک شروع میشه TDZ و تا انتهای بلاک که متغیر foo تعریف و مقدار دهی شده ادامه داره. این رو با انتهای کل بلاک اشتباه نگیرید.
دلیلی که اسمش Temporal هست اینه که اندازه این فضا به ترتیب اجرا شدن کد وابسته هست نه به ترتیبی که کد نوشته شده. یعنی وابستگیش زمانی هست تا مکانی.
برای نمونه:
این تیکه کد کار میکنه به خاطر اینکه اگر طبق زمان اجرا شدنشون بریم جلو، اول متغیر letVar تعریف و مقدار دهی شده و بعد func رو صدا زدیم. در زمان اجرای func هم خب میتونیم دسترسی داشته باشیم به مقدار letVar. ولی اگر نسبت به موقعیت مکانی میخواست حساب کنه باید همون اول که تابع func تعریف شده و لاگ گرفتیم از letVar به ما ارور میداد.
خب اینجا لازمه یکم در مورد این موضوع صحبت کنیم. در واقع Lexical Scope فضایی هست برای تعاریف! یعنی فرض کنید انگار جاواسکریپت که میخواد تعریف متغیر ها رو گروه بندی کنه یا تو conext های مختلفی اون ها رو نگهداری کنه از این مفهوم استفاده میشه. (البته نگهداری واژه درستی نیست) برای درک بهتر این موضوع این مثال رو ببینید:
چون متغیر i به صورت var تعریف شده، در تمام iteration های این حلقه مقدار قبلش رو تغییر میده و از اونجا که setTimeout یک تسک async هست بنابراین همیشه آخرین مقدار i رو داخلش داره که میشه ۴.
ولی کافیه اون i رو به صورت let تعریف کنیم، الان داستان متفاوت میشه، چون که توی هر بار اجرای این حلقه یک lexical context بوجود میاد، حتی با وجود setTimeout مقدار i رو چیزی نشون میده که اون زمان که این کد اجرا شده بوده مقدار داشته. توی این حالت میشه 0 و 1 و 2 و 3.
سعی کردم توی این تصویر اینو بهتر توضیح بدم:
تو هر مرحله از اجرا یک scope جدید بوجود میاد و مقادیر داخلش تعریف میشن. امیدوارم مفهومش رو متوجه شده باشید.
این کدی که اینجاست منجر میشه به ارور:
چرا؟ به خاطر اینکه زمانی که اینجا شرط داخل تابع درسته چون foo تعریف و مقدار دهی شده و کد وارد بلاک if میشه.
خب وارد بلاک if شدیم، حالا یه متغیر تعریف شده به اسم foo که داخل همین بلاک if هم وجود داره، یعنی چی؟ یعنی الان داخل TDZ هم هست و هنوز مقدار دهی نشده، حالا اگه بیایم بخوایم مقدار foo رو داخلش بریزیم ارور میده چون هنوز TDZ کارش باهاش تموم نشده! مثل همون کاری میمونه که بیایم سعی کنیم به مقدار foo دسترسی داشته باشیم قبل از اینکه مقدار دهیش کرده باشیم.
پس خوبه که بدونیم این دو تا موضوع کارشون باهم فرق داره و تو چنین مواقعی خطا هایی که میگیریم ممکنه این شکلی هم باشه.
زمانی که متغیر ها رو به صورت Array Destructure یا Object Destructure هم تعریف میکنید تمام اتفاقاتی که برای let یا const میوفته اونجا هم صادقه. مثلا:
میتونید چندتا متغیر رو باهم به صورت همزمان هم تعریف کنید:
اگر هنگام تعریف متغیر با const و let بلاک کد رو مشخص نکنید بهتون خطا میده:
دلیل این اتفاق اینه که let و const باید داخل یک بلاک از کد قرار بگیرن که TDZ بتونه کارش رو انجام بده. در غیر اینطورت نمیدونه scope این متغیر ها کجاست و خطا دادنش منطقی هست. اما از اونجا که var به صورت global تعریف میشه مشکلی براش پیش نمیاد. این اتفاق برای switch statement هم میوفته.
این جدول هم خوبه که ببینید با توجه به اینکه احتمالا تمام تعاریف رو الان بلدیم.
امیدوارم از خوندن این مطلب لذت برده باشید و یه چیز جدید یاد گرفته باشید. برای من نوشتن این مطالب انگیزه ای هست برای ادامه دادن و یاد گرفتن . عمیق تر شدن. حتما دوست دارم نظراتتون رو بشنوم و اگه دوست داشتید میتونید از طریق لینکدین باهام تماس بگیرید.