Javascript réduit sur un tableau d'objets


226

Disons que je veux additionner a.xpour chaque élément arr.

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

J'ai des raisons de croire que la hache n'est pas définie à un moment donné.

Les éléments suivants fonctionnent bien

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

Que fais-je de mal dans le premier exemple?


1
Je pense aussi que vous voulez dire arr.reduce(function(a,b){return a + b})dans le deuxième exemple.
Jamie Wong

1
Merci pour la correction. Je suis tombé sur réduire ici: developer.mozilla.org/en/JavaScript/Reference/Global_Objects/…
YXD

6
@Jamie Wong, il fait en fait partie de JavaScript 1.8
JaredMcAteer

@OriginalSyn ouais - je viens de voir ça. Intéressant, mais comme il ne dispose pas d'un support natif complet, l'implémentation est toujours importante pour répondre à des questions comme celle-ci.
Jamie Wong

3
Les versions JavaScript ne sont que des versions de l'interpréteur Firefox, il est difficile de les référencer. Il n'y a que ES3 et ES5.
Raynos

Réponses:


279

Après la première itération, vous retournez un nombre, puis essayez d'en obtenir la propriété xà ajouter à l'objet suivant, qui est undefinedet les mathématiques impliquant des undefinedrésultats NaN.

essayez de renvoyer un objet contenant une xpropriété avec la somme des propriétés x des paramètres:

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}

Explication ajoutée à partir des commentaires:

La valeur de retour de chaque itération est [].reduceutilisée comme avariable dans l'itération suivante.

Itération 1: a = {x:1}, b = {x:2}, {x: 3}affecté à aau Itération 2

Itération 2: a = {x:3}, b = {x:4}.

Le problème avec votre exemple est que vous renvoyez un littéral numérique.

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

1 Iteration: a = {x:1}, b = {x:2}, // returns 3comme adans la prochaine itération

2 Iteration: a = 3, b = {x:2}retoursNaN

Un littéral numérique 3n'a pas (généralement) de propriété appelée xdonc c'est undefinedet undefined + b.xretourne NaNet NaN + <anything>est toujoursNaN

Clarification : je préfère ma méthode à l'autre bonne réponse dans ce fil car je suis en désaccord avec l'idée que passer un paramètre facultatif pour réduire avec un nombre magique pour sortir un nombre primitif est plus propre. Il peut en résulter moins de lignes écrites mais imo il est moins lisible.


7
Bien sûr, réduire prend une fonction pour effectuer des opérations sur chacun des éléments d'un tableau. Chaque fois qu'il retourne une valeur qui est utilisée comme la prochaine variable «a» dans l'opération. Donc première itération a = {x: 1}, b = {x: 2} puis deuxième itération a = {x: 3} (valeur combinée de la première itération), b = {x: 4}. Le problème avec votre exemple dans la deuxième itération, il essayait d'ajouter 3.x + bx, 3 n'a pas de propriété appelée x, il a donc renvoyé undefined et l'ajout à bx (4) a renvoyé Pas un nombre
JaredMcAteer

Je comprends l'explication, mais je ne vois toujours pas comment {x: ...} empêche l'itération 2 d'appeler ax? Que signifie ce x? J'ai essayé d'utiliser cette approche avec une méthode et semble ne pas fonctionner
mck

en fait, il suffit de lire stackoverflow.com/questions/2118123/… et de comprendre comment cela fonctionne .. mais comment le feriez-vous lorsque vous avez une méthode, plutôt que de simples attributs?
mck

Dans ce cas, le paramètre de l'accumulateur doit avoir la xclé attendue pour qu'il fonctionne.
Wallace Sidhrée il y a

279

Une façon plus propre d'y parvenir consiste à fournir une valeur initiale:

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

La première fois que la fonction anonyme est appelée, elle est appelée avec (0, {x: 1})et revient 0 + 1 = 1. La prochaine fois, il est appelé avec (1, {x: 2})et revient 1 + 2 = 3. Il est ensuite appelé avec (3, {x: 4}), enfin de retour 7.


1
C'est la solution si vous avez une valeur initiale (2ème paramètre de réduction)
TJ.

Merci, je cherchais l'indice avec le deuxième argument pour reduce.
nilsole

1
C'est moins de code et plus direct que la réponse acceptée - y a-t-il un moyen pour que la réponse acceptée soit meilleure que cela?
jbyrd

1
Cela m'a aidé à prendre le cas lorsque la longueur du tableau n'est que 1 parfois.
HussienK

1
es6 way var arr = [{x: 1}, {x: 2}, {x: 4}]; soit a = arr.reduce ((acc, obj) => acc + obj.x, 0); console.log (a);
amar ghodke

76

TL; DR, définissez la valeur initiale

Utiliser la déstructuration

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

Sans déstructuration

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

Avec Typescript

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

Essayons la méthode de déstructuration:

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

La clé de ceci est la définition de la valeur initiale. La valeur de retour devient le premier paramètre de la prochaine itération.

La technique utilisée dans la première réponse n'est pas idiomatique

La réponse acceptée propose de NE PAS passer la valeur "facultative". C'est faux, car la manière idiomatique est que le deuxième paramètre soit toujours inclus. Pourquoi? Trois raisons:

1. Dangereux - Ne pas transmettre la valeur initiale est dangereux et peut créer des effets secondaires et des mutations si la fonction de rappel est négligente.

Voir

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

Si toutefois nous l'avions fait de cette façon, avec la valeur initiale:

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

Pour l'enregistrement, sauf si vous avez l'intention de muter l'objet d'origine, définissez le premier paramètre de Object.assignsur un objet vide. Comme ceci: Object.assign({}, a, b, c).

2 - Meilleure inférence de type - Lorsque vous utilisez un outil comme Typescript ou un éditeur comme VS Code, vous avez l'avantage de dire au compilateur l'initiale et il peut détecter des erreurs si vous le faites mal. Si vous ne définissez pas la valeur initiale, dans de nombreuses situations, elle pourrait ne pas être en mesure de deviner et vous pourriez vous retrouver avec des erreurs d'exécution effrayantes.

3 - Respectez les foncteurs - JavaScript brille mieux lorsque son enfant fonctionnel intérieur est libéré. Dans le monde fonctionnel, il existe une norme sur la façon dont vous "pliez" ou reduceun tableau. Lorsque vous pliez ou appliquez un catamorphisme au tableau, vous prenez les valeurs de ce tableau pour construire un nouveau type. Vous devez communiquer le type résultant - vous devez le faire même si le type final est celui des valeurs du tableau, d'un autre tableau ou de tout autre type.

Réfléchissons-y d'une autre manière. En JavaScript, les fonctions peuvent être transmises comme des données, c'est ainsi que fonctionnent les rappels, quel est le résultat du code suivant?

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

Va-t-il retourner un numéro? Un objet? Cela rend plus clair

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

En savoir plus sur les spécifications de programmation fonctionnelle ici: https://github.com/fantasyland/fantasy-land#foldable

Un peu plus de contexte

La reduceméthode prend deux paramètres,

Array.prototype.reduce( callback, initialItem )

La callbackfonction prend les paramètres suivants

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

Pour la première itération,

  • Si initialItemest fourni, la reducefonction transmet le en initialItemtant que accumulatoret le premier élément du tableau en tant que itemInArray.

  • Si initialItemn'est pas fourni, la reducefonction transmet le premier élément du tableau en tant que initialItemet le deuxième élément du tableau en tant itemInArrayque comportement déroutant.

J'enseigne et recommande de toujours régler la valeur initiale de réduire.

Vous pouvez consulter la documentation sur:

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

J'espère que cela t'aides!


1
La déstructuration est exactement ce que je voulais. Fonctionne comme un charme. Je vous remercie!
AleksandrH

24

D'autres ont répondu à cette question, mais j'ai pensé lancer une autre approche. Plutôt que de passer directement à la sommation de la hache, vous pouvez combiner une carte (de la hache à x) et réduire (pour ajouter les x):

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

Certes, cela va probablement être un peu plus lent, mais j'ai pensé qu'il valait la peine de le mentionner en option.


8

Pour formaliser ce qui a été souligné, un réducteur est un catamorphisme qui prend deux arguments qui peuvent être du même type par coïncidence, et retourne un type qui correspond au premier argument.

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

Cela signifie que le corps du réducteur doit être sur la conversion currentValueet la valeur actuelle du accumulatorà la valeur du nouveau accumulator.

Cela fonctionne de manière simple, lors de l'ajout, car l'accumulateur et les valeurs des éléments sont tous deux du même type (mais ont des objectifs différents).

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

Cela fonctionne simplement parce que ce sont tous des chiffres.

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

Nous donnons maintenant une valeur de départ à l'agrégateur. La valeur de départ doit être le type que vous attendez de l'agrégateur (le type que vous attendez comme valeur finale), dans la grande majorité des cas. Bien que vous ne soyez pas obligé de le faire (et ne devriez pas l'être), il est important de garder à l'esprit.

Une fois que vous savez cela, vous pouvez écrire des réductions significatives pour d'autres problèmes de relation n: 1.

Suppression de mots répétés:

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

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

Fournir un décompte de tous les mots trouvés:

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

Réduction d'un tableau de tableaux en un seul tableau plat:

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

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

Chaque fois que vous cherchez à passer d'un ensemble de choses à une valeur unique qui ne correspond pas à un 1: 1, réduire est quelque chose que vous pourriez envisager.

En fait, la carte et le filtre peuvent tous deux être mis en œuvre en tant que réductions:

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

J'espère que cela fournit un contexte supplémentaire pour l'utilisation reduce.

Le seul ajout à cela, que je n'ai pas encore analysé, c'est quand on s'attend à ce que les types d'entrée et de sortie soient spécifiquement destinés à être dynamiques, car les éléments du tableau sont des fonctions:

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

1
+1 pour l'expliquer clairement en termes de système de type. OTOH, je pense qu'en ouvrant avec l'idée que c'est un catamorphisme peut rebuter beaucoup de gens ...
Periata Breatta

6

Pour la première itération, 'a' sera le premier objet du tableau, donc ax + bx renverra 1 + 2, c'est-à-dire 3. Maintenant, dans la prochaine itération, le 3 renvoyé est affecté à a, donc a est un nombre maintenant n appelant ax donnera NaN.

Une solution simple consiste d'abord à mapper les nombres dans le tableau, puis à les réduire comme ci-dessous:

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

ici arr.map(a=>a.x)fournira un tableau de nombres [1,2,4] utilisant maintenant.reduce(function(a,b){return a+b}) va simplement ajouter ces nombres sans aucun hassel

Une autre solution simple consiste simplement à fournir une somme initiale égale à zéro en affectant 0 à «a» comme ci-dessous:

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

5

À chaque étape de votre réduction, vous ne retournez pas de nouvel {x:???}objet. Vous devez donc soit faire:

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

ou vous devez faire

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

3
Le premier exemple a besoin d'une valeur par défaut (telle que 0) sinon "a" n'est pas défini dans la première itération.
Griffin

2

Dans la première étape, cela fonctionnera correctement car la valeur de asera 1 et celle de b2 mais comme 2 + 1 sera retourné et dans l'étape suivante la valeur de bsera la valeur de retour from step 1 i.e 3et ainsib.x sera pas définie. .et undefined + anyNumber sera NaN et c'est pourquoi vous obtenez ce résultat.

Au lieu de cela, vous pouvez essayer cela en donnant la valeur initiale à zéro, c'est-à-dire

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


Veuillez mettre à jour ceci pour montrer en quoi votre approche est différente des autres.
kwelch

2

Si vous avez un objet complexe avec beaucoup de données, comme un tableau d'objets, vous pouvez adopter une approche pas à pas pour résoudre ce problème.

Par exemple:

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

Tout d'abord, vous devez mapper votre tableau dans un nouveau tableau de votre intérêt, il pourrait s'agir d'un nouveau tableau de valeurs dans cet exemple.

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

Cette fonction de rappel renverra un nouveau tableau contenant uniquement les valeurs du tableau d'origine et le stockera sur les valeurs const. Maintenant, vos valeurs const sont un tableau comme celui-ci:

values = [10, 20];

Et maintenant, vous êtes prêt à effectuer votre réduction:

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

Comme vous pouvez le voir, la méthode de réduction exécute la fonction de rappel plusieurs fois. Pour chaque fois, il prend la valeur actuelle de l'élément dans le tableau et la somme avec l'accumulateur. Donc, pour résumer correctement, vous devez définir la valeur initiale de votre accumulateur comme deuxième argument de la méthode de réduction.

Vous avez maintenant votre nouvelle somme constante avec la valeur 30.


0

réduire la fonction itère sur une collection

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

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

Se traduit par:

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

lors de chaque rappel d'index, la valeur de la variable "accumulateur" est passée dans le paramètre "a" de la fonction de rappel. Si nous n'initialisons pas "accumulateur", sa valeur ne sera pas définie. Appeler undefined.x vous donnerait une erreur.

Pour résoudre ce problème, initialisez "accumulateur" avec la valeur 0 comme la réponse de Casey l'a montré ci-dessus.

Pour comprendre les entrées et les sorties de la fonction "réduire", je vous suggère de consulter le code source de cette fonction. La bibliothèque Lodash a une fonction de réduction qui fonctionne exactement de la même manière que la fonction de réduction dans ES6.

Voici le lien: réduire le code source


0

pour renvoyer une somme de tous les xaccessoires:

arr.reduce(
(a,b) => (a.x || a) + b.x 
)

3
Fournir un contexte expliquant pourquoi cette réponse est meilleure que les autres acceptées ou votées peut aider les utilisateurs lorsqu'ils recherchent la solution à leur question.
Andrew

0

Vous pouvez utiliser la carte et réduire les fonctions

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

-1

La fonction de réduction de tableau prend trois paramètres, à savoir, initialValue (par défaut c'est 0), l'accumulateur et la valeur actuelle. Par défaut, la valeur de initialValue sera "0". qui est pris par l'accumulateur

Voyons cela dans le code.

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

Maintenant, même exemple avec la valeur initiale:

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

Il en va de même pour les tableaux d'objets (la question actuelle du 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

UNE CHOSE À NU DANS L'ESPRIT est InitialValue par défaut est 0 et peut être donné tout ce que je veux dire {}, [] et nombre


-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

vous ne devez pas utiliser ax pour l'accumulateur, mais vous pouvez faire comme ceci `arr = [{x: 1}, {x: 2}, {x: 4}]

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.