Scope و Closure در جاوااسکریپت

scope و closure دو مفهوم مهم و کاربردی در جاوااسکریپت است که به موضوع دسترسی به متغیرها، توابع و اشیا براساس موقعیتی که در برنامه دارند اشاره می‌کنند.

ممکن است هنگام کد نویسی به زبان جاوااسکریپت، به کدی مشابه کد زیر برخورد کرده باشید یا خودتان آن را نوشته باشید:

function sayWord(word) {
  return () => console.log(word);
}

const sayHello = sayWord("hello");

sayHello(); // "hello"

دو نکته جالب در این قطعه کد وجود دارد. اول این که ما می‌توانیم به word، که توسط تابع sayWord بازگردانده می‌شود دسترسی داشته باشیم. دوم، زمانی که تابع sayHello را فراخوانی می‌کنیم، به مقدار word دسترسی داریم – یعنی حتی اگر به word دسترسی مستقیم نداشته باشیم، تابع sayHello را فراخوانی می‌کنیم. در این مقاله قصد داریم تا بطور کامل در مورد scope و closure، که باعث این رفتار می‌شوند صحبت می‌کنیم.

معرفی scope در جاوااسکریپت

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

متغیرهای جاوااسکریپت دارای lexically scoped هستند، به این معنی که می‌توانیم scope متغیر را از جایی که در سورس کد اعلام شده است تعیین کنیم. (البته این موضوع کاملا درست نیست، متغیرهای var این چنین نیستند. در ادامه در مورد آن صحبت خواهیم کرد)

مثال زیر را در نظر بگیرید:

if (true) {
  const foo = "foo";
  console.log(foo); // "foo"
}

دستور if با استفاده از آکولاد، یک محدوده‌ی بلاک کد(block-scope) را تعریف می‌کند. بر این اساس می‌گوییم foo برای دستور if دارای block-scope است. به این معنی که، foo فقط از داخل آن بلاک کد قابل دسترسی می‌باشد.

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

if (true) {
  const foo = "foo";
  console.log(foo); // "foo"
}

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

عبارات دیگری که دارای بلاک کد هستند، مانند حلقه‌های for و while نیز یک محدوده برای متغیرهای محدوده بلاک کد ایجاد می‌کنند. به عنوان مثال، foo در محدوده‌ی بدنه تابع زیر قرار گرفته است:

function sayFoo() {
  const foo = "foo";
  console.log(foo);
}

sayFoo(); // "foo"

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

scopeها و توابع تودرتو

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

کد زیر را در نظر بگیرید که چندین دستور با بلاک کد را در خود جای داده است:

if (true) {
  const foo = "foo";
  console.log(foo); // "foo"

  if (true) {
    const bar = "bar";
    console.log(foo); // "foo"

    if (true) {
      console.log(foo, bar); // "foo bar"
    }
  }
}

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

function foo(bar) {
  function baz() {
    console.log(bar);
  }

  baz();
}

foo("bar"); // "bar"

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

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

محدوده let، const و var در جاوااسکریپت

ما می‌توانیم با استفاده از عبارات let، const و var متغیرها را تعریف کنیم. محدوده بلاک کد برای let و const، همانطور که در بالا توضیح داده شد کار می‌کند. با این حال، var رفتار متفاوتی دارد.

let و const

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

if (true) {
  const foo = "foo";
  console.log(foo); // "foo"
}

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

var

متغیرهای تعریف شده با var به نزدیک‌ترین تابع یا scope سراسری(global scope)، که در ادامه در مورد آن صحبت خواهیم کرد محدود می‌شوند. آن‌ها محدوده بلاک کد ندارند:

function foo() {
  if (true) {
    var foo = "foo";
  }
  console.log(foo);
}

foo(); // "foo"

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

اگر به نحوه رفتار var در مثال بالا علاقه‌مند هستید، این مقاله را بررسی کنید.

scope سراسری و ماژول در جاوااسکریپت

علاوه بر محدوده‌های بلاک کد، محدوده متغیرها را می‌توان به حالت سراسری و ماژول نیز تغییر داد.

در یک مرورگر وب، scope سراسری یک سطح بالا از اسکریپت است. این ریشه درخت scope است که قبلا توضیح دادیم، و شامل تمام scopeهای دیگر است. بنابراین تعریف یک متغیر در scope سراسری، باعث می‌شود تا آن در هر محدوده‌ای قابل دسترسی باشد:

<script>
  const foo = "foo";
</script>
<script>
  console.log(foo); // "foo"
    
  function bar() {
    if (true) {
      console.log(foo);
    }
  }

  bar(); // "foo"
</script>

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

<script type="module">
  const foo = "foo";
</script>
<script>
  console.log(foo); // Uncaught ReferenceError: foo is not defined
</script>

closureها در جاوااسکریپت

اکنون که مفهوم scope را درک کردیم، به مثالی که در مقدمه دیدیم برمی‌گردیم:

function sayWord(word) {
  return () => console.log(word);
}

const sayHello = sayWord("hello");

sayHello(); // "hello"

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

  1. تابع بازگشتی از sayWord می‌تواند به پارامتر word دسترسی داشته باشد.
  2. هنگامی که sayHello خارج از محدوده word فراخوانی می‌شود، تابع بازگشتی مقدار word را حفظ می‌کند.

اولین نکته را می‌توان با lexical scope توضیح داد، تابع بازگشتی می‌تواند به word دسترسی داشته باشد زیرا، در محدوده بیرونی آن قرار دارد.

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

closureها مراجع متغیرها را حفظ می‌کنند، این کار به توابع اجازه می‌دهد به متغیرهای خارج از محدوده خود دسترسی پیدا کنند. در واقع آن‌ها تابع و متغیرها را در محیط آن “محصور” می‌کنند.

مثال‌هایی از closureها در جاوااسکریپت

احتمالاً شما بدون این که از این موضوع اطلاع داشته باشید اغلب با closure مواجه شده‌اید و از آن استفاده کرده‌اید. در ادامه چند روش دیگر برای استفاده از closure را بررسی می‌کنیم.

توابع callback

معمولاً یک تابع callback به متغیری که خارج از خودش تعریف شده ارجاع می‌دهد. به عنوان مثال:

function getCarsByMake(make) {
  return cars.filter(x => x.make === make);
}

make به دلیل lexical scope در callback موجود است و زمانی که یک تابع ناشناس به دلیل closure توسط فیلتر فراخوانی می شود، مقدار make باقی می‌ماند.

ذخیره‌سازی state

ما می‌توانیم از closure برای بازگرداندن آبجکت‌ها از توابعی که حالت را ذخیره می‌کنند، استفاده کنیم. تابع makePerson زیر را در نظر بگیرید که آبجکتی که می‌تواند یک نام را ذخیره کند و تغییر دهد را برمی‌گرداند:

function makePerson(name) {
  let _name = name;

  return {
    setName: (newName) => (_name = newName),
    getName: () => _name,
  };
}

const me = makePerson("Zach");
console.log(me.getName()); // "Zach"

me.setName("Zach Snoek");
console.log(me.getName()); // "Zach Snoek"

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

private methodها

اگر با برنامه نویسی شی‌گرا آشنا هستید ممکن است متوجه این موضوع شوید که مثال قبلی شباهت زیادی به کلاسی دارد که حالت private را ذخیره می‌کند و متدهای setter و getter عمومی را نمایش می‌دهد. ما می‌توانیم این شی‌گرایی را موازی با استفاده از closureها برای پیاده‌سازی روش‌های private گسترش دهیم:

function makePerson(name) {
  let _name = name;

  function privateSetName(newName) {
    _name = newName;
  }

  return {
    setName: (newName) => privateSetName(newName),
    getName: () => _name,
  };
}

privateSetName مستقیماً در دسترس مصرف‌کنندگان نیست ولی می‌تواند از طریق closure، به متغیر حالت خصوصی name_ دسترسی داشته باشد.

React event handlerها

در نهایت، closureها در React event handlerها رایج هستند. کامپوننت Counter زیر از مستندات React مورد بررسی قرار گرفته است و تغییرات روی آن اعمال شده است:

function Counter({ initialCount }) {
  const [count, setCount] = React.useState(initialCount);

  return (
    <>
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount((prevCount) => prevCount - 1)}>
        -
      </button>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>
        +
      </button>
      <button onClick={() => alert(count)}>Show count</button>
    </>
  );
}

function App() {
  return <Counter initialCount={0} />;

Closureها در این مثال امکانات زیر را فراهم می‌کنند:

  • handler کلیک باتن‌های reset، decrement و increment برای دسترسی به setCount
  • باتن reset برای دسترسی به initialCount از props Counter
  • و باتن “Show count ” برای نمایش وضعیت شمارش.

closureها در سایر بخش‌های React، مانند propها و هوک‌ها نیز دارای اهمیت زیادی هستند.

جمع‌بندی

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

closure تابعی است که بین ارجاعاتی به متغیرهای موجود در scope بیرونی آن قرار دارد. closureها به توابع اجازه می‌دهند تا ارتباطات خود را با متغیرهای بیرونی، حتی خارج از scope متغیرها نیز حفظ کنند.

closureها کاربردهای زیادی دارد، از ایجاد ساختارهای کلاس مانند که متدهای private را ذخیره و اجرا می‌کنند تا ارسال callback به event handlerها.

 

منبع

دیدگاه‌ها:

افزودن دیدگاه جدید