Closures

Переклад цієї статті ще не завершено. Будь ласка, допоможіть перекласти цю статтю з англійської мови

Замикання - це функції, що посилаються на незалежні (вільні) змінні (змінні, які використовуються локально, але визначені в обмеженій області видимості). Іншими словами, ці функції "пам'ятають" середовище, в якому вони були створені.

Лексичне середовище

Розглянемо наступне:

function init() {
  var name = "Mozilla"; // name - це локальна змінна, створена в функції init
  function displayName() { // displayName() - це внутрішня функція, замикання
    alert(name); // використовує змінну, оголошену в батьківській функції    
  }
  displayName();    
}
init();

init() створює локальну змінну name та функцію displayName(). displayName() є вкладеною функцією, що оголошена всередині init() та є доступною тільки в тілі цієї функції. displayName() не має локальних змінних, проте вкладені функції мають доступ до змінних зовнішніх функцій саме тому displayName() може використовувати імена змінних, оголошених в батьківській функції init().

Запустіть цей код і побачите, що alert() всередині  функції displayName() успішно відображає змінну name, оголошену в батьківській функції. Це є прикладом лексичного середовища (lexical environment), яке описує, яким чином парсер звертається до змінних, коли функції вкладені. Слово лексичне  посилається на факт, що лексичне середовище використовує розташування, де в коді  була створена змінна задля визначення середовища, де ця змінна доступна. Вкладені функції мають доступ до змінних, оголошених у їх зовнішній області.

Замикання

Тепер розглянемо наступний приклад:

function makeFunc() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

Результат запуску цього коду буде таким самим, як і в попередньому прикладі функції init(): рядок  "Mozilla" буде відображений в спливаючому повідомленні alert(). Цікавою відмінністю є те, що внутрішня функція displayName() буде повернена з зовнішньої функції перш ніж вона буде виконана. 

Той факт, що цей код досі працює, може здаватись неочевидним. В деяких мовах програмування локальні змінні існують лише протягом часу виконання функції. Як тільки makeFunc() завершила своє виконання, слід очікувати, що змінна name не буде більше існувати. Однак у JavaScript цей підхід інакший, оскільки даний код працює, як і очікувалось.

Це відбувається тому що ці функції в JavaScript утворюють замикання. Замикання це комбінація функції і лексичного середовища (або просто середовища), всередині якого функція була оголошена. Середовище складається з будь-яких локальних змінних, які були в області видимості в той час, коли замикання було створене. В цьому випадку, myFunc є посиланням на екземпляр функції displayName створений, коли makeFunc була виконана. Екземпляр displayName включає посилання на власне лексичне середовище в середині якого існує змінна name. З цієї причини, коли myFunc викликається, змінна name залишається доступною для використання і "Mozilla" передається до alert. ​​​​​​

Ось трохи цікавіший приклад — a makeAdder function:

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

У цьому прикладі ми визначили функцію makeAdder(x), яка приймає єдиний аргумент x і повертає нову функцію. Функція, яку вона повертає, приймає єдиний аргумент y, і повертає суму  x і y.

По суті, makeAdder це фабрика функцій — вона створює функції, які можуть додавати певне значення до свого аргументу. У наведеному вище прикладі ми використовуємо нашу фабричну функцію для створення двох нових функцій — одна додає 5 до свого аргументу, і інша додає 10.

add5 і add10 є прикладами замикання. Вони поділяють одне визначення тіла функції, але зберігають різні лексичні середовища. В лексичному середовищі add5  x - це 5, а в лексичному середовищі add10 x- це 10.

Замикання на практиці

Замикання корисні, оскільки вони дозволяють зв'язати деякі дані (лексичне середовище) з функцією, яка працює з цими даними. Це має очевидні паралелі з об'єктно-орієнтованим програмуванням, де об'єкти дозволяють нам зв'язати деякі дані (властивості об'єкта) з одним або декількома методами.

Отже, замикання можна використовувати всюди, де  зазвичай використовується об'єкт з одним єдиним методом. 

Ситуації, в яких ви можливо захочете зробити це, особливо поширені в web-розробці. Велика частина front-end коду, написана на JavaScript, основана на обробці подій. Ви визначаєте деяку поведінку, а потім прикріплюєте її до події, яка запускається користувачем (наприклад, клік або натискання клавіші). Код, як правило, прив'язується до події як зворотній виклик (callback) - функція, яка виконується у відповідь на виникнення події.

Наприклад припустимо, що ми хочемо додати кілька кнопок на сторінку, які змінюватимуть розмір тексту. Один із способів зробити це - вказати font-size елементу body у пікселях, а потім встановити розмір інших елементів на сторінці (таких як заголовки) за допомогою відносних одиниць виміру em:

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.2em;
}

Такі інтерактивні кнопки розміру тексту можуть змінювати властивість font-size елемента body, і налаштування будуть підхоплені іншими елементами на сторінці завдяки відносним одиницям.

Ось JavaScript:

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

 Тепер size12, size14, і size16 є функціями, які будуть змінювати розмір тексту елемента body відповідно до 12, 14 та 16 пікселів. Ми можемо прив'язати їх до кнопок таким чином:

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a> 

Емуляція приватних методів з замиканнями

В таких мовах як Java є можливість об'являти методи приватними, що означає їх здатність бути викликаними лише іншими методами того ж класу.  

JavaScript не має власного способу зробити це, але є можливість емуляції приватних методів використовуючи замикання. Приватні методи корисні не лише для обмеження доступу до коду: вони також забезпечують потужний спосіб управління глобальним простором імен, зберігаючи несуттєві методи від захаращення публічного інтерфейсу до вашого коду.

Ось як визначити деякі загальнодоступні функції, які можуть отримати доступ до приватних функцій та змінних, використовуючи замикання, також відомі як модель модуля (module pattern).

var counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };   
})();

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1

Тут багато чого відбувається. В попередніх пригладах кожне замикання мало своє власне оточення. Тут, однак, існує одне лексичне середовище, яке поділяють три функції: counter.increment, counter.decrement, and counter.value.

Спільне лексичне середовище створюється в тілі анонімної функції, яка викликається одразу після створення. Навколишнє середовище містить два приватні складові: змінну privateCounter і функцію  changeBy. Жодна із цих приватних складових не може бути доступна безпосередньо за межами анонімної функції. Натомість до них мають бути доступні три загальнодоступні функції, які повертаються з анонімної обгортки.

Ці три  функції - це замикання, які мають одне і те ж середовище. Завдяки лексичному оточенню JavaScript, кожна з них має доступ до змінної privateCounter та функції changeBy.

Ми визначаємо анонімну функцію, яка створює лічильник, а потім ми викликаємо її негайно і присвоюємо результат змінній лічильника.Ми можемо зберігати цю функцію в окремій змінній makeCounter і використовувати її для створення декількох лічильників

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

var counter1 = makeCounter();
var counter2 = makeCounter();
alert(counter1.value()); /* Alerts 0 */
counter1.increment();
counter1.increment();
alert(counter1.value()); /* Alerts 2 */
counter1.decrement();
alert(counter1.value()); /* Alerts 1 */
alert(counter2.value()); /* Alerts 0 */

Зауважте, як кожен з двох лічильників підтримує свою незалежність від іншого. Його середовище під час виклику функції makeCounter () щоразу відрізняється. Змінна замикання privateCounter містить кожен раз інший екземпляр.

Використання замикання таким чином забезпечує ряд переваг, які зазвичай пов'язані з об'єктно-орієнтованим програмуванням, зокрема приховування даних та інкапсуляція.

Ланцюжок замикань

Кожне замикання має три оточення:

  • Local Scope (власне оточення)
  • Outer Functions Scope (зовнішнє функціональне оточення)
  • Global Scope (глобальне оточення)

Поширеною помилкою є не усвідомлення того, що у випадку, коли зовнішня функція сама є вкладеною функцією, доступ до оточення її зовнішньої функції включає вкладене оточення  її як зовнішньої функції - ефективно створюючи ланцюг областей оточення. Для демонстрації розглянемо наступний приклад коду.

// global scope
var 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

// You can also write without anonymous functions:

// global scope
var 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;
      }
    }
  }
}

var s = sum(1);
var s1 = s(2);
var s2 = s1(3);
var s3 = s2(4);
console.log(s3) //log 20

У наведеному вище прикладі є низка вкладених функцій, всі з яких мають доступ до області оточення зовнішніх функцій. У цьому контексті можна сказати, що замикання мають доступ до всіх зовнішніх областей функцій.

Creating closures in loops: A common mistake

До введення ключового слова let у ECMAScript 2015, загальна проблема із замиканням виникала, коли вони створювалися всередині циклу. Розглянемо наступний приклад: 

<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>
function showHelp(help) {
  document.getElementById('help').innerHTML = 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 = function() {
      showHelp(item.help);
    }
  }
}

setupHelp(); 

Масив helpText визначає три корисні підказки, кожну пов'язану з ID поля Input. Цикл циклично перераховує ці визначення, підключаючи подію onfocus до кожного, що показує пов'язаний метод help.

Якщо ви спробуєте цей код, ви побачите, що він не працює, як очікувалося. Незалежно від того, на якому полі ви зосереджені, відображатиметься повідомлення про ваш вік.

Причиною цього є те, що функції, призначені для фокусування, - це замикання; вони складаються з визначення функції та захопленого оточення з області видимосты функцій setupHelp.  Три замикання були створені циклом, але кожне ділиться тим самим єдиним середовищем, яке має змінну зі змінними значеннями (item.help).

Коли виконуються зворотні виклики onfocus, доступ до item.help в цей момент викликає таку поведінку (дійсно, оскільки значення змінної доступно / обчислюється лише під час виконання), оскільки цикл до цього часу виконувався, а об'єкт змінної елемента (розділений усіма трьома замиканнями) був залишений, вказуючи на останній запис у списку довідкового тексту.

Одне рішення в цьому випадку - використовувати більше замикань: зокрема, використовувати фабрику функцій, як описано раніше:

function showHelp(help) {
  document.getElementById('help').innerHTML = 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 створює нове середовище для кожного з них, в якій довідка посилається на відповідний рядок із масиву helpText.

Ще один спосіб написати вищезгадане - використання  анонімних  замикань:

function showHelp(help) {
  document.getElementById('help').innerHTML = 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();

Якщо ви не хочете більше використовувати замикання, можете скористатися ключовим словом let з ES6:

function showHelp(help) {
  document.getElementById('help').innerHTML = 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++) {
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

Цей приклад використовує let замість var, таким чином кожне замикання прив'язує блочну  змінну,це означає, що додаткові замикання не потрібні.

Іншою альтернативою може бути використання forEach () для повторення масиву helpText та приєднання слухача до кожного <p>, як показано:

function showHelp(help) {
  document.getElementById('help').innerHTML = 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();

Performance considerations

Нерозумно створювати функції в межах інших функцій, якщо замикання не потрібне для конкретного завдання, оскільки це негативно вплине на продуктивність сценарію як з точки зору швидкості обробки, так і споживання пам'яті.

Наприклад, при створенні нового об'єкта / класу методи, як правило, повинні бути пов'язані з прототипом об'єкта, а не визначені в конструкторі об'єктів. Причина полягає в тому, що щоразу, коли конструктор викликається, методи отримують перепризначення (тобто для кожного створення об'єкта).

Розглянемо наступний непрактичний, але показовий випадок:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

Попередній код не використовує переваги замикань, а тому може виглядати:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

Однак переробляти прототип не рекомендується, тому наступний приклад ще кращий, оскільки він додається до існуючого прототипу:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

Код вище також може бути записаний чистіше з тим же результатом:

function MyObject(name, message) {
    this.name = name.toString();
    this.message = message.toString();
}
(function() {
    this.getName = function() {
        return this.name;
    };
    this.getMessage = function() {
        return this.message;
    };
}).call(MyObject.prototype);

In the three previous examples, the inherited prototype can be shared by all objects and the method definitions need not occur at every object creation. See Details of the Object Model for more details.  У трьох попередніх прикладах успадкований прототип може бути спільним для всіх об'єктів, і визначення методів не повинно виникати при кожному створенні об'єкта. Докладніше див. Деталі Об'єктної Моделі (Details of the Object Model).