Замыкания — Python: Функции
Представим, что нам нужно передать значения между функциями, при этом нам не хочется создавать глобальные переменные или передавать их через параметры функции. В этом случае стоит использовать замыкания, которые мы разберем в этом уроке. Мы узнаем, что это такое, когда они возникают, как работать с ними и где они используются.
Что такое замыкания
Замыкания — это функция, которая запоминает значения из своей внешней области видимости, даже если эта область уже недоступна. Она создается, когда функция объявляется, и продолжает запоминать значения переменных даже после того, как вызывающая функция завершит свою работу.
Замыкания — это инструмент, который позволяет сохранять значения и состояние между вызовами функций, создавать функции на лету и возвращать их из других функций.
Когда возникают замыкания
Замыкание возникает, когда функция объявляется внутри другой функции и использует переменные из внешней функции. В этом случае внешняя функция создает замыкание, которое хранит ссылку на внешние переменные, используемые во внутренней функции. Замыкание позволяет внутренней функции получить доступ к этим переменным, даже если внешняя функция уже завершилась.
Рассмотрим пример кода, чтобы проиллюстрировать это понятие:
def outer_function(x): def inner_function(y): return x + y return inner_function closure = outer_function(10) print(closure(5)) # => 15
В этом примере мы создаем функцию outer_function , которая принимает аргумент x и возвращает внутреннюю функцию inner_function . Внутренняя функция также принимает аргумент y и возвращает сумму x и y .
Затем мы создаем замыкание closure , вызывая outer_function с аргументом 10 . Теперь closure ссылается на inner_function и хранит значение x как 10.
В конце вызываем closure с аргументом 5 и выводим результат — 15 . Замыкание closure сохраняет значение x как 10 между вызовами, поэтому оно может быть использовано внутри inner_function даже после того, как outer_function уже завершила свою работу.
Scope
Scope — это область видимости в Python, которая определяет доступность переменных внутри блока кода. Scope определяет, где переменные могут быть использованы, и какие имена переменных могут быть вызваны в каждой области кода.
В Python есть две области видимости переменных:
- Глобальная — относится к переменным, которые определены вне функций, классов или модулей. Если переменная определена в глобальной области видимости, она может быть использована в любом месте программы
- Локальная — относится к переменным, которые определены внутри функций, классов или методов. Если переменная определена в локальной области видимости функции, то она не может быть использована вне этой функции
x = 10 # глобальная переменная def my_func(): x = 5 # локальная переменная print("x внутри функции:", x) my_func() print("x вне функции:", x)
В этом примере у нас две переменные с именем x . Одна является глобальной переменной, определенной вне функции. А другая — локальная переменная, определенная внутри функции. Вывод этого кода будет следующим:
Когда мы обращаемся к переменной внутри функции, Python ищет ее сначала в локальной области видимости функции, а затем — в глобальной области видимости. Если переменная не найдена ни в одной из этих областей, Python генерирует исключение NameError .
Чтобы изменить значение глобальной переменной внутри функции, нужно явно объявить ее как глобальную с помощью ключевого слова global :
x = 5 print(x) # => 5 def foo(): global x x = 10 print(x) foo() # => 10 print(x) # => 10
Здесь мы объявляем переменную x как глобальную внутри функции foo с помощью ключевого слова global . После этого мы можем изменять ее значение, которое будет сохранено после выполнения функции.
nonlocal — это ключевое слово в Python, которое используется при замыканиях во внутренней функции. Оно позволяет изменять значение переменных, определенных во внешней функции.
def outer(): x = 1 def inner(): nonlocal x x = 2 print("outer:", x) inner() print("outer:", x) outer()
В этом примере мы создали две функции:
- outer — имеет переменную x со значением 1. Она выводит первоначальное значение x и затем вызывает функцию inner . После вызова inner она выводит новое значение x .
- inner — устанавливает значение x равному 2, используя ключевое слово nonlocal .
При выполнении этого кода будет выведено:
Значение переменной x изменяется в функции inner с помощью ключевого слова nonlocal и это изменение также отражается в функции outer .
Как работают замыкания
Замыкание состоит из двух частей:
- Внешняя функция
- Внутренняя функция
Внутренняя функция имеет доступ к переменным из внешней функции даже после того, как внешняя функция завершила свою работу. Это происходит, так как замыкание сохраняет ссылку на эти переменные, а не копирует их значение. Так замыкания могут использовать и изменять значения этих переменных между вызовами.
Теперь рассмотрим несколько примеров замыканий:
def counter(): count = 0 def inner(): nonlocal count count += 1 return count return inner c = counter() print(c()) # 1 print(c()) # 2 print(c()) # 3
В этом примере мы создаем функцию counter , которая возвращает внутреннюю функцию inner . Внутри counter мы определяем переменную count и возвращаем inner . Внутренняя функция inner использует переменную count , которая определена во внешней функции counter с помощью оператора nonlocal , и увеличивает ее значение на единицу при каждом вызове.
Затем мы создаем замыкание c , вызывая counter . Замыкание c ссылается на inner и хранит значение count равное 0 .
В конце вызываем c три раза и выводим результаты, которые должны быть 1 , 2 и 3 . Замыкание c сохраняет значение count между вызовами, поэтому переменная count увеличивается на единицу каждый раз, когда мы вызываем c .
Еще один пример:
def add_number(n): def inner(x): return x + n return inner add_five = add_number(5) add_ten = add_number(10) print(add_five(3)) # 8 print(add_ten(3)) # 13
В этом примере мы создаем функцию add_number , которая принимает аргумент n и возвращает внутреннюю функцию inner . Внутренняя функция inner также принимает аргумент x и возвращает сумму n и x .
Затем мы создаем два замыкания: add_five и add_ten , вызывая add_number с аргументами 5 и 10 соответственно.
В конце вызываем каждое замыкание с аргументом 3 и выводим результаты, которые должны быть 8 и 13 . Замыкания add_five и add_ten хранят значения n как 5 и 10 между вызовами, поэтому они могут быть использованы внутри inner_function даже после того, как add_number уже завершила свою работу.
Где применяются замыкания
Замыкания также используются для создания функций с доступом к переменным, которые не должны быть изменены другими функциями. Это может быть полезно, например, при создании генераторов случайных чисел или при работе с конфигурационными файлами.
Рассмотрим пример такой функции:
def password_protected(password): def inner(): if password == 'secret': print("Access granted") else: print("Access denied") return inner login = password_protected('secret') login() # Access granted
Здесь мы создаем функцию password_protected , которая возвращает внутреннюю функцию inner . Она проверяет, равен ли аргумент password , переданный при создании password_protected , ‘secret’ , и выводит соответствующее сообщение.
config = 'language': 'ru', 'timezone': 'UTC' > def get_config(key): def inner(): return config.get(key, None) return inner get_language = get_config('language') get_timezone = get_config('timezone') print(get_language()) # ru print(get_timezone()) # UTC
Здесь мы создаем словарь config и функцию get_config . Эта функция возвращает внутреннюю функцию inner , которая возвращает значение, связанное с переданным ключом. Затем мы создаем функции get_language и get_timezone с помощью get_config , чтобы получить значения из словаря config .
Выводы
Замыкания — это концепция функционального программирования, которая позволяет создавать функции с доступом к переменным, находящимся вне их области видимости. Они широко применяются в различных областях программирования, таких как работа с файлами, сетевыми протоколами и многопоточностью.
Основные преимущества замыканий в Python:
- Позволяют создавать функции, которые имеют доступ к переменным, находящимся вне их области видимости
- Позволяют создавать гибкие и эффективные решения для различных задач программирования
Основные недостатки замыканий:
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
JavaScript — Что такое замыкание?
Урок, в котором рассмотрим что такое замыкание в JavaScript и зачем оно нужно. После этого выполним несколько практических примеров. В первом примере разберём, как происходит замыкание, а во втором — некоторую реальную задачу с использованием front-end фреймворка Bootstrap. В конце урока познакомимся с тем, как можно использовать замыкания для создания приватных переменных и функций.
Замыкание. Как оно работает
В JavaScript функции могут находиться внутри других функций. Когда одна функция находится внутри другой, то внутренняя функция имеет доступ к переменным внешней функции. Другими словами, внутренняя функция, при вызове как бы «запоминает» место в котором она родилась (имеет ссылку на внешнее окружение).
Замыкание — это такой механизм в JavaScript, который даёт нам доступ к переменным внешней функции из внутренней.
В качестве примера рассмотрим функцию, которая в качестве результата будет возвращать другую функцию:
JavaScript
function sayHello() { const message = 'Привет, '; return function(name) { return message + name + '!'; } } const result = sayHello(); // ƒ (name) { return message + name + '!'; } console.log(result('Вася')); // "Привет, Вася!"
В этом примере внутреннюю функцию мы создали анонимной, т.к. к ней мы не будем обращаться по её имени. Это действие мы будем выполнять, используя result . Эту функцию при необходимости мы сможем вызвать в любом месте кода. При этом где бы мы это не делали, она всегда будет иметь доступ к своим внешним переменным.
Лексическое окружение
Чтобы разобраться, как этот пример работает, необходимо сначала рассмотреть, что такое лексическое окружение и когда оно создаётся. Лексическое окружение — это скрытый объект, который связан с функцией и создаётся при её запуске. В нём находятся все локальные переменные этой функции, ссылка на внешнее лексическое окружение, а также некоторая другая информация. Кстати, лексическое окружение в JavaScript создаётся также для скрипта и блоков кода.
- Глобальное лексическое окружение (1) будет создано самим скриптом, в нём будет находиться функция sayHello и константа result . У глобального окружения нет внешнего окружения (ссылка на внешнее окружение равна null ).
- Одно внутреннее лексическое окружение (2) будет создано при вызове функции sayHello , которая нам в качестве результата возвратит другую функцию (её мы сохраним в константу result ). В этом лексическом окружении (2) будет находиться переменная message со значением «Привет, » , и ссылка на внешнее (глобальное) окружение (1).
- Другое внутреннее лексическое окружение (3) соответствует вызову result(‘Вася’) . В нём находится одна переменная name со значением ‘Вася’ и ссылка на внешнее лексическое окружение (2), т.к. в JavaScript функция «запоминает» то место, в котором она была создана.
Таким образом, когда мы вызываем result(‘Вася’) , то создаётся лексическое окружение (3), в котором находится не только name со значением «Вася» , но и ссылка на внешнее окружение (2). Это внешнее окружение (2) было создано при запуске функции sayHello . Оно содержит переменную message со значением «Привет, » . Не смотря на то, что функция sayHello уже выполнилась, её лексическое окружение (2) нам доступно, т.к. у нас есть ссылка на него. А т.к. в лексическом окружении (3) нет переменной message , то оно будет искаться в следующем окружении, на которое указывает текущее. Т.е. в лексическом окружении (2). В этом окружении оно есть. Таким образом, в результате выполнения result(‘Вася’) нам будет возвращено «Привет, Вася!» .
Если после console.log(result(‘Вася’)) мы поместим ещё один вызов функции result , то для него создастся лексическое окружение (4), которое то же будет иметь ссылку на внешнего окружение (2). Но, так как в лексическом окружение (4) переменной message нет, то оно будет взято из окружения (2). В результате, нам в консоль будет выведено «Привет, Петя!» :
JavaScript
console.log(result('Петя')); // "Привет, Петя!"
Изменим немного пример:
JavaScript
const message = 'Привет, '; function sayHello() { return function(name) { return message + name + '!'; } } const result = sayHello(); // ƒ (name) { return message + name + '!'; } console.log(result('Вася')); // "Привет, Вася!"
В этом примере выполнение result(‘Вася’) нам также вернёт «Привет, Вася!» . Это произойдёт потому, что при поиске переменной message , интерпретатор, будет переходить по ссылкам, от одного лексического окружения к другому, начиная с текущего, пока не найдёт её. В данном случае он найдёт эту переменную в глобальном окружении.
Поиск переменной
Как же происходит поиск переменной? Поиск переменной всегда начинается с текущего лексического окружения. Т.е., если переменная будет сразу найдена в текущем лексическом окружении, то её дальнейший поиск прекратится и возвратится значение, которая эта переменная имеет здесь. Если искомая переменная в текущем окружении не будет найдена, то произойдёт переход к следующему окружению (ссылка на которое имеется в текущем). Если она не будет найдена в этом, то опять произойдёт переход к следующему окружению, и т.д. Если при поиске переменной, она будет найдена, то её дальнейший поиск прекратится и возвратится значение, которая она имеет здесь.
В качестве примера поместим константу message в другую функцию:
JavaScript
function getMessage() { const message = 'Привет, '; return message; } function sayHello() { return function(name) { return message + name + '!'; // Uncaught ReferenceError: message is not defined } } console.log(getMessage()); const result = sayHello(); // ƒ (name) { return message + name + '!'; } // произойдёт ошибка когда мы вызовем функцию result('Вася') console.log(result('Вася'));
В этом примере произойдёт ошибка, т.к. переменная message не будет найдена. Интерпретатор при её поиске перейдёт от текущего лексического окружения по ссылкам до глобального. А т.к. в нём этой переменной нет и ссылки на следующее окружение тоже (она равна null ), то интерпретатор выдаст ошибку и дальнейшее выполнение этого сценария прекратится.
Ещё один важный момент заключается в том, что лексические окружения создаются и изменяются в процессе выполнения кода. Рассмотрим это на следующем примере:
JavaScript
function sayHello() { return function(name) { return message + name + '!'; } } const result = sayHello(); // ƒ (name) { return message + name + '!'; } let message = 'Привет, '; console.log(result('Вася')); // "Привет, Вася!" message = 'Здравствуйте, '; console.log(result('Вася')); // "Здравствуйте, Вася!"
В этом примере, когда мы первый раз вызываем функцию result(‘Вася’) , в глобальном лексическом окружении переменная message имеет значение ‘Привет, ‘ . В результате мы получим строку «Привет, Вася!» . При втором вызове переменная message имеет уже значение ‘Здравствуйте, ‘ . В результате мы уже получим строку «Здравствуйте, Вася!» .
В JavaScript все функции, кроме функций-конструкторов, являются замыканиями. При вызове функций-конструкторов им в качестве внешнего окружения присваивается ссылка на глобальное окружение, и следовательно, они имеют доступ только к глобальным переменным.
Сборка мусора
В JavaScript лексическое окружение обычно удаляется после того, как функция выполнилась. Это происходит только тогда, когда у нас нет ссылок на это окружение. Как например, в этом примере:
JavaScript
function sayHello(name) { return 'Привет, ' + name + '!'; } console.log(sayHello('Вася')); // "Привет, Вася!"
Но в вышеприведённых примерах со вложенными функциями, у нас лексическое окружение внешней функции оставалась доступным после её выполнения. Т.к. на на неё оставалась ссылка у вложенной функции. А пока есть доступ к лексическому окружению, автоматический сборщик мусора не может его удалить, и оно остаётся держаться в памяти.
Для чего нужны замыкания? Замыкания, например, могут использоваться для «запоминания» параметров, защиты данных (инкапсуляции), привязывания функции к определённому контексту и др. Замыкания положены в основу многих паттернов (шаблонов для написания кода).
Использование замыкания для создания приватных переменных и функций
Замыкания в JavaScript можно использовать для создания приватных переменных и функций.
JavaScript
const counter = () => { // приватная переменная _counter let _counter = 0; // приватная функция _changeBy (изменяет значение переменой _counter на переданное ей значение в качестве аргумента) const _changeBy = (value) => { _counter += value; }; // возвращаемое значение функции (объект, состоящий из 3 методов) return { // публичный метод (функция) increment (для увеличения счетчика на 1) increment() { _changeBy(1); }, // публичный метод (функция) decrement (для уменьшения счетчика на 1) decrement() { _changeBy(-1); }, // публичный метод (функция) value (для получения текущего значения _counter) value() { return _counter; }, }; }; // создадим счетчик 1 const counter1 = counter(); // создадим счетчик 2 const counter2 = counter(); counter1.increment(); counter1.increment(); console.log(counter1.value()); // 2 counter1.decrement(); console.log(counter1.value()); // 1 counter2.decrement(); counter2.decrement(); console.log(counter2.value()); // -2
Напрямую обратиться к _counter и _changeBy нельзя.
JavaScript
console.log(counter1._counter); // undefined counter1._changeBy(1); // Uncaught TypeError: counter1._changeBy is not a function
Обратиться к ним можно только через функции increment , decrement и value .
Примеры для подробного рассмотрения лексического окружения и замыкания
JavaScript
function one() { console.log(num); // Uncaught ReferenceError: num is not defined } function two() { const num = 5; one(); } two();
В этом примере мы получим ошибку. Т.к. функция one имеет в качестве внешнего окружения глобальное, и, следовательно, не может получить доступ к переменной num даже не смотря на то, что вызываем мы её внутри функции two .
JavaScript
function one(num1) { console.log(num1 + num2); } function two() { const num2 = 20; one(num2); } two(); // ?
Какой ответ мы получим в результате выполнения этого примера?
JavaScript
const num2 = 3; function one(num1) { console.log(num1 + num2); } function two() { const num2 = 20; one(num2); } two(); // ?
Какой результат будет в результате выполнения этого примера?
JavaScript — Замыкание на примере
Рассмотрим на примере, как происходит замыкание в JavaScript.
Объявим некоторую функцию, например f1 . Внутри этой функции объявим ещё одну функцию f2 (внутреннюю) и вернём её в качестве результата первой. Функция f1 пусть имеет параметр (переменную) x , а функция f2 — параметр (переменную) y . Функция f2 кроме доступа к параметру x имеет ещё доступ и к параметру y (по цепочки областей видимости).
JavaScript
//родительская функция для f2 function f1(x) { //внутренняя функция f2 по отношению к f1 function f2(y) { return x + y; } //родительская функция возвращает в качестве результата внутреннюю функцию return f2; }
Теперь перейдём к самому интересному, а именно рассмотрим, что произойдёт, если некоторой переменной c1 присвоить вызов функции f1(2) .
JavaScript
var c1 = f1(2);
В результате выполнения функция f1(2) вернёт другую (внутреннюю) функцию f2 . Но, функция f2 в данном контексте позволяет получить значения переменных родительской функции ( f1 ) даже несмотря на то, что функция f1 уже завершила своё выполнение.
Посмотрим детальную информацию о функции:
JavaScript
console.dir(c1);
На изображение видно, что внутренняя функция запомнила окружение, в котором была создана. Она имеет доступ к переменной x родительской функции. Значение данной переменной ( x ) равно числу 2.
Теперь выведем в консоль значение функции c1(5) :
JavaScript
console.log(c1(5));
Данная инструкция отобразит в консоли результат сложения значений параметров x и y . Значение x функция f2 будет брать из родительской области видимости.
Повторим вышепредставленные действия, но уже используя другую переменную ( c2 ):
JavaScript
var c2= f1(5); console.dir(c2); console.log(c2(5));
Представим переменные и функции рассмотренного примера для наглядности в виде следующей схемы:
Итоговый js-код рассмотренного примера:
JavaScript
//родительская функция 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 являются очень интересной вещью. Они позволяют связать некоторые данные с функцией. Это очень похоже на то, как это реализовано в объекте, который позволяет связать свойства (переменные) и методы (действия над этими переменными). Такие задачи в веб-разработке попадаются очень часто. Давайте рассмотрим одну из подобных задач.
Допустим, необходимо создать несколько модальных окон на странице с привязкой их к конкретным кнопкам. Кроме этого в задании говорится ещё о том, что необходимо сделать так, чтобы можно было легко менять при необходимости заголовок и содержимое модального окна.
Кнопки, открывающие модальные окна:
Функция, возвращая в качестве результата другую функцию:
JavaScript
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); } }
Код, который выполняет создание модальных окон и установлением каждому из них заголовка и некоторого содержимого:
JavaScript
$(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.
'); });
Итоговый код (кнопки + скрипт):
Если необходимо изменить при наступлении каких-то событий заголовок и содержимое модального окна (например, второго), то это будет выглядеть так:
JavaScriptmodal2('Другой заголовок','
Другое содержимое.
');Для чего нужны замыкания (js)?
Читаю о замыканиях, хоть и не с первого раза, но немного стало понятно как это работает. Однако сразу,как у любого новичка, напросился вопрос, а для каких целей конкретно в js существуют замыкания? Объясните,пожалуйста, доступным языком.
- Вопрос задан более трёх лет назад
- 7932 просмотра
2 комментария
Простой 2 комментария
Думаю, не совсем корректный вопрос. Замыкания существуют не для каких-то целей, они просто существуют, потому что это особенности языка. Наверное стоит спросить, для каких целей используются замыкания. Хотя на этот вопрос будет непросто ответить.
Решения вопроса 1
Ну если доступным, языком, то тогда на примере.
Например, надо выводить в лог сообщение, а также номер строки и время прошедшее с момента зарузки страницы.
Если не использовать замыкание, то надо определить следующую функцию
function log(timespan, lineNumber, msg)
и две переменные
var start = Date.now(); var lineNumber = 1;
Вызываем log так
log(Date.now()-start, lineNumber++, "один"); log(Date.now()-start, lineNumber++, "два");
Очевидно, что это неудобно.
Если использовать замыкание, то пишем так
var log = (function () < // функция 1 var start = Date.now(); // текущее значение сохранятся в start var num = 1; // также используется в замыкании в функции 2. return function (msg) < // функция 2 - сохраняется в var log console.log(num++ + " " + (Date.now()-start) + " " + msg); >>)(); // () -- вызываем функцию 1
Вызываем так
log("один"); log("два");
lineNumber timespan msg ------------------------------ 1 0 один 2 1 два
Т.е. замыкание — это способ передачи данных в функцию.
Подробнее о замыканиях см Mozilla Developer Network
Примет взят отсюда
Ответ написан более трёх лет назад
Комментировать
Нравится 15 Комментировать
Ответы на вопрос 2
Замыкание это функция и внешние переменные, которые в нее приходят.
Переменная не приходит - замыкания нет.
Ответ написан более трёх лет назад
Комментировать
Нравится 1 Комментировать
Замыкания в JavaScript.
Допустим, мы вызываем внутри нашего кода какую-то функцию (назовем ее первичной функцией), т.е. какой-то код (назовем его вспомогательным), находящийся в теле этой функции. И этот вспомогательный код объявляет другую функцию (назовем ее вторичной функцией) и возвращает эту вторичную функцию в качестве возвращаемого значения.
Здесь ключевой момент– не в коде создается, а код создает. Т.е. если бы мы не вызвали код первичной функции, то вторичная функция бы не была создана. Ее и в памяти-то нигде не было бы. К ней нельзя было бы обратиться. Интерпретатор не размещает ее в памяти, пока не будет вызвана первичная функция.
В этом вспомогательном коде могут быть объявлены и инициализированы переменные, а внутри тела этой вторичной функции могут быть обращения к этим переменным, т.е. код может захватить некоторые ресурсы и ссылки на часть этих ресурсов передать в функцию.
Тогда эта возвращенная вторичная функция будет иметь такую особенность, что внутри нее будут ссылки на некие ресурсы, находящиеся где-то. И если бы эти ресурсы были удалены, то при запуске этой функции на выполнение произошла бы ошибка (были бы не найдены какие-то затребованные данные). Поэтому когда такая функция объявляется в выполняемом коде (т.е. создается динамически) и при возвращении из кода присваивается какой-то переменной, то чтобы эта операция имела смысл, интерпретатор сохраняет в памяти созданные в коде данные, на которые ссылается эта функция.
Именно память под данные, на которые ссылается вторичная функция, выделенная в момент выполнения кода первичной функции (в этот момент как раз создается и размещается в памяти и вторична функция) сохраняется. Остальная память, временно выделявшаяся при работе кода первичной функции, высвобождается.
Т.е. эту вторичную функцию можно рассматривать просто как обычный объект, содержащий в своих полях некоторые данные. Естественно, пока на этот объект где-то будут оставаться ссылки, будут сохраняться в памяти и данные этого объекта.
Пока где-то в коде существует действующая ссылка на эту вторичную функцию, будут работоспособными и все ссылки внутри этой функции на участки памяти с данными, которые выделялись при создании этой вторичной функции.
Если мы второй раз вызовем первичную функцию, мы создадим уже совершенно другой объект вторичной функции, со своими данными.
Эта вторичная функция, т.е. этот динамически созданный объект, называется, вы не поверите, - замыканием.
Ответ написан более трёх лет назад
Комментировать
Нравится 1 Комментировать
Ваш ответ на вопрос
Войдите, чтобы написать ответ
- JavaScript
- +1 ещё
Как добавлять в билд Vite файл js без type module?
- 1 подписчик
- 2 часа назад
- 9 просмотров
Замыкания в 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