関数バインディング

オブジェクトメソッドで setTimeout 使ったり、オブジェクトメソッドを渡すような場合、"this を失う" という既知の問題があります。

突然、this が正しく動作するのをやめます。この状況は初心者の開発者には典型的ですが、経験者でも同様に起こりえます。

“this” を失う

私たちはすでに、JavaScriptでは this を失うことが容易であることを知っています。 あるメソッドがオブジェクトから別の場所に渡されると、this は失われます。

ここで setTimeout を利用してどのように起こるのかを示します:

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(user.sayHi, 1000); // Hello, undefined!

上で分かる通り出力は、 this.firstName は “John” ではなく、 undefined です!

これは、 setTimeout はオブジェクトとは別に関数 user.sayHi を持っているためです。最後の行はこのように書き直すことができます:

let f = user.sayHi;
setTimeout(f, 1000); // user コンテキストを失います

ブラウザにおいて、メソッド setTimeout は少し特別です: 関数呼び出しでは this=window を設定します(Node.JS では、this はタイマーオブジェクトになりますが、ここではほとんど関係ありません)。従って、this.firstName は、存在しない window.firstName を取得しようとします。他の同様のケースでは、通常 thisundefined になります。

このタスクは非常に典型的です – オブジェクトメソッドをどこか別の場所(ここではスケジューラに渡して)から呼び出したい場合です。それが適切なコンテキストで呼び出されることはどのように確認すればよいでしょう?

解決策 1: 囲む

最もシンプルな解決策はラップされた関数を使うことです:

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(function() {
  user.sayHi(); // Hello, John!
}, 1000);

これは上手く動きます。なぜなら、外部のレキシカル環境から user を受け取り、メソッドを普通に呼び出すためです。

これも同じですが、より短い記法です:

setTimeout(() => user.sayHi(), 1000); // Hello, John!

良く見えますが、コード構造に僅かな脆弱性があります。

仮に setTimeout が動く前に(上の例では1秒の遅延があります!)、user が値を変更していたら?すると突然、間違ったオブジェクトを呼び出すでしょう!

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

setTimeout(() => user.sayHi(), 1000);

// ...1秒以内に次が行われると
user = { sayHi() { alert("Another user in setTimeout!"); } };

// Another user in setTimeout?!?

次の解決策はこのようなことが起きないことを保証します。

解決策 2: bind

関数は、this を固定できる組み込みメソッド bind を提供します。

基本の構文は次の通りです:

// より複雑な構文はもう少し後で
let boundFunc = func.bind(context);

func.bind(context) の結果は特別な関数ライクな “エキゾチックオブジェクト(exotic object)” です。これは関数として呼ぶことができ、functhis=context を透過的に渡します。

言い換えると、boundFunc の呼び出しは、固定された this での func 呼び出しです。

例えば、funcUserthis=user での func 呼び出しを渡します:

let user = {
  firstName: "John"
};

function func() {
  alert(this.firstName);
}

let funcUser = func.bind(user);
funcUser(); // John

ここで、func.bind(user)this=user で固定された func の “バインドされたバリアント” となります。

すべての引数はオリジナルの func に “そのまま” 渡されます。例:

let user = {
  firstName: "John"
};

function func(phrase) {
  alert(phrase + ', ' + this.firstName);
}

// this を user にバインドする
let funcUser = func.bind(user);

funcUser("Hello"); // Hello, John (引数 "Hello" が渡され, this=user)

さて、オブジェクトメソッドで試してみましょう。:

let user = {
  firstName: "John",
  sayHi() {
    alert(`Hello, ${this.firstName}!`);
  }
};

let sayHi = user.sayHi.bind(user); // (*)

sayHi(); // Hello, John!

setTimeout(sayHi, 1000); // Hello, John!

(*) の行で、メソッド user.sayHiuser にバインドしています。sayHi は “束縛(バインド)された” 関数であり、単独もしくは setTimeout に渡して呼び出すことができます。

引数が “そのまま” 渡され、this だけが bind によって固定されていることがわかります:

let user = {
  firstName: "John",
  say(phrase) {
    alert(`${phrase}, ${this.firstName}!`);
  }
};

let say = user.say.bind(user);

say("Hello"); // Hello, John ("Hello" 引数は say に渡されます)
say("Bye"); // Bye, John ("Bye" は say に渡されます)
便利なメソッド: bindAll

もしオブジェクトが多くのメソッドを持ち、それらをバインドする必要がある場合、すべてループでバインドできます。:

for (let key in user) {
  if (typeof user[key] == 'function') {
    user[key] = user[key].bind(user);
  }
}

JavaScriptライブラリはまた、便利な大量バインディングのための機能も提供しています。e.g. _.bindAll(obj) in lodash.

サマリ

メソッド func.bind(context, ...args) はコンテキスト this を固定した関数 func の “束縛されたバリアント” を返します。

通常は、オブジェクトメソッドで this を固定するために bind を適用し、どこかに渡すことができるようにします。たとえば、setTimeout に。 近代的な開発で “束縛する” 理由はまだまだありますが、私たちは後でそれらを知るでしょう。

タスク

重要性: 5

何が出力されるでしょう?

function f() {
  alert( this ); // ?
}

let user = {
  g: f.bind(null)
};

user.g();

解答: null.

function f() {
  alert( this ); // null
}

let user = {
  g: f.bind(null)
};

user.g();

バインドされた関数のコンテキストはハードコードされています。さらにそれを変える方法はありません。

従って、たとえ user.g() を実行しても、元の関数は this=null で呼ばれます。

重要性: 5

追加のバインディングで this を変更することができるでしょうか?

何が出力されるでしょう?

function f() {
  alert(this.name);
}

f = f.bind( {name: "John"} ).bind( {name: "Ann" } );

f();

解答: John.

function f() {
  alert(this.name);
}

f = f.bind( {name: "John"} ).bind( {name: "Pete"} );

f(); // John

f.bind(...) によって返却された バインドされた関数 オブジェクトは作成時にのみコンテキスト(と提供されていれば引数を)を覚えます。

関数を再バインドすることはできません。

重要性: 5

関数プロパティには値があります。bind 後それは変わるでしょうか?なぜ?詳細に述べてください。

function sayHi() {
  alert( this.name );
}
sayHi.test = 5;

let bound = sayHi.bind({
  name: "John"
});

alert( bound.test ); // 何が出力されるでしょう? それはなぜでしょう?

解答: undefined.

bind の結果は別のオブジェクトです。それは test プロパティを持っていません。

重要性: 5

下のコードの askPassword() の呼び出しは、パスワードをチェックし、その回答により user.loginOk/LoginFail を呼びます。

しかし、それはエラーになります、なぜでしょう?

すべてが正しく動作し始めるよう、ハイライトされた行を修正してください(他の行は変更しません)。

function askPassword(ok, fail) {
  let password = prompt("Password?", '');
  if (password == "rockstar") ok();
  else fail();
}

let user = {
  name: 'John',

  loginOk() {
    alert(`${this.name} logged in`);
  },

  loginFail() {
    alert(`${this.name} failed to log in`);
  },

};

askPassword(user.loginOk, user.loginFail);

ask はオブジェクトなしで関数 loginOk/loginFail を取得しているためにエラーが起きます。

それらを呼ぶとき、通常 this=undefined と想定します。

コンテキストを bind しましょう:

function askPassword(ok, fail) {
  let password = prompt("Password?", '');
  if (password == "rockstar") ok();
  else fail();
}

let user = {
  name: 'John',

  loginOk() {
    alert(`${this.name} logged in`);
  },

  loginFail() {
    alert(`${this.name} failed to log in`);
  },

};

askPassword(user.loginOk.bind(user), user.loginFail.bind(user));

これで動作します。

別の解答としては:

//...
askPassword(() => user.loginOk(), () => user.loginFail());

通常は動作しますが、user が要求して () => user.loginOk() を実行する間に上書きされる可能性のあるようなより複雑な状況の場合に失敗する可能性があります。

チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。