Skip to content

Latest commit

 

History

History
611 lines (390 loc) · 49.1 KB

File metadata and controls

611 lines (390 loc) · 49.1 KB

Вы не знаете JS: Область видимости и замыкания

Глава 3: Область видимости: функции против блоков

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

Но что именно конкретно создает новую зону? Только функция? Могут ли другие структуры в JavaScript создавать зоны областей видимости?

Область видимости из функций

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

Но сначала, давайте исследуем область видимости функции и ее применения.

Представьте такой код:

function foo(a) {
	var b = 2;

	// некоторый код

	function bar() {
		// ...
	}

	// еще код

	var c = 3;
}

В этом коде зона области видимости для foo(..) включает в себя идентификаторы a, b, c и bar. Не важно где в области видимости появится объявление, переменная или функция принадлежит к содержащей их зоне области видимости, вне зависимости от места объявления. Мы исследуем в следующей главе как это в точности работает.

У bar(..) есть своя собственная зона области видимости. Также и у глобальной области видимости, у которой есть всего один идентификатор: foo.

Так как a, b, c и bar все принадлежат к зоне области видимости foo(..), они недоступны вне foo(..). То есть, нижеприведенный код весь целиком приведет к ошибкам ReferenceError, так как идентификаторы недоступны в глобальной области видимости:

bar(); // ошибка

console.log( a, b, c ); // все 3 вызовут ошибку

Однако, все эти идентификаторы (a, b, c, foo и bar) доступны внутри foo(..) и конечно также доступны внутри bar(..) (предполагаем, что нет ни одного объявления затеняющих идентификаторов внутри bar(..)).

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

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

Прячемся на виду у всей области видимости

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

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

Почему же "прятать" переменные и функции — полезная техника?

Есть множество причин, мотивирующих на это сокрытие, основанное на области видимости. Они имеют тенденцию проистекать из принципа дизайна ПО "Принцип наименьших привилегий", также иногда называемый "Наименьшие полномочия" или "Наименьшая открытость". Этот принцип заявляет, что в дизайне ПО, таком как API для модуля/объекта, вам следует выставлять наружу только то, что минимально необходимо и "прятать" всё остальное.

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

Например:

function doSomething(a) {
	b = a + doSomethingElse( a * 2 );

	console.log( b * 3 );
}

function doSomethingElse(a) {
	return a - 1;
}

var b;

doSomething( 2 ); // 15

В этом коде, переменная b и функция doSomethingElse(..) — похоже "частные" детали того, как doSomething(..) выполняет свою работу. Предоставление окружающей области видимости "доступа" к b и doSomethingElse(..) не только не необходимо, но также возможно и "опасно" тем, что их могут нечаянно использовать, намеренно или нет, и это может нарушить предварительные соглашения в doSomething(..).

Более "правильный" дизайн скрыл бы эти частные детали внутри области видимости doSomething(..), например как тут:

function doSomething(a) {
	function doSomethingElse(a) {
		return a - 1;
	}

	var b;

	b = a + doSomethingElse( a * 2 );

	console.log( (b * 3) );
}

doSomething( 2 ); // 15

Теперь, b и doSomethingElse(..) недоступны для любого внешнего воздействия, а взамен управляются только в doSomething(..). Функциональность и конечный результат оказались не затронуты, а дизайн хранит частные детали частными, что обычно считается лучшим ПО.

Предотвращение коллизий

Еще одно преимущество "скрытия" переменных и функций внутри области действия — чтобы избежать неумышленных коллизий между двумя идентификаторами с одним и тем же именем, но с разным целевым использованием. Коллизии часто приводят к неумышленной перезаписи значений.

Например:

function foo() {
	function bar(a) {
		i = 3; // меняем `i` в окружающей области видимости цикла for-loop
		console.log( a + i );
	}

	for (var i=0; i<10; i++) {
		bar( i * 2 ); // упс, впереди бесконечный цикл!
	}
}

foo();

Присваивание i = 3 внутри bar(..) неожиданно перезаписывает i, которая была объявлена в foo(..) внутри цикла for-loop. В этом случае, это приведет к бесконечному циклу, так как i установлена в фиксированное значение 3 и она всегда будет оставаться < 10.

Присваиванию внутри bar(..) нужно объявить локальную переменную для своих нужд, независимо от того, какое имя идентификатора выбрано. var i = 3; исправило бы эту проблему (и создало бы ранее упоминавшееся объявление "затененной переменной" i). Дополнительная, не альтернативная возможность — выбрать совсем другое имя идентификатора, такое как var j = 3;. Но дизайн вашего ПО естественно может подразумевать использование одного и того же имени идентификатора, поэтому использование области видимости, чтобы "скрыть" ваше внутреннее объявление — это ваша лучшая/единственная возможность в этом случае.

Глобальные "Пространства имен"

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

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

Например:

var MyReallyCoolLibrary = {
	awesome: "stuff",
	doSomething: function() {
		// ...
	},
	doAnotherThing: function() {
		// ...
	}
};

Управление модулями

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

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

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

Функции как области видимости

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

Например:

var a = 2;

function foo() { // <-- вставляем это

	var a = 3;
	console.log( a ); // 3

} // <-- и это
foo(); // <-- и это

console.log( a ); // 2

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

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

К счастью, JavaScript предлагает решение обеих проблем.

var a = 2;

(function foo(){ // <-- вставляем это

	var a = 3;
	console.log( a ); // 3

})(); // <-- и это

console.log( a ); // 2

Давайте разберем в деталях что же тут происходит.

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

Примечание: Самый легкий путь отличить объявление от выражения — позиция слова "function" в операторе (не только строка, но и отдельный оператор). Если "function" — самое первое, что стоит в операторе, то это объявление функции. Иначе, это функциональное выражение.

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

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

Иными словами, (function foo(){ .. }) как выражение означает, что идентификатор foo может быть найден только в области видимости, которая обозначена .., но не во внешней области видимости. Скрывание имени foo внутри себя означает, что оно не будет неоправданно загрязнять окружающую область видимости.

Анонимный против названного

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

setTimeout( function(){
	console.log("I waited 1 second!");
}, 1000 );

Это называется "анонимное функциональное выражение", так как у function()... нет именованного идентификатора. Функциональные выражения могут быть анонимными, но объявления функций не могут опускать имя — это было бы невалидным синтаксисом JS.

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

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

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

  3. Анонимные функции опускают имя, что часто удобно для обеспечения большей читаемости/понятности кода. Наглядное же имя помогает самодокументировать рассматриваемый код.

Встраиваемые функциональные выражения — мощные и полезные инструменты, выбор между анонимными и именованными не умаляет их достоинств. Именование вашего функционального выражения достаточно эффективно решает все перечисленные недостатки, и при этом не имеет ощутимых минусов. Лучшая практика — это всегда именовать ваши функциональные выражения:

setTimeout( function timeoutHandler(){ // <-- Смотри, у меня есть имя!
	console.log( "I waited 1 second!" );
}, 1000 );

Вызов функциональных выражений по месту

var a = 2;

(function foo(){

	var a = 3;
	console.log( a ); // 3

})();

console.log( a ); // 2

Теперь, когда у нас есть функция как выражение благодаря обертыванию ее в пару ( ), мы можем вызвать эту функцию добавив еще одни () в конце, как тут (function foo(){ .. })(). Первая окружающая пара ( ) делает функцию выражением, а вторая () выполняет функцию.

Этот шаблон настолько в ходу, что несколько лет назад сообщество условилось о термине для него: IIFE, что означает Immediately (немедленно) Invoked (вызываемое) Function (функциональное) Expression (выражение).

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

var a = 2;

(function IIFE(){

	var a = 3;
	console.log( a ); // 3

})();

console.log( a ); // 2

Есть легкая вариация формы традиционной IIFE, которую предпочитают некоторые: (function(){ .. }()). Посмотрим ближе, чтобы увидеть разницу. В первой форме функциональное выражение обернуто в ( ), а затем пара вызывающих ее () снаружи прямо за ними. Во второй форме, вызывающая пара () переместилась внутрь внешней окружающей пары ( ).

Эти две формы идентичны по функциональности. Какую из них предпочесть — всего лишь ваш стилистический выбор.

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

Например:

var a = 2;

(function IIFE( global ){

	var a = 3;
	console.log( a ); // 3
	console.log( global.a ); // 2

})( window );

console.log( a ); // 2

Мы передаем ссылку на объект window, но параметр мы назвали global, так что у нас есть ясное стилистическое разграничение для глобальных и неглобальных ссылок. Конечно, вы можете передать внутрь что-то из окружающей области видимости, если хотите, и можете назвать параметр(ы) так, как удобно вам. Это в основном всего лишь стилистический выбор.

Еще одно применение этого шаблона решает (узкоспециализированную) проблему, что значение идентификатора по умолчанию undefined может быть некорректно перезаписано, приведя к неожиданным результатам. Давая параметру имя undefined, но не передавая никакого значения для этого аргумента, мы можем гарантировать, что идентификатор undefined фактически имеет незаданное значение в этом блоке кода:

undefined = true; // устанавливаем мину для другого кода! остерегайтесь!

(function IIFE( undefined ){

	var a;
	if (a === undefined) {
		console.log( "Undefined is safe here!" );
	}

})();

Еще одна вариация IIFE меняет порядок вещей на обратный, где вызываемая функция идет второй, после вызова и параметров, которые в нее передаются. Этот шаблон используется в проекте UMD (Universal Module Definition). Некоторые люди находят его более ясным для понимания, хотя он немного более многословный.

var a = 2;

(function IIFE( def ){
	def( window );
})(function def( global ){

	var a = 3;
	console.log( a ); // 3
	console.log( global.a ); // 2

});

Функциональное выражение def определяется во второй половине кода, а затем передается как параметр (также названный def) в функцию IIFE, определенную в первой половине кода. Наконец, параметр def (функция) вызывается, передавая window в нее как параметр global.

Блоки как области видимости

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

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

Но даже если вы никогда не писали ни строчки кода в стиле блочной области видимости, вы возможно все-таки знакомы с этой чрезвычайно общей идиомой в JavaScript:

for (var i=0; i<10; i++) {
	console.log( i );
}

Мы объявляем переменную i прямо внутри заголовка цикла for-loop, скорее всего потому, что наше намерение — использовать i только в контексте этого цикла for-loop и в основном игнорировать факт того, что переменная на самом деле заключает себя в окружающую область видимости (функции или глобальную).

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

var foo = true;

if (foo) {
	var bar = foo * 2;
	bar = something( bar );
	console.log( bar );
}

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

Блочная область видимости — это инструмент для расширения ранее упоминаемого "Принципа наименьшихей привилегий открытости" с сокрытия информации в функциях на сокрытие информации в блоках вашего кода.

Давайте еще раз посмотрим пример с for-loop:

for (var i=0; i<10; i++) {
	console.log( i );
}

Зачем загрязнять всю область видимости функции переменной i, которая будет использоваться (или только следует, по меньшей мере) в цикле for-loop?

Но более важно, что разработчики могут предпочесть проверить себя на предмет случайного (пере)использования переменных снаружи от их предполагаемой области действия, как например получить ошибку о неизвестной переменной, если вы пытаетесь использовать ее не в том месте. Блочная область видимости (если бы она была возможна) для переменной i сделала бы i доступной только для цикла for-loop, приводя к ошибке, если к i осуществляется доступ в другом месте функции. Это помогает убедиться, что переменные не будут переиспользованы в нечетких или труднообслуживаемых сценариях.

Но, печальная реальность состоит в том, что, на первый взгляд, в JavaScript нет возможности блочной области видимости.

Или, если точнее, пока вы не копнете немного глубже.

with

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

try/catch

Очень малоизвестным фактом является то, что JavaScript в ES3 специфицирует объявление переменной в блоке catch оператора try/catch как принадлежащее блочной области видимости блока catch.

Например:

try {
	undefined(); // нелегальная операция, чтобы вызвать исключение!
}
catch (err) {
	console.log( err ); // работает!
}

console.log( err ); // ReferenceError: `err` not found

Как видите, err существует только в блоке catch и выбрасывает ошибку когда вы пытаетесь сослаться на нее где-либо в другом месте.

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

Чтобы избежать таких ненужных предупреждений, некоторые разработчики именуют свои переменные в catch как err1, err2 и т.д.. Другие разработчики просто выключают проверку линтера на повторяющиеся имена переменных.

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

let

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

К счастью, ES6 поменял ситуацию и представляет новое ключевое слово let, которое соседствует с var как еще один путь объявления переменных.

Ключевое слово let присоединяет объявление переменной к области видимости того блока (обычно пара { .. }), в котором оно содержится. Иными словами, let неявно похищает у любой блочной области видимости ее объявления переменных.

var foo = true;

if (foo) {
	let bar = foo * 2;
	bar = something( bar );
	console.log( bar );
}

console.log( bar ); // ReferenceError

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

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

var foo = true;

if (foo) {
	{ // <-- явный блок
		let bar = foo * 2;
		bar = something( bar );
		console.log( bar );
	}
}

console.log( bar ); // ReferenceError

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

Примечание: Еще один путь объявить явные блочные области видимости есть в приложении B.

В главе 4 мы рассмотрим поднятие переменных (hoisting), которая расскажет об объявлениях, которые воспринимаются как существующие для всей области видимости, в которой они появляются.

Однако, объявления, сделанные с помощью let, не поднимаются во всей области видимости блока, в котором они появляются. Такие объявления очевидно не будут "существовать" в блоке до оператора объявления.

{
   console.log( bar ); // ReferenceError!
   let bar = 2;
}

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

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

Пример:

function process(data) {
	// делаем что-то интересное
}

var someReallyBigData = { .. };

process( someReallyBigData );

var btn = document.getElementById( "my_button" );

btn.addEventListener( "click", function click(evt){
	console.log("button clicked");
}, /*capturingPhase=*/false );

Обратный вызов обработчика щелчка click совсем не требует переменную someReallyBigData. Это значит, теоретически, что после выполнения process(..), большая памятезатратная структура данных может быть собрана сборщиком мусора. Однако, весьма вероятно (хотя зависит от реализации), что движок JS все еще должен будет оставить структуру в памяти, поскольку у функции click есть замыкание, действующее во всей области видимости.

Блочная область видимости может устранить этот недостаток, делая более явным для движка то, что ему не нужна someReallyBigData:

function process(data) {
	// делаем что-то интересное
}

// всё, что объявлено внутри этого блока, может исчезнуть после него!
{
	let someReallyBigData = { .. };

	process( someReallyBigData );
}

var btn = document.getElementById( "my_button" );

btn.addEventListener( "click", function click(evt){
	console.log("button clicked");
}, /*capturingPhase=*/false );

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

let в циклах

Особый случай, в котором let показывает себя с лучшей стороны — в случае с циклом for, как мы уже обсуждали ранее.

for (let i=0; i<10; i++) {
	console.log( i );
}

console.log( i ); // ReferenceError

let в заголовке цикла for не только привязывает i к телу цикла, но фактически, он перепривязывает ее в каждой итерации цикла, обязательно переприсваивая ей значение с окончания предыдущей итерации.

Вот еще один способ показать режим пред-итеративной привязки:

{
	let j;
	for (j=0; j<10; j++) {
		let i = j; // перепривязка в каждой итерации!
		console.log( i );
	}
}

Причина, по которой эта пред-итеративная привязка интересна, станет ясна в главе 5, когда мы обсудим замыкания.

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

Пример:

var foo = true, baz = 10;

if (foo) {
	var bar = 3;

	if (baz > bar) {
		console.log( baz );
	}

	// ...
}

Этот код довольно легко отрефакторить в такой:

var foo = true, baz = 10;

if (foo) {
	var bar = 3;

	// ...
}

if (baz > bar) {
	console.log( baz );
}

Но, остерегайтесь таких изменений, когда используете переменные блочной области видимости:

var foo = true, baz = 10;

if (foo) {
	let bar = 3;

	if (baz > bar) { // <-- не забудьте про `bar`, если будете перемещать этот блок!
		console.log( baz );
	}
}

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

const

В дополнение к let, ES6 представляет ключевое слово const, которое также создает переменную блочной области видимости, но чье значение фиксированно (константа). Любая попытка изменить это значение позже приведет к ошибке.

var foo = true;

if (foo) {
	var a = 2;
	const b = 3; // в блочной области видимости содержащего ее `if`

	a = 3; // просто отлично!
	b = 4; // ошибка!
}

console.log( a ); // 3
console.log( b ); // ReferenceError!

Обзор

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

Но функции — отнюдь не только единицы области видимости. Блочная область видимости ссылается на идею, что переменные и функции могут принадлежать произвольному блоку (обычно, любой паре { .. }) кода, нежели только окружающей функции.

Начиная с ES3, в структуре try/catch есть блочная область видимости в выражении catch.

В ES6 представлено ключевое слово let (родственница ключевого слова var), чтобы позволить объявления переменных в любом произвольном блоке кода. if (..) { let a = 2; } объявит переменную a, которая фактически похитит область видимости блока { .. } в if и присоединит себя к ней.

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

Принцип наименьших привилегий