Книга: JavaScript. Подробное руководство, 6-е издание

8.6. Замыкания

8.6. Замыкания

Как и в большинстве языков программирования, в JavaScript используются лексические области видимости. Это означает, что при выполнении функций действуют области видимости переменных, которые имелись на момент их определения, а не на момент вызова. Для реализации лексической области видимости внутренняя информация о состоянии объекта функции в языке JavaScript должна включать не только программный код функции, но еще и ссылку на текущую цепочку областей видимости. (Прежде чем продолжить чтение этого раздела, вам, возможно, следует повторно прочитать сведения об областях видимости переменных и цепочках областей видимости в разделах 3.10 и 3.10.3.) Такая комбинация объекта функции и области видимости (множества связанных переменных), в которой находятся переменные, используемые вызываемой функцией, в литературе по информационным технологиям называется замыканием}

Технически все функции в языке JavaScript образуют замыкания: они являются объектами и имеют ассоциированные с ними цепочки областей видимости. Большинство функций вызываются внутри той же цепочки областей видимости, которая действовала на момент определения функции, и в этой ситуации факт образования замыкания не имеет никакого значения. Интересные особенности замыканий начинают проявляться, когда их вызов производится в другой цепочке областей видимости, отличной от той, что действовала на момент определения. Чаще всего это происходит, когда объект вложенной функции возвращается функцией, вмещающей ее определение. Существует множество мощных приемов программирования, вовлекающих такого рода вложенные функции-замыкания, и их использование довольно широко распространено в программировании на языке JavaScript. Замыкания могут выглядеть малопонятными при первом знакомстве, однако вам необходимо хорошо понимать их, чтобы чувствовать себя уверенно при их использовании. 14 

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

var scope = "global scope”; // Глобальная переменная
function checkscope() {
  var scope = "local scope"; // Локальная переменная
  function f() { return scope; } // Вернет значение локальной переменной scope
  return f();
}
checkscope() // => "local scope"

Реализация замыканий

Понять суть замыканий будет совсем несложно, если усвоить правило лексической области видимости: во время выполнения функции используется цепочка областей видимости, которая действовала в момент ее определения. Однако некоторые программисты испытывают сложности при освоении замыканий, потому что не до конца понимают особенности реализации. Известно, думают они, что локальные переменные, объявленные во внешней функции, прекращают свое существование после выхода из внешней функции, но тогда как вложенная функция может использовать цепочку областей видимости, которая больше не существует? Если вы задавали себе такой вопрос, значит, у вас наверняка есть опыт работы с низкоуровневыми языками программирования, такими как С, и аппаратными архитектурами, использующими стек: если локальные переменные размещать на стеке, они действительно прекращают свое существование после завершения функции.

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

*********************************************************

Функция checkscope() объявляет локальную переменную и вызывает функцию, возвращающую значение этой переменной. Должно быть совершенно понятно, почему вызов checkscope() возвращает строку «local scope». Теперь немного изменим пример. Сможете ли вы сказать, какое значение вернет этот фрагмент?

var scope = "global scope"; // Глобальная переменная
function checkscope() {
  var scope = "local scope"; // Локальная переменная
  function f() { return scope; } // Вернет значение локальной переменной scope
  return f;
}
checkscope()() // Какое значение вернет этот вызов?

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

Напомню главное правило лексической области видимости: при выполнении функции в языке JavaScript используется цепочка областей видимости, действовавшая на момент ее определения. Вложенная функция f() была определена в цепочке видимости, где переменная scope связана со значением «local scope». Эта связь остается действовать и при выполнении функции f, независимо от того, откуда был произведен ее вызов. Поэтому последняя строка в примере выше вернет «local scope», а не «global scope». Проще говоря, эта особенность является самой удивительной и мощной чертой замыканий: они сохраняют связь с локальными переменными (и параметрами) внешней функции, где они были определены.

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

var uniquelnteger = (function() { // Определение и вызов
  var counter =0; // Частное значение для функции ниже
  return function() { return counter++; };
}());

Внимательно изучите этот пример, чтобы понять, как он действует. На первый взгляд, первая строка выглядит как инструкция присваивания функции переменной uniquelnteger. Фактически же это определение и вызов функции (как подсказывает открывающая круглая скобка в первой строке), поэтому в действительности переменной uniquelnteger присваивается значение, возвращаемое функцией. Если теперь обратить внимание на тело функции, можно увидеть, что она возвращает другую функцию. Именно этот объект вложенной функции и присваивается переменной uniquelnteger. Вложенная функция имеет доступ к переменным в ее области видимости и может использовать переменную counter, объявленную во внешней функции. После возврата из внешней функции никакой другой программный код не будет иметь доступа к переменной counter: вложенная функция будет обладать исключительным правом доступа к ней.

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

function counter() { var n = 0;
  return {
    count: function() { return n++; },
    reset: function() { n = 0; }
  };
}
var c = counter(), d = counter(); // Создать два счетчика
c.count()         // => 0
d.count()         // => 0: они действуют независимо
с.reset() // методы reset() и count() совместно
// используют одну переменную
c.count() // => 0: сброс счетчика с
d.count() // => 1: не оказывает влияния на счетчик d

Функция counter() возвращает объект «счетчика». Этот объект имеет два метода: count(), возвращающий следующее целое число, и reset(), сбрасывающий счетчик в начальное состояние. В первую очередь следует запомнить, что два метода совместно используют одну и ту же частную переменную n. Во-вторых, каждый вызов функции counter() создает новую цепочку областей видимости и новую скрытую переменную. То есть, если вызвать функцию counter() дважды, она вернет два объекта-счетчика с различными скрытыми переменными. Вызов методов count() и reset() одного объекта-счетчика не оказывает влияния на другой.

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

function counter(n) { // Аргумент n функции - скрытая переменная
  return {
    // Метод чтения свойства возвращает и увеличивает переменную счетчика,
    get count() { return n++; },
    // Метод записи в свойство не позволяет уменьшать значение n
    set count(m) {
      if (m >= n)
        n = m;
      else throw Error( "значение счетчика нельзя уменьшить");
    }
  };
}
var с = counter(1000);
с.count // => 1000
с.count // => 1001
с.count = 2000
с.count // => 2000
с.count = 2000 // => Ошибка!

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

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

Пример 8.4. Реализация методов доступа к частному свойству с использованием замыканий

// Эта функция добавляет методы доступа к свойству с заданным именем объекта о.
// Методы получают имена вида get<name> и set<name>. Если дополнительно предоставляется
// функция проверки, метод записи будет использовать ее для проверки значения
// перед сохранением. Если функция проверки возвращает false,
// метод записи генерирует исключение.
//
// Необычность такого подхода заключается в том, что значение свойства,
// доступного методам, сохраняется не в виде свойства объекта о, а в виде
// локальной переменной этой функции. Кроме того, методы доступа также определяются
// внутри этой функции и потому получают доступ к этой локальной переменной.
// Это означает, что значение доступно только этим двум методам и не может быть
// установлено или изменено иначе, как методом записи,
function addPrivateProperty(o, name, predicate) {
  var value; // Это значение свойства
  // Метод чтения просто возвращает значение.
  о["get" + name] = function() { return value; };
  // Метод записи сохраняет значение или возбуждает исключение,
  // если функция проверки отвергает это значение.
  o["set" + name] = function(v) {
    if (predicate && !predicate(v))
             throw Error("set" + name + недопустимое значение + v);
    else
      value = v;
  };
}
// Следующий фрагмент демонстрирует работу метода addPrivateProperty().
var о = {}; // Пустой объект
// Добавить к свойству методы доступа с именами getName() и setName()
// Обеспечить допустимость только строковых значений
addPrivateProperty(o, "Name", function(x) { return typeof x == "string"; });
o.setName("Frank"); // Установить значение свойства
console.log(o.getName()); // Получить значение свойства
о.setName(0); // Попробовать установить значение свойства неверного типа

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

// Эта функция возвращает функцию, которая всегда возвращает v
function constfunc(v) { return function() { return v; }; }
// Создать массив функций-констант:
var funcs = [];
for(var і = 0; і < 10; i++) funcs[i] = constfunc(i);
// Функция в элементе массива с индексом 5 возвращает 5.
funcs[5]() // => 5

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

// Возвращает массив функций, возвращающих значения 0-9
function constfuncs() { var funcs = [];
  for(var і = 0; і < 10; i++)
    funcs[i] = function() { return i; };
  return funcs;
}
var funcs = constfuncs();
funcs[5]() // Что вернет этот вызов?

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

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

var self = this; // Сохранить значение this в переменной для использования
// во вложенной функции.

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

var outerArguments = arguments; // Сохранить для использования во вложенных функциях

В примере 8.5, далее в этой главе, определяется замыкание, использующее эти приемы для получения доступа к значениям this и arguments внешней функции.

Оглавление книги


Генерация: 0.633. Запросов К БД/Cache: 2 / 0
поделиться
Вверх Вниз