Тот факт, что JavaScript допускает объявление вложенных функций, позволяет использовать функции как обычные данные и способствует организации взаи_ модействий между цепочками областей видимости, что позволяет получать ин_ тересные и мощные эффекты. Прежде чем приступить к описанию, рассмотрим функцию g, которая определяется внутри функции f. Когда вызывается функ_ ция f, ее цепочка областей видимости содержит объект вызова, за которым сле_ дует глобальный объект. Функция g определяется внутри функции f, таким об_ разом, цепочка областей видимости этой функции сохраняется как часть опре_ деления функции g. Когда вызывается функция g, ее цепочка областей видимо_ сти содержит уже три объекта: собственный объект вызова, объект вызова функции f и глобальный объект.
158 Глава 8. Функции
Порядок работы вложенных функций совершенно понятен, когда они вызыва_ ются в той же лексической области видимости, в которой определены. Напри_ мер, следующий фрагмент не содержит ничего необычного:
var x = "глобальная"; function f( ) {
var x = "локальная"; function g() { alert(x); } g();
}
f(); // При обращении к этой функции будет выведено слово "локальная"
Однако в JavaScript функции рассматриваются как обычные данные, поэтому их можно возвращать из других функций, присваивать свойствам объектов, со_ хранять в массивах и т. д. В этом нет ничего необычного до тех пор, пока на аре_ ну не выходят вложенные функции. Рассмотрим следующий фрагмент, где оп_ ределена функция, которая возвращает вложенную функцию. При каждом об_ ращении к ней она возвращает функцию. Сам JavaScript_код при этом не меня_ ется, но от вызова к вызову может изменяться область видимости, поскольку при каждом обращении к объемлющей функции могут изменяться ее аргумен_ ты. (То есть в цепочке областей видимости будет изменяться объект вызова объ_ емлющей функции.) Если попробовать сохранить возвращаемые функции в мас_ сиве и затем вызвать каждую из них, можно заметить, что они будут возвращать разные значения. Поскольку программный код функции при этом не изменяет_ ся и каждая из них вызывается в той же самой области видимости, единствен_ ное, чем можно объяснить разницу, – это различия между областями видимо_ сти, в которых функции были определены:
// Эта функция возвращает другую функцию
// От вызова к вызову изменяется область видимости,
// в которой была определена вложенная функция function makefunc(x) {
return function() { return x; }
}
// Вызвать makefunc() несколько раз и сохранить результаты в массиве: var a = [makefunc(0), makefunc(1), makefunc(2)];
// Теперь вызвать функции и вывести полученные от них значения.
// Хотя тело каждой функции остается неизменным, их области видимости
// изменяются, и при каждом вызове они возвращают разные значения: alert(a[0]( )); // Выведет 0
Результаты работы этого фрагмента в точности соответствуют ожиданиям, если строго следовать правилу лексической области видимости: функция исполняет_ ся в той области видимости, в которой она была определена. Однако самое инте_ ресное состоит в том, что области видимости продолжают существовать и после выхода из объемлющей функции. В обычной ситуации этого не происходит. Ко_ гда вызывается функция, создается объект вызова и размещается в ее области видимости. Когда функция завершает работу, объект вызова удаляется из це_ почки вызова. Пока дело не касается вложенных функций, цепочка видимости
8.8. Область видимости функций и замыкания
является единственной ссылкой на объект вызова. Когда ссылка на объект уда_ ляется из цепочки, в дело вступает сборщик мусора.
Однако ситуация меняется с появлением вложенных функций. Когда создается определение вложенной функции, оно содержит ссылку на объект вызова, по_ скольку этот объект находится на вершине цепочки областей видимости, в кото_ рой определяется функция. Если вложенная функция используется только внутри объемлющей функции, единственная ссылка на вложенную функцию – это объект вызова. Когда внешняя функция возвращает управление, вложенная функция ссылается на объект вызова, а объект вызова – на вложенную функ_ цию, и никаких других ссылок на них не существует, благодаря этому они ста_ новятся доступными для механизма сборки мусора.
Все меняется, если ссылка на вложенную функцию сохраняется в глобальной области видимости. Это происходит, когда вложенная функция передается в ви_ де возвращаемого значения объемлющей функции или сохраняется в виде свой_ ства какого_либо другого объекта. В этом случае появляется внешняя ссылка на вложенную функцию, при этом вложенная функция продолжает ссылаться на объект вызова объемлющей функции. В результате все объекты вызова, создан_ ные при каждом таком обращении к объемлющей функции, продолжают свое существование, а вместе с ними продолжают существование имена и значения аргументов функции и локальных переменных. JavaScript_программы не имеют возможности напрямую воздействовать на объект вызова, но его свойства явля_ ются частью цепочки областей видимости, создаваемой при любом обращении к вложенной функции. (Примечательно, что если объемлющая функция сохра_ нит глобальные ссылки на две вложенные функции, эти вложенные функции будут совместно использовать один и тот же объект вызова, а изменения, по_ явившиеся в результате обращения к одной из функций, будут видимы в другой.)
Функции в JavaScript представляют собой комбинацию исполняемого про_ граммного кода и области видимости, в которой этот код исполняется. Такая комбинация программного кода и области видимости в литературе по компью_ терной тематике называется замыканием (closure). Все JavaScript_функции яв_ ляются замыканиями. Однако все эти замыкания представляют интерес лишь в только что рассмотренной ситуации, когда вложенная функция экспортирует_ ся за пределы области видимости, в которой она была определена. Вложенные функции, используемые таким образом, нередко явно называют замыканиями.
Замыкания – это очень интересная и мощная техника программирования. Хотя замыкания используются довольно редко, они достойны того, чтобы изучить их. Если вы поймете механизм замыканий, вы без труда разберетесь в областях ви_ димости и без ложной скромности сможете назвать себя опытным программи_ стом на JavaScript.
Примеры замыканий
Иногда возникает необходимость, чтобы функция запоминала некоторое значе_ ние между вызовами. Значение не может сохраняться в локальной переменной, поскольку между обращениями к функции не сохраняется сам объект вызова. С ситуацией поможет справиться глобальная переменная, но это приводит к за_ хламлению пространства имен. В разделе 8.6.3 была представлена функция uniqueInteger(), которая задействует для этих целей собственное свойство. Одна_
160 Глава 8. Функции
ко можно пойти дальше и для создания частной (private) неисчезающей пере_ менной использовать замыкание. Вот пример такой функции, для начала без за_ мыкания:
// При каждом вызове возвращает разные значения uniqueID = function() {
if (!arguments.callee.id) arguments.callee.id = 0; return arguments.callee.id++;
};
Проблема заключается в том, что свойство uniqueID.id доступно за пределами функции и может быть установлено в значение 0, вследствие чего будет наруше_ но соглашение, по которому функция обязуется никогда не возвращать одно и то же значение дважды. Для решения этой проблемы можно сохранять значение в замыкании, доступ к которому будет иметь только эта функция:
uniqueID = (function() { // Значение сохраняется в объекте вызова функции var id = 0; // Это частная переменная, сохраняющая свое
// значение между вызовами функции
// Внешняя функция возвращает вложенную функцию, которая имеет доступ
// к этому значению. Эта вложенная функция сохраняется
// в переменной uniqueID выше.
return function() { return id++; }; // Вернуть и увеличить })(); // Вызов внешней функции после ее определения.
Пример 8.6 – это еще один пример замыкания. В нем демонстрируется, как част_ ные переменные, подобные той, что была показана ранее, могут совместно ис_ пользоваться несколькими функциями.
Пример 8.6. Создание частных свойств с помощью замыканий
// Эта функция добавляет методы доступа к свойству объекта "o"
// с заданными именами. Методы получают имена get<name>
// и set<name>. Если дополнительно предоставляется
// функция проверки, метод записи будет использовать ее
// для проверки значения перед сохранением. Если функция проверки
// возвращает false, метод записи генерирует исключение.
//
// Необычность такого подхода заключается в том, что значение
// свойства, доступного методам, сохраняется не в виде свойства
// объекта "o", а в виде локальной переменной этой функции.
// Кроме того, методы доступа определены локально, в этой функции
// и обеспечивают доступ к этой локальной переменной.
// Примечательно, что значение доступно только для этих двух методов
// и не может быть установлено или изменено иначе, как методом записи. function makeProperty(o, name, predicate) {
var value; // This is the property value
// Метод чтения просто возвращает значение. o["get" + name] = function() { return value; };
// Метод записи сохраняет значение или генерирует исключение,
// если функция проверки отвергает это значение.
o["set" + name] = function(v) {
if (predicate && !predicate(v))
throw "set" + name + ": неверное значение " + v;
8.8. Область видимости функций и замыкания
else
value = v;
};
}
// Следующий фрагмент демонстрирует работу метода makeProperty(). var o = {}; // Пустой объект
// Добавить методы доступа к свойству с именами getName() и setName()
// Обеспечить допустимость только строковых значений
makeProperty(o, "Name", function(x) { return typeof x == "string"; });
o.setName("Frank");
// Установить
значение свойства
print(o.getName( ));
//
Получить значение свойства
o.setName(0);
//
Попытаться
установить значение ошибочного типа
Самый практичный и наименее искусственный пример использования замыка_ ний, который мне известен, – это механизм точек останова, разработанный Сти_ вом Йеном (Steve Yen) и опубликованный на сайте http://trimpath.com как часть клиентской платформы TrimPath. Точка останова – это точка внутри функции, где останавливается исполнение программы, и разработчик получает возмож_ ность просмотреть значения переменных, вычислить выражения, вызвать функ_ ции и тому подобное. В механизме точек останова, придуманном Стивом, замы_ кания служат для хранения контекста исполнения текущей функции (включая локальные переменные и входные аргументы) и с помощью глобальной функ_ ции eval() позволяют просмотреть содержимое этого контекста. Функция eval() исполняет строки на языке JavaScript и возвращает полученные значения (по_ дробнее об этой функции можно прочитать в третьей части книги). Вот пример вложенной функции, которая работает как замыкание, выполняющее проверку своего контекста исполнения:
// Запомнить текущий контекст и позволить проверить его
// с помощью функции eval( )
var inspector = function($) { return eval($); }
В качестве имени аргумента эта функция использует малораспространенный идентификатор $, чем снижается вероятность конфликта имен в инспектируе_ мой области видимости.
Создать точку останова можно, передав это замыкание в функцию, как показано в примере 8.7.
Пример 8.7. Точки останова на основе замыканий
// Эта функция является реализацией точки останова. Она предлагает
// пользователю ввести выражение, вычисляет его с использованием
// замыкания и выводит результат. Используемое замыкание предоставляет
// доступ к проверяемой области видимости, таким образом любая функция
// будет создавать собственное замыкание.
//
// Реализовано по образу и подобию функции breakpoint() Стива Йена
function inspect(inspector, title) { var expression, result;
// Существует возможность отключать точки останова
162 Глава 8. Функции
// за счет создания свойства "ignore" у этой функции. if ("ignore" in arguments.callee) return;
while(true) {
// Определить, как вывести запрос перед пользователем var message = "";
// Если задан аргумент title, вывести его первым
if (title) message = title + "\n";
// Если выражение уже вычислено, вывести его вместе с его значением if (expression) message += "\n"+expression+" ==> "+result+"\n"; else expression = "";
// Типовое приглашение к вводу всегда должно выводиться:
message += "Введите выражение, которое следует вычислить:";
// Получить ввод пользователя, вывести приглашение и использовать
// последнее выражение как значение по умолчанию. expression = prompt(message, expression);
// Если пользователь ничего не ввел (или щелкнул на кнопке Отменить),
// работу в точке останова можно считать оконченной
// и вернуть управление.
if (!expression) return;
// В противном случае вычислить выражение с использованием
// замыкания в инспектируемом контексте исполнения.
// Результаты будут выведены на следующей итерации.
result = inspector(expression);
}
}
Обратите внимание: для вывода информации и ввода строки пользователя функ_ ция inspect() из примера 8.7 задействует метод Window.prompt() (подробнее об этом методе рассказывается в четвертой части книги).
Рассмотрим пример функции, вычисляющей факториал числа и использующей механизм точек останова:
function factorial(n) {
// Создать замыкание для этой функции
var inspector = function($) { return eval($); } inspect(inspector, "Вход в функцию factorial()");
var result = 1; while(n > 1) {
result = result * n; n__;
inspect(inspector, "factorial( ) loop");
}
inspect(inspector, "Выход из функции factorial()"); return result;