Js 高级函数

352 阅读10分钟

函数柯里化

什么是函数柯里化?

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

来看个栗子:

function getArr(a, b, c) {
    return [a,b,c];
}
getArr("a", "b", "c");   //["a","b","c"]
// 假设有一个curry函数可以将函数柯里化
var fn = curry(getArr);
fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

柯里化函数的用处

再来看一个栗子:

var person = [{name: 'yff'}, {name: 'lst'}]

当我们想要获取所有name的值,我们可以用map高阶函数

var names = person.map((item) => {
    return item.name
})

当我们有一个柯里化的函数时

var prop = curry(function (key, obj) {
    return obj[key]
});
// 这句话就很直白的告诉我们我们要拿取person数组元素中(prop)name的属性
var names = person.map(prop('name'))

柯里化函数初步实现

function curry(fn) {
    // 获得除传入的函数之外的其他参数
    var args = [].slice.call(arguments, 1)
    return function () {
        // 获得curry函数的额外参数和当前函数的额外参数
        var newArgs = args.concat([].slice.call(arguments))
        // 返回fn函数返回的结果
        return fn.apply(this, newArgs);
    };
}
function getArr(a, b, c) {
    return [a,b,c];
}
var cur_getArr = curry(getArr,'a','b')
console.log(cur_getArr('c'))    // ['a','b','c']

这并不是真正的柯里化函数,但是我们把这个函数用作辅助函数,下来我们实现一个真正的柯里化函数。

实现柯里化函数

function sub_curry(fn) {
    var args = [].slice.call(arguments, 1)
    return function () {
        var newArgs = args.concat([].slice.call(arguments))
        return fn.apply(this, newArgs);
    };
}
function curry(fn, length) {
    // 获取传入函数fn的参数长度
    length = length || fn.length;
    // 保存Array.prototype.slice方法
    var slice = Array.prototype.slice;
    return function () {
        if (arguments.length < length) {
            var combined = [fn].concat(slice.call(arguments));
            // 这里用到了递归的思想
            return curry(sub_curry.apply(this, combined), length - arguments.length);
        } else {
            return fn.apply(this, arguments);
        }
    };
}
function getArr(a, b, c) {
    return [a,b,c];
}
var fn = curry(getArr)
// console.log(fn("a", "b", "c")) // ["a", "b", "c"]
// console.log(fn("a", "b")("c")) // ["a", "b", "c"]
// console.log(fn("a")("b")("c")) // ["a", "b", "c"]
// console.log(fn("a")("b", "c")) // ["a", "b", "c"]

Function.length属性详情

Function.prototype.apply()方法详情

偏函数

什么是偏函数?

在计算机科学中,局部应用是指固定一个函数的一些参数,然后产生另一个更小元的函数。 什么是元?元是指函数参数的个数,比如一个带有两个参数的函数被称为二元函数。

来,举个栗子:

function getArr(a, b, c) {
    return [a,b,c];
}
getArr('a', 'b', 'c');   //["a","b","c"]
// 假设有一个partial函数可以做到局部应用
var fn = partial(getArr, 'a');
fn('b', 'c') // ["a", "b", "c"]

是否对这个例子感到似曾相识,他和上文柯里化的例子太像了。这里来引用一下冴羽大神的话,来说一说他们两个的区别:

柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。

局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。

实现偏函数

// 来看看这熟悉的代码
function partial(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        var newArgs = args.concat([].slice.call(arguments));
        return fn.apply(this, newArgs);
    };
};

惰性函数

Javascript的函数是运行时定义的,可以随时替换函数定义,惰性函数就是应用这个原理实现的。

惰性函数用来解决什么问题?

我们先写一个函数,这个函数返回首次调用时的 Date 对象。先写两个常规方法:

var t;
function foo() {
    if (t) return t;
    t = new Date()
    return t;
}
function foo() {
    if (foo.t) return foo.t;
    foo.t = new Date();
    return foo.t;
}

上面两个方法都没解决一个问题,就是调用时都要进行一次判断。现在我们只是对一个变量t进行判断,在实际开发中如果我们需要对一个非常复杂的函数做判断的话,那么用上述方法解决这个问题,是很消耗性能的。

惰性函数解决上述问题

var foo = function() {
    var t = new Date();
    // 对foo重新定义
    foo = function() {
        return t;
    };
    return foo();
};

组合函数

什么是组合函数

组合函数就是将若干个函数组合成一个新的函数,同时完成数据的传递,得出最终的结果。

来个栗子: 比如现在我们写一个函数,输入一个数字字符串,对这个字符串重新排序,最后输出重排后的字符串。

function toArray(str) {
    var array = str.split('');
    return array;
}

function sortArray(array) {
    return array.sort(function (value1, value2) {
        return value1 - value2
    })
}

function toStr(array) {
    return array.join('');
}
let newStr = toStr(sortArray(toArray('768123544275')));
console.log(newStr) //122344556778

如上面的例子,我们思考一下,如果我们要进行运算一个非常复杂的程序,那么我们就有可能进行许多层函数的嵌套,这样代码的可读性是非常不友好的。

那如果我们有一个compose函数可以将上面的那些函数组合起来形成一个新的函数,那就不会有上述的问题了:

var sortStr = compose(toStr, sortArray, toArray)
sortStr('768123544275')

这样代码的封装性和可读性是不是很好了。下来我们就来看一看compose函数是如何实现的:

compose函数(来自underscore)

function compose() {
    var args = arguments;
    var start = args.length - 1;
    return function() {
        var i = start;
        var result = args[start].apply(this, arguments);
        while (i--) result = args[i].call(this, result);
        return result;
    };
};

组合函数的实际用处

推荐阅读阮一峰老师的Pointfree 编程风格指南

记忆函数

什么是记忆函数?

函数记忆是指将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据。函数记忆只是一种编程技巧,本质上是牺牲算法的空间复杂度以换取更优的时间复杂度。

来看个栗子:

function add(x,y) {
    return x+y;
}
// 假设我们有一个函数memoize可实现函数记忆
var memoizedAdd = memoize(add);
memoizedAdd(1, 2) //3
memoizedAdd(1, 2) // 当我们以相同的参数进行第二次调用时,该函数会从其内部缓存中取出数据,而非重新计算一次

《JavaScript权威指南》如何实现memoize函数的

  • 原理: 将函数的参数进行转化并作为其内部对象的key,将函数的计算结果保存为对应key的value值。当函数调用时,判断内部对象是否存在对应的key,如果存在就直接返回其对应的value值。
function memoize(f) {
    var cache = {};
    return function(){
        var key = arguments.length + Array.prototype.join.call(arguments, ",");
        if (key in cache) {
            return cache[key]
        }
        else {
            return cache[key] = f.apply(this, arguments)
        }
    }
}
  • 不足: 上面的实现方法的内部调用了Array.prototype.join()方法,那我们想一想如果我们传递了一个对象作为参数,就会自动调用toString()方法将其转变为[Object object],再进行字符串拼接作为key值。
var propValue = function(obj){
    return obj.value
}
var memoizedAdd = memoize(propValue)
console.log(memoizedAdd({value: 1})) // 1
console.log(memoizedAdd({value: 2})) // 1

上面例子中,两者都返回了1,这显然是有问题的。

underscore 如何实现memoize函数的

var memoize = function(func, hasher) {
    var memoize = function(key) {
        var cache = memoize.cache;
        var address = '' + (hasher ? hasher.apply(this, arguments) : key);
        if (!cache[address]) {
            cache[address] = func.apply(this, arguments);
        }
        return cache[address];
    };
    memoize.cache = {};
    return memoize;
};

如果我们没有传递hasher函数,它会将我们传递进去的第一个参数作为key,当我们需要传递多个参数的时候,这显然是有问题的。所以当我们的函数需要传递多个参数,我们可以传进去一个我们自定义的hasher,来设置我们的key。

防抖和节流函数

引入

防抖和节流是在开发中常常用到的。比如说通过滚动事件来获取后台数据,或是说通过输入框输入值的改变事件来获取后台数据,都需要用到防抖和节流。如果处理不当,会导致前端界面不断向后台发送请求,导致数据阻塞,造成浏览器卡死。

这次通过一个特别简单的小示例,来做防抖和节流的一个小总结。 在这做一个鼠标移动事件,当鼠标在一个盒子里移动则令其里面的值加1。

<div id="container"></div>
<button id="btn">取消</button>
#container{    
    width: 100%;    
    height: 100px;    
    text-align: center;    
    line-height: 100px;   
    background-color: #000;    
    color: #fff;    
    font-size: 30px;
}
let box = document.querySelector('#container')    
let count = 0;    
function doSomething (e) {        
    box.innerHTML = count++;    
}
box.onmousemove = doSomething;

这时候我们便可发现,只要我们的鼠标在盒子里面移动,count值便不断的增加,如果我们的操作不是让count加1,而是获取后台数据,那么我们便会不断的向后台发送数据,容易造成浏览器卡死。而解决这个问题的方法便是防抖和节流。

防抖函数

原理

如果在一段时间(自己设置)内没有再次触发鼠标移动事件,那么就执行函数 如果在一段时间内再次触发鼠标移动事件,那么当前的计时取消,重新开始计时

  • 实现 : 定时器
  • 参数 : (1)要执行的函数,(2) 等待时间, (3)是否立即执行

代码实现

function debounce(func, wait, immediate) {
    var timeout, result;
    let decounced = function () {
        // 改变执行函数内部this的指向,让this指向事件调用者的对象
        let context = this; 
        // event指向问题,让event指向当前事件对象
        let args = arguments
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            let callNow = !timeout;
            timeout = setTimeout(function () {
                timeout = null;
            }, wait);
            // 立即执行
            if (callNow) result = func.apply(context, args)
        } else {
            // 不会立即执行
            timeout = setTimeout(function () {
                func.apply(context, args)
            }, wait);
        }
        return result;
    }
    decounced.cancel = function () {
        clearTimeout(timeout);
        // 防止内存泄漏
        timeout = null;
    }
    return decounced;
}
let btn = document.querySelector('#btn')
let count = 0;function doSomething (e) {      
    box.innerHTML = count++;
}
let doSome = debounce(doSomething, 2000, {leading: true, trailing: false})
btn.onclick = function () {    
    doSome.cancel()
}
box.onmousemove = doSome;

应用场景

  1. scroll事件滚动触发
  2. 搜索框输入查询
  3. 表单验证
  4. 按钮提交事件(防止用户不断点击)
  5. 浏览器窗口缩放,resize事件

节流函数

原理

如果你持续触发事件,每隔一段时间,只执行一次事件

  • 实现:时间戳,定时器
  • 参数:

(1)要执行的函数,

(2)等待时间,

(3)一个对象:

{leading:false, trailing:true} 第一次不会触发, 最后一次会触发

{leading:true, trailing:false} 第一次会触发, 最后一次不会触发

{leading:true, trailing:true} 第一次会触发, 最后一次会触发

注:节流函数不会实现第一次不触发和最后一次也不触发的情况,这和函数内部实现有关

代码实现

function throttle(func, wait, options) {
    // this指向变量, event对象变量, 定时器变量
    let context, args, timeout;
    // 之前的时间戳
    let old = 0;
    // 设置第三个参数默认值
    if (!options) options = {};
    let later = function () {
        old = new Date().valueOf();
        timeout = null;
        func.apply(context, args);
    }
    return function () {
        context = this;
        args = arguments;
        let now = new Date().valueOf();
        if (options.leading === false && !old) {
            old = now;
        }
        if (now - old > wait) {
            // 第一次会直接执行
            if (timeout) {
                clearTimeout(timeout)
                timeout = null;
            }
            func.apply(context, args);
            old = now
        }
        if (!timeout && options.trailing !== false) {
            // 最后一次也会被执行
            timeout = setTimeout(later, wait);
        }
    }
}
let box = document.querySelector('#container')
let btn = document.querySelector('#btn')
let count = 0;function doSomething (e) {
    box.innerHTML = count++;
}
let doSome = throttle(doSomething, 2000, {leading: true, trailing: false})
btn.onclick = function () {
    doSome.cancel()
}
box.onmousemove = doSome;

我们将上述代码分开来看:

  1. 应用事件戳来实现,第一次触发、最后不会被调用触发函数{leading:true, trailing:false}
function throttle(func, wait) {
    let context, args;
    // 之前的时间戳
    let old = 0;
    return function() {
        context = this;
        args = arguments;
        // 获取当前的时间戳
        let now = new Date().valueOf();
        if (now-old > wait) {
            // 立即执行
            func.apply(context, args)
            old = now;
        }
    }
}
  1. 应用计时器来实现,第一次不会触发, 最后一次会触发{leading:false, trailing:true}
function throttle(func, wait) {
    let context, args, timeout;
    return function() {
        context = this;
        args = arguments;
        if (!timeout) {
            timeout = setTimeout(() => {
                timeout = null;
                func.apply(context, args);
            }, wait);
        }
    }
}

应用场景

  1. DOM元素的拖拽功能实现
  2. 射击游戏
  3. 计算鼠标移动的距离
  4. 监听scroll滚动事件 上述为对防抖和节流的基本总结,另外大家可以到underscore.js找到其源码,并引入到自己的项目中,如果有对文章内容和代码不懂的小伙伴,推荐观看下面的视频:

www.bilibili.com/video/BV1pQ…

参考资料

冴羽大神的JavaScript专题系列中的函数部分

www.bilibili.com/video/BV1pQ…