オブジェクトメソッドをコールバックとして渡す場合(例えば、`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)
オブジェクトに多くのメソッドがあり、それを積極的に渡す予定がある場合は、ループでそれらをすべてバインドできます。
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` はタスクに対して常に同じである必要がある場合、部分関数を作成して使用できます。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグで囲み、10行を超える場合は、サンドボックス(plnkr、jsbin、codepen…)を使用してください。