緩存函數
memorizition
定義:將上次的計算結果緩存起來,當下次調用時,如果遇到相同的參數,就直接返回緩存中的數據。
let add = (a,b) => a+b;
let calc = memoize(add);
calc(10,20);//30
calc(10,20);//30 緩存
如果要實現以上功能,主要依靠 閉包 、柯里化、高階函數
實現原理:把參數和對應的結果數據存在一個對象中,調用時判斷參數對應的數據是否存在,存在就返回對應的結果數據,否則就返回計算結果。
理論有了,我們來實現一個緩存函數:
let memoize = function (func, content) {
let cache = Object.create(null)
content = content || this
return (...key) => {
if (!cache[key]) {
cache[key] = func.apply(content, key)
}
return cache[key]
}
}
過程分析:
- 在當前函數作用域定義了一個空對象,用於緩存運行結果
- 運用柯里化返回一個函數,返回的函數因為作用域鏈的原因,可以訪問到
cache - 然後判斷輸入參數是不是在
cache的中。如果已經存在,直接返回cache的內容,如果沒有存在,使用函數func對輸入參數求值,然後把結果存儲在cache中。
在Vue中也有所體現
/**
* Create a cached version of a pure function.
*/
function cached (fn) {
var cache = Object.create(null);
return (function cachedFn (str) {
var hit = cache[str];
return hit || (cache[str] = fn(str))
})
}
/**
* Capitalize a string.
*/
var capitalize = cached(function (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
});
...
capitalize(camelizedId)
適用場景:
- 需要大量重複計算
- 大量計算並且依賴之前的結果
curry與偏函數
curry
function currying 把接受多個參數的函數轉換成接受一個單一參數的函數
// 非函數柯里化
var add = function (x,y) {
return x+y;
}
add(3,4) //7
// 函數柯里化
var add2 = function (x) {
//**返回函數**
return function (y) {
return x+y;
}
}
add2(3)(4) //7
在上面的例子中,我們將多維參數的函數拆分,先接受第一個函數,然後返回一個新函數,用於接收後續參數。
就此,我們得出一個初步的結論:柯里化後的函數,如果形參個數等於實參個數,返回函數執行結果,否者,返回一個柯里化函數。
通過柯里化可實現代碼複用,使用函數式編程。
實現柯里化函數
從上面例子中,我們定義了有兩個形參的函數,為了實現柯里化,函數傳入第一個形參後返回一個函數用來接收第二個形參。那麼如果我們的定義的形參有三個,那麼也就需要嵌套2層,分別處理後兩個參數,如
var add3 = function (x) {
return function (y) {
return function (z) {
return x + y + z;
}
}
}
add3(1)(3)(5)
如果形參有5個,7個呢?這裏我們使用遞歸,進行簡化。不知有沒有看到規律,形參的個數決定了函數的嵌套層數。 即 有n個參數就得嵌套n-1個函數 ,那我們來改造一番。
// 通用型柯里化
function currying (fn) {
// 未柯里化函數所需的參數個數 https://www.cnblogs.com/go4it/p/9678028.html
var limit = fn.length;
var params = []; // 存儲遞歸過程的所有參數,用於遞歸出口計算值
return function _curry(...args) {
params = params.concat(args); // 收集遞歸參數
if (limit <= params.length) {
let tempParams=params.slice(0,limit)
if(limit===params.length){ //參數個數滿足時清除已緩存的參數
params=[]
}
// 返回函數執行結果
return fn.apply(null, params);
} else {
// 返回一個柯里化函數
return _curry;
}
};
}
function add(x,y,z){
return x + y+z;
}
// 函數柯里化
var addCurried=currying(add);
console.log(`addCurried(1)(2)(3)`,addCurried(1)(2)(3))//6
console.log(`addCurried(3,3,3)`,addCurried(3,3,3))//9
console.log(`addCurried(1,2)(3)`,addCurried(1,2)(3))//6
console.log(`addCurried(3)(4,5)`,addCurried(3)(4,5))//12
我們看看addCurried(1)(2)(3)中發生了什麼:
- 首先調用
`addCurried(1),將1保存在詞法環境中,然後遞歸調用_curry繼續收集後續參數 addCurried(1)(2),參數2與第一次的參數1,合併調用,因未達到形參個數要求,繼續遞歸返回_curry- 調用
addCurried(1)(2)(3),參數為3,在接下去的調用中,與1,2進行合併,傳入原函數add中
注意點
- 柯里化基於閉包實現,可能會導致內存泄露
- 使用遞歸,執行會降低性能,遞歸多時會發生棧溢出,需要進行遞歸優化,參考
- arguments是類數組,使用
Array.prototype.slice.call轉換為數組時,效率低。
偏函數
簡單描述,就是把一個函數的某些參數先固化,也就是設置默認值,返回一個新的函數,在新函數中繼續接收剩餘參數,這樣調用這個新函數會更簡單。
// 乘法
let multi = (x,y) => x * y;
// 構造一個對數值乘以2的函數
let double = multi.bind(null,2);
console.log(double(3));//6
console.log(double(5));//10
在這個例子中,我們使用bind 固定了 乘數,返回一個函數。該函數接受一個參數作為 被乘數。--將部分參數固定,只對剩餘參數進行計算。
基於以上推導,我們來實現一個無綁定上下文的偏函數:
/**
* 偏函數實現
* @param func 應用函數
* @param argsBound 固定參數
* @return {function(...[*]): *}
*/
let partial = (func, ...argsBound) => {
if (typeof func !== 'function') throw new TypeError(
`${typeof func} is not a function`)
return function (...args) { // (*)
if(func.length-argsBound.length>args.length) throw new Error(`miss arguments`)
return func.call(this, ...argsBound.concat(...args))
}
}
let partialMulti= partial(multi,2)
console.log(partialMulti());//Error: miss arguments
console.log(partialMulti(3));//6
partial(func[, arg1, arg2...]) 調用的結果是一個基於 func 的封裝函數,以及:
- 和它傳入的函數一致的
this - 然後傳入
...argsBound—— 來自偏函數調用傳入的參數 - 然後傳入
...args—— 傳入封裝函數的參數
區別
偏函數與柯里化很相似,下面我們做個對比:
柯里化:將一個對參數函數轉換成多個單參數的函數,也就是將一個n元函數轉換為n個一元函數。
偏函數:固定一個函數的一個或多個參數,也就是將一個n元函數轉換成一個n-x元函數。
個人理解:偏函數是柯里化的一種特定的應用場景
使用場景
- 動態生成函數
- 減少參數
- 延遲計算