Замыкание (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)