В этом руководстве вы узнаете о замыканиях в JavaScript, зачем они нужны, и как более эффективно использовать в своем коде.
В JavaScript замыкание — это функция, которая ссылается на переменные во внешней области видимости из своей внутренней области видимости и сохраняет внешнюю область внутри своей внутренней области.
Чтобы понять замыкания, вам нужно сначала узнать, как работает лексическая область видимости.
Лексическая область видимости
Лексическая область видимости определяет область действия переменной по положению этой переменной, объявленной в исходном коде. Например:
let name = 'John'; function greeting() { let message = 'Hi'; console.log(message + ' ' + name); }
В этом примере:
name
переменной является глобальной переменной. Он доступен из любого места, в том числе из функцииgreeting()
.- Переменная
message
— это локальная переменная, доступная только внутри функцииgreeting()
.
Если вы попытаетесь получить доступ к переменной message
вне функции greeting()
, вы получите сообщение об ошибке.
Таким образом, движок JavaScript использует область видимости для управления доступом к переменной.
В соответствии с лексической областью видимости области могут быть вложенными, и внутренняя функция может обращаться к переменным, объявленным во внешней области видимости. Например:
function greeting() { let message = 'Hi'; function sayHi() { console.log(message); } sayHi(); } greeting();
Функция Greeting( greeting()
создает локальную переменную с именем message
и функцию с именем sayHi()
.
sayHi()
— это внутренняя функция, доступная только в теле функции greeting()
.
Функция sayHi()
может обращаться к переменным внешней функции, таким как переменная message
функции greeting()
.
Внутри функции Greeting() мы вызываем функцию greeting()
sayHi()
для отображения сообщения Hi
.
Особенности
Давайте изменим функцию greeting()
:
function greeting() { let message = 'Hi'; function sayHi() { console.log(message); } return sayHi; } let hi = greeting(); hi(); // still can access the message variable
Теперь, вместо выполнения функции sayHi()
внутри функции greeting()
возвращается объект функции sayHi()
.
Обратите внимание, что функции являются объектами первого класса в JavaScript, поэтому вы можете вернуть функцию из другой функции.
Вне функции greeting()
мы присвоили переменной hi
значение, возвращаемое функцией greeting()
, которая является ссылкой на sayHi()
.
Затем мы выполнили sayHi()
, используя ссылку на эту функцию: hi()
. Если вы запустите код, вы получите тот же эффект, что и выше.
Однако интересным моментом здесь является то, что обычно локальная переменная существует только во время выполнения функции.
Это означает, что когда функция greeting()
завершила выполнение, переменная message
больше недоступна.
В этом случае мы выполняем функцию hi()
, которая ссылается на sayHi()
, переменная message
все еще существует.
Магия этого — закрытие. Другими словами, sayHi()
является замыканием.
Замыкание — это функция, которая сохраняет внешнюю область видимости во внутренней области видимости.
Дополнительный пример
Следующий пример иллюстрирует более практичный пример замыкания в JavaScript:
function greeting(message) { return function(name) { return message + ' ' + name; } } let sayHi = greeting('Hi'); let sayHello = greeting('Hello'); console.log(sayHi('John')); // Hi John console.log(sayHello('John')); // Hello John
Функция greeting()
принимает один аргумент с именем message
и возвращает функцию, которая принимает один аргумент с name
.
Функция возврата возвращает приветственное сообщение, которое является комбинацией переменных message
и name
.
Функция greeting()
ведет себя как фабрика функций. Она создает sayHi()
и sayHello()
с соответствующими сообщениями Hi
и Hello
.
sayHi()
и sayHello()
являются замыканиями. Они используют одно и то же тело функции, но хранят разные области видимости.
В sayHi()
message
Hi
, а в замыкании sayHello()
message
Hello
.
Замыкание в цикле
Рассмотрим следующий пример:
for (var index = 1; index & lt; = 3; index++) { setTimeout(function() { console.log('after ' + index + ' second(s):' + index); }, index * 1000); }
Выход
after 4 second(s):4 after 4 second(s):4 after 4 second(s):4
Код показывает то же сообщение.
Что мы хотели сделать в цикле, так это скопировать значение i
на каждой итерации во время ее выполнения, чтобы отобразить сообщение через 1, 2 и 3 секунды.
Причина, по которой вы видите то же сообщение через 4 секунды, заключается в том, что обратный вызов передал setTimeout()
закрытие. Он запоминает значение i
из последней итерации цикла, равное 4.
Кроме того, все три замыкания, созданные циклом for, совместно используют одну и ту же глобальную область видимости и получают доступ к одному и тому же значению i
.
Чтобы решить эту проблему, вам нужно создавать новую область закрытия в каждой итерации цикла.
Есть два популярных решения: IIFE и ключевое слово let
.
1) Использование решения IIFE
В этом решении вы используете немедленно вызываемое функциональное выражение (также известное как IIFE), потому что IIFE создает новую область действия, объявляя функцию и немедленно выполняя ее.
for (var index = 1; index & lt; = 3; index++) { (function(index) { setTimeout(function() { console.log('after ' + index + ' second(s):' + index); }, index * 1000); })(index); }
Выход
after 1 second(s):1 after 2 second(s):2 after 3 second(s):3
2) Использование ключевого слова let
в ES6
В ES6 вы можете использовать ключевое слово let
для объявления переменной с блочной областью действия.
Если вы используете ключевое слово let
в цикле for, оно будет создавать новую лексическую область видимости на каждой итерации. Другими словами, у вас будет новая index
переменная на каждой итерации.
Кроме того, новая лексическая область действия присоединяется к предыдущей области, так что предыдущее значение index
копируется из предыдущей области в новую.
for (let index = 1; index & lt; = 3; index++) { setTimeout(function() { console.log('after ' + index + ' second(s):' + index); }, index * 1000); }
Выход
after 1 second(s):1 after 2 second(s):2 after 3 second(s):3
Заключение
- Лексическая область видимости описывает, как движок JavaScript использует местоположение переменной в коде, чтобы определить, где эта переменная доступна.
- Замыкание — это комбинация функции и ее способности запоминать переменные во внешней области видимости.