【JavaScript】面试高频代码原理与实现,防抖节流、深拷贝、继承 、Promise....

405 阅读14分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

输出内容才能更好的理解输入的知识

前言🎀

手写代码是对过往知识的总结,实现的过程即是对知识的复习 绝不是为了面试什么的🙈

希望结合自身的理解能帮到更多的同学

如果觉得有收获还望大家点个赞🌹

题目📖

1. 防抖函数 debounce

规定时间后执行回调,多次调用重新计时,只触发最后一次。一般用 定时器 + 闭包 实现
场景:用户搜索联想,防止点击按钮重复提交,表单校验的触发时机

function debounce(fn, delay = 300) {
    let timer = null
    return function(...args) {
        clearTimeOut(timer)
        timer = setTimeOut(() => {
            fn.apply(null, [...args])
        }, delay)
    }
}

2. 节流函数 throttle

持续触发的事件,间隔时间只能生效一次,其余调用无效。通常用 定时器 + 闭包 实现
场景:滚动窗口,搜索联想,resize窗口

function throttle(fn, delay = 300) {
    let flag = true
    return function(...args) {
        if (flag) {
            flag = false
            fn.apply(null, [...args])
            setTimeOut(() => {
                flag = true
            }, delay)
        }
    }
}

3. 深拷贝 deepClone

引用类型对象保存的是指向数据地址的指针。
如果使用 =赋值 浅拷贝的对象在修改时会影响到原数据
如果使用 JSON序列化+反序列化 会丢失 方法属性/循环引用/Date、RegExp等特殊对象
使用 递归 + 条件判断 实现

// 基础版
function deepClone(obj) {
    if (typeof obj !== 'object') return obj
    const result = Array.isArray(obj) ? [] : {}
    for (let [key,value] of Object.entries(obj)) {
        if (typeof value !== 'object') {
            result[key] = value
        } else {
            result[key] = deepClone(value)
        }
    }
    return result
}
// 加强版 判断循环引用 null
function deepClone(obj, map = new WeakMap()) {
    if (typeof obj !== 'object' || obj === null) return obj
    if (map.has(obj)) return map.get(obj)
    const result = Array.isArray(obj) ? [] : {}
    map.set(obj, result)
    for (let [key,value] of Object.entries(obj)) {
        if (typeof value !== 'object' || value === null) {
            result[key] = value
        } else {
            result[key] = deepClone(value)
        }
    }
    return result
}

4. new 操作符

new通过构造函数创建了一个实例对象,需要理解的是创建对象时做了哪些操作
1.基于原型新创建一个对象
2.构造函数的this绑定新建的对象和构建参数
3.如果fn有执行结果 且为对象类型返回fn的结果,否则返回新创建的对象

function myNew(fn, ...args) {
    let obj = Object.create(fn.prototype)
    const res = fn.apply(obj, [...args])
    return res && typeof res === 'object' ? res : obj
}
// 测试
function Person(name, age) {
    this.name = name
    this.age = age
}
Person.prototype.sayHi = function() { console.log('Hi I am ' + this.name) }

let p1 = new Person('西维', 18)
p1.sayHi()
let p2 = myNew(Person, '青羽', 24)
p2.sayHi()

5. 继承 class

ES6的class其实是 寄生式继承 + 组合式继承 的语法糖
过程中抓住一个重点——JS中继承的核心思想是对原型、属性和方法的拷贝
理解 寄生组合式继承 即可扩散到 寄生式继承 和 组合式继承

原型式继承:利用空构造函数继承目标原型,缺点 被其他实例影响原型 无法传递参数
等同于Object.create()
寄生式继承:在 原型式继承 的基础上增强对象,缺点 同上

// 原型式
function object(obj) {
    function F() {}
    F.prototype = obj
    return new F()
}
// 寄生式
function objectEx(obj) {
    let clone = Object.create(obj)
    // 增强对象
    clone.sayHi = () => console.log('Hi I'm ' + clone.name)
    return clone
}

组合式继承:借用构造函数 组合原型+属性&方法,缺点 实例对象和原型中存在相同的属性和方法

// 父类构造函数
function SuperType(name){ 
    // some code
}
// 第一次调用 SuperType() 继承原型
SubType.prototype = new SuperType(); 
SubType.prototype.constructor = SubType;

function SubType(name, age){ 
    // 第二次调用SuperType() 继承属性
    SuperType.call(this, name); 
    this.age = age; 
}
var instance1 = new SubType("Nicholas", 29);

寄生组合式继承:顾名思义 整体思路是借用构造参数+寄生式继承,分别实现拷贝属性、方法+原型

function Parent(name) { //... }
Parent.prototype.sayHi = function() { //... }
......
// 继承原型
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
// 继承属性和方法
function Child(name, age) {
    Parent.call(this, name)
    this.age = age
}

6. 函数柯里化 currying

接收多个参数的函数 转换为 接收单一参数并多次执行的函数
主要作用:收集状态,延迟执行,参数复用
核心思想:利用闭包记录状态,拆分接收多参数的原函数 在函数内部返回调用下一个单参数的函数

例:sum(1)(2)(3)() = 6 支持多个数据 如sum(1)(2,3)() = 6
第一次容易写卡在判断返回函数还是返回结果,漏掉第一份数据的初始化(就是我🙈)
还有种重写toString的,感兴趣可以自己了解

function sum() {
    const nums = [...arguments]
    return function add() {
        if (arguments.length) {
            nums.push(...arguments)
            return add
        } else {
            return nums.reduce((pre, cur) => pre + cur)
        }
    }
}

可总结出模板:

function currying(fn, ...args) {
  const length = fn.length
  let allArgs = [...args]
  const res = (...newArgs) => {
    allArgs = [...allArgs, ...newArgs];
    // 判断 函数的参数数量 和 实际接收的参数数量
    // 相等 执行原函数 否则 返回函数接收剩余参数
    if (allArgs.length === length) {
      return fn(...allArgs);
    } else {
      return res;
    }
  }
  return res;
}

const add = (a, b, c) => a + b + c
const a = currying(add, 1);
console.log(a(2,3))

// currying可以再接收一个参数len代表原函数需要的参数数量,具体了解 函数.length 会返回多少

7. 作用域绑定 call/apply/bind

call、apply
作用:替换目标函数的作用域并执行函数(修改this指向 并传入参数)
区别:call接收多个参数项,apply接收一个参数数组
实现:主要实现方法为将函数赋值给对象的内部属性,利用函数的作用域是在声明时创建的特性

Function.prototype.myCall = function(context, ...args) {
    if (typeof this !== 'function') return new Error('type error')  // 确保this是函数
    context = context || window  // 默认指向window
    
    const sym = Symbol()  // 生成唯一值
    context[sym] = this   // this指向调用call的函数
    const res = context[sym](...args)  // 传入参数并执行
    delete context[sym]
    
    return res
}
Function.prototype.myApply(context, args) {
    // ....同上
    
    // ...
    const res = context[sym](...args)  // 传入参数并执行
    // ...
    
    // ....同上
}

bind
作用:替换目标函数的作用域并返回函数(修改this指向 并传入参数)

Function.prototype.myBind(context, ...args) {
    if (typeof this !== 'function') return new Error('type error')
    if (context === null || context === undefined) context = window
    else context = Object(context)  // 确保context是对象
    
    const self = this
    return function Fn(...newArgs) {
        // 如果当前函数执行中的this是Fn的实例,说明是new执行的,那么当前this就是函数的实例,否则是context
        if (this instanceof Fn) {
            return new selft(...args, ...newArgs)
        } else {
            return self.apply(context, [...args, ...newArgs])
        }
    }
}

8. 期约对象 Promise

Promise 常见的是对.all/.race原理的考查和对同步异步的理解

理清思路,先简单总结下Promise的特性:

  1. Promise 译为 期约,它是一个容器 里面装着某个未来才会结束的事件的结果,从它可以获取异步操作的消息
  2. Promise 有三种状态 pending, fulfilled, rejected,分别代表等待,执行,拒绝
    初始状态为pending,状态一经改变无法回退
  3. Promise 构造函数接收一个函数A作为参数 函数A的两个参数为函数resolve和reject 分别代表Promise进行 执行、拒绝 操作
    Promise 创建时调用了构造参数 传入的函数被立即执行
  4. Promise .then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行
  5. Promise 的调用流程其实是 观察者模式
    then收集依赖 -> resolve/reject变更触发通知 -> 取出对应依赖执行

暂不详述,更多知识建议看大佬写的关于Promise的文章

//promise有3种状态 (pending/Fulfilled/rejected) 默认pending 状态一旦变化不可再逆转
class Promise {
    constructor(executor) { //构造器 接收一个回调函数
        this.state = 'pending' //默认状态
        this.value = undefined; //Fulfilled成功时的值
        this.reason = undefined; //Rejected失败时的原因

        //使用队列是因为要满足then方法可以被一个promise调用多次 xx.then().then()
        this.resolveQueue = [] // then收集的执行成功的回调队列
        this.rejectQueue = [] // then收集的执行失败的回调队列


        //promise对象内可使用resolve  Fulfilled成功promise对象 并返回一个promise对象
        let resolve = value => {
            if (this.state === 'pending') {
                this.state = 'fulfilled'
                this.value = value
                //成功时调用成功回调队列
                while (this.resolveQueue.length) {
                    const callback = this.resolveQueue.shift()
                    callback()
                }
            }
        }
        //也可使用reject  Rejected成功promise对象 并返回一个promise对象
        let reject = reason => {
            if (this.state === 'pending') {
                this.state = 'rejected'
                this.reason = reason
                while (this.rejectQueue.length) {
                    const callback = this.rejectQueue.shift()
                    callback(reason)
                }
            }
        }

        //如果 执行器函数 执行报错,直接reject
        try {
            executor(resolve, reject)
        } catch (error) {
            reject(error)
        }
    }
    //then方法可以链式调用并需要拿到上一个then返回的值 所以then方法返回一个Promise
    //then方法顺序执行  then方法接收一个成功回调和一个失败回调
    then(onFulfilled, onRejected) {
        //静默处理非函数参数
        typeof onFulfilled !== 'function' ? onFulfilled = value => value : null
        typeof onRejected !== 'function' ? onRejected = reason => {
            throw new Error(reason instanceof Error ? reason.message : reason)
        } : null

        return new Promise((resolve, reject) => {
            //包装 成功回调函数和失败回调函数 再push进resolve队列 是为了能够获取回调的返回值进行分类操作
            const fulfilledFn = value => {
                try {
                    //执行第一个Promise的成功回调,并获取返回值
                    let x = onFulfilled(value)
                    //分类返回值 如果是Promise 等待Promise状态变更 否则直接resolve
                    //假设then方法返回Promise(A) 如果then方法的回调函数的返回值是普通值直接resolve(x)返回即可 
                    //但返回值也可能是个Promise(B) 实际应使用Promise(B)返回的值 
                    //因此需要传值 即Promise(B).state ===> Promise(A) 
                    //所以用then去收集他的状态改变,再把Promise(A)的解决函数(resolve/reject)传给它 
                    //这样在Promise(B)解决(resolve)时Promise(B)就解决了 同时它的值就传回给Promise(A)
                    x instanceof Promise ? x.then(resolve, reject) : resolve(x)
                } catch (error) {
                    reject(error)
                }
            }
            //失败回调包装同理
            const rejectedFn = error => {
                try {
                    let x = onRejected(error)
                    x instanceof Promise ? x.then(resolve, reject) : resolve(x)
                } catch (error) {
                    reject(error)
                }
            }


            switch (this.state) {
                // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行
                case 'pending':
                    this.resolveQueue.push(onFulfilled)
                    this.rejectQueue.push(onRejected)
                    break;
                    // 当状态已经变为resolve/reject时,直接执行then回调
                case 'fulfilled':
                    fulfilledFn(this.value) // this._value是上一个then回调return的值(见完整版代码)
                    break;
                case 'rejected':
                    rejectedFn(this.reason)
                    break;
            }
        })
    }

    //finally方法
    //在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数  还可以继续then
    finally(callback) {
        return this.then(
            value => MyPromise.resolve(callback()).then(() => value), //执行回调,并returnvalue传递给后面的then
            reason => MyPromise.resolve(callback()).then(() => {
                throw reason
            }) //reject同理
        )
    }

    //静态的resolve方法
    //返回一个以给定值解析后的Promise 对象
    static resolve(value) {
        if (value instanceof MyPromise) return value //根据规范, 如果参数是Promise实例, 直接return这个实例
        // 如果是 thenable 
        if (val && val.then && typeof val.then === 'function') {
            const promise = new MyPromise((resolve, reject) => {
                try {
                    val.then(resolve, reject)
                } catch (error) {
                    reject(error)
                }
            })
            return promise
        }
        return new MyPromise(resolve => resolve(value))
    }

    //静态的reject方法
    //返回一个带有拒绝原因的Promise对象
    static reject(reason) {
        return new MyPromise((resolve, reject) => reject(reason))
    }

    //静态的all方法 返回一个 Promise 实例
    //iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve)
    //如果参数中  promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。
    static all(promiseArr) {
        let index = 0
        let result = []
        return new MyPromise((resolve, reject) => {
            promiseArr.forEach((p, i) => {
                //Promise.resolve(p)用于处理传入值不为Promise的情况
                MyPromise.resolve(p).then(
                    val => {
                        index++
                        result[i] = val
                        if (index === promiseArr.length) {
                            resolve(result)
                        }
                    },
                    err => {
                        reject(err)
                    }
                )
            })
        })
    }

    //静态的race方法 返回一个 promise
    //一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝
    static race(promiseArr) {
        return new MyPromise((resolve, reject) => {
            //同时执行Promise,如果有一个Promise的状态发生改变,就变更新MyPromise的状态
            for (let p of promiseArr) {
                MyPromise.resolve(p).then(
                    //Promise.resolve(p)用于处理传入值不为Promise的情况
                    value => {
                        resolve(value)
                        //注意这个resolve是上边new MyPromise的
                    },
                    err => {
                        reject(err)
                    }
                )
            }
        })
    }
}

无注释版

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  constructor(executor) {
    this.status = PENDING
    this.value = undefined
    this.fulfilledQueue = []
    this.rejectedQueue = []
    let resolve = value => {
      const run = () => {
      if (this.status !== PENDING) return
      this.status = FULFILLED
      this.value = value
      while (this.fulfilledQueue.length) {
        const callback = this.fulfilledQueue.shift()
        callback()
      }
    }
    setTimeout(run)
    }

    let reject = reason => {
      //same as resolve
    }

    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }

  then(resolveFn, rejectFn) {
    if (typeof resolveFn !== 'function')  resolveFn = value => value
    if (typeof rejectFn !== 'function')  rejectFn = error => {
      throw new Error( error instanceof Error ? error.message : error )
    }
    return new MyPromise((resolve, reject) => {
    const fulfilledFn = value => {
      let x = resolveFn(value)
      x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
    }
    const rejectedFn = reason => {
      //same as fulfilledFn
    }
    switch (this.status) {
      case PENDING:
        this.fulfilledQueue.push(fulfilledFn)
        this.rejectedQueue.push(rejectedFn)
        break;
      case FULFILLED:
        this.rejectedQueue.push(fulfilledFn)  
        break;
      case REJECTED:
        this.rejectedQueue.push(rejectedFn)  
        break;
    }
  })
  }

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

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

  catch(rejectFn) {
    return this.then(undefined, rejectFn)
  }

  finally(fn) {
    return this.then(
      value => MyPromise.resolve(fn()).then(() => value),
      reason => MyPromise.resolve(fn()).then(() => { throw reason })
    )
  }

  static all(promiseArr) {
    let count = 0
    let result = []
    return new MyPromise((resolve, reject) => {
      promiseArr.forEach((item, idx) => {
        MyPromise.resolve(item).then(value => {
          count += 1
          result[idx] = value
          if (count === promiseArr.length) resolve(result)
        }).catch(reject)
      })
    })
  }

  static race(promiseArr) {
    return new MyPromise((resolve, reject) => {
      promiseArr.forEach((item, idx) => {
        MyPromise.resolve(item).then(value => resolve(value)).catch(reject)
      })
    })
  }
}

9. 数组扁平化 flat

将多维数组转为一维数组,实现方式为 递归 + 判断
还有更好的ES6 数组API reduce 在数组降维和去重中都有不错的表现

function flat(arr) {
    if (!arr.length) return
    const res = []
    for (let item of arr) {
        Array.isArray(item) ? res.push(...flat(item)) : res.push(item)
    }
    return res
}
// reduce
function flat(arr) {
    if (!arr.length) return
    return arr.reduce((pre, cur) => {
        return pre.concat(cur.constructor === Array ? flat(cur) : cur)
    }, [])
}
console.log(flat([1,[2,3],[[4,5]]))

10. 检查原型 instanceof

每个对象都有__proto__属性指向它的原型对象(prototype)
而原型对象也是对象也有__proto__属性指向它的原型对象(prototype),这样形成一条原型链

而 instanceof 的原理是 检查左对象的原型链上是否有右对象的原型,实现方式为 循环 + 判断

function _instanceof(leftObj, rightObj) {
    let rightProto = rightObj.prototype
    let leftProto = leftObj.__proto__
    // 迭代左对象原型链上的所有原型对象
    while (true) {
        if (leftProto === null) return false
        if (leftProto === rightProto) return true
        leftProto = leftProto.__proto__
    }
}

11. 数组去重

需要判断元素是否重复,实现方式有多种 set、reduce、map、filter...

// set
let uniqueArr = arr => [...new Set(arr)]
// reduce
function uniqueArr(arr) {
    return arr.reduce((pre, cur) => {
        pre.includes(cur) && pre.push(cur)
        return pre
    }, [])
}
// map
function uniqueArr(arr) {
    let map = new Map()
    let res = []
    for (let item of arr) {
        if (map.has(item)) continue
        map.set(item, 1)
        res.push(item)
    }
    return res
}
// filter
let uniqueArr = arr => arr.filter((item, index) => arr.indexOf(item) === index)

12. 发布订阅模式 EventEmeitter

发布订阅模式是node事件驱动的核心,也是Vue框架响应式更新的重点
它属于设计模式中三大类型之一的行为型模式
行为型模式关注对象之间的交互,研究运行时对象之间相互通信和协作,进一步明确对象职责

与 观察者模式 的区别
观察者模式 定义了对象之间的一对多的依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并更新 —— Head First设计模式

impicture_20220725_203042.png 发布订阅模式 则是在 订阅者Watcher、观察者Observer 两者之间增加了事件中心Dep
Watcher与Observer之间没有直接的关系。发布者无需关注订阅者,当对象改变时,会直接通知到Dep

class EventEmeitter {
    constructor() {
        // 准备一个数据结构缓存订阅者信息
        this._events = Object.create(null)
    }
    // 添加订阅方法
    on(type, callback) {
        // 判断当前事件是否已存在,决定如何缓存
        if (this._events[type]) {
            this._events[type].push(callback)
        } else {
            this._events[type] = [callback]
        }
    }
    // 移除订阅方法
    off(type, callback) {
        if (!this._events[type]) return
        this._events[type] = this._events[type].filter(item => item !== callback)
    }
    // 触发订阅更新
    emit(type, ...args) {
        if (this._events[type]?.length) {
            // 获取对应type缓存的回调函数执行
            this._events[type].forEach(fn => fn.call(this, ...args))
        }
    }
    // 只触发一次的订阅事件
    once(type, callback) {
        function fn() {
            callback()
            this.off(type, fn)
        }
        this.on(type, fn)
    }
}

let event = new EventEmeitter()
let outParam = (...args) => console.log(...args)
event.on('事件1', outParam)
event.on('事件1', () => console.log('事件1-2'))
event.emit('事件1', { name: '西维', age: '18' })

event.off('事件1', outParam)
event.emit('事件1', { name: '青羽', age: '24' })

event.once('事件2', () => console.log('delete after emit'))
event.emit('事件2')
event.emit('事件2')

13. 睡眠函数 sleep

阻塞代码执行流程指定时间,原理是用 async/await + 定时器 + 期约 在某个时间过后恢复执行流程

function sleep(delay = 100) {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(), delay)
    })
}
console.log('start,' + Date.now())
await sleep(3000)
console.log('end,' + Date.now())

14. Array 与 Tree 互相转换

偏算法,但确实是实际开发中经常遇到的问题,有很多种实现方式

数组转树
采用 map结构,迭代数组所有元素item

  • 判断item是否有pid(=0) 且 map中是否存在key为pid的元素
  • 没有pid(=0)则将其放到结果数组中
const arr = [
    {id: 1, pid: 0, content: 'xx1'},
    {id: 2, pid: 1, content: 'xx1-xx2'},
    {id: 3, pid: 1, content: 'xx1-xx3'},
    {id: 4, pid: 2, content: 'xx1-xx2-xx4'},
    {id: 5, pid: 2, content: 'xx1-xx2-xx5'},
    {id: 6, pid: 4, content: 'xx1-xx2-xx4-xx6'},
    {id: 7, pid: 0, content: 'xx7'},
    {id: 8, pid: 7, content: 'xx7-xx8'},
    {id: 9, pid: 0, content: 'xx9'},
]
function arrToTree(arr) {
    let map = new Map()
    let res = []
    for (let item of arr) {
        let param = { children: [], ...item }
        map.set(item.id, param)
        if (item.pid && map.has(item.pid)) {
            map.get(item.pid).children.push(param)
        } else if (!item.pid) {
            res.push(param)
        }
    }
    return res
}
arrToTree(arr)

树转数组
采用 bfs/dfs 都行,具体原理偏算法 不懂的同学可以去了解下 或者等我有空了写篇算法文章🙈

function treeToArr(tree) {
    let res = []
    let stack = []
    for (let item of tree) {
        stack.push(item)
    }
    while (stack.length) {
        let node = stack.pop()
        res.push(node)
        if (node.children?.length) stack.push(...node.children)
    }
    return res
}
treeToArr(arrToTree(arr))

15. 块级作用域 let、const

块级作用域的变量只在当前作用域及子作用域可以被访问, 通过 立即执行函数 模拟实现

// let
(function() {
    var a = 1
    consoloe.log(a)
})()
// const
var f = Object.freeze({'name':'admin'});

结语🎉

不要光看不实践哦,后续会持续更新前端和算法相关的知识
写作不易,如果觉得有收获欢迎大家点个赞谢谢🌹
顺便给自己写的动态规划引流一下😆感兴趣的同学看看

才疏学浅,如果文章有什么问题欢迎大家指教