JavaScript: область видимости и замыкание

 

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

Введение

В сети довольно много статей, в которых пытаются объяснить области видимости и замыкания, но в общем, я бы сказал, что большинство из них не совсем понятны. Кроме того, в некоторых статьях предполагается, что вы программировали до этого на 15 других языках, хотя как я считаю - большинство людей пишущих на JavaScript имеют лишь опыт в HTML и CSS, а не в C или Java.

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

Область видимости

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

Глобальная область видимости

Когда что-то является глобальным, это значит, что оно доступно из любого места в вашем коде. Рассмотрим пример:

var monkey = "Gorilla";

function greetVisitor () {
	return alert("Hello dear blog reader!");
}

Если бы этот код исполнялся в веб браузере, то областью видимости была бы window, тем она будет доступна для всего, что исполняется в window.

Локальная область видимости

В отличие от глобальной области видимости, локальная область видимости - это когда что-то определено и доступно только в некоторой части кода, как например функция. Рассмотрим пример:

function talkDirty () {
    var saying = "Oh, you little VB lover, you";
    return alert(saying);
}
alert(saying); // Throws an error

В данном примере переменная saying доступна только внутри функции talkDirty, за пределами которой она не определена. Замечание: если бы вы определили saying без ключевого слова var, то она автоматически стала бы глобальной.

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

function saveName (firstName) {
	function capitalizeName () {
		return firstName.toUpperCase();
	}
	var capitalized = capitalizeName();
	return capitalized;
}
alert(saveName("Robert")); // Returns "ROBERT"

Как вы только что увидели, внутренней функции capitalizeName не нужно передавать никаких параметров, т.к. она имеет полный доступ к параметру firstName во внешней функции saveName. Для большей ясности, рассмотрим еще один пример:

function siblings () {
	var siblings = ["John", "Liza", "Peter"];
	function siblingCount () {
		var siblingsLength = siblings.length;
		return siblingsLength;
	}
	function joinSiblingNames () {
		return "I have " + siblingCount() + " siblings:nn" + siblings.join("n");
	}
	return joinSiblingNames();
}
alert(siblings()); // Outputs "I have 3 siblings: John Liza Peter"

Как вы видите, обе внутренние функции имеют доступ к массиву siblings, и каждая внутренняя функция имеет доступ к другой внутренней функции того же уровня (в данном случае joinSiblingNames имеет доступ к siblingCount). Однако, переменная siblingsLength внутри siblingCount доступна лишь внутри этой функции, т.е. в этой области видимости.

Замыкание

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

function add (x) {
	return function (y) {
		return x + y;
	};
}
var add5 = add(5);
var no8 = add5(3);
alert(no8); // Returns 8

Вот это да! Что здесь происходит? Давайте разбираться:

1. Когда мы вызываем функцию add, она возвращает функцию.

2. Эта функция закрывает контекст и запоминает, каким был параметр x в это время (т.е. в данном случае значением 5)

3. Когда результат функции add присваивается переменной add5, она всегда будет знать, каким был x при создании этой переменной.

4. Переменная add5 ссылается на функцию, которая всегда будет добавлять значение 5 к любому переданному ей аргументу.

5. Это означает, что когда мы вызываем add5 со значением 3, она сложит числа 5 и 3, и вернет 8.

На самом деле, в мире JavaScript, функция add5 выглядит следующим образом:

function add5 (y) {
	return 5 + y;
}

Пресловутая проблема циклов
Сколько раз вы создавали циклы, в которых хотели присвоить значение i каким-либо образом, например элементу, и понимали, что возвращается лишь последнее значение i?

Неправильное обращение

Давайте посмотрим на этот некорректный код, который создает 5 элементов <a>, добавляет значение i как текст к каждому элементу и onclick, который как ожидается будет выдавать alert со значением i для данной ссылки, т.е. то же самое значение, что и в тексте элемента. Затем элементы добавляются к document body:

function addLinks () {
	for (var i=0, link; i<5; i++) {
		link = document.createElement("a");
		link.innerHTML = "Link " + i;
		link.onclick = function () {
			alert(i);
		};
		document.body.appendChild(link);
	}
}
window.onload = addLinks;

Каждый элемент содержит правильный текст, т.е. “Link 0″, “Link 1″ и т.д. Но какую бы ссылку вы не кликнули, она показывает alert с цифрой 5. В чем же дело? Причина в том, что значение переменной i увеличивается на 1 с каждой итерацией цикла, и т.к. событие onclick не исполняется, а просто применяется к элементу <a>, то значение увеличивается.

Следовательно, цикл продолжает работу, пока i не станет равным 5, что является последним значением перед выходом из функции addLinks. Далее, всякий раз при срабатывании события onclick, берется последнее значение i.

Правильное обращение

Что вам нужно сделать, так это создать замыкание. В результате, когда вы будете применять значение i к событию onclick элемента <a>, то будет присвоено значение i именно в тот момент времени. Например вот так:

function addLinks () {
	for (var i=0, link; i<5; i++) {
		link = document.createElement("a");
		link.innerHTML = "Link " + i;
		link.onclick = function (num) {
			return function () {
				alert(num);
			};
		}(i);
		document.body.appendChild(link);
	}
}
window.onload = addLinks;

Используя этот код, если вы кликните на первый элемент, alert выдаст "0", на второй - "1", и т.д. Решение состоит в том, что внутренняя функция, примененная к событию onclick, создает замыкание, в котором происходит обращение к параметру num, т.е. к значению i в тот момент времени.

Эта функция "запоминает" нужное значение, и может затем возвращать соответствующую цифру при срабатывании события onclick.

Безопасно-исполняющиеся функции

Безопасно-исполняющиеся функции - это такие функции, которые начинают исполняться сразу же и создают свое замыкание. Рассмотрим пример:

(function () {
	var dog = "German Shepherd";
	alert(dog);
})();
alert(dog); // возвращает undefined

Итак, переменная dog доступна только внутри данного контекста. Подумаешь, скрытая переменная dog... Но, друзья мои, с этого начинается самое интересное! Это решило нашу проблему с циклом, и это также является основой для Yahoo JavaScript Module Pattern.

Yahoo JavaScript Module Pattern

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

var person = function () {
	// Private
	var name = "Robert";
	return {
		getName : function () {
			return name;
		},
		setName : function (newName) {
			name = newName;
		}
	};
}();
alert(person.name); // Undefined
alert(person.getName()); // "Robert"
person.setName("Robert Nyman");
alert(person.getName()); // "Robert Nyman"

Преимущество данного подхода в том, что вы можете определить сами, что будет открытым в вашем объекте (и может быть изменено), и что закрытым, к чему никто не сможет обратиться или изменить. Переменная name скрыта вне контекста функции, но доступна функциям getName и setName, т.к. они создают замыкания, в которых есть ссылка на переменную name.

Заключение

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

Удачного кодинга!




Рекомендуем почитать


Комментарии

Рома

04.06.2017 - 12:30:43

Не "Безопасно-исполняющиеся функции", а "самовызывающиеся функции".

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


Ваше имя:


Комментарий:


Введите: Картинка