2023.07.23 更新前端面试问题总结(9道题)

391 阅读18分钟

2023.07.06 - 2023.07.23 更新前端面试问题总结(9道题)
获取更多面试相关问题可以访问
github 地址: github.com/pro-collect…
gitee 地址: gitee.com/yanleweb/in…

目录:

  • 初级开发者相关问题【共计 1 道题】

    • 483.如何实现页面顶部, 自定义滚动进度条样式【热度: 1,220】【CSS】【出题公司: 快手】
  • 中级开发者相关问题【共计 5 道题】

    • 475.[React] 合成事件和原生事件触发的先后顺序如何?【热度: 1,445】【web框架】
    • 477.[React] ref 有哪些使用场景,请举例【热度: 668】【web框架】【出题公司: 美团】
    • 478.下面代码的执行结果是多少(意义不大)【JavaScript】
    • 479.模拟new操作【热度: 1,186】【JavaScript】【出题公司: 滴滴】
    • 481.async/await 函数到底要不要加 try catch ?【热度: 645】【JavaScript】
  • 高级开发者相关问题【共计 1 道题】

    • 476.[React] 函数组件和 class 组件有什么区别?【热度: 1,029】【web框架】【出题公司: PDD】
  • 资深开发者相关问题【共计 2 道题】

    • 480.讲一下Webpack设计理念(过于硬核, 直接上文档了)【web框架】
    • 482.如何搭建一套灰度系统?【热度: 1,226】【工程化】【出题公司: 腾讯】

初级开发者相关问题【共计 1 道题】

483.如何实现页面顶部, 自定义滚动进度条样式【热度: 1,220】【CSS】【出题公司: 快手】

关键词:自定义滚动条、自定义顶部滚动条

要实现页面顶部的自定义滚动进度条样式,可以按照以下步骤进行:

  1. 在HTML中添加滚动进度条的容器元素,通常可以使用一个

    元素作为容器,放在页面顶部的合适位置。

<div id="scroll-progress"></div>
  1. 在CSS中定义滚动进度条的样式。可以使用背景颜色、高度、透明度等属性来自定义样式。
#scroll-progress {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 5px;
    background-color: #f00; /* 自定义进度条颜色 */
    opacity: 0.7; /* 自定义进度条透明度 */
    z-index: 9999; /* 确保进度条显示在最顶层 */
}
  1. 使用JavaScript来监听页面滚动事件,并更新滚动进度条的宽度。
 var scrollProgress = document.getElementById('scroll-progress');
var requestId;

function updateScrollProgress() {
  var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  var scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
  var progress = (scrollTop / (scrollHeight - window.innerHeight)) * 100;
  scrollProgress.style.width = progress + '%';
  requestId = null;
}

function scrollHandler() {
  if (!requestId) {
    requestId = requestAnimationFrame(updateScrollProgress);
  }
}

window.addEventListener('scroll', scrollHandler);

以上就是一个简单的实现页面顶部自定义滚动进度条样式的方法。根据自己的需求,可以调整CSS样式和JavaScript的逻辑来实现不同的效果。

完整代码:

<!DOCTYPE html>
<html>
<head>
  <title>自定义滚动进度条样式</title>
  <style>
    #scroll-progress {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 5px;
      background-color: #f00; /* 自定义进度条颜色 */
      opacity: 0.7; /* 自定义进度条透明度 */
      z-index: 9999; /* 确保进度条显示在最顶层 */
    }
  </style>
</head>
<body>
<div id="scroll-progress"></div>

<!-- 假设有很长的内容 -->
<div style="height: 2000px;"></div>

<script>
  var scrollProgress = document.getElementById('scroll-progress');
  var requestId;

  function updateScrollProgress() {
    var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
    var scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
    var progress = (scrollTop / (scrollHeight - window.innerHeight)) * 100;
    scrollProgress.style.width = progress + '%';
    requestId = null;
  }

  function scrollHandler() {
    if (!requestId) {
      requestId = requestAnimationFrame(updateScrollProgress);
    }
  }

  window.addEventListener('scroll', scrollHandler);
</script>
</body>
</html>

中级开发者相关问题【共计 5 道题】

475.[React] 合成事件和原生事件触发的先后顺序如何?【热度: 1,445】【web框架】

关键词:React合成事件、原生事件、事件执行先后顺序

在React中,合成事件和原生事件的触发顺序是先合成事件,然后是原生事件。

React使用了一种称为"合成事件" 的机制来处理事件。当你在组件中使用事件属性(例如onClick)时,React会在底层创建合成事件,并将其附加到相应的DOM元素上。合成事件是React自己实现的一套事件系统,它通过事件委托和其他技术来提供更好的性能和一致的事件处理方式。

当触发一个合成事件时,React会首先执行事件的处理函数,然后会调用合成事件的stopPropagation()方法来阻止事件冒泡。如果处理函数调用了stopPropagation(),则合成事件会终止,不再触发原生事件。

如果合成事件没有被终止,并且对应的DOM元素上还有原生事件监听器,React会触发相应的原生事件。原生事件是由浏览器提供的,React并没有对其进行改变或拦截。

因此,合成事件和原生事件的触发顺序是先合成事件,然后是原生事件。这意味着在事件处理函数中,你可以放心地使用合成事件对象,而不需要担心原生事件的影响。

为何有一些文章是说, 原生事件先执行?

原生事件先执行的说法是因为在React早期的版本中,React使用事件委托的方式来处理事件。事件委托是指将事件处理函数绑定在父元素上,然后利用事件冒泡机制,通过父元素捕获并处理子元素的事件。这种方式会导致在事件冒泡阶段,父元素的事件处理函数会先于子元素的事件处理函数执行。

在这种情况下,如果一个组件有一个合成事件和一个原生事件绑定在同一个元素上,原生事件的处理函数会在合成事件的处理函数之前执行。这就造成了一些文章中提到的原生事件先执行的观察结果。

然而,从React v16开始,React改变了事件处理的方式,不再使用事件委托,而是直接将事件处理函数绑定在目标元素上。这样做的好处是提高了性能,并且保证了事件处理函数的执行顺序与绑定顺序一致。

因此,根据React的最新版本,合成事件会先于原生事件执行。如果你发现有一些旧的文章提到原生事件先执行,那可能是因为这些文章对React的早期版本进行了描述,不适用于目前的React版本。

477.[React] ref 有哪些使用场景,请举例【热度: 668】【web框架】【出题公司: 美团】

关键词:ref 使用场景、ref 获取dom、ref 获取子组件属性和方法

React的ref用于获取组件或DOM元素的引用。它有以下几个常见的使用场景:

  1. 访问子组件的方法或属性:通过ref可以获取子组件的实例,并调用其方法或访问其属性。
import React, { useRef } from 'react';

function ChildComponent() {
  const childRef = useRef(null);

  const handleClick = () => {
    childRef.current.doSomething();
  }

  return (
    <div>
      <button onClick={handleClick}>Click</button>
      <Child ref={childRef} />
    </div>
  );
}

const Child = React.forwardRef((props, ref) => {
  const doSomething = () => {
    console.log('Doing something...');
  }

  // 将ref引用绑定到组件的实例
  React.useImperativeHandle(ref, () => ({
    doSomething
  }));

  return <div>Child Component</div>;
});
  1. 获取DOM元素:通过ref可以获取组件渲染后的DOM元素,并进行操作。
import React, { useRef } from 'react';

function MyComponent() {
  const inputRef = useRef(null);

  const handleClick = () => {
    inputRef.current.focus();
  }

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>Focus Input</button>
    </div>
  );
}
  1. 动态引用:通过ref可以在函数组件中动态地引用不同的组件或DOM元素。
import React, { useRef } from 'react';

function MyComponent() {
  const ref = useRef(null);
  const condition = true;

  const handleClick = () => {
    ref.current.doSomething();
  }

  return (
    <div>
      {condition ? (
        <ChildComponent ref={ref} />
      ) : (
        <OtherComponent ref={ref} />
      )}
      <button onClick={handleClick}>Click</button>
    </div>
  );
}

这些例子展示了一些使用React的ref的常见场景,但实际上,ref的用途非常灵活,可以根据具体需求进行扩展和应用。

478.下面代码的执行结果是多少(意义不大)【JavaScript】

执行结果是多少, 为什么?

var foo = function() {
  console.log("foo1")
}
foo()

var foo = function() {
  console.log("foo2")
}
foo()


function foo() {
  console.log("foo1")
}

foo()

function foo() {
  console.log("foo2")
}

foo()

执行结果是:

foo1
foo2
foo2
foo2

原因:  首先,变量foo被赋值为一个函数表达式function () { console.log("foo1") },然后立即调用foo(),输出结果为foo1

接下来,变量foo再次被赋值为另一个函数表达式function () { console.log("foo2") },然后再次调用foo(),输出结果为foo2

然后,函数声明function foo() { console.log("foo1") }被解析并提升到作用域的顶部,但由于变量foo已经被重新赋值为函数表达式,因此这个函数声明不会对变量foo产生影响。

最后,另一个函数声明function foo() { console.log("foo2") }也被解析并提升到作用域的顶部。然后再次调用foo(),由于变量foo指向最后一个函数声明,输出结果为foo2 。这也说明了后面的函数声明覆盖了前面的函数声明。

479.模拟new操作【热度: 1,186】【JavaScript】【出题公司: 滴滴】

关键词:模拟 new

可以使用以下代码来模拟new操作:

function myNew(constructor, ...args) {
  // 创建一个新对象,该对象继承自构造函数的原型
  const obj = Object.create(constructor.prototype);

  // 调用构造函数,并将新对象作为this值传递进去
  const result = constructor.apply(obj, args);

  // 如果构造函数返回一个对象,则返回该对象,否则返回新创建的对象
  return typeof result === 'object' && result !== null ? result : obj;
}

使用示例:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
}

const john = myNew(Person, "John", 25);
john.sayHello(); // 输出:Hello, my name is John and I'm 25 years old.

在上述代码中,myNew函数模拟了new操作的过程:

  1. 首先,通过Object.create创建了一个新对象obj,并将构造函数的原型对象赋值给该新对象的原型。
  2. 然后,使用apply方法调用构造函数,并传入新对象obj作为this值,以及其他参数。
  3. 最后,根据构造函数的返回值判断,如果返回的是一个非空对象,则返回该对象;否则,返回新创建的对象obj

这样,我们就可以使用myNew函数来模拟new操作了。

481.async/await 函数到底要不要加 try catch ?【热度: 645】【JavaScript】

关键词:async/await函数、async/await函数 是否需要 try/catch、async/await函数 与 try/catch 关系、try/catch 使用场景

当使用 async 函数的时候,很多文章都说建议用 try catch 来捕获异常, 可是实际上很多项目的代码,遵循的并不是严谨,很多都没有用,甚至 catch 函数都没写,这是为什么呢?

示例1 :使用 try catch

function getUserInfo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('请求异常')
    }, 1000)
  })
}

async function logined() {
  try {
    let userInfo = await getUserInfo()
    // 执行中断
    let pageInfo = await getPageInfo(userInfo?.userId)
  } catch (e) {
    console.warn(e)
  }
}

logined()

执行后会在 catch 里捕获 请求异常,然后 getUserInfo 函数中断执行,这是符合逻辑的,对于有依赖关系的接口,中断执行可以避免程序崩溃,这里唯一的问题是 try catch 貌似占据了太多行数,如果每个接口都写的话看起来略显冗余。

示例2: 直接 catch

鉴于正常情况下,await 命令后面是一个 Promise 对象, 所以上面代码可以很自然的想到优化方案:

function getUserInfo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('请求异常')
    }, 1000)
  })
}

async function logined() {
  let userInfo = await getUserInfo().catch(e => console.warn(e))
  // 执行没有中断,userInfo 为 undefined
  if (!userInfo) return // 需要做非空校验
  let pageInfo = await getPageInfo(userInfo?.userId)
}

logined()

执行后 catch 可以正常捕获异常,但是程序没有中断,返回值 userInfo 为 undefined, 所以如果这样写的话,就需要对返回值进行非空校验, if (!userInfo) return 我觉得这样有点反逻辑,异常时就应该中断执行才对;

示例3:在 catch 里 reject

可以继续优化,在 catch 里面加一行 return Promise.reject(e), 可以使 await 中断执行;

完整代码:

function getUserInfo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('请求异常')
    }, 1000)
  })
}

async function logined() {
  let userInfo = await getUserInfo().catch(e => {
    console.warn(e)
    return Promise.reject(e) // 会导致控制台出现 uncaught (in promise) 报错信息
  })
  // 执行中断
  let pageInfo = await getPageInfo(userInfo?.userId)
}

logined()

一般我们在项目里都是用 axios 或者 fetch 之类发送请求,会对其进行一个封装,也可以在里面进行 catch 操作,对错误信息先一步处理, 至于是否需要 reject,就看你是否想要在 await 命令异常时候中断了; 不使用 reject 则不会中断,但是需要每个接口拿到 response 后先 非空校验, 使用 reject 则会在异常处中断,并且会在控制台暴露 uncaught (in promise) 报错信息。

建议

不需要在 await 处异常时中断,可以这样写,需要做非空校验,控制台不会有报错信息

let userInfo = await getUserInfo().catch(e => console.warn(e))
if (!userInfo) return

需要在 await 处异常时中断,并且在意控制台报错,可以这样写

try {
  let userInfo = await getUserInfo()
  // 执行中断
  let pageInfo = await getPageInfo(userInfo?.userId)
} catch (e) {
  console.warn(e)
}

需要在 await 处异常时中断,但是不在意控制台报错,则可以这样写

let userInfo = await getUserInfo().catch(e => {
  console.warn(e)
  return Promise.reject(e) // 会导致控制台出现 uncaught (in promise) 报错信息
})
// 执行中断
let pageInfo = await getPageInfo(userInfo?.userId)

总结

几种写法,初看可能觉得第三种 catch 这种写法是最好的,但是细想下,从用户体验上来看,我觉得 try catch 是最好的,逻辑直观、符合同步编程思维,控制台不会暴露 uncaught (in promise) 报错信息;

而链式调用的 catch (里面再 reject),是传统 promise 的回调写法,既然已经用 async await 这种同步编程写法了,再用 catch 链式写法,感觉没必要。

高级开发者相关问题【共计 1 道题】

476.[React] 函数组件和 class 组件有什么区别?【热度: 1,029】【web框架】【出题公司: PDD】

关键词:React函数组件对比类组件、React函数组件对比类组件性能、React函数组件对比类组件状态管理、React函数组件与类组件

函数组件和类组件是React中两种定义组件的方式,它们有以下区别:

  1. 语法:函数组件是使用函数声明的方式定义组件,而类组件是使用ES6的class语法定义组件。
  2. 写法和简洁性:函数组件更为简洁,没有类组件中的繁琐的生命周期方法和this关键字。函数组件只是一个纯粹的JavaScript函数,可以直接返回JSX元素。
  3. 状态管理:在React的早期版本中,函数组件是无法拥有自己的状态(state)和生命周期方法的。但是从React 16.8开始,React引入了Hooks(钩子)机制,使得函数组件也能够拥有状态和使用生命周期方法。
  4. 性能:由于函数组件不拥有实例化的过程,相较于类组件,它的性能会稍微高一些。但是在React 16.6之后,通过React.memo和PureComponent的优化,类组件也能够具备相对较好的性能表现。

总体来说,函数组件更加简洁、易读,适合用于无需复杂逻辑和生命周期方法的场景,而类组件适合于需要较多逻辑处理和生命周期控制的场景。另外,使用Hooks后,函数组件也能够拥有与类组件类似的能力,因此在开发中可以更加灵活地选择使用哪种方式来定义组件。

状态管理方面做对比

从状态管理的角度来看,函数组件和类组件在React中的区别主要体现在以下几个方面:

  1. 类组件中的状态管理:类组件通过使用state属性来存储和管理组件的状态。state是一个对象,可以通过this.state进行访问和修改。类组件可以使用setState 方法来更新状态,并通过this.setState来触发组件的重新渲染。在类组件中,状态的更新是异步的,React会将多次的状态更新合并为一次更新,以提高性能。
  2. 函数组件中的状态管理:在React之前的版本中,函数组件是没有自己的状态的,只能通过父组件通过props传递数据给它。但是从React 16.8版本开始,通过引入Hooks机制,函数组件也可以使用useState 钩子来定义和管理自己的状态。useState返回一个状态值和一个更新该状态值的函数,通过解构赋值的方式进行使用。每次调用状态更新函数,都会触发组件的重新渲染。
  3. 类组件的生命周期方法:类组件有很多生命周期方法,例如componentDidMountcomponentDidUpdatecomponentWillUnmount 等等。这些生命周期方法可以用来在不同的阶段执行特定的逻辑,例如在componentDidMount中进行数据的初始化,在componentDidUpdate 中处理状态或属性的变化等等。通过这些生命周期方法,类组件可以对组件的状态进行更加细粒度的控制。
  4. 函数组件中的副作用处理:在函数组件中,可以使用useEffect钩子来处理副作用逻辑,例如数据获取、订阅事件、DOM操作等。useEffect 接收一个回调函数和一个依赖数组,可以在回调函数中执行副作用逻辑,依赖数组用于控制副作用的执行时机。函数组件的副作用处理与类组件的生命周期方法类似,但是可以更灵活地控制执行时机。

函数组件和类组件在状态管理方面的主要区别是函数组件通过使用Hooks机制来定义和管理状态,而类组件通过state属性来存储和管理状态。 函数组件中使用useState来定义和更新状态,而类组件则使用setState方法。 另外,函数组件也可以使用useEffect来处理副作用逻辑,类似于类组件的生命周期方法。通过使用Hooks,函数组件在状态管理方面的能力得到了大幅度的提升和扩展。

性能方面做对比

在性能方面,函数组件和类组件的表现也有一些区别。

  1. 初始渲染性能:函数组件相对于类组件来说,在初始渲染时具有更好的性能。这是因为函数组件本身的实现比类组件更加简单,不需要进行实例化和维护额外的实例属性。函数组件在渲染时更轻量化,因此在初始渲染时更快。

更新性能:当组件的状态或属性发生变化时,React会触发组件的重新渲染。在类组件中,由于状态的更新是异步的,React会将多次的状态更新合并为一次更新,以提高性能。而在函数组件中,由于每次状态更新都会触发组件的重新渲染,可能会导致性能略低于类组件。但是,通过使用React的memo或useMemo、useCallback等优化技术,可以在函数组件中避免不必要的重新渲染,从而提高性能。

  1. 代码拆分和懒加载:由于函数组件本身的实现比类组件更加简单,所以在进行代码拆分和懒加载时,函数组件相对于类组件更容易实现。React的Suspense和lazy技术可以在函数组件中实现组件的按需加载,从而提高应用的性能。

函数组件相对于类组件在初始渲染和代码拆分方面具有优势,在更新性能方面可能稍逊一筹。然而,React的优化技术可以在函数组件中应用,以提高性能并减少不必要的渲染。此外,性能的差异在实际应用中可能并不明显,因此在选择使用函数组件还是类组件时,应根据具体场景和需求进行综合考量。

资深开发者相关问题【共计 2 道题】

480.讲一下Webpack设计理念(过于硬核, 直接上文档了)【web框架】

参考文档: juejin.cn/post/717085…

482.如何搭建一套灰度系统?【热度: 1,226】【工程化】【出题公司: 腾讯】

关键词:灰度上线

这个是一个非常复杂的话题, 没法直接给出答案, 进提供一些实现的思路:

什么是灰度

灰度系统可以把流量划分成多份,一份走新版本代码,一份走老版本代码。

而且灰度系统支持设置流量的比例,比如可以把走新版本代码的流程设置为 5%,没啥问题再放到 10%,50%,最后放到 100% 全量。

这样可以把出现问题的影响降到最低。

而且灰度系统不止这一个用途,比如产品不确定某些改动是不是要的,就要做 AB 实验,也就是要把流量分成两份,一份走 A 版本代码,一份走 B 版本代码。

实现思路

  1. 后端支持:灰度上线需要后端的支持,通过后端的灰度发布控制,可以将不同版本的前端应用分配给不同用户。
  2. 搭建网关层: 支持一部分用户分发到 A 版本, 一部分用户分发到 B 版本 (通常使用 nginx 搭建)。
  3. 版本管控机制: 使用版本控制系统(如Git、package.version、hash version 等)来管理不同版本的前端应用代码。在灰度上线时,可以根据需要切换到特定的版本。
  4. 动态路由:通过动态路由配置,将用户请求导向不同版本的前端应用。例如,可以使用Nginx或其他反向代理服务器来实现动态路由。
  5. 流量染色:使用Cookie或Session来控制用户的灰度版本访问。可以通过设置不同的Cookie值或Session标记,将用户分配到不同的灰度版本。
  6. 更复杂的漏量配置: 例如要根据部门、权限、角色等方式来开放灰度;可以使用让用户访问应用的时候, 查询其权限和角色, 根据权限和角色来分发不同的页面路由。

参考文档