前端面试必备八股文——手写代码

前端面试必备八股文——手写代码

前言

这篇文章主要是收集面试中常见的手写题,包括我遇到的一些算法、功能函数、数据结构等问题。写法不一定是最优,可以用来参考,希望可以在面试中帮到你。

实现一个new操作符

js复制代码/** 手写 new 操作符
 * 用法:创建一个实例化对象
 * 思路:
 *  1、判断传入的 fn 是否为 function
 *  2、创建一个空对象
 *  3、将这个空对象的原型设置为构造函数的 prototype 属性。
 *  4、使用 apply 执行构造函数 并传入参数 arguments 获取函数的返回值
 *  5、判断这个返回值 如果返回的是 Object || Function 类型 就返回该对象 否则返回创建的对象
 * @param {Function} fn 构造函数
 * @return {*}
 */
function myNew(fn, ...args) {
  // 判断 fn 是否为函数
  if (typeof fn !== 'function') {
    return new TypeError('fn must be a function')
  }

  // 创建一个空的对象
  let obj = null

  // 将这个空对象的原型设置为构造函数的 prototype 属性。
  obj = Object.create(fn.prototype)

  // 通过 apply 执行构造函数 传入参数 获取返回值
  let result = fn.apply(obj, args)

  // 判断这个返回值 如果返回的是 Object || Function 类型 就返回该对象 否则返回创建的对象
  const flag = result && (typeof result === 'object' || typeof result === 'function')

  return flag ? result : obj
}

实现一个intanceof操作符

js复制代码/** 手写 instanceof 方法
 * 用法:instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
 * 思路:
 *  1、通过 Object.getPrototypeOf 获取 obj 的原型
 *  2、循环判断 objProtoType 是否和 constructor 的原型相等
 *    2.1、如果相等就返回 true
 *    2.2、如果不相等 就重新赋值一下 obj 的原型 进入下一次循环
 *  3、判断是 objProtoType 是否为空 如果为空就说明不存在 返回 false
 * @param {Object} obj 需要判断的数据
 * @param {Object} constructor
 * @return {*}
 */
function myInstanceof(obj, type) {
  let objPrototype = Object.getPrototypeOf(obj)

  while (true) {
    if (!objPrototype) return false
    if (objPrototype === type.prototype) return true

    objPrototype = Object.getPrototypeOf(objPrototype)
  }
}

手写 Object.create

js复制代码/** 手写 Object.create
 * 用法:创建一个新的对象,将传入的对象原型指向新对象并返回
 * 思路:
 *  1、将原型写入到一个函数里面,然后将函数返回
 * @param {*} obj
 * @return {*} 
 */
function myCreate(obj) {
	function F() {}
	F.prototype = obj

	return new F()
}

手写节流

函数在 n 秒内只执行一次,如果 n 秒内多次触发,则忽略执行。

js复制代码/** 手写节流
 * 用法:函数在 n 秒内只执行一次,如果多次触发,则忽略执行。
 * 思路:
 *  1、记录函数上一次执行的时间戳 startTime
 *  2、返回一个闭包函数 当被调用时会记录一下执行时间 nowTime
 *  3、比较两次执行时间间隔 是否超过了 wait 时间
 *  4、如果是大于 wait 时间 说明已经过了一个 wait 时间 可以执行函数
 *    4.1、更新 startTime 方便下次对比
 *    4.2、通过 apply 执行函数fn 传入 arguments 参数
 *  5、如果没有超过 wait 时间  说明是在 wait 时间内又执行了一次  忽略
 * @param {Function} fn 执行函数
 * @param {Number} wait 等待时间
 * @return {*} 
 */
function throttle(fn, wait) {
  let startTime = Date.now()

  return function () {
    const nowTime = Date.now()

    // 计算两次执行的间隔时间 是否大于 wait 时间
    if (nowTime - startTime >= wait) {
      startTime = nowTime
      return fn.apply(this, arguments)
    }
  }
}

手写防抖

函数在 n 秒后执行,如果多次触发,重新计时,保证函数在 n 秒后执行

js复制代码/** 手写防抖
 * 用法:函数在 n 秒后再执行,如果 n 秒内被触发,重新计时,保证最后一次触发事件 n 秒后才执行。
 * 思路:
 *  1、保存一个变量 timer
 *  2、返回一个闭包函数 函数内判断一下 timer 是否有值
 *    2.1、如果有值 说明 定时器已经开启 需要将定时器清空
 *  3、设置定时器 等待 wait 后执行 将定时器赋值给 timer 记录
 *  4、通过 apply 执行函数 传入 arguments
 * @param {*} fn
 * @param {*} wait
 * @param {boolean} [immediate=false]
 * @return {*} 
 */
function debounce(fn, wait, immediate = false) {
  let timer = null

  return function () {
    // 存在定时器 清空
    if (timer) {
      clearInterval(timer)
      timer = null
    }
    // 立即执行
    if (immediate) {
      // 判断是否执行过  如果执行过 timer 不为空
      const flag = !timer

      // 执行函数
      flag && fn.apply(this, arguments)

      // n 秒后清空定时器
      timer = setTimeout(() => {
        timer = null
      }, wait)
    } else {
      timer = setTimeout(() => {
        fn.apply(this, arguments)
      }, wait)
    }
  }
}

手写浅拷贝

js复制代码/** 浅拷贝
 * 用法:浅拷贝是指,一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值,如果是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。
 * 思路:
 *  1、判断是否为对象
 *  2、根据obj类型创建一个新的对象
 *  3、for in 遍历对象 拿到 key
 *  4、判断 key 是否在 obj 中
 *  5、将 key 作为新对象的key 并赋值 value
 *
 * @param {*} obj
 * @return {*} 
 */
function shallowCopy(obj) {
  // 只拷贝对象
  if (!obj || typeof obj !== 'object') {
    return obj
  }

  // 新的对象
  const newObj = Array.isArray(obj) ? [] : {}

  // 循环遍历 obj 将 key 作为 newObj 的 key 并赋值value
  for (const key in obj) {
    // 判断 key 是否在 obj 中
    if (obj.hasOwnProperty(key)) {
      newObj[key] = obj[key]
    }
  }

  return newObj
}

手写深拷贝

js复制代码/** 深拷贝
 * 用法:拷贝一个对象的属性值 如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用
 * 思路:
 *  1、判断是否为对象
 *  2、判段对象是否在 map 中 如果存在就不需要操作
 *  3、将 obj 放入 map 中 避免重复引用
 *  4、for in 遍历对象 拿到 key 判断 key 是否在 obj 中
 *  5、value 如果为对象 就递归拷贝 否则就赋值
 * @param {*} obj
 * @param {*} [map=new Map()]
 * @return {*} 
 */
function deepCopy(obj, map = new Map()){
  if (!obj || typeof obj !== 'object'){
    return obj
  }

  // 判断 obj 是否在 map 中存在 如果存在就不需要递归调用 直接返回数据
  if (map.get(obj)) {
    return map.get(obj)
  }
  const newObj = Array.isArray(obj) ? [] : {}

  // 放入 map 中 记录当前对象 避免重复拷贝 循环引用
  map.set(obj, newObj)

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 如果 value 还是一个对象 递归获取 否则就赋值
      newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key], map) : obj[key]
    }
  }

  return newObj
}

手写call

js复制代码/** 手写 call
 * 用法:call 方法用于调用一个函数,并指定函数内部 this 的指向,传入一个对象
 * 思路:
 *  1、判断 this 是否指向一个函数  只有函数才可以执行
 *  2、获取传入的 context 上下文 也就是我们要指向的 如果不存在就指向 window
 *  3、将当前 this 也就是外部需要执行的函数 绑定到 context 上 然后执行获取 result 传入 ...args 确保参数位置正确
 *  4、删除 context 对象的 fn 属性 并将 result 返回
 */

Function.prototype.myCall = function (context, ...args) {
  if (typeof this !== 'function') {
    return new TypeError('type error')
  }
  context = context || window

  // 缓存this

  context.fn = this

  const result = context.fn(...args)

  delete context.fn

  return result
}

手写apply

js复制代码/** 手写 apply
 * 用法:apply 方法用于调用一个函数,并指定函数内部 this 的指向,传入一个数组
 * 思路:
 *  1、判断 this 是否指向一个函数  只有函数才可以执行
 *  2、获取传入的 context 上下文 也就是我们要指向的 如果不存在就指向 window
 *  3、将当前 this 也就是外部需要执行的函数 绑定到 context 上的一个 fn 属性上
 *  4、执行 fn 函数 判断 args 是否有 如果没有参数就直接执行 如果有参数 将参数展开传入 fn
 *  5、删除 context 对象的 fn 属性 并将 result 返回
 */

Function.prototype.myApply = function (context, args) {
  if (typeof this !== 'function') {
    return new TypeError('type error')
  }

  // 和 call 一样 只不过传入的参数只有一个 类型为数组 在执行 fn 的时候将参数展开
  context = context || window

  context.fn = this

  const result = args ? context.fn(...args) : context.fn()

  delete context.fn

  return result
}

手写bind

js复制代码/** 手写 bind
 * 用法:bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
 * 思路:
 *  1、判断 this 是否指向一个函数  只有函数才可以执行
 *  2、获取传入的 context 上下文 也就是我们要指向的 如果不存在就指向 window
 *  3、将当前 this 也就是外部需要执行的函数 绑定到 context 上的一个 fn 属性上
 *  4、返回一个函数 供外部调用 执行函数后传入新的参数
 *  5、执行在闭包内缓存的 fn 将两次参数一起传入 删除 fn 返回 result
 */

Function.prototype.myBind = function (context, ...args1) {
  if (typeof this !== 'function') {
    return new TypeError('type error')
  }
  context = context || window
  context.fn = this

  // 和 call apply 不一样的是 bind 返回一个函数 需要在外部执行  参数为多个对象 且返回的对象里也会有参数
  return function (...args2) {
    const result = context.fn(...args1, ...args2)
    delete context.fn
    return result
  }
}

函数柯里化的实现

js复制代码/** 函数柯里化
 * 用法:函数柯里化是一种将接受多个参数的函数转换为接受一系列单一参数的函数的技术
 * 思路:
 *  1、使用 fn.length 获取函数的形参数量
 *  2、如果没有传入初始参数数组 则将其初始化为空数组 在递归的时候会接受上一次的形参
 *  3、返回一个闭包函数 接受函数的实参 将 args 中的形参和当前的形参进行合并 得到 newArgs
 *  4、如果新的参数数组 newArgs 长度大于等于 length 函数的形参数量 调用 apply 执行函数 传入 newArgs
 *  5、如果新的参数数组长度小于函数的形参数量 则再次调用 curry 函数 将新的参数数组作为初始参数传入 返回一个新的闭包函数
 * @param {*} fn
 * @param {*} args
 * @return {*} 
 */
function curry(fn, args) {
  // 获取 fn 获取 add 函数的形参数量
  const length = fn.length

  // 递归执行时传递的上一次参数 第一次执行 [] 第二次执行 [1]
  args = args || []
  return function () {
    // 将上一次参数和这次的参数进行合并  得到新的参数数组
    const newArgs = [...args, ...arguments]

    // 判断 newArgs 长度是否和 add 函数形参长度一致 如果超过就执行 fn 函数 传递 newArgs
    if (newArgs.length >= length) {
      return fn.apply(this, newArgs)
    } else {
      // 小于 add 函数形参长度 递归调用 curry 函数 累积参数 传递 newArgs
      return curry(fn, newArgs)
    }
  }
}

手写promise

js复制代码class MyPromise {
  constructor(executor) {
    this.state = 'pending'
    this.value
    this.reason

    this.onResolveCallbacks = []
    this.onRejectCallbacks = []

    const resolve = (value) => {
      if (this.state === 'pending') {
        this.value = value
        this.state = 'fulfilled'
        this.onResolveCallbacks.forEach((fn) => fn())
      }
    }

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.reason = reason
        this.state = 'rejected'
        this.onRejectCallbacks.forEach((fn) => fn())
      }
    }

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

  then(onFulfilled, onRejected) {
    // 判断类型
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : (reason) => {
            throw reason
          }

    const p2 = new MyPromise((resolve, reject) => {
      // 执行成功
      // 执行失败
      // pending状态放入任务队列
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value)
            this.resolvePromise(p2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0)
      } else if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason)
            this.resolvePromise(p2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0)
      } else {
        this.onResolveCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value)
              this.resolvePromise(p2, x, resolve, reject)
            } catch (error) {
              reject(error)
            }
          }, 0)
        })

        this.onRejectCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason)
              this.resolvePromise(p2, x, resolve, reject)
            } catch (error) {
              reject(error)
            }
          }, 0)
        })
      }
    })

    return p2
  }

  resolvePromise(p2, x, resolve, reject) {
    // 判断 p2 和x是否相等
    if (p2 === x) {
      return reject(new TypeError('type error'))
    }

    // 执行锁 确保执行一次完resolve或者reject后 不再执行
    let called = false

    // 判断x数据类型  如果是函数 对象 需要递归执行  如果是值类型 直接resolve
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
      try {
        // 判断 then是否为函数
        const then = x.then
        if (typeof then === 'function') {
          then.call(
            x,
            (y) => {
              if (called) return
              called = true
              this.resolvePromise(p2, y, resolve, reject)
            },
            (r) => {
              if (called) return
              called = true
              reject(r)
            },
          )
        } else {
          resolve(x)
        }
      } catch (error) {
        if (called) return
        called = true
        reject(error)
      }
    } else {
      resolve(x)
    }
  }
}

实现all

js复制代码  /**
   * 1.返回一个新的promise对象
   * 2.遍历传入的数据,将数据包装成一个 promise 对象
   * 3. 执行resolve 或者reject
   * 4. 返回结果
   * 这里的代码是一个 forEach 循环,对于每个 Promise,调用 MyPromise.resolve 方法将其转换为 Promise 对象,然后调用 then 方法,将 fulfilled 的值存储到 results 数组中,count 加 1。当 count 等于 promises 数组的长度时,说明所有的 Promise 都 fulfilled,此时调用 resolve 方法,将 results 数组作为返回值传递给新的 Promise。
   * 在遍历时记录当前promise在数组中的位置,这个位置就是index。
   */
  all(array) {
    return new MyPromise((resolve, reject) => {
      if (!Array.isArray(array)) {
        throw new TypeError('You must pass an array to all.')
      }
      const result = []
      let count = 0
      // 遍历 array 拿到每一条数据
      array.forEach((promise, index) => {
        MyPromise.resolve(promise).then(
          (value) => {
            result[index] = value
            count++
            // 判断 result 结果值的长度 和 array参数的长度相等  执行最外面的 resolve 返回 all 结果
            if (count === array.length) {
              resolve(array)
            }
          },
          (err) => {
            reject(err)
          },
        )
      })
    })
  }

实现race

js复制代码  race(array) {
    return new MyPromise((resolve, reject) => {
      if (!Array.isArray(array)) {
        throw new TypeError('You must pass an array to all.')
      }

      array.forEach((promise) => {
        MyPromise.resolve(promise).then(
          (value) => {
            resolve(value)
          },
          (reason) => {
            reject(reason)
          },
        )
      })
    })
  }

实现数组的扁平化

递归

判断当前项是否为数组 如果是数组递归调用 不是就push到新数组

js复制代码let arr = [1, [2, [3, 4, 5]]]

function flatten(arr) {
  let newArr = []

  // 递归获取数据
  for (let index = 0; index < arr.length; index++) {
    const element = arr[index]
    Array.isArray(element) ? (newArr = newArr.concat(flatten(element))) : newArr.push(element)
  }

  return newArr
}

新建一个栈来存储数据 每次从栈中取出一个数据 判断是否为数组 如果是 就将该数组放入到栈中 修改了栈的长度 开始下一次循环 如果不是 就放入新数组

js复制代码function flatten(arr) {
  const stack = [...arr]
  const result = []

  while (stack.length) {
    const next = stack.pop()
    if (Array.isArray(next)) {
      stack.push(...next)
    } else {
      result.push(next)
    }
  }
  return result
}

ES6 filter 结构赋值

js复制代码function flatten(arr) {
  while (arr.some((item) => Array.isArray(item))) {
    arr = [].concat(...arr)
  }
  return arr
}

实现数组去重

Set结构

js复制代码const arr = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8]
const uniqueArr = [...new Set(arr)]

使用 filter 和 indexOf

js复制代码const uniqueArr = arr.filter((item, index) => {
  return arr.indexOf(item) === index
})

使用Map存储

js复制代码function uniqueArray(array) {
  let map = {}
  let res = []
  for (var i = 0; i < array.length; i++) {
    if (!map.hasOwnProperty([array[i]])) {
      map[array[i]] = 1
      res.push(array[i])
    }
  }
  return res
}

实现reduce方法

reduce函数于累积数组元素并返回一个最终的累积结果,接受一个回调函数和一个初始值作为参数。回调函数将接受四个参数:累积结果(上一次的回调返回值或初始值)、当前元素、当前索引和原始数组。

js复制代码function myReduce(arr, callback, initialValue) {
  let accumulator = initialValue !== undefined ? initialValue : arr[0];
  const startIndex = initialValue !== undefined ? 0 : 1;

  for (let i = startIndex; i < arr.length; i++) {
    accumulator = callback(accumulator, arr[i], i, arr);
  }

  return accumulator;
}

实现数组 push 方法

push方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。

js复制代码Array.prototype.myPush = function () {
  // 循环遍历 arguments.length 也就是传入的参数个数
  for (let index = 0; index < arguments.length; index++) {
    // this.length 指向调用这个方法的数组 获取数组的长度 将当前元素放入最后一个
    this[this.length] = arguments[index]
  }

  return this.length
}
// let arr = [1,2,3]
// arr.myPush(6, 4, 5)

实现数组 filter 方法

filter方法创建给定数组一部分的浅拷贝,其包含通过所提供函数实现的测试的所有元素。

js复制代码Array.prototype.myFilter = function (callback) {
  if (!callback || typeof callback !== 'function') {
    throw Error('callback must be a function ')
  }

  const res = []
  // this.length 指向调用方法的数组
  for (let index = 0; index < this.length; index++) {
    // 执行 callback 函数传入数据 如果函数返回 true 就将当前数据放入 res 中
    callback(this[index], index) && res.push(this[index])
  }
  return res
}

// let arr = [1, 2, 3]
// console.log(
//   arr.myFilter((item, index) => {
//     console.log('item', item)
//     console.log('index', index)
//     return item > 2
//   })
// )

实现数组 map 方法

map创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成

js复制代码Array.prototype.myMap = function (callback) {
  if (!callback || typeof callback !== 'function') {
    throw Error('callback must be a function ')
  }
  const result = []

  // this.length 指向调用方法的数组
  for (let index = 0; index < this.length; index++) {
    result.push(callback(this[index], index))
  }
  return result
}

// const map1 = array1.map((x) => x * 2)
// console.log(map1)

实现 add(1)(2)(3)

参考柯里化实现

将数字每千分位用逗号隔开

不带小数

js复制代码function format(num) {
  if (!num && typeof num !== 'number') {
    return num
  }
  let str = num.toString()
  let len = str.length

  // 长度是否超过3
  if (len <= 3) {
    return num
  } else {
    // 判断是否为 3 的倍数
    let remainder = len % 3

    // 不是 3 的整倍数
    if (remainder > 0) {
      //  则根据数字长度对字符串进行拆分,每3位一组,最后再用逗号拼接起来

      // 被 3 整除余下的 也就是最前面第一个数字 如 1234567 最前面就是 1
      const firstNum = str.slice(0, remainder)

      // 获取剩下的数组 每 3 个用 , 拼接  也就是从 remainder 位置到最后一位
      const surplus = str.slice(remainder, len).match(/\d{3}/g)

      // 组合起来  第一位后面加上 ,
      return firstNum + ',' + surplus
    } else {
      // 是 3 的倍数 上面操作去掉第一位数据操作就是  直接用正则匹配数据 然后 join 拼接 ,
      return str.match(/\d{3}/g).join(',')
    }
  }
}

带小数

js复制代码function format1(num) {
  if (!num && typeof num !== 'number') {
    return num
  }
  let str = num.toString()
  let len = str.length

  let decimals = ''
  // 获取小数
  str.includes('.') ? (decimals = str.split('.')[1]) : decimals

  // 长度是否超过3
  if (len <= 3) {
    return num
  } else {
    // 判断是否为 3 的倍数
    let remainder = len % 3

    // 不是 3 的整倍数
    if (remainder > 0) {
      //  则根据数字长度对字符串进行拆分,每3位一组,最后再用逗号拼接起来

      // 被 3 整除余下的 也就是最前面第一个数字 如 1234567 最前面就是 1
      const firstNum = str.slice(0, remainder)

      // 获取剩下的数组 每 3 个用 , 拼接  也就是从 remainder 位置到最后一位
      const surplus = str.slice(remainder, len).match(/\d{3}/g)

      // 组合起来  第一位后面加上 ,  顺便带上小数
      return firstNum + ',' + surplus + '.' + decimals
    } else {
      // 是 3 的倍数 上面操作去掉第一位数据操作就是  直接用正则匹配数据 然后 join 拼接 , 顺便带上小数
      return str.match(/\d{3}/g).join(',') + '.' + decimals
    }
  }
}

数组转树

js复制代码let source = [
  {
    id: 1,
    pid: 0,
    name: 'body',
  },
  {
    id: 2,
    pid: 1,
    name: 'title',
  },
  {
    id: 3,
    pid: 2,
    name: 'div',
  },
  {
    id: 4,
    pid: 0,
    name: 'html',
  },
  {
    id: 5,
    pid: 4,
    name: 'div',
  },
  {
    id: 6,
    pid: 5,
    name: 'span',
  },
  {
    id: 7,
    pid: 5,
    name: 'img',
  },
][
  // 转为
  ({
    id: 1,
    pid: 0,
    name: 'body',
    children: [
      {
        id: 2,
        pid: 1,
        name: 'title',
        children: [{ id: 3, pid: 2, name: 'div' }],
      },
    ],
  },
  {
    id: 4,
    pid: 0,
    name: 'html',
    children: [
      {
        id: 5,
        pid: 4,
        name: 'div',
        children: [{ id: 7, pid: 5, name: 'img' }],
      },
    ],
  })
]

用map将所有id 做缓存 判断pid是否在 map 中 在就是子元素 不在就是跟元素

js复制代码function arrToTree(arr) {
  const map = {}
  const result = []
  for (const item of arr) {
    map[item.id] = item
  }
  
  for (let i = 0; i < arr.length; i++) {
    // 获取 pid  看是否在 map 中查询得到对应的
    const pid = arr[i].pid
    if (map[pid]) {
      // 当前 pid 在 map 中存在 将当前节点作为 map 中节点的子节点
      map[pid].children = map.children || []
      map[pid].children.push(arr[i])
    } else {
      // 不在 map 中 说明是根节点
      result.push(arr[i])
    }
  }
  return result
}

树转数组

js复制代码let source1 = [
  {
    id: 1,
    pid: 0,
    name: 'body',
    children: [
      {
        id: 2,
        pid: 1,
        name: 'title',
        children: [{ id: 3, pid: 2, name: 'div' }],
      },
    ],
  },
  {
    id: 4,
    pid: 0,
    name: 'html',
    children: [
      {
        id: 5,
        pid: 4,
        name: 'div',
        children: [{ id: 7, pid: 5, name: 'img' }],
      },
    ],
  },
][
  // 转为
  ({ id: 1, pid: 0, name: 'body' },
  { id: 4, pid: 0, name: 'html' },
  { id: 2, pid: 1, name: 'title' },
  { id: 5, pid: 4, name: 'div' },
  { id: 3, pid: 2, name: 'div' },
  { id: 7, pid: 5, name: 'img' })
]

[[手写代码#实现数组的扁平化#栈|参考数组扁平用栈方式]]

js复制代码function treeToArr(arr) {
  let stack = [...arr]
  const result = []

  while (stack.length) {
    // 从数组中获取第一个
    const first = stack.shift()

    // 判断它有没有children
    if (first['children'] && first.children.length) {
      // 有 children 将它展开再放入到栈中
      stack.push(...first.children)

      // 删除 children 属性
      delete first.children
    }

    result.push(first)
  }

  return result
}

实现每隔一秒打印 1,2,3,4

js复制代码// 使用 let 块级作用域
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}

使用 setTimeout 实现 setInterval

js复制代码function mySetInterval(fn, delay) {
  let timerId

  function interval() {
    // 执行回调
    fn()
    // 再次调用 interval
    timerId = setTimeout(interval, delay)
  }

  // 开始执行 interval
  setTimeout(interval, delay)

  return {
    clear() {
      clearTimeout(timerId)
    },
  }
}

实现发布-订阅模式

js复制代码class EventCenter {
  constructor() {
    // 事件中心
    this.events = {}
  }

  /**
   * 订阅事件
   *
   * @param {string} eventName
   * @param {function} callback
   * @memberof EventCenter
   */
  subscribe(eventName, callback) {
    // 确保当前 eventName 在事件中心是唯一的
    if (!this.events[eventName]) {
      // 创建事件容器
      this.events[eventName] = []
    }
    // 存放事件
    this.events[eventName].push(callback)
  }

  /**
   * 取消订阅
   *
   * @param {string} eventName
   * @param {function} callback
   * @return {*}
   * @memberof EventCenter
   */
  unSubscribe(eventName, callback) {
    // 事件中心里没有这个事件
    if (!this.events[eventName]) {
      return new Error('not find event ' + eventName)
    }

    // 只有事件名 移除事件
    if (!callback) {
      delete this.events[eventName]
    } else {
      // 找到索引
      const index = this.events[eventName].findIndex((el) => el === callback)

      if (index !== -1) {
        return new Error('not find callback')
      }

      // 移除事件下的某个函数
      this.events[eventName].splice(index, 1)

      // 查看事件容器是否为空 如果为空移除事件
      if (this.events[eventName].length === 0) {
        delete this.events[eventName]
      }
    }
  }

  /**
   * 触发事件
   *
   * @param {string} eventName
   * @param {Array} args
   * @return {*}
   * @memberof EventCenter
   */
  dispatch(eventName, ...args) {
    if (!this.events[eventName]) {
      return new Error('not find event ' + eventName)
    }

    // 触发事件
    this.events[eventName].forEach((el) => {
      el(...args)
    })
  }
}

const eventCenter = new EventCenter()

// 订阅事件
eventCenter.subscribe('click', (x, y) => {
  console.log(`clicked at (${x}, ${y})`)
})

// 发布事件
eventCenter.dispatch('click', 10, 20) // 输出:clicked at (10, 20)

实现斐波那契数列

递归方式

js复制代码function fn(n) {
	if (n = 0) return 0
	if (n = 1) return 1
	return fn(n -2) + fn(n -1)
}

非递归

js复制代码fnction fn(n) {
	let pre1 = 1
	let pre2 = 1
	let current = 2

	if (n = 2) return current 
	for( let i =2; i < n; i++) {
		pre1 = pre2
		pre2 = current
		current = pre1 + pre2
	}

	return current
}

排序算法

冒泡排序

从数组的第一个元素开始,依次比较相邻的两个元素,如果前一个元素大于后一个元素,就交换它们的位置,这样大的元素会逐步“冒泡”到数组的末尾。经过一轮比较,最大的元素会位于数组的最后一个位置。然后继续进行下一轮比较,但已经排序好的元素不再参与比较。

js复制代码function bubbleSort(arr) {
  const n = arr.length;
  for (let i = 0; i < n - 1; i++) {
    for (let j = 0; j < n - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
}

选择排序

在未排序部分选择最小的元素,然后将其与未排序部分的第一个元素交换,以此逐步构建已排序部分。在每一轮遍历中,算法会找到未排序部分的最小元素的索引,然后与当前轮的第一个元素交换位置,这样当前轮的第一个元素会是已排序部分的最小元素。

与冒泡排序不同,选择排序每轮只进行一次交换操作,因此交换次数相对较少,性能稍优。

js复制代码function selectionSort(arr) {
  const n = arr.length;
  for (let i = 0; i < n - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < n; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
  }
  return arr;
}

插入排序

将数组划分为已排序和未排序两个部分,初始状态下,第一个元素被视为已排序部分。然后从未排序部分逐个选择元素插入到已排序部分的正确位置,以此逐步构建有序数组。

对于每个待插入元素 current,算法会从已排序部分从后往前遍历,将比 current 大的元素往后移动一个位置,直到找到合适的位置插入 current

js复制代码function insertionSort(arr) {
  const n = arr.length;
  for (let i = 1; i < n; i++) {
    let current = arr[i];
    let j = i - 1;
    while (j >= 0 && arr[j] > current) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = current;
  }
  return arr;
}

希尔排序

将数组分成多个子数组,分别进行插入排序。初始时,选取一个递减的间隔值 gap(通常为数组长度的一半),然后按照这个间隔将数组分成若干组。然后对每组分别进行插入排序,不断缩小间隔值,直到间隔值为 1,完成最后一次插入排序。

希尔排序的关键在于选择合适的间隔序列,不同的间隔序列会影响算法的性能。经过一系列的实验和研究,确定了一些间隔序列,例如希尔序列(1,4,13,40,121…)。

js复制代码function shellSort(arr) {
  const n = arr.length;
  for (let gap = Math.floor(n / 2); gap > 0; gap = Math.floor(gap / 2)) {
    for (let i = gap; i < n; i++) {
      let temp = arr[i];
      let j = i;
      while (j >= gap && arr[j - gap] > temp) {
        arr[j] = arr[j - gap];
        j -= gap;
      }
      arr[j] = temp;
    }
  }
  return arr;
}

归并排序

组递归地划分为两个子数组,然后分别对这两个子数组进行递归的归并排序,最后将两个有序子数组合并成一个有序数组。

mergeSort 函数用于递归地将数组划分为子数组,直到每个子数组只包含一个元素或为空。然后,merge 函数用于将两个有序子数组合并成一个有序数组。在 merge 函数中,分别设置左子数组和右子数组的索引,然后依次比较两个子数组的元素,将较小的元素添加到 result 数组中,直到其中一个子数组的元素全部添加完毕。然后将剩余子数组的元素依次添加到 result 数组中,最终得到有序的合并数组。

js复制代码function mergeSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  const middle = Math.floor(arr.length / 2);
  const left = arr.slice(0, middle);
  const right = arr.slice(middle);
  return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
  let result = [];
  let leftIndex = 0;
  let rightIndex = 0;
  while (leftIndex < left.length && rightIndex < right.length) {
    if (left[leftIndex] < right[rightIndex]) {
      result.push(left[leftIndex]);
      leftIndex++;
    } else {
      result.push(right[rightIndex]);
      rightIndex++;
    }
  }
  return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}

快速排序

选择一个基准元素(通常是数组的第一个元素),然后将数组分成小于基准和大于基准的两个部分。接着,对这两个部分分别递归地应用快速排序,最终将所有子数组合并成有序数组。

快速排序的核心是将数组划分为小于基准和大于基准的两部分,这是通过遍历数组并根据元素与基准的比较结果来实现的。首先选择第一个元素作为基准 pivot,然后遍历数组的其他元素,将小于基准的元素放入 left 数组,将大于基准的元素放入 right 数组。

最后,递归地对 leftright 数组应用快速排序,然后将排序后的 left 数组、基准元素和排序后的 right 数组依次连接起来,形成最终有序的数组。

js复制代码function quickSort(arr) {
  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).concat(pivot, quickSort(right));
}

堆排序

将数组构建成一个堆(通常是最大堆),然后不断地将堆顶元素(最大元素)与堆末尾的元素交换,然后重新调整堆结构,使得交换后的堆仍然满足堆的性质。这样,每次交换后,堆中的最大元素会被置于末尾,最终形成一个有序的数组。

首先通过 heapify 函数构建初始的最大堆。然后进行两个阶段的循环:第一个循环用于构建最大堆,从数组的中间位置开始,依次向前调用 heapify 函数,使得整个数组满足最大堆的性质;第二个循环用于不断地将堆顶元素与堆末尾元素交换,并重新调整堆,直到整个数组有序。

js复制代码function heapSort(arr) {
  const n = arr.length;
  for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
    heapify(arr, n, i);
  }
  for (let i = n - 1; i > 0; i--) {
    [arr[0], arr[i]] = [arr[i], arr[0]];
    heapify(arr, i, 0);
  }
  return arr;
}

function heapify(arr, n, i) {
  let largest = i;
  const left = 2 * i + 1;
  const right = 2 * i + 2;
  if (left < n && arr[left] > arr[largest]) {
    largest = left;
  }
  if (right < n && arr[right] > arr[largest]) {
    largest = right;
  }
  if (largest !== i) {
    [arr[i], arr[largest]] = [arr[largest], arr[i]];
    heapify(arr, n, largest);
  }
}

计数排序

统计数组中每个元素出现的次数,然后根据统计信息构建有序数组。

首先,通过 Math.min()Math.max() 函数找到数组中的最小值和最大值,然后确定计数数组的范围。然后,创建一个 countArr 数组,用于统计每个元素出现的次数。接着,遍历原始数组,将每个元素映射到 countArr 数组中,并递增对应元素的计数值。

然后,通过累积计数数组,将计数数组中的每个元素更新为小于等于当前索引的元素总数。这样,countArr 数组中的值表示原数组中小于等于该元素值的元素总数。

接下来,从后往前遍历原始数组,根据元素值从 countArr 中获取位置,然后将元素放入输出数组 output 中,同时将对应计数数组的计数值减少 1。

最后,将 output 数组的值赋给原始数组,完成排序。

js复制代码function countingSort(arr) {
  const max = Math.max(...arr);
  const min = Math.min(...arr);
  const range = max - min + 1;
  const countArr = new Array(range).fill(0);
  const output = new Array(arr.length);
  
  for (let i = 0; i < arr.length; i++) {
    countArr[arr[i] - min]++;
  }
  for (let i = 1; i < range; i++) {
    countArr[i] += countArr[i - 1];
  }
  for (let i = arr.length - 1; i >= 0; i--) {
    output[countArr[arr[i] - min] - 1] = arr[i];
    countArr[arr[i] - min]--;
  }
  for (let i = 0; i < arr.length; i++) {
    arr[i] = output[i];
  }
  return arr;
}

桶排序

将待排序的数据元素分散到不同的桶中,然后对每个桶内的元素进行排序,最后将所有桶的元素按顺序合并起来,形成有序数组。

首先,通过 Math.min()Math.max() 函数找到数组中的最小值和最大值,然后确定需要的桶的数量。接着,创建一个数组 buckets,用于存放不同的桶,以及将待排序元素分散到各个桶中。每个元素根据其值映射到相应的桶中。

然后,对每个桶内的元素进行排序,这里使用了插入排序作为每个桶内部的排序算法。对每个桶使用插入排序的原因是,桶的数量可能较小,而插入排序在小规模数据的排序上性能较好。

最后,将排序后的各个桶的元素按顺序合并到原始数组中,完成整个排序过程。

js复制代码function bucketSort(arr, bucketSize = 5) {
  if (arr.length === 0) {
    return arr;
  }
  const minValue = Math.min(...arr);
  const maxValue = Math.max(...arr);
  const bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
  
  const buckets = new Array(bucketCount);
  for (let i = 0; i < bucketCount; i++) {
    buckets[i] = [];
  }
  for (let i = 0; i < arr.length; i++) {
    const bucketIndex = Math.floor((arr[i] - minValue) / bucketSize);
    buckets[bucketIndex].push(arr[i]);
  }
  arr.length = 0;
  for (let i = 0; i < bucketCount; i++) {
    insertionSort(buckets[i]);
    arr.push(...buckets[i]);
  }
  return arr;
}

基数排序

将待排序的非负整数按照个位、十位、百位等位数依次进行分配和收集,从低位到高位逐渐完成排序。

首先,通过 getMaxDigit 函数找到数组中最大元素的位数。然后,从个位开始到最高位,循环进行位数的分配和收集操作。

在每一轮循环中,创建一个桶列表 bucketList,其中包含 10 个桶(0 到 9)。然后遍历数组中的每个元素,根据当前位数的值将元素放入对应的桶中。之后,将桶列表中的元素按顺序取出并拼接,更新原数组,以便进入下一轮循环。

最终,经过所有位数的循环,数组中的元素将会被依次排列成有序的序列。

js复制代码function radixSort(arr) {
  const maxDigit = getMaxDigit(arr);
  for (let digit = 0; digit < maxDigit; digit++) {
    const bucketList = Array.from({ length: 10 }, () => []);
    for (let i = 0; i < arr.length; i++) {
      const digitValue = getDigitValue(arr[i], digit);
      bucketList[digitValue].push(arr[i]);
    }
    arr = bucketList.flat();
  }
  return arr;
}

function getMaxDigit(arr) {
  let max = 0;
  for (let i = 0; i < arr.length; i++) {
    max = Math.max(max, arr[i].toString().length);
  }
  return max;
}

function getDigitValue(num, digit) {
  return Math.floor(Math.abs(num) / Math.pow(10, digit)) % 10;
}
  • 26
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当谈到前端面试八股文时,以下是一些常见的问题及其答案: 1. 简要介绍一下前端开发的基本工作流程。 前端开发的基本工作流程包括需求分析、界面设计、编码实现、调试测试和部署上线。 2. 请解释一下浏览器的工作原理。 浏览器的工作原理主要包括四个步骤:解析HTML生成DOM树、解析CSS生成CSSOM树、合并DOM树和CSSOM树形成渲染树、将渲染树绘制到屏幕上。 3. 什么是跨域?如何解决跨域问题? 跨域是指在同源策略限制下,不同域之间进行资源请求或数据传输的过程。解决跨域问题可以通过代理服务器、JSONP、CORS等方式。 4. 请解释一下闭包的概念及其使用场景。 闭包是指函数可以访问和操作其词法作用域外的变量。闭包常用于创建私有变量、实现模块化等场景。 5. 请简要解释一下事件冒泡和事件捕获。 事件冒泡是指事件从最具体的元素开始触发,然后逐级向上传播至最不具体的元素。事件捕获则相反,是从最不具体的元素开始触发,然后逐级向下传播至最具体的元素。 6. 请解释一下防抖和节流函数的作用及其区别。 防抖函数用于减少触发频率,延迟执行函数。节流函数用于控制函数执行频率,限制单位时间内函数的执行次数。区别在于防抖函数在最后一次触发后会等待一段时间再执行,而节流函数在单位时间内只执行一次。 7. 请解释一下SPA(单页应用)的概念。 SPA是指通过动态加载页面内容并使用前端路由实现页面切换的一种应用模式。整个应用只有一个HTML页面,通过Ajax等技术加载数据并更新页面内容。 这些问题只是前端面试八股文的一小部分,你可以通过阅读相关的面试题和经验总结来进一步准备。记住,在面试中除了回答问题,也要展示自己的项目经验和解决问题的能力。祝你好运!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值