TL;DR
何の話か
- Javascriptのオブジェクト配列からfilterメソッドで条件に合う要素を抽出する際、callback関数に引数を渡そうとしたらうまく動かなかった
- ブラウザのDevToolsでデバッグしようとしたら、callback関数には引数が渡ってそうにも見えて、原因究明に数時間かかってしまった
わかったこと
- filterメソッドのcallback関数にアロー関数を設定すると
this
をバインドできない - scriptタグのtype属性を雑に設定するとよくない
type属性について参考:HTMLのscriptタグのtype属性について
問題発生時の状況
- Wordpress記事にReactアプリを埋め込むためにcdnでReactを取り込んで開発中、時刻データを含むオブジェクト配列からfilterメソッドで、変数で定義した時刻より前の時刻データを含むオブジェクトの数をカウントしたかったが、できなかった
開発環境(ローカルPC)
-
index.html
とapp.js
を用意
(下記のapp.js
は、該当部分だけ抽出し、処理で使う変数をconst
値に変更して記載)index.html<html><body> <script src="https://unpkg.com/react@latest/umd/react.development.js" crossorigin="anonymous"></script> <script src="https://unpkg.com/react-dom@latest/umd/react-dom.development.js"></script> <script src="https://unpkg.com/@mui/material@latest/umd/material-ui.development.js" crossorigin="anonymous"></script> <script src="https://unpkg.com/babel-standalone@latest/babel.min.js" crossorigin="anonymous"></script> <script type="text/babel" src="./app.js"></script> <div id="root"></div> </body></html>
app.jsconst date_a_array = [ "2014-05", "2014-06", "2014-07" ] const date_b = "2014-06" class App extends React.Component{ // --- 略 let count count = date_a_array.filter(e => ( new Date(e) < new Date(this) ) , date_b) console.log(count) // --- 略 } ReactDOM.createRoot(document.getElementById('root')).render(<App />)
- コンソールへの出力が
1
になってほしいところ、0
となったので修正を開始
DevToolsでデバッグ
Google Chromeの開発者ツールでデバッグした
- まず、filterメソッドのコールバック関数(
e => (...)
の部分)の中身(new Date(e) < new Date(this)
)にブレークポイントを設置し、this
にdate_b
が渡っているか確認したところ、下記のように確認できた
- 別の場所で下記の処理を実行すると、コンソールに
true
が出力されたconsole.log(new Date("2014-05") < new Date("2014-06"))
-
app.js
の該当部分を下記のように修正すると、コンソールに1
が出力されたcount = date_a_array.filter(e => ( new Date(e) < new Date("2014-06") ) , date_b) console.log(count)
- デバッグツール上では
date_b
の文字列がcallback関数に渡されているのに、処理が思い通りいっていないため、再度調べなおすことに
結局何がおかしいのか
調査と実験をいくつかやって、前節の結果の原因を下記2つに絞った
原因1: アロー関数へthis
がバインドされない
Many array methods take a callback function as an argument.
...
method(callbackFn, thisArg)
...
The thisArg argument (defaults to undefined) will be used as the this value when calling callbackFn.
...
The thisArg argument is irrelevant for any callbackFn defined with an arrow function, as arrow functions don't have their own this binding.
参考
- 多くのArrayメソッドでは、callback関数を(第一)引数として持ち、第二引数に渡した値を、callback関数内で
this
として利用できる - ただし、アロー関数では
this
がバインドされない - したがって、下記のようにコードを修正しなければならない
- アロー関数を使わずに
this
を使うcount = date_a_array.filter(function(e){ new Date(e) < new Date(date_b) } , date_b) console.log(count)
- アロー関数を使って
this
を使わないcount = date_a_array.filter(e => ( new Date(e) < new Date(date_b) )) console.log(count)
- アロー関数を使わずに
- これで想定通りのコンソールへ
1
を出力することを確認できた - しかし、アロー関数では
this
で値を渡せないはずなのに、なぜDevToolsでthis
に値が渡っているように見えたのか
原因2: type="text/babel"としてapp.jsを読み込んでいた
-
実験のため、下記のように確認したい該当箇所だけ抽出して、
_app.js
という新しいファイルを作成した_app.jsconst date_a_array = [ "2014-05", "2014-06", "2014-07" ] const date_b = "2014-06" let count // case 1他はここでは省略 // case 2 count = date_a_array.filter(e => ( new Date(e) < new Date(this) ) , date_b) console.log(count)
-
次に、
index.html
でscriptを取り込むとき、下記のように2パターンで取り込んで、前節と同様にDevToolsでブレークポイントを作成してthis
の値を調べた- パターン1:
type="text/babel"
<script type="text/babel" src="./_app.js"></script>
- パターン2:
type
を指定しない
<script src="./_app.js"></script>
- パターン1:
-
これらの結果より、最初にDevToolsで値が
this
にバインドされているように見えたのは、type="text/babel"としてapp.jsを読み込んでいたことが原因であると考えられる -
なぜbabelを読み込むとDevToolsでこのような挙動になるのかまでは未調査
(もし心当たりがある方いらっしゃいましたらシェアいただけるとうれしいです)
補足・メモ
- 今回のケースでは、DevToolsで
this
がバインドされているように見えても結局動いていないので、filterメソッドのcallback関数へ値を渡すコードの書き方は変えないといけないことに注意 - (Node.jsを使うなど)cdnを使わない場合も同じようになるのかは不明
- 参考:MIMEについて
所感
- 備忘録もかねてシェアさせていただきました
- babelのDevToolsへの影響についてはっきりさせないと、完全に原因特定したとは言えないが、あたりがつかなさすぎるため断念しました
(追記)
@murasuke様からかなり掘り下げた補足・追加調査をしていただきました。コメント欄参照。