前端常见手写函数及算法

1,418 阅读11分钟

这篇文章主要是对于自己近期学习的一个记录,后续有新内容的话会持续更新。

手写函数

eventBus(发布订阅模式)

发布-订阅者模式就是一对一模式,通过固定的事件名通知到对应的该事件名订阅者。

//发布订阅模式
class EventBus {
    constructor() {
        //定义事件总线
        this.eventList = {}
    }

    //监听,发布事件
    $on(event, fn) {
        // 如果对象中没有对应的 event 值,也就是说明没有订阅过,就给 event 创建个缓存列表
        // 如有对象中有相应的 event 值,把 fn 添加到对应 event 的缓存列表里
        let evenList = this.eventList[event]
        evenList ? evenList.push(fn) : this.eventList[event] = [fn]
    }

    //触发,订阅
    $emit(event, ...args) {
        let eventList = this.eventList[event]
        // 如果缓存列表中没有相应的 fn,返回false
        if (!eventList) return false
        
        // 遍历 event 值对应的缓存列表,依次执行 fn
        this.eventList[event] && this.eventList[event].forEach(fn => {
            fn(...args)
        });
    }

    //只执行一次
    $once(event, fn) {
        // 箭头函数改变this指向
        const on = (...args) => {
            fn(...args)
            // 调用函数后取消订阅
            this.$off(event, on)
        }
        this.$on(event, on)
    }

    //取消订阅
    $off(event, fn) {
        let eventList = this.eventList[event]
        // 如果缓存列表中没有相应的 fn,返回false
        if (!eventList) return false

        if (!fn) {
            // 如果没有传 fn 的话,就会将 event 值对应缓存列表中的 fn 都清空
            eventList = []
        } else {
            // 若有 fn,遍历缓存列表,看看传入的 fn 与哪个函数相同,如果相同就直接从缓存列表中删掉即可
            const i = eventList.findIndex(el => {
                return el === fn
            })
            eventList.splice(i, 1)
        }
        console.log(eventList)
    }
}

// function fnc(msg) {
//     console.log(`订阅了${msg}`)
// }

// function fnc2(msg) {
//     console.log(`订阅了${msg}2`)
// }

// let eventBus = new EventBus()

// eventBus.$on('sayHi', fnc)
// eventBus.$emit('sayHi', ['sayHi', 'sayHi3'])

// eventBus.$on('sayHi', fnc2)
// eventBus.$emit('sayHi', 'sayHi')

// eventBus.$off('sayHi', fnc2)

// eventBus.$once('once', fnc)
// eventBus.$emit('once', 'once')

观察者模式

观察者模式就是一对多事件,当事件发生的时候通知到多个观察者,所有观察者进行对应的更新操作update执行事件操作。

// 定义被观察者
class Subject {
    constructor(name) {
        this.name = name
        this.observers = [] //储存观察者的集合
        this.state = ''
    }

    attach(observer) {
        // 提供被观察的方法
        this.observers.push(observer)
    }  

    setState(newState) {
        // 修改参数并通知观察者
        this.state = newState
        this.observers.length && this.observers.map(el => {
            el.update(this.name, newState)
        })
    }
}

//定义观察者
class Observer {
    constructor(name) {
        this.name = name
    }

    // 通知被观察者已更新
    update(name,newState) {
        console.log(`${this.name}看到:${name}${newState}`)
    }
}

// let sub = new Subject('被观察者')
// let obs = new Observer('观察者')

// sub.attach(obs)
// sub.setState('change')
// sub.setState('change2')

手写 instanceOf

instanceof 运算符用来判断一个构造函数的prototype属性所指向的对象是否存在另外一个要检测对象的原型链上。

对于值类型,可以通过typeof判断,string/number/boolean都很清楚,但是typeof在判断到引用类型的时候,返回值只有object/function,你不知道它到底是一个object对象,还是数组,还是new Number等等,这个时候就需要用到instanceof。

function myInstanceof(left, right) {
    if (typeof left != 'object' || left === null) {
        return false
    }

    let L = Object.getPrototypeOf(left)
    let R = right.prototype
    
    while (true) {
        if (L === null) {
            return false
        }
        if (L === R){
            return true
        } 
 
        L = Object.getPrototypeOf(L)
    }
}

// console.log(myInstanceof(2, Number))

call, apply, bind

三者都可以改变函数的 this 对象指向。

三者第一个参数都是 this 要指向的对象,如果如果没有这个参数或参数为 undefined 或 null,则默认指向全局 window。

三者都可以传参,但是 apply 是数组,而 call 是参数列表,且 apply 和 call 是一次性传入参数,而 bind 可以分为多次传入

bind 是返回绑定 this 之后的函数,便于稍后调用;apply 、call 则是立即执行

bind()会返回一个新的函数,如果这个返回的新的函数作为构造函数创建一个新的对象,那么此时 this 不再指向传入给 bind 的第一个参数,而是指向用 new 创建的实例

Function.prototype.myCall = function (obj, ...arg) {
    let newObj = obj || window;
    newObj.fn = this
    let res = newObj.fn(...arg)
    delete newObj.fn
    return res
}

Function.prototype.myApply = function (obj, arr) {
    let newObj = obj || window;
    newObj.fn = this
    let res
    if (!arr) {
        res = obj.fn()
    } else {
        res = newObj.fn(...arr)
    }
    delete newObj.fn
    return res
}

Function.prototype.myBind = function(context) {
    if (typeof this !== 'function') {
        throw new TypeError('Error');
    }
    const _this = this; // this 即 f.myBind 的 f
    const args = [...arguments].slice(1);
    return function F() {
        if (this instanceof F) {
            return new _this(...args, ...arguments);
        }
        return _this.apply(context, args.concat(...arguments));
    }
};

防抖和节流

防抖和节流可以说是一对好基友,防抖和节流其实都是在规避频繁触发回调导致大量计算,从而影响页面发生抖动甚至卡顿。简单的说将多次回调比如页面点击或ajax调用变为一次。防抖和节流的区别在于以第一次为准还是最后一次为准。

节流Throttle 调用多次、只第一次调用有效

/**
 * 节流
 * @param {*} fn 将执行的函数
 * @param {*} time 节流规定的时间
 */
function throttle(fn, time) {
  let timer = null
  return (...args) => {
    // 若timer === false,则执行,并在指定时间后将timer重制
    if(!timer){
      fn.apply(this, args)
  
      timer = setTimeout(() => {
        timer = null
      }, time)
    }
  }
}

防抖Debounce 最后一次为准

function debounce(fn, time) {
  let timer = null
  
  return (...args) => {
    // 重新执行并停止上次执行(若上次还未执行则会被清除)
    if(timer){
      clearTimeout(timer)
    }

    timer = setTimeout(() => {
      timer = null
      fn.apply(this, args)
    }, time)
  }
}

实现new

new操作符做了这些事:

创建了⼀个全新的对象

会被执⾏[[Prototype]](也就是proto)链接,使this指向新创建的对象

通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上

如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调⽤将返回该对象引⽤

function myNew() {
    const obj = new Object()
    const Constructor = [].shift.call(arguments)
    obj.__proto__ = Constructor.prototype
    const result = Constructor.apply(obj, arguments)
    return typeof result === 'object' ? result : obj
}

Object.creat

新建一个空的构造函数F,然后让F.prototype属性指向参数对象obj,最后返回一个F的实例,从而实现让该实例继承obj的属性。

Object.create = function (obj) {
    var F={};
    Object.setPrototypeOf(F,obj);
    return F;  
};

深拷贝

在工作中对于对象的操作我们常常会用到深拷贝新开辟一个内存空间,防止数据相互影响,关于深拷贝的实现方式有很多种,这边采用递归的方式来实现,以追求代码的简洁清晰。

function deepClone(obj) {
    // 深拷贝
    let newObj = obj instanceof Array ? [] : {}
    if (typeof obj !== 'object') {
      // 非对象不进行处理
      return obj
    } else {
      for (let i in obj) {
        newObj[i] = typeof obj[i] === 'object' ? this.deepClone(obj[i]) : obj[i]
      }
    }
    return newObj
  }

实现reduce

reduce方法将会对数组元素从左到右依次执行reducer函数,然后返回一个累计的值,reduce的使用方式有很多种,常见数组扁平化及compose的实现等等。

function reduce(arr, cb, initValue) {
  let num = initValue == undefined ? arr[0] : initValue
  let i = initValue == undefined ? 1 : 0
  for (i; i < arr.length; i++) {
    num = cb(num, arr[i], i)
  }
  return num
}

function fn(result, currentValue, index) {
  return result + currentValue
}

// let arr = [2, 3, 4, 5]
// let b = reduce(arr, fn, 10)
// let c = reduce(arr, fn)
// console.log(b)   // 24

//扁平化数组
// const array = [[0, 1], [2, 3], [4, 5]]
// const flatten = arr => {
//   return arr.reduce((a, b) => {
//     return a.concat(b)
//   }, [])
// }
// console.log(flatten(array)); // [0, 1, 2, 3, 4, 5]

实现compose

compose在函数式编程中是一个很重要的工具函数,在这里实现的compose有三点说明

  • 第一个函数是多元的(接受多个参数),后面的函数都是单元的(接受一个参数)
  • 执行顺序的自右向左的
  • 所有函数的执行都是同步的
function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg
    }

    if (funcs.length === 1) {
        return funcs[0]
    }
    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

实现Promise.all 和 Promise.race

  • Promise.all是解决并发问题的,多个异步并发获取最终的结果(如果有一个失败则失败),需注意当其中一个promise失败时,其余promise也会继续执行
  • Promise.race方法, Promise处理多个请求时,采用最快的(谁先完成用谁)
function promiseAll(promiseArr) {
    return new Promise((resolve, reject) => {
        if (!Array.isArray(promiseArr)) {
            return reject(new Error('传入参数需为数组'))
        }

        let res = [],
            nums = promiseArr.length,
            counter = 0  //添加计数器,避免数组长度判断错误

        for (let i = 0; i < nums; i++) {
            // 将promiseArr[i]均转化为promise
            Promise.resolve(promiseArr[i]).then(value => {
                counter++
                // 这里不能直接使用push防止乱序
                res[i] = value

                if (counter === nums) {
                    resolve(res)
                }
            }).catch(e => { reject(e) })
        }
    })
}

const pro1 = new Promise((res, rej) => {
    setTimeout(() => { res('1') }, 1000)
})

const pro2 = new Promise((res, rej) => {
    setTimeout(() => { res('2') }, 500)
})

const pro3 = new Promise((res, rej) => {
    setTimeout(() => { res('3') }, 3000)
})

const proAll = promiseAll([pro1, pro2, pro3])
    .then(res => {
        console.log(res)
    })

/*
* Promise.race方法, Promise处理多个请求时,采用最快的(谁先完成用谁)
*/
function race(promiseArr) {
    return new Promise((resolve, reject) => {
        for (let i = 0; i < promiseArr.length; i++) {
            let value = promiseArr[i]

            if (value && typeof value.then === 'function') {
                value.then(resolve, reject)
            } else {
                resolve(value)
                break
            }
        }
    })
}

const proRace = race([pro1, pro2, pro3])
    .then(res => {
        console.log(res)
    })

二分查找

二分查找也称折半查找(Binary Search),是一种效率较高的查找方法(对数时间复杂度)。

function search(arr, findValue) {
    let left = 0
    let right = arr.length - 1

    if (left > right) {
        return -1
    }

    const mid = parseInt((left + right) / 2)
    if (findValue > arr[mid]) {
        // 要找的值大于中间值,向右递归
        return search(arr, findValue, mid + 1)
    } else if (findValue < arr[mid]) {
        // 要找的值小于中间值,向左递归
        return search(arr, findValue, mid - 1)
    } else {
        return mid
    }
}

快速排序

快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼·霍尔提出。在平均状况下,排序n个项目要O(nLogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序O(nLogn)通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。

quickSort = Array => {
    if (Array.length <= 1) {
        return Array
    }
    let [pivot, ...rest] = Array

    let small = rest.filter(i => ~~(i) <= ~~(pivot))
    let big = rest.filter(i => ~~(i) > ~~(pivot))

    return [...quickSort(small), pivot, ...quickSort(big)]
}

// console.log(quickSort([3, 5, 2, 4]))

算法(easy)

身为前端平时工作中会用到算法的地方不多,但是学习算法确实会让我们思考问题的时候多一些维度及思路,目前我也只刷到leecode easy的题目,这边只做一个简单的记录,具体大家可以去 leecode 上查看更多解析。

1. 两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

// @lc code=start
function twoSum(nums, target) {
    const map = new Map()
    for(let i = 0, len = nums.length; i < len; i++){
        if(map.has(target - nums[i])){
            return [map.get(target - nums[i]), i];
        }else{
            map.set(nums[i], i);
        }
    }
}
// @lc code=end

7. 整数反转

给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。

如果反转后整数超过 32 位的有符号整数的范围 [−231, 231 − 1] ,就返回 0。

假设环境不允许存储 64 位整数(有符号或无符号)。

// @lc code=start
/**
 * @param {number} x
 * @return {number}
 */
var reverse = function (x) {
    let rev = 0;
    while (x !== 0) {
        const digit = x % 10;
        x = ~~(x / 10);
        rev = rev * 10 + digit;
        if (rev < Math.pow(-2, 31) || rev > Math.pow(2, 31) - 1) {
            return 0;
        }
    }
    return rev;

};
// @lc code=end

9. 回文数

给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。

回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。例如,121 是回文,而 123 不是。

这题与第七题有些类似,都是用取余的方式来反转整个数字。

// @lc code=start
/**
 * @param {number} x
 * @return {boolean}
 */
var isPalindrome = function (x) {
    if (x < 0 || (!(x % 10) && x)) return false;
    let x2 = x, res = 0;
    while (x2) {
        res = res * 10 + x2 % 10;
        x2 = ~~(x2 / 10);
    }
    return res === x;
};
// @lc code=end

13. 罗马数字转整数

罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。

字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000 例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。

通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:

I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。 X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。 C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。 给定一个罗马数字,将其转换成整数。输入确保在 1 到 3999 的范围内。

// @lc code=start
/**
 * @param {string} s
 * @return {number}
 */
var romanToInt = function(s) {
    const symbolValues = new Map();
    symbolValues.set('I', 1);
    symbolValues.set('V', 5);
    symbolValues.set('X', 10);
    symbolValues.set('L', 50);
    symbolValues.set('C', 100);
    symbolValues.set('D', 500);
    symbolValues.set('M', 1000);  
    let ans = 0;
    const n = s.length;
    for (let i = 0; i < n; ++i) {
        console.log(i)
        const value = symbolValues.get(s[i]);
        if (i < n - 1 && value < symbolValues.get(s[i + 1])) {
            ans -= value;
        } else {
            ans += value;
        }
    }
    console.log(ans)
    return ans;
};
// @lc code=end

14. 最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 ""

// @lc code=start
/**
 * @param {string[]} strs
 * @return {string}
 */
var longestCommonPrefix = function (strs) {
    if (!strs.length) {
        return ''
    }

    let ans = strs[0]
    for (let i = 1; i < strs.length; i++) {
        let j = 0
        const el = strs[i]
        for (j = 0; j < ans.length; j++) {
            if (ans[j] != el[j]) {
                break
            }
        }
        ans = ans.substr(0, j)
        if(!ans){
            return ''
        }
    }
    return ans
};
// @lc code=end

20. 有效的括号

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。

这里主要是对栈的使用。

// @lc code=start
/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function (s) {
    let len = s.length
    if (!len || len % 2 === 1) {
        return false
    }

    let pair = new Map([
        [')', '('],
        ['}', '{'],
        [']', '[']
    ])
    let stack = []

    for (const ch of s) {
        if (pair.has(ch)) {
            if (!stack.length || stack[stack.length - 1] !== pair.get(ch)) {
                return false
            }
            stack.pop()
        } else {
            stack.push(ch)
        }
    }

    return !stack.length

};
// @lc code=end

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

**注意:**给定 n 是一个正整数。

// @lc code=start
/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function (n) {
    let p = 0, q = 0, r = 1;
    for (let i = 1; i <= n; i++) {
        p = q;
        q = r;
        r = p + q;
    }
    return r;
};
// @lc code=end

88. 合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。

请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

/**
 * @param {number[]} nums1
 * @param {number} m
 * @param {number[]} nums2
 * @param {number} n
 * @return {void} Do not return anything, modify nums1 in-place instead.
 */

var merge = function(nums1, m, nums2, n) {
    nums1.splice(m, nums1.length - m, ...nums2);
    nums1.sort((a, b) => a - b);
};

94. 二叉树的中序遍历

给定一个二叉树的根节点 root ,返回它的 中序 遍历。

// @lc code=start
/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function (root) {
    let res = []
    const inorder = (root) => {
        if (!root) {
            return
        }
        inorder(root.left)
        res.push(root.val)
        inorder(root.right)
    }
    inorder(root)
    return res
};
// @lc code=end

121. 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

// @lc code=start
/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function (prices) {
    let max = 0;
    let minPrice = prices[0]
    for (let i = 1; i < prices.length; i++) {
        minPrice = Math.min(minPrice, prices[i])
        max = Math.max(max, prices[i] - minPrice)
    }
    return max
};
// @lc code=end