この記事は JavaScript Advent Calendar 2020 の 8 日目の記事です。
今回は、最近 JavaScript を書き始めた初心者の方や、ES6 以降の JavaScript しか触っていない方などに向けて、
Function.prototype.bind()
とthis
束縛の歴史
について紹介していきたいと思います。
Function.prototype.bind()
って何 🤔
Function.prototype.bind()
は、関数が呼び出される時に、そのふるまいが依存してしまう実行コンテキストを指定するための関数です。
かつては現在ほど簡単ではなかった実行コンテキスト、あるいはthis
の扱いを、ぐっと容易にしたとても便利な関数です。
現在はアロー関数の登場により使う機会はほぼなくなっているものの、その機能にとても面白みを感じ、今回紹介することにしました。
MDN ドキュメントを見てみよう
bind() メソッドは、呼び出された際に this キーワードに指定された値が設定される新しい関数を生成します。この値は新しい関数が呼び出されたとき、一連の引数の前に置かれます。
出典: Function.prototype.bind() - JavaScript | MDN
これだけだと抽象的でよくわからないので、実際にコードを動かしながら解読していきましょう。
詳しく解説 💡
実行コンテキストの変化を追ってみる
クリスマスが近くなると、クリスマスソングを聴きたくなるのが人の常。
そこで、かの名曲を sing()
することができるオブジェクト、 whitney
を作りました。
const whitney = { sing: function () { console.log("And I will always love you"); }, };
実行時の結果は以下です。
whitney.sing(); // "And I will always love you"
ただ歌うだけでは芸がないので、 setTimeout()
関数を使って 3 秒後に歌わせる start()
関数を追加してみます。
const whitney = { sing: function () { console.log("And I will always love you"); }, start: function () { setTimeout(function () { this.sing(); }, 3000); }, };
実行してみます。
whitney.sing(); // "And I will always love you" whitney.start(); // Uncaught TypeError: this.sing is not a function
おや、エラーになってしまいました。なぜでしょう?
これは、setTimeout
経由で this.sing()
が呼ばれる時、その「実行コンテキスト」が通常( whitney.sing()
での呼び出し)とは違う状態になり、 this
の参照先がずれてしまうために起こります。
検証のため、setTimeout()
を使用しないプロパティも追加し、this
を確認するコンソールを仕込んでみます。
const whitney = { sing: function () { console.log("And I will always love you"); }, startPure: function () { console.log(this); this.sing(); }, startDelay: function () { setTimeout(function () { console.log(this); this.sing(); }, 3000); }, };
実行結果は以下のようになります。
whitney.startPure(); // {sing: ƒ, startPure: ƒ, startDelay: ƒ} // "And I will always love you" whitney.startDelay(); // Window {0: global, window: Window, self: Window, document: document, name: "", location: Location, …} // Uncaught TypeError: this.sing is not a function
このように、setTimeout
経由で this.sing()
を呼んだ場合に, this
の参照が whitney
オブジェクトではなくグローバルの window オブジェクトに向いていることがわかりました。
補足: WindowOrWorkerGlobalScope.setTimeout() - Web APIs | MDN - The "this" problem
では、きちんと "And I will always love you" を console するためにはどうすればよいのでしょうか?
実行コンテキストをコントロールする方法
その解決策は複数あり、それらを古い順(古い仕様の JavaScript で使われていた順)に解説していきます。
- 方法 ①: 変数 self による this の外出し - 方法 ②: Function.prototype.bind() - 方法 ③: アロー関数を使う
- 方法 ①: 変数
self
によるthis
の外出し
参照をコンテキストに依存してしまう this
を使わず、一時的な変数 self
アッパースコープで定義し、self
経由で目的の関数を呼び出します。
const whitney = { sing: function () { console.log("And I will always love you"); }, start: function () { const self = this; setTimeout(function () { self.sing(); }, 3000); }, };
この場合self
は whitney
を指すため、 self.sing()
は whitney.sing()
と同等に処理されます。
またこの実装方法は、「内側から外側のスコープへと順番に変数が定義されているか探す仕組み」であるスコープチェーンを利用したものです。
参考: 関数とスコープ · JavaScript Primer #jsprimer
- 方法 ②:
Function.prototype.bind()
Function.prototype.bind()
は、ES5で追加された、JavaScript の関数が呼び出される時に、そのふるまいが依存してしまう実行コンテキストを指定するための関数です。
実際に使ってみましょう。
const whitney = { sing: function () { console.log("And I will always love you"); }, start: function () { setTimeout(function () { this.sing(); }.bind(this), 3000); }, };
bind()
を使用して、 start()
実行時のコンテキストとして必ずthis
、つまり whitney
を参照するように強制します。
全く違うオブジェクトを渡すことで、this
を意図的にすり替えることも可能です。やってみましょう。
const dummySinger = { sing: function () { console.log("wooooo bahhhhh"); // 意味不明な文字列を歌うメソッド }, }; const whitney = { sing: function () { console.log("And I will always love you"); }, start: function () { setTimeout( function () { this.sing(); }.bind(dummySinger), // コンテキストの強制 3000 ); }, }; whitney.start(); // wooooo bahhhhh
なんと、whitney
が意味不明な文字列を歌ってしまいました。面白いですね。
似たような働きをできる関数として、以下のドキュメントもぜひご一読ください。
Function.prototype.call() - JavaScript | MDN
Function.prototype.apply() - JavaScript | MDN
- 方法 ③: アロー関数を使う
ES6 で導入されたアロー関数
を使うことで、this
の参照先に関して余計な心配をせず実装を進めることができます。
アロー関数内では、this
は宣言時のスコープ(レキシカルスコープと呼ばれる)の this
の値を参照します。
const whitney = { sing: function () { console.log("And I will always love you"); }, start: function () { setTimeout(() => { this.sing(); }, 3000); }, }; whitney.start(); // "And I will always love you"
アロー関数と通常の関数の違いについては、this
の参照以外にも興味深いものがたくさんあります。
以下の記事がよくまとまっていました。
JavaScript: 通常の関数とアロー関数の違いは「書き方だけ」ではない。異なる性質が 10 個ほどある。 - Qiita
まとめ
以上のように、this
のコントロールには過去様々な方法が試されてきました。
現在はアロー関数を使うことで安全・簡単に実装ができていますが、手段に乏しかった時代に生まれた Function.prototype.bind()
は、やろうとおもえば(宣言後でも)コンテキストを自由に注入できるという、面白い関数でした。実に面白い。👀
※ アロー関数で記述された関数のコンテキストを Function.prototype.bind()
でオーバーライドすることはできません。残念。
おわりに
現在 estie では, JavaScript や Ruby に強いエンジニアを積極採用中です!!
不動産のデータを使ってデータプラットフォームを構築したい、分析したい、プロダクトを作ってみたいという方はぜひ!
カジュアルに話聞いてみたいな!という方でも、Twitter などからお気軽に連絡ください。