مهدی عادلی فر
بنیانگذار توسینسو و برنامه نویس

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

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

دوره های شبکه، برنامه نویسی، مجازی سازی، امنیت، نفوذ و ... با برترین های ایران

 scope

کلمه scope در این مطلب به  معنی محدوده دسترسی به یک داده یا متغیر اشاره می کند. کد زیر را در نظر بگیرید:

function init() {
  var name = 'Tosinso'; // name is a local variable created by init
  function displayName() {
    // displayName() is the inner function, a closure
    console.log(name); // use variable declared in the parent function
  }
  displayName();
}
init();

در داخل تابع init کی متغیر محلی با نام name و یک تابع با نام displayName ساخته می شود. تابع displayName یک تابع داخلی است که داخل تابع init تعریف شده است و فقط در داخل این تابع قابل دسترسی است. دقت داشته باشید که تابع displayName در داخل خودش هیچ متغیری ندارد ولی چون یک تابع داخلی است به متغیر های تابع خارجی دسترسی دارد پس به متغیر name که در init تعریف شده است دسترسی دارد و اگر کد بالا اجرا شود می بینیم که بدون مشکل کار خواهد کرد و نتیجه این که توابع داخلی به متغیرها و اعضای توابع خارجی دسترسی دارند. 
مثال قبل مثالی از scope تابعی بود زیرا که دسترسی با توجه به محل تعریف تابع در تابع دیگر تعریف می شد. تا قبل از ES6 در جاوااسکریپت فقط دو نوع scope وجود داشت یک scope تابعی و دیگری scope سراسری. متغیر هایی که با کلمه کلیدی var تعریف می شدند با توجه به این که کجا تعریف شده اند  یا متغیر تابعی بودند و یا سراسری. یعنی اگر متغیری داخل یک تابع با var تعریف می شد تابعی به حساب می آمد و اگر خارج از تمام توابع بود سراسری بود. ولی به این نکته دقت داشته باشید که تعریف داخل یک بلوک کد هیچ تاثیری روی این که متغیر تابعی است یا سراسری تاثیری نداشت مثل کد زیر

if (Math.random() > 0.5) {
  var x = 1;
} else {
  var x = 2;
}
console.log(x);

اگر شما با زبانهایی مثل خانواده c و یا جاوا آشنا باشید احتمالا می گویید که کد بالا باید خطا بدهد زیرا که متغیر x داخل بلوک if و else تعریف شده است ولی در جاوااسکریپت این گونه نیست و بلوک ها تاثیری در scope متغیر ندارند. زیرا که متغیر x با کلمه کلیدی var تعریف شده است. اما از ES6 به بعد دو کلمه کلیدی جدید let, const معرفی شده اند که به بلوک ها حساس هستند. مثل کد زیر

if (Math.random() > 0.5) {
  const x = 1;
} else {
  const x = 2;
}
console.log(x); // ReferenceError: x is not defined

بررسی Closure

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

کد زیر را در نظر بگیرید:

function makeFunc() {
  const name = 'Tosinso';
  function displayName() {
    console.log(name);
  }
  return displayName;
}
const myFunc = makeFunc();
myFunc();

اگر کد بالا را اجرا کنید متوجه می شوید که دقیقا همان کاری را انجام می دهد که تابع init که در بالا تعریف کرده ایم انجام می دهد اما تفاوت جالبی که دارد تابع displayName است. زیرا که خود این تابع داخلی قبل از این که اجرا شود return می شود. شاید در نگاه اول نامفهوم باشد که چرا این برنامه اجرا می شود. چون که در برخی زبان های برنامه نویسی وقتی که اجرای یک تابع به پایان می رسد متغیرهای محلی آن تابع نیز از بین می روند. بنابراین شاید انتظار داشته باشید که بعد از این که تابع makeFunc اجرا شد متغیر name از بین برود ولی با توجه به این که کد بالا بدون مشکل اجرا می شود به این نتیجه می رسیم که در جاوااسکریپت به این صورت نیست و متغیر مورد نظر از بین نمی رود.
دلیل آن هم closure است. به عبارت دیگر closure ترکیبی از توابع و محیطی است که تابع در آن تعریف شده است. در مثال بالا محیط تعریف تابع، متغیرهای محلی هستند که هنگام تعریف تابع وجود داشته اند. بنابراین چون closure از بین نرفته است پس متغیر آن closure نیز هنوز وجود دارد. در این مثال  تابع myFunc یک رفرنس به نمونه ای از تابع displayName است که در زمان اجرای myFunc ساخته شده است. نمونه تابع displayName یک رفرنس به محیط خودش در اختیار دارد و در آن محیط متغیر name وجود دارد. به همین خاطر وقتی که تابع myFunc اجرا می شود متغیر name باقی می ماند.
کد زیر یک مثال جالب از closure را بیان می کند

function makeAdder(x) {
  return function (y) {
    return x + y;
  };
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12

در این مثال ما یک تابع با نام makeAdder تعریف کرده ایم که یک ورودی به نام x را می گیرد و یک تابع دیگر return می کند که آن تابع نیز یک ورودی به نام y می گیرد و در نتیجه جمع x , y را به ما می دهد. در حقیقت makeAdder یک function factory است (کارخانه تابع) زیرا که یک تابعی می سازد که به عدد داده شده یک مقدار مشخص اضافه می کند در مثال بالا با function factory دو عدد تابع ساخته ایم که یکی مقدار 5 و دیگری مقدار 10 را به ورودی اضافه می کنند و نتیجه را برمی گردانند. هر دو تابع add5 و add10 توابع closure هستند که در یکی x مقدار 5 و در دیگری 10 است.

شبیه سازی توابع خصوصی

زبان هایی مانند جاوا یا سی شارپ این امکان را دارند که بتوان داخل یک کلاس یا شی تابع خصوصی تعریف کرد. توابع خصوصی توابعی هستند که فقط توسط توابع داخل همان کلاس قابل دسترس هستند و از بیرون از کلاس نمی توان به آنها دسترسی داشت. در جاوااسکریپت تا قبل از کلاس ها این امکان وجود نداشت. ولی می توان با استفاده از closure این قابلیت را شبیه سازی کرد. در کد زیر مثالی برای این قضیه آورده شده است.

const counter = (function () {
  let privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }

  return {
    increment() {
      changeBy(1);
    },

    decrement() {
      changeBy(-1);
    },

    value() {
      return privateCounter;
    },
  };
})();
console.log(counter.value()); // 0.
counter.increment();
counter.increment();
console.log(counter.value()); // 2.
counter.decrement();
console.log(counter.value()); // 1.

در مثال بالا هر closure محیط مخصوص به خود را دارد. چیزی که در این مثال جالب است این است که یک محیط وجود دارد که بین ۳ تابع increment و decrementو value به اشتراک گذاشته شده است. در مثال بالا چون محیط مورد نظر ما در یک تابع بی نام تعریف شده است به محض ساخته شدن این تابع بی نام اجرا می شود. این محیط ما دارای ۲ عضو است یکی متغیر خصوصی privateCounter و دیگری تابع changeBy و شما نمی توانید از خارج از تابع بی نام به هیچکدام از این اعضا به صورت مستقیم دسترسی پیدا کنید ولی می توانید با استفاده از ۳ تابع عمومی که return می شوند به این محیط و اعضای خصوصی دسترسی داشته باشید. این ۳ تابع عمومی closure هایی هستند که یک محیط را با هم در اشتراک می گذارند.
همچنین به مثال زیر هم دقت کنید

const makeCounter = function () {
  let privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment() {
      changeBy(1);
    },

    decrement() {
      changeBy(-1);
    },

    value() {
      return privateCounter;
    },
  };
};

const counter1 = makeCounter();
const counter2 = makeCounter();

console.log(counter1.value()); // 0.

counter1.increment();
counter1.increment();
console.log(counter1.value()); // 2.

counter1.decrement();
console.log(counter1.value()); // 1.
console.log(counter2.value()); // 0.

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

زنجیره closure ها

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

// global scope
const e = 10;
function sum(a) {
  return function (b) {
    return function (c) {
      // outer functions scope
      return function (d) {
        // local scope
        return a + b + c + d + e;
      };
    };
  };
}

console.log(sum(1)(2)(3)(4)); // log 20



که کد بالا را می توان با توابع نامدار هم نوشت

const e = 10;
function sum(a) {
  return function sum2(b) {
    return function sum3(c) {
      // outer functions scope
      return function sum4(d) {
        // local scope
        return a + b + c + d + e;
      };
    };
  };
}

const sum2 = sum(1);
const sum3 = sum2(2);
const sum4 = sum3(3);
const result = sum4(4);
console.log(result); //log 20

استفاده از closure ها در سطح ماژول

اگر برنامه شما چند فایل و ماژول بندی شده باشد می توان از closure در آن به صورت زیر استفاده کرد. 

let x = 5;
export const getX = () => x;
export const setX = (val) => {
  x = val;
}

کد بالا یک جفت تابع getter-setter را بر می گرداند که می توان به کمک آنها خارج از ماژول به مقدار x دسترسی پیدا کرد و مقدار آن را تغییر داد. مانند کد زیر

import { getX, setX } from "./myModule.js";

console.log(getX()); // 5
setX(6);
console.log(getX()); // 6

حتی می توان setter , getter را هرکدام را داخل یک ماژول جدا تعریف کرد مانند کد زیر

// myModule.js
export let x = 1;
export const setX = (val) => {
  x = val;
}

// closureCreator.js
import { x } from "./myModule.js";

export const getX = () => x; // Close over an imported live binding


import { getX } from "./closureCreator.js";
import { setX } from "./myModule.js";

console.log(getX()); // 1
setX(2);
console.log(getX()); // 2


تعریف closure در داخل حلقه ها(یک اشتباه رایج)

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

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>

unction showHelp(help) {
  document.getElementById('help').textContent = help;
}

function setupHelp() {
  var helpText = [
    { id: 'email', help: 'Your e-mail address' },
    { id: 'name', help: 'Your full name' },
    { id: 'age', help: 'Your age (you must be over 16)' },
  ];

  for (var i = 0; i < helpText.length; i++) {
    // Culprit is the use of `var` on this line
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function () {
      showHelp(item.help);
    };
  }
}

setupHelp();


در کد بالا یک آرایه برای ذخیره راهنمایی ها داریم که سه عضو دارد که متن و آیدی فیلد ها در داخل آن ذخیره شده است. همچنین داخل حلقه رویداد onfocus هرکدام از فیلد ها را مقدار دهی کرده ایم. اما  اگر کد بالا را اجرا کنید متوجه می شوید که برنامه به درستی کار نخواهد  کرد و همیشه آخرین متن را در بخش help نشان می دهد. 
دلیل این مشکل این است که توابعی که به مقدار onfocus داده می شوند closure هستند. یعنی ترکیبی از تابع و متغیر های محیط تابع setupHelp هستند. در این حلقه ۳ عدد closure ساخته شده است اما در این سه closure متغیر محیط که همان item است به اشتراک گذاشته شده است. این به خاطر این است که متغیر item با استفاده از var تعریف شده است. وقتی که با var یک متغیر تعریف شده است خارج از آن بلوک هم در دسترس است و تغییر در یک closure باعث می شود که متغیر item چون اشتراکی است در همه توابع عوض شود. 
برای حل مشکل کد بالا یک راه حل این است که از closure های بیشتری استفاده کنیم یعنی از function factory که قبلا توضیح دادیم استفاده کنیم مانند کد زیر

function showHelp(help) {
  document.getElementById('help').textContent = help;
}

function makeHelpCallback(help) {
  return function () {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
    { id: 'email', help: 'Your e-mail address' },
    { id: 'name', help: 'Your full name' },
    { id: 'age', help: 'Your age (you must be over 16)' },
  ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();

اگر این کد را اجرا کنید متوجه خواهید شد که به درستی کار می کند زیرا با استفاده از makeHelpCallback برای هرکدام از closure ها یک محیط جدا درست می کند و مشکل حل می شود.
کد بالا را می توان به شکل closure بی نام هم نوشت.

function showHelp(help) {
  document.getElementById('help').textContent = help;
}

function setupHelp() {
  var helpText = [
    { id: 'email', help: 'Your e-mail address' },
    { id: 'name', help: 'Your full name' },
    { id: 'age', help: 'Your age (you must be over 16)' },
  ];

  for (var i = 0; i < helpText.length; i++) {
    (function () {
      var item = helpText[i];
      document.getElementById(item.id).onfocus = function () {
        showHelp(item.help);
      };
    })(); // Immediate event listener attachment with the current value of item (preserved until iteration).
  }
}

setupHelp();

در کد بالا می بینید که هنگام مقداردهی به onfocus از یک تابع بی نام استفاده شده است. 
حال اگر نمی خواهید از closure اضافه استفاده کنید می تواند به جای var از کلمات کلیدی let , const استفاده کنید مانند کد زیر

function showHelp(help) {
  document.getElementById('help').textContent = help;
}

function setupHelp() {
  const helpText = [
    { id: 'email', help: 'Your e-mail address' },
    { id: 'name', help: 'Your full name' },
    { id: 'age', help: 'Your age (you must be over 16)' },
  ];

  for (let i = 0; i < helpText.length; i++) {
    const item = helpText[i];
    document.getElementById(item.id).onfocus = () => {
      showHelp(item.help);
    };
  }
}

setupHelp();

در مثال بالا از const استفاده شده است که باعث می شود که متغیر ها block-scope شوند یعنی خارج از بلوک مورد نظر در دسترس نباشند. 
راه حل دیگری که برای مساله بالا وجود دارد این است که در حلقه به جای استفاده از for از forEachاده کنیم به شکل زیر:

function showHelp(help) {
  document.getElementById('help').textContent = help;
}

function setupHelp() {
  var helpText = [
    { id: 'email', help: 'Your e-mail address' },
    { id: 'name', help: 'Your full name' },
    { id: 'age', help: 'Your age (you must be over 16)' },
  ];

  helpText.forEach(function (text) {
    document.getElementById(text.id).onfocus = function () {
      showHelp(text.help);
    };
  });
}

setupHelp();

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


مهدی عادلی فر
مهدی عادلی فر

بنیانگذار توسینسو و برنامه نویس

مهدی عادلی، بنیان گذار TOSINSO. کارشناس ارشد نرم افزار کامپیوتر از دانشگاه صنعتی امیرکبیر و #C و جاوا و اندروید کار می کنم. در زمینه های موبایل و وب و ویندوز فعالیت دارم و به طراحی نرم افزار و اصول مهندسی نرم افزار علاقه مندم.

نظرات