TL;DR

пусть это новый вар. ES6 предоставляет два новых способа объявления переменных: let и const. Они в значительной степени заменяют способ объявления переменных ES3/ES5 с использованием var. Используя область видимости на уровне блоков, эти два ключевых слова помогают разработчикам JavaScript избежать распространенных ошибок, которые они допускают не потому, что они пишут плохой код, а потому, что они не полностью понимают особенности того, как JavaScript обрабатывает переменные.

Давайте посмотрим на пример:

function simpleExample(value) {
 const constValue = value;
if (value) {
 var varValue = value;
 let letValue = value;
console.log(‘inside block’, varValue, letValue);
 }
console.log(‘outside block’);
// varValue is available even though it was defined
 // in if-block because it was “hoisted” to function scope
 console.log(varValue);
try {
 // letValue is a ReferenceError because it
 // was defined w/in if-block
 console.log(letValue);
 }
 catch (e) {
 // e is a ReferenceError
 console.log(‘letValue not accessible’, e);
 }
// SyntaxError to try and update a variable
 // declared via const
 //constValue += 1;
}
simpleExample(2);

Переменные, объявленные через let, недоступны вне блока, в котором они объявлены. Переменные, объявленные через const, также не могут быть обновлены. Вы можете найти больше примеров в примерах кода области видимости на уровне блоков для Learning ES6 репозитория Github. Есть также несколько каталогов ES6 для проверки ваших знаний о области видимости блоков ES6.

Вы просто знаете, что вам интересно, так что продолжайте читать!

Беглый взгляд на вар

Прежде чем мы перейдем к let и const, давайте напомним себе, как работает var. В Истории ECMAScript вы узнаете, что Брэндон Эйх якобы создал JavaScript за 10 дней. Мне до сих пор трудно в это поверить, но то, как объявления var работают в JavaScript, вполне может быть доказательством того, что он действительно разработал его так быстро.

Николас С. Закас лучше всего объясняет это в своей книге Understanding ECMAScript 6:

Традиционно одной из сложных частей JavaScript было то, как работают объявления var. В большинстве языков на основе C [таких как C++, Java или C#] переменные создаются в том месте, где происходит объявление. Однако в JavaScript это не так. Переменные, объявленные с помощью var, поднимаются в начало функции (или глобальной области видимости) независимо от того, где происходит фактическое объявление.

Большую часть времени мы не сталкиваемся с какими-либо проблемами с var, но когда мы случайно сталкиваемся с ними, возникающие в результате ошибки могут быть неприятными:

function varExample() {
 var myVar = 7;
console.log(‘myVar after declaration’, myVar);
// even though laterVar is defined later on in the function
 // it is “hoisted” to the beginning of the function &
 // initialized to undefined. In most C-style languages this would
 // be an error.
 console.log(‘laterVar before declaration’, laterVar);
laterVar = 10;
// image some legitimate conditional
 if (myVar < 20) {
 // accidental redeclaration of myVar results
 // in outer defined myVar being reassigned
 // to ‘foo’
 var myVar = ‘foo’;
 var innerVar = true;
console.log(‘myVar inside block’, myVar);
 }
// since this declaration was “hoisted”, it’s as if it’s no
 // longer here but at the top of the function
 var laterVar;
// looking at the code laterVar *should* be undefined,
 // but it has the value 10 from earlier
 console.log(‘laterVar after declaration’, laterVar);
// we would expect myVar to still be 7
 // but it was redefined and overwritten
 // w/in the conditional
 console.log(‘myVar outside block’, myVar === 7);
// we would expect innerVar to no longer be accessible
 // since it was defined w/in the if-block, but it was
 // “hoisted” as well
 console.log(‘innerVar outside block’, innerVar);
}
varExample();

Вы можете себе представить, если бы ваша функция была более сложной, как вы могли бы случайно повторно объявить свои переменные и запутаться в том, почему функция ведет себя неправильно. Вот почему существуют правила JSHint и ESLint, согласно которым все объявления var должны находиться в начале функции. Здесь, в Eventbrite, мы используем ESLint, чтобы помочь реализовать стили кодирования JavaScript, которые мы считаем важными, либо для согласованности кода, либо для предотвращения ошибок. Мы используем плагин vars-on-top ESLint, чтобы гарантировать, что наши переменные JavaScript не подвержены поднятию переменных.

пусть это новый вар

let работает аналогично var, но объявляемая им переменная имеет блочную область видимости; он только существует в текущем блоке.

function letExample(value) {
 if (value) {
 let letValue = value;
console.log(‘inside block’, letValue);
// redeclaration of letValue would be a SyntaxError
 let letValue = ‘foo’;
 }
try {
 // Accessing letValue is a ReferenceError because it
 // was defined w/in if-block
 console.log(letValue);
// if we get here, it means that the JS engine didn’t
 // throw an exception, which means that the engine
 // (or transpiled code) did not faithfully reproduce
 // how let should work
 console.log(‘let not faithfully handled’);
 }
 catch (e) {
 // e is a ReferenceError
 console.log(‘letValue not accessible’, e);
 }
}
letExample(2);

Как видите, возникает ReferenceError, если вы пытаетесь получить доступ к переменной за пределами блока, в котором она была объявлена. С var мы бы получили undefined. Также повторное объявление переменной let является ошибкой типа. С объявлением var вы не получите такого предупреждения. Короче говоря, пусть работает так, как вы, вероятно, думали, работает var.

Транспилированный код let

Когда объявления let переносятся в ES3, они в основном преобразуются в объявления var. Если вы попытались транспилировать приведенный выше код, но с letValue = ‘foo’; без комментариев, и Babel, и TypeScript выдают ошибки компиляции. Они даже не транспилируют код из-за повторного объявления.

Однако доступ к letValue за пределами блока — это совсем другая история. Перенесенный код Babel заменяет все соответствующие варианты использования letValue на _letValue. В результате все равно возникает ошибка ReferenceError при доступе к letValue в блоке try-catch даже при транспилированном коде с использованием объявлений var. Довольно мило! Traceur, к сожалению, не так надежен, в результате чего console.log('пусть не верно обработано'); строка действительно выполняется. Мне интересно, не сбивает ли Трейсер с толку попытка-улов. О чем следует помнить при выборе транспилера.

Затенение переменных с помощью let

Ранее в примере с varExample мы видели, что когда вы повторно объявляете переменную с помощью var во вложенной области видимости (например, в блоке if), переменная на самом деле не переопределяется. Поскольку переменные имели одно и то же имя, второе объявление просто привело к переназначению значения переменной. Это не относится к let:

function letShadowExample() {
 let x = 15;
if (true) {
 // this x “shadows” the x defined in the outer scope.
 // this new x just exists within the scope of the
 // if-block
 let x = 21;
// x should be 21
 console.log(‘x inner block’, x);
 }
// x should be 15
 console.log(‘x outer block’, x);
}
letShadowExample();

Во вложенной области действия блока if объявление let элемента x отличается от объявления внешней области. Надеюсь, вы не станете писать такой код, потому что это очень запутанно, но, по крайней мере, теперь он работает как другие основные языки программирования. И Babel, и Traceur переименовывают вложенную переменную x во что-то другое, чтобы при переносе кода в ES3 с использованием объявлений var переменные обрабатывались по-разному.

Держите вещи постоянными

Объявление const работает так же, как и let, за исключением того, что вы должны немедленно инициализировать переменную значением. И это значение не может быть изменено впоследствии. Вы получите SyntaxError, если вам не удастся инициализировать переменную при объявлении или если вы попытаетесь переназначить ее значение. Давайте взглянем на быстрый пример:

function constExample() {
 const NAME_KEY = ‘name’;
 const UNFROZEN_OBJ_CONST = { key: ‘adam’, val: ‘eve’ };
 const FROZEN_OBJ_CONST = Object.freeze({ key: ‘jesus’, val: ‘paul’ });
// All const declarations must be initialized.
 // It’s a SyntaxError otherwise
 const VALUE_KEY;
// Const variables are read-only, so trying to
 // reassign is a SyntaxError too
 NAME_KEY = ‘key’;
// GOTCHA: even though the object is const, you can still
 // change properties of it. It’s the variable
 // that cannot be reassigned
 UNFROZEN_OBJ_CONST.key = ‘moses’;
// by freezing the object, using ES5 Object.freeze
 // its properties cannot be changed.
 // in strict mode this a TypeError. In non-strict
 // mode the value silently doesn’t change
 FROZEN_OBJ_CONST.val = ‘peter’;
console.log(‘const value’, NAME_KEY);
 console.log(‘unfrozen object’, UNFROZEN_OBJ_CONST);
 console.log(‘frozen object’, FROZEN_OBJ_CONST);
}
constExample();

Как показано в коде, переменная, объявленная через const, означает, что она не может быть переназначена, но это не означает, что ее содержимое нельзя изменить, когда она является объектом. Мы можем немного исправить эту проблему (если она есть), используя метод Object.freeze, который мы получили от ES5. Однако, если вы используете Object.freeze, ваш код больше не будет работать в движках JS, которые не поддерживают ES5 (таких как IE8 и ниже).

Здесь, в Eventbrite, мы в основном используем const для ссылки на зависимости модулей через require() в нашем Backbone/Marionette JavaScript:

‘use strict’;
const Marionette = require(‘marionette’);
const _ = require(‘underscore’);
const eventCardTemplate = require(‘./eventCard.handlebars’);
const eventCardModel = require(‘./eventCardModel.js’);
return Marionnette.ItemView.extend({
 template: eventCardTemplate,
 model: eventCardModel,
// other stuff
});

В идеале это должен быть новый синтаксис модуля ES6, но мы еще не настроили Babel для преобразования этого синтаксиса в ожидаемый нами формат CommonJS.

Вход во временную мертвую зону

Временная мертвая зона (TDZ) – это просто причудливый термин, используемый для обозначения периода времени, когда выполнение кода находится в области действия переменной, объявленной с помощью let или const, но до этого. фактически декларируется. Переменная находится в области видимости, но еще не инициализирована. Доступ к неинициализированной переменной вызывает ошибку ReferenceError. Давайте посмотрим на пример кода:

{
 // Uninitialized “binding” for `disciple` variable is created
 // upon entering scope. TDZ for `disciple` variable begins
// accessing a variable in TDZ either to get or set
 // is a ReferenceError
 disciple = ‘matthew’;
 console.log(disciple);
// TDZ ends at declaration and `disciple` is initialized
 // w/ `undefined` value
 let disciple;
console.log(disciple); // undefined
disciple = ‘thomas’;
 console.log(disciple); // ‘thomas’
}

Так почему же она называется временной мертвой зоной? Это связано с тем, что мертвая зона основана на периоде выполнения кода время, а не на том, где код фактически находится:

function temporalDeadZoneExample() {
 // TDZ for `value` begins
const func = function() {
 // Even though this function is defined *before*
 // `value` in the code, it’s not called until after
 // `value` is declared, so accessing it is OK.
 console.log(‘value is: ‘, value);
 }
// TDZ for `value` continues. Accessing `value`
 // here would be a ReferenceError. Calling `func`
 // here would cause a ReferenceError.
// TDZ ends with declaration of `value`
 let value = ‘foo’;
// no longer in TDZ when calling function so now
 // any access of `value` is ok
 func();
}
temporalDeadZoneExample();

Переменные, объявленные с помощью var, не имеют TDZ, потому что переменные «поднимаются» в начало функций. Поэтому они всегда объявляются, а также инициализируются со значением undefined.

пусть и петли

Без ведома большинства разработчиков JavaScript переменная итерации, объявленная с помощью var в начале цикла for (например, for (var i = 0; i ‹ 5; i++)) доступна вне цикла for. Из-за области видимости на уровне блоков с let это уже не так.

function simpleLoopExample () {
 for (var i = 0; i < 5; i++) {
 console.log(‘i=’, i);
 }
 for (let j = 0; j < 5; j++) {
 console.log(‘j=’, j);
 }
// i is accessible outside of the for loop
 // and has the value 5
 console.log(‘after i=’, i);
// j is not accessible outside of the for loop
 // and is a ReferenceError
 console.log(‘after j=’, j);
}
simpleLoopExample();

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

function callbackLoopVarExample() {
 var $body = $(‘body’);
for (var i = 0; i < 5; i++) {
 // create 5 buttons with the index in the name
 var $button = $(‘<button>var ‘ + i + ‘</button>’);
// wire click handler w/ callback using arrow function!
 $button.click(
 // BUG! When button is clicked, the value of `i` is 5!
 () => console.log(‘var button ‘ + i + ‘ clicked!’)
 );
// add button to the body
 $body.append($button);
 }
}
callbackLoopVarExample();

Для тех, кто не слишком знаком с разработкой JavaScript, может быть не сразу понятно, почему в сообщении console.log всегда есть «нажата кнопка 5 var!». Поскольку переменная i «поднимается» наверх функции, она все еще имеет значение после завершения цикла for. Это значение равно 5, что и вызвало завершение цикла. А так как i привязан ко всей функции, все функции обратного вызова привязаны к одному и тому же i, в результате чего все они отображают «нажата кнопка 5 var!».

Способ решения этой проблемы в ES3/ES5 заключался в использовании отдельной именованной функции или IIFE (немедленно вызываемого функционального выражения), которые создавали бы новую область действия для переменной итерации, так что каждая функция обратного вызова была бы привязана к своей собственной версии. . Вот пример:

function callbackLoopNamedFunctionExample() {
 var $body = $(‘body’);
// Create a named function passing in the loop iteration variable
 // which creates a unique scope for each iteration so
 // that the callback function binds to its own variable.
 var loop = function(index) {
 // create 5 buttons with the index in the name
 var $button = $(‘<button>function ‘ + index + ‘</button>’);
// wire click handler w/ callback using arrow function!
 $button.click(
 // Fixed! `index` is unique per iteration
 () => console.log(‘function button ‘ + index + ‘ clicked!’)
 );
// add button to the body
 $body.append($button);
 }
for (var i = 0; i < 5; i++) {
 loop(i);
 }
}
callbackLoopNamedFunctionExample();

Теперь, когда мы нажимаем каждую кнопку, отображается соответствующее сообщение. Эту проблему также можно было бы решить, если бы IIFE был определен в цикле for во многом так же, как была определена переменная функции цикла.

Однако потребность в таком обходном пути отпадает при объявлении переменной итерации через let:

function callbackLoopLetExample() {
 let $body = $(‘body’);
for (let i = 0; i < 5; i++) {
 // create 5 buttons with the index in the name
 let $button = $(‘<button>let ‘ + i + ‘</button>’);
// wire click handler w/ callback using arrow function!
 $button.click(
 // Fixed! `i` is a different variable declaration for
 // each iteration of the loop as one would expect!
 () => console.log(‘let button ‘ + i + ‘ clicked!’)
 );
// add button to the body
 $body.append($button);
 }
}
callbackLoopLetExample();

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

Следует отметить, что и Babel, и Traceur, когда замечают эту проблему, используют подход именованных функций при переносе кода ES6 в ES3. Это означает, что если в вашем коде есть ошибка, структура транспилируемого кода будет сильно отличаться от структуры вашего кода ES6. Пока у вас есть исходная карта в транспилированном коде и ваш код ES6 также доступен, любые номера строк, предоставленные движком, должны указывать вам на нужное место в вашем коде ES6.

Заключительное замечание о петлях. Переменные, объявленные с помощью let, работают так же и с циклами for-in. В ECMAScript 6 добавлен новый тип цикла, цикл for-of, который работает с массивами и итерируемыми объектами (также добавленный в ES6), но мы поговорим о них в следующей статье.

Работа с параметрами

Объявление переменной с let с тем же именем, что и параметр функции, является ошибкой типа:

function sellFruits(fruits) {
 let fruits = [];
}

Однако, если это объявление let происходит во вложенной области видимости (например, в блоке if), тогда переменная будет затенена:

function sellFruits(fruits) {
 // create a simple code block
 {
 // this let declaration of `fruits` shadows the
 // `fruits` parameter
 let fruits = [];
console.log(values); // []
 }
// `fruits` here is the parameter value
 console.log(fruits);
}

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

function sellFruits(fruits) {
 // this declaration does nothing
 var fruits;
// create simple code block
 {
 // the declaration does nothing, but the assignment
 // does assign the parameter value to []
 var fruits = [];
console.log(fruits); // []
 }
// `fruits` here is still [] from the assignment
 // in the block
 console.log(fruits);
}

var vs let vs const

Теперь, когда мы знаем, как работают let и const, когда нам следует использовать их вместо var? Вот несколько предложений:

  • Используйте const для переменных, которые вы хотите сделать неизменяемыми. Это лучше всего работает для примитивных значений (таких как число, строка, логическое значение и т. д.). Вы можете использовать const для объектов, но вам, вероятно, следует использовать Object.freeze совместно, чтобы сделать объект действительно неизменным. Вы можете использовать const для изменяемого объекта, но это побеждает «дух» const.
  • Используйте let для изменяемых переменных (т.е. всего остального)
  • Единственное время, когда вам может понадобиться использовать var, — это для объектов в глобальной области видимости, особенно для пространств имен или модулей в стиле ES3 и ES5. В идеале вы должны преобразовать их в модули в стиле ES6, но для обратной совместимости вам все равно может понадобиться использовать var.
  • Не смешивайте и сопоставляйте let и var в файле. Будьте последовательны, иначе это приведет к еще большей путанице.
  • Не выполняйте глобальный поиск и замену var на let. У вас может быть код, который непреднамеренно полагается на причудливость var. Вы должны выполнять преобразование вручную по одному файлу за раз.

Поддержка движка JavaScript

Согласно Таблице совместимости ECMAScript 6, следующие движки JavaScript поддерживают let и const:

  • Вавилон
  • Трейсер
  • Машинопись
  • Край
  • Chrome (с включенным экспериментальным флагом и в строгом режиме)
  • Firefox (блоки кода должны быть заключены в тег ‹script type="application/javascript;version=1.7›)
  • Опера (в строгом режиме)
  • Узел 5 (в строгом режиме)

Наиболее заметными отсутствующими движками являются Safari и iOS9. Конечно, IE 11 и более ранние версии не поддерживают никаких функций ES6. Чтобы движок правильно поддерживал let и const, особенно временную мертвую зону, вы должны убедиться, что ваши скрипты работают в строгом режиме.

Дополнительные ресурсы

Вы можете посетить Learning ES6 страницу примеров для Learning ES6 репозитория Github, где вы найдете весь код, использованный в этой статье, изначально работающий в браузере. (для тех, кто поддерживает let и const). Есть также примеры, проходящие транспиляцию Babel и Traceur.

Вы также можете практиковать все, чему научились на ES6 Katas. Он использует подход TDD (разработка через тестирование), чтобы вы могли реализовать функции ES6, чтобы все тесты прошли. Я очень рекомендую это!

Другие суперполезные ресурсы:

— Бен Илегбоду, старший инженер-программист Eventbrite