質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

Q&A

解決済

3回答

1218閲覧

【JS】オブジェクトにReduceを使用する方法

退会済みユーザー

退会済みユーザー

総合スコア0

JavaScript

JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

1グッド

1クリップ

投稿2020/06/27 05:41

問題

JavaScriptの練習問題で、文字列の中からどの文字が、何回使用されているかを探す問題です。
その結果をオブジェクトで出力します。

問題:

function howManyTimes(message) { // code } // {'a': 1, ' ': 2, 'c': 1, 'e': 1, 'I': 1, 'k': 1, 'l': 1, 'i': 1, 's': 1, 't': 1} howManyTimes('I like cats');

  
回答例と違うやり方でとけたのですが、回答例で使用されていたreduceのやり方が理解できませんでした。

私が書いたコード

function howManyTimes(message) { let result = {}; for (let i of message) { let counter = 0; for (let j of message) { if (i == j) counter++; } if (counter == 1) { result[i] = 1; } else if (counter > 1) { result[i] = counter; } } return result; }

わからないこと

以下のコードが回答例ですが、reduce部分が理解できません。

とくに、return (prev[current] = (prev[current] || 0) + 1) && prev;&& prevの箇所です。

return (prev[current] = (prev[current] || 0) + 1)はオブジェクトにない場合、追加しているのは理解できています。

function howManyTimes(message) { return message.split("").reduce(function(prev, current) { return (prev[current] = (prev[current] || 0) + 1) && prev; }, {}); }

よろしくお願いします。

miyabi_pudding👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

think49

2020/06/28 02:19 編集

@yusuke92 さん 私が回答する以前に解は出ていたように思います。 本質問の流れで未解消の問題があるのであれば、質問文を [編集] して、追記するか、各回答の解読できていないコードを個別にコメントで質問して下さい。 何も問題がなければ、ベストアンサーを選んでクローズしてください。
退会済みユーザー

退会済みユーザー

2020/06/28 02:26

think49さん ご回答ありがとうございました!大変勉強になりました。
guest

回答3

0

ベストアンサー

まず、reduce を使わない場合は、以下のように書くことができます。

javascript

1function howManyTimes(message) { 2 let rslt = {}; 3 for (let current of message) { 4 rslt[current] || (rslt[current]=0); // 初期値を与える 5 rslt[current]++; // カウント 6 } 7 return rslt; 8}
  • string は for of が利用でき、1文字ずつの総当りができる。
  • rslt[current] || (rslt[current]=0);if (!rslt[current]) rslt[current]=0; のイディオム

MDN Array#reduce() は、繰り返される accumulator を返却することに意味のある関数です。
最終要素(最後のcurrent)を処理して返却された accumulator が結果になるので、配列から別の型のデータを生成できる特徴があります。

とくに、return (prev[current] = (prev[current] || 0) + 1) && prev;の&& prevの箇所です。

回答例を分解:

javascript

1function howManyTimes(message) { 2 let rslt = {}; 3 return message.split("").reduce(function(prev, current) { 4 prev[current] || (prev[current]=0); // 初期値を与える 5 prev[current]++; // カウントする 6 return prev; // accumulator を返却することに意味がある。 7 }, rslt); 8}

(prev[current] = (prev[current] || 0) + 1)必要なら初期値を与えつつカウントする処理で、必ず 1 以上になる Truthy です。return truthy && prev;return prev となり、 accumulator を返却しています。

ワンライナーで記述されたコードは、演算子単位、ブラケット単位で分解してみるのが理解するための近道です。

投稿2020/06/27 06:25

AkitoshiManabe

総合スコア5432

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sousuke

2020/06/27 06:43

文と式(値)?っていうのかな、ごちゃ混ぜに書いたワンライナーは苦手だわ… 分解した回答例の方がよっぽどわかりやすいというか自然に見えるのは自分だけ? 回答例が回答例になってないと思ってしまう笑
AkitoshiManabe

2020/06/27 06:57

JavaScript はブラウザで利用する前提のため、minify を意識したコードが多いですからね。 Lhankor_Mhyさんも回答されているように、「三項演算」や「論理演算子」はワンライナーのコードでよく目にしますので、押さえておくとよいかも。
miyabi_takatsuk

2020/06/27 07:38 編集

なるほど、すっごいわかりやすかったです。 代入式評価した上で、右項を必ず返すってことですか・・・。 この問題の回答例も上手い。
think49

2020/06/27 08:26

@sousuke さん 読めますが、回り道なアルゴリズムをしてると思います。 (1) 前提として、rslt[current]++ が先にあり、これを生かす為に、初期値0を代入しています。 (2) if文の代わりに || を使用していて、別の書き方で表すと、  if (prev[current]) {} else prev[current] = 0;   or  if (!prev[current]) prev[current] = 0; というところでしょうか。 (if の代用に && を使用するのは見たことがありますが、|| は初めて見ました) 以上2点を把握すれば、読めると思います。 --- ちなみに、私は (1) で0を代入してからインクリメントする工程が無駄に思えてしまうので、こう書きます。 function howManyTimes (message) {  let counter = Object.create(null);  for (let char of message) {   char in counter ? ++counter[char] : counter[char] = 1;  }  return counter; } console.log(JSON.stringify(howManyTimes('Hello, World!\nHello, teratail!'))); // {"H":2,"e":3,"l":6,"o":3,",":2," ":2,"W":1,"r":2,"d":1,"!":2,"\n":1,"t":2,"a":2,"i":1} for-ofのブロック内は  counter[char] = char in counter ? counter[char] + 1 : 1; と書いても良いですが、その場合、counter[char] が2回評価されてしまいます。
退会済みユーザー

退会済みユーザー

2020/06/27 08:31

AkitoshiManabeさん 丁寧な回答ありがとうございます。すごくわかりやすかったです。 とくに、分解して考えることで、理解しやすいことがわかりました。 && prev; の箇所も以下のようにできることがわかりました。 function howManyTimes(message) { return message.split("").reduce(function(prev, current) { prev[current] = (prev[current] || 0) + 1; return prev; }, {}); } ワンライナーのコードをもっと理解できるように訓練しなければ....
AkitoshiManabe

2020/06/28 00:36

yusuke92さん > 分解して考えることで、理解しやすいことがわかりました コメント等で指摘もありますが、正直、「冗長なコード」になります。 ただ、ワンライナーのコードを読み解けるようになれば、think49 さんの回答のように最適化まで考える段階に発展できます。 「&& prev」 部分が 「, prev」とステートメント区切りにセミコロン(;)を置きたい箇所をコンマ(,)にするなど、実によく考察された回答になっていますのでご確認ください。
AkitoshiManabe

2020/06/28 02:58

think49 さん > (if の代用に && を使用するのは見たことがありますが、|| は初めて見ました) ECMAScriptという仕様が誕生した頃(使える機能が少なかった頃)に、仮引数の初期値を与える方法として、よく活用されていました。なんというか「オジサンの書くコード」と言えそうです。
think49

2020/06/28 03:36

@AkitoshiManabe さん > ECMAScriptという仕様が誕生した頃(使える機能が少なかった頃)に、仮引数の初期値を与える方法として、よく活用されていました。なんというか「オジサンの書くコード」と言えそうです。 はい。私もIE6全盛期の頃、  var target = event.target || event.srcElement; こんなコードはよく書きました。 そのあたりは躊躇なく読めるので、「なぜ読みにくかったのか」を自己分析してみると、私のコードを読むスタイルが影響していそうです。 「式」で || を使うのは良く見ますが、「式文」で || を使ったコードが初見でした。 それでも原理的には全く同じなので、読めるはずですが、私の場合、 1. 頭の中でコードの基本型を作る 2. 基本型とアルゴリズムの対応関係を覚える 3. コードを基本型にあてはめて、2. からアルゴリズムを起こす の手順でコードからアルゴリズムに翻訳して読んでいるので、基本型のない初見は 1. 2. の工程が必要な分、読む速度が低下していたのだと思います。 このあたりは無意識の行動なので、しっかり意識した事はありませんが、私が初見のコードが読みづらいのは、おそらくこういうことだと思います。
guest

0

expr1 && expr2
expr1 を false と見ることができる場合は、expr1 を返します。そうでない場合は、expr2 を返します

論理演算子 - JavaScript | MDN

ということで、

js

1(prev[current] = (prev[current] || 0) + 1) && prev

は、(prev[current] = (prev[current] || 0) + 1) が 0 以外の時に prev を返します。

投稿2020/06/27 06:01

Lhankor_Mhy

総合スコア36115

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

miyabi_takatsuk

2020/06/27 07:13

横槍すみません。 今更、論理演算の処理の流れがわかりました。 ||は、&&の逆の処理基準ってわけですね。 すごい勉強になりました。
Lhankor_Mhy

2020/06/27 07:27

ありがとうございます。 ただ、これ、Falsyな値を想定していないと思うので、ワンライナーにするなら return (prev[current] = (prev[current] || 0) + 1), prev; の方が筋がいいと思うんですよね。
miyabi_takatsuk

2020/06/27 07:36

あ、確かに。 falseを想定しないならそうですね。 結局返したいのは、最右ですもんね。
退会済みユーザー

退会済みユーザー

2020/06/27 08:35

Lhankor_Mhyさん ご回答ありがとうございます。 今まではif文でしか&&や||を使用してこなかったので、理解ができませんでした。 if (a && b) 問題例のコードも読めるようにしたいところです... ありがとうございました!
退会済みユーザー

退会済みユーザー

2020/06/27 08:48

調べてみたらこれを短縮評価(short circuit evaluation)と言うんですね....知らなかったです。
Lhankor_Mhy

2020/06/27 08:50 編集

短絡評価、の方が一般的な訳語だと思います。 ただ、今回のケースは短絡評価ではないと思います。
miyabi_takatsuk

2020/06/27 12:46

そうですね、短絡評価とは違うと思います。 今回の回答例は、 &&演算子の性質を利用して、 左辺を評価しつつ、必ずtrueにするようにして、最終は右辺を返しますから。 短絡評価は、左辺で式全体の評価が決定し、右辺は評価しない、 という場合が存在するのに用いられるものです。
退会済みユーザー

退会済みユーザー

2020/06/28 02:02

短絡評価と言うんですね。しかも短絡評価でなかったですか....。まだここの理解ができていないようです。もう少し自分で調べてみます。ありがとうございます。
guest

0

横槍を入れてしまった気がするので、既存回答と重複がありますが、簡単に回答しておきます。

元コードを別の形に変える

JavaScript

1function howManyTimes(message) { 2 return message.split("").reduce(function(prev, current) { 3 return (prev[current] = (prev[current] || 0) + 1) && prev; 4 }, {}); 5}

return 文を同等のコードに変更すると、

JavaScript

1return (prev[current] = prev[current] ? prev[current] + 1 : 0 + 1) && prev

こうなって、

JavaScript

1if (prev[current]) { 2 prev[current] = prev[current] + 1; 3} else { 4 prev[current] = 1; 5} 6return prev;

こうなります。

別バージョン

コメントで私が書いた for-of のコードをArray#reduceで書き直すと、

JavaScript

1function howManyTimes (message) { 2 return Array.from(message).reduce((counter, char) => { 3 char in counter ? ++counter[char] : counter[char] = 1; 4 return counter; 5 }, Object.create(null)); 6} 7console.log(JSON.stringify(howManyTimes('Hello, World!\nHello, teratail!'))); // {"H":2,"e":3,"l":6,"o":3,",":2," ":2,"W":1,"r":2,"d":1,"!":2,"\n":1,"t":2,"a":2,"i":1}

こうなって、

JavaScript

1function howManyTimes (message) { 2 return Array.from(message).reduce((counter, char) => ( 3 char in counter ? ++counter[char] : counter[char] = 1, counter 4 ), Object.create(null)); 5} 6console.log(JSON.stringify(howManyTimes('Hello, World!\nHello, teratail!'))); // {"H":2,"e":3,"l":6,"o":3,",":2," ":2,"W":1,"r":2,"d":1,"!":2,"\n":1,"t":2,"a":2,"i":1}

こうなります。

筋が悪い

筋が悪いといわれている(&私が感じている)のは、

  • {} にfalsyなプロパティ値が存在した場合に、prev[current] の代入が許可されてしまう
  • {} には __proto__ 等の元から存在するプロパティがある

全て解消させると、new Map が最良ですが、ここでは Object.create(null)in 演算子を使用しています。

Re: yusuke92 さん

投稿2020/06/27 09:07

編集2020/06/28 02:10
think49

総合スコア18164

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

think49

2020/06/27 09:08 編集

ベストアンサーを選ばないところをみるに、未解決のようなので、既存と重複があるものの回答しました。
miyabi_takatsuk

2020/06/27 12:34

横槍すみません、 一つ疑問だったのですが、 回答内前半の、 return prev[current] = prev[current] ? prev[current] + 1 : 0 && prev に関してですが、 prev[counter]がtrueである場合、左項が評価され、加算式が実行されるのはわかるのですが、 そうなると代入式の右辺が数値になり、 returnは、オブジェクトではなく、数値が返されてしまうと思ったのですが、この場合でも、最終のprevオブジェクトを返す式になっているのでしょうか? コード試してみたわけではないので、 仕様の勘違いなどでしたら、すみません。 (私が、特に三項演算の仕組みを理解しきれてないだけかも) あ、そうか、三項演算の評価自体は : 0 のところまでってことですかね・・・?
think49

2020/06/28 02:14

@miyabi_takatsuk さん ありがとうございます。 ご指摘の通りでしたので、修正しました。 演算子の優先順位の誤認と0代入ではなく、1代入すべきでした(0だと右辺のprevまで評価されないですね)
退会済みユーザー

退会済みユーザー

2020/06/28 02:24

thik49さん sousuke さんへのコメントに加えてこちらのご回答ありがとうございます。 また miyabi_takatsukさん が指摘していたコードを実行したところ 0 が返ってきたので、疑問に思っていたところでした。 return prev[current] = prev[current] ? prev[current] + 1 : 0 && prev 修正していただきありがとうございました!
miyabi_takatsuk

2020/06/28 07:59

think49さん> 修正、大変にありがとうございます。 回答、大変に勉強にさせていただきました。 Object.create(null)を使うことで、無駄がないのも素晴らしいと思いました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問