简体   繁体   中英

array reduce with an object as accumulator

with

let str = "baaabbabbsbsssssabbaaaa";

with this

[...str].reduce((characterMap, char) => {
    if (!characterMap[char]) characterMap[char] = 1;
    else characterMap[char]++;
    return characterMap;
}, {})

I get { b: 8, a: 9, s: 6 }

But when I shorten it to this,

[...str].reduce((characterMap, char) => !characterMap[char] ? characterMap[char] = 1 : characterMap[char]++, {});

It prints out 1 instead of { b: 8, a: 9, s: 6 }

Why?

am I using reduce wrong in the first place?

how do I shorten this?

By:

  1. Not trying to shoehorn it in to a reduce . :-) reduce is appropriately used when the accumulator value changes . In your case, it doesn't.

  2. Not using an array wrapper. Strings are iterable, so use that fact directly.

  3. Using the curiously-powerful || operator ¹ to default the character count if not present.

Just use a loop:

const characterMap = {};
for (const char of str) {
    characterMap[char] = (characterMap[char] || 0) + 1;
}

Live Example:

 let str = "baaabbabbsbsssssabbaaaa"; const characterMap = {}; for (const char of str) { characterMap[char] = (characterMap[char] || 0) + 1; } console.log(characterMap);

But if you really want to use reduce , you can apply the || trick and use a shorter name:

const characterMap = [...str].reduce((acc, ch) => {
    acc[ch] = (acc[ch] || 0) + 1;
    return acc;
}, {});

Live Example:

 let str = "baaabbabbsbsssssabbaaaa"; const characterMap = [...str].reduce((acc, ch) => { acc[ch] = (acc[ch] || 0) + 1; return acc; }, {}); console.log(characterMap);

You can (ab)use the comma operator to force that into a concise arrow function, but at the cost of readability and maintainability (IMHO).

But when I shorten it to this,

 [...str].reduce((characterMap, char) => !characterMap[char] ? characterMap[char] = 1 : characterMap[char]++, {});

It prints out 1 instead of { b: 8, a: 9, s: 6 }

Why?

Because your callback function returns the result of !characterMap[char] ? characterMap[char] = 1 : characterMap[char]++ !characterMap[char] ? characterMap[char] = 1 : characterMap[char]++ , not the object. So subsequent calls to it get that number, not the object, and characterMap[char] is always false (because 1["a"] is undefined , etc.).


¹ (on my anemic little blog)

You need to take the returned accumulator as result.

 var str = "baaabbabbsbsssssabbaaaa", result = [...str].reduce((characterMap, char) => { characterMap[char] = (characterMap[char] || 0) + 1; // take default value of zero return characterMap; }, {}); console.log(result)

But when I shorten it to this,

 [...str].reduce((characterMap, char) => !characterMap[char] ? characterMap[char] = 1 : characterMap[char]++, {});

It prints out 1 instead of { b: 8, a: 9, s: 6 }

Why?

Simply because in the first loop

!characterMap[char] ? characterMap[char] = 1 : characterMap[char]++

it takes this route

!characterMap[char] ? characterMap[char] = 1 : characterMap[char]++
^^^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^^
true                  characterMap['b'] = 1

returns 1

and in all following iterations, you take the number 1 and try to get a property a (or all following letters of the string) of this number, which is undefined

!characterMap[char] ? characterMap[char] = 1 : characterMap[char]++
^^^^^^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^^
!1['a']
true                  1['b'] = 1

The result is 1

If you want to stick with the arrow-expression syntax, you can use the comma operator :

[...str].reduce((characterMap, chr) => 
    (characterMap[chr] = (characterMap[chr] || 0) + 1, characterMap), {});

Or you can use Object.assign :

[...str].reduce((characterMap, chr) => 
    Object.assign(characterMap, {[chr]: (characterMap[chr] || 0) + 1}), {});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM