【JavaScript】Function.prototype.bind() とthis束縛の歴史

この記事は 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);
  },
};

この場合selfwhitney を指すため、 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 などからお気軽に連絡ください。

twitter.com

twitter.com

© 2019- estie, inc.