新聞中心
一些編碼人員可能會(huì)直接更改原始功能以達(dá)到某種目的。嗯,這是初級(jí)開(kāi)發(fā)人員常用的方法,也是一種直觀的方法。

創(chuàng)新互聯(lián)為客戶提供專業(yè)的成都網(wǎng)站設(shè)計(jì)、做網(wǎng)站、程序、域名、空間一條龍服務(wù),提供基于WEB的系統(tǒng)開(kāi)發(fā). 服務(wù)項(xiàng)目涵蓋了網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站程序開(kāi)發(fā)、WEB系統(tǒng)開(kāi)發(fā)、微信二次開(kāi)發(fā)、移動(dòng)網(wǎng)站建設(shè)等網(wǎng)站方面業(yè)務(wù)。
但在很多情況下,它并不是最好的解決方案,并且有一些缺點(diǎn)。在今天的內(nèi)容中,我將通過(guò)示例為您介紹一些通用的解決方案。
1、once
很多時(shí)候,我們想要一個(gè)只執(zhí)行一次的函數(shù)。
比如,我們開(kāi)發(fā)網(wǎng)頁(yè)的時(shí)候,總會(huì)有一些提交表單的按鈕。當(dāng)用戶點(diǎn)擊按鈕時(shí),會(huì)觸發(fā)它的 onclick 事件。
Document
為了簡(jiǎn)化演示問(wèn)題,該示例僅記錄一條消息,而不是向服務(wù)器發(fā)送數(shù)據(jù)。
但這里有一個(gè)問(wèn)題:由于網(wǎng)絡(luò)延遲,我們無(wú)法立即為用戶顯示結(jié)果。然后用戶可能繼續(xù)點(diǎn)擊該按鈕并多次向服務(wù)器提交表單。
所以,我們需要解決這個(gè)問(wèn)題,你的解決方案是什么?
一個(gè)常見(jiàn)的解決方案是在用戶第一次單擊按鈕后禁用該按鈕。
document.getElementById('submit').onclick = function()
document.getElementById('submit').disabled = true
console.log("sending data to the server")
}
嗯,這個(gè)解決方案沒(méi)有問(wèn)題。
另外,我們有一個(gè)不同的解決方案:
Document
在這個(gè)解決方案中,我們使用一個(gè)標(biāo)志來(lái)記錄該函數(shù)之前是否已執(zhí)行過(guò)。
如果我們使用圖表來(lái)表示程序,它可能是這樣的:
但是,我們能否為所有此類問(wèn)題找到一個(gè)通用的解決方案?
讓我們繼續(xù)一個(gè)類似的例子。很多時(shí)候,我們的程序中有一個(gè)init函數(shù)。
可以使用這個(gè)函數(shù)來(lái)設(shè)置變量、讀取配置等。這個(gè)函數(shù)應(yīng)該只執(zhí)行一次。為了確保它只執(zhí)行一次并避免意外,我們可以對(duì)函數(shù)進(jìn)行一些更改:
let init = function(){
console.log('init the enviorment')
}我們可以使用這個(gè)函數(shù)來(lái)設(shè)置變量、讀取配置等。這個(gè)函數(shù)應(yīng)該只執(zhí)行一次。為了確保它只執(zhí)行一次并避免意外,我們可以對(duì)函數(shù)進(jìn)行一些更改:
let hasInitialized = false
let init = function(){
if(hasInitialized) return;
console.log('init the enviorment')
hasInitialized = true
}
好的,init函數(shù)只會(huì)初始化環(huán)境一次。
我們還可以將程序繪制成圖表。
你發(fā)現(xiàn)表單提交和初始化函數(shù)有一些共同點(diǎn)嗎?是的,他們的程序非常相似!
如果我們做高級(jí)抽象,流程應(yīng)該是這樣的:
如果該函數(shù)之前已被調(diào)用是一般程序。我們可以編寫一個(gè)高階函數(shù)來(lái)密封這個(gè)過(guò)程。
這是一次函數(shù)的實(shí)現(xiàn):
function once(func) {
let hasExecuted = false;
let result;
return function () {
if (hasExecuted) return result;
hasExecuted = true;
result = func.apply(this, arguments);
func = null;
return result;
};
}現(xiàn)在,使用 once 函數(shù),我們可以輕松地歸檔執(zhí)行一次函數(shù)的目的。
提交一次:
document.getElementById('submit').onclick = once(function()
console.log("sending data to the server")
})
初始化一次:
好的,我們使用 once 函數(shù)來(lái)解決我們的需求。
使用 once 函數(shù)的核心思想是什么?
正如我在標(biāo)題中提到的:我們將一般過(guò)程抽象為高階函數(shù)。程序——只執(zhí)行一次函數(shù)——是一個(gè)通用過(guò)程。它會(huì)被多次使用。如果我們不做抽象,我們就必須在不同的函數(shù)中為相同的邏輯重復(fù)編寫代碼。
如果我們使用 once 函數(shù),有很多好處:
- 我們不需要改變?cè)瓉?lái)的功能。
- 保留業(yè)務(wù)邏輯和執(zhí)行邏輯的分隔符,這樣代碼會(huì)更易于維護(hù)。
- 一次函數(shù)是一個(gè)可重用的函數(shù)。
2、cache
讓我們來(lái)看另一個(gè)例子。
如果有這樣的一個(gè)功能:
function compute(str) {
// Suppose the calculation in the funtion is very time consuming
console.log('2000ms have passed')
return str.toUpperCase()
}(其實(shí)這個(gè)案例我是從 Vue 源碼中學(xué)到的。)
我們要緩存函數(shù)操作的結(jié)果。稍后調(diào)用時(shí),如果參數(shù)相同,則不再執(zhí)行該函數(shù),而是直接返回緩存中的結(jié)果。我們能做什么?
這里有一個(gè)建議:當(dāng)你需要增強(qiáng)一個(gè)函數(shù)時(shí),不要試圖直接修改它,考慮先寫一個(gè)通用的高階函數(shù)來(lái)包裝它。
緩存函數(shù)結(jié)果的一般過(guò)程是什么?這是一個(gè)流程:
這是緩存結(jié)果的實(shí)現(xiàn):
function cached(fn){
// Create an object to store the results returned after each function execution.
const cache = Object.create(null);
// Returns the wrapped function
return function cachedFn (str) {
// If the cache is not hit, the function will be executed
if ( !cache[str] ) {
let result = fn(str);
// Store the result of the function execution in the cache
cache[str] = result;
}
return cache[str]
}
}現(xiàn)在我們可以使用這個(gè)緩存函數(shù)來(lái)增強(qiáng) cumpute 函數(shù):
我們做這個(gè)抽象并不是為了炫耀技巧,其實(shí)這樣的緩存功能用途廣泛。
我們知道,有一個(gè)著名的序列叫做斐波那契數(shù)列。
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...
快速瀏覽后,您可以很容易地注意到序列的模式是每個(gè)值都是前 2 個(gè)值的總和,這意味著對(duì)于 N=5 → 2+3 或在數(shù)學(xué)中:
F(n) = F(n-1) + F(n-2)
現(xiàn)在我們要寫一個(gè)函數(shù):
給定一個(gè)數(shù)字N返回斐波那契數(shù)列的索引值。
怎么寫函數(shù)?
最簡(jiǎn)單的解決方案是遞歸解決方案:
}function fibonacci(num) {
if (num <= 1) return 1;
return fibonacci(num - 1) + fibonacci(num - 2);
}但是這個(gè)實(shí)現(xiàn)很耗時(shí),如果 num 大于 35,您將等待一段時(shí)間才能得到結(jié)果。
但是如果我們使用緩存函數(shù)來(lái)重構(gòu)實(shí)現(xiàn),我們會(huì)得到一個(gè)高性能的函數(shù)。
let cachedFibonacci = cached(function(num){
if(num <= 1) return 1;
return cachedFibonacci(num - 1) + cachedFibonacci(num - 2)
})
3、intercept
讓我們繼續(xù)。
假設(shè)您是一個(gè)庫(kù)的維護(hù)者,并且您將在未來(lái)?xiàng)売靡粋€(gè)名為 request 的舊 API。
function request(){
console.log('request to server')
}在當(dāng)前版本中,您希望通過(guò)記錄消息來(lái)警告用戶 API 將被棄用。
那你會(huì)怎么做?
最糟糕的方法是在函數(shù)中添加一個(gè) console.warn 語(yǔ)句:
function request(){
console.warn(`The request will be deprecated in the future`)
console.log('request to server')
}為什么這是最糟糕的解決方案?
您必須找到所有已棄用的 API 并對(duì)其進(jìn)行修改。這是一個(gè)非常繁瑣的過(guò)程,而且很容易導(dǎo)致錯(cuò)誤。如果沒(méi)有必要,不要更改現(xiàn)有功能。
如果我們用圖來(lái)表示程序,那就是:
正如我們?cè)谇懊鎯?nèi)容中所做的那樣,我們可以為該過(guò)程編寫一個(gè)高階函數(shù)。
function deprecate(fn, newApi) {
return function() {
console.log( `The ${fn.name} will be deprecated. Please use the ${newApi} instead.`);
return fn.apply(this, arguments);
}
}然后我們可以對(duì)我們的項(xiàng)目做一些改變:
,如果您的庫(kù)的用戶調(diào)用請(qǐng)求函數(shù),他們將收到一條消息。
// index.js
importre request from './request';
const _request = deprecate(request, 'fetch');
export {
request: _request
}
現(xiàn)在
好的,讓我們繼續(xù)一個(gè)類似的例子。
我們有一個(gè) fetch 函數(shù)來(lái)向服務(wù)器發(fā)送請(qǐng)求。它將返回 HTML 文本或 JSON 格式的文本。
var fetch = function(url){
let responseContent = null
console.log(`fetching ${url}`)
if(Math.random() < 0.5){
return 'hello world'
} else {
return '{"name": "bytefish"}'
}
}我們現(xiàn)在要做的是,如果我們發(fā)現(xiàn)響應(yīng)結(jié)果是 JSON 格式的字符串,我們將其轉(zhuǎn)換為 JSON 對(duì)象。如果是其他格式的字符串,則不進(jìn)行處理。我應(yīng)該怎么辦?
老規(guī)矩,先畫(huà)個(gè)圖:
具體原理已經(jīng)解釋過(guò)很多次了,這里我直接給出一個(gè)高階函數(shù):
function toJSON(fn) {
return function() {
let res = fn.apply(this, arguments)
try{
let json = JSON.parse(res)
return json
} catch(e){
return res
}
}
}用法:
這兩個(gè)例子有點(diǎn)簡(jiǎn)單。但附近還有一個(gè)更重要的想法。
- derecate功能旨在在執(zhí)行原始功能之前執(zhí)行某些操作。
- toJSON函數(shù)旨在執(zhí)行原始函數(shù)后執(zhí)行某些操作。
我們能把這個(gè)過(guò)程抽象成一個(gè)新的高階函數(shù)嗎?
我們當(dāng)然可以。
function intercept(fn, {before = null, after = null}) {
return function () {
if(before != null) {
before.apply(this, arguments)
}
const result = fn.apply(this, arguments)
if(after != null){
after.call(this, result)
}
return result
};
}如果你之前用過(guò) Axios 這個(gè)著名的 HTTP 請(qǐng)求庫(kù),你就會(huì)知道 Axios 有一個(gè)攔截器 API 供用戶攔截請(qǐng)求和響應(yīng)。
4、Batch
好的,這是我們的最后一個(gè)例子。
這是一個(gè)將輸入加倍的函數(shù)。
function double(num){
return num * 2
}嗯,很簡(jiǎn)單的功能,只是為了演示。
如果我們想讓這個(gè)函數(shù)接受一個(gè)數(shù)組作為參數(shù),那么將數(shù)組中所有元素的值加倍,然后返回一個(gè)新數(shù)組。你怎么寫代碼?
我們可以這樣寫:
function double(nums){
return nums.map(num => num * 2)
}確實(shí)可以這樣寫。
但遺憾的是,JavaScript 沒(méi)有函數(shù)重載,后者的函數(shù)會(huì)覆蓋前者。為了讓我們的double函數(shù)同時(shí)處理兩種參數(shù)類型,我們必須在函數(shù)體中做出判斷:
function double(arg){
if(Array.isArray(arg)){
return nums.map(num => num * 2)
}
return num * 2
}我們想要的是為所有這些問(wèn)題創(chuàng)建一個(gè)通用的解決方案:一個(gè)高階函數(shù),可以標(biāo)記一個(gè)函數(shù)來(lái)處理單個(gè)參數(shù)或類似數(shù)組的參數(shù)。
這是一個(gè)實(shí)現(xiàn):
function batch(fn) {
return function(subject, ...args) {
if(Array.isArray(subject)) {
return subject.map((s) => {
return fn.call(this, s, ...args);
});
}
return fn.call(this, subject, ...args);
}
}
總結(jié)
我想,我舉的例子已經(jīng)夠多了。無(wú)論是once,cache,intercept還是batch,它們都對(duì)某個(gè)進(jìn)程進(jìn)行了一些抽象。
- 我們想要一個(gè)只執(zhí)行一次的函數(shù),我們可以用 abstract once。
- 我們想要一個(gè)函數(shù)來(lái)緩存相應(yīng)參數(shù)的結(jié)果,我們可以 abstract cache 。
- 我們想要一個(gè)在執(zhí)行前后做某事的函數(shù),我們 可以 abstract intercept。
- 我們想要一個(gè)通過(guò)參數(shù)類型改變其執(zhí)行流程的函數(shù),我們可以 abstract batch。
- 它們都遵循一個(gè)共同的范式:即使用高階函數(shù)來(lái)abstract 任何一般過(guò)程。
Nested
恩,我想提的最后一件事:如果有必要,我們可以嵌套這些高階函數(shù)。
假設(shè)我們不僅要緩存計(jì)算函數(shù)的結(jié)果,還要在執(zhí)行它之前記錄它的參數(shù),并在執(zhí)行它之后記錄它的結(jié)果。然后,我們還想讓它能夠處理多重參數(shù)。我們可以這樣寫:
let computedEnhance = batch(intercept(cached(computed), {
before: arg => {
console.log(`processing ${arg}`)
},
after: res => {
console.log(`returned ${res}`)
}
})) 名稱欄目:如何像高級(jí)JavaScript開(kāi)發(fā)人員一樣為一般流程編寫高階函數(shù)
當(dāng)前URL:http://fisionsoft.com.cn/article/cojscso.html


咨詢
建站咨詢
