Назад | Учебник TypeScript | Вперёд
let и const — это два новых способа объявления переменных в JavaScript. С помощью let мы объявляем переменные, а с помощью const мы объявляем константы.
Объявление переменных с помощью var
В Javascript переменные традиционно объявлялись с помощью ключевого слова var.
1 |
var a = 10; |
Этим кодом мы только что объявили переменную a и присвоили ей начальное значение 10.
Мы также можем объявлять переменные внутри функций:
1 2 3 4 5 |
function f() { var message = "Hello, world!"; return message; } |
Вы также можете обратиться к переменной, объявленной во внешней функции:
1 2 3 4 5 6 7 8 9 10 |
function f() { var a = 10; return function g() { var b = a + 1; return b; } } var g = f(); g(); // вернёт '11' |
В этом примере выше g захватывает переменную a, объявленную в f. В любой точке вызова g значение переменной a будет связано со значением переменной a в f. Даже если функция g вызвана после завершения работы функции f, то всё равно будет возможно получить доступ к переменной a и изменить её.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function f() { var a = 1; a = 2; var b = g(); a = 3; return b; function g() { return a; } } f(); // вернёт '2' |
Область видимости объявлений с var
Для тех, кто привык к другим языкам программирования, у объявления переменных с помощью var есть несколько странных особенностей. Посмотрите пример:
1 2 3 4 5 6 7 8 9 10 |
function f(shouldInitialize: boolean) { if (shouldInitialize) { var x = 10; } return x; } f(true); // returns '10' f(false); // returns 'undefined' |
Обратите внимание, что переменная x была объявлена внутри блока if, но мы всё равно можем к ней обратиться снаружи этого блока.
В JavaScript объявления переменных с помощью var доступны везде внутри содержащей их функции, модуля, пространства имён или глобального пространства. Областью видимости переменных, объявленных с var в JavaScript являются функции. Именно поэтому в TypeScript их область действия такая же.
Подобная область видимости может привести к разным ошибкам. Это также усугубляется тем, что можно объявить одну и ту же переменную несколько раз, и это не будет считаться ошибкой:
1 2 3 4 5 6 7 8 9 10 11 |
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; } |
В приведённом примере вложенный for случайно использует (пытаясь её объявить, правда) переменную i внешнего цикла, но областью действия переменной, объявленной с var будет функция, а не блок for.
Причуды захвата переменных
Попробуйте угадать, каков будет результат кода:
1 2 3 |
for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 100 * i); } |
Для тех, кто не знает: setTimeout будет вызывать функцию после указанного количества миллисекунд (при этом ожидая, пока какой-нибудь другой код прекратит выполняться).
А теперь посмотрите на результат:
1 2 3 4 5 6 7 8 9 10 |
10 10 10 10 10 10 10 10 10 10 |
Многие JavaScript-разработчики незнакомы с подобным поведением. Многие ожидают такой вывод:
1 2 3 4 5 6 7 8 9 10 |
0 1 2 3 4 5 6 7 8 9 |
Вспомните, что мы упоминали про захват переменных? Каждое функциональное выражение, которое мы передаём в setTimeout обращается к той же самой переменной i в той же самой области видимости.
Давайте разберём, что на самом деле происходит. Функция setTimeout запускает переданную функцию после указанного количества миллисекунд, но только после окончания цикла for. По окончании цикла for значение переменной i будет равно 10, поэтому при каждом вызове функции она будет писать 10!
Объявление переменных с помощью let
Из-за описанных выше проблем объявления переменных с помощью var был введён новый способ объявления переменных — с помощью let.
1 |
let hello = "Hello!"; |
Основное отличие объявления переменных с помощью let — это то, что областью видимости переменных становится блок, в котором они объявлены. В отличие от переменных объявленных с помощь var, переменные, объявленные с помощью let, не видны снаружи блока или цикла for, в котором они объявлены.
1 2 3 4 5 6 7 8 9 10 11 12 |
function f(input: boolean) { let a = 100; if (input) { // Всё ещё можно обращаться к 'a' let b = a + 1; return b; } // Ошибка: 'b' здесь не существует. return b; } |
Переменные, объявленные в блоке catch, имеют такую же область видимости:
1 2 3 4 5 6 7 8 9 |
try { throw "Ох, нет!"; } catch (e) { console.log("Ох, да."); } // Ошибка: 'e' здесь не существует console.log(e); |
Ещё одно преимущество объявления переменных с помощью let — это то, что такие переменные не могут быть считаны или записаны до их объявления. Не смотря на то что эти переменные существуют по всей своей области видимости, всё, что находится до их объявления, является для них мёртвой зоной.
1 2 |
a++; // illegal to use 'a' before it's declared; let a; |
Однако вы всё ещё можете захватить (capture) такую переменную до её объявления. Однако использовать такую функцию до объявления этой переменной нельзя. Для ES2015 будет бросаться исключения, однако сейчас TypeScript не сообщает о такой ошибке.
1 2 3 4 5 6 7 8 9 |
function foo() { // можно захватить переменную 'a' return a; } // вызывать 'foo' до объявления 'a' нельзя. foo(); let a; |
Повторное объявление и затенение
Для var не имеет значения количество обращений переменных, сколько бы вы не объявляли переменная будет только одна.
1 2 3 4 5 6 7 8 |
function f(x) { var x; var x; if (true) { var x; } } |
В примере выше все объявления на самом деле указывают на одну и ту же x, это вполне допустимо. Это часто становится источником ошибок. Однако объявление переменных с помощью let позволяет избежать этого:
1 2 |
let x = 10; let x = 20; // error: can't re-declare 'x' in the same scope |
Переменные не обязаны быть обе объявлены с помощью let, чтобы TypeScript сообщил нам о проблеме:
1 2 3 4 5 6 7 8 |
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' } |
Переменная с блочной областью видимости ( let) может быть объявлена с именем переменной с областью видимости на всю функцию, если она объявлена в другом блоке:
1 2 3 4 5 6 7 8 9 10 11 |
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.
1 2 3 4 5 6 7 8 9 10 11 |
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, мы коротко прошлись по их поведению после захвата. Каждый раз при заходе в новую область видимости создаётся новая «среда» для переменных. Эта среда и захваченные переменные могут существовать даже после окончания выполнения кода внутри области видимости.
1 2 3 4 5 6 7 8 9 10 11 12 |
function theCityThatAlwaysSleeps() { let getCity; if (true) { let city = "Seattle"; getCity = function() { return city; } } return getCity(); } |
Так как мы захватили city в её области видимости, мы всё ещё можем к ней обратиться, не смотря на то, что блок if закончил своё выполнения.
Вспомните наш пример с setTimeout, где мы использовали IIFE для захвата значений переменных для каждой итерации for. В результаты мы создавали новую среду переменных для захваченных переменных. Это доставило немного боли, но, к счастью, нам никогда не придётся делать это с TypeScript.
Объявления переменных с let имеют совершенно другое поведения при объявлении в части цикла. Вместо создания новой среды для всего цикла они создают как-бы новую область для каждой итерации, что избавляет нас от необходимости использования IIFE:
1 2 3 |
for (let i = 0; i < 10 ; i++) { setTimeout(function() { console.log(i); }, 100 * i); } |
Результат:
1 2 3 4 5 6 7 8 9 10 |
0 1 2 3 4 5 6 7 8 9 |
Объявления const
Объявления с помощью const работают так же, как и let, но не позволяют менять значения своих переменных после присвоения.
1 |
const numLivesForCat = 9; |
Нельзя менять именно значения самих переменных, объявленных с помощью const, но значения, на которые они ссылаются вполне могут менять своё внутреннее состояние:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const numLivesForCat = 9; const kitty = { name: "Aurora", numLives: numLivesForCat, } // Ошибка. Пытаемся менять константную переменную kitty = { name: "Danielle", numLives: numLivesForCat }; // всё в порядке. Внутреннее состояние значения константы менять можно kitty.name = "Rory"; kitty.name = "Kitty"; kitty.name = "Cat"; kitty.numLives--; |
Деструктурирование
Это НЕ уничтожение.
Деструктурирование массивов
Самый простой способ деструктурирования массивов:
1 2 3 4 |
let input = [1, 2]; let [first, second] = input; console.log(first); // outputs 1 console.log(second); // outputs 2 |
Это создаёт две новые переменные: first и second . Это эквивалентно обращению по индексу, но более удобно:
Деструктурирование работает и с уже объявленными переменными:
1 2 |
// обмениваем значения переменных [first, second] = [second, first]; |
И с параметрами функций:
1 2 3 4 5 |
function f([first, second]: [number, number]) { console.log(first); console.log(second); } f([1, 2]); |
Вы можете создать переменную для оставшихся элементов в списке с помощью ...:
1 2 3 |
let [first, ...rest] = [1, 2, 3, 4]; console.log(first); // outputs 1 console.log(rest); // outputs [ 2, 3, 4 ] |
Разумеется, так как это JavaScript, то вы можете просто игнорировать элементы в конце, которые вам не важны:
1 2 |
let [first] = [1, 2, 3, 4]; console.log(first); // outputs 1 |
Или другие элементы:
1 |
let [, second, , fourth] = [1, 2, 3, 4]; |
Деструктурирование объектов
Вы также можете деструктурировать объекты:
1 2 3 4 5 6 |
let o = { a: "foo", b: 12, c: "bar" }; let { a, b } = o; |
Это создаст новые переменные a и b из o.a и o.b. Обратите внимание, что вы можете пропустить c, если она вам не нужна.
Так же как и с массивами можно делать присвоения без объявления:
1 |
({ a, b } = { a: "baz", b: 101 }); |
Обратите внимание, что мы использовали круглые скобки, так как JavaScript обычно рассматривает «{» как начало блока.
Вы можете создать переменную для оставшихся элементов в объекте с помощью ...:
1 2 |
let { a, ...passthrough } = o; let total = passthrough.b + passthrough.c.length; |
Переименование свойств
Вы также можете дать другие имена свойствам:
1 |
let { a: newName1, b: newName2 } = o; |
Этот синтаксис может привести в замешательство. Вы можете читать a: newName1 как « a as newName1». Направление чтения — слева направо, как если бы вы писали:
1 2 |
let newName1 = o.a; let newName2 = o.b; |
Сбивает с толку то, что двоеточие здесь НЕ указывает тип. Тип, если вы его указываете, нужно указывать после всей конструкции:
1 |
let { a, b }: { a: string, b: number } = o; |
Переменные по умолчанию
Переменные по умолчанию позволяют вам указать значение для случая, когда значение свойства не указано.
1 2 3 |
function keepWholeObject(wholeObject: { a: string, b?: number }) { let { a, b = 1001 } = wholeObject; } |
keepWholeObject теперь содержит как переменную wholeObject, так и свойства a и b, даже если b не задавалось.
Объявление функций
Деструктуризация также работает и с объявлением функций. Например:
1 2 3 4 |
type C = { a: string, b?: number } function f({ a, b }: C): void { // ... } |
Но указание значений по умолчанию чаще употребляется для параметров, поэтому использование деструктурирования для этого может быть запутано. Например, вам следует помнить, что нужно указать тип до значения по умолчанию:
1 2 3 4 |
function f({ a, b } = { a: "", b: 0 }): void { // ... } f(); // ok, default to { a: "", b: 0 } |
Затем вас следует помнить, что нужно дать значения по умолчанию для необязательных свойств в деструктурированном свойстве вместо основного инициализатора. Помните, что C было объявлено с опциональным b:
1 2 3 4 5 6 |
function f({ a, b = 0 } = { a: "" }): void { // ... } f({ a: "yes" }); // ok, default b = 0 f(); // ok, по умолчанию { a: "" }, затем с по умолчанию b = 0 f({}); // ошибка, 'a' - обязателен, если вы передаёте аргумент. |
Используйте деструктурирование с осторожностью. Как показано в предыдущем примере, всё, кроме самого простого деструктурирущего выражения сложно для восприятия. Это особенно заметно с глубоко вложенной деструктуризацией, которую затем действительно сложно понять без переименования, значений по умолчанию и аннотаций типов. Пытайтесь держать деструктурирующие выражения маленкими и простыми. Вы всегда можете использовать обычные присвоения вместо деструктурирования.
Распространение
Распространение обратно деструктурированию. Оно позволяет распространять массив в другой массив или объект в другой объект. Пример:
1 2 3 |
let first = [1, 2]; let second = [3, 4]; let bothPlus = [0, ...first, ...second, 5]; |
В результате мы получаем bothPlus со значением [0, 1, 2, 3, 4, 5]. Распространение создаёт теневую копию first и second. Они не меняются распространением.
Вы также можете распространять объекты:
1 2 |
let defaults = { food: "spicy", price: "$", ambiance: "noisy" }; let search = { ...defaults, food: "rich" }; |
Теперь search равен { food: "rich", price: "$$", ambiance: "noisy" }. Распространение объектов более сложно, чем распространение массивов. Как и в распространении массивов оно работает слева направо, но в результате всё равно объект. Это озачает, что свойства, кторые приходят позже в распространённый объект могут перезаписать свойства, пришедшие раньше. Если мы изменим предыдущий пример к распространению в конце:
1 2 |
let defaults = { food: "spicy", price: "$", ambiance: "noisy" }; let search = { food: "rich", ...defaults }; |
Тогда свойство food в defaults перезапишет food: "rich", что не совсем то, что мы хотим в этом случае.
Распространение объектов имеет некоторые ограничения. Во-первых, оно включает только свои, перечисляемые свойства. В основном это означает, что вы теряете методы при распространении экземпляров объектов:
1 2 3 4 5 6 7 8 9 |
class C { p = 12; m() { } } let c = new C(); let clone = { ...c }; clone.p; // ok clone.m(); // error! |
Во-вторых компилятор TypeScript не позволяет распространять обобщённые параметры в обобщённых функциях. Эта возможность ожидается в будущих реализациях языка.
Назад | Учебник TypeScript | Вперёд