以其他函式作為引數的函式
本章的所有程式碼,均在github.com/antgod/func…
關於傳遞函式的思考
- max
在很多程式語言的核心庫,都包含一個叫做max的函式。包括underscore也有這樣的函式。
const max = (data) => {
return data.reduce((maxer, next) => {
return maxer > next ? maxer : next
})
}
console.log(max([5, 1, 3, 4, 2])) // 輸出5複製程式碼
執行結果並沒有什麼奇怪,但是這樣特定的函式存在一個限制,如果我們不是從數字而是從物件中尋找最大值,該怎麼辦?
const max = (data, compare = item => item) => {
return data.reduce((maxer, next) => {
return compare(maxer) > compare(next) ? maxer : next
})
}
console.log(max([{ age: 64 }, { age: 32 }, { age: 50 }], item => item.age))複製程式碼
但是,在某些方面,這個函式仍然受限,並不是真正的函式式,讀者想想看,為什麼呢?
這個函式的大於號,是定死的。
我們可以構建一個新的函式,一個用來生成可比較的值,另一個用來比較兩個值返回最佳值。
const finder = (data, need, compare) => {
return data.reduce((last, next) => {
return compare(last) === need(compare(last), compare(next)) ? last : next
})
}
const identity = prop => prop
console.log(finder([1, 2, 3, 4, 5], Math.max, identity))複製程式碼
當我們要比較物件大小時候,就可以傳遞第三個引數了。
const finder = (data, need, compare) => {
return data.reduce((last, next) => {
return compare(last) === need(compare(last), compare(next)) ? last : next
})
}
const plucker = prop => item => item[prop]
console.log(finder([{ age: 64 }, { age: 32 }, { age: 50 }], Math.max, plucker('age')))複製程式碼
當我們要查詢以B開頭的名字,怎麼辦呢?
const finder = (data, need, compare) => {
return data.reduce((last, next) => {
return compare(last) === need(compare(last), compare(next)) ? last : next
})
}
const plucker = prop => item => item[prop]
console.log(finder([{ name: 'A', age: 64 }, { name: 'B', age: 32 }, { name: 'C', age: 50 }], (x, y) => {
return x.charAt(0) === 'B' ? x : y
}, plucker('name')))複製程式碼
發現問題
我們發現,函式雖然短小精幹,並且也按照了我們的預期工作。但是卻有一些重複性程式碼。
// in finder
return compare(last) === need(compare(last), compare(next)) ? last : next
// in used
return x.charAt(0) === 'B' ? x : y複製程式碼
你會發現,這兩段邏輯完全相同,也就是說,這兩種演算法都是返回最佳值或者當前值。
我們完全可以按照以下思路縮減程式碼:
- 如果第一個引數比第二個引數更好,返回第一個引數。
const bester = (data, need) => {
return data.reduce((last, next) => {
return need(last, next) ? last : next
})
}
console.log(bester([{ name: 'A', age: 64 }, { name: 'B', age: 32 }, { name: 'C', age: 50 }], (x, y) => {
return x.age < y.age
}))複製程式碼
關於傳遞函式的思考:重複、與條件
上一章中,我們建立了接受兩個函式引數的finder,並且簡化成一個函式引數的bester。事實上,在大多數js函式設計中,都不需要返回多個函式。
但是某些情況下,我們需要返回多個函式。讓我們以repeat開始,介紹為什麼要使用多個函式為引數。
const range = (times) => {
const ranges = []
for (let idx = 0; idx < times; idx++) {
ranges.push(null)
}
return ranges
}
const repeat = (value, time) => {
return range(time).map(() => value)
}
console.log(repeat(4, 3))複製程式碼
range函式對underscore的range做了下刪減,返回一個包含n個值為null的陣列。
使用函式,而不是值
作為repeat的常規實現方式,我們仍然有提高的空間。如果將重複值運算,那樣使用場景更廣泛。
比如我們要隨機生成10個10以內的數字(range函式與前文相同)。
const repeatness = (createValue, time) => {
return range(time).map((value, index) => createValue(index))
}
console.log(repeatness(() => Math.floor(Math.random() * 10) + 1, 10))複製程式碼
一個函式是不夠的,需要多參函式
有的時候,我們不知道函式需要呼叫多少次,我們只有一個條件。比如說,當我們不斷重複呼叫一個函式,當函式超過某個閾值或者條件,我們停止呼叫。使用repeatness明顯達不到我們的需求。
比如,我們想計算1024以下所有的2的整數指數值(2, 4, 8, 16, 32, 64, 128, 256, 512)。
const iterate = (createValue, checker, init) => {
const ret = []
let result = createValue(init)
while (checker(result)) {
ret.push(result)
result = createValue(result)
}
return ret
}
console.log(iterate(n => n + n, n => n < 1024, 1))複製程式碼
函式接收兩個函式引數,一個用來執行動作,一個用來校驗結果。當結果滿足時返回true。這算是repeatness的升級版了,連重複次數都是開放的,受到一個函式的執行結果影響。
返回其他函式的函式
回憶下之前repeatness返回三個常量的情況
repeatness(() => 'Odelay', 3)複製程式碼
這種返回常量的函式非常有用,是函數語言程式設計的一種設計模式,通過函式來返回值。我們經常稱之為k,為了清晰起見,我們稱之為alwasy。
const always = value => () => value
console.log(repeatness(always('Odelay'), 3))複製程式碼
always的行為可以用來解釋閉包。閉包用來捕獲一個值,並多次返回相同的值。每一個新的閉包都會返回不一樣的值
const f = always(() => {})
console.log(f() === f())
// => true
const g = always(() => {})
console.log(g() === f())
// => false複製程式碼
像always這樣的函式被稱為組合子。
高階函式捕獲引數
高階函式的引數用來配置返回函式的行為。
注意觀察以下程式碼:
const makeAdder = init => rest => init + rest
const add100 = makeAdder(100)
console.log(add100(38))
// => 138複製程式碼
你會經常看到一個函式返回了一個捕獲變數的函式。
捕獲變數的好處
比如你要生成一個特定字首的隨機字串。
const uniqueString = prefix => [prefix, new Date().getTime()].join('')
console.log(uniqueString('ghosts'))
console.log(uniqueString('turkey'))複製程式碼
看起來不錯,但是如果我們需要自增的索引,而不是時間戳作為字尾,怎麼辦?
可以使用閉包來實現:
const generator = (init, prefix) => {
let counter = init
return (pre = prefix) => {
return [pre, counter++].join('')
}
}
const g1 = generator(0, 'prefix')
console.log(g1())
console.log(g1())
console.log(g1('new'))
/*
=>
prefix0
prefix1
new2
*/複製程式碼
我們也可以用物件來實現:
const generator2 = (init, prefix) => {
return {
count: init,
uniqueString: function(pre = prefix) {
return [pre, this.count++].join('')
},
}
}
const g2 = generator2(0, 'prefix')
console.log(g2.uniqueString.call(g2))
console.log(g2.uniqueString.call(g2))
console.log(g2.uniqueString('new'))
/*
=>
prefix0
prefix1
new2
*/複製程式碼
注意這裡使用了this訪問資料,那麼函式不能再使用箭頭函式。物件的缺點是不安全,因為可以隨意訪問 count的值。很多時候隱藏實現細節是很重要的。事實上,我們可以把count像閉包一樣隱藏在函式內部。
const generator3 = (init, prefix) => {
let counter = init
return {
uniqueString: (pre = prefix) => {
return [pre, counter++].join('')
},
}
}
const g3 = generator3(0, 'prefix')
console.log(g3.uniqueString())
console.log(g3.uniqueString())
console.log(g3.uniqueString('new'))
/*
=>
prefix0
prefix1
new2
*/複製程式碼
閉包的方式乾淨、簡單,但是也充滿了陷阱。
改值時候要小心
雖然對於外界操作來說,該變數是安全的。但是它會增加複雜度,當一個函式的返回值只依賴引數時,被稱為引用透明。
這個詞看起來很花哨,意味著不破壞程式碼結構的情況下,用預期的值替換函式的任意呼叫。當你使用會改變內部程式碼變數的閉包時,你不一定能做到這一點。因為很多閉包的返回值是依賴於呼叫次數的。也就是說,呼叫uniqueString10次與10000次,返回值是不同的。
所以我們在任何地方呼叫閉包的函式時,都需要格外小心,有必要時,需要增加監控或者日誌。否則在閉包返回值值任意變化時,我們往往找不到變化原因。
防止不存在的函式: fnull(grund)
我們建立幾個高階函式的例子,第一個叫做fnull,我們先來舉例說明下它的目的。
假設我們有一組需要乘法的陣列:
const nums = [1, 2, 3, null, 5]
console.log(nums.reduce((total, n) => (total * n)))複製程式碼
很顯然null不會給我們任何有用的答案。這時候fnull函式很有用。
const nums = [1, 2, 3, null, 5]
const fillnull = (handle, ...args) => (...argvs) => handle(...argvs.map((argv, i) => argv || args[i]))
console.log(nums.reduce(fillnull((total, n) => { return total * n }, 1, 1)))複製程式碼
函式檢查每個傳入的引數是否是null或者undefined。如果是,則用預設值替換掉,然後再呼叫函式。
如果要查詢的目標是物件,可以用以下方式使用:
const defaults = d => (o, k) => {
const val = fillnull(identity, d[k])
return o && val(o[k])
}
const ages = [{ age: 100 }, { age: 120 }, { age: 150 }, { }, { age: 30 }]
const lookup = defaults({ age: 0 })
console.log(ages.reduce((total, age) => {
return total + lookup(age, 'age')
}, 0))複製程式碼
其中defaults函式用來配置預設值,返回一個函式lookup,之後每次執行lookup函式,如果遇到空值,都會返回預設值代替。