面经-2022-秋招-百度提前批一面


1.项目并发量级这么大,是怎么承受高并发的?怎么更新版本?

主要通过缓存,代理,消息队列和读写分离等等。

  1. Redis缓存:通过Redis缓存可以拦截大部分的数据库请求,从而降低数据库的压力。
  2. 反向代理:使用反向代理软件如Nginx可以将请求均匀分发到多个应用服务器上,从而提高了系统的并发处理能力。
  3. 系统拆分:将一个系统拆分为多个子系统,每个系统连接一个数据库,可以提高并发处理能力。
  4. 消息队列:对于高并发的写请求,可以使用消息队列如RabbitMQ、Kafka等,将请求放入队列中,再由后端系统逐个处理,降低了数据库的压力。
  5. 分库分表:将一个数据库拆分为多个库,每个库处理一部分数据,可以提高数据库的并发处理能力。同时,将一个表拆分为多个表,每个表的数据量保持较小,可以提高查询效率。
  6. 读写分离:大部分情况下,数据库的读请求远多于写请求,因此可以将主库设置为处理写请求,从库设置为处理读请求,从而分散数据库的压力。
  7. 代码优化:通过代码优化,减少不必要的数据库操作和计算,也可以提高系统的并发处理能力。例如,使用缓存来存储经常查询的数据,避免重复计算。

2.前端层面怎么设计可以减轻服务端压力,承受高并发?

前端可以通过缓存,异步处理等等进行减轻服务器压力的

  1. 前端性能优化
    • 校验:提前做好校验,可以限制大部分的请求。
    • 代码优化:如减少HTTP请求次数、利用CDN加速、利用浏览器缓存、减少不必要的JavaScript代码等。
    • 资源压缩与合并:如压缩CSS、JavaScript、图片等,减少文件大小。
    • 懒加载和按需加载:只加载当前页面的资源,或者根据用户的实际需求来加载不同的资源。
  2. 前端缓存:使用例如Service Workers等API,可以实现缓存常用的资源,减少与服务器的通信次数。
  3. 前端异步处理:将部分计算或处理逻辑放在前端进行,减少后端的工作量。
  4. 使用前端限流和降级策略:在流量过高或某些关键功能出现问题时,可以限制某些请求或降级部分功能以保证核心功能的可用性。

3.介绍一下HTTP缓存

HTTP缓存可以分为两种类型:强缓存和协商缓存。

强缓存是由浏览器直接控制的缓存机制,当浏览器认为某个资源已经过期时,就会重新向服务器发送请求,并带上一个If-Modified-Since头,询问服务器该资源是否有新的版本。

协商缓存则是浏览器和服务器之间进行的一种缓存机制,当浏览器认为某个资源已经过期时,会向服务器发送一个带有Range头的请求,询问服务器该资源的部分内容。

4.CDN有什么好处?原理呢?

CDN即内容分发网络,通过在网络中放置缓存服务器来分散用户的访问流量,解决由于地域、带宽和流量限制等因素导致的网站访问速度慢等问题。

  1. 提高网站加载速度:CDN的缓存服务器分布在全球各地,能够将内容快速分发到用户附近的节点,使用户能够更快地获取到网站内容,从而提高网站的加载速度。
  2. 降低带宽成本:通过CDN将内容分发到缓存服务器上,可以减少对源服务器的带宽需求,从而降低带宽成本。
  3. 提高全球内容可访问性:CDN能够覆盖全球范围,使得用户无论身处何处都能快速访问网站内容,提高全球内容的可访问性。
  4. 增加冗余和可用性:CDN的多个节点可以提供冗余和可用性,当某个节点出现故障时,可以快速切换到其他节点上,保证网站的可用性和稳定性。

5.离线包有了解过吗?

离线包通常指的是一种预先制作好的软件包,可以在没有互联网连接的情况下进行安装和升级。

6.你的safeRouter是怎么封装的?怎么做的?

  1. 设计思想:safeRouter的设计思想是提供一种更加安全和灵活的路由处理方式,以确保Web应用的安全性和可靠性。
  2. 安装和引入:首先,需要安装safeRouter库。可以使用npm或yarn进行安装。安装完成后,可以在需要使用路由的组件中引入safeRouter库。
  3. 路由配置:在safeRouter中,路由配置与React Router类似,可以使用<Route><Switch>等组件来定义路由。
  4. 路由处理:在safeRouter中,路由处理使用了React的组件化思想,将每个路由对应一个组件。当用户访问某个路由时,safeRouter会渲染对应的组件。

7.VueRouter的原理是什么?

VueRouter的实现原理主要包括两部分:hash模式和history模式。

在hash模式中,VueRouter使用URL的hash来模拟一个完整的URL,当页面的URL改变时,页面不会重新加载,即不会导致浏览器向服务器发出请求。每次hash值的变化,都会触发hashchange这个事件,我们可以通过这个事件就可以知道hash值发生了什么变化。这样我们就可以通过监听hashchange这个事件来实现更新页面部分内容的操作。

在history模式中,VueRouter利用HTML5 History API来管理路由,这种模式可以更新视图而不会重新请求页面,原理与hash模式类似。

8.个人项目的权限系统是怎么设计的?如果更复杂的权限系统你会怎么设计?

对于简单的个人项目,有三个就可以了

  • 用户(User):存储用户的基本信息,如用户名、密码等。
  • 角色(Role):定义不同的角色,如管理员、编辑、作者等。每个角色都有相应的权限。
  • 权限(Permission)

复杂的可以按照细粒度权限控制

  • 资源(Resource):每种资源都有自己的访问和操作权限。
  • 策略(Policy):基于角色的访问控制(RBAC)或基于策略的访问控制(ABAC),可以根据用户的角色、属性或上下文环境来动态地确定用户的访问权限。

9.封装一个通用的根据权限渲染不同内容的组件你会怎么设计?

  1. 定义权限数据结构:使用一个对象或数组来存储权限信息,其中每个键值对或元素表示一个角色和对应的权限。
  2. 创建组件:创建一个组件,该组件接受权限数据作为输入,并根据权限渲染不同的内容。
  3. 传递权限数据:将权限数据作为属性传递给组件。你可以在父组件中定义权限数据,并将其传递给该组件。
  4. 渲染不同内容:在组件内部,根据传递进来的权限数据,使用条件渲染来展示不同的内容。
  5. 处理无权限情况:可以在条件渲染中加入适当的逻辑来处理无权限的情况。
  6. 可扩展性:设计一些插槽(slot)或使用子组件等方式,让用户能够自定义渲染的内容和样式。
  7. 测试与验证:在实际应用之前,对组件进行充分的测试和验证,以确保其能够正确地根据权限渲染不同的内容。
import React from 'react';  
  
// 定义一个表示权限的数据结构  
const permissions = {  
  admin: ['viewAdminContent', 'editAdminContent'],  
  editor: ['viewEditorContent', 'editEditorContent'],  
  user: ['viewUserContent']  
};  
  
// 创建根据权限渲染不同内容的组件  
const PermissionComponent = ({ role }) => {  
  const { viewAdminContent, editAdminContent, viewEditorContent, editEditorContent, viewUserContent } = permissions[role];  
  
  return (  
    <div>  
      {viewAdminContent && <div>Admin Content</div>}  
      {editAdminContent && <div>Edit Admin Content</div>}  
      {viewEditorContent && <div>Editor Content</div>}  
      {editEditorContent && <div>Edit Editor Content</div>}  
      {viewUserContent && <div>User Content</div>}  
    </div>  
  );  
};  
  
export default PermissionComponent;

10.浏览器的时间循环机制描述一下?

浏览器的时间循环机制,也称为事件循环(Event Loop),是浏览器处理异步事件(如用户交互、网络请求等)的核心机制。

  1. 任务队列:当异步事件发生时(例如,用户点击按钮、网络请求完成等),这些事件会被添加到任务队列中。这些任务可以是宏任务(MacroTask)或微任务(MicroTask)。
  2. 宏任务:主要包括script(整个HTML文档)、setTimeout、setInterval、setImmediate、requestAnimationFrame、I/O、UI渲染等。
  3. 微任务:主要包括Promise.then、MutationObserver等。
  4. 事件循环:当当前宏任务执行完毕,会查看微任务队列,执行完微任务队列中的所有任务后再查看宏任务队列,以此类推不断循环。
  5. 执行栈:用来保存当前需要执行的宏任务和微任务的栈。当一个宏任务被添加到执行栈中,它会被执行。执行完毕后,如果还有微任务队列中的任务,就将其全部加入到执行栈中执行。
  6. 循环过程:当一个宏任务被执行完毕后,事件循环会检查微任务队列。如果微任务队列中有任务,事件循环会将其全部添加到执行栈中并执行。执行完毕后再次检查微任务队列,直到微任务队列为空。然后,事件循环会查看宏任务队列,将下一个宏任务添加到执行栈并执行。这个过程会不断重复,形成一个循环。
  7. 事件循环与计时器:计时器(如setTimeout和setInterval)相关的回调函数会被放在宏任务队列中。当浏览器渲染完一帧画面后,会从宏任务队列中取出一个回调函数放入执行栈中执行。

11.Node事件循环和浏览器有什么区别?

  1. 环境差异:浏览器中的事件循环主要用于处理用户交互事件(例如点击、滚动等)和网络请求等异步任务。而Node.js的事件循环主要用于处理I/O操作(例如文件操作、网络请求等)和自定义的事件。
  2. 任务队列:在浏览器中,任务队列由浏览器的任务调度器管理,而Node.js使用自己的任务队列和事件循环机制。
  3. 事件处理:浏览器的事件循环包括DOM事件处理,例如点击、滚动等用户与网页的交互。而Node.js的事件循环更侧重于处理服务器端事件,如读取文件、处理数据库操作等。
  4. 异步处理:浏览器的事件循环机制通常使用回调函数来处理异步操作,而Node.js在0.10版本之前也使用回调函数,但从0.11版本开始引入了Promise和Generator来更好地处理异步操作。
  5. 线程模型:浏览器通常在单线程环境中运行,而Node.js在V8引擎之外使用了libuv库来实现异步I/O操作的多线程支持。

12.遍历一个对象有什么形式?

  1. 使用for…in循环:这是遍历对象自身可枚举属性最常用的方法。
for (let key in object) {
  if (object.hasOwnProperty(key)) {
    console.log(key, object[key]);
  }
}
  1. 使用Object.keys()方法:该方法返回一个数组,包含对象自身的所有可枚举属性(不含Symbol属性)的键。可以结合for…of循环使用。
const object = {a: 1, b: 2};
for (const key of Object.keys(object)) {
  console.log(key, object[key]);
}
  1. 使用Object.getOwnPropertyNames()方法:该方法返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)的键。
const object = {a: 1, b: 2};
Object.defineProperty(object, 'c', {value: 3, enumerable: false});
for (const key of Object.getOwnPropertyNames(object)) {
  console.log(key, object[key]);
}
  1. 使用Reflect.ownKeys()方法:该方法返回一个数组,包含对象自身的所有属性,不管属性名是Symbol还是字符串,也不管是否可枚举。
const object = {a: 1, b: 2};
Object.defineProperty(object, 'c', {value: 3, enumerable: false});
for (const key of Reflect.ownKeys(object)) {
  console.log(key, object[key]);
}
  1. 使用for…of循环和Object.values()或Object.entries()方法:如果想要遍历对象的值或键值对,可以使用这些方法。
const object = {a: 1, b: 2};
for (const value of Object.values(object)) {
  console.log(value);
}
for (const [key, value] of Object.entries(object)) {
  console.log(key, value);
}

13.for in有什么缺陷?怎么避免?

  1. 遍历顺序不确定:使用for…in循环遍历对象的属性时,属性的遍历顺序是不确定的,取决于JavaScript引擎。
  2. 遍历对象继承属性:使用for…in循环遍历对象属性时,会遍历对象继承的属性。例如,如果一个对象通过Object.create继承了另一个对象的属性,使用for…in循环会遍历到这些继承的属性。
  3. 遍历对象非数字属性:使用for…in循环遍历对象属性时,会遍历对象的所有属性,包括非数字属性。

14.for of可以遍历对象吗?怎么让它能遍历

for...of 循环主要用于遍历数组和可迭代对象(如 Map、Set 等),而不是直接用于遍历对象。

以下是一个示例,演示如何使用 Object.entries() 方法将对象的属性转换为数组,并使用 for...of 循环遍历属性键值对:

const obj = {a: 1, b: 2, c: 3};

for (const [key, value] of Object.entries(obj)) {
  console.log(key, value);
}

Object.entries(obj) 将对象的属性转换为包含键值对的数组,然后 for...of 循环遍历这个数组,每次迭代都获取一个包含键和值的数组元素。

15.v-model的原理

v-model在背后进行的操作可以拆分为两个步骤:通过事件监听更新数据和通过数据响应更新视图。具体来说,Vue使用v-on指令监听表单元素的输入事件(如input事件),当用户输入内容时触发该事件,Vue会根据事件的值更新绑定的数据属性。

同时,Vue使用数据的响应式系统,将绑定的数据属性与表单元素的值进行关联。当绑定的数据属性发生变化时,Vue会通知相关的表单元素更新其显示的值,从而保持数据与视图的同步。

16.怎么实现多个位置绑定同一个v-model?

<template>
  <div>
    <input v-model="message" placeholder="输入1">
    <input v-model="message" placeholder="输入2">
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: ''
    }
  }
}
</script>

创建了两个input元素,它们都使用了v-model指令绑定到同一个数据属性message。无论哪个输入框中的值发生变化,都会反映到message属性上,并且在页面上的p元素中显示出来。这样就实现了多个位置绑定同一个v-model。

17.vue组件传值有哪些方式?尽可能多地说

在Vue中,组件之间的传值有多种方式

  1. props 属性:父组件通过 props 属性将数据传递给子组件。子组件通过定义 props 选项来声明接收的属性。

父组件:

<template>
  <child-component :message="parentMessage"></child-component>
</template>

<script>
export default {
  data() {
    return {
      parentMessage: 'Hello from parent'
    };
  }
};
</script>

子组件:

export default {
  props: ['message']
};
  1. **事件 (emit)**:子组件通过 $emit 方法触发自定义事件,并将数据传递给父组件。父组件监听该事件并获取数据。

子组件:

this.$emit('child-event', 'Hello from child');

父组件:

<template>
  <child-component @child-event="handleChildEvent"></child-component>
</template>
  1. $attrs$listeners:这两个选项可以用来传递非 prop 属性的值。$attrs包含父作用域中没有被当前组件 prop 选项解析的属性,$listeners 包含传递给组件的所有事件监听器。
  2. **插槽 (Slots)**:当需要将数据从父组件传递到子组件的模板中时,可以使用插槽。父组件可以在插槽内容中插入数据,然后在子组件的模板中访问这些数据。
  3. provide/inject:这是一个更高级的选项,允许你创建一个可注入的值或对象,可以在任何组件的任何层级注入。这对于全局状态管理非常有用。
  4. **事件总线 (Event Bus)**:这是一个轻量级的解决方案,允许你在不直接相互引用的情况下在组件之间进行通信。
  5. 自定义事件:你可以使用 $emit 在一个组件中触发一个自定义事件,然后在另一个组件中监听这个事件。

18.工程化了解过吗?Webpack的Loader和Plugin有什么区别?

Loader是Webpack中用于处理非JavaScript文件的模块转换器。换句话说,它们将非JavaScript文件转换为Webpack能够理解和处理的JavaScript模块。Webpack默认只能理解JavaScript和JSON文件,对于其他类型的文件,需要使用相应的loader进行转换。每个loader本质上都是一个导出为函数的JavaScript模块,每个loader都需要定义如何处理特定的文件类型。

Plugin则用于解决一些loader无法实现的功能。Plugin是Webpack的支柱功能,Webpack整体的程序架构也是基于插件系统之上搭建的。Plugin的目的在于解决loader无法实现的其他功能。通常来说,我们需要集成某款Plugin时,会先通过npm安装到本地,然后在配置文件(webpack.config.js)的头部引入,在plugins那一栏使用new关键字生成插件的实例注入到Webpack。Webpack注入了Plugin之后,那么在Webpack后续构建的某个时间节点就会触发Plugin定义的功能。

19.拆包有了解过吗?路由懒加载有了解过吗?讲一讲

拆包是一个大的模块或组件拆分成更小的、更易于管理的部分,以提高代码的可读性和可维护性。

路由懒加载是一种提高应用程序性能的技术。在单页面应用程序中,如果所有的路由组件在初始化时全部加载,会导致应用程序的初始加载时间过长,用户体验不佳。

通过路由懒加载,我们可以在需要的时候才加载相应的组件,从而提高应用程序的性能和用户体验。具体来说,当用户访问某个路由时,我们才加载该路由对应的组件,如果用户访问其他路由,则不加载该组件。这样可以节省网络带宽和内存占用,减少页面加载时间。同时,路由懒加载还可以使得应用程序更灵活地进行维护。

20.讲一讲你了解的数据结构

常见的数据结构包括数组、链表、栈、队列、树、图

21.链表的头插法、查询、有序插入时间复杂度是多少?

  1. 头插法:时间复杂度:O(1)。
  2. 查询:时间复杂度:O(n)。
  3. 有序插入:时间复杂度:O(n^2)。

22.二叉树的查询复杂度是多少?

对于按层顺序遍历(BFS)二叉树,查询复杂度是O(log⁡n)O(\log n)O(logn),其中n是二叉树中节点的数量。这是因为在最坏的情况下,需要遍历的节点数近似为二叉树的高度,而二叉树的高度是log⁡n\log nlogn。

如果使用深度优先搜索(DFS)策略,查询复杂度可能是O(n),因为在最坏的情况下,可能需要遍历整个二叉树。

但如果使用特定的数据结构(如平衡二叉搜索树、AVL树、红黑树等),可以在平均情况下将查询时间复杂度降低到O(log⁡n)O(\log n)O(logn)。

23.快排的时间复杂度是多少?

在最坏的情况下,快速排序的时间复杂度是O(n^2)。

在平均情况下,如果每次随机选择一个基准元素都能将数组均匀地分成两个部分,那么快速排序的时间复杂度是O(nlog⁡n)。

如果使用最优的划分策略,每次选择一个最小(或最大)的元素作为基准,那么在最坏的情况下,时间复杂度仍然是O(nlog⁡n)。

算法1:数组去重

在JavaScript中,可以使用多种方法来去除数组中的重复项。以下是一些常见的方法:

1. 使用Set

Set是一种ES6引入的数据结构,它类似于数组,但成员的值都是唯一的,没有重复的值。

let arr = [1, 2, 3, 4, 5, 3, 2, 6];
let uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1, 2, 3, 4, 5, 6]

2. 使用filter方法

filter方法创建一个新数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。

let arr = [1, 2, 3, 4, 5, 3, 2, 6];
let uniqueArr = arr.filter((value, index, self) => {
  return self.indexOf(value) === index;
});
console.log(uniqueArr); // [1, 2, 3, 4, 5, 6]

3. 使用reduce方法

reduce方法对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个输出值。

let arr = [1, 2, 3, 4, 5, 3, 2, 6];
let uniqueArr = arr.reduce((accumulator, current) => {
  if (!accumulator.includes(current)) {
    accumulator.push(current);
  }
  return accumulator;
}, []);
console.log(uniqueArr); // [1, 2, 3, 4, 5, 6]

算法2:爬楼梯

例如,如果有一段楼梯有N阶,每次你可以爬1阶或者2阶,你有多少种不同的方式爬上这段楼梯?

可以用一个数组dp来保存爬到第i阶楼梯的不同方式的数量。那么dp[i]就是爬到第i阶楼梯的不同方式的数量。

我们可以得到以下状态转移方程:

dp[i] = dp[i-1] + dp[i-2]

其中dp[i-1]表示爬1阶的方式,dp[i-2]表示爬2阶的方式。

下面是一个JavaScript实现的例子:

function climbStairs(n) {
    if (n <= 2) {
        return n;
    }
    let dp = new Array(n + 1).fill(0);
    dp[1] = 1;
    dp[2] = 2;
    for (let i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

反问环节、业务介绍

技术栈和部门业务

链接:https://juejin.cn/post/7025793782998106149


文章作者: 千羽
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 千羽 !
评论
  目录