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

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

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

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

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

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

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

Замикання

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

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> 

Emulating private methods with closures

Languages such as Java provide the ability to declare methods private, meaning that they can only be called by other methods in the same class.

JavaScript does not provide a native way of doing this, but it is possible to emulate private methods using closures. Private methods aren't just useful for restricting access to code: they also provide a powerful way of managing your global namespace, keeping non-essential methods from cluttering up the public interface to your code.

Here's how to define some public functions that can access private functions and variables, using closures which is also known as the 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

There's a lot going on here. In previous examples each closure has had its own environment; here we create a single environment which is shared by three functions: counter.increment, counter.decrement, and counter.value.

The shared environment is created in the body of an anonymous function, which is executed as soon as it has been defined. The environment contains two private items: a variable called privateCounter and a function called changeBy. Neither of these private items can be accessed directly from outside the anonymous function. Instead, they must be accessed by the three public functions that are returned from the anonymous wrapper.

Those three public functions are closures that share the same environment. Thanks to JavaScript's lexical scoping, they each have access to the privateCounter variable and changeBy function.

You'll notice we're defining an anonymous function that creates a counter, and then we call it immediately and assign the result to the counter variable. We could store this function in a separate variable makeCounter and use it to create several counters.

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 */

Notice how each of the two counters maintains its independence from the other. Its environment during the call of the makeCounter() function is different each time. The closure variable privateCounter contains a different instance each time.

Using closures in this way provides a number of benefits that are normally associated with object oriented programming, in particular data hiding and encapsulation.

Creating closures in loops: A common mistake

Prior to the introduction of the let keyword in ECMAScript 2015, a common problem with closures occurred when they were created inside a loop. Consider the following example:

<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(); 

The helpText array defines three helpful hints, each associated with the ID of an input field in the document. The loop cycles through these definitions, hooking up an onfocus event to each one that shows the associated help method.

If you try this code out, you'll see that it doesn't work as expected. No matter what field you focus on, the message about your age will be displayed.

The reason for this is that the functions assigned to onfocus are closures; they consist of the function definition and the captured environment from the setupHelp function's scope. Three closures have been created by the loop, but each one shares the same single environment which has a variable with changing values (item.help). When the onfocus callbacks are executed, accessing item.help at that moment causes this behaviour (indeed, as the value of the variable is only accessed/calculated at runtime), as the loop has run its course by that time, and the item variable object (shared by all three closures) has been left pointing to the last entry in the helpText list.

One solution in this case is to use more closures: in particular, to use a function factory as described earlier on:

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(); 

This works as expected. Rather than the callbacks all sharing a single environment, the makeHelpCallback function creates a new environment for each one in which help refers to the corresponding string from the helpText array.

One other way to write the above using anonymous closures is:

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();

If you don't want to use more closures, you can use the let keyword of 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();

In the above case, we used let instead of var, so every closure will bind the every block scoped variable, so it will work perfectly without using any extra closure.

Performance considerations

It is unwise to unnecessarily create functions within other functions if closures are not needed for a particular task, as it will negatively affect script performance both in terms of processing speed and memory consumption.

For instance, when creating a new object/class, methods should normally be associated to the object's prototype rather than defined into the object constructor. The reason is that whenever the constructor is called, the methods would get reassigned (that is, for every object creation).

Consider the following impractical but demonstrative case:

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

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

The previous code does not take advantage of the benefits of closures and thus could instead be formulated:

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

However, redefining the prototype is not recommended, so the following example is even better because it appends to the existing prototype:

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;
};

The above code can also be written in a cleaner way with the same result:

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.

Мітки документа й учасники

Мітки: 
Зробили внесок у цю сторінку: HelgeUa, AlexKutkanych, jen9, litashg, koshandor, piton13
Востаннє оновлена: HelgeUa,