Javascript уменьшить на массив объектов

226

Скажем, я хочу, чтобы сумма a.xдля каждого элемента в arr.

arr = [{x:1},{x:2},{x:4}]
arr.reduce(function(a,b){return a.x + b.x})
>> NaN

У меня есть основания полагать, что топор в какой-то момент не определен.

Следующее работает отлично

arr = [1,2,4]
arr.reduce(function(a,b){return a + b})
>> 7

Что я делаю не так в первом примере?

YXD
источник
1
Кроме того, я думаю, что вы имеете arr.reduce(function(a,b){return a + b})в виду во втором примере.
Джейми Вонг
1
Спасибо за исправление. Я столкнулся с сокращением здесь: developer.mozilla.org/en/JavaScript/Reference/Global_Objects/…
YXD
6
@ Джейми Вонг - это на самом деле часть JavaScript 1.8
JaredMcAteer
@OriginalSyn да - только что видел. Интересно, но так как он не имеет полной собственной поддержки, реализация по-прежнему имеет значение при ответах на подобные вопросы.
Джейми Вонг
3
Версии JavaScript - это всего лишь версии интерпретатора Firefox, ссылаться на них непонятно. Есть только ES3 и ES5.
Райнос

Ответы:

279

После первой итерации вы возвращаете число, а затем пытаетесь получить его свойство xдля добавления к следующему объекту, в котором есть undefinedматематические undefinedрезультаты NaN.

попробуйте вернуть объект, содержащий xсвойство с суммой свойств x параметров:

var arr = [{x:1},{x:2},{x:4}];

arr.reduce(function (a, b) {
  return {x: a.x + b.x}; // returns object with property x
})

// ES6
arr.reduce((a, b) => ({x: a.x + b.x}));

// -> {x: 7}

Объяснение добавлено из комментариев:

Возвращаемое значение каждой итерации [].reduceиспользуется как aпеременная в следующей итерации.

Итерация 1: a = {x:1}, b = {x:2}, {x: 3}назначается aв Итерация 2

Итерация 2: a = {x:3}, b = {x:4}.

Проблема с вашим примером в том, что вы возвращаете числовой литерал.

function (a, b) {
  return a.x + b.x; // returns number literal
}

Итерация 1: a = {x:1}, b = {x:2}, // returns 3а aв следующей итерации

Итерации 2: a = 3, b = {x:2}возвращаетNaN

Ряд буквальный 3не ( как правило) имеет свойства xтак это undefinedи undefined + b.xвозвращается , NaNи NaN + <anything>всегдаNaN

Пояснение : я предпочитаю свой метод перед другим топовым ответом в этой теме, поскольку я не согласен с идеей, что передача необязательного параметра для уменьшения с помощью магического числа для получения примитива числа является более чистой. Это может привести к меньшему количеству написанных строк, но, на мой взгляд, оно менее читабельно

JaredMcAteer
источник
7
Конечно, в функции Reduce используется функция для выполнения операций над каждым элементом массива. Каждый раз он возвращает значение, которое используется в качестве следующей переменной «a» в операции. Итак, первая итерация a = {x: 1}, b = {x: 2}, затем вторая итерация a = {x: 3} (объединенное значение первой итерации), b = {x: 4}. Проблема с вашим примером во второй итерации: он пытался добавить 3.x + bx, 3 не имеет свойства с именем x, поэтому он возвращает неопределенное значение, а добавление его в bx (4) возвращает не число
JaredMcAteer
Я понимаю объяснение, но я все еще не понимаю, как {x: ...} не позволяет итерации 2 вызывать ax? Что означает этот х? Я пытался использовать этот подход с методом, и, кажется, не работает
Mck
на самом деле, просто прочитайте stackoverflow.com/questions/2118123/… и поймите, как это работает ... но как бы вы это сделали, когда у вас есть метод, а не просто прямые атрибуты?
mck
В этом случае параметр аккумулятора должен иметь ожидаемый xключ для его работы.
Уоллес Сидре
279

Более чистый способ сделать это - предоставить начальное значение:

var arr = [{x:1}, {x:2}, {x:4}];
arr.reduce(function (acc, obj) { return acc + obj.x; }, 0); // 7
console.log(arr);

Когда анонимная функция вызывается в первый раз, она вызывается (0, {x: 1})и возвращает 0 + 1 = 1. В следующий раз он вызывается с (1, {x: 2})и возвращается 1 + 2 = 3. Затем он вызывается с (3, {x: 4}), наконец, возвращаясь 7.

Кейси Чу
источник
1
Это решение, если у вас есть начальное значение (2-й параметр снижения)
TJ.
Спасибо, я искал подсказку со вторым аргументом для reduce.
nilsole
1
Это меньше кода и более прямой, чем принятый ответ - есть ли способ, которым принятый ответ лучше, чем этот?
Jbyrd
1
Это помогло мне взять случай, когда длина массива иногда равна 1.
HussienK
1
es6 way var arr = [{x: 1}, {x: 2}, {x: 4}]; let a = arr.reduce ((acc, obj) => acc + obj.x, 0); console.log (а);
Амар Годке
76

TL; DR, установить начальное значение

Использование деструктуризации

arr.reduce( ( sum, { x } ) => sum + x , 0)

Без разрушения

arr.reduce( ( sum , cur ) => sum + cur.x , 0)

С машинописным шрифтом

arr.reduce( ( sum, { x } : { x: number } ) => sum + x , 0)

Давайте попробуем метод деструктуризации:

const arr = [ { x: 1 }, { x: 2 }, { x: 4 } ]
const result = arr.reduce( ( sum, { x } ) => sum + x , 0)
console.log( result ) // 7

Ключом к этому является установка начального значения. Возвращаемое значение становится первым параметром следующей итерации.

Техника, используемая в верхнем ответе, не идиоматична

В принятом ответе предлагается НЕ передавать «необязательное» значение. Это неправильно, так как идиоматический способ заключается в том, что второй параметр всегда включается. Зачем? Три причины:

1. Опасно - Не передавать начальное значение опасно и может создать побочные эффекты и мутации, если функция обратного вызова небрежна.

Вот

const badCallback = (a,i) => Object.assign(a,i) 

const foo = [ { a: 1 }, { b: 2 }, { c: 3 } ]
const bar = foo.reduce( badCallback )  // bad use of Object.assign
// Look, we've tampered with the original array
foo //  [ { a: 1, b: 2, c: 3 }, { b: 2 }, { c: 3 } ]

Если, однако, мы сделали это таким образом, с начальным значением:

const bar = foo.reduce( badCallback, {})
// foo is still OK
foo // { a: 1, b: 2, c: 3 }

Для записи, если вы не собираетесь изменять исходный объект, установите для первого параметра Object.assignпустой объект. Как это: Object.assign({}, a, b, c).

2 - лучший вывод типа - при использовании инструмента, такого как Typescript, или редактора, такого как VS Code, вы получаете преимущество, сообщая компилятору исходный код, и он может отлавливать ошибки, если вы делаете это неправильно. Если вы не установите начальное значение, во многих ситуациях его невозможно будет угадать, что может привести к жутким ошибкам во время выполнения.

3 - Уважение к функторам - JavaScript светит лучше, когда его внутренний функциональный дочерний элемент освобожден. В функциональном мире существует стандарт того, как вы «складываете» или reduceмассив. Когда вы складываете или применяете катаморфизм к массиву, вы берете значения этого массива для создания нового типа. Вам нужно сообщить результирующий тип - вы должны сделать это, даже если последний тип - это тип значений в массиве, другом массиве или любом другом типе.

Давайте подумаем об этом по-другому. В JavaScript функции могут передаваться как данные, как работают обратные вызовы, каков результат следующего кода?

[1,2,3].reduce(callback)

Будет ли возвращать номер? Объект? Это проясняет

[1,2,3].reduce(callback,0)

Подробнее о спецификации функционального программирования читайте здесь: https://github.com/fantasyland/fantasy-land#foldable

Еще немного фона

reduceМетод принимает два параметра,

Array.prototype.reduce( callback, initialItem )

callbackФункция принимает следующие параметры

(accumulator, itemInArray, indexInArray, entireArray) => { /* do stuff */ }

Для первой итерации

  • Если initialItemуказано, reduceфункция передает initialItemкак accumulatorи первый элемент массива как itemInArray.

  • Если initialItemэто не предусмотрено, то reduceфункция передает первый элемент в массив в качестве initialItemи второго элемента массива , как , itemInArrayкоторые могут ввести в заблуждение поведение.

Я учу и рекомендую всегда устанавливать начальное значение снижения.

Вы можете ознакомиться с документацией по адресу:

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

Надеюсь это поможет!

Babakness
источник
1
Разрушение это то, что я хотел. Работает как шарм. Спасибо!
АлександрH
24

Другие ответили на этот вопрос, но я решил бросить другой подход. Вместо того, чтобы перейти непосредственно к суммированию топора, вы можете объединить карту (от топора до x) и уменьшить (чтобы добавить x):

arr = [{x:1},{x:2},{x:4}]
arr.map(function(a) {return a.x;})
   .reduce(function(a,b) {return a + b;});

По общему признанию, это, вероятно, будет немного медленнее, но я думал, что стоит упомянуть это как выбор.

RHSeeger
источник
8

Чтобы формализовать то, что было указано, редуктор представляет собой катаморфизм, который принимает два аргумента, которые могут совпадать с одним типом, и возвращает тип, который соответствует первому аргументу.

function reducer (accumulator: X, currentValue: Y): X { }

Это означает, что тело редуктора должно быть преобразовано, currentValueа текущее значение в accumulatorзначение нового accumulator.

При добавлении это работает прямым образом, поскольку значения аккумулятора и элемента оказываются одного типа (но служат разным целям).

[1, 2, 3].reduce((x, y) => x + y);

Это просто работает, потому что они все числа.

[{ age: 5 }, { age: 2 }, { age: 8 }]
  .reduce((total, thing) => total + thing.age, 0);

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

Узнав об этом, вы можете написать значимые сокращения для других проблем отношений n: 1.

Удаление повторяющихся слов:

const skipIfAlreadyFound = (words, word) => words.includes(word)
    ? words
    : words.concat(word);

const deduplicatedWords = aBunchOfWords.reduce(skipIfAlreadyFound, []);

Предоставление подсчета всех найденных слов:

const incrementWordCount = (counts, word) => {
  counts[word] = (counts[word] || 0) + 1;
  return counts;
};
const wordCounts = words.reduce(incrementWordCount, { });

Сокращение массива массивов до единого плоского массива:

const concat = (a, b) => a.concat(b);

const numbers = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
].reduce(concat, []);

Каждый раз, когда вы хотите перейти от множества вещей к одному значению, которое не соответствует 1: 1, уменьшение - это то, что вы могли бы рассмотреть.

Фактически, карта и фильтр могут быть реализованы как сокращения:

const map = (transform, array) =>
  array.reduce((list, el) => list.concat(transform(el)), []);

const filter = (predicate, array) => array.reduce(
  (list, el) => predicate(el) ? list.concat(el) : list,
  []
);

Я надеюсь, что это дает некоторый дополнительный контекст для использования reduce.

Единственное дополнение к этому, которое я еще не взломал, - это когда ожидается, что типы ввода и вывода определенно должны быть динамическими, потому что элементы массива являются функциями:

const compose = (...fns) => x =>
  fns.reduceRight((x, f) => f(x), x);

const hgfx = h(g(f(x)));
const hgf = compose(h, g, f);
const hgfy = hgf(y);
const hgfz = hgf(z);
Norguard
источник
1
+1 за четкое объяснение с точки зрения системы типов. OTOH, я думаю, что, открыв идею, что это катаморфизм, может оттолкнуть многих ...
Periata Breatta
6

Для первой итерации 'a' будет первым объектом в массиве, следовательно, ax + bx вернет 1 + 2, т. Е. 3. Теперь в следующей итерации возвращенные 3 присваиваются a, поэтому a является числом, теперь n вызывающим ax даст NaN.

Простое решение - сначала отобразить числа в массиве, а затем уменьшить их, как показано ниже:

arr.map(a=>a.x).reduce(function(a,b){return a+b})

здесь arr.map(a=>a.x)предоставим массив чисел [1,2,4], теперь используя .reduce(function(a,b){return a+b})просто добавим эти числа без каких-либо проблем

Другое простое решение - просто предоставить начальную сумму как ноль, присваивая 0 'a', как показано ниже:

arr.reduce(function(a,b){return a + b.x},0)
fatimasajjad
источник
5

На каждом шаге сокращения вы не возвращаете новый {x:???}объект. Так что вам либо нужно сделать:

arr = [{x:1},{x:2},{x:4}]
arr.reduce(function(a,b){return a + b.x})

или вам нужно сделать

arr = [{x:1},{x:2},{x:4}]
arr.reduce(function(a,b){return {x: a.x + b.x}; }) 
Джейми Вонг
источник
3
В первом примере требуется значение по умолчанию (например, 0), иначе «a» не определено в первой итерации.
Гриффин
2

На первом шаге он будет работать нормально, так как значение aбудет равно 1, а значение of bбудет равно 2, но как 2 + 1 будет возвращено, а на следующем шаге значение bбудет возвращаемым значением from step 1 i.e 3и поэтому b.xбудет неопределенным. .and undefined + anyNumber будет NaN, и именно поэтому вы получаете этот результат.

Вместо этого вы можете попробовать это, задав начальное значение как ноль, т.е.

arr.reduce(function(a,b){return a + b.x},0);

Нитеш Ранджан
источник
Пожалуйста, обновите это, чтобы показать, как ваш подход отличается от других.
Квелч
2

Если у вас есть сложный объект с большим количеством данных, например, массив объектов, вы можете сделать шаг за шагом, чтобы решить эту проблему.

Например:

const myArray = [{ id: 1, value: 10}, { id: 2, value: 20}];

Во-первых, вы должны отобразить ваш массив в новый интересующий вас массив, в этом примере это может быть новый массив значений.

const values = myArray.map(obj => obj.value);

Эта функция обратного вызова вернет новый массив, содержащий только значения из исходного массива, и сохранит его в значениях const. Теперь ваши значения const - это массив вроде этого:

values = [10, 20];

И теперь вы готовы выполнить ваше сокращение:

const sum = values.reduce((accumulator, currentValue) => { return accumulator + currentValue; } , 0);

Как видите, метод Reduce выполняет функцию обратного вызова несколько раз. За каждый раз он принимает текущее значение элемента в массиве и суммирует с аккумулятором. Поэтому для правильного суммирования вам нужно установить начальное значение вашего аккумулятора как второй аргумент метода Reduce.

Теперь у вас есть новая постоянная сумма со значением 30.

Жоао Рамирес
источник
0

Функция Reduce перебирает коллекцию

arr = [{x:1},{x:2},{x:4}] // is a collection

arr.reduce(function(a,b){return a.x + b.x})

переводится как:

arr.reduce(
    //for each index in the collection, this callback function is called
  function (
    a, //a = accumulator ,during each callback , value of accumulator is 
         passed inside the variable "a"
    b, //currentValue , for ex currentValue is {x:1} in 1st callback
    currentIndex,
    array
  ) {
    return a.x + b.x; 
  },
  accumulator // this is returned at the end of arr.reduce call 
    //accumulator = returned value i.e return a.x + b.x  in each callback. 
);

во время каждого обратного вызова индекса значение переменной «аккумулятор» передается в параметр «a» в функции обратного вызова. Если мы не инициализируем «аккумулятор», его значение будет неопределенным. Вызов undefined.x приведет к ошибке.

Чтобы решить эту проблему, инициализируйте «аккумулятор» значением 0, как показано выше в ответе Кейси.

Чтобы понять входы «уменьшить» функцию, я бы посоветовал вам взглянуть на исходный код этой функции. Библиотека Lodash имеет функцию Reduce, которая работает точно так же, как функция «Reduce» в ES6.

Вот ссылка: уменьшить исходный код

Дин Джон
источник
0

вернуть сумму всех xреквизитов:

arr.reduce(
(a,b) => (a.x || a) + b.x 
)
Блэк Джек
источник
3
Предоставление контекста относительно того, почему этот ответ лучше, чем другие принятые или одобренные, может помочь пользователям при поиске решения их вопроса.
Андрей
0

Вы можете использовать карту и уменьшить функции

const arr = [{x:1},{x:2},{x:4}];
const sum = arr.map(n => n.x).reduce((a, b) => a + b, 0);
SupineDread89
источник
-1

Функция уменьшения массива принимает три параметра: initialValue (по умолчанию 0), аккумулятор и текущее значение. По умолчанию значение initialValue будет равно «0». который забирается аккумулятором

Давайте посмотрим это в коде.

var arr =[1,2,4] ;
arr.reduce((acc,currVal) => acc + currVal ) ; 
// (remember Initialvalue is 0 by default )

//first iteration** : 0 +1 => Now accumulator =1;
//second iteration** : 1 +2 => Now accumulator =3;
//third iteration** : 3 + 4 => Now accumulator = 7;
No more array properties now the loop breaks .
// solution = 7

Теперь тот же пример с начальным значением:

var initialValue = 10;
var arr =[1,2,4] ;
arr.reduce((acc,currVal) => acc + currVal,initialValue ) ; 
/
// (remember Initialvalue is 0 by default but now it's 10 )

//first iteration** : 10 +1 => Now accumulator =11;
//second iteration** : 11 +2 => Now accumulator =13;
//third iteration** : 13 + 4 => Now accumulator = 17;
No more array properties now the loop breaks .
//solution=17

То же самое относится и к массивам объектов (текущий вопрос stackoverflow):

var arr = [{x:1},{x:2},{x:4}]
arr.reduce(function(acc,currVal){return acc + currVal.x}) 
// destructing {x:1} = currVal;
Now currVal is object which have all the object properties .So now 
currVal.x=>1 
//first iteration** : 0 +1 => Now accumulator =1;
//second iteration** : 1 +2 => Now accumulator =3;
//third iteration** : 3 + 4 => Now accumulator = 7;
No more array properties now the loop breaks 
//solution=7

Значение ONE ONE IN MINE равно InitialValue по умолчанию равно 0, и ему можно дать все, что я имею в виду {}, [] и число

M.Dhee
источник
-1

    
  
 //fill creates array with n element
 //reduce requires 2 parameter , 3rd parameter as a length
 var fibonacci = (n) => Array(n).fill().reduce((a, b, c) => {
      return a.concat(c < 2 ? c : a[c - 1] + a[c - 2])
  }, [])
  console.log(fibonacci(8))

Раджеш Бозе
источник
-2

вы не должны использовать топор для аккумулятора, вместо этого вы можете сделать это `arr = [{x: 1}, {x: 2}, {x: 4}]

arr.reduce (function (a, b) {a + bx}, 0) `

Ракс БМ
источник