上班摸鱼时间,手撸前端算法(持续更新...)

137 阅读9分钟

摸鱼时间,手撸前端常见算法,持续更新...

感谢各位大佬的输出,参考文章详见:

1. 节流和防抖

1.1 什么是节流 / 防抖

防抖(debounce)是指 事件在被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

节流(throttle)是指 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

1.2 手撕代码

1.2.1 防抖
function debounce(fun, delay) {
    let timer;
    return function() {
        let that = this;
        // 如果这个函数已经触发了
        if(timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(function() {
            // 透传 this 和 参数
            fun.apply(that, arguments);
            timer = null;
        }, delay);
    }
}
1.2.1 节流
function throttle(fun, delay) {
    let timer;
    return function() {
        let that = this;
        if(timer) {
            return;
        }
        timer = setTimeout(function() {
            timer = null;
            fun.apply(that, arguments);
        }, delay)
    }
}

1.3 适用场景

防抖适用于:

  • 搜索输入框:用户在不断输入值时,用防抖来节约请求资源;
  • window 触发 resize 时,不断的调整浏览器窗口大小会不断的触发这个时间,用防抖使其只能触发一次。

节流适用于:

  • drag(拖动)事件或者 scroll(滚动)事件,使用节流,使其触发,但是不那么频繁。

2. 实现 Promise

2.1 实现 Promise 的解析与整体代码

Promise 的调用流程:

  • Promise 的构造方法接收一个 executor(),在 new Promise() 时就立刻执行这个 executor回调;
  • executor() 内部的异步任务被放入宏 / 微任务队列,等待执行;
  • then() 被执行,收集成功 / 失败回调,放入成功 / 失败队列;
  • executor() 的异步任务被执行,触发 resolve / reject,从成功 / 失败队列中取出回调依次执行。

Promise A+ 规范:

  • Promise 本质是一个状态机,且状态只能是以下三种:Pending(等待态)、Fulfilled(执行态)、Rejected(拒绝态);状态的变更是单向的,只能从 Pending --> Fulfilled 或者 Pending --> Rejected,状态的变更是不可逆的。
  • then 方法接收两个可选参数,分别对应状态改变时触发的回调函数。then 方法返回一个 promisethen 方法可以被同一个 promise 调用多次。
// Promise/A+ 规范的三种状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class MyPromise {
    constructor(executor) {
        this._status = PENDING;
        this._resolveQueue = []; // then 收集的执行成功的回调队列
        this._rejectQueue = [];
        
        let _resolve = (val) => {
            // 对应状态中的‘状态只能由pending到fulfilled或pending到rejected
            if(this._status !== PENDING) return;
            // 变更状态
            this._status = FULFILLED;
            while(this._resolveQueue.length) {
                // 从回调队列里取出回调依次执行
                const callback = this._resolveQueue.shift();
                callback(val);
            }
        }
        
        let _reject = (val) => {
            if(this._status !== PENDING) return;
            this._status = REJECTED;
            while(this._rejectQueue.length) {
                const callback = this._rejectQueue.shift();
                callback(val);
            }
        }
        // new Promise() 时立即执行executor,并传入resolve和reject
        executor(_resolve, _reject);
    }
    
    // then 方法,接收一个成功的回调和一个失败的回调,并push进对应的队列
    then(resolveFn, rejectFn) {
        this._resolveQueue.push(resolveFn);
        this._rejectQueue.push(rejectFn);
    }
}
  1. then 的链式调用:

  • .then() 需要返回一个 Promise,这样才能找到 then 方法,所以需要把 then 方法包装成 Promise;

  • .then() 的回调需要拿到上一个 .then() 的返回值;

  • .then() 的回调需要顺序执行。需要等待当前 Promise 状态变更后,再执行下一个 then 收集的回调,因此需要对 then 的返回值进行讨论。

  1. 细节处理

  • 根据规范,如果 then() 接收的参数不是 function,应该忽略。如果不忽略,会导致抛出异常,导致链式调用中断;

  • 处理状态为 resolve/reject 的情况:then() 的写法是对应状态为 pending 的情况。但是有时,resolve/rejectthen() 之前就被执行,这时如果还把 then() 回调 pushresolve/reject 队列中,回调将不会被执行。因此对于状态为 fulfilled/rejected 的情况,直接执行 then 回调;

  1. 兼容同步任务
  • Promise 的执行顺序:new Promise() --> then()收集回调 --> resolve/reject执行回调,是建立在 executor 是异步任务的基础上的,如果 executor 是同步任务,则执行顺序变成 new Promise() --> resolve/reject 执行回调 --> then() 收集回调
  • 为了兼容这种情况,使用 setTimeout 来包裹 resolve/reject 执行回调,使其异步执行。
then(resolveFn, rejectFn) {
    // 根据规范,如果 then 不是 function,则需要忽略,让链式调用继续往下执行
    typeof resolveFn !== 'function' ? resolveFn = value => value : null;
    typeof rejectFn !== 'function' ? rejectFn = reason => {
        throw new Error(reason instanceof Error ? reason.message: reason);
    } : null;
    
    // return 一个新的 promise
    return new MyPromise((resolve, reject) => {
        // 将 resolveFn 重新包装后,再 push 进 resolve 执行队列,对获取的回调返回值进行讨论
        const fulfilledFn = value => {
            try {
                let x = resolveFn(value);
                // 讨论返回值,如果是 Promise,等待 Promise 状态变更,否则直接 resolve
                // resolve 之后,就能被下一个 .then() 回调获取到返回值,从而实现链式调用
                x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
            } catch(error) {
                reject(error);
            }
        }
        // 将后续 then 收集的依赖都 push 进当前 Promise 的成功回调队列中,以保证顺序调用
        
        const rejectedFn = error => {
            try {
                let x = rejectFn(error);
                x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
            } catch(error) {
                reject(error);
            }
        }
        
        switch(this._status) {
            // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行
            case PENDING:
                this._resolveQueue.push(fulfilledFn);
                this._rejectQueue.push(rejectedFn);
                break;
            // 当状态已经变为resolve/reject时,直接执行then回调
            case FULFILLED:
                fulfilledFn(this._value);
                break;
            case REJECTED:
                rejectedFn(this._value);
                break;
        }
    })
}

完整版代码如下所示:

// Promise/A+ 规范的三种状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class MyPromise {
    constructor(executor) {
        this._status = PENDING;
        this._resolveQueue = []; // then 收集的执行成功的回调队列
        this._rejectQueue = [];
        
        let _resolve = (val) => {
            // 把 resolve执行回调的操作封装成一个函数,放进 setTimeout 里,用来兼容 executor 是同步代码的情况
            const run = () => {
                // 对应状态中的状态只能由 pending 到 fulfilled 或 pending 到rejected
                if(this._status !== PENDING) return;
                // 变更状态
                this._status = FULFILLED;
                while(this._resolveQueue.length) {
                    // 从回调队列里取出回调依次执行
                    const callback = this._resolveQueue.shift();
                    callback(val);
                }
            }
            setTimeout(run);
        }
        
        let _reject = (val) => {
            if(this._status !== PENDING) return;
            this._status = REJECTED;
            while(this._rejectQueue.length) {
                const callback = this._rejectQueue.shift();
                callback(val);
            }
        }
        // new Promise() 时立即执行 executor,并传入 resolve 和 reject
        executor(_resolve, _reject);
    }
    
    // then 方法,接收一个成功的回调和一个失败的回调,并push进对应的队列
    then(resolveFn, rejectFn) {
        // 根据规范,如果 then 不是 function,则需要忽略,让链式调用继续往下执行
        typeof resolveFn !== 'function' ? resolveFn = value => value : null;
        typeof rejectFn !== 'function' ? rejectFn = reason => {
            throw new Error(reason instanceof Error ? reason.message: reason);
        } : null;

        // return 一个新的 promise
        return new MyPromise((resolve, reject) => {
            // 将 resolveFn 重新包装后,再 push 进 resolve 执行队列,对获取的回调返回值进行讨论
            const fulfilledFn = value => {
                try {
                    let x = resolveFn(value);
                    // 讨论返回值,如果是 Promise,等待 Promise 状态变更,否则直接 resolve
                    // resolve 之后,就能被下一个 .then() 回调获取到返回值,从而实现链式调用
                    x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
                } catch(error) {
                    reject(error);
                }
            }
            // 将后续 then 收集的依赖都 push 进当前 Promise 的成功回调队列中,以保证顺序调用

            const rejectedFn = error => {
                try {
                    let x = rejectFn(error);
                    x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
                } catch(error) {
                    reject(error);
                }
            }

            switch(this._status) {
                // 当状态为 pending 时,把 then 回调 push 进 resolve/reject 执行队列,等待执行
                case PENDING:
                    this._resolveQueue.push(fulfilledFn);
                    this._rejectQueue.push(rejectedFn);
                    break;
                // 当状态已经变为 resolve/reject 时,直接执行 then 回调
                case FULFILLED:
                    fulfilledFn(this._value);
                    break;
                case REJECTED:
                    rejectedFn(this._value);
                    break;
            }
        })
    }
}

2.2 实现 Promise.all

Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolve)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因是第一个失败 promise 的结果。

static all(promiseAll) {
    let index = 0;
    let result = [];
    return new MyPromise((resolve, reject) => {
        // Promise.resolve(p) 用于处理传入值不为 Promise 的情况
        promiseArr.resolve(p).then(val => {
            index++;
            result[i] = val;
            // 所有 then 执行后,resolve 结果
            if(index === promiseAll.length) {
                resolve(result);
            }
        }, err => {
            //有一个 Promise 被 reject 时,MyPromise 的状态变为 reject
            reject(err);
        })
    })
}

2.3 实现 Promise.race

Promise.race(iterable)方法返回一个 promise,一旦迭代器中的某个 promise 被解决或者拒绝,返回的 promise 就会被解决或者拒绝。

static race(promiseArr) {
    return new MyPromise((resolve, reject) => {
        // 同时执行 promise,如果有一个 Promise 的状态变更,就更新 MyPromise 的状态
        for(let p of promiseArr) {
            MyPromise.resolve(p).then(val => {
                resolve(val); // resolve 的是上面 new MyPromise 的
            }, err => {
                reject(err);
            })
        }
    })
}

2.4 实现 Promise.resolve

static resolve(value) {
    if(value instanceof MyPromise) return value;
    return new MyPromise(resolve => resolve(value));
}

2.5 实现 Promise.reject

static reject(reason) {
    return new MyPromise((resolve, reject) => reject(reason));
}

3. 数组相关

3.1 实现 reduce

MDN:reduce()方法对数组中的每个元素按序执行一个由用户提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总成单个返回值。

参数:

  1. callbackFn:一个 reducer 函数,包含四个参数
  • previousValue:上一次调用 callbackFn 时的返回值。在第一次调用时,若指定了初始值 initialValue,其值为 initialValue,否则为数组索引为0的元素 array[0]
  • currentValue:数组中正在处理的元素。在第一次调用时,若指定了初始值 initialValue,其值则为数组索引为 0 的元素 array[0],否则为 array[1]
  • currentIndex:数组中正在处理的元素的索引。若指定了初始值 initialValue,则起始索引号为 0,否则从索引 1 起始;可选。
  • array:用于遍历的数组;可选。
  1. initialValue:可选。作为第一次调用 callback 函数时参数 previousValue 的值。若指定了初始值 initialValuecurrentValue 则使用数组的第一个元素;若未指定,则 previousValue 使用数组的第一个元素,currentValue 使用数组的第二个元素。
function myReduce(data, iteratee, memo, context) {
    // 重置 iteratee 函数的 this 指向
    iteratee = bind(iteratee, context);
    // 兼容对象,取出所有的 key 值
    let _keys = !Array.isArray && Object.keys(data); 
    // 长度赋值
    let len = (_key || data).length;
    
    // 判断是否传递了第三个参数
    let initial = arguments.length >= 3;
    // 初始的遍历下标
    let index = 0;
    
    // 若用户没有传入第三个参数,则取第一项作为默认值 
    if(!initial) {
        memo = data[_keys ? _keys[index] : index];
        // 遍历从第二项开始
        index += 1;
    }
    
    for(let i = 0;i < data.length; i++) {
        let currentKey = _keys ? _keys[i] : i;
        memo = iteratee(memo, data[currentKey], currentKey, data);
    }
    return memo;
}

// 绑定函数,使用 call 进行绑定
funtion bind(fn, context) {
    // 返回一个匿名函数,执行时重置 this 指向
    return function(memo, value, index, collection) {
        return fn.call(context, memo, value, index, collection);
    }
}

3.2 实现 Array.isArray

Array.IsArray = (arr) => {
    return Object.prototype.toString.call(arg) === '[Object Array]'
}

3.3 实现数组扁平化(拍平、降维)

3.3.1 ES6 的 flat()
const arr = [1, [1, 2], [1, 2, 3]];
arr.flat(); // [1, 1, 2, 1, 2, 3]
3.3.2 序列化后正则
const arr = [1, [1, 2], [1, 2, 3]];
const str = `[${JSON.stringfy(arr).replace(/(\[|\])/g, '')}]`
JSON.parse(str); // [1, 1, 2, 1, 2, 3]
3.3.3 递归
const arr = [1, [1, 2], [1, 2, 3]];
function flat(arr) {
    let result = [];
    for(const item of arr) {
        item instanceof Array ? result = result.concat(flat(item)) : result.push(item);
    }
    return result;
}
flat(arr); // [1, 1, 2, 1, 2, 3]
3.3.4 reduce() 递归
const arr = [1, [1, 2], [1, 2, 3]];
function flat(arr) {
    return arr.reduce((preValue, curValue) => {
        return preValue.concat(curValue instanceof Array ? flat(curValue) : curValue)
    }, []);
}
flat(arr); // [1, 1, 2, 1, 2, 3]
3.3.5 迭代 + 展开运算符
const arr = [1, [1, 2], [1, 2, 3, [4, 4, 4]]];
while(arr.some(Array.isArray)) {
    arr = [].concat(...arr);
}
flat(arr); // [1, 1, 2, 1, 2, 3]

3.4 实现数组去重

3.4.1 ES6 的 Set 去重

new Set 是 ES6 新推出的一种类型。和数组的区别是,Set 类型的数据中不能有重复的值。

适用于:数组中都是值类型的数据。

缺点:无法去重引用类型数据。

const arr = [1, 1, 2, 3, 3, 4, 5, 5];
const setData = Array.from(new Set(arr)); // [...new Set(arr)]
console.log(setData); // [1, 2, 3, 4, 5];
3.4.2 双重 for 循环 + splice
function unique(arr) {
    for(lei i = 0, len = arr.length; i < len; i++) {
        for(let j = i + 1; j < len; j++) {
            if(arr[i] === arr[j]) {
                arr.splice(j, 1);
                j--;
                len--;
            }
        }
    }
    return arr;
}
3.4.3 使用 includes 或 indexof + 新数组
function unique(arr) {
    let uniqueArr = [];
    for(let i = 0, len = arr.length; i < len; i++) {
        if(uniqueArr.indexof(arr[i]) === -1) { // !uniqueArr.includes(arr[i])
            uniqueArr.push(arr[i]);
        }
    }
    return uniqueArr;
}
3.4.4 使用哈希表存储元素(ES6 map)
function unique(arr) {
    let map = new Map();
    let uniqueArr = new Array();
    for(let i = 0, len = arr.length; i < len; i++) {
        if(map.has(arr[i])) { // 如果有该 key 值
            map.set(arr[i], true);
        } else {
            map.set(arr[i], false);
            uniqueArr.push(arr[i]);
        }
    }
    return uniqueArr;
}
3.4.5 filter
function unique(arr) {
    return arr.filter(function (item, index, arr) { 
        //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
        //不是那么就证明是重复项,就舍弃
        return arr.indexOf(item) === index;
    })
}
3.4.6 reduce
function unique(arr){
    let uniqueArr = arr.reduce((acc,cur) => {
        if(!acc.includes(cur)){
            acc.push(cur);
        }
        return acc;
    },[]) 
    return uniqueArr;
}

4. 实现 instanceOf

5. 实现 call、apply、bind

5.1 实现 call