Pull to refresh

Замыкания в Javascript [Часть 2]

Reading time 19 min
Views 38K
Original author: Richard Cornford
Предыдущая часть.

  • Замыкания
    • Автоматическая сборка мусора
    • Создание замыканий

  • Что можно сделать с помощью замыканий?
    • Пример 1: setTimeout c ссылкой на функцию
    • Пример 2: Ассоциирование функций с методами экземпляра объекта
    • Пример 3: Инкапсуляция взаимосвязанной функциональности
    • Другие примеры

  • Случайные замыкания
  • Проблема утечки памяти в Internet Explorer


Замыкания


Автоматическая сборка мусора


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

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

Создание замыканий


Замыкание образуется при возвращении объекта функции, который был создан в контексте исполнения вызова функции, из этого вызова функции, и присваивании ссылки на вложенную функцию свойству другого объекта. Или при прямом присваивании ссылки на такой объект функции, например, глобальной переменной, свойству глобально доступного объекта или объекту, переданному по ссылке в качестве аргумента вызову внешней функции.
function exampleClosureForm(arg1, arg2){
    var localVar = 8;
    function exampleReturned(innerArg){
        return ((arg1 + arg2)/(innerArg + localVar));
    }
    /* возвращаем ссылку на вложенную функцию, 
       определенную как exampleReturned 
    */
    return exampleReturned;
}
var globalVar = exampleClosureForm(2, 4);

Теперь объект функции, созданный в контексте исполнения вызова exampleClosureForm, не может быть обработан сборщиком мусора (garbage collected), потому что на него ссылается глобальная переменная и он все еще доступен, этот объект может быть даже выполнен при вызове globalVar(n).

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

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

Объект активации/переменных находится в ловушке, т.к. ссылка на него содержится в цепи областей видимости, присвоенной внутреннему свойству [[scope]] объекта функции, на который ссылается переменная globalVar. Объект активации/переменных сохраняется вместе со своим состоянием, т.е. со значениями его свойств. В процессе разрешения области видимости контекста исполнения вызовов внутренней функции будут вычисляться идентификаторы, соответствующие именованным свойствам этого объекта активации/переменных, просто как свойства этого объекта. Значения этих свойств все еще могут быть считаны и установлены, даже после выхода из контекста исполнения, в котором они были созданы.

В примере выше этот объект активации/переменных имеет состояние, которое представляет значения формальных параметров, определений внутренних функций и локальных переменных в тот момент, когда внешняя функция завершалась (завершился ее контекст исполнения). Свойство arg1 имеет значение 2, arg2 — значение 4, localVar — значение 8 и свойство exampleReturned — ссылка на объект вложенной функции, возвращающейся из внешней функции. (В дальнейшем для удобства обозначим этот объект активации\переменных как ActOuter1.)

Если еще раз вызвать exampleClosureForm
var secondGlobalVar = exampleClosureForm(12, 3);

— то будет создан новый контекст исполнения с новым объектом активации. И будет возвращен новый объект функции со своим собственным отдельным свойством [[scope]], ссылающимся на цепь областей видимости, содержащую объект активации из второго контекста исполнения со значениями аргументов arg112 и arg23. (В дальнейшем для удобства обозначим этот объект активации\переменных как ActOuter2.)

Второе и отличное от предыдущего замыкание было создано вторым вызовом exampleClosureForm.

Оба эти объекта функций, созданные выполнением exampleClosureForm, ссылки на которые были присвоены глобальным переменным globalVar и secondGlobalVar соответственно, возвращают выражение ((arg1 + arg2)/(innerArg + localVar)). Оно выполняет несколько операторов к четырем идентификаторам. То, как эти идентификаторы вычисляются, определяет назначение и смысл замыканий.

Рассмотрим выполнение объекта функции, на который ссылается globalVar: globalVar(2). Создается новый контекст исполнения и объект активации (назовем его ActInner1), который будет добавлен на вершину цепи областей видимости, на которую ссылается свойство [[scope]] выполненного объекта функции. ActInner1 получает свойство innerArg после того, как формальному параметру будет присвоено значение аргумента 2. Цепь областей видимости нового контекста исполнения будет такой: ActInner1-> ActOuter1-> глобальный объект.

Разрешение имен идентификаторов осуществится через цепь областей видимости, таким образом. чтобы вернуть значение выражения ((arg1 + arg2)/(innerArg + localVar)), значения этих идентификаторов будут определены путем поиска свойств с именами, соответствующими идентификаторам, в каждом объекте из цепи областей видимости по очереди.

Первый объект в цепи это ActInner1 и у него есть свойство innerArg со значением 2. Остальные 3 идентификатора соответствуют именованным свойствам ActOuter1; arg1 со значением 2, arg24 и localVar8. Вызов функции вернет ((2 + 4)/(2 + 8)).

Сравним это с выполнением другого идентичного объекта функции, на который ссылается secondGlobalVar: secondGlobalVar(5). Назовем объект активации нового контекста исполнения ActInner2, цепь будет такой: ActInner2-> ActOuter2-> глобальный объект. ActInner2 возвращает innerArg как 5 и ActOuter2 возвращает arg1, arg2 и localVar как 12, 3 и 8 соответственно. Возвращаемое значение: ((12 + 3)/(5 + 8)).

Если снова выполним secondGlobalVar, то появится новый объект активации в начале цепи областей видимости, но ActOuter2 все еще останется следующим объектом в цепи и значения его именованных свойств снова будут использованы в разрешении имен идентификаторов arg1, arg2 и localVar.

Именно таким способом вложенные функции ECMAScript получают и сохраняют доступ к формальным параметрам, декларациям вложенных функций и локальным переменным контекста исполнения, в котором они были созданы. И таким образом создание замыканий позволяет такому объекту функции продолжать ссылаться на эти значения, считывать и изменять их, пока он существует. Объект активации/переменных из контекста исполнения, в котором была создана вложенная функция, остается в цепи областей видимости, на которую ссылается свойство [[scope]] объекта функции, до тех пор, пока все ссылки на вложенную функцию не будут освобождены, а объект функции не станет доступным сборщику мусора (вместе со всеми, с этого момента ненужными, объектами в его цепи областей видимости).

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

Что можно сделать с помощью замыканий?


Может показаться странным, но ответ на этот вопрос — абсолютно все. Я говорю о том, что замыкания позволяют ECMAScript-у эмулировать что угодно, ограничения только в способности задумать и реализовать данную эмуляцию. Это немного запутано и, возможно, лучше начать с чего-то более практического.

Пример 1: setTimeout c ссылкой на функцию


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

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

Тем не менее, можно вызвать другую функцию, которая возвращает ссылку на объект вложенной функции, который передадим по ссылке функции setTimeout. Параметры, используемые вложенной функцией, отправлены при вызове функции, которая её возвращает. setTimout выполняет вложенную функцию, не передавая аргументы, но эта вложенная функция все еще имеет доступ к параметрам, предоставленным вызовом внешней функции, которая ее вернула.
function callLater(paramA, paramB, paramC){
    /* Вернем ссылку на анонимную вложенную функцию, созданную
       следующим выражением:
    */
    return (function(){
        /* Данная вложенная функция будет выполнена при помощи setTimeout,
           и когда она будет выполняться, то сможет выполнять действия над
           параметрами, отправленными во внешнюю функцию
        */
        paramA[paramB] = paramC;
    });
}

...

/* Вызовем функцию, которая вернет ссылку на объект вложенной функции,
   созданный в ее контексте исполнения. Отправим параметры, которые будут
   использоваться вложенной функцией во время выполнения, как аргументы
   внешней функции. Возвращенную ссылку на объект вложенной функции
   присвоим локальной переменной:
*/
var functRef = callLater(elStyle, "display", "none");
/* Вызовем функцию setTimeout, передав ссылку на вложенную функцию,
   присвоенную переменной funcRef, как первый аргумент:
*/
hideMenu=setTimeout(functRef, 500);

Пример 2: Ассоциирование функций с методами экземпляра объекта


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

Одним из примеров может быть объект javascript, который создан для инкапсуляции взаимодействия с определенным DOM элементом. Он содержит doOnClick, doMouseOver и doMouseOut методы для выполнения в тот момент, когда срабатывают соответствующие события на этом DOM элементе, но может быть любое количество экземпляров объектов javascript, созданных для ассоциирования с различными элементами DOM, и отдельные экземпляры объектов могут не знать, как они будут применены в коде, в котором они созданы. Эти экземпляры объектов не могут определить, каким образом на них ссылаются глобально, т.к. они не знают, каким глобальным переменным (если они есть) будут присвоены ссылки на них.

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

В последующем примере используется простая обобщенная функция, основанная на замыкании, которая связывает экземпляр объекта с обработчиками событий элементов. Допустим, что выполнение обработчика события приведет к вызову определенного метода экземпляра объекта, которому передается объект события и ссылка на ассоциированный элемент, и возвращению значения, которое возвращает этот метод.
/* Общая функция, которая связывает экземпляр объекта с
   обработчиком события. Возвращаемая вложенная функция используется как 
   обработчик события. Экземпляр объекта передается как параметр obj, а
   имя метода, который будет вызываться из этого объекта, передается как
   methodName - параметр строкового типа. 
*/
function associateObjWithEvent(obj, methodName){
    /* Возвращаемая вложенная функция, которая, предположительно, будет
       использована как обработчик события DOM элемента:
    */
    return (function(e){  
        /* Объект события, который интерпретирован как параметр e в случае, 
           если браузер работает по стандартам DOM, и нормализация объекта 
           события, заданного в IE, если он не был передан через 
           аргумент вложенной функции, обрабатывающей события
        */
        e = e||window.event;
       /*  Обработчик события вызывает метод объекта obj, имя которого
           содержится в переменной methodName в виде строки, в него
           отправляются нормализованный объект события и ссылка на
           элемент, с присвоенным обработчиком событий, которая передается 
           с помощью this (это сработает, т.к. вложенная функция выполняется 
           как метод этого элемента потому, что она была назначена 
           обработчиком события)
        */
        return obj[methodName](e, this);
    });
}

/* Эта функция-конструктор создает объекты, связанные с DOM элементами,
   чьи ID отправляются в конструктор в виде строки. Экземпляры объектов
   организуют все таким образом, что, когда соответствующие элементы
   вызывают события onclick, onmouseover и onmouseout, соответствующие
   методы вызываются из своего экземпляра объекта.
*/
function DhtmlObject(elementId){
    /* Вызовем функцию, которая извлекает ссылку на DOM элемент (или null,
       если он не найден) с помощью ID запрашиваемого элемента, переданного 
       как аргумент. Полученное значение присвоится локальной переменной el.
    */
    var el = getElementWithId(elementId);
    /* Значение el может быть преобразовано к типу boolean для
       использования в выражении if так, что если el ссылается на объект,
       то значение будет истинным, а в случае null - ложным. Таким образом,
       последующий блок будет выполнен, только если переменная el ссылается
       на элемент DOM.
    */
    if(el){
    /* Чтобы назначить функцию как обработчик события элемента, этот 
       объект вызывает функцию associateObjWithEvent, определяя себя (с 
       помощью ключевого слова this) как объект, из которого будет вызван 
       метод, и передавая имя этого метода. Функция associateObjWithEvent
       вернет ссылку на вложенную функцию, которая будет присвоена
       обработчику события DOM элемента. Данная вложенная функция будет
       вызывать запрошенный метод объекта javascript в ответ на события.
    */
        el.onclick = associateObjWithEvent(this, "doOnClick");
        el.onmouseover = associateObjWithEvent(this, "doMouseOver");
        el.onmouseout = associateObjWithEvent(this, "doMouseOut");
        ...
    }
}
DhtmlObject.prototype.doOnClick = function(event, element){
    ... // тело метода doOnClick.
}
DhtmlObject.prototype.doMouseOver = function(event, element){
    ... // тело метода doMouseOver.
}
DhtmlObject.prototype.doMouseOut = function(event, element){
    ... // тело метода doMouseOut.
}

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

Пример 3: Инкапсуляция взаимосвязанной функциональности


Замыкания могут быть использованы для того, чтобы создать дополнительные области видимости для группирования взаимосвязанного и взаимозависимого кода способом, минимизирующим риск случайного взаимодействия. Предположим, у нас есть функция, создающая строку, и для того, чтобы избежать повторения операции конкатенации (и создания множества промежуточных строк) желательно использовать массивы для хранения частей строки в определенном порядке, а затем выводить результат, используя метод Array.prototype.join (с пустой строкой в качестве аргумента). Массив используется как буфер для вывода, но если его определить локально, внутри функции, то при каждом выполнении функции он будет создаваться заново, что не обязательно, если с каждым вызовом функции будет меняться только переменное содержимое массива.

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

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

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

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

Ссылка на объект вложенной функции возвращается из выполнения, встроенного в функцию-выражение, и присваивается глобальной переменной, поэтому этот объект функции может быть вызван как глобальная функция. Буферный массив определен как локальная переменная во внешней функции-выражении. Он не выложен в глобальном пространстве имен и не пересоздается при каждом вызове функции, использующей ее.
/* Глобальная переменная getImgInPositionedDivHtml задана и ей присвоено
   значение вложенной функции-выражения, возвращенного из однократного
   вызова внешней функции-выражения.

   Эта вложенная функция возвращает строку HTML, которая представляет
   абсолютно позиционированный DIV, обёртывающий элемент IMG, так что
   все переменные значения атрибутов доступны как параметры вызова
   функции.
*/
var getImgInPositionedDivHtml = (function(){
    /* bufferAr - массив, присвоенный локальной переменной внешней функции-
       выражения. Он создается только один раз и этот экземпляр массива
       доступен вложенной функции, т.е. может быть использован при каждом
       выполнении этой вложенной функции.
       
       Пустые строки используются как метка-заполнитель для данных, которые
       будут вставлены в массив вложенной функцией.
    */
    var buffAr = [
        '<div id="',
        '',   //индекс 1, DIV ID аттрибут
        '" style="position:absolute;top:',
        '',   //индекс 3, DIV top позиция
        'px;left:',
        '',   //индекс 5, DIV left позиция
        'px;width:',
        '',   //индекс 7, DIV width
        'px;height:',
        '',   //индекс 9, DIV height
        'px;overflow:hidden;\"><img src=
        '',   //индекс 11, IMG URL
        '\" width=
        '',   //индекс 13, IMG width
        '\" height=
        '',   //индекс 15, IMG height
        '\" alt=
        '',   //индекс 17, IMG alt текст
        '\"><\/div>'
    ];
    /* Вернем объект вложенной функции, который является результатом
       выполнения функции-выражения. Именно этот объект вложенной функции 
       будет выполняться при каждом вызове getImgInPositionedDivHtml(...)
    */
    return (function(url, id, width, height, top, left, altText){
        /* Присвоим несколько параметров элементам с соответствующими
           позициями в буферном массиве
        */
        buffAr[1] = id;
        buffAr[3] = top;
        buffAr[5] = left;
        buffAr[13] = (buffAr[7] = width);
        buffAr[15] = (buffAr[9] = height);
        buffAr[11] = url;
        buffAr[17] = altText;
        /* Вернем строку, созданную соединением каждого элемента массива с
           пустой строкой в качестве разделителя (что то же самое, что и просто
           объединение элементов массива вместе) 
        */
        return buffAr.join('');
    }); //Конец вложенной функции-выражения.
})();
/*^^- встроенное (inline) выполнение внешней функции-выражения */

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

Другие примеры



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

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

Случайные замыкания


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

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

Распространенной является практика, когда вложенные функции используются в качестве обработчиков событий для DOM элементов. Например, последующий код может быть использован, чтобы добавить обработчик события onclick в элемент ссылку (link element).
/* Определим глобальную переменную, значение которой должно быть 
   добавлено в атрибут href элемента ссылки в качестве параметра строки 
   запроса с помощью последующей функции
*/
var quantaty = 5;
/* Когда элемент ссылка будет передан в эту функцию (в качестве аргумента 
   вызова функции linkRef), в этот элемент добавится обработчик события 
   onclick, который добавит значение глобальной переменной quantaty в 
   параметр href этого элемента в качестве значения параметра строки запроса, 
   а затем вернет true, таким образом, ссылка направит на ресурс, определенный
   в параметре href, который будет включать строку запроса.
*/ 
function addGlobalQueryOnClick(linkRef){
    /* Если параметр linkRef может быть преобразован в true (что произойдет,
       если он ссылается на объект)
    */
    if(linkRef){
        /* Вычислим функцию-выражение и присвоим ссылку на созданный в 
           результате объект функции свойству обработчика onclick элемента
           ссылки
        */
        linkRef.onclick = function(){
            /* Эта вложенная функция-выражение добавляет строку запроса к
               параметру href элемента, к которому она прикреплена в качестве
               обработчика событий
            */
            this.href += ('?quantaty='+escape(quantaty));
            return true;
        };
    }
}

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

Вышеприведенный код не использует преимущества того, что внутренние функции стали доступными вне функции, в которой они созданы (т.е получившихся в результате замыканий). Точно такого же эффекта можно было бы добиться в результате определения функции для использования в качестве обработчика события отдельно и затем присвоения ссылки на эту функцию свойству обработчика события. Создался бы всего один объект функции и все элементы, использующие этот обработчик событий, использовали бы ссылку на эту одну функцию.
/* Определим глобальную переменную, значение которой должно быть 
   добавлено в атрибут href элемента ссылки в качестве параметра строки 
   запроса с помощью последующей функции
*/
var quantaty = 5;

/* Когда элемент ссылка будет передан в эту функцию (в качестве аргумента 
   вызова функции linkRef), в этот элемент добавится обработчик события 
   onclick, который добавит значение глобальной переменной quantaty в 
   параметр href этого элемента в качестве значения параметра строки запроса, 
   а затем вернет true, таким образом, ссылка направит на ресурс, определенный
   в параметре href, который будет включать строку запроса.
*/
function addGlobalQueryOnClick(linkRef){
    /* Если параметр linkRef может быть преобразован в true (что произойдет,
       если он ссылается на объект)
    */
    if(linkRef){
        /* Присваиваем ссылку на глобальную функцию свойству обработчика 
           события элемента ссылка, таким образом, она сама становится 
           обработчиком событий.
        */
        linkRef.onclick = forAddQueryOnClick;
    }
}
/* Декларация глобальной функции, которая должна выступать в качестве
   обработчика события элемента ссылка, которая добавляет значение глобальной
   переменной в значение параметра href этого элемента.
*/
function forAddQueryOnClick(){
    this.href += ('?quantaty='+escape(quantaty));
    return true;
}

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

Аналогичный принцип касается и объекта функции-конструктора. Не редкость видеть код, подобный следующей структуре конструктора
function ExampleConst(param){
    /* Создаем методы объекта путем вычисления функций-выражений и присвоения 
       ссылок на получающиеся в результате объекты функций свойствам 
       создаваемого объекта
    */
    this.method1 = function(){
        ... // тело метода
    };
    this.method2 = function(){
        ... // тело метода
    };
    this.method3 = function(){
        ... // тело метода
    };
    /* Присваиваем параметр конструктора свойству объекта.
    */
    this.publicProp = param;
}

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

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

В таком случае было бы более эффективно создать объекты функций один раз и присваивать ссылки на них соответствующим свойствам объекта prototype конструктора, таким образом, они могут быть использованы всеми объектами, созданными этим конструктором.
function ExampleConst(param){
    /* Присваиваем параметр конструктора свойству объекта
    */
    this.publicProp = param;
}
/* Создадим методы для объектов путем вычисления функций-выражений и 
   присвоения ссылок на получающиеся в результате объекты функций свойствам 
   объекта prototype конструктора
*/
ExampleConst.prototype.method1 = function(){
    ... // тело метода
};
ExampleConst.prototype.method2 = function(){
    ... // тело метода
};
ExampleConst.prototype.method3 = function(){
    ... // тело метода
};


Проблема утечки памяти в Internet Explorer


Браузер Internet Explorer (проверено на версиях 4-6 (6 является текущей версией на момент написания статьи)) имеет дефект в системе сборки мусора, который препятствует обработке объектов ECMAScript и некоторых объектов среды, если эти объекты среды являются частью “циклической” ссылки. Под объектами среды в данном случае понимаются любые объекты DOM Node (включая объект document и его потомки) и объекты ActiveX. Если циклическая ссылка содержит один или несколько из них, то ни один из участвующих объектов не будет освобожден, а память, которую они используют, останется недоступной для системы до тех пор, пока браузер не будет закрыт.

Циклическая ссылка — это когда два или более объекта ссылаются друг на друга таким образом, что это приводит к начальной точке. Например, когда объект 1 имеет свойство, которое ссылается на объект 2, объект 2 имеет свойство, которое ссылается на объект 3, а объект 3 имеет свойство, которое ссылается на объект 1. В случае с чистыми объектами ECMAScript, как только на объекты 1, 2 или 3 перестают ссылаться другие объекты, факт, что они ссылаются только друг на друга, распознается, и тогда они становятся доступными для сборщика мусора. Но в случае с Internet Explorer, если хотя бы одним из этих объектов окажется объект DOM Node или ActiveX, то обработчик мусора не сможет увидеть, что это циклическое взаимоотношение изолировано от остальной системы, и освободить их. Вместо этого все они останутся в памяти до тех пор, пока браузер не будет закрыт.

Замыкания очень хороши в создании циклических ссылок. Если объект функции, создающей замыкание, будет присвоен, например, как обработчик события объекта DOM Node, а ссылка на этот Node была присвоена одному из свойств объектов активации/переменных в области видимости этой функции, тогда появится циклическая ссылка. DOM_Node.onevent -> function_object.[[scope]] -> scope_chain ->Activation_object.nodeRef -> DOM_Node. Это очень просто сделать, и непродолжительный просмотр сайта, формирущего такую ссылку в куске кода, общем для всех страниц, может использовать большую часть системной памяти (иногда всю).

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

      Написано Ричардом Корнфордом. Март 2004.
      С поправками и предложениями от:
            Martin Honnen.
            Yann-Erwan Perio (Yep).
            Lasse Reichstein Nielsen. (определение замыкания)
            Mike Scirocco.
            Dr John Stockton.
            Garrett Smith.

Благодарю за участие в переводе Людмилу Лиховид.
Tags:
Hubs:
+15
Comments 11
Comments Comments 11

Articles