前端面经真题与解析3:字节/抖音/实习(2万字长文)

第一面(45min)


第一面交叉面,主要考察了一些前端以及计算机网络等基础,另外也考察英文文档的阅读以及理解能力。

1.箭头函数相关(作用域、实例化)

箭头函数是 JavaScript 中的一种特殊函数表达式,引入了更简洁的语法和改变了函数的作用域行为。下面是对箭头函数的介绍,包括作用域和示例化:

1.语法:

(parameters) => { statements }

(parameters) => expression

2.箭头函数的作用域:

  • 箭头函数没有自己的 this 值,它继承了父级作用域中的 this 值。这意味着在箭头函数内部,this 的值是定义函数时所在的上下文的 this 值,而不是调用时的 this 值。
  • 箭头函数没有自己的 arguments 对象,但可以访问父级作用域中的 arguments 对象。

3.实例化(Instantiation):

  • 箭头函数不能用作构造函数,不能使用 new 关键字实例化。因此,它们没有自己的 prototype 对象。
  • 箭头函数没有 prototype 属性,无法通过 ArrowFunction.prototype 访问。

4.箭头函数的特性和用途:

  • 箭头函数通常用于简化函数表达式的语法,使代码更加简洁易读。
  • 由于箭头函数的作用域行为不同于普通函数,它们在处理 this 的问题上具有优势,避免了在回调函数等情况下常见的 this 绑定问题。
  • 箭头函数没有自己的 arguments 对象,可以通过扩展操作符或使用默认参数来传递参数。
  • 箭头函数不能用于定义对象的方法,因为它们没有自己的 this 值,无法正确引用对象本身。

2.TypeScript 相关(接口、枚举、声明)

TypeScript是一种由微软开发的开源编程语言,它是JavaScript的超集,意味着所有的JavaScript代码都是有效的TypeScript代码。TypeScript通过引入静态类型系统和其他新的语言特性,为JavaScript添加了一些额外的功能和增强。

以下是TypeScript中的一些重要特性和概念的介绍:

1.静态类型系统:

TypeScript引入了静态类型系统,允许开发者在代码编写阶段指定变量、函数和对象的类型。这样可以在编译阶段捕获一些常见的错误,提供更好的代码自动补全、重构和调试体验。

2.接口(Interfaces):

接口是TypeScript的一个关键概念,用于定义对象的结构和类型。通过接口,可以定义对象必须具备的属性和方法,从而在编码过程中进行类型检查和约束。

interface Person {
  name: string;
  age: number;
  greet(): void;
}

function sayHello(person: Person) {
  console.log(`Hello, ${person.name}!`);
}

在上述代码中,我们定义了一个名为Person的接口,它包含name、age和greet属性。接着,我们使用该接口作为函数参数的类型进行类型检查。

3.枚举(Enums):

枚举允许我们定义一组具名的常量。使用枚举可以提高代码的可读性和可维护性,使代码中的特定值具有语义化。

enum Color {
  Red,
  Green,
  Blue,
}

let myColor: Color = Color.Green;

在上述代码中,我们定义了一个名为Color的枚举,它包含Red、Green和Blue三个常量。我们可以将枚举值赋给变量,以表示特定的颜色。

4.声明文件(Declaration Files):

声明文件用于描述已存在的JavaScript代码库的类型信息,以便在TypeScript项目中使用它们。通过声明文件,我们可以获得代码的类型检查、代码补全和文档化等好处。

声明文件通常使用.d.ts作为文件扩展名,并遵循特定的声明语法。
当使用TypeScript与现有的JavaScript库或框架进行交互时,可能需要创建声明文件来描述这些库的类型信息。以下是一个声明文件的简单示例:

假设有一个名为lodash的JavaScript库,它提供了各种实用的函数。为了在TypeScript中使用lodash,我们可以创建一个声明文件lodash.d.ts,其中包含有关lodash的类型信息。

// lodash.d.ts

declare function chunk<T>(array: T[], size: number): T[][];

declare function capitalize(str: string): string;

declare const version: string;

interface Dictionary<T> {
  [key: string]: T;
}

declare namespace _ {
  export {
    chunk,
    capitalize,
    version,
  };
  export default _;
}

在上述声明文件中,我们使用declare关键字来声明函数、变量和接口。下面是每个声明的说明:

  • declare function chunk(array: T[], size: number): T[ ][ ];: 声明了一个名为chunk的函数,接受一个泛型数组array和一个size参数,并返回一个二维数组。

  • declare function capitalize(str: string): string;: 声明了一个名为capitalize的函数,接受一个字符串参数str,并返回一个首字母大写的字符串。

  • declare const version: string;: 声明了一个名为version的常量,它的类型为字符串。

  • interface Dictionary {…}: 声明了一个名为Dictionary的接口,表示一个字符串索引的字典,其值为泛型类型T。

  • declare namespace _ {…}: 声明了一个命名空间_,用于导出lodash的函数和变量。export关键字用于导出声明。export default _用于导出默认导出。

通过这个声明文件,我们可以在TypeScript中使用lodash库,并获得类型检查和代码补全的好处。

import _ from 'lodash';

const numbers = [1, 2, 3, 4, 5];
const chunks = _.chunk(numbers, 2); // 类型检查:chunks 是 number[][] 类型
const capitalized = _.capitalize('hello'); // 类型检查:capitalized 是 string 类型

console.log(_.version); // 类型检查:version 是 string 类型

const dictionary: _.Dictionary<number> = {
  a: 1,
  b: 2,
};

通过声明文件,TypeScript能够了解lodash库的函数签名、参数类型和返回值类型,以及其他相关的类型信息,从而提供更好的开发体验和代码安全性。

5.类

类(Classes):类是一种面向对象编程的基本概念,它允许我们创建具有属性和方法的对象。在TypeScript中,可以使用class关键字定义一个类。

class Person {
  private name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHello() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

const person = new Person('Alice');
person.sayHello(); // 输出:Hello, my name is Alice.

在上述代码中,我们定义了一个名为Person的类,它具有一个私有属性name和一个公共方法sayHello。我们可以使用new关键字创建Person类的实例,并调用其方法。

6.泛型(Generics):

泛型允许我们编写能够处理多种类型的可重用代码。通过使用泛型,我们可以在编写函数、类或接口时将类型作为参数进行传递。

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

const result = identity<string>('Hello');
console.log(result); // 输出:Hello

在上述代码中,我们定义了一个名为identity的泛型函数,它接受一个参数value并返回该参数。我们在调用identity函数时明确指定了泛型类型为string。

7.模块化(Modules):

模块化是将代码划分为可重用、独立的部分的概念。在TypeScript中,可以使用模块来组织代码,并在需要时导出和导入模块中的功能。

// math.ts 模块
export function sum(a: number, b: number): number {
  return a + b;
}

// app.ts 模块
import { sum } from './math';

const result = sum(2, 3);
console.log(result); // 输出:5

在上述代码中,我们将代码划分为两个模块:math.ts和app.ts。在math.ts模块中,我们导出了一个函数sum。在app.ts模块中,我们使用import语句导入了sum函数,并调用它。

3.HTTP1.x 和 HTTP2.0 的区别

HTTP(Hypertext Transfer Protocol)是一种用于传输超文本的应用层协议。HTTP 1.0、HTTP 1.1 和 HTTP 2.0 是 HTTP 协议的不同版本,它们在性能、功能和协议特性上有一些区别。

1. HTTP 1.0:

  • 顺序传输:HTTP 1.0 是基于请求/响应模型的协议,每个请求需要等待前一个请求的响应返回才能进行下一次请求。
  • 短连接:每个请求和响应都使用独立的连接,完成后立即关闭连接。这意味着每个请求都需要建立新的 TCP 连接,带来较大的开销。
  • 无状态:HTTP 1.0 默认是无状态的,每个请求之间相互独立,服务器不会保留客户端请求的任何状态信息。

2. HTTP 1.1:

  • 持久连接:HTTP 1.1 引入了持久连接,允许多个请求和响应复用同一个连接。这减少了建立和关闭连接的开销,提高了性能。
  • 流水线化:HTTP 1.1 支持请求和响应的流水线化,允许在一个连接上同时发送多个请求,提高了请求的并发性。
  • 虚拟主机:HTTP 1.1 支持虚拟主机,允许多个域名共享同一个 IP 地址,提高了服务器资源的利用率。
  • 缓存机制:HTTP 1.1 引入了更强大的缓存机制,包括强缓存和协商缓存,减少了网络传输和服务器负载。

3. HTTP 2.0:

  • 多路复用:HTTP 2.0 使用二进制分帧层,支持在同一个连接上同时发送多个请求和响应,实现了请求和响应的多路复用,提高了并发性能。
  • 服务器推送:HTTP 2.0 支持服务器主动推送资源,服务器可以在客户端请求之前将相关资源发送给客户端,减少了额外的请求延迟。
  • 头部压缩:HTTP 2.0 使用首部压缩算法,减少了头部信息的大小,降低了网络传输的开销。
  • 二进制传输:HTTP 2.0 将传输数据分解为二进制帧,提高了传输的效率和可靠性。

HTTP 2.0 相对于 HTTP 1.x 版本在性能方面有了显著的提升,主要是通过多路复用、头部压缩和二进制传输等特性实现的。它可以更高效地利用网络资源,减少延迟和带宽消耗。HTTP 2.0 的引入改进了用户体验,提升了网站的性能和效率。

4.DNS 的工作流程

1.查找本地DNS缓存:

  • 浏览器首先会查找自己的本地DNS缓存,该缓存保存了最近解析的域名和对应的IP地址。
  • 如果在本地缓存中找到了对应的域名和IP地址,浏览器会直接使用该IP地址进行连接。

2.发送DNS查询请求:

  • 如果在本地缓存中没有找到对应的IP地址,浏览器会向操作系统发送DNS查询请求。
  • 操作系统会根据配置的DNS服务器信息,选择一个可用的DNS服务器进行查询。

3.与DNS服务器通信:

  • 操作系统将DNS查询请求发送给所选择的DNS服务器,使用UDP或TCP协议进行通信。
  • DNS服务器一般由Internet服务提供商(ISP)或其他网络设备提供。

4.迭代或递归查询:

  • DNS服务器接收到查询请求后,它可以执行两种类型的查询:迭代查询或递归查询。
  • 迭代查询:如果DNS服务器支持迭代查询,它会向其他DNS服务器发送进一步的查询请求,以获取最终的IP地址。这个过程中,DNS服务器从根域名服务器开始,依次向下查询,直到找到对应的IP地址。
  • 递归查询:如果DNS服务器不支持迭代查询,它会尝试在自己的缓存中查找域名对应的IP地址。如果找到,则返回结果;否则,它会向其他DNS服务器发送递归查询请求,并等待结果。

5.获取IP地址:

  • DNS服务器最终会找到域名对应的IP地址。
  • 它将IP地址作为响应发送回操作系统,并由操作系统传递给浏览器。

6.缓存DNS解析结果:

浏览器收到IP地址后,会将该解析结果缓存在本地,以备将来的访问使用。
这样在下一次访问相同的域名时,就可以直接使用缓存的IP地址,而无需进行DNS解析。

通过这些步骤,浏览器能够将域名解析为对应的IP地址,以便建立与服务器的连接。DNS解析过程中可能会涉及多个DNS服务器之间的交互,以找到最终的IP地址。

5.讲一下所知道的状态码,重点说一下 304

HTTP状态码是由HTTP协议定义的一组三位数字代码,用于表示客户端和服务器之间的通信状态。以下是常见的HTTP状态码及其含义:

1.状态码介绍

1.1xx(信息提示):

表示服务器已接收到请求,正在继续处理。

  • 100(继续):服务器已接收到请求的起始部分,客户端应该继续发送剩余的请求。
  • 101(切换协议):服务器已根据客户端的请求切换协议。

2.2xx(成功):

表示请求已成功被服务器接收、理解和处理。

  • 200(成功):请求成功,并返回相应的结果。
  • 201(已创建):请求成功并在服务器上创建了新的资源。
  • 204(无内容):服务器已成功处理请求,但不需要返回任何实体内容。

3.3xx(重定向):

表示需要进一步操作来完成请求。

  • 301(永久重定向):请求的资源已永久移动到新的URL。
  • 302(临时重定向):请求的资源暂时移动到新的URL。
  • 304(未修改):请求的资源未修改,可以使用缓存的版本。

4.4xx(客户端错误):

表示客户端发送的请求有错误。

  • 400(错误请求):请求有语法错误或无法被服务器理解。
  • 401(未授权):请求需要身份验证。
  • 404(未找到):请求的资源不存在。

5.5xx(服务器错误):

表示服务器在处理请求时发生了错误。

  • 500(服务器内部错误):服务器遇到了意外的错误,无法完成请求。
  • 503(服务不可用):服务器暂时无法处理请求,通常是由于过载或维护。
  • 504 (网关超时):用于指示在充当网关或代理的服务器等待上游服务器的响应时发生超时。

这些HTTP状态码帮助客户端和服务器之间进行准确的通信,使得在请求和响应过程中能够传递必要的信息和指示。

2.304状态码介绍

HTTP 304状态码是HTTP协议中的一个状态码,表示"未修改"(Not Modified)。它是作为服务器响应的一部分返回给客户端,用于指示请求的资源自上次请求后未发生任何更改。

当客户端发送一个带有条件的GET请求(通常是带有If-Modified-Since或If-None-Match头部)时,服务器会检查请求的资源是否在给定条件下发生了修改。如果资源未发生修改,服务器会返回304状态码,告诉客户端可以使用其缓存的副本,并省略实际的响应体,以节省带宽和提高性能。

以下是一个示例的HTTP交互流程,其中涉及到HTTP 304状态码:

1.客户端发起带有条件的GET请求:

GET /example HTTP/1.1
Host: example.com
If-Modified-Since: Wed, 12 May 2023 10:00:00 GMT

2.服务器接收到请求并检查资源的修改情况。

3.如果资源未修改,服务器返回304状态码:

HTTP/1.1 304 Not Modified
Date: Thu, 18 May 2023 08:00:00 GMT

此响应不包含实际的响应体。

4.如果资源已修改,服务器返回200状态码,包含新的响应体。

HTTP/1.1 200 OK
Date: Thu, 18 May 2023 08:00:00 GMT
Content-Type: text/html
Content-Length: 1234

<html>
...
</html>

通过使用HTTP 304状态码,服务器可以减少网络传输的数据量,提高性能和效率。客户端可以利用缓存的副本,并且只在资源发生实际更改时才需要获取新的响应。

需要注意的是,使用HTTP 304状态码需要适当的缓存设置和正确的条件检查。客户端发送的条件请求头和服务器进行的资源修改比较是决定是否返回304的关键因素。同时,服务器响应中还可能包含其他缓存相关的头部,如Cache-Control和ETag,以帮助客户端进行缓存管理。

6.HTTP 缓存(强制缓存、协商缓存)

浏览器缓存策略可以分为强缓存和协商缓存。

  • 强缓存是指客户端向服务器发送的请求中包含了强缓存控制头,如“Cache-Control”和“Expires”等,服务器根据这些头中的信息来决定是否使用缓存数据。如果请求中包含了强缓存控制头,服务器将直接使用缓存数据,而不需要进行协商。

  • 协商缓存是指客户端向服务器发送的请求中不包含强缓存控制头,而是通过缓存协商的方式,由客户端和服务器之间进行协商,来确定是否使用缓存数据和缓存的有效期等信息。在协商缓存中,客户端会向服务器发送一个缓存请求,服务器根据请求中的信息来决定是否使用缓存数据,如果使用缓存数据,服务器也会返回一个缓存版本号给客户端,以便客户端判断是否需要更新缓存数据。

相比强缓存,协商缓存需要更多的网络交互和服务器资源,但是可以更好地利用缓存,避免缓存数据被重复使用,从而提高网站的性能和响应速度。

在实际应用中,强缓存和协商缓存可以结合使用,以实现更好的缓存控制效果。同时,需要注意缓存数据的安全和完整性,以避免缓存数据被篡改或失效。


强缓存:

服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行比较缓存策略。

  • 强缓存命中直接读取浏览器本地资源,在network中显示的是from memory 或者 from disk,如在微信小程序开发中,命中图片缓存,显示from disk。

  • 控制强缓存的字段有:Cache-Control(http1.1)和Expires(http1.0)

  • 协商缓存,让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,将缓存信息中的Etag和Last-Modified
    通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。

  • Cache-control是一个相对时间,用以表达自上次请求正确的资源之后的多少秒的时间段内缓存有效。

  • Expires是一个绝对时间。用以表达在这个时间点之前发起请求可以直接从浏览器中读取数据,而无需发起请求

  • Cache-Control的优先级比Expires的优先级高。前者的出现是为了解决Expires在浏览器时间被手动更改导致缓存判断错误的问题。
    如果同时存在则使用Cache-control。

强缓存-expires:

  • 优势特点
    1、HTTP 1.0 产物,可以在HTTP 1.0和1.1中使用,简单易用。
    2、以时刻标识失效时间。
  • 劣势问题
    1、时间是由服务器发送的(UTC),如果服务器时间和客户端时间存在不一致,可能会出现问题。
    2、存在版本问题,到期之前的修改客户端是不可知的。
    HTTP缓存都是从第二次请求开始的:

强缓存-cache-conche:

  • 优势特点
    1、HTTP 1.1 产物,以时间间隔标识失效时间,解决了Expires服务器和客户端相对时间的问题。
    2、比Expires多了很多选项设置。
  • 劣势问题
    1、存在版本问题,到期之前的修改客户端是不可知的。

协商缓存:

  • 协商缓存的状态码由服务器决策返回200或者304
  • 当浏览器的强缓存失效的时候或者请求头中设置了不走强缓存,并且在请求头中设置了If-Modified-Since 或者 If-None-Match 的时候,会将这两个属性值到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回 304 状态,加载浏览器缓存,并且响应头会设置 Last-Modified 或者 ETag 属性。
  • 对比缓存在请求数上和没有缓存是一致的,但如果是 304 的话,返回的仅仅是一个状态码而已,并没有实际的文件内容,因此 在响应体体积上的节省是它的优化点。
  • 协商缓存有 2 组字段(不是两个),控制协商缓存的字段有:Last-Modified/If-Modified-since(http1.0)和Etag/If-None-match(http1.1)
  • Last-Modified/If-Modified-since表示的是服务器的资源最后一次修改的时间;Etag/If-None-match表示的是服务器资源的唯一标
    识,只要资源变化,Etag就会重新生成。
  • Etag/If-None-match的优先级比Last-Modified/If-Modified-since高。

协商缓存-协商缓存-Last-Modified/If-Modified-since

  • 1.服务器通过 Last-Modified 字段告知客户端,资源最后一次被修改的时间,例如 Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
  • 2.浏览器将这个值和内容一起记录在缓存数据库中。
  • 3.下一次请求相同资源时时,浏览器从自己的缓存中找出“不确定是否过期的”缓存。因此在请求头中将上次的 Last-Modified 的值写入到请求头的 If-Modified-Since 字段
  • 4.服务器会将 If-Modified-Since 的值与 Last-Modified 字段进行对比。如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据。

优势特点

  • 1、不存在版本问题,每次请求都会去服务器进行校验。服务器对比最后修改时间如果相同则返回304,不同返回200以及资源内容。
    劣势问题
    1、只要资源修改,无论内容是否发生实质性的变化,都会将该资源返回客户端。例如周期性重写,这种情况下该资源包含的数据实际上一样的。
  • 2、以时刻作为标识,无法识别一秒内进行多次修改的情况。 如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒。
  • 3、某些服务器不能精确的得到文件的最后修改时间。
  • 4.、如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用。

协商缓存-Etag/If-None-match

为了解决上述问题,出现了一组新的字段 Etag 和 If-None-Match

Etag 存储的是文件的特殊标识(一般都是 hash 生成的),服务器存储着文件的 Etag 字段。之后的流程和 Last-Modified 一致,只是 Last-Modified 字段和它所表示的更新时间改变成了 Etag 字段和它所表示的文件 hash,把 If-Modified-Since 变成了 If-None-Match。服务器同样进行比较,命中返回 304, 不命中返回新资源和 200。

浏览器在发起请求时,服务器返回在Response header中返回请求资源的唯一标识。在下一次请求时,会将上一次返回的Etag值赋值给If-No-Matched并添加在Request Header中。服务器将浏览器传来的if-no-matched跟自己的本地的资源的ETag做对比,如果匹配,则返回304通知浏览器读取本地缓存,否则返回200和更新后的资源。
Etag 的优先级高于 Last-Modified。

优势特点

  • 1、可以更加精确的判断资源是否被修改,可以识别一秒内多次修改的情况。
  • 2、不存在版本问题,每次请求都回去服务器进行校验。
    劣势问题
  • 1、计算ETag值需要性能损耗。
  • 2、分布式服务器存储的情况下,计算ETag的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证时现ETag不匹配的情况。

第一次请求资源时,服务器返回资源,并在response header中回传资源的缓存策略;

第二次请求时,浏览器判断这些请求参数,击中强缓存就直接200,否则就把请求参数加到request header头中传给服务器,看是否击中协商缓存,击中则返回304,否则服务器会返回新的资源。

7.给一段 Lodash 英文文档,三分钟看完,然后解释 API 的作用

Lodash介绍

Lodash是一个流行的JavaScript实用工具库,提供了许多函数和方法来简化和增强JavaScript编程。它具有广泛的功能,包括集合操作、函数式编程、对象操作、数组操作、字符串处理、类型检查、数学运算等。Lodash被广泛用于前端开发和Node.js环境中。

以下是Lodash库的一些主要特点和功能:

1.函数式编程支持:

Lodash提供了一系列的函数式编程工具,如高阶函数(map、filter、reduce等)和函数组合,使代码更简洁、可读性更高。

2.集合操作:

Lodash提供了对数组和对象的各种集合操作,如排序、过滤、映射、合并、查找等。这些操作可以更轻松地处理集合数据,减少编写循环的代码量。

3.对象操作:

Lodash提供了丰富的对象操作方法,如克隆、合并、深度比较、查找、属性获取和设置等。这些方法使得操作对象变得更加简单和灵活。

4.数组操作:

Lodash提供了对数组的处理方法,如去重、切片、填充、查找、洗牌等。这些方法能够帮助开发人员更方便地处理和转换数组。

5.字符串处理:

Lodash提供了字符串相关的方法,如截取、填充、大小写转换、字符串拼接、格式化等。这些方法可以更方便地进行字符串操作和处理。

6.类型检查:

Lodash提供了一系列的类型检查工具,如检查对象类型、数组类型、函数类型、字符串类型等,有助于开发人员编写更健壮和可靠的代码。

7.工具函数:

Lodash还提供了许多实用的工具函数,如节流函数、深度克隆、异步函数处理、迭代器等。这些工具函数可以解决一些常见的编程问题,并提高开发效率。

总的来说,Lodash是一个功能丰富、易于使用的JavaScript实用工具库,它提供了许多函数和方法来简化常见的编程任务。通过使用Lodash,开发人员可以更轻松地处理集合、对象、数组、字符串等,并借助其提供的函数式编程工具编写更简洁和可读性更高的代码。


第二面


一半以上时间都在手写代码,中间夹杂着一些基础的问题。

1.React 生命周期

在React中,组件的生命周期表示组件在不同阶段的状态和行为。React 16.3之前,类组件使用生命周期方法来处理组件的各个阶段。自React 16.3开始,推荐使用函数组件和React钩子函数来管理组件的生命周期。

1.类组件的生命周期函数和作用:

  • componentDidMount(): 在组件挂载后调用,可以进行初始化操作,例如发起网络请求、订阅事件等。

  • componentDidUpdate(prevProps, prevState): 在组件更新后调用,可以对变化前后的props和state进行比较,执行相应的操作。

  • componentWillUnmount(): 在组件即将卸载前调用,可以进行清理操作,例如取消订阅、清除定时器等。

  • componentDidCatch(error, info): 在组件的子组件抛出错误时调用,可以用于捕获并处理错误。

这些生命周期函数允许开发人员在组件不同的生命周期阶段执行相应的操作,例如获取数据、更新UI、处理错误等。类组件的生命周期函数提供了丰富的控制能力,但也会增加组件的复杂性。

2.函数组件的生命周期:

在函数组件中,可以使用React提供的钩子函数来模拟类组件的生命周期行为。以下是一些常用的钩子函数和它们的作用:

  • useEffect(): 在组件渲染后执行副作用操作,相当于类组件的- componentDidMount和componentDidUpdate的结合。可以处理数据获取、订阅事件、操作DOM等。

  • useLayoutEffect(): 在组件渲染后同步执行副作用操作,与useEffect()类似,但在DOM更新之前执行,可以处理依赖于DOM布局的操作。
    useEffect(() => {}, []): 使用空依赖数组,只在组件挂载和卸载时执行一次,相当于类组件的componentDidMount和componentWillUnmount。

  • useMemo(): 对某个值进行缓存,只在依赖项变化时重新计算。

  • useCallback(): 缓存回调函数,以便在依赖项变化时不会重新创建。
    使用钩子函数可以使函数组件具有类组件的生命周期功能,并且更加简洁和易于管理。

需要注意的是,React 16.3及之后的版本更推荐使用函数组件和钩子函数来编写组件,因为它们提供了更好的性能和代码可读性。然而,仍然可以继续使用类组件和生命周期方法,因为它们仍然是React的一部分,并且对于某些特定的情况仍然有用

3.部分函数组件生命周期函数使用

当使用React函数组件时,可以使用useLayoutEffect、useMemo和useCallback来实现一些常见的场景和优化。

1.useLayoutEffect的例子:

useLayoutEffect在组件渲染后立即同步执行副作用操作,但在DOM更新之前。它类似于useEffect,但更适合处理需要访问DOM布局的操作。

import React, { useLayoutEffect, useRef } from 'react';

function Component() {
  const divRef = useRef();

  useLayoutEffect(() => {
    const { current } = divRef;
    // 访问DOM布局,例如获取元素的尺寸、位置等
    const width = current.offsetWidth;
    const height = current.offsetHeight;
    // 执行其他副作用操作
    // ...

    // 在此处可以进行一些DOM相关的操作,例如动画、样式修改等
    current.style.backgroundColor = 'red';
  }, []);

  return <div ref={divRef}>Example</div>;
}

在上述示例中,通过useLayoutEffect可以在组件渲染后立即获取DOM元素的尺寸,并执行其他相关的副作用操作。然后可以根据获取到的信息对DOM进行修改或其他操作。

2.useMemo的例子:

useMemo用于对某个值进行缓存,并在依赖项变化时重新计算。它可以避免重复计算,提高性能。

import React, { useMemo } from 'react';

function Component({ a, b }) {
  const result = useMemo(() => {
    // 根据a和b的值计算结果
    return a + b;
  }, [a, b]);

  return <div>{result}</div>;
}

在上述示例中,useMemo会根据a和b的值计算结果,并将结果缓存起来。只有在a或b发生变化时,才会重新计算结果。这可以避免不必要的计算开销。

3.useCallback的例子:
useCallback用于缓存回调函数,以便在依赖项变化时不会重新创建新的回调函数实例。这对于将回调函数作为props传递给子组件时很有用。

import React, { useCallback } from 'react';

function Component({ onClick }) {
  const handleClick = useCallback(() => {
    // 执行点击处理逻辑
    onClick();
  }, [onClick]);

  return <button onClick={handleClick}>Click me</button>;
}

在上述示例中,useCallback用于缓存handleClick回调函数,并在onClick发生变化时才重新创建新的回调函数实例。这样可以确保子组件不会在不必要的情况下重新渲染。

2.函数式组件与类组件的区别

函数式组件和类组件是React中两种不同的组件编写方式,它们在语法和特性上有一些区别。

1.函数式组件(Functional Components):

  • 使用函数来定义组件,通常使用箭头函数的形式。

  • 没有自己的状态(state)和生命周期方法。

  • 使用React Hooks来处理组件的状态和副作用。

  • 组件本身是纯函数,接收props作为输入并返回渲染的内容。

  • 在React 16.8及之后的版本中引入了Hooks,使函数式组件可以拥有状态和其他特性,例如useState、useEffect等。

2.类组件(Class Components):

  • 使用ES6类来定义组件,继承自React.Component。

  • 可以有自己的状态(state)和生命周期方法,例如constructor、componentDidMount等。

  • 可以通过this.state来访问和修改组件的状态。

  • 通过this.props来访问父组件传递的属性。

  • 可以使用setState方法来更新组件的状态并触发重新渲染。

3.下面是函数式组件和类组件之间的一些区别:

  • 语法:函数式组件使用函数声明,而类组件使用类声明。

  • 状态管理:函数式组件使用Hooks来管理状态,通过useState等钩子函数- 来定义和更新状态。类组件通过this.state和this.setState来管理状态。

  • 生命周期:函数式组件没有自己的生命周期方法,而类组件可以使用生命- 周期方法(例如componentDidMount、componentDidUpdate等)来执行特定的操作。

  • 可读性和维护性:函数式组件通常更简洁、易于理解和维护,尤其在处理简单场景时更具优势。类组件在处理复杂场景和大型应用程序时提供了更多的控制和结构。

从React 16.8版本开始,引入了Hooks,使函数式组件具备了状态管理和其他特性,使得函数式组件成为首选编写组件的方式。函数式组件相对于类组件更加简洁、灵活,并且在性能方面也有所优化。然而,对于某些特定的情况和需求,类组件仍然是一种有效的选择。

3.前端可以优化的地方

前端性能优化可以从多个方面入手,以下是一些常见的优化方面:

1.网络优化:

  • 减少HTTP请求:合并和压缩文件、使用雪碧图、使用字体图标等来减少请求次数。
  • 使用缓存:使用浏览器缓存、服务器端缓存和CDN缓存来减少重复请求。
  • 延迟加载:延迟加载不必要的资源,例如图片懒加载、按需加载JS模块等。

2.代码优化:

  • 优化代码结构:尽量减少不必要的嵌套和重复代码,使代码更简洁和可维护。
  • 使用合适的数据结构和算法:选择适当的数据结构和算法来提高代码的执行效率。
  • 避免不必要的计算:避免重复计算和频繁操作DOM等。
  • 使用节流和防抖:对于高频触发的事件,使用节流和防抖来限制触发次数。

3.图片和多媒体优化:

  • 图片压缩:使用合适的图片格式(如WebP、JPEG2000)和工具进行压缩。
  • 响应式图片:根据不同设备和屏幕大小提供适当大小的图片。
  • 视频和音频压缩:使用适当的编码和压缩设置来减小多媒体文件的大小。

4.页面渲染优化:

  • 减少重绘和回流:避免频繁操作DOM和样式,使用CSS动画替代JS动画。
  • 懒加载:将页面分成多个模块,根据需要延迟加载。
  • 使用虚拟列表和虚拟DOM:针对大型列表或频繁更新的情况,使用虚拟列表和虚拟DOM来减少渲染开销。

5.移动端优化:

  • 移动端适配:使用响应式布局或媒体查询来适应不同的屏幕尺寸。
  • 触摸事件优化:使用合适的触摸事件,避免滚动阻塞和触摸延迟。
  • 移动端特定的API使用:使用合适的API(如Web Workers、Service Workers)来提高性能和体验。

6.编译和打包优化:

  • 代码分割:将代码分割成多个模块,按需加载,减少初始加载时间。
  • Tree Shaking:通过静态分析删除未使用的代码,减小打包文件大小。
  • 压缩和混淆:使用压缩和混淆工具来减小文件大小和保护源代码。

7.首屏优化

1.压缩和缩小资源大小:

  • 压缩CSS、JavaScript和HTML:使用压缩工具(如UglifyJS、Terser、CSSNano等)来减小文件大小。
  • 图片压缩:使用适当的压缩工具和格式来减小图片大小。
  • 字体子集化:根据页面所需字符子集来生成字体文件,减小字体文件大小。

2.按需加载:

  • 代码分割:将应用程序拆分为多个模块,按需加载不同页面或功能模块。
  • 懒加载:延迟加载非关键内容,例如图片、视频、下拉菜单等,以减少首屏加载时间。
  • 路由懒加载:针对SPA(单页应用)使用路由懒加载,只在需要时加载相关组件和资源。

3.预加载关键资源:

  • 使用<link rel="preload">标签:提前加载关键资源,例如字体、CSS、JavaScript文件等。
  • 使用<link rel="prefetch"><link rel="prerender">标签:在空闲时间提前加载将来可能需要的资源。

4.缓存:

  • HTTP缓存:设置适当的缓存头(如Cache-Control、Expires),以减少对服务器的请求。
  • 浏览器缓存:使用Service Worker或Application Cache来缓存首屏所需的资源。

5.首屏关键资源优先加载:

  • 将关键CSS内联:将关键的CSS样式直接内联到HTML文档中,减少额外的请求和渲染等待时间。
  • 异步加载JavaScript:将非关键的JavaScript脚本设置为异步加载,以避免阻塞首屏渲染。

6.优化渲染性能:

  • 减少DOM元素数量:简化HTML结构,避免不必要的嵌套和冗余的DOM节点。
  • 使用CSS动画或GPU加速:使用CSS过渡和动画,避免使用JavaScript实现动画效果。
  • 减少重绘和回流:避免频繁修改DOM元素的样式和布局,合并多个操作,以减少页面重新渲染的次数。

4.分页组件(函数组件)

当使用React函数组件实现分页组件时,可以利用React Hooks来管理组件状态和生命周期。下面是一个使用React函数组件和Hooks实现的简单分页组件示例:

import React, { useState } from 'react';

const Pagination = ({ totalPages, onPageChange }) => {
  const [currentPage, setCurrentPage] = useState(1);

  const handlePageChange = (page) => {
    setCurrentPage(page);
    onPageChange(page);
  };

  return (
    <div>
      <button
        disabled={currentPage === 1}
        onClick={() => handlePageChange(currentPage - 1)}
      >
        Prev
      </button>
      {[...Array(totalPages).keys()].map((page) => (
        <button
          key={page}
          disabled={currentPage === page + 1}
          onClick={() => handlePageChange(page + 1)}
        >
          {page + 1}
        </button>
      ))}
      <button
        disabled={currentPage === totalPages}
        onClick={() => handlePageChange(currentPage + 1)}
      >
        Next
      </button>
    </div>
  );
};

export default Pagination;

在上面的示例中,我们定义了一个名为Pagination的函数组件,它接受两个props:totalPages表示总页数,onPageChange是一个回调函数,在页码变化时被调用。

组件内部使用useState Hook来管理当前页码的状态。handlePageChange函数用于处理页码变化事件,它更新当前页码状态并调用onPageChange回调函数。

在组件的返回部分,我们使用了按钮元素来表示页码按钮和上一页/下一页按钮。通过遍历生成页码按钮,我们使用currentPage状态来判断是否禁用当前页码按钮。

使用这个Pagination组件时,你可以将它放置在其他的React组件中,并传递合适的totalPages和onPageChange参数,以便实现自定义的分页逻辑。

import React, { useEffect } from 'react';
import Pagination from './Pagination';

const MyComponent = () => {
  const handlePageChange = (page) => {
    // 处理页码变化逻辑,例如根据页码获取对应的数据并更新组件状态
    console.log('Switch to page:', page);
  };

  useEffect(() => {
    // 初始化数据或其他操作
    // ...
  }, []);

  return (
    <div>
      {/* 其他组件内容 */}
      <Pagination totalPages={5} onPageChange={handlePageChange} />
      {/* 其他组件内容 */}
    </div>
  );
};

export default MyComponent;

在上述示例中,我们在MyComponent组件中使用了Pagination组件,并传递了totalPages和onPageChange参数。在onPageChange回调函数中,你可以实现自定义的逻辑,例如根据页码获取对应的数据并更新组件状态。

这是一个简单的使用React函数组件实现的分页组件示例,你可以根据自己的需求和项目结构进行相应的调整和扩展。

5.三数之和 (双指针,如何去除重复)

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

1.暴力循环

var threeSum = function(nums) {   
    const list = nums.sort((a,b) => a-b);
    let result = [];
    for (let i = 0; i<list.length; i++) {
        if (i>1 && list[i-1] === list[i]) {
            continue;
        }
        for (let j = i+1; j< list.length; j++) {
            for (let k = j+1; k<list.length; k++) {
                if (list[i] + list[j] + list[k] === 0) {
                    result.push([list[i],list[j],list[k]]);
                    break;
                }
            }
        }
    }
    result = Array.from(new Set(result.map(JSON.stringify)), JSON.parse);
    return result;
};

6.函数柯里化,实现下面的代码

函数柯里化的作用主要有以下几个方面:

1.参数复用:

柯里化可以将原本需要多个参数的函数转化为接收一个参数的函数序列。这样可以在后续的函数调用中复用已经传递的参数,而不需要重复传递相同的参数。

2.延迟执行:

柯里化允许我们逐步提供函数所需的参数,从而延迟函数的执行。这对于需要等待特定条件满足或需要处理异步操作的情况非常有用。

3.创建更专用的函数(提前确认):

通过柯里化,我们可以基于已有的函数创建更具体、更专用的函数。通过提供部分参数,我们可以生成针对特定场景或特定参数的函数,增加代码的可读性和可维护性。

函数柯里化(Currying)是一种将多个参数的函数转换为一系列单参数函数的过程。通过函数柯里化,可以将原本接收多个参数的函数转变为接收一个参数并返回一个新函数的形式。

函数柯里化的主要优势在于可以将函数的复用性和灵活性提高。它允许我们根据需要提供部分参数,延迟函数的执行,或者将柯里化函数作为参数传递给其他函数。

add函数示例

// 普通的add函数
function add(x, y) {
    return x + y
}
 
// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}
 
add(1, 2)           // 3
curryingAdd(1)(2)   // 3

实际上就是把add函数的x,y两个参数变成了先用一个函数接收x然后返回一个函数去处理y参数。现在思路应该就比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

1.参数复用

// 正常正则验证字符串 reg.test(txt)
 
// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}
 
check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true
 
// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}
 
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
 
hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false

上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。

2.提前确认

提前确认(Pre-filling)是柯里化的一种变体,它的作用是在函数柯里化的基础上,预先提供部分参数,生成一个新的函数。

通过提前确认,我们可以创建一个更加专用的函数,用于处理特定的场景或参数。这种方式可以降低函数的复杂性,提高函数的可读性和可维护性。

下面举个例子来说明提前确认的概念:

假设我们有一个函数 sendMessage,用于向指定的用户发送消息。这个函数接收三个参数:发送者、接收者和消息内容。现在我们想要创建一个用于发送给特定用户的消息的函数。

function sendMessage(sender, receiver, message) {
  console.log(`${sender} sent a message to ${receiver}: ${message}`);
}

const sendToUserA = sendMessage.bind(null, 'UserA'); // 提前确认 sender 参数
sendToUserA('Hello, UserA!'); // 只需传入剩余的参数

在上述例子中,我们使用 bind 方法对 sendMessage 函数进行提前确认,将第一个参数 sender 绑定为 ‘UserA’。这样,我们得到了一个新的函数 sendToUserA,它只需传入剩余的参数。当我们调用 sendToUserA(‘Hello, UserA!’) 时,它实际上等同于调用 sendMessage(‘UserA’, ‘UserA’, ‘Hello, UserA!’),只是第一个参数已经预先确定了。

通过提前确认,我们可以根据需要固定部分参数,创建更加专用的函数。这使得函数的用途更加明确,减少了参数的传递和复杂性,提高了代码的可读性和可维护性。

3.延迟执行

Function.prototype.bind = function (context) {
    var _this = this
    var args = Array.prototype.slice.call(arguments, 1)
 
    return function() {
        return _this.apply(context, args)
    }
}

4.通用柯里化函数及介绍

function progressCurrying(fn, args) {
 
    var _this = this
    var len = fn.length;
    var args = args || [];
 
    return function() {
        var _args = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, _args);
 
        // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数
        if (_args.length < len) {
            return progressCurrying.call(_this, fn, _args);
        }
 
        // 参数收集完毕,则执行fn
        return fn.apply(this, _args);
    }
}

这段代码实现了一个通用的柯里化函数 progressCurrying,它支持多参数传递,并逐步收集参数直到参数个数满足柯里化函数的要求。

下面是对代码中每个步骤的解释:

  • 1.首先,定义了 progressCurrying 函数,它接受两个参数:fn 是需要柯里化的函数,args 是已经收集的参数数组,默认为空数组。

  • 2.在函数内部,定义了变量 _this,用于存储函数执行时的 this 上下文。

  • 3.使用 fn.length 获取函数 fn 的参数个数,并存储在变量 len 中。

  • 4.如果参数 args 未提供,则将其默认设置为一个空数组。

  • 5.返回一个匿名函数作为柯里化后的函数。

  • 6.匿名函数内部,通过 Array.prototype.slice.call(arguments) 将传入的参数转换为真正的数组,存储在 _args 中。这样做是因为 arguments 对象并非真正的数组,使用 slice 方法可以将其转换为数组。

  • 7.使用 Array.prototype.push.apply(args, _args) 将 _args 中的参数添加到 args 数组中。这样做是为了收集所有传入的参数。

  • 8.如果 _args 的长度小于 len,说明参数个数仍然不足,继续递归调用 progressCurrying 函数,传入当前的函数 fn 和已收集的参数 _args。

  • 9.如果 _args 的长度等于 len,说明参数已经收集完毕,此时调用原始函数 fn,并使用 fn.apply(this, _args) 执行该函数,并返回执行结果。

总体而言,这个 progressCurrying 函数允许我们将一个多参数的函数转换为一个逐步收集参数的柯里化函数。通过逐步收集参数,我们可以更灵活地传递参数,延迟函数的执行,以及在参数不完整的情况下返回新的柯里化函数。

5.通用柯里化封装2

下面是一个通用的柯里化函数 curry 的实现,以及对每个步骤的解释:

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...moreArgs) {
        return curried.apply(this, args.concat(moreArgs));
      };
    }
  };
}
  • 1.函数 curry 接收一个函数 fn 作为参数,该函数需要被柯里化。

  • 2.在函数内部,定义了一个匿名函数 curried,它接收任意数量的参数 …args。

  • 3.判断 args 的长度是否大于等于 fn 的形参个数 fn.length。如果是,则说明参数已经足够,直接调用 fn 并传入参数 args,并返回结果。

  • 4.如果 args 的长度不够,说明还需要继续柯里化。返回一个新的匿名函数,该函数接收剩余的参数 …moreArgs。

  • 5.在新的匿名函数内部,使用 apply 方法将 args 和 moreArgs 合并为一个新的参数数组,并使用 curried 函数递归调用,传入新的参数数组。

  • 6.重复步骤3和步骤4,直到参数数量足够,调用原始函数 fn 并返回结果。

该通用柯里化函数的作用是将接收多个参数的函数转化为一个逐步接收参数的函数序列。它实现了参数的逐步收集和延迟执行的特性。

通过柯里化,我们可以更灵活地使用函数,根据需求逐步提供参数。这样可以实现参数复用、延迟执行和创建更专用的函数等优点。柯里化函数使代码更加模块化、可读性更高,并且提供了更好的函数组合性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值