W3Cschool
恭喜您成為首批注冊用戶
獲得88經驗值獎勵
JavaScript 在處理函數時提供了非凡的靈活性。它們可以被傳遞,用作對象,現(xiàn)在我們將看到如何在它們之間 轉發(fā)(forward) 調用并 裝飾(decorate) 它們。
假設我們有一個 CPU 重負載的函數 slow(x)
,但它的結果是穩(wěn)定的。換句話說,對于相同的 x
,它總是返回相同的結果。
如果經常調用該函數,我們可能希望將結果緩存(記?。┫聛?,以避免在重新計算上花費額外的時間。
但是我們不是將這個功能添加到 slow()
中,而是創(chuàng)建一個包裝器(wrapper)函數,該函數增加了緩存功能。正如我們將要看到的,這樣做有很多好處。
下面是代碼和解釋:
function slow(x) {
// 這里可能會有重負載的 CPU 密集型工作
alert(`Called with ${x}`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) { // 如果緩存中有對應的結果
return cache.get(x); // 從緩存中讀取結果
}
let result = func(x); // 否則就調用 func
cache.set(x, result); // 然后將結果緩存(記?。┫聛? return result;
};
}
slow = cachingDecorator(slow);
alert( slow(1) ); // slow(1) 被緩存下來了,并返回結果
alert( "Again: " + slow(1) ); // 返回緩存中的 slow(1) 的結果
alert( slow(2) ); // slow(2) 被緩存下來了,并返回結果
alert( "Again: " + slow(2) ); // 返回緩存中的 slow(2) 的結果
在上面的代碼中,cachingDecorator
是一個 裝飾器(decorator):一個特殊的函數,它接受另一個函數并改變它的行為。
其思想是,我們可以為任何函數調用 cachingDecorator
,它將返回緩存包裝器。這很棒啊,因為我們有很多函數可以使用這樣的特性,而我們需要做的就是將 cachingDecorator
應用于它們。
通過將緩存與主函數代碼分開,我們還可以使主函數代碼變得更簡單。
cachingDecorator(func)
的結果是一個“包裝器”:function(x)
將 func(x)
的調用“包裝”到緩存邏輯中:
從外部代碼來看,包裝的 slow
函數執(zhí)行的仍然是與之前相同的操作。它只是在其行為上添加了緩存功能。
總而言之,使用分離的 cachingDecorator
而不是改變 slow
本身的代碼有幾個好處:
cachingDecorator
? 是可重用的。我們可以將它應用于另一個函數。slow
? 本身的復雜性(如果有的話)。上面提到的緩存裝飾器不適用于對象方法。
例如,在下面的代碼中,?worker.slow()
? 在裝飾后停止工作:
// 我們將對 worker.slow 的結果進行緩存
let worker = {
someMethod() {
return 1;
},
slow(x) {
// 可怕的 CPU 過載任務
alert("Called with " + x);
return x * this.someMethod(); // (*)
}
};
// 和之前例子中的代碼相同
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func(x); // (**)
cache.set(x, result);
return result;
};
}
alert( worker.slow(1) ); // 原始方法有效
worker.slow = cachingDecorator(worker.slow); // 現(xiàn)在對其進行緩存
alert( worker.slow(2) ); // 蛤!Error: Cannot read property 'someMethod' of undefined
錯誤發(fā)生在試圖訪問 this.someMethod
并失敗了的 (*)
行中。你能看出來為什么嗎?
原因是包裝器將原始函數調用為 (**)
行中的 func(x)
。并且,當這樣調用時,函數將得到 this = undefined
。
如果嘗試運行下面這段代碼,我們會觀察到類似的問題:
let func = worker.slow;
func(2);
因此,包裝器將調用傳遞給原始方法,但沒有上下文 this
。因此,發(fā)生了錯誤。
讓我們來解決這個問題。
有一個特殊的內建函數方法 func.call(context, …args),它允許調用一個顯式設置 ?this
? 的函數。
語法如下:
func.call(context, arg1, arg2, ...)
它運行 func
,提供的第一個參數作為 this
,后面的作為參數(arguments)。
簡單地說,這兩個調用幾乎相同:
func(1, 2, 3);
func.call(obj, 1, 2, 3)
它們調用的都是 func
,參數是 1
、2
和 3
。唯一的區(qū)別是 func.call
還會將 this
設置為 obj
。
例如,在下面的代碼中,我們在不同對象的上下文中調用 sayHi
:sayHi.call(user)
運行 sayHi
并提供了 this=user
,然后下一行設置 this=admin
:
function sayHi() {
alert(this.name);
}
let user = { name: "John" };
let admin = { name: "Admin" };
// 使用 call 將不同的對象傳遞為 "this"
sayHi.call( user ); // John
sayHi.call( admin ); // Admin
在這里我們用帶有給定上下文和 phrase 的 call
調用 say
:
function say(phrase) {
alert(this.name + ': ' + phrase);
}
let user = { name: "John" };
// user 成為 this,"Hello" 成為第一個參數
say.call( user, "Hello" ); // John: Hello
在我們的例子中,我們可以在包裝器中使用 call
將上下文傳遞給原始函數:
let worker = {
someMethod() {
return 1;
},
slow(x) {
alert("Called with " + x);
return x * this.someMethod(); // (*)
}
};
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func.call(this, x); // 現(xiàn)在 "this" 被正確地傳遞了
cache.set(x, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow); // 現(xiàn)在對其進行緩存
alert( worker.slow(2) ); // 工作正常
alert( worker.slow(2) ); // 工作正常,沒有調用原始函數(使用的緩存)
現(xiàn)在一切都正常工作了。
為了讓大家理解地更清晰一些,讓我們更深入地看看 ?this
? 是如何被傳遞的:
worker.slow
? 現(xiàn)在是包裝器 ?function (x) { ... }
?。worker.slow(2)
? 執(zhí)行時,包裝器將 ?2
? 作為參數,并且 ?this=worker
?(它是點符號 ?.
? 之前的對象)。func.call(this, x)
? 將當前的 ?this
?(?=worker
?)和當前的參數(?=2
?)傳遞給原始方法。現(xiàn)在讓我們把 ?cachingDecorator
? 寫得更加通用。到現(xiàn)在為止,它只能用于單參數函數。
現(xiàn)在如何緩存多參數 ?worker.slow
? 方法呢?
let worker = {
slow(min, max) {
return min + max; // scary CPU-hogger is assumed
}
};
// 應該記住相同參數的調用
worker.slow = cachingDecorator(worker.slow);
之前,對于單個參數 x
,我們可以只使用 cache.set(x, result)
來保存結果,并使用 cache.get(x)
來檢索并獲取結果。但是現(xiàn)在,我們需要記住 參數組合 (min,max)
的結果。原生的 Map
僅將單個值作為鍵(key)。
這兒有許多解決方案可以實現(xiàn):
cache.set(min)
? 將是一個存儲(鍵值)對 ?(max, result)
? 的 ?Map
?。所以我們可以使用 ?cache.get(min).get(max)
? 來獲取 ?result
?。對于許多實際應用,第三種方式就足夠了,所以我們就用這個吧。
當然,我們需要傳入的不僅是 x
,還需要傳入 func.call
的所有參數。讓我們回想一下,在 function()
中我們可以得到一個包含所有參數的偽數組(pseudo-array)arguments
,那么 func.call(this, x)
應該被替換為 func.call(this, ...arguments)
。
這是一個更強大的 cachingDecorator
:
let worker = {
slow(min, max) {
alert(`Called with ${min},${max}`);
return min + max;
}
};
function cachingDecorator(func, hash) {
let cache = new Map();
return function() {
let key = hash(arguments); // (*)
if (cache.has(key)) {
return cache.get(key);
}
let result = func.call(this, ...arguments); // (**)
cache.set(key, result);
return result;
};
}
function hash(args) {
return args[0] + ',' + args[1];
}
worker.slow = cachingDecorator(worker.slow, hash);
alert( worker.slow(3, 5) ); // works
alert( "Again " + worker.slow(3, 5) ); // same (cached)
現(xiàn)在這個包裝器可以處理任意數量的參數了(盡管哈希函數還需要被進行調整以允許任意數量的參數。一種有趣的處理方法將在下面講到)。
這里有兩個變化:
(*)
? 行中它調用 ?hash
? 來從 ?arguments
? 創(chuàng)建一個單獨的鍵。這里我們使用一個簡單的“連接”函數,將參數 ?(3, 5)
? 轉換為鍵 ?"3,5"
?。更復雜的情況可能需要其他哈希函數。(**)
? 行使用 ?func.call(this, ...arguments)
? 將包裝器獲得的上下文和所有參數(不僅僅是第一個參數)傳遞給原始函數。我們可以使用 ?func.apply(this, arguments)
? 代替 ?func.call(this, ...arguments)
?。
內建方法 func.apply 的語法是:
func.apply(context, args)
它運行 func
設置 this=context
,并使用類數組對象 args
作為參數列表(arguments)。
call
和 apply
之間唯一的語法區(qū)別是,call
期望一個參數列表,而 apply
期望一個包含這些參數的類數組對象。
因此,這兩個調用幾乎是等效的:
func.call(context, ...args);
func.apply(context, args);
它們使用給定的上下文和參數執(zhí)行相同的 func
調用。
只有一個關于 args
的細微的差別:
...
? 允許將 可迭代對象 ?args
? 作為列表傳遞給 ?call
?。apply
? 只接受 類數組 ?args
?。……對于即可迭代又是類數組的對象,例如一個真正的數組,我們使用 call
或 apply
均可,但是 apply
可能會更快,因為大多數 JavaScript 引擎在內部對其進行了優(yōu)化。
將所有參數連同上下文一起傳遞給另一個函數被稱為“呼叫轉移(call forwarding)”。
這是它的最簡形式:
let wrapper = function() {
return func.apply(this, arguments);
};
當外部代碼調用這種包裝器 wrapper
時,它與原始函數 func
的調用是無法區(qū)分的。
現(xiàn)在,讓我們對哈希函數再做一個較小的改進:
function hash(args) {
return args[0] + ',' + args[1];
}
截至目前,它僅適用于兩個參數。如果它可以適用于任何數量的 args
就更好了。
自然的解決方案是使用 arr.join 方法:
function hash(args) {
return args.join();
}
……不幸的是,這不行。因為我們正在調用 hash(arguments)
,arguments
對象既是可迭代對象又是類數組對象,但它并不是真正的數組。
所以在它上面調用 join
會失敗,我們可以在下面看到:
function hash() {
alert( arguments.join() ); // Error: arguments.join is not a function
}
hash(1, 2);
不過,有一種簡單的方法可以使用數組的 join 方法:
function hash() {
alert( [].join.call(arguments) ); // 1,2
}
hash(1, 2);
這個技巧被稱為 方法借用(method borrowing)。
我們從常規(guī)數組 [].join
中獲?。ń栌茫﹋oin 方法,并使用 [].join.call
在 arguments
的上下文中運行它。
它為什么有效?
那是因為原生方法 arr.join(glue)
的內部算法非常簡單。
從規(guī)范中幾乎“按原樣”解釋如下:
glue
? 成為第一個參數,如果沒有參數,則使用逗號 ?","
?。result
? 為空字符串。this[0]
? 附加到 ?result
?。glue
? 和 ?this[1]
?。glue
? 和 ?this[2]
?。this.length
? 項目被粘在一起。result
?。因此,從技術上講,它需要 this
并將 this[0]
,this[1]
……等 join 在一起。它的編寫方式是故意允許任何類數組的 this
的(不是巧合,很多方法都遵循這種做法)。這就是為什么它也可以和 this=arguments
一起使用。
通常,用裝飾的函數替換一個函數或一個方法是安全的,除了一件小東西。如果原始函數有屬性,例如 func.calledCount
或其他,則裝飾后的函數將不再提供這些屬性。因為這是裝飾器。因此,如果有人使用它們,那么就需要小心。
例如,在上面的示例中,如果 slow
函數具有任何屬性,而 cachingDecorator(slow)
則是一個沒有這些屬性的包裝器。
一些包裝器可能會提供自己的屬性。例如,裝飾器會計算一個函數被調用了多少次以及花費了多少時間,并通過包裝器屬性公開(expose)這些信息。
存在一種創(chuàng)建裝飾器的方法,該裝飾器可保留對函數屬性的訪問權限,但這需要使用特殊的 Proxy
對象來包裝函數。我們將在后面的 Proxy 和 Reflect 中學習它。
裝飾器 是一個圍繞改變函數行為的包裝器。主要工作仍由該函數來完成。
裝飾器可以被看作是可以添加到函數的 “features” 或 “aspects”。我們可以添加一個或添加多個。而這一切都無需更改其代碼!
為了實現(xiàn) cachingDecorator
,我們研究了以下方法:
func
?。func
? 將 ?context
? 作為 ?this
? 和類數組的 ?args
? 傳遞給參數列表。通用的 呼叫轉移(call forwarding) 通常是使用 apply
完成的:
let wrapper = function() {
return original.apply(this, arguments);
};
我們也可以看到一個 方法借用(method borrowing) 的例子,就是我們從一個對象中獲取一個方法,并在另一個對象的上下文中“調用”它。采用數組方法并將它們應用于參數 arguments
是很常見的。另一種方法是使用 Rest 參數對象,該對象是一個真正的數組。
在 JavaScript 領域里有很多裝飾器(decorators)。通過解決本章的任務,來檢查你掌握它們的程度吧。
創(chuàng)建一個裝飾器 ?spy(func)
?,它應該返回一個包裝器,該包裝器將所有對函數的調用保存在其 ?calls
? 屬性中。
每個調用都保存為一個參數數組。
例如:
function work(a, b) {
alert( a + b ); // work 是一個任意的函數或方法
}
work = spy(work);
work(1, 2); // 3
work(4, 5); // 9
for (let args of work.calls) {
alert( 'call:' + args.join() ); // "call:1,2", "call:4,5"
}
P.S. 該裝飾器有時對于單元測試很有用。它的高級形式是 Sinon.JS 庫中的 sinon.spy
。
由 spy(f)
返回的包裝器應存儲所有參數,然后使用 f.apply
轉發(fā)調用。
function spy(func) {
function wrapper(...args) {
// using ...args instead of arguments to store "real" array in wrapper.calls
wrapper.calls.push(args);
return func.apply(this, args);
}
wrapper.calls = [];
return wrapper;
}
重要程度: 5
創(chuàng)建一個裝飾器 delay(f, ms)
,該裝飾器將 f
的每次調用延時 ms
毫秒。
例如:
function f(x) {
alert(x);
}
// create wrappers
let f1000 = delay(f, 1000);
let f1500 = delay(f, 1500);
f1000("test"); // 在 1000ms 后顯示 "test"
f1500("test"); // 在 1500ms 后顯示 "test"
換句話說,?delay(f, ms)
? 返回的是延遲 ?ms
? 后的 ?f
? 的變體。
在上面的代碼中,?f
? 是單個參數的函數,但是你的解決方案應該傳遞所有參數和上下文 ?this
?。
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
let f1000 = delay(alert, 1000);
f1000("test"); // shows "test" after 1000ms
注意這里是如何使用箭頭函數的。我們知道,箭頭函數沒有自己的 this
和 arguments
,所以 f.apply(this, arguments)
從包裝器中獲取 this
和 arguments
。
如果我們傳遞一個常規(guī)函數,setTimeout
將調用它且不帶參數,并且 this=window
(假設我們在瀏覽器環(huán)境)。
我們仍然可以通過使用中間變量來傳遞正確的 this
,但這有點麻煩:
function delay(f, ms) {
return function(...args) {
let savedThis = this; // 將 this 存儲到中間變量
setTimeout(function() {
f.apply(savedThis, args); // 在這兒使用它
}, ms);
};
}
debounce(f, ms)
裝飾器的結果是一個包裝器,該包裝器將暫停對 f
的調用,直到經過 ms
毫秒的非活動狀態(tài)(沒有函數調用,“冷卻期”),然后使用最新的參數調用 f
一次。
換句話說,debounce
就像一個“接聽電話”的秘書,并一直等到 ms
毫秒的安靜時間之后,才將最新的呼叫信息傳達給“老板”(調用實際的 f
)。
舉個例子,我們有一個函數 f
,并將其替換為 f = debounce(f, 1000)
。
然后,如果包裝函數分別在 0ms、200ms 和 500ms 時被調用了,之后沒有其他調用,那么實際的 f
只會在 1500ms 時被調用一次。也就是說:從最后一次調用開始經過 1000ms 的冷卻期之后。
……并且,它將獲得最后一個調用的所有參數,其他調用的參數將被忽略。
以下是其實現(xiàn)代碼(使用了 Lodash library 中的防抖裝飾器 ):
let f = _.debounce(alert, 1000);
f("a");
setTimeout( () => f("b"), 200);
setTimeout( () => f("c"), 500);
// 防抖函數從最后一次函數調用以后等待 1000ms,然后執(zhí)行:alert("c")
現(xiàn)在我們舉一個實際中的例子。假設用戶輸入了一些內容,我們想要在用戶輸入完成時向服務器發(fā)送一個請求。
我們沒有必要為每一個字符的輸入都發(fā)送請求。相反,我們想要等一段時間,然后處理整個結果。
在 Web 瀏覽器中,我們可以設置一個事件處理程序 —— 一個在每次輸入內容發(fā)生改動時都會調用的函數。通常,監(jiān)聽所有按鍵輸入的事件的處理程序會被調用的非常頻繁。但如果我們?yōu)檫@個處理程序做一個 1000ms 的 debounce
處理,它僅會在最后一次輸入后的 1000ms 后被調用一次。
任務是實現(xiàn)一個 debounce
裝飾器。
提示:如果你好好想想,實現(xiàn)它只需要幾行代碼 :)
function debounce(func, ms) {
let timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, arguments), ms);
};
}
調用 debounce
會返回一個包裝器。當它被調用時,它會安排一個在給定的 ms
之后對原始函數的調用,并取消之前的此類超時。
創(chuàng)建一個“節(jié)流”裝飾器 throttle(f, ms)
—— 返回一個包裝器。
當被多次調用時,它會在每 ms
毫秒最多將調用傳遞給 f
一次。
與防抖(debounce)裝飾器相比,其行為完全不同:
debounce
? 會在“冷卻(cooldown)”期后運行函數一次。適用于處理最終結果。throttle
? 運行函數的頻率不會大于所給定的時間 ?ms
? 毫秒。適用于不應該經常進行的定期更新。換句話說,throttle
就像接電話的秘書,但是打擾老板(實際調用 f
)的頻率不能超過每 ms
毫秒一次。
讓我們看看現(xiàn)實生活中的應用程序,以便更好地理解這個需求,并了解它的來源。
例如,我們想要跟蹤鼠標移動。
在瀏覽器中,我們可以設置一個函數,使其在每次鼠標移動時運行,并獲取鼠標移動時的指針位置。在使用鼠標的過程中,此函數通常會執(zhí)行地非常頻繁,大概每秒 100 次(每 10 毫秒)。
我們想要在鼠標指針移動時,更新網頁上的某些信息。
……但是更新函數 update()
太重了,無法在每個微小移動上都執(zhí)行。高于每 100ms 更新一次的更新頻次也沒有意義。
因此,我們將其包裝到裝飾器中:使用 throttle(update, 100)
作為在每次鼠標移動時運行的函數,而不是原始的 update()
。裝飾器會被頻繁地調用,但是最多每 100ms 將調用轉發(fā)給 update()
一次。
在視覺上,它看起來像這樣:
update
?。這很重要,用戶會立即看到我們對其動作的反應。100ms
? 沒有任何反應。裝飾的變體忽略了調用。100ms
? 結束時 —— 最后一個坐標又發(fā)生了一次 update。100ms
? 到期,然后用最后一個坐標運行一次 ?update
?。因此,非常重要的是,處理最終的鼠標坐標。一個代碼示例:
function f(a) {
console.log(a);
}
// f1000 最多每 1000ms 將調用傳遞給 f 一次
let f1000 = throttle(f, 1000);
f1000(1); // 顯示 1
f1000(2); // (節(jié)流,尚未到 1000ms)
f1000(3); // (節(jié)流,尚未到 1000ms)
// 當 1000ms 時間到...
// ...輸出 3,中間值 2 被忽略
P.S. 參數(arguments)和傳遞給 f1000
的上下文 this
應該被傳遞給原始的 f
。
function throttle(func, ms) {
let isThrottled = false,
savedArgs,
savedThis;
function wrapper() {
if (isThrottled) { // (2)
savedArgs = arguments;
savedThis = this;
return;
}
isThrottled = true;
func.apply(this, arguments); // (1)
setTimeout(function() {
isThrottled = false; // (3)
if (savedArgs) {
wrapper.apply(savedThis, savedArgs);
savedArgs = savedThis = null;
}
}, ms);
}
return wrapper;
}
調用 throttle(func, ms)
返回 wrapper
。
wrapper
? 只運行 ?func
? 并設置冷卻狀態(tài)(?isThrottled = true
?)。savedArgs/savedThis
? 中。請注意,上下文和參數(arguments)同等重要,應該被記下來。我們同時需要他們以重現(xiàn)調用。ms
? 毫秒后,觸發(fā) ?setTimeout
?。冷卻狀態(tài)被移除(?isThrottled = false
?),如果我們忽略了調用,則將使用最后記憶的參數和上下文執(zhí)行 ?wrapper
?。第 3 步運行的不是 func
,而是 wrapper
,因為我們不僅需要執(zhí)行 func
,還需要再次進入冷卻狀態(tài)并設置 timeout 以重置它。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: