javascript

Введение в часто используемые особенности ES6. Часть 1

  • суббота, 14 октября 2017 г. в 03:13:46
https://habrahabr.ru/post/340002/
  • JavaScript


Данная публикация является переводом статьи «Introduction to commonly used ES6 features» под авторством Zell Liew, размещенного здесь. Перевод разделён на 2 части.


JavaScript серьезно развился в последние несколько лет. Если вы изучаете язык в 2017 году и при этом не касались ECMAScript 6 (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. От вас не требуется знать все сразу. Далее поделюсь восемью особенностями, которые применяю каждый день. К ним относятся:


  1. let + const
  2. Стрелочные функции
  3. Параметры по умолчанию (Default parameters)
  4. Деструктуризация
  5. Rest parameter и spread operator
  6. Расширенные литералы объектов
  7. Шаблонные строки
  8. Промисы

Кстати, браузеры очень хорошо поддерживают 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. требуемого количества аргументов
  2. необходимости неявного возврата

Под 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

Выше рассмотрено то, что требуется знать о параметрах по умолчанию. Достаточно просто и очень удобно.