记录年初前端面试的一些问题,持续更新

127 阅读21分钟

最近面试的遇到的一些问题,记录一下

个人混迹于外包公司,能力有限,欢迎大家批评讨论 部分问题的答案来自于OpenAI,每次提问的上下文语境不同AI给出的回答也不一致非标准答案

1. Class和Hooks的区别(OpenAI)

React Hooks是React 16.8中引入的新特性,它允许在函数组件中使用状态(state)、生命周期方法(lifecycle methods)和其他React特性。 以下是React Hooks和类组件之间的一些区别:

  1. 代码复用性:在类组件中,组件逻辑分散在不同的方法中(例如render、componentDidMount等),而在函数组件中,逻辑可以使用多个自定义Hooks进行分离和复用。
  2. 状态管理:在类组件中,状态可以通过this.state和this.setState方法进行管理。在函数组件中,状态可以通过useState Hook进行管理。
  3. 生命周期方法:在类组件中,生命周期方法(如componentDidMount、componentWillUnmount)可以用来处理组件生命周期的不同阶段。在函数组件中,可以使用useEffect Hook来模拟这些生命周期方法的功能。
  4. 代码结构:类组件通常需要在constructor中初始化状态和绑定方法。而在函数组件中,可以将这些逻辑拆分到不同的Hook中,使代码结构更加清晰。
  5. 性能:函数组件通常比类组件执行更快。因为函数组件不需要创建实例,所以它们的内存占用量更小。

2. useMemo和useCallback的区别以及useRef的使用(OpenAI)

useMemo 和 useCallback 都是 React 中用于优化性能的 Hook,它们的作用是避免不必要的重复计算和渲染,但是它们的使用场景和使用方式不同。

useMemo 用于在组件中缓存计算结果,只有在依赖项发生变化时才重新计算,可以避免不必要的重复计算。useMemo 接受两个参数:第一个参数是一个函数,用于进行计算;第二个参数是一个数组,用于指定依赖项。只有当依赖项发生变化时,useMemo 才会重新计算并返回计算结果。例如:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

这里,computeExpensiveValue 是一个耗时的计算函数,只有当 a 和 b 发生变化时,useMemo 才会重新计算 memoizedValue,否则直接返回上一次的结果。

useCallback 用于缓存回调函数,避免在每次渲染时创建新的回调函数实例,可以提高性能。useCallback 接受两个参数:第一个参数是回调函数,第二个参数是依赖项数组,用于指定依赖项。只有当依赖项发生变化时,useCallback 才会返回新的回调函数。例如:

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

这里,doSomething 是一个需要传入参数 a 和 b 的回调函数,只有当 a 和 b 发生变化时,useCallback 才会返回新的回调函数,否则直接返回上一次的回调函数实例。

总的来说,useMemo 和 useCallback 都可以用于性能优化,但是使用场景和使用方式不同。useMemo 适用于缓存计算结果,避免重复计算,而 useCallback 适用于缓存回调函数,避免创建新的函数实例。需要根据具体情况选择使用

useRef 是 React 中用于获取组件中某个元素的引用的 Hook。它可以用于保存任何可变值,类似于在类组件中使用 this 来保存值。useRef 返回一个对象,该对象的 current 属性指向传入的初始值。使用 useRef 通常有两种常见的用途:

  1. 获取 DOM 元素的引用
    在函数组件中获取 DOM 元素的引用通常是困难的,因为函数组件没有实例。但是,使用 useRef,可以在函数组件中轻松地获取 DOM 元素的引用,例如:

    import { useRef } from 'react';
    
    function MyComponent() {
     const inputRef = useRef(null);
    
     function handleClick() {
       inputRef.current.focus();
     }
    
     return (
    
    
         Focus Input
    
     );
    }
    

    在这个例子中,inputRef 是一个 useRef 对象,它的 current 属性指向 input 元素。在 handleClick 函数中,可以使用 inputRef.current 获取到这个 input 元素,并调用 focus() 方法来使其获取焦点

  2. 缓存上一次的值
    使用 useRef,可以缓存上一次的值,并在每次组件重新渲染时获取这个值。这在处理一些需要在组件重新渲染时执行的操作时非常有用,例如:

    import { useState, useEffect, useRef } from 'react';
    
    function MyComponent() {
     const [count, setCount] = useState(0);
     const prevCountRef = useRef();
    
     useEffect(() => {
       prevCountRef.current = count;
     }, [count]);
    
     const prevCount = prevCountRef.current;
    
     return (
    
         Current count: {count}
         {prevCount !== undefined && (
           Previous count: {prevCount}
         )}
          setCount(count + 1)}>Increment
    
     );
    }
    

    在这个例子中,prevCountRef 是一个 useRef 对象,它的 current 属性保存了上一次的 count 值。在 useEffect 钩子中,可以通过 prevCountRef.current = count 将 count 值保存在 prevCountRef.current 中,这样每次组件重新渲染时,prevCount 就能获取到上一次的 count 值。

总之,useRef 可以用于获取 DOM 元素的引用和缓存上一次的值,这两种用途都能够在某些场景下非常方便

useMemo如何实现

useMemo 是 React 提供的一个 Hook,用于在组件重新渲染时,避免重复计算成本高昂的操作。

具体实现过程如下:

  1. 定义 useMemo 函数的参数:一个回调函数和一个依赖数组;
  2. 在 useMemo 内部,维护一个“上一次计算结果”以及一个“上一次依赖数组”;
  3. 每次组件重新渲染时,先将当前的依赖数组和上一次依赖数组进行比较;
  4. 如果依赖数组中的所有元素都和上一次相同,那么直接返回上一次的计算结果;
  5. 如果依赖数组中有任何一个元素与上一次不同,那么调用回调函数计算新的结果,并将新的结果和新的依赖数组存储起来;
  6. 返回新的计算结果。

下面是一个简单的实现示例:

function useMemo(callback, dependencies) {
  const [lastResult, setLastResult] = useState(null);
  const [lastDependencies, setLastDependencies] = useState(null);

  if (lastDependencies && dependencies.every((dep, i) => dep === lastDependencies[i])) {
    return lastResult;
  }

  const newResult = callback();
  setLastResult(newResult);
  setLastDependencies(dependencies);

  return newResult;
}

在这个实现中,我们使用了 React 内置的 useState Hook 来维护“上一次计算结果”和“上一次依赖数组”。当依赖数组中的元素和上一次相同时,直接返回上一次的计算结果;否则,调用回调函数计算新的结果,并将新的结果和新的依赖数组存储起来。最后,返回新的计算结果

3. 对redux的了解(OpenAI)

Redux 是一个用于管理 React 应用程序状态的 JavaScript 库,它通过创建一个单一的、可预测的应用程序状态管理中心来帮助管理应用程序状态。

Redux 的核心理念是单向数据流,即应用程序状态只能通过一个中心存储库进行更改,然后将这些更改广播给应用程序中的所有组件。这个存储库被称为“store”,它保存了应用程序的整个状态。组件可以通过调度“action”来更新这个存储库中的状态,然后“reducer”函数将这些操作转化为新的状态并返回给存储库。

Redux 提供了一组 API 来帮助开发者管理应用程序状态,其中包括:

  1. createStore(reducer, [preloadedState], [enhancer]):用于创建一个存储库,它接受一个“reducer”函数作为参数,这个函数将状态和一个“action”作为输入,并返回一个新的状态。可选的“preloadedState”参数用于在创建存储库时初始化状态。可选的“enhancer”参数用于增强存储库的功能。
  2. combineReducers(reducers):用于将多个 reducer 合并成一个 reducer,以便在创建存储库时使用。
  3. dispatch(action):用于向存储库分派一个 action,这个 action 会被传递给 reducer。
  4. subscribe(listener):用于注册一个监听器,每当存储库中的状态发生变化时,这个监听器都会被调用。
  5. getState():用于获取当前存储库中的状态。

Redux 还提供了一些用于与 React 集成的工具,其中包括:

  1. Provider 组件:用于在 React 应用程序中提供 Redux 存储库的访问。
  2. connect(mapStateToProps, mapDispatchToProps) 函数:用于将 React 组件连接到 Redux 存储库中的状态和操作。通过这个函数,组件可以从存储库中获取状态,并将 action 分派到存储库中。
  3. useSelector(selector) 和 useDispatch() Hook:用于在函数组件中获取存储库中的状态和操作。

总之,Redux 提供了一种可预测的状态管理机制,它可以让开发者更加清晰地组织应用程序中的状态和操作。它适用于大型应用程序,并可以方便地与 React 集成。

4. 对antd组件库的了解(组件实现方面)

这个没标准答案,看自己实际应用过程中的一些使用和问题处理吧 欢迎大家留言讨论

5. 对chrome调试工具的使用

自己对调试工具粗浅的理解

  • Elements 查看修改节点 调试样式
  • Console 查看打印日志 运行下js代码
  • Sources 查看源文件打断点调试
  • Network 查看网络请求 分析数据源
  • Application 本地一些持久化存储的数据
  • Performance 可以查看网页的性能数据,并分析网页的性能瓶颈。

简单总结下 面试官又问对于生产的问题如何排查,个人理解比较粗浅没有面试官想要的答案 面试官提示说把生产的代码代理到本地来跑,后续学习补充(来自OpenAI的回答)

  1. 打开 Chrome 浏览器,并打开需要代理的网站。
  2. 打开开发者工具,选择“Network”选项卡,并勾选“Preserve log”复选框,以保留所有网络请求。
  3. 刷新网页,以便让 Chrome 浏览器捕获所有网络请求。
  4. 找到生产代码所在的文件,例如 JavaScript 文件或 CSS 文件,并将其下载到本地。
  5. 在本地编辑文件,并保存更改。
  6. 在开发者工具中,选择“Sources”选项卡,并选择“Filesystem”子选项卡。
  7. 点击“+Add folder to workspace”按钮,将本地文件夹添加到 Chrome 工作区。
  8. 打开“Sources”选项卡下的相应生产代码文件,并将其替换为本地编辑的文件。
  9. 保存更改后,刷新网页,以便 Chrome 浏览器加载本地文件而不是生产文件。

6. history和hash路由

Hash

url中带一个#

  • 可以改变URL,但不会触发页面重新加载(hash的改变会记录在  window.hisotry  中)因此并不算是一次 HTTP 请求,所以这种模式不利于 SEO 优化
  • 只能修改#后面的部分,因此只能跳转与当前 URL 同文档的 URL
  • 只能通过字符串改变 URL
  • 通过window.onhashchange监听hash的改变,借此实现无刷新跳转的功能。
  • 每改变一次hash( window.location.hash),都会在浏览器的访问历史中增加一个记录。
  • 路径中从#开始,后面的所有路径都叫做路由的 哈希值 并且哈希值它不会作为路径的一部分随着 http 请求,发给服务器

History

  • 新的URL可以是与当前URL同源的任意 URL,也可以与当前URL一样,但是这样会把重复的一次操作记录到栈中。
  • 通过参数stateObject可以添加任意类型的数据到记录中。
  • 可额外设置title属性供后续使用。
  • 通过pushState、replaceState实现无刷新跳转的功能。
  • 路径直接拼接在端口号后面,后面的路径也会随着http请求发给服务器,因此前端的URL必须和向发送请求后端URL保持一致,否则会报404错误。
  • 由于History API的缘故,低版本浏览器有兼容性问题。
  • 刷新 404 的问题,可以在 nginx  做代理转发

7. 从浏览器输入url到页面展示网络层面发生了什么 (OpenAI

  1. DNS解析:浏览器会先检查本地缓存中是否存在该域名的DNS记录,如果没有则向本地DNS服务器发起查询请求,如果本地DNS服务器也没有该记录,则向根域名服务器发送请求进行解析,最终获取该域名的IP地址。

  2. TCP连接:浏览器会向目标IP地址的服务器发起TCP连接请求,进行三次握手建立连接。

  3. 发送HTTP请求:浏览器会发送HTTP请求报文,其中包括请求方法(如GET、POST等)、请求头(如User-Agent、Accept等)、请求体(如POST请求中的参数等)等信息。

  4. 服务器处理请求:服务器收到请求后,会根据请求信息进行相应的处理,比如查询数据库、生成动态内容等,并将处理结果封装成HTTP响应报文返回给浏览器。

  5. 接收HTTP响应:浏览器接收到HTTP响应报文后,会解析报文内容,包括响应状态码、响应头信息和响应体(如HTML页面、图片等)。

  6. 渲染页面:浏览器根据HTML、CSS、JavaScript等资源,将页面进行渲染,并在页面上展示出来。

  7. 断开TCP连接:页面展示完成后,浏览器会向服务器发送断开连接请求,进行四次挥手断开TCP连接。

以上就是从浏览器输入URL到页面展示的网络层面的主要步骤。在实际过程中,还会涉及到网络传输的各种细节和优化,比如TCP拥塞控制、HTTP缓存、压缩等。

参考文章

8. 遍历下面一个数据

遍历每一子节点,并获取id 数据格式大概是这样(面试官给出的数据结构)

const root = {
  id: 1,
  children: [
    {
      id: 3,
      children: [
        { id: 7, },
        { id: 8, }
      ]
    },
    {
      id: 4,
      children: [
        { id: 9 },
        { id: 10 }
      ]
    },
  ],
  id: 2,
  children: [
    {
      id: 5,
      children: [
        { id: 11 },
        { id: 12 }
      ]
    },
    {
      id: 6,
      children: [
        { id: 13 },
        { id: 14 }
      ]
    },
  ]
}

一个递归函数就可以实现

function demo (obj) {
    console.log(obj.id);
    if(obj?.children.length > 0) {
        obj.children.forEach((item)=>{
            demo(item)
        })
    }
}

递归只是最简单的解法,却不是最优解,往下看

深度优先和广度优先(OpenAI)

广度优先遍历(BFS)是一种图的遍历算法,它从图的某个节点开始,先访问该节点的所有邻居节点,然后访问邻居节点的所有未访问过的邻居节点,以此类推,直到遍历完所有与起始节点连通的节点。

下面是一种广度优先遍历的方法:

  1. 将起始节点入队,并标记为已访问。
  2. 如果队列非空,则重复执行步骤 3 ~ 5。
  3. 取出队首节点。
  4. 遍历队首节点的所有相邻节点,对于每个未访问过的相邻节点,将其入队,并标记为已访问。
  5. 返回步骤 2。

这个方法保证了在访问当前节点的相邻节点之前,先访问了所有深度相同但较远的节点,因此可以得到广度优先遍历的结果。

这个方法可以使用队列实现。在实现中,可以使用一个已访问节点集合和一个节点路径映射表来记录已经访问过的节点和每个节点是从哪个节点扩展而来的,以便在需要构建结果路径时使用

function bfs(startNode) {
  // 初始化队列,将起始节点入队
  const queue = [startNode];

  // 初始化已访问节点集合,将起始节点标记为已访问
  const visitedNodes = new Set([startNode]);

  // 遍历队列,直到队列为空
  while (queue.length > 0) {
    // 取出队首节点
    const currNode = queue.shift();

    // 访问当前节点
    console.log(currNode);

    // 遍历当前节点的相邻节点
    for (let neighbor of currNode.neighbors) {
      // 如果相邻节点已经访问过,则跳过
      if (visitedNodes.has(neighbor)) {
        continue;
      }

      // 将相邻节点入队,并标记为已访问
      queue.push(neighbor);
      visitedNodes.add(neighbor);
    }
  }
}

深度优先遍历(DFS)是另一种图的遍历算法,它从图的某个节点开始,先访问该节点,然后递归地访问该节点的未访问过的相邻节点,直到访问完所有与起始节点连通的节点。

下面是一种深度优先遍历的方法:

  1. 访问当前节点。
  2. 遍历当前节点的所有相邻节点,对于每个未访问过的相邻节点,递归调用步骤 1 ~ 2。

这个方法保证了先访问深度较深的节点,因此可以得到深度优先遍历的结果。

这个方法可以使用递归实现。在实现中,可以使用一个已访问节点集合来记录已经访问过的节点,以防止重复访问。

下面是使用 JavaScript 实现深度优先遍历的示例代码

function dfs(currNode, visitedNodes) {
  // 访问当前节点
  console.log(currNode);

  // 将当前节点标记为已访问
  visitedNodes.add(currNode);

  // 遍历当前节点的相邻节点
  for (let neighbor of currNode.neighbors) {
    // 如果相邻节点已经访问过,则跳过
    if (visitedNodes.has(neighbor)) {
      continue;
    }

    // 递归访问相邻节点
    dfs(neighbor, visitedNodes);
  }
}

9. 实现一个promise

Promise 必然处于以下几种状态之一 👇:

  • 待定 (pending): 初始状态,既没有被兑现,也没有被拒绝。
  • 已成功 (fulfilled): 意味着操作成功完成。
  • 已拒绝 (rejected): 意味着操作失败。

1、执行了resolve(),Promise状态会变成fulfilled,即 已完成状态

2、执行了reject(),Promise状态会变成rejected,即 被拒绝状态

3、Promise只以第一次为准,第一次成功就永久fulfilled,第一次失败就永远状态为rejected

4、Promise中有throw的话,就相当于执行了reject()

参考文章

10. 如何用React实现Vue的v-if和v-show(这题应该放前面的)

  • v-if:相当于React 中的三元或者条件表达式
  • v-show:

11. 说一下你在项目封装组件的一个思路

前端组件封装是前端开发中非常重要的一部分,它可以提高开发效率、减少代码复杂度、增加代码复用性和维护性。以下是前端组件封装应该注意的一些方面:

  1. 功能单一:组件的功能应该尽可能单一,这样可以提高组件的复用性和可维护性。
  2. 参数灵活:组件应该提供灵活的参数配置,以便于在不同场景下使用组件,并且可以通过参数的配置来改变组件的行为和样式。
  3. 可定制性:组件应该提供可定制的接口,以便于用户在需要时进行二次开发和定制,例如提供插槽和事件等。
  4. 兼容性:组件应该考虑兼容性问题,例如兼容不同浏览器、不同设备等,并且应该对用户使用组件时可能遇到的问题提供友好的错误提示。
  5. 高内聚低耦合:组件内部应该实现高内聚低耦合的原则,组件内部的代码应该尽可能的独立,并且和外部代码的耦合性应该尽可能的降低,这样可以提高组件的可维护性和扩展性。
  6. 文档说明:组件应该提供详细的文档说明,包括组件的使用方式、参数配置、插槽和事件等,以便于用户使用组件时快速上手。

综上所述,前端组件封装需要考虑到组件的功能单一、参数灵活、可定制性、兼容性、高内聚低耦合和文档说明等方面,这样才能提高组件的可维护性、复用性和扩展性。

12. 对自动化部署的了解

13. 熟悉微前端吗

14. 在componentDidMount生命周期中 直接有两个setState(),并且setTimeout()中有两个setState(),render()方法会执行几次

componentDidMount(){
    this.setState({name: 'zero' });
    this.setState({ name: 'one' });
    setTimeout(()=>{
        this.setState({ name: 'two' });
        this.setState({ name: 'three' });
    },2000)
}

15. 浏览本地存储的几种方式

  1. Cookies:一种小型的文本文件,由服务器发送到浏览器,保存在用户的计算机上。可以设置过期时间,支持设置域名、路径等限制访问条件。但是,由于大小受限,且存储在本地明文中,容易被攻击者窃取,因此一般用于存储少量信息,如用户登录状态、购物车等。
  2. Web Storage:包括localStorage和sessionStorage两种。localStorage和sessionStorage都是HTML5提供的本地存储方式,可以用来存储字符串类型的数据。localStorage的存储内容没有时间限制,除非手动删除;而sessionStorage只能存储会话期间的数据,浏览器关闭后即被清除。相对于Cookies,Web Storage的存储容量更大,且存储的内容不会随着每次HTTP请求发送到服务器端,因此相对更安全。
    LocalStorageSessionStorage都是浏览器提供的本地存储功能,但它们有以下区别:
    • 数据存储位置:LocalStorage和SessionStorage都存储在客户端,不需要从服务器请求数据。LocalStorage数据存储在浏览器的本地存储区域,而SessionStorage数据存储在会话存储区域。
    • 存储数据的生命周期:LocalStorage数据的生命周期是永久性的,除非用户手动清除或通过代码清除;而SessionStorage的生命周期只在当前会话期间有效,即关闭浏览器窗口后,存储的数据将被清除。
    • 数据的作用域:LocalStorage和SessionStorage都是基于同源策略的。LocalStorage数据在同源的所有窗口和标签页之间共享,而SessionStorage数据只在同一窗口或标签页中共享。
    • 数据存储大小:LocalStorage可以存储更多的数据(一般为5-10MB),而SessionStorage一般只能存储较小的数据(一般为2-5MB)。
  3. IndexedDB:一种浏览器内置的非关系型数据库,可以存储结构化数据。IndexedDB支持大容量的存储,可以离线使用,支持事务管理和索引查询等操作,适用于存储大量数据的场景。但是使用IndexedDB需要编写复杂的异步代码,对于开发者而言门槛较高。
  4. Web SQL Database:一种关系型数据库,支持使用SQL语言进行操作。但是由于该标准已经被废弃,不被推荐使用。

Web Storage和Cookies都是浏览器提供的本地存储方式,但它们之间有以下几个区别:

  1. 存储容量:Web Storage的存储容量一般比Cookies要大得多。对于localStorage,每个域名下可以存储5MB左右的数据,而对于Cookies,每个域名下可以存储的数据只有几KB。
  2. 数据传输:Cookies每次HTTP请求都会将自身的信息传输到服务器端,而Web Storage则不会。这意味着如果需要在客户端和服务器端之间频繁传输数据,使用Cookies可能会增加网络带宽和延迟。
  3. 有效期限:Cookies可以设置过期时间,而Web Storage一般是永久存储,除非手动删除。
  4. 安全性:Cookies存储在本地,容易被黑客攻击窃取,因此需要设置安全性措施,如限制域名、路径等访问条件,加密存储等。Web Storage由于不会在每次HTTP请求中传输数据,因此相对安全一些。

综上所述,Web Storage相对于Cookies来说,具有更大的存储容量、更高的安全性和更好的性能,适合存储中小型数据。但如果需要设置过期时间、支持跨站点访问等需求,还是需要使用Cookies。需要根据实际情况选择适合的存储方式。

总体来说,Cookies适合存储少量简单的数据,Web Storage适合存储中小型数据,IndexedDB适合存储大量结构化数据。使用哪种存储方式要根据实际的需求来选择。

16. 多站点登录会话保存的方式

最后说下今年找工作的一个感受

其实上面那些问题,可能应届生都比我答的好,现在大环境不太好,岗位少要求高,现在看到网上还有人说毕业去培训机构学前端的我只能说,49年入国军。还有人高中没上完就去培训机构学这个的,这种情况出来想找个工作是很难的,现在IT行业非常看重学历,我大专学历一个月也就两三个面试,卷的厉害。