Отсроченное назначение в чистом 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));