Замыкание (closure) представляют собой конструкцию, когда функция, созданная в одной области видимости, запоминает свое лексическое окружение даже в том случае, когда она выполняет вне своей области видимости.
Для организации замыкания необходимы три компонента:
внешняя функция, которая определяет некоторую область видимости и в которой определены некоторые переменные - лексическое окружение
переменные (лексическое окружение), которые определены во внешней функции
вложенная функция, которая использует эти переменные
function outer(){ // внешняя функция
let n; // некоторая переменная
return inner(){ // вложенная функция
// действия с переменной n
}
}
Грубо говоря, функция inner и представляет в данном случае замыкание, которое запонимание свое лексического окружение - переменную х.
Рассмотрим замыкания на простейшем примере:
function outer(){
let x = 5;
function inner(){
x++;
console.log(x);
};
return inner;
}
const fn = outer(); // fn = inner, так как функция outer возвращает функцию inner
// вызываем внутреннюю функцию inner
fn(); // 6
fn(); // 7
fn(); // 8
Здесь функция outer задает область видимости, в которой определены внутренняя функция inner и переменная x. Переменная x представляет лексическое окружение для функции inner. В самой функции inner инкрементируем переменную x и выводим ее значение на консоль. В конце функция outer возвращает функцию inner.
Далее вызываем функцию outer:
const fn = outer();
Поскольку функция outer возвращает функцию inner, то константа fn будет хранить ссылку на функцию inner. При этом эта функция запомнила свое окружение - то есть внешнюю переменную x.
Далее мы фактически три раза вызываем функцию Inner, и мы видим, что переменная x, которая определена вне функции inner, инкрементируется:
fn(); // 6 fn(); // 7 fn(); // 8
То есть несмотря на то, что переменная x определена вне функции inner, эта функция запомнила свое окружение и может его использовать, несомотря на то, что она вызывается вне функции outer, в которой была определена. В этом и суть замыканий.
Причем для каждой копии замыкания определяется своя копия лексического окружения:
// определяем объект-пространство имен
function outer(){
let x = 5;
function inner(){
x++;
console.log(x);
};
return inner;
}
const fn1 = outer();
const fn2 = outer();
fn1(); // 6
fn1(); // 7
fn2(); // 6
fn2(); // 7
Здесь видно, что для fn1 и fn2 есть своя копия переменной х, которой они манипулируют независимо друг от друга.
Рассмотрим еще один пример:
function multiply(n){
let x = n;
return function(m){ return x * m;};
}
const fn1 = multiply(5);
const result1 = fn1(6); // 30
console.log(result1); // 30
const fn2= multiply(4);
const result2 = fn2(6); // 24
console.log(result2); // 24
Итак, здесь вызов функции multiply() приводит к вызову другой внутренней функции. Внутренняя же функция:
function(m){ return x * m;};
запоминает окружение, в котором она была создана, в частности, значение переменной x.
В итоге при вызове функции multiply определяется константа fn1, которая и представляет собой замыкание, то есть
объединяет две вещи: функцию и окружение, в котором функция была создана. Окружение состоит из любой локальной переменной, которая была в области действия
функции multiply во время создания замыкания.
То есть fn1 — это замыкание, которое содержит и внутреннюю функцию
function(m){ return x * m;}, и переменную x, которая существовала во время создания замыкания.
При создании двух замыканий: fn1 и fn2, для каждого из этих замыканий создается свое окружение.
При этом важно не запутаться в параметрах. При определении замыкания:
const fn1 = multiply(5);
Число 5 передается для параметра n функции multiply.
При вызове внутренней функции:
const result1 = fn1(6);
Число 6 передается для параметра m во внутреннюю функцию function(m){ return x * m;};.
Также мы можем использовать другой вариант для вызова замыкания:
function multiply(n){
let x = n;
return function(m){ return x * m;};
}
const result = multiply(5)(6); // 30
console.log(result);
Хотя объектно-ориентированное программирование будет рассматриваться далее, тем не менее нельзя не заметить, что замыкания по сути явились предтечей объектно-ориентированного программирования. И использование замыканий в некоторой степени позволяет имитировать создание объектов и работу с ними. Например, рассмотрим следующий код:
function person(name, age){
console.log("Person", name, "created");
function print(){
console.log("Person ", name, " (" +age +")");
}
function work(){
console.log("Person ", name, " works");
}
function incrementAge(value){
age = age + value;
}
return [print, work, incrementAge];
}
const tom = person("Tom", 39);
tom[0](); // print
tom[1](); // work
tom[2](1); // incrementAge
tom[0](); // print
Функция person принимает два параметра и определяет три вложенных функции. По сути эти вложенных функции и образуют замыкания, а в качестве лексического окружения используют параметры функции person.
Здесь функция person выступает своего рода конструктором объекта person - условного человека, а ее параметры name и age принимают извне соответственно имя и возраст человека. Три вложенных функции - print, work, incrementAge могут обращаться к своему лексическому окружению - параметрам функции person.
Чтобы к этим функциям можно было обратиться извне, функция person возвращает вложенные функции в виде массива:
return [print, work, incrementAge];
Далее мы можем вызвать функцию person, передав в нее некоторые параметры, и получить ее результат - условного человека:
const tom = person("Tom", 39);
Что такое в данном случае константа tom? По сути это возвращенный массив из трех функций, котоорые позволяют манипулировать человеком tom. Соответственно, обратившись к определенному элементу в массиве, мы можем вызвать соответствующую функцию:
tom[0](); // print
То есть выражение tom[0] возвращает функцию print, а выражение tom[0]() вызывает ее. В итоге в данном случае вывод консоли браузера будет следующим:
Person Tom created Person Tom (39) Person Tom works Person Tom (40)