2023.03.26 更新前端面试问题总结(22道题)
获取更多面试问题可以访问
github 地址: https://github.com/pro-collection/interview-question/issues
gitee 地址: https://gitee.com/yanleweb/interview-question/issues
目录:
初级开发者相关问题【共计 1 道题】
中级开发者相关问题【共计 9 道题】
高级开发者相关问题【共计 11 道题】
资深开发者相关问题【共计 1 道题】
is
是 TypeScript 中的一个关键字,用于创建类型保护。在 TypeScript 中,类型保护是一种用于确定变量是否符合某种类型的方法。当我们使用 is
关键字创建一个类型保护时,它会在运行时对变量进行判断,然后返回一个布尔值。
具体来说,我们可以通过定义一个返回值为布尔类型的函数,并在函数内部进行类型判断来创建类型保护。比如:
csharpCopy codefunction isString(value: any): value is string {
return typeof value === 'string';
}
在这个例子中,我们定义了一个名为 isString
的函数,它接收一个任意类型的参数 value
,并通过 typeof
运算符判断 value
是否为字符串。如果是字符串,函数返回 true
,否则返回 false
。
使用时,我们可以通过将变量传递给 isString
函数来判断变量是否为字符串类型:
rustCopy codeconst str = 'hello';
if (isString(str)) {
console.log(str.length);
}
在这个例子中,由于 str
是字符串类型,所以 isString(str)
返回 true
,if
语句内的代码会被执行,输出字符串的长度。如果 str
不是字符串类型,isString(str)
返回 false
,if
语句内的代码不会被执行。
这样,在使用变量之前进行类型保护,可以避免在运行时出现类型错误,提高代码的健壮性。
HTTP/3(或称为HTTP-over-QUIC)是一个基于QUIC协议的新版本的HTTP协议。QUIC(Quick UDP Internet Connections)是由Google设计的一个基于UDP协议的传输层协议,旨在解决HTTP/2协议存在的一些问题。
HTTP/3中引入了QUIC的一些特性,如0-RTT连接建立、基于UDP的传输、数据流多路复用和快速恢复等,这些特性有助于提高性能和安全性。与HTTP/2相比,HTTP/3采用了新的二进制编码协议(QUIC Crypto)来加密和验证数据,以提供更好的安全性。
此外,HTTP/3还可以更好地适应现代网络环境下的多元化应用需求。由于QUIC协议基于UDP协议,因此可以更好地适应移动网络和高丢包率网络等不稳定的网络环境。同时,HTTP/3可以更好地支持多媒体内容和实时通信等应用场景。
前端实现截图需要使用 HTML5 的 Canvas 和相关 API,具体步骤如下:
toDataURL()
方法将 Canvas 转化为 base64 编码的图片数据。以下是一个简单的例子,实现了对整个页面的截图:
htmlCopy codeDOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>截图示例title>
<style>
#canvas {
position: fixed;
left: 0;
top: 0;
z-index: 9999;
}
style>
head>
<body>
<h1>截图示例h1>
<p>这是一个简单的截图示例。p>
<button id="btn">截图button>
<canvas id="canvas">canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const btn = document.getElementById('btn');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
btn.addEventListener('click', () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(document.documentElement, 0, 0);
const imgData = canvas.toDataURL();
console.log(imgData);
});
script>
body>
html>
这个例子中,在页面中创建了一个 canvas
元素,并设置其宽高和样式,将其放在页面最上方。在点击“截图”按钮时,通过 toDataURL()
方法将整个页面的截图转换为 base64 编码的图片数据,并打印到控制台上。
当QPS达到峰值时,可以从以下几个方面来进行优化:
数据库优化:数据库的优化包括优化SQL语句、使用索引、避免全表扫描、分表分库等措施,以提高数据库的读写性能。
缓存优化:缓存可以降低对数据库的访问频率,提高响应速度。可以使用Redis、Memcached等缓存技术,减轻服务器负载。
代码优化:优化代码可以提高代码的执行效率,减少不必要的开销。可以通过一些优化手段,如减少不必要的代码执行、避免循环嵌套、避免不必要的递归调用等来提高代码的性能。
负载均衡:负载均衡可以将请求分发到多个服务器上,减少单个服务器的负载,提高整个系统的性能和可用性。
异步处理:将一些计算量大、耗时长的操作异步处理,减少对主线程的阻塞,提高响应速度。
CDN加速:使用CDN技术可以将静态资源缓存到CDN节点上,提高资源的加载速度,减少服务器的负载。
硬件升级:可以通过升级服务器硬件,增加带宽等方式来提高系统的处理能力。
以上是一些常见的优化手段,需要根据具体情况进行选择和实施。
在 JavaScript 中,超过 Number.MAX_VALUE
的数值被认为是 Infinity
(正无穷大)。如果要处理超过 Number.MAX_VALUE
的数值,可以使用第三方的 JavaScript 库,如 big.js
或 bignumber.js
,这些库可以处理任意精度的数值。
例如,使用 big.js
库可以将两个超过 Number.MAX_VALUE
的数相加:
const big = require('big.js');
const x = new big('9007199254740993');
const y = new big('100000000000000000');
const result = x.plus(y);
console.log(result.toString()); // 输出:100009007194925474093
这里创建了两个 big.js
对象 x
和 y
,分别存储超过 Number.MAX_VALUE
的数值。通过 plus
方法将它们相加,得到了正确的结果。最后,通过 toString
方法将结果转换为字符串。
JavaScript 中,数值超过了 Number 最大值时,可以使用 BigInt 类型来处理,它可以表示任意精度的整数。
使用 BigInt 类型时,需要在数值后面添加一个 n
后缀来表示 BigInt 类型。例如:
const bigNum = 9007199254740993n; // 注意:数字后面添加了 'n' 后缀
注意,BigInt 类型是 ECMAScript 2020 新增的特性,因此在某些浏览器中可能不被支持。如果需要在不支持 BigInt 的环境中使用 BigInt,可以使用 polyfill 或者第三方库来实现。
执行 ['1', '2', '3'].map(parseInt)
会得到 [1, NaN, NaN]
,这个结果可能和人们预期的不一样。
这是因为 map
方法会传入三个参数:当前遍历到的元素、当前遍历到的索引、原数组本身。而 parseInt
函数则接受两个参数:需要被解析的值、用于解析的进制数。在执行 ['1', '2', '3'].map(parseInt)
时,实际传入 parseInt
的参数如下:
'1'
、0
(表示解析为十进制):解析后得到数字 1
。'2'
、1
(表示解析为一进制):解析后得到 NaN
。'3'
、2
(表示解析为二进制):解析后得到 NaN
。所以结果为 [1, NaN, NaN]
。
深度优先遍历(Depth-First-Search,DFS)和广度优先遍历(Breadth-First-Search,BFS)是图和树的两种遍历方式。
深度优先遍历采用深度优先的策略遍历整张图或树,即从当前节点开始,先访问其所有子节点,再依次访问子节点的子节点,直到遍历完整张图或树。
DFS 可以使用递归或栈来实现。
递归实现:
function dfsRecursive(node, visited) {
if (!node || visited.has(node)) {
return;
}
visited.add(node);
console.log(node.value);
for (let i = 0; i < node.children.length; i++) {
dfsRecursive(node.children[i], visited);
}
}
栈实现:
function dfsStack(node) {
const visited = new Set();
const stack = [node];
while (stack.length > 0) {
const current = stack.pop();
if (!current || visited.has(current)) {
continue;
}
visited.add(current);
console.log(current.value);
for (let i = current.children.length - 1; i >= 0; i--) {
stack.push(current.children[i]);
}
}
}
广度优先遍历采用广度优先的策略遍历整张图或树,即从当前节点开始,先访问所有相邻节点,再访问所有相邻节点的相邻节点,以此类推,直到遍历完整张图或树。
BFS 可以使用队列来实现。
队列实现:
function bfsQueue(node) {
const visited = new Set();
const queue = [node];
while (queue.length > 0) {
const current = queue.shift();
if (!current || visited.has(current)) {
continue;
}
visited.add(current);
console.log(current.value);
for (let i = 0; i < current.children.length; i++) {
queue.push(current.children[i]);
}
}
}
总的来说,深度优先遍历和广度优先遍历都有自己的应用场景,比如:
深度优先思想实现拷贝函数可以采用递归的方式遍历对象或数组,对每个元素进行复制。如果当前元素是一个对象或数组,则递归调用拷贝函数,如果是基本数据类型则直接进行复制。以下是一个用深度优先思想实现拷贝函数的示例代码:
function deepClone(obj) {
// 如果obj是基本数据类型或null,则直接返回
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let result;
// 判断obj是数组还是对象
if (Array.isArray(obj)) {
result = [];
} else {
result = {};
}
// 递归遍历obj的每个属性或元素,并进行拷贝
for (let key in obj) {
result[key] = deepClone(obj[key]);
}
return result;
}
广度优先思想实现拷贝函数可以使用队列的方式,将每个元素放入队列中,然后循环遍历队列。如果当前元素是一个对象或数组,则将其属性或元素放入队列中,然后继续循环遍历队列。如果是基本数据类型则直接进行复制。以下是一个用广度优先思想实现拷贝函数的示例代码:
function breadthClone(obj) {
// 如果obj是基本数据类型或null,则直接返回
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let result;
// 判断obj是数组还是对象
if (Array.isArray(obj)) {
result = [];
} else {
result = {};
}
let queue = [obj];
let resQueue = [result];
// 循环遍历队列
while (queue.length > 0) {
let curObj = queue.shift();
let curRes = resQueue.shift();
// 遍历当前元素的每个属性或元素,并进行拷贝
for (let key in curObj) {
let val = curObj[key];
if (typeof val === 'object' && val !== null) {
// 如果当前属性或元素是一个对象或数组,则将其放入队列中
let newVal = Array.isArray(val) ? [] : {};
curRes[key] = newVal;
queue.push(val);
resQueue.push(newVal);
} else {
// 如果是基本数据类型则直接进行复制
curRes[key] = val;
}
}
}
return result;
}
Promise 构造函数是同步执行的,而 then 方法是异步执行的。
在 Promise 构造函数中,Promise 的状态(pending/resolved/rejected)是同步确定的。但是 Promise 中的异步操作可能还没有完成,因此 Promise 对象本身的值可能还没有可用的值。所以,当我们在构造函数中使用 resolve/reject 时,它们并不会立即触发 then 中注册的回调函数执行。
而 then 方法则是异步执行的。当我们在一个 Promise 对象上调用 then 方法并注册了回调函数时,这些回调函数并不会立即执行。相反,它们会被添加到一个任务队列中,等到当前 JavaScript 上下文中的所有同步代码执行完成后再执行。
这也是 Promise 非常重要的特性之一,即能够在异步任务完成后执行回调函数,避免了回调地狱等问题。
观察者模式(又称发布-订阅模式)是一种行为型设计模式,它定义了对象之间的一对多依赖关系,使得当一个对象的状态发生改变时,其相关的依赖对象都能够得到通知并被自动更新。
在 JavaScript 中实现观察者模式,可以分为以下几个步骤:
创建一个主题对象(Subject),用来存储观察者对象,并提供添加、删除、通知观察者的接口。
创建观察者对象(Observer),它有一个 update 方法,用来接收主题对象的通知,并进行相应的处理。
下面是一个简单的示例:
class Subject {
constructor() {
this.observers = [];
}
// 添加观察者
addObserver(observer) {
this.observers.push(observer);
}
// 删除观察者
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
// 通知观察者
notifyObservers() {
this.observers.forEach(observer => observer.update());
}
}
class Observer {
constructor(name) {
this.name = name;
}
update() {
console.log(`${this.name} received the notification.`);
}
}
const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notifyObservers();
// Output:
// Observer 1 received the notification.
// Observer 2 received the notification.
在这个示例中,Subject 是主题对象,Observer 是观察者对象。Subject 提供了添加、删除、通知观察者的接口,Observer 有一个 update 方法,用来接收主题对象的通知,并进行相应的处理。在使用时,我们可以通过调用 Subject 的 addObserver 方法,将 Observer 对象添加到主题对象中。当主题对象的状态发生改变时,我们可以调用 notifyObservers 方法,通知所有的观察者对象进行更新。
以上仅是一个简单的示例,实际应用中还需要考虑更多的细节问题。
async/await 是 ECMAScript 2017(ES8)中引入的一个语言特性,用于处理异步编程。async/await 实际上是对 Promise 的封装,通过让开发者以同步的方式编写异步代码,使得代码更加易读和易于维护。
async/await 是一种更加高级的异步编程方式,它使用了 Promise 作为底层实现,可以更好地处理异步编程中的错误和异常,避免了回调地狱和代码可读性差的问题。
async/await 的实现可以通过封装 Promise 和 Generator 函数来实现,下面是一个简单的手写实现示例:
function delay(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
function* generator() {
console.log("start");
yield delay(1000);
console.log("after 1 second");
yield delay(2000);
console.log("after 2 more seconds");
}
function async(generatorFunc) {
const iterator = generatorFunc();
function handle(iteratorResult) {
if (iteratorResult.done) {
return Promise.resolve(iteratorResult.value);
}
return Promise.resolve(iteratorResult.value).then((res) => {
return handle(iterator.next(res));
});
}
return handle(iterator.next());
}
async(function () {
return generator();
}).then(() => {
console.log("all done");
});
HTTP协议的不同版本的主要特点如下表所示:
版本 | 发布时间 | 主要特点 |
---|---|---|
HTTP/0.9 | 1991年 | 只支持GET方法,没有Header和Body |
HTTP/1.0 | 1996年 | 引入Header、POST方法、响应码、缓存等特性 |
HTTP/1.1 | 1999年 | 引入持久连接、管道化请求、分块传输编码、Host头、缓存控制等特性 |
HTTP/2 | 2015年 | 引入二进制分帧、头部压缩、流量控制、多路复用等特性 |
HTTP/3 | 2021年 | 引入QUIC协议,改进网络传输性能 |
需要注意的是,HTTP/1.x和HTTP/2都支持TLS加密传输,即HTTPS协议。HTTP/3则是基于QUIC协议的,使用TLS 1.3进行加密传输。
HTTP/1.1和HTTP/2都是HTTP协议的不同版本,在网络传输和性能方面有很大的差别。
HTTP/1.1使用的是“管线化请求”和“持久连接”来提高性能,而HTTP/2则引入了更多的特性,其中最重要的特性是“多路复用”。
“管线化请求”是HTTP/1.1提出的一种优化方法,它可以让浏览器同时发出多个请求,从而避免了HTTP/1.1中因为请求阻塞导致的性能问题。但是,由于HTTP/1.1的“管线化请求”存在“队头阻塞”(head-of-line blocking)问题,即前面一个请求没有得到响应时,后面的请求必须等待,导致性能并没有得到很大提升。
“持久连接”是HTTP/1.1中另一种提高性能的方法,它可以在一个TCP连接中传输多个HTTP请求和响应,避免了每个请求都需要建立和关闭连接的开销。但是,由于HTTP/1.1中的“持久连接”是按顺序发送请求和响应的,所以依然存在“队头阻塞”的问题。
HTTP/2则引入了“多路复用”(multiplexing)这一特性,可以在一个TCP连接上同时传输多个HTTP请求和响应,避免了“队头阻塞”问题。它使用二进制分帧(binary framing)技术将HTTP请求和响应分成多个帧(frame),并使用流(stream)来标识不同的请求和响应,从而实现了更高效的网络传输和更低的延迟。此外,HTTP/2还引入了头部压缩(header compression)和服务器推送(server push)等特性。
因此,HTTP/2的多路复用比HTTP/1.1的管线化请求和持久连接更为高效、灵活,能够更好地支持现代Web应用的性能要求。
前端可以通过以下方式防止加载外域脚本:
使用 Content Security Policy (CSP):CSP 是一个 HTTP 头,可以限制页面可以从哪些源加载资源。通过 CSP,可以禁止加载外域脚本,从而防止 XSS 攻击等安全问题。
使用 Subresource Integrity (SRI):SRI 是一个浏览器功能,可以确保在加载外部资源时,它们的内容没有被篡改过。通过在 script 标签中添加 integrity 属性,可以指定资源的校验和,浏览器会校验资源是否与 integrity 值匹配,从而确保资源没有被篡改过。
避免使用动态脚本加载:使用 document.createElement(‘script’) 创建 script 元素,并手动设置其 src 属性,可以避免使用 eval() 动态执行脚本。动态加载脚本的方式可能会受到中间人攻击,从而加载恶意脚本。
避免使用不安全的协议加载资源:使用 HTTPS 加载资源可以确保资源在传输过程中不会被篡改。避免使用 HTTP 或者 file 协议加载资源,这些协议容易受到中间人攻击。
综上,前端可以通过以上方式防止加载外域脚本,提高应用程序的安全性。
中间人攻击(Man-in-the-Middle Attack, MITM),也称中间攻击,是指黑客通过拦截通信过程中的数据,然后在两个通信的用户之间进行欺骗和篡改的攻击方式。在 HTTPS 中,如果黑客能够截获用户和服务器之间的通信,那么他就可以使用类似于伪造证书、中断连接、插入恶意代码等方式,对数据进行篡改或窃取用户敏感信息。
为了防止中间人攻击,在 HTTPS 中通常采用数字证书认证机制,即服务器会向可信的第三方机构申请数字证书,证明其身份的真实性。当用户访问该服务器时,服务器会把自己的数字证书发送给用户,用户的浏览器会验证该证书是否由可信的第三方机构签发,是否过期等,如果验证通过,则可以建立安全的 HTTPS 连接。如果数字证书无效,则会弹出警告页面,提示用户存在安全风险。
前端单页应用使用 History 路由模式,需要将路由的控制交给前端,而不是后端处理,因此需要配置 nginx 让其将所有请求都指向 index.html 页面,让前端来处理路由。
具体配置可以参考以下 nginx 配置:
bashCopy codeserver {
listen 80;
server_name example.com; # 域名或 IP 地址
location / {
root /path/to/your/app; # 项目根目录
index index.html;
try_files $uri $uri/ /index.html; # 将所有请求都指向 index.html 页面
}
}
以上配置中,try_files
指令会尝试将请求映射到对应的文件,如果找不到,则会将请求转发到 /index.html
页面,由前端来处理路由。同时,root
指令需要指向前端应用的根目录。
JavaScript异步解决方案的发展历程主要有以下几个阶段:
最初,JavaScript采用回调函数的方式来解决异步编程问题。回调函数即在异步任务完成后调用的回调函数。例如,setTimeout
函数就是一个使用回调函数的例子。
setTimeout(() => {
console.log('Hello, world!');
}, 1000);
回调函数的优点是简单易懂,缺点是嵌套层次多、代码难以维护。
jQuery.Deferred()是jQuery提供的一种异步编程解决方案。它是一种Promise风格的API,使得异步操作可以更加简单和可读性更高。
jQuery.Deferred()可以用于串行和并行异步操作的组织和控制,避免了回调地狱和代码复杂性。
在使用过程中,通过使用jQuery.Deferred()的resolve()和reject()方法来决定异步操作的成功或失败,并且可以使用then()方法添加成功和失败的回调函数。
jQuery.Deferred()主要的优点包括:
而缺点则包括:
Promise是ES6引入的一种异步编程解决方案,用于解决回调函数的嵌套问题。Promise是一个对象,表示异步操作的最终完成或失败。它有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
Promise的优点是解决了回调函数嵌套的问题,使得代码可读性和可维护性更好。缺点是语法相对复杂。
// Promise示例
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello, world!');
}, 1000);
});
}
fetchData().then((data) => {
console.log(data);
}).catch((error) => {
console.log(error);
});
Generator 可以使用 yield 语句来暂停函数执行,并返回一个 Generator 对象,通过这个对象可以控制函数的继续执行和结束。
ES8引入了Async/Await语法,使得异步编程更加简单和可读。Async/Await是基于Promise实现的,可以看作是对Promise的一种封装。Async/Await语法可以让异步代码像同步代码一样书写,让代码的可读性更高。
// Async/Await示例
async function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello, world!');
}, 1000);
});
}
async function run() {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.log(error);
}
}
run();
Async/Await 的优点是语法简单易懂、可读性好,缺点是需要掌握Promise的基本用法。
综上,JavaScript 异步编程方案的发展历程从最初的回调函数到Promise再到Async/Await,每个阶段都解决了前一阶段存在的问题,使得异步编程更加方便和易读。但是,不同方案都有自己的优缺点,需要根据实际情况选择使用。
从 HTTP 1.1 迁移到 HTTP/2 通常需要进行以下步骤:
升级服务器:首先,你需要将你的服务器升级到支持 HTTP/2。大多数主流服务器,如Apache、Nginx等,都已经支持 HTTP/2。
使用 HTTPS:HTTP/2 只支持加密连接,因此需要使用 HTTPS。所以,你需要获得一个 SSL 证书,并使用 HTTPS 连接来替代原来的 HTTP 连接。
修改网页代码:为了利用 HTTP/2 的多路复用特性,你需要将网页中的多个小文件(例如 CSS、JavaScript、图像等)合并为一个文件,以减少请求的数量。此外,你还需要避免在一个请求中同时传输大量数据,以免阻塞其他请求的传输。
配置服务器:为了使 HTTP/2 能够充分发挥性能,你需要进行一些服务器配置,例如启用 HTTP/2、调整 TLS 版本和密码套件等。
需要注意的是,HTTP/2 是一个复杂的协议,迁移过程中需要仔细审查每一个步骤,并且对性能进行监测和测试,以确保迁移后的网站性能更好。
当 B 机器重启时,TCP 连接会被断开,此时 A 机器会检测到 TCP 连接异常断开,将 TCP 状态修改为 FIN_WAIT_1 状态。A 机器会继续等待来自 B 机器的响应,如果等待的时间超过了一定时间(通常为几分钟),A 机器会放弃等待并关闭 TCP 连接,将 TCP 状态修改为 CLOSED 状态。
观察者模式和订阅-发布模式都属于事件模型,它们都是为了解耦合而存在,但是它们之间还是有一些不同之处的:
观察者模式中,主题(被观察者)和观察者之间是直接联系的,观察者订阅主题,主题状态发生变化时会直接通知观察者;而订阅-发布模式中,发布者和订阅者之间没有直接的联系,发布者发布消息到消息中心,订阅者从消息中心订阅消息。
在观察者模式中,主题和观察者是一对多的关系,一个主题可以有多个观察者,而在订阅-发布模式中,发布者和订阅者是多对多的关系,一个发布者可以有多个订阅者,一个订阅者也可以订阅多个发布者。
在观察者模式中,主题状态发生变化时,观察者会被直接通知,通知的方式可以是同步或异步的,观察者可以决定如何处理通知;而在订阅-发布模式中,消息是通过消息中心进行传递的,订阅者从消息中心订阅消息,发布者发布消息到消息中心,消息中心再将消息发送给订阅者,这个过程是异步的,订阅者不能决定何时接收消息。
在观察者模式中,主题和观察者之间存在强耦合关系,如果一个观察者被移除,主题需要知道这个观察者的身份;而在订阅-发布模式中,发布者和订阅者之间没有强耦合关系,发布者不需要知道订阅者的身份,订阅者也不需要知道发布者的身份。
综上所述,观察者模式和订阅-发布模式都是事件模型,但它们之间的区别在于关注点的不同,观察者模式更关注主题和观察者之间的交互,而订阅-发布模式更关注发布者和订阅者之间的交互。
订阅-发布模式是一种常用的设计模式,它可以实现对象间的解耦,让它们不需要相互知道对方的存在,只需要关注自己需要订阅的事件即可。当一个对象的状态发生变化时,它可以发布一个事件通知其他对象,其他对象可以订阅该事件,当事件发生时得到通知并执行相应的处理。
在 JavaScript 中,订阅-发布模式也被称为事件模型。事件模型由两个主要组件组成:事件触发器和事件监听器。事件触发器负责触发事件,而事件监听器则负责监听事件并执行相应的回调函数。
下面是一个简单的实现订阅-发布模式的例子:
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]) {
this._events[event] = this._events[event].filter((l) => l !== listener);
}
}
}
这个实现包括三个方法:
on(event, listener)
:订阅事件,当事件被触发时执行监听器 listener
;emit(event, ...args)
:触发事件,并将参数 ...args
传递给监听器;off(event, listener)
:取消订阅事件,不再执行监听器 listener
。使用方法如下:
const emitter = new EventEmitter();
// 订阅事件
emitter.on("event", (arg1, arg2) => {
console.log(`event: ${arg1}, ${arg2}`);
});
// 触发事件
emitter.emit("event", "hello", "world");
// 取消订阅事件
emitter.off("event");
以上代码将输出:
csharpCopy codeevent: hello, world
订阅-发布模式在事件驱动的系统中非常常见,例如浏览器中的 DOM 事件、Node.js 中的异步 IO 事件等。
HTTP/3是基于UDP的协议,因此在设计时需要考虑安全性问题。为了保障安全性,HTTP/3使用了一个新的加密协议——QUIC Crypto。
QUIC Crypto使用了一种名为"0-RTT安全连接"的机制,允许客户端在第一次请求时就可以建立安全连接,从而减少连接建立的延迟。此外,HTTP/3还使用了数字证书来验证服务器身份,以确保通信的安全性。
在HTTP/3中,每个数据包都使用一个独特的标识符(Connection ID)来标识。这个标识符会在每个数据包中包含,以便服务器能够识别它们。这种方式可以防止攻击者进行连接欺骗,从而提高了安全性。
另外,HTTP/3还使用了一些其他的技术来提高安全性,如0-RTT加密、零轮延迟、源地址验证、密钥派生和更新等。
综上所述,HTTP/3采用了一系列安全机制来保护通信安全,使其能够在基于UDP的网络环境下运行,并提供更好的性能和安全性。