Table of Contents #

Variable Declarations #

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(); // возвращает 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(); // возвращает 2

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

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

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

    return x;
}

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

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

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

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, потому что i имеет области видимости внутри функции sumMatrix. Опытные разработчики знают, что похожие ошибки проскальзывают при code review и могут быть причиной бесконечной фрустрации.

Variable capturing quirks

Попробуйте быстро догадаться, какой будет вывод у этого кода:

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

Помните, что мы упомянули ранее о замыкании переменных?

В любой точке, где будет вызвана g, значение a будет связано со значением a в функции f.

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

Самый простой способ решить проблему - использовать немедленный запуск анонимной функции, чтобы захватить 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);
}

Этот странно выглядящий шаблон на самом деле не редок.

Объявления let #

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

let hello = "Hello!";

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

Блочная область видимости

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

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

    if (input) {
        // Здесь мы видим переменную 'a'
        let b = a + 1;
        return b;
    }

    // Ошибка: 'b' не существует в этом блоке
    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++; // неверно использовать 'a' до ее объявления;
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; // Ошибка: нельзя переопределить 'x' в одной области видимости

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

function f(x) {
    let x = 100; // ошибка: пересекается с параметром функции
}

function g() {
    let x = 100;
    var x = 100; // ошибка: нельзя два раза объявить '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);
}

and as expected, this will print out

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,
}

// Ошибка
kitty = {
    name: "Danielle",
    numLives: numLivesForCat
};

// Все хорошо
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;

Несмотря на то, что переменная была объявлена как const, ее внутренее состояние все еще может быть изменено. К счастью, TypeScript позволяет вам определить свойства объекта доступными только на чтение: readonly. Раздел в Интерфейсах объясняет детали этого.

let или const? #

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

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

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

Деструктурирование #

Еще одно нововведение из стандарта ECMAScript 2015, которое есть в TypeScript, это деструктурирование (прим. переводчика "destructuring" - не "уничтожение"). За полной информацией об этом пройдите по ссылке статья на 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(input);

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

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];

Деструктурирование объекта

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

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: newName1, b: newName2} = o;

Этот синтакс может немного сущать. Вы читаете a: newName1 как "a as newName1". Можно записать это по-другому, чтобы было понятнее:

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

Сбивает с толку то, что двоеточие здесь не обозначает тип. Тип, если вы задаете его, все еще должен быть записан после деструктурирования:

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

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

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

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

Функция keepWholeObject имеет переменную для wholeObject, как и свойства a and b, даже если b не определено.

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

Деструктурирование также работает с объявлением функций Ниже приведен простой пример:

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

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

function f({a, b} = {a: "", b: 0}): void {
    // ...
}
f(); // ok, по умолчанию {a: "", b: 0}

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

function f({a, b = 0} = {a: ""}): void {
    // ...
}
f({a: "yes"}) // ok, по умолчанию b = 0
f() // ok, по умолчанию {a: ""}, что также подразумевает b = 0
f({}) // ошибка, свойство 'a' в таком случае требуется задать 

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

Источник







Поддержите перевод документации:



Поддерживатель | Github Репозиторий


Documentation generated by mdoc.
Молния! Обновления, новости и статьи Typescript.