阅读 25

JS 常用函数垫片

常用的一些函数垫片,意在加深js基础概念,同时对js也是个很好的总结。以下案例为个人实践,考虑主流程完整,但有些边界问题未考虑,不推荐在工程项目中使用。正式项目推荐使用lodash

call/apply

问题

var foo = { value: 1 }
function bar() { console.log(this.value) }
bar.call(foo) // 期待打印:1
bar.apply(foo) // 期待打印:1
复制代码

思路

call/apply立即执行函数,同时函数中的this改为指向context。类似等价于以下

var foo = {
    value: 1,
    fn: function bar() { console.log(this.value) }
}
复制代码
Function.prototype.call = function(context, ...args) {
    context = context || window
    context.fn = this // 这里的this代表函数
    context.fn(...args) // 给context添加属性fn,所以执行fn方法时,里面的this代表context
    delete context.fn
}

Function.prototype.apply = function(context, ...args) {
    context = context || window
    context.fn = this
    context.fn(args) // apply传递数组
    delete context.fn
}
复制代码

bind

问题

var foo = { value: 1 }
function bar() { console.log(this.value) }
let barBind = bar.bind(foo)
barBind() // 期待打印:1
复制代码

思路

通过apply改变this,并且返回一个函数

Function.prototype.bind = function (context, ...args) {
    var fn = this
    return function() {
        return fn.apply(context, args)
    }
}
复制代码

curry

问题

let addFun = function(a, b, c) { return a + b + c }
let curryFun = curry(addFun)
curryFun(1)(2)(3) === 6 // true
复制代码

思路

递归,当执行的参数个数等于原本函数的个数,执行函数

var curry = function(fn) {
    var limit = fn.length // fn函数参数个数
    return function judgeCurry (...args) {
        if (args.length >= limit) {
            return fn.apply(null, args)
        } else {
            return function(...args2) {
                return judgeCurry.apply(null, args.concat(args2))
            }
        }
    }
}

// or es6
var curry = function(fn, ...args) {
    if (args.length >= fn.length) {
        return fn(...args)
    }

    return function (...args2) {
        return curry(fn, ...args, ...args2)
    }
}
复制代码

pipe/compose

pipe

  • pipe(fn1,fn2,fn3,fn4)(args)等价于fn4(fn3(fn2(fn1(args)))
  • 第一个函数的结果,作为第二个函数的参数,以此类推...

compose

  • compose(fn1,fn2,fn3,fn4)(args)等价于fn1(fn2(fn3(fn4(args)))
  • 与pipe相反,先计算倒数第一个结果,作为倒数第二的参数,以此类推...
let loopItem = (prevFn, nextFn) => (...args) => prevFn(nextFn(...args))

const compose = (...fns) => fns.reduce(loopItem);
const pipe = (...fns) => fns.reduceRight(loopItem)

const example = pipe(
    (x, y) => x * y,
    x => x + 1
);
console.log(example(3, 4)) // 13
复制代码

flatten

深度为1的展平

// before:[1, 2, [3, 4, [5, 6]]]
// after flat: [1, 2, 3, 4, [5, 6]]

// 思路:使用reduce或map
function flatSingle(arr) {
    return arr.reduce((pre, val) => pre.concat(val), [])
}

// or
let flatSingle = arr => [].concat(...arr)
复制代码

深度无限的展平

// before: [1,2,3,[1,2,3,4, [2,3,4]]]
// after flatDeep: [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

// 思路:深度优先递归,使用reduce连接起来
// 深度优先算法 - 递归
function flatDeep(arr) {
    return arr.reduce((pre, val) => pre.concat(Array.isArray(val) ? flatDeep(val) : val), [])
}

// 深度优先算法 - 堆栈
function flatDeep(arr) {
    const stack = [...arr]
    const res = []
    while (stack.length) {
        const val = stack.pop() // 从尾部开始
        Array.isArray(val) ? stack.push(val) : res.push(val)
    }

    return res.reverse()
}

// 取巧,利用Array.toString()
function flatDeep(arr) {
    return arr.toString().split(',')
}
复制代码

指定深度的展平

深度的含义是指每一项展平的次数

// before: [1,2,3,[1, [2]], [1, [2, [3]]]]
// after: [ 1, 2, 3, 1, 2, 1, 2, [ 3 ] ]

function flatDeep(arr, depth = 1) {
    if (depth === 1) return arr.reduce((pre, val) => pre.concat(val), [])
    return arr.reduce((pre, val) => pre.concat(Array.isArray(val) ? flatDeep(val, depth - 1) : val), [])
}
复制代码

去重

数组去除重复

// before: [2, 1, 3, 2]
// after: [2, 1, 3]

function removeRepeat(arr) {
    return arr.filter((item, index) => arr.indexOf(item) === index)
}

// or es6
let removeRepeat = arr =>  Array.from(new Set(arr))
let removeRepeat = arr =>  [...new Set(arr)]
复制代码

浅拷贝/深拷贝

// 浅拷贝
function clone(source) {
    var target = {}
    for (var i in source) {
        source.hasOwnProperty(i) && target[i] = source[i]
    }

    return target
}

// or es6
const clone = source => Object.assign({}, source)
const clone = source => { ...source }
复制代码
// 深拷贝
// 思路:递归赋值
const deepClone = source => {
    if (!source || typeof source !== 'object') {
        throw new Error('error arguments', 'shallowClone')
    }

    // 区分array和object对象
    let target = source instanceof Array ? [] : {}
    for (let key in source) {
        if (source.hasOwnProperty(key)) {
            target[key] = typeof source[key] === 'object' ? deepClone(source[key]) : source[key]
        }
    }
    return target
}

// or 取巧方法
// 注意这种取巧方法是有限制的
// 1. 只能解析Number、String、Array等能够被json表示的数据结构
// 2. 不能处理循环引用
const deepClone = source => JSON.parse(JSON.stringify(source))
复制代码

防抖/节流

  • 防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。适合多次事件一次响应。
  • 节流:规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。适合大量事件按时间做平均分配触发。
// 防抖案例:窗口resize停止才执行最终的函数
function debounce(fn, wait, ...args) {
    var that = this
    fn.tId && clearTimeout(fn.tId)
    fn.tId = setTimeout(function() {
        fn.apply(that, args)
    }, wait)
}

function handle(e) {
    console.log('resize end event')
    console.log(e) // Event{}
}
// 缺点:handle不能写成匿名函数,因为把tId存储在handle函数对象上。所以间接导致传递参数e较为复杂
window.onresize = function(e) { debounce(handle, 1000, e) }

// 改进版
// 思路: 用闭包把tId存储起来
function debounce(fn, wait) {
    var tId
    return function() {
        var that = this
        var args = arguments
        tId && clearTimeout(tId)
        tId = setTimeout(function() {
            fn.apply(that, args)
        }, wait)
    }
}
function handle(e) {
    console.log('resize end event')
    console.log(e) // Event{}
}
window.onresize = debounce(handle, 1000)
复制代码
// 节流案例: 不停scroll时,滚动条每隔100ms固定频率执行函数
function throttle(fn, wait) {
    var cur = new Date()
    fn.last = fn.last || 0
    if (cur - fn.last > wait) {
        fn.call()
        fn.last = cur
    }
}

function handle() {
    console.log('scroll event')
}
window.onscroll = function() { throttle(handle, 100) }
复制代码

参考文章

关注下面的标签,发现更多相似文章
评论