Pull to refresh

Размышления о стандартной библиотеке JavaScript. Core.js

Reading time 64 min
Views 103K
Один пацан писал все на JavaScript, и клиент, и сервер, говорил что нравится, удобно, читабельно. Потом его в дурку забрали, конечно.
— С просторов интернета

К чему это я? Занятная штука — JavaScript. Основа современного web и на фронтэнде альтернатив как таковых не имеет.

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

Ванильная стандартная библиотека JavaScript, в целом, неплоха. Это не только стандартная библиотека по спецификации языка ECMA-262 актуальных версий — от 3 до черновика 6. Часть API вынесена в отдельные спецификации, например, API интернационализации ECMA-402. Многие возможности, без которых сложно представить JavaScript, например, setTimeout, относятся к web-стандартам. Консоль не стандартизована вовсе — приходится полагаться на стандарт де-факто.

Вот только не такая уж она и стандартная — везде разная. Есть старые IE, в которых из коробки мы получаем стандартную библиотеку ES3 90-бородатого года даже без Array#forEach, Function#bind, Object.create и консоли, и есть, например, Node.js, на которой многие уже вовсю используют возможности грядущего ES6.

Хочется иметь универсальную, действительно стандартную библиотеку, как на сервере, так и в любом браузере, максимально соответствующую современным стандартам, а также реализующую необходимый функционал, что (пока?) не стандартизован. Статья посвящена библиотеке core.js — реализация моих соображений по поводу стандартной библиотеки JavaScript. Кроме того, эта статья еще и шпаргалка по современной стандартизованной стандартной библиотеке JavaScript и заметки о её перспективах.

Содержание, или что получим на выходе:



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

# Подходы



Основных подхода к созданию библиотек, что можно назвать стандартными, в JavaScript три:

Первый — использование только полифилов, только стандартизованного функционала. Железобетонная уверенность в том, что со временем API не сломается. Для работы с такой библиотекой её знание не нужно, нужно только знание соответствующего API языка. Обычно, полифилы ограничены одним стандартом или его частью. Например, es5-shim или es6-shim. По этой причине, для обеспечения возможностей, что хотелось бы иметь по умолчанию, приходится подключать несколько полифилов. Их внутренние компоненты часто дублируют друг друга, так что такой набор часто разрастается до сотен килобайт. Да и не все возможности, что хотелось бы иметь, стандартизованы. Возможность конфликтов с другими библиотеками незначительна, но в качестве зависимости при написании библиотеки их использовать я бы не стал.

Второй — набор утилит в собственном пространстве имён. Либо экспорт в модульной системе, либо создание одного глобального объекта. Например, Undescore или её форк LoDash. Обычно, довольно компактен, но возможности ограничиваются набором простых утилит. Так как не расширяет нативные объекты и часто присутствует метод noConflict, откатывающий изменения, возможность конфликтов с другими библиотеками минимальна, лучше других упомянутых здесь способов подходит как безопасная зависимость для других библиотек.

Третий — расширение нативних объектов не только стандартизованным функционалом. Например, добавление собственных методов в прототип массива, что обычно удобнее передачи массива в функцию. Сейчас в этой категории рулит Sugar, в своё время — MooTools и Prototype. Добавляют много полезного функционала, но часто методы почти полностью дублируют друг друга. Здесь бы развернуться полифилам — но из полифилов подобные библиотеки обычно ограничиваются методами прототипа массива, Function#bind и еще несколькими, игнорируя большую часть стандартов. Что же касается конфликтов, то здесь всё совсем плохо. Подобные библиотеки часто расширяют нативные объекты методами с одним именем, но разной сигнатурой. Во избежание конфликтов, при разработке конечного приложения не стоит применять больше одной библиотеки, расширяющей нативные объекты, не считая полифилов, а при написании библиотеки такие зависимости вообще недопустимы.

Вместо одной универсальной стандартной библиотеки, для обеспечения возможностей, которые бы хотелось иметь без лишних заморочек, мы вынуждены тянуть солянку из Undescore / LoDash / Sugar + es5-shim, es6-shim, es6-symbol, setImmediate.js / asap, Moment.js / Intl.js, заглушку консоли… и так далее.

# Попытаемся взять лучшее у каждого из данных подходов. Концепция core.js такова:

  • В стандартной библиотеке должен быть весь необходимый для комфортной работы минимум возможностей, не включающий в себя возможности для работы с API конкретной платформы.
  • Стандарты — наше всё. Основная часть библиотеки — полифилы. Вот только не весь необходимый функционал стандартизован.
  • Если функционал, имеющийся в системе, реализован по спецификации или стандарту де-факто, оставляем нативный, но если функционал не стандартизован — во избежание конфликтов в будущем, замещаем принудительно.
  • Библиотека должна быть компактной и хорошо сжиматься.
  • Модульность, возможность собрать только необходимый функционал.
  • Пишите конечное приложение — вы здесь царь и бог и имеете полное право использовать библиотеку, расширяющую нативные объекты. Главное, что бы это делала только одна библиотека.
  • Пишите библиотеку или npm модуль — использовать библиотеку, расширяющую нативные объекты, нельзя ни в коем случае. Рискуете обречь на конфликты программиста, пишущего конечное приложение. На этот случай есть возможность сборки без их расширения.

# В случае обычной сборки, работа с core.js вполне очевидна:

console.log(Array.from(new Set([1, 2, 3, 2, 1]))); // => [1, 2, 3]
console.log('*'.repeat(10));                       // => '**********'
Promise.resolve(32).then(console.log);             // => 32
setImmediate(console.log, 42);                     // => 42

# В случае сборки без расширения нативных объектов, функционал экспортируется либо в глобальный объект core, либо в модульную систему. Например, конструктор Promise доступен как core.Promise, а метод Array.from как core.Array.from. Методы, предназначенные к добавлению в прототип уже существующих, а не добавляемых библиотекой, конструкторов становятся статическими, например, core.String.repeat это статическая версия метода String.prototype.repeat.

var log  = core.console.log;
log(core.Array.from(new core.Set([1, 2, 3, 2, 1]))); // => [1, 2, 3]
log(core.String.repeat('*', 10));                    // => '**********'
core.Promise.resolve(32).then(log);                  // => 32
core.setImmediate(log, 42);                          // => 42

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

# Установка на Node.js:

npm i core-js

Подключить можно на выбор одну из сборок:

// Максимальная сборка:
require('core-js');
// Сборка без расширения нативных объектов:
var core = require('core-js/library');
// Сборка, содержащая только полифилы:
require('core-js/shim');

# Сборки для браузера:


# Если же вас не устраивает ни одна из этих сборок, можно сделать свою собственную. Например, вам нужны только модуль консоли и простое форматирование даты, притом без расширения нативных объектов. Для этого ставим Node.js, после чего устанавливаем grunt-cli, core-js с необходимыми для сборки зависимостями и собираем:

npm i -g grunt-cli
npm i core-js
cd node_modules/core-js && npm i
grunt build:date,console,library --path=custom uglify

В итоге, получим файлы custom.js, custom.min.js весом 4.8кб и custom.min.map. Флаг library указывает на сборку без расширения нативных объектов. Посмотреть, к какому модулю относится необходимый функционал, можно здесь (последний столбец).

# Часть первая: Костыли


Если кто не понял, под костылями, в контексте статьи, подразумеваются полифилы стандартизованного функционала, имеющиеся в библиотеке. Итак, поехали:

# ECMAScript 5



Пожалуй, все знают, что добавляет ECMAScript 5 в стандартную библиотеку. Вымерли почти все браузеры, не поддерживающие ES5. За исключением старых IE. До сих пор, заказчики часто просят поддержку IE8, а в самых упоротых случаях даже IE6. Надеюсь, в ближайшее время ситуация изменится. Самым популярным полифилом ES5 является этот es5-shim, часть возможностей присутствует в Sugar, MooTools, Prototype, но только часть. Так как это далеко не новинка, обойдемся без лишних подробностей — краткое описание и, если нужно, некоторые особенности реализации. Само собой, важно помнить, что если код пишется с поддержкой IE8-, ни о какой работе с дескрипторами не может быть и речи.

# Методы массива


# Начнем с методов прототипа массива. Это всем известные:

Array#indexOf возвращает индекс первого элемента, равного указанному значению, или -1, если значение не найдено.
Array#lastIndexOf аналогичен предыдущему, но возвращает индекс последнего элемента.
Array#forEach вызывает функцию для каждого элемента массива.
Array#map возвращает новый массив с результатом вызова функции для каждого элемента данного массива.
Array#filter возвращает новый массив со всеми элементами этого массива, удовлетворяющими условию проверяющей функции.
Array#every проверяет, каждый ли элемент в массиве удовлетворяет условию проверяющей функции.
Array#some проверяет, есть ли хотя бы один элемент массива, удовлетворяющий условию проверяющей функции.
Array#reduce выполняет свертку массива с применением функции, слева — направо.
Array#reduceRight выполняет свертку массива с применением функции, справа — налево.

[1, 2, 3, 2, 1].indexOf(2);     // => 1
[1, 2, 3, 2, 1].lastIndexOf(2); // => 3
[1, 2, 3].forEach(function(val, key){
  console.log(val);             // => 1, 2, 3
  console.log(key);             // => 0, 1, 2
});
[1, 2, 3].map(function(it){
  return it * it;
});                             // => [1, 4, 9]
[1, 2, 3].filter(function(it){
  return it % 2;
});                             // => [1, 3]
function isNum(it){
  return typeof it == 'number';
}
[1, '2', 3].every(isNum);       // => false
[1, '2', 3].some(isNum);        // => true
function add(a, b){
  return a + b;
}
[1, 2, 3].reduce(add);          // => 6
[1, 2, 3].reduceRight(add, ''); // => '321'

Данные методы реализуются элементарно, но есть одна особенность. Методы массива — дженерики и могут быть вызваны в контексте не только массива, но и любого array-like объекта, подробнее об этом # будет ниже. Так вот, по спецификации ES5 строки являются array-like объектами, букву строки можно получить по индексу, например, 'string'[2] // => 'r', а в старых IE таковыми они не является. В случае применения данных методов в контексте строк, приводим строки к массиву. Для решения этой же проблемы, при необходимости подменяем в старых IE Array#slice и Array#join.

Array.prototype.map.call('123', function(it){
  return it * it;
});                                    // => [1, 4, 9]
Array.prototype.slice.call('qwe', 1);  // => ['w', 'e']
Array.prototype.join.call('qwe', '|'); // => 'q|w|e'

Ну и не забывайте древнюю истину: никогда не обходите массив циклом for-in. Это не только медленно, но и вынуждает, если нужна поддержка IE8-, проверять, является ли ключ собственным — иначе выполните обход не только элементов массива, но и методов его прототипа :)

# К этой же категории относится статический метод Array.isArray. Метод проверяет, является ли объект массивом не по цепочке прототипов, а по внутреннему классу. Полезно, но не универсально. О классификации объектов подробно мы поговорим # во второй, велосипедной, части статьи.

Array.isArray([1, 2, 3]);                      // => true
Array.isArray(Object.create(Array.prototype)); // => false

# Объектное API


Полная эмуляция всех методов объектного API ECMAScript 5 на базе ECMAScript 3 невозможна, частичная — возможна для многих. ES5 добавляет в объектное API следующие категории методов: работа с прототипом (создание из / получение), получение ключей объекта, работа с дескрипторами.

# Метод Object.create создает объект из прототипа. Передав null, можно создать объект без прототипа, что сделать на базе ECMAScript 3 невозможно. Приходится использовать лютый трэш на базе iframe. Зачем это нам будет раскрыто # во второй части. Опционально принимает объект # дескрипторов, аналогично Object.defineProperties.

function Parent(/*...*/){ /*...*/ }
Parent.prototype = {constructor: Parent /*, ... */}
function Child(/*...*/){
  Parent.call(this /*, ...*/);
  // ...
}
// Было в ES3 (нутрянка всяких inherit и extend'ов):
function Tmp(){}
Tmp.prototype = Parent.prototype;
Child.prototype = new Tmp;
Child.prototype.constructor = Child;
// Стало с ES5:
Child.prototype = Object.create(Parent.prototype, {constructor: {value: Child}});

var dict = Object.create(null);
dict.key = 42;
console.log(dict instanceof Object); // => false
console.log(dict.toString)           // => undefined
console.log(dict.key)                // => 42

# Метод Object.getPrototypeOf возвращает прототип объекта. В ECMAScript 3 нет гарантированного способа получения прототипа объекта. Если объект содержит свойство constructor, возможно прототипом будет constructor.prototype. Для объектов, созданных через Object.create, добавим # символ, содержащий прототип, и будем игнорировать его при # получении ключей. Но на инстансе конструктора, прототип которого был переопределен без указания «правильного» свойства constructor, Object.getPrototypeOf будет работать некорректно.

var parent = {foo: 'bar'}
  , child  = Object.create(parent);
console.log(Object.getPrototypeOf(child) === parent);      // => true

function F(){}
console.log(Object.getPrototypeOf(new F) === F.prototype); // => true

F.prototype = {constructor: F /*, ...*/};
console.log(Object.getPrototypeOf(new F) === F.prototype); // => true

F.prototype = {};
console.log(Object.getPrototypeOf(new F) === F.prototype); // В IE8- будет работать некорректно

# Метод Object.keys возвращает массив собственных перечисляемых ключей объекта. Object.getOwnPropertyNames возвращает массив собственных ключей объекта, в т.ч. и неперечисляемых. С Object.keys, вроде, всё просто — перебираем объект через for-in и проверяем, являются ли свойства собственными. Если бы не баг с «неперечисляемыми перечисляемыми» свойствами в IE. Так что приходится проверять наличие таковых свойств отдельно. Аналогично, с дополнительной проверкой по списку имеющихся скрытых свойств, работает и Object.getOwnPropertyNames.

console.log(Object.keys({q: 1, w: 2, e: 3}));       // => ['q', 'w', 'e']
console.log(Object.keys([1, 2, 3]));                // => ['0', '1', '2']
console.log(Object.getOwnPropertyNames([1, 2, 3])); // => ['0', '1', '2', 'length']

# С дескрипторами всё плохо, ECMAScript 3 их не поддерживает. Задать геттеры / сеттеры нет возможности. Браузеры, где есть Object#__define[GS]etter__, но отсутствует Object.defineProperty, давно вымерли. В старых IE есть возможность создать объект с геттерами / сеттерами через извращения с VBScript, но это отдельная тема. enumerable: false свойства есть, их не задать, но есть возможность проверить является ли оно таковым, через Object#propertyIsEnumerable. В IE8 есть методы для работы с дескрипторами, но лучше бы их не было (работают только с DOM объектами). Итого, всё, что мы можем сделать для IE8- — заглушки. Установка значения свойства по value дескриптора в Object.defineProperty и Object.defineProperties да честное получение value и enumerable в Object.getOwnPropertyDescriptor.

# А что насчет Object.freeze, Object.preventExtensions, Object.seal? Мало того, что их эмуляция невозможна, можно сделать разве что заглушки, но есть и такая точка зрения:
Object.freeze, Object.preventExtensions, Object.seal, with, eval
Crazy shit that you will probably never need. Stay away from it.
— Felix Geisendörfer
И я с ней полностью согласен, так что обойдемся без них.

# Прочее


# В ECMAScript 5 добавлены привязка контекста и базовые возможности частичного применения через Function#bind. Метод замечательный, но для раскрытия потенциала частичного применения и привязки контекста в JavaScript одного его мало, подробно тема раскрывается в # соответствующем разделе.

var fn = console.log.bind(console, 42);
fn(43); // => 42 43

# Метод Date.now возвращает текущее время в числовом представлении, результат выполнения аналогичен +new Date.

Date.now(); // => 1400263401642

# Метод String#trim удаляет пробельные символы из начала и конца строки.

'\n   строка   \n'.trim(); // => 'строка'

Что же касается модуля JSON, то он поддерживается IE8 и, в рамках данной библиотеки, реализовывать его я не вижу смысла. Если он вам понадобится в совсем уж доисторических IE — никто не мешает использовать, например, этот полифил.

# ECMAScript 6



Спецификация ECMAScript 5 была написана наспех вместо так и не принятой спецификации ECMAScript 4 и мало расширяла принятую ещё в прошлом тысячелетии ECMAScript 3. Сейчас уже почти завершена куда более серьёзно расширяющая язык, в т.ч. и стандартную библиотеку, спецификация ECMAScript 6. Добавление новых фич в неё заморожено, все серьёзные изменения идут в предложения по ECMAScript 7, в последнее время большинство изменений черновика спецификации это исправления ошибок. Так что в нашей стандартной библиотеке будем ориентироваться, в основном, на ES6.

Что с её поддержкой в актуальных движках хорошо видно по данной таблице.

  • Лучше всех с её поддержкой у Firefox — уже доступно очень многое.
  • # В v8 (Chrome, Opera, Node.js) тоже доступно довольно многое, но значительная часть возможностей по умолчанию заблокирована, для их активации в браузере необходимо поставить флажок «Включить экспериментальный JavaScript» (парсер съедает ссылку chrome://flags/#enable-javascript-harmony), а Node.js запустить с флажком --harmony. Что-то доступно и без флажка, например, Promise, WeakMap и WeakSet, а начиная с Chrome 38 доступны и Symbol, Map, Set, итераторы. Node.js в этом плане сильно отстаёт, так как, особенно в стабильной ветке, v8 там обновляется редко. Зато её, в отличии от браузера пользователя, вам никто не помешает запустить с флажком.
  • У IE, как обычно, всё плохо, но в 11ой версии были добавлены коллекции и еще пара возможностей. В ближайшем будущем обещают очень многое.
  • Добавить функционал ES6 попытались и в Safari. Вот только почти всё, что они добавили, не соответствует стандарту и порождает лишние проблемы, так что лучше бы и не добавляли. По ссылке только небольшая часть проблем.

Самым популярным полифилом ES6 является es6-shim от paulmillr.

Часть стандартной библиотеки ECMAScript 6, например, Proxy (а это одна из самых вкусных возможностей), на базе ECMAScript 5 и уж тем более ECMAScript 3 реализовать невозможно, но большую часть можно реализовать если не полностью, то хотя бы частично и «нечестно».

# Немного про препроцессоры ECMAScript 6+


Что касается синтаксиса, то, в рамках этой библиотеки, его поддержку мы добавить не можем. Однако на помощь тут приходят препроцессоры, преобразующие синтаксис ECMAScript 6+ в ECMAScript 3 или 5. Их огромное количество, рассмотрим только пару популярных.

Есть такой старый и мощный проект — Google Traceur. Он генерирует нечитаемый код и использует довольно тяжелый runtime, так что от его использования я отказался.

Другой проект мне кажется куда более привлекательным — 6to5. Проект свежий и развивается потрясающе быстро. Он генерирует легко читаемый код и не использует собственный runtime, исключение — runtime regenerator, который он использует для компиляции генераторов. Зато вместо него активно использует стандартную библиотеку ES6 — например, # Symbol.iterator. По умолчанию — es6-shim и es6-symbol. Их легко заменяет наша библиотека, что делает данный препроцессор идеальной её парой. Преобразует код в ECMAScript 5, но, главным образом, это касается стандартной библиотеки — с заглушками методов вроде # Object.defineProperties почти всё будет работать и в старых IE.

С использованием препроцессора и полифила стандартной библиотеки, начинать использовать ECMAScript 6 на полную катушку можно уже сейчас. За исключением, разве что, некоторых мелочей.

Набросал совсем простенькую песочницу с возможностью компиляции ES6 через 6to5 и подключенной библиотекой core.js, примеры будут со ссылками на неё. Однако, так как наша библиотека никак не привязана к синтаксису ECMAScript 6, большая часть примеров будет с использованием синтаксиса ECMAScript 5.



Ну а теперь перейдём к стандартной библиотеке по ECMAScript 6. Новые конструкторы и концепции мы рассмотрим в отдельных главах, это # символы, # коллекции, # итераторы и # обещания, остальное рассмотрим здесь.

# Object.assign


Этого метода ждали многие. Object.assign банально копирует все собственные перечисляемые свойства объекта (объектов) источника в целевой объект. Пример:

var foo = {q: 1, w: 2}
  , bar = {e: 3, r: 4}
  , baz = {t: 5, y: 6};
Object.assign(foo, bar, baz); // => foo = {q: 1, w: 2, e: 3, r: 4, t: 5, y: 6}

Планировалось также добавить метод Object.mixin, который копировал еще и неперечисляемые свойства, учитывал дескрипторы и переназначал родителя, получаемого через ключевое слово super. Однако, его добавление решили отложить. Его аналог есть в # велосипедной части библиотеки.

# Object.is


Операторы сравнения в JavaScript вообще довольно странно себя ведут. Забудем даже такой оператор, как == с его приведениями, посмотрим на ===:

NaN === NaN  // => false
0 === -0     // => true, но при этом:
1/0 === 1/-0 // => false

Как раз для этого случая, в языке есть внутренний алгоритм сравнения SameValue. Для него NaN равен NaN, а +0 и -0 различны. В ECMAScript 6 его хотели вынести наружу как операторы is и isnt, но, похоже, поняв, что операторов сравнения в языке уже и так не мало, да и для обратной совместимости, вынесли как метод Object.is. Пример:

Object.is(NaN, NaN); // => true
Object.is(0, -0);    // => false
Object.is(42, 42);   // => true, аналогично '==='
Object.is(42, '42'); // => false, аналогично '==='

# Также в ES6 и далее активно используется другой алгоритм сравнения — SameValueZero, для которого NaN равен NaN, и, в отличии от предыдущего, -0 равен +0. Им обеспечивается уникальность ключа # коллекций, он применяется при проверке вхождения элемента в коллекцию через # Array#includes.

# Object.setPrototypeOf


В ES6 для установки прототипа существующего объекта появляется метод Object.setPrototypeOf. Пример:

function Parent(){}
function Child(){}
Object.setPrototypeOf(Child.prototype, Parent.prototype);
new Child instanceof Child;  // => true
new Child instanceof Parent; // => true

function fn(){}
Object.setPrototypeOf(fn, []);
typeof fn == 'function';     // => true 
fn instanceof Array;         // => true

var object = {};
Object.setPrototypeOf(object, null);
object instanceof Object;    // => false

Странно, что такая, казалось бы, необходимая, учитывая прототипную ориентированность языка, возможность — посмотрите, хотя бы, первый пример — простое и очевидное прототипное наследование, отсутствует в ECMAScript 5. Единственный способ изменить прототип уже существующего объекта без данного метода — нестандартное свойство __proto__. На текущий момент, оно поддерживается всеми актуальными браузерами, кроме IE10-, в текущей реализации — геттер / сеттер в прототипе Object.

Примитивная, без лишних проверок и возврата объекта, версия Object.setPrototypeOf выглядела бы просто — выдернем сеттер __proto__ и сделаем из него функцию:

var setPrototypeOf = Function.call.bind(Object.getOwnPropertyDescriptor(Object.prototype, '__proto__').set);

Однако, тут появляется еще одна проблема — в старых, но где-то еще актуальных, версиях v8, сеттер __proto__ нельзя использовать как функцию. Для реализации Object.setPrototypeOf в них остается только установка значения по ключу __proto__.

Так и живем:

  • Для IE10- эмуляция Object.setPrototypeOf отсутствует полностью, так как невозможна.
  • В старых версиях v8 Object.setPrototypeOf не работает, если в цепочке прототипов цели отсутствует Object.prototype или свойство __proto__ переопределено через, например, Object.defineProperty.

Также, ECMAScript 6 и наша библиотека изменяет логику работы Object#toString. Тема серьёзная, но о ней мы поговорим # во второй части статьи.

# Методы массива


Статические методы Array.from и Array.of — дженерики, если они запущены в контексте функции, отличной от Array, они создают её инстансы. Если есть желание ознакомиться с этим подробней — хорошо описано в этой статье по новым методам массива.

# ECMAScript 6 добавляет очень полезный метод — Array.from. Это универсальное приведение к массиву # итерируемых и array-like объектов. Во большинстве случаев он заменит Array.prototype.slice.call без указания начальной и конечной позиций. Дополнительно, метод принимает опциональный map-коллбэк и контекст его исполнения. В случае передачи итерируемого объекта и без map-коллбэка, результат аналогичен использованию оператора # spread в литерале массива — [...iterable]. Пример:

Array.from(new Set([1, 2, 3, 2, 1]));      // => [1, 2, 3]
Array.from({0: 1, 1: 2, 2: 3, length: 3}); // => [1, 2, 3]
Array.from('123', Number);                 // => [1, 2, 3]
Array.from('123', function(it){
  return it * it;
});                                        // => [1, 4, 9]

# В отличие от предыдущего, метод Array.of на текущий момент практически бесполезен. Он нужен, в первую очередь, для подклассов Array, как аналог литерала массива []. Пример:

Array.of(1);       // => [1]
Array.of(1, 2, 3); // => [1, 2, 3]

# Методы Array#find и Array#findIndex осуществляют поиск по массиву через вызов коллбэка. Пример:

function isOdd(val){
  return val % 2;
}
[4, 8, 15, 16, 23, 42].find(isOdd);      // => 15
[4, 8, 15, 16, 23, 42].findIndex(isOdd); // => 2
[4, 8, 15, 16, 23, 42].find(isNaN);      // => undefined
[4, 8, 15, 16, 23, 42].findIndex(isNaN); // => -1

# Метод массива Array#fill заполняет массив переданным значением. Опциональные аргументы — стартовая и конечная позиции. Пример:

Array(5).map(function(){
  return 42;
});                // => [undefined × 5], потому как .map пропускает "дырки" в массиве
Array(5).fill(42); // => [42, 42, 42, 42, 42]

# Методы строки


Тут всё просто. String#includes (до недавнего времени — String#contains, но # Array#includes утянул его за собой, пока доступен и по старому имени) проверяет вхождение подстроки в строку. String#startsWith и String#endsWith проверяют, начинается ли или заканчивается ли строка на заданную подстроку. Эти 3 метода принимают дополнительный аргумент — стартовую позицию. Пример:

'foobarbaz'.includes('bar');      // => true
'foobarbaz'.includes('bar', 4);   // => false
'foobarbaz'.startsWith('foo');    // => true
'foobarbaz'.startsWith('bar', 3); // => true
'foobarbaz'.endsWith('baz');      // => true
'foobarbaz'.endsWith('bar', 6);   // => true

Метод String#repeat возвращает строку, повторенную заданное число раз. Пример:

'string'.repeat(3); // => 'stringstringstring'

Библиотека пока не добавляет методы ECMAScript 6 / 7 для лучшей поддержки многобайтовых символов и честный # итератор строки, для строк используется итератор массива. Сейчас их нет только потому, что они лично мне просто не нужны. Было бы неплохо добавить их в ближайшем будущем.

# Работа с числами


В ECMAScript 6 добавлено огромное количество математических функций и констант. Обойдемся без их описания и примеров, только ссылки: Number.EPSILON, Number.parseFloat, Number.parseInt, Number.isFinite, Number.isInteger, Number.isNaN, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, Number.isSafeInteger, Math.acosh, Math.asinh, Math.atanh, Math.cbrt, Math.clz32, Math.cosh, Math.expm1, Math.hypot, Math.imul, Math.log1p, Math.log10, Math.log2, Math.sign, Math.sinh, Math.tanh, Math.trunc.

# ECMAScript 6: Символы



В JavaScript с сокрытием свойств объектов дела обстоят довольно плохо. Приватные данные можно хранить в замыканиях, что вынуждает объявлять методы для работы с ними внутри конструктора, а не в прототипе объекта. Начиная с ECMAScript 5, можно объявлять enumerable: false свойства, что скроет свойства объекта от перечисления в for-in и от Object.keys, но это не обеспечит надежного сокрытия — ключ не уникален, его можно легко подобрать, могут возникнуть конфликты имен и, из-за необходимости использования Object.defineProperty с объектом-дескриптором, это довольно громоздко.

В ECMAScript 6 для упрощения инкапсуляции появляется новый тип данных — Symbol, ранее известный как Name. Символы предназначены для использования в качестве уникальных ключей объектов. Пример:

var Person = (function(){
  var NAME = Symbol('name');
  function Person(name){
    this[NAME] = name;
  }
  Person.prototype.getName = function(){
    return this[NAME];
  };
  return Person;
})();

var person = new Person('Вася');
console.log(person.getName());          // => 'Вася'
console.log(person['name']);            // => undefined
console.log(person[Symbol('name')]);    // => undefined, каждый вызов Symbol возвращает уникальный ключ
for(var key in person)console.log(key); // => только 'getName', символы не участвуют в обходе объекта

console.log(typeof Symbol());           // => 'symbol'

Символы не являются полностью приватными — метод Object.getOwnPropertySymbols возвращает собственные символы объекта, что даёт возможность отладки и низкоуровневых операций вроде клонирования. Хранение по настоящему приватных данных можно реализовать на базе # WeakMap. Хотя, ИМХО, более удачным решением проблемы было бы добавление полностью приватной версии символов.

На текущий момент символы доступны в v8, начиная с Chrome 38 (в более ранних версиях — # с флажком экспериментальных возможностей) и в ночных сборках Firefox, начиная с 33. Скоро обещают и в IE, так что в самом ближайшем будущем будет доступен во всех основных современных браузерах.

# Конечно, полноценный полифил символов на базе ES5 невозможен, но базовые возможности — создание уникальных не участвующих в обходе объекта через for-in и не возвращаемых Object.keys ключей — реализуется довольно просто, например, так:

window.Symbol || (function(){
  var id = 0;
  window.Symbol = function(description){
    if(this instanceof Symbol)throw new TypeError('Symbol is not a constructor');
    var symbol = Object.create(Symbol.prototype)
      , tag    = 'Symbol(' + description + ')_' + (++id + Math.random()).toString(36);
    symbol.tag = tag;
    Object.defineProperty(Object.prototype, tag, {
      configurable: true,
      set: function(it){
        Object.defineProperty(this, tag, {
          enumerable  : false,
          configurable: true,
          writable    : true,
          value       : it
        });
      }
    });
    return symbol;
  }
  Symbol.prototype.toString = function(){
    return this.tag;
  }
})();

При вызове Symbol мы генерируем уникальный ключ-строку, например, "Symbol(description)_m.y9oth2pcqaypsyvi", и по этому ключу в Object.prototype устанавливаем сеттер. При попытке установить значение по строке-ключу, к которой приведется наш «символ», сеттер устанавливает enumerable: false свойство в текущий объект. Однако, у подобных «символов» есть огромное количество минусов, вот только часть:

  • Пишет сеттер в Object.prototype: не стоит особо злоупотреблять, может повлиять на производительность.
  • Работает на базе дескрипторов: в IE8- работает (за счет # заглушки Object.defineProperty), не будет сеттеров в Object.prototype, «символы» будут перечисляемы.
  • В объектах, не содержащих под собой Object.prototype (например, Object.create(null)), «символы» будут перечисляемы.
  • Symbol() in {} вернет true — нужно проверять наличие символов в объекте иным способом.
  • typeof Symbol() будет равен 'object' — средствами JavaScript мы не можем добавить новый тип данных.
  • Не добавляем Object.getOwnPropertySymbols — лишняя обертка для Object.getOwnPropertyNames плохо скажется на производительности, соответственно, Object.getOwnPropertyNames будет возвращать, в том числе, и «символы».

# Если решение настолько сомнительно, что оно тут делает? Если в проекте нужно незначительное количество символов и не планируется их использовать с объектами, не содержащими под собой Object.prototype, данное решение вполне сойдет. Для остальных случаев добавим в библиотеку пару хелперов. Symbol.pure, если доступны нативные символы, возвращающий символ, нет — возвращающий уникальную строку-ключ без добавления сеттера в Object.prototype, и Symbol.set, если доступны нативные символы — просто устанавливающий в объект значение по ключу, нет — устанавливающий значение, используя Object.defineProperty с enumerable: false. Таким примитивным образом, избавляемся от половины описанных выше проблем. Использованный выше пример с использованием данных хелперов вместо вызова Symbol выглядит так:

var Person = function(){
  var NAME = Symbol.pure('name');
  function Person(name){
    Symbol.set(this, NAME, name);
  }
  Person.prototype.getName = function(){
    return this[NAME];
  };
  return Person;
}();

# Как уже было отмечено выше, метод Object.getOwnPropertySymbols мы не добавляем. А хочется иметь более или менее универсальный, притом стандартизованный, способ для обхода всех ключей, как строк, так и символов. ECMAScript 6 добавляет модуль Reflect — в первую очередь, набор заглушек для Proxy. Так как мы не имеем возможность эмулировать Proxy, модуль Reflect нам особо и не нужен. Однако в нём есть метод Reflect.ownKeys, возвращающий все собственные ключи объекта — как строки, так и символы, т.е. Object.getOwnPropertyNames + Object.getOwnPropertySymbols. Добавим этот метод. Пример:

var O = {a: 1};
Object.defineProperty(O, 'b', {value: 2});
O[Symbol('c')] = 3;
Reflect.ownKeys(O); // => ['a', 'b', Symbol(c)]

# Также ES6 добавляет такую сомнительную штуку, как глобальный регистр символов. Для работы с ним есть пара методов — Symbol.for и Symbol.keyFor. Symbol.for ищет в регистре и возвращает символ по ключу-строке, не находит — создаёт новый, добавляет в регистр и возвращает его. Symbol.keyFor возвращает строку, которой в регистре соответствует переданный символ. Пример:

var symbol = Symbol.for('key');
symbol === Symbol.for('key'); // true
Symbol.keyFor(symbol);        // 'key'

Ко всему прочему, библиотека активно использует символы # Symbol.iterator и # Symbol.toStringTag.

# ECMAScript 6: Коллекции



В ECMAScript 6 появляются 4 новых вида коллекций: Map, Set, WeakMap и WeakSet. Есть еще типизированные массивы, но пока обойдёмся без них.


# Итак, что собой представляют данные коллекции?

# Map — коллекция ключ — значение, в качестве ключей могут выступать любые сущности JavaScript — как примитивы, так и объекты. Есть возможность обхода — имеют # итераторы и метод .forEach, количество элементов доступно через свойство .size. Пример:

var a = [1];

var map = new Map([['a', 1], [42, 2]]);
map.set(a, 3).set(true, 4);

console.log(map.size);        // => 4
console.log(map.has(a));      // => true
console.log(map.has([1]));    // => false
console.log(map.get(a));      // => 3
map.forEach(function(val, key){
  console.log(val);           // => 1, 2, 3, 4
  console.log(key);           // => 'a', 42, [1], true
});
map.delete(a);
console.log(map.size);        // => 3
console.log(map.get(a));      // => undefined
console.log(Array.from(map)); // => [['a', 1], [42, 2], [true, 4]]

# Set — коллекция уникальных значений. Как и у Map, есть возможность обхода. Пример:

var set = new Set(['a', 'b', 'a', 'c']);
set.add('d').add('b').add('e');
console.log(set.size);        // => 5
console.log(set.has('b'));    // => true
set.forEach(function(it){
  console.log(it);            // => 'a', 'b', 'c', 'd', 'e'
});
set.delete('b');
console.log(set.size);        // => 4
console.log(set.has('b'));    // => false
console.log(Array.from(set)); // => ['a', 'c', 'd', 'e']

# WeakMap — коллекция ключ-значение, в качестве ключей могут выступать только объекты. Использует слабую связь — когда объект-ключ удаляется (сборщиком мусора), удаляется и пара ключ-значение из коллекции. Нет возможности обойти — нет итераторов и метода .forEach, нет свойства .size. Это еще один способ хранения приватных данных, более «честный», но и более ресурсоёмкий, по сравнению с использованием # символов. Если в будущем в JavaScript таки добавят abstract references, для подобных приватных полей появится удобный синтаксис. Пример:

var a = [1]
  , b = [2]
  , c = [3];

var wmap = new WeakMap([[a, 1], [b, 2]]);
wmap.set(c, 3).set(b, 4);
console.log(wmap.has(a));   // => true
console.log(wmap.has([1])); // => false
console.log(wmap.get(a));   // => 1
wmap.delete(a);
console.log(wmap.get(a));   // => undefined

// Так можно хранить приватные данные
var Person = (function(){
  var names = new WeakMap;
  function Person(name){
    names.set(this, name);
  }
  Person.prototype.getName = function(){
    return names.get(this);
  };
  return Person;
})();

var person = new Person('Вася');
console.log(person.getName());          // => 'Вася'
for(var key in person)console.log(key); // => только 'getName'

# WeakSet — ну вы поняли. Появился в черновике спецификации относительно недавно, так что имеет довольно слабую поддержку браузерами. Пример:

var a = [1]
  , b = [2]
  , c = [3];

var wset = new WeakSet([a, b, a]);
wset.add(c).add(b).add(c);
console.log(wset.has(b));   // => true
console.log(wset.has([2])); // => false
wset.delete(b);
console.log(wset.has(b));   // => false

Все эти коллекции должны обеспечивать сублинейное время поиска. Уникальность ключа обеспечивается алгоритмом сравнения # SameValueZero.

# Что с поддержкой этих коллекций у современных движков js? Очень даже неплохо.

  • В Firefox есть полноценные Map, Set и WeakMap. В ночных сборках появился и WeakSet. Map и Set принимают итерируемый объект. Map и Set имеют итераторы и метод .forEach.
  • В v8 — читай Chrome, Opera и Node.js, есть все 4 новых вида коллекций. Начиная с Chrome 38, все они доступны без каких либо манипуляций. Чуть раньше были открыты WeakMap и WeakSet. В более ранних версиях коллекции доступны # с флажком экспериментальных возможностей. До самых последних версий v8 конструкторы не принимали итерируемый объект, а у Map и Set отсутствовали итераторы и метод .forEach, что делало нативные Map и Set чуть менее, чем полностью бесполезными.
  • В IE11 появились те же Map, Set и WeakMap. Конструкторы не принимают итерируемый объект. Map и Set не имеют итераторов, но у них есть метод .forEach.
  • В Safari всё просто замечательно, лучше бы вообще никак. Есть Map, Set и WeakMap. Инициализация итератором отсутствует. Вроде бы, есть методы, возвращающие итераторы, но, внезапно, у некоторых итераторов отсутствует метод next. Есть forEach, но метод передаёт в коллбэк не 3 аргумента, как должно быть, а только 1 для Set и 2 для Map.

Почти во всех текущих реализациях методы коллекций .add и .set не возвращают this — заполнить коллекцию цепочкой этих методов не получится. Но это легко лечится.

Для инициализации коллекции итератором также достаточно обертки для конструктора, которая создаст коллекцию и добавит элементы. Про сами итераторы поговорим в # следующей главе.

Ну а полифилы самих коллекций рассмотрим дальше. Полноценная реализация данных коллекций — быстрых, при этом чистых и без утечек памяти (для WeakMap), на базе ECMAScript 5 невозможна, однако, можно найти разумный компромисс.

# Реализация Map и Set


Что в большинстве полифилов представляют собой реализации Map? В инстансе Map — 2 массива, ключи и значения. При получении элемента, ищем совпадение в массиве ключей и возвращаем элемент из массива значений по полученному индексу. Или, для оптимизации удаления, альтернативный вариант — цепочка объектов-вхождений. Что не так и в том, и в другом случае? Это очень медленно, сложность поиска элемента — O(n), сложность операции uniq — O(n2). Чем это нам грозит? Вот небольшой тест:

var array = [];
for(var i = 0; i < 100000; i++)array.push([{}, {}]);
array = array.concat(array);
console.time('Map test');
var map = new Map(array);
console.timeEnd('Map test');
console.log('Map size: ' + map.size);

Создаём массив из 200000 тысяч пар объектов (будущие ключ-значение), 100000 из которых уникальны, после чего создаём из этого массива коллекцию Map.

Испытаем нативный Map, например, в Firefox:

Map test: таймер запущен
Map test: 46.25мс
Map size: 100000

А теперь в нём же Map из самого популярного полифила ECMAScript 6:

Map test: таймер запущен
Map test: 506823.31мс
Map size: 100000

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

Сублинейной скорости полифила можно добиться используя хэш-таблицы. В ECMAScript 5 это только Object, принимающий в качестве ключа исключительно строки. В протестированном выше полифиле есть небольшая оптимизация — поисковый индекс для ключа-строки или числа через простую функцию, уменьшающий среднюю сложность поиска элемента коллекции до O(1):

function fastKey(key){
  if(typeof key === 'string')return '$' + key;
  else if(typeof key === 'number')return key;
  return null;
};

Точно так же можно реализовать быстрый доступ и для остальных примитивов. Вот только зачем нужен Map, в качестве ключей которого можно эффективно использовать только примитивы? Object.create(null) с этим # вполне справляется.

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

var STOREID = Symbol('storeId')
  , id      = 0;
function fastKey(it){
  // Возвращаем с префиксом 'S' если строка и с префиксом 'P' если другой примитив
  if(it !== Object(it))return (typeof it == 'string' ? 'S' : 'P') + it;
  // Если у объекта отсутствует идентификатор - добавляем
  if(!Object.hasOwnProperty.call(it, STOREID))it[STOREID] = ++id;
  // Возвращаем идентификатор с префиксом 'O'
  return 'O' + it[STOREID];
}

Реализуем Map не на 2 массивах или цепочке объектов-элементов, а на 2 хэшах Object, так же для ключей и значений. Другого хранилища ключей / значений для обхода коллекции в порядке добавления нам не нужно: во всех движках ключи объектов хранятся в порядке добавления, за исключением ключей-чисел, но так как здесь у всех ключей префикс-буква, таковые отсутствуют. Итого, получаем:

Map test: таймер запущен
Map test: 669.93мс
Map size: 100000

Конечно, медленнее нативных, но, думаю, вполне сойдет. Да, мы пишем скрытое свойство в ключ-объект — невозможно использовать в качестве ключей # frozen-объекты, но зато получаем приемлемую скорость работы. Set реализуется аналогично, на 1 хэше.

# Реализация слабосвязанных коллекций


Слабосвязанные коллекции реализуем еще проще. У них нет итераторов, метода .forEach, свойства .size. В случае хранения ключей и значений в объекте коллекции, она уже не будет слабосвязанной — ключи / значения не будут удаляться, получим просто урезанную версию Set и Map. Единственное более или менее разумное решение давно известно — хранить значения на ключе, а в объекте коллекции — только её идентификатор. Так как значения хранятся на ключе, в полифиле теряется полная приватность хранимых данных.

Сильно упрощённая их реализация выглядит так:
window.WeakMap || (function(){
  var id       = 0
    , has      = Function.call.bind(Object.prototype.hasOwnProperty)
    , WEAKDATA = Symbol('WeakData')
    , ID       = Symbol('ID');
  window.WeakMap = function(){
    if(!(this instanceof WeakMap))throw TypeError();
    this[ID] = id++;
  }
  Object.assign(WeakMap.prototype, {
    'delete': function(key){
      return this.has(key) && delete key[WEAKDATA][this[ID]];
    },
    has: function(key){
      return key === Object(key) && has(key, WEAKDATA) && has(key[WEAKDATA], this[ID]);
    },
    get: function(key){
      if(key === Object(key) && has(key, WEAKDATA))return key[WEAKDATA][this[ID]];
    },
    set: function(key, value){
      if(key !== Object(key))throw TypeError();
      if(!has(key, WEAKDATA))key[WEAKDATA] = {};
      key[WEAKDATA][this[ID]] = value;
      return this;
    }
  });
})();

Давайте убедимся, что при удалении ссылки на ключ он не остаётся в коллекции и, соответственно, память не утекает:

// <- тут делаем snapshot 1
var array = [];
for(var i = 0; i < 100000; i++)array[i] = {};
var wm = new WeakMap();
for(var i = 0; i < 100000; i++)wm.set(array[i], {});
// <- тут делаем snapshot 2
array = null;
// <- тут делаем snapshot 3



Но в некоторых случаях проблема утечки памяти остаётся. После удаления объекта коллекции, значение останется связанным с ключом, что приведет к утечке памяти до момента удаления объекта, бывшего ключом. Так что по-хорошему систему проектировать стоит так, что бы коллекции WeakMap жили дольше, чем их ключи. Кто-то пытается обойти проблему утечки памяти, но это из разряда эзотерики — память утекает точно в тех же случаях.

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

# ECMAScript 6: Итераторы



В ECMAScript 6 появился протокол итераторов — универсальный способ обхода коллекций, и не только. Так как к нему относятся и синтаксические конструкции, рассмотрим и их. Но, в первую очередь, это не часть стандартной библиотеки или синтаксиса, а концепция. К протоколу итераторов можно отнести:


Так как часть этого — синтаксис, в этой главе также рассмотрим модуль $for, что реализует часть возможностей этих синтаксических конструкций. Если собираетесь использовать библиотеку с # препроцессором ES6+, можете спокойно собирать её без данного модуля.

# Итератор — объект, имеющий метод .next, который должен возвращать объект с полями .done — завершен ли обход итератора, и .value — значение текущего шага. Пример — метод, создающий итератор для положительного числа, позволяющий обойти все целые от 0 до заданного (песочница):

function NumberIterator(number){
  var i = 0;
  return {
    next: function(){
      return i < number
        ? {done: false, value: i++}
        : {done: true};
    }
  }
}

var iter = NumberIterator(3);
iter.next(); // => {done: false, value: 0}
iter.next(); // => {done: false, value: 1}
iter.next(); // => {done: false, value: 2}
iter.next(); // => {done: true}

# Итерируемый объект (Iterable) — объект, у которого по ключу Symbol.iterator содержится метод, возвращающий для него итератор. Соответственно, что бы и итератор был итерируемым, по ключу Symbol.iterator у него должен быть метод, возвращающий this. Для примера, сделаем числа итерируемыми (песочница):

Number.prototype[Symbol.iterator] = function(){
  return NumberIterator(this);
}

Array.from(10); // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Обратите внимание на Symbol.iterator. В Firefox поддерживается протокол итераторов, но в нём, в стабильных сборках, пока нет # символов и вместо Symbol.iterator используется строка "@@iterator". В ночных сборках появились символы и даже Symbol.iterator, но в протоколе итераторов пока продолжает использоваться строка "@@iterator". Чтобы не сломать протокол итераторов в Firefox, в нем в нашей библиотеке мы будем дублировать методы для получения итератора и по ключу Symbol.iterator (создаём, если отсутствует), и по ключу "@@iterator". В v8 полноценная поддержка протокола итераторов появилась с Chrome 38.

# Генератор — функция, выполнение которой можно приостановить. Возвращает объект с расширенным интерфейсом итератора. Подробно их мы рассматривать не будем — синтаксис — смотрите, например, в этой статье. Для препроцессора, пожалуй, самая страшная часть ECMAScript 6. Пример с итерируемыми числами при использовании генератора выглядит совсем просто (песочница):

Number.prototype[Symbol.iterator] = function*(){
  for(var i = 0; i < this;)yield i++;
}

Array.from(10); // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

# Цикл for-of предназначен для обхода итерируемых объектов. На нашем примере с итерируемыми числами он работает так (песочница):

for(var num of 5)console.log(num); // => 0, 1, 2, 3, 4

# В ECMAScript 6 искаропки итерируемы String, Array, Map, Set и Arguments. Кроме того, Array, Map и Set имеют методы .keys, .values и .entries, которые возвращают итераторы соответственно по ключам, значениям и паре ключ-значение. Core.js добавляет данные итераторы и методы. Вместе с циклом for-of это выглядит так (песочница):

var string = 'abc';

for(var val of string)console.log(val);         // => 'a', 'b', 'c'

var array = ['a', 'b', 'c'];

for(var val of array)console.log(val);          // => 'a', 'b', 'c'. Итератор по умолчанию - .values
for(var val of array.values())console.log(val); // => 'a', 'b', 'c'
for(var key of array.keys())console.log(key);   // => 0, 1, 2
for(var [key, val] of array.entries()){
  console.log(key);                             // => 0, 1, 2
  console.log(val);                             // => 'a', 'b', 'c'
}

var map = new Map([['a', 1], ['b', 2], ['c', 3]]);

for(var [key, val] of map){                     // Итератор по умолчанию - .entries
  console.log(key);                             // => 'a', 'b', 'c'
  console.log(val);                             // => 1, 2, 3
}
for(var val of map.values())console.log(val);   // => 1, 2, 3
for(var key of map.keys())console.log(key);     // => 'a', 'b', 'c'
for(var [key, val] of map.entries()){
  console.log(key);                             // => 'a', 'b', 'c'
  console.log(val);                             // => 1, 2, 3
}

var set = new Set([1, 2, 3, 2, 1]);

for(var val of set)console.log(val);            // => 1, 2, 3. Итератор по умолчанию - .values
for(var val of set.values())console.log(val);   // => 1, 2, 3
for(var key of set.keys())console.log(key);     // => 1, 2, 3. Итератор .keys аналогичен .values
for(var [key, val] of set.entries()){           // У Set в итераторе .entries ключ и значение равны
  console.log(key);                             // => 1, 2, 3
  console.log(val);                             // => 1, 2, 3
}

var list = (function(){return arguments})(1, 2, 3);

for(var val of list)console.log(val);           // => 1, 2, 3

  • В актуальном Firefox есть все данные итераторы, кроме итератора arguments. Цикл работает, но на базе собственного, устаревшего, протокола.
  • В v8, цикл for-of, начиная с Chrome 38, работает корректно (не считая деструкции массива) и доступны все данные итераторы, кроме arguments. В старых версиях (с флажком) ожидал итератор, а не итерируемый объект.
  • В IE, само собой, данный цикл пока не работает и итераторы отсутствуют.

# Раз всё так плохо с поддержкой синтаксиса данного цикла, для тех, кто не использует препроцессоры ES6+, добавим в библиотеку хелпер, реализующий функционал, аналогичный for-of (песочница):

$for(new Set([1, 2, 3, 2, 1])).of(function(it){
  console.log(it); // => 1, 2, 3
});

// 2й аргумент $for - флаг entries - в коллбэк передаётся 2 аргумента
$for([1, 2, 3].entries(), true).of(function(key, value){
  console.log(key);   // => 0, 1, 2
  console.log(value); // => 1, 2, 3
});

// 2й аргумент .of - контекст исполнения коллбэка
$for('abc').of(console.log, console); // => 'a', 'b', 'c'

// Можно прервать обход итератора, вернув из коллбэка false
$for([1, 2, 3, 4, 5]).of(function(it){
  console.log(it); // => 1, 2, 3
  if(it == 3)return false;
});

# Прототип объекта argumentsObject.prototype, так что положить метод для получения его итератора в прототип мы не можем. Также есть вариант сборки core.js как библиотеки, без расширения нативных объектов. По этим причинам, вынесем наружу пару хелперов, для проверки, является ли объект итерируемым и для получения итератора объекта — $for.isIterable(foo), как аналог Symbol.iterator in foo и $for.getIterator(foo), как аналог foo[Symbol.iterator]():

var list = (function(){return arguments})(1, 2, 3);

console.log($for.isIterable(list)); // => true
console.log($for.isIterable({}));   // => false

var iter = $for.getIterator(list);
console.log(iter.next());           // => {value: 1, done: false}
console.log(iter.next());           // => {value: 2, done: false}
console.log(iter.next());           // => {value: 3, done: false}
console.log(iter.next());           // => {value: undefined, done: true}

# В ECMAScript 6 итераторы используются для инициализации # коллекций Map, Set, WeakMap, WeakSet, массивов # через Array.from, их ожидают методы Promise.all, Promise.race для работы с # обещаниями.

# Оператор spread, применимый при вызове функций, конструкторов и в литерале массива, также ожидает итерируемый объект, но это уже совсем далекая от стандартной библиотеки тема. Есть желание пользоваться уже сейчас — препроцессоры в зубы. Пример:

[...new Set([1, 2, 3, 2, 1])]; // => [1, 2, 3]
console.log(1, ...[2, 3, 4]);  // => 1, 2, 3, 4
var map = new Map([[1, 'a'], [2, 'b']]);
new Array(...map.keys(), 3);   // => [1, 2, 3]

# Ко всему прочему, к протоколу итераторов относятся и array / generator comprehensions (абстракция массива / генератора?). Раньше они присутствовали в черновике ECMAScript 6, но отложены до ECMAScript 7, хотя давно поддерживаются в Firefox. Это возможность генерации массива или итератора из итерируемого объекта с фильтрацией и преобразованием. Т.е. синтаксис filter и map для любых итерируемух объектов. Примеры, кроме последнего — не поддерживается деструкция массива, работают в FF. В 6to5 и Traceur работает всё. Пример:

var ar1 = [for(i of [1, 2, 3])i * i];    // => [1, 4, 9]

var set = new Set([1, 2, 3, 2, 1]);
var ar2 = [for(i of set)if(i % 2)i * i]; // => [1, 9]

var iter = (for(i of set)if(i % 2)i * i);
iter.next(); // => {value: 1, done: false}
iter.next(); // => {value: 9, done: false}
iter.next(); // => {value: undefined, done: true}

var map1 = new Map([['a', 1], ['b', 2], ['c', 3]]);
var map2 = new Map((for([k, v] of map1)if(v % 2)[k + k, v * v])); // => Map {aa: 1, cc: 9}

Как по мне — штука потрясающая. Вот только это тоже синтаксис.

# Для тех, кто не намерен пользоваться препроцессором ES6, добавим подобное, совсем уж велосипед, в рамках модуля $for. Вызов $for возвращает итератор, расширенный методами of, о котором выше, filter, map и array. Методы filter и map возвращают итератор, что, соответственно, фильтрует или преобразует значения предыдущего итератора. Этот итератор расширен теми же методами, что и итератор $for. Метод array преобразует текущий итератор в массиву, он принимает опциональный map-коллбэк. У всех этих методов второй, опциональный, аргумент — контекст исполнения. Если $for принимает флаг entries, все коллбэки цепочки запускаются с парой аргументов.

Пример:

var ar1 = $for([1, 2, 3]).array(function(v){
  return v * v;
}); // => [1, 4, 9]

var set = new Set([1, 2, 3, 2, 1]);
var ar1 = $for(set).filter(function(v){
  return v % 2;
}).array(function(v){
  return v * v;
}); // => [1, 9]

var iter = $for(set).filter(function(v){
  return v % 2;
}).map(function(v){
  return v * v;
});
iter.next(); // => {value: 1, done: false}
iter.next(); // => {value: 9, done: false}
iter.next(); // => {value: undefined, done: true}

var map1 = new Map([['a', 1], ['b', 2], ['c', 3]]);
var map2 = new Map($for(map1, true).filter(function(k, v){
  return v % 2;
}).map(function(k, v){
  return [k + k, v * v];
})); // => Map {aa: 1, cc: 9}

С литералом функции из ES5 выходит довольно громоздко, но со стрелочными функциями будет почти так же, как и с использованием синтаксиса comprehensions.

Можно было бы добавить и другие операций над итераторами в модуль $for, что дало бы универсальный и ленивый (таков протокол итераторов) способ обхода и преобразования итерируемых объектов. Но отложим это на будущее. А может и к чёрту.

Библиотека добавляет еще пару итераторов (# раз, # два), конструктор # Dict ожидает итерируемый объект, но не будем в эту главу перетаскивать совсем уж все велосипеды, связанные с итераторами.

# ECMAScript 6: Обещания



Асинхронность и JavaScript для многих уже практически синонимы. Вот только про асинхронность в стандарте ECMAScript 5 нет вообще ничего. Даже такие базовые методы, как setTimeout и setInterval, обеспечиваются web-стандартами W3C и WHATWG, # о них поговорим чуть дальше. Разве что функции как объекты первого класса — удобная передача коллбэков. Это порождает коллбэк-ад. Один из способов упрощения параллельного и последовательного исполнения асинхронных функций — библиотеки вроде async.js.

Другой подход к упрощению асинхронного программирования — шаблон Promise (Обещание). С помощью объекта-обещания, что может вернуть асинхронная функция, можно подписаться на её результат. Методы, с помощью которых можно подписаться на результат, возвращают новые обещания, что помогает лучше структурировать код, выстраивая обещания в цепочки. Также обещания решают проблему обработки ошибок: в асинхронном коде try-catch не работает, приходится передавать ошибки аргументами коллбэка, что иногда еще больше запутывает код. В конце цепочки обещаний можно подписаться на любую ошибку, что может в ней возникнуть — как брошенную методом reject, так и через throw.

Популярны такие библиотеки обещаний, как Q или RSVP. Со временем появился стандарт Promises/A+, согласно которому все обещания разрешаются асинхронно, а на результат можно подписаться при помощи метода .then (первый аргумент — функция, что выполнится при успешном завершении, второй — при возникновении ошибки).

И вот, дабы окончательно стандартизовать работу с асинхронным кодом, в ECMAScript 6 была добавлена реализация обещаний, совместимая со стандартом Promises/A+ и с минимальным, но покрывающим большую часть потребностей, функционалом. Особо их расписывать не буду, подробно ознакомиться с ними можно здесь (перевод, но слегка устарел), здесь или здесь. ES6 Promise уже доступны в v8 и Firefox, есть полифилы — es6-promise и native-promise-only.

# Реализация обещаний из ECMAScript 6 представляет из себя конструктор Promise, принимающий функцию, в которую передается 2 коллбэка — первый разрешает обещание, второй завершает с ошибкой. Кроме then, обещания ES6 содержат метод catch — сокращение для then с пропущенным первым аргументом, с помощью него можно подписаться на ошибку. Пример:

var log = console.log.bind(console);
function sleepRandom(time){
  return new Promise(function(resolve, reject){
    // resolve разрешает обещание успешно, reject - с ошибкой
    // разрешим обещание через заданное время
    setTimeout(resolve, time * 1e3, 0 | Math.random() * 1e3);
  });
}

log('Поехали');                // => Поехали
sleepRandom(5).then(function(result){
  log(result);                 // => 869, через 5 сек.
  return sleepRandom(10);
}).then(function(result){
  log(result);                 // => 202, через 10 сек. 
}).then(function(){
  log('Сразу после прошлого'); // => Сразу после прошлого
  throw Error('Ашыпка!');
}).then(function(){
  log('не будет выведено - ошибка');
}).catch(log);                 // => Error: 'Ашыпка!'

Так как мне было лень полностью адаптировать свой движок обещаний под стандарт, ядро обещаний core.js базируется на библиотеке native-promise-only, от кода которой мало что осталось. Для обеспечения асинхронности используется process.nextTick и способы из полифила # setImmediate.

# Пара хелперов Promise.resolve и Promise.reject возвращают завершенное, соответственно, успешно или с ошибкой обещание с переданным им значением. Если Promise.resolve принимает обещание — его и возвращает. Также его можно использовать для преобразования других thenable (например, jQuery Deferred) в обещания, в старой версии стандарта для этого был отдельный метод — Promise.cast. Пример:

Promise.resolve(42).then(log); // => 42
Promise.reject(42).catch(log); // => 42

Promise.resolve($.getJSON('/data.json')); // => ES6 promise

# Хелпер Promise.all возвращает обещание, которое разрешится, когда разрешатся все обещания из переданной ему # итерируемой коллекции (в v8 сейчас работает только с массивами, исправлять в рамках библиотеки пока не стал — не вижу особого смысла использовать его с чем-то ещё). Элементы коллекции, не являющиеся обещаниями, приводятся к обещаниям через Promise.resolve. Пример:

Promise.all([
  'foo',
  sleepRandom(5),
  sleepRandom(15),
  sleepRandom(10)  // через 15 секунд выведет что-то вроде
]).then(log);      // => ['foo', 956, 85, 382]

# Хелпер Promise.race похож на предыдущий, но возвращает обещание, которое разрешится, когда разрешится хотя бы одно обещание из переданной ему коллекции. По моему скромному мнению, в отличие от Promise.all, чуть менее, чем полностью, бесполезен. Разве что с его помощью становится чуть проще повесить ограничение на время разрешения обещания. Пример:

function timeLimit(promise, time){
  return Promise.race([promise, new Promise(function(resolve, reject){
    setTimeout(reject, time * 1e3, Error('Await > ' + time + ' sec'));
  })]);
}

timeLimit(sleepRandom(5), 10).then(log);   // => через 5 секунд получили результат 853
timeLimit(sleepRandom(15), 10).catch(log); // Error: Await > 10 sec

# Даже с обещаниями, асинхронному JavaScript есть еще куда стремиться в плане удобства, потому к добавлению в ECMAScript 7 предложены асинхронные функции, расширяющие синтаксис ключевыми словами async / await и базирующиеся на # генераторах, обещаниях и генераторах, возвращающих обещания :) Данный синтаксис уже поддерживается и Traceur, и 6to5. Пример:

var delay = time => new Promise(resolve => setTimeout(resolve, time));

async function sleepRandom(time){
  await delay(time * 1e3);
  return 0 | Math.random() * 1e3;
}
async function sleepError(time, msg){
  await delay(time * 1e3);
  throw Error(msg);
}

(async () => {
  try {
    log('Поехали');            // => Поехали
    log(await sleepRandom(5)); // => 936, через 5 сек.
    var [a, b, c] = await Promise.all([
      sleepRandom(5),
      sleepRandom(15),
      sleepRandom(10)
    ]);
    log(a, b, c);              // => 210 445 71, через 15 сек.
    await sleepError(5, 'Ашыпка!');
    log('Не будет выведено');
  } catch(e){
    log(e);                    // => Error: 'Ашыпка!', через 5 сек.
  }
})();

Со временем, обещания изменят значительную часть стандартного асинхронного API JavaScript. Уже стандартизована (полифил) глобальная функция fetch — простая и удобная обертка над XMLHttpRequest, возвращающая обещание. Предлагают добавить, и, думаю, добавят, простую, но часто необходимую, функцию delay, аналогичную функции из предыдущего примера, возвращающую обещание, что разрешится через заданное время — прощай, setTimeout. Пожалуй, delay было бы неплохо добавить и в данную библиотеку.

# Mozilla JavaScript: Статические версии методов массива



Выше мы рассматривали протокол итераторов. Вот только это не единственный стандарт обхода коллекций в JavaScript. Есть куда более простой, быстрый и древний. Это array-like объекты.

Кроме массивов, в JavaScript много сущностей, подобных им, но при этом массивами не являющихся. Эти объекты, как и массивы, содержат длину .length и элементы по ключу [от 0 до .length), которые могут быть пропущены — тогда возникают «дырки». Они не содержат под собой Array.prototype, соответственно не имеют и методов массива. Это объект arguments, строки (формально — IE8+), типизированные массивы (массивы, но не содержат под собой Array.prototype), коллекции элементов DOM, jQuery объекты и т.д.

Почти все методы прототипа массива — дженерики (как сообщает перевод спецификации — «нарочито родовые» функции). Они не требуют, что бы объект, из контекста которого они запускались, был массивом. Разве что .concat не совсем. Думаю, многим знакомы такие конструкции:

Array.prototype.slice.call(arguments, 1);
// или
[].slice.call(arguments, 1);

Громоздко и невнятно.

ECMAScript 6 добавляет метод # Array.from, с его помощью можно привести к массиву итерируемые и array-like объекты.

Array.from(arguments).slice(1);

Вроде удобно, но далеко не дёшево — даже для самой простой операции мы вынуждены приводить весь объект к массиву, притом, в большинстве случаев, через довольно тяжелый протокол итераторов.

В JavaScript от Mozilla, в версии языка 1.6 и, соответственно, в Firefox, еще в 2005 году, вместе с методами массива, позднее вошедшими в ECMAScript 5, были добавлены и статические версии методов массива. Ни в 5ю, ни в 6ю редакцию ECMAScript они не попали, хоть и присутствуют в ветке разработки Strawman довольно давно, но я лично надеюсь на их появлении в одной из будущих версий ECMAScript. Добавим в библиотеку и их — реализуются элементарно, а так как они уже имеются в огнелисе — отнесем их в категорию костылей, а не велосипедов.

Array.slice(arguments, 1);

Array.join('abcdef', '+'); // => 'a+b+c+d+e+f'

var form = document.getElementsByClassName('form__input');
Array.reduce(form, function(memo, it){
  memo[it.name] = it.value;
  return memo; 
}, {}); // => например, {name: 'Вася', age: '42', sex: 'yes, please'}

# Отложенное исполнение: setTimeout, setInterval, setImmediate



# setTimeout, setInterval


Пожалуй, начнем с привычных всем setTimeout и setInterval. Многие не знают, что по стандарту (W3C, WHATWG) эти функции кроме коллбэка и времени задержки, принимают дополнительные аргументы, с какими запускается переданный коллбэк. Но тут, как обычно, проблема в IE. В IE9- setTimeout и setInterval принимают только 2 аргумента, это лечится оберткой в несколько строчек.

// Было:
setTimeout(log.bind(null, 42), 1000);
// Стало:
setTimeout(log, 1000, 42);

# setImmediate


JavaScript однопоточен и иногда это довольно неприятно. Любые длительные тяжелые вычисления на клиенте подвесят пользовательский интерфейс, а на сервере — обработка запросов. В этом случае спасает разбиение тяжелой задачи на лёгкие подзадачи, исполняемые асинхронно, между которыми может проходить ввод / вывод.

Также в JavaScript (пока) не оптимизируется хвостовая рекурсия. Когда функция будет вызвана определённое число раз рекурсивно, будет сгенерирована ошибка RangeError: Maximum call stack size exceeded. Это число, в зависимости от платформы, варьирует от нескольких сотен до нескольких десятков тысяч. Асинхронный рекурсивный вызов спасает от переполнения стека. Правда, рекурсивный вызов обычно легко переписывается в обычный цикл, что предпочтительней.

Для решения подобных проблем можно использовать setTimeout с минимальной задержкой, но получится очень неспешно. Минимальная задержка setTimeout по спецификации — 4 мс, а на некоторых платформах и того больше. Итого, максимум, ~250 рекурсивных вызовов в секунду в современных браузерах и, например, ~64 в IE8. Приходилось, да и приходится, велосипедить, так как есть способы сделать эффективное отложенное исполнение.

Если на Node.js был process.nextTick, то на клиент помощь пришла откуда не ждали. В IE10 Microsoft добавил метод setImmediate, устанавливающий задачу, выполняемую сразу после того, как отработает ввод / вывод, и предложил его для стандартизации W3C. Позже он появился и на Node.js. FF и Chromium его добавлять не спешат. Популярен такой полифил.

Способов реализации эффективного отложенного исполнения функций огромное количество. Для достижения максимального быстродействия на различных платформах нам придется использовать многие (аналогичны способам из упомянутого выше полифила). Это:

  • Для старых версий Node.js — process.nextTick
  • Для современных браузеров — postMessage
  • Для WebWorker'ов — MessageChannel
  • Для IE8- — script.onreadystatechange
  • И уж если эти способы не помогли — setTimeout с минимальной задержкой

setImmediate(function(arg1, arg2){
  console.log(arg1, arg2); // => Сообщение будет выведено асинхронно с минимальной задержкой
}, 'Сообщение будет выведено асинхронно', 'с минимальной задержкой');

clearImmediate(setImmediate(function(){
  console.log('Сообщение не будет выведено');
}));

Кроме setImmediate, существует его более быстрая альтернатива — концепция asap (as soon as possible, библиотека) — по возможности, создаём microtask, что выполнится до любого ввода / вывода. Добавить такой глобальный метод в стандарт языка думают и в tc39. Может и его стоит добавить в библиотеку?

# Консоль



Консоль — единственное универсальное средство вывода и отладки как в браузере, так и на сервере. При этом, консоль не является частью спецификации ECMAScript и вообще не стандартизована. Есть заброшенные наброски спецификации, фактическая реализация на всех платформах различается.

# В IE7- консоль отсутствует полностью. У некоторых браузеров «консоль Гейзенберга» — console определена только тогда, когда пользователь за ней наблюдает. Ну и, конечно, не все методы доступны на всех платформах. Свою консоль в стиле Firebug Lite мы изобретать не будем, просто сделаем заглушки для методов, дабы можно было их использовать, не проверяя наличия.

// Было:
if(window.console && console.log)console.log(42);
// Стало:
console.log(42);

# В Firefox и Chromium методы консоли должны быть запущены из контекста console, так что если нужно передать их в качестве коллбэка придется привязать — например, # console.log.bind(console). В IE, Firebug и на Node.js можно передавать, не привязывая, что гораздо удобней. Соответственно, привяжем методы к объекту console.

// Было:
setTimeout(console.log.bind(console, 42), 1000);
[1, 2, 3].forEach(console.log, console);
// Стало:
setTimeout(console.log, 1000, 42);
[1, 2, 3].forEach(console.log);

Но есть здесь и одна проблема: на некоторых платформах, при вызове методов консоли выводится и строка, откуда был вызван метод. При переопределении методов консоли, этой строкой будет строка в сore.js. Если это для вас критично — можете собрать библиотеку без модуля консоли.

Так как консоль не стандартизована, добавим немного отсебятины:


# Возможность выключить вывод в консоль. Конечно, на продакшне вызовы методов консоли лучше не оставлять в коде, удаляя их либо ручками, либо используя более продвинутые средства, но это удобно и нужно далеко не всегда.

console.disable();
console.warn('Консоль отключена, вы не увидите этого сообщения.');
console.enable();
console.warn('Консоль снова включена.');

Привязка контекста и возможность отключить консоль также присутствует, например, в этой библиотеке от TheShock.

# Часть вторая: Велосипеды


Вариантов ноль, вариантов нет.
Хотелось полмира — хватило на велосипед.
— Петля пристрастия

Велосипеды, в контексте данной статьи / библиотеки — весь нестандартизованный функционал. Собственно, то, чего, по моему мнению, не хватает стандартной библиотеке языка, даже если она будет реализована согласно всем имеющимся на данный момент стандартам. Сюда же отнесём и то, что предлагают в ES7+, так как это может быть далеко не раз пересмотрено или вообще отклонено.

# Классификация данных



# Тут начнем совсем с банальщины. В JavaScript по спецификации ECMAScript 5 есть 6 типов данных: Undefined, Null, Boolean, String, Number и Object. ECMAScript 6 добавляет еще один тип данных — # Symbol. Для определения типа данных есть оператор typeof. Вот только работает он специфично. Так уж исторически сложилось, что typeof null возвращает 'object' и попытка исправить это в Harmony успехом не увенчалась. Для типа Object typeof возвращает либо 'object', либо 'function', в зависимости от наличия внутреннего метода [[Call]]. Итого получаем, что оператор typeof возвращает именно тип данных только для примитивов, и то не всех.

Если проверить, является ли переменная null просто, достаточно её с ним сравнить, то с Object придется либо каждый раз писать сотни кода, либо хелпер. Можно, конечно, сделать так — Object(foo) === foo, но данное решение далеко не самое быстрое — приводит примитив к объекту. В ранних черновиках ES6 присутствовал метод Object.isObject, но, видимо, из-за попытки исправить typeof null, был удален. А проверять, является ли переменная объектом, приходится постоянно. Так что добавим хелпер Object.isObject, реализованный проще некуда (песочница):

Object.isObject = function(it){
  return it != null && (typeof it == 'object' || typeof it == 'function');
}
// ...
typeof {};              // => 'object'
typeof isNaN;           // => 'function'
typeof null;            // => 'object'

Object.isObject({});    // => true
Object.isObject(isNaN); // => true
Object.isObject(null);  // => false

# А вот с классификацией объектов интересней. Оператор instanceof проверяет цепочку прототипов. Если создать объект из прототипа или установить объекту в качестве прототипа Function.prototype, это его не сделает функцией. Свойство инстанса constructor не может ничего гарантировать, а constructor.name мало того, что теряет всякий смысл при сжатии кода, еще и не поддерживается IE. Помочь с классификацией объекта может внутреннее свойство [[Class]]. Единственный способ выдрать его наружу — хорошо знакомая многим страшная конструкция Object.prototype.toString.call(foo).slice(8, -1). Пример:

Object.prototype.toString.call(1).slice(8, -1);   // => 'Number'
Object.prototype.toString.call([]).slice(8, -1);  // => 'Array'
Object.prototype.toString.call(/./).slice(8, -1); // => 'RegExp'

На базе получения внутреннего класса большинство библиотек добавляет набор утилит по типу # Array.isArray: Object.isType в Sugar, _.isType в Undescore и т.п.

Мы поступим иначе — один универсальный метод Object.classof для классификации данных, похожий на оператор typeof! из LiveScript (пример).

Вот только у Object.prototype.toString.call(foo).slice(8, -1) есть пара проблем:
  • В старых IE, применимо к null, undefined и arguments, данная конструкция возвращает "Object". Это легко лечится дополнительными проверками.
  • Что делать с инстансами конструкторов из ECMAScript 6, полифилы которых мы добавили в нашей библиотеке? Так же будут возвращать "Object". Тут на помощь приходит измененная логика работы Object#toString в ECMAScript 6.

Внезапно, но в ECMAScript 6 вообще отсутствует такое внутреннее свойство объектов, как [[Class]]. Object#toString ES6, через проверку специальных внутренних свойств, возвращает принадлежность переменной к Undefined, Null, Array, String, Arguments, Function, Error, Boolean, Number, Date или RegExp, а у остальных ищет подсказку по # символу Symbol.toStringTag. Если метод находит подсказку и она не является именем одного из встроенных «классов» — возвращает её, не находит — Object.

Исправим логику работы Object#toString, благо благодаря # одному противному, но веселому, багу мы можем сделать это и в IE8-, не сломав при этом for-in. Ну и, конечно, реализуем данный подход в методе Object.classof. В качестве бонуса мы получаем возможность классификации инстансов пользовательских конструкторов. Пример:

var classof = Object.classof;

classof(null);                 // => 'Null'
classof(undefined);            // => 'Undefined'
classof(1);                    // => 'Number'
classof(true);                 // => 'Boolean'
classof('string');             // => 'String'
classof(Symbol());             // => 'Symbol'

classof(new Number(1));        // => 'Number'
classof(new Boolean(true));    // => 'Boolean'
classof(new String('string')); // => 'String'

var fn   = function(){}
  , list = (function(){return arguments})(1, 2, 3);

classof({});                   // => 'Object'
classof(fn);                   // => 'Function'
classof([]);                   // => 'Array'
classof(list);                 // => 'Arguments'
classof(/./);                  // => 'RegExp'
classof(new TypeError);        // => 'Error'

classof(new Set);              // => 'Set'
classof(new Map);              // => 'Map'
classof(new WeakSet);          // => 'WeakSet'
classof(new WeakMap);          // => 'WeakMap'
classof(new Promise(fn));      // => 'Promise'

classof([].values());          // => 'Array Iterator'
classof(new Set().values());   // => 'Set Iterator'
classof(new Map().values());   // => 'Map Iterator'

classof(Math);                 // => 'Math'
classof(JSON);                 // => 'JSON'

function Example(){}
Example.prototype[Symbol.toStringTag] = 'Example';

classof(new Example);          // => 'Example'

# Словари



В JavaScript объекты и словари (ассоциативные массивы) — одно. В этом есть как плюсы — это кажется очень удобным, не зря так распространён основанный на объектной системе JavaScript формат обмена данными JSON, так и минусы.

Для словарей нет разницы между получением элемента по ключу и метода из прототипа, что, при наличии под объектом прототипа (а у объекта, заданного нотацией — фигурными скобками, это Object.prototype), ломает базовые операции над словарем. Для объектов же это ограничения на расширение Object.prototype.

В ECMAScript 6 появился # уже упомянутый выше новый вид коллекций ключ-значение — Map. Его быстродействие иногда даже выше, чем у объектов (само собой, не касается полифилов). Вот только ИМХО в большинстве случаев их не заменит. У Map, в отличии от словарей-объектов, отсутствует простая запись литералом, доступ к свойствам происходит через методы — не такой лаконичный. Map'ы далеки от любимого всеми JSON и не столь универсальны. Да и обычно в ключах словаря не требуется ничего, кроме строк.


# Проблема: Object.prototype и словари


В Object.prototype, как подсказывает Mozilla Developer Network, в зависимости от реализации, могут находиться:

Object.prototype.constructor();
Object.prototype.hasOwnProperty();
Object.prototype.isPrototypeOf();
Object.prototype.propertyIsEnumerable();
Object.prototype.toLocaleString();
Object.prototype.toString();
Object.prototype.valueOf();
Object.prototype.__proto__;
Object.prototype.__count__;
Object.prototype.__parent__;
Object.prototype.__noSuchMethod__;
Object.prototype.__defineGetter__();
Object.prototype.__defineSetter__();
Object.prototype.__lookupGetter__();
Object.prototype.__lookupSetter__();
Object.prototype.eval();
Object.prototype.toSource();
Object.prototype.unwatch();
Object.prototype.watch();

Чем это нам может грозить?

Предположим, у нас есть примитивный телефонный справочник, и пользователь имеет доступ к его API:

var phone = (function(){
  var db = {
    'Вася': '+7987654',
    'Петя': '+7654321'
  };
  return {
    has: function(name){
      return name in db;
    },
    get: function(name){
      return db[name];
    },
    set: function(name, phone){
      db[name] = phone;
    },
    delete: function(name){
      delete db[name];
    }
  };
})();

Получаем:

console.log(phone.has('Вася'));     // => true
console.log(phone.get('Вася'));     // => '+7987654'
console.log(phone.has('Дима'));     // => false
console.log(phone.get('Дима'));     // => undefined
console.log(phone.has('toString')); // => true
console.log(phone.get('toString')); // => function toString() { [native code] }

Свойство, при отсутствии, берется из цепочки прототипов, in аналогично проверяет его наличие. Давайте добавим / заменим in на метод hasOwnProperty, проверяющий наличие свойства в объекте без учета цепочки прототипов. Получаем:

// ...
    has: function(name){
      return db.hasOwnProperty(name);
    },
    get: function(name){
      if(db.hasOwnProperty(name))return db[name];
    },
// ...
console.log(phone.get('Вася'));              // => '+7987654'
phone.set('hasOwnProperty', '+7666666'); // Добавляем нового "абонента"
console.log(phone.get('Вася'));              // TypeError: string is not a function

Уже довольно серьезно, особенно если этот «телефонный справочник» находится на серверной стороне. Методы прототипа можно перекрыть. Соответственно, нужно использовать метод hasOwnProperty, отвязанный от объекта. Получаем необходимость использования на каждый чих громоздких проверок. Примерно такой трэш:

// ...
    has: function(name){
      return Object.prototype.hasOwnProperty.call(db, name);
    },
    get: function(name){
      if(Object.prototype.hasOwnProperty.call(db, name))return db[name];
    },
// ...

Для решения данной проблемы в языке пригодился бы оператор проверки, является ли свойство собственным, похожий на in.

Вы уже решили, что проблемы закончились? Ничего подобного:

phone.set('__proto__', '+7666666'); // Добавляем нового "абонента"
console.log(phone.get('__proto__'));    // => undefined

В Object.prototype есть еще и «магический» геттер / сеттер __proto__, установка примитива по этому ключу будет игнорироваться, а объекта — повредить, например, при обходе свойств. В старых движках были и другие «магические» свойства. Тут поможет разве что Object.defineProperty (песочникца):

// ...
    set: function(name, phone){
      Object.defineProperty(db, name, {
        enumerable  : true,
        configurable: true,
        writable    : true,
        value       : phone
      });
    },
// ...

Про обход словарей особо говорить не будем — если всё не совсем плохо, словарь не содержит в прототипе перечесляемых свойств, при обходе словаря через for-in можно обойтись без проверки hasOwnProperty. Вот только баг с # «неперечисляемыми перечисляемыми» свойствами всё равно делает обход через for-in словарей, под которыми находится Object.prototype, неполноценным в старых IE.

В ECMAScript 5 появился способ создания объекта без прототипа — Object.create(null), с ним можно использовать реализацию методов, предложенную изначально (песочница):

var phone = (function(){
  var db = Object.create(null);
  Object.assign(db, {
    'Вася': '+7987654',
    'Петя': '+7654321'
  });
  return {
    has: function(name){
      return name in db;
    },
    get: function(name){
      return db[name];
    },
    set: function(name, phone){
      db[name] = phone;
    },
    delete: function(name){
      delete db[name];
    }
  };
})();

Всё замечательно, вот только его создание и инициализация, даже с Object.assign, не особо компактны.

Так как у подобных словарей отсутствует прототип, отсутствуют и методы toString и valueOf. Чем это нам грозит?
  • Не удастся привести к числу, например +Object.create(null), или строке, '' + Object.create(null), — TypeError
  • Соответственно, не удастся сравнить объект алгоритмом абстрактного равенства, например, ==, с примитивами — TypeError
Для кого как, а для меня это скорее плюс, чем минус.

# Конструктор Dict


Итак, создание словаря через Object.create(null) и его заполнение куда более громоздко, чем при создании словаря через {}. Конечно, самым красивым решением было бы добавление в язык литерала словаря, но это, по крайней мере, в ближайшей перспективе, маловероятно. Отсутствует возможность инициализации литералом. Есть запись {__proto__: null, foo: 'bar'}, но поддерживается не везде, на данный момент приводит к деоптимизации кода, да и всё равно довольно громоздкая.

Обсуждалось одно довольно интересное решение — сделать «конструктор» Dict как сокращение Object.create(null). Как оно сейчас поживает и поживает ли вообще, я не в курсе. Но почему бы не взять его, слегка расширив? Заодно получим пространство имен для методов для работы с объектами как словарями. Добавим возможность инициализации итератором entries или объектом без итератора, этакая версия # Array.from для словарей.

Так как Dict() instanceof Dict работать не будет и # Object.classof(Dict()) будет возвращать 'Object', добавим для идентификации словарей метод Dict.isDict.

Примерно вот так:

function Dict(props){
  var dict = Object.create(null);
  if(props != null){
    if(Symbol.iterator in props){
      for(var [key, val] of props)dict[key] = val;
    } else Object.assign(dict, props);
  }
  return dict;
}
Dict.prototype = null;
Dict.isDict = function(it){
  return Object.isObject(it) && Object.getPrototypeOf(it) === null;
}

// ...

var map = new Map([['a', 1], ['b', 2], ['c', 3]]);

Dict();                                            // => {__proto__: null}
Dict({a: 1, b: 2, c: 3});                          // => {__proto__: null, a: 1, b: 2, c: 3}
Dict(map);                                         // => {__proto__: null, a: 1, b: 2, c: 3}
Dict([1, 2, 3].entries());                         // => {__proto__: null, 0: 1, 1: 2, 2: 3}
Dict((for([k, v] of map)if(v % 2)[k + k, v * v])); // => {__proto__: null, aa: 1, cc: 9}

Dict.isDict({});     // => false
Dict.isDict(Dict()); // => true

# Методы для безопасной работы со словарем, имеющим под собой прототип


На случай, если всё же придется работать с объектом, имеющим под собой прототип, как со словарем, добавим методы для безопасной работы с собственными свойствами.

Dict.has — банально, статическая версия hasOwnProperty. В черновиках ECMAScript 6, в модуле Reflect — наборе заглушек для Proxy, до недавнего времени присутствовала статическая версия метода hasOwnProperty — метод Reflect.hasOwn. Однако в последних версиях черновика спецификации данный метод удалили.

Dict.get — получение значения по ключу с проверкой, является ли свойство собственным. Не является — возвращаем undefined.

Dict.set — метод для совсем уж параноиков. Позволяет установить свойство словаря, игнорирую сеттеры, такие, как __proto__. Использует defineProperty.

Ну а оператор delete и так работает как надо.

Пример:

var dict = {a: 1, b: 2, c: 3};

console.log(Dict.has(dict, 'a'));         // => true
console.log(Dict.has(dict, 'toString'));  // => false

console.log(Dict.get(dict, 'a'));         // => 1
console.log(Dict.get(dict, 'toString'));  // => undefined

Dict.set(dict, '__proto__', 42);
console.log(Dict.get(dict, '__proto__')); // => 42

# Методы для работы со словарем


Методы, добавленные ECMAScript 5 в прототип массива для его обхода (forEach, map, some и т.п.), очень удобны. Их статические аналоги для словарей присутствуют в практически всех фреймворках / библиотеках общего назначения. А вот подвижек с добавлением их в стандарт нет.

Добавим их в рамках нашего модуля Dict. Тут всё просто, методы аналогичны статическим версиям методов массива. Это: Dict.forEach, Dict.map, Dict.filter, Dict.some, Dict.every, Dict.find, Dict.findKey, Dict.keyOf, # Dict.includes, Dict.reduce, # Dict.turn. Key в названии соответствует index у методов массива. «Правые» версии и опциональный аргумент-индекс (пока?) отсутствуют, так как порядок обхода ключей объектов пока не везде одинаков. Перебираются только собственные перечисляемые элементы объекта. Эти методы — дженерики в том же плане, что и Array.from или Array.of. Например, Dict.map(dict, fn) вернет новый Dict, а Dict.map.call(Object, dict, fn) — новый Object. А в общем, всё примитивно, скучно и как везде (песочница):

var dict = {a: 1, b: 2, c: 3};
Dict.forEach(dict, console.log, console);
// => 1, 'a', {a: 1, b: 2, c: 3}
// => 2, 'b', {a: 1, b: 2, c: 3}
// => 3, 'c', {a: 1, b: 2, c: 3}
Dict.map(dict, function(it){
  return it * it;
}); // => {a: 1, b: 4, c: 9}
Dict.filter(dict, function(it){
  return it % 2;
}); // => {a: 1, c: 3}
Dict.some(dict, function(it){
  return it === 2;
}); // => true
Dict.every(dict, function(it){
  return it === 2;
}); // => false
Dict.find(dict, function(it){
  return it > 2;
}); // => 3
Dict.find(dict, function(it){
  return it > 4;
}); // => undefined
Dict.findKey(dict, function(it){
  return it > 2;
}); // => 'c'
Dict.findKey(dict, function(it){
  return it > 4;
}); // => undefined
Dict.keyOf(dict, 2);    // => 'b'
Dict.keyOf(dict, 4);    // => undefined
Dict.includes(dict, 2); // => true
Dict.includes(dict, 4); // => false
Dict.reduce(dict, function(memo, it){
  return memo + it;
});     // => 6
Dict.reduce(dict, function(memo, it){
  return memo + it;
}, ''); // => '123'
Dict.turn(dict, function(memo, it, key){
  memo[key + key] = it;
});     // => {aa: 1, bb: 2, cc: 3}
Dict.turn(dict, function(memo, it, key){
  it % 2 && memo.push(key + it);
}, []); // => ['a1', 'c3']

# Что касается цепочек методов, # по вполне очевидной причине, в рамках модуля Dict их нет, и не предвидятся. Спасением тут могут стать abstract references. А вот в рамках # $for и # Map вполне возможно и появятся.

# Итерация по словарю


Близится светлое ES6 будущее с # итераторами и # циклом for-of. Вот только объектам как словарям от этого ни тепло, ни холодно — для них в ES6 итераторы не предусмотрены. Соответственно, нет простого способа перебрать их через for-of, инициализировать # Map словарем и т.п. Добавление методов .keys, .values и .entries в Object.prototype маловероятно — там и так достаточно мусора, см. описание предыдущей проблемы. Зато вполне вероятны два других сценария:

Первый — добавление статических методов, возвращающих итератор, в пространство имён DictDict.{keys, values, entries}. Но как уже писал, что с перспективой добавления этого модуля в стандарт мне неизвестно.

Второй — добавление методов Object.{values, entries}, по типу Object.keys, возвращающих массив, а не итератор, и уже через итератор массива обходить объект.

Что из этого появится — не знаю и гадать боюсь. Для получения массива значения словаря использовать довольно тяжелый протокол итераторов, как и использовать промежуточный массив для итерации по объекту не рационально. Так что, хоть это частично и дублирующий друг друга функционал, реализуем в нашей библиотеке оба набора методов. Примеры:

var dict = {a: 1, b: 2, c: 3};

console.log(Object.values(dict));  // => [1, 2, 3]
console.log(Object.entries(dict)); // => [['a', 1], ['b', 2], ['c', 3]]

for(var key of Dict.keys(dict))console.log(key); // => 'a', 'b', 'c'

for(var [key, val] of Dict.entries(dict)){
  console.log(key); // => 'a', 'b', 'c'
  console.log(val); // => 1, 2, 3
}

$for(Dict.values(dict)).of(console.log); // => 1, 2, 3

new Map(Dict.entries(dict)); // => Map {a: 1, b: 2, c: 3}

new Map((for([k, v] of Dict.entries(dict))if(v % 2)[k + k, v * v])); // =>  Map {aa: 1, cc: 9}

# Возможные перспективы


Можно было бы зайти чуть дальше, сделав Dict не просто сокращением для Object.create(null) с возможностью инициализации итератором и объектом, а полноценным конструктором с прототипом, не содержащим ключей-строк, только # символы. Примерно так:

function Dict(props){
  if(!(this instanceof Dict))return new Dict(props);
  if(props != null){
    if(Symbol.iterator in props){
      for(var [key, val] of props)this[key] = val;
    } else Object.assign(this, props);
  }
}
Dict.prototype = Object.create(null);
Dict.prototype[Symbol.toStringTag] = 'Dict';
Dict.prototype[Symbol.iterator] = function(){
  return Dict.entries(this);
};

Что бы это нам дало?

  • new Dict instanceof Dict.
  • for(var [key, value] of dict){...}, new Map(dict) без необходимости получать итератор через Dict.entries.
  • # Object.classof(new Dict) возвращал бы 'Dict', а не 'Object'.

Однако, есть причины, как минимум, отложить внедрение этого подхода до момента, когда IE8- вымрет окончательно, а также Firefox полностью перейдет на протокол итераторов ECMAScript 6. А может и вообще не стоит — рискуем окончательно потерять совместимость со стандартом, когда / если модуль Dict туда попадёт.

# Частичное применение



Пожалуй, одним из самых полезных нововведений в ECMAScript 5 был метод # Function#bind. Вот только возможности частичного применения данный метод раскрывает далеко не полностью. В этой главе мы рассмотрим такие вещи, как:


Можно было бы добавить каррирование, но в JavaScript именно каррирование, а не частичное применение, требуется достаточно редко. Как и «правые» версии методов. Добавлю ссылку на годную (и так, пожалуй, хорошо известную) статью по теме.

# Частичное применение без привязки контекста


Function#bind совмещает в себе частичное применение и привязку контекста this. Последнее нужно далеко не всегда, и в этом случае привязываемый this не только «лишний» аргумент, что нужно писать. Если контекст, в котором должна запускаться частично применённая функция, заранее неизвестен, метод Function#bind неприменим. Например, если это метод прототипа (песочница):

Array.prototype.compact = [].filter.bind(Array.prototype, function(val){
  return val != null;
});

[0, null, 1, undefined, 2].compact(); // => [] - метод запускается в контексте Array.prototype, а не данного массива

// Придется частично применять вручную:
Array.prototype.compact = function(){
  return this.filter(function(val){
    return val != null;
  });
};

[0, null, 1, undefined, 2].compact(); // => [0, 1, 2];

Добавим метод частичного применения без привязки thisFunction#part (песочница):

Array.prototype.compact = [].filter.part(function(val){
  return val != null;
});

[0, null, 1, undefined, 2].compact(); // => [0, 1, 2];

var fn = console.log.part(1, 2);
fn(3, 4); // => 1, 2, 3, 4

# Частичное применение произвольных аргументов


Часто при частичном применении нужно передать произвольные аргументы — не, например, первые 2, а только второй и четвёртый или второй и третий. Тут Function#bind нам помочь не сможет — придется писать обертку вручную под каждый конкретный случай.

function fn1(a, c){
  console.log(a, 2, c, 4);
};
fn1(1, 3); // => 1, 2, 3, 4

function fn2(b, c){
  console.log(1, b, c, 4);
};
fn2(2, 3); // => 1, 2, 3, 4

Для облегчения подобной задачи добавим плейсхолдер — объект, замещающий аргумент, что будет передан при вызове конечной функции. В качестве ссылки на плейсхолдер так и напрашивается глобальная переменная _ (как, например, в LiveScript, песочница), однако, эту переменную используют библиотеки Undescore.js (кстати, в ней это тоже плейсхолдер для _.partial) и LoDash как свой неймспейс. Во избежание конфликтов с ними, создаём новый глобальный объект _ только если таковой отсутствует, а во время работы используем глобальный объект _, что бы там ни лежало. В случае сборки без расширения нативных объектов, в качестве плейсхолдера используем объект core._. Пример:

var fn1 = console.log.part(_, 2, _, 4);
fn1(1, 3);    // => 1, 2, 3, 4

var fn2 = console.log.part(1, _, _, 4);
fn2(2, 3);    // => 1, 2, 3, 4

fn1(1, 3, 5); // => 1, 2, 3, 4, 5
fn1(1);       // => 1, 2, undefined, 4

Также добавим метод Function#by, аналогичный Function#bind, но с возможностью использования плейсхолдера для аргументов. Можно было бы обернуть Function#bind, заставив работать с плейсхолдером, но это — нарушение спецификации, да и метод этот и так довольно тормозной почти во всех движках.

var fn = console.log.by(console, _, 2, _, 4);
fn(1, 3, 5); // => 1, 2, 3, 4, 5

# Извлечение метода из объекта


В большинстве случаев, например, при передаче коллбэка в функцию, метод нам нужно привязать именно к тому объекту, из которого его и получаем. И тут возникает проблема — fn(foo.bar.baz.bind(foo.bar)). Мы вынуждены писать foo.bar 2 раза, это явное нарушение принципа DRY. Надеюсь, в будущем от этой проблемы спасут abstract references, но предложенная реализация проблему не решает. Пожалуй, самым вкусным и красивым решением было бы добавление в язык оператора доступа с сохранением контекста, аналогичного ~ из LiveScriptfn(foo.bar~baz) (песочница).

Решений проблемы на базе библиотеки в голову приходит не много — разве что извлечение метода из объекта по ключу. Это либо статический метод, например, _.bindKey из LoDash (но с ранним связыванием), однако он тоже довольно громоздкий и еще больше ухудшает читаемость, либо аналогичный по функционалу метод в Object.prototype, например, Object#boundTo из Eddy.js.

Как бы это страшно ни звучало, мы добавим метод в Object.prototype. Рисковать, расширяя Object.prototype методом по короткому ключу-строке, мы, по крайней мере, пока, не будем — сложно избежать конфликтов, да и сломаем for-in в IE8-. Ранее в этой главе мы уже использовали глобальную переменную _. Дабы не плодить лишних сущностей и для краткости, применим её и здесь. Заменим у объекта _ метод toString (соответственно, если использовать совместно с Undescore.js или LoDash — нужно подключать core.js после них). Он будет возвращать уникальный ключ-строку, аналогично # ключу полифила символа. Добавим по этому ключу метод в Object.prototype. За счет использования грязного хака с # веселым багом, мы добавляем этот метод и в IE8-, при этом не сломав for-in.

Итого, из примера, с которого начали, получим fn(foo.bar[_]('baz')) — далековато от идеала, но хоть от второго упоминания объекта избавились. Возвращаемый метод кэшируется. Примеры:

['foobar', 'foobaz', 'barbaz'].filter(/bar/[_]('test')); // => ['foobar', 'barbaz']

var has = {}.hasOwnProperty[_]('call');

console.log(has({key: 42}, 'foo')); // => false
console.log(has({key: 42}, 'key')); // => true

var array = []
  , push  = array[_]('push');
push(1);
push(2, 3);
console.log(array); // => [1, 2, 3];

По-хорошему, после отказа от поддержки IE8- библиотекой, метод стоит переименовать, а то страшновато это как-то :) tie, boundTo, bindKey или что-то в этом духе, подобрав наименее конфликтный ключ.

Использование Proxy из ES6 здесь было бы куда симпатичней — обычный доступ к свойству вместо передачи ключа в метод — fn(foo.bar[_].baz), но без Proxy подобное (геттер, обход объекта и привязка всех методов), серьёзно не теряя при этом в производительности, мы пока себе позволить не можем.

Пример с Proxy, работает пока только в ночном огнелисе
var _ = Symbol();
Object.defineProperty(Object.prototype, _, {
  get: function(){
    return new Proxy(this, {
      apply: function(){ /* аналогично текущей логике [_] для обратной совместимости */ },
      get: function(context, name){
        return context[name].bind(context);
      }
    });
  }
});

['foobar', 'foobaz', 'barbaz'].filter(/bar/[_].test); // => ['foobar', 'barbaz']

var has = {}.hasOwnProperty[_].call;

console.log(has({key: 42}, 'foo')); // => false
console.log(has({key: 42}, 'key')); // => true

var array = []
  , push  = array[_].push;
push(1);
push(2, 3);
console.log(array); // => [1, 2, 3];

# Ограничение количества аргументов


Проблема необязательных аргументов рассмотрена в данной статье. Пример в ней — parseInt — довольно невразумительный, никто не мешает приводить строки к числу, например, используя Number, что не ожидает дополнительных аргументов. Дело тут не в их «опасности», а в необходимости писать лишнюю обертку.

Например, мы хотим вывести в консоль все элементы массива, только сами элементы и ничего больше:

[1, 2, 3].forEach(console.log);
// => 1 0 [1, 2, 3]
// => 2 1 [1, 2, 3]
// => 3 2 [1, 2, 3]

Метод .forEach, как и многие другие, передаёт коллбэку необязательные аргументы — индекс и сам массив. А нам они не нужны. Так что каждый раз коллбэк придется оборачивать в еще одну функцию:

[1, 2, 3].forEach(function(it){
  console.log(it);
}); // => 1, 2, 3

В статье, упомянутой выше, для ограничения аргументов функций был предложен метод Function#only. Реализуем его вариант. Первый аргумент — максимальное число аргументов, второй, опциональный — контекст. Пример:

[1, 2, 3].forEach(console.log.only(1)); // => 1, 2, 3

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

# Форматирование даты



Казалось бы, простая задача — форматирования даты, в JavaScript не такая уж и простая. Что делать, если нам нужно получить строку формата «18.11.2014 06:07:25»? Всё довольно страшно:

var date = new Date;
function lz2(it){
  return it > 9 ? it : '0' + it;
}
var format = [date.getDate(), date.getMonth() + 1, date.getFullYear()].map(lz2).join('.') + ' ' +
             [date.getHours(), date.getMinutes(), date.getSeconds()].map(lz2).join(':');
console.log(format); // => '18.11.2014 06:07:25 '

А что делать, если нужно получить, например, строку формата «Вторник, 18 Ноября 2014 г., 6:07:25»?

В начале статьи был упомянут стандарт интернационализации ECMA402, спецификация. Стандарт добавляет в JavaScript объект Intl, содержащий средства локализованного форматирования даты, чисел, сравнения строк. На базовом уровне Intl рассмотрен в этой статье. Ко всему прочему, этот стандарт перегружает методы Date#toLocaleString, Date#toLocaleDateString, Date#toLocaleTimeString, добавляя в них 2 аргумента: локализация и опции формата. Используя их, строку, близкую к упомянутому выше формату, можно получить так:

new Date().toLocaleString('ru-RU', {
  weekday: 'long',
  year:    'numeric',
  month:   'long',
  day:     'numeric',
  hour:    'numeric',
  minute:  '2-digit',
  second:  '2-digit'
}); // => 'вторник, 18 ноября 2014 г., 6:07:25'

Громоздко, конечно, но лучше уж так, чем никак. Касательно поддержки стандарта — в целом, неплохо. Поддерживается Chrome, Opera, IE11, с недавних пор и Firefox. Но обычно нужна поддержка IE10-, Safari, мобильных платформ и чёрт еще знает чего. На этот случай есть полифил. Но вот незадача — реализация данного функционала будет слишком много весить, даже без учета локалей. По этой причине в core.js и отсутствует полифил ECMA402.

# Добавим простое форматирование даты.


Что значит «простое»? Как часто вам нужна полноценная локализация или какие другие продвинутые средства работы с датой? Мне — не очень, обычно хочется простого удобного форматирования даты строкой формата. Ну а если нужны — никто не мешает подключить Moment.js или полифил Intl. Здесь же весь модуль работы с датой — несколько десятков строк.

Добавляем метод Date#format и его UTC версию Date#formatUTC (песочница):

new Date().format('W, D MM Y г., h:mm:ss', 'ru');    // => 'Вторник, 18 Ноября 2014 г., 6:07:25'
new Date().formatUTC('W, D MM Y г., h:mm:ss', 'ru'); // => 'Вторник, 18 Ноября 2014 г., 0:07:25'

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

s  | Секунды             | 0-59
ss | Секунды, 2 цифры    | 00-59
m  | Минуты              | 0-59
mm | Минуты, 2 цифры     | 00-59
h  | Часы                | 0-23
hh | Часы, 2 цифры       | 00-23
D  | Дата                | 1-31
DD | Дата, 2 цифры       | 01-31
W  | День недели, строка | Вторник
N  | Месяц               | 1-12
NN | Месяц, 2 цифры      | 01-12
M  | Месяц, строка       | Ноябрь
MM | Месяца, строка      | Ноября
Y  | Год, полный         | 2014
YY | Год, 2 цифры        | 14

Библиотека уже включает русскую (ru) и английскую (en) локали. Локаль задается либо методом core.locale, либо вторым аргументом методов Date#format и Date#formatUTC (песочница):

new Date().format('W, D MM Y', 'ru'); // => 'Вторник, 18 Ноября 2014'
new Date().format('W, D MM Y');       // => 'Tuesday, 18 November 2014'
core.locale('ru');
new Date().format('W, D MM Y');       // => 'Вторник, 18 Ноября 2014'

Формат локали представлен ниже. В собственном коде можно ограничиться core.addLocale, но из-за возможности сборки библиотеки без расширения нативных объектов, универсальный модуль-локаль будет выглядеть так:

(typeof core != 'undefined' ? core : require('core-js/library')).addLocale('ru', {
  weekdays: 'Воскресенье,Понедельник,Вторник,Среда,Четверг,Пятница,Суббота',
  months: 'Январ:я|ь,Феврал:я|ь,Март:а|,Апрел:я|ь,Ма:я|й,Июн:я|ь,Июл:я|ь,Август:а|,Сентябр:я|ь,Октябр:я|ь,Ноябр:я|ь,Декабр:я|ь'
});

Несколько примеров:

new Date().format('DD.NN.YY');         // => '18.11.14'
new Date().format('hh:mm:ss');         // => '06:07:25'
new Date().format('DD.NN.Y hh:mm:ss'); // => '18.11.2014 06:07:25'
new Date().format('W, D MM Y года');   // => 'Вторник, 18 Ноября 2014 года'
new Date().format('D MM, h:mm');       // => '18 Ноября, 6:07'
new Date().format('M Y');              // => 'Ноябрь 2014'

# Объектное API



# Первая проблема: в ECMAScript 5 добавлена возможность объявить геттеры и сеттеры в литерале объекта. А вот добавить геттеры / сеттеры уже существующих объектов можно только с использованием Object.defineProperty / Object.defineProperties, что вынуждает передавать на каждое свойство полный (если не хочется таких дополнительных опций, как неперечисляемость или невозможность переопределить) объект дескриптора, а это громоздко.

Кроме метода # Object.assign, в ECMAScript 6 планировалось добавить метод Object.mixin, который копировал свойства объекта-источника в целевой объект с учетом дескрипторов. Кроме этого, метод должен был переназначать родителя методов объекта-источника, получаемого через ключевое слово super. Однако, его решили переработать и отложили добавление в стандарт.

Добавим метод Object.define, работающий как описанный Object.mixin — копирующий свойства объекта-источника в цель с учетом дескрипторов, но не переопределяющий родителя, за отсутствием ключевого слова super в ECMAScript 5.

// Было:
Object.defineProperty(target, 'c', {
  enumerable: true,
  configurable: true,
  get: function(){
    return this.a + this.b;
  }
});

// Стало:
Object.define(target, {
  get c(){
    return this.a + this.b;
  }
});

# Вторая проблема: в ECMAScript 5 также добавлена возможность создания объекта без использования конструктора, через Object.create. Было бы неплохо добавлять собственные свойства объекта при создании, но вторым аргументом Object.create, как и Object.defineProperties, принимает объект, содержащий объекты дескрипторов свойств, что страшно громоздко.

Добавим метод Object.make — аналог Object.create, вторым аргументом ожидающий не объект дескрипторов, а простой объект, из которого копируются собственные свойства в создаваемый объект с учетом дескрипторов.

// Поверхностное копирование объекта с учетом прототипа и дескрипторов:
var copy = Object.make(Object.getPrototypeOf(src), src);

// Пример с наследованием:
function Vector2D(x, y){
  this.x = x;
  this.y = y;
}
Object.define(Vector2D.prototype, {
  get xy(){
    return Math.hypot(this.x, this.y);
  }
});
function Vector3D(x, y, z){
  Vector2D.apply(this, arguments);
  this.z = z;
}
Vector3D.prototype = Object.make(Vector2D.prototype, {
  constructor: Vector3D,
  get xyz(){
    return Math.hypot(this.x, this.y, this.z);
  }
});

var vector = new Vector3D(9, 12, 20);
console.log(vector.xy);  // => 15
console.log(vector.xyz); // => 25
vector.y++;
console.log(vector.xy);  // => 15.811388300841896
console.log(vector.xyz); // => 25.495097567963924

В ECMAScript 7 предлагают добавить метод Object.getOwnPropertyDescriptors, возвращающий, как ясно из его названия, объект, содержащий все дескрипторы собственных свойств объекта. Идеальная пара для создания второго аргумента Object.defineProperties и Object.create и, в некоторой степени, альтернатива нашим Object.make и Object.define. Вот только слишком громоздкая.

# Массивы



# Метод Array#includes (до недавнего времени — Array#contains, переименован из-за бага MooTools, пока доступен и по старому имени) планируется к добавлению в ECMAScript 7. Он банально проверяет вхождение элемента в массив. В отличии от Array#indexOf, использует алгоритм сравнения # SameValueZero и не игнорирует «дырки». Второй, опциональный, аргумент — стартовая позиция. Примеры:

[1, 2, 3].includes(2);        // => true
[1, 2, 3].includes(4);        // => false
[1, 2, 3].includes(2, 2);     // => false

[NaN].indexOf(NaN);           // => -1
[NaN].includes(NaN);          // => true
Array(1).indexOf(undefined);  // => -1
Array(1).includes(undefined); // => true

# А вот метод Array#turn — плод моей больной фантазии. Хотя, как выяснилось, не уникальный — ему аналогичен метод _.transform из LoDash. Это альтернатива методу Array#reduce для свёртки массива в произвольный объект-аккумулятор (по умолчанию — новый массив) без необходимости возвращать аккумулятор из коллбэка. Сигнатура метода и коллбэка аналогична Array#reduce. Можно прервать обход коллекции, вернув из коллбэка false. Примеры:

// Свёртка в словарь:
[1, 2, 3, 4, 5].reduce(function(memo, it){
  memo['key' + it] = !!(it % 2);
  return memo;
}, {}); // => {key1: true, key2: false, key3: true, key4: false, key5: true}

[1, 2, 3, 4, 5].turn(function(memo, it){
  memo['key' + it] = !!(it % 2);
}, {}); // => {key1: true, key2: false, key3: true, key4: false, key5: true}

// filter + map + slice, делаем лишнюю работу:
[1, 2, 3, 4, 5, 6, 7, 8, 9].map(function(it){
  return it * it;
}).filter(function(it){
  return it % 2;
}).slice(0, 2); // => [1, 9]

[1, 2, 3, 4, 5, 6, 7, 8, 9].turn(function(memo, it){
  it % 2 && memo.push(it * it);
  if(memo.length == 2)return false;
}); // => [1, 9]

# Числа



# Помните # пример с итерируемыми числами из главы про итераторы? Слишком вкусная и универсальная возможность, что бы отказываться от подобного в стандартной библиотеке. Очень краткий цикл, выполняемый заданное число раз, на базе for-of, простая генерация массива заданной длины через # Array.from (а может и # spread) и т.д. Так что добавим итератор чисел, хоть и не в такой примитивной реализации. Примеры:

// Классический цикл:
for(var i = 0; i < 3; i++)console.log(i); // => 0, 1, 2

// for-of с итератором числа:
for(var i of 3)console.log(i); // => 0, 1, 2

// При отсутствии for-of, хелпер:
$for(3).of(console.log); // => 0, 1, 2

// Генерация массива заданной длины:
// .map пропускает "дырки" в массиве
Array(10).map(Math.random); // => [undefined × 10]

// ES5 костыль, пара вспомогательных массивов:
Array.apply(undefined, Array(10)).map(Math.random); // => [0.9442228835541755, 0.8101077508181334, ...]

// ES6 костыль, заполняем вспомогательный массив:
Array(10).fill(undefined).map(Math.random); // => [0.5587614295072854, 0.009569905698299408, ...]

// Number Iterator:
Array.from(10, Math.random); // => [0.9817775336559862, 0.02720663254149258, ...]

Array.from(10); // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Array.from(10, function(it){
  return this + it * it;
}, .42); // => [0.42, 1.42, 4.42, 9.42, 16.42, 25.42, 36.42, 49.42, 64.42, 81.42]

// Comprehensions:
[for(i of 10)if(i % 2)i * i]; // => [1, 9, 25, 49, 81]

Dict((for(i of 3)['key' + i, !(i % 2)])); // => {key0: true, key1: false, key2: true}

$for(10).filter(function(i){
  return i % 2;
}).array(function(i){
  return i * i;
});  // => [1, 9, 25, 49, 81]

Dict($for(3).map(function(i){
  return ['key' + i, !(i % 2)];
})); // => {key0: true, key1: false, key2: true}

# Математические функции в Number.prototype — из разряда приятных мелочей. Точно также как в Sugar и MooTools, вынесем методы из объекта Math в Number.prototype. Тут и говорить особо нечего — контекст становится первым аргументом математической функции. Может и дублирует уже имеющийся, стандартизованный, функционал, но это довольно удобно :)

Отдельной строкой упомянем метод Number#random. Он возвращает случайное число между числом-контекстом и переданным аргументом (по умолчанию — 0).

Примеры:

3..pow(3);           // => 27
(-729).abs().sqrt(); // => 27

10..random(20);         // => Случайное число (10, 20), например, 16.818793776910752
10..random(20).floor(); // => Случайное целое [10, 19], например, 16

var array = [1, 2, 3, 4, 5];
array[array.length.random().floor()]; // => Случайный элемент массива, например, 4

# Экранирование спецсимволов



# Если я внезапно, под конец статьи, скажу, что JavaScript используется, в первую очередь, для работы с HTML, то великой тайны не открою. Для работы с HTML как на клиенте, так и на сервере нам требуется его экранировать. Кто-то может сказать, что это задача фреймворка или шаблонизатора. Вот только стоит ли для такой примитивной задачи их тянуть? Методы для экранирования HTML есть во всех стандартных библиотеках. В Sugar, Prototype, MooTools это методы escapeHTML и unescapeHTML в прототипе строки. Не будем нарушать данную традицию:

'<script>doSomething();</script>'.escapeHTML(); // => '&lt;script&gt;doSomething();&lt;/script&gt;'
'&lt;script&gt;doSomething();&lt;/script&gt;'.unescapeHTML(); // => '<script>doSomething();</script>'

# Часто возникает необходимость создать регулярное выражение из пользовательских данных, а для корректной / безопасной работы нужно экранировать и их. Методы для этого есть в Sugar, Prototype, MooTools, где-то как статический метод RegExp, где-то метод String.prototype. Давно обсуждается добавление такого метода в ECMAScript. Надеюсь, мы дождемся этого, а пока реализуем предложенный вариант в нашей библиотеке:

RegExp.escape('Привет -[]{}()*+?.,\\^$|'); // => 'Привет \-\[\]\{\}\(\)\*\+\?\.\,\\\^\$\|'

# Заключение



Ну вот как-то так.

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

Что касается планов на будущее библиотеки, они, в основном, раскиданы по тексту статьи. Еще нужно, конечно, оптимизировать производительность и лучше покрыть код тестами — с этим пока особо не заморачивался.

Интересует ваше мнение о том, что я, возможно, пропустил и что можно реализовать лучше.

Да. И еще, раз подобным страдать начал — скучно мне. Ищу интересный проект с достойной з/п.
Tags:
Hubs:
+77
Comments 40
Comments Comments 40

Articles