Данная публикация является переводом статьи «Introduction to commonly used ES6 features» под авторством Zell Liew, размещенного здесь. Перевод разделён на 2 части.
JavaScript серьезно развился в последние несколько лет. Если ты изучаешь язык в 2017 году и при этом не касался ES6, то упускаешь легкий способ читать и писать на нём.
Не волнуйся, если ты еще не знаток языка. От тебя не требуется превосходно знать его, чтобы пользоваться преимуществами «бонусов», добавленных в ES6. В этой статье хотел бы поделиться восемью особенностями ES6, которые использую каждый день как разработчик, что поможет тебе легче погрузиться в новый синтаксис.
Примечание переводчика: в статье представлены семь особенностей («Промисы» рассматриваются в отдельной статье).
Список особенностей ES6
Во-первых, ES6 — это огромное обновление JavaScript. Здесь приведён большой перечень особенностей (спасибо Luke Hoban), если тебе интересно, какие нововведения появились:
- Стрелочные функции
- Классы
- Расширенные литералы объектов
- Шаблонные строки
- Деструктуризация
- Default + rest + spread
- let + const
- Итераторы + for..of
- Генераторы
- Дополнения для поддержки Unicode
- Модули
- Загрузчики модулей
- Типы коллекций Map + set + weakmap + weakset
- Прокси
- Тип данных Symbols
- Создание подклассов
- Промисы
- Math + number + string + array + object api
- Двоичные и восьмеричные литералы
- Reflect api
- Хвостовые вызовы
Не пугайся такого большого списка особенностей ES6. От тебя не требуется знать все сразу. Далее поделюсь восемью особенностями, которые применяю каждый день. К ним относятся:
- let + const
- Стрелочные функции
- Параметры по умолчанию (Default parameters)
- Деструктуризация
- Rest parameter и spread operator
- Расширенные литералы объектов
- Шаблонные строки
- Промисы
Кстати, браузеры очень хорошо поддерживают ES6. Почти все особенности обеспечиваются нативно, если пишешь код для последних версий браузеров (Edge, последние версии FF, Chrome и Safari).
Для написания кода на ES6 не нужен такой инструмент, как Webpack. При отсутствии поддержки браузером всегда можно обратиться к полифиллам, созданными сообществом. Просто погугли!
Итак, приступим к первой особенности.
Let и const
В ES5 («старый» JavaScript) было принято объявлять переменные через ключевое слово var
. В ES6 это слово может быть заменено let
и const
, двумя мощными ключевыми словами, которые делают разработку проще.
Сначала посмотрим на разницу между let
и var
, чтобы понять, почему let
и const
лучше.
Let vs var
Сначала рассмотрим знакомый нам var
.
Во-первых, мы можем объявлять переменные через ключевое слово var
. Как только объявление произошло, переменная может быть использована везде в текущей области видимости.
var me = 'Zell' ;
console.log(me); // Zell
В примере выше объявлена глобальная переменная me
. Эта переменная также может быть использована в функции. Например, так:
var me = 'Zell';
function sayMe () {
console.log(me);
}
sayMe(); // Zell
Однако, обратное не верно. Если переменную объявить в функции, то будет невозможно использовать её снаружи функции.
function sayMe() {
var me = 'Zell';
console.log(me);
}
sayMe(); // Zell
console.log(me); // Uncaught ReferenceError: me is not defined
Так, можем говорить, что у var
область видимости «функциональная». Это означает, что всякий раз, когда переменная объявляется с помощью var
в функции, она будет существовать только в пределах функции.
Если переменная создается снаружи функции, она будет существовать во внешней области видимости.
var me = 'Zell'; // глобальная область видимости
function sayMe () {
var me = 'Sleepy head'; // локальная область видимости
console.log(me);
}
sayMe(); // Sleepy head
console.log(me); // Zell
У let
, с другой стороны, область видимости «блочная». Это означает, что всякий раз, когда переменная объявляется с помощью let, она будет существовать только в пределах блока.
Подождите, но что же значит «блок»?
Блоком в JavaScript называется то, что находится внутри пары фигурных скобок. Ниже примеры блоков:
{
// новый блок
}
if (true) {
// новый блок
}
while (true) {
// новый блок
}
function () {
// новый блок
}
Разница между переменными, объявленными «блочно» и «функционально», огромна. При объявлении переменной «функционально» в дальнейшем можно случайно переписать значение этой переменной. Ниже пример:
var me = 'Zell';
if (true) {
var me = 'Sleepy head';
}
console.log(me); // 'Sleepy head'
Этот пример показывает, что значение me
становится 'Sleepy head'
после выполнения кода внутри if
блока. Такой пример, скорее всего, не вызовет какие-либо проблемы, т.к. вряд ли будут объявляться переменные с одинаковым именем.
Однако любой, кто будет работать с помощью var
через цикл for
, может оказаться в странной ситуации из-за способа, которым объявлены переменные. Представим следующий код, который выводить переменную i
четыре раза, а затем выводит i
c помощью функции setTimeout
.
for (var i = 1; i < 5; i++) {
console.log(i);
setTimeout(function () {
console.log(i);
}, 1000)
};
Какой результат можно ожидать при выполнения этого кода? Ниже то, что в действительности произойдет:
i будет четыре раза выведена со значением «5» в функции-таймере
Как i
получил значение «5» четыре раза внутри таймера? Всё из-за того, что var
определяет переменную «функционально», и значение i
становится равным «4» еще до того, как таймер начнёт выполняться.
Для того чтобы получить правильное значение i
внутри setTimeout
, которая будет выполняться позже, необходимо создать другую функцию, допустим logLater
, для гарантии того, что значение i
не изменится в цикле for
до начала выполнения setTimeout
:
function logLater (i) {
setTimeout(function () {
console.log(i);
});
}
for (var i = 1; i < 5; i++) {
console.log(i);
logLater(i);
};
i правильно выводится как 1, 2, 3 и 4
Кстати, это называется замыканием.
Хорошая новость в том, что «странность» «функционально» определенной области видимости, показанная на примере с for
циклом, не происходит с let
. Такой же пример с таймером может быть переписан так, что будет работать без добавления дополнительных функций:
for (let i = 1; i < 5; i++) {
console.log(i);
setTimeout(function () {
console.log(i);
}, 1000);
};
i правильно выводится как 1, 2, 3 и 4
Как можно увидеть, «блочно» объявленные переменные, обходя недостатки «функционально» объявленных переменных, намного упрощают разработку. Для упрощения советую с этого момента использовать let
вместо var
каждый раз, когда необходимо объявлять переменные в JavaScript.
Разобравшись, что делает let
, рассмотрим разницу между let
и const
.
Let vs const
Как и let
, const
также имеет «блочно» определенную область видимости. Разница в том, что значение const
нельзя изменять после объявления.
const name = 'Zell';
name = 'Sleepy head'; // TypeError: Assignment to constant variable.
let name1 = 'Zell';
name1 = 'Sleepy head';
console.log(name1); // 'Sleepy head'
Раз const
нельзя изменить, то можно использовать для переменных, которые не будут меняться.
Допустим, на сайте имеется кнопка, которая запускает модальное окно, и мы знаем, что такая кнопка одна и изменяться не будет. В этом случае, используем const
:
const modalLauncher = document.querySelector('.jsModalLauncher');
При объявлении переменных по возможности всегда отдавайте предпочтение const
вместо let
, т.к. получаете дополнительную отметку, что переменная не будет переназначена. Для остальных случаев используйте let
.
Далее рассмотрим стрелочные функции
Стрелочные функции
Стрелочные функции обозначаются стрелкой (=>), что можно увидеть повсюду в коде на ES6. Такое сокращенное обозначение применяется для создания анонимных функций. Они могут быть использованы везде, где имеется ключевое слово function
. Например:
let array = [1,7,98,5,4,2];
// ES5 вариант
var moreThan20 = array.filter(function (num) {
return num > 20;
});
// ES6 вариант
let moreThan20 = array.filter(num => num > 20);
Стрелочные функции довольно удобны, т.к. с их помощью можно сократить код, тем самым уменьшив места, где могут скрываться ошибки. Также код, написанный на них, легче понимать, если ты знаком с их синтаксисом.
Рассмотрим стрелочные функции поближе, чтобы научиться распознавать и использовать их.
Подробнее о стрелочных функциях
Сначала обратим внимание на создание функций.
В JavaScript, вероятно, привычно создавать функции следующим способом:
function namedFunction() {
// код функции
}
// вызов функции
namedFunction();
Второй способ создания функций связан с написанием анонимной функции и её присвоением переменной. Для создания анонимной функции необходимо вынести её название из объявления функции.
var namedFunction = function() {
// код функции
}
Третий способ создания функций заключается в том, чтобы передавать их напрямую как аргумент в другую функцию или в метод. Этот способ является наиболее распространенным для анонимных функций. Ниже пример:
// Применение анонимной callback-функции
button.addEventListener('click', function() {
// код функции
});
Поскольку стрелочные функции в ES6 представляют собой сокращенное обозначение анонимных функций, их можно внедрять везде, где создаются анонимные функции.
Ниже то, как это выглядит:
// Обычная функция
const namedFunction = function (arg1, arg2) { /* do your stuff */}
// Стрелочная функция
const namedFunction2 = (arg1, arg2) => {/* do your stuff */}
// callback обычной функции
button.addEventListener('click', function () {
// код функции
})
// callback стрелочной функции
button.addEventListener('click', () => {
// код функции
})
Заметили сходство? По существу, удаляют ключевое слово function
и заменяют стрелкой => в немного другом месте.
Так в чём же заключается суть стрелочных функций? Только в замене function
на =>?
На самом деле дело не только в замене function
на =>. Синтаксис стрелочной функции может быть изменён в зависимости от двух факторов:
- требуемого количества аргументов
- необходимости неявного возврата
Под 1-ым фактором понимается количество аргументов, передаваемых стрелочной функции. Если передается только один аргумент, то можно опустить круглые скобки, в которые заключены аргументы. Если аргументы не требуются, то круглые скобки могут быть заменены нижним подчёркиванием _
.
Все ниже перечисленные примеры являются допустимыми стрелочными функциями.
const zeroArgs = () => {/* код функции */}
const zeroWithUnderscore = _ => {/* код функции */}
const oneArg = arg1 => {/* код функции */}
const oneArgWithParenthesis = (arg1) => {/* код функции */}
const manyArgs = (arg1, arg2) => {/* код функции */}
Под 2-ым фактором понимается необходимость неявного возврата. Стрелочные функции, по умолчанию, автоматически создают ключевое слово return, если код занимает одну строку и не обернут в фигурные скобки {}.
Итак, ниже два равнозначных примера:
const sum1 = (num1, num2) => num1 + num2
const sum2 = (num1, num2) => { return num1 + num2 }
Эти два фактора могут служить основанием для сокращения кода как с moreThan20
, который был представлен выше:
let array = [1,7,98,5,4,2];
// ES5 вариант
var moreThan20 = array.filter(function (num) {
return num > 20;
});
// ES6 вариант
let moreThan20 = array.filter(num => num > 20);
В заключение отмечу, стрелочные функции довольно хороши. Потратив немного времени и испытав их, можно привыкнуть к ним и везде использовать.
Прежде чем ты присоединишься к «сторонникам» стрелочных функций, познакомимся с другой их особенностью, которая вызывает много путаницы, а именно с лексическим this
.
Лексический this
this
— уникальное ключевое слово, значение которого меняется в зависимости от контекста вызова. При вызове снаружи функции this
ссылается на объект Window
в браузере.
console.log(this); // Window
При вызове this
через простую функцию оно ссылается на глобальный объект. В случае с браузерами this
всегда будет Window
.
function hello () {
console.log(this);
}
hello(); // Window
JavaScript всегда устанавливает this
окну браузера при вызове простой функции. В свою очередь, это объясняет то, что this
в функциях-таймерах типа setTimeout
указывает на Window
.
При вызове this
в методе объекта, оно ссылается на сам объект:
let o = {
sayThis: function() {
console.log(this);
}
}
o.sayThis(); // o
При вызове функции-конструктора this
ссылается на конструируемый объект.
function Person (age) {
this.age = age;
}
let greg = new Person(22);
let thomas = new Person(24);
console.log(greg); // this.age = 22
console.log(thomas); // this.age = 24
При использовании в обработчике события this
ссылается на элемент, который запустил событие.
let button = document.querySelector('button');
button.addEventListener('click', function() {
console.log(this); // button
});
Как можно заметить из ситуаций выше, значение this
определяется функцией, которая его вызывает. Каждая функция устанавливает собственное значение this
.
В стрелочных функциях this
никогда не приобретает новое значение вне зависимости от того, как функция вызвана. this
всегда будет иметь такое же значение, какое имеет this
в окружающем стрелочную функцию коде. Кстати, лексический означает ссылающийся (связанный), из-за чего, как полагаю, лексический this
и получил своё название.
Итак, это звучит запутанно, поэтому рассмотрим несколько реальных примеров.
Во-первых, не используй стрелочные функции для объявления методов объектов, т.к. в случае необходимости больше не сможешь сослаться на объект через this
.
let o = {
// Не делай так
notThis: () => {
console.log(this); // Window
this.objectThis(); // Uncaught TypeError: this.objectThis is not a function
},
// Делай так
objectThis: function () {
console.log(this); // o
}
// Или таким новым сокращенным способом
objectThis2 () {
console.log(this); // o
}
}
Во-вторых, стрелочные функции могут не подойти для создания обработчиков событий, т.к. this
больше не будет закреплен за элементом, к которому привязан обработчик события.
Однако всегда можно будет получить правильный контекст для this
через event.currentTarget
. По этой причине было сказано, что «могут не подойти».
button.addEventListener('click', function () {
console.log(this); // button
});
button.addEventListener('click', e => {
console.log(this); // Window
console.log(event.currentTarget); // button
});
В-третьих, лексический this
может использоваться в тех ситуациях, когда привязка this
может неожиданно меняться. Примером является функция-таймер, при которой не нужно будет иметь дело с this
, that
или self
глупостями.
let o = {
// Старый вариант
oldDoSthAfterThree: function () {
let that = this;
setTimeout(function () {
console.log(this); // Window
console.log(that); // o
})
},
// Вариант со стрелочной функцией
doSthAfterThree: function () {
setTimeout(() => {
console.log(this); // o
}, 3000)
}
}
Такое применение особенно полезно, если необходимо добавить или удалить класс спустя некоторое время:
let o = {
button: document.querySelector('button');
endAnimation: function () {
this.button.classList.add('is-closing');
setTimeout(() => {
this.button.classList.remove('is-closing');
this.button.classList.remove('is-open');
}, 3000)
}
}
В заключение, используй стрелочные функции везде, чтобы сделать код чище и короче, как в примере выше с moreThan20
:
let array = [1,7,98,5,4,2];
let moreThan20 = array.filter(num => num > 20);
Продолжим дальше
Параметры по умолчанию
Default parameters в ES6 позволяют устанавливать параметры по умолчанию при создании функций. Рассмотрим пример, чтобы понять насколько это полезно.
Создадим функцию, которая оглашает имя игрока команды. Если написать эту функцию на ES5, то получится:
function announcePlayer (firstName, lastName, teamName) {
console.log(firstName + ' ' + lastName + ', ' + teamName);
}
announcePlayer('Stephen', 'Curry', 'Golden State Warriors');
// Stephen Curry, Golden State Warriors
На первый взгляд, кажется, что с кодом всё в порядке. Однако, вдруг необходимо будет огласить игрока, который не состоит ни в одной из команд?
Текущий код просто не справиться с задачей, если пропустить teamName
:
announcePlayer('Zell', 'Liew');
// Zell Liew, undefined
Вполне уверен, что undefined
— это не команда.
Если игрок не относится ни к одной из команд, то оглашение Zell Liew, unaffiliated
будет иметь больше смысла, чем Zell Liew, undefined
. Согласен?
Для того чтобы announcePlayer
оглашала Zell Liew, unaffiliated
, возможно, как вариант, передать строку unaffiliated
вместо teamName
:
announcePlayer('Zell', 'Liew', 'unaffiliated');
// Zell Liew, unaffiliated
Хотя такой подход работает, но лучше внести улучшение в announcePlayer
через проверку наличия teamName
.
В ES5 отрефакторенный код стал бы таким:
function announcePlayer (firstName, lastName, teamName) {
if (!teamName) {
teamName = 'unaffiliated';
}
console.log(firstName + ' ' + lastName + ', ' + teamName);
}
announcePlayer('Zell', 'Liew');
// Zell Liew, unaffiliated
announcePlayer('Stephen', 'Curry', 'Golden State Warriors');
// Stephen Curry, Golden State Warriors
Или при знании тернарных операторов возможен выбор более короткого варианта:
function announcePlayer (firstName, lastName, teamName) {
var team = teamName ? teamName : 'unaffiliated';
console.log(firstName + ' ' + lastName + ', ' + team);
}
В ES6 с помощью параметров по умолчанию можно добавлять знак равенства = всякий раз при указании параметра. При применении такого подхода ES6 автоматически присваивает значение по умолчанию, когда параметр не определён.
Так в коде ниже, когда teamName
не определён, то teamName
принимает значение по умолчанию unaffiliated
.
const announcePlayer = (firstName, lastName, teamName = 'unaffiliated') => {
console.log(firstName + ' ' + lastName + ', ' + teamName);
}
announcePlayer('Zell', 'Liew');
// Zell Liew, unaffiliated
announcePlayer('Stephen', 'Curry', 'Golden State Warriors');
// Stephen Curry, Golden State Warriors
Довольно не плохо, не правда?
Обратим внимание на еще один момент. Если требуется активировать значение по умолчанию, можно передать undefined
вручную. Такая ручная передача undefined
оказывается к месту, когда параметр по умолчанию не является последним аргументом функции.
announcePlayer('Zell', 'Liew', undefined);
// Zell Liew, unaffiliated
Выше рассмотрено то, что требуется знать о параметрах по умолчанию. Всё это просто и очень удобно.
Автор: NarekPK