Как работают замыкания javascript. JavaScript: область видимости и замыкание

Как работают замыкания javascript. JavaScript: область видимости и замыкание

Это практическое руководство по работе с замыканиями в JavaScript

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

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

В этой статье я попробую объяснить, как устроены замыкания и как они работают в JavaScript.

Начнём без промедлений 🙂

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

Замыкание - это функция, которая имеет доступ к своему внешнему окружению, даже после того, как внешняя функция возвращена. Другими словами - замыкание помнит и имеет доступ к переменным и аргументам внешней функции, даже после её завершения.

Перед тем как продолжить, давайте разберёмся с лексической областью видимости.

Что такое лексическая область видимости?

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

Let a = "global"; function outer() { let b = "outer"; function inner() { let c = "inner" console.log(c); // prints "inner" console.log(b); // prints "outer" console.log(a); // prints "global" } console.log(a); // prints "global" console.log(b); // prints "outer" inner(); } outer(); console.log(a); // prints "global"

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

Иерархия областей видимости в этом коде выглядит так:

Global { outer { inner } }

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

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

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

Пример №1

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

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

Пример №2

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

Обратите внимание, что значение counter не сбрасывается на 0 при каждом вызове функции count , как это обычно бывает.

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

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

Мы говорили о том, что такое замыкания и как они применяются на практике. Теперь давайте разберёмся, как они работают в JavaScript.

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

Контекст исполнения

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

В текущий момент может быть только один контекст исполнения (потому что JavaScript - однопоточный язык). Этот процесс управляется структурой данных стека, известным как Execution Stack или Call Stack.

Execution Stack - это стек со структурой LIFO (Last in, first out), в котором элементы могут быть добавлены или удалены только с верху стека.

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

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

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

Стек исполнения для этого кода выглядит вот так:

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

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

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

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

В лексическом окружении есть два компонента: 1) запись о внешних условиях и 2) ссылка на внешнюю среду.

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

Концептуально лексическое окружение выглядит так:

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

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

GlobalLexicalEnvironment = { environmentRecord: { a: "Hello World!", first: < reference to function object > } outer: null }

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

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

FunctionLexicalEnvironment = { environmentRecord: { b: 25, } outer: }

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

Примечание

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

Примеры замыканий. В деталях

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

Пример №1

Разберём этот код:

Function person() { let name = "Peter"; return function displayName() { console.log(name); }; } let peter = person(); peter(); // prints "Peter"

После выполнения функции person , движок JavaScript создаёт новый контекст исполнения и лексическое окружение для функции. После её завершения, мы возвращаем функцию displayName и присваиваем её к переменной peter .

PersonLexicalEnvironment = { environmentRecord: { name: "Peter", displayName: < displayName function reference> } outer: }

Когда функция person завершена, её контекст исполнения удаляется из стека. Но её лексическое окружение остаётся в памяти, потому что на него ссылается лексическое окружение внутренней функции displayName . Поэтому её переменные всё ещё доступны в памяти.

Когда функция peter выполнена (она является отсылкой к функции displayName), движок создаёт новый контекст исполнения и лексическое окружение для этой функции.

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

DisplayNameLexicalEnvironment = { environmentRecord: { } outer:

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

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

Пример №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: }

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

Движок найдёт переменную, выведет её в консоли и инкрементирует переменную счётчик в лексическом окружении функции getCounter .

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

GetCounterLexicalEnvironment = { environmentRecord: { counter: 1, : < reference to function> } outer: }

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

Заключение

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

Статья в доработке!

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

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

В JavaScript функции могут находиться внутри других функций.

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

Этот способ организации кода в JavaScript позволяет создавать замыкания .

Рассмотрим пример создания замыкания :

Function outerF(numA) { var numB = 5; function innerF(numC) { return numA + numB + numC; } return innerF; } var result = outerF(3); console.log(result(7)); // 15 console.log(result(10)); // 18

В переменной result будет находиться результат выполнения функции outerF(3) , т.е. ссылка на функцию innerF .

Функции в JavaScript «запоминают» окружение , в котором они были созданы. Осуществляют это они посредством скрытого свойства [].

В этом примере функция innerF «запомнит» своё окружение, т.е. она в своём скрытом свойстве [] будет содержать ссылку на область видимости, в которой она была создана, в данном случае это outerF(3) .

В результате получилось замыкание (closure) , т.е. такое состояние в котором некоторая функция (в примере это innerF) имеет доступ к внешнему окружению, в данном случае другой функции outerF(3) , которая завершило уже своё выполнение.

Почему так происходит? В JavaScript для очистки памяти используется автоматический сборщик мусора. Он, после того как функция завершает своё выполнение, просматривает «окружение», которое было создано при её запуске и если на него нет ссылок, то он его уничтожает. В вышеприведённом примере этого не произошло, т.к. переменная result , находящаяся в глобальной области видимости, содержит ссылку на функцию innerF . А функция innerF содержит ссылку посредством своего скрытого свойства [] на «окружение» outerF(3) . В результате «окружение» outetF(3) не может быть удалено автоматическим сборщиком мусора даже несмотря на то, что функция уже завершила своё выполнение. Это происходит из-за того, что на «окружение» outerF(3) существует [] ссылка в функции innerF , а ссылка на innerF имеется в переменной result , находящейся в глобальной области видимости.

Для чего нужны замыкания? Замыкания, например, могут использоваться для «запоминания» параметров, защиты данных (инкапсуляции), привязывания функции к определённому контексту и др. Замыкания положены в основу многих паттернов (шаблонов для написания кода).

JavaScript - Замыкание на примере

Рассмотрим на примере, как происходит замыкание в JavaScript.

Объявим некоторую функцию, например f1 . Внутри этой функции объявим ещё одну функцию f2 (внутреннюю) и вернём её в качестве результата первой. Функция f1 пусть имеет параметр (переменную) x , а функция f2 - параметр (переменную) y . Функция f2 кроме доступа к параметру x имеет ещё доступ и к параметру y (по цепочки областей видимости).

//родительская функция для f2 function f1(x) { //внутренняя функция f2 по отношению к f1 function f2(y) { return x + y; } //родительская функция возвращает в качестве результата внутреннюю функцию return f2; }

Теперь перейдём к самому интересному, а именно рассмотрим, что произойдёт, если некоторой переменной c1 присвоить вызов функции f1(2) .

Var c1 = f1(2);

В результате выполнения функция f1(2) вернёт другую (внутреннюю) функцию f2 . Но, функция f2 в данном контексте позволяет получить значения переменных родительской функции (f1) даже несмотря на то, что функция f1 уже завершила своё выполнение.

Посмотрим детальную информацию о функции:

Console.dir(c1);

На изображение видно, что внутренняя функция запомнила окружение, в котором была создана. Она имеет доступ к переменной x родительской функции. Значение данной переменной (x) равно числу 2.

Теперь выведем в консоль значение функции c1(5) :

Console.log(c1(5));

Данная инструкция отобразит в консоли результат сложения значений параметров x и y . Значение x функция f2 будет брать из родительской области видимости.

Повторим вышепредставленные действия, но уже используя другую переменную (c2):

Var c2= f1(5); console.dir(c2); console.log(c2(5));

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

Итоговый js-код рассмотренного примера:

//родительская функция function f1(x) { //внутренняя функция f2 function f2(y) { return x + y; } //родительская функция возвращает в качестве результата внутреннюю функцию return f2; } var c1 = f1(2); var c2 = f1(5); //отобразим детальную информацию о функции c1 console.dir(c1); //отобразим детальную информацию о функции c2 console.dir(c2); console.log(c1(5)); //7 console.log(c2(5)); //10

Замыкания на практике

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

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

Кнопки, открывающие модальные окна:

Кнопка 1 Кнопка 2 Кнопка 3

Функция, возвращая в качестве результата другую функцию:

Function modalContent(idModal,idButton){ //переменная, содержащая код модального окна Bootstrap var modal=""+ ""+ ""+ ""+ "×"+ ""+ ""+ "Закрыть"+ ""; //инструкция, добавляющая HTML-код модального окна сразу после открывающего тега body $(modal).prependTo("body"); //связываем модальное окно с кнопкой: $("#"+idButton).click(function(){ $("#"+idModal).modal("show"); }); // функция modalContent возвращает в качестве результата другую функцию return function(modalTitle,modalBody) { //устанавливаем заголовок модальному окну $("#"+idModal).find(".modal-title").html(modalTitle); //устанавливаем модальному окну содержимое $("#"+idModal).find(".modal-body").html(modalBody); } }

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

$(function(){ //1 модальное окно var modal1 = modalContent("modal1","myButton1"); modal1("Заголовок 1","

Содержимое 1...

Содержимое 2...

Содержимое 3...

"); });

Итоговый код (кнопки + скрипт):

function modalContent(idModal,idButton){ var modal=""+ ""+ ""+ ""+ "×"+ ""+ ""+ "Закрыть"+ ""; $(modal).prependTo("body"); $("#"+idButton).click(function(){ $("#"+idModal).modal("show"); }); return function(modalTitle,modalBody) { $("#"+idModal).find(".modal-title").html(modalTitle); $("#"+idModal).find(".modal-body").html(modalBody); } } $(function(){ //1 модальное окно var modal1 = modalContent("modal1","myButton1"); modal1("Заголовок 1","

Содержимое 1...

"); //2 модальное окно var modal2 = modalContent("modal2","myButton2"); modal2("Заголовок 2","

Содержимое 2...

"); //3 модальное окно var modal3 = modalContent("modal3","myButton3"); modal3("Заголовок 3","

Содержимое 3...

"); }); Кнопка 1 Кнопка 2 Кнопка 3

Если необходимо изменить при наступлении каких-то событий заголовок и содержимое модального окна (например, второго), то это будет выглядеть так:

Modal2("Другой заголовок","

Другое содержимое...

");

Создание приватных методов посредством замыканий

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

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

HTML-код кнопок:

Кнопка 1 Кнопка 2

Функция, имеет приватную переменную _count и функцию (метод) incrementCount . Для управления приватными методами предназначены публичные методы (increment() и value), которая данная функция возвращает в качестве результата.

Var countButtonClick = function () { //приватная переменная var _count = 0; //приватная функция (увеличивает значение на 1) function incrementCount() { _count++; } //результат, который возвращает функция в результате своего выполнения return { increment: function() { incrementCount(); }, value: function() { return _count; } } }

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

$(function(){ var countClickBtn1 = countButtonClick(); var countClickBtn2 = countButtonClick(); $("#btn1").click(function(){ countClickBtn1.increment() console.log(countClickBtn1.value()); }); $("#btn2").click(function(){ countClickBtn2.increment() console.log(countClickBtn2.value()); }); });

Итоговый код:

Кнопка 1 Кнопка 2 var countButtonClick = function () { //приватная переменная var _count = 0; //приватная функция (увеличивает значение на 1) function incrementCount() { _count++; } //результат, который возвращает функция в результате своего выполнения return { increment: function() { incrementCount(); }, value: function() { return _count; } } } $(function(){ var countClickBtn1 = countButtonClick(); var countClickBtn2 = countButtonClick(); $("#btn1").click(function(){ countClickBtn1.increment() console.log(countClickBtn1.value()); }); $("#btn2").click(function(){ countClickBtn2.increment() console.log(countClickBtn2.value()); }); });

Всем привет! В этой статье мы рассмотрим, что такое замыкание в javascript .

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

Function greeting(name) {
// LexicalEnvironment = {name: "Николай", text: undefined}
var text = "Здравствуйте, " + name;
// LexicalEnvironment = {name: "Николай", text: "Здравствуйте, Николай"}
alert(text);
}

Greeting("Николай");

Что здесь происходит и что такое LexicalEnvironment ? Давайте разберемся.

Когда функция вызывается, у нее создается объект LexicalEnvironment , в который записываются все локальные переменные и функции, а также ссылка на внешнюю область видимости(об этом позже). В нашем случае у нас есть локальная переменная name , у которой сразу есть значение(то, которое мы передаем) и это "Николай". В одной из статей я уже писал, однако напомню, что интерпретатор все знает про все переменные заранее. Именно по этому у нас в самом начале функции уже есть переменная text , интерпретатор знает про нее, но так как мы еще не дошли по присваивания этой переменной какого-то значения, то она равна undefined . Теперь мы присваиваем переменной значение, и наш объект LexicalEnvironment меняется. Его свойство text становится равным тому, что мы записали("Здравствуйте, Николай" в нашем случае). После того, как функция отработала, объект LexicalEnvironment уничтожается. При последующих вызовах функции он будет создан снова и т.д.

Теперь перейдем к следующему примеру. Скажите, что будет выведено в этом случае?

Var b = 2;
function x(a) {
alert(a + b);
}
x(1);

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

На самом деле в javascript есть скрытое свойство, которое называется [] . Когда функция объявляется, то она всегда объявляется где-то. Эта функция может быть в другой функции, может быть в глобальном объекте и т.д. В нашем случае функция объявлена в глобальном объекте window , поэтому свойство x.[] = window .

Var b = 2;
function x(a) { // x.[] = window
// LexicalEnvironment = {a: 1} -> window
alert(a + b);
}
x(1);

Эта стрелочка у объекта LexicalEnvironment - это ссылка на внешнюю область видимости, и эта ссылка устанавливается по свойству [] . Таким образом в объекте LexicalEnvironment у нас будет ссылка на внешний объект window . Когда интерпретатор ищет переменную, то он сначала ищет ее в объекте LexicalEnvironment , затем, если он не нашел переменную, то он смотрим в ссылку, переходит во внешнюю область видимости и ищет ее там и так до конца. Если он нигде этой переменной не нашел, то будет ошибка. В нашем случае переменную a интерпретатор возьмет из объекта LexicalEnvironment , а переменную b из объекта window . Конечно, если у нас будет локальная переменная b с каким-то значением, то она запишется в объект LexicalEnvironment и впоследствии будет взята оттуда, а не из внешней области видимости.

ВАЖНО! Запомните, что свойство [] устанавливается по тому месту, где функция была объявлена, а не вызвана, именно поэтому код ниже выведет число 3, а не 5, как некоторые могли подумать.

Bar b = 2;
function x(a) {
alert(a + b);
}

Function y() {
var b = 4;
x(1);
}

Это все была прелюдия только для того, чтобы вы поняли, как это все работает, и вам было легче понять, как работают замыкания. А теперь перейдем непосредственно к теме статьи.

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

Function greeting(name) {
// LexicalEnvironment = {name: "Николай"}
return function() { // [] = LexicalEnvironment
alert(name);
};
}

Var func = greeting("Николай");
greeting = null;
func();

Давайте посмотрим, что мы сделали. Сначала мы создаем функцию greeting , в которую передается имя. В функции создается объект LexicalEnvironment , где создается свойство(наша локальная переменная) name и ей присваивается имя "Николай". А теперь важно: мы возвращаем из функции другую функцию, внутри которой выводим через alert переменную name . Дальше мы присваиваем переменной func значение, возвращенное из функции greeting , а это значение - наша функция, которая выводит имя. Теперь мы greeting присваиваем null , т.е. мы просто уничтожаем нашу функцию greeting , однако, когда мы вызовем func , то увидим значение переменной name ("Николай") функции greeting . Как такое возможно, скажете вы? А очень просто. Все дело в том, что наша возвращаемая функция также имеет свойство [] , которое ссылается на внешнюю область видимости, а эта внешняя область видимости в нашем случае - объект LexicalEnvironment нашей функции greeting . Поэтому, несмотря на то, что мы удалили нашу функцию greeting , объект LexicalEnvironment не удалился и остался в памяти, и он будет оставаться в памяти до тех пор, пока на него будет хотя бы одна ссылка. У нас эта ссылка - наша возвращаемая функция, которая использует переменную name этого объекта LexicalEnvironment .

Итак, давайте теперь дадим определение тому, что такое замыкание .

Замыкание - функция вместе со всеми переменными, которые ей доступны.

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

Function makeCounter() {
var currentCount = 0;

Return function() {
currentCount++;
return currentCount;
};
}

Var counter = makeCounter();
counter();
counter();
alert(counter()); // 3

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

Function внешняя(x) { var tmp = 3; function внутренняя(y) { alert(x + y + (++tmp)); // выведет 16 } внутренняя(10); } внешняя(2);

Этот код всегда выдаёт 16, потому, что функция внутренняя видит x , который является переменной в функуции внешняя. В данном случае аргументом функции. Так же внутренняя() может видить tmp из внешней() .

Это и называется замыкание или closure. Если точнее, замыканием называется именно внешняя функция, а всё что внутри неё называется closure environment или среда замыкания.

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

Function foo(x) { var tmp = 3; return function (y) { alert(x + y + (++tmp)); // will also alert 16 } } var bar = foo(2); // bar is now a closure. bar(10);

Приведённая выше функция также выдаст 16, поскольку bar даже после завершения foo продолжает иметь доступ к x и tmp , пусть даже сама переменная bar и не находится внутри области видимости в которой они были объявлены.

При этом, поскольку переменная tmp всё ещё находится внутри замыкания bar , она продолжает увеличиваться при каждом вызове bar .

Вот простейший пример замыкания:

Var a = 10; function test() { console.log(a); // вывод 10 console.log(b); // вывод 6 } var b = 6; test();

При запуске функции в JavaScript, для неё создаётся окружение, то есть список всех видимых ей переменных, не только аргументов и переменных объявленных внутри неё, но и снаружи, в данном примере это "a" и "b".

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

Поскольку в нашем примере x это число, то его значение копируется в foo как его аргумент x .

С другой стороны, в JavaScript всегда используются ссылки, когда передаются объекты. Если бы вы вызвали foo с объектом в качестве аргумента, то возвращённое замыкание вернуло бы ссылку на оригинальный объект!

Function foo(x) { var tmp = 3; return function (y) { alert(x + y + tmp); x.memb = x.memb ? x.memb + 1: 1; alert(x.memb); } } var age = new Number(2); var bar = foo(age); // bar теперь замыкание ссылающееся на age. bar(10);

Как и следовало ожидать, каждый вызов bar(10) увеличивает x.memb . Чего вы могли не ожидать, так это, что x продолжает ссылаться на тот же самый объект, что и age ! После двух вызовов bar , age.memb будет равен 2! Кстати, так и происходят утечки памяти в HTML объектах.

Доброго времени суток, гики веб-разработки. Сегодня мы углубим ваши знания языка и разберем замыкания в JavaScript. Это очень важный, ключевой раздел при изучении JS, без которого по сути и «каши не сваришь».

Поэтому в текущей публикации мы пройдемся с вами по основным моментам замыкания для того, чтобы не быть чайниками. Я объясню, что это такое и с какими ошибками можно столкнуться. Также приведу несколько примеров для лучшего понимания материала. Как говорится: «Меньше слов, больше дела». Так что за дело!

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

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

Такое окружение называется лексической областью видимости – Lexical scoping . Если более простыми словами, то это механизм, который позволяет вложенным функциям использовать переменные, объявленные вовне их тела, и «замыкать» последние на себе.

Рассмотрим пример. В коде создается функция с названием IntCounter () , в которой объявляется локальная переменная calls и вложенная функция. Последняя должна возвращать количество вызовов в основном коде.

1 2 3 4 5 6 7 8 9 10 function IntCounter() { var calls = 0; return function() { return ++calls; } } var CountСalls = IntCounter (); CountСalls(); //1 CountСalls(); //2 CountСalls(); //3

function IntCounter() { var calls = 0; return function() { return ++calls; } } var CountСalls = IntCounter (); CountСalls(); //1 CountСalls(); //2 CountСalls(); //3

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

Именно поэтому в прикрепленном выше примере переменная calls продолжает свое существование и сохраняет последнее присвоенное значение.

Почему данный механизм возможен?

Вот тут будьте внимательны!!! Попытайтесь хорошенько разобрать и запомнить прочитанное. В будущем это поможет вам понимать более сложные вещи в .

Итак, ссылки на внешние переменные, объекты хранятся во внутреннем свойстве вложенной функции под названием [[ Scope]] . Это скрытое свойство, которое присваивается функциям при их создании и ссылается на их Lexical scoping .

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

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

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

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

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

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 Выберите размер шрифта
12 16 20

Том первый. Глава вторая.

Практический пример использования замыкания body { font-family: Arial, sans-serif; font-size: 14px; } h1 { font-size: 1.5em; } h2 { font-size: 1.2em; } Выберите размер шрифта
12 16 20

Здесь написан какой-то текст исторического романа.

Том первый. Глава вторая.

Продолжение увлекательной истории...

function ChangeSize(newSize) { return function() { document.body.style.fontSize = newSize + "px"; }; } var size12 = ChangeSize(12); var size16 = ChangeSize(16); var size20 = ChangeSize(20); document.getElementById("size_12").onclick = size12; document.getElementById("size_16").onclick = size16; document.getElementById("size_20").onclick = size20;

Наиболее распространенные ошибки

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

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

Помощник при изучении английских слов

Наведите на слово для получения перевода.

elections

electricity

electric

function showTranslation (translation) { document.getElementById("help").innerHTML = translation; } function DictionaryHelp() { var helpText = [ {"id": "1", "help": "Выборы"}, {"id": "2", "help": "Электричество"}, {"id": "3", "help": "Электрический"} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onmouseover = function() { showTranslation (item.help); } } } DictionaryHelp();

Однако при запуске программы и наведении на любое из слов, ответ всегда будет один и тот же – перевод слова «электрический».

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

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

Для решения этой проблемы в новых версиях (начиная с ECMAScript 6 ) можно использовать ключевое let . В других ситуациях следует обратиться за помощью к function factory .

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

Скрипт изменится следующим образом:

function showTranslation(translation) { document.getElementById("help").innerHTML = translation; } function HelpCallback(help) { return function() { showTranslation(help); }; } function DictionaryHelp() { var helpText = [ {"id": "1", "help": "Выборы"}, {"id": "2", "help": "Электричество"}, {"id": "3", "help": "Электрический"} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onmouseover = HelpCallback(item.help); } } DictionaryHelp();