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/html
、application/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 加密过程
-
建立连接:
- 当客户端(如浏览器)向服务器发起 HTTPS 请求时,首先会建立一个 SSL/TLS 连接。
-
握手过程:
- 客户端向服务器发送支持的 SSL/TLS 版本和加密算法。
- 服务器选择合适的版本和算法,并将其数字证书发送给客户端。
- 客户端验证证书的有效性(如检查证书链和颁发者)。
- 客户端生成一个随机数(称为预主密钥),并用服务器的公钥加密后发送给服务器。
- 服务器使用私钥解密预主密钥。
-
生成会话密钥:
- 客户端和服务器使用预主密钥生成对称加密的会话密钥。
-
加密通信:
- 使用会话密钥加密后续的数据传输。
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: websocket
和Connection: 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 的区别
特性 | HTTP | WebSocket |
---|---|---|
连接类型 | 无状态的请求-响应模型 | 持久的全双工连接 |
建立连接 | 每个请求都需要建立和关闭连接 | 仅在初始握手时建立一次连接 |
数据传输 | 单向请求-响应 | 双向实时传输 |
数据格式 | 文本格式 | 二进制和文本格式 |
性能 | 每次请求都需重新传输 HTTP 头部 | 传输数据时只需较小的帧头 |
适用场景 | 适合请求-响应的应用 | 适合实时应用(如聊天、游戏) |
6.多种方式实现数组去重、扁平化、对比优缺点
1. 数组去重
1.1 使用 Set
const uniqueArray = [...new Set(array)];
优点
- 简洁明了。
- 性能较好,时间复杂度为 O(n)。
缺点
- 不支持对象的深度去重(只对基本数据类型有效)。
1.2 使用 filter
和 indexOf
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 使用 concat
和 apply
const flatten = (arr) => [].concat.apply([], arr);
优点
- 简洁,适合一层的扁平化。
缺点
- 仅适合一层,无法实现深度扁平化。
- 性能较差,尤其在大数组中。
3. 总结
数组去重
- Set 和 对象/Map 方法 性能较好,语法简洁。
- filter 和 reduce 方法代码简单,但性能较差,适合小数组。
数组扁平化
- flat 方法 是最简单的选择,但兼容性较差。
- 递归方法(reduce 和 forEach)适合深度扁平化,但在深层嵌套时可能导致性能问题。
- concat 和 apply 方法适合一层扁平化,代码简洁但性能较差。
7.JSON.parse和JSON.stringify
JSON.parse
和 JSON.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));
优点
- 简单易用:实现非常简单,适合快速应用。
- 兼容性好:在大多数浏览器中均可使用。
缺点
- 无法处理函数:会丢失对象中的方法。
- 无法处理特殊对象:如
Date
、RegExp
、Set
、Map
等,均会被转化为普通对象。 - 无法处理循环引用:会导致错误。
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;
};
优点
- 灵活性高:可以处理多种数据类型,包括嵌套对象和数组。
- 可以处理自定义对象:可以处理任何类型的对象。
缺点
- 性能问题:对于深层嵌套的对象,可能导致性能下降。
- 无法处理特殊对象:如
Date
、RegExp
、Set
、Map
等。
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);
优点
- 内置函数:不需要引入库,直接使用。
- 支持多种数据类型:可以处理
Date
、Map
、Set
、ArrayBuffer
等。
缺点
- 浏览器支持:目前仅在部分现代浏览器(如 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 如何工作
- Promise:
sleep
函数返回一个 Promise 对象,Promise 的resolve
方法在setTimeout
的回调中被调用。 - setTimeout:
setTimeout
用于在指定的毫秒数后执行回调函数,这里用来控制暂停的时间。 - 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
方法,可以劫持对象的属性访问和赋值,进而实现数据变化时自动更新视图。
实现步骤
- 劫持数据属性:使用
Object.defineProperty
为对象的属性定义 getter 和 setter。 - 视图更新:在 setter 中添加视图更新的逻辑,当数据变化时触发更新。
优点
- 兼容性好,适用于大多数浏览器。
- 实现相对简单。
缺点
- 只能劫持对象的已有属性,无法响应动态添加的属性。
- 对数组的操作处理较为复杂。
2. Proxy 实现双向绑定
原理
使用 Proxy
对对象进行代理,可以拦截对对象的基本操作,如读取和写入,提供更灵活的方式来实现双向绑定。
实现步骤
- 创建 Proxy 对象:使用
Proxy
定义一个 handler,拦截get
和set
操作。 - 更新视图:在
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');
解释
-
HTML 结构:
- 包含一个输入框和一个用于显示数据的段落。
-
JavaScript 实现:
- 获取输入框和段落的 DOM 元素。
- 定义一个观察者对象,包含订阅和通知的方法。
- 定义
bindData
函数,用于绑定数据和视图。
-
观察者对象:
subscribe
方法用于订阅回调函数。notify
方法用于通知所有订阅者数据已更新。
-
绑定数据和视图:
- 当输入框的内容改变时,更新数据。
- 定义一个
updateView
函数,用于更新视图。 - 通过
observer.subscribe
方法订阅updateView
函数。 - 初始时调用
updateView
函数更新视图。
工作原理
-
初始化:
- 调用
bindData
函数,初始化数据和视图。
- 调用
-
输入框变化:
- 当用户在输入框中输入文本时,触发
input
事件。 - 事件处理函数更新数据,并通知观察者。
- 当用户在输入框中输入文本时,触发
-
更新视图:
- 观察者收到通知后,调用订阅的
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');
解释
- 通用函数:
twoWayBind
函数接受输入框元素、视图元素和初始值。- 设置输入框和视图的初始值。
- 定义
updateData
函数,用于更新数据并通知观察者。 - 定义
updateView
函数,用于更新视图。 - 为输入框添加
input
事件监听器。 - 初始更新视图。