Итерируем всё вместе с Collection

в 10:03, , рубрики: javascript, node.js, Веб-разработка, итераторы, коллекции, функциональное программирование, метки: , , , ,

В современном JavaScript существует целый пласт различных видов итерируемых структур данных:

  • Array
  • TypedArrays
  • Object
  • Map
  • Set
  • String

А также появились генераторы и универсальный протокол @@iterator, который позволят итерировать что угодно и как угодно. И в тоже время для некоторых типов данных (в основном для массивов) определены различные методы для удобной функциональной работы и простого итерирования, например, forEach, map или reduce, и всё было бы хорошо, однако:

  • Большинство методов определены только для массивов;
  • Определяемое API слишком примитивно и не покрывает целый ряд задач;
  • До сих пор скорость выполнения нативных итераторов далека от идеала и существенно уступает реализации на циклах.

Большинство методов определены только для массивов

Тут в общем всё ясно, но всё же рассмотрим на примере

// Допустим есть некоторый объект
var obj = {a: 1, b: 2, c: 3};

// Чтобы перебрать его элементы мы либо пишем цикл
for (var key in obj) {
    if (!obj.hasOwnProperty(key)) {
        continue;
    }

    console.log(obj[key]);
}

// Либо такой хак
Object.keys(obj).forEach(function (key) {
    console.log(obj[key]);
});

// А очень хотелось бы просто делать так
obj.forEach(function (el) {
    console.log(el);
});

Разумеется, что никаких map, reduce, filter и т.д. и в помине также нет и если для простого Object можно поплясать с Object.keys, то для Map или Set уже всё хуже. Давайте напишем несколько примеров с использованием Collection.

$C({a: 1, b: 2, c: 3}).forEach(function (el) {
    console.log(el);
});

$C(new Set([1, 2, 3, 4])).filter(function (el) { return el > 1; }) // Set([2, 3, 4])

$C(new Map([[1, 2], [2, 1]])).map(function (el) { return el * 2; }) // Map([[1, 4], [2, 2]])

$C(function *() {
    for (var i = 0; i < 5; i++) {
        yield i;
    }
}).reduce(function (res, el) { res += el; return res; }, 0) // 10

Как видите, что всё отличие от нативных методов заключается в добавлении конструктора $C, т.к. большинство методов Collection похожи на встроенное API массивов, хотя есть и отличия, главным из которых является отсутствие параметра для передачи ссылки на this, т.к. this внутри Collection ссылается на сам экземпляр (это сделано для использования контекстных методов), а если нужно передать свой this, то для этого лучше использовать arrow function (тем более сейчас уже есть неплохие трансляторы ECMAScript6), переменные замыкания или .bind (хотя не рекомендуется, т.к. bind функции работают значительно медленнее).

Определяемое API слишком примитивно и не покрывает целый ряд задач

К сожалению это так, вот самые частые грабли

// Невозможность прервать операцию
[1, 2, 3, 4].forEach(function (el) {
    if (el == 2) {
        break; // Будет ошибка
    }
});

// Приходится делать так
[1, 2, 3, 4].some(function (el) {
    if (el == 2) {
        return true;
    }
});

// В Collection это делается элементарно в любом итерационном методе
$C([1, 2, 3, 4]).forEach(function (el) {
    if (el == 2) {
        this.break();

        // Если нужно также прервать текущее выполнение, то
        return false;
    }
});
// Невозможность изменить направление итерации,
// худший способ (но нужно помнить, что reverse меняет исходный объект)
[1, 2, 3, 4].reverse().forEach(function (el) {
    ...
});

// В Collection все итерационные методы могут принимать параметр reverse
$C([1, 2, 3, 4]).forEach(function (el) {
    ...
}, {reverse: true});
// Невозможность "перезапустить" выполнение текущего итератора,
// но в Collection можно сделать
$C([1, 2, 3, 4]).forEach(function (el) {
    this.reset();
});
// Невозможность изменить итерационный индекс, т.е.
[1, 2, 3, 4].forEach(function (el, i, data) {
    console.log(el);
    
    if (el == 2) {
        data.splice(i, 1);
        i--; // Не будет работать :(
    }
}) // 1 2 4, т.е. из-за того, что splice изменил исходный массив элемент 3 был пропущен

// В Collection
$C([1, 2, 3, 4]).forEach(function (el, i, data) {
    console.log(el);
    
    if (el == 2) {
        data.splice(i, 1);
        this.modi(-1);
    }
}, {live: true}) // 1 2 3 4

Collection вводит целый ряд параметров, которые работают во всех методах и с любыми структурами данных, а также пласт дополнительных методов.

До сих пор скорость выполнения нативных итераторов далека от идеала и существенно уступает реализации на циклах

Конечно VM развиваются, и от версии к версии скорость выполнения растёт, но до сих пор отрыв между нативными итераторами и циклами есть, но не в Collection :) В Collection каждая операция перед запуском предварительно изучается: исследуются входные параметры, тип данных, количество аргументов в функции callback и т.д. и на основе этой информации генерируется цикл, который будет выполнять итерации и делает это очень быстро. Разумеется применяются методики кеширования циклов по ключам оптимизации, чтобы свести к минимуму оверхед на запуске итератора. Но главная цель этих оптимизаций не делать работу за JIT VM, а добиться, чтобы богатый функционал библиотеки не сказывался на скорости её работы.

Заключение

Статья получилось совсем короткой, т.к. мне не хотелось дублировать документацию, а просто попытаться коротко объяснить «что это и зачем», надеюсь я смог добиться этого.

Ссылка на проект
Ссылка на документацию

UPD: по просьбе Aetet напишу: Collection — это моя разработка, которую я веду уже больше 4-х лет и использую во всех своих проектах. Библиотека работает где угодно, т.е. node.js, worker, браузер и т.д. Документация на github содержит 100% описание всех фич.

Автор: kobezzza

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js