Что и как в ES6: хитрости, лучшие практики и примеры. Часть первая. let/const, блоки, стрелочные функции, строки, деструктуризация, модули, параметры, классы

Шпаргалка для повседневного использования, содержащая подборку советов по ES2015 [ES6] с примерами. Делитесь своими советами в комментариях! 

var против let / const

Кроме var нам теперь доступны 2 новых идентификатора для хранения значений — let и const. В отличие от var, let и const обладают блочной областью видимости.

Пример использования var:

var snack = 'Meow Mix';

function getFood(food) {
    if (food) {
        var snack = 'Friskies';
        return snack;
    }
    return snack;
}

getFood(false); // undefined

А вот что происходит при замене var на let:

let snack = 'Meow Mix';

function getFood(food) {
    if (food) {
        let snack = 'Friskies';
        return snack;
    }
    return snack;
}

getFood(false); // 'Meow Mix'

Такое изменение в поведении показывает, что нужно быть осторожным при рефакторинге старого кода, в котором используется var. Простая замена var на let может привести к непредсказуемому поведению программы.

Примечание: let и const видимы лишь в своём блоке. Таким образом, попытка вызвать их до объявления приведёт к ReferenceError.

console.log(x); // ReferenceError: x is not defined

let x = 'hi';

Лучшая практика: Оставьте var в legacy-коде для дальнейшего тщательного рефакторинга. При работе с новым кодом используйте let для переменных, значения которых будут изменяться, и const — для неизменных переменных.

Замена немедленно вызываемых функций (IIFE) на блоки (Blocks)

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

(function () {
    var food = 'Meow Mix';
}());

console.log(food); // Reference Error

Пример ES6 Blocks:

{
    let food = 'Meow Mix';
};

console.log(food); // Reference Error

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

Часто при использовании вложенных функций бывает нужно отделить контекст this от его лексической области видимости. Пример приведён ниже:

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    return arr.map(function (character) {
        return this.name + character; // Cannot read property 'name' of undefined
    });
};

Распространённым решением этой проблемы является хранение контекста this в переменной:

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    var that = this; // Store the context of this
    return arr.map(function (character) {
        return that.name + character;
    });
};

Также мы можем передать нужный контекст this:

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    return arr.map(function (character) {
        return this.name + character;
    }, this);
};

Или привязать контекст:

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    return arr.map(function (character) {
        return this.name + character;
    }.bind(this));
};

Используя стрелочные функции, лексическое значение this не скрыто, и вышеприведённый код можно переписать так:

function Person(name) {
    this.name = name;
}

Person.prototype.prefixName = function (arr) {
    return arr.map(character => this.name + character);
};

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

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

var squares = arr.map(function (x) { return x * x }); // Function Expression
const arr = [1, 2, 3, 4, 5];
const squares = arr.map(x => x * x); // Arrow Function for terser implementation

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

Строки

С приходом ES6 стандартная библиотека сильно увеличилась. Кроме предыдущих изменений появились строчные методы, такие как .includes() и .repeat().

.includes( )

var string = 'food';
var substring = 'foo';

console.log(string.indexOf(substring) > -1);

Вместо проверки возвращаемого значения > -1 для проверки на наличие подстроки можно использовать .includes(), возвращающий логическую величину:

const string = 'food';
const substring = 'foo';

console.log(string.includes(substring)); // true

.repeat( )

function repeat(string, count) {
    var strings = [];
    while(strings.length < count) {
        strings.push(string);
    }
    return strings.join('');
}

В ES6 всё намного проще:

// String.repeat(numberOfRepetitions)
'meow'.repeat(3); // 'meowmeowmeow'

Шаблонные литералы

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

var text = "This string contains \"double quotes\" which are escaped.";
let text = `This string contains "double quotes" which don't need to be escaped anymore.`;

Шаблонные литералы также поддерживают интерполяцию, что делает задачу конкатенации строк и значений:

var name = 'Tiger';
var age = 13;

console.log('My cat is named ' + name + ' and is ' + age + ' years old.');

Гораздо проще:

const name = 'Tiger';
const age = 13;

console.log(`My cat is named ${name} and is ${age} years old.`);

В ES5 мы обрабатывали переносы строк так:

var text = (
    'cat\n' +
    'dog\n' +
    'nickelodeon'
);

Или так:

var text = [
    'cat',
    'dog',
    'nickelodeon'
].join('\n');

Шаблонные литералы сохраняют переносы строк:

let text = ( `cat
dog
nickelodeon`
);

Шаблонные литералы также могут обрабатывать выражения:

let today = new Date();
let text = `The time and date is ${today.toLocaleString()}`;

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

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

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

var arr = [1, 2, 3, 4];
var a = arr[0];
var b = arr[1];
var c = arr[2];
var d = arr[3];
let [a, b, c, d] = [1, 2, 3, 4];

console.log(a); // 1
console.log(b); // 2

Деструктуризация объектов

var luke = { occupation: 'jedi', father: 'anakin' };
var occupation = luke.occupation; // 'jedi'
var father = luke.father; // 'anakin'
let luke = { occupation: 'jedi', father: 'anakin' };
let {occupation, father} = luke;

console.log(occupation); // 'jedi'
console.log(father); // 'anakin'

Модули

До ES6 нам приходилось использовать библиотеки наподобие Browserify для создания модулей на клиентской стороне, и require в Node.js. С ES6 мы можем прямо использовать модули любых типов (AMD и CommonJS).

Экспорт в CommonJS

module.exports = 1;
module.exports = { foo: 'bar' };
module.exports = ['foo', 'bar'];
module.exports = function bar () {};

Экспорт в ES6

В ES6 мы можем использовать разные виды экспорта.

Именованный экспорт:

export let name = 'David';
export let age  = 25;

Экспорт списка объектов:

function sumTwo(a, b) {
    return a + b;
}

function sumThree(a, b, c) {
    return a + b + c;
}

export { sumTwo, sumThree };

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

export function sumTwo(a, b) {
    return a + b;
}

export function sumThree(a, b, c) {
    return a + b + c;
}

И наконец, можно экспортировать связывания по умолчанию:

function sumTwo(a, b) {
    return a + b;
}

function sumThree(a, b, c) {
    return a + b + c;
}

let api = {
    sumTwo,
    sumThree
};

export default api;

/* Which is the same as
 * export { api as default };
 */

Лучшая практика: всегда используйте метод export default в конце модуля. Это чётко покажет, что именно экспортируется, и сэкономит время.

Импорт в ES6

ES6 предоставляет различные виды импорта. Мы можем импортировать целый файл:

import 'underscore';

Важно заметить, что импортирование всего файла приведёт к исполнению всего кода на внешнем уровне этого файла.

Как и в Python, есть именованный импорт:

import { sumTwo, sumThree } from 'math/addition';

Который можно переименовывать:

import {
    sumTwo as addTwoNumbers,
    sumThree as sumThreeNumbers
} from 'math/addition';

Кроме того, можно импортировать пространство имён:

import * as util from 'math/addition';

И наконец, список значений из модуля:

import * as additionUtil from 'math/addition';
const { sumTwo, sumThree } = additionUtil;

Импорт из связывания по умолчанию выглядит так:

import api from 'math/addition';
// Same as: import { default as api } from 'math/addition';

Экспортирование лучше упрощать, но иногда можно смешивать импорт по умолчанию с чем-то ещё. Когда мы экспортируем так:

// foos.js
export { foo as default, foo1, foo2 };

Импортировать их можно так:

import foo, { foo1, foo2 } from 'foos';

При импортировании модуля, экспортированного с использованием синтаксиса CommonJS (как в React), можно сделать:

import React from 'react';
const { Component, PropTypes } = React;

Это можно упростить:

import React, { Component, PropTypes } from 'react';

Примечание: экспортируемые значения — это связывания, не ссылки. Поэтому изменение в одном модуле повлечёт за собой изменение в другом.

Параметры

В ES5 было несколько способов обработки функций со значениями по умолчанию, неопределёнными аргументами и именованными параметрами. В ES6 всё это реализуется, причём с понятным синтаксисом.

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

function addTwoNumbers(x, y) {
    x = x || 0;
    y = y || 0;
    return x + y;
}

В ES6 можно просто указать параметры функции по умолчанию:

function addTwoNumbers(x=0, y=0) {
    return x + y;
}
addTwoNumbers(2, 4); // 6
addTwoNumbers(2); // 2
addTwoNumbers(); // 0

Остаточные параметры

В ES5 неопределённое количество аргументов обрабатывалось так:

function logArguments() {
    for (var i=0; i < arguments.length; i++) {
        console.log(arguments[i]);
    }
}

Используя остаточный оператор, можно передавать неопределённое число аргументов:

function logArguments(...args) {
    for (let arg of args) {
        console.log(arg);
    }
}

Именованные параметры

Одним из шаблонов ES5 для работы с именованными параметрами был шаблон options object, взятый из jQuery.

function initializeCanvas(options) {
    var height = options.height || 600;
    var width  = options.width  || 400;
    var lineStroke = options.lineStroke || 'black';
}

Тоже самое можно получить, используя деструктор в качестве формального параметра функции:

function initializeCanvas(
    { height=600, width=400, lineStroke='black'}) {
        // Use variables height, width, lineStroke here
    }

Если мы хотим сделать всё значение опциональным, можно деструктурировать пустой объект:

function initializeCanvas(
    { height=600, width=400, lineStroke='black'} = {}) {
        // ...
    }

Оператор расширения

В ES5 можно было найти максимум массива, используя метод apply над Math.max:

Math.max.apply(null, [-1, 100, 9001, -32]); // 9001

В ES6 можно использовать оператор расширения для передачи массива значений в качестве параметров функции:

Math.max(...[-1, 100, 9001, -32]); // 9001

Также можно интуитивно конкатенировать массивы литералов:

let cities = ['San Francisco', 'Los Angeles'];
let places = ['Miami', ...cities, 'Chicago']; // ['Miami', 'San Francisco', 'Los Angeles', 'Chicago']

Классы

До ES6 классы нужно было создавать, добавляя к функции-конструктору свойства, расширяя прототип:

function Person(name, age, gender) {
    this.name   = name;
    this.age    = age;
    this.gender = gender;
}

Person.prototype.incrementAge = function () {
    return this.age += 1;
};

А расширенные классы — так:

function Personal(name, age, gender, occupation, hobby) {
    Person.call(this, name, age, gender);
    this.occupation = occupation;
    this.hobby = hobby;
}

Personal.prototype = Object.create(Person.prototype);
Personal.prototype.constructor = Personal;
Personal.prototype.incrementAge = function () {
    Person.prototype.incrementAge.call(this);
    this.age += 20;
    console.log(this.age);
};

ES6 предоставляет весьма удобный синтаксический сахар. Классы можно создавать таким образом:

class Person {
    constructor(name, age, gender) {
        this.name   = name;
        this.age    = age;
        this.gender = gender;
    }

    incrementAge() {
      this.age += 1;
    }
}

А расширять — используя ключевое слово extends:

class Personal extends Person {
    constructor(name, age, gender, occupation, hobby) {
        super(name, age, gender);
        this.occupation = occupation;
        this.hobby = hobby;
    }

    incrementAge() {
        super.incrementAge();
        this.age += 20;
        console.log(this.age);
    }
}

Лучшая практика: хотя такой синтаксис и скрывает реализацию, новичкам в нём будет проще разобраться, да и написанный код будет чище.

По материалам es6-cheatsheet