SPRINT-11

267 阅读11分钟

425、206状态码底层原理,分片上传原理

206状态码底层原理

HTTP状态码206表示Partial Content(部分内容),它是一种表示服务器成功处理了部分GET请求的状态码。当客户端发送一个包含Range头字段的GET请求时,服务器可以使用206状态码来返回部分请求的内容。

底层原理是这样的:客户端在请求头中通过Range字段指定需要获取的数据范围,例如"Range: bytes=0-999"表示获取文件的前1000个字节。服务器收到带有Range字段的请求后,会检查请求中指定的范围是否合法,如果合法则返回206状态码,同时在响应头中包含Content-Range字段和Content-Length字段,用于描述返回的部分内容的范围和长度。

例如,响应头可能是这样的:
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-999/5000
Content-Length: 1000

这样,客户端就知道服务器返回的是文件的一部分内容,并且知道该部分内容在整个文件中的范围和总长度。

206状态码常用于文件下载、断点续传等场景,在网络传输大文件时可以分多次请求获取文件的不同部分,从而提高下载效率和用户体验。

需要注意的是,服务器并不一定支持返回部分内容,如果服务器不支持或者请求的范围无效,服务器会返回其他适当的状态码,例如200(OK)表示返回整个文件内容。

分片上传原理

分片上传是一种将大文件切分成小块进行上传的技术,它可以提高上传效率并降低失败率。下面是分片上传的原理介绍:

  1. 文件切片:在客户端将要上传的文件被切分成多个固定大小的块,每个块通常是几百KB或几MB大小。切片的大小可以根据实际情况进行调整,不同的应用可能有不同的策略。

  2. 上传请求:客户端将切片上传到服务器时,会发送一个初始的上传请求。该请求包含文件的基本信息和切片的数量等参数。

  3. 切片上传:客户端按照事先确定的顺序逐个上传切片。每个切片都会带有一个唯一的标识符,用于在服务器端进行关联和组装。客户端可以使用HTTP协议的POST请求或其他上传接口来发送每个切片。

  4. 服务器接收:服务器接收到每个切片后,会将其暂存到临时存储区,比如内存或磁盘中,然后进行校验。校验可以是简单的校验和,也可以是更复杂的哈希算法,以确保切片完整无误。

  5. 切片组装:当所有切片上传完成后,服务器开始进行切片的组装。它通过标识符将所有切片按顺序组合成原始的文件。在组装过程中,服务器可能会进行一些额外的检查,如完整性验证和去重操作。

  6. 上传完成:一旦文件完整地被组装,服务器就会通知客户端上传成功,并返回相应的成功响应。客户端可以根据需要进行后续的处理,比如展示上传结果、保存文件信息等。

分片上传技术广泛应用于各种场景,如云存储服务、大型文件传输、视频直播等。它能够提高上传速度、减少传输失败的影响范围,并且允许断点续传,使得大文件的上传更加可靠和高效。

426、csp底层原理,具体怎么设置

CSP(Content Security Policy)是一种用于增强网站安全性的机制,它通过限制网页内容的来源和执行的方式来减少恶意攻击。下面是CSP的底层原理以及如何设置的简要介绍:

CSP的底层原理:

  1. 服务器设置策略:网站的服务器通过HTTP响应头中的Content-Security-Policy字段或者Content-Security-Policy-Report-Only字段来设置CSP策略。这些字段包含了一系列指令,用于定义允许加载的资源和执行的行为。

  2. 浏览器执行策略:当浏览器加载网页时,会解析服务器返回的CSP策略并执行。浏览器会根据策略限制网页中各种资源的加载和执行方式,并阻止不符合策略的操作。

  3. 检查资源来源:浏览器会检查网页中所有资源(如脚本、样式表、图片等)的来源是否符合CSP策略中定义的规则。如果资源的来源与策略不匹配,浏览器将阻止加载该资源。

  4. 限制脚本执行:CSP还可以限制网页中内联脚本(inline script)的执行,通过禁止或限制内联脚本的使用,可以减少XSS(跨站脚本攻击)的风险。

具体设置CSP策略的步骤如下:

  1. 了解网站的资源:首先,需要了解网站中所使用的各种资源(脚本、样式表、图片等),以及这些资源的来源。

  2. 制定策略规则:根据网站的需求和安全考虑,制定CSP策略规则。可以使用CSP指令来定义允许加载的资源来源,如default-src、script-src、style-src等。还可以使用其他指令限制特定操作,如禁止内联脚本使用的指令。

  3. 设置HTTP响应头:将制定好的CSP策略规则添加到服务器的HTTP响应头中的Content-Security-Policy字段或Content-Security-Policy-Report-Only字段中。可以通过服务器配置或编程方式实现。

  4. 测试和调试:在设置完CSP策略后,需要进行测试和调试,确保策略不会影响网站的正常功能。可以使用浏览器的开发者工具查看CSP报告和错误信息,以及逐步调整策略规则。

需要注意的是,CSP的设置需要根据具体的网站需求和安全风险来制定,过于严格的策略可能会影响网站的功能或用户体验,需要在安全和便利之间做出权衡。另外,CSP还支持上报功能,可以将违反策略的操作信息发送给服务器进行分析和记录,以进一步提升网站的安全性。

427、图片怎么转cancas,canvas转图片

要将图片转换为Canvas,可以使用HTML5中的<canvas>元素和JavaScript的drawImage()方法。以下是实现的步骤:

创建一个Canvas元素:在HTML中,使用<canvas>标签创建一个Canvas元素,并指定其宽度和高度。

<canvas id="myCanvas" width="500" height="300"></canvas>

获取Canvas上下文:在JavaScript中,使用getContext()方法获取Canvas的上下文对象。通过传入参数"2d",我们可以获取2D渲染上下文。

var canvas = document.getElementById("myCanvas");
var ctx = canvas.getContext("2d");

绘制图片到Canvas:使用drawImage()方法将图片绘制到Canvas上,该方法接受三个参数:要绘制的图像、起始点的x坐标和y坐标。

var img = new Image();
img.onload = function() {
  ctx.drawImage(img, 0, 0);
}
img.src = "image.jpg";

在上述代码中,我们首先创建了一个Image对象,并设置其加载完成后执行的回调函数。在回调函数中,我们使用drawImage()方法将图片绘制到Canvas上。

要将Canvas转换为图片,可以使用Canvas的toDataURL()方法,该方法将Canvas内容转换为一个Base64编码的数据URL。

Canvas转换为图片:调用toDataURL()方法将Canvas转换为图片的数据URL。可以将数据URL赋值给一个<img>标签的src属性,或者使用它进行其他操作,如下载图片。

var canvas = document.getElementById("myCanvas");
var dataURL = canvas.toDataURL("image/png");

通过上述步骤,你可以将图片转换为Canvas并将Canvas转换为图片。请注意,在转换为图片时,可能会遇到跨域问题,需要确保图片源和Canvas位于同一个域下或使用合适的跨域解决方法。

428、ssr优缺点,实现的具体过程

SSR(Server Side Rendering,服务器端渲染)是一种将页面的生成工作移至服务器端的技术,它可以提高页面的首屏加载速度、SEO效果和用户体验。下面是SSR的优缺点以及实现的具体过程:

优点:

  1. 首屏加载速度快:由于SSR在服务器端生成页面并输出HTML,所以可以在客户端接收到响应前就已经有一部分页面内容展示了出来,从而提高了首屏加载速度。

  2. SEO优化效果好:由于搜索引擎主要抓取HTML文本,因此使用SSR可以将完整的HTML文本返回给搜索引擎,提高网站的搜索排名和曝光率。

  3. 用户体验更好:由于首屏加载速度快,用户可以更快地看到页面内容,从而提高了用户的满意度和留存率。

缺点:

  1. 服务器压力大:由于SSR需要在服务器端进行页面的生成和输出,因此会增加服务器的负载压力,需要考虑服务器性能和扩展性。

  2. 开发成本高:SSR需要在服务器端进行页面模板的编写和数据的获取,需要更多的后端开发人力投入。

实现过程:

  1. 配置服务器环境:需要在服务器端搭建Node.js环境,并安装相关依赖模块。可以使用Express或Koa等Web框架来简化开发。

  2. 创建服务器入口:创建一个服务器入口文件,通过监听HTTP请求并返回相应的HTML文本来实现SSR。可以在入口文件中使用渲染引擎(如EJS、Pug等)来处理HTML模板,并将数据传递给模板进行渲染。

  3. 配置路由:在服务器端配置路由,根据不同的请求路径返回相应的HTML文本。可以通过路由参数、查询字符串或POST请求等方式传递数据给服务器端进行处理。

  4. 客户端脚本:在HTML文本中插入客户端脚本,用于处理后续的页面交互和异步请求。

  5. SEO优化:为了优化SEO效果,需要在HTML文本中添加关键字、描述和标题等元数据,并且确保页面内容能够正常被搜索引擎抓取和解析。

以上是SSR的基本实现过程,当然,具体实现还需要根据项目的需求和技术栈做出相应的调整和优化。

429、路由懒加载原理

路由懒加载是一种优化技术,可以在需要时才加载页面组件代码,从而提高应用的性能和体验。它的实现原理是利用了ES6的import()函数动态地加载模块。

对于import()函数,是不是所有import()都会分出一个文件,这个文件以什么形式存在,什么时间点下载什么时候解析,这取决于具体的打包工具和配置。一般来说,当我们在代码中使用import()函数时,打包工具(如Webpack)会将对应的模块代码单独分割出来,并生成一个独立的chunk(代码块)文件。这些chunk文件可以是JS、CSS或其他资源类型,根据具体的配置和需求进行优化。

这些chunk文件在何时下载和解析也取决于具体的配置和需求。一般来说,当页面加载时,只会加载当前页面所需的主chunk文件,而其他chunk文件则会在需要时再进行异步加载。当import()函数被调用时,相应的chunk文件会被下载并解析,从而完成加载模块的过程。

路由懒加载一般是在路由配置中使用import()函数来实现,当用户访问某个路由时,对应的组件代码才会被加载和解析。这样可以避免在初次加载应用时加载过多的组件代码,从而提高应用的性能和体验。

路由懒加载一般是由前端框架或其它库(如Vue、React等)来实现的,打包工具(如Webpack)则会根据配置来生成对应的chunk文件。因此,路由懒加载既涉及到前端框架或库的使用,也与打包工具的配置和优化密切相关。

430、富文本编辑器中如何实现上传图片

富文本编辑器中实现上传图片一般有两种方式:

通过表单提交上传图片

该方式的实现步骤如下:

  • 在富文本编辑器中插入一个图片上传按钮或者拖拽上传功能,让用户选择或者拖拽图片文件;
  • 将图片文件转换成二进制格式,并通过表单提交到后台服务器;
  • 服务器端接收到图片文件并处理,返回一个图片的URL地址;
  • 将这个URL地址插入到富文本编辑器中。

使用 AJAX 异步上传图片

该方式的实现步骤如下:

  • 在富文本编辑器中插入一个图片上传按钮或者拖拽上传功能,让用户选择或者拖拽图片文件;
  • 将图片文件转换成二进制格式,并通过AJAX异步请求上传到后台服务器;
  • 服务器端接收到图片文件并处理,返回一个图片的URL地址;
  • 将这个URL地址插入到富文本编辑器中。

不同的富文本编辑器实现方式可能有所不同,但是大致思路都是类似的。在实现上传功能时,需要注意一些问题,例如:

  1. 安全性问题:对上传文件进行有效的检查和过滤,防止上传恶意代码;

  2. 文件大小限制:设置上传文件大小上限,避免上传过大的文件导致服务器性能下降。

  3. 图片预览:上传前要对图片进行预览,确保上传的是正确的图片。

  4. 上传进度条:在上传过程中显示上传进度。

常见的富文本编辑器如ueditor、TinyMCE、Quill等都支持图片上传功能,可以根据具体的需求选择合适的富文本编辑器并结合上述步骤进行实现。

431、pnpm原理

pnpm 是一个 JavaScript 包管理工具,它的原理可以概括为以下几个方面:

  1. 硬链接(Hard Linking):pnpm 使用硬链接的方式来共享依赖包。当项目 A 和项目 B 都依赖同一个版本的包时,pnpm 会在硬盘上只存储一份该版本的包,并使用硬链接将其链接到两个项目中,以节省磁盘空间。

  2. 冗余移除(Pruning):pnpm 在安装依赖时,会移除不再需要的依赖包。当一个项目不再依赖某个包时,pnpm 会检测并将该包从硬盘上移除,以避免产生冗余。

  3. 压缩存储(Content Addressable Storage):pnpm 使用内容可寻址存储(Content Addressable Storage)的方式来存储已下载的依赖包。每个包都会被哈希,并用哈希值作为目录名,这样可以避免重复下载相同的包,同时也方便进行缓存和共享。

  4. 并行安装(Parallel Installation):pnpm 支持并行安装依赖包。它会同时下载和构建多个包,以加快安装速度,提高效率。

  5. 锁定文件(Lockfile):pnpm 使用 lockfile 来记录项目的依赖关系和版本信息。这个文件会被锁定,确保在后续安装或构建过程中使用相同的依赖版本。

综上所述,pnpm 通过硬链接、冗余移除、压缩存储、并行安装和锁定文件等机制,实现了高效、可共享的依赖管理方式,有效地减少了磁盘空间占用和重复下载的问题,并提高了安装速度。

432、权限模型是怎么设计的,前端如何限制,其他还有什么鉴权方式,cookie session

权限模型是一个应用程序中重要的安全设计,用于控制用户或系统角色对资源(如页面、API等)的访问权限。下面介绍一些常见的权限模型和前端的限制方式,以及其他一些鉴权方式:

  1. 基于角色的访问控制模型(RBAC):RBAC定义了一系列角色,并将访问权限分配给这些角色,而不是单个用户。当用户被分配到某个角色时,他们会继承其对应的权限。前端的限制方式可以是通过在UI界面上隐藏未授权的操作按钮或菜单项等来进行限制。

  2. 基于资源的访问控制模型(ABAC):ABAC根据资源本身的属性和上下文环境(如用户信息、设备信息等)来决定是否允许访问该资源。前端的限制方式可以是在请求数据前,检查用户的身份和其他相关信息,只返回其有权限操作的数据。

  3. 访问令牌模型(Access Token):在客户端向服务器请求受保护资源时,需要提供访问令牌以证明自己的身份。服务器会验证令牌的有效性并决定是否授权访问请求资源。前端限制方式可以是在请求资源时,必须携带有效的访问令牌。

  4. 基于声明的访问控制模型(Claims-Based Access Control):在此模型中,用户的权限信息被编码为一个或多个声明,这些声明包含有关用户的信息,如角色、组、名称等。前端的限制方式可以是捕获到声明后,在UI界面上显示其他功能和操作。

关于Cookie和Session,它们主要用于Web应用程序的用户身份验证和鉴权。Cookie是一种存储在用户计算机上的文本文件,它包含了一些用户特征和会话信息,应用程序可以根据这些信息来识别用户并进行相应的操作。Session指的是服务器内存中存储的数据块,包含了会话信息和其他相关信息。当用户通过Cookie认证后,应用程序会在服务器上创建一个新的Session,并将Session ID存储在Cookie中。

总的来说,权限模型的设计和前端的限制方式都需要根据具体应用场景来进行,以确保应用程序的安全性。同时,选择适当的鉴权方式,如令牌模型、声明模型等也非常重要,以确保应用程序的稳定性和可扩展性。

433、用过ts吗,讲一下泛型

泛型(Generics)是一种在 TypeScript 中用于增强代码复用性和类型安全性的特性。它允许我们在定义函数、类或接口时,使用参数化类型,这些参数化类型在使用时可以根据需要进行具体化。

使用泛型可以编写更通用、灵活的代码,使其能够适应多种类型的数据,而不仅限于特定的类型。通过在参数、返回值或变量声明中使用泛型类型参数,我们可以达到以下目标:

  1. 提高代码的重用性:泛型使得函数、类或接口可以适应多种类型的数据,从而减少了代码的重复编写。
  2. 增强类型安全性:通过在编译时对类型进行检查,避免在运行时发生类型错误。

下面是一些使用泛型的示例:

泛型函数

function identity<T>(arg: T): T {
  return arg;
}

// 使用泛型函数
let output = identity<string>('Hello World');
console.log(output); // 输出: Hello World

// 类型推断
let output2 = identity('Hello TypeScript');
console.log(output2); // 输出: Hello TypeScript

在上面的示例中,identity 是一个泛型函数,它接受一个类型参数 T,并将其应用于函数参数 arg 和返回值。通过传递具体的类型参数(如 string),我们可以指定函数参数和返回值的类型。同时,如果没有显式指定类型参数,TypeScript 会根据传入的参数自动推断出类型。

泛型类

class Box<T> {
  private value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }
}

// 创建一个泛型类的实例
let stringBox = new Box<string>('Hello');
console.log(stringBox.getValue()); // 输出: Hello

// 类型推断
let numberBox = new Box(42);
console.log(numberBox.getValue()); // 输出: 42

在上面的示例中,Box 是一个泛型类。它接受一个类型参数 T,并将其应用于类的成员变量和方法的返回值。通过创建泛型类的实例时指定类型参数,我们可以限制实例的属性和方法的类型。同时,如果没有显式指定类型参数,TypeScript 会根据赋值给构造函数的参数自动推断出类型。

泛型接口

interface Pair<T, U> {
  first: T;
  second: U;
}

// 使用泛型接口
let pair: Pair<number, string> = { first: 1, second: 'two' };
console.log(pair); // 输出: { first: 1, second: 'two' }

在上面的示例中,Pair 是一个泛型接口。它接受两个类型参数 TU,并定义了两个属性 firstsecond,对应不同的类型。通过声明变量时指定类型参数,我们可以使用泛型接口创建符合接口定义的对象。

通过泛型,我们可以编写更加灵活和可复用的代码,同时增强代码的类型安全性。在 TypeScript 中,泛型是非常有用的工具,可以以一种类型安全的方式处理不同类型的数据。

434、为什么用vue可以直接用this.xxx来赋值,而react不行

Vue 和 React 是两种不同的 JavaScript 框架,它们使用了不同的状态管理方式

在 Vue 中,组件内部的数据和方法都被封装在一个 Vue 实例中,通过在组件内部使用 this 关键字,可以直接访问和修改组件实例的数据和方法。Vue 提供了响应式系统,当组件实例的数据改变时,Vue 能够检测到这些变化并自动更新视图。因此,在 Vue 中,通过 this.xxx 直接给数据赋值可以很方便地更新视图。

而在 React 中,组件状态是通过 stateprops 来管理的。在 React 中,任何一个数据或方法的改变都需要通过调用 setState 方法来触发重新渲染视图。这是因为 React 并没有像 Vue 一样提供响应式系统,因此需要手动触发视图更新。

而且,在 React 中,为了保证性能,setState 方法一般是异步执行的。这意味着如果你立即访问 this.statethis.props,你可能无法得到最新的值。因此,你不能直接通过 this.xxx 给数据赋值。

总而言之,Vue 和 React 的状态管理方式不同,导致了在两者中操作数据的方式也不同。在 Vue 中,通过 this.xxx 直接给数据赋值可以很方便地更新视图,在 React 中则需要调用 setState 方法触发重新渲染视图。

435、为什么react要使用setState显式的设置值

React 使用 setState 方法来显式地设置组件的状态,这是为了保证可控性和性能。

  1. 可控性(Controlled):React 鼓励将组件的状态设置为不可变(Immutable),即不直接修改原始数据,而是通过创建新的副本来更新数据。通过使用 setState 方法,React 会在状态改变时进行合并和比较,确保只更新需要更新的部分,从而达到优化性能的目的。

  2. 性能优化:React 使用了称为 Virtual DOM 的机制来优化页面渲染性能。当状态发生改变时,React 会将新的状态与之前的状态进行比较,找出需要更新的部分,并仅更新这些部分。如果直接修改状态而不使用 setState 方法,React 将无法追踪到状态的变化,从而无法准确地知道哪些部分需要重新渲染,导致性能下降。

此外,使用 setState 方法可以充分利用 React 的生命周期方法。在 setState 方法中,你可以传递一个更新函数来获取之前的状态并返回新的状态,这使得你可以在更新状态前进行一些额外的操作,如根据当前状态计算新的状态。

总之,通过显式地使用 setState 方法来设置组件状态,React 能够提供更好的性能和控制性。它能够保证只更新需要更新的部分,并充分利用 React 的生命周期方法进行状态的处理和计算,从而提升应用的性能和代码的可维护性。

436、hooks为什么不能在循环或条件语句中执行

Hooks 在循环、条件语句或嵌套函数中执行会导致 React 无法保证 Hooks 的执行顺序和稳定性,因此它们不应该出现在这些位置。

  1. Hooks 的执行顺序问题:React 依赖于 Hooks 的执行顺序来管理组件的状态。当组件渲染时,Hooks 需要按照顺序被调用,以确保每个 Hook 对应的状态都能正确地关联到组件。如果将 Hooks 放置在循环、条件语句或嵌套函数中,那么 Hooks 的执行顺序可能会发生变化,导致状态关联错误,从而引发错误或不一致的行为。

  2. Hooks 的稳定性问题:React 要求每次渲染时 Hooks 的数量和顺序必须保持稳定。如果在循环或条件语句中使用 Hooks,循环迭代或条件判断可能会改变 Hooks 的数量和顺序,从而违反了 React 的要求。这可能导致 React 无法正确地追踪和管理组件的状态,造成意外的结果。

为了解决这个问题,React 提供了一些规范来确保 Hooks 的正确使用。具体来说,Hooks 的调用必须满足以下两个条件:

  • Hooks 只能在函数组件的顶层中被调用,并且不能出现在条件语句、循环或嵌套函数中。
  • Hooks 的调用顺序必须保持稳定,不能有条件地执行或跳过某些 Hooks。

通过遵循这些规范,React 可以正确地追踪和管理组件的状态,并保证 Hooks 在每次渲染中按照相同的顺序被调用,从而保证组件的行为一致性和可预测性。

437、express和koa有什么区别,中间件执行方面

Express和Koa都是流行的Node.js的Web框架,它们在中间件执行方面有一些区别:

  1. 中间件执行机制

    • Express使用基于回调函数的中间件执行机制。每个中间件函数被依次调用,通过调用next()函数传递控制权到下一个中间件。中间件可以通过修改reqres对象来处理请求和响应。
    • Koa使用基于Promise的中间件执行机制。每个中间件函数返回一个Promise对象,通过await next()语句将控制权传递给下一个中间件。中间件可以通过修改ctx对象来处理请求和响应。
  2. 异步流程控制

    • Express:在Express中,异步流程控制需要使用回调函数、Promise、生成器等方式手动进行处理,使得代码可读性和维护性相对较低。
    • Koa:Koa内置了对异步流程控制的支持,通过使用await关键字可以优雅地处理异步操作,使得代码更简洁和易于理解。
  3. 错误处理

    • Express:在Express中,错误处理是通过中间件的错误处理函数(error handling middleware)来完成的。需要手动编写专门处理错误的中间件,并在需要时调用next(err)将错误传递给错误处理中间件。
    • Koa:Koa使用try...catch语句来捕获中间件中的错误,并将错误传递给全局的错误处理中间件(error middleware)。不需要手动调用next(err),错误处理更加简洁和直观。

总体而言,Express更早出现,它的中间件执行机制比较简单且广泛应用。而Koa基于ES6的新特性Promise和Generator,提供了更优雅的编程方式和更好的异步流程控制能力。选择使用Express还是Koa取决于项目需求、个人喜好和团队经验。

438、实现一个eventBus(发布订阅)

下面是一个简单的事件总线(EventBus)的实现,用于实现发布订阅模式:

class EventBus {
  constructor() {
    this.events = {};
  }

  // 订阅事件
  subscribe(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  // 发布事件
  publish(eventName, data) {
    const eventCallbacks = this.events[eventName];
    if (eventCallbacks) {
      eventCallbacks.forEach(callback => {
        callback(data);
      });
    }
  }

  // 取消订阅事件
  unsubscribe(eventName, callback) {
    const eventCallbacks = this.events[eventName];
    if (eventCallbacks) {
      this.events[eventName] = eventCallbacks.filter(cb => cb !== callback);
    }
  }
}

使用示例:

// 创建事件总线实例
const eventBus = new EventBus();

// 定义事件订阅回调函数
function callback1(data) {
  console.log("Callback 1:", data);
}

function callback2(data) {
  console.log("Callback 2:", data);
}

// 订阅事件
eventBus.subscribe("event1", callback1);
eventBus.subscribe("event2", callback2);

// 发布事件
eventBus.publish("event1", "Hello, Event 1!");
eventBus.publish("event2", "Hello, Event 2!");

// 取消订阅事件
eventBus.unsubscribe("event1", callback1);

// 再次发布事件
eventBus.publish("event1", "This callback should not be called");
eventBus.publish("event2", "Hello again, Event 2!");

在上述示例中,首先创建了一个事件总线实例eventBus,然后定义了两个回调函数callback1callback2用于订阅事件。接下来通过subscribe()方法将回调函数订阅到相应的事件上,然后使用publish()方法发布事件并传递相应的数据。最后,通过unsubscribe()方法可以取消订阅事件。

当发布事件时,所有订阅了该事件的回调函数都会被执行,并传递相应的数据。取消订阅后,对应的回调函数就不会再被执行。

439、封装hooks

完成以下封装hooks:useSearch

// https://xxx.com?status=1&keyword=test
// request.get(url, {})
function useSearch() {
    //your code
    return {
        data
    }
}

实现

import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';

function useSearch() {
  const location = useLocation();
  const [data, setData] = useState(null);

  useEffect(() => {
    // 解析 URL 参数
    const searchParams = new URLSearchParams(location.search);
    const status = searchParams.get('status');
    const keyword = searchParams.get('keyword');

    // 请求数据
    request.get(url, {})
      .then(response => {
        // 处理返回的数据
        setData(response.data);
      })
      .catch(error => {
        console.error('Error:', error);
      });
  }, [location]);

  return {
    data
  };
}

440、项目上用了redux,有对比过用或者不用redux的优缺点吗

Redux是一个状态管理库,它通过一个全局的state对象来存储应用程序的所有状态。这个state对象在组件之间共享,使得数据流动更加可控和可预测。相比不使用Redux,使用Redux有以下优缺点:

优点:

  1. 全局状态管理,便于数据流动:Redux可以存储整个应用的状态,而不是每个组件各自维护自己的状态,这样就可以很好地控制数据的流动。
  2. 便于调试:Redux一般与开发工具(如Redux DevTools)一起使用,帮助开发者更好地跟踪和调试应用程序。
  3. 方便的时间旅行功能:Redux提供了强大的时间旅行功能,可以在应用中的任何时刻还原状态,方便开发者测试和调试。

缺点:

  1. 学习成本高:Redux有一定的学习难度,需要理解Redux中的概念和原理。
  2. 代码冗长:使用Redux意味着会增加一些额外的代码,例如action creators和reducers等,可能会让代码变得冗长。
  3. 不适用于简单应用:对于简单应用(如小型CRUD应用),Redux可能会增加过多的复杂性,因此并不是必要的选择。

综上所述,Redux适用于需要管理全局状态且复杂度较高的应用程序,但对于简单应用来说,可能会增加额外的复杂性。

441、redux具体是怎么做到精准更新的

Redux通过将应用的状态存储在一个不可变的单一数据源中,使用纯函数的reducer来处理状态更改,以及通过派发action来触发状态更改,实现了状态的精准更新。

1.单一数据源:Redux应用的状态存储在一个称为"Store"的单一数据源中。这个Store包含了应用的整个状态树,这样整个应用的状态都被集中存储,使得状态更容易管理。
2.状态是只读的:Redux中的状态是不可变的。这意味着一旦状态被创建,它不能被直接修改。任何状态的更改都必须通过创建一个新的状态对象来实现,这确保了状态的不可变性。
3.纯函数的 reducer:Redux中的状态更改通过纯函数称为reducer来实现。Reducer接收当前状态和一个描述状态如何更改的操作(action),然后返回一个新的状态。由于reducer是纯函数,相同的输入将始终产生相同的输出,这确保了状态更改的可预测性。
4.Action:Redux应用中的状态更改由一个叫做"Action"的普通JavaScript对象来描述。Action对象包含一个类型字段,用于指示要执行的操作的类型,以及可选的负载数据,用于传递更改状态所需的信息。
5.派发操作:要更改Redux状态,需要使用store.dispatch(action)方法来派发一个action。Redux会将action传递给reducer,reducer将根据action的类型和负载数据来生成新的状态。
6.纯粹性:Redux强调了纯粹性的概念,这意味着相同的输入将始终产生相同的输出。这是因为reducer是纯函数,不会依赖于任何外部状态或副作用,从而确保了状态更改的精准性和可预测性。
7.时间旅行调试:Redux还提供了一个强大的调试工具,允许开发人员回溯到先前的状态,以便更容易调试和追踪状态更改。

442、有了解过其他设计模式吗

以下是一些常见的设计模式:

  1. 单例模式 (Singleton Pattern):确保一个类只有一个实例,并提供一个全局访问点以访问该实例。
  2. 工厂模式 (Factory Pattern):在不暴露对象创建逻辑的情况下创建对象,通过一个工厂方法来创建对象。
  3. 抽象工厂模式 (Abstract Factory Pattern):提供一个创建一组相关对象的接口,而无需指定其具体类。
  4. 建造者模式 (Builder Pattern):用于创建复杂对象,通过将其构建过程拆分成多个步骤,允许相同的构建过程创建不同的表示。
  5. 原型模式 (Prototype Pattern):通过复制现有对象来创建新对象,通常用于创建对象的成本较高时。
  6. 适配器模式 (Adapter Pattern):将一个接口转换为另一个接口,以便不兼容的接口可以一起工作。
  7. 装饰器模式 (Decorator Pattern):动态地为对象添加额外的功能,而无需改变其结构。
  8. 策略模式 (Strategy Pattern):定义一系列算法,将每个算法封装起来,并使它们可以互相替换。
  9. 观察者模式 (Observer Pattern):定义对象间的一对多依赖关系,使一个对象的状态变化会通知其所有依赖对象。
  10. 命令模式 (Command Pattern):将请求或操作封装成一个对象,允许将请求的参数化、队列化、记录、撤销和重做。
  11. 状态模式 (State Pattern):允许对象在其内部状态发生变化时改变其行为,使其看起来好像改变了其类。
  12. 访问者模式 (Visitor Pattern):允许在不改变对象结构的前提下定义作用于对象结构的新操作。
  13. 策略模式 (Strategy Pattern):定义一系列算法,将每个算法封装起来,并使它们可以互相替换。
  14. 模板方法模式 (Template Method Pattern):定义算法的骨架,允许子类实现算法的特定步骤。
  15. 桥接模式 (Bridge Pattern):将抽象部分与它的实现部分分离,从而使它们可以独立地变化。

443、观察者模式原理是怎么样的

观察者模式(Observer Pattern)是一种行为设计模式,它用于定义对象之间的一对多依赖关系,使得一个对象的状态变化会通知其所有依赖对象,并自动更新它们的状态。观察者模式的原理如下:

  1. 主题(Subject):主题是被观察的对象,它维护一组观察者对象,并提供方法来添加、删除和通知观察者。主题的状态发生变化时,它会通知所有注册的观察者。
  2. 观察者(Observer):观察者是依赖主题的对象,它定义一个更新接口,通常包括一个 update 方法。当主题的状态发生变化时,观察者的 update 方法会被调用,以便观察者可以根据主题的状态进行相应的更新。
  3. 注册和移除观察者:主题允许观察者注册(订阅)和移除(取消订阅)自身。这通常是通过 addObserver 和 removeObserver 等方法来实现的。
  4. 通知观察者:主题在其状态发生变化时,会遍历已注册的观察者列表,调用每个观察者的 update 方法,将状态信息传递给它们。

观察者模式的关键思想是实现了一种松散耦合的方式,主题和观察者相互独立,不需要彼此了解内部细节,但仍然可以有效地进行通信。这有助于增加代码的可维护性和可扩展性,因为您可以轻松地添加或移除观察者,而不会影响主题或其他观察者。
以下是一个简单的示例,演示了观察者模式的工作原理:

// 主题对象
class Subject {
  constructor() {
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    this.observers = this.observers.filter(obs =&gt; obs !== observer);
  }

  notifyObservers(data) {
    this.observers.forEach(observer => {
      observer.update(data);
    });
  }
}

// 观察者对象
class Observer {
  update(data) {
    console.log(`Received data: ${data}`);
  }
}

// 创建主题和观察者
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

// 注册观察者
subject.addObserver(observer1);
subject.addObserver(observer2);

// 主题状态发生变化,通知观察者
subject.notifyObservers('Hello, observers!');

在上面的示例中,Subject 是主题对象,Observer 是观察者对象。主题维护了一组观察者,并可以通知它们状态的变化。观察者被注册到主题上,当主题的状态发生变化时,观察者的 update 方法被调用,以接收状态信息。这样,主题和观察者之间实现了松散的耦合。

444、Js在观察数据和状态是否发生变化是怎么做的

在JavaScript中,可以使用不同的方法来观察数据和状态的变化。以下是一些常见的方法和技术:

手动轮询:您可以定期检查数据或状态的变化,然后采取相应的行动。这通常涉及设置定时器,以一定的时间间隔重复检查数据。这种方法适用于简单的情况,但会浪费资源,因为它需要不断轮询数据。
事件监听器:JavaScript允许您为特定事件(例如,DOM事件、自定义事件等)注册监听器。当事件发生时,相关的监听器函数将被调用。这可以用于检测状态变化。例如,在Web开发中,您可以使用DOM事件来检测用户交互和DOM元素的变化。
回调函数:您可以通过将回调函数传递给数据或状态的变化点来观察变化。当数据或状态发生变化时,相应的回调函数将被调用。这种方法常用于异步操作,例如Ajax请求的回调函数。ES6 Proxy:ES6引入了代理(Proxy)对象,它允许您拦截对象的操作,包括属性的读取、写入、删除等。通过创建一个代理对象,您可以监视和响应对象属性的变化。这是一种强大的机制,可以用于数据绑定和状态管理。

   const data = { value: 42 };
   const handler = {
     set(target, key, value) {
       console.log(`Setting ${key} to ${value}`);
       target[key] = value;
     }
   };
   const proxy = new Proxy(data, handler);
   proxy.value = 50; // 这会触发代理的set拦截器,并输出日志

观察者模式:如前面所提到的,您可以使用观察者模式来实现数据和状态的变化通知。主题对象负责维护观察者列表,并在数据或状态变化时通知观察者。

在现代的前端开发中,常用的状态管理库(如React的useState和useEffect、Vue.js的响应式数据、Redux等)已经内置了对状态变化的观察和更新机制,使得开发者可以更方便地处理数据和状态的变化。这些库通常结合了事件监听、回调函数和代理等技术,提供了强大的工具来处理数据和状态的变化。

445、PC端和移动端项目CSS的适配区别

PC端和移动端项目CSS的适配区别

  1. 像素单位的使用:在 PC 端开发中,通常使用像素(px)作为单位,因为 PC 设备屏幕分辨率相对固定,可以更精确地控制元素尺寸。而在移动端开发中,通常使用设备独立像素(dp/dip)或视口单位(vw/vh)等相对单位,因为移动设备屏幕尺寸多样,需要进行自适应布局,保证在不同设备上的显示效果一致。

  2. 视口设置: 在 PC 端开发中,页面通常以固定宽度展示,而在移动端开发中,需要设置视口的宽度等于设备宽度,以确保页面可以自适应不同尺寸的屏幕。可以使用以下代码设置视口:

    <meta name="viewport" content="width=device-width, initial-scale=1">
    
  3. Flexbox 布局:在移动端开发中,Flexbox 布局可以更方便地实现页面的自适应布局。通过设置 display: flex 和相关属性来控制子元素的排列,使其在不同设备上的显示效果一致。

  4. 字号和行高:在移动端开发中,通常需要将字号和行高设置为相对单位(如 em、rem),以便根据视口调整页面布局和字体大小。

446、屏幕大小变化的时候是如何适配

媒体查询(Media Queries)

流式布局(Fluid Layout):通过使用相对单位(例如百分比)来定义布局中的尺寸和位置,使页面元素能够相对于父元素或视口进行适应性调整。这样,在屏幕大小变化时,元素会随之伸缩以适应新的容器尺寸。

弹性盒模型(Flexbox):使用弹性盒模型可以轻松地实现灵活的布局和对齐方式。弹性容器(父元素)的子元素可以根据设置的弹性属性自动调整尺寸和位置,以适应不同屏幕大小。

网格布局(Grid Layout):使用网格布局可以将页面划分为行和列的网格,并在网格中放置元素。通过定义网格模板和设置自动放置或对齐方式,可以轻松地实现响应式的布局。

447、直接写rem计算屏幕的宽高是怎么获取

// 获取屏幕宽度
var screenWidth = window.innerWidth || document.documentElement.clientWidth;

// 设定基准值(例如,假设设计稿基准宽度为750px)
var baseWidth = 750; // 设计稿基准宽度

// 计算rem基准值
var baseFontSize = screenWidth / baseWidth * 100;

// 设置HTML根元素的字体大小
document.documentElement.style.fontSize = baseFontSize + 'px';

在以上示例中,首先获取了屏幕的宽度(screenWidth),然后通过除以设计稿的基准宽度(baseWidth)并乘以100,得到了rem基准值(baseFontSize)。最后,将计算得到的基准值设置给HTML根元素(document.documentElement)的fontSize属性,以实现rem单位的自适应布局。

在接下来的CSS中,可以使用rem单位进行布局和样式设置,它们会根据屏幕宽度的变化自动进行调整。

.container {
  width: 10rem;
  height: 5rem;
  font-size: 1.6rem;
}

448、逻辑像素和物理像素的区别

逻辑像素(logical pixel)和物理像素(physical pixel)是两个在移动设备屏幕上经常提到的概念,它们之间存在着一定的区别。

  • 逻辑像素:也称为设备无关像素(device-independent pixel),是一种相对单位,它是CSS布局和渲染的基础。在CSS中我们通常使用px作为逻辑像素的单位。逻辑像素是相对于设备视口的单位,它会随着缩放比例的改变而改变,使得页面可以在不同的屏幕分辨率下保持相似的布局。

  • 物理像素:也称为设备像素(device pixel),是显示设备的最小物理单位。物理像素是显示屏幕上实际存在的点,它们组成了屏幕上的图像。物理像素的数量决定了屏幕的分辨率。

在传统的PC屏幕上,逻辑像素与物理像素通常是一一对应的,即一个逻辑像素对应一个物理像素。但是在高分辨率的移动设备上,由于屏幕尺寸有限,需要在同样大小的屏幕上显示更多的内容,因此引入了逻辑像素。

当设备的像素密度(每英寸的物理像素数)提高时,一个逻辑像素可能对应多个物理像素。例如,苹果的Retina显示屏上,一个逻辑像素相当于2×2个物理像素。

为了在移动设备上进行适配,开发者通常使用逻辑像素来设置元素的尺寸和间距,使其在不同的屏幕密度下保持一致的外观。CSS中使用的px单位实际上是指逻辑像素。

总结来说,逻辑像素是CSS布局和渲染所使用的单位,它相对于设备视口,可以根据设备的缩放比例进行调整;而物理像素是实际存在于设备屏幕上的点,决定了屏幕的分辨率。

449、Css下载的过程会阻塞js的下载吗?为什么阻塞?

CSS 的下载过程会阻塞 JavaScript 的下载,这是因为在浏览器渲染页面时,DOM 和 CSSOM 是并行构建的,即在构建 DOM 树的同时也在构建 CSSOM 树。而 JavaScript 的执行是会阻塞 DOM 的构建的,因为 JavaScript 可能会修改 DOM 结构,如果 JavaScript 在 DOM 未构建完成前就开始执行,就有可能导致操作无效。

因此,在浏览器下载和解析 HTML 时,它遇到了一个外部的 CSS 文件,它必须先下载并解析该文件,生成 CSSOM 树,然后才能继续构建 DOM 树。如果该 CSS 文件很大或者服务器响应时间很长,那么浏览器就必须等待 CSS 文件下载完毕后才能继续进行下一步 DOM 树的构建,从而导致 JavaScript 也被阻塞。

为了避免这种情况,可以采取以下措施:

  • 将 CSS 文件放在 <head> 标签内,这样可以尽早地开始下载 CSS 文件;
  • 将 JavaScript 文件放在 <body> 标签的底部,这样可以确保 DOM 已经构建完成;
  • 使用异步加载(async)或延迟加载(defer)标记来加载 JavaScript 文件,这样可以使得 JavaScript 和 CSS 文件的下载和解析更加异步化,提高渲染性能。

450、项目性能优化的方式

js方面

  • 减少http请求 :节流、防抖、缓存(keep-alive);
  • 及时消除对象引用,清除定时器,清除事件监听器;
  • 使用常量,避免全局变量;
  • 减少dom 操作,
  • 删除冗余代码(没有使用到的代码)
  • 推迟js 加载:defer

css 方面

  • 使用<link>不使用@import
  • 减少重绘和回流,减少table 表格布局,html 层级嵌套不要太深;
  • 合理配置图片加载方式(图片压缩上传、iconfont、base64、file文件、cdn、预加载、懒加载)
  • 开启硬件加速(GPU加速)

工程化方面

  • webpack :打包压缩、Loader 、插件;
  • 合理利用浏览器缓存(首次缓存)
  • 开启gzip压缩(减少文件访问体积)
  • 使用ssr服务端渲染。
  • 路由、组件、长页面使用懒加载
  • 减少重定向请求

451、tree shaking的限制条件?比如模块化方式commonJS或ES6module的方法能实现吗?

Tree shaking 是一种用于在打包过程中去除未使用代码的优化技术。它的原理是通过静态分析代码来确定哪些代码被实际使用,然后将未使用的代码从最终的构建结果中剔除掉,减少包的大小。

tree shaking 的可行性和效果受到一些限制条件的约束

  1. 静态分析能力:Tree shaking 需要对代码进行静态分析,判断哪些代码是未使用的。因此,只有那些在编译时可以确定的导入(import)和导出(export)才能被正确地识别和消除。这就意味着对于动态导入(dynamic import)、通过字符串拼接生成导入路径等动态加载方式,无法进行准确的静态分析。

  2. ES6 模块化支持:Tree shaking 最适用于 ES6 模块化语法(import/export)。ES6 模块化的特点是静态导入,使得编译器能够更容易地进行静态分析,以便确定未使用的代码。相比之下,CommonJS(require/module.exports)等模块化方式无法进行静态分析,因此无法通过传统的 tree shaking 技术来消除未使用的代码。

  3. 代码格式和工具支持:Tree shaking 的有效性还取决于代码的格式和所使用的构建工具。使用正确的代码格式(例如纯函数、无副作用)有助于编译器更好地识别未使用的代码。另外,构建工具(如Webpack、Rollup)需要支持 tree shaking,并能正确解析模块依赖关系才能进行优化。

因此,要发挥 tree shaking 的最佳效果,推荐使用 ES6 模块化语法,并结合支持 tree shaking 的构建工具进行打包。对于 CommonJS 等其他模块化方式,通常需要借助其他工具或手动优化以减少未使用的代码的影响。

452、ES6的class编译完成后产物是什么样的,比如是函数or对象or数组?

在 ES6 的 class 语法中,最终的产物是一个函数对象,它与传统的基于原型链的继承机制是一致的。

ES6 中的 class 关键字实际上只是在语法上对原有基于原型的继承机制的语法糖。在 JavaScript 引擎中,class 定义的类仍然是一个函数。

当我们使用 class 定义一个类时,最终编译完成后会生成一个构造函数和一个原型对象。构造函数是类的实例化函数,用于创建类的实例,而原型对象则包含了该类定义的所有方法。在类内定义的静态方法会被添加到构造函数对象本身上,而不是原型对象上。

下面是一个简单的示例:

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

  sayHello() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }

  static createDefaultPerson() {
    return new Person('Alice', 18);
  }
}

const p = new Person('Bob', 25);
p.sayHello();

const defaultPerson = Person.createDefaultPerson();
defaultPerson.sayHello();

在上述示例中,我们使用 class 定义了一个 Person 类,并在其中定义了一个构造函数、一个实例方法以及一个静态方法。编译完成后,Person 类的产物是一个函数对象,同时还有一个包含实例方法的原型对象和一个包含静态方法的函数对象。可以通过以下代码验证:

console.log(typeof Person); // 'function'
console.log(Person.prototype); // { constructor: f Person, sayHello: f }
console.log(typeof Person.createDefaultPerson); // 'function'

因此,在 ES6 的 class 语法中,最终的产物是一个函数对象,它与传统的基于原型链的继承机制是一致的。

453、ES6中继承的原理

ES6 中的继承是通过类与类之间的extends关键字以及super关键字来实现的。这种继承方式被称为基于类的继承。

基于类的继承在底层仍然使用了原型链的机制。当一个类通过 extends 关键字继承另一个类时,实际上是创建了一个新的构造函数,并将其原型对象指向被继承类的实例。

下面是一个简单的示例,演示了如何使用 extendssuper 进行继承:

class Animal {
  constructor(name) {
    this.name = name;
  }

  eat() {
    console.log(`${this.name} is eating.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  bark() {
    console.log(`Woof! My name is ${this.name}. I am a ${this.breed}.`);
  }
}

const dog = new Dog('Bobby', 'Golden Retriever');
dog.eat(); // 继承自父类 Animal 的方法
dog.bark(); // 子类 Dog 自己的方法

在上述示例中,Dog 类通过 extends 关键字继承了 Animal 类。在 Dog 的构造函数中,我们使用 super() 来调用父类 Animal 的构造函数,并传递相应的参数。这样可以确保在创建 Dog 实例时,父类的构造函数也会被执行。

继承后,Dog 类实例既可以调用自身的方法(例如 bark()),也可以调用父类 Animal 的方法(例如 eat())。在 Dog 类中,我们可以通过 this.namethis.breed 分别访问自身的属性,以及通过 super 访问父类的属性和方法。

通过 extendssuper 实现的继承机制,使得子类能够从父类继承属性和方法,并且可以对其进行扩展或重写。同时,父类的原型链上的其他祖先类的属性和方法也会被子类所继承。这样就实现了基于类的继承。

454、Promise函数里有error会怎么样?必须catch吗,用then呢?

在 Promise 函数中,如果抛出一个异常(错误),则会导致 Promise 的状态变为已拒绝(rejected),并且传递该异常作为拒绝原因(rejected reason)。此时,如果没有手动捕获该异常,就会导致运行时错误。

对于这种情况,最好是使用 .catch() 方法来进行异常处理,以便在发生错误时能够及时捕获和处理。.catch() 方法可以链式调用多次,以便处理不同类型的错误或重试失败的操作。

function asyncFunc() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      try {
        throw new Error('Something went wrong');
        resolve('Async operation completed');
      } catch (error) {
        reject(error);
      }
    }, 1000);
  });
}

asyncFunc()
  .then(function(result) {
    console.log(result);
  })
  .catch(function(error) {
    console.error('Error:', error.message);
  });

在上述示例中,Promise 函数中使用 try-catch 块捕获了一个异常,并使用 reject 函数将 Promise 对象的状态设置为已拒绝(rejected)。在 then 方法中,如果 Promise 的状态变为已解决(fulfilled)则打印异步操作完成的结果,如果状态变为已拒绝(rejected)则打印错误信息。这样就充分利用了 Promise 的链式调用特性,使得代码更加简洁、易读和易维护。

在 Promise 中也可以使用 then 方法的第二个参数来进行异常处理。这样的话,如果 Promise 的状态变为已拒绝(rejected)则会直接执行该方法,而不需要显式地使用 .catch() 方法。

function asyncFunc() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      try {
        throw new Error('Something went wrong');
        resolve('Async operation completed');
      } catch (error) {
        reject(error);
      }
    }, 1000);
  });
}

asyncFunc()
  .then(
    function(result) {
      console.log(result);
    },
    function(error) {
      console.error('Error:', error.message);
    }
  );

虽然使用 then 方法的第二个参数也可以处理异常,但是最好还是使用 .catch() 方法进行异常处理,因为它更加专门化。如果异常处理代码比较复杂,使用 .catch() 方法可以使代码更加清晰、易读和易于维护。

455、async/await原理

async/await 是 ECMAScript 2017 引入的一种异步编程语法,它通过使用 asyncawait 关键字来简化 Promise 的使用,并使异步代码看起来更像同步代码。

下面是 async/await 的基本原理:

  1. async 函数:

    • 通过在函数定义前面加上 async 关键字,将普通函数转换为 async 函数。
    • async 函数内部可以使用 await 表达式来等待一个 Promise 对象的解决结果,并暂停函数的执行。在等待期间,async 函数会立即返回一个 Promise 对象。
  2. await 表达式:

    • async 函数内部,可以使用 await 关键字来等待一个 Promise 对象的解决结果。
    • await 表达式后面跟着一个 Promise 对象,它可以是一个调用返回 Promise 的异步函数或直接使用 new Promise() 构造出来的 Promise 对象。
    • 当遇到 await 表达式时,async 函数会暂停执行,等待 Promise 对象的状态变为已解决(fulfilled)并返回解决结果,然后继续执行后面的代码。

实际上,async/await 是在 Promise 之上的一种语法糖,它基于 Promise 提供的异步操作能力进行封装和简化。

以下是一个使用 async/await 的简单示例:

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function asyncFunc() {
  console.log('Async function started');
  await delay(1000);  // 等待1秒钟
  console.log('Async function resumed');
  return 'Async operation completed';
}

asyncFunc()
  .then(result => console.log(result))
  .catch(error => console.error(error));

console.log('After async function');

在上述示例中,通过在函数定义前面加上 async 关键字,将 asyncFunc 转换为一个 async 函数。在 asyncFunc 内部,使用 await 表达式等待 delay(1000) 返回的 Promise 对象执行完成。在等待期间,asyncFunc 函数会暂停执行,并且 console.log('Async function resumed') 这行代码会在等待结束后执行。最后,通过 .then() 方法获取异步操作的结果,并在控制台输出。

总结:async/await一种基于 Promise 的异步编程语法糖,通过使用 asyncawait 关键字,可以以更直观、更类似同步的方式编写异步代码,提高代码的可读性和可维护性。

456、await后面跟一个1或者字符串可以吗?

在使用 await 关键字时,它后面必须跟着一个 Promise 对象。Promise 对象是用来表示异步操作的结果或状态的对象,它包含了异步操作成功或失败的回调函数。

await 后面跟着一个非 Promise 对象(如数字、字符串等)时,会自动将其转换为一个已经被解决(fulfilled)的 Promise 对象。这个 Promise 对象的解决值就是跟在 await 后面的那个非 Promise 对象。

async function asyncFunc() {
  const result = await 'Hello';
  console.log(result);  // 输出:Hello

  const number = await 123;
  console.log(number);  // 输出:123
}

asyncFunc();

在上述示例中,await 'Hello'await 123 跟在 await 后面的字符串和数字会被自动转换为一个已解决的 Promise 对象。然后,await 表达式暂停了函数的执行,直到这些 Promise 对象解决完毕,并返回相应的解决值。

需要注意的是,由于这些非 Promise 对象没有真正的异步行为,它们的处理速度与同步代码相同。因此,在实际开发中,通常将 await 用于等待真正的异步操作,例如调用返回 Promise 的异步函数或使用异步 API。

总结:await 后面必须跟着一个 Promise 对象,但是当跟着非 Promise 对象时,会自动将其转换为一个已解决的 Promise 对象,并返回相应的结果。在实际开发中,建议将 await 用于真正的异步操作,以充分发挥其优势。

457、Generator函数接触过吗

迭代器(Iterator)和生成器(Generator)是 JavaScript 中的两个重要概念,它们之间有着密切的关系。

迭代器是一种对象,它提供了一种方法来访问一个容器(如数组或对象)中的元素,而不需要暴露容器的内部实现。迭代器对象必须实现一个 next() 方法,该方法返回一个包含 value 和 done 两个属性的对象。value 属性表示当前迭代到的元素,done 属性表示迭代是否结束。

生成器是一种特殊的函数,它可以在执行过程中暂停并恢复。生成器函数使用 function* 关键字定义,它内部可以使用 yield 关键字来暂停执行并返回一个值。生成器函数返回的是一个迭代器对象,可以通过调用 next() 方法来依次访问生成器函数中 yield 返回的值。

因此,可以说生成器是一种特殊的迭代器,它可以通过 yield 关键字来暂停执行并返回值,而不需要手动实现 next() 方法。生成器函数返回的是一个迭代器对象,可以通过调用 next() 方法来依次访问生成器函数中 yield 返回的值。

下面是一个简单的示例,演示了迭代器和生成器的关系:

function* generateNumbers() {
  let i = 0;
  while (i < 5) {
    yield i++;
  }
}

const iterator = generateNumbers();
console.log(iterator.next()); // { value: 0, done: false }
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

上述代码中,我们定义了一个 generateNumbers() 生成器函数,它可以生成 0 到 4 的数字。我们通过调用 generateNumbers() 函数来获取一个迭代器对象,然后依次调用 next() 方法来访问生成器函数中 yield 返回的值。每次调用 next() 方法时,生成器函数会从上次暂停的位置继续执行,直到遇到下一个 yield 关键字或者函数结束。当生成器函数执行完毕后,迭代器对象的 done 属性为 true,value 属性为 undefined。

458、跨域请求的时候带cookie怎么带?

在跨域请求中,当带有 Cookie 的请求时,会受到浏览器的限制,不会自动发送 Cookie。默认情况下,浏览器只会发送同源请求的 Cookie。

如果需要在跨域请求中携带 Cookie,可以进行如下配置:

  1. 前端设置:在发送跨域请求时,设置 withCredentialstrue。例如使用 XMLHttpRequest 发送请求时,可以添加以下代码:

    const xhr = new XMLHttpRequest();
    xhr.withCredentials = true;
    // 发送请求...
    
  2. 服务器设置:后端服务接收到跨域请求后,需要在响应中设置特定的响应头信息,允许请求携带 Cookie。常见的响应头字段是 Access-Control-Allow-Credentials,并将其值设置为 true

例如,在使用 Node.js 的 Express 框架下,可以使用 cors 中间件并进行如下配置:

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

const app = express();

app.use(cors({
  credentials: true, // 允许请求携带 Cookie
  origin: 'http://example.com', // 设置允许跨域的源地址
}));

// 处理跨域请求...

需要注意的是,为了确保安全性,服务器需要明确指定允许跨域的源地址,不能将 origin 设置为通配符 *

在进行跨域请求时,同时携带 Cookie 需要前后端进行配合设置,前端设置 withCredentialstrue,后端设置响应头信息允许携带 Cookie。这样就可以在跨域请求中成功携带 Cookie。

459、websocket连接断开期间的消息会丢失吗,怎么重连?

当 WebSocket 连接断开期间,如果服务器端发送了消息,客户端是接收不到这些消息的。WebSocket 本身并不会保证消息的可靠性,因此需要开发者自行处理消息丢失和重连机制。

在实现 WebSocket 重连机制时,一般有以下几个步骤:

  1. 当 WebSocket 连接断开时,根据具体情况决定是否尝试重新连接。
  2. 如果选择重新连接,需要等待一段时间(如数秒或数十秒)后再次尝试建立连接,以避免频繁的连接尝试对服务器资源造成压力。
  3. 在重连时,可以保持之前的连接参数(如 URL、认证信息等)不变,以便更快地恢复连接。
  4. 如果多次重连仍然失败,可以提示用户或记录日志,并停止重连。

下面是一个简单的 WebSocket 重连示例代码,演示了如何在连接断开时尝试重连:

let websocket = null;
let reconnectTimer = null;

function connectWebSocket() {
  websocket = new WebSocket('ws://localhost:8000');

  websocket.addEventListener('open', function() {
    console.log('WebSocket connected!');
    clearInterval(reconnectTimer);
  });

  websocket.addEventListener('message', function(event) {
    console.log('Received message:', event.data);
  });

  websocket.addEventListener('close', function(event) {
    console.log('WebSocket closed with code', event.code, 'and reason', event.reason);
    reconnectWebSocket();
  });

  websocket.addEventListener('error', function(error) {
    console.error('WebSocket error:', error);
    reconnectWebSocket();
  });
}

function reconnectWebSocket() {
  if (websocket && websocket.readyState === WebSocket.OPEN) {
    return;
  }

  clearTimeout(reconnectTimer);
  reconnectTimer = setTimeout(function() {
    console.log('Reconnecting WebSocket...');
    connectWebSocket();
  }, 3000); // 等待 3 秒后尝试重新连接
}

connectWebSocket(); // 初始连接

通过以上方法,可以实现 WebSocket 的自动重连。当 WebSocket 连接断开时,会等待一段时间后尝试重新连接。如果多次重连仍然失败,则可以提示用户或记录日志,并停止重连。在实际使用中,我们可以根据特定的业务场景和需求进行优化和改进,以实现更加稳定和可靠的 WebSocket 连接。

460、axios的请求拦截和响应拦截底层实现原理是什么?

Axios 的请求拦截和响应拦截的底层实现原理是通过使用拦截器(interceptor)来实现的。

请求拦截器和响应拦截器都是基于 Axios 的拦截器机制来实现的。拦截器是一个函数,可以在请求发送之前或响应返回之后对其进行处理。Axios 通过 interceptors 对象提供了 requestresponse 属性来访问请求拦截器和响应拦截器。

具体实现原理如下:

  1. 请求拦截器原理:

    • Axios 使用 axios.interceptors.request.use() 方法添加请求拦截器。
    • 该方法接收两个参数:一个是成功回调函数,一个是错误回调函数。
    • 当发送请求时,请求会先经过请求拦截器的成功回调函数,然后再发往服务器。
    • 成功回调函数可以对请求进行修改或增加额外的配置信息等。
    • 如果请求拦截器的成功回调函数中发生错误,会触发错误回调函数。
  2. 响应拦截器原理:

    • Axios 使用 axios.interceptors.response.use() 方法添加响应拦截器。
    • 该方法也接收两个参数:一个是成功回调函数,一个是错误回调函数。
    • 当接收到服务器响应后,响应会先经过响应拦截器的成功回调函数,然后再返回给调用方。
    • 成功回调函数可以对响应进行修改、过滤或处理等。
    • 如果响应拦截器的成功回调函数中发生错误,会触发错误回调函数。

通过使用请求拦截器和响应拦截器,我们可以在请求发出前和响应返回后对其进行预处理以及统一处理错误。例如,在请求拦截器中可以设置统一的请求头,而在响应拦截器中可以对返回的数据进行格式化或错误处理。

需要注意的是,拦截器是基于 Promise 实现的,可以通过 Promise.resolve()Promise.reject() 来控制拦截器的执行流程。同时,拦截器可以添加多个,它们会按照添加的顺序依次执行。

461、对同一个接口发多次请求,当其中一个接口有消息返回中断其他的请求怎么实现?

要在其中一个接口有消息返回时中断其他请求,可以使用 AbortControllerfetch 结合的方法。AbortController 是一个用于中断请求的 API,它可以通过调用 abort() 方法来中断正在进行的请求。

下面是使用 JavaScript 实现中断其他请求的示例代码:

async function sendRequests(urls) {
  const controller = new AbortController();

  // 创建一个计时器,在指定时间后中断所有请求
  const timeout = setTimeout(() => {
    controller.abort();
    console.log('Requests aborted.');
  }, 5000); // 设置超时时间为 5 秒

  try {
    // 发送多个请求
    const requests = urls.map(url => fetch(url, { signal: controller.signal }));

    // 等待所有请求完成
    await Promise.all(requests);

    // 取消计时器,因为所有请求都已经完成
    clearTimeout(timeout);

    console.log('All requests have been sent and processed.');
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Requests aborted due to timeout.');
    } else {
      console.error('Failed to send requests:', error);
    }
  }
}

// 示例用法
const urls = ['url1', 'url2', 'url3', 'url4', 'url5'];
sendRequests(urls);

在上述代码中,我们首先创建了一个 AbortController 对象 controller,并使用 setTimeout 在指定时间后调用 controller.abort() 方法来中断所有请求。然后,我们使用 fetch 函数发送多个请求,并在每个请求中传入 signal: controller.signal,以便将该信号关联到每个请求中。

接着,我们使用 Promise.all 等待所有请求完成。如果其中一个请求由于超时被中断,会抛出一个名为 'AbortError' 的错误。我们可以通过捕获这个错误来判断是否有请求被中断。

通过上述代码,当其中一个接口有消息返回时,我们可以中断其他的请求,并在超时时间到达后,打印相应的信息。

462、如果想给localstorage添加有效时间,有没有什么思路去实现

localstorage 是 HTML5 提供的一种客户端存储数据的简单机制,可以将数据存储在浏览器本地。但是,它并没有提供设置有效时间的方法。

实现 localstorage 的有效时间可以通过以下思路:

  1. 存储时同时记录时间戳:将需要存储的对象和当前时间戳一起存储,例如:

    const data = { key: 'value', expireAt: Date.now() + 24 * 60 * 60 * 1000 // 设置过期时间为 24 小时后 }

    localStorage.setItem('data', JSON.stringify(data));

  2. 获取数据时判断时间戳是否过期:在获取数据时,先将存储的对象解析出来,然后判断当前时间是否大于等于过期时间戳,如果是,则认为数据已过期,删除数据即可。

    const storedData = localStorage.getItem('data'); if (storedData) { const data = JSON.parse(storedData); if (Date.now() >= data.expireAt) { localStorage.removeItem('data'); } else { // 数据未过期,可以使用 } }

这样就可以轻松实现给 localstorage 添加有效时间的功能了。可以根据需要设置过期时间,以便在一定时间后自动清除无用的本地存储数据,节省空间和提升性能。

463、讲一下对JSONP的理解

JSONP(JSON with Padding)是一种在跨域请求数据时使用的一种解决方案。

由于同源策略的限制,浏览器默认不允许跨域请求数据。但是通过 script 标签的 src 属性发起的请求是允许跨域的,这就是 JSONP 利用的原理。

JSONP 的工作原理是通过动态创建一个 <script> 标签,将跨域的请求地址作为 src 属性的值,并指定一个回调函数的名称作为参数传递给服务器。服务器接收到请求后,将数据包装在回调函数中作为响应返回。

客户端接收到响应后,浏览器会自动执行回调函数,并将服务器返回的数据作为参数传入回调函数中。这样就实现了从跨域请求中获取到数据的目的。

需要注意的是,由于安全性的考虑,客户端和服务器需要事先约定好回调函数的名称,以确保客户端能够正确解析和处理返回的数据。

尽管 JSONP 解决了跨域请求数据的问题,但它也存在一些限制,例如只支持 GET 请求、不支持发送头部信息等。另外,由于数据是通过脚本执行的方式返回,所以无法使用 XMLHttpRequest 对象的错误处理机制。因此,在使用 JSONP 时需要谨慎考虑安全性和兼容性的问题。

近年来,随着现代浏览器对跨域问题的解决方案的不断发展,JSONP 的使用逐渐减少,取而代之的是更为灵活和安全的跨域请求方式,如 CORS(跨域资源共享)等。

464、有用过web worker吗

Web Worker 是 HTML5 中提供的一项新特性,它允许在 Web 应用程序中创建多个线程(或称为工作线程),从而能够在后台执行计算密集型的任务,而不会对 Web 应用程序的主线程造成阻塞。

具体来说,Web Worker 的实现方式是通过创建一个独立的 JavaScript 运行环境来实现多线程的效果。这个独立的 JavaScript 运行环境与 Web 应用程序的主线程相互独立,它们之间不能直接互相访问,但可以通过消息传递机制进行通信。当我们需要在后台执行某个耗时操作时,可以将这个操作封装成一个 Worker 线程,并在主线程中向该线程发送消息来触发计算任务的执行。当计算任务执行完成后,Worker 线程又会将计算结果返回给主线程,从而达到了异步执行的目的。

Web Worker 可以显著提高 Web 应用程序的响应性能和用户体验,尤其是在处理大量数据、图像处理等方面有着明显的优势。但需要注意的是,由于每个 Worker 线程都是独立运行的,所以它们之间无法共享变量和状态,因此在使用 Web Worker 时需要特别小心。

465、什么时候用OPTION请求

OPTION请求是一种HTTP请求方法,主要用于跨域资源共享(CORS)中的预检请求。当使用跨域AJAX请求时,将会发起OPTION请求用于询问服务器是否允许该AJAX请求。这是由于浏览器的同源策略限制了跨域AJAX请求的自由发起。

跨域AJAX请求一般分为简单请求和非简单请求

  • 使用GET、HEAD或POST请求方法之一;
  • 除了常见的简单请求头(如Accept、Accept-Language、Content-Language、Content-Type的值为application/x-www-form-urlencoded、multipart/form-data、text/plain之一)外,HTTP头部仅使用了以下字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type的值为application/x-www-form-urlencoded、multipart/form-data、text/plain之一。

对于简单请求,浏览器会直接发送AJAX请求,并在HTTP头部添加一个Origin字段,表示请求的来源。如果服务器接受跨域请求,将在HTTP头部返回一个Access-Control-Allow-Origin字段,表示允许此来源的请求。如果服务器不允许跨域请求,将返回一个403 Forbidden错误。

对于非简单请求,由于可能存在复杂的请求头和请求体,需要先发送一个OPTION请求进行预检,检查服务器是否允许该请求,然后才能发送实际的AJAX请求。OPTION请求在HTTP头部添加了一个Access-Control-Request-Method字段,表示实际的请求方法,以及可能存在的Access-Control-Request-Headers字段,表示实际的请求头。服务器需要在HTTP头部返回一个Access-Control-Allow-Methods字段,表示允许的请求方法,以及一个Access-Control-Allow-Headers字段,表示允许的请求头。如果服务器不允许跨域请求,将返回一个403 Forbidden错误。

综上所述,当使用跨域AJAX请求并且请求为非简单请求时,需要先发送OPTION请求进行预检

466、跨域的场景下一定会发OPTION吗

不一定,当跨域AJAX请求满足一定条件时,不需要发送OPTION请求。

根据CORS规范,只有当跨域AJAX请求满足以下条件之一时,浏览器才会自动发起OPTION请求进行预检:

  1. 使用了并非GET、HEAD或POST的HTTP方法。如PUT、DELETE等。
  2. 使用了POST请求方法,并且Content-Type的值不是application/x-www-form-urlencoded、multipart/form-data、text/plain中的一种。
  3. 使用了自定义请求头(除了以下几种常见字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type),即使用了未在标准中定义的自定义请求头。

对于这三种情况之外的跨域AJAX请求都属于简单请求,而且简单请求也不需要设置额外的请求头,因此浏览器会自动将Origin字段添加到HTTP头部中,然后直接发送AJAX请求。如果服务器允许该跨域请求,则在响应头中添加Access-Control-Allow-Origin字段来表明同意跨域访问,否则浏览器将抛出跨域访问错误。因此在这种情况下,OPTION预检请求是没有必要的。

467、Vue:发布订阅(如何实现发布订阅)

发布-订阅模式也叫做观察者模式。通过一对一或者一对多的依赖关系,当对象发生改变时,订阅方都会收到通知。在现实生活中,也有很多类似场景,比如我需要在购物网站上购买一个产品,但是发现该产品目前处于缺货状态,这时候我可以点击有货通知的按钮,让网站在产品有货的时候通过短信通知我。

在实际代码中其实发布-订阅模式也很常见,比如我们点击一个按钮触发了点击事件就是使用了该模式

<ul id="ul"></ul>
<script>
    let ul = document.querySelector('#ul')
    ul.addEventListener('click', (event) => {
        console.log(event.target);
    })
</script>

在 Vue 中,如何实现响应式也是使用了该模式。对于需要实现响应式的对象来说,在get的时候会进行依赖收集,当改变了对象的属性时,就会触发派发更新。

class EventCenter{
  // 1. 定义事件容器,用来装事件数组
    let handlers = {}
​
  // 2. 添加事件方法,参数:事件名 事件方法
  addEventListener(type, handler) {
    // 创建新数组容器
    if (!this.handlers[type]) {
      this.handlers[type] = []
    }
    // 存入事件
    this.handlers[type].push(handler)
  }
​
  // 3. 触发事件,参数:事件名 事件参数
  dispatchEvent(type, params) {
    // 若没有注册该事件则抛出错误
    if (!this.handlers[type]) {
      return new Error('该事件未注册')
    }
    // 触发事件
    this.handlers[type].forEach(handler => {
      handler(...params)
    })
  }
​
  // 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和发布
  removeEventListener(type, handler) {
    if (!this.handlers[type]) {
      return new Error('事件无效')
    }
    if (!handler) {
      // 移除事件
      delete this.handlers[type]
    } else {
      const index = this.handlers[type].findIndex(el => el === handler)
      if (index === -1) {
        return new Error('无该绑定事件')
      }
      // 移除事件
      this.handlers[type].splice(index, 1)
      if (this.handlers[type].length === 0) {
        delete this.handlers[type]
      }
    }
  }
}

468、React:用react的时候遇到的难点

在使用React开发应用时,可能会遇到以下几个难点:

  1. 状态管理:React是一个状态驱动的框架,但如果状态管理不当,可能会导致性能问题和代码复杂度增加。因此,需要选择适合项目的状态管理方案,如Redux或MobX,并严格遵循其规范使用。

  2. 组件通信:React中的组件通信可以通过props、context、事件等方式实现。但在复杂的应用场景中,组件通信可能变得困难和混乱。为了简化组件通信,可以使用第三方库如React Router、Redux等,或者自行设计合理的组件结构。

  3. 性能优化:React的虚拟DOM机制使得它具有出色的性能表现。但是,在处理大型数据、频繁更新、渲染复杂组件等情况下,仍然可能会出现性能问题。为了优化性能,需要使用React提供的优化API(如shouldComponentUpdatePureComponentReact.memo等),并注意避免频繁地进行不必要的更新。

  4. 生命周期管理:React组件有多个生命周期方法,例如componentDidMountcomponentWillUnmount等。如果不合理地使用这些生命周期方法,可能会导致内存泄漏、资源浪费等问题。因此,需要仔细设计组件的生命周期,并在适当的时候进行清理和销毁。

  5. 跨浏览器兼容性:尽管React已经解决了大部分跨浏览器兼容性问题,但在某些特定情况下,仍然需要进行额外的兼容性处理。例如,在使用第三方库或组件时,需要注意其在不同浏览器中的行为差异,并针对性地进行调整。

469、编程:实现Promise.all

  1. 创建一个新的Promise对象,并返回它。

  2. 遍历传入的可迭代对象(通常是数组),对每个元素执行以下操作:

    • 如果元素不是Promise对象,则使用Promise.resolve将其转换为Promise对象。
    • 对每个Promise对象,等待其状态变为fulfilled,并收集解决的值。
  3. 如果所有Promise对象都成功解决,则使用resolve将所有收集到的解决值作为数组传递给新创建的Promise对象。

  4. 如果任何一个Promise对象被拒绝,则使用reject将该拒绝原因传递给新创建的Promise对象。

    function customPromiseAll(iterable) {
      return new Promise((resolve, reject) => {
        const promises = Array.from(iterable);
        const results = [];
        let completedCount = 0;
    
        if (promises.length === 0) {
          resolve(results);
          return;
        }
    
        promises.forEach((promise, index) => {
          Promise.resolve(promise)
            .then((result) => {
              results[index] = result;
              completedCount++;
    
              if (completedCount === promises.length) {
                resolve(results);
              }
            })
            .catch((error) => {
              reject(error);
            });
        });
      });
    }
    
    // 示例用法:
    const promise1 = Promise.resolve(1);
    const promise2 = Promise.resolve(2);
    const promise3 = Promise.resolve(3);
    
    customPromiseAll([promise1, promise2, promise3])
      .then((results) => {
        console.log(results); // 输出 [1, 2, 3]
      })
      .catch((error) => {
        console.error(error);
      });
    

470、js中async和defer的区别

  • async属性:当<script>标签包含async属性时,脚本会异步下载,不会阻塞HTML解析。下载完成后,脚本会立即执行,但仍然可能阻塞后续的脚本执行。
  • **defer**属性:当<script>标签包含defer属性时,脚本会异步下载,不会阻塞HTML解析。与async不同,defer脚本会等到文档解析完成后,按照它们在文档中出现的顺序执行。

最佳实践

  • 如果脚本不依赖于DOM,可以使用async属性来避免阻塞。
  • 如果脚本之间有依赖关系,或者需要在DOM完全解析后执行,使用defer属性。
  • 对于内联脚本(即直接在<script>标签中编写JavaScript代码),通常不需要asyncdefer属性,除非脚本非常短,且不依赖于页面其他部分的执行。

471、介绍一下节流与防抖的区别及应用场景

  • 节流:是在每个时间段里,最多只允许运行一次。比如说resize调整窗口,在调整窗口的过程中,事件一直在高频率的触发,可以利用节流函数让其在一定的间隔时间段内最多触发一次。
  • 防抖:在高频调用中,只有足够的空闲时间,代码才会执行一次,常见的就是input的change事件,只有停顿输入的事件大于指定的时间,代码才会执行一次。

节流

时间戳方式:记录上一次执行的时间戳,当当前时间戳 - 上一次执行的时间戳 >= 指定时间间隔时,才执行相应的操作或事件;否则不执行

function throttle(func, wait) {
  let lastTime = 0;
  return function() {
    const context = this;
    const args = arguments;
    const currentTime = +new Date();
    if (currentTime - lastTime > wait) {
      func.apply(context, args);
      lastTime = currentTime;
    }
  };
}

定时器方式:使用定时器控制函数的执行次数,在指定时间间隔内,如果有多个操作或事件被触发,则只执行最后一次操作或事件

function throttle(func, wait) {
  let timeout = null;
  return function() {
    const context = this;
    const args = arguments;
    if (!timeout) {
      timeout = setTimeout(function() {
        func.apply(context, args);
        timeout = null;
      }, wait);
    }
  };
}

防抖

时间戳方式:记录上一次执行的时间戳,当当前时间戳 - 上一次执行的时间戳 >= 指定时间间隔时,才执行相应的操作或事件;否则重新计时

function debounce(func, wait) {
  let timeout = null;
  return function() {
    const context = this;
    const args = arguments;
    const currentTime = +new Date();
    if (timeout) clearTimeout(timeout);
    if (currentTime - lastTime > wait) {
      func.apply(context, args);
      lastTime = currentTime;
    } else {
      timeout = setTimeout(function() {
        func.apply(context, args);
      }, wait);
    }
  };
}

定时器方式:使用定时器控制函数的执行,在指定时间间隔内,如果有多个操作或事件被触发,则只执行最后一次操作或事件

function debounce(func, wait) {
  let timeout = null;
  return function() {
    const context = this;
    const args = arguments;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(function() {
      func.apply(context, args);
    }, wait);
  };
}

472、Webpack的构建,常用的plugin

  1. HtmlWebpackPlugin:用于生成 HTML 文件,并将打包后的 JavaScript 和 CSS 自动引入到 HTML 文件中。

  2. MiniCssExtractPlugin:用于将样式文件从 JavaScript 中提取出来,并分离出单独的 CSS 文件。

  3. CleanWebpackPlugin:用于清除构建目录中的无用文件,在重新构建之前保持代码库干净和整洁。

  4. DefinePlugin:用于定义全局常量,例如环境变量等等。

  5. CopyWebpackPlugin:用于复制文件和文件夹到构建目录中,例如图片、字体等等。

  6. UglifyJsPlugin:用于压缩 JavaScript 代码,减少文件大小。

  7. ProvidePlugin:用于自动加载模块,例如 jQuery、lodash 等等。

  8. ExtractTextWebpackPlugin:用于将样式表从 JavaScript 中提取出来,并生成单独的 CSS 文件。

  9. BundleAnalyzerPlugin:用于分析构建结果,并生成一个交互式的网页报告,帮助开发者找到构建过程中的优化点。

  10. HotModuleReplacementPlugin:用于启用热模块替换功能,在开发过程中实现快速热更新,提升开发效率。

473、vite知道吗?

Vite 是一种快速的现代化前端构建工具,相比传统的打包工具(如 webpack)具有以下优点:

  1. 快速的冷启动:Vite 利用 ES 模块的特性,采用了基于原生 ES 模块的开发服务器。它使用浏览器原生支持的 ES 模块加载方式,无需预构建和打包,直接在浏览器中运行。这样,在开发过程中保持了快速的冷启动时间,加快了开发反馈速度。

  2. 按需编译:Vite 只会编译当前正在编辑的文件,而不是重新构建整个项目。当你保存修改后,只有被修改的文件会被重新编译,大大缩短了每次保存的构建时间,提高了开发效率。

  3. 真正的模块化热更新:Vite 支持真正的模块级热更新,它通过在开发服务器中搭建一个简单的 WebSocket 服务器,与运行在浏览器中的开发服务之间建立了一个实时的连接。这使得只有相关模块发生更改时才会触发热更新,而不会影响其他模块,提供了更快速、可靠的热更新体验。

  4. 零配置:Vite 提供了一种零配置的开发体验。它内置了对常见的前端框架(如 Vue、React 和 Preact)的支持,不需要复杂的配置即可开始开发。同时,Vite 也提供了可扩展的插件系统,以便进行更高级的定制和配置。

  5. 构建速度快:虽然 Vite 在开发过程中不需要进行传统的打包,但它仍然提供了一个生产环境的构建命令。Vite 使用 Rollup 进行构建,通过 ESBuild 进行快速的 JavaScript 编译,以及采用了增量构建的方式,使得构建速度非常快。

总的来说,Vite 提供了一种现代化的开发体验,通过利用浏览器原生支持的特性,加速了项目的冷启动时间和热更新速度,从而提高了开发效率。

474、Redux讲一下干嘛的?

Redux 是一种状态管理库,它可以用于 JavaScript 应用程序中的状态管理。Redux 的内部实现主要包括三个部分:

  1. State: Redux 中的状态存储在一个单一的对象中,即 store.getState() 返回的对象。这个对象是不可变的,因此每次更新状态时都会创建新的状态对象。

  2. Action: Action 是与状态交互的唯一途径。它们是描述性的对象,用于表示发生了什么事件或操作。每个 action 都必须有一个 type 字段,用于指定该 action 的类型。除了 type 字段外,action 还可以携带其他任意数据。

  3. Reducer: Redux 状态的更改由 reducer 函数处理,reducer 函数接受两个参数:旧状态和一个 action,并返回一个新状态。Reducer 函数必须是纯函数,即不会修改其输入参数,并且具有相同的输出,给定相同的输入。

在 Redux 的内部实现中,通过将 action 派发到 reducer 函数来更改状态,进而在应用程序中实现状态管理。在单向数据流的架构中,整个应用程序的状态是由一个单一的 store 维护,并且状态的更新是通过 dispatch 一个 action 触发的。然后,由 reducer 函数根据传入的 action 和当前状态计算出新的状态,并返回新的状态对象。

475、Redux中间件了解过吗

  • Redux中间件是一种在Redux应用程序中增强数据流的机制。它允许开发者在Redux的action被派发到reducer之前或之后,插入额外的逻辑。

  • 中间件可以用于处理异步操作、日志记录、错误处理、路由跳转等。

  • Redux中间件工作的基本原理是,它拦截Redux的dispatch方法,并在action到达reducer之前或之后执行特定的逻辑。这使得开发者能够在派发action时执行自定义的逻辑,例如发送网络请求、修改action等。

常见的Redux中间件有以下几种:

  1. Redux Thunk: Redux Thunk允许开发者在action中编写异步代码。它将函数类型的action识别为异步操作,并在合适的时机派发实际的action。这使得开发者能够在action中进行异步操作,例如发送网络请求,并在异步操作完成后更新应用状态。

  2. Redux Saga: Redux Saga是一个基于generator函数的Redux中间件。它使用了ES6的generator特性,以一种简洁而强大的方式来处理副作用(如异步操作)。通过定义saga函数,开发者可以非常直观地编写复杂的异步流程,例如监听多个action、并发请求等。

  3. Redux Observable: Redux Observable是基于RxJS的Redux中间件。它利用RxJS的强大功能来处理异步操作。通过使用Observable对象,开发者可以以声明式的方式组合和转换异步事件流,从而编写可维护和可测试的异步逻辑。

  4. Redux Promise: Redux Promise是一个简单的Redux中间件,用于处理基于Promise的异步操作。它允许开发者在action中返回一个Promise对象,当Promise对象被解决时,自动派发另一个action。

476、对闭包的理解?什么情况下会产生闭包?

浏览器在加载页面会把代码放在栈内存( ECStack )中执行,函数进栈执行会产生一个私有上下文( EC ),此上下文能保护里面的使用变量( AO )不受外界干扰,并且如果当前执行上下文中的某些内容,被上下文以外的内容占用,当前上下文不会出栈释放,这样可以保存里面的变量和变量值,所以我认为闭包是一种保存和保护内部私有变量的机制。

477、对Js单线程的理解?缺点是什么?设计初衷是什么?

JavaScript 是一种单线程的编程语言,意味着它只有一个主线程来执行代码。这意味着 JavaScript 一次只能执行一个任务,即同一时间只能处理一个事件或操作。

设计初衷:
JavaScript 最初被设计为一种用于在网页上添加动态交互的脚本语言。在网页浏览器中,JavaScript 主要用于处理用户交互、更新页面内容和响应事件等。在这种情况下,单线程设计被认为是简单而有效的方式,因为它使得开发者不必担心多个线程之间的同步问题,使得编写和调试 JavaScript 程序更加容易。

优点:

  1. 简单性:单线程模型使得 JavaScript 编程相对简单,而且不需要考虑线程同步的复杂性。
  2. 节约资源:相比于多线程的设计,JavaScript 占用的系统资源较少。
  3. 更好的兼容性:JavaScript 在浏览器环境中广泛使用,单线程模型更易于与浏览器的事件循环机制结合,使得异步编程更加方便。

缺点:

  1. 阻塞:当一个事件处理或操作较为耗时时,会阻塞主线程的执行,导致页面冻结或无响应。
  2. 无法充分利用多核处理器:由于 JavaScript 的单线程特性,无法充分利用多核处理器的优势,不能同时进行并行计算。
  3. 长时间运行的任务会阻塞线程:如果一个处理耗时很长的 JavaScript 任务运行在主线程上,将导致整个页面的响应变慢,用户体验下降。

为了解决 JavaScript 单线程的缺点,在浏览器环境中引入了一些异步编程机制,例如回调函数、Promise、async/await 等,使得我们能够执行非阻塞的异步操作,提高程序的性能和响应能力。此外,Web Workers API 也允许开发者在后台创建多线程来处理一些复杂或耗时的任务,以改善单线程模型的局限性。

478、对webWorker的理解?使用限制有哪些?

Web Worker 是 HTML5 提供的一项技术,允许 JavaScript 在后台创建多个线程来执行任务,以提高网页的性能和响应能力。Web Worker 可以在主线程之外运行,与主线程并行工作,并且不会阻塞页面的交互。

主要特点和用途:

  1. 多线程运行:Web Worker 允许 JavaScript 代码在后台创建一个或多个线程,独立于主线程运行。这样可以执行一些耗时的计算、处理大量数据、执行复杂的算法等任务,而不会影响主线程的响应性能。
  2. 并行处理:由于 Web Worker 运行在独立的线程中,可以与主线程并行工作,从而提高页面整体的性能和响应速度。
  3. 不阻塞主线程:由于 Web Worker 运行在单独的线程中,不会阻塞主线程的执行,使得页面保持流畅的用户体验。
  4. 可以进行复杂的计算:Web Worker 适用于需要进行复杂计算或处理大量数据的场景,例如图像处理、音视频编解码、物理模拟等。

Web Worker 的使用限制包括:

  1. 同源策略:Web Worker 遵循同源策略,即只能与加载它的脚本具有相同的域、协议和端口。
  2. 无法直接操作 DOM:Web Worker 不能直接访问或操作页面的 DOM 结构。这是为了确保多线程操作不会导致不可预测的结果或竞态条件。
  3. 消息传递:Web Worker 与主线程之间的通信需要通过消息传递机制,即通过 postMessage() 方法发送消息,并通过 onmessage 事件接收消息。这种通信方式有一定的开销和限制。
  4. 无法访问某些全局对象和方法:Web Worker 中无法访问 window、document 和其他一些浏览器特定的全局对象和方法。

总结起来,Web Worker 是一种在后台运行的多线程技术,可以提高网页的性能和响应能力。然而,由于同源策略、无法直接操作 DOM、消息传递等限制,Web Worker 的使用场景和方式需要根据具体需求进行合理的设计和选择。

479、git hook实现代码commit前检验怎么只测增量代码

要在代码提交前只检查增量代码,您可以编写一个自定义的 Git 钩子(hook),在提交代码前对增量代码进行检查。以下是一种可能的实现方式:

  1. 编写脚本:编写一个脚本来检查本次提交中的增量代码。您可以使用 Git 命令来获取本次提交与上一次提交之间的差异,并针对这些差异进行检查。

  2. 使用 pre-commit 钩子:将编写的脚本作为 pre-commit 钩子的一部分。pre-commit 钩子会在执行 git commit 命令时触发,在实际提交代码前执行脚本进行检查。

演示了如何编写一个 Bash 脚本来实现这个功能:

#!/bin/bash

# 获取本次提交中的增量文件列表
changed_files=$(git diff --cached --name-only)

# 检查增量文件列表中的文件
for file in $changed_files; do
  # 进行你的增量代码检查逻辑,比如格式检查、语法检查等
  # 如果有不符合规范的文件,可以输出错误信息并返回非零退出码
done

然后,将这个脚本保存为 .git/hooks/pre-commit,并赋予执行权限。这样,在每次执行 git commit 时,Git 就会自动运行这个脚本,对增量代码进行检查。

480、href和src区别

hrefsrc 是 HTML 中常见的属性,它们用于指定页面中外部资源(如样式表、脚本文件、图像等)的位置。它们之间的主要区别在于它们所表示的外部资源类型不同。

  1. href 属性:

    • 一般用于链接外部样式表文件(CSS 文件),用于告诉浏览器页面在哪里可以找到样式定义。
    • 例如:<link href="styles.css" rel="stylesheet">
  2. src 属性:

    • 一般用于指定外部脚本文件(JavaScript 文件)或嵌入的图像、音频、视频等资源文件的位置。
    • 例如:<script src="script.js"></script><img src="image.jpg" alt="Image">

总的来说,href 用于建立当前文档与外部资源之间的关联,比如样式表文件;而 src 则用于在当前文档中嵌入或引用外部资源,比如脚本文件、图像等。这两个属性的作用是为了丰富页面内容或者增强页面的交互性和视觉效果。

481、Css黑暗主题如何适配?

使用 CSS 变量:定义一组与主题相关的 CSS 变量,例如背景颜色、文本颜色等,并根据当前主题设置这些变量的值。通过切换不同的主题,可以轻松地改变整个应用的外观。

:root {
  --background-color: #ffffff;
  --text-color: #000000;
}

.dark-theme {
  --background-color: #1a1a1a;
  --text-color: #ffffff;
}

body {
  background-color: var(--background-color);
  color: var(--text-color);
}

使用媒体查询:根据用户的首选颜色方案(light 或 dark)使用媒体查询来选择合适的样式。例如,可以监听 prefers-color-scheme 媒体查询,根据用户系统设置的主题模式应用相应的样式。

@media (prefers-color-scheme: dark) {
  /* Dark mode styles */
  body {
    background-color: #1a1a1a;
    color: #ffffff;
  }
}

使用特定的类名或属性:在 HTML 元素上添加特定的类名或属性,以便根据主题进行样式切换。

<body class="dark-theme">
  <!-- Content -->
</body>


body.dark-theme {
  background-color: #1a1a1a;
  color: #ffffff;
}

无论采用哪种方法,都应该确保黑暗主题的设计符合用户体验原则,包括足够的对比度、易读性和清晰的界面元素。另外,还需要考虑到用户切换主题时的平滑过渡,以确保用户体验的连贯性。

482、富文本如何防止xss攻击

防止富文本内容被恶意注入和导致 XSS 攻击是非常重要的。以下是一些防范措施:

  1. 输入过滤:在接受用户输入并展示为富文本内容之前,对输入进行严格的过滤和验证。移除或转义所有潜在的恶意脚本标记,例如 <script><iframe> 等。

  2. 内容安全策略(Content Security Policy,CSP):通过在 HTTP 头部中设置 CSP,可以限制页面加载的资源来源,并阻止执行内联脚本和未经授权的第三方脚本。这有助于减少 XSS 攻击的风险。

  3. HTML 标签白名单:定义一个白名单,只允许特定的 HTML 标签和属性出现在富文本内容中,其他标签和属性将被过滤掉。可以使用专门的 HTML 清理库来实现这一点,如 DOMPurify。

  4. 输出编码:在将富文本内容输出到页面上时,确保对 HTML 实体进行适当的编码,以防止被解释为 HTML 标签或 JavaScript 代码。例如,使用类似于 htmlspecialchars() 函数的工具进行输出编码。

  5. 安全的富文本编辑器:选择使用经过安全审计和广泛使用的富文本编辑器,以确保其在设计上考虑了安全性,并提供了预防 XSS 攻击的功能。

  6. 定期更新:及时更新富文本编辑器和相关的安全组件,以获取最新的安全修复和漏洞补丁。

483、客户界面白屏怎么排查

客户界面白屏是 H5 项目中常见的问题,可能由多种原因引起。以下是一些排查白屏问题的常见方法:

  1. 检查网络请求:首先检查页面的网络请求情况,确保所需的 HTML、CSS、JavaScript 文件能够正确加载。可以通过浏览器的开发者工具中的 Network 标签页来查看页面的网络请求情况,确认文件是否成功加载。

  2. 查看控制台错误:打开浏览器的开发者工具,查看控制台中是否有任何报错信息。可能会有 JavaScript 错误、资源加载失败等信息,这些都可以帮助定位问题。

  3. 检查 JavaScript 错误:在控制台中查看 JavaScript 错误信息,排查可能导致页面无法正常渲染的 JavaScript 错误。通常情况下,JavaScript 错误可能会导致页面停止渲染并出现白屏。

  4. 检查页面结构:检查 HTML 结构、CSS 样式是否正确,以及是否存在不符合规范的语法或结构。特别注意可能对页面布局和渲染产生影响的部分。

  5. 清除缓存:有时候浏览器缓存可能导致页面无法正确加载,尝试清除浏览器缓存后重新加载页面进行测试。

  6. 检查兼容性:某些浏览器或设备可能不支持某些 CSS 和 JavaScript 特性,导致页面无法正常显示。确保代码在各种主流浏览器和设备上都能正常运行。

  7. 监控性能:使用性能监控工具,了解页面加载和渲染的性能数据,可能会发现某些资源加载过慢或消耗过多的问题。

  8. 逐步注释代码:如果以上方法无法定位问题,可以尝试逐步注释掉部分代码,逐步缩小范围确定问题所在。

484、Webpack的摇树原理

Webpack 的 ‌**Tree Shaking(摇树优化)**‌ 是一种通过静态分析代码依赖关系来消除未使用代码(Dead Code)的技术,它的核心目标是减少最终打包文件的体积。

  1. ‌依赖 ES6 模块语法
    Tree Shaking 基于 ES6 的模块系统(import/export),因为 ES6 模块的依赖关系是‌静态的‌(在编译时就能确定),而 CommonJS(require)的依赖是动态的(运行时才能确定)。

  2. ‌静态代码分析
    Webpack 在构建时会对代码进行静态分析,标记出哪些 export 的代码被其他模块 import 并使用,哪些未被引用。未被引用的 export 会被标记为未使用代码。

  3. ‌代码消除
    在最终打包时,通过压缩工具(如 TerserPlugin)将这些未被使用的代码从打包结果中移除。

Tree Shaking 生效的前提条件

  1. 使用 ES6 模块语法
    代码必须使用 import/export,而非 CommonJS 的 require/module.exports

  2. ‌**避免副作用(Side Effects)**‌

    • 模块中的代码不能有“副作用”(例如:直接执行函数、修改全局变量等),否则 Webpack 无法确定这些代码是否可以安全移除。
    • 可以通过在 package.json 中添加 "sideEffects": false 声明模块无副作用,或指定具体有副作用的文件(如 CSS 文件)。
  3. 生产模式配置
    在 Webpack 配置中启用生产模式(mode: 'production'),这会默认开启代码优化(包括 Tree Shaking)。

常见问题与解决方案‌

  1. **‌Tree Shaking 不生效?**‌

    • 检查模块语法是否为 ES6 的 import/export
    • 确保 package.json 中未全局禁用 sideEffects,或正确配置了副作用文件。
    • 使用 Webpack 的 stats 选项分析打包结果:webpack --stats=verbose
  2. **‌第三方库不支持 Tree Shaking?**‌

    • 许多库(如 Lodash)默认导出为 CommonJS 格式。可改用其 ESM 版本(如 lodash-es)。
  3. ‌Babel 配置问题
    如果使用 Babel,确保它不会将 ES6 模块转换为 CommonJS。在 .babelrc 中设置:

    {
      "presets": [["@babel/preset-env", { "modules": false }]]
    }
    

485、说一下for...in 和 for...of的区别

for...of遍历获取的是对象的键值, for...in获取的是对象的键名; for...in会遍历对象的整个原型链, 性能非常差不推荐使用,而for...of只遍历当前对象不会遍历原型链;

对于数组的遍历,for...in会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for...of只返回数组的下标对应的属性值;

总结:for...in循环主要是为了遍历对象而生,不适用遍历数组; for....of循环可以用来遍历数组、类数组对象、字符串、Set、Map以及Generator对象

486、点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别?

  • 点击刷新按钮或者按 F5: 浏览器直接对本地的缓存文件过期,但是会带上If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是 304,也有可能是 200。
  • 用户按 Ctrl+F5(强制刷新): 浏览器不仅会对本地文件过期,而且不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是 200。
  • 地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。

487、Promise.race

Promise.race是一个Promise方法,它接收一个包含多个Promise对象的可迭代对象(如数组)作为参数。它返回一个新的Promise对象,该对象将与最先解决或拒绝的Promise对象保持相同的状态。

当传递给Promise.race的可迭代对象中的任意一个Promise对象解决或拒绝时,返回的Promise对象会立即解决或拒绝,并采用第一个解决或拒绝的Promise的值或原因。

这种特性使得Promise.race在处理竞态条件(race condition)时非常有用。例如,可以使用Promise.race来实现超时功能,即如果某个异步操作在指定时间内未完成,就进行相应的处理。

只要promises中有一个率先改变状态,就返回这个率先改变的Promise实例的返回值。

Promise.race = function(promises){
    return new Promise((resolve, reject) => {
        if(Array.isArray(promises)) {
            if(promises.length === 0) return resolve(promises);
            promises.forEach((item) => {
                Promise.resolve(item).then(
                    value => resolve(value), 
                    reason => reject(reason)
                );
            })
        }
        else return reject(new TypeError("Argument is not iterable"));
    });
}

488、Promise.any

  • Promise.any是一个Promise方法,它接收一个包含多个Promise对象的可迭代对象(如数组)作为参数。它返回一个新的Promise对象,该对象将与第一个解决的Promise对象保持相同的状态。

  • 与Promise.race不同,Promise.any会等待传递给它的所有Promise对象都被拒绝后才会拒绝,只有当至少有一个Promise对象被解决时,返回的Promise对象才会解决。它将采用第一个解决的Promise的值作为其解决值。

  • 这种特性使得Promise.any在处理多个异步操作的情况下非常有用,只要有一个操作成功完成,即可继续执行相关的逻辑。如果所有操作都失败,则可以在Promise.any返回的Promise对象上进行错误处理。

只要 promises 中有一个fulfilled,就返回第一个fulfilledPromise实例的返回值。

Promise.any = function(promises) {
    return new Promise((resolve, reject) => {
        if(Array.isArray(promises)) {
            if(promises.length === 0) return reject(new AggregateError("All promises were rejected"));
            let count = 0;
            promises.forEach((item, index) => {
                Promise.resolve(item).then(
                    value => resolve(value),
                    reason => {
                        count++;
                        if(count === promises.length) {
                            reject(new AggregateError("All promises were rejected"));
                        };
                    }
                );
            })
        }
        else return reject(new TypeError("Argument is not iterable"));
    });
}

489、闭包的应用场景

  1. return回一个函数
  2. 函数作为参数
  3. IIFE(自执行函数)
  4. 循环赋值
  5. 使用回调函数就是在使用闭包
  6. 节流防抖
  7. 函数柯里化