前端面试部分手写题总结(一)

137 阅读7分钟

前言

最近工作任务太多,到了年底冲刺阶段,以至于没有时间浏览前端新的技术点,在每天偶尔的空闲时间,梳理了面试中遇到频率比较高的手写算法题,都是比较基础部分,也在此做个总结分享。

1.防抖

对于防抖的理解,即在频繁触发的事件结束指定的时间后,执行一次。如输入框输入查询。

/**
 *
 * @param {function} fn
 * @param {number} [ms=1000]
 * @return {function} 
 */
function debounce (fn, ms = 1000) {
  // 创建一个函数作用域,并初始化变量 timeout
  let timeout = null;
  return function () {
    if (timeout) {
      // 清除执行
      clearTimeout(timeout)
    }
    // 不再有事件,最后执行一次
    timeout = setTimeout(() => {
      // 调用函数并传入参数,绑定this
      fn.apply(this, arguments)
    }, ms)
  }
}
2.节流

对于节流的理解,即频繁触发的事件,加以控制按一定的时间间隔执行,如鼠标拖动窗口缩放。

function throttle (fn, ms = 200) {
  // 创建一个函数作用域,并初始化变量 timeout
  let timeout = null;
  return function () {
    // 上一次循环没有执行,则直接跳过
    if (timeout) {
      // 节流和防抖的区别,即在此处不会清除前一次创建
      return;
    }
    timeout = setTimeout(() => {
      fn.apply(this, arguments);
      // 可以执行下一次循环了
      timeout = null;
    }, ms)
  }
}
3.深拷贝

深拷贝,其主要就是依据数据值类型区分进行递归

function deepClone(value) {
  // 简单值,直接返回
  if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
    return value;
  }
  // undefined null
  if (typeof value === 'undefined' || (typeof value === 'object' && !value )) {
    return value;
  }
  // Date
  if (target instanceof Date) {
    return new Date(target);
  }
  // RegExp
  if (target instanceof RegExp) {
    return new RegExp(target);
  }
  // Array
  if (Array.isArray(value)) {
    const newArr = [];
    value.forEach(item => {
      newArr.push(deepClone(item))
    })
    return newArr;
  }
  // Object
  if (typeof value === 'object' && value) {
    const newObj = {};
    for (let k in value) {
      newObj[k] = deepClone(value[k])
    }
    return newObj
  }
  // function 函数拷贝的意义不大,可直接返回
  if (typeof value === 'function') {
    return value;
  }

}
4. call apply实现

call和apply的实现基本一致,区别在于传递参数,这里以call举例

Function.prototype._call = function(ctx){
  // ctx 指向修改对象,window 或者 obj
  // this 指向 _call的调用着 fn
  // 调用对象为function
  if (typeof this !== 'function') console.error('type error')
  // 默认指向的对象,如果不存在默认指向window
  ctx = ctx || window;
  // 唯一值
  let fn = Symbol();
  // _call函数的this指向调用函数,如fn._call(), _call函数里面的this指向fn, 即将fn作为ctx的一个属性指向的函数
  ctx[fn] = this;
  // args,保存第一个参数外的所有参数
  let args = [...arguments].splice(1);
  // ctx[fn],指向fn函数,调用即修改fn函数的this指向,并传入参数
  let result = ctx[fn](...args);
  // 删除属性
  delete ctx[fn];
  // 返回函数执行的结果
  return result;
}
5. 函数柯里化

柯里化,即接受多个参数的函数调用,可以分次传入参数调用

function curryFn (fn) {
  if (typeof fn !== 'function') {
    console.error('curryFn arguments required function')
  }
  // 返回一个函数
  return function curried () {
    // 判断当前函数已经接受的参数个数,与fn本身需要的参数个数是否一致,一致则执行fn并返回结果
    if (arguments.length >= fn.length) {
      return fn.apply(this, arguments)
    } else {
      // 缓存之前参数
      var arg = arguments;
      // 再返回一个匿名函数,接受剩余不够参数
      return function () {
        return curried.apply(this, [...arg, ...arguments]) // 之前参数,当前参数
      }
    }
  }
}
6. 函数组合

函数组合,是函数式编程对函数的使用技巧。

function compose (...funcs) {
  // funcs,传递的函数集合
  return function proxy(...args) {
    // args, 第一次调用函数传递的参数
    const funLen = funcs.length;
    // 没有fn,直接返回参数
    if (funLen.length === 0) {
      return args;
    }
    // 只有一个fn,执行fn并返回结果
    if (funLen.length === 1) {
      return funLen[0](...args)
    }
    // 多个fn
    funcs.reverse().reduce((x,y) => {
      return typeof x === 'function' ? y(x(...args)) : y(x)
    })
  }
}
7. 铺平数组

数组扁平化,即将一个多维的数组,展开成一个一维数组返回,其主要通过循环遍历数组每一项值,值为数组类型则进行递归。

/**
 * 数组扁平化
 *
 * @param {Array} arr
 * @return {Array} 
 */
function flatArr(arr) {
  // 非数组直接返回
  if(!Array.isArray(arr)) {
    return arr;
  }

  const _crr = [];
  // 递归
  arr.forEach(v => {
    if(Array.isArray(v)) {
     // 值为数组,递归
     _crr.push(...flatArr(v))
    } else {
      // 非数组,则直接添加
      _crr.push(v)
    }
  })
  return _crr;
}
8. arguments的注意点
function test(){
    console.log(arguments)
}
  • arguments是一个对应于传递给函数的参数的--类数组
  • 箭头函数没有arguments
  • 通过arguments可以访问和设置函数参数
9. 平行结构树数据构建树数据
/**
 * @describe 平行结构树数据转树结构
 *
 * @param {Array} [list]
 * @return {Array} 
 */
function listToTree (list = []) {
  // 限制元数据类型为数组
  if (!(list instanceof Array)) {
    return []
  }
  if (list.length === 0) {
    return []
  }
  const createObj = {};
  // map id--item
  for (let i = 0, l = list.length; i<l; i++) {
    if (!createObj[list[i].id]) {
      createObj[list[i].id] = list[i];
    }
  }
  // 构建树,利用了引用数据类型的特殊性
  for (let i = 0, l = list.length; i<l; i++) {
    if (list[i].pid) {
      createObj[list[i].pid].children ? createObj[list[i].pid].children.push(list[i]) : createObj[list[i].pid].children = [list[i]]
    }
  }
  const treeArr = [];
  // 过滤掉多余子节点,剩余根节点
  for (let k in createObj) {
    if (!createObj[k].pid) {
      treeArr.push(createObj[k])
    }
  }
  return treeArr;
}
10. 树数据扁平化
/**
 * @describe 树数据扁平化
 *
 * @param {Array} [tree]
 * @return {Array} 
 */
function flatTreeToArr(tree = []) {
  if (!(tree instanceof Array)) {
    return []
  }
  if (tree.length === 0) {
    return tree;
  }
  let createArr = [];
  for (let i = 0, l = tree.length; i < l; i++) {
    createArr.push(tree[i])
    // 递归
    if (tree[i].children && tree[i].children.length > 0) {
      createArr = createArr.concat(flatTreeToArr(tree[i].children))
    }
    // 删除原有children
    delete tree[i].children
  }
  return createArr;
}
11. 两个数组相加

说明,a = [1,2,3,4,5], b=[7,8,9],返回 [1,3,1,3,4],即从两个数组的右边开始相加,返回加之后的数组

/**
 * @describe 找出两个数组中长度较小的一个
 *
 * @param {*} l1
 * @param {*} l2
 * @return {*} 
 */
function findMin(l1,l2){
  return l1.length - l2.length > 0 ? l2 : l1;
}
/**
 * @describe 找出两个数组中长度较大的一个
 *
 * @param {*} l1
 * @param {*} l2
 * @return {*} 
 */
function findMax(l1, l2) {
  return l1.length - l2.length > 0 ? l1 : l2;
}
/**
 *
 *
*/
function addTwoNumbers(l1, l2) {
  if (l1.length === 0) {
      return l2
  }
  if(l2.length === 0){
      return l1
  }
  const ma = findMax(l1,l2)
  const mi = findMin(l1,l2)
  // 暂存相加
  const rnArr = [];
  // 进位值
  let add = 0;
  // 较大长度索引
  let maLength = ma.length - 1;
  // 倒序遍历较小长度一项
  for(i = mi.length - 1; i>=0; i--) {
      if(mi[i] + ma[maLength] >= 10) {
        // 相加大于10,进位1
        rnArr.unshift(mi[i] + ma[maLength] - 10 + add)
          add = 1;
      } else {
        // 相加小于10,进位0  
        rnArr.unshift(mi[i] + ma[maLength])
          add = 0;
      }
      // 较大索引左移动
      maLength--;
  }
  // 较大剩余未加部分
  const moreArr = ma.splice(0, ma.length - mi.length);
  // 是否含有进位
  add > 0 ? moreArr[moreArr.length - 1] = moreArr[moreArr.length - 1] + 1 : null;
  return [...moreArr,...rnArr]
};
12. 数字项数组最后一项加1

说明,即最后一项加1,返回加之后的数组,这里主要考虑了数组的长度不固定,很可能超过javascript的最大精度表示,所以采用了BigInt

function plusOne (digits) {
  // 空数组
  if(digits.length === 0) {
    return digits;
  }
  // 转为数字
  const numArr = BigInt(digits.join(''))
  // 加1和
  const total = numArr + BigInt(1);
  // 数字转数组,由于每一项是字符串,再转数字
  return total.toString().split('').map(item => Number(item))
};
13. 无限加

此方法跟函数柯里化比较像,不过柯里化是讲一个接受固定参数的函数柯里化,此处是函数不固定参数,可以随意调用

add(1)(2)(3) = 6

add(1)(2)(3)(4) = 10

  function moreAdd(x) {
    // 缓存和
    let sum = x;
    // 返回一个函数,接受参数并处理 和 + 新参数,再返回这个函数,无限循环
    let temp = function (item) {
      sum += item;
      return temp;
    }
    // 每个对象都有toString()方法,重写toString(),调用返回函数的toString()返回结果
    temp.toString = () => sum

    return temp;
  }
  
  console.log(moreAdd(1)(2)(3).toString())
14. 是否是回文字符串

回文,即左边开始读和右边开始读一样

/**
 * 验证一个字符串是否是回文,忽略大小写和其他字符
 *
 * @param {string} s
 * @return {boolean}
 */
function isPalinStr(str){
  // 空或1个字符
  if(str.length < 2) {
    return true;
  }
  // 匹配非 0-9,a-z
  const reg = /[^0-9a-z]/g;
  // 不区分大小写
  const smallStr = str.toLowerCase();
  // 忽略其他符号
  const realStr = smallStr.replace(reg, '');
  // 中间位置
  const midleIndex = parseInt(realStr.length / 2);
  let result = true;
  for(let i = 0; i < midleIndex; i++) {
    if(realStr.charAt(i) !== realStr.charAt(realStr.length - 1 - i)) {
      result = false;
      break;
    }
  }
  return result;

}
15. 有效的括号
/**
 *
 * ({[()]})
 * 利用栈,遍历字符串,遇到左括号,压入栈,遇到右括号,弹出栈顶的值必须和现在的右括号成对。
 * @param {string} str
 * @return {boolean}
 */
function isValidKuoHao(str){
  const keyMap = {
    "(": ")",
    "{": "}",
    "[": "]"
  }
  // 空字符 false
  if(str.length===0) {
    return false;
  }
  // 单字符
  if(str.length === 1) {
    // 一个左括号,false
    if(keyMap[str]) {
      return false
    }
    // 不是左括号,也不是右括号 ?
    return !keyMap[str] && !Object.values(keyMap).includes(str) ? true : false;
  }
  let result = true;
  // 模拟栈
  const keyArr = [];
  for(let i = 0, l = str.length; i < l; i++) {
    // 左括号,压栈
    if(keyMap[str.charAt(i)]) {
      keyArr.push(str.charAt(i))
    }
    // 右括号
    if(Object.values(keyMap).includes(str.charAt(i))) {
      // 先弹出一个值
      if(keyMap[keyArr.pop()] !== str.charAt(i)) {
        // 弹出的左括号如果和现在的右括号不成对,直接返回false
        result = false;
        break
      }
    }
  }
  // 栈为空,则是有效的括号,否则不是
  keyArr.length > 0 ? result = false : null;

  return result;
}
16. 最长公共前缀

给定一个数组['asd', 'as', 'asdfr'],寻找该数组公共前缀最长字符

/**
 *
 *
 * @param {string[]} strs
 * @return {string}
 */
function longestCommonPrefix(strs){
  // 空数组
  if (strs.length === 0) {
    return '';
  }
  // 只含一位,公共全部
  if (strs.length === 1) {
    return strs[0]
  }
  // 保存数组每一项字符串长度
  const itemStrLengthmap = {};
  strs.forEach((item, index) => {
    itemStrLengthmap[index] = item.length;
  })
  // 获取所有字符串长度
  const lengthArr = Object.values(itemStrLengthmap);
  // 包含空字符串
  if(lengthArr.includes(0)){
    return ''
  }
  let result = '';
  // 最小长度
  const minLen = Math.min.apply(null, lengthArr);
  w:for(let k =0; k <= minLen; k++) {
    const activeStr = strs[0].charAt(k);
    i:for(let i = 0, l = strs.length; i<l; i++) {
      if(strs[i].charAt(k) !== activeStr) {
        break w;
      }
      if(i === l - 1) {
        result += activeStr;
      }
    }
  }
  return result;
}
17. 合并两个有序数组
/**
 * 合并两个有序数组
 *
 * @param {Array} list1
 * @param {Array} list2
 * @return {Array} 
 */
function mergeTwoLists (list1,list2) {
  // 记录插入的位置,下次遍历从此开始,因为是有序数组
  let forIndex = 0;
  // 遍历list1,向list2插入
  w:for(let i = 0, l = list1.length; i<l; i++) {
    i:for(let k = forIndex; k < list2.length; k++) {
      if(list1[i] <= list2[k]) {
        list2.splice(k, 0, list1[i])
        // 记录插入位置
        forIndex = k;
        break i;
      }
      if(k === list2.length - 1 && list1[i] > list2[k]) {
        // 添加最后
        list2.push(list1[i])
        // 记录插入位置
        forIndex = k + 1;
        break i;
      }
    }
  }
  return list2
}
18. 未重复字符串最长字符

举例,qqqqweeee,未重复最长字符为,qwe

var lengthOfLongestSubstring = function(s) {
  if(s.length < 2) {
     return s
  }
  const obj = {};
  const toMap = function (s) {
    // 字符串转数组
    const arr = s.split('');
    // 保存未重复字符
    const startArr = [];
    for(let i = 0, l = arr.length; i < l; i++) {
        if (startArr.includes(arr[i])) {
          // 保存当前未重复字符
          obj[startArr.length] = startArr.join('')
          const findRepeatOldIndex = startArr.findIndex(n => n === arr[i])
          // 未重复字符串保留当前重复位置的右侧字符
          const concatArr = startArr.splice(findRepeatOldIndex + 1);
          // 右侧字符和未遍历字符🈴️成新的字符串,重新开始寻找
          const yuArr = [...concatArr, ...arr.splice(i)];
          yuArr.length > 0 ?toMap(yuArr.join('')) : null;
          break;
        } else {
          startArr.push(arr[i])
        }
    }
    obj[startArr.length] = startArr.join('')
  }

      toMap(s)
 
     return obj[Math.max(...Object.keys(obj))]
 };
19. 原地删除有序数组重复项

说明,重点在于原地删除,即返回的数组等于原数组

/**
 *
 *
 * @param {Array} arr
 * @return {number} 
 * @descripe 原地删除有序数组重复项
 */
// 原地删除,即返回的数组==原数组
function removeRepeatArr(arr){
  // 移动卡尺
  let step = 0;
  for(let i = step; i < arr.length; i+=step) {
    if(arr[i] === arr[i + 1]) {
      // 比较当前元素和下一位元素是否重复,重复则删除下一位,循环位置不变,继续与下一个元素比较
      arr.splice(i+1, 1)
      // 不移动
      step = 0
    } else {
      // 当前元素和下一个原属不重复,循环位置递增1,移动下一位
      step = 1;
    }
  }
  return arr;
}

写在最后

刀越磨越利,脑越用越灵,一起加油!