В этом посте мы разберемся с областями видимости в JavaScript. Обратим внимание на две вещи, которые могут приводить к проблемам, если вы о них не позаботитесь: область видимости переменных в JavaScript - функции, и они могут быть захвачены в замыкания.
Область видимости переменных определяет, где эта переменная доступна. Например, если переменная определена в начале функции, то она доступная внутри этой функции, но не снаружи, и обычно "умирает" с завершением функции. В этом случае, область видимости переменной - функция. Когда поток управления доходит до области видимости, создается новое окружение, которое отображает имена переменных на их значения. Области видимости могут быть вложенными. Переменная доступна в своей области видимости и во всех вложенных областях видимости.
Ловушка 1: Область видимости переменных функций
Большинство популярных языков имеют блочную область видимости: новое окружение создается при входе в блок и области видимости составлены из вложенных блоков. В отличии от JavaScript, где переменные имеют область видимости в функции, новое окружение создается только при входе в функцию и составлено из вложенных функций. Это значит, что даже если вы определите переменную внутри блока "тогда" оператора if
, то она будет доступна везде в окружающей функции. Код ниже демонстрирует это:
var myvar = "global";
function f() {
print(myvar); // (*)
if (true) {
var myvar = "local"; // (**)
}
print(myvar);
}
> f();
undefined
local
> myvar
global
Как вы видите, даже первое обращение к myvar
ссылается на локальную переменную (значение которой еще не присвоено на (*)
). Причина в том, что определение var
переменной в JavaScript поднимается (hoisting): var myvar = "local"
равносильно определению переменной myvar
на начале функции f()
, а присвоение происходит на строке (**)
. Поэтому, передовые практики в JavaScript рекомендуют использовать только один var
в начале функции.
Ловушка 2: Замыкания
В JavaScript области видимости статические, они определены вложенностью синтаксических конструкций. Для обеспечения статической области видимости, окружение связано с значениями, которые имеют доступ к переменными в окружении. Пример такого значения - возвращенная функция в следующем коде:
function f() {
var x = "abc";
return function() {
return x;
}
}
Использование:
> var g = f();
> g()
abc
Переменная x
доступная в возвращенной функции, и к ней невозможно получить доступ, кроме как в этой функции. Присоединив окружение, можно получить значение x
, как мы ожидали, с учетом статической области видимости. Эта пара значения и окружения называется замыкание, потому что переменные замкнуты. Замыкания в JavaScript очень мощны. Вы их можете использовать даже для сохранения свойств объектов, как продемонстрировано в коде ниже:
function objMaker(color) {
return {
getColor: function() {
return color;
},
setColor: function(c) {
color = c;
}
};
}
Использование:
> var c = objMaker("blue");
> c.getColor()
blue
> c.setColor("green");
> c.getColor()
green
Значение цвета сохраняется в окружении, которое было создано при вызове objMaker()
. Это окружение присоединяется к возвращаемому значению, поэтому color
все еще доступен даже после того, как закончилось исполнение objMaker()
.
Нечаянный обмен средой
Замыкания и функциональная область видимости приводит к неожиданному поведению, имея следующий код:
function f() {
var arr = [ "red", "green", "blue" ];
var result = [];
for (var i=0; i < arr.length-1; i++) {
var func = function() {
return arr[i];
};
result.push(func);
}
return result;
}
Эта функция возвращает коллекцию из двух функций. Обе функции все еще имеют доступ к окружению f
и, как следствие, к arr
. По факту, они имеют одинаковые окружения. Но в том окружении i
имеет значение 2, и поэтому функции возвращают значение "blue" при вызове:
> f()[0]()
blue
Это не то что нам нужно. Для того чтоб это полечить нам необходимо сделать снимок того что находится в индексе i
, перед созданием функции которая его использует.
Создание нового окружения
В языке с блочной областью видимости, следующий код был бы рабочим:
function f() {
var arr = [ "red", "green", "blue" ];
var result = [];
for(var i=0; i<arr.length-1; i++) {
// Новое окружение для каждой итерации? Только не в JS!
var color = arr[i]; // НЕ новая копия в JS!
var func = function() {
return color;
};
result.push(func);
}
return result;
}
JavaScript имеет функциональную область видимости, поэтому color
обрабатывается так, если бы был определен в начале функции f()
, и мы не получим другого окружения для каждой возвращаемой функции. Только функции могут создавать окружения, поэтому мы используем функцию для симуляции блока. Это выглядит следующим образом:
(function() { // open block
// inside block
}()); // close block
Так, как мы сразу вызываем функцию, поведение обернутого кода такое же, как и в блока: код выполняется сразу. Зачем нужны круглые скобки вокруг конструкции? Они там для избежания синтаксической ошибки. Если выражение начинается с функции, то ожидается определение функции. Таким образом, без открытия круглых скобок, синтаксический анализатор будет жаловаться на отсутствующее имя. Вас может посетить идея дописать необходимое имя - в таком случае, анализатор будет жаловаться на круглые скобки, которые следуют за функцией. В обоих случаях, функция не будет выполнена. Таким образом открытие круглых скобок необходимо для того, чтоб указать, что далее будет следовать выражение-функция-выражение. Это называется Немедленно Вызываемые Функции (Immediately Invoked Function Expression aka IIFE). Переписав в пример выше, на псевдокоде с помощью IIFE получим:
function f() {
var arr = [ "red", "green", "blue" ];
var result = [];
for(var i=0; i<arr.length-1; i++) {
(function() {
var color = arr[i]; // fresh copy
var func = function() {
return color;
};
result.push(func);
}());
}
return result;
}
Теперь новое окружение будет создано для каждой итерации в цикле. Теперь получим ожидаемый результат:
> f()[0]()
red
Так же вы можете использовать color
как параметр для IIFE:
function f() {
var arr = ["red", "green", "blue"];
var result = [ ];
for (var i = 0; arr.length - 1 > i; i++) {
(function(color) {
var func = function() {
return color;
};
result.push(func);
} (arr[i]));
}
return result;
}
Обратите внимание, пример имеет практический смысл, потому что похожи сценарии имеют место при добавлении обработчиков на DOM элементы.
Источник