2022年10月14日

関数バインディング

オブジェクトメソッドをコールバックとして渡す場合(例えば、`setTimeout` に渡す場合)、よく知られた問題として「`this` の消失」があります。

この章では、それを解決する方法を見ていきます。

「this」の消失

既に「`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); // lost user context

ブラウザの `setTimeout` メソッドは少し特殊です。関数呼び出しに対して `this=window` を設定します(Node.js の場合、`this` はタイマーオブジェクトになりますが、ここではあまり重要ではありません)。そのため、`this.firstName` は `window.firstName` を取得しようとしますが、存在しません。他の同様のケースでは、通常 `this` は単に `undefined` になります。

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

解決策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);

// ...the value of user changes within 1 second
user = {
  sayHi() { alert("Another user in setTimeout!"); }
};

// Another user in setTimeout!

次の解決策では、そのようなことが起こらないことが保証されます。

解決策2:bind

関数は、`this` を修正できる組み込みメソッド bind を提供しています。

基本的な構文は次のとおりです。

// more complex syntax will come a little later
let boundFunc = func.bind(context);

`func.bind(context)` の結果は、関数として呼び出し可能であり、`this=context` を設定して `func` への呼び出しを透過的に渡す特殊な関数のような「エキゾチックオブジェクト」です。

言い換えると、`boundFunc` を呼び出すことは、`this` が固定された `func` を呼び出すようなものです。

たとえば、ここでは `funcUser` は `this=user` で `func` への呼び出しを渡します。

let user = {
  firstName: "John"
};

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

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

ここでは、`this=user` で固定された `func` の「バインドされたバリアント」として `func.bind(user)` を使用します。

すべての引数は、元の `func` に「そのまま」渡されます。たとえば、

let user = {
  firstName: "John"
};

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

// bind this to user
let funcUser = func.bind(user);

funcUser("Hello"); // Hello, John (argument "Hello" is passed, and this=user)

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

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

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

// can run it without an object
sayHi(); // Hello, John!

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

// even if the value of user changes within 1 second
// sayHi uses the pre-bound value which is reference to the old user object
user = {
  sayHi() { alert("Another user in setTimeout!"); }
};

行 `(*)` では、メソッド `user.sayHi` を取得して `user` にバインドします。`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" argument is passed to say)
say("Bye"); // Bye, John! ("Bye" is passed to say)
便利なメソッド:`bindAll`

オブジェクトに多くのメソッドがあり、それを積極的に渡す予定がある場合は、ループでそれらをすべてバインドできます。

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

JavaScript ライブラリも、便利な一括バインディングの関数を提供しています。たとえば、lodash の _.bindAll(object, methodNames) などです。

部分関数

これまで、`this` のバインディングについてのみ説明してきました。さらに一歩進めてみましょう。

`this`だけでなく、引数もバインドできます。これはめったに実行されませんが、場合によっては便利です。

`bind` の完全な構文

let bound = func.bind(context, [arg1], [arg2], ...);

これにより、`this` としてコンテキストと、関数の最初の引数をバインドできます。

たとえば、乗算関数 `mul(a, b)` があります。

function mul(a, b) {
  return a * b;
}

それを基にして `double` 関数を `bind` を使用して作成してみましょう。

function mul(a, b) {
  return a * b;
}

let double = mul.bind(null, 2);

alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10

`mul.bind(null, 2)` への呼び出しは、`mul` への呼び出しを渡し、`null` をコンテキストとして、`2` を最初の引数として固定する新しい関数 `double` を作成します。それ以降の引数は「そのまま」渡されます。

これは 部分関数適用 と呼ばれます。既存の関数のいくつかのパラメーターを固定することによって新しい関数を作成します。

ここでは実際には `this` を使用していません。しかし、`bind` はそれを必要とするため、`null` のようなものを配置する必要があります。

以下のコードの関数 `triple` は値を3倍にします。

function mul(a, b) {
  return a * b;
}

let triple = mul.bind(null, 3);

alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( triple(5) ); // = mul(3, 5) = 15

通常、部分関数を作成するのはなぜですか?

利点は、読みやすい名前(`double`、`triple`)の独立した関数を作成できることです。それを使い、毎回最初の引数を渡す必要はありません。`bind` で固定されているからです。

他のケースでは、非常に汎用的な関数があり、便宜上、それほど汎用性のないバリアントが必要な場合に、部分適用が役立ちます。

たとえば、`send(from, to, text)` 関数があるとします。次に、`user` オブジェクト内で、その部分バリアント `sendTo(to, text)` を使用して、現在のユーザーから送信することができます。

コンテキストなしの部分関数

コンテキスト `this` ではなく、いくつかの引数を固定したい場合はどうでしょうか?たとえば、オブジェクトメソッドの場合です。

ネイティブの `bind` はそれを許可しません。コンテキストを省略して引数にジャンプすることはできません。

幸いなことに、引数のみをバインドするための関数 `partial` は簡単に実装できます。

このように

function partial(func, ...argsBound) {
  return function(...args) { // (*)
    return func.call(this, ...argsBound, ...args);
  }
}

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

// add a partial method with fixed time
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());

user.sayNow("Hello");
// Something like:
// [10:00] John: Hello!

`partial(func[, arg1, arg2...])` 呼び出しの結果は、`func` を次のように呼び出すラッパー `(*)` です。

  • 取得した `this` と同じ(`user.sayNow` 呼び出しの場合は `user`)
  • 次に、`...argsBound` を渡します。`partial` 呼び出しからの引数(`"10:00"`)
  • 次に、`...args` を渡します。ラッパーに渡された引数(`"Hello"`)

スプレッド構文を使用すると非常に簡単ですね!

また、lodash ライブラリからすぐに使用できる _.partial 実装もあります。

まとめ

メソッド `func.bind(context, ...args)` は、コンテキスト `this` と、指定されている場合は最初の引数を固定する関数 `func` の「バインドされたバリアント」を返します。

通常、オブジェクトメソッドの `this` を修正するために `bind` を適用して、それをどこか(たとえば `setTimeout` に)渡します。

既存関数のいくつかの引数を固定する場合、結果として得られる(汎用性が低い)関数は、部分適用された関数または部分関数と呼ばれます。

部分関数は、同じ引数を何度も繰り返したくない場合に便利です。たとえば、`send(from, to)` 関数があり、`from` はタスクに対して常に同じである必要がある場合、部分関数を作成して使用できます。

課題

重要度: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 ); // what will be the output? why?

答え:`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());

通常、それも機能し、良さそうです。

ただし、`askPassword` が呼び出された後、訪問者が回答して `() => user.loginOk()` を呼び出す前までに `user` 変数が変更される可能性のある、より複雑な状況では、信頼性が少し低くなります。

重要度:5

"this"を失う関数の修正 の少し複雑なバリアントの課題です。

`user` オブジェクトが変更されました。2つの関数 `loginOk/loginFail` の代わりに、単一の関数 `user.login(true/false)` があります。

以下のコードで `askPassword` に何を渡せば、`ok` として `user.login(true)` を呼び出し、`fail` として `user.login(false)` を呼び出せますか?

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

let user = {
  name: 'John',

  login(result) {
    alert( this.name + (result ? ' logged in' : ' failed to log in') );
  }
};

askPassword(?, ?); // ?

変更は、強調表示された部分のみを変更する必要があります。

  1. ラッパー関数を使用するか、簡潔にするためにアロー関数を使用します。

    askPassword(() => user.login(true), () => user.login(false));

    ここで、外部変数からuserを取得し、通常の方法で実行します。

  2. または、userをコンテキストとして使用し、最初の引数が正しいuser.loginから部分関数を作成します。

    askPassword(user.login.bind(user, true), user.login.bind(user, false));
チュートリアルマップ

コメント

コメントする前にこれをお読みください…
  • 改善点の提案があれば、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • 少数のコードを挿入するには<code>タグを使用し、複数行の場合は<pre>タグで囲み、10行を超える場合は、サンドボックス(plnkrjsbincodepen…)を使用してください。