Что такое "закрытие"?


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

15 335

15 ответов:

переменная

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

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

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

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

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

это то, как мы обычно ожидаем, что все будет работать.

замыкание-это постоянная локальная переменная scope

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

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

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

вот очень простой пример в 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 был определен также persist. Переменная a был закрыт - он находится в пределах закрытия.

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

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

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

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

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

я приведу пример (в 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, он знает, где найти значение a для выполнения добавления.

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

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

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

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

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

(define x 3)

(define y 4)

(+ x y) returns 7

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

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

(define x 3)

(define y 4)

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

x returns 3

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

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

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

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

(define x 3)

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

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

мы определяем x как 3 и плюс-x, чтобы быть его параметром, y, плюс значение x. наконец, мы называем плюс-x в среде, где x был замаскирован новым x, это значение 5. Если мы просто сохраним операцию (+ x y) для функции 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

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

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

например, когда у вас есть Функция JavaScript:

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

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

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

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

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

этой 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;
   }

}

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

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

{
  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 чтобы быть там, когда это называется! Другими словами, он требует, чтобы переменные от его закрытия каким-то образом переживет функции-оболочки и быть там, когда это необходимо. Поэтому внутренняя функция должна сделать снимок из этих переменных, которые делают его закрытия и хранить их в безопасном месте для последующего использования. (Где-то за пределами вызова стек.)

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

вот реальный пример того, почему закрытие пинает задницу... Это прямо из моего кода 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 С 5000ms, первый фрагмент запускается и сохраняет переданный аргументы в его закрытии. Затем 5 секунд спустя, когда setTimeout обратный вызов происходит, закрытие по-прежнему поддерживает эти переменные, поэтому он может вызвать исходную функцию с исходными параметрами.
Это разновидность карринга, или функционального украшения.

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

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

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

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
}

src: https://leanpub.com/javascriptallongesix/read#leanpub-auto-if-functions-without-free-variables-are-pure-are-closures-impure

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

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.

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

.

tl; dr

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

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

согласно Википедии, закрытие - это:

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

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

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

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's scope,x обязан 1 или 5 (в зависимости от закрытия).

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

лексической области видимости

как Википедия говорит сфера:

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

есть два способа:

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

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

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

накрутка (closuring) до

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

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

карринг

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

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

local old_dofile = dofile

function dofile( filename )
  if filename == nil then
    error( 'Can not use default of stdin.' )
  end

  old_dofile( filename )
end

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

От Lua.org:

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

если вы из мира Java, вы можете сравнить закрытие с функцией-членом класса. Посмотрите на этот пример

var f=function(){
  var a=7;
  var g=function(){
    return a;
  }
  return g;
}

функции g - это закрытие: g закрывается a in. Так что g можно сравнить с функцией-членом, a можно сравнить с полем класса, а функцию f С классом.

закрытие Всякий раз, когда у нас есть функция, определенная внутри другой функции, внутренняя функция имеет доступ к переменным, объявленным во внешней функции. Закрытие лучше всего объяснить примерами. В листинге 2-18 видно, что внутренняя функция имеет доступ к переменной (variableInOuterFunction) из списка внешний объем. Переменные во внешней функции были закрыты (или связаны) внутренней функцией. Отсюда и термин закрытие. Концепция сама по себе достаточно проста и справедлива интуитивный.

Listing 2-18:
    function outerFunction(arg) {
     var variableInOuterFunction = arg;

     function bar() {
             console.log(variableInOuterFunction); // Access a variable from the outer scope
     }
     // Call the local function to demonstrate that it has access to arg
     bar(); 
    }
    outerFunction('hello closure!'); // logs hello closure!

источник: http://index-of.es/Varios/Basarat%20Ali%20Syed%20(auth.)-Beginning%20Node.js-Apress%20(2014).pdf