当前位置:首页 > 技术分析 > 正文内容

收集整理各大厂前端面试问题 03.14 - 03.24 更新(45道题)

ruisui883个月前 (02-09)技术分析11

2023.03.14 - 2023.03.24 更新收集面试问题(45道题)
获取更多面试问题可以访问
github 地址:
https://github.com/pro-collection/interview-question/issues

gitee 地址:
https://gitee.com/yanleweb/interview-question/issues

目录:

  • 初级开发者相关问题【共计 1 道题】
    • 111.null 和 undefined 的区别,如何让一个属性变为 null?【JavaScript】
  • 中级开发者相关问题【共计 20 道题】
    • 73.express middleware(中间件) 工作原理是什么??【Nodejs】
    • 104.说一说 cookie sessionStorage localStorage 区别?【JavaScript】
    • 105.promise.race、promise.all、promise.allSettled 有哪些区别?【JavaScript】
    • 106.手写代码实现 promise.race【JavaScript】
    • 109.JavaScript 有几种方法判断变量的类型?【JavaScript】
    • 110.样式优先级的规则是什么?【CSS】
    • 115.Proxy 和 Object.defineProperty 的区别是啥?【JavaScript】
    • 117.css 中 三栏布局的实现方案 的实现方案有哪些?【CSS】
    • 119.vue 的 keep-alive 的原理是啥?【web框架】
    • 125.当使用 new 关键字创建对象时, 会经历哪些步骤?【JavaScript】
    • 126.es5 和 es6 使用 new 关键字实例化对象的流程是一样的吗?【JavaScript】
    • 127.如何实现可过期的 localstorage 数据?【JavaScript】
    • 132.React setState 是同步还是异步的?【web框架】
    • 133.react 18 版本中 setState 是同步还是异步的?【web框架】
    • 134.【React】合成事件了解多少【web框架】
    • 135.【React】绑定事件的原理是什么?【web框架】
    • 139.pnpm 和 npm 的区别?【工程化】
    • 142.事件循环原理?【JavaScript】
    • 143.[vue] 双向数据绑定原理?【web框架】
    • 146.nodejs 进程间如何通信?【Nodejs】
  • 高级开发者相关问题【共计 22 道题】
    • 77.虚拟 dom 原理是啥,手写一个简单的虚拟 dom 实现?【JavaScript】
    • 107.手写代码实现 promise.all【JavaScript】
    • 108.手写实现 Promise.allSettled【JavaScript】
    • 112.CSS 尺寸单位有哪些?【CSS】
    • 113.React Router 中 HashRouter 和 BrowserRouter 的区别和原理?【web框架】
    • 114.Vue3.0 实现数据双向绑定的方法是什么?【web框架】
    • 118.浏览器垃圾回收机制?【浏览器】
    • 120.常见的 web 前端网路攻击有哪些?【网络】
    • 121.如何防止 跨站脚本攻击(Cross-Site Scripting, XSS)?【网络】
    • 122.跨站请求伪造(Cross-Site Request Forgery, CSRF)具体实现步骤是啥, 如何防止?【网络】
    • 123.script 标签 defer 和 async 区别?【浏览器】
    • 124.Vue 中 $nextTick 作用与原理是啥?【web框架】
    • 128.axios的拦截器原理及应用、简单手写核心逻辑?【web框架】
    • 129.有什么方法可以保持前后端实时通信?【网络】
    • 130.react 遍历渲染节点列表, 为什么要加 key ?【web框架】
    • 131.react lazy import 实现懒加载的原理是什么?【web框架】
    • 136.如何分析页面加载慢?【工程化】
    • 137.【性能】以用户为中心的前端性能指标有哪些?【工程化】
    • 138.浏览器渲染进程了解多少?【浏览器】
    • 140.pnpm 了解多少?【工程化】
    • 141.如何组织 monorepo 工程?【工程化】
    • 144.[vue] 是怎么解析template的?【web框架】
  • 资深开发者相关问题【共计 2 道题】
    • 116.React Diff算法是怎么实现的?【JavaScript】
    • 145.实现 JS 沙盒的方式有哪些?【工程化】

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

111.null 和 undefined 的区别,如何让一个属性变为 null?【JavaScript】

null和undefined都是JavaScript中表示缺失或无值的特殊值。

undefined是一个变量没有被赋值时的默认值,或者在访问对象属性或数组元素不存在时返回的值。例如:

let a;
console.log(a); // 输出 undefined

const obj = {};
console.log(obj.nonexistentProp); // 输出 undefined

而null表示一个变量被明确地赋值为没有值。例如:

const a = null;
console.log(a); // 输出 null

要将一个属性的值设置为null,可以像这样:

const obj = { prop: 'value' };
obj.prop = null;
console.log(obj.prop); // 输出 null

如果要删除对象的属性并将其值设置为null,可以使用delete操作符:

const obj = { prop: 'value' };
delete obj.prop;
obj.prop = null;
console.log(obj.prop); // 输出 null

请注意,尝试访问一个已删除的属性将返回undefined而不是null。

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

73.express middleware(中间件) 工作原理是什么??【Nodejs】

express middleware 工作原理是什么?

Express middleware 的工作原理是通过拦截 HTTP 请求,对请求进行处理,然后将请求传递给下一个中间件或应用程序的路由处理。在 Express 中,中间件可以是一个或多个函数,每个函数都可以对请求进行操作或响应,从而实现对请求的处理和过滤。

当 Express 应用程序接收到一个 HTTP 请求时,请求将首先被传递给第一个注册的中间件函数。这个中间件函数可以对请求进行操作,例如修改请求的头信息、检查请求是否包含有效的身份验证令牌等等。当这个中间件函数完成操作后,它可以选择将请求传递给下一个中间件函数,或者直接将响应返回给客户端。

如果中间件函数选择将请求传递给下一个中间件函数,它可以调用 next() 函数来将控制权传递给下一个中间件。这个过程可以一直持续到所有中间件函数都被执行完毕,最后将请求传递给应用程序的路由处理函数。

通过使用中间件,开发人员可以将应用程序的功能模块化,从而实现更好的代码组织和可维护性。同时,中间件还可以实现各种功能,例如身份验证、日志记录、错误处理等等,从而为应用程序提供更丰富的功能和更好的用户体验。

它的设计模式是啥?写一个简单的示例呢

Express middleware 的设计模式是基于责任链模式。在责任链模式中,每个对象都有机会处理请求,并将其传递给下一个对象,直到请求被完全处理为止。在 Express 中,每个中间件函数都有机会对请求进行处理,并可以选择将请求传递给下一个中间件函数或应用程序的路由处理函数。

以下是一个简单的示例,演示如何使用 Express middleware 实现身份验证:

const express = require('express');
const app = express();

// 定义一个中间件函数,用于验证用户身份
function authenticate(req, res, next) {
  const token = req.headers.authorization;
  if (token === 'secret-token') {
    // 如果令牌有效,则将控制权传递给下一个中间件函数
    next();
  } else {
    // 否则,返回 401 错误
    res.status(401).send('Unauthorized');
  }
}

// 注册中间件函数,用于验证用户身份
app.use(authenticate);

// 定义一个路由处理函数,用于返回受保护的资源
app.get('/protected', (req, res) => {
  res.send('Protected resource');
});

// 启动应用程序
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

在上面的示例中,我们定义了一个名为 authenticate 的中间件函数,它用于验证用户的身份。在这个函数中,我们检查请求头中是否包含有效的身份验证令牌。如果令牌有效,则将控制权传递给下一个中间件函数或路由处理函数。否则,返回 401 错误。

然后,我们通过调用 app.use() 方法来注册这个中间件函数,以便在每个请求中都进行身份验证。最后,我们定义一个名为 /protected 的路由处理函数,它表示受保护的资源。只有在身份验证通过后,才能访问这个路由处理函数。

通过这个简单的示例,我们可以看到如何使用 Express middleware 实现基本的身份验证功能。中间件函数充当责任链中的一个环节,通过对请求进行处理和过滤,为应用程序提供更好的安全性和用户体验。

104.说一说 cookie sessionStorage localStorage 区别?【JavaScript】

cookie、sessionStorage和localStorage都是存储在浏览器端的客户端存储方式,用于存储一些客户端数据。

它们之间的区别如下:

  1. 生命周期

cookie的生命周期由Expires和Max-Age两个属性控制。当设置了Expires属性时,cookie的生命周期为设置的过期时间;当设置了Max-Age属性时,cookie的生命周期为设置的秒数。cookie在浏览器关闭时也会过期。而sessionStorage和localStorage的生命周期则与浏览器窗口相关,当窗口被关闭时,sessionStorage数据也会被清空,而localStorage数据则会一直存在,直到用户手动删除。

  1. 存储容量

cookie的存储容量限制为4KB,而sessionStorage和localStorage的存储容量则较大,可以达到5MB或更高。

  1. 数据共享

cookie可以被所有同源窗口(指协议、域名、端口相同)访问,而sessionStorage和localStorage只能被创建它们的窗口访问。

  1. 传输方式

cookie会随着http请求发送到服务器,而sessionStorage和localStorage不会发送到服务器,只存在于浏览器端。

  1. 数据类型

cookie只能存储字符串类型的数据,而sessionStorage和localStorage可以存储除了对象以外的数据类型,如数字、布尔值、数组、甚至是其他复杂的数据结构。但是,它们都可以通过JSON.stringify和JSON.parse方法将数据转化为字符串进行存储和读取。

综上所述,这三种存储方式都有各自的优缺点和适用场景。在实际应用中,我们需要根据实际情况选择合适的存储方式。

105.promise.race、promise.all、promise.allSettled 有哪些区别?【JavaScript】

Promise.race()、Promise.all()、Promise.allSettled() 都是 JavaScript 中的 Promise 相关 API,它们的区别如下:

  1. Promise.race()

Promise.race() 接收一个包含多个 Promise 的数组作为参数,返回一个新的 Promise。该 Promise 将会在数组中的任意一个 Promise 状态变为 fulfilled 或 rejected 时被解决,且以第一个解决的 Promise 的结果作为其结果返回。

如果数组中所有 Promise 都被拒绝,则返回的 Promise 将会以最先被拒绝的 Promise 的原因作为其原因拒绝。

  1. Promise.all()

Promise.all() 接收一个包含多个 Promise 的数组作为参数,返回一个新的 Promise。该 Promise 将会在数组中所有 Promise 状态均为 fulfilled 时被解决,并且以数组形式返回所有 Promise 的结果。

如果数组中有任何一个 Promise 被拒绝,则返回的 Promise 将会以最先被拒绝的 Promise 的原因作为其原因拒绝。

  1. Promise.allSettled()

Promise.allSettled() 接收一个包含多个 Promise 的数组作为参数,返回一个新的 Promise。该 Promise 将会在数组中所有 Promise 状态都被解决时被解决,并且以数组形式返回所有 Promise 的结果。和 Promise.all() 不同,Promise.allSettled() 不会在有 Promise 被拒绝时拒绝该 Promise。

返回的 Promise 的数组中的每个元素都是一个对象,该对象表示原始 Promise 的结果。每个对象都有一个 status 属性,表示原始 Promise 的状态,其值为字符串 'fulfilled' 或 'rejected'。如果 Promise 被解决,对象还会包含一个 value 属性,表示 Promise 的解决值。如果 Promise 被拒绝,对象还会包含一个 reason 属性,表示 Promise 的拒绝原因。

综上所述,Promise.race()、Promise.all() 和 Promise.allSettled() 的主要区别在于它们对多个 Promise 的状态处理方式不同,以及返回的 Promise 所包含的数据类型和结构不同。

106.手写代码实现 promise.race【JavaScript】

下面是手写实现 Promise.race() 方法的代码:

Promise.race = function (promises) {
  return new Promise((resolve, reject) => {
    promises.forEach((promise) => {
      Promise.resolve(promise).then(resolve, reject);
    });
  });
};

实现原理:

Promise.race() 方法接收一个包含多个 Promise 的数组作为参数,并返回一个新的 Promise。该 Promise 将会在数组中的任意一个 Promise 状态变为 fulfilled 或 rejected 时被解决,且以第一个解决的 Promise 的结果作为其结果返回。

我们可以通过创建一个新的 Promise,然后遍历 Promise 数组并将每个 Promise 包装在一个 Promise.resolve() 中,然后使用 .then() 方法将它们的解决值和拒绝原因分别传递给新的 Promise 的 resolve() 和 reject() 方法。由于 Promise 的状态只能改变一次,所以一旦第一个 Promise 被解决,新的 Promise 的状态也将被解决,并且以第一个解决的 Promise 的结果作为其结果返回。

109.JavaScript 有几种方法判断变量的类型?【JavaScript】

JavaScript 中有以下几种方法可以判断变量的类型

  • typeof 运算符:可以用于判断基本数据类型(如字符串、数字、布尔值、Undefined 等)和函数类型,但对于对象类型(如数组、日期、正则表达式等)不能准确判断。
  • instanceof 运算符:可以用于判断一个对象是否为某个构造函数的实例,但不能判断基本数据类型。
  • Object.prototype.toString() 方法:可以返回一个对象的具体类型字符串,可以判断所有数据类型,但需要注意的是需要使用 call 或 apply 方法将要判断的对象传递给 toString 方法。
  • Array.isArray() 方法:可以判断一个对象是否为数组类型。
  • constructor 属性:可以返回一个对象的构造函数,但需要注意的是 constructor 属性可以被修改,因此不能保证准确性。

举例 Object.prototype.toString() 是如何判断js 类型的

Object.prototype.toString() 方法是用来返回当前对象的类型字符串,其实现方式是返回一个类似 "[object Type]" 的字符串,其中 Type 是当前对象的类型。

Object.prototype.toString.call("hello") // "[object String]"
Object.prototype.toString.call(123) // "[object Number]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"

var arr = [1, 2, 3];
Object.prototype.toString.call(arr) // "[object Array]"

var date = new Date();
Object.prototype.toString.call(date) // "[object Date]"

var reg = /abc/;
Object.prototype.toString.call(reg) // "[object RegExp]"

var func = function() {};
Object.prototype.toString.call(func) // "[object Function]"

通过这种方式,可以精确判断变量的类型,包括基本数据类型和对象类型。

110.样式优先级的规则是什么?【CSS】

在 CSS 中,当多个选择器应用于同一个元素并设置了相同的属性时,就会出现样式冲突的问题。此时,CSS 会根据一定的规则来决定哪个样式具有更高的优先级,从而确定最终的样式效果。CSS 样式优先级的规则如下:

  1. !important:具有最高优先级,用于强制覆盖其它样式。
  2. 内联样式:直接在 HTML 元素的 style 属性中定义的样式,其优先级高于后面提到的其它选择器。
  3. ID 选择器:通过 #id 定义的样式,其优先级高于后面提到的 class 选择器和标签选择器。
  4. 类选择器、属性选择器、伪类选择器:通过 .class、[attribute. 或 :pseudo 定义的样式,其优先级高于后面提到的标签选择器。
  5. 标签选择器、伪元素选择器:通过 tagname 或 ::pseudo 定义的样式,优先级最低。

需要注意的是,当出现多个选择器具有相同的优先级时,CSS 会按照样式表中出现的顺序来决定样式的优先级,越后出现的样式会覆盖前面出现的样式。此外,继承自父元素的样式的优先级比上述任何选择器都低。

115.Proxy 和 Object.defineProperty 的区别是啥?【JavaScript】

Proxy 和 Object.defineProperty 是 JavaScript 中两个不同的特性,它们的作用也不完全相同。

Object.defineProperty 允许你在一个对象上定义一个新属性或者修改一个已有属性。通过这个方法你可以精确地定义属性的特征,比如它是否可写、可枚举、可配置等。该方法的使用场景通常是需要在一个对象上创建一个属性,然后控制这个属性的行为。

Proxy 也可以用来代理一个对象,但是相比于 Object.defineProperty,它提供了更加强大的功能。使用 Proxy 可以截获并重定义对象的基本操作,比如访问属性、赋值、函数调用等等。在这些操作被执行之前,可以通过拦截器函数对这些操作进行拦截和修改。因此,通过 Proxy,你可以完全重写一个对象的默认行为。该方法的使用场景通常是需要对一个对象的行为进行定制化,或者需要在对象上添加额外的功能。

总结来说,Object.defineProperty 是用来定义对象的属性,而 Proxy 则是用来代理对象并对其进行操作拦截和修改。两者的应用场景不同,但都可以用来对对象的行为进行定制化。

以下是 Proxy 和 Object.defineProperty 的一些具体应用场景的不同:

  1. Object.defineProperty 适用于需要精确地控制对象属性行为的场景,比如控制属性是否可写、可枚举、可配置等。它的应用场景包括但不限于:
  • 对象属性访问权限控制;
  • 对象属性计算;
  • 对象属性懒加载。
  1. Proxy 适用于需要代理对象并对其进行操作拦截和修改的场景。它的应用场景包括但不限于:
  • 对象属性访问控制;
  • 对象属性修改控制;
  • 对象属性缓存;
  • 对象属性计算;
  • 对象属性监听;
  • 对象属性校验;
  • 对象属性劫持等。

总的来说,Object.defineProperty 主要用于单个对象属性的控制和修改,而 Proxy 则适用于对整个对象或对象的多个属性进行控制和修改。由于 Proxy 的功能更加强大,它在一些高级应用场景中比 Object.defineProperty 更加适用。但是在一些简单场景下,使用 Object.defineProperty 可能更加方便和实用。

117.css 中 三栏布局的实现方案 的实现方案有哪些?【CSS】

三栏布局指的是在一个页面中,将内容分为左、中、右三栏,并使它们具有相应的宽度和高度。实现三栏布局有多种方案,以下是其中的几种:

  1. 使用浮动
cssCopy code.left {
  float: left;
  width: 200px;
  height: 100%;
  background-color: #ccc;
}

.right {
  float: right;
  width: 200px;
  height: 100%;
  background-color: #ccc;
}

.middle {
  margin: 0 200px;
  height: 100%;
  background-color: #eee;
}

在这个方案中,左栏和右栏使用浮动来实现,中间栏使用 margin 来占据剩余的宽度。

  1. 使用绝对定位
cssCopy code.container {
  position: relative;
  height: 100%;
}

.left {
  position: absolute;
  left: 0;
  width: 200px;
  height: 100%;
  background-color: #ccc;
}

.right {
  position: absolute;
  right: 0;
  width: 200px;
  height: 100%;
  background-color: #ccc;
}

.middle {
  position: absolute;
  left: 200px;
  right: 200px;
  height: 100%;
  background-color: #eee;
}

在这个方案中,左栏和右栏使用绝对定位来实现,中间栏使用左右 padding 来占据剩余的宽度。

  1. 使用Flexbox布局
cssCopy code.container {
  display: flex;
  height: 100%;
}

.left {
  width: 200px;
  height: 100%;
  background-color: #ccc;
}

.right {
  width: 200px;
  height: 100%;
  background-color: #ccc;
}

.middle {
  flex: 1;
  height: 100%;
  background-color: #eee;
}

在这个方案中,父容器使用Flexbox布局,左、中、右三栏都是Flex项,中间栏使用 flex: 1 来占据剩余的宽度。

这些方案都可以实现三栏布局,但每种方案都有自己的优缺点。在选择方案时,需要考虑浏览器兼容性、性能、可维护性和可扩展性等因素。

119.vue 的 keep-alive 的原理是啥?【web框架】

是 Vue.js 提供的一个抽象组件,它可以使被包含的组件保留在内存中,而不是每次重新渲染的时候销毁并重建,从而提高了应用的性能。

具体来说, 的实现原理如下:

  1. 当一个组件被包裹在 组件内时,它会被缓存起来,而不是被销毁。
  2. 如果这个组件被包裹的父组件从它的视图中移除,那么这个组件不会被销毁,而是被缓存起来。
  3. 如果这个组件再次被包裹的父组件添加回视图中,那么它会被重新激活,而不是重新创建。

组件通过一个内部的缓存对象来缓存组件实例,这个缓存对象会在组件被包裹在 组件中时创建。当一个被缓存的组件需要被激活时, 组件会从缓存中取出该组件的实例并将其挂载到视图上,从而实现了组件的复用。

需要注意的是,被缓存的组件并不是一直存在于内存中,它们会在一定条件下被销毁,比如缓存的组件数量超过了一定的阈值,或者系统内存占用过高等。

125.当使用 new 关键字创建对象时, 会经历哪些步骤?【JavaScript】

在 JavaScript 中,new 关键字用于创建一个对象实例。当使用 new 关键字创建对象时,会发生以下几个步骤:

  1. 创建一个空的对象。
  2. 将这个空对象的 [[Prototype]] 属性设置为构造函数的 prototype 属性。
  3. 将这个空对象赋值给构造函数内部的 this 关键字,用于初始化属性和方法。
  4. 如果构造函数返回一个对象,那么返回这个对象;否则,返回第一步创建的对象实例。

以下是一个示例,演示如何使用 new 关键字创建一个对象实例:

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

const person = new Person("John", 30);
console.log(person.name); // "John"
console.log(person.age); // 30

在上面的示例中,new Person("John", 30) 会创建一个新的对象实例。在构造函数 Person 中,this.name 和 this.age 会被赋值为 "John" 和 30。最终,new 关键字会返回这个新的对象实例。

需要注意的是,在 JavaScript 中,函数也是对象。因此,我们可以向对象一样定义属性和方法。当我们使用 new 关键字调用一个函数时,这个函数会被视为构造函数,从而创建一个新的对象实例。

126.es5 和 es6 使用 new 关键字实例化对象的流程是一样的吗?【JavaScript】

ES5 和 ES6 使用 new 关键字实例化对象的流程基本上是一样的,只是在细节上存在一些差异。

在 ES5 中,当使用 new 关键字调用一个函数时,会创建一个新的对象,并将这个新对象的 [[Prototype]] 属性指向构造函数的 prototype 属性。此外,new 关键字还会将构造函数内部的 this 关键字绑定到新创建的对象上,从而允许我们在构造函数内部添加属性和方法。

在 ES6 中,这些基本的流程也是相同的。但是,ES6 引入了类(class)的概念,从而为面向对象编程提供了更加便利的语法。使用类定义一个对象时,需要使用 constructor 方法作为构造函数,而不是普通的函数。类定义的语法糖实际上是对函数的封装,使用 new 关键字创建类的实例时,实际上也是在调用类的 constructor 方法。

在 ES6 中,可以使用类的继承来创建更复杂的对象。当使用 new 关键字创建一个继承自另一个类的类的实例时,会先调用父类的 constructor 方法,再调用子类的 constructor 方法,从而完成对象实例的创建过程。

需要注意的是,虽然 ES6 的类看起来像是其他面向对象语言中的类,但在 JavaScript 中,类仍然是基于原型继承的。在创建一个类的实例时,实际上是在创建一个新对象,并将这个新对象的原型指向类的原型。因此,实例化对象的流程与使用普通函数或类定义的对象的流程基本上是相同的。

127.如何实现可过期的 localstorage 数据?【JavaScript】

要实现可过期的 localStorage 数据,可以结合使用 localStorage 和 Date 对象。

首先,在存储数据时,需要将数据和过期时间一起存储在 localStorage 中。可以使用 JSON 格式来将数据和过期时间打包存储。代码示例如下:

function setWithExpiry(key, value, ttl) {
  const now = new Date()
  const item = {
    value: value,
    expiry: now.getTime() + ttl
  }
  localStorage.setItem(key, JSON.stringify(item))
}

在上面的代码中,setWithExpiry 函数接受三个参数:key 表示存储数据的键名,value 表示要存储的数据,ttl 表示数据的过期时间(单位为毫秒)。在函数内部,我们首先获取当前时间 now,然后将过期时间 expiry 计算为当前时间加上过期时间。最后,我们将数据和过期时间打包成一个对象,并使用 JSON.stringify 方法将其转换为字符串,存储在 localStorage 中。

接下来,我们可以在获取数据时检查数据是否已过期。如果数据已过期,则需要将其从 localStorage 中删除并返回 null。代码示例如下:

function getWithExpiry(key) {
  const item = localStorage.getItem(key)
  if (!item) return null
  const parsedItem = JSON.parse(item)
  const now = new Date()
  if (now.getTime() > parsedItem.expiry) {
    localStorage.removeItem(key)
    return null
  }
  return parsedItem.value
}

在上面的代码中,getWithExpiry 函数接受一个参数 key,表示要获取的数据的键名。在函数内部,我们首先使用 localStorage.getItem 方法获取存储在 localStorage 中的数据,如果不存在则返回 null。然后,我们将存储的字符串数据解析为对象,并获取过期时间。如果当前时间已经超过了过期时间,则需要将数据从 localStorage 中删除并返回 null。否则,我们返回存储的数据。

通过结合使用 localStorage 和 Date 对象,我们可以实现可过期的 localStorage 数据。这种方法简单易用,适用于需要在客户端存储数据,并且需要在一定时间后将其自动删除的场景。

132.React setState 是同步还是异步的?【web框架】

在 React 中,setState 方法有时是异步的,有时是同步的,具体取决于使用方式和环境。

当我们在 React 中调用 setState 方法时,React 会将新状态合并到组件的状态队列中,并在未来的某个时间更新组件的状态。这个更新过程是异步的,即不能保证在 setState 调用后立即更新状态,而是会将状态更新推迟到下一个渲染周期中。这种情况下,setState 方法会返回一个 Promise 对象,但该对象不包含任何有用的信息。

然而,当 setState 方法被直接调用,而不是在事件处理函数、异步代码或生命周期函数中被调用时,更新过程是同步的。在这种情况下,React 会立即更新组件的状态,并在更新后立即执行一次重新渲染。这种情况下,setState 方法不会返回任何信息。

需要注意的是,在 React 中异步更新状态的机制可以提高性能和优化页面渲染速度,因为它可以让 React 在适当的时候批量更新组件状态,从而避免过多的渲染和浏览器的性能问题。如果我们需要在更新状态后立即执行某些操作,可以使用 setState 方法的回调函数或生命周期函数 componentDidUpdate 来实现。例如:

jsCopy codeclass MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  handleClick() {
    this.setState({ count: this.state.count + 1 }, () => {
      console.log('New count:', this.state.count);
    });
  }

  render() {
    return (
      

Count: {this.state.count}

); } }

这里的 setState 方法接受一个回调函数作为第二个参数,在状态更新完成后调用该函数并传递更新后的状态作为参数。在这个回调函数中可以执行任何需要在状态更新后立即执行的操作,例如输出调试信息、发送网络请求等。

133.react 18 版本中 setState 是同步还是异步的?【web框架】

在 React 18 中,setState 的行为仍然是异步的。React 团队并没有改变 setState 的默认行为,因为这会带来一些潜在的性能问题和不确定性。异步更新状态可以让 React 在适当的时候批量更新组件状态,从而避免过多的渲染和浏览器的性能问题。

但是,React 18 引入了一个新的特性:批量更新(Batching)。当我们在事件处理函数、异步代码或生命周期函数中调用 setState 时,React 会将多个状态更新合并到同一个批次中,从而减少渲染的次数,提高应用的性能。这意味着,即使我们在多个地方调用 setState 方法,React 也会将这些调用合并到同一个更新队列中,并在下一个渲染周期中一次性更新所有状态。

在 React 18 中,我们可以使用新的 startTransition API 来告诉 React,我们正在进行一次可中断的异步操作,并且希望在操作完成后批量更新组件状态。这个 API 的用法如下:

jsCopy codeimport { startTransition } from 'react';

function handleClick() {
  startTransition(() => {
    setState({ count: count + 1 });
    // 执行其他异步操作
  });
}

在这个例子中,我们通过 startTransition API 包装 setState 和其他异步操作,告诉 React 我们正在进行一次可中断的异步操作,并且希望在操作完成后批量更新组件状态。这样做可以让我们的应用更加流畅和响应,并且可以提高用户体验。

需要注意的是,startTransition API 并不是必须的,如果我们不使用这个 API,React 仍然会在适当的时候批量更新组件状态。这个 API 只是为了让我们更加精确地控制更新的时机,并在必要时进行优化。

134.【React】合成事件了解多少【web框架】

在 React 中,合成事件是一种封装了浏览器原生事件对象的高级事件机制。它是由 React 提供的一种用于处理事件的抽象层,可以让开发者更方便地处理和管理事件。

React 的合成事件机制提供了一些优秀的特性:

  1. 跨浏览器兼容性:React 的合成事件可以屏蔽浏览器的差异,保证在各种浏览器上运行一致。
  2. 性能优化:React 的合成事件可以对事件进行池化处理,重用事件对象,避免创建大量的事件对象,从而提高性能。
  3. 事件委托:React 的合成事件可以实现事件委托机制,将事件处理程序绑定在组件树的根节点上,统一管理和处理组件内部和外部的事件,从而避免多次绑定事件处理程序的问题。
  4. 支持自定义事件:React 的合成事件可以支持自定义事件,开发者可以自定义组件事件,提供更多的自定义能力。

React 的合成事件机制通过事件冒泡和事件委托来实现。当在组件中触发事件时,React 会将该事件包装成一个合成事件对象,并在组件树中冒泡传递,直到根节点处。在组件树中,React 使用事件委托机制将事件处理程序绑定到根节点上,统一处理所有组件的事件。

在处理合成事件时,React 提供了一些常用的事件处理函数,例如 onClick、onMouseOver、onSubmit 等,可以在组件中直接使用。此外,开发者还可以自定义事件处理函数,通过 on 前缀加上事件名称的方式来绑定自定义事件。例如,我们可以定义一个 onCustomEvent 方法来处理自定义事件:

jsxCopy codeclass MyComponent extends React.Component {
  handleCustomEvent() {
    // 处理自定义事件
  }

  render() {
    return (
      
); } }

在这个例子中,我们定义了一个名为 handleCustomEvent 的方法来处理自定义事件,然后在组件中通过 onClick 属性来绑定该方法。当用户点击按钮时,React 会将该事件包装成一个合成事件对象,并调用 handleCustomEvent 方法来处理事件。

135.【React】绑定事件的原理是什么?【web框架】

在 React 中,绑定事件的原理是基于合成事件(SyntheticEvent)的机制。合成事件是一种由 React 自己实现的事件系统,它是对原生 DOM 事件的封装和优化,提供了一种统一的事件处理机制,可以跨浏览器保持一致的行为。

当我们在 React 组件中使用 onClick 等事件处理函数时,实际上是在使用合成事件。React 使用一种称为“事件委托”的技术,在组件的最外层容器上注册事件监听器,然后根据事件的目标元素和事件类型来触发合适的事件处理函数。这种机制可以大大减少事件监听器的数量,提高事件处理的性能和效率。

在使用合成事件时,React 会将事件处理函数包装成一个合成事件对象(SyntheticEvent),并将其传递给事件处理函数。合成事件对象包含了与原生 DOM 事件相同的属性和方法,例如 target、currentTarget、preventDefault() 等,但是它是由 React 实现的,并不是原生的 DOM 事件对象。因此,我们不能在合成事件对象上调用 stopPropagation() 或 stopImmediatePropagation() 等方法,而应该使用 nativeEvent 属性来访问原生 DOM 事件对象。

绑定事件的实现原理也涉及到 React 的更新机制。当组件的状态或属性发生变化时,React 会对组件进行重新渲染,同时重新注册事件监听器。为了避免不必要的事件处理函数的创建和注册,React 会对事件处理函数进行缓存和复用,只有在事件处理函数发生变化时才会重新创建和注册新的事件处理函数。这种机制可以大大提高组件的性能和效率,尤其是在处理大量事件和频繁更新状态的情况下。

139.pnpm 和 npm 的区别?【工程化】

pnpm 和 npm 是两个不同的 JavaScript 包管理工具,它们有以下区别:

  1. 包的存储方式:npm 将每个包都下载到项目的 node_modules 目录中,而 pnpm 会在全局安装一个存储库,并在项目中创建一个符号链接到该存储库中的每个包。
  2. 空间占用: 由于 pnpm 使用符号链接,它的空间占用通常比 npm 小,因为它避免了在多个项目中重复存储相同的依赖项。
  3. 安装速度: 由于 pnpm 在全局安装中共享依赖项,因此安装速度通常比 npm 更快。
  4. 命令行接口:pnpm 的命令行接口与 npm 不同,但它们都提供了一组相似的命令来管理包。
  5. 兼容性: 由于 pnpm 的存储方式不同于 npm,因此某些与 npm 兼容的工具可能无法与 pnpm 一起使用。

总的来说,pnpm 与 npm 相比具有更小的空间占用和更快的安装速度,但由于其不同的存储方式可能会导致与某些工具的不兼容。

142.事件循环原理?【JavaScript】

通过一道题进入浏览器事件循环原理:

console.log('script start')
setTimeout(function () {
  console.log('setTimeout')
}, 0);
Promise.resolve().then(function () {
  console.log('promise1')
}).then(function () {
  console.log('promise2')
})
console.log('script end')

可以先试一下,手写出执行结果,然后看完这篇文章以后,在运行一下这段代码,看结果和预期是否一样

单线程

定义

单线程意味着所有的任务需要排队,前一个任务结束,才能够执行后一个任务。如果前一个任务耗时很长,后面一个任务不得不一直等着。

原因

javascript的单线程,与它的用途有关。作为浏览器脚本语言,javascript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定javascript同时有两个线程,一个在添加DOM节点,另外一个是删除DOM节点,那浏览器应该应该以哪个为准,如果在增加一个线程进行管理多个线程,虽然解决了问题,但是增加了复杂度,为什么不使用单线程呢,执行有个先后顺序,某个时间只执行单个事件。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,运行javascript创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个标准并没有改变javascript单线程的本质

浏览器中的Event Loop

事件循环这个名字来源于它往往这么实现:

while(queue.waitForMessage()) {
    queue.processNextMessage();
}

这个模型的优势在于它必须处理完一个消息(run to completion),才会处理下一个消息,使程序可追溯性更强。不像C语言可能随时从一个线程切换到另一个线程。但是缺点也在于此,若同步代码阻塞则会影响用户交互

macroTask和microTask

宏队列,macroTask也叫tasks。包含同步任务,和一些异步任务的回调会依次进入macro task queue中,macroTask包含:

  • script代码块
  • setTimeout
  • requestAnimationFrame
  • I/O
  • UI rendering

微队列, microtask,也叫jobs。另外一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包含:

  • Promise.then
  • MutationObserver

下面是Event Loop的示意图

一段javascript执行的具体流程就是如下:

  1. 首先执行宏队列中取出第一个,一段script就是相当于一个macrotask,所以他先会执行同步代码,当遇到例如setTimeout的时候,就会把这个异步任务推送到宏队列队尾中。
  2. 当前macrotask执行完成以后,就会从微队列中取出位于头部的异步任务进行执行,然后微队列中任务的长度减一。
  3. 然后继续从微队列中取出任务,直到整个队列中没有任务。如果在执行微队列任务的过程中,又产生了microtask,那么会加入整个队列的队尾,也会在当前的周期中执行
  4. 当微队列的任务为空了,那么就需要执行下一个macrotask,执行完成以后再执行微队列,以此反复。
    总结下来就是不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask是否为空,不让过不为空就执行队列中的所有microtask。然后在取下一个task以此循环

调用栈和任务队列

调用栈是一个栈结构,函数调用会形成一个栈帧。栈帧:调用栈中每个实体被称为栈帧,帧中包含了当前执行函数的参数和局部变量等上下文信息,函数执行完成后,它的执行上下文会从栈中弹出。 下面是调用栈和任务队列的关系:

分析文章开头的题目,可以通过在题目前面添加debugger,结合chrome的call stack进行分析:

(这里不知道怎么画动图,在晚上找的一张图,小伙伴们有好的工具,求分享); 下面借助三个数组来分析一下这段代码的执行流程,call stack表示调用栈,macroTasks表示宏队列,microTasks表示微队列:

  1. 首先代码执行之前都是三个队列都是空的:
callStack: []
macroTasks: [main]
microTasks: []

在前面提到,整个代码块就相当于一个macroTask,所以首先向callStack中压入main(),main相当于整个代码块
2. 执行main,输出同步代码结果:

callStack: [main]
macroTasks: []
microTasks: []

在遇到setTimeout和promise的时候会向macroTasks与microTasks中分别推入
3. 此时的三个队列分别是:

callStack: [main]
macroTasks: [setTimeout]
microTasks: [promise]

当这段代码执行完成以后,会输出:

script start
script end
  1. 当main执行完成以后,会取microTasks中的任务,放入callStack中,此时的三个队列为:
callStack: [promise]
macroTasks: [setTimeout]
microTask: []

当这个promise执行完成后会输出

promise1

后面又有一个then,在前面提到如果还有microtask就在微队列队尾中加入这个任务,并且在当前tick执行。所以紧接着输出promise2
5. 当前的tick也就完成了,最后在从macroTasks取出task,此时三个队列的状态如下:

callStack: [setTimeout]
macroTasks: []
microTask: []

最后输出的结果就是setTimeout。
所谓的事件循环就是从两个队列中不断取出事件,然后执行,反复循环就是事件循环。经过上面的示例,理解起来是不是比较简单

143.[vue] 双向数据绑定原理?【web框架】

在目前的前端面试中,vue的双向数据绑定已经成为了一个非常容易考到的点,即使不能当场写出来,至少也要能说出原理。本篇文章中我将会仿照vue写一个双向数据绑定的实例,名字就叫myVue吧。结合注释,希望能让大家有所收获。

1、原理

Vue的双向数据绑定的原理相信大家也都十分了解了,主要是通过Object对象的defineProperty属性,重写data的set和get函数来实现的,这里对原理不做过多描述,主要还是来实现一个实例。为了使代码更加的清晰,这里只会实现最基本的内容,主要实现v-model,v-bind 和v-click三个命令,其他命令也可以自行补充。

添加网上的一张图

2、实现

页面结构很简单,如下

包含:

1. 一个input,使用v-model指令
2. 一个button,使用v-click指令
3. 一个h3,使用v-bind指令。

我们最后会通过类似于vue的方式来使用我们的双向数据绑定,结合我们的数据结构添加注释

var app = new myVue({
      el:'#app',
      data: {
        number: 0
      },
      methods: {
        increment: function() {
          this.number ++;
        },
      }
    })

首先我们需要定义一个myVue构造函数:

function myVue(options) {

}

为了初始化这个构造函数,给它添加一 个_init属性

function myVue(options) {
  this._init(options);
}
myVue.prototype._init = function (options) {
    this.$options = options;  // options 为上面使用时传入的结构体,包括el,data,methods
    this.$el = document.querySelector(options.el); // el是 #app, this.$el是id为app的Element元素
    this.$data = options.data; // this.$data = {number: 0}
    this.$methods = options.methods;  // this.$methods = {increment: function(){}}
  }

接下来实现_obverse函数,对data进行处理,重写data的set和get函数

并改造_init函数

 myVue.prototype._obverse = function (obj) { // obj = {number: 0}
    var value;
    for (key in obj) {  //遍历obj对象
      if (obj.hasOwnProperty(key)) {
        value = obj[key];
        if (typeof value === 'object') {  //如果值还是对象,则遍历处理
          this._obverse(value);
        }
        Object.defineProperty(this.$data, key, {  //关键
          enumerable: true,
          configurable: true,
          get: function () {
            console.log(`获取${value}`);
            return value;
          },
          set: function (newVal) {
            console.log(`更新${newVal}`);
            if (value !== newVal) {
              value = newVal;
            }
          }
        })
      }
    }
  }

 myVue.prototype._init = function (options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;

    this._obverse(this.$data);
  }

接下来我们写一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新

function Watcher(name, el, vm, exp, attr) {
    this.name = name;         //指令名称,例如文本节点,该值设为"text"
    this.el = el;             //指令对应的DOM元素
    this.vm = vm;             //指令所属myVue实例
    this.exp = exp;           //指令对应的值,本例如"number"
    this.attr = attr;         //绑定的属性值,本例为"innerHTML"

    this.update();
  }

  Watcher.prototype.update = function () {
    this.el[this.attr] = this.vm.$data[this.exp]; //比如 H3.innerHTML = this.data.number; 当number改变时,会触发这个update函数,保证对应的DOM内容进行了更新。
  }

更新_init函数以及_obverse函数

myVue.prototype._init = function (options) {
    //...
    this._binding = {};   //_binding保存着model与view的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,我们会触发其中的指令类更新,保证view也能实时更新
    //...
  }

  myVue.prototype._obverse = function (obj) {
    //...
      if (obj.hasOwnProperty(key)) {
        this._binding[key] = {    // 按照前面的数据,_binding = {number: _directives: []}                                                                                                                                                  
          _directives: []
        };
        //...
        var binding = this._binding[key];
        Object.defineProperty(this.$data, key, {
          //...
          set: function (newVal) {
            console.log(`更新${newVal}`);
            if (value !== newVal) {
              value = newVal;
              binding._directives.forEach(function (item) {  // 当number改变时,触发_binding[number]._directives 中的绑定的Watcher类的更新
                item.update();
              })
            }
          }
        })
      }
    }
  }

那么如何将view与model进行绑定呢?接下来我们定义一个_compile函数,用来解析我们的指令(v-bind,v-model,v-clickde)等,并在这个过程中对view与model进行绑定。

 myVue.prototype._init = function (options) {
   //...
    this._complie(this.$el);
  }

myVue.prototype._complie = function (root) { root 为 id为app的Element元素,也就是我们的根元素
    var _this = this;
    var nodes = root.children;
    for (var i = 0; i < nodes.length; i++) {
      var node = nodes[i];
      if (node.children.length) {  // 对所有元素进行遍历,并进行处理
        this._complie(node);
      }

      if (node.hasAttribute('v-click')) {  // 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++
        node.onclick = (function () {
          var attrVal = nodes[i].getAttribute('v-click');
          return _this.$methods[attrVal].bind(_this.$data);  //bind是使data的作用域与method函数的作用域保持一致
        })();
      }

      if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { // 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件
        node.addEventListener('input', (function(key) {
          var attrVal = node.getAttribute('v-model');
           //_this._binding['number']._directives = [一个Watcher实例]
           // 其中Watcher.prototype.update = function () {
           //	node['vaule'] = _this.$data['number'];  这就将node的值保持与number一致
           // }
          _this._binding[attrVal]._directives.push(new Watcher(
            'input',
            node,
            _this,
            attrVal,
            'value'
          ))

          return function() {
            _this.$data[attrVal] =  nodes[key].value; // 使number 的值与 node的value保持一致,已经实现了双向绑定
          }
        })(i));
      }

      if (node.hasAttribute('v-bind')) { // 如果有v-bind属性,我们只要使node的值及时更新为data中number的值即可
        var attrVal = node.getAttribute('v-bind');
        _this._binding[attrVal]._directives.push(new Watcher(
          'text',
          node,
          _this,
          attrVal,
          'innerHTML'
        ))
      }
    }
  }

至此,我们已经实现了一个简单vue的双向绑定功能,包括v-bind, v-model, v-click三个指令。效果如下图

附上全部代码,不到150行



  myVue



  

如果喜欢请关注我的Github,给个Star吧,我会定期分享一些JS中的知识,^_^

146.nodejs 进程间如何通信?【Nodejs】

在 Node.js 中,进程间通信(IPC)可以通过以下几种方式进行:

  1. 使用子进程模块:可以使用 Node.js 的子进程模块(child_process)来创建子进程,并使用进程间通信机制(如进程间管道)来实现通信。
  2. 使用共享内存:Node.js 中的共享内存模块(sharedArrayBuffer)可以在多个进程间共享内存,从而实现进程间通信。
  3. 使用进程间消息传递:Node.js 提供了一个内置的进程间通信机制,可以使用 process.send() 方法在不同的进程之间发送消息。
  4. 使用进程间的 TCP 通信:可以使用 Node.js 的 net 模块建立 TCP 服务器和客户端,从而在不同的进程之间进行通信。

需要注意的是,不同的进程之间通信可能会导致一些并发问题,例如竞态条件和死锁。因此,在设计进程间通信方案时,需要仔细考虑并发问题,并采取相应的措施来保证并发安全。

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

77.虚拟 dom 原理是啥,手写一个简单的虚拟 dom 实现?【JavaScript】

vdom 概念

用JS模拟DOM结构。
DOM变化的对比,放在JS层来做。
提升重绘性能。

比如有abc 三个dom, 如果我们要删除b dom, 以前浏览器的做法是 全部删除abc dom , 然后 在添加b dom 。这样做的成本会非常高。

用JS模拟 dom

例如下面的一个dom 结构:

  • item1
  • item2

这样的dom 结构,可以模拟为下面的JS :

let dom = {
    tag: 'ul',
    attrs: {
        id: 'list'
    },
    children: [
        {
            tag: 'li',
            attrs: {className: 'item'},
            children: ['item1']
        },
        {
            tag: 'li',
            attrs: {className: 'item'},
            children: ['item2']
        }
    ]
}

浏览器操作dom 是花销非常大的。执行JS花销要小非常多,所以这就是为什么虚拟dom 出现的一个根本原因。

jquery实现virtual-dom

一个需求场景

1、数据生成表格。 2、随便修改一个信息,表格也会跟着修改。



实际上上面的这段代码也是不符合预期的,因为每次使用render 方法,都会全部渲染整个table, 但是并未没有只渲染我们想要的第二行。

遇到的问题
DOM 操作是非常 "昂贵" 的, JS 运行效率高。虚拟dom 的核心就是diff算法,对比出不同的dom数据,定点渲染不同的数据。

107.手写代码实现 promise.all【JavaScript】

下面是手写实现 Promise.all() 方法的代码:

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    let results = [];
    let count = 0;
    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(
        (result) => {
          results[index] = result;
          count++;
          if (count === promises.length) {
            resolve(results);
          }
        },
        (reason) => {
          reject(reason);
        }
      );
    });
  });
};

实现原理:

Promise.all() 方法接收一个包含多个 Promise 的数组作为参数,并返回一个新的 Promise。该 Promise 将会在数组中所有 Promise 状态均为 fulfilled 时被解决,并且以数组形式返回所有 Promise 的结果。

我们可以通过创建一个新的 Promise,然后遍历 Promise 数组并将每个 Promise 包装在一个 Promise.resolve() 中,然后使用 .then() 方法将它们的解决值和拒绝原因分别传递给新的 Promise 的 resolve() 和 reject() 方法。我们还需要维护一个计数器和一个结果数组来跟踪所有 Promise 的状态。每当一个 Promise 被解决时,我们将其结果存储在结果数组中,然后将计数器增加 1。当计数器等于 Promise 数组的长度时,说明所有 Promise 均已被解决,此时我们可以使用 resolve() 方法并将结果数组作为参数传递给它。如果有任何一个 Promise 被拒绝,则使用 reject() 方法并将其拒绝原因作为参数传递给它。

需要注意的是,如果 Promise 数组为空,则 Promise.all() 将立即被解决,并返回一个空数组。

108.手写实现 Promise.allSettled【JavaScript】

Promise.allSettled 方法会接收一个 Promise 数组,并返回一个新的 Promise 对象。该新 Promise 对象会在所有输入的 Promise 都被 resolved 或 rejected 后变为 settled 状态,并且它的值是一个包含所有 Promise 状态的对象数组。

以下是手写实现 Promise.allSettled 方法的代码:

function allSettled(promises) {
  return new Promise((resolve) => {
    const results = [];
    let settledCount = 0;

    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(
        (value) => {
          results[index] = { status: 'fulfilled', value };
        },
        (reason) => {
          results[index] = { status: 'rejected', reason };
        }
      ).finally(() => {
        settledCount++;

        if (settledCount === promises.length) {
          resolve(results);
        }
      });
    });
  });
}

上述代码中,我们首先创建一个新的 Promise 对象,并在其中执行了一个异步操作。然后我们遍历了传入的 Promise 数组,并为每个 Promise 添加了一个 then 方法的回调函数,以便在 Promise 状态发生变化时收集 Promise 的结果。对于每个 Promise,我们都使用 Promise.resolve 方法将其转换为 Promise 对象,以确保我们处理的是 Promise 对象。我们使用一个 finally 方法来在 Promise settled 时更新 settledCount,以确保在所有 Promise settled 后我们只会执行一次 resolve 方法。

最终,我们将所有 Promise 的状态都收集到了 results 数组中,并将该数组作为 Promise 的值解析。这样,我们就实现了 Promise.allSettled 方法的功能。

112.CSS 尺寸单位有哪些?【CSS】

CSS尺寸设置的单位包括:

  1. 像素(Pixel,缩写为px):是最常用的单位,表示屏幕上的一个点,可以精确地指定元素的大小。
  2. 百分比(Percentage,缩写为%):相对于父元素的大小,可以根据父元素的大小来设置子元素的大小。
  3. em:相对于当前元素的字体大小,用于设置字体大小时很常用。
  4. rem:相对于根元素(即html元素)的字体大小。
  5. vh/vw:相对于视口(Viewport)的高度和宽度。
  6. cm、mm、in、pt、pc等长度单位:用于打印样式表,不建议在Web开发中使用。
  7. 自定义单位:可以通过CSS的calc()函数自定义单位,比如使用“1fr”作为网格布局中的单位。

需要注意的是,不同的浏览器可能会有不同的计算方式和默认值,因此在设置尺寸时需要进行充分的测试和兼容性处理。

113.React Router 中 HashRouter 和 BrowserRouter 的区别和原理?【web框架】

React Router 是一个流行的第三方库,它允许在 React 应用程序中实现路由功能。React Router 支持两种路由方式:HashRouter 和 BrowserRouter。

  1. HashRouter

HashRouter 使用 URL 中的 hash 部分(即 #)来实现路由。在 React 中,可以使用 组件来创建 HashRouter。例如:

jsxCopy codeimport { HashRouter, Route, Link } from 'react-router-dom';

function App() {
  return (
    
      
      
      
    
  );
}

在使用 HashRouter 时,URL 中的路径看起来像这样:http://example.com/#/about。HashRouter 不会向服务器发送请求,因为 # 符号后面的内容被浏览器认为是 URL 的一部分,而不是服务器请求的一部分。这意味着在使用 HashRouter 时,React 应用程序可以在客户端上运行,而无需服务器支持。

  1. BrowserRouter

BrowserRouter 使用 HTML5 的 history API 来实现路由。在 React 中,可以使用 组件来创建 BrowserRouter。例如:

jsxCopy codeimport { BrowserRouter, Route, Link } from 'react-router-dom';

function App() {
  return (
    
      
      
      
    
  );
}

在使用 BrowserRouter 时,URL 中的路径看起来像这样:http://example.com/about。BrowserRouter 通过 history API 在客户端和服务器之间发送请求,因此需要服务器支持。

  1. 区别

HashRouter 和 BrowserRouter 的主要区别在于它们如何处理 URL。HashRouter 使用 URL 中的 # 部分来实现路由,而 BrowserRouter 使用 HTML5 的 history API 来实现路由。HashRouter 不需要服务器支持,而 BrowserRouter 需要服务器支持。

  1. 原理

HashRouter 的原理是通过监听 window.location.hash 的变化来实现路由。当用户点击链接时,React Router 会根据链接的路径渲染相应的组件,并将路径添加到 URL 中的 # 部分。当用户点击浏览器的“后退”按钮时,React Router 会根据上一个 URL 中的 # 部分来渲染相应的组件。

BrowserRouter 的原理是通过 HTML5 的 history API 来实现路由。当用户点击链接时,React Router 会使用 history API 将路径添加到浏览器的历史记录中,并渲染相应的组件。当用户点击浏览器的“后退”

114.Vue3.0 实现数据双向绑定的方法是什么?【web框架】

Vue3.0 通过使用 Composition API 中的 reactive 和 ref 函数来实现数据双向绑定。

  1. reactive 函数

reactive 函数是 Vue3.0 中用来创建响应式对象的函数。将一个 JavaScript 对象传递给 reactive 函数,它会返回一个新的响应式对象。响应式对象是一个 Proxy 对象,可以在应用程序中使用它来自动追踪数据的变化。

例如,我们可以这样使用 reactive 函数来创建一个响应式对象:

import { reactive } from 'vue';

const state = reactive({
  message: 'Hello, world!'
});

在上面的示例中,我们使用 reactive 函数创建了一个包含一个 message 属性的响应式对象。

  1. ref 函数

ref 函数是 Vue3.0 中用来创建一个包含单个值的响应式对象的函数。将一个初始值传递给 ref 函数,它会返回一个新的响应式对象。响应式对象是一个普通对象,它有一个名为 value 的属性,该属性保存了当前值。当 value 属性的值发生改变时,Vue3.0 会自动更新应用程序的界面。

例如,我们可以这样使用 ref 函数来创建一个响应式对象:

import { ref } from 'vue';

const count = ref(0);

在上面的示例中,我们使用 ref 函数创建了一个包含初始值为 0 的响应式对象。

  1. 双向绑定的实现

Vue3.0 中的双向绑定可以通过在模板中使用 v-model 指令来实现。v-model 指令是 Vue3.0 中用来实现表单元素和组件的双向数据绑定的指令。例如,我们可以这样使用 v-model 指令来实现一个表单输入框的双向绑定:

htmlCopy code

在上面的示例中,我们在模板中使用 v-model 指令将输入框和 message 响应式对象进行双向绑定。当用户在输入框中输入文本时,message 响应式对象的值会自动更新,当 message 响应式对象的值发生改变时,界面上的文本也会自动更新。

总之,Vue3.0 使用 reactive 和 ref 函数来实现数据双向绑定。使用 reactive 函数可以创建包含多个属性的响应式对象,使用 ref 函数可以创建包含单个值的响应式对象。通过在模板中使用 `v-model

指令可以实现表单元素和组件的双向数据绑定,将表单元素的值绑定到响应式对象的属性上,当响应式对象的属性值变化时,自动更新绑定的表单元素的值。

除了使用 v-model 指令实现双向绑定,Vue3.0 也提供了 watch 函数和 watchEffect 函数来实现响应式数据的监听和副作用函数的执行。这些函数可以用来监听响应式数据的变化,从而执行特定的操作。下面是一个使用 watch 函数监听响应式数据变化的示例:

htmlCopy code

在上面的示例中,我们使用 watch 函数监听 count 响应式对象的变化,当 count 响应式对象的值发生变化时,会自动调用回调函数,打印出 count 变化前和变化后的值。

另外,Vue3.0 中还提供了 computed 函数用来计算一个响应式对象的值,toRefs 函数用来将一个响应式对象转换为普通的对象,并且在 TypeScript 中使用时可以使用 defineComponent 函数来定义组件的类型,从而提高代码的可读性和可维护性。

118.浏览器垃圾回收机制?【浏览器】

浏览器垃圾回收机制是指浏览器在运行时自动回收不再使用的内存空间的过程。以下是浏览器垃圾回收机制的几个方面:

  1. 标记清除:这是一种最常用的垃圾回收机制。它的工作原理是标记所有当前正在使用的对象,然后清除未标记的对象。这种方法的优点是效率高,缺点是可能会导致内存碎片。
  2. 引用计数:这种垃圾回收机制会跟踪每个对象被引用的次数,当引用计数为零时,就会回收该对象。这种方法的优点是可以立即回收不再使用的对象,缺点是无法处理循环引用。
  3. 分代回收:这是一种结合了标记清除和引用计数的垃圾回收机制。它将对象分为几代,然后在不同的代上使用不同的回收策略。新创建的对象会被分配到第一代,随着时间的推移,如果它们仍然存活,它们会被转移到下一代。这种方法的优点是可以更精细地控制回收策略。

浏览器垃圾回收机制可以帮助开发人员避免内存泄漏和减少程序崩溃的风险。不同的浏览器和不同的 JavaScript 引擎实现可能有不同的垃圾回收机制,但它们的基本原理是相似的。

120.常见的 web 前端网路攻击有哪些?【网络】

以下是一些常见的 web 前端网络攻击类型:

  1. 跨站脚本攻击(Cross-Site Scripting, XSS):XSS攻击利用了 Web 应用程序对用户输入的不当处理,以将恶意代码注入到 Web 页面中。当用户访问包含恶意代码的页面时,攻击者可以利用这些代码窃取用户的敏感信息、劫持用户会话等。
  2. 跨站请求伪造(Cross-Site Request Forgery, CSRF):CSRF攻击利用了用户已经登录了受信任网站的身份,通过在受害者的浏览器中执行恶意代码,将伪造的请求发送到受信任网站上,从而执行某些操作或者获取某些信息。
  3. 点击劫持(Clickjacking):点击劫持是一种利用透明 iframe 层来覆盖网页上的其他内容,欺骗用户点击不可见的按钮或链接,以执行攻击者所需的操作。
  4. HTML 注入攻击:HTML 注入攻击利用了 Web 应用程序对用户输入的不当处理,以将恶意的 HTML 代码插入到 Web 页面中。这种攻击通常被用来修改页面内容、欺骗用户或者实施其他恶意行为。
  5. 敏感数据泄露(Sensitive Data Leakage):敏感数据泄露可能会发生在 Web 应用程序中,其中攻击者可以通过暴力破解、SQL 注入等攻击方式,获取存储在数据库中的敏感数据(如用户名、密码、信用卡信息等)。
  6. 带宽滥用(Bandwidth Abuse):带宽滥用是指攻击者利用 Web 应用程序或服务器的漏洞来消耗服务器的资源和带宽,从而使服务器变得缓慢或无法正常工作。
  7. HTTP 请求欺骗(HTTP Request Spoofing):HTTP 请求欺骗是一种利用 Web 应用程序对输入的不当处理,以篡改 HTTP 请求的攻击方式。攻击者可以通过伪造 HTTP 请求头信息、修改 HTTP 请求方法等方式,欺骗 Web 应用程序执行攻击者所需的操作。

需要注意的是,这些攻击类型通常会结合使用,攻击者会利用多种攻击方式,以更好地实现攻击目标。

121.如何防止 跨站脚本攻击(Cross-Site Scripting, XSS)?【网络】

以下是一些防范跨站脚本攻击的常见方法:

  1. 输入过滤:对于所有输入的数据(如表单数据、URL 参数等),应该进行过滤和验证。特别是对于敏感数据(如密码、信用卡信息等),应该进行严格的验证,防止恶意的脚本注入。可以使用一些开源的输入验证工具,如OWASP ESAPI来过滤恶意输入。
  2. 对特殊字符进行转义:对于所有输出到页面上的数据,应该对特殊字符进行转义,比如将 < 转义为 <、将 > 转义为 > 等。这可以防止攻击者通过在页面上注入恶意的脚本。
  3. CSP(Content Security Policy):CSP是一种浏览器安全机制,可以限制 Web 页面可以加载哪些资源。通过设置合适的 CSP,可以防止恶意脚本的注入。
  4. HttpOnly Cookie:通过设置 HttpOnly 标志,可以防止脚本访问 Cookie。这可以防止攻击者窃取用户的身份验证信息。
  5. 随机化 Session ID:在用户登录后,应该为其分配一个随机化的 Session ID,防止攻击者通过猜测 Session ID 来劫持用户会话。
  6. 使用安全的编程语言和框架:使用安全的编程语言和框架可以降低跨站脚本攻击的风险。比如使用最新的版本的编程语言和框架,以获得更好的安全性。

需要注意的是,防范跨站脚本攻击需要综合多种方法,单一的措施并不能完全防止攻击。此外,开发人员应该始终关注最新的安全漏洞和攻击技术,及时采取相应的防范措施。

122.跨站请求伪造(Cross-Site Request Forgery, CSRF)具体实现步骤是啥, 如何防止?【网络】

跨站请求伪造(Cross-Site Request Forgery, CSRF)是一种常见的网络攻击方式,攻击者可以利用已登录的用户身份,通过伪造用户的请求,对服务器上的资源进行非法操作。下面是一种常见的 CSRF 攻击方式:

  1. 用户在浏览器中登录了某个网站,并获取了该网站的 Cookie。
  2. 攻击者诱导用户访问一个恶意网站,并在该网站上放置了一段恶意代码,用于发起 CSRF 攻击。
  3. 当用户在恶意网站上执行某个操作时,比如点击某个按钮或链接,恶意代码会自动向目标网站发送一个 HTTP 请求,请求中包含攻击者想要执行的操作和参数,同时也会携带用户的 Cookie。
  4. 目标网站接收到请求后,会认为这是一个合法的请求,因为它携带了用户的 Cookie。于是服务器会执行攻击者想要的操作,比如删除用户的数据、修改用户的密码等。

为了防止 CSRF 攻击,开发人员可以采取以下措施:

  1. 随机化 Token:为每个请求生成一个随机化的 Token,将 Token 放入表单中,并在服务器端进行验证。这可以防止攻击者伪造合法的请求。
  2. 使用 Referer 验证:在服务器端进行 Referer 验证,只允许来自合法来源的请求。这可以防止攻击者在自己的网站上放置恶意代码,进行 CSRF 攻击。
  3. 使用验证码:在某些敏感操作上,比如修改密码、删除数据等,可以要求用户输入验证码。这可以降低攻击者的成功率,因为攻击者很难获取验证码。

需要注意的是,以上措施并不能完全防止 CSRF 攻击,因为攻击者总是可以通过一些复杂的方法来规避这些防御措施。因此,开发人员需要综合考虑多种防范措施,以提高网站的安全性。

123.script 标签 defer 和 async 区别?【浏览器】

defer 和 async 是用于控制脚本加载和执行的 HTML