ある日、先輩とペアプロしている時にこんな問題を出された
JavaScriptで以下のような関数があったとする
コールバック関数が書ける形ならなんでもいいのだが、ここではPromiseのthenを使うこととする
hogeMethodは別のところで定義しているとして、stringの引数を受け取ってなんやかんや処理する関数だとする
const promiseHandler = () => {
const promise = new Promise<string>((resolve) => {
resolve("hello")
})
return promise.then(value => hogeMethod(value))
}
コールバック関数の部分は以下のように省略した形で書ける
const promiseHandler = () => {
const promise = new Promise<string>((resolve) => {
resolve("hello")
})
return promise.then(hogeMethod)
}
これ自体は知ってたんだけど、じゃあこれをconsole.logで同じことができるのか?
const promiseHandler = () => {
const promise = new Promise<string>((resolve) => {
resolve("hello")
})
return promise.then(console.log) // これは可能?
}
結論、今はこの実装は通る
ただ昔はこれはエラーになっていたらしい
エラーになるとして、なぜ省略しない形ではエラーにならずに省略した時にはエラーになるのか、その理由を考えた上でそのような挙動になるように実際に自分で実装してみてというのが今回出された問題
答えとしてはJavaScriptで難解なものの代表としてしばしば語られるthisと関数実行のコンテキストがその挙動に大きく関わっていたのでその辺をまとめる
thisの挙動
問題を出されたときすぐにthisだなとはわからなかったのだが、色々調べていくうちにthisのスコープの違いが関係していそうだなと思えてきた
色々試した結果、以下のように記述すると問題の挙動を再現できた
const thisTest = {
value: "",
log: function (value: string) {
console.log('this -->', this)
this.value = value
console.log(this.value)
}
}
const promiseHandler = () => {
const promise = new Promise<string>((resolve) => {
resolve("hello")
})
// return promise.then(thisTest.log) // これはエラーになる
return promise.then(value => thisTest.log(value)) // helloが出力される
}
thisTestというオブジェクトの中にlogというメソッドを用意して、その中で受け取った引数のvalueをthis.valueとして格納する
このthis.valueをconsole.logで出力する
これだけだとよくわからないので、それぞれの書き方の時に4行目のthisで何が出力されるのかを見てみる
省略しないコールバック関数の場合
出力結果は以下のようになっており、thisはthisTestオブジェクトを指していることがわかる
よってthisTest.logで引数を渡してやればオブジェクトのvalueに引数のvalueが格納されてそれが出力される
// return promise.then(value => thisTest.log(value))の場合
this --> { value: '', log: [Function: log] }
ここでは記載していないが5行目のthis.value = value
の後にthisを出力させるとvalue: 'hello'
となっているのが確認できた
ここまでは想像通り
省略形のコールバック関数の場合
この場合、thisはundefinedとなるためエラーになる
// return promise.then(thisTest.log)の場合
this --> undefined
このように省略形の場合はthisがundefinedになってしまっているため、this.valueでエラーになる
ではなぜ、呼び出し方によってthisが指すものが変わってしまうのか
キーワードは実行コンテキスト
実行コンテキスト
まずはthisの大まかな性質から見ていき、その後今回のケースでなぜ挙動が変わったのかを見ていく
といってもthisの挙動はいくつかのケースで変わるため、今回は関数内で使用されるthisについて見ていく
関数コンテキストでのthisの挙動
そもそもthisはほとんどの場合、どのように関数が呼ばれたかによって決定される実行時結合という性質を持っている。
関数内でのthisは呼び出され方によって値が異なり、その関数が実行されるコンテキストであるオブジェクトへの参照が格納されている。
ざっくりいうと、関数が呼ばれたときに初めてthisが何かが決定する
(ちなみにアロー関数は定義した段階でthisが何かが決定する)
メソッドの実行では、その実行コンテキストのオブジェクトがthisとして自動的に渡されている
厳密には違うのかもしれないが、hoge.do()みたいな形でメソッドを呼び出した時は.
の前のオブジェクトがthisとして渡されるという理解で良さそうな気がしている
hoge.do() // do内部のthisはhogeオブジェクトを指す
今回のケース
さて、では上記の説明を今回のケースに当てはめて見ていく
先程の例でのthisの挙動をおさらいすると以下の通り
// return promise.then(value => thisTest.log(value))の場合
this --> { value: '', log: [Function: log] }
// return promise.then(thisTest.log)の場合
this --> undefined
まず、thisTest.log(value)とした場合はlogメソッドのthisはthisTestになるためちゃんとそれが格納されている。
ここまでは大丈夫。
問題は単にthisTest.logとした場合の挙動
これまでの説明だとこれもthisの中身はthisTestになりそうだが、実際にはundefinedとなっている
ポイントはthisTest.log()
ではなくthisTest.log
なので、これはメソッドの実行ではなく単にメソッドへの参照を渡しているだけであるということ
中々直感的には分かりにくいが、個人的には以下のようにすると理解しやすかった
一度thisTest.logというメソッドへの参照をlogReferenceというオブジェクトに格納して、それを渡す
const logReference = thisTest.log
return promise.then(logReference) // thisはundefined
単純に参照先を示しているだけだから実行コンテキストが定まっていないのでthisが不定になりundefinedとなる
さて、ではここで疑問が出てきた
元々うまくいっていた形を、以下の形に変更するとどうなるだろうか?
つまり、同じように一度メソッドへの参照を格納したオブジェクトを経由する形にするとどうなるだろうか
return promise.then(value => thisTest.log(value)) // この場合のthisはthisTest
const logReference = thisTest.log
return promise.then(value => logReference(value)) // この場合のthisはどうなる?
個人的には非常に興味深い挙動になったのでぜひ一度考えてみて欲しい
答えを示すと以下のようになる
const logReference = thisTest.log
return promise.then(value => logReference(value))
// this --> undefined
なんと、thisの中身がundefinedになってしまった!
thisTest.log(value)としていた時はlogメソッドの実行コンテキストがthisTestになっていた
しかし、thisTest.logメソッドへの参照をlogReferenceとして格納してそれをlogReference(value)として使用すると、logReferenceの実行コンテキストが存在しないためundefinedになるというわけ
たったこれだけで実行コンテキストが変わるため、thisの中身が変わってしまう
thisを使用する場合は実行コンテキストに気をつける必要がある
おまけ (ES5のStrictモードについて)
完全に余談だが、実行コンテキストがない場合にthisはundefinedになっていたが、これはJSのStrictモードの場合の挙動
StrictモードはES5で追加された仕様でこれによって意図しないグローバル汚染を防ぐことができる
非Strictモードでは実行コンテキストが不定の場合のthisはグローバルオブジェクト、つまりブラウザではwindow, Nodeではglobalオブジェクトが入るようになっていた
この場合、簡単にglobalオブジェクトのプロパティを変更できてしまうのでこの仕様はさすがにまずいでしょってことでStrictモードが導入された
まとめ
thisって普段は使用すること全然ないんだけど、ちゃんと理解しておきたかったから今回先輩に問題出してもらえてめちゃくちゃ勉強になった
もちろんthisの挙動に関しては他にも理解すべきことはあるからこれで全部じゃないけど、実行コンテキストと絡めて実際に挙動確認できたのは納得感も強くてよきだった
こんな問題出してもらえる環境と先輩に感謝!
参考
