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

Что такое замыкание в программировании

  • автор:

Замыкание (программирование)

Замыкание (англ. closure ) в программировании — процедура или функция, в теле которой присутствуют ссылки на переменные, объявленные вне тела этой функции и не в качестве её параметров (а в окружающем коде). Говоря другим языком, замыкание — это процедура или функция, которая ссылается на свободные переменные в своём лексическом контексте.

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

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

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

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

Реализации замыкания в языках программирования

Pascal

Пример работы замыканий на Pascal (Delphi c 2009 версии):

type TGenericFunction = reference to function: string; function Factory(const ASomeText: string):TGenericFunction; begin Result := function: string begin Result := ASomeText; end; end; var f1, f2: TGenericFunction; procedure TForm1.Button1Click(Sender: TObject); begin f1 := Factory('First'); f2 := Factory('Second'); Memo1.Lines.Add(f1); Memo1.Lines.Add(f2); end; 

В версиях начиная с 2009, этот код выведет в Memo строки First и Second. Когда переменной типа reference to *** присваивается совместимая по спецификации анонимная подпрограмма или метод, неявно создаётся и инициализируется экземпляр анонимного класса, с полями для хранения значений, используемых подпрограммой из контекста её объявления, методом выполнения (присвоенной подпрограммой) и счётчиком ссылок.

Scheme

Пример работы замыканий на Scheme:

(define (make-adder n) ; возвращает замкнутое лямбда-выражение (lambda (x) ; в котором x - связанная переменная, (+ x n))) ; а n - свободная (захваченная из внешнего контекста) (define add1 (make-adder 1)) ; делаем процедуру для прибавления 1 (add1 10) ; печатает 11 (define sub1 (make-adder -1)); делаем процедуру для вычитания 1 (sub1 10) ; печатает 9 

C#

Анонимные методы в C# 2.0 могут замыкаться на локальный контекст:

int[] ary =  1, 2, 3 >; int x = 2; var ary1 = Array.ConvertAllint, int>(ary, delegate(int elem)  return elem * x; >); // // or.. var ary2 = Array.ConvertAllint, int>(ary, elem =>  return elem * x; >); //

Функция Array.ConvertAll преобразует один список/массив в другой, применяя для каждого элемента передаваемую ей в качестве параметра функцию.

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

int[] ary =  1, 2, 3 >; var x = 2; var ary1 = ary.Select(elem => elem * x); //

Метод Select аналогичен методу Array.ConvertAll за тем исключением, что он принимает и возвращает IEnumerable.

C++

В языке C++ замыкание долгое время не поддерживалось. Однако новый стандарт языка C++11 вводит лямбда-функции и выражения, ограниченно поддерживающие замыкание:

functionint()> f()  int x = 0; return [=] () mutable return ++x; >; > auto fun = f(); for (int i = 0; i  5; ++i)  cout  ()  ; > 

VB.NET

В VB.NET 9.0 лямбда-функции могут быть только однострочными. Начиная с версии 10.0, можно использовать синтаксис для описания многострочных лямбда-функций.

Dim ary As Integer() = 1, 2, 3> Dim x As Integer = 2 ' VB.NET 9.0 - Dim ary1() As Integer = Array.ConvertAll(Of Integer, Integer)(ary, Function(elem) elem * x) ' VB.NET 10.0 - Dim ary2() As Integer = Array.ConvertAll(Of Integer, Integer)(ary, Function(elem) Return elem * x End Function) 

Ruby

Некоторые языки, такие как Ruby, позволяют выбирать различные способы замыканий по отношению к оператору возврата return . Вот пример на Ruby:

# ruby def foo f = Proc.new  return "return from foo from inside proc" > f.call # после вызова функции замыкания f осуществляется выход из foo # результатом работы функции foo является результат работы f замыкания return "return from foo" end def bar f = lambda  return "return from lambda" > f.call # после вызова функции замыкания f продолжается выполнение bar return "return from bar" end puts foo # печатает "return from foo from inside proc" puts bar # печатает "return from bar" 

И Proc.new , так же как и lambda , в этом примере — это способы создания замыкания, но семантика замыканий различна по отношению к оператору return .

PHP

PHP имеет встроенную поддержку замыканий начиная с версии 5.3. Пример замыкания. Локальная переменная $id будет увеличиваться при вызове возвращаемой функцией getAdder вложенной функции:

function getAdder()  $id = 1; return function() use (&$id) // use (&$id) для того чтобы передать в возвращаемую функцию внешнюю переменную $id return $id++; >; > $test= getAdder(); echo $test(); //1 $id увеличивается только после того, как возвращается, так как написано $id++ echo $test(); //2 echo $test(); //3 echo $test(); //4 

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

Java

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

class CalculationWindow extends JFrame  private JButton btnSave; . public final void calculateInSeparateThread(final URI uri)  // Выражение "new Thread() < . >" представляет собой пример анонимного класса. new Thread()  public void run()  // Имеет доступ к финальным (final) переменным: calculate(uri); // Имеет доступ к приватным членам содержащего класса: btnSave.setEnabled(true); > >.start(); > > 

Предполагалось, что версия Java-7 будет включать полную поддержку концепции замыканий, которые официально должны были называться «лямбда-выражения» (Lambda expressions), но этого не произошло. Теперь поддержка «лямбда-выражений» заявлена в версии Java-8 [2] .

Python

Пример с использованием замыканий и карринга:

# Реализация с помощью именованных функций: def taskerize(func_object): def unbound_closure(*args, **kwarg): def bound_closure(): return func_object(*args, **kwarg) return bound_closure return unbound_closure # Равносильная реализация с использованием lambda: taskerize = lambda func_object: ( lambda *args, **kwarg: ( lambda: func_object(*args, **kwarg) ) ) @taskerize # применение декоратора равнозначно записи testfunc = taskerize(testfunc) после объявления функции. def testfunc(a, b, c): return a + b * c f = testfunc(1, 2, 3) print f() # выведет 7 

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

# Реализация с помощью именованных функций: def make_adder(x): def adder(n): return x + n # захват переменной "x" из внешнего контекста return adder # То же самое, но через безымянные функции: make_adder = lambda x: ( lambda n: ( x + n ) ) f = make_adder(10) print f(5) # 15 print f(-1) # 9 
# Функция с кучей аргументов (26 шт.), делающая что-то невразумительное. def longfunc(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z): print 'Меня вызвали с такими аргументами: ', a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z return a + b * c - d / e + f / g - h * i - (j * (k - l) + m) + (n * o) / (p - q + r) + (s * (t + (u * (v + w)))) - (x * y * z) def curry(func_object, *args): def innerfunc(*local_args): # в функции выполняется замыкание на args и func_object из внешнего контекста return func_object(*(args + local_args)) # а еще нам нужно прилепить в конец тех аргументов, что у нас были, новые return innerfunc # По уже сложившейся традиции — то же самое, только лямбдами: curry = lambda func_object, *args: ( lambda *local_args: ( func_object( *(args + local_args) ) ) ) # "достраиваем" функцию, как пожелаем. f1 = curry(longfunc, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100) f2 = curry(f1, 110, 120, 130, 140) f3 = curry(f2, 150, 160, 170, 180, 190, 200) f4 = curry(f3, 210) # не обязательно использовать функцию, к которой был применен карринг, только один раз. f5 = curry(f4, 220, 230, 240, 250, 260) # раз f5b = curry(f4, 220, 230, 240, 250) # два! f6b = curry(f5b, 260) print f5() # выведет 2387403 print f6b() # опять выведет 2387403 # контроль того, что карринг всё сделал верно (вызываем функцию со всеми её 26-ю параметрами): print longfunc( # перенос значений аргументов функций на несколько строк не имеет ничего общего с каррингом. Нет, правда. 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260 ) # да, опять выведет 2387403. 

JavaScript

В JavaScript областью видимости локальных переменных (объявляемых словом var) является тело функции, внутри которой они определены. [3]

Если вы объявляете функцию внутри другой функции, первая получает доступ к переменным и аргументам последней:

function outerFn(myArg)  var myVar; function innerFn()  // имеет доступ к myVar и myArg > > 

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

Рассмотрим пример — функцию, возвращающую количество собственных вызовов:

function createCounter()  var numberOfCalls = 0; return function()  return ++numberOfCalls; > > var fn = createCounter(); fn(); // 1 fn(); // 2 fn(); // 3 

Perl

Пример с использованием замыканий на Perl:

# возвращает анонимную функцию sub adder my $x = shift(); # в котором x - свободная переменная, sub my $y = shift(); # а y - связанная переменная $x + $y; >; > $add1 = adder(1); # делаем процедуру для прибавления 1 print $add1->(10); # печатает 11 $sub1 = adder(-1); # делаем процедуру для вычитания 1 print $sub1->(10); # печатает 9 

Lua

Пример с использованием замыканий на Lua:

function makeaddfunc(x) -- Возвращает новую анонимную функцию, которая добавляет x к аргументу return function(y) -- Когда мы ссылаемся на переменную x, которая вне текущей области, -- и время жизни которой меньше, чем этой анонимной функции, -- Lua создаёт замыкание. return x + y end end plustwo = makeaddfunc(2) print(plustwo(5)) -- Выводит 7 

Haskell

В Haskell замыкания используются повсеместно в виде частичного применения аргументов к функциям (также известного как каррирование).

sum3 :: Int -> Int -> Int -> Int sum3 x y z = x + y + z

Определение функции «sum3» напоминает следующий код на C:

int sum3(int x, int y, int z)  return(x + y + z); > 

На самом деле «sum3» эквивалентна функции «sum3_desugared», по определению которой видно, что «sum3_desugared» принимает один аргумент «x» и возвращает новую функцию со связанной переменной «x». Новая функция также принимает только один аргумент «y» и возвращает функцию от одного аргумента «z».

sum3_desugared :: Int -> Int -> Int -> Int sum3_desugared = \x -> \y -> \z -> x + y + z

Псевдоопределение таких функций выглядит следующим образом («bounded» — это некоторые фиксированные значения, которые неявно хранятся вместе с функциями):

sum2_closure :: Int -> Int -> Int sum2_closure = \y -> \z -> bounded_from_sum3 + y + z sum1_closure :: Int -> Int sum1_closure = \z -> bounded_from_sum3 + bounded_from_sum2 + z sum_value :: Int sum_value = bounded_from_sum3 + bounded_from_sum2 + bounded_from_sum1 sum2_with42 = sum3 42 sum2_with42 = \y -> \z -> 42 + y + z sum1_with42_with13 = sum3 42 13 sum1_with42_with13 = sum2_with42 13 sum1_with42_with13 = \z -> 42 + 13 + z sum_with42_with13_with66 = sum3 42 13 66 sum_with42_with13_with66 = sum2_with42 13 66 sum_with42_with13_with66 = sum1_with42_with13 66 sum_with42_with13_with66 = 42 + 13 + 66 

Такой подход очень часто применяется для создания «специализированных» функций из более общих:

— (&&) :: Bool -> Bool -> Bool — liftM2 :: (Monad m) => (a -> b -> c) -> m a -> m b -> m c ( ) (Monad m) => m Bool -> m Bool -> m Bool ( ) = liftM2 (&&) — foldr :: (a -> b -> b) -> b -> [a] -> b custom_fold :: [a] -> b custom_fold = foldr k z where z = k x z =

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

Smalltalk

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

createClosureWithComparator: aComparator ^[ :each | ^ each < aComparator ] 

Выполнение метода создает замыкание, при использовании которого будет происходить сравнение произвольного аргумента each и связанного значения aComparator.

MATLAB

Пример реализации замыкания в MATLAB с использованием nested функций:

function d = some_func(a) function c = nested_func(b) c = a + b; end d = @nested_func; end >> f = some_func(10); f = @some_func/nested_func >> f(5) ans = 15 

Пример реализации замыкания в MATLAB с использованием анонимных функций:

>> f = @(x) @(y) x + y f = @(x)@(y)x+y >> ff = f(10) ff = @(y)x+y >> ff(5) ans = 15 

Objective-C

Пример реализации замыкания в Objective-c с использованием блоков(blocks):

typedef int (^Add)(int x,int y); Add addBlock = (^(int x, int y)  return x + y; >); int res = addBlock(5,6); NSLog(@"%i",res); >>11 

Common LISP

(defun photon-energy-common (planck) (lambda (freq) (* planck freq))) (setq photon-energy-hbar (photon-energy-common 1.054571726E-23)) (setq photon-energy-h (photon-energy-common 6.62606957E-23)) (funcall photon-energy-h 10E12) 

Go

package main import "fmt" func fibonacci() func() int  a, b := 0, 1 return func() int  a, b = b, a + b return b > > func main()  f := fibonacci() for i := 0; i < 10; ++i  fmt.Println(f()) > > 

См. также

  • Лямбда-исчисление с типами
  • Подстановка
  • Модель акторов

Примечания

  1. Blocks Can Be Closures — Containers, Blocks, and Iterators — Programming Ruby. The Pragmatic Programmer’s Guide.
  2. OpenJDK: Project Lambda
  3. Владимир Агафонкин Замыкания в JavaScript
  • Концепции языков программирования

Wikimedia Foundation . 2010 .

Полезное

Смотреть что такое «Замыкание (программирование)» в других словарях:

  • Замыкание (математика) — Замыкание: Термины В математике Замыкание (геометрия) Алгебраическое замыкание поля Оператор замыкания Замыкание отношения Замыкание относительно операции Замыкание (программирование) подпрограмма, сохраняющая контекст (привязку к переменным)… … Википедия
  • Замыкание множества — Замыкание: Термины В математике Замыкание (геометрия) Алгебраическое замыкание поля Оператор замыкания Замыкание отношения Замыкание относительно операции Замыкание (программирование) подпрограмма, сохраняющая контекст (привязку к переменным)… … Википедия
  • Замыкание — В Викисловаре есть статья «замыкание» Замыкание процесс или результат действия, сводящегося к ограничению или спрямлению чего либо … Википедия
  • Функциональное программирование на Питоне — Функциональное программирование является одной из парадигм, поддерживаемых языком программирования Python. Основными предпосылками для полноценного функционального программирования в Python являются: функции высших порядков, развитые средства… … Википедия
  • Функциональное программирование на Python — Функциональное программирование является одной из парадигм, поддерживаемых языком программирования Python. Основными предпосылками для полноценного функционального программирования в Python являются: функции высших порядков, развитые средства… … Википедия
  • Функциональная зависимость (программирование) — Функциональная зависимость концепция, лежащая в основе многих вопросов, связанных с реляционными базами данных, включая, в частности, их проектирование. Математически представляет бинарное отношение между множествами атрибутов данного… … Википедия
  • Продолжение (программирование) — Продолжение (англ. continuation) представляет состояние программы в определённый момент, которое может быть сохранено и использовано для перехода в это состояние. Продолжения содержат всю информацию, чтобы продолжить выполнения программы с… … Википедия
  • ECMAScript — Класс языка: мультипарадигменный: объектно ориентированное, обобщённое, функциональное, императивное, аспектно ориентированное, событийно ориентированное, прототипное программирование Появился в: 1995 Автор(ы) … Википедия
  • Лямбда-выражения — Лямбда выражение (в программировании) это специальный синтаксис для объявления анонимных функторов по месту их использования. Используя лямбда выражения, можно объявлять функции в любом месте кода. Обычно лямбда выражение допускает… … Википедия
  • Лямбда-выражение — В программировании лямбда или лямбда выражения это безымянная функция, объявляемая по месту ее непосредственного использования. Обычно лямбда допускает замыкание на лексический контекст, в котором она объявлена. Смотри также Лямбда исчисление… … Википедия
  • Обратная связь: Техподдержка, Реклама на сайте
  • �� Путешествия

Экспорт словарей на сайты, сделанные на PHP,
WordPress, MODx.

  • Пометить текст и поделитьсяИскать в этом же словареИскать синонимы
  • Искать во всех словарях
  • Искать в переводах
  • Искать в ИнтернетеИскать в этой же категории

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

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 .

Введение в замыкание. Closure

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

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

Инкапсуляция позволяет нам скрывать/показывать свойства функций и объектов.

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

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

Замыкания подобны объектам в том смысле, что они представляют собой механизм для хранения состояния:

Например, в приведенном ниже примере, мы не хотим показывать функцию launch для её вызова, а также не даём доступ к timeWithoutDesctruction :

const makeNuclearButton = () =>  // Переменные, опредёленные в области действия фабрики или конструктора // являются приватным для этой функции. // К ним нет доступа, если только мы не вернем их в качестве свойств объекта. let timeWithoutDesctruction = 0; const passTime = () => timeWithoutDesctruction++; // totalPeaceTime является привилегированной, так как она определена в области замыкания - // поэтому у неё есть доступ к timeWithoutDesctruction const totalPeaceTime = () => timeWithoutDesctruction; const launch = () =>  timeWithoutDesctruction = -1; return '��' > setInterval(passTime, 1000); return totalPeaceTime>; > const button = makeNuclearButton(); button // button.totalPeaceTime(); // 1

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

Эффективное использование памяти

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

const closureTest1 = function()  const bigArray = new Array(7000).fill('1'); console.log('created'); return function(index)  return bigArray[index]; > > const closureTest1Fn = closureTest1(); closureTest1Fn(500); closureTest1Fn(300); closureTest1Fn(100); created // from console.log('created'); // returned "1"
// IIFE (Immediately Invoked Function Expression) const closureTest2 = (function()  const bigArray = new Array(7000).fill('1'); console.log('created'); return function(index)  return bigArray[index]; > >)(); closureTest2(500); closureTest2(300); closureTest2(100); created // from console.log('created'); // returned "1"

Мы вызываем функцию closureTest1Fn и closureTest2 3 раза, но console.log выводится только один раз. Это происходит потому, что мы, благодаря замыканию, сохраняем в памяти значения bigArray и console.log .

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

function closureTest(index)  const bigArray = new Array(7000).fill('1'); console.log('created'); return bigArray[index]; >; closureTest(500); closureTest(300); closureTest(100); created // from console.log('created'); created // from console.log('created'); created // from console.log('created'); // returned "1"

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

Замыкания — 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 студентов

Наши выпускники работают в компаниях:

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

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