ES6: Блочные области видимости

До введения стандарта ES6 основой всех областей видимости являлись функции. У любой функции существует своя область видимости. Проще всего это можно рассмотреть на примере:

var num = 10;
var func = function() {
  var num = 20;
  console.log(num); // 20
};
console.log(num); // 10

Объявленные с помощью ключевого слова var переменные внутри функции не влияют на переменные из других областей видимости, в том числе и глобальной. Именно на этом свойстве основана хорошая практика обворачивания всего кода в самовызывающуюся анонимную функцию (self-executing anonymous function):

(function() {
  'use strict';
  // Переменные a и b находятся в области видимости 
  // самовызывающейся анонимной функции и не доступны
  // на более высоких уровнях
  var a = 10;
  var b = 20;
  // Для вывода переменной в глобальную область видимости
  // используется подобная конструкция
  window.b = b;
})();

console.log(a); // undefined
console.log(b); // 20

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

for (var i = 0; i < 5; i++) {
  console.log('Что-то было сделано ' + i + ' раз'); // 0 1 2 3 4
}

console.log('Переменная i до сих пор доступна и равна ' + i); // 5

Пример с циклом for относительно безвреден и после последней итерации оставляет переменную i. Подобное использование циклов широко распространено и, скорее всего, не станет причиной ошибки. Неочевидные вещи начинают появляться при использовании других блочных конструкций:

if (true) {
  var a = 10;
}

console.log(a); // 10

Очевидно, что после запуска данного кода будет создана переменная a, содержащая в себе число 10. Все становится не так очевидно, когда условие переданное в конструкцию if не является правдивым:

if (false) {
  var a = 10;
}

console.log(a); // ???

Какой результат можно ожидать? Код внутри конструкции if не запускался, а значит и перменная a не была инициализирована. Логично предположить, что единственным возможным результатом является ошибка (попытка обратиться к несуществующей переменной обычно выдает ReferenceError). Тем не менее, подобной ошибки не возникает, и в консоль выводится undefined. Такое поведение объясняется поднятием переменных (hoisting). Конструкция, указанная выше, интерпретируется следующим образом:

var a;
if (false) {
  a = 10;
}

console.log(a); // undefined

Оператор let

С релизом стандарта ES6 появилась возможность создавать переменные, приуроченные к отдельным блокам. Это означает, что для создания лексического окружения (scope) достаточно просто обвернуть код в фигурные скобки: { ... }:

var a = 10;

{
  let a = 20;
  console.log(a); // 20
}

console.log(a); // 10

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

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

for (let i = 0; i < 5; i++) {
  console.log(i); // 0 1 2 3 4
}

console.log(i); // ReferenceError: i is not defined

Подобное поведение будет наблюдаться и в других блочных конструкциях:

if (true) {
  let num = 10;
  console.log(num); // 10
}

console.log(num); // ReferenceError: num is not defined

Hoisting

При использовании ключевого слова let происходит поднятие переменных (hoisting). Но сам процесс поднятия реализуется совершенно другим образом:

if (true) {
  console.log(b);
  let b = 10; // ReferenceError: b is not defined
}

Запустив подобный пример вы скорее всего можете прийти к выводу, что поднятия не происходит. Это не так. Для того, чтобы убедиться в наличии поднятия достаточно объявить еще одну переменную вне блока:

let b = 20;
if (true) {
  console.log(b);
  let b = 10; // ReferenceError: b is not defined
}

Несмотря на то, что переменная b была объявлена вне блока и, таким образом, должна быть доступна, результатом выполнения кода все равно является ReferenceError. Подобное поведение называется “временной мёртвой зоной”.

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

if (true) { let num = 20;
  console.log(num); // 20
}

Временная мёртвая зона не распространяется на функции до первого их вызова:

var f = function() {
  return num;
};

let num = 10;
console.log(f()); // 10

Тем не менее, если вы попытаетесь вызвать функцию f до того, как будет объявлена переменная num, то всё равно получите ошибку:

var f = function() {
  return num;
};

console.log(f()); // ReferenceError: num is not defined
let num = 10;

Оператор const

Оператор const, как и let, работает с блочными областями видимости (также подвергается правилам временной мёртвой зоны) и предназначен для создания констант - переменных, для которых доступно только чтение после их инициализации:

{
  const num = 10;
  console.log(num); // 10
  num = 20; // TypeError: 'num' is read-only
}

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

const a = 20;
let a = 10; // TypeError: 'a' is read-only

Заново инициализировать переменную с помощью оператора const тоже не получится:

let a = 10;
const a = 10; // TypeError: Identifier 'a' has already been declared

Константы непостоянны

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

const lit = 4;
lit = 5; // TypeError: Литерал изменить нельзя

const obj = { a: 1 };
obj.a = 2; // Значения внутри объекта изменить можно
console.log(obj); // { a: 2 }
obj = { a: 3 }; // TypeError: Ссылку менять нельзя

const arr = [1, 2, 3];
arr.push(4); // Значения внутри массива изменить можно
console.log(arr); // [1, 2, 3, 4]
obj = [4, 3, 2, 1]; // TypeError: Ссылку менять нельзя

Чтобы сделать константу, содержащую объект, настоящей константой слудует использовать Object.freeze().

Ссылки по теме

Комментарии