Случаи использования замкнутых выражений JavaScript

Юрий Зайцев| 14 мая 2010 г.

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

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

Что такое замкнутое выражение

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

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

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

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

function outer() {
  var x = 'foo';
  return function () {
    return x;
  };
}

Когда вызывается функция "outer", создается экземпляр внутренней функции. В момент создания экземпляра эта внутренняя функция имеет доступ к переменной "x", и таким образом функция становится замкнутой по этой переменной. Даже после выполнения функции "outer" внутренняя функция еще имеет доступ к переменной "x".

Но вместо рассмотрения искусственных примеров давайте изучим некоторые примеры из реальной жизни.

Классический пример: обработчики событий в цикле

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

for (var i = 0; i < 10; i++) {
  document.getElementById('box' + i).onclick = function() {
    alert('You clicked on box #' + i);
  };
}

Как можно видеть, в этом фрагменте 10 функций назначаются в качестве обработчиков событий соответствующим элементам. Замысел автора в том, чтобы иметь разные значения "i" в каждом из обработчиков событий. Но это не все, что в действительности происходит. Если взглянуть внимательнее, то можно легко увидеть, что происходит на самом деле. Каждая из функций, назначенных "onclick", имеет доступ к переменной "i". Поскольку переменная "i" привязана ко всей охватывающей области, как и любая другая переменная, объявленная с помощью "var" или в объявлении функции, каждая из функций имеет доступ к той же переменной "i". После выполнения этого фрагмента переменная "i" сохраняет свое последнее значение, то, которое было установлено при окончании цикла.

Как это можно исправить? Конечно, используя преимущества замкнутых выражений. Решение простое: нужно задать область каждого из значений "i" на уровне обработчика событий. Давайте посмотрим, как это делается.

for (var i = 0; i < 10; i++) {
  document.getElementById('box' + i).onclick = (function(index){
    return function() {
      alert('You clicked on box #' + index);
    };
  })(i);
}

Вместо назначения "onclick" функции сначала создается дополнительная область путем выполнения анонимной функции. Мы передаем значение "i" в эту функцию и возвращаем внутреннюю функцию обработчика событий. Обратите внимание, что анонимная функция имеет один аргумент "index". Когда функция выполняется, и в нее передается значение "i", этот тот самый аргумент "index", который приводит к созданию переменной <sup><a href="">[1]</a></sup> в области анонимной функции-оболочки. И поскольку эта внутренняя функция обработки событий объявляется в этой оболочке, она также имеет доступ к этой переменной "index".

Мы только что создали замкнутое выражение. Теперь каждый из обработчиков событий имеет доступ к переменной "i" с соответствующими значениями — 0, 1, 2, 3 и так далее.

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

function makeHandler(index) {
  return function() {
    alert('You clicked on box #' + index);
  };
}
for (var i = 0; i < 10; i++) {
  document.getElementById('box' + i).onclick = makeHandler(i);
}

Также имеет смысл указать, что имена переменных, передаваемых в функцию-оболочку, и соответствующих аргументов не обязательно должны различаться. Я использовал "i" и "index" только для того, чтобы однозначно на них ссылаться. Если не требуется доступ к внешней переменной "i" из обработчика событий, совершенно нормально назвать их одинаково:

...
document.getElementById('box' + i).onclick = (function(i){
  return function() {
    alert('You clicked on box #' + index);
  };
})(i);

Привязка функции, карринг функции

Распространенная идиома в мире JavaScript — привязка функции. Впервые появившееся в библиотеке прототипов JavaScript и позднее стандартизованное в 5 выпуске ECMAScript, выражение Function.prototype.bind является совершенным примером замкнутого выражения на практике. Задача привязки ("bind") проста: обеспечить, чтобы конкретная функция вызывалась с конкретным значением "this". При активизации в функции "bind" возвращает новую функцию, которая активизирует первоначальную функцию со значением "this", ссылающимся на указанный объект.

Причина такой распространенности привязки функций в JavaScript заключается в своеобразной природе значения "this". Что интересно в значении "this", так это то, что оно всегда определяется путем вызова функции. Если функция вызывается как свойство объекта, это тот объект, на который ссылается "this". Если функция назначается свойству другого объекта, а затем вызывается как это свойство, то значение "this" будет ссылаться на новый объект.

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

/* Упрощение "Function.prototype.bind" из ES5 (без проверки на ошибки) */
Function.prototype.bind = function(thisArg) {
  var fn = this, args = Array.prototype.slice.call(arguments, 1);
  return function() {
    return fn.apply(thisArg, args.concat(Array.prototype.slice.call(arguments, 0)));
  };
};

Внутренняя возвращаемая функция имеет доступ к переменным, объявленным в "bind": "fn" — первоначальная функция, "thisArg" — конкретное значение "this" для вызова функции и "args" — аргументы, переданные в "bind" после "thisArg". Долгое время после выполнения "bind" внутренняя функция может использовать эти переменные для выполнения своих действий.

function Person(name) {
  this.name = name;
}
Person.prototype.speak = function() {
  alert('Hello, my name is ' + this.name);
};
    
var john = new Person('John');
john.speak(); // 'Hello, my name is John'
document.body.onclick = john.speak; // не работает, как ожидается

Причина того, что обработчик "onclick" не работает как ожидается, состоит в том, что функция вызывается значением "this", ссылающимся на "document.body" (так работает "onclick"). Но привязка гарантирует, что функция всегда вызывается "this", ссылающимся на конкретный объект, в данном случае это "john".

    document.body.onclick = john.speak.bind(john); // работает, как ожидается

Обратите внимание, что выражение "Function.prototype.bind" построено для захвата не только значения "this", но также и аргументов, переданных в него. Затем эти аргументы передаются в связанную функцию перед какими-либо аргументами самой связанной функции. Этот процесс также называется каррингом и также использует преимущества замкнутых выражений.

Давайте рассмотрим пример.

function Circle(radius) {
  this.radius = radius;
}

Circle.prototype.setRadius = function(value) {
  this.radius = value;
};

var myCircle = new Circle(10);
var setRadiusTo20 = myCircle.setRadius.bind(myCircle, 20);
document.body.onclick = setRadiusTo20;

Теперь "document.body.onclick" ссылается на функцию, которая привязана к "myCircle" и вызывается со значением 20 в качестве первого аргумента. Мы также может объединять в цепочку аргументы, переданные в "bind", и аргументы, переданные в связанную функцию:

Circle.prototype.setXY = function(x, y) {
  this.x = x;
  this.y = y;
};
var myCircle = new Circle(10);
var setXY = myCircle.setXY.bind(myCircle, 10);
setXY(20);

Первое значение, переданное в привязку, это значение 10, которое в данном случае соответствует "x". Второе значение, переданное в связанную функцию, это значение 20, которое в данном случае соответствует "y". Мы можем жонглировать этими значениями, по разному перемещая их, и получать идентичный результат (обратите внимание на расположение скобок):

myCircle.setXY.bind(myCircle, 10, 20)();
myCircle.setXY.bind(myCircle, 10)(20);
myCircle.setXY.bind(myCircle)(10, 20);

Все это функционально идентично, поскольку связанная функция всегда вызывается с одним и тем же списком аргументов — значениями 10 и 20. Различие в том, что в первом случае 10 и 20 находятся в замкнутом выражении, а во втором случае в замкнутом выражении находится только значение 10. В последнем сценарии ни 10, ни 20 не находятся в замкнутом выражении, и вместо этого передаются в связанную функцию напрямую. 

Метод setTimeout

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

var x = 10;
window.setTimeout(function(){
  alert(x);
}, 100);

В этом случае функция, которая передается в метод setTimeout, имеет доступ к переменной x долгое время после выполнения этого кода — около 100 мсек.

Шаблон модуля

Шаблон модуля, популяризируемый Дугласом Крокфордом (Douglas Crockford), является прекрасным примером применения замкнутых выражений на практике. Основная мысль состоит в том, чтобы инкапсулировать закрытую логику и предоставить только конкретные "общедоступные" методы. Давайте возьмем вариант той же реализации привязки, но на этот раз возьмем свойство объекта "functionUtils", а не "Function.prototype".

var functionUtils = (function(){
  /* закрытый метод "slice" */
  function slice(arrayLike, index) {
    return Array.prototype.slice.call(arrayLike, index || 0);
  }
  return {
      
    /* предоставленный общедоступный метод "bind" */
    bind: function(fn, thisArg) {
      var args = slice(arguments, 2);
      return function() {
        return fn.apply(thisArg, args.concat(slice(arguments)));
      };
    }
  };
})();

Обратите внимание на знакомую самовыполняющуюся анонимную функцию. Внутри этой функции имеется закрытый метод "slice" и общедоступный метод "bind". Это опять работает по принципу замкнутого выражения. Самовыполняющаяся функция возвращает объект со свойством "bind", ссылающимся на вторую функцию. Поскольку эта функция объявляется в той же области, что и "slice", она имеет доступ к "slice" даже после выполнения функции-оболочки и назначения "functionUtils" объекту.

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

var functionUtils = (function(){
  return {
    bind: function() {
      /* здесь используется "slice" */
    }
  };
  function slice() {
    /* ... */
  }
})();

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

var functionUtils = new function(){
  function slice(){
    /* ... */
  }
  this.bind = function() {
    /* здесь используется "slice" */
  };
};

Закрытые методы

Распространенная реализация закрытых методов — не что иное, как шаблон модуля, примененный к конструкторам. И как и шаблон модуля, основывается на замкнутых выражениях для обеспечения конфиденциальности и предоставления общедоступным методам внутреннего доступа к частным данным. Ниже приводится простой пример конструктора "Circle", в котором только общедоступные методы getRadius и setRadius имеют доступ к "внутреннему" значению радиуса.

function Circle(radius) {
  this.getRadius = function() {
    return radius;
  };
  this.setRadius = function(value) {
    if (value < 0) {
      throw new Error('radius should be of positive value');
    }
    radius = value;
  };
}

Во время создания "Circle" функции "getRadius" и "setRadius" создаются в конструкторе. В результате они формируют замкнутые выражения вокруг аргумента, передаваемого в функцию "Circle" — "radius". Данное значение "radius" существует только в области этих общедоступных методов "circle", и никогда не предоставляется вовне:

var myCircle = new Circle(10);
myCircle.getRadius(); // 10
    
myCircle.setRadius(-2); // Ошибка: радиус должен быть положительным значением
myCircle.getRadius(); // 10 (пока)
    
myCircle.setRadius(25);
myCircle.getRadius(); // 25 (теперь радиус изменился)

Следует отметить, что при реализации закрытых методов с помощью этого шаблона было бы хорошо понимать последствия для производительности. Во-первых, создание функций "getRadius" и "setRadius" в "Circle" мало попадает в динамическое выполнение. Во вторых, что более важно, теперь существует по 2 объекта функций, созданных для каждого созданного экземпляра "Circle".  Если бы "getRadius" и "setRadius" были созданы как методы "Circle.prototype", количество объектов функций было бы постоянным:

Circle.prototype.getRadius = function() {
  return this._radius;
};
Circle.prototype.setRadius = function(value) {
  if (value < 0) {
    throw new Error('radius should be of positive value');
  }
  this._radius = value;
};

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

Предоставление элементу уникального идентификатора

Интересный пример практической реализации замкнутого выражения — предоставление элементу уникального идентификатора. Это иногда бывает полезно для уникальной идентификации элемента в документе. Идентификаторы в целом могут это осуществить, но не все элементы имеют идентификаторы. Распространенное решение состоит в идентификации элемента по идентификатору, если он имеется, и предоставлении элементу уникального идентификатора, если он отсутствует. Некоторые библиотеки JavaScript даже предлагают такие методы как часть своих общедоступных API (например в Prototype.js имеется метод Element#identify).

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

var getUniqueId = (function(){
  var id = 0;
  return function(element) {
    if (!element.id) {
      element.id = 'generated-uid-' + id++;
    }
    return element.id;
  };
})();

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

var elementWithId = document.createElement('p');
elementWithId.id = 'foo-bar';
var elementWithoutId = document.createElement('p');
getUniqueId(elementWithId); // 'foo-bar'
getUniqueId(elementWithoutId); // 'generated-id-0'

Кэширование (запоминание)

Кэширование является одним из хорошо известных способов повышения производительности. И снова замкнутые выражения позволяют изящно реализовать эту методику оптимизации. Для примера возьмем "hasClassName" — незаменимый вспомогательный метод при написании скриптов для браузеров. Одна из возможных реализаций "hasClassName" основывается на анализе регулярных выражений в className элемента. Однако это означает, что регулярное выражение необходимо создавать динамически на основе значения className, предоставленного функции.

function hasClassName(element, className) {
  var re = new RegExp('(?:^|\\s)' + className + '(?:\\s|$)');
  return re.test(element.className);
}

Регулярное выражение должно компилироваться всякий раз при выполнении "hasClassName". Это в целом замедляет, но позволяет использовать кэширование.     

var hasClassName = (function(){
  var cache = { };
  return function(element, className) {
    var re = (cache[className] || 
      (cache[className] = new RegExp('(?:^|\\s)' + className + '(?:\\s|$)')));
    return re.test(element.className);
  };
})();

Как обычно, самовыполняющаяся функция приводит к закрытию значения "cache" над внутренней, возвращенной функцией. Затем эта внутренняя, возвращенная функция назначается "hasClassName". Поскольку возвращенная функция и "cache" объявляются в одной области, возвращенная функция имеет доступ к "cache" даже после назначения ее "hasClassName".

document.body.className = 'foo bar baz-qux';
hasClassName(document.body, 'foo'); // истина
hasClassName(document.body, 'baz'); // ложь

Обратите внимание, что данная реализация "hasClassName" не работает надежно с именами, которые сталкиваются с элементами "Object.prototype.*": "toString", "valueOf", "propertyIsEnumerable" и др. Это скорее крайний случай, но было бы хорошо позаботиться об этом ограничении.

"Короткое" разрешение переменной и свойства

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

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

(function(){
  var outer = 'foo';
  (function(){
    var inner = 'bar';
    return [inner, outer];
  })();
})();

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

Как же могут помочь замкнутые выражения? Путем создания локальных "псевдонимов" (в частности, локальных переменных) можно ускорить процесс разрешения. И чтобы "скрыть" эти локальные переменные, их можно просто сохранить в замкнутом выражении.

var keys = (function(){
  var hasOwnProperty = Object.prototype.hasOwnProperty;
  return function(object) {
    var keys = [ ];
    for (var property in object) {
      if (hasOwnProperty.call(object, property)) {
        keys.push(property);
      }
    }
    return keys;
  };
})();

Это простая реализация свойства "keys", аналогичная методу "Object.keys" в 5 выпуске ECMAScript. Этот метод возвращает массив имен, соответствующих собственным свойствам объекта.

keys({ x: 1, y: 2 }); // ['x', 'y']
keys({ }); // [ ]

Изучив реализацию, можно увидеть задание псевдонима "Object.prototype.hasOwnProperty" локальной переменной "hasOwnProperty". Поскольку эта переменная хранится в замкнутом выражении, разрешение идентификатора "hasOwnProperty" в цикле существующей функции "keys" теперь должно выполняться быстрее. Вместо прохода вплоть до глобальной области, в которой определен "Object", "hasOwnProperty" теперь разрешается в следующем объекте цепочки областей, соответствующем внешней функции-оболочке.

Другое преимущество такого назначения псевдонимов в том, что исключается многократный доступ к свойству. "Object.prototype.hasOwnProperty" сначала разрешает свойство "prototype" объекта "Object", а затем "hasOwnProperty" объекта "Object.prototype". Нет необходимости делать это с локальной переменной "hasOwnProperty".

Поэтому с "Object.prototype.hasOwnProperty" интерпретатору сначала необходимо разрешить "Object", пройдя по цепочке областей вплоть до последнего объекта — глобального объекта. Затем он выполняет разрешение двух свойств: "prototype" объекта "Object", а затем "hasOwnProperty" объекта "Object.prototype". Для локального "hasOwnProperty" это только разрешение идентификатора, при этом короткое. Преимущество последнего подхода очевидно.

Обратите внимание, что данная реализация "keys" не заботится о достаточно неприятной ошибке JScript DontEnum. Я опустил этот обходной прием для краткости, и добавить его будет нетрудно.

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

(function(){
  /* ... прочий код из библиотеки */
  var keys = (function(){
    var hasOwnProperty = Object.prototype.hasOwnProperty;
    return function() {
      /* ... */
    };
  })();
})();

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

(function(){
  /* ... прочий код из библиотеки */
  var hasOwnProperty = Object.prototype.hasOwnProperty;
  function keys(){
    /* ... */
  }
})();

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

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

Многократное использование объектов

Другая интересная оптимизация, которая иногда включает замкнутые выражения, состоит в многократном использовании объектов. Идея аналогична кэшированию (т.е. стремиться избегать создания объектов). Только на этот раз объекты создаются один раз во время "загрузки", а не несколько раз во время выполнения.

Давайте взглянем на хорошо знакомый метод "clone"/"beget" как пример такого усовершенствования. Впервые введенный Лассе Райхштейном Нильсеном (Lasse Reichstein Nielsen) в 2003 году, а затем популяризованный Дугласом Крокфордом (Douglas Crockford), метод "clone" предоставляет способ создания объекта, который наследует от другого объекта. В своей простейшей и наиболее популярной форме "clone" выглядит следующим образом:

function clone(parent) {
  function F(){}
  F.prototype = parent;
  return new F;
}

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

var clone = (function(){
  function F(){}
  return function(parent) {
    F.prototype = parent;
    return new F;
  };
})();

На этот раз функция F создается в анонимной "охватывающей" функции и затем повторно используется в возвращаемой функции "clone". Она создается один раз, и не во время выполнения, и таким образом экономится память и время выполнения.

Аналогичный пример многократного использования объектов можно видеть в методе "escapeHTML". Метод "escapeHTML", как можно предположить исходя из его имени, предоставляет способ исключения строки html. Наиболее прямой способ реализации его состоит в использовании регулярных выражений:

function escapeHTML(string) {
  return string.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
escapeHTML('foo < bar & baz '); // "foo &lt; bar &amp; baz "

Однако существует интересное сокращение, включающее получение преимуществ нестандартного (однако на самом деле в настоящий момент классифицированного в HTML5 свойства "innerHTML". Такой подход фактически использовался библиотекой Prototype.js довольно давно.

var escapeHTML = (function(){
  var div = document.createElement('div');
  var text = document.createTextNode('');
  div.appendChild(text);
  return function(str) {
    text.data = str;
    return div.innerHTML;
  };
})();

Здесь нет ручной замены каждого из символов "<", ">" и "&" соответствующими символьными сущностями ("&lt;", "&gt;" и "&amp;"). Вместо этого создается текстовый узел, и в качестве его значения устанавливается соответствующая строка с помощью свойства "data". Однако самого по себе текстового узла недостаточно, нам требуется свойство "innerHTML", чтобы извлечь Escape-представление элемента, а текстовые узлы его не имеют. Поэтому создается элемент HTML, и к нему добавляется текстовый узел. Текстовый узел и элемент HTML являются многоразовыми объектами. Функция "escapeHTML" имеет доступ к обоим объектам. Во время инициации она устанавливает свойство "data" текстового узла и извлекает Escape-строку из свойства "innerHTML" элемента, который содержит этот текстовый узел.

Это отличный прием, но не без проблем. Поскольку "innerHTML" — это собственность API, целостность возвращаемого представления в действительности не гарантируется. Например, в Internet Explorer обработка новых строк с помощью "innerHTML" отличается от такой обработки в других браузерах. Если планируется использовать этот метод, рекомендуется выполнить тестирование на какие-либо отклонения. Другая проблема — это увеличение потребления памяти в некоторых версиях Internet Explorer. Непосредственная утечка памяти отсутствует, но поскольку объекты модели DOM хранятся в замкнутых выражениях, IE знает, что надо освобождать меньше памяти при закрытии приложением вкладки или окна.

Используйте с осторожностью.

Послесловие

Я надеюсь, что этот обзор был глубоким исследованием некоторых способов практического применения замкнутых выражений в JavaScript. Здесь мы коснулись малого числа способов, и я уверен, что, пойдя дальше, вы найдете намного более интересные случаи использования.

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

[1] Технически аргумент не является переменной, но здесь мы не будем углубляться в детали различий.