手撕高频JS面试题,轻松获offer

209 阅读6分钟

前端面试中有一个重要的环节,也是面试者最担心的一个环节。对于"手撕代码"的考察需要面试者平时的总结和积累。比如leetcode不刷个40,50道你敢去大厂面试吗?我之前一直就是温水煮青蛙,出去面试了几次才被教各种做人。

一般来说,如果代码写得好,即使理论知识答的不够好,也有大概率通过面试的,其实很多的手写背后往往就考察了你对相关理论知识的认识。

浅拷贝&深拷贝

1、浅拷贝指的是创建新的数据,这个数据有这有着原始数据属性值的一份精准拷贝,如果属性值是基本类型,拷贝的是基本类型的值,如果是引用类型,拷贝的事内存地址,浅拷贝只拷贝一层。深层次的引用类型则是共享内存地址。

    function shallowClone(obj){
        const newObj={};
        for(let prop of obj){
            if(obj.hasOwnProperty(prop)){
                newObj[prop] = obj[prop]
            }
        }
        return newObj;
    }

2、深拷贝开辟一个新栈,两个对象属性完全相同,但对应不同的地址,修改一个对象的属性,不会改变另一个对象的属性。

    function deepClone(obj,hash=new WeakMap()){
        if(obj===null) return obj;
        if(obj instanceof Date) return new Date(obj);
        if(typeof obj!=="object") return obj;
        // 如果是对象的话,进行深拷贝
        if(hash.get(obj)) return hash.get(obj)
        let cloneObj = new obj.constructor();
        // 找到的是所属类原型上的constructor,而原型上的constructor指向的是当前类本身
        hash.set(obj,cloneObj)
        for(let key of obj){
         cloneObj[key]=deepClone(obj[key],hash)
        }
        return cloneObj
    }

防抖&节流

防抖,n秒后在执行改事件,如在n秒内重复触发,则重新计算。

    function debounce(fn,awit){
        let timer =nullreturn function(){
            let context = this;
            if(timer){
                clearTimeout(timer)
            }
            timer = setTimeout(()=>{
                fn.apply(context,arguments)
            },awit)
        }
    }

节流,n秒内只运行一次,若在n秒内重复触发,只有一个生效。

function throttle(fn,delay){
    let preTime = Date.now();
    return function(){
        let context = this;
        let nowTime = Data.now;
        // 如果两次时间超过了指定时间,则执行该函数
        if(nowTime - preTime >= delay){
            return fn.apply(context,arguments)
        }
    }   
}

实现call方法

Function.prototype.myCall = function myCall(){
    let [thisArg,...args ] = Array.form(arguments);
    if(!thisArg){
        thisArg = typeof window==="undefined" ? global : window
    }
    // this的指向是当前函数func
    // 为thisArg对象添加func方法,func指向myCall,所以func中this指向thisArg
    thisArg.func = this;
    // 执行函数
    let result = thisArg.func(...args)
    // thisArg 上并没有func属性,因此需要移除
    delete thisArg.func;
    return result;
}

实现apply方法

Function.prototype.myApply = function myApply(){
    // 第一个参数为this对象,第二个参数为数组。
    let [ thisArg,args] = Array.from(arguments);
    if(!thisArg){
        thisArg = typeof window === "undefined" ? global : window;
    }
    // this 的指向是当前函数,func
    thisArg.func = this;
    // 执行函数
    let result = thisArg.func(...args);
    // thisArg 上并没有func属性,因此需要移除
    delete thisArg.func;
    return result;
}

渲染几万条数据不卡住页面

setTimeout(()=>{
  const total = 10000;
  // 一次插入的数据
  const once = 20;
  const loopCount = Math.ceil(total/once);
  let countOfRender = 0;
  const ul = document.querySelector("ul");
  // 添加数据的方法
  function add(){
    // createDocumentFragment 创建一个文档碎片,是一个虚拟节点,不是文档树,把所有的新节点附加其上,然后再将文档碎片的内容一次性添加,
    // 当我们把documentFragment 节点插入文档树时,插入的不是DocumentFragment本身,而是他的所有子孙节点,这使得DocumentFragment成了一个有用的占位符,暂时存放那些一次性插入文档的节点。
    /**
     * 当需要向页面添加许多DOM元素时,如果一个个createElement出来,然后在一个个appendChild上去,会频繁的操作DOM,很影响性能。
     */
    const fragment = document.createDocumentFragment();
    for(let i=0;i<once;i++){
      const li = document.createElement("li");
      li.innerText = Math.floor(Math.random() * total);
      fragment.appendChild(li)
    }
    ul.appendChild(fragment);
    countOfRender+=1;
    loop();
  }

  function loop(){
    if(countOfRender < loopCount){
      //requestAnimationFrame 类似于一个setInterval,它不需要设置时间间隔。他的时间间隔由系统定义,一般为16.67ms
      window.requestAnimationFrame(add)
    }
  }
},0)

手写new 操作符

    function myNew(Func,...args){
        // 创建一个对象
        const obj = {};
        // 新对象的原型指向构造函数的原型对象
        obj.__proto__ = FunC.prototype;
        // 将构造函数的this 指向新对象
        let result = Func.apply(this,args)
        // 判断返回值,如果是值类型,返回创建的类型,如果是引用对象,就返回这个引用对象
        return result instanceOf Object ? result : obj   
    }
    

手写一个数组分块chunk

    function chunk (arr,size){
       if(arr.length===0 || size===1){
            return arr;
       }
       let arr2=[];
       for(let i=0;i<arr.length;i+=size){
       // slice 从已有数组中返回选定的元素,返回一个新数组,包含从start到end,不包含end,不会改变原数组。
       // end 参数没传,默认表示从start到数组结束的所有元素
           arr2.push(arr.slice(i,i+size)
       }
       return arr2;
    }

leetCode 1941. 检查是否所有字符出现的次数相同

    function areOccurrencesEqual(s){
        let map=new Map();
        for(let i=0;i<s.length;i++){
            if(!map.get(s[i]){
                map.set(s[i],1)
            }else{
            map.set(s[i],map.get(s[i])+1)
        }
        return [...new Set([...map.values()])].length === 0   
        // return [...map.values()].every(i=>map.get[0]==i)
    }
    console.log(areOccurrencesEqual("abacbc")) // true

leetCode 448. 找到所有数组中消失的数字

给你一个含n个整数的数组nums,其中nums[i]在区间[1,n]内,请你找出所有在[1,n]范围内但没有出现在nums中的数字,并以数组的形式返回。

    // 解法1
    function findDisappearedNumber(nums){
        let res=[];
        for(let i=1;i<nums.length+1;i++){
            if(!nums[i].includes(i)){
                res.push(i)
            }
        }
        return res;
    }
     console.log(maxProfit([4,2,3,7,8,2,3,1])) // [5,6] 

leetCode 121. 买股票的最佳时机

给定一个数组price,他的第i个元素price[i]表示一支给定股票第i天的价格。 你只能选择某一天买入这只股票,并在未来的某天卖出。使的你获得能获取的最大的利润。

    function maxProfit(price){
        // minPrice 先定义第一天为最低价
        let profit = 0, minPrice=price[0];
        // 遍历数据
        for(let i=1;i<price.length;i++){
            // 如果发现比最低价还低,更新最低价
            minPrice = Math.min(minPrice,price[i]);
            // 如果发现利润比之前还大,更新利润
            profit = Math.max(profit,price[i]-minPrice)
        }
        return profit
    }
     console.log(maxProfit([7,1,5,3,6,4])) // [5]   

leetCode 349. 两个数组的交集

    function intersection(nums1,nums2){
        let arr=[];
        for(let ch of nums1){
            if(nums2.includes(ch) && !arr.includes(ch)){
                return arr.push(ch)
            }
        }
        return arr
    }
    console.log(intersection([4,9,5],[9,4,9,8,4])) // [4,9]

leetCode 136. 只出现一次的数字

除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

    // a^a = 0
    // a^0 = a
    // a^b^a = (a^a)^b = 0^b=b
    function singleNumber(nums){
        let single = 0;
        for(let ch of nums){
            single = single ^ ch
        }
        return single
    }
    console.log(singleNumber(2,2,1)) // 1

leetCode 14. 最长公共前缀

查找字符串数组中最长的公共前缀,如果不存在公共前缀,返回空字符串""。

    function longestCommonPrefix(strs){
        if(!strs.length) return "";
        let res = strs[0];
        for(ch of strs){
            for(let i=0;i<res.length;i++){
                if(ch[i]!==res[i]){
                    res = res.slice(0,i)
                }
            }
        }
        return res;
    }
    console.log(longestCommonPrefix(["flower","flow","flight"])) // fl

leetCode 20.有效的括号

给定一个只包括"(",")","[","]","{","}"的字符串a,判断字符串是否有效。 有效字符串需满足,左括号必须用相同的右括号闭合,左括号必须以正常的顺序闭合。

function isValid(s){
    let s1 = s.length;
    // 字符串长度为奇数,肯定是不匹配
    if(s1 % 2) return false;
    let map = {
        ")":"(",
        "]":"[",
        "}":"{",
    }
    let stack =[];
    for(let i of s){
        let topIndex = stack.length-1;
        // 获取栈顶的元素
        let top = stack[topIndex] ? stack[topIndex]:-1;
        if(map[i]==top){
            stack.pop();
        }else{
            stack.push(i)
        }
    }
    return !stack.length;
}

找出有序数组中与目标值最近的元素

1、使用reduce

 // 如 1 2 3 6 7 8 ,目标值为4,返回 2
 getClosestNumber(arr,target){
     return arr.reduce((pre,next)=>{
         return Math.abs(pre-target) < Math.abs( next-target) ? pre : next
     },{})
 }
 // console.log(getClosestNumber2([1,3,4,5],2))  // 1

2、二分法

 getClosestNumber2(arr,target){
    let left = 0;
    let right = arr.length -1;
    let mid = ""
    while( right - left > 1){
        mid = Math.floor((right + left)/2);
        if(arr[mid] > target){
            right = mid;
        }else{
            left = mid
        }
    }
    return Math.abs(arr[left]-target) > Math.abs(arr[right]-target) ? arr[right] : arr[left]
 }  
console.log(getClosestNumber3([1,2,4,6,7,4,5],4),"二分法求")   // 4

大数相加

// 这里只考虑数字字符串相加
function bigNumber(str1,str2){
    const arr1 = str1.split("").reverse();
    const arr2 = str2.split("").reverse();
    const length = Math.max(arr1.length,arr2.length);
    let flag = 0; // 是否需要进位
    let res = [];  
    for(let i=0;i<length;i++){
        const num1 = Number(arr1[i] || 0);
        const num2 = Number(arr2[i] || 0);
        let sum = num1 + num2 + flag
        if(sum > 10){
            sum = sum % 10;
            flag = 1;
        }else{
           flag = 0
        }
        res.push(num);
        if(i===length-1 && flag){  
            res.push(flag)
        }
    }
    return res.reverse().join("")
}

手动实现发布订阅模式

发布订阅模式,他其实是对象间一对多的依赖关系,一个对象状态发生改变时,所有依赖他的对象都将得到状态改变的通知

class EventEmitter{
    constructor(){
        this.cache = {}
    }
    // 发布事件
    on(eventName,fn){
        // 判断是否发布过事件名称 ?添加发布 :创建并添加发布
        if(this.cache[eventName]){
            this.cache[eventName].push(fn)
        }else{
            this.cache[eventName]=[fn]
        }
    }
    // 订阅事件
    emit(eventName){
        if(!eventName) throw new Error("请传入事件名")
        // 获取订阅的参数
        const data = [...arguments].slice(1)
        if(this.cache[eventName]){
            this.cache[eventName].forEach((i)=>{
                try {
                  i(...data)  
                } catch (error) {
                  console.log(e,"eventName" + eventName)
                }
            })
        }
    }
    // 取消订阅
    off(eventName,fn){
        // 不传入参数时,全部取消订阅
        if(!argument.length){
            return this.cache = {}
        }
        // eventName 传入是一个数组时,取消多个订阅
        if(Array.isArray(eventName)){
            eventName.forEach((event)=>{
                 this.off(event,fn)
            })
        }
        // 不传fn时取消事件名下所有的列队
        if(arguments.length==1 || !fn){
            this.cache[eventName] = []
        }
        // 取消事件名下所有的列队
        this.cache[eventName] = this.cache[eventName].filter(f=>f!==fn)
    }
}

移动零

给定一个数组,将所有的0都移动到数组的末尾。

function zero(arr){
    let res = []
    let j=0;
    for(let i=0;i<array.length;i++){
      if(array[i]!==0){
         res.push(array[i])
      }else{
         j++
      }
    } 
  for(let i=j;i<array.length;i++){
    res.push(0)
  }
  return res;
}
// console.log(zeroMove([0,1,0,0,3,12,0,4,5,2,0])) // [1, 3, 12, 4, 5,2, 0,  0, 0, 0,0, 0]

统计对象的层数

function getLeval(obj){
    let result = 0;
    if(obj===null) return 0;
    
    function getObjLeval(params,level=0){
        if(typeof params==="object" && params!==null){
            Object.keys(params).forEach((item)=>{
                if(typeof item==="object" && item!==null){
                    getObjLeval(item,level+1)
                }else{
                    result = level +1 > result ? level +1 : result
                }
            })
        }else{
            result = level > result ? level : result
        }
    }
    getObjLeval(obj)
    return result
}

随机生成一个长度为10 的整数类型数组

例如:[2,10,3,35,5,11,10,11,20]将其排列成一个新数组,要求新数组的形式如下 [[2,3,5],[10,11],[20],[35]]

// 生成0-99 之间的随机数字
// 生成5-10之间的随机数,Math.random()生成0到1, 0-1 * (5 -10)
function randomNumber(min,max){
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random()*(max-min +1))
}
let initArray = Array.from({length:10},()=>randomNumber(0,99));
// 去重
initArray = new Set([...initArray]);

const map = {};
initArray.forEach((item)=>{
    const key = Math.floor(item / 10);
    if(map[key]){
        map[key]=[]
    }
    map[key].push(item)
})
const result = [];
for(let key of map){
    result.push(map[key])
}

无重复字符串的最长子串

给定一个字符串s,请你找出其中不含有重复字符的最长子串的长度。例如输入s="abcabcab",输出3,因为无重复最长子串是"abc",所以长度是3

    function longestSubstring(s){
        let arr = [];
        let max = 0;
        for(let i=0;i<s.length;i++){
            // indexOf 判断元素是否在数组中出现过
            let index = arr.indexOf(s[i]);
            if(index!==-1){
            // 出现过,则从数组开头到当前字符串全部截取调
                arr.splice(0,index+1)
            }
            // 放入新的字符
            arr.push(s[i])
            // 更新下最大值
            max = Math.max(arr.length,max);
        }
        return max
    }
    console.log(longestSubstring("pwwkew"))  // 3

数组转成树形结构

原数组如下:

 const data = [ { id: '01', name: '张大大', pid: '', job: '项目经理' }, 
 { id: '02', name: '小亮', pid: '01', job: '产品leader' }, 
 { id: '03', name: '小美', pid: '01', job: 'UIleader' },
 { id: '04', name: '老马', pid: '01', job: '技术leader' }, 
 { id: '05', name: '老王', pid: '01', job: '测试leader' }, 
 { id: '06', name: '老李', pid: '01', job: '运维leader' },
 { id: '07', name: '小丽', pid: '02', job: '产品经理' }, 
 { id: '08', name: '大光', pid: '02', job: '产品经理' }, 
 { id: '09', name: '小高', pid: '03', job: 'UI设计师' }, 
 { id: '10', name: '小刘', pid: '04', job: '前端工程师' },
 { id: '11', name: '小华', pid: '04', job: '后端工程师' }, 
 { id: '12', name: '小李', pid: '04', job: '后端工程师' }, 
 { id: '13', name: '小赵', pid: '05', job: '测试工程师' },
 { id: '14', name: '小强', pid: '05', job: '测试工程师' }, 
 { id: '15', name: '小涛', pid: '06', job: '运维工程师' } ]

转成树形结构:不使用递归

    function arrToTree(data){
      let tree = [];
      if(!Array.isArray(data)){
          return tree
      }
  // 将数组转成对象,id作为属性名,原来的数组里的对象作为属性值
      let map={};
      data.forEach((item)=>{
          map[item.id] = item
      })
  // 通过对象的属性名ID,来找到父节点,将存到map里的对象取出来放到父节点里的children数组中。
      data.forEach((item)=>{
          let parent = map[item.pid];
          item['label']=item.name;
          if(parent){
              (parent.children || (parent.children = [])).push(item)
          }else{
              tree.push(item)
          }
      })
      return tree
}
console.log(JSON.stringify(arrToTree(list)),"arrToTree");