۲۵ ژوئیه ۲۰۲۲

محدوده متغیر، کلوژِر

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

ما از قبل می‌دانیم که یک تابع می‌تواند به متغیرهای بیرون از خودش دسترسی داشته باشد (متغیرهای «بیرونی»).

اما اگر متغیرهای بیرونی از زمانی که یک تابع ساخته شد تغییر کنند چه اتفاقی می‌افتد؟ آیا تابع مقدارهای جدید را دریافت می‌کند یا قدیمی‌ها را؟

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

بیایید دانش خود را گسترده‌تر کنیم تا این سناریوها و پیچیده‌تر از اینها را درک کنیم.

اینجا ما درباره متغیرهای let/const حرف می‌زنیم

در جاوااسکریپت، 3 راه برای تعریف یک متغیر وجود دارد: let، const (این دو روش مدرن هستند) و var (که از گذشته باقی مانده است).

  • در این مقاله ما از متغیرهای let در مثال‌ها استفاده می‌کنیم.
  • متغیرهایی که با const تعریف شوند، رفتار مشابهی دارند پس این مقاله درباره const هم هست.
  • var قدیمی چند تفاوت قابل توجه دارد که در مقاله ‌کلمه‌ی "var" قدیمی پوشش داده می‌شوند.

بلوک‌های کد

اگر یک متغیر درون بلوک کد {...} تعریف شود، فقط درون همان بلوک قابل رویت است.

برای مثال:

{
  // یک کار با متغیرهای محلی که از بیرون نباید شناخته شوند انجام دهید

  let message = "Hello"; // فقط درون این بلوک قابل رویت است

  alert(message); // Hello
}

alert(message); // تعریف نشده است message :ارور

ما می‌توانیم از این خاصیت برای ایزوله کردن یک قطعه از کد که دارای متغیرهایی است که فقط به آن تعلق دارند و کار خودش را انجام می‌دهد استفاده کنیم:

{
  // نمایش پیام
  let message = "Hello";
  alert(message);
}

{
  // نمایش پیامی دیگر
  let message = "Goodbye";
  alert(message);
}
بدون وجود بلوک‌ها ارور ایجاد خواهد شد

لطفا در نظر داشته باشید که بدون بلوک‌های جدا اگر ما از let همراه با یک متغیر موجود استفاده کنیم، یک ارور ایجاد خواهد شد.

// نمایش پیام
let message = "Hello";
alert(message);

// نمایش پیامی دیگر
let message = "Goodbye"; // ارور: متغیر از قبل تعریف شده است
alert(message);

برای if، for، while و بقیه، متغیرهایی که درون {...} تعریف شده باشند تنها درون آنها قابل رویت هستند:

if (true) {
  let phrase = "Hello!";

  alert(phrase); // Hello!
}

alert(phrase); // !ارور، چنین متغیری وجود ندارد

اینجا، بعد از اینکه if تمام می‌شود، alert متغیر phrase را نمی‌بیند و به همین دلیل ارور ایجاد می‌شود.

این عالی است چون به ما اجازه می‌دهد که متغیرهایی در سطح بلوک محلی بسازیم که به یک شاخه if اختصاص دارند.

همین موضوع برای حلقه‌های for و while هم صادق است:

for (let i = 0; i < 3; i++) {
  // قابل رویت است for فقط درون این حلقه i متغیر
  alert(i); // اول 0، سپس 1، سپس 2
}

alert(i); // ارور، چنین متغیری وجود ندارد

از لحاظ ظاهری، let i بیرون از {...} است. اما اینجا ساختار for خاص است: متغیری که درون آن ساخته شود، جزئی از بلوک کد فرض می‌شود.

تابع‌های تودرتو

به تابعی که درون تابع دیگری ساخته شود «تودرتو» گفته می‌شود.

این کار را به راحتی در جاوااسکریپت می‌توان انجام داد.

می‌توانیم از آن برای سازماندهی کد خود استفاده کنیم، مثلا اینگونه:

function sayHiBye(firstName, lastName) {

  // تابع کمک کننده که پایین‌تر استفاده می‌شود
  function getFullName() {
    return firstName + " " + lastName;
  }

  alert( "سلام " + getFullName() );
  alert( "خداحافظ " + getFullName() );

}

اینجا تابع تودرتو getFullName() برای راحتی استفاده شده است. این تابع می‌تواند به متغیرهای بیرونی دسترسی داشته باشد پس می‌تواند اسم کامل را برگرداند. تابع‌های تودرتو در جاوااسکریپت بسیار رایج هستند.

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

در کد پایین، makeCounter تابع «شمارنده» را می‌سازد که با هر بار فراخوانی عدد بعدی را برمی‌گرداند:

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2

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

این چگونه کار می‌کند؟ اگر ما چند شمارنده بسازیم، آیا آنها مستقل خواهند بود؟ چه چیزی در حال رخ دادن روی متغیرها است؟

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

محیط لغوی (Lexical Environment)

مطالب ناشناخته‌ای وجود دارند!

توضیحات عمیق فنی را ادامه می‌خوانید.

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

برای واضح بودن، توضیحات به چند مرحله تقسیم شده‌اند.

مرحله 1. متغیرها

در جاوااسکریپت، هر تابع در حال اجرا، بلوک کد {...} و تمام اسکریپت، یک شیء درونی (پنهان) اختصاص یافته دارد که به عنوان محیط لغوی شناخته می‌شود.

شیء محیط لغوی شامل دو بخش است:

  1. ذخایر محیط (Environment Record) – یک شیء که تمام متغیرهای محلی را به عنوان ویژگی‌های خود (و اطلاعات دیگری مانند مقدار this) ذخیره می‌کند.
  2. یک رجوع به محیط لغوی بیرونی (outer)، محیطی که به کد بیرونی اختصاص دارد.

یک «متغیر» فقط یک ویژگی از شیء خاص درونی Environment Record است. «دریافت یا تغییر یک متغیر» به معنی «دریافت یا تغییر یک ویژگی از آن شیء» است.

در این کد ساده که تابعی ندارد، تنها یک محیط لغوی وجود دارد:

این همان محیط لغوی گلوبال است که به تمام کد اختصاص یافته.

در تصویر بالا، مستطیل به معنای ذخایر محیط (ذخایر متغیر) است و کمان به معنی مرجع بیرونی. محیط لغوی گلوبال مرجع بیرونی ندارد و به همین دلیل است که کمان به null اشاره می‌کند.

همانطور که کد شروع به اجرا شدن می‌کند و ادامه می‌یابد، محیط لغوی تغییر می‌کند.

یک کد طولانی‌تر را اینجا داریم:

مستطیل‌های سمت راست نشان می‌دهند که محیط لغوی گلوبال در حین اجرا شدن چگونه تغییر می‌کند:

  1. زمانی که اسکریپت شروع می‌کند، محیط لغوی از تمام متغیرهای تعریف شده پر می‌شود.
    • در ابتدا، آنها در حالت «بدون مقدار اولیه (Uninitialized)» هستند. این یک حالت درونی خاص است و به این معنی است که موتور درباره متغیر آگاه است اما تا زمانی که با let تعریف شود نمی‌توان به آن رجوع کرد. تقریبا مانند این است که متغیر وجود ندارد.
  2. تعریف let phrase نمایان می‌شود. هنوز مقداردهی نشده است، پس مقدار آنها undefined است. ما می‌توانیم از اینجا به بعد از متغیر استفاده کنیم.
  3. phrase یک مقدار گرفته است.
  4. phrase مقدار را تغییر می‌دهد.

تا اینجا همه چیز ساده بنظر می‌رسد نه؟

  • یک متغیر، ویژگی یک شیء خاص درونی است که به بلوک/تابع/اسکریپتی که در حال اجرا است اختصاص یافته.
  • کارکردن با متغیرها در واقع کارکردن با ویژگی‌های آن شیء است.
محیط لغوی یک شیء درون مشخصات است

«محیط لغوی» یک شیء درون مشخصات است: این شیء فقط «به صورت تئوری» در مشخصات زبان وجود دارد تا چگونگی کارکردن چیزها را توصیف کند. ما نمی‌توانیم این شیء را در کدمان دریافت کنیم و آن را به صورت مستقیم دستکاری کنیم.

تا آنجایی که رفتار قابل مشاهده همانطور که توصیف شد باقی بماند، موتورهای جاوااسکریپت ممکن است آن را بهینه کنند مثلا برای صرفه‌جویی در اشغال حافظه متغیرهایی که استفاده نمی‌شوند را حذف کنند و ترفندهای درونی دیگری را اجرا کنند.

مرحله 2. Function Declaration

یک تابع هم مانند یک متغیر، مقدار است.

تفاوت اینجاست که Function Declaration سریعا به طور کامل مقداردهی می‌شوند.

زمانی که یک محیط لغوی ساخته می‌شود، یک Function Declaration سریعا به یک تابع آماده استفاده تبدیل می‌شود (برخلاف let که تا زمان تعریف آن در کد غیر قابل استفاده است).

به همین دلیل است که از تابعی که به صورت Function Declaration تعریف شده باشد، حتی قبل از رسیدن به تعریف آن می‌توانیم استفاده کنیم.

برای مثال، زمانی که ما یک تابع اضافه می‌کنیم وضعیت اولیه محیط لغوی گلوبال اینگونه است:

طبیعتا، این رفتار فقط برای Function Declarations است نه برای اعلان تابع Expression که ما یک متغیر را برابر با یک تابع قرار می‌دهیم مانند let say = function(name)....

مرحله 3. محیط‌های لغوی درونی و بیرونی

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

برای مثال، برای say("John)" اینگونه بنظر می‌رسد (فرایند اجرا شدن در خطی است که با کمان نشانه گذاری شده است):

در حین فراخوانی تابع ما دو محیط لغوی داریم: محیط درونی (برای فراخوانی تابع) و محیط بیرونی (گلوبال):

  • محیط لغوی درونی متناظر با فرایند اجرای کنونی say است. این محیط یک ویژگی(property) دارد: name که همان آرگومان تابع است. ما say("John") را فراخوانی کردیم پس مقدار name برابر با "John" خواهد بود.
  • محیط لغوی بیرونی همان محیط لغوی گلوبال است. این محیط متغیر phrase و خود تابع را شامل می‌شود.

محیط لغوی درونی یک رجوع به محیط outer(بیرونی) دارد.

زمانی که کد می‌خواهد به یک متغیر دسترسی پیدا کند – اول محیط لغوی درونی جستجو می‌شود، سپس محیط بیرونی، سپس محیط بیرونی‌تر و همینطور تا محیط لغوی گلوبال ادامه پیدا می‌کند

اگر متغیری جایی پیدا نشود، در حالت سخت‌گیرانه(strict mode) ارور ایجاد می‌شود (بدون use strict، برای سازگاری با کدهای قدیمی اگر یک متغیر که موجود نیست را برای مقداردهی استفاده کنیم، یک متغیر گلوبال جدید ساخته می‌شود).

در این مثال، جستجو اینگونه پیش می‌رود:

  • برای متغیر name، alert که درون say است بلافاصله آن را در محیط لغوی درونی پیدا می‌کند.
  • زمانی که می‌خواهد به phrase دسترسی پیدا کند، هیچ phrase محلی موجود نیست، پس به محیط لغوی بیرونی رجوع و آن را آنجا پیدا می‌کند.

مرحله 4. برگرداندن یک تابع

بیایید به مثال makeCounter برگردیم.

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();

در ابتدای هر فراخوانی makeCounter، یک شیء محیط لغوی جدید ساخته می‌شود تا متغیرها را برای این فراخوانیِ makeCounter ذخیره کند.

بنابراین ما دو محیط لغوی تودرتو داریم، درست مانند مثال بالا:

تفاوتی که وجود دارد این است که در حین اجرای makeCounter، یک تابع کوچک تودرتو فقط به خاطر یک خط ایجاد می‌شود: return count++. ما هنوز این را اجرا نمی‌کنیم فقط می‌سازیم.

تمام تابع‌ها محیط لغوی‌ای که در آن ساخته شده‌اند را به یاد می‌سپارند. از لحاظ فنی، هیچ جادویی اینجا وجود ندارد: تمام تابع‌ها یک ویژگی پنهان [[Environment]] دارند که یک رجوع به محیط لغوی‌ای که تابع در آن ساخته شده است دارد:

بنابراین counter.[[Environment]] یک رجوع به محیط لغوی {count: 0} دارد. اینگونه است که تابع بدون توجه به اینکه کجا فراخوانی شده است، جایی که ساخته شده را به یاد می‌سپارد. مرجع [[Environment]] فقط یک بار و برای همیشه در زمان ساخت تابع تنظیم می‌شود.

بعدا، زمانی که counter() فراخوانی می‌شود، یک محیط لغوی جدید برای آن فراخوانی ایجاد می‌شود و مرجع محیط لغوی بیرونی آن از counter.[[Environment]] گرفته می‌شود:

حالا زمانی که کد، درون counter() را برای متغیر count جستجو می‌کند، ابتدا محیط لغوی خودش را جستجو می‌کند (که به دلیل نبود متغیری محلی خالی است)، سپس محیط لغویِ فراخوانیِ بیرونیِ makeCounter() را جستجو و آنجا آن را پیدا می‌کند و تغییرش می‌دهد.

یک متغیر در محیط لغوی‌ای که وجود دارد تغییر می‌یابد.

بعد از اجرا شدن وضعیت اینگونه است:

اگر ما counter() را چند بار فراخوانی کنیم، متغیر count به 2، 3 و بیشتر در جای یکسانی افزایش می‌یابد.

Closure(کلوژِر)

یک عبارت کلی برنامه‌نویسی به نام “کلوژر” وجود دارد که به طور کلی توسعه‌دهندگان باید درباره آن بدانند.

یک کلوژر تابعی است که متغیرهای بیرون از خودش را به یاد دارد و می‌تواند به آنها دسترسی پیدا کند. در بعضی از زبان‌ها، این موضوع ممکن است یا باید یک تابع به گونه‌ای نوشته شود که این کار را انجام دهد. اما همانطور که در بالا توضیح داده شد، در جاوااسکریپت، تمام تابع‌ها به طور طبیعی کلوژر هستند (تنها یک استثنا وجود دارد که در سینتکس "new Function" پوشش می‌دهیم).

این یعنی اینکه: آنها به طور خودکار جایی که ساخته شده‌اند را با استفاده از ویژگی پنهان [[Environment]] به یاد می‌سپارند و سپس کد آنها می‌تواند به متغیرهای بیرونی دسترسی پیدا کند.

زمانی که در مصاحبه کاری هستید و یک توسعه‌دهنده فرانت‌اند سوالی درباره اینکه «کلوژر چیست؟» دریافت می‌کند، به عنوان یک پاسخ معتبر می‌توانید تعریف کلوژر و یک توضیح درباره اینکه تمام تابع‌ها در جاوااسکریپت کلوژر هستند را بگویید و شاید چند کلمه درباره جزییات فنی: ویژگی [[Environment]] و اینکه محیط لغوی چگونه کار می‌کند.

زباله‌روبی

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

گرچه، اگر تابع‌ای تودرتو وجود داشته باشد که بعد از پایان یک تابع هنوز قابل دسترس باشد، پس یک ویژگی [[Environment]] دارد که به محیط لغوی رجوع می‌کند.

در این صورت محیط لغوی حتی بعد از تکمیل تابع هنوز قابل دسترس است پس از بین نمی‌رود.

برای مثال:

function f() {
  let value = 123;

  return function() {
    alert(value);
  }
}

let g = f(); // یک رجوع به محیط لغوی را ذخیره می‌کند g.[[Environment]]
// است f() که همان محیط لغوی فراخوانی

لطفا در نظر داشته باشید که اگر f() چند بار فراخوانی شود و تابع‌های برگردانده‌شده ذخیره شوند، سپس تمام شیءهای محیط لغوی متناظر هم در حافظه نگهداری می‌شود. در کد زیر هر 3تای آنها ذخیره می‌شود:

function f() {
  let value = Math.random();

  return function() { alert(value); };
}

// در آرایه 3 تابع وجود دارد، هر کدام به محیط لغوی متصل هستند
// f() محیطی از فراخوانی متناظر
let arr = [f(), f(), f()];

یک شیء محیط لغوی زمانی که غیر قابل دسترس شود ازبین می‌رود (درست مانند هر شیء دیگری). به عبارتی دیگر، فقط تا زمانی که حداقل یک تابع تودرتو وجود داشته باشد که به آن رجوع کند وجود خواهد داشت.

در کد زیر، بعد از اینکه تابع تودرتو حذف شود، محیط لغوی ضمیمه شده به آن (و از این رو value) از حافظه پاک می‌شود:

function f() {
  let value = 123;

  return function() {
    alert(value);
  }
}

let g = f(); // در حافظه می‌ماند value وجود داشته باشد، متغیر g تا زمانی که تابع

g = null; // و حالا حافظه تمیز شده...

بهینه‌سازی در واقعیت

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

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

یک عارضه جانبی در موتور V8 (Chrome، Edge، Opera) این است که چنین متغیرهایی در دیباگ کردن غیر قابل دسترس می‌شوند.

سعی کنید که مثال پایین را در Chrome بعد از بازکردن Developer Tools اجرا کنید.

زمانی که متوقف می‌شود، در کنسول alert(value) را تایپ کنید.

function f() {
  let value = Math.random();

  function g() {
    debugger; // ؛ چنین متغیری وجود نداردalert(value) :در کنسول تایپ کنید
  }

  return g;
}

let g = f();
g();

همانطور که دیدید، چنین متغیری وجود نداشت! از لحاظ تئوری، باید قابل دسترس باشد، اما موتور با بهینه‌سازی آن را ازبین برد.

این موضوع ممکن است منجر به مشکل‌های جالب (شاید وقت‌گیر) در زمان دیباگ کردن شود. یک از آنها این است که امکان دارد ما یک متغیر که هم نام با متغیر مورد نظر ما است را ببینیم:

let value = "سوپرایز!";

function f() {
  let value = "value نزدیک ترین متغیر";

  function g() {
    debugger; // !؛ سوپرایزalert(value) در کنسول: تایپ کنید
  }

  return g;
}

let g = f();
g();

خوب است که درباره این خاصیت V8 بدانید. اگر در حال دیباگ کردن با Chrome/Edge/Opera باشید، به دیر یا زود با آن روبرو می‌شوید.

این یک باگ در debugger نیست بلکه یک ویژگی خاص V8 است. شاید زمانی آن را تغییر دهند. شما همیشه می‌توانید با اجرای مثال‌های این صفحه آن را بررسی کنید.

تمارین

اهمیت: 5

تابع sayHi از یک متغیر خارجی name استفاده می‌کند. زمانی که تابع اجرا می‌شود، کدام مقدار استفاده خواهد شد؟

let name = "John";

function sayHi() {
  alert("Hi, " + name);
}

name = "Pete";

sayHi(); // ؟"Pete" یا "John" :چه چیزی نمایش خواهد داد

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

پس سوال این است: آیا این تابع آخرین تغییرات را دریافت می‌کند؟

جواب: Pete.

یک تابع متغیرهای بیرونی را همانطور که هستند دریافت می‌کند و از آخرین مقدارها استفاده می‌کند.

مقدارهای قدیمی متغیر جایی ذخیره نمی‌شوند. زمانی که تابعی یک متغیر را می‌خواهد، مقدار کنونی آن را از محیط لغوی خودش یا محیط بیرونی دریافت می‌کند.

اهمیت: 5

تابع makeWorker پایین یک تابع دیگر می‌سازد و آن را برمی‌گرداند. تابع جدید می‌تواند در جایی دیگر فراخوانی شود.

این تابع به متغیرهای بیرون از جایی که ساخته شد دسترسی خواهد داشت یا جایی که فراخوانی می‌شود یا هر دو؟

function makeWorker() {
  let name = "Pete";

  return function() {
    alert(name);
  };
}

let name = "John";

// ساخت یک تابع
let work = makeWorker();

// فراخوانی آن
work(); // چه چیزی نمایش خواهد داد؟

کدام مقدار را نمایش خواهد داد؟ “Pete” یا “John”؟

جواب: Pete.

تابع work() در کد زیر name را از طریق مرجع محیط لغوی بیرونی، از جایی که منشا گرفته است دریافت می‌کند:

پس اینجا نتیجه "Pete" است.

اما اگر let name در makeWorker() وجود نداشت، همانطور که در زنجیره بالا هم می‌بینیم، سپس جستجو به بیرون می‌رفت و متغیر گلوبال را دریافت می‌کرد. در این صورت نتیجه "John" می‌شد.

اهمیت: 5

اینجا ما دو شمارنده می‌سازیم: counter و counter2 با استفاده از تابع یکسان makeCounter.

آیا آنها مستقل هستند؟ دومین شمارنده چه چیزی را نمایش خواهد داد؟ 0,1 یا 2,3 یا چیز دیگری؟

function makeCounter() {
  let count = 0;

  return function() {
    return count++;
  };
}

let counter = makeCounter();
let counter2 = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1

alert( counter2() ); // ?
alert( counter2() ); // ?

جواب: 0,1.

تابع‌های counter و counter2 با فراخوانی‌های متفاوتِ makeCounter ساخته شده‌اند.

پس آنها محیط‌های لغوی بیرونی مستقل دارند که هر کدام آنها count خودش را دارد.

اهمیت: 5

اینجا یک شیء شمارنده با کمک تابع سازنده ساخته شده است.

آیا کار می‌کند؟ چه چیزی را نمایش خواهد داد؟

function Counter() {
  let count = 0;

  this.up = function() {
    return ++count;
  };
  this.down = function() {
    return --count;
  };
}

let counter = new Counter();

alert( counter.up() ); // ?
alert( counter.up() ); // ?
alert( counter.down() ); // ?

مسلما به درستی کار خواهد کرد.

هر دو تابع تودرتو در محیط لغوی بیرونی یکسانی ساخته شده‌اند پس آنها به متغیر count یکسان دسترسی دارند:

function Counter() {
  let count = 0;

  this.up = function() {
    return ++count;
  };

  this.down = function() {
    return --count;
  };
}

let counter = new Counter();

alert( counter.up() ); // 1
alert( counter.up() ); // 2
alert( counter.down() ); // 1
اهمیت: 5

به کد زیر نگاه بیاندازید. نتیجه فراخوانی در خط اخر چه چیزی خواهد بود؟

let phrase = "Hello";

if (true) {
  let user = "John";

  function sayHi() {
    alert(`${phrase}, ${user}`);
  }
}

sayHi();

نتیجه یک ارور است.

تابع sayHi درون if تعریف شده است پس فقط درون آن وجود دارد. بیرون از آن sayHi نداریم.

اهمیت: 4

تابع sum را بنویسید که اینگونه کار می‌کند: sum(a)(b) = a+b.

بله دقیقا به این شکل، با استفاده از دو پرانتز (اشتباه تایپی نیست).

برای مثال:

sum(1)(2) = 3
sum(5)(-1) = 4

برای اینکه پرانتز دوم کار کند، پرانتز اول باید یک تابع برگرداند.

مانند این:

function sum(a) {

  return function(b) {
    return a + b; // را از محیط لغوی بیرونی می‌گیرد "a" متغیر
  };

}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1) ); // 4
اهمیت: 4

نتیجه این کد چه خواهد بود؟

let x = 1;

function func() {
  console.log(x); // ?

  let x = 2;
}

func();

پی‌نوشت: یک تله در این تکلیف وجود دارد. راه حل بدیهی نیست.

نتیجه: ارور.

سعی کنید آن را اجرا کنید:

let x = 1;

function func() {
  console.log(x); // ReferenceError: Cannot access 'x' before initialization
  let x = 2;
}

func();

در این مثال ما می‌توانیم تفاوت خاص بین یک متغیر «ناموجود» و «بدون مقداردهی اولیه» را بفهمیم.

همانطور که در مقاله محدوده متغیر، کلوژِر خواندید، زمانی که یک بلوک کد (یا یک تابع) شروع به اجرا شدن می‌کند، یک متغیر در حالت «بدون مقداردهی اولیه» قرار می‌گیرد. و تا زمانی که به دستور let برسد بدون مقداردهی اولیه می‌ماند.

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

کد بالا این را نشان می‌دهد.

function func() {
  // از ابتدای اجرای تابع برای موتور شناخته‌شده است x متغیر محلی
  // برسد، «بدون مقداردهی اولیه» (غیر قابل استفاده) است («منطقه مرگ») let اما تا به
  // از این رو با ارور مواجه می‌شویم

  console.log(x); // ReferenceError: Cannot access 'x' before initialization

  let x = 2;
}

این منطقه که یک متغیر به طور موقتی غیر قابل استفاده است (از ابتدای بلوک کد تا let) را بعضی اوقات «منطقه مرگ» می‌گویند.

اهمیت: 5

ما یک متد درون‌ساخت arr.filter(f) برای آرایه‌ها داریم. این متد تمام المان‌ها را از طریق f جداسازی می‌کند. اگر true برگرداند، سپس آن المان در آرایه حاصل برگردانده می‌شود.

یک مجموعه از جداسازی‌های «آماده استفاده» بسازید:

  • inBetween(a, b) – بین a و b یا برابر با آنها (شامل آنها هم می‌شود).
  • inArray([...]) – در آرایه داده شده.

طریقه استفاده از آنها باید اینگونه باشد:

  • arr.filter(inBetween(3,6)) – تنها مقدارهای بین 3 و 6 را انتخاب کند.
  • arr.filter(inArray([1,2,3])) – تنها المان‌هایی که با یکی از اعداد [1,2,3] برابر هستند را برگرداند.

برای مثال:

/* .. inArray و inBetween کد شما برای */
let arr = [1, 2, 3, 4, 5, 6, 7];

alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6

alert( arr.filter(inArray([1, 2, 10])) ); // 1,2

باز کردن یک sandbox همراه با تست‌ها.

Filter inBetween

function inBetween(a, b) {
  return function(x) {
    return x >= a && x <= b;
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert( arr.filter(inBetween(3, 6)) ); // 3,4,5,6

Filter inArray

function inArray(arr) {
  return function(x) {
    return arr.includes(x);
  };
}

let arr = [1, 2, 3, 4, 5, 6, 7];
alert( arr.filter(inArray([1, 2, 10])) ); // 1,2
function inArray(arr) {
  return x => arr.includes(x);
}

function inBetween(a, b) {
  return x => (x >= a && x <= b);
}

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

اهمیت: 5

ما یک ارایه از شیءها را برای مرتب‌سازی دریافت کرده‌ایم:

let users = [
  { name: "John", age: 20, surname: "Johnson" },
  { name: "Pete", age: 18, surname: "Peterson" },
  { name: "Ann", age: 19, surname: "Hathaway" }
];

راه معمولی برای انجام آن می‌تواند این باشد:

// (Ann، John، Pete) براساس اسم
users.sort((a, b) => a.name > b.name ? 1 : -1);

// (Pete، Ann، John) براساس سن
users.sort((a, b) => a.age > b.age ? 1 : -1);

آیا می‌توانیم آن را کوتاه‌تر کنیم، مثلا اینگونه؟

users.sort(byField('name'));
users.sort(byField('age'));

پس به جای اینکه یک تابع بنویسیم، فقط byField(fieldName) را قرار می‌دهیم.

تابع byField را بنویسید که می‌تواند برای این کار استفاده شود.

باز کردن یک sandbox همراه با تست‌ها.

function byField(fieldName){
  return (a, b) => a[fieldName] > b[fieldName] ? 1 : -1;
}

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

اهمیت: 5

کد پایین یک ارایه از shooters(تیراندازها) را می‌سازد.

هر تابع باید عدد آن را نمایش دهد. اما یک چیز اشتباه است…

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
    let shooter = function() { // shooter ساخت یک تابع
      alert( i ); // که باید عدد خود را نمایش دهد
    };
    shooters.push(shooter); // و آن را به ارایه اضافه کند
    i++;
  }

  // ها را برگرداندshooter و آرایه‌ای از...
  return shooters;
}

let army = makeArmy();

// ها به جای عدد خود یعنی ...3 ،2 ،1 ،0 عدد 10 را نمایش می‌دهندshooter تمام
army[0](); // عدد 10 از تیرانداز 0
army[1](); // عدد 10 از تیرانداز 1
army[2](); // عدد 10 و همینطور ادامه می‌یابد

چرا تمام shooterها مقدار یکسان را نمایش می‌دهند؟

کد را درست کنید تا همانطور که می‌خواهیم کار کند.

باز کردن یک sandbox همراه با تست‌ها.

بیایید بررسی کنیم که درون makeArmy دقیقا چه چیزی پیش می‌آید و راه‌حل واضح می‌شود.

  1. یک آرایه خالی shooters می‌سازد:

    let shooters = [];
  2. آن را با استفاده از shooters.push(function) درون حلقه از تابع‌ها پر می‌کند.

    هر المان تابع است پس نتیجه آرایه اینگونه بنظر می‌رسد:

    shooters = [
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); }
    ];
  3. آرایه از تابع برگردانده می‌شود.

    سپس بعدها، فراخوانی هر عددی، برای مثال army[5]() المان army[5] را از آرایه دریافت می‌کند (که یک تابع است) و آن را فراخوانی می‌کند.

    حالا چرا تمام تابع‌ها مقدار یکسان 10 را برمی‌گردانند؟

    به دلیل اینکه هیچ متغیر محلی i درون تابع‌های shooter وجود ندارد. زمانی که چنین تابعی صدا زده می‌شود، i را از محیط لغوی بیرونی خود دریافت می‌کند.

    سپس مقدار i چه چیزی خواهد بود؟

    اگر ما به کد منبع نگاه کنیم:

    function makeArmy() {
      ...
      let i = 0;
      while (i < 10) {
        let shooter = function() { // shooter تابع
          alert( i ); // باید عدد خودش را نشان دهد
        };
        shooters.push(shooter); // اضافه کردن تابع به آرایه
        i++;
      }
      ...
    }

    می‌توانیم ببینیم که تمام تابع‌های shooter در محیط لغوی تابع makeArmy() ساخته شده‌اند. اما زمانی که army[5]() فراخوانی می‌شود، makeArmy از قبل کار خودش را انجام داده و مقدار نهایی i برابر با 10 است (while در i=10 می‌ایستد).

    به عنوان نتیجه، تمام تابع‌های shooter مقدار یکسان را از محیط لغوی بیرونی دریافت می‌کنند و به همین دلیل، مقدار آخر i=10 است.

    همانطور که در بالا می‌بینید، در هر تکرار از بلوک while {...}، یک محیط لغوی جدید ساخته می‌شود. پس برای درست کردن این، ما مقدار i را در یک متغیر درون بلوک while {...} کپی می‌کنیم، مانند این:

    function makeArmy() {
      let shooters = [];
    
      let i = 0;
      while (i < 10) {
          let j = i;
          let shooter = function() { // shooter تابع
            alert( j ); // باید عدد خودش را نشان دهد
          };
        shooters.push(shooter);
        i++;
      }
    
      return shooters;
    }
    
    let army = makeArmy();
    
    // حالا کد به درستی کار می‌کند
    army[0](); // 0
    army[5](); // 5

    اینجا let j = i یک متغیر «محلی در هر تکرار» j را تعریف می‌کند و i را در آن کپی می‌کند. مقدارهای اولیه «با مقدار خود» کپی می‌شوند پس در واقع ما یک کپی مستقل از i داریم که به تکرار کنونی حلقه تعلق دارد.

    تیراندازها به درستی کار می‌کنند چون حالا مقدار i کمی نزدیک‌تر شده است. درون محیط لغوی makeArmy() نیست بلکه در محیط لغوی متناظر با تکرار کنونی حلقه وجود دارد:

    اگر ما در ابتدا از for استفاده می‌کردیم، از چنین مشکلی جلوگیری میشد، مثل این:

    function makeArmy() {
    
      let shooters = [];
    
      for(let i = 0; i < 10; i++) {
        let shooter = function() { // shooter تابع
          alert( i ); // باید عدد خودش را نشان دهد
        };
        shooters.push(shooter);
      }
    
      return shooters;
    }
    
    let army = makeArmy();
    
    army[0](); // 0
    army[5](); // 5

    این کد اساسا یکسان است چون for در هر تکرار یک محیط لغوی جدید را ایجاد می‌کند که متغیر i خودش را دارد. پس shooter که در هر تکرار ایجاد شده است به i خودش از همان تکرار رجوع می‌کند.

حالا همانطور که شما زحمت زیادی را برای خواندن این راه‌حل کشیدید، دستور العمل نهایی بسیار ساده است – فقط از for استفاده کنید، شاید بپرسید آیا ارزش آن را داشت؟

خب اگر شما می‌توانستید به راحتی سوال را جواب دهید، راه‌حل را نمی‌خواندید. پس خوشبختانه این تکلیف باید به شما برای فهمیدن این نکات کمی کمک کرده باشد.

علاوه بر آن، مواردی وجود دارد که کسی while را به for ترجیح دهد یا سناریوهای دیگر در میان باشند که چنین مشکلاتی واقعا پیش بیایند.

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

نقشه آموزش

نظرات

قبل از نظر دادن این را بخوانید…
  • اگر پیشنهادی برای بهبود ترجمه دارید - لطفا یک ایشوی گیت‌هاب یا یک پول‌ریکوئست به جای کامنت‌گذاشتن باز کنید.
  • اگر چیزی را در مقاله متوجه نمی‌شوید – به دقت توضیح دهید.
  • برای قراردادن یک خط از کد، از تگ <code> استفاده کنید، برای چندین خط – کد را درون تگ <pre> قرار دهید، برای بیش از ده خط کد – از یک جعبهٔ شنی استفاده کنید. (plnkr، jsbin، codepen…)