いろいろとダメな所があるので一つずつ追っていきましょう。
- Array.prototype.mapの役割・使い方が誤っている
- Promiseの中に一度入ると同期処理には帰ってこれない
ある配列の中のオブジェクトをマップで参照し、それぞれに新しいプロパティを追加したい
mapは数学用語の写像です。
Aの集合体一つ一つに対して、とある加工を施しA'の集合体を作り出すものです。
プログラミングでmapを扱う場合、
A'の集合体を作る時にAが勝手に変更されないようにしましょう。
質問文のプロパティを追加したいと代入演算子を使ってプロパティを書き換えていますが、
これはmapという意味ではやってはならない行為です。
加工するなら最初からforEachやfor...ofを使いましょう。
js
1// このオブジェクトの配列があり、年齢を見ながら成年か否かというプロパティを追加したいとする
2const users1 = [
3 {name: "taro", age: 20},
4 {name: "jiro", age: 17}
5];
6
7// ×: mapのこういう使い方はダメ
8// 同僚は写像の結果を返り値として受け取る事を期待するので混乱の元になる
9users1.map(user => user.isAdult = user.age >= 20);
10
11// ○: 処理しかしていないのでforEachを使う
12// users1変数を勝手に書き換える行為自体がお行儀が悪いのでこれもあまり良くない
13users1.forEach(user => user.isAdult = user.age >= 20);
14
15// ◎: オブジェクトの配列をmapで扱う場合、
16// スプレッドプロパティでシャローコピーしながら新しくオブジェクトを生成するのがベストプラクティス
17// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Object_initializer
18const users2 = users1.map(user => ({...user, isAdult: user.age >= 20}));
値の加工の仕方関してはこれでOK
promiseFoo().then()
直下では代入できませんでした。
promiseFoo().then()
から戻り値を取り出すか、直下でオブジェクトを操作するにはどうすればよいでしょうか。
まずPromiseの機能とその限界から説明しましょう。
JavaScript名物の非同期処理のネストによる
「コールバック地獄」という単語を耳にした事はありますか?
Promiseはこのコールバック地獄という
非同期処理をネストさせる度に一生ソースコードのネストが深くなっていくというクソみたいな状況に対して、
オブジェクト指向プログラミングのテクニックでねじ伏せて解決する事を目的として作られました。
js
1// 古の非同期処理
2getIcon(url1, (err, icon1) => {
3 getIcon(url2, (err, icon2) => {
4 getIcon(url3, (err, icon3) => {
5 const icons = [icon1, icon2, icon3];
6 console.log(icons);
7 })
8 })
9});
10
11// [url1, url2, url3]にしてfor文で非同期処理を書けるようにして?
12// いやーきついでしょ。
13
14// Promise.thenを使うとコードのネストが1段階より先にいかない
15Promise.resolve([])
16 .then(icons => getIcons(url1).then(value => [...icons, value]))
17 .then(icons => getIcons(url2).then(value => [...icons, value]))
18 .then(icons => getIcons(url3).then(value => [...icons, value]))
19 .then(icons => {
20 console.log(icons);
21 });
22
23// Promiseの登場により可変回数の非同期処理も簡単に記述出来るようになった
24let promise = Promise.resolve([]);
25for (const url of [url1, url2, url3]) {
26 promise = promise.then(icons =>
27 getIcon(url).then(value => [...icons, value])
28 );
29}
30promise.then(icons => {
31 console.log(icons);
32});
逆に言うとPromiseはこの.then(fn)
メソッドを数珠つなぎにして
非同期処理を延々とぶら下げるしか機能がありません。
そもそもJavaScript(Node.js)はシングルスレッドなので
低速なHDD読み込みや、HTTP通信を待ってられないから、
非同期処理やイベントループ等の機能を作って扱う仕組みを設けているわけで、
一度非同期処理に入ってしまうと元の流れに戻る事は出来ません。
つまり、質問文の「promiseFoo().then()から戻り値を取り出す」事は不可能です。
JavaScript(Node.js)プログラマの要望は微塵も満たせてないですね。
さて、上記を元に質問文を実現するためのコードを書いていきましょう。
まずはPromiseだけでやる場合のコードを示します。
質問文ではPromiseの配列を作った所で困ってますが、
Promise.allという要望そのものの機能が存在します。
なのでPromiseの配列をPromise.all(promises).then(fn)
でオブジェクトの配列に変換してから.then
メソッドで中身を確認しにいく流れとなります。
js
1const main1 = () => {
2 // 一度Promiseの配列を作る
3 const promises = filteredResults.map(({link, icon}) =>
4 getIcon(link).then(value => ({...value, icon}))
5 );
6 // Promise.allでそれぞれのPromiseから値を取り出す
7 // 一度Promiseという非同期処理の中に入ったら、同期処理には戻れないので`.then`メソッドを叩いて中で確認する事になる
8 Promise.all(promises).then(icons => {
9 console.log(icons);
10 });
11}
12
13// 上記を清書するとこういうコードになる
14const main2 = () => {
15 Promise.all(
16 filteredResults.map(({link, icon}) =>
17 getIcon(link).then(value => ({...value, icon}))
18 )
19 ).then(icons => {
20 console.log(icons);
21 });
22}
これはPromiseを熟知していないと書けませんし、
それなりの中級者・上級者でも一生これを読み書きしろと言われれば顔をしかめます。
JavaScript(Node.js)のエンジニアが楽をするためには
結局の所Promiseの値を同期処理で記述する所までひきずり下ろすしかありません。
しかし、Promiseは非同期処理なので同期処理には帰ってこれない。
そこで、async/awaitという糖衣構文がES2017で実装されました。
awaitというワードを使うとPromise.then(fn)
を自動実行して、
fn
の第一引数を手前に引っ張り上げてくるようなコードに変換されます。
js
1// async関数を定義してawait構文を有効にする
2const main3 = async () => {
3 const icons = await Promise.all(
4 filteredResults.map(async ({link, icon}) =>
5 const value = await getIcon(link);
6 return {...value, icon};
7 )
8 );
9 console.log(icons);
10};
これはmain2と全く同じ挙動をするコードです。
async関数を実行すると、内部ではPromiseを使いまくる事になるので100%Promiseのインスタンスを返します。
await構文を使ってpromise.then(fn)
の第一引数を手前に引っ張り出してくる事の威力が垣間見えますね。
thenのネストがコードから消えるだけでここまで読みやすくなります。
そして、async/await構文は普通のfor文をサポートします。
なのでmapをかませるよりはfor文使った方が簡素なコードになる事が多いですね。
mapを使えば一度に全てに要素に対して処理を実行しますが、通信先に負荷を掛けてしまう。
for文で毎回完了まで待つようにすれば通信先には負荷を掛けないが遅くなる。
この違いはありますが、読みやすさは段違いです。
js
1// もうこれでよくね?
2const main4 = async () => {
3 const icons = [];
4 for (const {link, icon} of filteredResults) {
5 const value = await getIcon(link);
6 icons.push({...value, icon});
7 }
8 console.log(icons);
9};