Советы и приёмы ES6, которые сделают ваш код чище, лаконичней и читабельней

Sergey Kabardinov
devSchacht
Published in
10 min readFeb 15, 2018

Перевод статьи Sam Williams: ES6 tips and tricks to make your code cleaner, shorter, and easier to read!

Литералы шаблонов

Литералы шаблонов облегчают работу со строками. Они обозначаются обратным апострофом ` и дают возможность вставлять значения переменных, используя конструкцию ${variable}. Сравните эти 2 строки ниже:

var fName = 'Peter', sName = 'Smith', age = 43, job= 'photographer';
var a = 'Hi, I\'m ' + fName + ' ' + sName + ', I\'m ' + age + ' and work as a ' + job + '.';
var b = `Hi, I'm ${ fName } ${ sName }, I'm ${ age } and work as a ${ job }.`;

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

Блочные области видимости

Область видимость в JavaScript всегда ограничена функцией, именно поэтому стало общепринятым оборачивать файл с кодом на JavaScript в функцию, которая будет немедленно вызвана (IIFE). Этого достаточно для изоляции всех переменных в рамках файла, таким образом избегаются возможные конфликты переменных.

Теперь у нас появилась области видимости в рамках блока {...} и два новых способа объявления переменных, областью видимости которых будет блок, в котором они были объявлены.

Объявления let

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

var a = 'car' ;
{
let a = 5;
console.log(a) // 5
}
console.log(a) // car

Областью видимости переменной, объявленной при помощи let, будет блок {}. Это помогает дать ответ на классический вопрос, который задают на собеседованиях: "что выведет console.log и как сделать так, чтобы код работал ожидаемо?".

for (var i = 1; i < 5; i++) {
setTimeout(() => { console.log(i); }, 1000);
}

В данном случае в консоль выводится “5 5 5 5 5”, потому что переменная i изменяется на каждой итерации цикла.

Если заменить var на let, то всё изменится. Теперь на каждой итерации цикла создаётся новая блочная область видимости со своим значением i. Это всё равно, что написать:

{let i = 1; setTimeout(() => { console.log(i) }, 1000)}
{let i = 2; setTimeout(() => { console.log(i) }, 1000)}
{let i = 3; setTimeout(() => { console.log(i) }, 1000)}
{let i = 4; setTimeout(() => { console.log(i) }, 1000)}
{let i = 5; setTimeout(() => { console.log(i) }, 1000)}

Ещё одно различие между var и let это то, что let не поднимается в начало определения функции подобно var.

{
console.log(a); // undefined
console.log(b); // ReferenceError
var a = 'car';
let b = 5;
}

Из-за более жёстко ограниченной области видимости и более предсказуемого поведения бытует мнение, что необходимо использовать let вместо var, исключая случаи, когда вам специально нужно поднятие или более свободная область видимости, которую дает var.

Объявления const

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

Теперь у нас появился способ объявления констант.

{
const c = "tree";
console.log(c); // tree
c = 46; // TypeError!
}

const не делает переменную иммутабельной, просто блокирует её повторное присваивание. Если у вас сложный тип данных (объект или массив), то его значения могут быть изменены.

{
const d = [1, 2, 3, 4];
const dave = { name: 'David Jones', age: 32};
d.push(5);
dave.job = "salesman";
console.log(d); // [1, 2, 3, 4, 5]
console.log(dave); // { age: 32, job: "salesman", name: 'David Jones'}
}

Проблемы функций с блочной областью видимости

Отныне функции определены только в рамках блока, в котором они были объявлены (FD).

{
bar(); // работает
function bar() { /* что-то делаем */ }
}
bar(); // не работает

Такая проблема может случиться, если вы объявите функцию внутри выражения if.

Рассмотрим такой случай:

if ( something) {
function baz() { console.log('I passed') }
} else {
function baz() { console.log('I didn\'t pass') }
}
baz();

До ES6 обе функции поднимались и результатом был бы'I didn\'t pass' в независимости от something. Теперь мы получим 'ReferenceError', так как baz всегда будет иметь блочную область видимости.

Оператор Spread

ES6 вводит оператор ..., который называется "оператор spread". У него есть две основные функции: добавление существующего массива или объекта в новый и объединение множества параметров в массив.

Первый вариант использования наверное самый востребованный, рассмотрим его первым.

let a = [3, 4, 5];
let b = [1, 2, ...a, 6];
console.log(b); // [1, 2, 3, 4, 5, 6]

Это может быть очень полезно для передачи массива в качестве параметров в функцию.

function foo(a, b, c) { console.log(`a=${a}, b=${b}, c=${c}`)}
let data = [5, 15, 2];
foo( ...data); // a = 5, b = 15, c = 2

Объект тоже может быть распределён, то есть каждая пара ключ-значение будет добавлена в новый объект (распределение объекта находится на стадии 4 черновика и официально должен быть включен в ES2018. Сейчас поддерживает только Chrome 60 или выше, Firefox 55 или выше и Node 6.4.0 или выше).

let car = { type: 'vehicle ', wheels: 4};
let fordGt = { make: 'Ford', ...car, model: 'GT'};
console.log(fordGt); // {make: 'Ford', model: 'GT', type: 'vehicle', wheels: 4}

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

let a = [1, 2, 3];
let b = [ ...a ];
let c = a;
b.push(4);
console.log(a); // [1, 2, 3]
console.log(b); // [1, 2, 3, 4] ссылки на разные массивы
c.push(5);
console.log(a); // [1, 2, 3, 5]
console.log(c); // [1, 2, 3, 5] ссылки на тот же массив

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

function foo(...args) {
console.log(args);
}
foo( 'car', 54, 'tree'); // [ 'car', 54, 'tree' ]

Параметры по умолчанию

В ES6 функции могут быть объявлены с параметрами по умолчанию. Опущенные параметры или же параметры со значением undefined будут инициализированы значениями по умолчанию. Но будьте осторожны — null и false приводятся к 0.

function foo( a = 5, b = 10) {
console.log( a + b);
}
foo(); // 15
foo( 7, 12 ); // 19
foo( undefined, 8 ); // 13
foo( 8 ); // 18
foo( null ); // 10, так как null приводится к 0

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

function foo( a ) { return a * 4; }
function bar( x = 2, y = x + 4, z = foo(x)) {
console.log([ x, y, z ]);
}
bar(); // [ 2, 6, 8 ]
bar( 1, 2, 3 ); // [ 1, 2, 3 ]
bar( 10, undefined, 3 ); // [ 10, 14, 3 ]

Деструктуризация

Деструктуризация — это процесс разделения массива или объекта с левой стороны от знака равенства. Массив или объект может хранится в переменой или возвращаться функцией или же быть результатом вычисления выражения.

let [ a, b, c ] = [ 6, 2, 9];
console.log(`a=${a}, b=${b}, c=${c}`); // a = 6, b = 2, c = 9
function foo() { return ['car', 'dog', 6 ]; }
let [ x, y, z ] = foo();
console.log(`x=${x}, y=${y}, z=${z}`); // x = car, y = dog, z = 6

При деструктуризации объекта ключи объекта могут быть перечислены в фигурных скобках для извлечения пары ключ-значение.

function bar() { return {a: 1, b: 2, c: 3}; }
let { a, c } = bar();
console.log(a); // 1
console.log(c); // 3
console.log(b); // undefined

Иногда вам может потребоваться извлечь значение, но присвоить его новой переменной. Этого можно достичь, используя конструкцию ‘key: variable’ в левой части от знака равенства.

function baz() {
return {
x: 'car',
y: 'London',
z: { name: 'John', age: 21}
};
}
let { x: vehicle, y: city, z: { name: driver } } = baz();
console.log(
`I'm going to ${city} with ${driver} in their ${vehicle}.`
); // I'm going to London with John in their car.

Ещё деструктуризация объекта позволяет присваивать одно значение нескольким переменным.

let { x: first, x: second } = { x: 4 };
console.log( first, second ); // 4, 4

Литерал объекта и краткие параметры

При создании объекта с помощью литерала объекта из переменных ES6 позволяет опустить ключи, если они совпадают с именами переменных.

let a = 4, b = 7;
let c = { a: a, b: b };
let concise = { a, b };
console.log(c, concise) // {a: 4, b: 7}, {a: 4, b: 7}

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

function foo() {
return {
name: 'Anna',
age: 56,
job: { company: 'Tesco', title: 'Manager' }
};
}
// до ES6
let a = foo(), name = a.name, age = a.age, company = a.job.company;
// ES6 деструктуризация и краткие параметры
let { name, age, job: {company}} = foo();

Также это может быть использовано для деструктуризации объекта переданного в функцию. Методы 1 и 2 показывают, как это можно было сделать до появления ES6, а метод 3 — с использованием деструктуризации и лаконичных параметров.

let person = {
name: 'Anna',
age: 56,
job: { company: 'Tesco', title: 'Manager' }
};
// метод 1
function old1( person) {
var yearOfBirth = 2018 - person.age;
console.log( `${ person.name } works at ${ person.job.company } and was born in ${ yearOfBirth }.`);
}
// метод 2
function old1( person) {
var age = person.age,
yearOfBirth = 2018 - age,
name = person.name,
company = person.job.company;
console.log( `${ name } works at ${ company } and was born in ${ yearOfBirth }.`);
}
// метод 3
function es6({ age, name, job: {company}) {
var yearOfBirth = 2018 - age;
console.log( `${ name } works at ${ company } and was born in ${ yearOfBirth }.`);
}

Используя ES6, мы смогли получить age, name и company без объявления дополнительных переменных.

Динамические имена свойств

В ES6 появилась возможность создавать или добавлять поля с динамически вычисляемыми именами ключей.

let  city= 'sheffield_';
let a = {
[ city + 'population' ]: 350000
};
a[ city + 'county' ] = 'South Yorkshire';
console.log(a);
// {sheffield_population: 350000, sheffield_county: 'South Yorkshire' }

Стрелочные функции

Стрелочные функции имеют два главных аспекта: их структура и их привязка контекста this.

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

var foo = function( a, b ) {
return a * b;
}
let bar = ( a, b ) => a * b;

Если такой функции требуется сделать что-то большее, чем простые вычисления, то можно использовать фигурные скобки и тогда функция вернёт то, что будет возвращено из блока тела функции.

let baz = ( c, d ) => {
let length = c.length + d.toString().length;
let e = c.join(', ');
return `${e} and there is a total length of ${length}`;
}

Одно из самых полезных мест, где стрелочные функции могут быть применены, это функции для работы с массивами такими как .map, .forEach или .sort.

let arr = [ 5, 6, 7, 8, 'a' ];
let b = arr.map( item => item + 3 );
console.log(b); // [ 8, 9, 10, 11, 'a3' ]

Помимо более короткого синтаксиса, стрелочные функции также устраняют проблему привязки контекста this. Решение этой проблемы до ES6 заключалось в том, что функции должны были получать ссылку на заранее сохранённый контекст this, часто как переменную self.

var clickController = {
doSomething: function (..) {
var self = this;
btn.addEventListener(
'click',
function() { self.doSomething(..) },
False
);
}
};

Это приходилось делать потому, что this привязывался динамически. Это означает, что this внутри обработчика события и this внутри doSomething не указывают на одно и то же.

Внутри стрелочной функции this привязывается лексически, а не динамически. Это является главной фишкой стрелочных функций.

Однако, хотя лексическая привязка this это здорово, это не всегда то, что нужно.

let a = {
oneThing: ( a ) => {
let b = a * 2;
this.otherThing(b);
},
otherThing: ( b ) => {....}
};
a.oneThing(6);

Когда мы вызываем a.oneThing(6), разрешить ссылку this.otherThing(b) не получится, так как this указывает не на объект a, а на внутреннюю область видимости. Если вы соберетесь переписывать старый код на ES6, имейте это в виду.

Цикл for … of

ES6 добавляет возможность перебора по каждому значению в массиве. Отличие этого цикла от уже существующего for ... in в том, что существующий перебирает массив по ключу.

let a = ['a', 'b', 'c', 'd' ];
// ES6
for ( var val of a ) {
console.log( val );
} // "a" "b" "c" "d"
// до ES6
for ( var idx in a ) {
console.log( idx );
} // 0 1 2 3

Использование нового цикла for … of позволяет не делать let val = a[idx] внутри каждой итерации.

По стандарту JavaScript массивы, строки, генераторы и коллекции являются итерируемыми типами данных. Обычно объекты не могут быть нормально итерированы, конечно, если вы не определили для них итераторы.

Числовые литералы

ES5 код позволял замечательно работать с числами в десятеричной и шестнадцатеричной системе счисления, но формат записи восьмеричной системы счисления не был описан. Фактически, такие числа были запрещены в строгом режиме.

ES6 добавил новый формат записи числа, добавьте o после начального 0 для описания числа в восьмеричной системе счисления. Кроме того, был добавлен формат для двоичных чисел.

Number( 29 )  // 29
Number( 035 ) // 35 в старой восьмеричной форме записи
Number( 0o35 ) // 29 в новой восьмеричной форме записи
Number( 0x1d ) // 29 в шестнадцатеричной форме записи
Number( 0b11101 ) // 29 в двоичной форме записи

И многое другое…

Это далеко не всё, что ES6 может предложить нам для того, чтобы наш код стал чище, лаконичней и проще в понимании. Я нацелился на написание продолжения этой статьи, которая будет раскрывать менее известные части ES6.

Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.

--

--