前端面试常见手写代码题

2,765 阅读11分钟

内推

字节飞书内推地址,欢迎大家投递

杭州:job.toutiao.com/s/iNcUyd1p
北京:job.toutiao.com/s/iNcUqwQw

引言

对于前端面试而言,手撕代码一定是一道面试必考的题目,通常面试给出的手写代码题目大体分为两种, 一种是根据前端基础知识引申出的各种手写代码题目,比如手写Promise、手写防抖节流,深浅拷贝等,另外一种就是纯算法题,本篇文章将主要介绍第一种形式的手写代码题,对面试中常见的手写代码题目进行归纳和总结。

本文将持续更新,敬请期待~

常见手写代码题目总结

Promise相关

手写Promise

以下代码已通过Promise A+规范测试

请结合Promise A+规范一起看

class Promise{
    constructor(executor){
        this.state = 'pending' // 状态
        this.value = null // 成功的终值
        this.reason = null // 失败的拒因
        this.onFulfilledCallbacks = []
        this.onRejectedCallbacks = []
        const resolve = (value) =>{
            this.state = 'fulfilled'
            this.value = value
            this.onFulfilledCallbacks.forEach(fn=>fn())
        }
        const reject = (reason) =>{
            this.state = 'rejected';
            this.reason = reason
            this.onRejectedCallbacks.forEach(fn=>fn())
        }
        try{
            executor(resolve,reject)
        }catch(e){
            reject(e)
        }
    }
    then(onFulfilled,onRejected){
        // 判断是否是函数
        if(typeof onFulfilled !== 'function'){
            onFulfilled = function(value){
                return value
            }
        }
        if(typeof onRejected !== 'function'){
            onRejected = function(reason){
                throw reason
            }
        }
        let promise2 = new Promise((resolve,reject)=>{
            if(this.state === 'fulfilled'){
                setTimeout(()=>{
                    try{
                        // promise的值作为onFulfilled的参数
                        let x = onFulfilled(this.value)
                        resolvePromsie(promise2,x,resolve,reject)
                    }catch(e){
                        reject(e)
                    }
                },0)
            }
            if(this.state === 'rejected'){
                setTimeout(()=>{
                    try{
                        // promise的值作为onFulfilled的参数
                        let x = onRejected(this.reason)
                        resolvePromsie(promise2,x,resolve,reject)
                    }catch(e){
                        reject(e)
                    }
                },0)
            }
            if(this.state === 'pending'){
                this.onFulfilledCallbacks.push(()=>{
                    setTimeout(()=>{
                        try{
                            // promise的值作为onFulfilled的参数
                            let x = onFulfilled(this.value)
                            resolvePromsie(promise2,x,resolve,reject)
                        }catch(e){
                            reject(e)
                        }
                    },0)
                })
                this.onRejectedCallbacks.push(()=>{
                    setTimeout(()=>{
                        try{
                            // promise的值作为onFulfilled的参数
                            let x = onRejected(this.reason)
                            resolvePromsie(promise2,x,resolve,reject)
                        }catch(e){
                            reject(e)
                        }
                    },0)
                })
            }
        })
        return promise2
    }
}
function resolvePromsie(promise2,x,resolve,reject){
    if(x === promise2){
        return reject(new TypeError('chaining cycle'))
    }
    let called;
    if(x !== null && (typeof x === 'object' || typeof x === 'function')){
        try{
            let then = x.then
            if(typeof then === 'function'){
                then.call(x,value=>{
                    if(called)return
                    called = true
                    resolvePromsie(promise2,value,resolve,reject)
                },err=>{
                    if(called)return
                    called = true
                    reject(err)
                })
            }else{
                resolve(x)
            }
        }catch(e){
            if(called)return
            called = true
            reject(e)
        }
    }else{
        resolve(x)
    }
}

Promise.resolve

Promise.resolve = function(val){
    return new Promise((resolve,reject)=>{
        resolve(val)
    })
}

Promise.reject

Promise.reject = function(val){
    return new Promise((resolve,reject)=>{
        reject(val)
    })
}

Promise.race

Promise.race = function(promises){
    return new Promise((resolve,reject)=>{
        for(let i=0;i<promises.length;i++){
            promises[i].then(resolve,reject)
        }
    })
}

Promise.all

Promise.all = function(promiseArray){
    return new Promise((resolve,reject)=>{
        // 参数类型的判断
        if(!Array.isArray(promiseArray)){
            return reject(new TypeError('arguments must be array'))
        }
        const promiseNum = promiseArray.length
        const res = []
        let counter = 0
        for(let i=0;i<promiseNum;i++){
            // 注意数组元素类型
            Promise.resolve(promiseArray[i]).then(value=>{
                count++
                // 不能用push,会导致顺序不一致,因为push是每次加在最后面
                // res.push(value)
                res[i] = value
                // 用counter计数 不能用res.length 判断 因为如果[1,2,3] 3很快执行 那么会导致res[2]=3
                // 此时哪怕1 2 没执行 res的长度也已经是3了
                if(counter === promiseNum){
                    resolve(res)
                }
            },err=>{
                reject(error)
            }).catch(e=>{
                reject(e)
            })
        }
    })
}

new的实现

function objectFactory(){
    // 创建一个新对象
    let obj = new Object()
    let Constructor = [].shift.call(arguments)
    // 对象__proto__属性指向构造函数原型
    obj.__proto__ = Constructor.prototype
    // 绑定this,考虑存在返回值的情况
    let result = Constructor.apply(obj,arguments)
    // 判断:如果返回值是一个对象 那实例只能访问对象的属性
    return typeof result === 'object' ? result : obj
}

apply call bind的实现

call的模拟实现

Function.prototype.call = function (context) {
    var context = context || window
    context.fn = this
    var args = []
    for(var i=1;i<arguments.length;i++){
        args.push('argument['+i+']')
    }
    var result  = eval('context.fn('+args+')')
    delete context.fn
    return result
}

// es6 实现
Function.prototype.call = function(context,...args){
    let ctx = context
    ctx.fn = this
    let result = ctx.fn(...args)
    delete ctx.fn
    return result
}

apply的模拟实现

Function.prototype.apply = function (context, arr) {
    var context = context || window;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
        // es6实现
        // let result =  context.fn(...arr)
    }

    delete context.fn
    return result;
}

bind的模拟实现

Function.prototype.bind = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    //bind时传参
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        // bind返回函数执行时也可以传参
        var bindArgs = Array.prototype.slice.call(arguments);
        // bind返回函数作为构造函数时 this失效,但传入参数有效
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

柯里化函数

function curry(fn,args){
    var length = fn.length
    args = args || []
    return function(){
        var _args = args.slice(0);
        for(var i=0;i<arguments.length;i++){
            _args.push(arguments[i])
        }
        if(_arg.length < length){
            return curry.call(this,fn,_args)
        }else{
            return fn.apply(this,_args)
        }
    }
}

ES6

function curry(fn) {
    let judge = (...args) => {
        if (args.length == fn.length) return fn(...args)
        return (...arg) => judge(...args, ...arg)
    }
    return judge
}

实现一个compose函数

function fn1(x){
    return x+1
}
function fn2(x){
    return x+2
}
function fn3(x){
    return x+3
}
function fn4(x){
    return x+4
}

const a = compose(fn1,fn2,fn3,fn4)
console.log(a(1))

function compose(){
    const argFnList = [...arguments]
    return (num)=>{
        return argFnList.reduce((prev,next)=>{
            return next(prev)
        },num)
    }
}

实现一个基于洋葱模型的compose

// 洋葱模型
function compose(middlewares){
    const copyMiddlewares = [...middlewares]
    let index = 0
    const fn = ()=>{
        if(index > copyMiddlewares.length){
            return
        }
        const middleware = copyMiddlewares[index]
        index++
        return middleware(fn)
    }
    return fn
}

实现一个异步任务调度并发控制器


const urls = [
    {
        info:'link1',
        time:3000,
        priority:1
    },
    {
        info:'link2',
        time:2000,
        priority:1
    },
    {
        info:'link3',
        time:5000,
        priority:2
    },
    {
        info:'link4',
        time:1000,
        priority:1
    },
    {
        info:'link5',
        time:1200,
        priority:1
    },
    {
        info:'link6',
        time:2000,
        priority:5
    },
    {
        info:'link7',
        time:800,
        priority:1
    },
    {
        info:'link8',
        time:3000,
        priority:1
    },
]

class PromiseQueue{
    constructor(options = {}){
        this.concurrency = options.concurrency || 1
        this.currentCount = 0;
        this.pendingList = [];
    }
    add(task){
        this.pendingList.push(task)
        this.run()
    }
    run(){
        if(this.pendingList.length === 0) return
        if(this.currentCount === this.concurrency) return
        this.currentCount++
        // 优先级排序
        const {fn} = this.pendingList.sort((a,b)=> b.priority - a.priority).shift()
        const promise = fn()
        promise.then(this.completeOne.bind(this)).catch(this.completeOne.bind(this))
    }
    completeOne(){
        this.currentCount--
        this.run()
    }
}
const q = new PromiseQueue({
    concurrency:3 // 最大并发数量
})

const formatTask = (url)=>{
    return{
        fn:()=>loadImg(url),
        priority:url.priority
    }
}

urls.forEach(url=>{
    q.add(formatTask(url))
})

const highPriorityTask = {
    priority:10,
    info:'high priority',
    time:2000
}

q.add(formatTask(highPriorityTask))


function loadImg(url){
    return new Promise((resolve,reject)=>{
        console.log('---- '+url.info + ' start!!')
        setTimeout(()=>{
            console.log(url.info+' OK!!')
            resolve()
        },url.time)
    })
}

实现数组扁平化

递归实现

    function flatten(arr){
        let result = []
        for(let i=0,len=arr.length;i<len;i++){
            if(Array.isArray(arr[i])){
                result = result.concat(flatten(arr[i]))
            }else{
                result.push(arr[i])
            }
        }
        return result
    },

toString方式

这种方式只适用于数字类型的数组

    function flatten(arr){
        return arr.toString().split(',').map(function (item) {
            return parseInt(item)
            // return +item
        })
    }

reduce方式实现

    function flatten(arr){
        return arr.reduce((prev,next)=>{
            return prev.concat(Array.isArray(next) ? this.flatten(next): next)
        },[])
    }

ES6扩展运算符

    function flatten(arr){
        while(arr.some(item=>Array.isArray(item))){
            arr = [].concat(...arr)
        }
        return arr
    }

防抖和节流

防抖 防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行

function debounce(func,wait){
    let timeout;
    return function(){
        let context = this
        let args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context,args)
        },wait)
    }
}

立即执行版本:有时候我们需要立即执行函数,然后等待停止触发n秒后,在重新执行

function debounce(func,wait,immediate){
    let timeout;
    return function(){
        let context = this
        let args = arguments;
        if(timeout) clearTimeout(timeout)
        if(immediate){
            var callNow = !timeout
            timeout = setTimeout(function(){
                timeout = null
            },wait)
            if(callNow) func.apply(context, args)
        }else{
            timeout = setTimeout(function(){
                func.apply(context,args)
            },wait)
        }
    }
}

节流

函数节流会用在比input, keyup更频繁触发的事件中,如resize, touchmove, mousemove, scroll。throttle 会强制函数以固定的速率执行。因此这个方法比较适合应用于动画相关的场景

使用时间戳:

function throttle(func,wait){
    var context,args
    var previous = 0
    return function(){
        var now  = +new Date()
        context = this
        args = arguments
        if(now-previous > wait){
            fn.apply(context,args)
            previous = now
        }
    }
}

使用定时器

function throttle(func,wait){
    var context,args
    var timeout
    return function(){
        context = this
        args = arguments
        if(!timeout){
            timeout = setTimeout(function(){
                timeout = null
                func.apply(context,args)
            },wait)
        }
    }
}

所以比较两个方法:

第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行 第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件

深浅拷贝的实现

浅拷贝实现

Object.assign()

let obj = {
    name:"rudy",
    info:{
        "age":18,
        "sex":"male"
    }
}
let obj2 = Object.assign({},obj);
obj2.info.age = 24;
console.log(obj.info.age) // 24

Array.prototype.concat()

let arr = [1,2,{
    name:"rudy"
}];
let arr2 = arr.concat();
arr2[2].name = "tony";
console.log(arr[2]) // {name:"tony"}

Array.prototype.slice()

let arr = [1,2,{
    name:"rudy"
}];
let arr3 = arr.slice();
arr3[2].name = "tony";
console.log(arr[2]) // {name:"tony"}

手动实现

var shallowCopy  = function(obj){
    if(typeof obj !== 'object') return
    var newObj = obj instanceof Array ? [] : {}
    for(var key in obj){
        if(obj.hasOwnProperty(key)){
            newObj[key] = obj[key]
        }
    }
    return newObj
}

深拷贝实现

JSON.parse(JSON.stringify())

let obj = {
    name:"rudy",
    info:{
        sex:"male",
        age:18
    }
}
let deepObj = JSON.parse(JSON.stringify(obj));
deepObj.info.age = 24;
console.log(obj.info.age) // 18

简单版本

var deepCopy  = function(obj){
    if(typeof obj !== 'object') return
    var newObj = obj instanceof Array ? [] : {}
    for(var key in obj){
        if(obj.hasOwnProperty(key)){
            newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]
        }
    }
    return newObj
}

考虑多种类型

function deepCopy(obj,hash = new WeakMap()){
    if(typeof obj === null){
        return
    }
    if( obj instanceof Date){
        return new Date(obj)
    }
    if(obj instanceof RegExp){
        return new RegExp(obj)
    }
    if(typeof obj !== 'object'){
        return obj
    }
    if(hash.has(obj)){
        return hash.get(obj)
    }

    const resObj = Array.isArray(obj) ? [] : {}
    hash.set(obj,resObj)
    Reflect.ownKeys(obj).forEach(key=>{
        resObj[key] = deepCopy(obj[key],hash)
    })
    return resObj
}

编写一个深度克隆函数,满足以下需求

function deepClone(obj) {}

// deepClone 函数测试效果
const objA = {
  name: 'jack',
  birthday: new Date(),
  pattern: /jack/g,
  body: document.body,
  others: [123,'coding', new Date(), /abc/gim,]
};

const objB = deepClone(objA);
console.log(objA === objB); // 打印 false
console.log(objA, objB); // 对象内容一样

实现:

const deepCopy = (sourceObj) => {
    // 如果不是对象则退出(可停止递归)
    if(typeof sourceObj !== 'object') return;
    
    // 深拷贝初始值:对象/数组
    let newObj = (sourceObj instanceof Array) ? [] : {};
  
    // 使用 for-in 循环对象属性(包括原型链上的属性)
    for (let key in sourceObj) { 
      // 只访问对象自身属性
      if (sourceObj.hasOwnProperty(key)) {
        // 当前属性还未存在于新对象中时
        if(!(key in newObj)){
          if (sourceObj[key] instanceof Date) {
            // 判断日期类型
            newObj[key] = new Date(sourceObj[key].getTime());
          } else if (sourceObj[key] instanceof RegExp) {
            // 判断正则类型
            newObj[key] = new RegExp(sourceObj[key]);
          } else if ((typeof sourceObj[key] === 'object') && sourceObj[key].nodeType === 1 ) {
            // 判断 DOM 元素节点
            let domEle = document.getElementsByTagName(sourceObj[key].nodeName)[0];
            newObj[key] = domEle.cloneNode(true);
          } else {
            // 当元素属于对象(排除 Date、RegExp、DOM)类型时递归拷贝
            newObj[key] = (typeof sourceObj[key] === 'object') ? deepCopy(sourceObj[key]) : sourceObj[key];
          }
        }
      }
    }
    return newObj;
  }

图片懒加载的实现

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Lazyload</title>
    <style>
      .image-item {
	    display: block;
	    margin-bottom: 50px;
	    height: 200px;
	}
    </style>
</head>
<body>
<img src="" class="image-item" lazyload="true"  data-original="images/1.png"/>
<img src="" class="image-item" lazyload="true"  data-original="images/2.png"/>
<img src="" class="image-item" lazyload="true"  data-original="images/3.png"/>
<script>
const viewHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; //获取可视区高度
function lazyload(){
    const eles=document.querySelectorAll('img[data-original][lazyload]');
    Array.prototype.forEach.call(eles,function(item,index){
        if(item.dataset.original==="") {
            return;
        }
        const rect=item.getBoundingClientRect(); // 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
        if(rect.bottom>=0 && rect.top < viewHeight){
            let img=new Image()
            img.src=item.dataset.original
            img.onload=function(){
                item.src=img.src
            }
            item.removeAttribute("data-original")//移除属性,下次不再遍历
            item.removeAttribute("lazyload");
        }
    })
}
lazyload()//刚开始还没滚动屏幕时,要先触发一次函数,初始化首页的页面图片
document.addEventListener("scroll",lazyload)
</script>
</body>
</html>

实现一个响应式函数,对能一个对象内的所有key添加响应式

const render = (key, val) => {
    console.log(`SET key=${key} val=${val}`)
}

const defineReactive = (obj, key, val) => {
    reactive(val)
    Object.defineProperty(obj, key, {
        get() {
            return val
        },
        set(newVal) {
            if (val === newVal) return
            val = newVal
            render(key, val)
        }
    })
}
const reactive = obj => {
    if (typeof obj === 'object') {
        for (const key in obj) {
            defineReactive(obj, key, obj[key])
        }
    }
}
const data = {
    a: 1,
    b: 2,
    c: {
        c1: {
            af: 999
        },
        c2: 4
    }
}

实现数组响应式


const render = (action,...args)=>{
    console.log(`Action=${action} args=${args.join(',')}`)
}
const arrPrototype = Array.prototype
const newArrPrototype = Object.create(arrPrototype)

const methods = ['push','pop','shift','unshift','sort','splice','reverse']
methods.forEach(methodName=>{
    newArrPrototype[methodName] = function(){
        // 执行原数组的方法
        arrPrototype[methodName].call(this,...arguments)
        // 触发渲染 
        render(methodName,...arguments)
    }
})

const reactive = obj=>{
    if(Array.isArray(obj)){
        obj.__proto__ = newArrPrototype
    }
}
const data = [1,2,3,4]
reactive(data)
data.push(5) // Action = push, args = 5
data.splice(0,2) // Action=splice,args=0,2

实现delete操作的响应式处理

前置知识点:Proxy、Reflect

let observeStore = new Map()

function makeObservable(target){
   let handleName = Symbol('handler')
   observeStore.set(handleName,[])
   target.observe = function(handler){
       observeStore.get(handleName).push(handler)
   }
   const proxyHandler = {
       get(target,property,receiver){
           // 处理嵌套对象
           if(typeof target[property] === 'object' && target[property] !== null){
               return new Proxy(target[property],proxyHandler)
           }
           let success = Reflect.get(...arguments)
           if(success){
               observeStore.get(handleName).forEach(handler=>handler('GET',property,target[property]))
           }
           return success
       },
       set(target,property,value,receiver){
           let success = Reflect.set(...arguments)
           if(success){
               observeStore.get(handleName).forEach(handler=>handler('SET',property,value))
           }
       },
       deleteProperty(target,property){
           let success = Reflect.deleteProperty(...arguments)
           if(success){
               observeStore.get(handleName).forEach(handler=>handler('DELETE',property))
           }
       }
   }
   // 创建proxy 拦截更改
   return new Proxy(target,proxyHandler)
}
let user = {}
user = makeObservable(user)
user.observe((action,key,value)=>{
   console.log(`${action} key=${key} value=${value || ''}`)
})


user.name = 'john' // SET key=name value=john
console.log(user.name) // GET key=name value=john
delete user.name // DELETE key=name value=

虚拟DOM转成真实DOM

// 将vnode转成真实DOM元素
const vnode = {
    tag:'DIV',
    attrs:{
        id:'app'
    },
    children:[{
        tag:'SPAN',
        children:[{
            tag:'A',
            children:[]
        }]
    },
    {
        tag:'SPAN',
        children:[
            {
            tag:'A', 
            children:[]
            },
        {
            tag:'A',
            children:[]
        }
        ]
    }]
}

function render(vnode){
    if(typeof vnode === 'number'){
        vnode = String(vnode)
    }

    if(typeof vnode === 'string'){
        return document.createTextNode(vnode)
    }
    const element = document.createElement(vnode.tag)
    if(vnode.attrs){
        Object.keys(vnode.attrs).forEach(attrKey=>{
            element.setAttribute(attrKey,vnode.attrs[attrKey])
        })
    }
    if(vnode.children){
        vnode.children.forEach(childNode=>{
            element.appendChild(render(childNode))
        })
    }
    return element
}

console.log(render(vnode))

如何让一个对象可以被 for ... of遍历

const obj = {
    count:0,
    [Symbol.iterator]:()=>{
        return{
            next:()=>{
                obj.count++
                if(obj.count <= 10){
                    return {
                        value:obj.count,
                        done:false
                    }
                }else{
                    return{
                        value:undefined,
                        done:true
                    }
                }
            }
        }
    }
}

for(const item of obj){
    console.log(item)
}

实现instanceof

function instanceOf(left,right){
 if(typeof left !=='object' || left === null){
     return false
 }
 while(true){
     if(left === null) return false
     if(left.__proto__ === right.prototype){
         return true
     }
     left = left.__proto__
 }
}

扁平数组转树

let input = [
  {
    id: 1,
    val: "学校",
    parentId: null,
  },
  {
    id: 2,
    val: "班级1",
    parentId: 1,
  },
  {
    id: 3,
    val: "班级2",
    parentId: 1,
  },
  {
    id: 4,
    val: "学生1",
    parentId: 2,
  },
  {
    id: 5,
    val: "学生2",
    parentId: 3,
  },
  {
    id: 6,
    val: "学生3",
    parentId: 3,
  },
];
function buildTree(arr, parentId, childrenArray) {
  arr.forEach((item) => {
    if (item.parentId === parentId) {
      item.children = [];
      buildTree(arr, item.id, item.children);
      childrenArray.push(item);
    }
  });
}
function arrayToTree(input, parentId) {
  const array = [];
  buildTree(input, parentId, array);
  return array.length > 0 ? (array.length > 1 ? array : array[0]) : {};
}
const obj = arrayToTree(input, null);
console.log(obj);

手写发布订阅

class EventEmitter {
    constructor() {
      // handlers是一个map,用于存储事件与回调之间的对应关系
      this.handlers = {}
    }
  
    // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
    on(eventName, cb) {
      // 先检查一下目标事件名有没有对应的监听函数队列
      if (!this.handlers[eventName]) {
        // 如果没有,那么首先初始化一个监听函数队列
        this.handlers[eventName] = []
      }
  
      // 把回调函数推入目标事件的监听函数队列里去
      this.handlers[eventName].push(cb)
    }
  
    // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
    emit(eventName, ...args) {
      // 检查目标事件是否有监听函数队列
      if (this.handlers[eventName]) {
        // 这里需要对 this.handlers[eventName] 做一次浅拷贝,主要目的是为了避免通过 once 安装的监听器在移除的过程中出现顺序问题
        const handlers = this.handlers[eventName].slice()
        // 如果有,则逐个调用队列里的回调函数
        handlers.forEach((callback) => {
          callback(...args)
        })
      }
    }
  
    // 移除某个事件回调队列里的指定回调函数
    off(eventName, cb) {
      const callbacks = this.handlers[eventName]
      const index = callbacks.indexOf(cb)
      if (index !== -1) {
        callbacks.splice(index, 1)
      }
    }
  
    // 为事件注册单次监听器
    once(eventName, cb) {
      // 对回调函数进行包装,使其执行完毕自动被移除
      const wrapper = (...args) => {
        cb(...args)
        this.off(eventName, wrapper)
      }
      this.on(eventName, wrapper)
    }
  }