Javascript reduce en una matriz de objetos

226

Digamos que quiero sumar a.xpara cada elemento en arr.

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

Tengo motivos para creer que hacha no está definida en algún momento.

Lo siguiente funciona bien

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

¿Qué estoy haciendo mal en el primer ejemplo?

YXD
fuente
1
Además, creo que te refieres arr.reduce(function(a,b){return a + b})al segundo ejemplo.
Jamie Wong
1
Gracias por la corrección. Encontré reducir aquí: developer.mozilla.org/en/JavaScript/Reference/Global_Objects/…
YXD
66
@Jamie Wong en realidad es parte de JavaScript 1.8
JaredMcAteer
@OriginalSyn, sí, acabo de ver eso. Interesante, pero dado que no tiene soporte nativo completo, la implementación sigue siendo importante al responder preguntas como esta.
Jamie Wong
3
Las versiones de JavaScript son solo versiones del intérprete de Firefox, es confuso hacer referencia a ellas. Solo hay ES3 y ES5.
Raynos

Respuestas:

279

Después de la primera iteración, está devolviendo un número y luego tratando de obtener la propiedad xdel mismo para agregarlo al siguiente objeto que es undefinedy las matemáticas que involucran undefinedresultados NaN.

intente devolver un objeto que contenga una xpropiedad con la suma de las propiedades x de los parámetros:

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}

Explicación agregada de los comentarios:

El valor de retorno de cada iteración de [].reduceutilizado como avariable en la siguiente iteración.

La iteración 1: a = {x:1}, b = {x:2}, {x: 3}asignado a aen la iteración 2

Iteración 2: a = {x:3}, b = {x:4}.

El problema con su ejemplo es que está devolviendo un número literal.

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

La iteración 1: a = {x:1}, b = {x:2}, // returns 3como aen la siguiente iteración

Iteración 2: a = 3, b = {x:2}vuelveNaN

Un número literal 3no tiene (normalmente) una propiedad llamada, xpor lo que es undefinedy undefined + b.xdevuelve NaNy NaN + <anything>siempre esNaN

Aclaración : prefiero mi método sobre la otra respuesta principal en este hilo, ya que no estoy de acuerdo con la idea de que pasar un parámetro opcional para reducir con un número mágico para obtener un número primitivo es más limpio. Puede dar lugar a menos líneas escritas, pero en mi opinión es menos legible.

JaredMcAteer
fuente
77
Claro que reduce toma una función para realizar operaciones en cada uno de los elementos de una matriz. Cada vez que devuelve un valor que se utiliza como la próxima variable 'a' en la operación. Entonces, la primera iteración a = {x: 1}, b = {x: 2} luego la segunda iteración a = {x: 3} (valor combinado de la primera iteración), b = {x: 4}. El problema con su ejemplo en la segunda iteración fue que estaba tratando de agregar 3.x + bx, 3 no tiene una propiedad llamada x, por lo que regresó indefinido y agregar eso a bx (4) devolvió Not a Number
JaredMcAteer
Entiendo la explicación, pero todavía no veo cómo {x: ...} evita que la iteración 2 llame a ax. ¿Qué significa eso x? Intenté usar este enfoque con un método, y parece que no funciona
mck
en realidad, solo lea stackoverflow.com/questions/2118123/… y entienda cómo funciona ... pero ¿cómo lo haría cuando tenga un método, en lugar de solo atributos directos?
mck
En este caso, el parámetro acumulador debe tener la xclave esperada para que funcione.
Wallace Sidhrée hace
279

Una forma más limpia de lograr esto es proporcionando un valor inicial:

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

La primera vez que se llama a la función anónima, se llama con (0, {x: 1})y vuelve 0 + 1 = 1. La próxima vez, se llama con (1, {x: 2})y regresa 1 + 2 = 3. Luego se llama con (3, {x: 4}), finalmente regresando 7.

Casey Chu
fuente
1
Esta es la solución si tiene un valor inicial (segundo parámetro de reducción)
TJ.
Gracias, estaba buscando la pista con el segundo argumento reduce.
nilsole
1
Esto es menos código y más directo que la respuesta aceptada. ¿Hay alguna forma en que la respuesta aceptada sea mejor que esta?
jbyrd
1
Esto me ayudó a tomar el caso cuando la longitud de la matriz es solo 1 a veces.
HussienK
1
es6 manera var arr = [{x: 1}, {x: 2}, {x: 4}]; deje a = arr.reduce ((acc, obj) => acc + obj.x, 0); console.log (a);
amar ghodke
76

TL; DR, establece el valor inicial

Usando la desestructuración

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

Sin desestructuración

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

Con mecanografiado

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

Probemos el método de desestructuración:

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

La clave para esto es establecer el valor inicial. El valor de retorno se convierte en el primer parámetro de la siguiente iteración.

La técnica utilizada en la respuesta superior no es idiomática

La respuesta aceptada propone NO pasar el valor "opcional". Esto está mal, ya que la forma idiomática es que siempre se incluya el segundo parámetro . ¿Por qué? Tres razones:

1. Peligroso : no pasar el valor inicial es peligroso y puede crear efectos secundarios y mutaciones si la función de devolución de llamada es descuidada.

Mirad

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 } ]

Sin embargo, si lo hubiéramos hecho de esta manera, con el valor inicial:

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

Para el registro, a menos que tenga la intención de mutar el objeto original, establezca el primer parámetro Object.assignen un objeto vacío. De esta manera: Object.assign({}, a, b, c).

2 - Mejor inferencia de tipos: al usar una herramienta como TypeScript o un editor como VS Code, obtienes el beneficio de decirle al compilador la inicial y puede detectar errores si lo estás haciendo mal. Si no establece el valor inicial, en muchas situaciones podría no ser capaz de adivinar y podría terminar con errores de tiempo de ejecución espeluznantes.

3 - Respeta a los Functores - JavaScript brilla mejor cuando se desata su hijo funcional interno. En el mundo funcional, hay un estándar sobre cómo "doblar" o reduceuna matriz. Cuando dobla o aplica un catamorfismo a la matriz, toma los valores de esa matriz para construir un nuevo tipo. Debe comunicar el tipo resultante; debe hacerlo incluso si el tipo final es el de los valores de la matriz, otra matriz o cualquier otro tipo.

Pensemos de otra manera. En JavaScript, las funciones se pueden pasar como datos, así es como funcionan las devoluciones de llamada, ¿cuál es el resultado del siguiente código?

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

¿Devolverá un número? ¿Un objeto? Esto lo hace más claro

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

Lea más sobre las especificaciones de programación funcional aquí: https://github.com/fantasyland/fantasy-land#foldable

Un poco más de fondo

El reducemétodo toma dos parámetros,

Array.prototype.reduce( callback, initialItem )

La callbackfunción toma los siguientes parámetros

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

Para la primera iteración,

  • Si initialItemse proporciona, la reducefunción pasa initialItemcomo el accumulatory el primer elemento de la matriz como itemInArray.

  • Si initialItemse no se proporciona, la reducefunción pasa el primer elemento en la matriz como el initialItemy el segundo elemento de la matriz como itemInArrayla que puede ser confuso comportamiento.

Enseño y recomiendo siempre establecer el valor inicial de reducir.

Puede consultar la documentación en:

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

¡Espero que esto ayude!

Babakness
fuente
1
Destruir es justo lo que quería. Funciona de maravilla. ¡Gracias!
AleksandrH
24

Otros han respondido a esta pregunta, pero pensé que tendría otro enfoque. En lugar de ir directamente a la suma de ax, puede combinar un mapa (de ax a x) y reducir (para agregar las x):

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

Es cierto que probablemente sea un poco más lento, pero pensé que valía la pena mencionarlo como una opción.

RHSeeger
fuente
8

Para formalizar lo que se ha señalado, un reductor es un catamorfismo que toma dos argumentos que pueden ser del mismo tipo por coincidencia, y devuelve un tipo que coincide con el primer argumento.

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

Eso significa que el cuerpo del reductor debe estar relacionado con la conversión currentValuey el valor actual accumulatordel valor nuevo accumulator.

Esto funciona de manera directa, cuando se agrega, porque los valores del acumulador y del elemento son del mismo tipo (pero tienen diferentes propósitos).

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

Esto simplemente funciona porque son todos números.

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

Ahora le estamos dando un valor inicial al agregador. El valor inicial debe ser el tipo que espera que sea el agregador (el tipo que espera que salga como valor final), en la gran mayoría de los casos. Si bien no está obligado a hacer esto (y no debería serlo), es importante tenerlo en cuenta.

Una vez que sepa eso, puede escribir reducciones significativas para otros problemas de relación n: 1.

Eliminar palabras repetidas:

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

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

Proporcionar un recuento de todas las palabras encontradas:

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

Reducción de una matriz de matrices, a una única matriz plana:

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

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

Cada vez que esté buscando pasar de una variedad de cosas, a un solo valor que no coincida con 1: 1, reducir es algo que podría considerar.

De hecho, el mapa y el filtro pueden implementarse como reducciones:

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,
  []
);

Espero que esto proporcione algún contexto adicional sobre cómo usarlo reduce.

La única adición a esto, en la que aún no he entrado, es cuando existe la expectativa de que los tipos de entrada y salida están específicamente destinados a ser dinámicos, porque los elementos de la matriz son funciones:

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
fuente
1
+1 para explicarlo claramente en términos del sistema de tipos. OTOH, creo que al comenzar con la idea de que es un catamorfismo puede ser contraproducente para muchos ...
Periata Breatta
6

Para la primera iteración, 'a' será el primer objeto de la matriz, por lo tanto ax + bx devolverá 1 + 2, es decir 3. Ahora en la siguiente iteración, el 3 devuelto se asigna a a, por lo que a es un número que ahora se llama ax le dará NaN.

La solución simple es primero mapear los números en la matriz y luego reducirlos de la siguiente manera:

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

aquí arr.map(a=>a.x)proporcionará una serie de números [1,2,4] que ahora .reduce(function(a,b){return a+b})usará simplemente agregará estos números sin ningún problema

Otra solución simple es proporcionar una suma inicial como cero asignando 0 a 'a' como se muestra a continuación:

arr.reduce(function(a,b){return a + b.x},0)
fatimasajjad
fuente
5

En cada paso de su reducción, no está devolviendo un nuevo {x:???}objeto. Entonces, o debes hacer:

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

o necesitas hacer

arr = [{x:1},{x:2},{x:4}]
arr.reduce(function(a,b){return {x: a.x + b.x}; }) 
Jamie Wong
fuente
3
El primer ejemplo necesita un valor predeterminado (como 0), de lo contrario, "a" no está definido en la primera iteración.
Griffin
2

En el primer paso, funcionará bien ya que el valor de aserá 1 y el de bserá 2, pero como 2 + 1 se devolverá y en el siguiente paso el valor de bserá el valor de retorno from step 1 i.e 3y, por b.xlo tanto , será indefinido. .y undefined + anyNumber será NaN y es por eso que obtienes ese resultado.

En cambio, puede intentar esto dando el valor inicial como cero, es decir

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

Nitesh Ranjan
fuente
Actualice esto para mostrar cómo su enfoque es diferente de los demás.
kwelch
2

Si tiene un objeto complejo con muchos datos, como una matriz de objetos, puede tomar un enfoque paso a paso para resolver esto.

Por ejemplo:

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

Primero, debe asignar su matriz a una nueva matriz de su interés, podría ser una nueva matriz de valores en este ejemplo.

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

Esta función de devolución de llamada devolverá una nueva matriz que contiene solo valores de la matriz original y la almacenará en valores const. Ahora su const de valores es una matriz como esta:

values = [10, 20];

Y ahora estás listo para realizar tu reducción:

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

Como puede ver, el método reduce ejecuta la función de devolución de llamada varias veces. Para cada vez, toma el valor actual del artículo en la matriz y suma con el acumulador. Entonces, para sumarlo correctamente, debe establecer el valor inicial de su acumulador como el segundo argumento del método de reducción.

Ahora tienes tu nueva suma constante con el valor de 30.

João Ramires
fuente
0

la función reduce itera sobre una colección

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

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

se traduce en:

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. 
);

durante cada devolución de llamada de índice, el valor de la variable "acumulador" se pasa al parámetro "a" en la función de devolución de llamada. Si no inicializamos "acumulador", su valor será indefinido. Llamar a undefined.x le daría un error.

Para resolver esto, inicialice el "acumulador" con el valor 0 como la respuesta de Casey mostró arriba.

Para comprender las entradas y salidas de la función "reducir", le sugiero que mire el código fuente de esta función. La biblioteca Lodash tiene una función de reducción que funciona exactamente igual que la función de "reducción" en ES6.

Aquí está el enlace: reducir el código fuente

Deen John
fuente
0

para devolver una suma de todos los xaccesorios:

arr.reduce(
(a,b) => (a.x || a) + b.x 
)
Jack negro
fuente
3
Proporcionar un contexto de por qué esta respuesta es mejor que las otras aceptadas o votadas puede ayudar a los usuarios a la hora de buscar la solución a su pregunta.
Andrew
0

Podrías usar el mapa y reducir funciones

const arr = [{x:1},{x:2},{x:4}];
const sum = arr.map(n => n.x).reduce((a, b) => a + b, 0);
SupineDread89
fuente
-1

La función de reducción de matriz toma tres parámetros, es decir, initialValue (el valor predeterminado es 0), acumulador y valor actual. Por defecto, el valor de initialValue será "0". que es tomado por acumulador

Veamos esto en código.

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

Ahora mismo ejemplo con valor inicial:

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

Lo mismo se aplica también a las matrices de objetos (la pregunta actual de 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

UNA COSA PARA DESCUBRIRSE es InitialValue por defecto es 0 y se le puede dar cualquier cosa, quiero decir {}, [] y número

M.Dhee
fuente
-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))

Rajesh Bose
fuente
-2

no debe usar ax para el acumulador, en su lugar puede hacer esto `arr = [{x: 1}, {x: 2}, {x: 4}]

arr.reduce (función (a, b) {a + bx}, 0) `

RAks BM
fuente