Что такое «закрытие»? - PullRequest
       160

Что такое «закрытие»?

369 голосов
/ 31 августа 2008

Я задал вопрос о карринге, и были упомянуты закрытия. Что такое закрытие? Как это связано с карри?

Ответы [ 19 ]

608 голосов
/ 19 сентября 2011

Переменная область действия

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

function() {
  var a = 1;
  console.log(a); // works
}    
console.log(a); // fails

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

var a = 1;
function() {
  console.log(a); // works
}    
console.log(a); // works

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

Так мы обычно ожидаем, что все заработает.

Закрытие - это постоянная область видимости локальной переменной

Закрытие - это постоянная область действия, которая сохраняется в локальных переменных даже после того, как выполнение кода вышло из этого блока. Языки, которые поддерживают закрытие (такие как JavaScript, Swift и Ruby), позволят вам сохранить ссылку на область (включая ее родительские области) даже после того, как завершится выполнение блока, в котором были объявлены эти переменные, при условии сохранения ссылки в этот блок или функцию где-то.

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

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

Например

Вот действительно простой пример в JavaScript, который иллюстрирует суть:

outer = function() {
  var a = 1;
  var inner = function() {
    console.log(a);
  }
  return inner; // this returns a function
}

var fnc = outer(); // execute outer to get inner 
fnc();

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

Обычно при выходе из функции все ее локальные переменные стираются. Однако если мы возвращаем внутреннюю функцию и присваиваем ее переменной fnc, чтобы она сохранялась после выхода outer, все переменные, которые находились в области видимости, когда было определено inner, также сохраняются, Переменная a была закрыта - она ​​находится в закрытии.

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

Как вы могли догадаться, когда я звоню fnc(), он печатает значение a, которое равно "1".

На языке без замыканий переменная a была бы собрана и отброшена при выходе из функции outer. Вызов fnc привел бы к ошибке, потому что a больше не существует.

В JavaScript переменная a сохраняется, поскольку переменная область создается при первом объявлении функции и сохраняется до тех пор, пока функция продолжает существовать.

a относится к сфере применения outer. Область действия inner имеет родительский указатель на область действия outer. fnc - это переменная, которая указывает на inner. a сохраняется до тех пор, пока сохраняется fnc. a находится в закрытии.

85 голосов
/ 31 августа 2008

Я приведу пример (в JavaScript):

function makeCounter () {
  var count = 0;
  return function () {
    count += 1;
    return count;
  }
}

var x = makeCounter();

x(); returns 1

x(); returns 2

...etc...

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

Вот мой пример карри:

function add (a) {
  return function (b) {
    return a + b;
  }
}

var add3 = add(3);

add3(4); returns 7

Что вы можете видеть, так это то, что когда вы вызываете add с параметром a (который равен 3), это значение содержится в закрытии возвращаемой функции, которую мы определяем как add3. Таким образом, когда мы вызываем add3, он знает, где найти значение для выполнения сложения.

54 голосов
/ 31 августа 2008

Ответ Кайла довольно хорош. Я думаю, что единственным дополнительным разъяснением является то, что замыкание - это, по сути, снимок стека в момент создания лямбда-функции. Затем, когда функция выполняется повторно, стек восстанавливается до этого состояния перед выполнением функции. Таким образом, как упоминает Кайл, это скрытое значение (count) доступно при выполнении лямбда-функции.

28 голосов
/ 27 апреля 2016

Прежде всего, вопреки тому, что большинство людей здесь говорят вам, замыкание - это , а не функция ! Так что же это это?
Это набор символов, определенных в «окружающем контексте» функции (известном как environment ), которые делают его выражением CLOSED (то есть выражением, в котором каждый символ определен и имеет значение, поэтому его можно оценить).

Например, если у вас есть функция JavaScript:

function closed(x) {
  return x + 3;
}

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

Но если у вас есть такая функция:

function open(x) {
  return x*y + 3;
}

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

Это y просит об определении, но это определение не является частью функции - оно определяется где-то еще, в его «окружающем контексте» (также известном как среда ). По крайней мере, это то, на что мы надеемся: P

Например, это можно определить глобально:

var y = 7;

function open(x) {
  return x*y + 3;
}

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

var global = 2;

function wrapper(y) {
   var w = "unused";

   return function(x) {
     return x*y + 3;
   }

}

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

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

{
  global: 2,
  w: "unused",
  y: [whatever has been passed to that wrapper function as its parameter `y`]
}

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

{
  y: [whatever has been passed to that wrapper function as its parameter `y`]
}

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

Подробнее о теории здесь: https://stackoverflow.com/a/36878651/434562

Стоит отметить, что в приведенном выше примере функция-обертка возвращает свою внутреннюю функцию в качестве значения. Момент, когда мы вызываем эту функцию, может быть удален во времени с момента, когда функция была определена (или создана). В частности, его функция обертывания больше не работает, а его параметры, которые были в стеке вызовов, больше не присутствуют: P Это создает проблему, потому что внутренней функции требуется y, чтобы быть там при вызове! Другими словами, он требует, чтобы переменные были закрыты, чтобы как-то пережить функцию-обертку и быть там, когда это необходимо. Поэтому внутренняя функция должна сделать снимок этих переменных, которые закрывают их и хранят где-нибудь в безопасном месте для дальнейшего использования. (Где-то за пределами стека вызовов.)

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

26 голосов
/ 31 августа 2008

Закрытие - это функция, которая может ссылаться на состояние в другой функции. Например, в Python это использует замыкание «внутреннее»:

def outer (a):
    b = "variable in outer()"
    def inner (c):
        print a, b, c
    return inner

# Now the return value from outer() can be saved for later
func = outer ("test")
func (1) # prints "test variable in outer() 1
23 голосов
/ 31 августа 2008

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

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

(define x 3)

(define y 4)

(+ x y) returns 7

Определительные выражения хранят значение 3 в месте для x и значение 4 в месте для y. Затем, когда мы вызываем (+ x y), интерпретатор ищет значения в пространстве имен и может выполнить операцию и вернуть 7.

Однако в Scheme есть выражения, позволяющие временно переопределить значение символа. Вот пример:

(define x 3)

(define y 4)

(let ((x 5))
   (+ x y)) returns 9

x returns 3

Что делает ключевое слово let, так это вводит новое пространство имен с x в качестве значения 5. Вы заметите, что он по-прежнему может видеть, что y равен 4, что делает возвращаемую сумму равной 9. Вы также можете увидеть это, как только выражение закончилось, х вернулся к значению 3. В этом смысле х был временно замаскирован локальным значением.

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

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

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

(define x 3)

(define (plus-x y)
  (+ x y))

(let ((x 5))
  (plus-x 4)) returns ?

Мы определяем x как 3 и плюс-x как его параметр, y, плюс значение x. Наконец, мы вызываем plus-x в среде, где x был замаскирован новым x, этот оценивается как 5. Если мы просто сохраняем операцию (+ xy) для функции plus-x, так как мы находимся в контексте если x равен 5, возвращаемый результат будет равен 9. Это то, что называется динамической областью видимости.

Однако в Scheme, Common Lisp и многих других языках есть то, что называется лексической областью видимости - в дополнение к сохранению операции (+ x y) мы также сохраняем пространство имен в этой конкретной точке. Таким образом, когда мы ищем значения, мы видим, что x в данном контексте действительно равно 3. Это закрытие.

(define x 3)

(define (plus-x y)
  (+ x y))

(let ((x 5))
  (plus-x 4)) returns 7

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

10 голосов
/ 07 ноября 2010

Вот реальный пример того, почему Closures надрывают задницу ... Это прямо из моего кода Javascript. Позвольте мне проиллюстрировать.

Function.prototype.delay = function(ms /*[, arg...]*/) {
  var fn = this,
      args = Array.prototype.slice.call(arguments, 1);

  return window.setTimeout(function() {
      return fn.apply(fn, args);
  }, ms);
};

А вот как вы бы это использовали:

var startPlayback = function(track) {
  Player.play(track);  
};
startPlayback(someTrack);

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

startPlayback.delay(5000, someTrack);
// Keep going, do other things

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

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

6 голосов
/ 05 июля 2017

ТЛ; др

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

Подробное объяснение стиля Википедии

Согласно Википедии, закрытие составляет:

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

Что это значит? Давайте посмотрим на некоторые определения.

Я объясню замыкания и другие связанные определения, используя этот пример:

function startAt(x) {
    return function (y) {
        return x + y;
    }
}

var closure1 = startAt(1);
var closure2 = startAt(5);

console.log(closure1(3)); // 4 (x == 1, y == 3)
console.log(closure2(3)); // 8 (x == 5, y == 3)

Первоклассные функции

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

В приведенном выше примере startAt возвращает функцию ( анонимная ), функция которой назначается на closure1 и closure2. Итак, как вы видите, JavaScript обрабатывает функции так же, как и любые другие объекты (граждане первого класса).

Привязка имени

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

В приведенном выше примере:

  • В области видимости внутренней анонимной функции y связан с 3.
  • В области действия startAt, x связано с 1 или 5 (в зависимости от закрытия).

Внутри области действия анонимной функции x не привязан ни к какому значению, поэтому его необходимо разрешить в верхней (startAt) области.

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

Как Википедия говорит , область действия:

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

Есть две техники:

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

Для более подробного объяснения, проверьте этот вопрос и взгляните на Википедию .

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

Завершение (закрытие) вверх

В нашем примере, когда мы вызываем startAt, он вернет (первоклассную) функцию, которая будет присвоена closure1 и closure2, таким образом, создается замыкание, потому что переданные переменные 1 и 5 будет сохранено в области действия startAt, которая будет заключена в возвращенную анонимную функцию. Когда мы вызываем эту анонимную функцию через closure1 и closure2 с одинаковым аргументом (3), значение y будет найдено немедленно (так как это параметр этой функции), но x не ограничен областью действия анонимной функции, поэтому разрешение продолжается в (лексически) верхней области функции (которая была сохранена в замыкании), где x связано с 1 или 5. Теперь мы знаем все для суммирования, поэтому результат можно вернуть, а затем распечатать.

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

Карринг

Да, и вы также узнали, что такое curry : вы используете функции (замыкания) для передачи каждого аргумента операции вместо использования одной функции с несколькими параметрами.

5 голосов
/ 07 февраля 2016

Функции, не содержащие свободных переменных, называются чистыми функциями.

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

var pure = function pure(x){
  return x 
  // only own environment is used
}

var foo = "bar"

var closure = function closure(){
  return foo 
  // foo is a free variable from the outer environment
}

Источник: https://leanpub.com/javascriptallongesix/read#leanpub-auto-if-functions-without-free-variables-are-pure-are-closures-impure

4 голосов
/ 31 августа 2008

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

def n_times(a_thing)
  return lambda{|n| a_thing * n}
end

в приведенном выше коде lambda(|n| a_thing * n} - это закрытие, потому что лямбда (создатель анонимной функции) указывает a_thing.

Теперь, если вы поместите полученную анонимную функцию в переменную функции.

foo = n_times(4)

foo нарушит нормальное правило видимости и начнет использовать 4 внутри.

foo.call(3)

возвращает 12.

...