Кирилл Мыльников

Frontend разработчик в Usetech

Всем привет, я — Кирилл Мыльников, frontend разработчик компании Usetech.

Сегодня хочу рассказать о полифилах JavaScript: что это и зачем они нужны? На практике мы реализуем несколько полифилов: map, forEach, filter, reduce. 

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

Итак, начнём с определения полифила, а затем перейдём к методам.

Что такое полифил?

Полифил — это код, реализующий какую-то функциональность, которая не поддерживается в некоторых браузерах. Реализация собственного полифила обеспечивает единообразное поведение функциональности в разных браузерах.

Как я писал выше, сегодня мы будем реализовывать несколько полифилов: map, forEach, filter, reduce.

Метод map

Метод map вызывает функцию для каждого элемента и возвращает новый массив.  Аргумент функции принимает три значения:

  1. Элемент массива;

  2. Индекс данного элемента;

  3. Сам массив.

Реализуем полифил на примере:

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;
}

А теперь поэтапно разберём, что тут происходит. Сначала нам нужно обработать возникающие ошибки:

  1. Функцию обратного вызова могут не передать;

  2. Данный метод вызывается не для массива.

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 нужно учесть несколько моментов:

  1. Он используется только для перебора и ничего не возвращает;

  2. Изменяет оригинальный массив.

Реализация полифила будет очень похожа на метод 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. В основном его применяют для вычисления какого-нибудь единого значения на основе всего массива. Функция применяется по очереди ко всем элементам и переносит свой результат на следующий вызов.

Аргументы функции:

  1. previousValue — результат предыдущего вызова;

  2. item — элемент массива;

  3. index — индекс данного элемента;

  4. 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)


  1. kovserg
    09.09.2022 08:19
    +10

    «Полифилы». Есть же хорошее слово — костыли.


  1. lexasss
    09.09.2022 09:22
    +3

    Ожидал увидеть список браузеров где не поддерживается ES6. У вас какой-то специфический заказчик с браузером 8-10 -летней давности? Или что-то не-браузерное с зафиксированным движком?


    1. rock
      09.09.2022 15:24

      Хмм, а какие браузеры полностью поддерживают ES6? Я ни одного не знаю. Например, оптимизация хвостовой рекурсии (вернее, PTC) поддерживается сейчас только в Safari.


      1. lexasss
        09.09.2022 16:10

        Вопрос был в конексте статьи и относился к тем четырём функциям Array добавленным в ES6, речь о полной поддержке не шла. Только я в спешке его не совсем корректно задал: автор в начале статьи обещал что раскажет

        о полифилах JavaScript: что это и зачем они нужны?

        но так и не рассказал, зачем же нужны именно эти полифилы. Надеюсь, ответит позже здесь ниже.


        1. rock
          09.09.2022 16:30

          Данные методы были добавлены в ES5 (IE9), ES6 разве что незначительно меняет их семантику - но тут даже о корректности по ES5 говорить не приходится -)


  1. 13DeadGnomes
    09.09.2022 09:56
    +1

    Я так понимаю что данные методы реализованы в основном для доэджевой эпохи интернет эксплореров. Ослами сейчас пользуются по статистике яндекса и лайвинета 0.19% и я так понимаю по большей части из-за специфики организаций(типа гос.структур, где это предусмотрено регламентом и какими-то местными шаманскими уставами). Есть ли вообще сейчас смысл в подобных фичах? Может я не прав.


    1. juwon22
      09.09.2022 13:33

      Также я написал, что эта статья в основном для подготовки к собеседованию, часто такие вопросы спрашивают


  1. mayorovp
    09.09.2022 10:14
    +6

    Вот только то, что приведено в посте — это не полифилы. Полифил метода map должен так и называться — map, а вовсе не myMap.


    1. gruzoveek
      09.09.2022 11:13
      +3

      Точно. И еще нужна проверка на наличие такой функции


  1. 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 и работают с учетом этой ошибки и могут падать на вашем полифиле.


  1. Taritsyn
    09.09.2022 12:39

    До перехода на core-js, я долгое время использовал в качестве ES5-полифилов наработки Дугласа Крокфорда: ES5 и JSON-js. По мере выхода новых версий языка, также пользовался полифилами из MDN.


  1. rock
    09.09.2022 18:30
    +3

    Давайте разберем, что не так в ваших полифилах, на примере .map.

    1. Эти методы должны быть объявлены как неперечесляемые свойства.

    Зачем? Есть такие странные люди, что обходят массивы при помощи цикла for-in:

    var array = [1, 2, 3];
    for (var key in array) console.log(array[key]);
    // => 1
    // => 2
    // => 3
    // => function myMap !!!
    

    Подобные "полифилы" очень часто ломает чужой код. И это, пожалуй, самая популярная ошибка, при их написании.

    1. .map и прочие методы массива, добавленные в ES5, игнорируют дырки в массивах.

    Array(5).map((_, i) => i); // => [empty × 5]
    Array(5).myMap((_, i) => i); // => [0, 1, 2, 3, 4]
    
    1. Как выше упомянул @kahi4, длина массива должна запоминаться до начала итерации

    const array = [1];
    array.map(i => array.push(i)); // => [2]
    array.myMap(i => array.push(i)); // => бесконечный цикл
    

    Кроме того, в этот момент длина должна приводиться с помощью внутренней операции ToLength - но это уже мелочи жизни.

    1. С 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
    
    1. "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']
    
    1. и далее. Обычно, полифилы данных методов, добавленных в ES5, рассчитаны на ES3 браузеры - и у них есть своя специфика.

    Например, как было показано на примерах выше, данные методы - дженерики. Это значит, что они могут работать не только на массивах - но и на любых array-like сущностях - например, строках. Вот только в старых движках (IE8-, где строки не индексированы) без дополнительных костылей с этим проблемы - а где-то, например в старых версиях V8, есть соответствующие баги. И это только один момент специфики старых движков, что нужно учитывать.

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

    var myMap = [].map || function map() { /* ... */ };
    

    но обычно все куда сложнее.

    Пожалуйста, не пишите полифилы ES для использования в вашем проекте сами, если на все 100 не уверены, что без этого никак - обычно, можно найти уже готовые, проверенные, где кол-во ошибок сведено к минимуму.