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

Один пацан писал все на 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, evalCrazy 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#contains.# 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#contains проверяет вхождение подстроки в строку. String#startsWith и String#endsWith проверяют, начинается ли или заканчивается ли строка на заданную подстроку. Эти 3 метода принимают дополнительный аргумент — стартовую позицию. Пример: 'foobarbaz'.contains ('bar'); // => true 'foobarbaz'.contains ('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©] # Также 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©.add (b).add©; 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),

© Habrahabr.ru