Полифилим правильно

Давеча накидал вариант полифила для [].flat . Показал быстрое решение, но если вникать в детали, оно не очень верное. Если заглянуть в спецификацию, то увидим что у данного метода есть аргумент — depth.

Собственно на этот косяк мне указал Roman Dvornov, увидев мой полифил. Так же Рома предложил вариант еще короче. Пойдем по порядку, мой вариант был таким:

if (!Array.prototype.flat) Array.prototype.flat = function () {
return (function f(arr) {
return arr.reduce(
(a, v) =>
Array.isArray(v)
? a.concat(f(v))
: a.concat( v )
, []
)
})(this)
};
// Usage
[1,2,3,[1,2,3,4, [2,3,4]]].flat()
// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

Рома верно заметил, что тут лишняя функция. Можно ее убрать и получим:

Array.prototype.flat = function () {
return this.reduce(
(a, v) =>
Array.isArray(v)
? a.concat(v.flat())
: a.concat(v)
, []
)
};

Когда я пробовал такой (похожий вариант) похоже я где-то опечатался, так как у меня вызывало это бесконечную рекурсию. Возможно я ошибся и что-то напутал, поэтому я и сделал решение с карированием. Так что вариант реально можно упростить. Но если возвращаться именно к полифилу, то правильнее было бы реализовать его полностью вместе с флагом depth. На MDN уже есть вариант готового полифила:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat#Polyfill

if (!Array.prototype.flat) {
Array.prototype.flat = function() {
var depth = arguments[0];
depth = depth === undefined ? 1 : Math.floor(depth);
if (depth < 1) return Array.prototype.slice.call(this);
return (function flat(arr, depth) {
var len = arr.length >>> 0;
var flattened = [];
var i = 0;
while (i < len) {
if (i in arr) {
var el = arr[i];
if (Array.isArray(el) && depth > 0)
flattened = flattened.concat(flat(el, depth - 1));
else flattened.push(el);
}
i++;
}
return flattened;
})(this, depth);
};
}

Вообще не самый красивый и оптимальный код, как по мне и я согласен с Романом, что все это можно было бы сделать проще (как минимум используя меньше кода). Рома предложил такой вариант:

if (!Array.prototype.flat) Array.prototype.flat = function (depth = 1) {
depth = isNaN(depth) ? 0 : Math.floor(depth);
if (depth < 1) return this.slice();
if (depth === 1) return [].concat(...this);
return [].concat(...this.map(v => Array.isArray(v) ? v.flat(depth - 1) : v));
};

Если отрефакторить этот код, то можно сделать его еще чуточку короче:

if (!Array.prototype.flat)
Array.prototype.flat = function (depth = 1) {
return [].concat(
...((isNaN(depth) ? 1 : Math.floor(depth)) < 2)
? this
: this.map(v => Array.isArray(v) ? v.flat(depth - 1) : v)
)
};

Так как согласно документации по дефолту depth = 1, то любое невалидное значение я бы предложил воспринимать как дефолтное, т.е. = 1. Поэтому в любой непонятной ситуации depth=1. Но!

Но согласно тому же полифилу от Мозиллы и спецификации в случае если передается аргумент меньше 1, то нужно вернуть копию массива как есть:

if (depth < 1) return Array.prototype.slice.call(this);

Поэтому вариант от Романа более точный и верный. Итого, имеем финальный правильный вариант, соответствующей спеке:

if (!Array.prototype.flat)
Array.prototype.flat = function (depth = 1) {
depth = isNaN(depth) ? 0 : Math.floor(depth);
if (depth < 1) return this.slice();
return [].concat(
...(depth < 2)
? this
: this.map(v => Array.isArray(v) ? v.flat(depth - 1) : v)
)
};

Итого такое вот UPD по теме: как сделать плоским массив 🙂

Я понимаю почему в MDN такой большой код — он сделан в ES5 и должен работать в старых IE. Но если у вас ES6+, то можно полифилить используя современный синтаксис. Спасибо Роману за ревью и примеры.

Итоговый вариант полифила я добавил в русскую версию документации на MDN:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat#Polyfill