Продолжение шпаргалки для повседневного использования по ES2015 [ES6] с примерами. Делитесь своими советами в комментариях!
Мэпы
Мэпы — это очень полезная структура данных. До ES6 хеш-мэпы создавались через объекты:
var map = new Object();
map[key1] = 'value1';
map[key2] = 'value2';
Однако это не защищает от случайной перегрузки функций с конкретными именами свойств:
> getOwnProperty({ hasOwnProperty: 'Hah, overwritten'}, 'Pwned');
> TypeError: Property 'hasOwnProperty' is not a function
Настоящие мэпы позволяют устанавливать значения (set
), брать их (get
), искать их (search
) и многое другое.
let map = new Map();
> map.set('name', 'david');
> map.get('name'); // david
> map.has('name'); // true
Самое замечательное — это то, что теперь мы не обязаны использовать только строки. Можно использовать любой тип как ключ, и он не будет приведён к строковому виду.
let map = new Map([
['name', 'david'],
[true, 'false'],
[1, 'one'],
[{}, 'object'],
[function () {}, 'function']
]);
for (let key of map.keys()) {
console.log(typeof key);
// > string, boolean, number, object, function
}
Примечание: использование сложных величин (функций, объектов) невозможно при проверке на равенство при использовании методов наподобие
map.get()
. Поэтому используйте простые величины: строки, логические переменные и числа.
Также по мэпам можно итерироваться через .entries()
:
for (let [key, value] of map.entries()) {
console.log(key, value);
}
Слабые мэпы
В версиях младше ES6 было несколько способов хранения приватных данных. Например, можно было использовать соглашения по именованию:
class Person {
constructor(age) {
this._age = age;
}
_incrementAge() {
this._age += 1;
}
}
Но такие соглашения могут запутать, да и не всегда их придерживаются. Вместо этого можно использовать WeakMaps:
let _age = new WeakMap();
class Person {
constructor(age) {
_age.set(this, age);
}
incrementAge() {
let age = _age.get(this) + 1;
_age.set(this, age);
if (age > 50) {
console.log('Midlife crisis');
}
}
}
Фишкой WeakMaps является то, что ключи приватных данных не выдают имена свойств, которые можно увидеть, используя Reflect.ownKeys()
:
> const person = new Person(50);
> person.incrementAge(); // 'Midlife crisis'
> Reflect.ownKeys(person); // []
Практическим примером использования WeakMaps является хранение данных, связанных с элементом DOM, при этом сама DOM не захламляется:
let map = new WeakMap();
let el = document.getElementById('someElement');
// Store a weak reference to the element with a key
map.set(el, 'reference');
// Access the value of the element
let value = map.get(el); // 'reference'
// Remove the reference
el.parentNode.removeChild(el);
el = null;
// map is empty, since the element is destroyed
Как видно выше, когда объект уничтожается сборщиком мусора, WeakMap автоматически удаляет пару ключ-значение, которая идентифицировалась этим объектом.
Обещания
Обещания, о которых мы подробно рассказывали в отдельной статье, позволяют превратить «горизонтальный» код:
func1(function (value1) {
func2(value1, function (value2) {
func3(value2, function (value3) {
func4(value3, function (value4) {
func5(value4, function (value5) {
// Do something with value 5
});
});
});
});
});
В вертикальный:
func1(value1)
.then(func2)
.then(func3)
.then(func4)
.then(func5, value5 => {
// Do something with value 5
});
До ES6, приходилось использовать bluebird или Q. Теперь Promises реализованы нативно:
new Promise((resolve, reject) =>
reject(new Error('Failed to fulfill Promise')))
.catch(reason => console.log(reason));
У нас есть два обработчика, resolve (функция, вызываемая при выполнении обещания) и reject (функция, вызываемая при невыполнении обещания).
Преимущества Promises: обработка ошибок с кучей вложенных коллбэков — это ад. Обещания же выглядят гораздо приятнее. Кроме того, значение обещания после его разрешения неизменно.
Вот практический пример использования Promises:
var request = require('request');
return new Promise((resolve, reject) => {
request.get(url, (error, response, body) => {
if (body) {
resolve(JSON.parse(body));
} else {
resolve({});
}
});
});
Мы также можем распараллеливать обещания для обработки массива асинхронных операций, используя Promise.all()
:
let urls = [
'/api/commits',
'/api/issues/opened',
'/api/issues/assigned',
'/api/issues/completed',
'/api/issues/comments',
'/api/pullrequests'
];
let promises = urls.map((url) => {
return new Promise((resolve, reject) => {
$.ajax({ url: url })
.done((data) => {
resolve(data);
});
});
});
Promise.all(promises)
.then((results) => {
// Do something with results of all our promises
});
Генераторы
Подобно обещаниям, позволяющим нам избежать ада коллбэков, генераторы позволяют сгладить код, придавая асинхронному коду синхронный облик. Генераторы — это функции, которые могут приостановить своё выполнение и впоследствии вернуть значение выражения.
Простой пример использования приведён ниже:
function* sillyGenerator() {
yield 1;
yield 2;
yield 3;
yield 4;
}
var generator = sillyGenerator();
> console.log(generator.next()); // { value: 1, done: false }
> console.log(generator.next()); // { value: 2, done: false }
> console.log(generator.next()); // { value: 3, done: false }
> console.log(generator.next()); // { value: 4, done: false }
next позволяет передать генератор дальше и вычислить новое выражение. Пример выше предельно прост, но на самом деле генераторы можно использовать для написания асинхронного кода в синхронном виде:
// Hiding asynchronousity with Generators
function request(url) {
getJSON(url, function(response) {
generator.next(response);
});
}
А вот функция-генератор, которая возвращает наши данные:
function* getData() {
var entry1 = yield request('http://some_api/item1');
var data1 = JSON.parse(entry1);
var entry2 = yield request('http://some_api/item2');
var data2 = JSON.parse(entry2);
}
Благодаря силе yield
мы можем быть уверены, что в entry1
будут нужные данные, которые будут переданы в data1
.
Тем не менее, для обработки ошибок придётся что-то придумать. Можно использовать Promises:
function request(url) {
return new Promise((resolve, reject) => {
getJSON(url, resolve);
});
}
И мы пишем функцию, которая будет проходить по генератору, используя next
, который в свою очередь будет использовать метод request
:
function iterateGenerator(gen) {
var generator = gen();
(function iterate(val) {
var ret = generator.next();
if(!ret.done) {
ret.value.then(iterate);
}
})();
}
Дополняя обещанием наш генератор, мы получаем понятный способ передачи ошибок путём .catch
и reject
. При этом использовать генератор всё так же просто:
iterateGenerator(function* getData() {
var entry1 = yield request('http://some_api/item1');
var data1 = JSON.parse(entry1);
var entry2 = yield request('http://some_api/item2');
var data2 = JSON.parse(entry2);
});
Async Await
Хотя эта функция появится только в ES2016, async await
позволяет нам делать то же самое, что и в предыдущем примере, но с меньшими усилиями:
var request = require('request');
function getJSON(url) {
return new Promise(function(resolve, reject) {
request(url, function(error, response, body) {
resolve(body);
});
});
}
async function main() {
var data = await getJSON();
console.log(data); // NOT undefined!
}
main();
По сути, это работает так же, как и генераторы, но использовать лучше именно эту функцию.
Геттеры и сеттеры
ES6 привнесла поддержку геттеров и сеттеров. Вот пример:
class Employee {
constructor(name) {
this._name = name;
}
get name() {
if(this._name) {
return 'Mr. ' + this._name.toUpperCase();
} else {
return undefined;
}
}
set name(newName) {
if (newName == this._name) {
console.log('I already have this name.');
} else if (newName) {
this._name = newName;
} else {
return false;
}
}
}
var emp = new Employee("James Bond");
// uses the get method in the background
if (emp.name) {
console.log(emp.name); // Mr. JAMES BOND
}
// uses the setter in the background
emp.name = "Bond 007";
console.log(emp.name); // Mr. BOND 007
Свежие браузеры также позволяют использовать геттеры / сеттеры в объектах, и тогда их можно использовать для вычисленных свойств, добавляя слушатели перед ними:
var person = {
firstName: 'James',
lastName: 'Bond',
get fullName() {
console.log('Getting FullName');
return this.firstName + ' ' + this.lastName;
},
set fullName (name) {
console.log('Setting FullName');
var words = name.toString().split(' ');
this.firstName = words[0] || '';
this.lastName = words[1] || '';
}
}
person.fullName; // James Bond
person.fullName = 'Bond 007';
person.fullName; // Bond 007
Символы
Символы существовали и до ES6, но теперь стал доступен публичный интерфейс для их прямого использования. Символы неизменяемы и уникальны, и их можно использовать в качестве ключей любого хеша.
Symbol( )
Вызов Symbol()
или Symbol(description)
создаст уникальный символ, недоступный глобально. Symbol()
обычно используется для добавления своей логики в сторонние объекты или пространства имён, но нужно быть осторожным с дальнейшими обновлениями этих библиотек. Например, если вы хотите добавить метод refreshComponent
в класс React.Component
, убедитесь, что он не совпадёт с методом, добавленном в следующем обновлении:
const refreshComponent = Symbol();
React.Component.prototype[refreshComponent] = () => {
// do something
}
Symbol.for(key)
Symbol.for(key)
создаст символ, который по-прежнему будет неизменяемым и уникальным, но доступным глобально. Два идентичных вызова Symbol.for(key)
вернут одну и ту же сущность Symbol.
Примечание: это неверно для
Symbol(description)
:
Symbol('foo') === Symbol('foo') // false
Symbol.for('foo') === Symbol('foo') // false
Symbol.for('foo') === Symbol.for('foo') // true
Символы, в частности, Symbol.for(key)
, обычно используют для интероперабельности, производя поиск символьного поля в аргументах стороннего объекта с известным интерфейсом, например:
function reader(obj) {
const specialRead = Symbol.for('specialRead');
if (obj[specialRead]) {
const reader = obj[specialRead]();
// do something with reader
} else {
throw new TypeError('object cannot be read');
}
}
А в другой библиотеке:
const specialRead = Symbol.for('specialRead');
class SomeReadableType {
[specialRead]() {
const reader = createSomeReaderFrom(this);
return reader;
}
}
В качестве примера использования символов для интероперабельности стоит отметить
Symbol.iterator
, существующий во всех итерируемых типах ES6: массивах, строках, генераторах и т.д. При запуске метода возвращается объект с интерфейсом итератора.
Кстати, познакомиться с примерами использования всех новых возможностей можно, пройдя обучающий видеокурс по JavaScript, о котором мы писали ранее.
По материалам es6-cheatsheet