JavaScript は、関数を扱う際に優れた柔軟性をもたらします。これらは渡したり、オブジェクトとして使用したりできます。次に、それらの間で呼び出しを転送し、装飾する方法を見ていきます。
CPU に負荷がかかる関数slow(x)
があるとしますが、その結果は安定しています。言い換えれば、同じx
に対しては常に同じ結果が返されます。
関数が頻繁に呼び出される場合は、再計算に余分な時間を費やすことを避けるために、結果をキャッシュ (記憶) したい場合があります。
ただし、その機能をslow()
に追加する代わりに、キャッシュを追加するラッパー関数を作成します。これから説明するように、そうすることには多くの利点があります。
コードは次のとおりです。説明は次のとおりです。
関数遅い(x) { // ここには CPU を大量に使用するジョブが存在する可能性があります alert(`${x}` で呼び出されました); x を返します。 } 関数 cachingDecorator(func) { let キャッシュ = new Map(); 戻り関数(x) { if (cache.has(x)) { // キャッシュにそのようなキーがある場合 戻りキャッシュ.get(x); // そこから結果を読み取ります } let 結果 = func(x); // それ以外の場合は関数を呼び出します キャッシュ.set(x, 結果); // そして結果をキャッシュ(記憶)します 結果を返します。 }; } 遅い = cachingDecorator(遅い); アラート(遅い(1) ); // low(1) がキャッシュされ、結果が返されます alert( "また: " +slow(1) ); // キャッシュから返されたslow(1)の結果 アラート(遅い(2) ); // low(2) がキャッシュされ、結果が返されます alert( "また: " +slow(2) ); // キャッシュから返されたslow(2)の結果
上記のコードでは、 cachingDecorator
デコレータ、つまり別の関数を受け取り、その動作を変更する特別な関数です。
アイデアは、任意の関数に対してcachingDecorator
を呼び出すことができ、キャッシュ ラッパーを返すということです。これは素晴らしいことです。そのような機能を使用できる関数がたくさんあり、それらにcachingDecorator
適用するだけで済むからです。
キャッシュをメイン関数コードから分離することで、メインコードもよりシンプルに保ちます。
cachingDecorator(func)
の結果は、 func(x)
の呼び出しをキャッシュ ロジックに「ラップ」する「ラッパー」: function(x)
です。
外部コードから見ると、ラップされたslow
関数は依然として同じことを行います。その動作にキャッシュの側面が追加されただけです。
要約すると、 slow
自体のコードを変更する代わりに別のcachingDecorator
使用することには、いくつかの利点があります。
cachingDecorator
は再利用可能です。それを別の関数に適用できます。
キャッシュ ロジックは別個であり、 slow
自体の複雑さは増加しませんでした (存在する場合)。
必要に応じて、複数のデコレータを組み合わせることができます (他のデコレータもこれに続きます)。
前述のキャッシュ デコレーターは、オブジェクト メソッドの操作には適していません。
たとえば、以下のコードでは、 worker.slow()
装飾後に動作を停止します。
// worker.slow キャッシュを作成します ワーカー = { にします someMethod() { 1を返します。 }、 遅い(x) { // 恐ろしい CPU 負荷の高いタスクがここにあります alert(" + x で呼び出されました); return x * this.someMethod(); // (*) } }; // 前と同じコード 関数 cachingDecorator(func) { let キャッシュ = new Map(); 戻り関数(x) { if (cache.has(x)) { 戻りキャッシュ.get(x); } let 結果 = func(x); // (**) キャッシュ.set(x, 結果); 結果を返します。 }; } アラート(worker.slow(1) ); // 元のメソッドは機能します worker.slow = cachingDecorator(worker.slow); // 今度はキャッシュにします アラート(worker.slow(2) ); // おっと!エラー: 未定義のプロパティ 'someMethod' を読み取れません
this.someMethod
にアクセスしようとして失敗する行(*)
でエラーが発生します。理由がわかりますか?
その理由は、ラッパーが行(**)
で元の関数をfunc(x)
として呼び出しているためです。そして、そのように呼び出すと、関数はthis = undefined
を取得します。
以下を実行しようとすると、同様の症状が観察されます。
let func = worker.slow; 関数(2);
したがって、ラッパーは呼び出しを元のメソッドに渡しますが、コンテキストthis
は渡しません。したがって、エラーが発生します。
修正しましょう。
this
明示的に設定して関数を呼び出すことができる特別な組み込み関数メソッド func.call(context, …args) があります。
構文は次のとおりです。
func.call(context, arg1, arg2, ...)
最初の引数をthis
として指定し、次の引数を引数として指定してfunc
を実行します。
簡単に言うと、これら 2 つの呼び出しはほぼ同じことを行います。
関数(1, 2, 3); func.call(obj, 1, 2, 3)
どちらも引数1
、 2
、 3
指定してfunc
呼び出します。唯一の違いは、 func.call
もthis
obj
に設定することです。
例として、以下のコードでは、さまざまなオブジェクトのコンテキストでsayHi
を呼び出します。sayHi.call sayHi.call(user)
、 this=user
を指定してsayHi
を実行し、次の行はthis=admin
設定します。
関数sayHi() { アラート(この名前); } ユーザー = { 名前: "ジョン" }; let admin = { 名前: "管理者" }; // call を使用して、さまざまなオブジェクトを「this」として渡します SayHi.call( ユーザー ); // ジョン SayHi.call( 管理者 ); // 管理者
ここでは、特定のコンテキストとフレーズでcall
to call say
を使用しています。
関数 Say(フレーズ) { アラート(this.name + ': ' + フレーズ); } ユーザー = { 名前: "ジョン" }; // ユーザーがこれになり、「Hello」が第一引数になります Say.call( ユーザー, "こんにちは" ); // ジョン: こんにちは
私たちの場合、ラッパーでcall
使用してコンテキストを元の関数に渡すことができます。
ワーカー = { にします someMethod() { 1を返します。 }、 遅い(x) { alert(" + x で呼び出されました); return x * this.someMethod(); // (*) } }; 関数 cachingDecorator(func) { let キャッシュ = new Map(); 戻り関数(x) { if (cache.has(x)) { 戻りキャッシュ.get(x); } let result = func.call(this, x); // "this" が正しく渡されるようになりました キャッシュ.set(x, 結果); 結果を返します。 }; } worker.slow = cachingDecorator(worker.slow); // 今度はキャッシュにします アラート(worker.slow(2) ); // 動作します アラート(worker.slow(2) ); // 動作しますが、オリジナル (キャッシュされた) は呼び出されません
今はすべて順調です。
すべてを明確にするために、 this
どのように伝わるかをさらに詳しく見てみましょう。
装飾後のworker.slow
、ラッパーfunction (x) { ... }
になります。
したがって、 worker.slow(2)
が実行されると、ラッパーは引数として2
取得し、 this=worker
(ドットの前のオブジェクトです) を取得します。
ラッパー内では、結果がまだキャッシュされていないと仮定して、 func.call(this, x)
現在のthis
( =worker
) と現在の引数 ( =2
) を元のメソッドに渡します。
次に、 cachingDecorator
さらに汎用的にしましょう。これまでは、単一引数の関数でのみ機能していました。
では、複数引数のworker.slow
メソッドをキャッシュするにはどうすればよいでしょうか?
ワーカー = { にします 遅い(最小、最大) { 最小値 + 最大値を返します。 // 恐ろしいCPUホガーが想定される } }; // 同じ引数の呼び出しを覚えておく必要があります worker.slow = cachingDecorator(worker.slow);
以前は、単一の引数x
に対して、 cache.set(x, result)
で結果を保存し、 cache.get(x)
で結果を取得することができました。ただし、引数(min,max)
の組み合わせの結果を覚えておく必要があります。ネイティブMap
キーとして単一の値のみを受け取ります。
考えられる解決策は数多くあります。
より汎用性が高く、マルチキーが可能な新しい (またはサードパーティの) マップのようなデータ構造を実装します。
ネストされたマップを使用します。cache.set cache.set(min)
ペア(max, result)
を格納するMap
になります。したがって、 result
cache.get(min).get(max)
として取得できます。
2 つの値を 1 つに結合します。私たちの特定のケースでは、文字列"min,max"
Map
キーとして使用するだけです。柔軟性を高めるために、多数の値から 1 つの値を作成する方法を認識するハッシュ関数をデコレーターに提供できるようにすることができます。
多くの実際的なアプリケーションでは、3 番目のバリアントで十分なので、これを使い続けます。
また、 x
だけでなく、 func.call
のすべての引数を渡す必要があります。 function()
では、引数の疑似配列をarguments
として取得できるので、 func.call(this, x)
をfunc.call(this, ...arguments)
に置き換える必要があることを思い出してください。
より強力なcachingDecorator
は次のとおりです。
ワーカー = { にします 遅い(最小、最大) { alert(`${min},${max}` で呼び出されました); 最小値 + 最大値を返します。 } }; 関数 cachingDecorator(func, hash) { let キャッシュ = new Map(); 戻り関数() { let key = ハッシュ(引数); // (*) if (cache.has(key)) { 戻りキャッシュ.get(キー); } let result = func.call(this, ...arguments); // (**) キャッシュ.セット(キー, 結果); 結果を返します。 }; } 関数ハッシュ(引数) { args[0] + ',' + args[1] を返します。 } worker.slow = cachingDecorator(worker.slow, ハッシュ); アラート(worker.slow(3, 5) ); // 動作します alert( "また" + worker.slow(3, 5) ); // 同じ(キャッシュされた)
現在は、任意の数の引数を使用して動作します (ただし、任意の数の引数を許可するには、ハッシュ関数も調整する必要があります。これを処理する興味深い方法については、以下で説明します)。
変更点は 2 つあります。
行(*)
では、 hash
呼び出して、 arguments
から単一のキーを作成します。ここでは、引数(3, 5)
をキー"3,5"
に変換する単純な「結合」関数を使用します。より複雑なケースでは、他のハッシュ関数が必要になる場合があります。
次に、 (**)
func.call(this, ...arguments)
を使用して、コンテキストとラッパーが取得したすべての引数 (最初の引数だけでなく) の両方を元の関数に渡します。
func.call(this, ...arguments)
の代わりにfunc.apply(this, arguments)
使用できます。
組み込みメソッド func.apply の構文は次のとおりです。
func.apply(コンテキスト、引数)
this=context
を設定し、配列のようなオブジェクトargs
引数のリストとして使用してfunc
実行します。
call
とapply
構文の唯一の違いは、 call
引数のリストを期待するのに対し、 apply
引数とともに配列のようなオブジェクトを受け取ることです。
したがって、これら 2 つの呼び出しはほぼ同等です。
func.call(context, ...args); func.apply(context, args);
これらは、指定されたコンテキストと引数を使用して同じfunc
呼び出しを実行します。
args
に関しては微妙な違いがあるだけです。
スプレッド構文...
使用すると、 call
対象のリストとして反復可能なargs
を渡すことができます。
apply
配列のようなargs
のみを受け入れます。
…実際の配列など、反復可能かつ配列のようなオブジェクトの場合は、どれでも使用できますが、ほとんどの JavaScript エンジンが内部的に最適化するため、 apply
おそらく高速になります。
すべての引数をコンテキストとともに別の関数に渡すことを、コール転送 と呼びます。
それが最も単純な形式です。
let ラッパー = function() { return func.apply(this, argument); };
外部コードがこのようなwrapper
を呼び出す場合、元の関数func
の呼び出しと区別できません。
ここで、ハッシュ関数にもう 1 つの小さな改良を加えてみましょう。
関数ハッシュ(引数) { args[0] + ',' + args[1] を返します。 }
現時点では、2 つの引数に対してのみ機能します。任意の数のargs
を結合できればもっと良いでしょう。
自然な解決策は、arr.join メソッドを使用することです。
関数ハッシュ(引数) { 戻り args.join(); }
…残念ながら、それはうまくいきません。これは、 hash(arguments)
を呼び出しており、 arguments
オブジェクトは反復可能で配列に似ていますが、実際の配列ではないためです。
したがって、以下に示すように、それにjoin
呼び出すと失敗します。
関数ハッシュ() { アラート(arguments.join() ); // エラー: argument.join は関数ではありません } ハッシュ(1, 2);
それでも、配列結合を使用する簡単な方法があります。
関数ハッシュ() { alert( [].join.call(arguments) ); // 1,2 } ハッシュ(1, 2);
このトリックはメソッド借用と呼ばれます。
通常の配列 ( [].join
) から join メソッドを取得 (借用) し、 [].join.call
を使用してarguments
のコンテキストで実行します。
なぜ効果があるのでしょうか?
これは、ネイティブ メソッドarr.join(glue)
の内部アルゴリズムが非常に単純であるためです。
仕様からほぼ「そのまま」抜粋:
glue
最初の引数にするか、引数がない場合はカンマ","
にします。
result
空の文字列にします。
this[0]
result
に追加します。
接着glue
とthis[1]
。
接着glue
とthis[2]
。
… this.length
アイテムが接着されるまでこれを行います。
result
を返します。
したがって、技術的にはthis
を取得し、 this[0]
、 this[1]
などを結合します。これは、 this
ような配列を許可する方法で意図的に書かれています (偶然ではなく、多くのメソッドがこの慣例に従っています)。そのため、 this=arguments
でも機能します。
一般に、1 つの小さな点を除いて、関数またはメソッドを装飾されたものに置き換えることは安全です。元の関数にfunc.calledCount
などのプロパティがあった場合、装飾された関数はそれらを提供しません。それはラッパーだからです。したがって、それらを使用する場合は注意が必要です。
たとえば、上記の例では、 slow
関数にプロパティがある場合、 cachingDecorator(slow)
それらのプロパティを持たないラッパーになります。
デコレータによっては、独自のプロパティを提供する場合があります。たとえば、デコレータは関数が呼び出された回数とそれに要した時間をカウントし、ラッパー プロパティを介してこの情報を公開する場合があります。
関数のプロパティへのアクセスを維持するデコレーターを作成する方法は存在しますが、これには特別なProxy
オブジェクトを使用して関数をラップする必要があります。これについては、「プロキシとリフレクト」の記事で後ほど説明します。
デコレーターは、関数の動作を変更する関数のラッパーです。メインのジョブは引き続き関数によって実行されます。
デコレータは、関数に追加できる「機能」または「側面」として見ることができます。 1 つ追加することも、複数追加することもできます。しかもコードを変更することなくこれらすべてを実行できます。
cachingDecorator
実装するために、次のメソッドを検討しました。
func.call(context, arg1, arg2…) – 指定されたコンテキストと引数を使用してfunc
を呼び出します。
func.apply(context, args) – context
をthis
として渡してfunc
呼び出し、配列のようなargs
引数のリストに渡します。
一般的な通話の転送は、通常apply
で行われます。
let ラッパー = function() { returnoriginal.apply(this,arguments); };
また、オブジェクトからメソッドを取得し、それを別のオブジェクトのコンテキストでcall
場合のメソッド借用の例も見ました。配列メソッドを取得してarguments
に適用することは非常に一般的です。別の方法は、実数配列である残りのパラメータ オブジェクトを使用することです。
そこには多くのデコレーターがいます。この章のタスクを解決して、どれだけうまく理解できたかを確認してください。
重要度: 5
関数へのすべての呼び出しをcalls
プロパティに保存するラッパーを返すデコレーターspy(func)
を作成します。
すべての呼び出しは引数の配列として保存されます。
例えば:
関数 work(a, b) { アラート( a + b ); // work は任意の関数またはメソッドです } 仕事 = スパイ(仕事); 仕事(1, 2); // 3 仕事(4, 5); // 9 for (work.calls の引数を許可) { alert( 'call:' + args.join() ); // "呼び出し:1,2"、"呼び出し:4,5" }
PS このデコレータは単体テストに役立つことがあります。その高度な形式は、Sinon.JS ライブラリのsinon.spy
です。
テストを含むサンドボックスを開きます。
spy(f)
によって返されるラッパーは、すべての引数を保存し、 f.apply
を使用して呼び出しを転送する必要があります。
関数スパイ(関数) { 関数ラッパー(...args) { // 引数の代わりに ...args を使用して、wrapper.calls に「実際の」配列を格納します Wrapper.calls.push(args); return func.apply(this, args); } ラッパー.コール = []; リターンラッパー; }
サンドボックス内のテストを含むソリューションを開きます。
重要度: 5
f
の各呼び出しをms
ミリ秒遅らせるデコレータdelay(f, ms)
を作成します。
例えば:
関数 f(x) { アラート(x); } // ラッパーを作成する f1000 = 遅延(f, 1000); とします。 f1500 = 遅延(f, 1500); とします。 f1000("テスト"); // 1000ms 後に「test」を表示 f1500("テスト"); // 1500ms 後に「test」を表示
つまり、 delay(f, ms)
f
の「 ms
によって遅延された」バリアントを返します。
上記のコードでは、 f
1 つの引数の関数ですが、ソリューションではすべての引数とコンテキストthis
渡す必要があります。
テストを含むサンドボックスを開きます。
解決策:
関数遅延(f, ms) { 戻り関数() { setTimeout(() => f.apply(this, argument), ms); }; } f1000 = 遅延(アラート, 1000); f1000("テスト"); // 1000ms 後に「test」を表示
ここでアロー関数がどのように使用されているかに注目してください。ご存知のとおり、アロー関数には独自のthis
とarguments
ありません。そのため、 f.apply(this, arguments)
ラッパーからthis
とarguments
受け取ります。
通常の関数を渡す場合、 setTimeout
引数なしでthis=window
使用してその関数を呼び出します (ブラウザーを使用していると仮定して)。
中間変数を使用して正しいthis
渡すこともできますが、それは少し面倒です。
関数遅延(f, ms) { return function(...args) { 「savedThis = this;」とします。 // これを中間変数に格納します setTimeout(function() { f.apply(savedThis, args); // ここで使用します }、 MS); }; }
サンドボックス内のテストを含むソリューションを開きます。
重要度: 5
debounce(f, ms)
デコレータの結果は、 ms
ミリ秒の非アクティブ状態 (呼び出しなし、「クールダウン期間」) が続くまでf
への呼び出しを一時停止し、その後、最新の引数を使用してf
1 回呼び出すラッパーです。
言い換えれば、 debounce
「電話」を受けて、 ms
秒間沈黙するまで待つ秘書のようなものです。そしてそのとき初めて、最新の呼び出し情報が「ボス」に転送されます (実際のf
呼び出します)。
たとえば、関数f
あり、それをf = debounce(f, 1000)
に置き換えました。
次に、ラップされた関数が 0ms、200ms、500ms に呼び出され、その後呼び出しがない場合、実際のf
1500ms に 1 回だけ呼び出されます。つまり、最後の通話から 1000 ミリ秒のクールダウン期間が経過した後です。
…そして、最後の呼び出しの引数を取得します。他の呼び出しは無視されます。
そのコードは次のとおりです (Lodash ライブラリのデバウンス デコレータを使用します)。
let f = _.debounce(alert, 1000); f("a"); setTimeout( () => f("b"), 200); setTimeout( () => f("c"), 500); // デバウンスされた関数は最後の呼び出し後 1000 ミリ秒待ってから実行されます:alert("c")
次に実際的な例です。たとえば、ユーザーが何かを入力し、入力が完了したらサーバーにリクエストを送信したいとします。
入力された文字ごとにリクエストを送信するのは意味がありません。代わりに、待ってから結果全体を処理したいと思います。
Web ブラウザでは、入力フィールドが変更されるたびに呼び出される関数であるイベント ハンドラーをセットアップできます。通常、イベント ハンドラーは、入力されたキーごとに非常に頻繁に呼び出されます。ただし、1000 ミリ秒でdebounce
と、最後の入力から 1000 ミリ秒後に呼び出されるのは 1 回だけになります。
この実際の例では、ハンドラーは結果を下のボックスに入れます。試してみてください。
見る? 2 番目の入力は debounced 関数を呼び出すため、その内容は最後の入力から 1000 ミリ秒後に処理されます。
したがって、 debounce
、一連のイベント (キーの押下、マウスの動きなど) を処理する優れた方法です。
最後の呼び出し後、指定された時間待機してから、結果を処理できる関数を実行します。
タスクは、 debounce
デコレータを実装することです。
ヒント: よく考えてみれば、それはほんの数行です :)
テストを含むサンドボックスを開きます。
関数 debounce(func, ms) { タイムアウトさせます。 戻り関数() { クリアタイムアウト(タイムアウト); timeout = setTimeout(() => func.apply(this, argument), ms); }; }
debounce
を呼び出すとラッパーが返されます。呼び出されると、指定されたms
後に元の関数呼び出しをスケジュールし、以前のタイムアウトをキャンセルします。
サンドボックス内のテストを含むソリューションを開きます。
重要度: 5
ラッパーを返す「スロットリング」デコレータthrottle(f, ms)
を作成します。
複数回呼び出された場合、最大ms
ミリ秒ごとに呼び出しをf
に渡します。
デバウンス デコレータと比較すると、動作は完全に異なります。
debounce
、「クールダウン」期間の後に関数を 1 回実行します。最終結果の処理に適しています。
throttle
指定されたms
時間を超えて実行することはありません。それほど頻繁ではない定期的な更新に適しています。
言い換えれば、 throttle
、電話を受け付ける秘書のようなものですが、上司に迷惑をかける (実際のf
呼び出す) のはms
ミリ秒に 1 回以下です。
その要件をよりよく理解し、その要件がどこから来ているのかを確認するために、実際のアプリケーションをチェックしてみましょう。
たとえば、マウスの動きを追跡したいとします。
ブラウザでは、マウスが動くたびに関数を実行し、マウスの移動に応じてポインタの位置を取得する関数を設定できます。マウスをアクティブに使用している間、この関数は通常非常に頻繁に実行され、1 秒あたり 100 回 (10 ミリ秒ごと) に達する場合もあります。ポインタが移動したときに Web ページ上の情報を更新したいと考えています。
…しかし、関数update()
を更新するのは重すぎて、微小な動きごとに実行することはできません。また、100 ミリ秒に 1 回を超える頻度で更新しても意味がありません。
そこで、これをデコレータにラップします。元のupdate()
の代わりに、マウスの移動ごとに実行する関数としてthrottle(update, 100)
を使用します。デコレーターは頻繁に呼び出されますが、呼び出しをupdate()
に転送するのは最大 100 ミリ秒に 1 回です。
視覚的には次のようになります。
最初のマウス移動では、修飾されたバリアントは直ちにupdate
への呼び出しを渡します。これは重要です。ユーザーは、自分の動きに対する私たちの反応をすぐに見ることができます。
その後、マウスを動かしても、 100ms
まで何も起こりません。装飾されたバリアントは呼び出しを無視します。
100ms
の終わりに、最後の座標でもう 1 つのupdate
行われます。
そして、ついにマウスはどこかで止まります。装飾されたバリアントは100ms
が経過するまで待機し、その後最後の座標でupdate
実行します。したがって、非常に重要なことですが、最終的なマウス座標が処理されます。
コード例:
関数 f(a) { コンソール.ログ(a); } // f1000 は最大 1000 ミリ秒に 1 回呼び出しを f に渡します f1000 = スロットル(f, 1000); f1000(1); // 1を表示します f1000(2); // (スロットリング中、1000 ミリ秒はまだ出ていない) f1000(3); // (スロットリング中、1000 ミリ秒はまだ出ていない) // 1000ms タイムアウトになると... // ...出力 3、中間値 2 は無視されました
PS 引数とf1000
に渡されるthis
は、元のf
に渡される必要があります。
テストを含むサンドボックスを開きます。
関数スロットル(関数, ミリ秒) { Throttled = false にします。 保存された引数、 これを保存しました。 関数ラッパー() { if (isThrottled) { // (2) 保存されたArgs = 引数; 保存済みこれ = これ; 戻る; } isThrottled = true; func.apply(this, 引数); // (1) setTimeout(function() { isThrottled = false; // (3) if (savedArgs) { Wrapper.apply(savedThis,savedArgs); 保存されたArgs = 保存されたThis = null; } }、 MS); } リターンラッパー; }
throttle(func, ms)
を呼び出すと、 wrapper
返されます。
最初の呼び出し中に、 wrapper
func
を実行し、クールダウン状態 ( isThrottled = true
) を設定するだけです。
この状態では、すべての呼び出しがsavedArgs/savedThis
に記憶されます。コンテキストと引数はどちらも同様に重要であり、覚えておく必要があることに注意してください。通話を再現するにはそれらが同時に必要です。
ms
ミリ秒が経過すると、 setTimeout
トリガーされます。クールダウン状態は削除され ( isThrottled = false
)、呼び出しを無視した場合は、最後に記憶された引数とコンテキストを使用してwrapper
が実行されます。
3 番目のステップはfunc
ではなくwrapper
実行します。これは、 func
実行するだけでなく、再度クールダウン状態に入り、それをリセットするためのタイムアウトを設定する必要があるためです。
サンドボックス内のテストを含むソリューションを開きます。