Отсроченное назначение в чистом javascript
В этот вопрос я столкнулся с следующей упрощенной проблемой:
Начнем с массива объектов с атрибутом value. Мы хотим рассчитать для каждого значения, какой процент от суммы значений он есть, и добавить его в структуру как свойство. Для этого нам нужно знать сумму значений, но эта сумма не рассчитывается заранее.
//Original data structure
[
{ "value" : 123456 },
{ "value" : 12146 }
]
//Becomes
[
{
"value" : 123456,
"perc" : 0.9104
},
{
"value" : 12146 ,
"perc" : 0.0896
}
]
Легкое и, вероятно, наиболее читаемое решение состоит в том, чтобы дважды пройти через структуру данных. Сначала мы вычисляем сумму, затем вычисляем процент и добавляем его в структуру данных.
var i;
var sum = 0;
for( i = 0; i < data.length; i++ ) {
sum += data[i].value;
}
for( i = 0; i < data.length; i++ ) {
data[i].perc = data[i].value / sum;
}
Можем ли мы вместо этого просто пройти через структуру данных один раз и как-то сказать, что процентное выражение должно оцениваться только после того, как будет известна вся сумма?
В первую очередь меня интересуют ответы, которые касаются чистого javascript. То есть: без каких-либо библиотек.
Ответы
Ответ 1
Способ сделать это с одним меньшим циклом состоит в том, чтобы записать весь оператор суммирования, составленный из всех возможных элементов, например
var sum = (data[0] ? data[0].value : 0) +
(data[1] ? data[1].value : 0) +
(data[2] ? data[2].value : 0) +
...
(data[50] ? data[50].value : 0);
for( i = 0; i < data.length; i++ ) {
data[i].perc = data[i].value / sum;
}
Не то, чтобы это действительно реальное решение
Вы можете использовать функцию уменьшения массива, но это все равно цикл в фоновом режиме и вызов функции для каждого элемента массива:
var sum = data.reduce(function(output,item){
return output+item.value;
},0);
for( i = 0; i < data.length; i++ ) {
data[i].perc = data[i].value / sum;
}
Вы можете использовать Promise ES6, но там вы все еще добавляете кучу вызовов функций
var data = [
{ "value" : 123456 },
{ "value" : 12146 }
]
var sum = 0;
var rej = null;
var res = null;
var def = new Promise(function(resolve,reject){
rej = reject;
res = resolve;
});
function perc(total){
this.perc = this.value/total;
}
for( i = 0; i < data.length; i++ ) {
def.then(perc.bind(data[i]));
sum+=data[i].value;
}
res(sum);
Perf Tests
Заявление о добавлении
10,834,196
± 0,44%
быстрый
Уменьшить
3,552,539
± 1,95%
67% медленнее
Promise
26325
± 8,14%
На 100% медленнее
Для петель
9,640,800
± 0,45%
11% медленнее
Ответ 2
Решение с самомодифицирующимся кодом.
Он перемещает функцию f
для вычисления до конца итерации, а затем переходит через цепочечные функции для присвоений процента отдельных элементов.
var data = [
{ "value": 123456 },
{ "value": 12146 },
];
data.reduceRight(function (f, a, i) { // reduceRight, i=0 is at the end of reduce required
var t = f; // temporary save previous value or function
f = function (s) { // get a new function with sum as parameter
a.perc = a.value / s; // do the needed calc with sum at the end of reduce
t && t(s); // test & call the old func with sum as parameter
};
f.s = (t.s || 0) + a.value; // add value to sum and save sum in a property of f
i || f(f.s); // at the last iteration call f with sum as parameter
return f; // return the function
}, 0); // start w/ a value w/ a prop (undef/null don't work)
document.write('<pre>' + JSON.stringify(data, 0, 4) + '</pre>');
Ответ 3
Это решение использует один цикл для вычисления суммы и размещения вычисленного свойства perc
для каждого элемента с помощью getter
:
function add_percentage(arr) {
var sum = 0;
arr.forEach(e => {
sum += e.value;
Object.defineProperty(e, "perc", {
get: function() { return this.value / sum; }
});
});
}
Прямая отсрочка будет просто
function add_percentage(arr) {
var sum = 0;
arr.forEach(e => {
sum += e.value;
setTimeout(() => e.perc = e.value / sum);
});
}
Но в чем смысл этого делать?
Ответ 4
В соответствии с моим комментарием, я не вижу способа сделать это без эффективного циклирования над ним дважды.
- Чтобы действительно подсчитать значения
- Чтобы оценить каждое значение против общего количества
Чтобы ответить на "отложенную" часть вашего вопроса, одно из возможных решений, хотя и медленнее (просто угадать из-за вызова функции?) и, вероятно, не то, что вы хотели бы использовать (JSFiddle):
var data = [
{ value: 10 },
{ value: 20 },
{ value: 20 },
{ value: 50 }
];
var total = 0;
for (var i = 0; i < data.length; i++) {
var current = data[i];
total += current["value"];
current.getPercent = function() { return this["value"] / total; };
}
for (var i = 0; i < data.length; i++) {
var current = data[i];
console.log(current.getPercent());
}
Выходы:
0.1
0.2
0.2
0.5
Это имеет дополнительное преимущество: не вычислять значение до тех пор, пока оно вам не понадобится, но когда оно его вычислит, будет более высокая стоимость процессора (из-за вызова функции и т.д.).
Это может быть незначительно оптимизировано, изменив строку getPercent
на:
current.getPercent = function() {
return this["percent"] || (this["percent"] = this["value"] / total);
}
Это гарантирует, что расчет будет выполняться только в первый раз. Обновлено Fiddle
ИЗМЕНИТЬ
Я провел несколько тестов (забыл сохранить до того, как я разбил хром, проверив слишком много итераций, но их достаточно просто, чтобы реплицировать).
Я получал
- Начальный метод Sumurai (1000000 объектов со значением 0 → 9999999) = 2200 мс
- Мой начальный метод (тот же) = 3800 мс
- Мой "оптимизированный" метод (тот же) = 4200 мс
Ответ 5
Глядя на проблему еще немного, желаемый эффект наиболее легко воспроизводится с использованием стека. Самый простой способ сделать это - создать рекурсивную функцию вместо цикла. Рекурсивная функция будет действовать как цикл, а дестабилизация может использоваться для установки свойства процента.
/**
* Helper function for addPercentage
* @param arr Array of data objects
* @param index
* @param sum
* @return {number} sum
*/
function deferredAddPercentage(arr, index, sum) {
//Out of bounds
if (index >= arr.length) {
return sum;
}
//Pushing the stack
sum = deferredAddPercentage(arr, index + 1, sum + arr[index].value);
//Popping the stack
arr[index].percentage = arr[index].value / sum;
return sum;
}
/**
* Adds the percentage property to each contained object
* @param arr Array of data Objects
*/
function addPercentage(arr) {
deferredAddPercentage(arr, 0, 0);
}
// ******
var data = [{
"value": 10
}, {
"value": 20
}, {
"value": 20
}, {
"value": 50
}];
addPercentage(data);
console.log( data );
Ответ 6
OP уже предоставил пример рекурсивного решения. Хотя я считаю, что нерекурсивная функция без хвоста является идеальным подходом для этой задачи, я думаю, что их реализация имеет два недостатка:
- Он изменяет состояние родительской области
- Это очень специфично и, следовательно, вряд ли можно использовать повторно
Я пытаюсь реализовать более общее решение без изменения глобального состояния. Обратите внимание, что я обычно решал бы эту проблему, комбинируя несколько более мелких функций многократного использования. Однако условие OP имеет только один цикл. Это интересная задача, и моя реализация не предназначена для использования в реальном коде!
Я вызываю функцию defmap
, то есть отложенное отображение:
const xs = [
{ "value" : 10 },
{ "value" : 20 },
{ "value" : 20 },
{ "value" : 50 }
];
const defmap = red => map => acc => xs => {
let next = (len, acc, [head, ...tail]) => {
map = tail.length
? next(len, red(acc, head), tail)
: map([], red(acc, head), len);
return map(Object.assign({}, head));
};
return next(xs.length, acc, xs);
};
const map = f => (xs, acc, len) => o => xs.length + 1 < len
? map(f) (append(f(o, acc), xs), acc, len)
: append(f(o, acc), xs);
const append = (xs, ys) => [xs].concat(ys);
const reducer = (acc, o) => acc + o.value;
const mapper = (o, acc) => Object.assign(o, {perc: o.value / acc});
console.log(defmap(reducer) (map(mapper)) (0) (xs));