前端杂学录(八)

1.HTTP请求报文和响应报文的具体组成,能理解常见请求头的含义,有几种请求方式,区别是什么

1. HTTP 请求报文的组成

一个 HTTP 请求报文通常包含以下几个部分:

1.1 请求行

  • 请求方法:指明请求的类型(如 GET、POST 等)。
  • 请求 URI:指定请求的资源地址。
  • HTTP 版本:标明使用的 HTTP 协议版本(如 HTTP/1.1)。
GET /index.html HTTP/1.1

1.2 请求头

请求头包含了多个字段,用于传递额外的信息。常见的请求头包括:

  • Host:请求的主机名,必须包含在 HTTP/1.1 请求中。
  • User-Agent:客户端的类型和版本信息。
  • Accept:告诉服务器客户端可接受的内容类型(如 text/htmlapplication/json)。
  • Content-Type:表示发送内容的类型,通常在 POST 请求中使用。
  • Authorization:用于身份验证的凭证。
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

1.3 空行

请求头与请求体之间有一个空行,表示请求头部分的结束。

1.4 请求体

请求体包含了要发送给服务器的数据,通常在 POST、PUT 等方法中使用。GET 方法通常没有请求体。

name=John&age=30

2. HTTP 响应报文的组成

一个 HTTP 响应报文同样包含几个部分:

2.1 响应行

  • HTTP 版本:使用的 HTTP 协议版本。
  • 状态码:表示请求的处理结果(如 200、404 等)。
  • 状态消息:对状态码的简短描述。
HTTP/1.1 200 OK

2.2 响应头

响应头同样包含多个字段,用于传递额外的信息。常见的响应头包括:

  • Content-Type:返回内容的类型。
  • Content-Length:返回内容的字节数。
  • Set-Cookie:设置客户端的 cookie。
  • Cache-Control:缓存控制指令。
Content-Type: text/html; charset=UTF-8
Content-Length: 1234

2.3 空行

响应头与响应体之间有一个空行,表示响应头部分的结束。

2.4 响应体

响应体包含服务器返回的数据(如 HTML 页面、JSON 数据等)。

示例:

<html>...</html>

3. 常见请求方式及其区别

HTTP 支持多种请求方法,主要包括:

3.1 GET

  • 作用:从服务器获取资源。
  • 特点
    • 请求参数通常通过 URL 传递。
    • 无请求体。
    • 数据在 URL 中可见,适合获取数据,且可被缓存。

3.2 POST

  • 作用:向服务器提交数据,通常用于创建新资源。
  • 特点
    • 请求参数通过请求体传递。
    • 数据在请求体中,不易被缓存。
    • 适合上传文件或提交表单。

3.3 PUT

  • 作用:向服务器上传数据,用于更新已存在的资源。
  • 特点
    • 请求参数通过请求体传递。
    • 通常是幂等的(多次请求结果相同)。

3.4 DELETE

  • 作用:请求服务器删除指定的资源。
  • 特点
    • 通常是幂等的。

3.5 HEAD

  • 作用:类似于 GET,但只请求响应头,不返回响应体。
  • 特点
    • 用于获取资源的元数据。

3.6 OPTIONS

  • 作用:请求服务器支持的 HTTP 方法。
  • 特点
    • 常用于跨域请求的预检。

2.HTTP所有状态码的具体含义,看到异常状态码能快速定位问题

1. 1xx:信息性状态码

这些状态码表示请求已被接收并正在处理。

  • 100 Continue:初始请求已接受,客户端可以继续发送请求的其余部分。
  • 101 Switching Protocols:服务器已理解客户端的请求,并将切换协议(如从 HTTP/1.1 切换到 WebSocket)。

2. 2xx:成功状态码

这些状态码表示请求已成功处理。

  • 200 OK:请求成功,服务器返回所请求的资源。
  • 201 Created:请求成功并创建了新资源,通常在 POST 请求后返回。
  • 202 Accepted:请求已接受,但尚未处理,通常用于异步处理。
  • 204 No Content:请求成功,但没有返回内容,常用于 DELETE 请求。

3. 3xx:重定向状态码

这些状态码表示请求需要进一步操作才能完成。

  • 300 Multiple Choices:请求的资源有多个选择,客户端可以选择其中之一。
  • 301 Moved Permanently:请求的资源已永久移动到新 URI,客户端应使用新 URI 进行后续请求。
  • 302 Found:请求的资源临时被移动到新 URI,客户端应继续使用原 URI。
  • 303 See Other:请求应使用 GET 方法访问另一个 URI,以获取响应。
  • 304 Not Modified:资源未被修改,客户端可以使用缓存的版本。
  • 307 Temporary Redirect:请求的资源临时重定向到新 URI,客户端应使用原请求方法进行请求。
  • 308 Permanent Redirect:请求的资源永久重定向到新 URI,客户端应使用原请求方法进行请求。

4. 4xx:客户端错误状态码

这些状态码表示请求存在问题,导致服务器无法处理。

  • 400 Bad Request:请求无效,服务器无法理解。
  • 401 Unauthorized:未提供身份验证凭证或凭证无效,访问被拒绝。
  • 403 Forbidden:服务器理解请求,但拒绝执行,客户端没有访问权限。
  • 404 Not Found:请求的资源不存在,URI 无效。
  • 405 Method Not Allowed:请求方法不被允许,服务器不支持该方法。
  • 408 Request Timeout:请求超时,服务器未能在等待请求时收到有效数据。
  • 429 Too Many Requests:客户端在短时间内发送了过多请求,服务器拒绝处理。

5. 5xx:服务器错误状态码

这些状态码表示服务器在处理请求时发生了错误。

  • 500 Internal Server Error:服务器遇到意外情况,无法完成请求。
  • 501 Not Implemented:服务器不支持请求的功能,无法实现请求。
  • 502 Bad Gateway:作为网关或代理的服务器从上游服务器收到无效响应。
  • 503 Service Unavailable:服务器当前无法处理请求,通常因为过载或维护。
  • 504 Gateway Timeout:作为网关或代理的服务器未能在规定时间内从上游服务器获得响应。

6. 如何快速定位问题

当遇到异常状态码时,可以根据状态码的类别和具体含义快速定位问题:

  • 1xx 状态码:通常不是错误,表明请求正在处理。
  • 2xx 状态码:表示请求成功,通常不需要关注。
  • 3xx 状态码:检查 URI 是否正确,是否需要处理重定向。
  • 4xx 状态码:通常是客户端请求的问题,检查请求参数、身份验证和资源路径。
  • 5xx 状态码:表示服务器问题,通常需要检查服务器日志、配置或后端服务。

3.HTTP1.1、HTTP2.0带来的改变

1. HTTP/1.1 的特点

  • 文本协议:HTTP/1.1 是基于文本的协议,所有请求和响应都是以文本形式传输。
  • 连接管理:每个请求都需要建立一个独立的 TCP 连接,尽管支持持久连接(keep-alive),但仍然存在队头阻塞问题。
  • 请求/响应模型:每个请求都是独立的,客户端必须等待当前请求完成才能发送下一个请求。
  • 无压缩:HTTP/1.1 的头部信息未经过压缩,可能导致带宽浪费。
  • 支持缓存:通过缓存控制字段(如 Cache-Control)来管理缓存行为。

2. HTTP/2.0 的改变

2.1 二进制协议

  • 二进制分帧:HTTP/2.0 采用二进制格式,所有数据都被分帧传输。这使得协议更高效,易于解析。
  • 流和帧:HTTP/2.0 将数据分为不同的流(stream)和帧(frame),允许在同一连接上并行处理多个请求和响应。

2.2 多路复用

  • 并行请求:HTTP/2.0 允许在单一 TCP 连接上并行发送多个请求和响应,消除了 HTTP/1.1 中的队头阻塞问题。
  • 流优先级:可以为不同的流设置优先级,优化资源的分配和加载顺序。

2.3 头部压缩

  • HPACK 压缩:HTTP/2.0 使用 HPACK 算法对请求和响应头进行压缩,减少了传输的字节数,提高了带宽利用率。

2.4 服务器推送

  • 服务器推送:服务器可以主动推送资源到客户端,而不必等待客户端请求。这对于需要资源的页面非常有效,如 CSS 和 JavaScript 文件。

2.5 连接复用

  • 单一连接:HTTP/2.0 使用单一连接处理多个请求和响应,减少了连接建立和关闭的开销。

2.6 更好的流量控制

  • 流量控制:HTTP/2.0 允许更复杂的流量控制机制,以管理数据流的传输和接收,改善网络性能。

3. 总结

HTTP/2.0 在多个方面对 HTTP/1.1 进行了显著改进,主要包括:

  • 从文本协议转变为二进制协议,提高了解析效率。
  • 引入多路复用,允许并行请求,消除了队头阻塞。
  • 采用头部压缩,减少了带宽消耗。
  • 实现服务器推送,提高了资源加载效率。
  • 简化连接管理,使用单一连接处理多个请求。

4.HTTPS的加密原理,如何开启HTTPS,如何劫持HTTPS请求

1. HTTPS 的加密原理

1.1 SSL/TLS 协议

HTTPS 基于 SSL(安全套接层)或 TLS(传输层安全协议)协议工作。其主要功能包括:

  • 数据加密:使用对称加密和非对称加密技术加密数据,确保数据在传输过程中不被窃听。
  • 身份验证:通过数字证书验证服务器的身份,防止中间人攻击。
  • 数据完整性:使用消息摘要算法(如 SHA-256)确保数据在传输过程中未被篡改。

1.2 加密过程

  1. 建立连接

    • 当客户端(如浏览器)向服务器发起 HTTPS 请求时,首先会建立一个 SSL/TLS 连接。
  2. 握手过程

    • 客户端向服务器发送支持的 SSL/TLS 版本和加密算法。
    • 服务器选择合适的版本和算法,并将其数字证书发送给客户端。
    • 客户端验证证书的有效性(如检查证书链和颁发者)。
    • 客户端生成一个随机数(称为预主密钥),并用服务器的公钥加密后发送给服务器。
    • 服务器使用私钥解密预主密钥。
  3. 生成会话密钥

    • 客户端和服务器使用预主密钥生成对称加密的会话密钥。
  4. 加密通信

    • 使用会话密钥加密后续的数据传输。

2. 如何开启 HTTPS

要在网站上启用 HTTPS,需要遵循以下步骤:

2.1 获取 SSL/TLS 证书

  • 选择证书颁发机构(CA):选择一个可信的证书颁发机构(如 Let’s Encrypt、Comodo、DigiCert 等)。
  • 申请证书:根据 CA 的要求生成证书签名请求(CSR),并提交申请。CA 会验证你的身份后颁发证书。

2.2 安装证书

  • 服务器配置:将获得的 SSL/TLS 证书安装到你的 Web 服务器上。不同的服务器(如 Apache、Nginx、IIS 等)有不同的安装步骤。
  • 配置 HTTPS:修改服务器配置文件,启用 HTTPS,指定证书和私钥的路径。

2.3 重定向 HTTP 到 HTTPS

  • 配置重定向:在服务器上设置 HTTP 请求自动重定向到 HTTPS,以确保所有流量都是安全的。

2.4 测试 HTTPS

  • 检查配置:使用在线工具(如 SSL Labs)检查 SSL/TLS 配置的安全性,确保没有漏洞。

3. 如何劫持 HTTPS 请求

尽管 HTTPS 提供了安全保障,但在一些情况下,攻击者仍可能尝试劫持 HTTPS 请求。以下是一些常见的攻击方法:

3.1 中间人攻击(MITM)

  • 伪造证书:攻击者可能会伪造证书,冒充合法服务器。用户访问时可能无法察觉,尤其是如果用户未仔细检查证书的有效性。
  • DNS 劫持:攻击者可能通过 DNS 劫持将用户请求重定向到恶意服务器,尽管连接是 HTTPS,但仍然可能受到攻击。

3.2 SSL 剥离攻击

  • SSL 剥离:攻击者使客户端与服务器之间的 HTTPS 连接被降级为 HTTP 连接,用户在不知情的情况下输入敏感信息。此攻击通常利用用户访问不安全的 HTTP 页面后,再转向安全的 HTTPS 页面。

3.3 恶意软件

  • 安装恶意软件:通过恶意软件,攻击者可以获取用户的 HTTPS 流量,尽管数据在传输过程中是加密的,但如果恶意软件在设备上运行,用户的信息仍然会被窃取。

5.理解WebSocket协议的底层原理、与HTTP的区别

1. WebSocket 协议的底层原理

1.1 建立连接

  • 握手过程

    • WebSocket 连接始于一个 HTTP 请求,以升级请求(Upgrade Request)的形式发送。
    • 客户端向服务器发送一个 HTTP 请求,要求将连接升级到 WebSocket 协议。请求中包含特定的头部字段,如 Upgrade: websocketConnection: Upgrade

    示例请求:

    GET /chat HTTP/1.1
    Host: example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Sec-WebSocket-Version: 13
    
  • 服务器响应

    • 如果服务器支持 WebSocket,它会回复一个 101 Switching Protocols 响应,表示连接已成功升级。
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: [生成的接收密钥]
    

1.2 数据帧结构

  • 数据帧:WebSocket 数据通过“帧”进行传输。每个帧包含一个标头和有效载荷,标头包括信息如数据类型(文本、二进制)、帧长度等。

  • 帧类型

    • 文本帧:用于传输文本数据(UTF-8 编码)。
    • 二进制帧:用于传输二进制数据。
    • 控制帧:用于管理连接(如关闭连接、Ping/Pong 心跳机制)。

1.3 持久连接

  • 全双工通信:一旦 WebSocket 连接建立,客户端和服务器可以随时发送消息,而无需重新建立连接。这种持久连接极大地减少了延迟,提高了实时性。

  • 心跳机制:为了保持连接的活跃性,WebSocket 通常使用 Ping/Pong 帧,客户端和服务器会定期发送 Ping 帧以确认连接仍然有效。

2. WebSocket 与 HTTP 的区别

特性HTTPWebSocket
连接类型无状态的请求-响应模型持久的全双工连接
建立连接每个请求都需要建立和关闭连接仅在初始握手时建立一次连接
数据传输单向请求-响应双向实时传输
数据格式文本格式二进制和文本格式
性能每次请求都需重新传输 HTTP 头部传输数据时只需较小的帧头
适用场景适合请求-响应的应用适合实时应用(如聊天、游戏)

6.多种方式实现数组去重、扁平化、对比优缺点

1. 数组去重

1.1 使用 Set

const uniqueArray = [...new Set(array)];
优点
  • 简洁明了。
  • 性能较好,时间复杂度为 O(n)。
缺点
  • 不支持对象的深度去重(只对基本数据类型有效)。

1.2 使用 filterindexOf

const uniqueArray = array.filter((item, index) => array.indexOf(item) === index);
优点
  • 语法简单,易读。
缺点
  • 性能较差,时间复杂度为 O(n^2),对于大数组效率低下。

1.3 使用 reduce

const uniqueArray = array.reduce((acc, item) => {
  if (!acc.includes(item)) {
    acc.push(item);
  }
  return acc;
}, []);
优点
  • 灵活,适合复杂的去重逻辑。
缺点
  • 性能较差,时间复杂度为 O(n^2)。

1.4 使用对象或 Map

const uniqueArray = Object.keys(array.reduce((acc, item) => {
  acc[item] = true;
  return acc;
}, {}));
优点
  • 性能较好,时间复杂度为 O(n)。
缺点
  • 只对基本数据类型有效,不支持对象深度去重。

2. 数组扁平化

2.1 使用 flat 方法(ES2019)

const flatArray = array.flat();
优点
  • 语法简单,直接。
  • 支持指定层数的扁平化。
缺点
  • 仅在 ES2019 及以上版本可用,兼容性问题。

2.2 使用 reduce 递归

const flatten = (arr) => arr.reduce((acc, item) =>
  Array.isArray(item) ? acc.concat(flatten(item)) : acc.concat(item), []);
优点
  • 可以实现深度扁平化。
缺点
  • 代码较长,可能影响可读性。
  • 在深层嵌套数组时可能导致栈溢出。

2.3 使用 forEach 递归

const flatten = (arr) => {
  const result = [];
  arr.forEach(item => {
    if (Array.isArray(item)) {
      result.push(...flatten(item));
    } else {
      result.push(item);
    }
  });
  return result;
};
优点
  • 代码清晰,易于理解。
缺点
  • 在深层嵌套数组时可能导致栈溢出。

2.4 使用 concatapply

const flatten = (arr) => [].concat.apply([], arr);
优点
  • 简洁,适合一层的扁平化。
缺点
  • 仅适合一层,无法实现深度扁平化。
  • 性能较差,尤其在大数组中。

3. 总结

数组去重

  • Set对象/Map 方法 性能较好,语法简洁。
  • filterreduce 方法代码简单,但性能较差,适合小数组。

数组扁平化

  • flat 方法 是最简单的选择,但兼容性较差。
  • 递归方法(reduce 和 forEach)适合深度扁平化,但在深层嵌套时可能导致性能问题。
  • concat 和 apply 方法适合一层扁平化,代码简洁但性能较差。

7.JSON.parse和JSON.stringify

JSON.parseJSON.stringify 是 JavaScript 中用于处理 JSON 数据的两个内置函数。

JSON.parse

JSON.parse() 函数用于将 JSON 字符串转换为 JavaScript 对象。

语法:

JSON.parse(text[, reviver])
  • text: 一个符合 JSON 格式的字符串。
  • reviver(可选): 一个函数,它可以在 JSON 字符串被转换为 JavaScript 对象后,对结果进行后处理。这个函数接受两个参数:属性的键和值。

示例:

const jsonString = '{"name": "Kimi", "age": 30}';
const obj = JSON.parse(jsonString);
console.log(obj.name); // 输出: Kimi

如果提供了 reviver 函数,它会对解析后的对象中的每个键值对进行处理:

const jsonString = '{"name": "Kimi", "age": 30}';
const obj = JSON.parse(jsonString, (key, value) => {
  if (key === "age") {
    return value + 10; // 修改年龄
  }
  return value;
});
console.log(obj.age); // 输出: 40

JSON.stringify

JSON.stringify() 函数用于将 JavaScript 对象转换为 JSON 字符串。

语法:

JSON.stringify(value[, replacer[, space]])
  • value: 要转换为 JSON 字符串的 JavaScript 值。
  • replacer(可选): 一个函数或数组,用于过滤 JSON 中的值。
    • 函数接受两个参数:属性的键和值,并返回一个值,该值被转换为 JSON 字符串。
    • 数组包含要包含在 JSON 字符串中的属性名。
  • space(可选): 一个数字或字符串,用于美化输出,即缩进。

示例:

const obj = { name: "Kimi", age: 30 };
const jsonString = JSON.stringify(obj);
console.log(jsonString); // 输出: {"name":"Kimi","age":30}

如果提供了 replacer 函数,它会决定哪些属性/对象应该被包含在 JSON 字符串中:

const obj = { name: "Kimi", age: 30, city: "Shanghai" };
const jsonString = JSON.stringify(obj, (key, value) => {
  if (key === "city") {
    return undefined; // 从 JSON 字符串中排除 'city'
  }
  return value;
});
console.log(jsonString); // 输出: {"name":"Kimi","age":30}

如果提供了 space 参数,可以美化输出:

const jsonString = JSON.stringify(obj, null, 2);
console.log(jsonString);
/*
输出:
{
  "name": "Kimi",
  "age": 30
}
*/

注意事项

  • JSON.parse 可以处理有效的 JSON 字符串,如果字符串不是有效的 JSON 格式,它会抛出 SyntaxError
  • JSON.stringify 只能序列化 JavaScript 的原生数据类型(如对象、数组、字符串、数字、布尔值等),不能序列化函数、undefined、循环引用等。
  • 通过 replacer 函数,你可以控制序列化过程,决定哪些属性被包含或排除,以及如何转换值。
  • space 参数不仅可以用于美化输出,还可以用于提高可读性或防止某些字符冲突。

8.多种方式实现深拷贝、对比优缺点

1. 使用 JSON 方法

代码示例

const deepCopy = (obj) => JSON.parse(JSON.stringify(obj));

优点

  • 简单易用:实现非常简单,适合快速应用。
  • 兼容性好:在大多数浏览器中均可使用。

缺点

  • 无法处理函数:会丢失对象中的方法。
  • 无法处理特殊对象:如 DateRegExpSetMap 等,均会被转化为普通对象。
  • 无法处理循环引用:会导致错误。

2. 使用递归

代码示例

const deepCopy = (obj) => {
  if (obj === null || typeof obj !== 'object') {
    return obj; // 基本类型直接返回
  }

  if (Array.isArray(obj)) {
    return obj.map(item => deepCopy(item)); // 处理数组
  }

  const copy = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepCopy(obj[key]); // 递归拷贝
    }
  }
  return copy;
};

优点

  • 灵活性高:可以处理多种数据类型,包括嵌套对象和数组。
  • 可以处理自定义对象:可以处理任何类型的对象。

缺点

  • 性能问题:对于深层嵌套的对象,可能导致性能下降。
  • 无法处理特殊对象:如 DateRegExpSetMap 等。

3. 使用 Lodash 库的 cloneDeep

代码示例

// 需要安装 lodash
const _ = require('lodash');
const deepCopy = (obj) => _.cloneDeep(obj);

优点

  • 功能强大:能够处理多种数据类型,包括函数、Date、RegExp、Set、Map 等。
  • 经过优化:性能相对较好,处理复杂对象时稳定。

缺点

  • 依赖第三方库:需要引入 Lodash,增加了项目的体积。
  • 学习曲线:对于不熟悉 Lodash 的开发者,可能需要时间去学习。

4. 使用 Object.assign(适合浅拷贝)

代码示例

const shallowCopy = (obj) => Object.assign({}, obj);

优点

  • 简单易用:语法简单,适合快速实现浅拷贝。

缺点

  • 仅适合浅拷贝:只拷贝第一层属性,嵌套对象仍为引用。
  • 无法处理数组及特殊对象:如果对象中有数组或特殊对象,无法正确深拷贝。

5. 使用 structuredClone(现代浏览器)

代码示例

const deepCopy = (obj) => structuredClone(obj);

优点

  • 内置函数:不需要引入库,直接使用。
  • 支持多种数据类型:可以处理 DateMapSetArrayBuffer 等。

缺点

  • 浏览器支持:目前仅在部分现代浏览器(如 Chrome 17+、Firefox 63+)中支持,可能存在兼容性问题。

6. 总结

适用场景和总结

  • JSON 方法:适合简单、无函数和特殊对象的场景,代码简洁。
  • 递归方法:灵活性高,适合需要处理复杂嵌套对象的情况。
  • Lodash 的 cloneDeep:适合需要深拷贝各种复杂类型对象的场景,功能强大。
  • structuredClone:适合现代浏览器中需要处理多种数据类型的场景。
  • Object.assign:仅适合浅拷贝,不适合深拷贝的场景。

9.手写函数柯里化工具函数、并理解其应用场景和优势

1. 手写柯里化函数

代码示例

function curry(fn) {
  // 获取函数的参数数量
  const arity = fn.length;

  // 内部递归函数实现柯里化
  function curried(...args) {
    // 如果传入的参数数量满足要求,调用原函数
    if (args.length >= arity) {
      return fn(...args);
    }
    // 否则,返回一个新函数,继续接收参数
    return (...next) => curried(...args, ...next);
  }

  return curried;
}

使用示例

// 一个简单的加法函数
function add(x, y, z) {
  return x + y + z;
}

// 使用柯里化工具函数
const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3));  // 6
console.log(curriedAdd(1, 2, 3));  // 6

2. 应用场景

2.1 参数复用

柯里化可以让你创建一个新的函数,固定某些参数。这样可以避免重复传递相同的参数。

const multiply = (x, y) => x * y;
const curriedMultiply = curry(multiply);

const double = curriedMultiply(2); // 固定第一个参数为 2
console.log(double(5)); // 10

2.2 函数组合

柯里化使得函数组合变得更加简单。可以将多个小函数组合成一个复杂的函数。

const add = (x, y) => x + y;
const multiply = (x, y) => x * y;

const curriedAdd = curry(add);
const curriedMultiply = curry(multiply);

const incrementAndDouble = (num) => curriedMultiply(2)(curriedAdd(1)(num));
console.log(incrementAndDouble(3)); // 8

2.3 延迟执行

柯里化可以用于实现延迟执行,直到满足特定条件时才调用函数。

const logMessage = (level) => (message) => {
  console.log(`[${level}] ${message}`);
};

const infoLogger = logMessage('INFO');
infoLogger('This is an info message.'); // [INFO] This is an info message.

3. 优势

  • 提高可读性:柯里化使得代码更加清晰易懂,尤其在函数参数较多的情况下。
  • 增强灵活性:通过固定部分参数,可以创建更灵活的函数。
  • 便于测试:柯里化后的函数可以更容易地进行单元测试,因为它们接收的参数较少。
  • 函数复用:可以通过柯里化创建多个特定的函数,重复使用相同的逻辑。

10.手写防抖和节流工具函数、并理解其内部原理和应用场景

1. 防抖(Debouncing)

1.1 原理

防抖的核心思想是:在事件被触发后,设定一个延迟时间,如果在这个时间内再次触发事件,则重新计时。只有在事件触发结束后的延迟时间内没有再次触发,才会执行回调函数。

1.2 代码示例

function debounce(func, delay) {
  let timer;

  return function (...args) {
    const context = this;

    clearTimeout(timer); // 清除之前的定时器
    timer = setTimeout(() => {
      func.apply(context, args); // 在延迟时间后执行回调
    }, delay);
  };
}

1.3 应用场景

  • 输入框实时搜索:在用户输入时,防止每输入一个字符就发送请求,而是在用户停止输入一段时间后再发送请求。
  • 窗口调整事件:在窗口大小调整时,防止频繁触发事件,只有在用户停止调整后再执行回调。

2. 节流(Throttling)

2.1 原理

节流的核心思想是:规定在一定时间内只允许函数执行一次,无论该事件触发多少次,回调函数只会在规定的时间间隔内被调用一次。

2.2 代码示例

function throttle(func, limit) {
  let lastFunc;
  let lastRan;

  return function (...args) {
    const context = this;

    if (!lastRan) {
      func.apply(context, args); // 立即执行
      lastRan = Date.now(); // 记录执行时间
    } else {
      clearTimeout(lastFunc); // 清除之前的定时器
      lastFunc = setTimeout(() => {
        if (Date.now() - lastRan >= limit) {
          func.apply(context, args); // 在限制时间后执行
          lastRan = Date.now(); // 更新上次执行时间
        }
      }, limit - (Date.now() - lastRan));
    }
  };
}

2.3 应用场景

  • 滚动事件:在用户滚动页面时,限制滚动事件的处理频率,避免性能问题。
  • 窗口调整事件:在窗口大小调整时,限制回调执行的频率,防止性能瓶颈。

3. 总结

3.1 防抖与节流的区别

特性防抖(Debounce)节流(Throttle)
触发时机事件触发后延迟执行在规定时间间隔内只执行一次
应用场景适合处理用户输入、搜索等需要延迟的操作适合处理高频率的事件,如滚动、窗口调整

3.2 选择使用

  • 防抖:适用于希望在特定操作完成后再执行某个操作的场景。
  • 节流:适用于需要限制操作频率的场景,尤其是在高频事件处理时。

11.实现一个sleep函数

1. 使用 Promise 实现 sleep 函数

代码示例

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

使用示例

你可以通过 async/await 语法来使用这个 sleep 函数:

async function demo() {
  console.log("Start sleeping...");
  await sleep(2000); // 暂停 2 秒
  console.log("Awake after 2 seconds!");
}

demo();

2. 解释

2.1 如何工作

  • Promisesleep 函数返回一个 Promise 对象,Promise 的 resolve 方法在 setTimeout 的回调中被调用。
  • setTimeoutsetTimeout 用于在指定的毫秒数后执行回调函数,这里用来控制暂停的时间。
  • async/await:通过 await 关键字,可以暂停 async 函数的执行,直到 Promise 被解决。

2.2 优势

  • 非阻塞:由于使用了 Promise 和 async/await,这个 sleep 函数不会阻塞主线程,其他代码仍然可以执行。
  • 易于使用:可以方便地与其他异步操作结合使用。

12.手动实现call、apply、bind

实现 call 方法

Function.prototype.call = function(context, ...args) {
  // 若没有传入上下文或者传入的上下文为null,则默认上下文为window
  if (typeof context !== 'object' && typeof context !== 'function') {
    context = window;
  }
  
  // 给 context 添加一个属性,值为函数本身,然后执行函数
  const fnSymbol = Symbol(); // 使用Symbol保证不会重复
  context[fnSymbol] = this;

  // 执行函数
  const result = context[fnSymbol](...args);

  // 删除刚才添加的属性
  delete context[fnSymbol];

  // 返回执行结果
  return result;
};

实现 apply 方法

Function.prototype.apply = function(context, argsArray) {
  // 若没有传入上下文或者传入的上下文为null,则默认上下文为window
  if (typeof context !== 'object' && typeof context !== 'function') {
    context = window;
  }
  
  // 给 context 添加一个属性,值为函数本身,然后执行函数
  const fnSymbol = Symbol();
  context[fnSymbol] = this;

  // 执行函数
  let result;
  if (argsArray) {
    result = context[fnSymbol](...argsArray);
  } else {
    result = context[fnSymbol]();
  }

  // 删除刚才添加的属性
  delete context[fnSymbol];

  // 返回执行结果
  return result;
};

实现 bind 方法

Function.prototype.bind = function(context, ...args) {
  // 保存原函数
  const fn = this;

  // 绑定函数
  return function(...newArgs) {
    // 若传入的上下文为null或undefined,则默认上下文为window
    if (typeof context !== 'object' && typeof context !== 'function') {
      context = window;
    }

    // 如果原函数是构造函数,则无法使用bind
    if (this instanceof fn.bind) {
      return new fn(...args, ...newArgs);
    }

    // 执行原函数
    return fn.apply(context, args.concat(newArgs));
  };
};

解释

  • call

    • call 方法允许你调用一个具有给定 this 值和单独给出的参数的函数。
    • 它立即执行函数。
  • apply

    • apply 方法与 call 类似,但允许你以数组的形式传递参数。
    • 它立即执行函数。
  • bind

    • bind 方法创建了一个新的函数,当被调用时,它的 this 被设置为提供的值,预置参数为给定的参数。
    • 它返回一个新的函数,而不是立即执行。

注意事项

  • 在实现 call 和 apply 时,需要考虑 context 为 null 或 undefined 的情况。
  • 在实现 bind 时,需要考虑 new 操作符的情况,确保绑定函数可以正确地作为构造函数使用。
  • 这些实现简化了原始方法的一些复杂性,例如处理原生函数和内置函数的特殊行为。

 13.手写一个EventEmitter实现事件发布、订阅

1. EventEmitter 实现

代码示例

class EventEmitter {
  constructor() {
    this.events = {}; // 存储事件及其对应的回调函数
  }

  // 订阅事件
  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = []; // 创建事件数组
    }
    this.events[event].push(listener); // 添加回调函数
  }

  // 发布事件
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(listener => {
        listener(...args); // 调用所有回调函数
      });
    }
  }

  // 取消订阅
  off(event, listener) {
    if (!this.events[event]) return;
    this.events[event] = this.events[event].filter(l => l !== listener); // 移除指定的回调函数
  }

  // 只监听一次的事件
  once(event, listener) {
    const onceListener = (...args) => {
      listener(...args); // 调用原回调函数
      this.off(event, onceListener); // 取消订阅
    };
    this.on(event, onceListener); // 注册一次性回调
  }
}

2. 使用示例

const emitter = new EventEmitter();

// 订阅事件
const greet = (name) => {
  console.log(`Hello, ${name}!`);
};

emitter.on('greet', greet);

// 发布事件
emitter.emit('greet', 'Alice'); // Hello, Alice!
emitter.emit('greet', 'Bob');   // Hello, Bob!

// 取消订阅
emitter.off('greet', greet);
emitter.emit('greet', 'Charlie'); // 没有输出,因为回调已被取消

// 只监听一次的事件
emitter.once('onceEvent', (msg) => {
  console.log(`This will be logged only once: ${msg}`);
});

emitter.emit('onceEvent', 'First Call'); // This will be logged only once: First Call
emitter.emit('onceEvent', 'Second Call'); // 没有输出

3. 总结

3.1 功能概述

  • on:用于订阅事件,将回调函数存储在事件列表中。
  • emit:用于发布事件,调用所有与该事件关联的回调函数,并传递参数。
  • off:用于取消订阅,移除指定事件的回调函数。
  • once:用于只监听一次的事件,回调函数在调用后自动取消订阅。

3.2 扩展功能

  • 事件参数:可以通过 emit 方法传递参数给回调函数。
  • 多次订阅:同一个事件可以被多个不同的回调函数订阅。

14.说出两种实现双向绑定的方案

1. Object.defineProperty 实现双向绑定

原理

通过 Object.defineProperty 方法,可以劫持对象的属性访问和赋值,进而实现数据变化时自动更新视图。

实现步骤

  1. 劫持数据属性:使用 Object.defineProperty 为对象的属性定义 getter 和 setter。
  2. 视图更新:在 setter 中添加视图更新的逻辑,当数据变化时触发更新。

优点

  • 兼容性好,适用于大多数浏览器。
  • 实现相对简单。

缺点

  • 只能劫持对象的已有属性,无法响应动态添加的属性。
  • 对数组的操作处理较为复杂。

2. Proxy 实现双向绑定

原理

使用 Proxy 对对象进行代理,可以拦截对对象的基本操作,如读取和写入,提供更灵活的方式来实现双向绑定。

实现步骤

  1. 创建 Proxy 对象:使用 Proxy 定义一个 handler,拦截 getset 操作。
  2. 更新视图:在 set 拦截中添加视图更新的逻辑,当数据变化时触发更新。

优点

  • 更灵活,可以拦截多种操作,包括数组操作和动态属性。
  • 代码简洁,易于维护。

缺点

  • 在较老的浏览器中支持较差。
  • 可能会增加一些性能开销。

15.手动实现双向绑定

HTML 结构

<div>
  <input type="text" id="inputElement" />
  <p>Value: <span id="textElement"></span></p>
</div>

JavaScript 实现

// 获取 DOM 元素
const inputElement = document.getElementById('inputElement');
const textElement = document.getElementById('textElement');

// 初始化数据
let data = '';

// 观察者对象
const observer = {
  subscribe(callback) {
    this._callbacks = callback;
  },
  notify() {
    if (this._callbacks) {
      this._callbacks();
    }
  }
};

// 将数据和视图绑定
function bindData(dataInitialValue) {
  data = dataInitialValue;

  // 当输入框的内容改变时,更新数据
  inputElement.addEventListener('input', (event) => {
    data = event.target.value;
    observer.notify();
  });

  // 更新视图
  function updateView() {
    textElement.textContent = data;
  }

  // 订阅并更新视图
  observer.subscribe(updateView);

  // 初始更新视图
  updateView();
}

// 初始化绑定
bindData('Initial Value');

解释

  1. HTML 结构:

    • 包含一个输入框和一个用于显示数据的段落。
  2. JavaScript 实现:

    • 获取输入框和段落的 DOM 元素。
    • 定义一个观察者对象,包含订阅和通知的方法。
    • 定义 bindData 函数,用于绑定数据和视图。
  3. 观察者对象:

    • subscribe 方法用于订阅回调函数。
    • notify 方法用于通知所有订阅者数据已更新。
  4. 绑定数据和视图:

    • 当输入框的内容改变时,更新数据。
    • 定义一个 updateView 函数,用于更新视图。
    • 通过 observer.subscribe 方法订阅 updateView 函数。
    • 初始时调用 updateView 函数更新视图。

工作原理

  1. 初始化:

    • 调用 bindData 函数,初始化数据和视图。
  2. 输入框变化:

    • 当用户在输入框中输入文本时,触发 input 事件。
    • 事件处理函数更新数据,并通知观察者。
  3. 更新视图:

    • 观察者收到通知后,调用订阅的 updateView 函数。
    • updateView 函数更新段落中的文本内容。

扩展

为了使其更加通用,我们可以创建一个更加灵活的双向绑定函数:

function twoWayBind(inputElement, viewElement, initialValue) {
  let data = initialValue;

  const observer = {
    subscribe(callback) {
      this._callbacks = callback;
    },
    notify() {
      if (this._callbacks) {
        this._callbacks();
      }
    }
  };

  inputElement.value = data;
  viewElement.textContent = data;

  function updateData(event) {
    data = event.target.value;
    observer.notify();
  }

  function updateView() {
    viewElement.textContent = data;
  }

  inputElement.addEventListener('input', updateData);
  observer.subscribe(updateView);

  updateView(); // 初始更新视图
}

const inputElement = document.getElementById('inputElement');
const textElement = document.getElementById('textElement');
twoWayBind(inputElement, textElement, 'Initial Value');

解释

  1. 通用函数:
    • twoWayBind 函数接受输入框元素、视图元素和初始值。
    • 设置输入框和视图的初始值。
    • 定义 updateData 函数,用于更新数据并通知观察者。
    • 定义 updateView 函数,用于更新视图。
    • 为输入框添加 input 事件监听器。
    • 初始更新视图。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

真的不想学习啦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值