2023.03.23 - 2023.03.26 更新前端面试问题总结(26道题)
获取更多面试问题可以访问
github 地址: github.com/pro-collect…
gitee 地址: gitee.com/yanleweb/in…
目录:
-
初级开发者相关问题【共计 3 道题】
- 157.CSS 文档流 是什么概念?【CSS】
- 158.CSS 中 position 常见属性有哪些,大概讲一下?【CSS】
- 171.[Vue] 响应式数据流驱动页面 和 传统的事件绑定命令式驱动页面, 有何优劣?【web框架】
-
中级开发者相关问题【共计 10 道题】
- 147.HTTP 与 HTTPS 的区别?【网络】
- 148.HTTPS 解决了什么问题?【网络】
- 149.HTTPS 中的 SSL/TLS 是什么?【网络】
- 154.常见的前端内存泄露场景有哪些?【JavaScript、浏览器】
- 156.实现 table header 吸顶, 有哪些实现方式?【CSS】
- 159.[Vue] 父子组件通信方式有哪些?【web框架】
- 160.什么是洋葱模型?【web框架】
- 164.[koa] 中间件的异常处理是怎么做的?【web框架】
- 173.为什么小程序里拿不到dom相关的api【web框架】
- 178.[React] useRef、ref、forwardsRef 的区别是什么?【web框架】
-
高级开发者相关问题【共计 11 道题】
- 152.页面崩溃如何监控?【网络】
- 153.如何监控前端页面内存持续增长情况?【网络】
- 155.常见的前端检测内存泄露的方法有哪些?【JavaScript、浏览器】
- 161.如何实现洋葱模式?【web框架】
- 168.[koa] 在没有async await 的时候, koa是怎么实现的洋葱模型?【web框架】
- 169.[koa] body-parser 中间件实现原理?【web框架】
- 170.文件上传和上传文件解析的原理是啥?【网络、浏览器】
- 172.es6 class 装饰器是如何实现的?【JavaScript】
- 174.Promise then 第二个参数和 Promise.catch 的区别是什么?【JavaScript】
- 175.Promise finally 怎么实现的?【JavaScript】
- 179.[React] useEffect的第二个参数,如何判断依赖是否发生变化?【web框架】
-
资深开发者相关问题【共计 2 道题】
- 151.HTTPS 加密算法和加解密过程是啥?【网络】
- 176.WebWorker、SharedWorker 和 ServiceWorker 有哪些区别?【JavaScript】
初级开发者相关问题【共计 3 道题】
157.CSS 文档流 是什么概念?【CSS】
CSS 的文档流(Document Flow)是指文档中元素按照其在 HTML 中出现的顺序自上而下布局的方式,也称为常规流(Normal Flow)或默认流。文档流定义了元素的布局顺序和定位方式,包括元素的位置、大小、间距等属性。
在文档流中,每个元素都会占据一定的空间并尽可能充满其包含块的宽度。每个元素的位置都会受到前面元素的影响,如果前面的元素发生位置变化,那么后面的元素的位置也会发生相应的变化。
文档流中的元素按照下面的规则排列:
- 块级元素:块级元素会独占一行,并在前面自动添加一个垂直间距。例如:
<p>、<div>、<h1>等。 - 行内元素:行内元素会在一行中排列,并且宽度根据内容自适应。例如:
<a>、<span>、<img>等。 - 行内块级元素:行内块级元素与行内元素类似,但是它可以设置宽度、高度等块级元素的属性。例如:
<input>、<button>、<textarea>等。
文档流是 CSS 中最基本、最重要的概念之一,它决定了网页的整体布局和排版方式,也是实现网页布局的基础。在实际开发中,我们需要理解文档流的特性和工作原理,以便更好地掌握网页布局和样式的设计。
158.CSS 中 position 常见属性有哪些,大概讲一下?【CSS】
CSS 中 position 属性用于指定元素的定位方式,它有以下常见的属性值:
static:默认值,元素在文档流中正常排列。relative:元素在文档流中正常排列,但是可以通过设置top、right、bottom、left属性相对于其正常位置进行偏移,不会影响其它元素的位置。absolute:元素脱离文档流,相对于最近的非static定位的祖先元素进行定位,如果没有则相对于<html>元素进行定位。通过设置top、right、bottom、left属性进行偏移,如果祖先元素发生位置变化,则元素的位置也会发生相应的变化。fixed:元素脱离文档流,相对于浏览器窗口进行定位,始终保持在窗口的固定位置,不会随页面滚动而滚动。通过设置top、right、bottom、left属性进行偏移。sticky:元素在文档流中正常排列,当元素滚动到指定的位置时,停止滚动并固定在该位置,直到其祖先元素发生滚动时才会取消固定。通过设置top、right、bottom、left属性和z-index属性进行设置。
以上是 position 属性的常见属性值和简单说明,不同的值会对元素进行不同的定位方式,开发人员可以根据需要选择合适的值来实现页面布局。
171.[Vue] 响应式数据流驱动页面 和 传统的事件绑定命令式驱动页面, 有何优劣?【web框架】
Vue 响应式数据流驱动页面和传统的事件绑定命令式驱动页面是两种不同的前端开发方式,它们的优劣势主要体现在代码编写方式、页面效果、开发效率和维护难度上。
- 响应式数据流驱动页面:Vue 使用响应式的数据流来驱动页面的渲染和更新。Vue 的响应式系统会自动侦测数据的变化,并且重新渲染页面,开发者只需要专注于数据和页面的关系,而不用手动操作 DOM 元素。相比传统的命令式开发方式,响应式数据流驱动页面的代码更简洁、易于维护,开发效率更高。同时,Vue 的组件化开发模式也可以让开发者轻松地实现组件复用和代码复用。
- 传统的事件绑定命令式驱动页面:传统的事件绑定命令式驱动页面是通过手动绑定事件和操作 DOM 元素来实现页面交互效果。这种开发方式需要编写大量的事件处理函数和 DOM 操作代码,容易出现逻辑混乱和代码冗余的问题。同时,由于每个事件都需要手动绑定和处理,开发效率也会受到一定的影响。
综上所述,响应式数据流驱动页面和传统的事件绑定命令式驱动页面都有其优缺点,选择何种开发方式需要根据具体的需求和实际情况来决定。一般来说,响应式数据流驱动页面更适合用于构建数据驱动的、组件化的页面,而传统的事件绑定命令式驱动页面更适合用于构建交互性强、动态性高的页面。
中级开发者相关问题【共计 10 道题】
147.HTTP 与 HTTPS 的区别?【网络】
HTTPS
基础
https 是 http 的“升级”版本:
HTTPS = HTTP+ SSL/TLS
SSL 是安全层,TLS 是传输层安全,是SSL 的继承。使用SSL或TLS 可确保传输数据的安全性。
使用 HTTP 可能看到传输数据是: “这是明文信息”
使用 HTTPS 可能看到: “283hd9saj9cdsncihquhs99ndso”
HTTPS 传输的不再是文本,而是二进制流,使得传输更高效,且加密处理更加安全。
HTTPS 的工作流程
1、客户端请求 HTTPS 请求并连接到服务器的 443 端口,此过程和请求 HTTP 请求一样,进行三次握手;
2、服务端向客户端发送数字证书,其中包含公钥、证书颁发者、到期日期
现比较流行的加解密码对,即公钥和私钥。公钥用于加密,私钥用于解密。所以服务端会保留私钥,然后发送公钥给客户端。
3、客户端收到证书,会验证证书的有效性。验证通过后会生成一个随机的 pre-master key。再将密钥通过接收到的公钥加密然后发送给服务端
4、服务端接收后使用私钥进行解密得到 pre-master key
5、获得 pre-master key 后,服务器和客户端可以使用主密钥进行通信。
HTTP 与 HTTPS 区别
所以在回答 HTTP 与 HTTPS 的区别的问题,可以从下面几个方面进行回答:
- 加密: HTTPS 是 HTTP 协议的更加安全的版本,通过使用SSL/TLS进行加密传输的数据;
- 连接方式: HTTP(三次握手)和 HTTPS (三次握手+数字证书)连接方式不一样;
- 端口: HTTP 默认的端口是 80和 HTTPS 默认端口是 443
HTTP2 是什么?
HTTP/2 超文本传输协议第2版,是 HTTP/1.x 的扩展。所以 HTTP/2没有改动HTTP的应用语义,仍然使用HTTP的请求方法、状态码和头字段等规则。
它主要修改了HTTP的报文传输格式,通过引入二进制分帧层实现性能的提升。
现有很多主流浏览器的 HTTPS/2 的实现都是基于SSL/TLS的,所以基于 SSL/TLS 的 HTTP/2 连接建立过程和 HTTPS 差不多。在建立连接过程中会携带标识期望使用 HTTP/2 协议,服务端同样方式回应。
参考文档
148.HTTPS 解决了什么问题?【网络】
HTTPS 解决了什么问题
一个简单的回答可能会是 HTTP 它不安全。由于 HTTP 天生明文传输的特性,在 HTTP 的传输过程中,任何人都有可能从中截获、修改或者伪造请求发送,所以可以认为 HTTP 是不安全的;在 HTTP 的传输过程中不会验证通信方的身份,因此 HTTP 信息交换的双方可能会遭到伪装,也就是没有用户验证;在 HTTP 的传输过程中,接收方和发送方并不会验证报文的完整性,综上,为了结局上述问题,HTTPS 应用而生。
什么是 HTTPS
你还记得 HTTP 是怎么定义的吗?HTTP 是一种 超文本传输协议(Hypertext Transfer Protocol) 协议,它 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范,那么我们看一下 HTTPS 是如何定义的
HTTPS 的全称是 Hypertext Transfer Protocol Secure,它用来在计算机网络上的两个端系统之间进行安全的交换信息(secure communication),它相当于在 HTTP 的基础上加了一个 Secure 安全的词眼,那么我们可以给出一个 HTTPS 的定义:HTTPS 是一个在计算机世界里专门在两点之间安全的传输文字、图片、音频、视频等超文本数据的约定和规范。 HTTPS 是 HTTP 协议的一种扩展,它本身并不保传输的证安全性,那么谁来保证安全性呢?在 HTTPS 中,使用传输层安全性(TLS)或安全套接字层(SSL)对通信协议进行加密。也就是 HTTP + SSL(TLS) = HTTPS。
HTTPS 做了什么
HTTPS 协议提供了三个关键的指标
加密(Encryption), HTTPS 通过对数据加密来使其免受窃听者对数据的监听,这就意味着当用户在浏览网站时,没有人能够监听他和网站之间的信息交换,或者跟踪用户的活动,访问记录等,从而窃取用户信息。数据一致性(Data integrity),数据在传输的过程中不会被窃听者所修改,用户发送的数据会完整的传输到服务端,保证用户发的是什么,服务器接收的就是什么。身份认证(Authentication),是指确认对方的真实身份,也就是证明你是你(可以比作人脸识别),它可以防止中间人攻击并建立用户信任。
有了上面三个关键指标的保证,用户就可以和服务器进行安全的交换信息了。那么,既然你说了 HTTPS 的种种好处,那么我怎么知道网站是用 HTTPS 的还是 HTTP 的呢?给你两幅图应该就可以解释了。
HTTPS 协议其实非常简单,RFC 文档很小,只有短短的 7 页,里面规定了新的协议名,默认端口号443,至于其他的应答模式、报文结构、请求方法、URI、头字段、连接管理等等都完全沿用 HTTP,没有任何新的东西。
也就是说,除了协议名称和默认端口号外(HTTP 默认端口 80),HTTPS 协议在语法、语义上和 HTTP 一样,HTTP 有的,HTTPS 也照单全收。那么,HTTPS 如何做到 HTTP 所不能做到的安全性呢?关键在于这个 S 也就是 SSL/TLS 。
149.HTTPS 中的 SSL/TLS 是什么?【网络】
什么是 SSL/TLS
认识 SSL/TLS
TLS(Transport Layer Security) 是 SSL(Secure Socket Layer) 的后续版本,它们是用于在互联网两台计算机之间用于身份验证和加密的一种协议。
注意:在互联网中,很多名称都可以进行互换。
我们都知道一些在线业务(比如在线支付)最重要的一个步骤是创建一个值得信赖的交易环境,能够让客户安心的进行交易,SSL/TLS 就保证了这一点,SSL/TLS 通过将称为 X.509 证书的数字文档将网站和公司的实体信息绑定到加密密钥来进行工作。每一个密钥对(key pairs) 都有一个 私有密钥(private key) 和 公有密钥(public key),私有密钥是独有的,一般位于服务器上,用于解密由公共密钥加密过的信息;公有密钥是公有的,与服务器进行交互的每个人都可以持有公有密钥,用公钥加密的信息只能由私有密钥来解密。
什么是
X.509:X.509 是公开密钥证书的标准格式,这个文档将加密密钥与(个人或组织)进行安全的关联。X.509 主要应用如下
SSL/TLS 和 HTTPS 用于经过身份验证和加密的 Web 浏览
通过 S/MIME 协议签名和加密的电子邮件
代码签名:它指的是使用数字证书对软件应用程序进行签名以安全分发和安装的过程。
通过使用由知名公共证书颁发机构(例如SSL.com)颁发的证书对软件进行数字签名,开发人员可以向最终用户保证他们希望安装的软件是由已知且受信任的开发人员发布;并且签名后未被篡改或损害。
- 还可用于文档签名
- 还可用于客户端认证
- 政府签发的电子身份证(详见 www.ssl.com/article/pki…
我们后面还会讨论。
HTTPS 的内核是 HTTP
HTTPS 并不是一项新的应用层协议,只是 HTTP 通信接口部分由 SSL 和 TLS 替代而已。通常情况下,HTTP 会先直接和 TCP 进行通信。在使用 SSL 的 HTTPS 后,则会先演变为和 SSL 进行通信,然后再由 SSL 和 TCP 进行通信。也就是说,HTTPS 就是身披了一层 SSL 的 HTTP。(我都喜欢把骚粉留在最后。。。)
SSL 是一个独立的协议,不只有 HTTP 可以使用,其他应用层协议也可以使用,比如 SMTP(电子邮件协议)、Telnet(远程登录协议) 等都可以使用。
154.常见的前端内存泄露场景有哪些?【JavaScript、浏览器】
大多数情况下,垃圾回收器会帮我们及时释放内存,一般不会发生内存泄漏。但是有些场景是内存泄漏的高发区,我们在使用的时候一定要注意:
-
我们在开发的时候经常会使用
console在控制台打印信息,但这也会带来一个问题:被console使用的对象是不能被垃圾回收的,这就可能会导致内存泄漏。因此在生产环境中不建议使用console.log()的理由就又可以加上一条避免内存泄漏了。 -
被全局变量、全局函数引用的对象,在Vue组件销毁时未清除,可能会导致内存泄漏
// Vue3 <script setup> import {onMounted, onBeforeUnmount, reactive} from 'vue' const arr = reactive([1,2,3]); onMounted(() => { window.arr = arr; // 被全局变量引用 window.arrFunc = () => { console.log(arr); // 被全局函数引用 } }) // 正确的方式 onBeforeUnmount(() => { window.arr = null; window.arrFunc = null; }) </script> -
定时器未及时在Vue组件销毁时清除,可能会导致内存泄漏
// Vue3 <script setup> import {onMounted, onBeforeUnmount, reactive} from 'vue' const arr = reactive([1,2,3]); const timer = reactive(null); onMounted(() => { setInterval(() => { console.log(arr); // arr被定时器占用,无法被垃圾回收 }, 200); // 正确的方式 timer = setInterval(() => { console.log(arr); }, 200); }) // 正确的方式 onBeforeUnmount(() => { if (timer) { clearInterval(timer); timer = null; } }) </script>setTimeout和setInterval两个定时器在使用时都应该注意是否需要清理定时器,特别是setInterval,一定要注意清除。 -
绑定的事件未及时在Vue组件销毁时清除,可能会导致内存泄漏
绑定事件在实际开发中经常遇到,我们一般使用
addEventListener来创建。// Vue3 <script setup> import {onMounted, onBeforeUnmount, reactive} from 'vue' const arr = reactive([1,2,3]); const printArr = () => { console.log(arr) } onMounted(() => { // 监听事件绑定的函数为匿名函数,将无法被清除 window.addEventListener('click', () => { console.log(arr); // 全局绑定的click事件,arr被引用,将无法被垃圾回收 }) // 正确的方式 window.addEventListener('click', printArr); }) // 正确的方式 onBeforeUnmount(() => { // 注意清除绑定事件需要前后是同一个函数,如果函数不同将不会清除 window.removeEventListener('click', printArr); }) </script> -
被自定义事件引用,在Vue组件销毁时未清除,可能会导致内存泄漏
自定义事件通过
emit/on来发起和监听,清除自定义事件和绑定事件差不多,不同的是需要调用off方法// Vue3 <script setup> import {onMounted, onBeforeUnmount, reactive} from 'vue' import event from './event.js'; // 自定义事件 const arr = reactive([1,2,3]); const printArr = () => { console.log(arr) } onMounted(() => { // 使用匿名函数,会导致自定义事件无法被清除 event.on('printArr', () => { console.log(arr) }) // 正确的方式 event.on('printArr', printArr) }) // 正确的方式 onBeforeUnmount(() => { // 注意清除自定义事件需要前后是同一个函数,如果函数不同将不会清除 event.off('printArr', printArr) }) </script>
除了及时清除监听器、事件等,对于全局变量的引用,我们可以选择WeakMap、WeakSet等弱引用数据类型。这样的话,即使我们引用的对象数据要被垃圾回收,弱引用的全局变量并不会阻止GC。
156.实现 table header 吸顶, 有哪些实现方式?【CSS】
实现 table header 吸顶的方法有多种,以下是一些基于 CSS 的实现方式:
-
使用 position: sticky 属性:在表格头部的 CSS 中,使用 position: sticky 属性可以使表格头部保持在视窗的顶部或底部,而不会随着滚动而消失。例如:
cssCopy codeth { position: sticky; top: 0; background-color: #fff; } -
使用 CSS transform 属性:在表格头部的 CSS 中,使用 CSS transform 属性可以使表格头部保持固定位置,而不会随着滚动而消失。例如:
cssCopy codeth { position: relative; z-index: 1; } thead { position: fixed; top: 0; visibility: hidden; z-index: 2; transform: translateY(0); } tbody { margin-top: 50px; } -
使用 JavaScript 和 CSS:使用 JavaScript 和 CSS 可以使表格头部保持在视窗的顶部或底部,而不会随着滚动而消失。例如:
htmlCopy code<div class="table-wrapper"> <table> <thead> <tr> <th>Column 1</th> <th>Column 2</th> <th>Column 3</th> </tr> </thead> <tbody> <tr> <td>Row 1, Column 1</td> <td>Row 1, Column 2</td> <td>Row 1, Column 3</td> </tr> <tr> <td>Row 2, Column 1</td> <td>Row 2, Column 2</td> <td>Row 2, Column 3</td> </tr> ... </tbody> </table> </div> <script> window.onscroll = function() { var header = document.querySelector(".table-wrapper thead"); if (window.pageYOffset > 150) { header.classList.add("sticky"); } else { header.classList.remove("sticky"); } }; </script> <style> .table-wrapper { position: relative; } .table-wrapper thead { position: fixed; top: 0; z-index: 1; background-color: #fff; } .table-wrapper th { height: 50px; } .table-wrapper.sticky thead { position: absolute; top: 50px; } </style>
通过以上方法的一些组合使用,可以实现 table header 吸顶,提升表格的用户体验和易用性。
159.[Vue] 父子组件通信方式有哪些?【web框架】
Vue 父子组件通信
- Prop(常用)
- $emit (组件封装用的较多)
- .sync语法糖 (较少)
- attrs & listeners (组件封装用的较多)
- provide & inject (高阶组件/组件库用的较多)
- slot-scope & v-slot (vue@2.6.0+)新增
- scopedSlots 属性
- 其他方式通信
具体使用场景参考链接:juejin.cn/post/684490…
160.什么是洋葱模型?【web框架】
说到洋葱模型,就必须聊一聊中间件,中间件这个概念,我们并不陌生,比如平时我们用的 redux、express 、koa 这些库里,都离不开中间件。
那 koa 里面的中间件是什么样的呢?其本质上是一个函数,这个函数有着特定,单一的功能,koa将一个个中间件注册进来,通过组合实现强大的功能。
先看 demo :
// index.js
const Koa = require("koa")
const app = new Koa();
// 中间件1
app.use(async (ctx, next) => {
console.log("1")
await next()
console.log("2")
});
// 中间件2
app.use(async (ctx, next) => {
console.log("3")
await next()
console.log("4")
});
// 中间件3
app.use(async (ctx, next) => {
console.log("5")
await next()
console.log("6")
});
app.listen(8002);
先后注册了三个中间件,运行一下index.js ,可以看到输出结果为:
1
3
5
6
4
2
没接触过洋葱模型的人第一眼可能会疑惑,为什么调用了一个 next 之后,直接从1 跳到了 3 ,而不是先输出1 ,再输出2呢。 其实这就是洋葱模型特点,下图是它的执行过程:
一开始我们先后注册了三个中间件,分别是中间件1,中间件2,中间件3,调用
listen方法,打开对应端口的页面,触发了中间件的执行。
首先会先执行第一个中间件的 next 的前置语句,相当于 demo 里面的 console.log('1') ,当调用 next() 之后,会直接进入第二个中间件,继续重复上述逻辑,直至最后一个中间件,就会执行 next 的后置语句,然后继续上一个中间件的后置语句,继续重复上述逻辑,直至执行第一个中间件的后置语句,最后输出。
164.[koa] 中间件的异常处理是怎么做的?【web框架】
在 Koa 中,中间件函数的异常处理可以通过两种方式来实现:
- 使用
try...catch捕获异常:在中间件函数中使用try...catch语句来捕获异常,然后通过ctx.throw()方法抛出异常信息,例如:
vbnetCopy codeasync function myMiddleware(ctx, next) {
try {
await next();
} catch (err) {
ctx.throw(500, 'Internal Server Error');
}
}
在这个例子中,await next() 表示调用下一个中间件函数,如果这个函数抛出异常,就会被捕获到,然后通过 ctx.throw() 方法抛出一个包含错误状态码和错误信息的异常。
- 使用 Koa 的错误处理中间件:Koa 提供了一个错误处理中间件
koa-json-error,可以通过在应用程序中使用该中间件来处理异常。这个中间件会自动捕获应用程序中未被处理的异常,并将错误信息以 JSON 格式返回给客户端。例如:
const Koa = require('koa');
const jsonError = require('koa-json-error');
const app = new Koa();
// 注册错误处理中间件
app.use(jsonError());
// 中间件函数
async function myMiddleware(ctx, next) {
await next();
throw new Error('Internal Server Error');
}
// 应用中间件
app.use(myMiddleware);
// 启动服务器
app.listen(3000);
在这个例子中,koa-json-error 中间件会自动捕获应用程序中未被处理的异常,并将错误信息以 JSON 格式返回给客户端。开发人员可以通过自定义错误处理函数来处理异常,例如:
const Koa = require('koa');
const jsonError = require('koa-json-error');
const app = new Koa();
// 自定义错误处理函数
function errorHandler(err, ctx) {
ctx.status = err.status || 500;
ctx.body = {
message: err.message,
status: ctx.status
};
}
// 注册错误处理中间件
app.use(jsonError(errorHandler));
// 中间件函数
async function myMiddleware(ctx, next) {
await next();
throw new Error('Internal Server Error');
}
// 应用中间件
app.use(myMiddleware);
// 启动服务器
app.listen(3000);
在这个例子中,我们自定义了一个错误处理函数 errorHandler,将错误信息格式化为 JSON 格式,并设置响应状态码。然后将这个函数作为参数传递给 koa-json-error 中间件,用于处理异常。
173.为什么小程序里拿不到dom相关的api【web框架】
ES6 中的装饰器是一种特殊的语法,用于动态修改类的行为。在 JavaScript 中,装饰器本质上是一个函数,它可以接受一个类作为参数,并返回一个新的类,实现了类的增强或修改。装饰器可以被用于类、方法、属性等各种地方,可以方便地实现类似 AOP、元编程等功能。
装饰器是 ES7 中的一个提案,目前还没有正式纳入标准。在 ES6 中使用装饰器需要借助第三方库,如 babel-plugin-transform-decorators-legacy。
装饰器实现的基本原理是,在装饰器函数和被装饰对象之间建立一个代理层,通过代理层来实现装饰器的逻辑。在类的装饰器中,装饰器函数的第一个参数是被装饰的类本身,装饰器函数内部可以访问、修改该类的属性和方法。在方法和属性的装饰器中,装饰器函数的第一个参数分别是被装饰的方法或属性所在的类的原型对象,装饰器函数内部可以访问、修改该方法或属性的属性描述符等信息。
以下是一个简单的装饰器示例,用于给类的方法添加一个计时器:
function timer(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
console.time(name);
const result = originalMethod.apply(this, args);
console.timeEnd(name);
return result;
};
return descriptor;
}
class MyClass {
@timer
myMethod() {
// do something
}
}
在上面的示例中,timer 函数就是一个装饰器函数,它接受三个参数,分别是被装饰的方法所在类的原型对象、被装饰的方法的名称、被装饰的方法的属性描述符。在 timer 函数内部,将被装饰的方法替换为一个新的方法,新方法先执行 console.time() 方法,再执行原始方法,最后执行 console.timeEnd() 方法。最后将新的属性描述符返回,完成方法的装饰。
通过类似这种方式,我们可以方便地实现各种类型的装饰器,以增强或修改类的行为。
178.[React] useRef、ref、forwardsRef 的区别是什么?【web框架】
在 React 中,ref 是一种用于访问 DOM 元素或组件实例的方法,useRef 和 forwardRef 是 ref 的两个相关 Hook 和高阶组件。
ref:ref是 React 中用于访问 DOM 元素或组件实例的方法。在函数组件中,可以使用useRefHook 来创建一个ref对象,然后将其传递给需要引用的元素或组件。在类组件中,可以直接在类中定义ref属性,并将其设置为元素或组件的实例。useRef:useRef是 React 中的 Hook,用于创建一个ref对象,并在组件生命周期内保持其不变。useRef可以用于访问 DOM 元素或组件实例,并且在每次渲染时都会返回同一个ref对象。通常情况下,useRef更适合用于存储不需要触发重新渲染的值,例如定时器的 ID 或者其他副作用。forwardRef:forwardRef是一个高阶组件,用于将ref属性转发给其子组件。通常情况下,如果一个组件本身并不需要使用ref属性,但是其子组件需要使用ref属性,那么可以使用forwardRef来传递ref属性。forwardRef接受一个函数作为参数,并将ref对象作为第二个参数传递给该函数,然后返回一个新的组件,该组件接受ref属性并将其传递给子组件。
简而言之,ref 是 React 中访问 DOM 元素或组件实例的方法,useRef 是一个 Hook,用于创建并保持一个不变的 ref 对象,forwardRef 是一个高阶组件,用于传递 ref 属性给子组件。
高级开发者相关问题【共计 11 道题】
152.页面崩溃如何监控?【网络】
页面崩溃如何监控?
对于 web 页面线上监控,如果页面崩溃了,通常会出现 500 或 404 状态码,或者页面停止响应或显示白屏等情况。
以下是一些监控崩溃的方法:
- 使用网站性能监测工具:这些工具可以检测页面的状态码和响应时间,如果页面崩溃了,就会发出警报。一些流行的性能监测工具包括 New Relic, Pingdom, 和 UptimeRobot 等。
- 设置异常检测:异常检测可以监测页面异常的行为,例如页面响应时间超过特定时间限制,或者页面元素加载失败等。通过设置这些异常检测,可以在页面崩溃时自动触发警报。
- 实时用户行为监测:实时监测用户行为可以帮助识别用户在页面上的行为,例如页面停留时间,点击按钮的位置等,以便检测页面异常行为。这些监测可以使用 Google Analytics, Mixpanel 等网站分析工具实现。
- 前端代码错误监测:使用前端监测工具,例如 Sentry, Raygun, 和 Bugsnag 等,可以监测前端代码错误,包括 JavaScript 和 CSS 错误,以便快速识别和解决问题。
通过以上方法的一些组合使用,可以帮助您监控 web 页面的崩溃,及时发现和解决问题,提升用户体验和网站可靠性。
如果是页面运行时页面崩溃, 如何监控?
如果在运行时发生页面崩溃,可以使用以下方法进行监控:
- 实时监控日志:可以设置日志监控,将日志实时发送到日志收集工具,例如 ELK Stack、Splunk 等。这些工具可以分析和提取有关页面崩溃的信息,例如错误消息、堆栈跟踪等,以便快速识别和解决问题。
- 页面截图:当页面崩溃时,可以使用截图工具进行截屏,以捕获页面的当前状态。这些截图可以用于快速检查页面崩溃的根本原因。
- 人工检测:可以雇用专业的质量测试人员或专业服务公司进行页面质量测试,以便在页面崩溃时进行手动检测和识别。
- 实时异常检测:实时监测页面异常的行为,例如页面响应时间超过特定时间限制,或者页面元素加载失败等。通过设置这些异常检测,可以在页面崩溃时自动触发警报。
通过以上方法的一些组合使用,可以帮助您在运行时监控 web 页面的崩溃,及时发现和解决问题,提升用户体验和网站可靠性。
153.如何监控前端页面内存持续增长情况?【网络】
监控前端页面内存持续增长可以帮助我们及时发现内存泄漏和其他内存问题,从而优化前端页面的性能和稳定性。以下是一些监控前端页面内存持续增长的方法:
- 使用浏览器开发工具:现代浏览器的开发工具提供了内存监控功能。您可以使用 Chrome 开发者工具、Firefox 开发者工具等浏览器工具来监控内存的使用情况,并在内存使用超过阈值时进行警报。
- 手动检查页面代码:您可以手动检查页面的代码,特别是 JavaScript 代码和其他 DOM 操作,以查找可能导致内存泄漏的问题。例如,可能存在未清理的定时器、事件监听器、未释放的 DOM 元素等。
- 使用性能监测工具:性能监测工具,例如 New Relic、AppDynamics 等,可以监测前端页面的性能,并提供关于内存使用的警报和报告。
- 使用内存检测工具:内存检测工具,例如 memoryjs、heapdump.js 等,可以帮助检测内存泄漏和内存问题。这些工具可以生成内存快照,分析内存使用情况,以及识别潜在的内存泄漏问题。
通过以上方法的一些组合使用,可以帮助您监控前端页面内存持续增长的情况,及时发现和解决内存问题,提升用户体验和网站可靠性。
155.常见的前端检测内存泄露的方法有哪些?【JavaScript、浏览器】
怎么检测内存泄漏
内存泄漏主要是指的是内存持续升高,但是如果是正常的内存增长的话,不应该被当作内存泄漏来排查。排查内存泄漏,我们可以借助Chrome DevTools的Performance和Memory选项。举个栗子:
我们新建一个memory.html的文件,完整代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
text-align: center;
}
</style>
</head>
<body>
<p>检测内存变化</p>
<button id="btn">开始</button>
<script>
const arr = [];
// 数组中添加100万个数据
for (let i = 0; i < 100 * 10000; i++) {
arr.push(i)
}
function bind() {
const obj = {
str: JSON.stringify(arr) // 浅拷贝的方式创建一个比较大的字符串
}
// 每次调用bind函数,都在全局绑定一个onclick监听事件,不一定非要执行
// 使用绑定事件,主要是为了保持obj被全局标记
window.addEventListener('click', () => {
// 引用对象obj
console.log(obj);
})
}
let n = 0;
function start() {
setTimeout(() => {
bind(); // 调用bind函数
n++; // 循环次数增加
if (n < 50) {
start(); // 循环执行50次,注意这里并没有使用setInterval定时器
} else {
alert('done');
}
}, 200);
}
document.getElementById('btn').addEventListener('click', () => {
start();
})
</script>
</body>
</html>
页面上有一个按钮用来开始函数调用,方便我们控制。点击按钮,每个200毫秒执行一次bind函数,即在全局监听click事件,循环次数为50次。
在无法确定是否发生内存泄漏时,我们可以先使用Performance来录制一段页面加载的性能变化,先判断是否有内存泄漏发生。
Performance
本次案例仅以Chrome浏览器展开描述,其他浏览器可能会有些许差异。首先我们鼠标右键选择检查或者直接F12进入DevTools页面,面板上选择Performance,选择后应该是如下页面:
在开始之前,我们先点击一下Collect garbage和clear来保证内存干净,没有其他遗留内存的干扰。然后我们点击Record来开始录制,并且同时我们也要点击页面上的开始按钮,让我们的代码跑起来。等到代码结束后,我们再点击Record按钮以停止录制,录制的时间跟代码执行的时间相比会有出入,只要保证代码是完全执行完毕的即可。停止录制后,我们会得到如下的结果:
Performance的内容很多,我们只需要关注内存的变化,由此图可见,内存这块区域的曲线是在一直升高的并且到达顶点后并没有回落,这就有可能发生了内存泄漏。因为正常的内存变化曲线应该是类似于“锯齿”,也就是有上有下,正常增长后会有一定的回落,但不一定回落到和初始值一样。而且我们还可以隐约看到程序运行结束后,内存从初始的6.2MB增加到了差不多351MB,这个数量级的增加还是挺明显的。我们只是执行了50次循环,如果执行的次数更多,将会耗尽浏览器的内存空间,导致页面卡死。
虽然是有内存泄漏,但是如果我们想进一步看内存泄漏发生的地方,那么Performance就不够用了,这个时候我们就需要使用Memory面板。
Memory
DevTools的Memory选项主要是用来录制堆内存的快照,为的是进一步分析内存泄漏的详细信息。有人可能会说,为啥不一开始就直接使用Memory呢,反而是先使用Performance。因为我们刚开始就说了,内存增长不表示就一定出现了内存泄漏,有可能是正常的增长,直接使用Memory来分析可能得不到正确的结果。
我们先来看一下怎么使用Memory:
首先选择Memory选项,然后清除缓存,在配置选项中选择堆内存快照。内存快照每次点击录制按钮都会记录当前的内存使用情况,我们可以在程序开始前点击一下记录初始的内存使用,代码结束后再点一下记录最终的内存使用,中间可以点击也可以不点击。最后在快照列表中至少可以得到两个内存记录:
初始内存我们暂时不深究,我们选择列表的最后一条记录,然后在筛选下拉框选择最后一个,即第一个快照和第二个快照的差异。
这里我们重点说一下Shallow Size和Retained Size的区别:
- Shallow Size:对象自身占用的内存大小,一般来说字符串、数组的Shallow Size都会比较大
- Retained Size:这个是对象自身占用的内存加上无法被GC释放的内存的大小,如果Retained Size和Shallow Size相差不大,基本上可以判定没有发生内存泄漏,但是如果相差很大,例如上图的
Object,这就表明发生了内存泄漏。
我们再来细看一下Object,任意展开一个对象,可以在树结构中发现每一个对象都有一个全局事件绑定,并且占用了较大的内存空间。解决本案例涉及的内存泄漏也比较简单,就是及时释放绑定的全局事件。
关于Performance和Memory的详细使用可以参考:手把手教你排查Javascript内存泄漏
161.如何实现洋葱模式?【web框架】
思路
- 首先调用
use方法收集中间件,调用listen方法执行中间件。 - 每一个中间件都有一个
next参数(暂时不考虑ctx参数),next参数可以控制进入下一个中间件的时机。
需要解决的问题
- 最后一个中间件调用next如何处理
- 如何解决同一个中间件多次调用next
完整代码
其中最精华的部分就是compose函数,细数一下,只有11行代码,1比1还原了koa的compose函数(去除了不影响主逻辑判断)。
koa是利用koa-compose这个库进行组合中间件的,在koa-compose里面,next返回的都是一个promise函数。
function Koa () {
this.middleares = [];
}
Koa.prototype.use = function (middleare) {
this.middleares.push(middleare);
return this;
}
Koa.prototype.listen = function () {
const fn = compose(this.middleares);
}
function compose(middleares) {
let index = -1;
const dispatch = (i) => {
if(i <= index) throw new Error('next() 不能调用多次');
index = i;
if(i >= middleares.length) return;
const middleare = middleares[i];
return middleare('ctx', dispatch.bind(null, i + 1));
}
return dispatch(0);
}
const app = new Koa();
app.use(async (ctx, next) => {
console.log('1');
next();
console.log('2');
});
app.use(async (ctx, next) => {
console.log('3');
next();
console.log('4');
});
app.use(async (ctx, next) => {
console.log('5');
next();
console.log('6');
});
app.listen();
使用
const Koa = require('koa');
const app = new Koa();
// 中间件过多,可以创建一个middleares文件夹,将cors函数放到middleares/cors.js文件里面
const cors = () => {
return async (ctx, next) => {
ctx.set('Access-Control-Allow-Headers', 'X-Requested-With')
ctx.set('Access-Control-Allow-Origin', '*')
ctx.set('Access-Control-Allow-Methods', 'GET,HEAD,PUT,POST,DELETE,PATCH')
await next();
}
};
app.use(cors());
app.use(async (ctx, next) => {
console.log('第一个中间件', ctx.request.method,ctx.request.url);
await next();
ctx.body = 'hello world'
});
koa的中间件都是有固定模板的,首先是一个函数,并且返回一个async函数(闭包的应用),这个async函数有两个参数,一个是koa的context,一个是next函数。
168.[koa] 在没有async await 的时候, koa是怎么实现的洋葱模型?【web框架】
在没有 async/await 的时候,Koa 通过使用 ES6 的生成器函数来实现洋葱模型。具体来说,Koa 中间件函数是一个带有 next 参数的生成器函数,当中间件函数调用 next 方法时,它会挂起当前的执行,转而执行下一个中间件函数,直到执行完最后一个中间件函数,然后将执行权返回到前一个中间件函数,继续执行下面的代码。这个过程就像一层一层剥开洋葱一样,因此被称为洋葱模型。
下面是一个使用生成器函数实现的简单的 Koa 中间件函数:
function* myMiddleware(next) {
// 中间件函数的代码
console.log('Start');
yield next;
console.log('End');
}
在这个中间件函数中,yield next 表示挂起当前的执行,执行下一个中间件函数。假设我们有两个中间件函数 middleware1 和 middleware2,它们的代码如下:
function* middleware1(next) {
console.log('middleware1 Start');
yield next;
console.log('middleware1 End');
}
function* middleware2(next) {
console.log('middleware2 Start');
yield next;
console.log('middleware2 End');
}
我们可以使用 compose 函数将它们组合成一个洋葱模型:
scssCopy codeconst compose = require('koa-compose');
const app = compose([middleware1, middleware2]);
app();
在这个例子中,compose 函数将 middleware1 和 middleware2 组合成一个函数 app,然后调用这个函数即可执行整个中间件链。执行的结果如下:
sqlCopy codemiddleware1 Start
middleware2 Start
middleware2 End
middleware1 End
可以看到,这个结果与洋葱模型的特点相符。
169.[koa] body-parser 中间件实现原理?【web框架】
Koa 中间件 koa-bodyparser 的原理是将 HTTP 请求中的 request body 解析成 JavaScript 对象,并将其挂载到 ctx.request.body 属性上,方便后续的处理。
具体来说,koa-bodyparser 中间件会监听 HTTP 请求的 data 事件和 end 事件,然后将请求中的数据流解析成一个 JavaScript 对象,并将其作为参数传递给 ctx.request.body 属性,最后调用 await next(),将控制权交给下一个中间件。
在实现过程中,koa-bodyparser 中间件会根据请求头中的 Content-Type 字段来判断请求体的类型,支持解析的请求体类型有 application/json、application/x-www-form-urlencoded 和 multipart/form-data。对于其他类型的请求体,koa-bodyparser 会将其解析成一个空对象 {}。
下面是一个简单的 koa-bodyparser 中间件的实现示例:
function bodyParser() {
return async (ctx, next) => {
if (ctx.request.method === 'POST' || ctx.request.method === 'PUT') {
let data = '';
ctx.req.on('data', (chunk) => {
data += chunk;
});
ctx.req.on('end', () => {
if (ctx.request.headers['content-type'] === 'application/json') {
ctx.request.body = JSON.parse(data);
} else if (ctx.request.headers['content-type'] === 'application/x-www-form-urlencoded') {
ctx.request.body = querystring.parse(data);
} else if (ctx.request.headers['content-type'].startsWith('multipart/form-data')) {
// 解析 multipart/form-data 请求体
// ...
} else {
ctx.request.body = {};
}
return next();
});
} else {
return next();
}
};
}
在这个实现中,如果请求方法为 POST 或者 PUT,则开始监听 data 事件和 end 事件,将请求体数据解析成一个 JavaScript 对象并挂载到 ctx.request.body 上,最后调用 next() 将控制权交给下一个中间件。对于其他请求方法,则直接调用 next() 交给下一个中间件处理。注意,这个实现只支持解析 application/json 和 application/x-www-form-urlencoded 类型的请求体,对于其他类型的请求体需要进行特殊处理。
170.文件上传和上传文件解析的原理是啥?【网络、浏览器】
前端如何上传图片等文件
在前端上传图片或其他文件字段,一般使用 HTML 中的 <input> 元素,并设置 type="file",这样就可以让用户选择本地的文件进行上传。
例如:
htmlCopy code<form enctype="multipart/form-data">
<input type="file" name="image" accept="image/*">
<input type="submit" value="上传">
</form>
在这个例子中,<form> 元素的 enctype 属性设置为 multipart/form-data,这样就可以将文件作为二进制数据进行上传。
当用户选择了本地的文件后,提交表单时就会将文件作为 FormData 对象的一部分进行发送。在 JavaScript 中,可以通过 FormData 构造函数来创建一个 FormData 对象,并使用 append() 方法向其中添加上传的文件数据。例如:
const formData = new FormData();
const fileInput = document.querySelector('input[type="file"]');
formData.append('image', fileInput.files[0]);
这样就可以将文件数据添加到 formData 对象中,然后使用 fetch() 方法或其他方式将其发送到后端进行处理。
需要注意的是,由于安全原因,浏览器限制了用户上传文件的大小和类型。可以使用 accept 属性来限制文件的类型,例如 accept="image/*" 表示只允许上传图片类型的文件。可以使用 multiple 属性来允许用户选择多个文件进行上传。同时,还需要在后端对上传的文件进行处理和验证,以确保安全性和正确性。
后端如何解析?koa 为例
在 Koa 中解析上传的文件需要使用一个叫做 koa-body 的中间件,它可以自动将 multipart/form-data 格式的请求体解析成 JavaScript 对象,从而获取到上传的文件和其他表单数据。
以下是一个使用 koa-body 中间件解析上传文件的例子:
const Koa = require('koa');
const koaBody = require('koa-body');
const app = new Koa();
// 注册 koa-body 中间件
app.use(koaBody({
multipart: true, // 支持上传文件
}));
// 处理上传文件的请求
app.use(async (ctx) => {
const { files, fields } = ctx.request.body; // 获取上传的文件和其他表单数据
const file = files && files.image; // 获取上传的名为 image 的文件
if (file) {
console.log(`Received file: ${file.name}, type: ${file.type}, size: ${file.size}`);
// 处理上传的文件
} else {
console.log('No file received');
}
// 返回响应
ctx.body = 'Upload success';
});
app.listen(3000);
在上述代码中,使用 koa-body 中间件注册了一个解析请求体的函数,并在请求处理函数中获取到了上传的文件和其他表单数据。其中,files 对象包含了所有上传的文件,fields 对象包含了所有非文件类型的表单数据。
可以根据实际需要从 files 对象中获取到需要处理的文件,例如上面的例子中使用了 files.image 来获取名为 image 的上传文件。可以使用上传文件的属性,如 name、type 和 size 来获取文件的信息,并进行处理。最后返回响应,表示上传成功。
需要注意的是,koa-body 中间件需要设置 multipart: true 才能支持上传文件。另外,在处理上传文件时需要注意安全性和正确性,可以使用第三方的文件上传处理库来进行处理。
解析上传文件的原理是啥?
在 HTTP 协议中,上传文件的请求通常使用 multipart/form-data 格式的请求体。这种格式的请求体由多个部分组成,每个部分以一个 boundary 字符串作为分隔符,每个部分都代表一个字段或一个文件。
对于一个上传文件的请求,浏览器会将请求体按照 multipart/form-data 格式构造,其中每个部分都有一些描述信息和内容,例如文件名、文件类型、文件大小、内容等。
服务器端需要对这些部分进行解析,提取出所需要的信息。常见的解析方式有两种:
- 手动解析:根据
multipart/form-data格式的规范,按照 boundary 字符串将请求体切分为多个部分,然后解析每个部分的头部和内容,提取出文件名、文件类型、文件大小等信息。这种方式比较麻烦,需要手动处理较多的细节,容易出错。 - 使用第三方库:可以使用第三方的解析库,如
multer、formidable、busboy等,来方便地解析multipart/form-data格式的请求体。这些库通常会将解析出的信息存储到一个对象中,方便进一步处理。
在 Node.js 中,使用 http 模块自己实现 multipart/form-data 的解析比较麻烦,常见的做法是使用第三方库来解析上传文件,例如在 Koa 中使用 koa-body 中间件就可以方便地处理上传文件。
172.es6 class 装饰器是如何实现的?【JavaScript】
ES6 中的装饰器是一种特殊的语法,用于动态修改类的行为。在 JavaScript 中,装饰器本质上是一个函数,它可以接受一个类作为参数,并返回一个新的类,实现了类的增强或修改。装饰器可以被用于类、方法、属性等各种地方,可以方便地实现类似 AOP、元编程等功能。
装饰器是 ES7 中的一个提案,目前还没有正式纳入标准。在 ES6 中使用装饰器需要借助第三方库,如 babel-plugin-transform-decorators-legacy。
装饰器实现的基本原理是,在装饰器函数和被装饰对象之间建立一个代理层,通过代理层来实现装饰器的逻辑。在类的装饰器中,装饰器函数的第一个参数是被装饰的类本身,装饰器函数内部可以访问、修改该类的属性和方法。在方法和属性的装饰器中,装饰器函数的第一个参数分别是被装饰的方法或属性所在的类的原型对象,装饰器函数内部可以访问、修改该方法或属性的属性描述符等信息。
以下是一个简单的装饰器示例,用于给类的方法添加一个计时器:
function timer(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args) {
console.time(name);
const result = originalMethod.apply(this, args);
console.timeEnd(name);
return result;
};
return descriptor;
}
class MyClass {
@timer
myMethod() {
// do something
}
}
在上面的示例中,timer 函数就是一个装饰器函数,它接受三个参数,分别是被装饰的方法所在类的原型对象、被装饰的方法的名称、被装饰的方法的属性描述符。在 timer 函数内部,将被装饰的方法替换为一个新的方法,新方法先执行 console.time() 方法,再执行原始方法,最后执行 console.timeEnd() 方法。最后将新的属性描述符返回,完成方法的装饰。
通过类似这种方式,我们可以方便地实现各种类型的装饰器,以增强或修改类的行为。
174.Promise then 第二个参数和 Promise.catch 的区别是什么?【JavaScript】
Promise.then() 方法可以接受两个参数,第一个参数是 onFulfilled 回调函数,第二个参数是 onRejected 回调函数。当 Promise 状态变为 fulfilled 时,将会调用 onFulfilled 回调函数;当 Promise 状态变为 rejected 时,将会调用 onRejected 回调函数。其中,第二个参数 onRejected 是可选的。
Promise.catch() 方法是一个特殊的 Promise.then() 方法,它只接受一个参数,即 onRejected 回调函数。如果 Promise 状态变为 rejected,则会调用 onRejected 回调函数;如果状态变为 fulfilled,则不会调用任何回调函数。因此,Promise.catch() 方法可以用来捕获 Promise 中的错误,相当于使用 Promise.then(undefined, onRejected)。
区别主要在于使用的方式不同。Promise.then(onFulfilled, onRejected) 可以同时传递两个回调函数,用来处理 Promise 状态变为 fulfilled 或者 rejected 的情况;而 Promise.catch(onRejected) 则只能用来处理 Promise 状态变为 rejected 的情况,并且使用更加简洁明了。
175.Promise finally 怎么实现的?【JavaScript】
Promise.finally() 方法是在 ES2018 中引入的,用于指定不管 Promise 状态如何都要执行的回调函数。与 Promise.then() 和 Promise.catch() 不同的是,Promise.finally() 方法不管 Promise 是成功还是失败都会执行回调函数,而且不会改变 Promise 的状态。如果返回的值是一个 Promise,那么 Promise.finally() 方法会等待该 Promise 执行完毕后再继续执行。
Promise.finally() 方法的实现思路如下:
Promise.finally()方法接收一个回调函数作为参数,返回一个新的 Promise 实例。- 在新的 Promise 实例的
then()方法中,首先调用原 Promise 的then()方法,将原 Promise 的结果传递给下一个then()方法。 - 在新的 Promise 实例的
then()方法中,调用回调函数并将原 Promise 的结果传递给回调函数。 - 如果回调函数返回一个 Promise,则需要在新的 Promise 实例的
then()方法中等待该 Promise 执行完毕,再将结果传递给下一个then()方法。 - 在新的 Promise 实例的
finally()方法中,返回一个新的 Promise 实例。
下面是一个简单的实现示例:
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
}
这个实现方法中,使用了 Promise.resolve() 来返回一个新的 Promise 实例,因此可以避免了 Promise 链的状态改变。另外,由于 finally() 方法只是在 Promise 链的最后执行回调函数,因此不需要使用异步函数。
179.[React] useEffect的第二个参数,如何判断依赖是否发生变化?【web框架】
useEffect的第二个参数是一个依赖数组,用于判断副作用函数的依赖是否发生变化。React使用JavaScript的Object.is方法来判断依赖项是否发生变化。在比较依赖项时,React首先检查依赖项的值是否相等。如果依赖项的值是引用类型,React会比较它们的引用地址,而不是比较它们的属性值。因此,在比较引用类型时,即使对象具有相同的属性值,但它们的引用地址不同,React仍然认为它们是不同的。
需要注意的是,如果依赖项是一个数组或对象,由于它们是引用类型,因此即使数组或对象中的元素或属性没有发生变化,但数组或对象本身的引用地址发生变化,也会导致React重新执行副作用函数。在这种情况下,我们可以使用useCallback和useMemo来缓存回调函数和计算结果,以便避免在依赖数组发生变化时重新计算和创建。
资深开发者相关问题【共计 2 道题】
151.HTTPS 加密算法和加解密过程是啥?【网络】
探究 HTTPS
我说,你起这么牛逼的名字干嘛,还想吹牛批?你 HTTPS 不就抱上了 TLS/SSL 的大腿么,咋这么牛批哄哄的,还想探究 HTTPS,瞎胡闹,赶紧改成 TLS 是我主,赞美我主。
SSL 即安全套接字层,它在 OSI 七层网络模型中处于第五层,SSL 在 1999 年被 IETF(互联网工程组)更名为 TLS ,即传输安全层,直到现在,TLS 一共出现过三个版本,1.1、1.2 和 1.3 ,目前最广泛使用的是 1.2,所以接下来的探讨都是基于 TLS 1.2 的版本上的。
TLS 用于两个通信应用程序之间提供保密性和数据完整性。TLS 由记录协议、握手协议、警告协议、变更密码规范协议、扩展协议等几个子协议组成,综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术(如果你觉得一项技术很简单,那你只是没有学到位,任何技术都是有美感的,牛逼的人只是欣赏,并不是贬低)。
说了这么半天,我们还没有看到 TLS 的命名规范呢,下面举一个 TLS 例子来看一下 TLS 的结构(可以参考 www.iana.org/assignments…
ECDHE-ECDSA-AES256-GCM-SHA384
这是啥意思呢?我刚开始看也有点懵啊,但其实是有套路的,因为 TLS 的密码套件比较规范,基本格式就是 密钥交换算法 - 签名算法 - 对称加密算法 - 摘要算法 组成的一个密码串,有时候还有分组模式,我们先来看一下刚刚是什么意思
使用 ECDHE 进行密钥交换,使用 ECDSA 进行签名和认证,然后使用 AES 作为对称加密算法,密钥的长度是 256 位,使用 GCM 作为分组模式,最后使用 SHA384 作为摘要算法。
TLS 在根本上使用对称加密和 非对称加密 两种形式。
对称加密
在了解对称加密前,我们先来了解一下密码学的东西,在密码学中,有几个概念:明文、密文、加密、解密
明文(Plaintext),一般认为明文是有意义的字符或者比特集,或者是通过某种公开编码就能获得的消息。明文通常用 m 或 p 表示密文(Ciphertext),对明文进行某种加密后就变成了密文加密(Encrypt),把原始的信息(明文)转换为密文的信息变换过程解密(Decrypt),把已经加密的信息恢复成明文的过程。
对称加密(Symmetrical Encryption)顾名思义就是指加密和解密时使用的密钥都是同样的密钥。只要保证了密钥的安全性,那么整个通信过程也就是具有了机密性。
TLS 里面有比较多的加密算法可供使用,比如 DES、3DES、AES、ChaCha20、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK 等。目前最常用的是 AES-128, AES-192、AES-256 和 ChaCha20。
DES 的全称是 Data Encryption Standard(数据加密标准) ,它是用于数字数据加密的对称密钥算法。尽管其 56 位的短密钥长度使它对于现代应用程序来说太不安全了,但它在加密技术的发展中具有很大的影响力。
3DES 是从原始数据加密标准(DES)衍生过来的加密算法,它在 90 年代后变得很重要,但是后面由于更加高级的算法出现,3DES 变得不再重要。
AES-128, AES-192 和 AES-256 都是属于 AES ,AES 的全称是Advanced Encryption Standard(高级加密标准),它是 DES 算法的替代者,安全强度很高,性能也很好,是应用最广泛的对称加密算法。
ChaCha20 是 Google 设计的另一种加密算法,密钥长度固定为 256 位,纯软件运行性能要超过 AES,曾经在移动客户端上比较流行,但 ARMv8 之后也加入了 AES 硬件优化,所以现在不再具有明显的优势,但仍然算得上是一个不错算法。
(其他可自行搜索)
加密分组
对称加密算法还有一个分组模式 的概念,对于 GCM 分组模式,只有和 AES,CAMELLIA 和 ARIA 搭配使用,而 AES 显然是最受欢迎和部署最广泛的选择,它可以让算法用固定长度的密钥加密任意长度的明文。
最早有 ECB、CBC、CFB、OFB 等几种分组模式,但都陆续被发现有安全漏洞,所以现在基本都不怎么用了。最新的分组模式被称为 AEAD(Authenticated Encryption with Associated Data),在加密的同时增加了认证的功能,常用的是 GCM、CCM 和 Poly1305。
比如 ECDHE_ECDSA_AES128_GCM_SHA256 ,表示的是具有 128 位密钥, AES256 将表示 256 位密钥。GCM 表示具有 128 位块的分组密码的现代认证的关联数据加密(AEAD)操作模式。
我们上面谈到了对称加密,对称加密的加密方和解密方都使用同一个密钥,也就是说,加密方必须对原始数据进行加密,然后再把密钥交给解密方进行解密,然后才能解密数据,这就会造成什么问题?这就好比《小兵张嘎》去送信(信已经被加密过),但是嘎子还拿着解密的密码,那嘎子要是在途中被鬼子发现了,那这信可就是被完全的暴露了。所以,对称加密存在风险。
非对称加密
非对称加密(Asymmetrical Encryption) 也被称为公钥加密,相对于对称加密来说,非对称加密是一种新的改良加密方式。密钥通过网络传输交换,它能够确保及时密钥被拦截,也不会暴露数据信息。非对称加密中有两个密钥,一个是公钥,一个是私钥,公钥进行加密,私钥进行解密。公开密钥可供任何人使用,私钥只有你自己能够知道。
使用公钥加密的文本只能使用私钥解密,同时,使用私钥加密的文本也可以使用公钥解密。公钥不需要具有安全性,因为公钥需要在网络间进行传输,非对称加密可以解决密钥交换的问题。网站保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。
非对称加密算法的设计要比对称算法难得多(我们不会探讨具体的加密方式),常见的比如 DH、DSA、RSA、ECC 等。
其中 RSA 加密算法是最重要的、最出名的一个了。例如 DHE_RSA_CAMELLIA128_GCM_SHA256。它的安全性基于 整数分解,使用两个超大素数的乘积作为生成密钥的材料,想要从公钥推算出私钥是非常困难的。
ECC(Elliptic Curve Cryptography)也是非对称加密算法的一种,它基于椭圆曲线离散对数的数学难题,使用特定的曲线方程和基点生成公钥和私钥, ECDHE 用于密钥交换,ECDSA 用于数字签名。
TLS 是使用对称加密和非对称加密 的混合加密方式来实现机密性。
混合加密
RSA 的运算速度非常慢,而 AES 的加密速度比较快,而 TLS 正是使用了这种混合加密方式。在通信刚开始的时候使用非对称算法,比如 RSA、ECDHE ,首先解决密钥交换的问题。然后用随机数产生对称算法使用的会话密钥(session key),再用公钥加密。对方拿到密文后用私钥解密,取出会话密钥。这样,双方就实现了对称密钥的安全交换。
现在我们使用混合加密的方式实现了机密性,是不是就能够安全的传输数据了呢?还不够,在机密性的基础上还要加上完整性、身份认证的特性,才能实现真正的安全。而实现完整性的主要手段是 摘要算法(Digest Algorithm)
摘要算法
如何实现完整性呢?在 TLS 中,实现完整性的手段主要是 摘要算法(Digest Algorithm)。摘要算法你不清楚的话,MD5 你应该清楚,MD5 的全称是 Message Digest Algorithm 5,它是属于密码哈希算法(cryptographic hash algorithm)的一种,MD5 可用于从任意长度的字符串创建 128 位字符串值。尽管 MD5 存在不安全因素,但是仍然沿用至今。MD5 最常用于验证文件的完整性。但是,它还用于其他安全协议和应用程序中,例如 SSH、SSL 和 IPSec。一些应用程序通过向明文加盐值或多次应用哈希函数来增强 MD5 算法。
什么是加盐?在密码学中,
盐就是一项随机数据,用作哈希数据,密码或密码的单向函数的附加输入。盐用于保护存储中的密码。例如 什么是单向?就是在说这种算法没有密钥可以进行解密,只能进行单向加密,加密后的数据无法解密,不能逆推出原文。
我们再回到摘要算法的讨论上来,其实你可以把摘要算法理解成一种特殊的压缩算法,它能够把任意长度的数据压缩成一种固定长度的字符串,这就好像是给数据加了一把锁。
除了常用的 MD5 是加密算法外,SHA-1(Secure Hash Algorithm 1) 也是一种常用的加密算法,不过 SHA-1 也是不安全的加密算法,在 TLS 里面被禁止使用。目前 TLS 推荐使用的是 SHA-1 的后继者:SHA-2。
SHA-2 的全称是Secure Hash Algorithm 2 ,它在 2001 年被推出,它在 SHA-1 的基础上做了重大的修改,SHA-2 系列包含六个哈希函数,其摘要(哈希值)分别为 224、256、384 或 512 位:SHA-224, SHA-256, SHA-384, SHA-512。分别能够生成 28 字节、32 字节、48 字节、64 字节的摘要。
有了 SHA-2 的保护,就能够实现数据的完整性,哪怕你在文件中改变一个标点符号,增加一个空格,生成的文件摘要也会完全不同,不过 SHA-2 是基于明文的加密方式,还是不够安全,那应该用什么呢?
安全性更高的加密方式是使用 HMAC,在理解什么是 HMAC 前,你需要先知道一下什么是 MAC。
MAC 的全称是message authentication code,它通过 MAC 算法从消息和密钥生成,MAC 值允许验证者(也拥有秘密密钥)检测到消息内容的任何更改,从而保护了消息的数据完整性。
HMAC 是 MAC 更进一步的拓展,它是使用 MAC 值 + Hash 值的组合方式,HMAC 的计算中可以使用任何加密哈希函数,例如 SHA-256 等。
现在我们又解决了完整性的问题,那么就只剩下一个问题了,那就是认证,认证怎么做的呢?我们再向服务器发送数据的过程中,黑客(攻击者)有可能伪装成任何一方来窃取信息。它可以伪装成你,来向服务器发送信息,也可以伪装称为服务器,接受你发送的信息。那么怎么解决这个问题呢?
认证
如何确定你自己的唯一性呢?我们在上面的叙述过程中出现过公钥加密,私钥解密的这个概念。提到的私钥只有你一个人所有,能够辨别唯一性,所以我们可以把顺序调换一下,变成私钥加密,公钥解密。使用私钥再加上摘要算法,就能够实现数字签名,从而实现认证。
到现在,综合使用对称加密、非对称加密和摘要算法,我们已经实现了加密、数据认证、认证,那么是不是就安全了呢?非也,这里还存在一个数字签名的认证问题。因为私钥是是自己的,公钥是谁都可以发布,所以必须发布经过认证的公钥,才能解决公钥的信任问题。
所以引入了 CA,CA 的全称是 Certificate Authority,证书认证机构,你必须让 CA 颁布具有认证过的公钥,才能解决公钥的信任问题。
全世界具有认证的 CA 就几家,分别颁布了 DV、OV、EV 三种,区别在于可信程度。DV 是最低的,只是域名级别的可信,EV 是最高的,经过了法律和审计的严格核查,可以证明网站拥有者的身份(在浏览器地址栏会显示出公司的名字,例如 Apple、GitHub 的网站)。不同的信任等级的机构一起形成了层级关系。
通常情况下,数字证书的申请人将生成由私钥和公钥以及证书签名请求(CSR)组成的密钥对。CSR是一个编码的文本文件,其中包含公钥和其他将包含在证书中的信息(例如域名,组织,电子邮件地址等)。密钥对和 CSR生成通常在将要安装证书的服务器上完成,并且 CSR 中包含的信息类型取决于证书的验证级别。与公钥不同,申请人的私钥是安全的,永远不要向 CA(或其他任何人)展示。
生成 CSR 后,申请人将其发送给 CA,CA 会验证其包含的信息是否正确,如果正确,则使用颁发的私钥对证书进行数字签名,然后将其发送给申请人。
总结
本篇文章我们主要讲述了 HTTPS 为什么会出现 ,HTTPS 解决了 HTTP 的什么问题,HTTPS 和 HTTP 的关系是什么,TLS 和 SSL 是什么,TLS 和 SSL 解决了什么问题?如何实现一个真正安全的数据传输?
文章参考
crypto.stackexchange.com/questions/2…
www.comparitech.com/blog/inform…
《极客时间-透析 HTTP 协议》
www.tutorialsteacher.com/https/how-s…
support.google.com/webmasters/…
www.cloudflare.com/learning/ss…
www.freecodecamp.org/news/web-se…
176.WebWorker、SharedWorker 和 ServiceWorker 有哪些区别?【JavaScript】
前言
众所周知,JavaScript 是单线程的语言。当我们面临需要大量计算的场景时(比如视频解码等),UI 线程就会被阻塞,甚至浏览器直接卡死。现在前端遇到大量计算的场景越来越多,为了有更好的体验,HTML5 中提出了 Web Worker 的概念。Web Worker 可以使脚本运行在新的线程中,它们独立于主线程,可以进行大量的计算活动,而不会影响主线程的 UI 渲染。当计算结束之后,它们可以把结果发送给主线程,从而形成了高效、良好的用户体验。Web Worker 是一个统称,具体可以细分为普通的 Worker、SharedWorker 和 ServiceWorker 等,接下来我们一一介绍其使用方法和适合的场景。
普通 Worker
- 创建 Worker 通过 new 的方式来生成一个实例,参数为 url 地址,该地址必须和其创建者是同源的。
const worker = new Worker('./worker.js'); // 参数是url,这个url必须与创建者同源
- Worker 的方法
- onmessage 主线程中可以在 Worker 上添加 onmessage 方法,用于监听 Worker 的信息。
- onmessageerror 主线程中可以在 Worker 上添加 onmessageerror 方法,用于监听 Worker 的错误信息。
- postMessage() 主线程通过此方法给 Worker 发送消息,发送参数的格式不限(可以是数组、对象、字符串等),可以根据自己的业务选择。
- terminate() 主线程通过此方法终止 Worker 的运行。
- 通信
Worker 的作用域跟主线程中的 Window 是相互独立的,并且 Worker 中是获取不到 DOM 元素的。所以在 Worker 中你无法使用 Window 变量。取而代之的是可以用 self 来表示全局对象。self 上有哪些方法和属性,感兴趣的小伙伴可以自行输出查看。比较常用的方法是 onmessage、postMessage,主要用来跟主线程进行通信。
- Worker 中引用其他脚本的方式
跟常用的 JavaScript 一样,Worker 中也是可以引入其他的模块的。但是方式不太一样,是通过 importScripts 来引入。这边我为了演示,新建了一个 constant.js。在 constant.js 定义了一些变量和函数。
示例:
// Worker.js
importScripts('constant.js');
// 下面就可以获取到 constant.js 中的所有变量了
// constant.js
// 可以在 Worker 中使用
const a = 111;
// 不可以在 Worker 中使用,原因未知
const b = function () {
console.log('test');
};
// 可以在 Worker 中使用
function c() {
console.log('test');
}
- 调试方法
写代码难免要进行调试。Worker 的调试在浏览器控制台中有专门展示的地方, 以 chrome 浏览器为例: dev tools --> source --> worker.js
-
常见使用场景
- 一般的视频网站 以优酷为例,当我们开始播放优酷视频的时候,就能看到它会调用 Worker,解码的代码应该写在 Worker 里面。
- 需要大量计算的网站 比如 imgcook 这个网站,它能在前端解析 sketch 文件,这部分解析的逻辑就写在 Worker 里。
SharedWorker
SharedWorker 是一种特定的 Worker。从它的命名就能知道,它是一种共享数据的 Worker。它可以同时被多个浏览器环境访问。这些浏览器环境可以是多个 window, iframes 或者甚至是多个 Worker,只要这些 Workers 处于同一主域。为跨浏览器 tab 共享数据提供了一种解决方案。
-
创建 SharedWorker
创建的方法跟上面普通 Worker 完全一模一样。
const worker = new SharedWorker("./shareWorker.js"); // 参数是url,这个url必须与创建者同源
-
SharedWorker 的方法
SharedWorker 的方法都在 port 上,这是它与普通 Worker 不同的地方。
-
port.onmessage
主线程中可以在 worker 上添加 onmessage 方法,用于监听 SharedWorker 的信息
-
port.postMessage()
主线程通过此方法给 SharedWorker 发送消息,发送参数的格式不限
-
port.start()
主线程通过此方法开启 SharedWorker 之间的通信
-
port.close()
主线程通过此方法关闭 SharedWorker
-
通信
SharedWorker 跟普通的 Worker 一样,可以用 self 来表示全局对象。不同之处是,它需要等 port 连接成功之后,利用 port 的onmessage、postMessage,来跟主线程进行通信。当你打开多个窗口的时候,SharedWorker 的作用域是公用的,这也是其特点。
-
Worker 中引用其他脚本
这个与普通的 Worker 方法一样,使用 importScripts
-
调试方法
在浏览器中查看和调试 SharedWorker 的代码,需要输入 chrome://inspect/
ServiceWorker
ServiceWorker 一般作为 Web 应用程序、浏览器和网络之间的代理服务。他们旨在创建有效的离线体验,拦截网络请求,以及根据网络是否可用采取合适的行动,更新驻留在服务器上的资源。他们还将允许访问推送通知和后台同步 API。
- 创建 ServiceWorker
// index.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker
.register('./serviceWorker.js', { scope: '/page/' })
.then(
function (registration) {
console.log(
'ServiceWorker registration successful with scope: ',
registration.scope
);
},
function (err) {
console.log('ServiceWorker registration failed: ', err);
}
);
});
}
只要创建了 ServiceWorker,不管这个创建 ServiceWorker 的 html 是否打开,这个 ServiceWorker 是一直存在的。它会代理范围是根据 scope 决定的,如果没有这个参数,则其代理范围是创建目录同级别以及子目录下所有页面的网络请求。代理的范围可以通过registration.scope 查看。
- 安装 ServiceWorker
// serviceWorker.js
const CACHE_NAME = 'cache-v1';
// 需要缓存的文件
const urlsToCache = [
'/style/main.css',
'/constant.js',
'/serviceWorker.html',
'/page/index.html',
'/serviceWorker.js',
'/image/131.png',
];
self.oninstall = (event) => {
event.waitUntil(
caches
.open(CACHE_NAME) // 这返回的是promise
.then(function (cache) {
return cache.addAll(urlsToCache); // 这返回的是promise
})
);
};
在上述代码中,我们可以看到,在 install 事件的回调中,我们打开了名字为 cache-v1 的缓存,它返回的是一个 promise。在打开缓存之后,我们需要把要缓存的文件 add 进去,基本上所有类型的资源都可以进行缓存,例子中缓存了 css、js、html、png。如果所有缓存数据都成功,就表示 ServiceWorker 安装成功;如果控制台提示 Uncaught (in promise) TypeError: Failed to execute 'Cache' on 'addAll': Request failed,则表示安装失败。
- 缓存和返回请求
self.onfetch = (event) => {
event.respondWith(
caches
.match(event.request) // 此方法从服务工作线程所创建的任何缓存中查找缓存的结果
.then(function (response) {
// response为匹配到的缓存资源,如果没有匹配到则返回undefined,需要fetch资源
if (response) {
return response;
}
return fetch(event.request);
})
);
};
在 fetch 事件的回调中,我们去匹配 cache 中的资源。如果匹配到,则使用缓存资源;没有匹配到则用 fetch 请求。正因为 ServiceWorker 可以代理网络请求,所以为了安全起见,规范中规定它只能在 https 和 localhost 下才能开启。
-
调试方法
在浏览器中查看和调试 ServiceWorker 的代码,需要输入 chrome://inspect/#service-workers
-
常见使用场景
缓存资源文件,加快渲染速度
这个我们以语雀为例。我们在打开语雀网站的时候,可以看到它使用 ServiceWorker 缓存了很多 css、js 文件,从而达到优化的效果。
总结
| 类型 | Worker | SharedWorker | ServiceWorker |
|---|---|---|---|
| 通信方式 | postMessage | port.postMessage | 单向通信,通过addEventListener 监听serviceWorker 的状态 |
| 使用场景 | 适合大量计算的场景 | 适合跨 tab、iframes之间共享数据 | 缓存资源、网络优化 |
| 兼容性 | >= IE 10>= Chrome 4 | 不支持 IE、Safari、Android、iOS>= Chrome 4 | 不支持 IE>= Chrome 40 |
本文介绍了 3 种 Worker,他们分别适合不同的场景,总结如上面表格。普通的 Worker 可以在需要大量计算的时候使用,创建新的线程可以降低主线程的计算压力,不会导致 UI 卡顿。SharedWorker 主要是为不同的 window、iframes 之间共享数据提供了另外一个解决方案。ServiceWorker 可以缓存资源,提供离线服务或者是网络优化,加快 Web 应用的开启速度,更多是优化体验方面的。