Ecma International — основанная в 1961 году ассоциация, деятельность которой посвящена стандартизации информационных и коммуникационных технологий. Изначально ассоциация называлась ECMA — European Computer Manufacturers Association, однако она сменила название в 1994 году в связи с глобализацией деятельности. Вследствие этого название Ecma перестало быть аббревиатурой и больше не пишется заглавными буквами. Когда JavaScript был создан, он был представлен Netscape и Sun Microsystems для Ecma, и они дали ему имя ECMA-262 ( псевдоним ECMAScript).
ECMAScript — стандарт, на котором основан JavaScript, его часто называют ES.
До ES2015 спецификации ECMAScript обычно назывались их редакцией. Таким образом, ES5 (пятая редакция стандарта) является официальным названием обновления спецификации ECMAScript, опубликованной в 2009 году. Обновленный стандарт обычно публикуется в июне.
С 2015 года было принято решение пересматривать стандарт один раз в год и именовать его в соответствии с годом принятия, но комьюнити по инерции продолжает использовать именование по порядку редакции стандарта.
Таким образом:
ES6 = ES2015
ES7 = ES2016
ES8 = ES2017
ES9 = ES2018
ES10 = ES2019
…
Браузеры - самая распространенная среда исполнения JavaScript, в свою очередь, имеют свой релизный цикл и свои темпы разработки и имплементации новых стандартов.
Например мажорная версия Safari выходит вместе с выходом новой MacOs в сентябре, промежуточные обновления выходят в среднем раз в полтора месяца.
Chrome, FireFox, Opera, их мобильные версии - все выходят в разное время, более того, если вышла новая версия, поддерживающая новый стандарт, огромное количество пользователей продолжают пользоваться более старыми версиями браузеров, или просто не обновляя версию, или будучи заложниками привязки определенной версии железа и операционной системы к определенной версии браузера.
В итоге, появившиеся в последнем стандарте EcmaScript нововведения языка еще долго не могут быть напрямую использованы в браузерах пользователей.
Существуют инструменты, которые преобразуют код написанный на более новой версии стандарта для исполнения в старых браузерах. К таким инструментам, например, относится транспилятор Babel.
Что он делает?
Он берет новый синтаксис языка, и, в соответствии с конфигурацией и подключенными плагинами, переводит его в версию,
понятную старым браузерам.
Например в версии ES6 появилась возможность объявлять переменные через let
и const
let a = 3
const b = 4
В ES5 же были доступны только var
var a = 3
var b = 4
Кроме того, используя плагины (например @babel/preset-env
), можно настроить Babel для поддержки старых версий, добавив
polyfill’ы отсутствующих методов и браузерных API.
Polyfill’ы - это кусочки кода, которые запускаются перед началом исполнения вашего скрипта, и добавляют браузеру недостающие
методы.
Например, магическая фраза в конфигурации плагина last 2 Chrome versions
скажет Babel загрузить базу данных всех версий браузеров
на рынке, выбрать из них две последние версии браузера Chrome, и загрузить список поддерживаемых ими методов и API.
После этого, Babel, оценив ваш код и выяснив какие именно фичи вы использовали при его написании, добавит в начало выходного
файла polyfill’ы.
Здесь надо отметить, что это не всегда возможно, полифиллы сами по себе будут написаны на том же JS, только более старой версии, и если вам требуется какая-то принципиально несовместимая со старым браузером функциональность - то ничего не получится. Например если старая версия браузера не имела доступа до сервиса геолокации на вашем телефоне, то как код не преобразовывай - она не появится.
Однако много что таки можно реализовать, давайте попробуем сделать это сами.
Array.prototype.flat()
- это метод массивов, уплощающий список вплоть до указанной глубины.
Например:
let array = [1, [2, 3], [[4, 5], [6, 7], [8, 9]]]
array.flat() // -> [1,2,3,4,5,6,7,8,9]
array.flat(2) // -> [1,2,3,[4,5],[6,7],[8,9]]
Давайте представим что мы разработчик Babel плагина в 2017 году (текущий стандарт на тот момент ES7/ES2016), и мы хотим реализовать функциональность, которая уже существует в других языках, а в ECMAScript появится только через три года в стандарте ES10(ES2019)
let foldedList = [1, [[2, 3, 4], [5, 6, 7], [8, 9, [10, 11, 12]]], [13, 14, 15], [], [16, 17, 18], [19, 20]]
console.log(foldedList.flat())
/* -> [1, 2, 3, 4, 5, 6, 7,
8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20 ]
*/
В этом примере я так же разберу работу генераторов, которые уже стали частью стандарта в ES7/ES2016, мы попробуем
написать полифилл с их помощью. Особенный интерес представляют циклические генераторы с синтаксисом yield*
Про работу генераторов и итераторов можно более детально почитать тут
Как бы тогда мог выглядеть наш полифилл?
Сам полифилл я буду реализовывать не в виде метода класса Array, а задам метод конкретного объекта массива, чтобы
можно было сравнить поведение нативного метода с реализованным нами, запустив в рантайме, поддерживающем ES10. Чтобы
полифилл стал настоящим вместо
foldedList.flat = function (maxDepth) ...
надо будет просто написать Array.prototype.flat = function (maxDepth) ...
Также, проведя этот эксперимент, вы заметите что нативный метод делает по-умолчанию развертку массива до глубины 2, а
наш полифилл - до максимальной глубины вложенности. Это поведение конечно можно поменять, заменив Infinity
на 2
.
Начнем с создания простого генератора, который будет возвращать нам каждый элемент массива по прядку:
function* flatteningGenerator(foldedList) {
// перечисляем все элементы массива, и возвращаем их из генератора один за другим
for (let i = 0; i < foldedList.length; i++) {
yield foldedList[i]
}
}
Одна из замечательных особенностей JS - возможность писать рекурсивные функции. К ним добавилась возможность писать
вложенные генераторы.
Оператор yield*
в отличие от обычного yield
, получив аргументом инструкции другой генератор, не просто
возвращает его, а начинает циклически обрабатывать инструкции yield
полученного генератора. Таким образом, добавив
рекурсию мы можем развернуть список до самых глубин вложенности, написав простую конструкцию:
function* flatteningGenerator(foldedList) {
for (let i = 0; i < foldedList.length; i++) {
// Если текущий элемент - не массив, то просто верни его
if (!Array.isArray(foldedList[i])) yield foldedList[i]
// а если сам элемент является массивом, то начни по очереди возвращать его элементы
else yield* flatteningGenerator(foldedList[i])
}
}
Добавим возможность управлять глубиной, до которой мы хотим разворачивать список:
// maxDepth - максимальная, на которую нам позволено забраться, depth - текущая глубина на которой мы находимся в ходе развертки
function* flatteningGenerator(foldedList, maxDepth = Infinity, depth = 1) {
for (let i = 0; i < foldedList.length; i++) {
if (!Array.isArray(foldedList[i])) yield foldedList[i]
else {
depth < maxDepth
? yield* flatteningGenerator(foldedList[i], maxDepth, depth + 1)
: yield foldedList[i]
}
}
}
Остается определить у нашего исходного массива foldedList
метод .flat()
, задав его как
итератор descendant[Symbol.iterator]
объекта
обертки descendant
(чтобы не переопределять итератор foldedList
), осуществив развертку в контексте исходного
массива foldedList
, при помощи написанного ранее рекурсивного генератора flatteningGenerator
.
function* flatteningGenerator(foldedList, maxDepth = Infinity, depth = 1) {
for (let i = 0; i < foldedList.length; i++) {
if (!Array.isArray(foldedList[i])) yield foldedList[i]
else {
depth < maxDepth
? yield* flatteningGenerator(foldedList[i], maxDepth, depth + 1)
: yield foldedList[i]
}
}
}
let foldedList = [1, [[2, 3, 4], [5, 6, 7], [8, 9, [10, 11, 12]]], [13, 14, 15], [], [16, 17, 18], [19, 20]]
console.log('native flat()', Array.prototype.flat.call(foldedList))
foldedList.flat = function (maxDepth) {
const descendant = Object.create(this)
descendant[Symbol.iterator] = function () {
return flatteningGenerator(this, maxDepth)
}
return [...descendant]
}
console.log('custom flat()', foldedList.flat())
На выходе получим то, что и ожидалось, уплощенный развернутый массив:
native flat() [
1,
[ 2, 3, 4 ],
[ 5, 6, 7 ],
[ 8, 9, [ 10, 11, 12 ] ],
13,
14,
15,
16,
17,
18,
19,
20
]
custom flat() [
1, 2, 3, 4, 5, 6, 7,
8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20
]
Надеюсь мне удалось показать, как на примере синтаксиса, доступного в стандарте ES7, можно написать polyfill для метода, который появился в стандарте ES10 только три года спустя, а так же с какими трудностями приходится сталкиваться frontend разработчикам из-за разнообразия браузеров и сред исполнения, вызванного неравномерной реализацией стандата в разных браузерах.