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

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

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 сохраняется, поскольку область переменных создается при первом объявлении функции и сохраняется до тех пор, пока функция продолжает существовать.

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. Наконец, мы называем plus-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

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


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

например, когда у вас есть Функция 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;
   }

}

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

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

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

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


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

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


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

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

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

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

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

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

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

существует два метода:

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

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

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

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

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

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

карринг

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


вот еще один пример реальной жизни, и с помощью скриптового языка, популярного в играх-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