Кирилл Мыльников
Frontend разработчик в Usetech
Всем привет, я — Кирилл Мыльников, frontend разработчик компании Usetech.
Сегодня хочу рассказать о полифилах JavaScript: что это и зачем они нужны? На практике мы реализуем несколько полифилов: map, forEach, filter, reduce.
Эта статья подойдёт новичкам, которые готовятся к собеседованию, и опытным специалистам. В комментариях вы можете рассказать о том, как реализуете полифилы в своей работе.
Итак, начнём с определения полифила, а затем перейдём к методам.
Что такое полифил?
Полифил — это код, реализующий какую-то функциональность, которая не поддерживается в некоторых браузерах. Реализация собственного полифила обеспечивает единообразное поведение функциональности в разных браузерах.
Как я писал выше, сегодня мы будем реализовывать несколько полифилов: map, forEach, filter, reduce.
Метод map
Метод map вызывает функцию для каждого элемента и возвращает новый массив. Аргумент функции принимает три значения:
Элемент массива;
Индекс данного элемента;
Сам массив.
Реализуем полифил на примере:
Array.prototype.myMap = function (callback, arg) {
if (this == null || this === window) {
throw TypeError('myMap called on null or undefined')
}
if (typeof callback !== 'function') {
throw TypeError(`{callback} is not a function`)
}
const newArray = [];
for (let i = 0; i < this.length; i++) {
newArray[i] = callback.call(arg, this[i], i, this)
}
return newArray;
}
А теперь поэтапно разберём, что тут происходит. Сначала нам нужно обработать возникающие ошибки:
Функцию обратного вызова могут не передать;
Данный метод вызывается не для массива.
this === null || this === window
, условие сработает в том случае, если метод вызывается как отдельная функция.
Пример:
const myMap = Array.prototype.myMap;
myMap();
Внутри функции myMap this уже будет как global, не в строгом режиме будет window, в строгом undefined. Для этого кейса мы выкидываем ошибку.
Также функцию обратного вызова могут не передать, и на этот случай мы делаем проверку на typeof callback === ‘function’
Наша функция принимает второй аргумент arg. Для чего это нужно? Если наша функция обратного вызова должна быть вызвана в контексте, для внутреннего callback должно быть установлено значение arg. Это можно сделать с помощью call()
.
Вот таким простым способом мы с вами реализовали полифил метода map. Теперь перейдём к следующему методу.
Метод forEach
При реализации следующего полифила метода forEach нужно учесть несколько моментов:
Он используется только для перебора и ничего не возвращает;
Изменяет оригинальный массив.
Реализация полифила будет очень похожа на метод map. Посмотрим на примере:
Array.prototype.myForEach = function (callback, arg) {
if (this == null || this === window)
throw TypeError('myForEach called on null or undefined');
if (typeof callback !== 'function')
throw TypeError(`${callback} is not a function`);
for (let i = 0; i < this.length; i++) {
callback.call(arg, this[i], i, this);
}
};
Проверки остались такими же, как и реализация, только мы ничего не возвращаем, а просто перебираем.
Метод filter
Этот метод возвращает новый массив всех подходящих элементов. Посмотрим пример:
Array.prototype.myfilter = function (callback, arg) {
if (this == null || this === window)
throw TypeError('myfilter called on null or undefined');
if (typeof callback !== 'function')
throw TypeError(`${callback} is not a function`);
const newArr = [];
for (let i = 0; i < this.length; i++) {
if (callback.call(arg, this[i], i, this)) newArr.push(this[i]);
}
return newArr;
};
Как вы видите, в этом случае присутствует обработка ошибок, как и у всех.
Единственное, что поменялось, это то, что мы написали проверку, удовлетворяющую условию, перед тем как добавить в массив и вернуть его.
Перейдём к последнему методу — reduce.
Метод reduce
Прежде чем приступить к разбору на практике, нужно вспомнить, как работает метод reduce. В основном его применяют для вычисления какого-нибудь единого значения на основе всего массива. Функция применяется по очереди ко всем элементам и переносит свой результат на следующий вызов.
Аргументы функции:
previousValue — результат предыдущего вызова;
item — элемент массива;
index — индекс данного элемента;
array — сам массив.
Теперь перейдём к реализации полифила reduce:
Array.prototype.myReduce = function (callback, initValue) {
if (this === null || this === window)
throw TypeError('myReduce called on null or undefined');
if (typeof callback !== 'function')
throw TypeError(`${callback} is not a function`);
let previousValue = initValue;
let startIndex = 0;
if (initValue === null) {
previousValue = this[0];
startIndex = 1;
}
if (previousValue == null)
throw TypeError('Reduce of empty array with no initial value');
for (let index = startIndex; index < this.length; index++) {
previousValue = callback(previousValue, this[index], index, this);
}
return previousValue;
};
Первые две проверки я уже описывал выше. Но появилась новая проверка: если previousValue будет undefined, если массив пуст и не указан initialValue, то тоже выдаём ошибку.
Второй аргумент reduce initialValue необязательный, и он используется для инициализации previousValue. Если он не указан, то мы инициализируем первый элемент массива, и начинаем обход со второго элемента.
Мы разобрали несколько примеров реализации полифилов, а также возникающие ошибки и методы их исправления. Спасибо за то, что уделили время прочтению статьи. А в комментариях можете поделиться своим опытом создания и реализации полифилов.
Комментарии (12)
lexasss
09.09.2022 09:22+3Ожидал увидеть список браузеров где не поддерживается ES6. У вас какой-то специфический заказчик с браузером 8-10 -летней давности? Или что-то не-браузерное с зафиксированным движком?
rock
09.09.2022 15:24Хмм, а какие браузеры полностью поддерживают ES6? Я ни одного не знаю. Например, оптимизация хвостовой рекурсии (вернее, PTC) поддерживается сейчас только в Safari.
lexasss
09.09.2022 16:10Вопрос был в конексте статьи и относился к тем четырём функциям Array добавленным в ES6, речь о полной поддержке не шла. Только я в спешке его не совсем корректно задал: автор в начале статьи обещал что раскажет
о полифилах JavaScript: что это и зачем они нужны?
но так и не рассказал, зачем же нужны именно эти полифилы. Надеюсь, ответит позже здесь ниже.
rock
09.09.2022 16:30Данные методы были добавлены в ES5 (IE9), ES6 разве что незначительно меняет их семантику - но тут даже о корректности по ES5 говорить не приходится -)
13DeadGnomes
09.09.2022 09:56+1Я так понимаю что данные методы реализованы в основном для доэджевой эпохи интернет эксплореров. Ослами сейчас пользуются по статистике яндекса и лайвинета 0.19% и я так понимаю по большей части из-за специфики организаций(типа гос.структур, где это предусмотрено регламентом и какими-то местными шаманскими уставами). Есть ли вообще сейчас смысл в подобных фичах? Может я не прав.
juwon22
09.09.2022 13:33Также я написал, что эта статья в основном для подготовки к собеседованию, часто такие вопросы спрашивают
kahi4
09.09.2022 11:13+4Отвечу на пару комментариев сверху: хоть map, reduce и прочие уже 7 лет как везде есть, есть другие полифилы, которые по сей день выполняют свою работу. Статья же учебная, показывает что такое полифилл на простых и понятных примерах.
Но только это вредная статья и вот почему:
и опытным специалистам
не льстите себе
Полифил — это код, реализующий какую-то функциональность, которая не поддерживается в некоторых браузерах.
Не "какая-то", а крайне конкретная. Полифил по определению исключительно внесение новых API из спецификации в старые рантаймы. Вы не можете изменить прототип Array методом myMap и назвать это полифилом. Это будет просто патчингом прототипа, а это -- плохая практика. Так же ваш полифил обязан вести себя как метод в спецификации, потому что какая-то библиотека может использовать этот метод и завалиться, потому что ожидала другого поведения.
В статье, например, это правило не удовлетворено:
const foo = [5]; // оригинал foo.map((i) => { foo.push(i) }); // > [undefined] // теперь ваш myMap foo.myMap((i) => { foo.push(i) }); // падает в бесконечный цикл
И какая-то слегка криво написанная библиотека прекрасно может положить ваш код.
Как я писал выше, сегодня мы будем реализовывать несколько полифилов: map, forEach, filter, reduce.
А теперь что-то по-интереснее, типа генератора, можно? А можно (вопрос с подвохом) на proxy? Да и core-js все эти методы реализует хитрым способом за один раз: https://github.com/zloirock/core-js/blob/master/packages/core-js/internals/array-iteration.js
И далеко не все полифилы -- это обновление прототипа. BigInt самостоятельный такой объект.
Еще пункт: вообще сперва хорошо бы проверить, нет ли уже этого метода (поставленного другим полифилом или нативно в браузере)
А в комментариях можете поделиться своим опытом создания и реализации полифилов.
А вот тут самое главное: НЕ ПИШИТЕ СВОИ ПОЛИФИЛЫ. Для 99% подойдет core-js. Вместе с babel-env он умеет определять что используется в вашем коде и что нужно под какие-то браузеры и самостоятельно их расставлять и tree shake. И эта библиотека оттестирована миллионами разработчиков и миллиардами пользователей. Хуже того -- даже если там и есть ошибка, все остальные библиотеки используют core-js и работают с учетом этой ошибки и могут падать на вашем полифиле.
rock
09.09.2022 18:30+3Давайте разберем, что не так в ваших полифилах, на примере
.map
.Эти методы должны быть объявлены как неперечесляемые свойства.
Зачем? Есть такие странные люди, что обходят массивы при помощи цикла
for-in
:var array = [1, 2, 3]; for (var key in array) console.log(array[key]); // => 1 // => 2 // => 3 // => function myMap !!!
Подобные "полифилы" очень часто ломает чужой код. И это, пожалуй, самая популярная ошибка, при их написании.
.map
и прочие методы массива, добавленные в ES5, игнорируют дырки в массивах.
Array(5).map((_, i) => i); // => [empty × 5] Array(5).myMap((_, i) => i); // => [0, 1, 2, 3, 4]
Как выше упомянул @kahi4, длина массива должна запоминаться до начала итерации
const array = [1]; array.map(i => array.push(i)); // => [2] array.myMap(i => array.push(i)); // => бесконечный цикл
Кроме того, в этот момент длина должна приводиться с помощью внутренней операции
ToLength
- но это уже мелочи жизни.С ES6, для поддержки субклассинга,
.map
использует@@species
паттерн, его поддержка реализуется в несколько дополнительных строк.
class MyArray extends Array {} MyArray.of(1, 2, 3).map(i => i) instanceof MyArray; // => true MyArray.of(1, 2, 3).myMap(i => i) instanceof MyArray; // => false
"
this === null || this === window
условие сработает в том случае, если метод вызывается как отдельная функция" - что это?
В спецификации прописаны 2 вещи:
this
приводится к объекту с помощью операцииToObject
- данная операция кидает ошибку наnull
иundefined
.Метод должен быть задан в строгом режиме.
По идее, никто не запрещает запускать
.map
наwindow
:[].map.call(window, it => it); // => [global]
А у вас, кроме этого, полифил не будет работать в окружении, отличном от браузера, где нет
window
- та же NodeJS.Если ваш метод таки задается в строгом режиме, что явно не прописано (
'use strict'
- но может быть и контекст модуля), проблем у вас больше. Например, примитивы не будут приводиться к объектам:[].map.call('1', (v, i, o) => typeof o); // => ['object'] [].myMap.call('1', (v, i, o) => typeof o); // => ['string']
и далее. Обычно, полифилы данных методов, добавленных в ES5, рассчитаны на ES3 браузеры - и у них есть своя специфика.
Например, как было показано на примерах выше, данные методы - дженерики. Это значит, что они могут работать не только на массивах - но и на любых array-like сущностях - например, строках. Вот только в старых движках (IE8-, где строки не индексированы) без дополнительных костылей с этим проблемы - а где-то, например в старых версиях V8, есть соответствующие баги. И это только один момент специфики старых движков, что нужно учитывать.
Полифил должен обнаруживать уже имеющуюся нативную фичу и использовать её, если есть такая возможность. Хотя бы так,
var myMap = [].map || function map() { /* ... */ };
но обычно все куда сложнее.
Пожалуйста, не пишите полифилы ES для использования в вашем проекте сами, если на все 100 не уверены, что без этого никак - обычно, можно найти уже готовые, проверенные, где кол-во ошибок сведено к минимуму.
kovserg
«Полифилы». Есть же хорошее слово — костыли.