前端面试JS手写题整理

140 阅读12分钟

new一个对象的过程

  1. 创建一个新对象;

  2. 设置实例对象的原型链;将构造函数的原型对象设置为实例对象的原型

    1. 这样实例对象就能访问构造函数原型上的属性和方法
  3. 使用 apply 方法调用构造函数;obj 作为 this 上下文;展开之前收集的参数传入构造函数

    1. 这一步会执行构造函数中的代码,并将属性添加到 obj 上
  4. 返回新对象。

function newFn (ctor, ...args) {
    let obj = {}
    obj.__proto__ = Object.create(ctor.prototype)
    let res = ctor.apply(obj, [...args])
    return res
}

call的实现

// 在Function原型上添加myCall方法,实现call函数的功能
Function.prototype.myCall = function(context, ...args) {
    // 如果没有传入上下文对象,默认为window对象
    var context = context || window
    // 将当前函数设置为上下文对象的一个属性
    // this在这里指向调用myCall的函数
    context.fn = this
    // 使用eval执行函数,并传入参数
    // context.fn(...args)会在context上下文中执行函数
    var result = eval('context.fn(...args)')
    // 删除临时添加的函数属性
    delete context.fn
    // 返回函数执行结果
    return result
}

apply的实现

// 在Function原型上添加myApply方法,实现apply函数的功能
Function.prototype.myApply = function(context, args) {
    // 如果没有传入上下文对象,默认为window对象
    var context = context || window
    // 将当前函数设置为上下文对象的一个属性
    // this在这里指向调用myApply的函数
    context.fn = this
    // 使用eval执行函数,并展开args数组作为参数传入
    // context.fn(...args)会在context上下文中执行函数
    var result = eval('context.fn(...args)')
    // 删除临时添加的函数属性
    delete context.fn
    // 返回函数执行结果
    return result
}

bind的实现

// 在Function原型上添加myBind方法,实现bind函数的功能
Function.prototype.myBind = function(context, ...args) {
    // 保存当前函数的引用
    // this指向调用myBind的函数
    const self = this
    // 返回一个新函数,实现柯里化
    // 新函数可以接收额外的参数
    return function(...args2) {
        // 使用apply改变this指向,并合并两次传入的参数
        // [...args, ...args2]将第一次和第二次传入的参数合并为一个数组
        return self.apply(context, [...args, ...args2])
    }
}

数组扁平化

// 方法一:使用递归实现数组扁平化
const flatten1 = function (arr) {
    // 创建结果数组
    let result = []
    // 遍历输入数组
    for (let i = 0; i < arr.length; i++) {
        // 判断当前元素是否为数组
        if (Array.isArray(arr[i])) {
            // 如果是数组,递归调用flatten1并将结果合并到result中
            result = result.concat(flatten1(arr[i]))
        } else {
            // 如果不是数组,直接将元素添加到result中
            result.push(arr[i])
        }
    }
    return result
}
​
// 方法二:使用ES6的flat方法实现数组扁平化
const flatten2 = function(arr) {
    // flat(Infinity)会将多维数组完全展开为一维数组
    return arr.flat(Infinity)
}

数组去重

/**
 * ES5 方式实现数组去重
 * @param {Array} arr - 需要去重的数组
 * @return {Array} - 返回去重后的新数组
 */
function uniqueES5 (arr) {
    // 创建一个新数组用于存储去重后的结果
    var result = []
    // 遍历原数组中的每个元素
    arr.forEach(item => {
        // 检查当前元素是否已存在于结果数组中
        // indexOf 方法返回元素在数组中的索引,如果不存在则返回 -1
        if (result.indexOf(item) === -1) {
            // 如果元素不存在于结果数组中,则将其添加到结果数组
            result.push(item)
        }
    })
    // 返回去重后的数组
    return result
}
​
/**
 * ES6 方式实现数组去重
 * @param {Array} arr - 需要去重的数组
 * @return {Array} - 返回去重后的新数组
 */
function uniqueES6 (arr) {
    // 利用 Set 数据结构的特性(Set 中的元素不会重复)
    // 1. 将数组转换为 Set
    // 2. 使用展开运算符 ... 将 Set 转回数组
    return [...new Set(arr)]
}

冒泡排序

const bubbleSort = function(arr) {
    // 获取数组长度
    const len = arr.length
    // 外层循环控制需要比较的轮数
    for (let i = 0; i < len; i++) { 
        // 内层循环控制每轮比较的次数
        // 注意:这里的 j < i 是一个bug,应该是 j < len - i - 1
        // 因为每轮比较后,最大的元素会被移动到末尾,所以下一轮可以少比较一次
        for (let j = 0; j < i; j++) {
            // 如果前面的元素大于后面的元素,则交换它们的位置
            if (arr[j] > arr[i]) {
                // 使用临时变量完成元素交换
                const temp = arr[j]
                arr[j] = arr[i]
                arr[i] = temp
            }
        }
    }
    // 返回排序后的数组
    return arr
}

快速排序

const quickSort = function(arr) {
    // 基本情况:如果数组长度小于等于1,直接返回
    if (arr.length <= 1) return arr
​
    // 选择第一个元素作为基准值
    const pivot = arr[0]
    // 初始化左右两个数组,用于分区
    const left = []
    const right = []
​
    // 从第二个元素开始遍历(因为第一个元素是基准值)
    for (let i = 1; i < arr.length; i++) {
        // 小于基准值的元素放入左边数组
        if (arr[i] < pivot) {
            left.push(arr[i])
        } else {
            // 大于等于基准值的元素放入右边数组
            right.push(arr[i])
        }
    }
​
    // 递归排序左右两个数组,并与基准值合并
    // 使用展开运算符...来合并数组
    return [...quickSort(left), pivot, ...quickSort(right)]
}

发布订阅模式

// 实现一个简单的事件发射器类,用于事件的订阅和发布
class EventEmitter {
    // 初始化事件存储对象
    constructor () {
        // 用对象存储事件监听器,key为事件名,value为监听器数组
        this.events = {}
    }
​
    // 注册事件监听器
    // @param {string} eventName - 事件名称
    // @param {Function} listener - 事件监听器函数
    on (eventName, listener) {
        // 如果该事件名称下还没有监听器数组,则初始化一个空数组
        if (!this.events[eventName]) {
            this.events[eventName] = []
        }
        // 将监听器函数添加到该事件的监听器数组中
        this.events[eventName].push(listener)
    }
​
    // 触发指定事件
    // @param {string} eventName - 事件名称
    // @param {...any} args - 传递给监听器的参数
    emit(eventName, ...args) {
        // 如果该事件没有注册任何监听器,直接返回
        if (!this.events[eventName]) return
        // 遍历执行该事件的所有监听器,并传入参数
        this.events[eventName].forEach(listener => {
            listener(...args)
        });
    }
​
    // 移除指定事件的监听器
    // @param {string} eventName - 事件名称
    // @param {Function} listener - 要移除的监听器函数
    remove(eventName, listener) {
        // 如果该事件没有注册任何监听器,直接返回
        if (!this.events[eventName]) return
        // 注意:这里有一个bug,filter方法不会修改原数组
        // 应该改为:this.events[eventName] = this.events[eventName].filter(item => item !== listener)
        this.events[eventName].filter(item => item !== listener)
    }
​
    // 注册一次性事件监听器,触发一次后自动移除
    // @param {string} eventName - 事件名称
    // @param {Function} listener - 事件监听器函数
    once(eventName, listener) {
        // 创建一个包装函数,在执行完监听器后自动移除自身
        const onceListener = (...args) => {
            listener(...args)
            this.remove(eventName, onceListener)
        }
        // 注册包装后的监听器
        this.on(eventName, onceListener)
    }
}

实现Promise.allSellted

// 模拟实现 Promise.allSettled 方法
// @param {Array<Promise>} promises - Promise数组
// @return {Promise} - 返回一个新的Promise,该Promise总是resolve一个数组,数组中包含所有Promise的结果
function allSellted(promises) {
    // 存储所有Promise的执行结果
    const result = []
    // 获取传入的Promise数组长度
    const length = promises.length
    
    return new Promise((resolve) => {
        if (length === 0) {
            resolve(result)
            return
        }
        
        // 记录已完成的Promise数量(包括fulfilled和rejected)
        let completed = 0
        
        // 遍历处理每个Promise
        promises.forEach((element, index) => {
            // 使用Promise.resolve包装,确保element是Promise对象
            Promise.resolve(element).then(value => {
                // Promise成功时,记录状态和值
                result[index] = {status: 'fulfilled', value}
                completed++
                // 当所有Promise都完成时,返回结果数组
                if (completed === length) {
                    resolve(result)
                }
            }).catch(error => {
                // Promise失败时,记录状态和错误原因
                result[index] = {status: 'rejected', reason: error}
                completed++
                // 当所有Promise都完成时,返回结果数组
                if (completed === length) {
                    resolve(result)
                }
            })
        })
    })
}

实现Promise.race

/**
 * 模拟实现 Promise.race 方法
 * Promise.race 会返回一个新的 Promise,一旦迭代器中的某个 Promise 解决或拒绝,
 * 返回的 Promise 就会解决或拒绝,并采用第一个 Promise 的值作为它的值
 * @param {Array} promises - Promise 对象数组
 * @return {Promise} - 返回一个新的 Promise 对象
 */
function race(promises) {
    return new Promise ((resolve, reject) => {
        // 标记是否已经解决,防止多次调用 resolve 或 reject
        let isResolved = false
        // 遍历所有传入的 Promise
        for (const promise of promises) {
            Promise.resolve(promise).then(value => {
                // 如果还没有解决,则将结果传递给外部 Promise
                if (!isResolved) {
                    // 标记为已解决
                    isResolved = true
                    // 使用第一个完成的 Promise 的值来 resolve 外部 Promise
                    resolve(value)
                }
            }, reason => {
                // 如果有任何一个 Promise 被拒绝,也标记为已解决
                isResolved = true
                // 使用第一个被拒绝的 Promise 的原因来 reject 外部 Promise
                reject(reason)
            })
        }
    })
}

数组转树

/**
 * 将扁平数组转换为树形结构
 * @param {Array} arr - 包含节点对象的扁平数组,每个节点应有id和parentId属性
 * @return {Array|Object} - 返回树形结构,如果只有一个根节点则返回该节点对象,否则返回根节点数组
 */
function arrayToTree(arr) {
    // 创建一个映射表,用于快速查找节点
    const nodeMap = {}
    
    // 第一次遍历:初始化每个节点的children数组,并建立节点id到节点的映射
    arr.forEach(node => {
        // 为每个节点添加children属性
        node.children = []
        // 将节点添加到映射表中,便于后续快速查找
        nodeMap[node.id] = node
    })
    
    // 存储所有根节点的数组
    const rootArr = []
    
    // 第二次遍历:构建父子关系
    arr.forEach(node => {
        if (node.parentId !== null && node.parentId !== undefined) {
            // 查找父节点
            const parentNode = nodeMap[node.parentId]
            // 如果父节点存在,将当前节点添加到父节点的children数组中
            if (parentNode) {
                parentNode.children.push(node)
            }
        } else {
            // 如果节点没有父节点,则它是根节点
            rootArr.push(node)
        }
    })
    
    // 如果只有一个根节点,则直接返回该节点;否则返回根节点数组
    return rootArr.length === 1 ? rootArr[0] : rootArr
}

树转数组

/**
 * 将树形结构转换为扁平数组
 * @param {Object|Array} tree - 树形结构对象或对象数组
 * @return {Array} - 返回扁平化后的节点数组
 */
function treeToArray(tree) {
  // 用于存储所有节点的结果数组
  const result = [];
​
  /**
   * 递归遍历树节点的辅助函数
   * @param {Object} node - 当前处理的节点
   */
  function traverse(node) {
    // 将当前节点添加到结果数组中
    result.push(node);
    // 如果当前节点有子节点,则递归处理每个子节点
    if (node.children && node.children.length > 0) {
      node.children.forEach((item) => traverse(item));
    }
  }
​
  // 处理输入参数:可能是单个树对象或树对象数组
  if (Array.isArray(tree)) {
    // 如果是数组,遍历每个顶级节点
    tree.forEach((item) => traverse(item));
  } else {
    // 如果是单个对象,直接处理
    traverse(tree);
  }
  
  // 返回扁平化后的节点数组
  return result;
}
​

深拷贝

/**
 * 简单的深拷贝函数实现
 * @param {Object} obj - 需要深拷贝的对象
 * @return {Object} - 返回深拷贝后的新对象
 */
const simpleDeepClone = (obj) => {
    // 创建一个新的空对象作为克隆对象
    let cloneObj = {}
    // 遍历原对象的所有属性
    for (let key in obj) {
        // 如果属性值是对象类型,则递归调用深拷贝函数
        if (typeof obj[key] === 'object') {
            cloneObj[key] = simpleDeepClone(obj[key])
        } else {
            // 如果属性值是基本类型,则直接复制
            cloneObj[key] = obj[key]
        }
    }
    // 返回克隆后的对象
    return cloneObj
}
​
/**
 * 完整的深拷贝函数实现
 * 解决了循环引用问题,并支持更多的数据类型
 * @param {Object} obj - 需要深拷贝的对象
 * @param {WeakMap} hash - 用于存储已经拷贝过的对象,解决循环引用问题
 * @return {Object} - 返回深拷贝后的新对象
 */
const deepClone = (obj, hash = new WeakMap()) => {
    // 处理 null 的情况
    if (obj === null) return null
    // 处理日期对象
    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)
    // 使用对象的构造函数创建新的对象实例(支持数组等特殊对象类型)
    let cloneObj = new obj.constructor()
    // 将当前对象与拷贝对象的映射关系存入 WeakMap,用于处理循环引用
    hash.set(obj, cloneObj)
    // 遍历对象的所有自有属性
    for (let key in obj) {
        // 只拷贝对象自身的属性,不拷贝原型链上的属性
        if (obj.hasOwnProperty(key)) {
            // 递归拷贝属性值,并传递 hash 映射表
            cloneObj[key] = deepClone(obj[key], hash)
        }
    }
    // 返回克隆后的对象
    return cloneObj
}

防抖

/**
 * 防抖函数 - 用于限制函数的执行频率,在一定时间内多次触发只执行最后一次
 * 常用于搜索框输入、窗口调整大小等高频触发的场景
 * @param {Function} fn - 需要进行防抖处理的原始函数
 * @param {Number} delay - 延迟执行的时间(毫秒)
 * @return {Function} - 返回经过防抖处理的新函数
 */
const debounce = (fn, delay) => {
  // 定义一个定时器变量,用于存储当前的延时操作
  let timer;
  // 返回一个新的函数,这个函数就是经过防抖处理的函数
  return function (...args) {
    // 保存函数调用时的上下文(this),确保原函数执行时的上下文不丢失
    const context = this;
    // 如果已经存在定时器,则清除之前的定时器
    // 这样做可以确保在delay时间内多次调用,只有最后一次会被执行
    if (timer) clearTimeout(timer);
    // 设置新的定时器,delay毫秒后执行原始函数
    timer = setTimeout(() => {
      // 使用apply调用原始函数,确保this指向正确,并传递所有参数
      fn.apply(context, args);
    }, delay);
  };
}

节流

/**
 * 节流函数 - 用于限制函数的执行频率,在指定的时间间隔内最多执行一次
 * 常用于滚动事件、窗口调整、按钮点击、拖拽事件等高频触发的场景
 * 与防抖不同,节流会按时间间隔执行函数,而不是等待最后一次触发
 * @param {Function} fn - 需要进行节流处理的原始函数
 * @param {Number} delay - 规定的时间间隔(毫秒)
 * @return {Function} - 返回经过节流处理的新函数
 */
const throttle = function(fn, delay) {
    // 记录上一次函数执行的时间戳
    let prev = Date.now()
    // 返回一个新的函数,这个函数就是经过节流处理的函数
    return function(...args) {
        // 保存函数调用时的上下文(this)
        const context = this
        // 获取当前时间戳
        let now = Date.now()
        // 判断当前时间与上一次执行时间的时间差是否大于等于指定的延迟时间
        if(now - prev >= delay) {
            // 如果时间差满足条件,则执行函数
            // 使用 apply 调用原始函数,确保 this 指向正确,并传递所有参数
            fn.apply(context, args)
            // 更新上一次函数执行的时间戳
            prev = Date.now()
        }
    }
}