前端100道优质面试题(进大厂必会)

371 阅读15分钟

一、数据结构和算法

题目1. 将一个数组旋转k步 查看答案

  • 输入一个数组[1,2,3,4,5,6,7]
    
  • k = 3,即旋转3
  • 输出[5,6,7,1,2,3,4]
    

题目2. 用JS实现快速排序,并且说明时间复杂度 查看答案

题目3.判断字符串是否括号匹配 查看答案

  • 一个字符串s可能包含{}()[]三种括号
    
  • 判断s是否是括号匹配的
    
  • 如(a{b}c)匹配,而{a(b或{a(b}c)就不匹配
    

题目4.反转单向链表 查看答案

  • 输入一个单向链表,输出它的反转(头变尾,尾变头)
    

二、前端基础知识

题目5.Ajax、Fetch、Axios的区别 查看答案

题目6.箭头函数的缺点,哪里不能用箭头函数? 查看答案

题目7.Vue组件通讯方式有几种?尽量说全面 查看答案

题目8.Vuex中action和mutation有什么区别? 查看答案

题目9.节流和防抖的区别,分别应用哪些场景 查看答案

题目10.px、% 、rem、 rm、 vw、 vh 的区别 查看答案

题目11.描述TCP三次握手和四次挥手 查看答案

题目12.for...in 和 for...of 有什么区别 查看答案

题目13.for await...of 有什么作用? 查看答案

题目14.offsetHeight、scrollHeight、clientHeight 的区别 查看答案

题目15.描述TCP三次握手和四次挥手 查看答案

题目16.解决跨域请求的方法 查看答案

题目17.在跨域请求前为何会发送一个options请求? 查看答案

题目18.什么是内存垃圾回收?垃圾回收的算法? 查看答案

题目19.闭包是内存泄漏吗? 如何检测JS内存是否泄漏?内存泄漏出现的场景有哪些?查看答案

题目20. Vue生命周期都在做什么?查看答案

题目21. Vue里父子组件生命周期的顺序查看答案

题目22. Vue里Ajax在哪个生命周期里执行最合适查看答案

题目23. Vue什么时候操作DOM比较合适 查看答案

题目24. Vue3 Composition API 生命周期有什么区别? 查看答案

题目25. Vue2,Vue3,React的diff算法有什么区别 查看答案

题目26. Vue、React 循环里为什么必须使用key 查看答案

题目27. 什么是同步和异步? 查看答案

题目28. 异步有哪些分类?它们的执行顺序是什么? 查看答案

题目29. 浏览器和nodejs的事件循环的区别? 查看答案

题目30. 说一下对vdom和diff算法的理解 查看答案

题目31. for 和 forEach 哪个快? 查看答案

题目32. 什么是进程,什么是线程? 查看答案

题目33. nodeJs 如何开启进程,进程如何通讯? 查看答案

题目34.requestIdleCallback 和 requestAnimationFrame的区别 查看答案

答案

题目1. 将一个数组旋转k步

解题思路:

1⃣️旋转k步,可以循环遍历数组k次,用pop弹出最后一位,再用unshift拼接到前面,就可以完成数组的旋转

/**
 * 旋转数组step步 使用pop unshift 时间复杂度为O(n^2)
 * @param arr arr
 * @param step step
 * @returns arr
 */
export function rotate1(arr: number[], step: number) {
    let length = arr.length;
    if (length === 0 || step === 0) return arr;
    // 获取stp的绝对值
    const stpAbs = Math.abs(step % length);
    for (let i = 0; i < stpAbs; i++) {
        const arrPop = arr.pop() as number;
        if (arrPop != null) arr.unshift(arrPop); // unshift操作 非常慢
    }
    return arr;
}

2⃣️用slice切割数组,把获取到的数组进行相反的拼接,可以通过concat进行操作

/**
 *  旋转数组step步 使用concat 时间复杂度为O(n)
 * @param arr arr
 * @param stp stp
 */
export function rotate2(arr: number[], step: number) {
    let length = arr.length;
    if (length === 0 || step === 0) return arr;
    // 获取stp的正数
    const stpAbs = Math.abs(step % length);
    const arr1 = arr.slice(-stpAbs);
    const arr2 = arr.slice(0, length - stpAbs);
    const arr3 = arr1.concat(arr2);
    return arr3;
}

考察点:思路1里通过遍历数组和unshift操作,使得时间复杂度达到了O(n^2),是非常不可取的,而思路2中的时间复杂度为O(n),所以思路2是优解

题目2. 用JS实现快速排序,并且说明时间复杂度

解题思路:找到数组的中间值,然后遍历循环数组,并且和中间值进行比较,定义2个数组,right数组存放比中间值大的值,left数组存放比中间值小的值,然后返回一个 递归left 拼接 中间数 拼接 递归right,就实现了从小到大的排序。

这里我将用2种方法来实现,一个是splice来找到中间值,会改变原数组。一个是用slice来找到中间值,不会改变原数组

上面两个算法,都用到了for循环和二分法,所以时间复杂度为O(n*logn)

/**
 * @description 快速排序
 * @author 李扬
 */

/**
 * 快速排序(splice)
 * @param arr number arr
 */
export function quickSort1(arr: number[]): number[] {
    const length = arr.length;
    if (length === 0) return arr;
    const middleIndex = Math.floor(length / 2);
    // 获取中间的值
    const middleValue = arr.splice(middleIndex, 1)[0];
    // 存放比中间值小的数
    const left = [];
    // 存放比中间值大的数或相等的值
    const right = [];
    // splice 改变了原数组的长度,所以这里用arr.length 重新获取一下原数组的长度
    for (let i = 0; i < arr.length; i++) {
        const n = arr[i]
        if (n < middleValue) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }
    // 递归实现拼接
    return quickSort1(left).concat([middleValue], quickSort1(right));
}

/**
 * 快速排序(slice)
 * @param arr number arr
 */
 export function quickSort2(arr: number[]): number[] {
    const length = arr.length;
    if (length === 0) return arr;
    const middleIndex = Math.floor(length / 2);
    // 获取中间的值
    const middleValue = arr.slice(middleIndex, middleIndex + 1)[0];
    // 存放比中间值小的数
    const left = [];
    // 存放比中间值大的数或相等的值
    const right = [];
    for (let i = 0; i < length; i++) {
        if(i !== middleIndex) {
            const n = arr[i]
            if (n < middleValue) {
                left.push(arr[i]);
            } else {
                right.push(arr[i]);
            }
        }
       
    }
    // 递归实现拼接
    return quickSort2(left).concat([middleValue], quickSort2(right));
}

// 功能测试
const arr = [1,4,53,25,28,89,100,28]
const res = quickSort2(arr)
console.log('res2', res)



题目3. 判断字符串是否括号匹配

解题思路:

1⃣️考察对栈的理解,可以定义一个空栈,然后循环遍历字符串,当遇见“({[” 匹配其中遍历的字符的时候,进行入栈,当遇见“)]}” 匹配遍历的字符的时候进行判断,如果右括号匹配的字符为栈顶的字符向对应的时候,进行出栈操作,最后返回栈的长度是否为0, 其时间复杂度为O(n)

/**
 * @param leftMatch 左括号
 * @param rightMatch 右括号
 * @returns boolean 是否匹配
 */
function isMatch(leftMatch: string, rightMatch: string): boolean {
    if (leftMatch === "(" && rightMatch === ")") return true;
    if (leftMatch === "{" && rightMatch === "}") return true;
    if (leftMatch === "[" && rightMatch === "]") return true;
    return false;
}
/**
 * 判断括号是否匹配
 * @param str
 */
export function matchBracket(str: string) {
    const length = str.length;
    if (length === 0) return true;
    const leftStr = "({[";
    const rightStr = ")}]";
    let stack = []; // 定义一个空栈
    for (let i = 0; i < length; i++) {
        const s = str[i];
        if (leftStr.includes(s)) {
            stack.push(s);
        }
        if (rightStr.includes(s)) {
            const stackEnd = stack[stack.length - 1];
            if (isMatch(stackEnd, s)) {
                stack.pop()
            } else {
                return false
            }
        }
    }
    return stack.length === 0;
}

// 功能测试
const str1 = "1(11[2]1){1}";
console.log("str1", matchBracket(str1)); // true
const str2 = "111[2]1){1}";
console.log("str2", matchBracket(str2)); // false

题目4.反转单向链表

/** 
 * 根据数组创建单项列表
 * @param arr number arr
 */
 export interface ILinkList {
    value: number;
    next?: ILinkList;
}
/**
 * 反转单项列表,并把反转后的列表返回出去
 * @param listNode 
 */
export const reverseListNode = (listNode:ILinkList): ILinkList =>{
    // 定义三个指针
    let preNode:ILinkList | undefined = undefined
    let creNode:ILinkList | undefined = undefined
    let nextNode:ILinkList | undefined = listNode
    // 以nextNode为主,遍历列表 
    while(nextNode) {
        if(!preNode && creNode) {
            delete creNode!.next
        }
        if(preNode && creNode) {
            creNode.next  = preNode
        }
        // 整体后移
        preNode = creNode
        creNode = nextNode
        nextNode = nextNode?.next
    }
    creNode!.next = preNode
    return creNode!
}
/**
 * 创建链表的结构
 * @param arr number[]
 */
export const createLinkList = (arr: number[]): ILinkList => {
    const length = arr.length;
    if (length === 0) throw new Error("数组为空");
    let curNode: ILinkList = {
        value: arr[length - 1],
    };
    if (length === 1) return curNode;
    for (let i = length - 2; i >= 0; i--) {
        curNode = {
            value: arr[i],
            next: curNode,
        };
    }
    return curNode;
};
const array = [100, 200, 300, 400, 500];
const res = createLinkList(array);
console.log("res", res);
const array1 = reverseListNode(res)
console.log("array1:", array1);

题目5.Ajax、Fetch、Axios的区别

    Ajax 是一种技术统称

    Fetch 是一个原生API

    Axios 是一个第三方库

题目6.箭头函数的缺点?,哪里不能用箭头函数?

箭头函数的缺点:

  1. 没有arguments
  2. 无法通过bind、call、apply 改变this的指向

不能用箭头函数的地方:

  1. 不适用对象里的方法
  2. 不适用原型方法
  3. 不适用构造函数
  4. 不适用动态上下文的回调函数
  5. 不适用Vue生命周期和 method

题目7.Vue组件通讯方式有几种?尽量说全面

  1. props 和 $emit
  2. 自定义事件:任意两个组件通讯
  3. $attrs:是props 和emits的候补
  4. $parent:与父组件通讯
  5. $refs:与子组件通讯
  6. proivde/inject:上下级组件(跨多级)通讯
  7. 全局用vuex

题目8.Vuex中action和mutation有什么区别?

  1. mutation:原子操作;必须是同步代码
  2. action:可包含多个mutation; 可包含异步代码

题目9.节流和防抖的区别,分别应用哪些场景

防抖: 不间断触发事件时候,间隔大于某一自定义时间后,才执行最后一次的触发事件。类似于你先抖动着,累了我再执行!

防抖的应用场景:

1、输入框连续输入文本然后进行模糊搜索的时候。

2、改变浏览器窗口大小,然后获取浏览器宽高的时候。

3、快速点击按钮(像搜索按钮),然后只需要触发最后一次的时候

节流: 在连续快速的触发事件的时候,每间隔一个自定义的时间内只允许触发一次。

节流的应用场景:

1、 在拖拽和滚动的时候

// 防抖函数
function debounce(fn, delay = 200) {
    let timer = 0

    return function () {
        if (timer) clearTimeout(timer)

        timer = setTimeout(() => {
            fn.apply(this, arguments) // 透传 this 和参数
            timer = 0
        }, delay)
    }
}

// 节流函数
function throttle(fn, delay = 200) {
    let timer = 0

    return function () {
        if (timer) return

        timer = setTimeout(() => {
            fn.apply(this, arguments) // 透传 this 和参数
            timer = 0
        }, delay)
    }
}

题目10. px、% 、rem、 rm、 vw、 vh 的区别

  1. px是固定的单位
  2. % 是相对于父盒子的宽高的百分比
  3. rem 是相对于根节点的font-size
  4. em 是相对于当前元素的font-size
  5. vw 是相对于屏幕宽度的1%
  6. vh 是相对于屏幕高度的1%
  7. vmin和vmax是指vw和vh的最小值和最大值

题目11. 描述TCP三次握手和四次挥手

三次握手:

  1. Client发包,Server接收,Server知道有Client要找自己
  2. Server发包,Client接收,Client知道Server已经接收到消息了
  3. Client发包,Server接收,Server知道Client准备要发包了

四次挥手:

  1. Client发包,Server接收,Server知道Client已请求结束
  2. Server发包,Client接收,Client知道Server已收到,我等待他关闭
  3. Server发包,Client接收,Client知道Server已经可以关闭了
  4. Client发包,Server接收,Server知道此时可以关闭连接了(然后关闭连接)

题目12. for...in 和 for...of 有什么区别

  1. for...in 遍历出的是key,用于可枚举数据,如对象,数组,字符串
  2. for...of 遍历出的是value,用于可迭代数据,如数组,字符串,Set,Map

题目13. for await...of 有什么作用?

答:可以遍历多个Promise,作用和Promise.all()一样

题目14. offsetHeight、scrollHeight、clientHeight 的区别

  1. offsetHeight、offsetWidth border + padding + content
  2. scrollHeight、scrollWidth: padding + 实际内容尺寸
  3. clientHeight、clientWidth: padding + content

题目15. JS中严格模式有什么特点?

  1. 全局变量必须提前声明
  2. 不能使用with方法
  3. 创建eval作用域
  4. 函数的this不能指向window
  5. 函数接收的形参的命名不能重复

题目16. 解决跨域请求的方法

  1. JSONP:利用script标签可以不被同源策略限制的功能,来解决跨域,
  2. CORS:通过CORS配置来解决跨域
  3. 上面两个方法都需要服务端的配合才能实现

题目17. 在跨域请求前为何会发送一个options请求?

答:这个options请求是浏览器自动发起的,无须我们去干预,是跨域请求的预检查,

三、前端知识🧩-知识深度

题目18. 什么是内存垃圾回收?垃圾回收的算法?

答:

内存垃圾回收指:函数执行后,内部的数据没有用了,并且没有被外部引用,浏览器会释放出其所占用的内存空间。

垃圾回收算法:在之前是用 引用计数 的方法检测的,即排查该数据是否被引用,来决定是否可以被回收,但是存在循环引用带来的问题,导致内存垃圾无法被回收掉。现在采用 标记清除 的方法,即js定期遍历根节点,也就是window,看看能不能得到某个对象,没有得到则回收掉。

题目19.闭包是内存泄漏吗? 如何检测JS内存是否泄漏?内存泄漏出现的场景有哪些?

答:严格来说闭包不属于内存泄漏,因为内存泄漏的定义是指非预期的情况下,内存垃圾没有被回收导致的。而闭包是预期情况,虽然它的内存垃圾也没有被回收

如何检测JS内存是否泄漏:可以通过Chrome Tools 里的Performance,观察HEAP是否为锯齿状,如果是一个直线上升的状态则,内存可能存在泄漏

20230319155441.jpg

内存泄漏出现的场景:

  1. 全局变量、函数引用,组件销毁后未清除
  2. 全局事件、定时器引用,组件销毁后未清除
  3. 被自定义事件(Vue)引用,组件销毁后未清除

题目20. Vue生命周期都在做什么?

答:

  • beforeCreate:创建一个空白的Vue实例,data method尚未初始化,不可使用
  • created:Vue实例初始化完成,完成响应式绑定,data method 都已经初始化完成,可调用
  • beforeMount:编译模板,调用render生成vdom,还没开始渲染DOM
  • mounted:完成DOM渲染,组件创建完成
  • beforeUpdate:数据发生变化,准备更新DOM
  • updtated:DOM更新完成之后
  • beforeUnmount:卸载组件之前,可以移除、解绑一些全局事件、自定以事件
  • unmounted:组件完成了卸载,子组件也都被销毁了

题目21. Vue里父子组件生命周期的顺序

答:父beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created =>子beforeMount => 子mounted => 父mounted

题目22. Vue里Ajax在哪个生命周期里执行最合适

答:Ajax在created和mounted里执行都可以,更推荐在mounted里来执行,因为从created到mounted的时间相对于Ajax所消耗的时间可以忽略不计。

题目23. Vue什么时候操作DOM比较合适

答:mounted和updated都不能保证子组件全部挂载完成(子组件有可能是异步组件),所以在mounted或者updtated里使用$nextTick渲染DOM是最安全的

题目24. Vue3 Composition API 生命周期有什么区别?

答:
用setUp代替了beforeCreate 和 created
使用Hooks函数的形式,如mounted改为onMounted()

题目25. Vue2,Vue3,React的diff算法有什么区别

答:传统的diff算法是遍历全部dom树来找到不同,时间复杂度为O(n^3),是不可用算法,vue和react都对diff算法做了优化,只进行同层比较,不进行跨级比较,tag不同时删掉重建,子节点通过key区分,使得时间复杂度降到了O(n)。

    1. Vue2,采用了双端比较法
    1. Vue3,采用了最长递增子序列比较
    1. React,采用了仅右移比较

无论是哪种比较,它们有一个共同的目的,就是尽量减少DOM操作,能不动就不动,能移动DOM就不删除重建DOM

题目26. Vue、React 循环里为什么必须使用key

答:vdom diff算法会根据key判断元素是否删除,匹配到了key则只移动元素,性能较好,没有匹配到key,则删除重建元素,性能较差。

题目27. 什么是同步和异步?

js是单线程语言,无论是在浏览器还是在node.js中都是单线程的,所以执行js函数和进行DOM渲染的时候共用一个线程,就会造成只能有一个在执行,为了解决执行js函数的时候无法渲染DOM而造成的卡顿的现象,就出现了异步这样的解决方式。

题目28. 异步有哪些分类?它们的执行顺序是什么?

异步分为宏任务和微任务
宏任务包括:网络请求,setTimeOut、setInterval。
微任务包括:Promise,asnyc/await
它们的执行顺序是:同步任务 => 微任务 => DOM渲染 => 宏任务

题目29. 浏览器和nodejs的事件循环的区别?

浏览器的事件循环:执行js代码的时候,先执行同步任务,遇见异步任务后进行分类,放入微任务队列或者宏任务队列中等待执行,如果是定时器或者网络请求这样的异步任务,则等到时间到了或者请求完毕的时候再放入宏任务队列中,等到同步任务执行完毕,则先在微任务队列执行,执行完毕后再在宏任务中执行,执行完毕后,有一个 Event Loop 事件循环机制,重新检查是否还有待执行的同步、异步任务

nodeJs 的事件循环:nodeJs的事件循环过程和浏览器事件循环一致,也是单线程的,也需要异步,区别在于它的宏任务、微任务的类别划分的更细致,并且在同一个宏任务队列中,存在优先级,而浏览器的宏任务队列是按进队列先后的时间顺序执行的。

题目30. 说一下对vdom和diff算法的理解

vdom:是用js对象来模拟一个dom树节点数据,vue,react这些框架都是基于此来实现内部组件的更新。当数据发生改变的时候,遍历这个虚拟的dom树,找到需要更改的地方进行更改,这是数据视图分离、数据驱动视图的理念。使我们的开发更加关注业务数据的操作而不是dom的操作。

diff算法:当数据变化的时候,触发diff算法,对比新旧vnode,找到需要更新的点去进行更新dom

题目31. for 和 forEach 哪个快?

答:for和forEach的时间复杂度都是O(n),但是 for 会比 forEach 要快。因为forEach在每次遍历的时候都会创建一个函数来调用,而函数需要独立的作用域,会有额外的开销。而for则不用。

题目32. 什么是进程,什么是线程?

答:
进程: OS进行资源分配和调度的最小单位,有独立内存空间
线程: OS进行运算调度的最小单位,共享进程内存空间

题目33. nodeJs 如何开启进程,进程如何通讯?

答:
如何开启:开启子进程 用child_process.forkcluster.fork
如何通讯:使用send 和on传递信息

题目34.requestIdleCallback 和 requestAnimationFrame的区别

答:
requestAnimationFrame每次渲染完成都会执行,优先级高
requestIdleCallback空闲时才会执行,优先级低

四、前端知识🧩-知识广度

题目34.移动端H5点击有300ms延迟,如何解决。

五、前端知识🧩-实际工作

题目34.如果一个网页访问 慢,你该如何分析问题原因?

题目34.Vue应该如何监听JS报错

题目34.你遇到了哪些如何难点,如何解决?

题目34.H5页面如何进行首屏优化?

  1. 路由懒加载
  2. 服务器渲染SSR Nuxt.js(Vue)、Next.js(React)
  3. App预取
  4. 分页
  5. 图片懒加载
  6. Hybrid

五、前端知识🧩-编写高质量代码