オブジェクト メソッドをコールバックとして渡す場合、たとえばsetTimeout
に渡す場合、「 this
が失われる」という既知の問題があります。
この章では、それを修正する方法を見ていきます。
this
失う例はすでに見てきました。メソッドがオブジェクトとは別にどこかに渡されると、 this
失われます。
setTimeout
でどのように起こるかは次のとおりです。
ユーザー = { にします 名前:「ジョン」、 SayHi() { alert(`こんにちは、${this.firstName}!`); } }; setTimeout(user.sayHi, 1000); // こんにちは、未定義です!
ご覧のとおり、出力にはthis.firstName
として「John」が表示されず、 undefined
表示されます。
これは、 setTimeout
オブジェクトとは別に関数user.sayHi
を取得したためです。最後の行は次のように書き換えることができます。
f = user.sayHi にします。 setTimeout(f, 1000); // ユーザーコンテキストが失われました
ブラウザー内のsetTimeout
メソッドは少し特殊です。関数呼び出しに対してthis=window
を設定します (Node.js の場合、 this
タイマー オブジェクトになりますが、ここではあまり重要ではありません)。したがって、 this.firstName
については、存在しないwindow.firstName
を取得しようとします。他の同様のケースでは、通常、 this
単にundefined
になります。
このタスクは非常に典型的なものです。オブジェクト メソッドを別の場所 (ここではスケジューラ) に渡し、そこで呼び出されたいと考えています。正しいコンテキストで呼び出されることを確認するにはどうすればよいでしょうか?
最も簡単な解決策は、ラッピング関数を使用することです。
ユーザー = { にします 名前:「ジョン」、 SayHi() { alert(`こんにちは、${this.firstName}!`); } }; setTimeout(function() { user.sayHi(); // こんにちは、ジョン! }, 1000);
これで、外部の字句環境からuser
を受け取り、通常どおりメソッドを呼び出すことができるようになりました。
同じですが、短くなります:
setTimeout(() => user.sayHi(), 1000); // こんにちは、ジョン!
問題ないようですが、コード構造にわずかな脆弱性があります。
setTimeout
がトリガーされる前に (1 秒の遅延があります!) user
値を変更した場合はどうなるでしょうか?すると突然、間違ったオブジェクトを呼び出してしまいます。
ユーザー = { にします 名前:「ジョン」、 SayHi() { alert(`こんにちは、${this.firstName}!`); } }; setTimeout(() => user.sayHi(), 1000); // ...user の値は 1 秒以内に変更されます ユーザー = { SayHi() {alert("別のユーザーが setTimeout にいます!"); } }; // setTimeout 内の別のユーザー!
次の解決策は、そのようなことが起こらないことを保証します。
関数は、 this
修正できる組み込みメソッド バインドを提供します。
基本的な構文は次のとおりです。
// より複雑な構文は少し後で説明します voidboundFunc = func.bind(context); にします。
func.bind(context)
の結果は特別な関数のような「エキゾチック オブジェクト」であり、関数として呼び出すことができ、 this=context
設定して呼び出しを透過的にfunc
に渡します。
つまり、 boundFunc
の呼び出しは、 this
修正したfunc
と似ています。
たとえば、ここではfuncUser
this=user
を指定してfunc
に呼び出しを渡します。
ユーザー = { にします 名前:「ジョン」 }; 関数 func() { アラート(this.firstName); } let funcUser = func.bind(user); funcUser(); // ジョン
ここでfunc.bind(user)
func
の「バインドされたバリアント」として、 this=user
が固定されています。
すべての引数は、元のfunc
に「そのまま」渡されます。次に例を示します。
ユーザー = { にします 名前:「ジョン」 }; 関数 func(フレーズ) { アラート(フレーズ + ', ' + this.firstName); } // これをユーザーにバインドします let funcUser = func.bind(user); funcUser("こんにちは"); // こんにちは、ジョン (引数 "Hello" が渡され、this=user)
次に、オブジェクト メソッドを試してみましょう。
ユーザー = { にします 名前:「ジョン」、 SayHi() { alert(`こんにちは、${this.firstName}!`); } }; letsayHi = user.sayHi.bind(user); // (*) // オブジェクトなしでも実行可能 こんにちは(); // こんにちは、ジョン! setTimeout(sayHi, 1000); // こんにちは、ジョン! // user の値が 1 秒以内に変更された場合でも //sayHi は古いユーザー オブジェクトへの参照である事前バインド値を使用します ユーザー = { SayHi() {alert("別のユーザーが setTimeout にいます!"); } };
行(*)
では、メソッドuser.sayHi
を取得し、それをuser
にバインドします。 sayHi
は「バインドされた」関数であり、単独で呼び出すことも、 setTimeout
に渡すこともできます。コンテキストは正しいので問題ありません。
ここでは、引数が「そのまま」渡されることがわかりますが、 this
のみがbind
によって修正されます。
ユーザー = { にします 名前:「ジョン」、 言う(フレーズ) { alert(`${phrase}, ${this.firstName}!`); } }; let Say = user.say.bind(user); Say(「こんにちは」); // こんにちは、ジョン! (「Hello」引数は言うために渡されます) Say(「さようなら」); // さようなら、ジョン! (「さようなら」と言うのが通されます)
簡易メソッド: bindAll
オブジェクトに多くのメソッドがあり、それを積極的に渡すことを計画している場合は、それらすべてをループ内でバインドできます。
for (ユーザーにキーを入力させます) { if (ユーザーのタイプ[キー] == '関数') { ユーザー[キー] = ユーザー[キー].bind(ユーザー); } }
JavaScript ライブラリは、lodash の _.bindAll(object, methodNames) など、便利な一括バインディング用の関数も提供します。
これまでは、 this
バインディングについてのみ説明してきました。さらに一歩進めてみましょう。
this
だけでなく、引数もバインドできます。これはめったに行われませんが、場合によっては便利です。
bind
の完全な構文:
letbound = func.bind(context, [arg1], [arg2], ...);
これにより、コンテキストをthis
および関数の開始引数としてバインドできるようになります。
たとえば、乗算関数mul(a, b)
があります。
関数 mul(a, b) { a * b を返します。 }
bind
使用して、そのベースに関数double
作成しましょう。
関数 mul(a, b) { a * b を返します。 } let double = mul.bind(null, 2); アラート( double(3) ); // = mul(2, 3) = 6 アラート( double(4) ); // = mul(2, 4) = 8 アラート( double(5) ); // = mul(2, 5) = 10
mul.bind(null, 2)
の呼び出しは、呼び出しをmul
に渡す新しい関数double
を作成し、コンテキストとしてnull
、最初の引数として2
固定します。さらなる引数は「そのまま」渡されます。
これは部分関数適用と呼ばれます。既存の関数の一部のパラメーターを修正して新しい関数を作成します。
ここでは実際にはthis
使用しないことに注意してください。しかし、 bind
それが必要なので、 null
のようなものを入れる必要があります。
以下のコードの関数triple
、値を 3 倍にします。
関数 mul(a, b) { a * b を返します。 } トリプル = mul.bind(null, 3); とします。 アラート( トリプル(3) ); // = mul(3, 3) = 9 アラート( トリプル(4) ); // = mul(3, 4) = 12 アラート( トリプル(5) ); // = mul(3, 5) = 15
通常、部分関数を作成するのはなぜでしょうか?
利点は、読みやすい名前 ( double
、 triple
) を持つ独立した関数を作成できることです。これはbind
で修正されるため、毎回最初の引数を指定する必要がなく、これを使用できます。
また、非常に汎用的な関数があり、利便性のためにその汎用性の低いバリアントが必要な場合には、部分適用が役立ちます。
たとえば、関数send(from, to, text)
があります。次に、 user
オブジェクト内で、その部分的なバリアント、つまり現在のユーザーから送信するsendTo(to, text)
使用できます。
いくつかの引数を修正したいが、コンテキストthis
修正したくない場合はどうすればよいでしょうか?たとえば、オブジェクト メソッドの場合です。
ネイティブbind
それが許可されません。コンテキストを省略して議論にジャンプすることはできません。
幸いなことに、引数のみをバインドするpartial
関数は簡単に実装できます。
このような:
関数部分(関数, ...argsBound) { return function(...args) { // (*) return func.call(this, ...argsBound, ...args); } } // 使用法: ユーザー = { にします 名前:「ジョン」、 Say(時間、フレーズ) { alert(`[${time}] ${this.firstName}: ${phrase}!`); } }; // 固定時間の部分メソッドを追加します user.sayNow = Partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); user.sayNow("こんにちは"); // 次のようなもの: // [10:00] ジョン:こんにちは!
partial(func[, arg1, arg2...])
呼び出しの結果は、次の関数を使用してfunc
を呼び出すラッパー(*)
です。
this
は取得したものと同じです( user.sayNow
の場合はuser
呼び出します)
次に、それを与えます...argsBound
– partial
呼び出しからの引数 ( "10:00"
)
次に、それを与えます...args
– ラッパーに与えられる引数 ( "Hello"
)
スプレッド構文を使用するととても簡単ですよね?
また、lodash ライブラリからの _.partial 実装も用意されています。
メソッドfunc.bind(context, ...args)
コンテキストthis
と最初の引数 (指定されている場合) を固定する関数func
の「バインドされたバリアント」を返します。
通常、 bind
を適用してオブジェクト メソッドのthis
を修正し、それをどこかに渡すことができるようにします。たとえば、 setTimeout
にします。
既存の関数の一部の引数を修正すると、結果として得られる (汎用性が低い) 関数は、部分的に適用された関数または部分的に適用された関数と呼ばれます。
部分的は、同じ議論を何度も繰り返したくない場合に便利です。 send(from, to)
関数があり、 from
タスクに対して常に同じである必要がある場合と同様に、部分関数を取得して続行できます。
重要度: 5
出力は何になりますか?
関数 f() { アラート(これ); //? } ユーザー = { にします g: f.bind(null) }; user.g();
答えは、 null
。
関数 f() { アラート(これ); // null } ユーザー = { にします g: f.bind(null) }; user.g();
バインドされた関数のコンテキストは固定的に固定されています。それをさらに変える方法はありません。
したがって、 user.g()
を実行している間でも、元の関数はthis=null
で呼び出されます。
重要度: 5
バインディングを追加することでthis
変更できますか?
出力は何になりますか?
関数 f() { アラート(この名前); } f = f.bind( {名前: "ジョン"} ).bind( {名前: "アン" } ); f();
答えは「ジョン」です。
関数 f() { アラート(この名前); } f = f.bind( {名前: "ジョン"} ).bind( {名前: "ピート"} ); f(); // ジョン
f.bind(...)
によって返されるエキゾチックなバインド関数オブジェクトは、作成時にのみコンテキスト (および指定されている場合は引数) を記憶します。
関数を再バインドすることはできません。
重要度: 5
関数のプロパティには値があります。 bind
後は変わりますか?なぜ、あるいはなぜそうではないのでしょうか?
関数sayHi() { アラート( this.name ); } SayHi.test = 5; letbound =sayHi.bind({ 名前:「ジョン」 }); アラート(バウンド.テスト); // 出力はどうなるでしょうか?なぜ?
答えはundefined
。
bind
の結果は別のオブジェクトになります。 test
プロパティはありません。
重要度: 5
以下のコードのaskPassword()
の呼び出しでは、パスワードをチェックし、その答えに応じてuser.loginOk/loginFail
を呼び出す必要があります。
しかし、それはエラーにつながります。なぜ?
すべてが正しく動作するように、強調表示された行を修正します (他の行は変更しないでください)。
関数 askPassword(ok、失敗) { let パスワード = プロンプト("パスワード?", ''); if (パスワード == "ロックスター") ok(); それ以外の場合は失敗します(); } ユーザー = { にします 名前:「ジョン」、 loginOk() { alert(`${this.name} がログインしました`); }、 ログイン失敗() { alert(`${this.name} はログインに失敗しました`); }、 }; askPassword(user.loginOk, user.loginFail);
このエラーは、 askPassword
オブジェクトなしで関数loginOk/loginFail
取得するために発生します。
これらを呼び出すと、当然のことながらthis=undefined
とみなされます。
コンテキストbind
ましょう:
関数 askPassword(ok、失敗) { let パスワード = プロンプト("パスワード?", ''); if (パスワード == "ロックスター") ok(); それ以外の場合は失敗します(); } ユーザー = { にします 名前:「ジョン」、 loginOk() { alert(`${this.name} がログインしました`); }、 ログイン失敗() { alert(`${this.name} はログインに失敗しました`); }、 }; askPassword(user.loginOk.bind(user), user.loginFail.bind(user));
今ではそれが機能します。
別の解決策は次のとおりです。
//... askPassword(() => user.loginOk(), () => user.loginFail());
通常、これでも機能し、見栄えも良くなります。
ただし、 askPassword
が呼び出された後、訪問者が応答して() => user.loginOk()
を呼び出す前にuser
変数が変更される可能性があるより複雑な状況では、信頼性は少し低くなります。
重要度: 5
このタスクは、「this」を失った関数を修正するのをもう少し複雑にしたものです。
user
オブジェクトが変更されました。 2 つの関数loginOk/loginFail
の代わりに、1 つの関数user.login(true/false)
が追加されました。
user.login(true)
ok
として呼び出し、 user.login(false)
をfail
として呼び出すには、以下のコードでaskPassword
に何を渡す必要がありますか?
関数 askPassword(ok、失敗) { let パスワード = プロンプト("パスワード?", ''); if (パスワード == "ロックスター") ok(); それ以外の場合は失敗します(); } ユーザー = { にします 名前:「ジョン」、 ログイン(結果) { alert( this.name + (result ? ' ログインしました' : ' ログインに失敗しました') ); } }; askPassword(?, ?); //?
変更は、強調表示されたフラグメントのみを変更する必要があります。
ラッパー関数を使用するか、簡潔にするために矢印を使用します。
askPassword(() => user.login(true), () => user.login(false));
これで、外部変数からuser
を取得し、通常の方法で実行されます。
または、 user.login
からuser
コンテキストとして使用し、正しい最初の引数を持つ部分関数を作成します。
askPassword(user.login.bind(user, true), user.login.bind(user, false));