Spec-Zone.ru › TypeScript 5.1

Объявление переменных

let и const — это две относительно новые концепции для объявления переменных в JavaScript. Как мы уже упоминали, let в некоторых аспектах похожа на var, но позволяет пользователям избежать распространённых «ловушек», с которыми они сталкиваются в JavaScript.

const — это расширение let, которое предотвращает повторную привязку значения к переменной.

Так как TypeScript является расширением JavaScript, язык естественным образом поддерживает let и const. Здесь мы более подробно рассмотрим эти новые объявления и почему они предпочтительнее var.

Если вы использовали JavaScript поверхностно, следующий раздел может быть хорошим способом освежить вашу память. Если вы хорошо знакомы со всеми особенностями объявления var в JavaScript, вы можете пропустить его.

Объявления var

Объявление переменной в JavaScript традиционно выполнялось с помощью ключевого слова var.

var a = 10;

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

Мы также можем объявить переменную внутри функции:

function f() {
  var message = "Hello, world!";

  return message;
}

и мы также можем получить доступ к тем же переменным внутри других функций:

function f() {
  var a = 10;
  return function g() {
    var b = a + 1;
    return b;
  };
}

var g = f();
g(); // returns '11'

В этом примере g получила доступ к переменной a , объявленной в f. В любой момент, когда вызывается g , значение a будет привязано к значению a в f . Даже если g вызывается после того, как завершится выполнение f , он сможет получить доступ и изменить a.

function f() {
  var a = 1;

  a = 2;
  var b = g();
  a = 3;

  return b;

  function g() {
    return a;
  }
}

f(); // returns '2'

Правила области видимости

Объявления var имеют необычные правила области видимости для тех, кто привык к другим языкам. Рассмотрим следующий пример:

function f(shouldInitialize: boolean) {
  if (shouldInitialize) {
    var x = 10;
  }

  return x;
}

f(true); // returns '10'
f(false); // returns 'undefined'

Некоторые читатели могут удивленно перечитать этот пример. Переменная x была объявлена внутри блока if, и тем не менее мы смогли получить к ней доступ снаружи этого блока. Это потому, что объявления var доступны в любом месте внутри их содержащей функции, модуля, пространства имён или глобальной области видимости — о чём мы поговорим позже — независимо от содержащего блока. Некоторые люди называют это var-областью видимости или областью видимости функции. Параметры также имеют область видимости функции.

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

function sumMatrix(matrix: number[][]) {
  var sum = 0;
  for (var i = 0; i < matrix.length; i++) {
    var currentRow = matrix[i];
    for (var i = 0; i < currentRow.length; i++) {
      sum += currentRow[i];
    }
  }

  return sum;
}

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

Особенности захвата переменных

Подумайте на секунду, какой результат выведет следующий фрагмент кода:

for (var i = 0; i < 10; i++) {
  setTimeout(function () {
    console.log(i);
  }, 100 * i);
}

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

Готовы? Посмотрите:

10
10
10
10
10
10
10
10
10
10

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

0
1
2
3
4
5
6
7
8
9

Помните, что мы упоминали ранее о захвате переменных? Каждый выражение функции, которое мы передаём в setTimeout , фактически ссылается на ту же переменную i из той же области видимости.

Давайте на мгновение подумаем, что это означает. setTimeout выполнит функцию через какое-то количество миллисекунд, но только после того, как цикл for завершит свою работу; К тому моменту, когда цикл for закончит выполнение, значение i будет 10. Таким образом, каждый раз, когда вызывается данная функция, она будет выводить 10!

Распространённым решением является использование IIFE (непосредственно вызываемого выражения функции) для захвата состояния i на каждой итерации:

for (var i = 0; i < 10; i++) {
  // capture the current state of 'i'
  // by invoking a function with its current value
  (function (i) {
    setTimeout(function () {
      console.log(i);
    }, 100 * i);
  })(i);
}

Этот странный шаблон на самом деле довольно распространён. i в списке параметров фактически затеняет i , объявленную в цикле for , но так как мы дали им одинаковые имена, нам не пришлось сильно изменять тело цикла.

Объявления let

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

let hello = "Hello!";

Ключевое различие заключается не в синтаксисе, а в семантике, с которой мы сейчас и познакомимся.

Область видимости блока

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

function f(input: boolean) {
  let a = 100;

  if (input) {
    // Still okay to reference 'a'
    let b = a + 1;
    return b;
  }

  // Error: 'b' doesn't exist here
  return b;
}

Здесь у нас две локальные переменные a и b . Область видимости a ограничена телом f , а область видимости b ограничена содержащим оператором if.

Переменные, объявленные в операторе catch , также подчиняются подобным правилам области видимости.

try {
  throw "oh no!";
} catch (e) {
  console.log("Oh well.");
}

// Error: 'e' doesn't exist here
console.log(e);

Ещё одна особенность переменных с областью видимости блока заключается в том, что к ним нельзя обращаться или писать перед фактическим объявлением. Хотя эти переменные «существуют» в пределах своей области видимости, все моменты до их объявления являются частью их временной мёртвой зоны. Это просто сложный способ сказать, что к ним нельзя получить доступ до оператора let , и, к счастью, TypeScript об этом сообщит.

a++; // illegal to use 'a' before it's declared;
let a;

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

function foo() {
  // okay to capture 'a'
  return a;
}

// illegal call 'foo' before 'a' is declared
// runtimes should throw an error here
foo();

let a;

Для получения дополнительной информации о временных мёртвых зонах обратитесь к соответствующему контенту на сайте Mozilla Developer Network.

Повторные объявления и затенение

При объявлениях var мы упоминали, что не важно, сколько раз вы объявляете переменные; у вас будет только одна.

function f(x) {
  var x;
  var x;

  if (true) {
    var x;
  }
}

В приведённом примере все объявления x фактически ссылаются на одну и ту же переменную x , и это совершенно допустимо. Это часто приводит к ошибкам. К счастью, объявления let не такие снисходительные.

let x = 10;
let x = 20; // error: can't re-declare 'x' in the same scope

Переменные не обязательно должны иметь область видимости блока, чтобы TypeScript сообщило о проблеме.

function f(x) {
  let x = 100; // error: interferes with parameter declaration
}

function g() {
  let x = 100;
  var x = 100; // error: can't have both declarations of 'x'
}

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

function f(condition, x) {
  if (condition) {
    let x = 100;
    return x;
  }

  return x;
}

f(false, 0); // returns '0'
f(true, 0); // returns '100'

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

function sumMatrix(matrix: number[][]) {
  let sum = 0;
  for (let i = 0; i < matrix.length; i++) {
    var currentRow = matrix[i];
    for (let i = 0; i < currentRow.length; i++) {
      sum += currentRow[i];
    }
  }

  return sum;
}

Эта версия цикла фактически выполнит суммирование правильно, потому что внутренний цикл i затеняет i из внешнего цикла.

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

Захват переменных с областью видимости блока

Когда мы впервые затронули идею захвата переменных с объявлением var , мы кратко описали, как ведут себя переменные после захвата. Чтобы лучше понять это, каждый раз, когда выполняется область видимости, создаётся «среда» переменных. Эта среда и её захваченные переменные могут существовать даже после того, как всё внутри области видимости завершило выполнение.

function theCityThatAlwaysSleeps() {
  let getCity;

  if (true) {
    let city = "Seattle";
    getCity = function () {
      return city;
    };
  }

  return getCity();
}

Так как мы захватили city из его среды, мы по-прежнему можем получить к нему доступ, несмотря на то, что блок if завершил выполнение.

Вспомните, что в нашем предыдущем примере setTimeout нам пришлось использовать IIFE для захвата состояния переменной для каждой итерации цикла for . По сути, мы создавали новую среду переменных для наших захваченных переменных. Это было немного затруднительно, но, к счастью, в TypeScript вам это больше не придётся делать.

Объявления let имеют существенно иное поведение при объявлении в цикле. Вместо того, чтобы просто вводить новую среду для самого цикла, эти объявления как бы создают новую область видимости на каждой итерации. Так как это то, что мы делали и раньше с нашими IIFE, мы можем изменить наш старый пример setTimeout , чтобы просто использовать объявление let.

for (let i = 0; i < 10; i++) {
  setTimeout(function () {
    console.log(i);
  }, 100 * i);
}

и, как ожидалось, это выведет

0
1
2
3
4
5
6
7
8
9

Объявления const

Объявления const — ещё один способ объявления переменных.

const numLivesForCat = 9;

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

Это не следует путать с идеей, что значения, на которые они ссылаются, неизменны.

const numLivesForCat = 9;
const kitty = {
  name: "Aurora",
  numLives: numLivesForCat,
};

// Error
kitty = {
  name: "Danielle",
  numLives: numLivesForCat,
};

// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;

Если вы не предпринимаете специальных мер, внутреннее состояние переменной const всё ещё можно изменить. К счастью, TypeScript позволяет указать, что члены объекта readonly . Подробности можно найти в главе по интерфейсам.

let и const

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

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

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

В большинстве разделов этого руководства используются объявления let.

Деструктуризация

Ещё одной функцией ECMAScript 2015, которая есть в TypeScript, является деструктуризация. Для полной справки см. статью на сайте Mozilla Developer Network. В этом разделе мы дадим краткий обзор.

Деструктуризация массивов

Простейшей формой деструктуризации является присваивание с деструктуризацией массива:

let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2

Это создает две новые переменные с именами first и second. Это эквивалентно использованию индексации, но гораздо удобнее:

first = input[0];
second = input[1];

Деструктуризация работает и с уже объявленными переменными:

// swap variables
[first, second] = [second, first];

И с параметрами функции:

function f([first, second]: [number, number]) {
  console.log(first);
  console.log(second);
}
f([1, 2]);

Вы можете создать переменную для оставшихся элементов списка, используя синтаксис ...:

let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]

Конечно, поскольку это JavaScript, вы можете просто проигнорировать хвостовые элементы, которые вас не интересуют:

let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1

Или другие элементы:

let [, second, , fourth] = [1, 2, 3, 4];
console.log(second); // outputs 2
console.log(fourth); // outputs 4

Деструктуризация кортежей

Кортежи могут быть деструктурированы так же, как массивы; переменные деструктуризации получают типы соответствующих элементов кортежа:

let tuple: [number, string, boolean] = [7, "hello", true];

let [a, b, c] = tuple; // a: number, b: string, c: boolean

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

let [a, b, c, d] = tuple; // Error, no element at index 3

Как и с массивами, вы можете деструктурировать оставшуюся часть кортежа с помощью ..., чтобы получить более короткий кортеж:

let [a, ...bc] = tuple; // bc: [string, boolean]
let [a, b, c, ...d] = tuple; // d: [], the empty tuple

Или проигнорировать хвостовые элементы или другие элементы:

let [a] = tuple; // a: number
let [, b] = tuple; // b: string

Деструктуризация объектов

Вы также можете деструктурировать объекты:

let o = {
  a: "foo",
  b: 12,
  c: "bar",
};
let { a, b } = o;

Это создает новые переменные a и b из o.a и o.b соответственно. Обратите внимание, что вы можете пропустить c если вам это не нужно.

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

({ a, b } = { a: "baz", b: 101 });

Обратите внимание, что нам пришлось заключить это выражение в скобки. JavaScript обычно интерпретирует { как начало блока.

Вы можете создать переменную для оставшихся элементов объекта, используя синтаксис ...:

let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;

Переименование свойств

Вы также можете дать разные имена свойствам:

let { a: newName1, b: newName2 } = o;

Здесь синтаксис начинает усложняться. Вы можете читать a: newName1 как «a как newName1». Направление слева направо, как если бы вы написали:

let newName1 = o.a;
let newName2 = o.b;

По странности, двоеточие здесь не указывает на тип. Тип, если вы его указываете, всё равно нужно писать после всей деструктуризации:

let { a: newName1, b: newName2 }: { a: string; b: number } = o;

Значения по умолчанию

Значения по умолчанию позволяют указать значение по умолчанию в случае, если свойство не определено:

function keepWholeObject(wholeObject: { a: string; b?: number }) {
  let { a, b = 1001 } = wholeObject;
}

В этом примере

function keepWholeObject(wholeObject: { a: string; b?: number }) {
  let { a, b = 1001 } = wholeObject;
}
указывает, что b необязательно, поэтому оно может быть undefined. keepWholeObject теперь имеет переменную для wholeObject, а также свойства a и b, даже если b не определено.

Объявления функций

Деструктуризация также работает в объявлениях функций. В простых случаях это просто:

type C = { a: string; b?: number };
function f({ a, b }: C): void {
  // ...
}

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

function f({ a = "", b = 0 } = {}): void {
  // ...
}
f();

Приведённый выше фрагмент является примером вывода типа, объяснённым ранее в руководстве.

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

function f({ a, b = 0 } = { a: "" }): void {
  // ...
}
f({ a: "yes" }); // ok, default b = 0
f(); // ok, default to { a: "" }, which then defaults b = 0
f({}); // error, 'a' is required if you supply an argument

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

Распространение

Оператор распространения — это противоположность деструктуризации. Он позволяет распространять массив в другой массив или объект в другой объект. Например:

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];

Это даёт bothPlus значение [0, 1, 2, 3, 4, 5]. Распространение создаёт поверхностную копию first и second. Они не изменяются при распространении.

Вы также можете распространять объекты:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };

Теперь search — это { food: "rich", price: "$$", ambiance: "noisy" }. Распространение объектов сложнее, чем распространение массивов. Как и при распространении массивов, оно происходит слева направо, но результатом всё равно является объект. Это означает, что свойства, которые появляются позже в распространяемом объекте, перезаписывают свойства, которые появляются раньше. Поэтому, если мы изменим предыдущий пример, чтобы распространение происходило в конце:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };

Тогда свойство food в defaults перезаписывает food: "rich", что в данном случае не является желаемым результатом.

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

class C {
  p = 12;
  m() {}
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!

Во-вторых, компилятор TypeScript не разрешает распространение параметров типа из универсальных функций. Эта функция ожидается в будущих версиях языка.

© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/variable-declarations.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API