Что такое замыкание в javascript
Перейти к содержимому

Что такое замыкание в javascript

  • автор:

Замыкания в Javascript (что такое замыкание в JS + 3 примера)

В этой статье мы с вами разберемся как что такое, и как работают замыкания (closure) в Javascript. Эта тема однозначно будет всплывать в большинстве ваших интервью. Поэтому, нужно обязательно в ней разбираться и быть готовым объяснить ее суть.

На самом деле, в замыканиях нет ничего сложного. Суть замыкания в JS, заключается в возможности одной функции (назовем ее 1-я функция), которая возвращается из родительской функции (2-я функция), получать доступ к переменным, которые находятся в области видимости родительской, то есть 1-й функции.

Тонкий момент заключается в том, что это происходит даже после того, как родительская, то есть 1-я функция завершила свое выполнение.

Замыкания JS (пример №1)

Для примера, давайте создадим 2 функции:

1// Внешняя функция
2 function external()
3 const externalVar = 'Я - внешняя функция.';
4
5 // Внутренняя функция
6 function internal()
7 const internalVar = 'Я - внутренняя функция.';
8 console.log(internalVar);
9 console.log(externalVar);
10 >
11
12 // Запускаем функцию internal внутри функции external
13 internal();
14 >

Теперь давайте запустим функцию external:

1external();
2 // Получаем в лог значения 2-х переменных:
3 // Я - внутренняя функция.
4 // Я - внешняя функция.

Таким образом, автоматически запускается наша внутренняя функция internal, и мы получаем в консоле 2 лога, которые содержат значения переменных externalVar и internalVal:

  • internalVar, потому что она находится внутри области видимости текущей функции internal.
  • externalVar, потому что она находится внутри области видимости родительской функции — external.

Так где же здесь замыкание?

Мы получим замыкание, если будем запускать нашу функцию internal не внутри родительской функции external, а за ее пределами.

Как это сделать?

Для этого нам нужно сделать 2 вещи:

  1. Мы должны возвратить функцию internal из родительской функции external:
1function external()
2 const externalVar = 'Я - внешняя функция.';
3
4 // Возвращаем внутреннюю функцию
5 return function internal()
6 const internalVar = 'Я - внутренняя функция.';
7 console.log(internalVar);
8 console.log(externalVar);
9 >;
10 >

Если мы сейчас запустим нашу функцию external, то мы получим «определение» внутренней функции «internal».

1external();
2
3 // Получаем определение функции internal:
4 // ƒ internal()
5 // const internalVar = 'Я - внутренняя функция.';
6 // console.log(internalVar);
7 // console.log(externalVar);
8 // >
  1. Далее, нужно присвоить результат работы функции external какой-либо новой переменной:
1const internalFn = external();

Если мы выведем в лог значение нашей новой переменной internalFn, то мы также получим определение нашей внутренней функции internal.

1console.log(internalFn);
2
3 // Получаем определение функции internal:
4 // ƒ internal()
5 // const internalVar = 'Я - внутренняя функция.';
6 // console.log(internalVar);
7 // console.log(externalVar);
8 // >
  1. Теперь мы можем запустить нашу внутреннюю функцию internal — за пределами внешней функции external. Для этого используем новую перменную internalFn:
1internalFn();
2
3 // Получаем в лог значения 2-х переменных:
4 // Я - внутренняя функция.
5 // Я - внешняя функция.

Так где же здесь замыкание?

Идея замыкания в JS — заключается в том, что даже после того, как наша внешняя функция завершила свое выполнение, компилятор Javascript оставляет в памяти значение нашей переменной externalVar.
Это значение остается доступным внутри нашей новой функции internalFn — за пределами внешней функции external.

То есть замыкание в JS можно представить себе, как некое «магическое» пространство между внешней и внутренней функциями, которое остается доступным даже после завершения выполнения внешней функции external.

1function external()
2 // Магическое пространство начинается:
3 const externalVar = 'Я - внешняя функция.';
4 // Магическое пространство завершается
5 return function internal()
6 const internalVar = 'Я - внутренняя функция.';
7 console.log(internalVar);
8 console.log(externalVar);
9 >;
10 >

Замыкания Javascript (пример №2)

Для второго примера давайте создадим следующую функцию:

1function createAddress(type)
2 const address = type.toUpperCase();
3 return function (name)
4 return `$address> $name>`;
5 >;
6 >

Наша функция будет принимать на вход переменную type — тип обращения («Гражданин» или «Гражданка») и записывать значение в переменную address в верхнем регистре.

Наша внутренняя безымянная функция принимает переменную name — имя человека.

Теперь давайте создадим 2 новые переменные, в которых используем функцию createAddress с 2-мя типами обращений:

1const addressGrazhdanin = createAddress('Гражданин');
2 const addressGrazhdanka = createAddress('Гражданка');

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

При этом, в каждом случае — компилятор Javascript «запомнит» разные значения переменной address. Эти значения будут доступны внутри безымянной функции — за пределами внешней функции createAddress (даже после завершения ее выполнения).

То есть, если мы, позже, используем переменную addressGrazhdanin — то получим значение переменной address, равное ‘ГРАЖДАНИН’.

1console.log(addressGrazhdanin('Василий'));
2 // ГРАЖДАНИН Василий

В случае с переменной addressGrazhdanka — получаем значение переменной address, равное ‘ГРАЖДАНКА’.

1console.log(addressGrazhdanka('Александра'));
2 // ГРАЖДАНКА Александра

Замыкание JS (пример №3)

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

1function createPlayer(name)
2 let score = 0;
3 return function scoreCount()
4 score++;
5 return `$name> - $score> балла`;
6 >;
7 >

Ниже, давайте создадим переменные для каждого игрока.

1const playerOne = createPlayer('Василий');
2 const playerTwo = createPlayer('Андрей');

Теперь, каждый раз, когда мы будем вызывать какую-либо из наших новых функций, в ответ будем получать обновленное количество баллов для каждого конкретного игрока.

1playerOne();
2 playerOne();
3 // Получаем увеличение очков для каждого конкретного игрока
4 // "Василий - 2 балла"
1playerTwo();
2 playerTwo();
3 playerTwo();
4 // Получаем увеличение очков для каждого конкретного игрока
5 // "Андрей - 3 балла"

Это происходит, потому что при создании переменных playerOne и playerTwo мы создали замыкание со своим собственным значением переменной score.

Поднятие в JS (Hoisting в Javascript + 3 примера)

Стрелочные Функции JS — Arrow Functions (просто и с примерами)

Популярные статьи

  • Задачи JavaScript для начинающих
  • Типы данных в JavaScript
  • Как проверить объект JavaScript на пустоту?
  • Обработчики Событий в JS
  • Деструктуризация в Javascript
  • Массивы Javascript: перебирающие методы
  • Операторы Spread и Rest в Javascript
  • Объект Date: Текущая Дата и Время в Javascript
  • Переменные JavaScript var, let и const

Замыкание в JavaScript: основные принципы и примеры

Замыкание (closure) в JavaScript это одно из самых мощных и интересных понятий, которое позволяет сохранять доступ к переменным внутри функции даже после ее завершения. В простых словах, замыкание позволяет функции запомнить и использовать значения переменных, которые были доступны в момент ее создания.

Когда вы создаете функцию внутри другой функции, внутренняя функция может получить доступ к переменным, определенным внутри внешней функции, даже после того, как внешняя функция уже завершила свое выполнение. Это происходит потому, что замыкание создает окружение, в котором внутренняя функция всегда имеет доступ к переменным внешней функции.

Замыкания особенно полезны, когда вам необходимо сохранить состояние переменных между вызовами функции или скрыть приватную информацию. Они широко используются в практической разработке для создания модульных и безопасных программных компонентов.

В этой статье мы рассмотрим основы замыканий в JavaScript, и объясним, как они работают с помощью примеров. Мы также рассмотрим некоторые важные моменты и практические применения замыканий, которые помогут вам лучше понять и использовать их в своем коде.

Что такое замыкание в JavaScript: объяснение и примеры

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

Практический пример использования замыканий — создание приватных переменных и методов в объектах JavaScript. Взглянем на пример:

function counter() < var count = 0; function increment() < count++; console.log(count); >return increment; > var myCounter = counter(); 

В данном примере функция counter создает и возвращает внутреннюю функцию increment . Внутри функции increment есть доступ к переменной count , хотя она уже вышла из области видимости функции counter . Это происходит потому, что функция increment использует замыкание, которое сохраняет доступ к переменной count .

Таким образом, замыкание позволяет сохранять внутри себя состояние и использовать его даже после завершения выполнения внешней функции.

Заключение

Замыкание — это мощный механизм в JavaScript, который позволяет сохранять доступ к переменным и функциям даже после того, как они были закрыты и сняты с области видимости. Знание и понимание замыканий помогает разрабатывать более гибкий и масштабируемый код в JavaScript.

Определение и суть замыкания

Замыкание — это способность функции сохранять доступ к своей лексической области видимости, даже после того, как функция была выполнена и вышла из своего контекста. Оно возникает благодаря тому, что в JavaScript функции являются объектами первого класса, то есть их можно присваивать переменным, передавать в качестве аргументов, возвращать из других функций и т.д.

Смотрите также: Флешки для Linux

Когда функция создается внутри другой функции, она может обращаться к переменным и параметрам этой внешней функции, даже после того, как внешняя функция завершила свое выполнение. Такие внутренние функции называются замыканиями. Замыкание фактически создает «карман» или «контейнер» окружающей функции, который сохраняет ссылки на все переменные и параметры этой окружающей функции.

Замыкания позволяют нам создавать и использовать приватные переменные и функции в JavaScript. Таким образом, замыкания помогают нам создавать модульный, безопасный и гибкий код.

Применение замыканий в JavaScript включает в себя создание частных свойств и методов в объектах, создание функций-обработчиков с сохранением состояния, создание функций высшего порядка и многое другое.

Как работает замыкание в JavaScript

Когда функция создается в JavaScript, она создает собственный лексический контекст, который содержит все переменные и параметры, определенные внутри этой функции. Каждый раз, когда функция вызывается, создается новый экземпляр ее лексического контекста, и переменные и параметры сохраняют свое значение в каждом созданном экземпляре.

Однако, когда функция внутри другой функции создает и возвращает другую функцию, эта внутренняя функция сохраняет доступ не только к своему собственному лексическому контексту, но и к лексическому контексту родительской функции. Этот механизм называется замыканием.

Замыкания часто используются для создания приватных переменных и функций в JavaScript. Путем создания функций, которые заключают свои переменные в замыкания, можно контролировать доступ к этим переменным и предотвратить их изменение или использование извне.

Примером использования замыкания может быть следующий код:

 function createCounter() < let count = 0; return function() < count++; console.log(count); >; > let counter = createCounter(); 
Преимущества замыкания: Недостатки замыкания:
Сохранение состояния переменных между вызовами функции Потребление памяти, если замыкание не используется эффективно
Создание приватных переменных и функций Потенциальная сложность понимания кода с использованием замыкания
Управление доступом к переменным и предотвращение их изменения извне

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

Практические примеры замыкания

Пример 1: Счетчик

Один из самых классических примеров замыкания — счетчик. Рассмотрим следующий код:

 function createCounter() < let count = 0; function increment() < count++; console.log(count); >return increment; > const counter = createCounter(); counter(); // Выведет: 1 counter(); // Выведет: 2 

Пример 2: Приватные переменные

Еще одним полезным примером замыкания является создание приватных переменных. Рассмотрим следующий код:

 function createPerson(name) < let privateName = name; return < getName: function() < return privateName; >, setName: function(newName) < privateName = newName; >>; > const person = createPerson('John'); console.log(person.getName()); // Выведет: John person.setName('Mike'); console.log(person.getName()); // Выведет: Mike 

В данном примере функция createPerson создает переменную privateName, которая является приватной и недоступной извне. Затем она возвращает объект с двумя методами: getName и setName. Метод getName возвращает значение переменной privateName, а метод setName изменяет ее значение. Таким образом, мы получаем контролируемый доступ к приватной переменной, используя замыкание.

Смотрите также: Как задать массив в Java

Плюсы и минусы использования замыкания

Плюсы использования замыкания:

1. Упрощение и улучшение читаемости кода:

Замыкания позволяют создавать более компактный и понятный код, исключая необходимость передачи аргументов при вызове функции. Кроме того, использование замыканий может повысить модульность кода и улучшить его структуру.

2. Сокрытие приватных данных:

С помощью замыканий можно сокрыть некоторые приватные данные и методы внутри функции, предоставляя доступ к ним только через публичные методы. Это позволяет более безопасно и контролируемо использовать эти данные.

Минусы использования замыкания:

1. Потребление памяти:

Использование замыканий может приводить к потреблению большего объема памяти, так как каждый экземпляр функции будет хранить свои уникальные значения переменных из внешнего контекста.

2. Проблемы с утечкой памяти:

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

3. Производительность:

Замыкания могут снизить производительность кода, особенно если функция вызывается в цикле или используется много раз. Это связано с дополнительной нагрузкой на процессор и память при создании каждого нового замыкания.

Несмотря на некоторые ограничения и проблемы, правильное использование замыкания может значительно улучшить качество и гибкость кода в JavaScript.

Как использовать замыкание для повышения эффективности кода

Замыкания могут использоваться для повышения эффективности кода в нескольких различных сценариях.

1. Сокрытие данных

Одним из основных преимуществ использования замыкания является возможность сокрытия данных. Это позволяет создать приватные переменные и функции, к которым нельзя получить доступ извне.

Например, можно создать функцию-конструктор, которая будет иметь приватные переменные и методы, а публичные методы смогут использовать эти приватные данные. Такой подход позволяет сохранить безопасность и целостность данных в приложении.

2. Кэширование результатов

Замыкания также могут быть использованы для кэширования результатов вычислений. Вместо повторного вычисления сложных операций или получения данных из внешних источников, можно сохранить результат вычислений или полученные данные в замыкании и использовать их в дальнейшем.

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

Смотрите также: Как запустить файл в Python

Использование замыкания предоставляет гибкость и возможность улучшить производительность кода в различных ситуациях. Но также важно помнить, что неправильное использование замыкания может привести к утечкам памяти или проблемам с производительностью. Поэтому всегда следует быть внимательным и заботиться о правильной организации кода.

Вопрос-ответ:

Что такое замыкание и как оно работает в JavaScript?

Замыкание — это комбинация функции и связанного с ней лексического контекста. При создании функции внутри другой функции, внутренняя функция будет иметь доступ к переменным и параметрам внешней функции, даже после того, как внешняя функция завершит свое выполнение. Это происходит потому, что внутренняя функция сохраняет ссылку на лексический контекст в момент своего создания, что позволяет ей обращаться к этим переменным и параметрам в будущем.

Каким образом замыкания могут быть полезными в программировании на JavaScript?

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

Можете привести пример использования замыкания в JavaScript?

Конечно! Например, рассмотрим следующий код:

Можно ли избежать замыканий при написании кода на JavaScript?

Замыкания — это нечто встроенное в язык JavaScript, и они автоматически возникают во многих ситуациях. Но если вам не нужно использовать замыкания в вашем коде, вы можете избежать их, избегая создания функций внутри других функций или удаляя все ссылки на внешние переменные из внутренних функций. Однако замыкания часто используются в JavaScript для создания элегантных и мощных решений задач, поэтому стоит хорошо понять их и уметь правильно использовать.

Что такое замыкание в JavaScript?

Замыкание в JavaScript — это функция, которая запоминает свою лексическую среду, включая переменные и функции, которые были доступны в момент ее создания. Таким образом, замыкание может получать доступ к этим переменным, даже если они были созданы вне функции.

Как создать замыкание в JavaScript?

Для создания замыкания в JavaScript нужно определить функцию внутри другой функции и возвращать ее как результат. Внутренняя функция будет иметь доступ к переменным, определенным во внешней функции, даже после завершения выполнения внешней функции.

Зачем использовать замыкания в JavaScript?

Замыкания в JavaScript имеют множество применений. Они позволяют создавать приватные переменные и функции, которые недоступны извне. Также замыкания позволяют сохранять состояние функции между ее вызовами и могут использоваться для создания функций-оберток или функций высшего порядка.

Область видимости переменных, замыкание

JavaScript – язык с сильным функционально-ориентированным уклоном. Он даёт нам много свободы. Функция может быть динамически создана, скопирована в другую переменную или передана как аргумент другой функции и позже вызвана из совершенно другого места.

Мы знаем, что функция может получить доступ к переменным из внешнего окружения, эта возможность используется очень часто.

Но что произойдёт, когда внешние переменные изменятся? Функция получит последнее значение или то, которое существовало на момент создания функции?

И что произойдёт, когда функция переместится в другое место в коде и будет вызвана оттуда – получит ли она доступ к внешним переменным своего нового местоположения?

Разные языки ведут себя по-разному в таких случаях, и в этой главе мы рассмотрим поведение JavaScript.

Мы будем говорить о переменных let/const здесь

В JavaScript существует три способа объявить переменную: let , const (современные), и var (пережиток прошлого).

  • В этой статье мы будем использовать переменные let в примерах.
  • Переменные, объявленные с помощью const , ведут себя так же, так что эта статья и о них.
  • Старые переменные var имеют несколько характерных отличий, они будут рассмотрены в главе Устаревшее ключевое слово «var».

Блоки кода

Если переменная объявлена внутри блока кода <. >, то она видна только внутри этого блока.

 < // выполняем некоторые действия с локальной переменной, которые не должны быть видны снаружи let message = "Hello"; // переменная видна только в этом блоке alert(message); // Hello >alert(message); // ReferenceError: message is not defined

С помощью блоков <. >мы можем изолировать часть кода, выполняющую свою собственную задачу, с переменными, принадлежащими только ей:

Без блоков была бы ошибка

Обратите внимание, что без отдельных блоков возникнет ошибка, если мы используем let с существующим именем переменной:

// показать сообщение let message = "Hello"; alert(message); // показать другое сообщение let message = "Goodbye"; // SyntaxError: Identifier 'message' has already been declared alert(message);

Для if , for , while и т.д. переменные, объявленные в блоке кода <. >, также видны только внутри:

if (true) < let phrase = "Hello"; alert(phrase); // Hello >alert(phrase); // Ошибка, нет такой переменной!

В этом случае после завершения работы if нижний alert не увидит phrase , что и приведет к ошибке.

И это замечательно, поскольку это позволяет нам создавать блочно-локальные переменные, относящиеся только к ветви if .

То же самое можно сказать и про циклы for и while :

for (let i = 0; i < 3; i++) < // переменная i видна только внутри for alert(i); // 0, потом 1, потом 2 >alert(i); // Ошибка, нет такой переменной!

Визуально let i = 0; находится вне блока кода <. >, однако здесь в случае с for есть особенность: переменная, объявленная внутри (. ) , считается частью блока.

Вложенные функции

Функция называется «вложенной», когда она создаётся внутри другой функции.

Это очень легко сделать в JavaScript.

Мы можем использовать это для упорядочивания нашего кода, например, как здесь:

function sayHiBye(firstName, lastName) < // функция-помощник, которую мы используем ниже function getFullName() < return firstName + " " + lastName; >alert( "Hello, " + getFullName() ); alert( "Bye, " + getFullName() ); >

Здесь вложенная функция getFullName() создана для удобства. Она может получить доступ к внешним переменным и, значит, вывести полное имя. В JavaScript вложенные функции используются очень часто.

Что ещё интереснее, вложенная функция может быть возвращена: либо в качестве свойства нового объекта (если внешняя функция создаёт объект с методами), либо сама по себе. И затем может быть использована в любом месте. Не важно где, она всё так же будет иметь доступ к тем же внешним переменным.

Ниже, makeCounter создает функцию «счётчик», которая при каждом вызове возвращает следующее число:

function makeCounter() < let count = 0; return function() < return count++; // есть доступ к внешней переменной "count" >; > let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter() ); // 2

Несмотря на простоту этого примера, немного модифицированные его варианты применяются на практике, например, в генераторе псевдослучайных чисел и во многих других случаях.

Как это работает? Если мы создадим несколько таких счётчиков, будут ли они независимыми друг от друга? Что происходит с переменными?

Понимание таких вещей полезно для повышения общего уровня владения JavaScript и для более сложных сценариев. Так что давайте немного углубимся.

Лексическое окружение

Здесь водятся драконы!

Глубокое техническое описание – впереди.

Как бы мне ни хотелось избежать низкоуровневых деталей языка, любое представление о JavaScript без них будет недостаточным и неполным, так что приготовьтесь.

Для большей наглядности объяснение разбито на несколько шагов.

Шаг 1. Переменные

В JavaScript у каждой выполняемой функции, блока кода <. >и скрипта есть связанный с ними внутренний (скрытый) объект, называемый лексическим окружением LexicalEnvironment .

Объект лексического окружения состоит из двух частей:

  1. Environment Record – объект, в котором как свойства хранятся все локальные переменные (а также некоторая другая информация, такая как значение this ).
  2. Ссылка на внешнее лексическое окружение – то есть то, которое соответствует коду снаружи (снаружи от текущих фигурных скобок).

«Переменная» – это просто свойство специального внутреннего объекта: Environment Record . «Получить или изменить переменную», означает, «получить или изменить свойство этого объекта».

Например, в этом простом коде только одно лексическое окружение:

Это, так называемое, глобальное лексическое окружение, связанное со всем скриптом.

На картинке выше прямоугольник означает Environment Record (хранилище переменных), а стрелка означает ссылку на внешнее окружение. У глобального лексического окружения нет внешнего окружения, так что она указывает на null .

По мере выполнения кода лексическое окружение меняется.

Вот более длинный код:

Прямоугольники с правой стороны демонстрируют, как глобальное лексическое окружение изменяется в процессе выполнения кода:

  1. При запуске скрипта лексическое окружение предварительно заполняется всеми объявленными переменными.
    • Изначально они находятся в состоянии «Uninitialized». Это особое внутреннее состояние, которое означает, что движок знает о переменной, но на нее нельзя ссылаться, пока она не будет объявлена с помощью let . Это почти то же самое, как если бы переменная не существовала.
  2. Появляется определение переменной let phrase . У неё ещё нет присвоенного значения, поэтому присваивается undefined . С этого момента мы можем использовать переменную.
  3. Переменной phrase присваивается значение.
  4. Переменная phrase меняет значение.

Пока что всё выглядит просто, правда?

  • Переменная – это свойство специального внутреннего объекта, связанного с текущим выполняющимся блоком/функцией/скриптом.
  • Работа с переменными – это на самом деле работа со свойствами этого объекта.

Лексическое окружение – объект спецификации

«Лексическое окружение» – это объект спецификации: он существует только «теоретически» в спецификации языка для описания того, как все работает. Мы не можем получить этот объект в нашем коде и манипулировать им напрямую.

JavaScript-движки также могут оптимизировать его, отбрасывать неиспользуемые переменные для экономии памяти и выполнять другие внутренние действия, но при этом видимое поведение остается таким, как описано.

Шаг 2. Function Declaration

Функция – это тоже значение, как и переменная.

Разница заключается в том, что Function Declaration мгновенно инициализируется полностью.

Когда создается лексическое окружение, Function Declaration сразу же становится функцией, готовой к использованию (в отличие от let , который до момента объявления не может быть использован).

Именно поэтому мы можем вызвать функцию, объявленную как Function Declaration, до самого её объявления.

Вот, к примеру, начальное состояние глобального лексического окружения при добавлении функции:

Конечно, такое поведение касается только Function Declaration, а не Function Expression, в которых мы присваиваем функцию переменной, например, let say = function(name) <. >.

Шаг 3. Внутреннее и внешнее лексическое окружение

Когда запускается функция, в начале ее вызова автоматически создается новое лексическое окружение для хранения локальных переменных и параметров вызова.

Например, для say(«John») это выглядит так (выполнение находится на строке, отмеченной стрелкой):

В процессе вызова функции у нас есть два лексических окружения: внутреннее (для вызываемой функции) и внешнее (глобальное):

  • Внутреннее лексическое окружение соответствует текущему выполнению say . В нём находится одна переменная name , аргумент функции. Мы вызываем say(«John») , так что значение переменной name равно «John» .
  • Внешнее лексическое окружение – это глобальное лексическое окружение. В нём находятся переменная phrase и сама функция.

У внутреннего лексического окружения есть ссылка на внешнее outer .

Когда код хочет получить доступ к переменной – сначала происходит поиск во внутреннем лексическом окружении, затем во внешнем, затем в следующем и так далее, до глобального.

Если переменная не была найдена, это будет ошибкой в строгом режиме ( use strict ). Без строгого режима, для обратной совместимости, присваивание несуществующей переменной создаёт новую глобальную переменную с таким же именем.

Давайте посмотрим, как происходит поиск в нашем примере:

  • Для переменной name , alert внутри say сразу же находит ее во внутреннем лексическом окружении.
  • Когда alert хочет получить доступ к phrase , он не находит её локально, поэтому вынужден обратиться к внешнему лексическому окружению и находит phrase там.

Шаг 4. Возврат функции

Давайте вернёмся к примеру с makeCounter :

function makeCounter() < let count = 0; return function() < return count++; >; > let counter = makeCounter();

В начале каждого вызова makeCounter() создается новый объект лексического окружения, в котором хранятся переменные для конкретного запуска makeCounter .

Таким образом, мы имеем два вложенных лексических окружения, как в примере выше:

Отличие заключается в том, что во время выполнения makeCounter() создается крошечная вложенная функция, состоящая всего из одной строки: return count++ . Мы ее еще не запускаем, а только создаем.

Все функции помнят лексическое окружение, в котором они были созданы. Технически здесь нет никакой магии: все функции имеют скрытое свойство [[Environment]] , которое хранит ссылку на лексическое окружение, в котором была создана функция:

Таким образом, counter.[[Environment]] имеет ссылку на лексического окружения. Так функция запоминает, где она была создана, независимо от того, где она вызывается. Ссылка на [[Environment]] устанавливается один раз и навсегда при создании функции.

Впоследствии, при вызове counter() , для этого вызова создается новое лексическое окружение, а его внешняя ссылка на лексическое окружение берется из counter.[[Environment]] :

Теперь, когда код внутри counter() ищет переменную count , он сначала ищет ее в собственном лексическом окружении (пустом, так как там нет локальных переменных), а затем в лексическом окружении внешнего вызова makeCounter() , где находит count и изменяет ее.

Переменная обновляется в том лексическом окружении, в котором она существует.

Вот состояние после выполнения:

Если мы вызовем counter() несколько раз, то в одном и том же месте переменная count будет увеличена до 2 , 3 и т.д.

В программировании есть общий термин: «замыкание», – который должен знать каждый разработчик.

Замыкание – это функция, которая запоминает свои внешние переменные и может получить к ним доступ. В некоторых языках это невозможно, или функция должна быть написана специальным образом, чтобы получилось замыкание. Но, как было описано выше, в JavaScript, все функции изначально являются замыканиями (есть только одно исключение, про которое будет рассказано в Синтаксис «new Function»).

То есть они автоматически запоминают, где были созданы, с помощью скрытого свойства [[Environment]] , и все они могут получить доступ к внешним переменным.

Когда на собеседовании фронтенд-разработчику задают вопрос: «что такое замыкание?», – правильным ответом будет определение замыкания и объяснения того факта, что все функции в JavaScript являются замыканиями, и, может быть, несколько слов о технических деталях: свойстве [[Environment]] и о том, как работает лексическое окружение.

Сборка мусора

Обычно лексическое окружение удаляется из памяти вместе со всеми переменными после завершения вызова функции. Это связано с тем, что на него нет ссылок. Как и любой объект JavaScript, оно хранится в памяти только до тех пор, пока к нему можно обратиться.

Однако если существует вложенная функция, которая все еще доступна после завершения функции, то она имеет свойство [[Environment]] , ссылающееся на лексическое окружение.

В этом случае лексическое окружение остается доступным даже после завершения работы функции.

function f() < let value = 123; return function() < alert(value); >> let g = f(); // g.[[Environment]] хранит ссылку на лексическое окружение // из соответствующего вызова f()

Обратите внимание, что если f() вызывается много раз и результирующие функции сохраняются, то все соответствующие объекты лексического окружения также будут сохранены в памяти. В приведенном ниже коде – все три:

function f() < let value = Math.random(); return function() < alert(value); >; > // 3 функции в массиве, каждая из которых ссылается на лексическое окружение // из соответствующего вызова f() let arr = [f(), f(), f()];

Объект лексического окружения исчезает, когда становится недоступным (как и любой другой объект). Другими словами, он существует только до тех пор, пока на него ссылается хотя бы одна вложенная функция.

В приведенном ниже коде после удаления вложенной функции ее окружающее лексическое окружение (а значит, и value ) очищается из памяти:

function f() < let value = 123; return function() < alert(value); >> let g = f(); // пока существует функция g, value остается в памяти g = null; // . и теперь память очищена.

Оптимизация на практике

Как мы видели, в теории, пока функция жива, все внешние переменные тоже сохраняются.

Но на практике движки JavaScript пытаются это оптимизировать. Они анализируют использование переменных и, если легко по коду понять, что внешняя переменная не используется – она удаляется.

Одним из важных побочных эффектов в V8 (Chrome, Edge, Opera) является то, что такая переменная становится недоступной при отладке.

Попробуйте запустить следующий пример в Chrome с открытой Developer Tools.

Когда код будет поставлен на паузу, напишите в консоли alert(value) .

function f() < let value = Math.random(); function g() < debugger; // в консоли: напишите alert(value); Такой переменной нет! >return g; > let g = f(); g();

Как вы можете видеть – такой переменной не существует! В теории, она должна быть доступна, но попала под оптимизацию движка.

Это может приводить к забавным (если удаётся решить быстро) проблемам при отладке. Одна из них – мы можем увидеть не ту внешнюю переменную при совпадающих названиях:

let value = "Сюрприз!"; function f() < let value = "ближайшее значение"; function g() < debugger; // в консоли: напишите alert(value); Сюрприз! >return g; > let g = f(); g();

Эту особенность V8 полезно знать. Если вы занимаетесь отладкой в Chrome/Edge/Opera, рано или поздно вы с ней столкнётесь.

Это не баг в отладчике, а скорее особенность V8. Возможно со временем это изменится. Вы всегда можете проверить это, запустив примеры на этой странице.

Задачи

Учитывает ли функция последние изменения?

важность: 5

Функция sayHi использует имя внешней переменной. Какое значение будет использоваться при выполнении функции?

let name = "John"; function sayHi() < alert("Hi, " + name); >name = "Pete"; sayHi(); // что будет показано: "John" или "Pete"?

Такие ситуации встречаются как при разработке для браузера, так и для сервера. Функция может быть назначена на выполнение позже, чем она была создана, например, после действия пользователя или сетевого запроса.

Итак, вопрос: учитывает ли она последние изменения?

Ответ: Pete.

Функция получает внешние переменные в том виде, в котором они находятся сейчас, она использует самые последние значения.

Старые значения переменных нигде не сохраняются. Когда функция обращается к переменной, она берет текущее значение из своего или внешнего лексического окружения.

Какие переменные доступны?

важность: 5

Приведенная ниже функция makeWorker создает другую функцию и возвращает ее. Эта новая функция может быть вызвана из другого места.

Будет ли она иметь доступ к внешним переменным из места своего создания, или из места вызова, или из обоих мест?

function makeWorker() < let name = "Pete"; return function() < alert(name); >; > let name = "John"; // создаём функцию let work = makeWorker(); // вызываем её work(); // что будет показано?

Какое значение будет показано? «Pete» или «John»?

Ответ: Pete.

Функция work() в приведенном ниже коде получает name из места его происхождения через ссылку на внешнее лексическое окружение:

Таким образом, в результате мы получаем «Pete» .

Но если бы в makeWorker() не было let name , то поиск шел бы снаружи и брал глобальную переменную, что мы видим из приведенной выше цепочки. В этом случае результатом было бы «John» .

Независимы ли счётчики?

важность: 5

Здесь мы делаем два счётчика: counter и counter2 , используя одну и ту же функцию makeCounter .

Они независимы? Что покажет второй счётчик? 0,1 или 2,3 или что-то ещё?

function makeCounter() < let count = 0; return function() < return count++; >; > let counter = makeCounter(); let counter2 = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter2() ); // ? alert( counter2() ); // ?

Ответ: 0,1.

Функции counter и counter2 созданы разными вызовами makeCounter .

Так что у них независимые внешние лексические окружения, у каждого из которых свой собственный count .

Замыкания в JavaScript для начинающих

Замыкания — это одна из фундаментальных концепций JavaScript, вызывающая сложности у многих новичков, знать и понимать которую должен каждый JS-программист. Хорошо разобравшись с замыканиями, вы сможете писать более качественный, эффективный и чистый код. А это, в свою очередь, будет способствовать вашему профессиональному росту.

Материал, перевод которого мы публикуем сегодня, посвящён рассказу о внутренних механизмах замыканий и о том, как они работают в JavaScript-программах.

Что такое замыкание?

Замыкание — это функция, у которой есть доступ к области видимости, сформированной внешней по отношению к ней функции даже после того, как эта внешняя функция завершила работу. Это значит, что в замыкании могут храниться переменные, объявленные во внешней функции и переданные ей аргументы. Прежде чем мы перейдём, собственно, к замыканиям, разберёмся с понятием «лексическое окружение».

Что такое лексическое окружение?

Понятие «лексическое окружение» или «статическое окружение» в JavaScript относится к возможности доступа к переменным, функциям и объектам на основе их физического расположения в исходном коде. Рассмотрим пример:

let a = 'global'; function outer() < let b = 'outer'; function inner() < let c = 'inner' console.log(c); // 'inner' console.log(b); // 'outer' console.log(a); // 'global' >console.log(a); // 'global' console.log(b); // 'outer' inner(); > outer(); console.log(a); // 'global'

Здесь у функции inner() есть доступ к переменным, объявленным в её собственной области видимости, в области видимости функции outer() и в глобальной области видимости. Функция outer() имеет доступ к переменным, объявленным в её собственной области видимости и в глобальной области видимости.

Цепочка областей видимости вышеприведённого кода будет выглядеть так:

Global < outer < inner >>

Обратите внимание на то, что функция inner() окружена лексическим окружением функции outer() , которая, в свою очередь, окружена глобальной областью видимости. Именно поэтому функция inner() может получить доступ к переменным, объявленным в функции outer() и в глобальной области видимости.

Практические примеры замыканий

Рассмотрим, прежде чем разбирать тонкости внутреннего устройства замыканий, несколько практических примеров.

▍Пример №1

function person() < let name = 'Peter'; return function displayName() < console.log(name); >; > let peter = person(); peter(); // 'Peter'

Здесь мы вызываем функцию person() , которая возвращает внутреннюю функцию displayName() , и сохраняем эту функцию в переменной peter . Когда мы, после этого, вызываем функцию peter() (соответствующая переменная, на самом деле, хранит ссылку на функцию displayName() ), в консоль выводится имя Peter .

При этом в функции displayName() нет переменной с именем name , поэтому мы можем сделать вывод о том, что эта функция может каким-то образом получать доступ к переменной, объявленной во внешней по отношению к ней функции, person() , даже после того, как эта функция отработала. Возможно это так из-за того, что функция displayName() , на самом деле, является замыканием.

▍Пример №2

function getCounter() < let counter = 0; return function() < return counter++; >> let count = getCounter(); console.log(count()); // 0 console.log(count()); // 1 console.log(count()); // 2

Тут, как и в предыдущем примере, мы храним ссылку на анонимную внутреннюю функцию, возвращённую функцией getCounter() , в переменной count . Так как функция count() представляет собой замыкание, она может обращаться к переменной counter функции getCount() даже после того, как функция getCounter() завершила работу.

Обратите внимание на то, что значение переменной counter не сбрасывается в 0 при каждом вызове функции count() . Может показаться, что оно должно сбрасываться в 0, как могло бы быть при вызове обычной функции, но этого не происходит.

Всё работает именно так из-за того, что при каждом вызове функции count() для неё создаётся новая область видимости, но существует лишь одна область видимости для функции getCounter() . Так как переменная counter объявлена в области видимости функции getCounter() , её значение между вызовами функции count() сохраняется, не сбрасываясь в 0.

Как работают замыкания?

До сих пор мы говорили о том, что такое замыкания, и рассматривали практические примеры. Теперь поговорим о внутренних механизмах JavaScript, обеспечивающих их работу.

Для того чтобы понять замыкания, нам нужно разобраться с двумя важнейшими концепциями JavaScript. Это — контекст выполнения (Execution Context) и лексическое окружение (Lexical Environment).

▍Контекст выполнения

Контекст выполнения — это абстрактное окружение, в котором вычисляется и выполняется JavaScript-код. Когда выполняется глобальный код, это происходит внутри глобального контекста выполнения. Код функции выполняется внутри контекста выполнения функции.

В некий момент времени может выполняться код лишь в одном контексте выполнения (JavaScript — однопоточный язык программирования). Управление этими процессами ведётся с использованием так называемого стека вызовов (Call Stack).

Стек вызовов — это структура данных, устроенная по принципу LIFO (Last In, First Out — последним вошёл, первым вышел). Новые элементы можно помещать только в верхнюю часть стека, и только из неё же элементы можно изымать.

Текущий контекст выполнения всегда будет в верхней части стека, и когда текущая функция завершает работу, её контекст выполнения извлекается из стека и управление передаётся контексту выполнения, который был расположен ниже контекста этой функции в стеке вызовов.

Рассмотрим следующий пример для того, чтобы лучше разобраться в том, что такое контекст выполнения и стек вызовов:

Пример контекста выполнения

Когда выполняется этот код, JavaScript-движок создаёт глобальный контекст выполнения для выполнения глобального кода, а когда встречает вызов функции first() , создаёт новый контекст выполнения для этой функции и помещает его в верхнюю часть стека.

Стек вызовов этого кода выглядит так:

Стек вызовов

Когда завершается выполнение функции first() , её контекст выполнения извлекается из стека вызовов и управление передаётся контексту выполнения, находящемуся ниже его, то есть — глобальному контексту. После этого будет выполнен оставшийся в глобальной области видимости код.

▍Лексическое окружение

Каждый раз, когда JS-движок создаёт контекст выполнения для выполнения функции или глобального кода, он создаёт и новое лексическое окружение для хранения переменных, объявляемых в этой функции в процессе её выполнения.

Лексическое окружение — это структура данных, которая хранит сведения о соответствии идентификаторов и переменных. Здесь «идентификатор» — это имя переменной или функции, а «переменная» — это ссылка на объект (сюда входят и функции) или значение примитивного типа.

Лексическое окружение содержит два компонента:

  • Запись окружения (environment record) — место, где хранятся объявления переменных и функций.
  • Ссылка на внешнее окружение (reference to the outer environment) — ссылка, позволяющая обращаться к внешнему (родительскому) лексическому окружению. Это — самый важный компонент, с которым нужно разобраться для того, чтобы понять замыкания.
lexicalEnvironment = < environmentRecord: < : , : > outer: < Reference to the parent lexical environment>>

Взглянем на следующий фрагмент кода:

let a = 'Hello World!'; function first() < let b = 25; console.log('Inside first function'); >first(); console.log('Inside global execution context');

Когда JS-движок создаёт глобальный контекст выполнения для выполнения глобального кода, он создаёт и новое лексическое окружение для хранения переменных и функций, объявленных в глобальной области видимости. В результате лексическое окружение глобальной области видимости будет выглядеть так:

globalLexicalEnvironment = < environmentRecord: < a : 'Hello World!', first : < reference to function object >> outer: null >

Обратите внимание на то, что ссылка на внешнее лексическое окружение ( outer ) установлена в значение null , так как у глобальной области видимости нет внешнего лексического окружения.

Когда движок создаёт контекст выполнения для функции first() , он создаёт и лексическое окружение для хранения переменных, объявленных в этой функции в ходе её выполнения. В результате лексическое окружение функции будет выглядеть так:

functionLexicalEnvironment = < environmentRecord: < b : 25, >outer: >

Ссылка на внешнее лексическое окружение функции установлена в значение , так как в исходном коде код функции находится в глобальной области видимости.

Обратите внимание на то, что когда функция завершит работу, её контекст выполнения извлекается из стека вызовов, но её лексическое окружение может быть удалено из памяти, а может и остаться там. Это зависит от того, существуют ли в других лексических окружениях ссылки на данное лексическое окружение в виде ссылок на внешнее лексическое окружение.

Подробный разбор примеров работы с замыканиями

Теперь, когда мы вооружились знаниями о контексте выполнения и о лексическом окружении, вернёмся к замыканиям и более глубоко проанализируем те же фрагменты кода, которые мы уже рассматривали.

▍Пример №1

Взгляните на данный фрагмент кода:

function person() < let name = 'Peter'; return function displayName() < console.log(name); >; > let peter = person(); peter(); // 'Peter'

Когда выполняется функция person() , JS-движок создаёт новый контекст выполнения и новое лексическое окружение для этой функции. Завершая работу, функция возвращает функцию displayName() , в переменную peter записывается ссылка на эту функцию.

Её лексическое окружение будет выглядеть так:

personLexicalEnvironment = < environmentRecord: < name : 'Peter', displayName: < displayName function reference>> outer: >

Когда функция person() завершает работу, её контекст выполнения извлекается из стека. Но её лексическое окружение остаётся в памяти, так как ссылка на него есть в лексическом окружении её внутренней функции displayName() . В результате переменные, объявленные в этом лексическом окружении, остаются доступными.

Когда вызывается функция peter() (соответствующая переменная хранит ссылку на функцию displayName() ), JS-движок создаёт для этой функции новый контекст выполнения и новое лексическое окружение. Это лексическое окружение будет выглядеть так:

displayNameLexicalEnvironment = < environmentRecord: < >outer: >

В функции displayName() нет переменных, поэтому её запись окружения будет пустой. В процессе выполнения этой функции JS-движок попытается найти переменную name в лексическом окружении функции.

Так как в лексическом окружении функции displayName() искомое найти не удаётся, поиск продолжится во внешнем лексическом окружении, то есть, в лексическом окружении функции person() , которое всё ещё находится в памяти. Там движок находит нужную переменную и выводит её значение в консоль.

▍Пример №2

function getCounter() < let counter = 0; return function() < return counter++; >> let count = getCounter(); console.log(count()); // 0 console.log(count()); // 1 console.log(count()); // 2

Лексическое окружение функции getCounter() будет выглядеть так:

getCounterLexicalEnvironment = < environmentRecord: < counter: 0, : < reference to function>> outer: >

Эта функция возвращает анонимную функцию, которая назначается переменной count .

Когда выполняется функция count() , её лексическое окружение выглядит так:

countLexicalEnvironment = < environmentRecord: < >outer: >

При выполнении этой функции система будет искать переменную counter в её лексическом окружении. В данном случае, опять же, запись окружения функции пуста, поэтому поиск переменной продолжается во внешнем лексическом окружении функции.

Движок находит переменную, выводит её в консоль и инкрементирует переменную counter , хранящуюся в лексическом окружении функции getCounter() .

В результате лексическое окружение функции getCounter() после первого вызова функции count() будет выглядеть так:

getCounterLexicalEnvironment = < environmentRecord: < counter: 1, : < reference to function>> outer: >

При каждом следующем вызове функции count() JavaScript-движок создаёт новое лексическое окружение для этой функции и инкрементирует переменную counter , что приводит к изменениям в лексическом окружении функции getCounter() .

Итоги

В этом материале мы поговорили о том, что такое замыкания, и разобрали глубинные механизмы JavaScript, лежащие в их основе. Замыкания — одна из важнейших фундаментальных концепций JavaScript, её должен понимать каждый JS-разработчик. Понимание замыканий — это одна из ступеней пути к написанию эффективных и качественных приложений.

Уважаемые читатели! Если вы обладаете опытом JS-разработки — просим поделиться с начинающими практическими примерами применения замыканий.

  • Блог компании RUVDS.com
  • Веб-разработка
  • JavaScript

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *