前端面试手撕代码

195 阅读9分钟

持续更新中...🥬就多练! 前端必备 or 我面试过的 欢迎指正!!!

1、new

面试

function myNew(){
    // 新建一个对象
    const obj = new Object()
    // 获取arguments的第一个参数并返回
    Constructor = [].shift.call(arguments)
    
    obj.__proto__ = Constructor.prototype
    
    const ret = Constructor.apply(obj,arguments)

    return typeof ret === 'object' ? ret : obj
}

2、图片懒加载

面试

<body>
    <img src = '' class = 'image-item' data-original = "./1.svg"  lazyload = 'true'/>
    <img src = '' class = 'image-item' data-original = "./2.svg"  lazyload = 'true'/>
    <img src = '' class = 'image-item' data-original = "./3.svg"  lazyload = 'true'/>
    ...
    
    <script>
        // 获取视口高度
        var viewHeight = document.body.clientHeight || window.clientHeight || document.documentElement.innerHeight
        function lazyLoad (){
            var eles = document.querySelectAll('img[lazyload][data-original]')
            eles.forEach((item,index) => {
                if(!item.dataset.original) return 
                var rect = document.getBoundingClientRect() // 获取相对视口的高度
                if(rect.top < viewHeight && rect.bottom >= 0){
                    // 在可视范围内
                    var 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>

3. call、bind、apply

3.1 call

// 使用
var foo = {
    value:1
}
function bar(){
    console.log(this.value)
}
bar.call(foo)

Function.prototype.myCall = function(context,...args){
    if(typeof context === 'undefined' || context == null){
        context = window
    }
    // 声明一个属性
    var fnSymbol = new Symbol()
    // 给属性赋值为this
    context[fnSymbol] = this
    // 执行
    var fn = context[fnSymbol](...args)
    
    // 删除这个属性
    delete context[fnSymbol]
    return fn
}

3.2 apply

// 思想和call一样,入参变为数组
Function.prototype.myApply = function(context,arr){
    if(typeof context === 'undefined' || context == null) context = window
    
    const fnSymbol = new Symbol()
    context[fnSymbol] = this
    const fn = context[fnSymbol](...arr)
    delete context[fnSymbol]
    return fn
}

3.3 bind

// 用apply或call实现,返回一个函数
Function.prototype.myBind = function(context){
    if(typeof context === 'undefined' || context == null) context = window
    const self = this
    return function(...args){
        return self.apply(context, args)
    }
}

4.防抖和节流

面试

4.1 防抖

// 一段时间内如果重复触发,只在最后触发之后的wait秒后执行,适用于搜索框
function debounce(func,wait){
    let timeout;
    return function(){
        const context = this
        const args = arguments
        clearTimeout(timeout)
        timeout = setTimeout(()=>{
            func.apply(context,args)
        },wait)
    }
}

4.2 立即执行防抖

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

4.3 hook防抖

import {useRef, useState} from 'react'

export function UseDebounce(value, delay = 500){
    const [debounceValue, setDebounceValue] = useState('')
    const timer = useRef()
    
    useEffect(()=>{
        if(timer.current) clearTimeout(timer)
        
        timer.current = setTimeout(()=>{
            setDebounceValue(value)
        },delay)
        
        return () => {
            if(timer.current) clearTimeout(timer)
        }
    },[delay,value])
    
    return debounceValue
}

4.4 节流

// 在固定的时间内,只能执行一次时间,适用于用户滚动
function throttle(func,wait){
    let previous = 0
    return function(){
        const context = this
        const args = arguments
        let now = +Date.now()
        if(now - previous > wait){
            func.apply(context,args)
            previous = now
        }
    }
}

5、异步任务并发控制器

面试

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) => a.priority - b.priority).shift()
        const promise = fn()
        promise.then(this.completeOne.bind(this)).catch(this.completeOne().bind(this))
    }
    
    completeOne(){
        this.currentCount--
        this.run() // 如果线程池里还有,继续执行
    }
}

// 答案2:

/**
 * 并发池执行异步任务
 * @param {number} poolLimit - 最大并发数
 * @param {Array<() => Promise>} tasks - 返回 Promise 的工厂函数数组
 * @returns {Promise<Array>} - 按任务顺序 resolve 的结果数组
 */
async function asyncPool(poolLimit, tasks) {
  const results = new Array(tasks.length);
  const executing = new Set();          // 当前正在跑的 Promise

  const wrap = (i) =>
    tasks[i]().then((res) => {
      results[i] = res;                 // 按顺序保存结果
      executing.delete(wrap(i));        // 释放池位
    });

  for (let i = 0; i < tasks.length; i++) {
    const p = wrap(i);
    executing.add(p);
    if (executing.size >= poolLimit) {
      await Promise.race(executing);    // 任一完成再放下一个
    }
  }
  await Promise.all(executing);         // 等最后一批收尾
  return results;
}

/* ---------------- 测试 ---------------- */
const tasks = Array.from({ length: 10 }, (_, i) => {
  const cost = Math.random() * 1000;
  return () =>
    new Promise((resolve) =>
      setTimeout(() => resolve(`任务${i} 完成`), cost)
    );
});

(async () => {
  console.time('pool');
  const res = await asyncPool(3, tasks);
  console.timeEnd('pool');
  console.log(res); // ["任务0 完成", "任务1 完成", ..., "任务9 完成"]
})();

6、订阅发布

面试

class EventEmitter{
    constructor(){
        // 这里必须是一个map
        this.handlers = {}
    }
    
    // 注册事件
    on(eventName,cb){
        if(!this.handlers[eventName]){
            this.handlers[eventName] = []
        }
        this.handlers[eventName].push(cb)
    }
    
    // 触发事件
    emit(eventName,...args){
        if(!this.handlers[eventName]) return
        const callbacks = this.handlers[eventName].slice()
        callbacks.forEach(callback=>{
            callback(...args)
        })
    }
    
    // 注销事件的某个cb
    off(eventName,cb){
        if(!this.handlers[eventName]) return
        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)
    }
}

7、setTimeout实现setInterval

7.1 setTimeout

 // 递归
 // 参数:回调函数,间隔时间
 function mySetInterval(callback,interval){
     let timerId;
     
     // 递归这里
     function execute(){
         callback()
         timerId = setTimeoout(execute,interval)
     }
     
     // 初始执行
     timerId = setTimeoout(execute,interval)
     
     // 返回clearTimeout
     return {
         clear:function(){
             clearTimeout(timerId)
         }
     }
 }

7.2 setTimeout 高精度版

- 还是会js单线程的限制
- 但是每次调用都会校验一次精度
function setInterval(callback,interval){
    const expect = Date.now() + interval; // 用于计算精度
    const timer = () => {
        const diff = Date.now() - expect; // 和期望值之间的差距
        callback()
        
        setTimeout(timer,Math.max(interval - diff, 0))
    }
    
    setTimeout(timer,interval)
}

8、数组扁平化

面试

8.1 普通递归

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

8.2 reduce

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

8.3 flat

function flatten(arr,depth = 1){
    return arr.flat(depth) // 默认Infinity
}

9. 树的层序遍历

面试

树结构:
            A
          / | \
         B  C  D
        / \    |
       E   F   G
          / \
         H   I

9.1 简洁版

function levelOrder(root){
    if(!root) return
    const queue = [root]
    const result = []
    
    while(queue.length){
        const node = queue.shift() // 队列队头拿一个元素
        result.push(node.val)
        queue.push(...node.children)
    }
    
    return result
}
// 结果: [A,B,C,D,E,F,G,H,I]

9.2 层级版

function levelOrder(root){
    if(!root) return []
    const result = []
    const queue = [root]
    while(queue.length){
        const size = queue.length
        const currentLevel = []
        
        for(let i = 0; i < size; i++){
               const currentNode = queue.shift() // 队头取出节点
               currentLevel.push(currentNode.value)
               
               // queue放入左右子节点
               if(currentLevel.lnode){
                   queue.push(currentLevel.lnode))
               }
               if(currentLevel.rnode){
                   queue.push(currentLevel.rnode))
               }
        }
        result.push(currentLevel)
    }
    return result
}
// 结果 [['A'],['B','C','D'],['E','F','G'],['H','I']]

10.拷贝

面试

10.1 浅拷贝

  • Object.assign()
  • Array.prototype.concat()
  • Array.prototype.slice()
// 拷贝的值,不是地址,所以一旦复制后改变了值,原始数据也跟着改变
function shallowCopy(obj){
    if(typeof obj !== 'object') return
    
    const newObj = Array.isArray(obj) ? [] : {}
    
    for(var key in obj){
        if(obj.hasOwnProperty(key)){
           newObj[key] = obj[key] 
        }
    }
    return newObj

}

10.2 深拷贝

  • JSON.stringify(JSON.parse())
// 使用WeakMap,避免Map强引用类型,可以自动垃圾回收,避免内存泄露
function 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)
    
    const newObj = Array.isArray(obj) ? [] : {}
    // 存起来,用于下次缓存
    hash.set(obj, newObj)
    // Reflect可以遍历枚举和Symbol类型
    Reflect.ownKeys(obj).forEach(key=>{
        newObj[key] = deepClone(obj[key],hash)
    })
    return newObj
}

11.去重

11.1 new Set()

function unique(arr){
    return [...new Set(arr)]
}

11.2 indexOf + filter

function unique(arr){
    return arr.filter((item,index) => arr.indexOf(item) === index)
}

11.3 reduce + includes

function unique(arr){
    return arr.reduce((accu,val) =>{
        return arr.includes(val) ? accu: [...accu,val]
    },[])
}

11.4 处理NaN

function unique(arr){
    const result = []
    const seen = new Set()
    const flag = false
    for(const ele of arr){
        if(Number.isNaN(ele)){
            if(!flag){
                result.push(NaN)
                flag = true
            }
            continue
        }
        
        if(!seen.has(ele)){
            seen.add(ele)
            result.push(ele)
        }
    }
    return result
}

11.5 处理对象

function unique(arr){
    const result = []
    const seen = new WeakSet()
    
    for(const ele of arr){
        if(typeof ele === 'obj' && ele !== null){
            if(!seen.has(ele)){
                seen.add(ele)
                result.push(ele)
            }
        }else{
            if(!result.includes(ele)){
                result.push(ele)
            }
        }
    }
    return result
}

11.6 js实现有序数组原地去重

/**
 * @param {number[]} nums
 * @return {number}
 */
 // 方法1: 直接新开一个数组,如果和上一个不相同就放进去
 // 方法2: 快慢指针,快指针先走,和慢指针做对比
 // 1 2 2 3
 // i = 0, j = 1 
var removeDuplicates = function(nums) {
   if(nums.length < 2) return nums.length
   let slow = 0;
   for(let fast = 0; fast< nums.length; fast++){
        if(nums[slow] !== nums[fast]){
            slow++
            nums[slow] = nums[fast]
        }
   }
   return slow + 1
};

12.compose

12.1 普通版

面试

// 上一个函数的返回值作为下一个函数的入参
function fn1(x){
    return x + 1
}
function fn2(x){
    return x + 2
}
function fn3(x){
    return x + 3
}
const fn = compose(fn1,fn2,fn3)
// 用fn2调用,fn1的返回值 => next(prev)
console.log(fn(1)) // 7

// 实现
// 一定是返回一个可调用的function
function compose(){
    const args = [...arguments]
    return function(num){
        return args.reduce((prev,next)=>{
            return next(prev)
        },num)
    }
}

function compose(fns){
    // 参数
    return num => {
        return fns.reduce((prev,next) => next(prev),num)
    }
}

12.2 洋葱圈版

13.柯里化

面试

// 只传一部分的参数,让它的返回值去处理剩下的参数
// 返回值同样也是函数,如果传入的参数 > 函数本身的参数,函数调用结束,直接执行,否则继续调用,返回的还是函数
// fn指的传入的是参数
function curry(fn){
    return function curried(...args){
        // 如果参数足够,直接调用原函数
        if(args.length >= fn.length){
            return fn.apply(this,args)
        }else{
            // 如果参数不够,返回新的柯里化
            return function(...nextArgs){
                return curried.apply(this,args.concat(nextArgs))
            }
        }
    }
}
// 如果实现一个currysum(1)(2)返回结果 3
const sum = (a, b) => a + b;
const currysum = curry(sum);

console.log(currysum(1)(2)); // 3

14.requestAnimationFrame

14.1 实现setTimeout

// 本质是递归
// requestAnimationFrame和页面渲染时间同步,且在后台时会暂停执行,性能优化
function setTimeoutRAF(callback,delay){
    let start = performance.now()
    
    function checkTime(currentTime){
        const elapsed  = currentTime - start; // 60hz执行一次,1000ms/16.67
        if(elapsed >= delay){
            callback()
        }else{
            requestAnimationFrame(checkTime)
        }
    }
    // 初始执行
    requestAnimationFrame(checkTime)
}

14.2 实现平移动画

// 递归,每16.67ms调用一次
const box = document.getElementById('box')
const container = document.getElementById('container')
const speed = 2
let pos = 0
const maxPos = container.offsetWidth - box.offsetWidth 

function animate(){
    // 每次调用走的距离
    pos += speed;
    // 边界
    if(pos >= maxPos) pos = maxPos
    // 更新位置
    box.style.left = pos + 'px'
    
    if(pos < maxPos) requestAnimationFrame(animate)
}
requestAnimationFrame(animate)

15、写一个react组件

react组件A,传入src='hello world'和target = 'world'让target高亮显示。

import React from 'react'
const HighlightText = ({src,target} => {
    if(!target){
        return <span>{src}</span>
    }
    const parts = src.split(target)
    return (
        <>
            {parts.map((part,index)=>(
                <span key={index}>
                    {part}
                    {index < parts.length - 1 && <mark>{target}</mark>}
                </span>
            ))}
        </>
    )
})
export default HighlightText

16、promise

面试

16.1 Promise.all

function myPromiseAll(promises){
    const result = []
    let count = 0
    return new Promise((resolve,reject) => {
        const addData = (value,index) => {
            result[index] = value
            count++
            if(count === promises.length) resolve(result)
        }
        promises.forEach((promise,index) => {
             if(promise instanceof Promise){
                 promise.then(res => {
                     addData(res,index)
                 })
                 .catch(err=> reject(err))
             }else{
                 addData(promise,index)
             }
        })
    })
}

16.2 primise.all用var和for编程

function myPromiseAll(promises){
    var result = []
    var count = 0
    return new Promise((resolve,reject) => {
        for(var i = 0; i < promises.length; i++){
            (function(i){
                var promise = promises[i]
                promise.then(res => {
                    result[i] = res
                    count++
                    if(count == promises.length) resolve(result)
                })
                .catch(err => reject(err))
            })(i)
        }
    })      
}

16.3 Promise.race

function myPromiseRace(promises){
    return new Promise((resolve,reject) => {
        promises.forEach((promise,index) => {
            if(promise instanceof Promise){
                promise
                .then(res => resolve(res),
                err => reject(err))
            }else{
                resolve(promise)
            }
        })
    })
}

16.4 promise完整链路

class MyPromise{
    constructor(executor){
        this.initValue()
        this.initBind()
        try{
            executor(this.resolve,this.reject)
        }catch(e){
            this.reject(e)
        }
    }
    
    initValue(){
        this.result = null
        this.state = 'pending'
        
        this.onFulfilledCallbacks = []
        this.onRejectedCallbacks = []
    }
    
    initBind(){
        this.resolve = this.resolve.bind(this)
        this.reject = this.reject.bind(this)
    }
    
    resolve(value){
        if(this.state !== 'pending') return
        this.state = 'fulfulled'
        this.result = value
        while(this.onFulfilledCallbacks.length)
            this.onFulfilledCallbacks.shift()(this.result)
    }
    
    reject(reason){
        if(this.state !== 'pending') return
        this.state = 'rejected'
        this.result = reason
        while(this.onRejectedCallbacks.length)
            this.onRejectedCallbacks.shift()(this.result)
    }
    
    then(onFulfilled,onRejected){
        onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : val => val
        onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
        
        var thenPromise = new MyPromise((resolve,reject) => {
            const resolvePromise = cb => {
                const x = cb(this.result)
                
                if(x === thenPromise){
                    throw new Error('不能和返回值相同')
                }
                
                if(x instanceof MyPromise){
                    x.then(resolve,reject)
                }else{
                    resolve(x)
                }
            }
            
            if(this.state === 'fulfilled'){
                resolvePromise(onFulfilled)
            }else if(this.state === 'rejected'){
                resolvePromise(onRejected)
            }else{
                this.onFulfilledCallbacks.push(onFulfilled.bind(this))
                this.onRejectedCallbacks.push(onRejected.bind(this))
            }
        })
        return thenPromise
    }
}

17、数组增序排列

18、最长不重复字串

面试(最少三次)

    - 编写方法,返回最长无重复子串的长度
    - 无重复子串指:子串中每个字符都不相同
    - 例如:s = 'aaabcddddefghh' ,其中,最长的无重复子串为'defgh'长度为5
    // 滑动窗口原理,需要左右指针,根据滑动窗口更新最大长度
    function lengthOfLongestSubstring(s){
        let left = right = length = maxLength = 0
        const set = new Set()
        while(right < s.length){
            if(!set.has(s[right])){
                set.add(s[right])
                length++
                if(maxLength < length) maxLength = length
                right++
            }else{
                while(set.has(s[right])){
                    set.delete(s[left])
                    left++
                    length--
                }
                set.add(s[right])
                right++
                length++
            }
        }
        return maxLength
    }
      

19、结构体转换,数组结构和树结构相互转换

- 树和数组结构
const listTree = [
  {
    id: 1,
    name: '部门1',
    pid: 0,
    children: [
      {
        id: 2,
        name: '部门1-1',
        pid: 1,
        children: [
          {
            id: 4, 
            name: '部门1-1-1', 
            pid: 2,
            children: []
          }
        ]
      },
      {
        id: 3,
        name: '部门1-2',
        pid: 1,
        children: [
          {
            id: 5, 
            name: '部门1-2-1', 
            pid: 3,
            children: []
          }
        ]
      }
    ]
  },
  {
    id: 6,
    name: '部门2',
    pid: 0,
    children: [
      {
        id: 7, 
        name: '部门2-1', 
        pid: 6,
        children: []
      }
    ]
  },
  {
    id: 8,
    name: '部门3',
    pid: 0,
    children: []
  }
]

期望结果:
const list = [
  {id: 1, name: '部门1', pid: 0},
  {id: 2, name: '部门1-1', pid: 1},
  {id: 3, name: '部门1-2', pid: 1},
  {id: 4, name: '部门1-1-1', pid: 2},
  {id: 5, name: '部门1-2-1', pid: 3},
  {id: 6, name: '部门2', pid: 0},
  {id: 7, name: '部门2-1', pid: 6},
  {id: 8, name: '部门3', pid: 0},
]

19.1 树结构转换为数组结构

// 深度遍历
function treeToListDFS(tree){
    const result = []
    function traverse(node){
        const {children,...rest} = node
        result.push(rest)
        if(children){
            children.forEach(child=>traverse(child))
        }
    }
    return result
}

// 广度遍历
function treeToListBFS(tree){
    const result = []
    const queue = [...tree]
    while(queue.length){
        const node = queue.shift()
        const {children,...rest} = node
        result.push(rest)
        queue.push(...children)
    }
    return result
}

19.2 数组结构转换为树

// Map实现 o(n)
function arrToTree(lists){
    const result = []
    const map = new Map()
    
    lists.forEach(item=>{
        map.set(item.id,{...item,children:[]})
    })
    
    lists.forEach(item=>{
        const node = map.get(item.id)
        if(node.pid === 0){
            result.push(node)
        }else{
            const parent = map.get(node.pid)
            if(parent){
                parent.children.push(node)
            }
        }
    })
    return result
}

20、实现一个中间件(koa或者express)

21、正则匹配30,str = '1 apple cost 30$'

22.拍平键路径字符串版

输入:{a:{b:{c:1}}, d:[{e:2}]} 输出:拍平键路径字符串版 {'a.b.c':1, 'd[0].e':2} 进阶:反向实现「展开」函数,把拍平对象还原成深层结构,用于 localStorage 存配置

23、虚拟dom转换为真实dom的过程

24、找第K大元素

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
    const newNums = nums.sort((a,b) => a -b)
    return newNums[nums.length - k]
};

25、lodash中的get方法

26、['A,'B'.['A','C',['B','D'.['D”,A”],每个子数组表示一个路径的起点和终点。判断是否有环,并输出路径

27、UseRequest

面试

27.1 简易TS版本

import { useState,useEffect,useCallback } from "react";
interface Options<TData, TParams extends any[]>{
     fetchFn: (...args:TParams) => Promise<TData>;
     onSuccess: (data:TData) => void;
     onError: (error:Error) => void;
}

interface Result<TData, TParams extends any[]>{
    data:TData | undefined;
    error: Error | undefined;
    loading: boolean;
    run: (...args:TParams) => Promise<TData>
}

function UseRequest<TData, TParams extends any[]>(
    options:Options<TData, TParams>
): Result<TData, TParams>{
    const {fetchFn,onSuccess,onError} = options
    const [data,setData] = useState<TData>()
    const [loading,setLoading] = useState<boolean>(false)
    const [error, setError] = useState<Error>()
    const run = useCallback(async (...params:TParams):Promise<TData>=>{
        setLoading(true)
        try{
            const result = await fetchFn(...params)
            setData(result)
            onSuccess?.(result)
            return result
        }catch(error){
            const err = error instanceof Error ? error : new Error(String(error))
            setError(err)
            onError?.(err)
            throw err
        }finally{
            setLoading(false)
        }
    },[fetchFn,onSuccess,onError])
    useEffect(()=>{
        run(...([] as unknown as TParams))
    },[])
    return {data,error,loading,run}
}

// 使用
async function fetchUser(userId: number){
    const res = await fetchUser(`/api/user/${userId}`)
    return res.json()
}
UseRequest({
    fetchFn:fetchUser,
    onSuccess:(data) => {
    
    },
    onError:(err) => {
    
    }
})

28、数组排序 给有序数组nums1和nums2,长度为m + n和n,将nums2的元素插入nums1中,并排序

面试

 - 输入 nums1 = [1,2,4,5,6,0] nums2 = [3] m = 5 n = 1
 - 输出 nums1 = [1,2,3,4,5,6]
 
 
 // 方法1:逆向双指针的归并
 function merge(nums1,m,nums2,n){
     let i = m - 1, j = n - 1, idx = m + n - 1;
     while(i >=0 && j >= 0){
         if(nums1[i] > nums2[j]) nums1[idx--] = nums1[i--]
         else nums1[idx--] = nums2[j--]
     }
     while(j >= 0) nums1[idx--] = nums2[j--]
 }
 // 方法2: 插入之后排序
 function merge(nums1,m,nums2,n){
     nums1.splice(m, nums2.length, ...nums2)
     nums1.sort((a,b) => a - b)
 }

29、instanceof

30、括号匹配

// 括号匹配(含 {} [] () 三种)

// 输入:'({[()]})'
// 输出:true
// 进阶:实时监听 textarea 输入,边框变红提示非法,要求 16 ms 内反馈

31、版本号排序

输入:['2.10.1', '1.2.3', '2.1.0', '2.10.0'] 输出:升序 ['1.2.3', '2.1.0', '2.10.0', '2.10.1']

32、k个一组,翻转链表

/**
   1->2->3->4->5 k = 2
  a. 虚拟头节点 hair -> 1->2->3->4->5
  循环while(true)
  b. start = end = hair 让end找到k的一组的尾节点,用for用while都行,end不存在就跳出循环
  c. 存储startNext 和 endNext,此时 start = hair,end = 2 startNext = 1,是翻转后的尾节点 endNext = 3 是下一个链的头节点
  d. 前k个断链,end.next = null
  e. 翻转函数抽出来,返回翻转后的头节点pre
  f. start现在位于虚拟头节点,和pre连接,startNext位于翻转后的尾节点,和endNext连接,startNext变成下一个链的虚拟头节点
  g. start = end = startNext,构建下一个链的虚拟头节点
  h.返回hair.next
  
    reverse实现
    1->2->null
    pre = null cur = 1 next = 2
    while(cur)循环{
        cur.next = pre 1->null
        pre = cur; cur=next  pre = 1 cur = 2
        next = cur->next
    }然后循环
    2->1->null
    返回pre
    
*/
function reverse(head){
    let pre = null, cur = head;
    while(cur){
        const next = cur.next
        cur.next = pre;
        pre = cur; 
        cur = next;
    }
    return pre;
}
function reverseKGroup(head, k){
    // 虚拟头节点
    const hair = new ListNode()
    hair.next = head;
    let start = end = hair;
    while(true){
        // 找到一个链的头尾节点
        for(let i = 0; i < k && end; i++) end = end.next
        if(!end) break
        // startNext这个链翻转后的尾节点,endNext下一个链的头节点
        let startNext = start.next, endNext = end.next;
        end.next = null // 断链
        // 重新连接
        start.next = reverse(start)
        startNext.next = endNext
        start = end = startNext
    }
    return hair.next
}

33、翻转left和right之间的链表

// 记录一下left前的节点、left节点、right节点和right后的节点就可以了
// 示例:1->2->3->4->5 left = 2 right = 4
// 输入 1->4->3->2->5
function reverse(head){
    if(head == null || head.next == null) return head
    let cur = head,pre = null
    while(cur){
        const nex = cur.next
        cur.next = pre
        pre = cur
        cur = nex
    }
}
function reverseBetween(head,left,right){
    // 虚拟头节点
    const hair = new ListNode()
    hair.next = head
    // 找到左节点的前一个节点
    let start = hair
    for(let i = 0; i < left - 1; i++) start = start.next
    
    // 找到右节点
    let rightNode = start
    for(let i = 0; i < right - left + 1; i++) rightNode = rightNode.next
    
    // 找到左节点和右节点的下一个节点
    let leftNode = start.next
    let end = rightNode.next
    
    // 断链
    start.next = null
    rightNode.next = null
    
    // 翻转
    reverse(leftNode)
    
    // 再连上
    start.next = rightNode
    leftNode.next = end
    return hair.next
    
}

34、斐波那契数列

面试

// 方法1、直接递归 o(n^2) o(n)
function Fibonacci(n){
    if(n <= 1) return n
    return Fibonacci(n - 1) + Fibonacci(n - 2)
}

// 方法2、map缓存 o(n) o(n)
function Fibonacci(n, map = new Map()){
    if(n <= 1) return n
    if(map.has(n)) return map.get(n)
    
    const res = Fibonacci(n-1,map) + Fibonacci(n-2,map)
    map.set(n,res)
    return res
}

// 方法3、动态规划,数组存储,数组下标为n
function Fibonacci(n){
    if(n <= 1) return n
    const arr = []
    arr[0] = 0
    arr[1] = 1
    for(let i = 2; i <= n; i++){
        arr[i] = arr[i - 1] + arr[i - 2]
    }
    return arr[n]
}

35、统计网页 DOM 标签频次

输出:对象 {标签名: 出现次数},按次数降序排序 进阶: 用 document.querySelectorAll('*') 会卡吗?试试 NodeIterator 优化

36、求两个日期之间的「自然月」差

输入:'2022-01-31'、'2022-05-15' 输出:4(完整自然月差)

37、颜色十六进制 → RGB 对象

输入:'#1be3'(简写 + alpha) 输出:{r:17, g:238, b:51, a:0.2} 进阶:支持省略 #、大小写混写,返回结果直接喂给 canvas.ctx.fillStyle

38、数组交集(保留重复)

输入:[1,2,2,3], [2,2,2,4] 输出:[2,2]

39、手机号脱敏

输入:'13812345678' 输出:'138****5678' 进阶:支持 11 位手机号 & 带区号 8613812345678,用同一函数处理

40、hooks手写

40.1 usePrevious普通版

import {useEffect, useRef} from 'react'
function usePrevious(value){
    // 通过 ref存储
    const ref = useRef()
    useEffect(() => {
        ref.current = value
    },[])
    return ref.current
}

40.2 usePrevious根据值更新版

import {useEffect, useRef, useState} from 'react'
function usePrevious(value){
    // 通过 ref存储当前值,通过 useState存储上一次的值
    const [previousValue, setPreviousValue] = useState()
    const ref = useRef()
    useEffect(() => {
        if(previousValue !== value){
            setPreviousValue(ref.current)
            ref.current = value
        }
    },[value])
    return previousValue
}

41.给定任意二维数组,输出所有的排列组合项

比如 [['A','B'], ['a','b'], [1, 2]],

输出 ['Aa1','Aa2','Ab1','Ab2','Ba1','Ba2','Bb1','Bb2']

function permutate(arr){
    // 拿出第一组
    let res = arr[0].slice() // 浅拷贝
    for(let i = 1; i < arr.length; i++){
        const pre = res.slice()
        res = []
        pre.forEach(item => {
            arr[i].forEach(curr => {
                res.push(item + curr)
            })
        })
    }
    return res
}

42 、场景题

场景:页面 1w 个按钮,均用 单一事件委托 监听; 输入:点击流 ['save','cancel','save','delete','save'] 输出:实时维护 Map {'save':3, 'cancel':1, 'delete':1},且支持 O(1) 撤销上一次计数(用户点了撤回)。 进阶:撤销操作来自键盘 Ctrl+Z,用栈记录上一次按钮类型即可

43、form 组件

44、找出字符串中出现次数最多的字母,并对前面的数字求和(mid)

45、合并商品标签

输入:两个商品标签数组,可能有重复
输出:去重后按字典序合并
进阶:结果用于 <select multiple>,需高亮本次新增标签。

46、回溯

[[1,2,3],[4,5],[6,7,8]]生成 [[1,4,6],[1,4,7]..]

47、短横线连接的字符串转换为驼峰形式

48、千位分隔符

输入:1234567.89 输出:'1,234,567.89' 进阶:支持负数、小数,用于表格金额列

49、找出所有消失的数字

输入:[4,3,2,7,8,2,3,1] 输出:[5,6](1~n 中缺失的) 彩蛋:数组长度 1e5,要求 O(n) 时间、O(1) 额外空间。

50 、每次调用prime()函数,都可以执行并输出下一个质数