Это вторая часть независимых статей о синтаксисе JavaScript. В первом мы исследовали различия между объявлениями функций и выражениями. Во время этих публикаций мы затронули несколько аспектов языка JavaScript, которые мы просто не заметили, но в этом сообщении мы рассмотрим подробнее; мы подробно рассмотрим различия между разными способами объявления переменных.

Если во многих других языках существует один способ объявления переменных, то в JavaScript их три. Все они служат разным целям, и вы можете использовать их в разное время. Не существует простого правила, которое гласит, что используйте его всегда. Эти вещи субъективны и личны. Лучший способ выбрать один - это знать, что каждый из них делает, и принять обоснованное решение.

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

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

Объявление или назначение?

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

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

var foo;

В этом случае у нас будет неопределенная переменная. Заявлено, но не назначено. Доступ к нему не имеет особого смысла, поскольку он пуст. Это случай объявления без присваивания. Можем ли мы сделать обратное, назначая без объявления? Да мы можем*:

foo = 'Hello, World!';

Я пометил предыдущее предложение звездочкой. Вы можете назначить переменную без предварительного объявления, если вы не находитесь в строгом режиме. Строгий режим - это особый режим в JavaScript, представленный в 2009 году, поскольку они не могут вносить некоторые изменения и по-прежнему поддерживают обратную совместимость. Оказывается, назначать переменную без предварительного объявления - плохая идея. Он стал глобальным и доступным во всех областях, независимо от того, где он определен. Это приводит к высокой вероятности коллизии при именовании и к тому, что когда вы ссылаетесь на переменную, она содержит что-то еще, чем вы ожидали. И это баг, который сложно отследить (существуют такие инструменты, как линтеры, которые могут нам в этом помочь).

Если вы работаете в строгом режиме, предыдущий пример имеет недопустимый синтаксис. Это означает, что делая

'use strict'; // activate strict mode
foo = 'Hello, World!';

вызовет ошибку:

ReferenceError: assignment to undeclared variable foo

И это хорошо. Мы заблаговременно выявляем антишаблон и предотвращаем возможные ошибки.

Три способа управлять ими всеми

Игнорируя отсутствие объявления вообще, есть три способа создания переменных: var (классический), let (переназначаемый) и const константа.

С самого начала JavaScript var был единственным способом объявления переменных. Используя var, мы можем объявить повторно назначаемые переменные, ограниченные функциями. Мы рассмотрим, что это означает, позже в блоге. Мы можем присвоить все значения var:

var num = 42;
var str = 'String';
var bool = 1 > 3;
var fn = () => {};

То же самое и для let и const. Хотя это более позднее дополнение к языку JavaScript (версия спецификации 2015 г.), теперь мы можем использовать их в большинстве браузеров.

let num = 42;
let str = 'String';
let bool = 1 > 3;
let fn = () => {};
const num = 42;
const str = 'String';
const bool = 1 > 3;
const fn = () => {};

Со всеми альтернативами мы можем выполнять несколько назначений для каждого объявления:

var num = 42, str = 'String', bool = 1 > 3;
let num = 42, str = 'String', bool = 1 > 3;
const num = 42, str = 'String', bool = 1 > 3;

С vars мы можем делать несколько объявлений одной и той же переменной, но не с _10 _ / _ 11_:

var foo = 42;
var foo = 'String';
console.log(foo); //=> 'String'

// But doesn't work:
let foo = 42;
let foo = 'String';
// SyntaxError: Identifier 'foo' has already been declared

Почему в языке были введены let и const? Что побудило к изменению? Две вещи: блокировка области и ограниченное переназначение.

Объем функций 360

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

(function () {
  var num = 42;
  // Can access num.
  console.log(num); //=> 42
}());
console.log(num); //=> ReferenceError: num is not defined

И мы можем вложить область видимости и получить доступ к внешним:

(function () {
  var num = 42;
  // Can access num.
  var myFunction = function () {
    var str = 'String';
    console.log(num);
  };
  myFunction(); //=> 42
  // Can not access str as it's defined in a inner scope
  console.log(str); //=> ReferenceError: str is not defined
}());
// Can not access str as it's defined in a inner scope
console.log(num); //=> ReferenceError: num is not defined

Здесь я использую функции для создания новой области видимости. Но если у вас есть опыт работы с другими языками программирования на основе C, вы знаете, что любой { } может указать область действия. Но с var это не так:

for (var i = 0; i < 10; i++) {
  var num = 42 + i;
  // Can access num here:
  console.log(num);
}
// But we can also access num here, outside of the scope:
console.log(num);

Фактически, вам даже не нужно обращаться к коду для объявления переменной:

if (false) {
  var foo = 1;
}
console.log(foo); // undefined

Как мы видели, отсутствие здесь ReferenceError означает, что переменная фактически объявлена. Не назначается, но определенно декларируется. foo не ограничен if-блоком, даже если это мертвый код. Здесь действуют и другие правила, но мы вернемся к ним в следующем разделе, когда поговорим о подъеме.

Эта концепция функциональных областей вместо областей видимости блока может оттолкнуть, если вы перейдете к JavaScript с другого языка, ожидая, что у него будет область видимости блока. Что ж, теперь это так. С помощью let и const вы можете ограничивать области действия блоками в JavaScript:

for (var i = 0; i < 10; i++) {
  let num = 42 + i;
  // Can access num here:
  console.log(num);
}
// Cannot access num here
console.log(num);
// ReferenceError: num is not defined

То же самое и с const. И let, и const имеют области действия блока, а var по-прежнему имеют область действия. Это важно для JavaScript. Спецификация ECMAScript (стандарт, реализацией которого является JavaScript) довольно усердно работает над обеспечением обратной совместимости. let был представлен как альтернатива var, но с блоком. Они не могли изменить семантику var, так как это привело бы к сбою многих веб-сайтов. Вы могли бы ввести новый тег, например "use strict", но это приведет к дальнейшей фрагментации кода, и будет проще ввести новое ключевое слово в качестве расширения синтаксиса.

Если let равно var с областью блока, что такое const? const похож на let, но без возможности переназначения:

let num = 42;
num = 52; // Totally valid
// Not as valid:
const str = 'String';
str = 'Another string';
// TypeError: invalid assignment to const `str'

Это также означает, что вы не можете объявить const переменную без ее назначения:

const str;
// SyntaxError: missing = in const declaration

Я здесь явно использую слово переназначить. Возможно, вы слышали, что об этом говорят как о константе, но это действительно создает неправильные ассоциации. const не гарантирует, что ваши значения останутся неизменными (неизменными или неизменными). Объявление устанавливает только семантику переменной, но не значение, на которое переменная ссылается. Когда у вас есть примитивные типы данных, такие как числа и строки, использование const практически означает, что у вас всегда есть ссылка на эти значения, поскольку они не могут быть изменены в JavaScript. Но это не так, скажем, для Объектов:

const obj = { num: 42 };
obj = { num: 52 };
// TypeError: invalid assignment to const `obj'
// But I can do this:
obj.num = 52;

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

Поднять, или мертвую зону, или и то, и другое?

Является ли единственная разница между блоком и областью функции? Нет. Есть еще одно теоретическое отличие. И я говорю здесь теоретически, как я утверждаю на практике, на самом деле это не имеет значения. Здесь я говорю о чем-то в JavaScript, называемом подъемом. Подъем - это концепция, которая может показаться чуждой, но она была с JavaScript с самого начала. Он берет все объявленные переменные (и объявления функций) и перемещает их так, чтобы они происходили в верхней части области во время выполнения. Для переменных это происходит только для объявлений (см. Раздел объявления и присваивания), но не для присваивания. Мы не перемещаем значения в верхнюю часть диапазона.

(function () {
  console.log(foo); //=> undefined
  var foo = 'Hello, World';
}());

по существу будет таким же, как

(function () {
  var foo;
  console.log(foo); //=> undefined
  foo = 'Hello, World';
}());

Неявно. При запуске кода. Вот почему в JavaScript давно практикуется явное перемещение всех объявлений в верхнюю часть области видимости (как в последнем примере). Таким образом, вы не удивитесь, если сможете ссылаться на переменную до того, как она будет определена. Однако с let и const это ведет себя немного иначе. Пробуем то же самое в блоке:

// Empty block statement (causing a scope for let)
{
  console.log(foo);
  // ReferenceError: foo is not defined
  let foo = 'Hello, World!';
}

Здесь мы видим не undefined, а ошибку ReferenceError. Но если копнуть дальше, я думаю, что let и const технически все еще подняты на вершину области видимости:

let foo = 'Bye, World!';
{
  console.log(foo);
  // ReferenceError: foo is not defined
  let foo = 'Hello, World!';
}

Вы можете ожидать, что этот пример сработает, но на самом деле он дает тот же результат, что и предыдущий. Кажется, что объявление все еще как бы поднято в верхнюю часть области видимости, но вы находитесь во временном состоянии, когда вам не разрешено ссылаться на переменную, не вызывая ReferenceError. Это временное состояние называется Temporal Dead Zone (TDZ): область между вершиной области и местом, где присвоено значение.

Есть несколько случаев, когда использование var и _51 _ / _ 52_ может отличаться из-за TDZ. Рассмотрим этот случай:

function logList(l) {
  for (var l of l) console.log(l);
}
logList([1, 2, 3]);
// Outputs:
// => 1
// => 2
// => 3

Это плохой вариант именования, но он работает. Если вы внимательно посмотрите на for-цикл, мы называем нашу изменяющуюся переменную так же, как список, который мы пытаемся повторить. l из цикла for-of затеняет список параметров. Другое дело - let или const:

function logList(l) {
  for (const l of l) console.log(l);
}
logList([1, 2, 3]);
// ReferenceError: can't access lexical declaration `l` before initialization

Как я вижу, здесь действует несколько правил. vars можно объявлять несколько раз, а TDZ. Возьмем первый пример. По сути, это переводится на что-то похожее на

function logList(l) {
  var l;  // doesn't change contents of l.
  // Remains what ever value we pass in as argument
  l = l[0];
  console.log(l);
  l = l[1];
  console.log(l);
  l = l[2];
  console.log(l);
}

Это не вызывает никаких проблем, поскольку мы можем выполнять несколько объявлений без изменения содержимого переменной при использовании var:

function log (foo) {
  var foo; // doesn't change contents of foo
  var foo; // doesn't change contents of foo
  var foo; // doesn't change contents of foo
  console.log(foo);
}
log('Hello');
//=> 'Hello'
// Same goes on top level
var bar = 'Hello';
var bar;
var bar;
console.log(bar);
//=> 'Hello'

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

function logList(l) {
  let l = l[0];
  console.log(l);
  l = l[1];
  console.log(l);
  l = l[2];
  console.log(l);
}

И теперь мы знаем, что ссылка на переменную между объявлением и верхней частью области видимости вызывает ReferenceError и временную мертвую зону, поэтому начальное l[0] не работает.

Заключение

Следуя стилю этой серии блогов, я не скажу вам, что использовать. Все, что я могу вам сказать, это что к чему и как это работает. Остальное зависит от тебя. Вы всегда должны адаптировать свои решения к вашей проблеме. Это означает знание того, какие есть решения и с какой проблемой вы сталкиваетесь. Вы не можете сказать всегда использовать const или никогда не использовать var. Они служат разным целям. const хорош, когда вы поддерживаете новый синтаксис и хотите убедиться, что вы переназначаете переменную в более новой версии. Это не поможет вам создавать неизменяемые структуры, но неизменные ссылки. let и const все еще подняты, но они создают временную мертвую зону и помогают отладить, что не так. let и const позволяют ограничить ваши переменные до block, а var позволяют думать об объемах, ограниченных телами функций.

Несмотря на то, что let и const в спецификациях языка новее, это не означает, что вы должны использовать их все время в пользу var. Используйте их там, где это имеет смысл, и где их функции помогут вам выразить то, что вы хотите для конкретного случая.

Фото Жипенг Я и Петр Прихарский на Unsplash. Спасибо Торгейру за рецензию!