Советы и приёмы ES6, которые сделают ваш код чище, лаконичней и читабельней
Перевод статьи 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.