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

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

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

JSON(JavaScript Object Notation)は軽量なデータ記述言語の1つである。構文はJavaScriptをベースとしていますが、JavaScriptに限定されたものではなく、様々なソフトウェアやプログラミング言語間におけるデータの受け渡しが行えるように設計されています。

Node.js

Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

JavaScript

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

TypeScript

TypeScriptは、マイクロソフトによって開発された フリーでオープンソースのプログラミング言語です。 TypeScriptは、JavaScriptの構文の拡張であるので、既存の JavaScriptのコードにわずかな修正を加えれば動作します。

Q&A

解決済

4回答

2186閲覧

(解決済み)JavaScript Promise.then()でオブジェクトを操作できない

poppy1115

総合スコア12

JSON

JSON(JavaScript Object Notation)は軽量なデータ記述言語の1つである。構文はJavaScriptをベースとしていますが、JavaScriptに限定されたものではなく、様々なソフトウェアやプログラミング言語間におけるデータの受け渡しが行えるように設計されています。

Node.js

Node.jsとはGoogleのV8 JavaScriptエンジンを使用しているサーバーサイドのイベント駆動型プログラムです。

JavaScript

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

TypeScript

TypeScriptは、マイクロソフトによって開発された フリーでオープンソースのプログラミング言語です。 TypeScriptは、JavaScriptの構文の拡張であるので、既存の JavaScriptのコードにわずかな修正を加えれば動作します。

0グッド

3クリップ

投稿2022/10/11 15:16

編集2022/10/12 12:53

前提

実現したいこと

ある配列の中のオブジェクトをマップで参照し、それぞれに新しいプロパティを追加したいのですが、map()直下ではオブジェクトを代入できたのに対し、promiseFoo().then()直下では代入できませんでした。promiseFoo().then()から戻り値を取り出すか、直下でオブジェクトを操作するにはどうすればよいでしょうか。

該当のソースコード

TypeScript

1filteredResults.map((result: any) => { 2 getIcon(result.link).then((value: any) => { 3 console.log('URL: ' + value) 4 result.icon = value 5 }) 6})

Powershell

1Got an icon from https://stackoverflow.com/questions/68788222/filtering-an-axios-response | https://cdn.sstatic.net/Sites/stackoverflow/Img/favicon.ico?v=ec617d715196 2URL: https://cdn.sstatic.net/Sites/stackoverflow/Img/favicon.ico?v=ec617d715196 3Got an icon from https://stackoverflow.com/questions/38750705/filter-object-properties-by-key-in-es6 | https://cdn.sstatic.net/Sites/stackoverflow/Img/favicon.ico?v=ec617d715196 4URL: https://cdn.sstatic.net/Sites/stackoverflow/Img/favicon.ico?v=ec617d715196 5

これだと全く代入されていませんが、ご覧の通りPromiseオブジェクトから値を取り出すことには成功しています。そして前述のようにmap()直下ならオブジェクトに普通に代入できます。

試したこと

TypeScript

1filteredResults.map((result: any) => { 2 result.icon = getIcon(result.link).then((value: any) => { 3 console.log('URL: ' + value) 4 return value 5 }) 6})

これだとオブジェクトにはPromiseオブジェクトが代入されてしまいます。

補足情報(FW/ツールのバージョンなど)

JSON

1{ 2 "ts-node": "^10.9.1", 3 "typescript": "^4.8.4", 4 "@swc/core": "^1.3.4", 5}

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

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

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

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

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

maisumakun

2022/10/11 22:20

> これだと全く代入されていませんが どうやって確認しましたか?
poppy1115

2022/10/11 22:49

オブジェクトをJSONファイルに保存して確認しました
maisumakun

2022/10/11 22:52

保存コードを実行するタイミングが悪かった、という可能性もあります。
poppy1115

2022/10/11 23:23

``` filteredResults.map((result: any) => { getIcon(result.link).then((value: any) => { console.log('URL: ' + value) return value }).then((value: any) => (result.icon = value)) }) ``` このコードでも解決しませんでした。
maisumakun

2022/10/12 01:08

JSONへの保存コードはどこへどのように書いたのですか?
guest

回答4

0

いろいろとダメな所があるので一つずつ追っていきましょう。

  • 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};

投稿2022/10/12 00:19

編集2022/10/12 06:00
miyabi-sun

総合スコア21158

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

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

poppy1115

2022/10/12 12:56

調べた中で一番わかりやすい解説でした。Googleでも上位に来るレベルですね! 解決したコードを載せましたので、ぜひご参照ください。 forを使ったやり方は簡単そうでしたが、finalResultsにチェーンさせるのが一番簡単そうでしたのでmapを採用しました。またgetIcon()の戻り値はstringでしたので、説明不足を痛感しました。 ご解説ありがとうございました。
guest

0

自己解決

解決したコード

TypeScript

1const finalResults = () => { 2 return filteredResults.map(async (result: any) => { 3 const value = await getIcon(result.link) 4 console.log('URL: ' + value) 5 return { ...result, icon: value } 6 }) 7}

filteredResutlsにチェーンさせて、もっとシンプルにしました。

TypeScript

1const filteredResults = response.data.items 2 .map((item: object) => 3 Object.fromEntries( 4 Object.entries(item).filter(key => 5 requiredFields.some(field => key.includes(field)) 6 ) 7 ) 8 ) 9 .map(async (result: any) => { 10 const value = await getIcon(result.link) 11 console.log('URL: ' + value) 12 return { ...result, icon: value } 13 })

自分が持っていた問題をまとめてみました。

  • async/awaitPromiseオブジェクトの値を取り出せないと思っていた
  • filteredResutls配列内のオブジェクトをマップで取り出すとき、Spread構文にしていなかった
  • returnの理解が甘かった。特にアロー関数のせいでreturnを忘れがちだった

超簡単な別解

ただGoogleのSecret APIを使うだけで要件は満たせました。くだらないスクレイピングの実装で数日失ってしまいましたが、とても勉強になりました。async/awaitが分かると言えるようになったと思います。

JavaScript

1`https://www.google.com/s2/favicons?domain=${domain}&sz=${size}`

TypeScript

1const finalResults = () => { 2 return filteredResults.map(async (result: any) => { 3 const iconUrl = `https://www.google.com/s2/favicons?domain=${result.domain}&sz=${256}` 4 console.log('URL: ' + iconUrl) 5 if (iconUrl === undefined) return { ...result, icon: '' } 6 return { ...result, icon: iconUrl } 7 }) 8}

投稿2022/10/12 12:46

編集2022/10/13 14:28
poppy1115

総合スコア12

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

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

shiracamus

2022/10/12 13:59 編集

filteredResults は Promise の配列になりますが、意図した結果でしょうか? > console.log(filteredResults) [ Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]
poppy1115

2022/10/12 14:13 編集

Axiosを使っているので、Promiseオブジェクトの一種ですね。意図していませんでしたが、最終的にstringにできています。 本来filteredResultsはAxiosResponse<any, any>という型となります。ですが、 if (filteredResults.length < perpage) break といった感じで長さを求めているので、any型になっているみたいです。 responses.push(filteredResults) const results = await Promise.all(responses.flat()) // flat() combines multi-dimensional array elements const bookmarks: string = JSON.stringify(results, null, 2) filteredResultsを格納するresponses配列の型はAxiosResponse<any, any>[]です。 bookmarksで初めてstring型になります。
guest

0

Promise.then()やPromise.catch()の戻り値はPromiseです。
Promiseから値を取り出すにはawaitしないといけません。
awaitするにはasync関数でなければいけません。

javascript

1filteredResults.map(async (result: any) => { 2 result.icon = await getIcon(result.link).then((value: any) => { 3 console.log('URL: ' + value) 4 return value 5 }) 6})

async関数にするならthenを使う必要はありません。

javascript

1filteredResults.map(async (result: any) => { 2 const value: any = await getIcon(result.link); 3 console.log('URL: ' + value); 4 result.icon = value; 5})

当然のことながら、mapした結果はPromiseの配列になるので、必要ならPromise.allで処理するなどしてください。
Promise.allもまたPromiseを返しますけどね。

投稿2022/10/11 23:22

編集2022/10/11 23:55
shiracamus

総合スコア5406

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

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

0

(コメントのつもりが回答になっていたので削除)

投稿2022/10/12 13:55

編集2022/10/12 13:55
shiracamus

総合スコア5406

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

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問