javascript 2.2.3 Замыкание. Блоки кода

Блоки кода и циклы, IIFE

Предыдущие примеры сосредоточены на функциях. Но лексическое окружение существует для любых блоков кода {...}.

Лексическое окружение создаётся при выполнении блока кода и содержит локальные переменные для этого блока. Вот пара примеров.

If

В следующем примере переменная user существует только в блоке if:

Когда выполнение попадает в блок if, для этого блока создаётся новое лексическое окружение.

У него есть ссылка на внешнее окружение, так что phrase может быть найдена. Но все переменные и Function Expression, объявленные внутри if, остаются в его лексическом окружении и не видны снаружи.

Например, после завершения if следующий alert не увидит user, что вызовет ошибку.

For, while

Для цикла у каждой итерации своё отдельное лексическое окружение. Если переменная объявлена в for(let ...), то она также в нём:

for (let i = 0; i < 10; i++) {
  // У каждой итерации цикла своё собственное лексическое окружение
  // {i: value}
}

alert(i); // Ошибка, нет такой переменной

Обратите внимание: let i визуально находится снаружи {...}. Но конструкция for – особенная в этом смысле, у каждой итерации цикла своё собственное лексическое окружение с текущим i в нём.

И так же, как и в if, ниже цикла i невидима.

Блоки кода

Мы также можем использовать «простые» блоки кода {...}, чтобы изолировать переменные в «локальной области видимости».

Например, в браузере все скрипты (кроме type="module") разделяют одну общую глобальную область. Так что, если мы создадим глобальную переменную в одном скрипте, она станет доступна и в других. Но это становится источником конфликтов, если два скрипта используют одно и то же имя переменной и перезаписывают друг друга.

Это может произойти, если название переменной – широко распространённое слово, а авторы скрипта не знают друг о друге.

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

{
  // сделать какую-нибудь работу с локальными переменными, которые не должны быть видны снаружи

  let message = "Hello";

  alert(message); // Hello
}

alert(message); // Ошибка: переменная message не определена

Из-за того, что у блока есть собственное лексическое окружение, код снаружи него (или в другом скрипте) не видит переменные этого блока.

IIFE

В прошлом в JavaScript не было лексического окружения на уровне блоков кода.

Так что программистам пришлось что-то придумать. И то, что они сделали, называется «immediately-invoked function expressions» (аббревиатура IIFE), что означает функцию, запускаемую сразу после объявления.

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

IIFE выглядит так:

(function() {

  let message = "Hello";

  alert(message); // Hello

})();

Здесь создаётся и немедленно вызывается Function Expression. Так что код выполняется сразу же и у него есть свои локальные переменные.

Function Expression обёрнуто в скобки (function {...}), потому что, когда JavaScript встречает "function" в основном потоке кода, он воспринимает это как начало Function Declaration. Но у Function Declaration должно быть имя, так что такой код вызовет ошибку:

// Попробуйте объявить и сразу же вызвать функцию
function() { // <-- Error: Unexpected token (

  let message = "Hello";

  alert(message); // Hello

}();

Даже если мы скажем: «хорошо, давайте добавим имя», – это не сработает, потому что JavaScript не позволяет вызывать Function Declaration немедленно.

// ошибка синтаксиса из-за скобок ниже
function go() {

}(); // <-- не можете вызывать Function Declaration немедленно

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

Кроме скобок, существуют и другие пути показать JavaScript, что мы имеем в виду Function Expression:

// Пути создания IIFE

(function() {
  alert("Скобки вокруг функции");
})();

(function() {
  alert("Скобки вокруг всего");
}());

!function() {
  alert("Выражение начинается с логического оператора NOT");
}();

+function() {
  alert("Выражение начинается с унарного плюса");
}();

Во всех перечисленных случаях мы объявляем Function Expression и немедленно выполняем его. Ещё раз заметим, что в настоящий момент нет необходимости писать подобный код.

Сборка мусора

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

function f() {
  let value1 = 123;
  let value2 = 456;
}

f();

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

…Но, если есть вложенная функция, которая всё ещё доступна после выполнения f, то у неё есть свойство [[Environment]], которое ссылается на внешнее лексическое окружение, тем самым оставляя его достижимым, «живым»:

function f() {
  let value = 123;

  function g() { alert(value); }

  return g;
}

let g = f(); // g доступно и продолжает держать внешнее лексическое окружение в памяти

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

function f() {
  let value = Math.random();

  return function() { alert(value); };
}

// три функции в массиве, каждая из них ссылается на лексическое окружение
// из соответствующего вызова f()
let arr = [f(), f(), f()];

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

В следующем коде, после того как g станет недоступным, лексическое окружение функции (и, соответственно, value) будет удалено из памяти;

function f() {
  let value = 123;

  function g() { alert(value); }

  return g;
}

let g = f(); // пока g существует,
// соответствующее лексическое окружение существует

g = null; // ...а теперь память очищается

Оптимизация на практике

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

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

Одним из важных побочных эффектов в V8 (Chrome, Opera) является то, что такая переменная становится недоступной при отладке.

Попробуйте запустить следующий пример в Chrome с открытой Developer Tools.

Когда код будет поставлен на паузу, напишите в консоли alert(value).

function f() {
  let value = Math.random();

  function g() {
    debugger; // в консоли: напишите alert(value); Такой переменной нет!
  }

  return g;
}

let g = f();
g();

Как вы можете видеть – такой переменной не существует! В теории, она должна быть доступна, но попала под оптимизацию движка.

Это может приводить к забавным (если удаётся решить быстро) проблемам при отладке. Одна из них – мы можем увидеть не ту внешнюю переменную при совпадающих названиях:

let value = "Сюрприз!";

function f() {
  let value = "ближайшее значение";

  function g() {
    debugger; // в консоли: напишите alert(value); Сюрприз!
  }

  return g;
}

let g = f();
g();

До встречи!

Эту особенность V8 полезно знать. Если вы занимаетесь отладкой в Chrome/Opera, рано или поздно вы с ней встретитесь.

Это не баг в отладчике, а скорее особенность V8. Возможно со временем это изменится.

Запись опубликована в рубрике javascript. Добавьте в закладки постоянную ссылку.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *