偏函数
先来看定义:在计算机科学中,局部应用是指固定一个函数的一些参数,然后产生另一个更小元的函数。
什么是元?元是指函数参数的个数,比如一个带有两个参数的函数被称为二元函数。
这是一本书的定义,看起来跟柯里化函数有点像。
对比柯里化函数:柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。
偏函数则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。
举个简单的例子:
function isType(type, obj) {
return {}.toString.call(obj) == "[object " + type + "]"
}
var isObject = isType("Object", { a: 'maile' })
var isObject = isType("Object", { b: 'maile' })
如果要判断一个参数obj是不是对象,是不是需要调用很多次isObject,掉用多次是没有问题的,但是其中一个参数却会一直重复出现,这个时候就可以使用偏函数的思想,来固定一个参数。
function isType(type) {
return function(obj) {
return {}.toString.call(obj) == "[object " + type + "]"
}
}
var isObject = isType("Object")
var isString = isType("String")
var isArray = Array.isArray || isType("Array")
var isFunction = isType("Function")
var isUndefined = isType("Undefined")
这样就不需要每次都传递同一个参数'Object'了。再需要判断是不是对象的时候,只需要isObject({a: 'maile'})这样调用就可以了。
这样需要更改原函数isType,如果说我想固定2个参数,或者3个参数,是不是又要去更改isType函数,这样会显得很不智能,能不能实现一个函数,让我们不必更改isType函数,可以任意固定参数?
underScore中就实现了这样一个方法,partial方法,先来看看使用方法:
var obj = { name: '麦乐' }
function isType(type, obj) {
return {}.toString.call(obj) == "[object " + type + "]"
}
var isObject = _.partial(isType, 'Object')
console.log(isObject(obj)) // true
固定两个参数,下面这个函数用来判断某个对象是否函数某个属性:
var obj = { name: '麦乐' }
function hasKey(type, obj, key) {
return {}.toString.call(obj) == "[object " + type + "]" && obj.hasOwnProperty(key)
}
var nameClass = _.partial(hasKey, 'Object', obj)
console.log(nameClass('name')) // true
只要调用一个partial函数就可以任意的固定参数,下面来实现一下这个函数:
function partial(func) {
var args = Array.prototype.slice.call(arguments, 1)
var bound = function() {
var index = 0,
length = args.length,
rest = Array(length)
for (var i = 0; i < length; i++) {
rest[i] = args(i)
}
for (; index < arguments.length; index++) {
rest.push(arguments[index])
}
func.apply(this, args)
}
}
调用partial函数,返回了bound函数,看上面的例子,再调用isObject函数实际就是调用bound函数,partial函数接受的func函数,就是isType函数,最后需要调用这个函数并把所有的参数传递给这个参数,func.apply(this, args),可以看出,ages里面包含了所有的参数,bound函数内部主要做的就是收集参数并且调用,而收集参数离不开arguments,这里就是运用arguments这个属性来完成的。
ags收集到是调用partial函数过着中传递进来的参数,不包含第一个,因为第一个是一个函数:
var args = Array.prototype.slice.call(arguments, 1)
rest就是复制了args数组,并且把isObject调用的过程中传递的参数收集。这样就实现了一个偏函数。
underScore源码中是借助restArguments这个函数来实现的偏函数
在前面章节中《underScore专题-剩余参数-restArguments》中有关于这个函数的详细讲解,这里就略过了,直接看偏函数:
var partial = restArguments(function(func, boundArgs) {
var placeholder = partial.placeholder;
var bound = function() {
var position = 0, length = boundArgs.length;
var args = Array(length);
for (var i = 0; i < length; i++) {
args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i];
}
while (position < arguments.length) args.push(arguments[position++]);
return executeBound(func, bound, this, this, args);
};
return bound;
});
partial.placeholder = _;
这里生成的partial函数就是restArguments函数参数的副本,调用过程中除了第一个参数之后所有的参数都以数组的形式存在了boundArgs中,类似于es6中的剩余参数。
function isType(type, obj) {
return {}.toString.call(obj) == "[object " + type + "]"
}
var isObject = _.partial(isType, 'Object')
这样就可以随意的固定参数,实现不同的功能。
缓存函数
1 首先来看一个简单的计算函数:
let count = 0
function add(a, b) {
count ++
return a + b
}
add(7, 8)
add(7, 8)
add(7, 8)
add(7, 8)
console.log('计算的次数', count) // 计算的次数 4
但是,如果传入的值一样, 就会出现重复计算,每调用一次就会计算一次。我们首先来考虑,怎样避免这些重复计算?
下面代码实现了一个闭包。里面存储了计算的结果,相同的参数避免了重复的计算。
function cache(fn) {
var cacheObj = {}
return function() {
// 把arguments伪数组变成真数组
let argument = Array.prototype.slice.apply(arguments)
// 对象中如果已经有该参数,就直接去取值 不用计算
if(cacheObj.hasOwnProperty(argument)) {
return cacheObj[argument]
}
// 如果没有 就调用函数去计算,并把结果存储在cacheObj中
return cacheObj[argument] = fn.apply(this, argument)
}
}
const cacheAdd = cache(add)
console.log(cacheAdd(7,8)) // 15
console.log(cacheAdd(7,8)) // 15
console.log(cacheAdd(7,8)) // 15
console.log(cacheAdd(7,8)) // 15
console.log('计算的次数', count) // 计算的次数 1
缓存函数写好了,下面我们就来测试一下这两个函数的性能。理论上讲,缓存函数应该比简单的add函数快很多,事实是这样吗?
let count = 0
function add(a, b) {
count ++
return a + b
}
console.time('add')
for(let i=0; i<10000; i++) {
add(1, 1)
}
console.timeEnd('add') // 执行次数 10000
console.log('执行次数', count)
// add: 0.77294921875ms 不太稳定
let count = 0
function add(a, b) {
count ++
return a + b
}
const cacheAdd = cache(add)
console.time('cacheAdd')
for(let i=0; i<10000; i++) {
cacheAdd(1, 1)
}
console.timeEnd('cacheAdd') // 执行次数 1
console.log('执行次数', count)
// cacheAdd: 17.5810546875ms
对比一下时间发现,缓存函数并没有比非缓存函数快,相反,还会比非缓存函数慢很多。这是为什么呢?
如果我们把函数修改一下,改为 a*b, 传入大数字值,结果会怎样?
let count = 0
function add(a, b) {
count ++
return a * b
}
const cacheAdd = cache(add)
console.time('cacheAdd')
for(let i=0; i<10000; i++) {
cacheAdd(645, 893)
}
console.timeEnd('cacheAdd') // 执行次数 1
console.log('执行次数', count)
// cacheAdd: 14.232177734375ms
let count = 0
function add(a, b) {
count ++
return a * b
}
// const cacheAdd = cache(add)
console.time('add')
for(let i=0; i<10000; i++) {
// cacheAdd(645, 893)
add(645, 893)
}
console.timeEnd('add') // 执行次数 10000
console.log('执行次数', count)
// add: 0.810791015625ms
然而并没有出现我们想要的结果,那是不是说缓存函数没有呢?看下面的例子
/*
这是一个生成斐波纳契数列的函数,传入的参数,就是数列在第几位上的值
在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=2, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
*/
let count = 0
function Fibonacci(n) {
count++
return n <= 2 ? 1 : Fibonacci(n-1) + Fibonacci(n-2)
}
console.time('Fibonacci')
Fibonacci(30)
console.timeEnd('Fibonacci') // Fibonacci: 10.988037109375ms
console.log('执行次数', count) // 执行次数 1664079
console.log(Fibonacci(30)) // 832040
我传递的是30,大家可以试试如果传递50会怎样?浏览器会卡死,一直转圈圈。使用缓存函数来计算:
let count = 0
function Fibonacci(n) {
count++
return n <= 2 ? 1 : cacheFibonacci(n-1) + cacheFibonacci(n-2)
}
function cache(fn) {
var cacheObj = {}
return function() {
// 把arguments伪数组变成真数组
let argument = Array.prototype.slice.apply(arguments)
// 对象中如果已经有该参数,就直接去取值 不用计算
if(cacheObj.hasOwnProperty(argument)) {
return cacheObj[argument]
}
// 如果没有 就调用函数去计算,并把结果存储在cacheObj中
return cacheObj[argument] = fn.apply(this, argument)
}
}
let cacheFibonacci = cache(Fibonacci)
console.time('cacheFibonacci')
cacheFibonacci(30)
console.timeEnd('cacheFibonacci') // cacheFibonacci: 0.118896484375ms
console.log('执行次数', count) // 执行次数 30
console.log(cacheFibonacci(30)) // 832040
区别已经很明显,运行快了很多。计算次数也小了很多,这样我们算出第100位斐波那契数列值也不是问题。
2 上面我们亲自验证了缓存函数的好处,那我们写的缓存函数有没有什么问题呢?是不是百分之百都回返回正确的值呢?看下面的例子
function add(obj) {
return obj.a
}
function cache(fn) {
var cacheObj = {}
return function() {
// 把arguments伪数组变成真数组
let argument = Array.prototype.slice.apply(arguments)
// 对象中如果已经有该参数,就直接去取值 不用计算
if(cacheObj.hasOwnProperty(argument)) {
return cacheObj[argument]
}
// 如果没有 就调用函数去计算,并把结果存储在cacheObj中
return cacheObj[argument] = fn.apply(this, argument)
}
}
let cacheAdd = cache(add)
console.log(cacheAdd({a:1})) // 1
console.log(cacheAdd({a:2})) // 1
console.log(cacheAdd({a:3})) // 1
参数传入的是对象,每次返回的值都是1 这是为什么?我门来看下cacheObj是什么样的?
console.log(cacheObj)
/* {[object Object]: 1}
[object Object]: 1
__proto__: Object
*/
可以看到cacheObj中只有一个属性,[object,object],因为参数会作为key存储,key在对象中以字符串的形式存在的,这里有一个隐式转换,生成key的过程中,调用了对象的toString方法,参数都会转变成[object,object]。每次执行的时候就会去取第一次存下来来的值,所以一直就只能拿到一个结果。
[function aa() {},8].toString()
"function aa() {},8"
[{a:3}].toString()
"[object Object]"
[4,5,6].toString()
"4,5,6"
我们可以在 Array.prototype.slice.apply(arguments)改一下代码,JSON.stringfy(Array.prototype.slice.apply(arguments))就可以了。
let count = 0
function add(obj) {
count++
return obj.a
}
function cache(fn) {
var cacheObj = {}
return function() {
// 把arguments伪数组变成真数组
let argument = Array.prototype.slice.apply(arguments)
let argumentString = JSON.stringify(Array.prototype.slice.apply(arguments))
console.log(cacheObj)
// 对象中如果已经有该参数,就直接去取值 不用计算
if(cacheObj.hasOwnProperty(argument)) {
return cacheObj[argumentString]
}
// 如果没有 就调用函数去计算,并把结果存储在cacheObj中
return cacheObj[argumentString] = fn.apply(this, argument)
}
}
let cacheAdd = cache(add)
console.log(cacheAdd({a:1, b:3, c:4})) // 1
console.log(cacheAdd({a:1, c:4, b:3})) // 1
console.log(cacheAdd({c:1, b:3, a:3})) // 1
console.log('执行次数', count) // 执行次数 3
看下上面的例子,我传递的对象是一样的,就是换了顺序,函数就执行了3次,而我们希望的值执行一次。打印一下cacheObj会发现{[{"a":1,"b":3,"c":4}]: 1, [{"a":1,"c":4,"b":3}]: 1}结果是这样的。所以在使用缓存函数的时候,要注意参数传递的问题。
underScore中也实现了一个缓存函数,支持传递key来标记参数,也直接传递函数生成key,也是address。
function memoize(func, hasher) {
var memoize = function(key) {
var cache = memoize.cache;
// 如果传递hasher则使用hasher函数来标记key,
var address = '' + (hasher ? hasher.apply(this, arguments) : key);
if (!_has(cache, address)) cache[address] = func.apply(this, arguments);
return cache[address];
};
memoize.cache = {};
return memoize;
}
再来求解一次斐波那契数:
function hasher() {
var n = arguments[0]
return n + 'mai'
}
let count = 0
function fibonacci(n) {
count++
return n <= 2 ? 1 : cacheFibonacci(n - 1) + cacheFibonacci(n - 2)
}
let cacheFibonacci = _.memoize(fibonacci, hasher)
console.time('cacheFibonacci')
cacheFibonacci(10)
console.timeEnd('cacheFibonacci') // cacheFibonacci: 0.118896484375ms
console.log('执行次数', count) // 执行次数 30
console.log(cacheFibonacci(10)) // 832040
console.log(cacheFibonacci.cache)