ES6: Let, Const и «Временная мёртвая зона» (ВМЗ) изнутри
Перевод статьи ES6 Let, Const and the “Temporal Dead Zone” (TDZ) in Depth с сайта ponyfoo.com, опубликовано на css-live.ru с разрешения автора — Николаса Беваквы.
Вот и ещё один выпуск «ES6 изнутри». Вы здесь впервые? Приветствую! Мы уже обсудили такие фичи, как деструктирование, литералы шаблона, стрелочные функции, оператор расширения и оставшиеся параметры, литерал объекта, и еще одну важную штуку — что такое классы в ES6. Сегодня мы встретимся с набором простых фич языка, которые появились в ES6 — let
, const
и нечто с жутковатым названием «Временная мертвая зона»
Как и в прошлых статьях, рекомендую вам установить Babel и повторять за мной, копируя примеры с помощью интерактивной оболочки REPL, либо командной строки babel-node
и файла. Это поможет гораздо лучше усвоить идеи, обсуждаемые в серии. Если вы не из тех, кто любит устанавливать что-либо на свой компьютер, то вам есть смысл залезть на CodePen и кликнуть иконку с шестерёнкой для JavaScript — у него есть препроцессор Babel, который с лёгкостью позволяет опробовать ES6. Ещё одна довольно полезная альтернатива, это использовать онлайновый REPL для Babel — он показывает скомпилированный ES5-код справа от ES6-кода, чтобы быстро сравнить.
Начнём?
Оператор Let
Оператор let
— одна из самых известных фич в ES6, именно поэтому я объединил её с другими новыми фичами. let
напоминает var
, но с иными правилами области видимости. Области видимости в JavaScript всегда были сложными для понимания, сводя многих программистов с ума, когда те впервые пытались разобраться в работе переменных.
Как только вы откроете для себя штуку, называемую поднятием, всё сразу проясняется. Смысл поднятия в том, что переменные переносятся из места объявления в коде в верх их области видимости. К примеру, есть такой код:
function areTheyAwesome (name) { if (name === 'nico') { var awesome = true } return awesome } areTheyAwesome('nico') // <- true areTheyAwesome('christian heilmann') // <- undefined
Причина, по которой это приходится учитывать — то, что, как мы знаем, область видимости var
ограничена функцией. В сочетаниии с поднятием это означает, что на самом деле наш код соответствует чему-то вроде следующего примера.
function areTheyAwesome (name) { var awesome if (name === 'nico') { awesome = true } return awesome }
Нравится нам или нет (либо мы просто привыкли к этому — по себе знаю), но это явно запутаннее, чем когда область видимости переменных ограничена блоком. Блочная область видимости работает на уровне скобок, а не на уровне функции.
С блочной областью видимости незачем объявлять новую функцию function
, если нужно глубже ограничить видимость переменной, а можно просто воспользоваться имеющимися блоками от операторов if
, for
, while
и т.п., а также создавать новые блоки {}
где угодно. Если вы вдруг не знали, в языке JavaScript можно создавать сколько угодно блоков просто так.
{{{{{var insane = 'да, конечно'}}}}} console.log(insane) // <- 'да, конечно'
Однако, с var
по-прежнему можно достучаться до переменной снаружи этих многочисленных блоков и не получить ошибку. Но бывает весьма кстати, если в такой ситуации выскочит ошибка. Тем более, если хотя бы одно из перечисленного верно.
- Доступ к внутренней переменной нарушает своего рода принцип инкапсуляции в коде
- Внутренняя переменная вообще не относится к внешней области видимости
- У блока кода могут быть соседи, которые тоже рады бы использовать это же имя переменной.
- Имя переменной, которое мы хотим использовать во внутреннем блоке, уже задействовано в одном из его родительских блоков.
Так как же этот let
работает?
Оператор
let
— альтернатива var. Он следует правилам блочной области видимости, а не область видимости, ограниченная функцией — правилам по умолчанию. Это значит, что нам больше не нужны целые функции ради новой области видимости — блока{}
достаточно!
let outer = 'Я так эксцентричен!' { let inner = 'Я играю с соседями в блоке и сточных трубах' { let innermost = 'Я играю только с соседями в блоке' } // обращение к самой внутренней переменной innermost здесь вызвало бы ошибку } // обращение к внутренней переменной inner здесь вызвало бы ошибку // обращение к самой внутренней переменной innermost здесь вызвало бы ошибку
Вот это уже интереснее. Пока я писал этот пример, то размышлял: «Ладно, но если мы теперь объявляем функцию внутри блока и получим доступ к ней снаружи этого блока, то наверняка всё пойдет наперекосяк». Исходя из опыта с ES5, я ожидал, что следующий фрагмент кода должен работать (и в ES5 это так), но в ES6 он ломается. И это была бы проблема, ведь при поднятии функции из блока наружу было бы слишком легко нарушить инкапсуляцию свойств, которые должны быть видимы лишь внутри него. Я не ожидал, что это выбросит ошибку.
{ let _nested = 'secret' function nested () { return _nested } } console.log(nested()) // nested не определена
Оказывается, это был не баг в Babel, а на самом деле (весьма отрадное) изменение в семантике языка ES6.
Цитата из твиттера:
Николас Беваква (@nzgb), твит
@rauschma @sebmck Запись `var nested = fn` работает, но `function nested ()` — нет
@RReverser
http://t.co/7EjTzJ6YrR
http://t.co/0vLQrmJTDZ
Ингвар Степанян (@RReverser), твит
@nzgb @rauschma @sebmck ЕМНИП, так и должно быть — ES6 наконец определил, что функции в блоках должны вести себя как с блочной областью видимости
Заметьте, что вы можете по-прежнему сделать видимыми вложенные переменные с let
для внешней области видимости, просто присваивая их переменной, у которой больше доступа. Однако, я бы не рекомендовал так делать, поскольку в таких ситуациях наверняка можно сделать код чище — например, не использовать let
, если не нужна блочная область видимости.
var nested { let _nested = 'секрет' nested = function () { return _nested } } console.log(nested()) // <- 'секрет'
Подводя итог, блочная область видимости могла бы быть весьма полезна в новом коде. Одни предложат отказаться от var
навсегда в пользу let
. Другие — никогда не использовать let
, поскольку его не приемлет Единственно Верный Путь JavaScript. Моя позиция со временем может измениться, но вот она на данный момент:
Я планирую использовать
var
почти всё время, alet
там, где ни к чему бессмысленное поднятие переменных, которые по логике принадлежат блоку условия или цикла, в начало всей функции.
Временная мёртвая зона и Дары Смерти
Последнее, что касается let
— таинственное понятие под названием «Временная мёртвая зона» (ВМЗ) — уу… как страшно, знаю.
Попросту говоря: следующий код выбросит ошибку.
there = 'там обитают' // <- ReferenceError: there не определена let there = 'драконы'
Если в коде попробовать обратиться к there до объявления let there
, то программа вызовет ошибку. Объявление метода, который ссылается на there
до определения последнего — это не страшно, при условии, что метод не выполняется, пока there
находится в ВМЗ, а there будет находиться в ВМЗ, пока мы не дойдем до оператора let there
(хотя область видимости уже началась). Этот фрагмент кода не вызовет ошибку, поскольку return there
не выполняется до тех пор, пока there
не выйдет из ВМЗ.
function readThere () { return there } let there = 'драконы' console.log(readThere()) // <- 'драконы'
Но этот фрагмент кода вызовет, поскольку обращение к there
происходит до выхода из ВМЗ для there
.
function readThere () { return there } console.log(readThere()) // ReferenceError: there не определена let there = 'драконы'
Заметьте, что даже если при первом объявлении there не присваивать ей значения, семантика для этих примеров не изменится. Код ниже всё ещё выкидывает ошибку, поскольку мы по-прежнему пытаемся обратиться к there
до выхода из ВМЗ.
function readThere () { return there } console.log(readThere()) // ReferenceError: there не определяется let there
Этот код всё ещё работает, поскольку мы по-прежнему выходим из ВМЗ до какого-либо обращения к there
.
function readThere () { return there } let there console.log(readThere()) // <- undefined
Единственная хитрость, о которой стоит помнить (когда дело касается ВМЗ), что функции до своего первого выполнения действуют как «чёрные ящики», поэтому вполне нормально размещать there
внутри функций, которые не выполняются, пока мы не выйдем из ВМЗ.
Весь смысл ВМЗ — легче вылавливать ошибки там, где обращение к переменной до её объявления приводит к неожиданностям. Это зачастую происходило в ES5 из-за поднятия и непродуманных соглашений о стиле кода. Учтите, что поднятие применяется и к
let
тоже — это значит, что переменные будут созданы, когда мы зайдем в область видимости (и ВМЗ появится), но они будут недоступны до тех пор, пока выполнение кода не дойдёт до места фактического объявления переменной, и в этот момент мы выходим из ВМЗ и вправе делать с переменной что угодно.
Оператор Const
Уф. Я написал про let
больше, чем планировал. К счастью, const
очень похож на let
.
- У
const
тоже блочная область видимости. const
также наслаждается прелестями семантики ВМЗ.
А также есть пара основных отличий.
- Переменные
const
должны объявляться с помощью инициализатора. - Переменные
const
могут присваиваться только единожды, в этом инициализаторе. - Переменные
const
не гарантируют, что присвоенное значение нельзя будет изменить. - Присваивание
const
тихо проигнорируется - Переопределение переменной с тем же именем выбросит ошибку
Вернёмся к примерам. Прежде всего в этом коде видно, что он следует правилам блочной области видимости, как и let
.
const cool = 'ponyfoo' { const cool = 'драконы' console.log(cool) // <- 'драконы' } console.log(cool) // <- 'ponyfoo'
После объявления const
невозможно изменить ссылку или литерал, которые ему присвоены.
const cool = { people: ['вы', 'я', 'тесла', 'маск'] } cool = {} // <- "cool" только для чтения
Однако, можно изменять саму ссылку. Она не становится неизменной. Чтобы сделать само значение неизменный, вам пришлось бы использовать Object.freeze.
const cool = { people: [вы, я, тесла, 'маск'] } cool.people.push('бернерс-ли') console.log(cool) // <- { people: ['вы', 'я', 'тесла', 'маск', 'бернерс-ли'] }
Также можно создать другие ссылки для const
, и вот они-то уже могут изменяться.
const cool = { people: ['вы', 'я', 'тесла', 'маск'] } var uncool = cool uncool = { people: ['эдисон'] } // так неприятно, что он в полном одиночестве console.log(uncool) // <- { people: ['эдисон'] }
Я считаю, что const
великолепен, поскольку позволяет отмечать то, что нужно сохранить в неизменности. Представьте, следующий код, встречающийся в некоторых ситуациях — извиняюсь за бурную фантазию.
function code (groceries) { return {eat} function eat () { if (groceries.length === 0) { throw new Error('Всё. Пожалуйста, купите больше продуктов, чтобы покормить код.') } return groceries.shift() } } var groceries = ['морковь', 'лимон', 'картофель', 'индейка'] var eater = code(groceries) console.log(eater.eat()) // <- 'морковь'
Порой сталкиваюсь с кодом, где кто-то пытается добавить больше продуктов в список groceries
, и думает, что вот это сработает. Зачастую это так. Однако, если мы передаем ссылку на продукты чему-то ещё, повторное присваивание не будет передано в то другое место, что приведет к трудноотлаживаемым последствиям.
// Несколькими сотнями строк кода позже... groceries = ['сердцевина пальмы', 'помидор', 'ветчина']
Если бы в коде выше groceries
были константой, то повторное присваивание было бы гораздо легче обнаружить. Ура, ES6! В будущем я точно буду пользоваться const
сплошь и рядом, вот только освоюсь с ним получше.
Теперь дело только за практикой
P.S. Это тоже может быть интересно:
В комментариях к оригиналу статьи в очередной раз всплыл вопрос отличия «поднятия объявления переменной с ВМЗ» от отсутствия поднятия вообще. Может показаться, что раз переменная не определена до своего объявления, то она и не поднимается. Но имя этой переменной «резервируется» для нее с самого начала блока, даже при наличии внешней переменной с тем же именем: