ECMAScript 2015 нам поможет

Будущее уже наступило, дорогой друг! Хватить уже оправдывать свою лень тем, что браузеры что-то там не поддерживают. Пора решать старые задачи по-новому! Пора уже взять и выучить все новые возможности JavaScript из «новой» спецификации ECMAScript 2015 (также известна как ES6). Решил я написать обзорную статью с примерами нововведений. Зачем? Во-первых, хочется вас ещё раз подтолкнуть к изучению нового, а, во-вторых, самому лишний раз вспомнить все новинки. Ну, а в-третьих, лишним дополнительный материал никогда не будет, кому-нибудь обязательно пригодится.

Constants

В JS появились константы также известные как неизменяемые переменные. Под ними подразумеваются такие переменные, которым не может быть присвоено новое значение. Тут важно отметить, что неизменяема именно переменная, а не её содержимое. Например, мы можем поменять свойства любого объекта, объявленного как константа.

const COUNT = 5;
const NAME = "JavaScript";
const VALID = true;

// Теперь, если мы расскомментируем хоть одну из строк ниже, 
// тем самым попробуем поменять ссылку на значение у любой из этих констант,
// то незамедлительно получим "Uncaught TypeError: Assignment to constant variable."
// COUNT = 6;
// NAME = "Java";
// VALID = false;

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

const user = {
    login: "user_123",
    firstName: "Akkakiy"
};
user.login = "user_12345";

Let

Переменная, объявленная через оператор let видна только внутри того блока, в котором она объявлена. Можно сказать, что область видимости у неё блочная. Если вкратце, то такие переменные отличаются тем, что:

  • их нельзя использовать до объявления. С таким объявлением мы не будет наблюдать «поднятия» переменных (hoisting);
  • нельзя в одном блоке дважды объявлять одну и ту же переменную через let, но можно в каждом вложенном блоке, причём они будут существовать, не переопределяя друг друга;
  • при использовании let переменной в качестве счётчика цикла, при каждой итерации будет создаваться новая переменная. Это избавляет разработчиков от создания лишних функций-обёрток в случаях, когда вам, например, внутри какого-нибудь обработчика нужно использовать номер итерации (наверняка, вам это приходилось делать);
  • через let можно также объявлять и функции, которые будут видны только в определённых блоках.

Посмотрим всё на примерах. Если раскомментировать строчку в этом примере, получим ошибку "Uncaught SyntaxError: Identifier 'a' has already been declared":

let a = 1;
// let a = 2;

В следующем примере, если раскомментировать строчку с выводом b, получим ошибку. В то же время для переменной c проблемы не наблюдается, т.к. она объявлена через var и происходит поднятие переменных (hoisting):

// console.log(b);
// Проявления поднятия переменных (hoisting)
console.log(c); // undefined

let b = 3;
var c = 4;

А это еще один случай, когда использование let вам облегчит жизнь:

for (var i = 0; i < 3; i++) {
    setTimeout(function(){
        console.log(i); // для всех итераций цикла выведется 3
    }, 1000)
}

for (let i = 0; i < 3; i++) {
    setTimeout(function(){
        console.log(i); // выведется 0, 1, 2
    }, 1000)
}

Как видите, деструктурировать можно по-разному. Как и где это делать - выбор за вами!

Arrow Functions

Стрелочные функции - это всегда анонимные функции, имеющие в сравнении с функциональными выражениями очень короткий и лаконичные синтаксис. Причиной появления этого вида функций стали компактность и особенное определение контекста this. Использовать стрелочные функции очень просто, удобно и, главное, приятно! Примеры с объяснениями вам помогут:

// В случае отсутствия входных параметров необходимо использовать синтаксис со скобками:
setTimeout(() => console.log("Timeout!"), 5000);

// В случае одного входного параметра обрамляющие скобки можно убрать
// Если параметров больше одного, скобки нужно ставить
[1, 2, 4, 8].forEach(item => console.log("My number is", item));

// Также обратите внимание, что в случае одной строки кода в стрелочной функции,
// её тело можно не обрамлять фигурными скобками (что в обычных функциях всегда обязательно)
// и можно не писать оператор return. Стрелочная функция автоматически вернёт результат выполнения выражения.
const doubles = [1, 2, 4, 8].map(item => item * 2);

const arrowF = () => 123;
// Если раскомментировать, получим ошибку, потому что 
// стрелочные функции не могут быть использованы в качестве конструктора
// const arrInstance = new arrowF();

А в следующем примере показана разница в работе с контекстом this между стрелочной функцией и функциональным выражением:

function Car() {
    this.speed = 0;
    setTimeout(() => {
        console.log(this.speed); // 0
        this.speed = 60;
        console.log(this.speed); // 60
    }, 1000);
}

function CarFunc() {
    this.speed = 0;
    setTimeout(function() {
        console.log(this.speed); // undefined
        this.speed = 60;
        console.log(this.speed); // 60
    }, 1000);
}

const car = new Car();
const carFunc = new CarFunc();

Всех разработчиков очень раздражало, что, например, при использовании какой-нибудь анонимной функции им приходится снаружи делать переменную self, в которую записывали контекст объекта this и уже с этой переменной можно было работать внутри функций. Либо приходилось явно привязывать контекст через .bind(anyObj).

Но в стрелочных функциях такой проблемы нет - контекст this берётся у окружающего контекста. Этот контекст нельзя переопределить даже при помощи call, bind и apply.

И еще одна особенность стрелочных функций - у них нет своего объекта arguments, поэтому при обращении к этому объекту из неё будет выдаваться объект аргументов родительской области видимости (как и со всеми переменными в JavaScript) либо ошибка Uncaught ReferenceError: arguments is not defined, если такой переменной не найдётся.

В стрелочных функциях есть некоторые моменты и ограничения, которые непривычны для начинающего изучать эту новую штучку, но, поверьте, они очень сильно упростят вам код!

Default Parameter Values

Теперь у параметров функций можно задавать значение по умолчанию. Раньше для этих целей приходилось использовать дефолтный оператор (a = a || 5;) или тернарный оператор (a = a !== undefined ? a: 5;). Но теперь можно в объявлении параметров сразу задавать значение по умолчанию. В примерах видно, как этим пользоваться:

function add(value, increment = 1) {
    return value + increment;
}

add(10); // 11
add(10, 10); // 20
add(10, -10); // 0

function getFullName(firstName = "Ivan", middleName = "Ivanovich", lastName = "Ivanov"){
    return firstName + " " + middleName + " " + lastName;
}

getFullName(); // "Ivan Ivanovich Ivanov"
getFullName("Akakkiy"); // "Akakkiy Ivanovich Ivanov"
getFullName("Akakkiy", "Akkakievich"); // "Akakkiy Akkakievich Ivanov"
getFullName(undefined, "Akkakievich"); // "Ivan Akkakievich Ivanov"

Обратите внимание на последний пример. Если мы явно указываем значение аргумента undefined, то используется значение по умолчанию.

Rest Parameters

Rest Parameters или Оставшиеся параметры - это новый синтаксис языка, позволяющий уменьшить количество шаблонного кода. Благодаря этому нововведению можно получить неограниченное количество аргументов в виде массива. Для того, чтобы выделить оставшиеся параметры, нужно использовать префикс ... . Если такой префикс присутствует, все элементы, начиная с позиции текущего и до самого последнего, попадут в этот массив. Смотрим пример:

// функция возвращает все аргументы кроме первого:
function cutTail(head, ...tail) {
    // тут мы уже обращаемся как к обычной переменной, без многоточия
    return tail;
}

cutTail(1, 2, 3, 4, 5); // [2, 3, 4, 5]

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

В общем-то, между arguments и rest parameters есть много общего и они могут решать похожие задачи, но есть и различия:

  • arguments массивом не является, а оставшиеся параметры - полноценный массив со всеми присущими в JS массивам методами для манипуляции данными;
  • arguments - это всегда все элементы, которые были переданы в функцию, а в rest parameters попадают все, за исключением выделенных в отдельную переменную;
  • у arguments есть вспомогательные свойства, например, callee, а у оставшихся параметров только те, что есть у массивов.

Примеры на закрепление:

function argCount(...allArgs) {
  console.log(allArgs.length);
}

argCount(); // 0
argCount("a"); // 1
argCount("a", "b", "c", "d"); // 4

Следующая функция принимает первым параметром количество повторений, а дальше - строки, которые нужно повторить:

function repeatTimes(count, ...strings) {
    strings.forEach((str) => console.log(str.repeat(count)));
}

repeatTimes(1, "a", "b"); // "a", "b"
repeatTimes(2, "a", "b", "c"); // "aa", "bb", "cc"
repeatTimes(5, "a"); // "aaaaa"

Как видите, бывают ситуации, когда новый синтаксис rest parameters может быть очень удобным в использовании.

Template Literals

Шаблонные литералы или шаблонные строки - это новый вид синтаксиса, который позволяет интерполировать (вставлять значения переменных) строки. А ещё с помощью шаблонных литералов можно делать многострочные шаблоны. Все эти прелести избавляют разработчиков от постоянных конкатенаций переменных со строками (хотя, если транспайлить код при помощи Babel, то всё для обратной совместимости будет переведено обратно на конкатенацию) и экранирования конца строки символом обратного слэша \ в многострочных литералах. А это значит, что ваш код станет ещё лучше! Пример покажет возможности этой новинки:

const user = { 
    name: "Arny"
};
const greeting = `Hello, ${user.name}!`; // "Hello, Arny!"
const letter = `${greeting}
${'We will rock you! x' + Math.pow(2, 10)}
${user.name.toUpperCase()}!`;
// Результат этого шаблонного литерала будет таким:
// "Hello, Arny!
// We will rock you! x1024
// ARNY!"

const bbb = `${"AAA".toUpperCase().split("").map(() => "B").join("")}`; // "BBB"

Как видно, в качестве интерполируемого значения может быть любое JS-выражение. Последний пример с "BBB" больше для демонстрации, старайтесь не громоздить сложные выражения в шаблонах.

Destructuring Assignment

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

var names = ["Ivan", "Efrem", "Arkadiy"];

// без деструктурирования надо было бы делать как-то так:
var firstName = names[0];
var secondName = names[1];
var thirdName = names[2];

// с деструктурированием всё гораздо компактнее:
var [firstName, secondName, thirdName] = names;

Ещё один пример показывает решение всем известной задачи обмена значений переменных без использования третьей:

var a = 1;
var b = 3;
[a, b] = [b, a];
console.log(a, b); // 3, 1

А в следующем примере показано, как из списка можно достать только нужные значения, игнорируя ненужные:

var positions = ["first", "second", "third"];
// второе значение нам не нужно
var [first, , third] = positions;

Аналогично можно деструктурирующее назначение можно производить и с объектами:

var man = {name: "Oleg", height: 176, weight: 75};
// Достаём нужные свойства объекта в отдельные переменные
var {height, weight} = man;
console.log(height, weight); // 176, 75
// А можно скопировать эти свойства в переменные с другим именем:
var {height: myHeight, weight: myWeight} = man;
console.log(myHeight, myWeight); // 176, 75

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

function congratulate({name: firstPlayerName}, {name: secondPlayerName}, {name: thirdPlayerName}, ...other) {
    console.log(firstPlayerName, "gets a gold medal");
    console.log(secondPlayerName, "gets a silver medal");
    console.log(thirdPlayerName, "gets a bronze medal");
    // other - это массив с оставшимися участниками
    other.forEach(({name}) => console.log(name, "is looser... :("));
}

congratulate({name: "Ivan", scores: 100}, {name: "Maxim", scores: 90}, {name: "John", scores: 80}, {name: "Igor", scores: 70}, {name: "Leonid", scores: 60});

// Получим такой вывод:
// Ivan gets a gold medal
// Maxim gets a silver medal
// John gets a bronze medal
// Igor is looser... :(
// Leonid is looser... :(

Видите, как теперь можно быстро, компактно и удобно доставать нужные свойства из входных параметров! Но тут надо помнить, что если в функцию congratulate послать меньше 3-х аргументов, то мы получим ошибку Uncaught TypeError: Cannot match against 'undefined' or 'null', т.к. интерпретатор не сможет соотнести ожидаемые входные данные с реальными. Если послать ровно 3 параметра, в others получим пустой массив [ ].

А ещё можно деструктурировать вложенные объекты:

var user = {name: "Yura", profession: {name: "programmer"}};
var {profession: {name: professtionName}} = user;
console.log(professtionName); // "programmer"

В общем, деструктурировать можно много где и много как:

var people = [
    {name: "Ivan", family: {father: "Sergey", mother: "Elena"}}, 
    {name: "Oleg", family: {father: "Nikolay", mother: "Olga"}}
];

for (var {name: n, family: { father: f } } of people) {
  console.log("Name: " + n + ", Father: " + f);
}
// или с таким же эффектом можно сделать так:
people.forEach(({name: n, family: { father: f }}) => console.log("Name: " + n + ", Father: " + f));

Modules

Теперь стандартными средствами JavaScript можно что-нибудь импортировать или экспортировать, не засоряя при этом глобальную область видимости. Сообщество уже давно решило эту проблему такими форматами модулей, как AMD, CommonJS, UMD и др., но для работы с ними часто приходилось использовать вспомогательные библиотеки типа RequreJS. Теперь появился стандартный формат JS-модулей, который уже сейчас можно использовать. Вот только не забудьте прогнать всё через Babel, поддержка у браузеров этой функциональности пока очень слабая.

lib/strings.js
// Выдумаем пару методов нашей библиотеки
export function reverse(str) {
    return str.split("").reverse().join("");
}

export function getFirstLetter(str) {
    return str[0];
}

export const EMPTY_STRING = "";

export function isEmpty(str) {
    return !str.trim();
}
lib/parser.js
// по умолчанию экспортируем эту функцию. См. ниже, как она импортируется
export default function parse(str) {
    return str.split(",");
}

export const trimParse = (str) => {
    return parse(str).map((statement) => {
        return statement.trim();
    });
}
index.js
import * as strLib from './lib/strings.js';
import parserLib, {trimParse} from './lib/parser.js';

strLib.reverse("asd"); // "dsa"
strLib.getFirstLetter("asd"); // "a"
console.log(strLib.EMPTY_STRING); // ""
strLib.isEmpty("asd"); // false

parserLib("123 , 456 , 789"); // ["123 ", " 456 ", " 789"]
trimParse("123 , 456 , 789"); // ["123", "456", "789"]

В общем-то, ознакомившись с примерами, думаю, вы поняли, как всё это работает. Можно импортировать как определённое свойство или модуль целиком, так и его экспортируемое значение по умолчанию. Отмечу import * as strLib и export default .... Импорт со звёздочкой означает импорт всех экспортируемых объектов модуля в переменную strLib. Экспорт по умолчанию (export default ...) указывает на объект, который будет импортироваться из модуля, если импортируется весь модуль целиком в одну переменную. Экспортируемый по умолчанию элемент может быть в модуле только один.

Поддержка браузерами

Конечно, разработчики стараются реализовывать все новые возможности языка в новых версиях браузеров, но на данный момент вы скорее всего не сможете взять написанный согласно ECMAScript 2016 спецификации код и выложить на production. Будут ошибки, особенно в не самых новых браузерах.

"Как быть? Зачем я всё это читал?" спросите вы? Тут ответ очень прост! Современные инструменты разработчиков очень радуют нас своими возможностями. Babel - один из тех инструментов, который вам всенепременно нужно освоить. "Babel - the compiler for writing next generation JavaScript" = "Babel - компилятор для написания JavaScript следующего поколения" - именно так позиционируют разработчики свой инструмент. Думаю, вам понятно его назначение - он берёт JS-код, написанный согласно одной спецификации языка и транслирует в код, поддерживающий более старую спецификацию.

Именно эта трансформация кода позволяет вам писать код в новом варианте и не думать о совместимости со старыми браузерами. Такой код можно выкладывать на production и не переживать по поводу использования стрелочных функций, шаблонных литералов и иже с ними.

В заключение

Вы представляете? Всё то, о чём вы сейчас прочли, можно и нужно УЖЕ использовать! Хотя тут ещё пока описаны не всё новое! Язык возобновил своё развитие и спецификация ECMAScript 2015 - тому подтверждение! Рывок очень заметный! Поэтому не делайте по-старому то, что можно сделать намного лаконичнее, проще и красивее по-новому. Будьте на волне новых технологии и ваша ценность как IT-специалиста станет выше! Иначе можно сильно отстать и вообще перестать понимать происходящее в языке.

Я постараюсь постепенно дополнить статью всеми новинками спецификации с примерами, сейчас тут не всё.

Надеюсь, вам понравилось! До новых статей! :)