Что и как в ES6: хитрости, лучшие практики и примеры. Часть вторая. Мэпы, слабые мэпы, обещания, генераторы, async / await, геттеры / сеттеры, символы

Продолжение шпаргалки для повседневного использования по 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