笔记整理二

444 阅读1小时+

五、计算机网络

1 谈谈你对OSI模型的理解

OSI(开放式系统互联)模型是一个用于描述计算机网络协议的七层参考模型。它是ISO(国际标准化组织)在20世纪80年代提出的,旨在促进不同类型的计算机和网络系统之间的互通性和互操作性。OSI模型可以帮助我们更好地理解网络通信过程中的各个步骤,以及如何在这些步骤中组织和使用各种协议。

OSI模型从下到上共有7层,每一层都有特定的职责,它们分别是:

  1. 物理层(Physical Layer):这一层负责处理与传输介质(如电缆、光纤)有关的硬件方面的问题,包括比特(0和1)的传输、电压、接口等。物理层的主要目标是确保数据在传输过程中的完整性和可靠性。
  2. 数据链路层(Data Link Layer):数据链路层负责在网络节点之间建立、维护和拆除数据链路。它将物理层传输的比特组合成帧(frame),同时负责错误检测和流量控制。典型的数据链路层协议有以太网(Ethernet)、Wi-Fi和PPP(点对点协议)等。
  3. 网络层(Network Layer):网络层负责将数据包(packet)从源节点路由到目标节点。它处理IP地址、路由选择、分片和重组等问题。网络层的主要协议有IP(互联网协议)、ICMP(互联网控制消息协议)等。
  4. 传输层(Transport Layer):传输层负责在源端和目标端之间提供端到端(end-to-end)的数据传输服务。它处理数据的分段、重组、流量控制、错误检测和校正等问题。常见的传输层协议有TCP(传输控制协议)和UDP(用户数据报协议)。
  5. 会话层(Session Layer):会话层负责在通信双方之间建立、管理和终止会话。会话层的主要职责是维护会话状态、同步数据流和进行恢复操作。尽管OSI模型中包含了会话层,但在实际的互联网协议中,很多功能已经与其他层次结合在一起,例如TCP协议。
  6. 表示层(Presentation Layer):表示层负责处理数据的表示、编码和解码,以便在不同系统之间进行交换。这包括数据压缩、加密和字符集转换等等功能。表示层的一个典型例子是SSL/TLS协议,它在应用层协议(如HTTP)之下提供数据加密和解密服务。然而,在现代网络协议中,表示层的很多功能通常直接集成在应用层协议中,如JSON、XML等数据格式。
  7. 应用层(Application Layer):应用层是OSI模型中最高层,它负责处理用户与网络之间的交互,提供面向用户的服务。应用层协议定义了各种应用程序如何与网络进行通信,例如发送电子邮件、浏览网页等。常见的应用层协议包括HTTP(超文本传输协议)、FTP(文件传输协议)、SMTP(简单邮件传输协议)等。

OSI模型的一个重要优点是它将网络功能划分为若干模块化的层次,每个层次都有明确的职责。这种分层架构有助于降低网络设计和实现的复杂性,同时还有利于对现有协议的改进和替换,以适应不断变化的技术需求。

然而,需要注意的是,OSI模型主要是一个理论框架,用于帮助我们更好地理解网络协议的组织和交互。实际上,许多现代网络协议并不完全遵循OSI模型的严格分层结构,而是采用更灵活的混合模式。例如,TCP/IP模型是互联网中最常用的网络协议体系,它将OSI模型的7层合并为4层(应用层、传输层、网络层和链路层)。尽管如此,OSI模型仍然是一个有用的概念工具,可以帮助我们更好地理解复杂的计算机网络。

2 什么是HTTP,它的用途是什么?

HTTP(HyperText Transfer Protocol,超文本传输协议)是一种用于在互联网上进行数据通信的应用层协议。它基于 TCP/IP 协议栈工作,通常使用 TCP 作为传输层协议。HTTP 的主要作用是规定了客户端(例如浏览器)和服务器之间的请求和响应的格式与规则,从而实现数据的交换。

HTTP 的主要用途是:

  1. 资源获取:客户端可以通过 HTTP 向服务器发送请求,获取 HTML、CSS、JavaScript、图片、视频等各种资源,以便呈现和运行网页或其他应用。
  2. 数据提交:客户端可以通过 HTTP 将数据提交给服务器,例如表单提交、文件上传等。
  3. API 交互:客户端和服务器可以通过 HTTP 进行 API 交互,以便在前后端之间传递数据。常见的数据格式包括 JSON 和 XML。

HTTP 协议的特点:

  1. 无状态:HTTP 是无状态的,意味着服务器不会存储与客户端之间交互的信息。为了实现有状态的会话,可以使用 Cookie 和 Session 等技术。
  2. 简单快速:HTTP 协议简单,易于实现。客户端和服务器之间的请求和响应报文格式统一,便于解析和处理。
  3. 可扩展:HTTP 允许自定义请求头和响应头,可以根据需要扩展协议的功能。
  4. 明文传输:HTTP 协议本身不提供数据加密功能,所有数据以明文形式传输。为了提高数据安全性,可以使用 HTTPS(HTTP Secure,基于 SSL/TLS 的 HTTP)进行加密传输。

HTTP 协议包括一系列请求方法(如 GET、POST、PUT、DELETE 等),用于指定请求的类型和目的。此外,HTTP 协议还定义了一系列状态码,用于表示请求的结果和相关信息。

3 TCP中三次握手和四次挥手的含义

在 TCP(传输控制协议)中,三次握手和四次挥手是用来建立和终止连接的重要过程。下面分别解释它们的含义:

三次握手(Three-Way Handshake)

三次握手是建立 TCP 连接的过程,确保数据传输的可靠性和连接的双向通信。这个过程包含三个步骤:

  1. 客户端向服务器发送一个 SYN(同步)报文,表示客户端希望建立连接。报文中包含一个随机生成的序列号(Sequence Number)。
  2. 服务器收到 SYN 报文后,回复一个 SYN+ACK(同步确认)报文,其中 ACK 的值为客户端序列号加一。同时,服务器也会发送一个自己的随机序列号。
  3. 客户端收到 SYN+ACK 报文后,发送一个 ACK(确认)报文给服务器,其中 ACK 的值为服务器序列号加一。

完成这三个步骤后,客户端和服务器之间的 TCP 连接就建立好了,可以进行双向通信。

四次挥手(Four-Way Handshake)

四次挥手是终止 TCP 连接的过程。当数据传输完成后,客户端和服务器需要通过四次挥手来关闭连接。这个过程包含四个步骤:

  1. 客户端发送一个 FIN(完成)报文给服务器,表示客户端已完成数据发送。
  2. 服务器收到 FIN 报文后,回复一个 ACK(确认)报文给客户端,表示已收到客户端的关闭请求。此时,客户端到服务器的连接被关闭,但服务器到客户端的连接仍然存在。
  3. 当服务器完成数据发送后,也会发送一个 FIN 报文给客户端,表示服务器准备关闭连接。
  4. 客户端收到服务器的 FIN 报文后,回复一个 ACK 报文给服务器,表示已收到服务器的关闭请求。经过一段时间(通常为 2MSL,即最长报文段寿命的两倍)后,客户端关闭与服务器的连接。

完成这四个步骤后,客户端和服务器之间的 TCP 连接就被完全关闭了。

4 TCP提供了什么服务?

传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。它为应用程序之间提供可靠的数据传输服务。TCP在Internet协议族中被广泛应用,与网络层协议IP(Internet Protocol)共同组成TCP/IP协议簇。

TCP提供的服务包括:

  1. 面向连接:TCP在数据传输前建立一个连接。连接的建立、维护和终止都由TCP管理。这种面向连接的方式使得TCP适用于需要可靠通信的应用。
  2. 可靠传输:TCP通过使用确认机制、重传机制和错误校验来确保数据的可靠传输。如果数据包丢失或损坏,TCP会自动重发数据包,从而保证数据的完整性和正确性。
  3. 流量控制:TCP使用滑动窗口机制来实现流量控制,确保接收方的缓冲区不会被发送方的数据溢出。这样,发送方和接收方可以根据自身处理能力和网络状况动态调整传输速率,提高传输效率。
  4. 拥塞控制:为了防止网络拥塞,TCP使用拥塞控制算法来调整发送数据的速率。当网络出现拥塞时,TCP会减少发送速率,避免网络拥塞进一步恶化。当网络恢复正常时,TCP会逐渐提高发送速率。
  5. 数据排序:TCP会为每个数据包分配一个序号,接收方可以根据序号重新组合数据包,从而确保数据按正确顺序传输。
  6. 复用与分用:TCP允许多个应用程序在同一台主机上通过不同的端口共享网络资源。这意味着多个应用程序可以在同一时间使用TCP进行数据传输,而不会互相干扰。

5 SSL/TLS是如何握手的?

SSL(安全套接层)和TLS(传输层安全)协议用于在互联网上建立安全的、加密的通信连接。尽管TLS是SSL的后续版本,但在实际用语中,它们经常互换使用。TLS握手过程可以分为以下几个步骤:

  1. 客户端发起连接:客户端(例如,浏览器)向服务器发起一个安全连接请求。这个请求包括客户端支持的TLS版本、支持的加密套件(加密算法和密钥交换方法的组合)以及一个随机数(ClientHello 随机数)。
  2. 服务器响应:服务器从客户端提供的加密套件列表中选择一个加密套件,并向客户端返回服务器的证书(包含公钥和服务器身份信息)、选择的TLS版本和加密套件,以及一个随机数(ServerHello 随机数)。此外,如果需要客户端身份验证,服务器还会请求客户端的证书。
  3. 证书验证:客户端验证服务器证书的有效性。证书应由受信任的证书颁发机构(CA)签名。这一步的目的是确保与客户端通信的服务器是可信的,防止中间人攻击。
  4. 密钥交换:客户端使用服务器证书中的公钥加密一个新的随机数(称为 Pre-Master Secret),并将其发送给服务器。服务器使用其私钥解密 Pre-Master Secret。然后,客户端和服务器分别使用 ClientHello 随机数、ServerHello 随机数和 Pre-Master Secret,通过一个伪随机函数生成主密钥(Master Secret)。这个主密钥将用于之后的对称加密和解密操作。
  5. 客户端和服务器协商参数:客户端和服务器根据主密钥生成一组加密参数(例如,对称密钥、初始化向量、消息验证码等),这些参数将用于保护后续的数据传输。
  6. 客户端和服务器验证握手过程:客户端发送一个加密的 Finished 消息,服务器解密并验证消息的完整性。服务器也发送一个加密的 Finished 消息,客户端解密并验证。这一步的目的是确保双方已正确完成密钥交换和参数协商。
  7. 加密数据传输:完成握手过程后,客户端和服务器使用协商的加密参数进行安全的数据传输。此时,所有传输的数据都是加密的,以确保其机密性和完整性。

TLS握手过程旨在在客户端和服务器之间建立一个安全、加密的通信通道,以保护数据在互联网上的传输。

6 Websocket与Ajax的区别?

WebSocket 和 Ajax 是两种不同的 Web 技术,它们在实现客户端与服务器之间通信时有一些区别。以下是它们之间的主要区别:

  1. 连接方式
    • WebSocket:WebSocket 是一种全双工通信协议,建立连接后,客户端和服务器之间可以同时进行数据发送和接收。WebSocket 连接在建立后会保持连接状态,直至显式断开连接。
    • Ajax:Ajax(Asynchronous JavaScript and XML)是一种基于 HTTP 协议的异步请求技术。每次 Ajax 请求都需要建立一个新的 HTTP 连接,请求完成后连接会被关闭。因此,Ajax 是一种基于请求-响应模式的半双工通信方式。
  2. 实时性
    • WebSocket:由于 WebSocket 是全双工通信,服务器可以在任何时候主动向客户端发送数据,因此实时性较强,适用于实时通信、在线游戏等场景。
    • Ajax:由于 Ajax 基于请求-响应模式,服务器不能主动向客户端发送数据,客户端需要定期发起请求以获取更新。因此,实时性相对较弱,适用于非实时的数据获取和更新。
  3. 开销和性能
    • WebSocket:WebSocket 在建立连接后会保持连接状态,因此减少了频繁建立和断开连接的开销。同时,WebSocket 使用自己的二进制分帧格式进行数据传输,帧头较小,数据传输效率较高。
    • Ajax:每次 Ajax 请求都需要建立新的 HTTP 连接,这会导致一定的性能开销。另外,HTTP 协议的请求和响应头部较大,相对降低了数据传输效率。

总结:WebSocket 适用于实时性要求较高、需要双向通信的场景,而 Ajax 更适用于传统的 Web 应用,用于获取和更新数据。根据实际应用需求,可以选择合适的技术进行通信。

7 HTTP/2相比HTTP/1.1新增了什么

HTTP/2 是 HTTP/1.1 的升级版本,旨在解决 HTTP/1.1 中的一些性能问题,提高网络传输的效率。HTTP/2 相比于 HTTP/1.1 主要有以下新增特性:

  1. 二进制分帧:HTTP/2 将数据以二进制格式进行传输,请求和响应被分解为更小的帧,每个帧有自己的类型和标识符。这使得数据传输更加高效,易于解析。
  2. 多路复用:HTTP/2 允许在同一个 TCP 连接上同时发送和接收多个请求和响应,避免了 HTTP/1.1 中的队头阻塞问题。这样可以提高页面加载速度,减少网络延迟。
  3. 头部压缩:HTTP/2 使用 HPACK 压缩算法对请求和响应头部进行压缩,减小了传输数据的大小,从而降低了传输延迟。
  4. 服务器推送:HTTP/2 服务器可以主动将一些尚未被客户端请求的资源推送给客户端,以便客户端在需要时立即使用,提高了资源加载速度。

8 HTTP/3相比HTTP/2解决了哪些问题?

HTTP/3 是互联网工程任务组(IETF)制定的新一代 HTTP(超文本传输协议)标准,它基于 QUIC 协议(快速 UDP 互联网连接),旨在解决 HTTP/2 存在的一些问题,提高网络性能和安全性。相较于 HTTP/2,HTTP/3 主要解决了以下问题:

  1. 首部阻塞(Head-of-line blocking):在 HTTP/2 中,多个请求和响应通过同一个 TCP 连接进行多路复用。由于 TCP 是面向连接的、可靠的传输协议,要求数据按顺序到达,当其中一个数据包丢失或延迟时,整个连接的其他数据包必须等待,直到丢失的数据包被重新传输并到达。这种现象被称为首部阻塞。HTTP/3 采用基于 UDP 的 QUIC 协议,它允许单独处理每个数据流,从而消除了首部阻塞问题。
  2. 连接建立延迟:HTTP/2 在建立 TCP 连接之后,还需要通过 TLS 握手建立安全连接。这意味着需要多次往返才能完成连接建立。而 HTTP/3 使用 QUIC 协议,它将传输层(TCP)和安全层(TLS)整合在一起,从而减少了连接建立所需的往返次数。对于已经与服务器建立过连接的客户端,QUIC 甚至可以实现 0-RTT(零往返时间)连接建立,大幅降低了延迟。
  3. 更好的网络路径迁移:当客户端或服务器的 IP 地址发生变化时(如移动设备在 Wi-Fi 和移动网络之间切换),TCP 连接可能中断,需要重新建立。HTTP/3 的 QUIC 协议提供了内置的连接迁移特性,允许在 IP 地址变化时保持连接状态,减少中断和延迟。
  4. 拥塞控制和恢复:由于 HTTP/3 使用 QUIC 协议,它可以在单独的数据流上实现更细粒度的拥塞控制。这意味着当一个数据流受到网络拥塞影响时,其他数据流可以继续正常传输,不受影响。同时,QUIC 采用了更先进的丢包恢复机制,使得数据传输更加高效和可靠。

尽管 HTTP/3 有很多优势,但它目前仍在逐步推广和普及。浏览器和服务器需要支持 QUIC 协议,以便充分利用 HTTP/3 带来的改进。

六、 前端性能优化

1 前端常见的性能优化有哪些?

前端性能优化是为了提高用户体验和页面响应速度的一种手段。以下是一些常见的前端性能优化方法:

  1. 优化图片:对图片进行压缩、使用合适的格式(例如,WebP)和尺寸,以及利用图片懒加载技术。
  2. 使用CDN(内容分发网络):将静态资源分发到全球各地的服务器,减少用户加载资源的延迟。
  3. 缓存策略:利用浏览器缓存策略,如Cache-Control和ETag,减少重复请求。
  4. 代码压缩与优化:通过压缩和移除不必要的代码,减小文件大小。同时优化代码逻辑,提高代码执行效率。
  5. 异步加载:使用异步加载技术(如,async和defer属性)减少阻塞渲染的脚本。
  6. 利用浏览器渲染优化:避免强制同步布局,减少重排和重绘。
  7. 使用CSS3硬件加速:利用GPU加速,提高动画和页面渲染性能。
  8. 优化CSS选择器:使用简洁、高效的CSS选择器,提高渲染速度。
  9. 代码分割与按需加载:通过代码分割和按需加载技术,降低首次页面加载时间。
  10. 使用Web Workers:利用Web Workers进行后台处理,避免阻塞主线程。
  11. 服务端渲染(SSR)与预渲染:利用服务端渲染和预渲染技术,加快首屏渲染速度。
  12. 优化字体加载:减少字体文件大小,使用字体加载策略避免阻塞渲染。
  13. 使用HTTP/2:使用HTTP/2协议,实现多路复用,降低网络延迟。
  14. 优化资源优先级:利用<link rel="preload"><link rel="prefetch"> 标签,优化资源加载顺序。
  15. 使用事件委托,利用冒泡机制处理事件。
  16. 使用防抖、节流处理频繁触发的事件。

以上只是一部分常见的前端性能优化方法,实际应用时还需要根据项目具体需求和场景来选择合适的优化策略。

2 讲下虚拟列表的原理

虚拟列表(Virtual List)又称无限滚动列表或者长列表优化,是一种前端性能优化技术。其主要目的是在处理大量数据列表时,提高用户体验和页面性能。以下是虚拟列表的基本原理:

  1. 渲染可视区域的列表项:虚拟列表的核心思想是仅渲染可见部分的列表项。当用户滚动列表时,根据可视区域的大小,计算需要显示的列表项,然后只渲染这些列表项。
  2. 列表项的位置计算:虚拟列表需要计算每个列表项的位置信息,以便在滚动过程中正确地显示列表项。这通常通过索引和固定高度(或者预估高度)来实现。
  3. 复用列表项DOM元素:为了减少创建和销毁DOM元素的性能开销,虚拟列表通常会复用列表项的DOM元素。当一个列表项滚出可视区域时,它的DOM元素会被重新利用,用于显示新的列表项。
  4. 占位元素:虚拟列表通常会使用一个占位元素(如一个空的div)来模拟整个列表的高度。这样可以使滚动条保持正确的位置和大小,使用户能够像正常列表一样进行滚动操作。
  5. 事件监听与更新:虚拟列表需要监听滚动事件,以便在用户滚动时实时更新可视区域的列表项。此外,如果列表数据发生变化,虚拟列表还需要重新计算位置信息并更新可视区域的列表项。

通过上述原理,虚拟列表能够显著减少大量列表项的渲染成本,从而提高页面的性能和用户体验。在实际应用中,根据项目的具体需求和场景,可能还需要对虚拟列表进行一定的定制和优化。

3 讲讲常见的性能指标有哪些?它们分别对应的作用是什么?

Web前端性能指标是衡量一个网站或Web应用程序加载速度、交互性和用户体验的关键因素。以下是一些常见的Web前端性能指标及其解释:

  1. 首次绘制(FP,First Paint):页面在用户设备上开始渲染的时间点。这个指标可以用来衡量页面加载速度的初步感知。

  2. 首次内容绘制(FCP,First Contentful Paint):页面上的任何内容(如文字、图片等)首次渲染的时间点。这个指标可以用来衡量用户看到页面内容的速度。

  3. 首次有效绘制(FMP,First Meaningful Paint):页面主要内容呈现给用户的时间点。这个指标可以用来衡量页面的视觉完整度。

  4. 首次输入延迟(FID,First Input Delay):用户首次与页面交互(如点击按钮、输入等)所需时间。这个指标可以用来衡量页面的交互性。

  5. 速度指数(SI,Speed Index):描述页面加载过程中的视觉体验的一个指标。速度指数越低,用户体验越好。

  6. 大致加载时间(TTFB,Time To First Byte):从用户发出请求到接收到服务器响应的第一个字节所需的时间。这个指标可以用来衡量网络延迟和服务器处理速度。

  7. 页面完全加载时间(Load Time):从用户发出请求到页面完全加载所需的时间。这个指标可以用来衡量页面加载速度的综合体验。

  8. 页面体积:页面的所有资源(HTML、CSS、JavaScript等)的总大小。页面体积越小,加载速度越快。

  9. 请求次数:加载页面所需的网络请求次数。请求次数越少,加载速度越快。

  10. 首次CPU空闲时间(First CPU Idle):页面首次达到CPU空闲状态的时间点。这个指标可以用来衡量页面在何时可以响应用户输入。

  11. 最大潜在首次输入延迟(Max Potential FID):一个预测性指标,衡量在页面首次可交互之前可能发生的最大输入延迟。

  12. 累计布局偏移(CLS,Cumulative Layout Shift):页面在加载过程中元素位置变化的总和。这个指标可以用来衡量页面的视觉稳定性。

七、前端安全

1 谈一谈你对XSS攻击理解

跨站脚本攻击(XSS,Cross-site Scripting)是一种常见的网络安全漏洞,它允许攻击者将恶意代码注入到受害者访问的网站中。这种攻击通常通过JavaScript来实现,但也可能涉及到其他脚本语言。在XSS攻击中,攻击者的目标是利用用户对网站的信任,进而窃取用户的数据、破坏网站的功能或者进行其他恶意行为。

XSS攻击可以分为三种类型:

  1. 存储型XSS攻击(Stored XSS):攻击者将恶意代码提交到目标网站的数据库中,当其他用户访问受影响的页面时,恶意代码将被加载并执行。这种类型的XSS攻击是最危险的,因为攻击者可以长期控制受害者的浏览器。
  2. 反射型XSS攻击(Reflected XSS):攻击者通过创建一个包含恶意代码的URL,诱使受害者点击这个链接。当受害者访问这个URL时,恶意代码会在其浏览器中执行。这种类型的XSS攻击需要用户的互动,因此相对存储型XSS攻击来说,风险较低。
  3. DOM型XSS攻击(DOM-based XSS):这种类型的XSS攻击是通过操作网页的Document Object Model(DOM)来实现的。攻击者会寻找可以用来插入恶意代码的DOM节点,当用户访问受影响的页面时,恶意代码将被执行。这种类型的攻击与反射型XSS相似,但更难以检测和防御。

为了防范XSS攻击,网站开发者和运维人员可以采取以下措施:

  1. 对用户输入进行过滤和验证:确保所有的用户输入都经过适当的验证和过滤,以防止恶意代码的注入。
  2. 使用安全的编码方法:对用户输入的数据进行编码,将特殊字符转换为HTML实体,以防止代码在浏览器中被解析和执行。
  3. 设置Content Security Policy(CSP):使用CSP可以限制浏览器加载和执行外部资源,降低XSS攻击的风险。
  4. 使用HttpOnly Cookies:将敏感信息(如会话ID)存储在HttpOnly Cookies中,以防止恶意脚本通过浏览器窃取这些信息。
  5. 保持软件和库的更新:确保使用的开发工具、库和框架是最新的,并修复已知的安全漏洞。

2 谈一谈你对CSRF攻击理解

跨站请求伪造(CSRF,Cross-Site Request Forgery)是一种常见的网络安全漏洞,攻击者通过诱使受害者执行不知情的操作来利用受害者在网站上的身份。这种攻击是基于用户在其他网站上的登录状态和网站的信任机制。

在CSRF攻击中,攻击者创建一个恶意网站或发送一个包含恶意代码的电子邮件。当受害者访问恶意网站或查看电子邮件时,浏览器会在后台向目标网站发送伪造的请求。由于受害者已经在目标网站上登录,因此这些请求将带有有效的凭据(如cookies),使攻击者能够以受害者的身份执行操作。

为了防范CSRF攻击,网站开发者和运维人员可以采取以下措施:

  1. 使用CSRF令牌:在用户提交表单或执行敏感操作时,为每个请求生成一个随机的、唯一的CSRF令牌。将这个令牌与用户的会话关联,并在请求中包含该令牌。服务器端需要验证每个请求的令牌,确保它与用户会话的令牌匹配。这样可以防止攻击者伪造有效的请求。
  2. 验证请求来源:检查请求的来源,例如HTTP的Referer头或Origin头,确保请求来自于合法的域名。这有助于防止跨域的CSRF攻击。
  3. 使用SameSite Cookies属性:设置SameSite属性为“Strict”或“Lax”,可以防止浏览器在跨站请求时发送cookies。这可以降低CSRF攻击的风险,但可能不适用于所有场景。
  4. 要求用户重新验证身份:在执行敏感操作(如修改密码或执行交易)时,要求用户重新输入密码或进行二次验证。这可以降低CSRF攻击的成功率。
  5. 提高安全意识:教育用户识别和避免钓鱼网站、恶意邮件等,以降低CSRF攻击的成功率。

3 谈谈你对SQL注入的理解

SQL注入(SQL Injection)是一种网络安全漏洞,它允许攻击者通过在输入数据中插入恶意SQL代码,来控制或操纵应用程序与数据库之间的交互。这种攻击通常发生在应用程序未对用户输入进行充分验证和过滤的情况下。攻击者可以利用SQL注入漏洞来窃取、篡改或删除数据,甚至可能获得对整个数据库系统的控制权。

SQL注入攻击的常见类型包括:

  1. 联合查询注入(Union-based SQL Injection):攻击者通过构造包含UNION语句的恶意SQL查询,使其与原始查询合并,从而获取额外的数据。
  2. 基于错误的SQL注入(Error-based SQL Injection):攻击者利用数据库在执行恶意查询时产生的错误信息,获取有关数据库结构和数据的信息。
  3. 盲注SQL注入(Blind SQL Injection):攻击者通过逐步尝试不同的输入值,并根据应用程序的响应来推断数据库结构和数据。这种攻击方式较为缓慢,但在目标系统没有显示具体错误信息的情况下仍然有效。

为了防止SQL注入攻击,开发者和运维人员可以采取以下措施:

  1. 使用预编译语句和参数化查询:预编译语句和参数化查询可以将SQL代码与数据分离,从而避免恶意代码的注入。这是防止SQL注入的最有效方法。
  2. 对用户输入进行验证和过滤:确保所有用户输入都经过适当的验证和过滤,以防止恶意代码的注入。使用白名单验证策略,仅允许已知安全的输入值。
  3. 最小权限原则:为应用程序的数据库账户分配最小必要权限,以减少潜在的损害。例如,如果应用程序仅需要读取数据,不应给予其写入和删除权限。
  4. 数据库错误信息处理:避免在应用程序中显示详细的数据库错误信息,以防攻击者利用这些信息进行攻击。可以使用自定义错误页面或者错误日志来记录错误信息,以便进行调试。
  5. 定期进行安全审计和更新:定期检查应用程序和数据库的安全设置,修复已知的漏洞,确保使用的软件和库是最新的。进行代码审查和安全测试,以发现潜在的安全漏洞。

八、设计模式

1 谈谈你对设计模式的理解,为什么需要设计模式?

设计模式是针对软件设计中反复出现的问题所提出的通用解决方案。它们是在大量实际软件开发经验中总结出的优秀设计实践,描述了在特定场景下解决问题的方法和思路。设计模式旨在提高代码的可复用性、可扩展性和可维护性,使软件具有更好的灵活性和稳定性。

设计模式的重要性主要体现在以下几个方面:

  1. 高效解决问题:设计模式为软件设计中常见问题提供了成熟、可靠的解决方案,使开发者能够更高效地解决问题,避免重复发明轮子。
  2. 促进代码复用:设计模式是通用的设计结构,可以在不同的场景和项目中复用,提高了代码的可复用性,减少了开发时间和成本。
  3. 提高代码质量:设计模式强调模块间的低耦合、高内聚,有助于提高代码的可读性、可维护性和稳定性,降低了软件的错误率。
  4. 促进团队协作:设计模式提供了一套通用的设计语言,使得团队成员在交流和理解代码时能够更加顺畅,提高了团队协作效率。
  5. 有益于个人成长:学习和熟练运用设计模式有助于提高开发者的设计能力和编程水平,使得开发者能够在面对复杂问题时更加从容应对。

设计模式主要分为三大类:

  1. 创建型模式(Creational Patterns):关注对象的创建过程,如工厂模式、单例模式、原型模式和建造者模式。
  2. 结构型模式(Structural Patterns):关注类和对象的组合,如适配器模式、桥接模式、装饰器模式、组合模式、外观模式、享元模式和代理模式。
  3. 行为型模式(Behavioral Patterns):关注对象间的通信,如责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式和访问者模式。

需要注意的是,设计模式并非万能的,它们只是解决特定问题的方法和思路。在实际开发中,应根据具体需求和场景选择合适的设计模式,并且不要过度追求设计模式,以免引入不必要的复杂性。

2 谈谈你对工厂模式的理解

工厂模式(Factory Pattern)是一种创建型设计模式,它的核心思想是将对象的创建过程封装起来,让客户端不直接与具体类进行交互,而是通过一个统一的接口来创建所需的对象。这样,当需要修改或者扩展对象的创建逻辑时,我们只需要修改工厂类,而不需要修改客户端的代码,从而达到降低耦合度和提高代码可维护性的目的。

工厂模式主要分为以下几种类型:

  1. 简单工厂模式(Simple Factory Pattern):一个工厂类根据传入的参数创建对应的对象。这种模式的缺点是,当需要添加新的产品时,需要修改工厂类的代码,违反了开放封闭原则。
  2. 工厂方法模式(Factory Method Pattern):定义一个工厂接口,让各个具体工厂类实现这个接口,负责创建对应的产品。客户端只需要与工厂接口进行交互,而不关心具体的工厂和产品类。这种模式遵循了开放封闭原则,当需要添加新的产品时,只需添加相应的具体工厂类,不需要修改其他代码。
  3. 抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或者相互依赖对象的接口,而无需指定它们具体的类。抽象工厂模式可以应对更复杂的场景,比如创建多个产品族的产品。客户端同样只需与抽象工厂接口进行交互,实现了对象创建过程的解耦。

工厂模式的优点:

  1. 降低了客户端与具体产品类之间的耦合度,提高了代码可维护性。
  2. 代码结构清晰,易于扩展和修改。
  3. 可以实现对象的复用,节省资源。

工厂模式的缺点:

  1. 工厂类职责过重,当产品种类繁多时,代码可能变得复杂。
  2. 增加了系统的抽象性和理解难度。

总之,工厂模式通过封装对象的创建过程,降低了客户端与具体产品类之间的耦合度,提高了代码的可维护性和扩展性。在实际开发中,可以根据项目的具体需求来选择适用的工厂模式类型。

3 谈谈你对单例模式的理解

单例模式(Singleton Pattern)是一种创建型设计模式,其核心思想是确保一个类只有一个实例,并提供一个全局访问点来获取该实例。这种模式适用于那些在整个系统中需要唯一实例的场景,如数据库连接池、配置管理器、日志记录器等。单例模式可以确保系统中该类的实例具有全局唯一性,避免了多次创建和销毁实例带来的资源浪费和潜在的错误。

实现单例模式的关键点包括:

  1. 将构造方法私有化,以防止客户端通过 new 关键字创建实例。
  2. 在类内部创建一个静态私有实例变量。
  3. 提供一个公共的静态方法(通常命名为 getInstance())来获取该实例。

单例模式有多种实现方式,主要包括以下几种:

  1. 懒汉式:实例在第一次调用 getInstance() 方法时创建。这种方式的优点是实现了延迟加载,缺点是在多线程环境下可能出现线程安全问题,需要通过加锁等手段解决。
  2. 饿汉式:实例在类加载时创建。这种方式的优点是线程安全,缺点是没有实现延迟加载,如果实例的创建过程比较耗时或资源消耗较大,可能会导致性能问题。
  3. 双重检查锁定(Double-Checked Locking):结合懒汉式和同步锁,在 getInstance() 方法内部进行双重判断,确保线程安全且避免了不必要的同步开销。
  4. 静态内部类:利用 Java 的静态内部类特性,在静态内部类中创建单例实例,实现了延迟加载且线程安全。
  5. 枚举:使用枚举类型实现单例,这种方式是线程安全的,而且代码简洁易懂。这也是《Effective Java》一书推荐的实现方式。

单例模式的优点:

  1. 保证了实例的全局唯一性,避免了资源浪费和潜在的错误。
  2. 可以实现全局访问,方便使用。

单例模式的缺点:

  1. 单例类的职责过重,可能违反单一职责原则。
  2. 若实例需要扩展,可能会引入修改困难。

总之,单例模式通过确保一个类只有一个实例,并提供一个全局访问点来获取该实例,可以在一定程度上提高系统的性能和资源利用率。在实际开发中,应根据具体需求选择合适的单例模式实现方式。

4 谈谈你对策略模式的理解

策略模式(Strategy Pattern)是一种行为型设计模式,其核心思想是定义一系列算法,将它们封装成策略类,并使它们可以相互替换。策略模式使得算法可以独立于使用它的客户端而变化,这样可以实现在不修改客户端代码的情况下,灵活地改变和扩展算法。策略模式将算法的定义和使用分离,降低了算法之间的耦合,提高了代码的可扩展性和可维护性。

策略模式通常包含以下几个部分:

  1. 策略接口(Strategy Interface):定义一个公共接口,用于声明所有策略类需要实现的方法。
  2. 具体策略类(Concrete Strategy):实现策略接口,封装具体的算法和行为。
  3. 上下文类(Context):持有一个策略接口的引用,用于调用具体策略类的方法。客户端可以通过修改上下文类持有的策略引用来改变算法。

策略模式的优点:

  1. 算法和客户端解耦:策略模式将算法的定义和使用分离,使得客户端与具体算法解耦,降低了模块间的耦合度。
  2. 易于扩展和替换:策略模式将每个算法封装成独立的策略类,可以方便地添加新的策略或替换现有策略,而无需修改客户端代码。
  3. 提高代码可读性:策略模式将不同的算法和行为封装到具体策略类中,使得代码结构更加清晰,提高了代码的可读性和可维护性。
  4. 遵循开放封闭原则:通过策略模式,可以在不修改客户端代码的前提下,灵活地改变和扩展算法,符合开放封闭原则。

策略模式的缺点:

  1. 增加了代码数量:每个策略都需要定义一个具体策略类,当策略较多时,会导致类数量的增加。
  2. 客户端需要了解策略的区别:虽然策略模式将算法的使用与实现分离,但客户端仍需要了解不同策略之间的区别,以便选择合适的策略。

总之,策略模式通过将算法封装成策略类并定义一个统一的策略接口,实现了算法和客户端的解耦,提高了代码的复用性。

5 谈谈你对观察者模式的理解

观察者模式(Observer Pattern),又称发布-订阅模式(Publish-Subscribe Pattern),是一种行为型设计模式。它定义了对象之间的一对多依赖关系,当一个对象(被观察者)的状态发生改变时,所有依赖于它的对象(观察者)都会得到通知并自动更新。观察者模式用于实现事件驱动的架构,降低了对象之间的耦合,提高了代码的灵活性和可扩展性。

观察者模式主要包含以下几个部分:

  1. 抽象被观察者(Subject):定义了添加、删除和通知观察者的方法。被观察者维护一个观察者列表,用于存储所有注册的观察者。
  2. 具体被观察者(Concrete Subject):实现抽象被观察者的接口,具有一些状态,当这些状态发生变化时,通知所有注册的观察者。
  3. 抽象观察者(Observer):定义一个更新方法,用于接收被观察者状态变化的通知。
  4. 具体观察者(Concrete Observer):实现抽象观察者的接口,当接收到被观察者状态变化的通知时,执行相应的更新操作。

观察者模式的优点:

  1. 降低耦合度:观察者模式使得被观察者与观察者之间的依赖关系变得松散,它们可以独立地变化和复用,提高了代码的灵活性。
  2. 支持事件驱动:观察者模式可以实现基于事件的通知机制,使得对象之间可以相互通知和响应,有助于实现复杂的事件驱动架构。
  3. 易于扩展:通过添加新的观察者,可以方便地扩展系统的功能,而无需修改被观察者的代码。

观察者模式的缺点:

  1. 可能引发循环调用:在实际应用中,如果观察者与被观察者之间存在循环依赖关系,可能会引发循环调用,导致系统崩溃。
  2. 通知顺序不可控:被观察者通知观察者的顺序是不可控的,这可能导致一些依赖于通知顺序的问题。

总之,观察者模式通过定义对象之间的一对多依赖关系,实现了事件驱动的架构,降低了对象间的耦合,提高了代码的灵活性和可扩展性。在实际开发中,观察者模式广泛应用于实现各种事件监听和通知机制,如用户界面的事件处理、消息推送、日志记录等。

在使用观察者模式时,还需要注意以下几点:

  1. 避免循环调用:确保被观察者和观察者之间不存在循环依赖关系,以防止循环调用导致的问题。
  2. 考虑性能问题:观察者模式的通知机制可能会导致大量的观察者被通知,从而影响系统性能。在设计时,应合理选择观察者的数量和通知策略,以减轻性能压力。
  3. 封装通知逻辑:为了让被观察者更加通用,可以考虑将通知观察者的逻辑封装到一个单独的类中,使得被观察者聚焦于自身业务逻辑。
  4. 使用现有框架:许多现代编程语言和框架都提供了对观察者模式的支持,如 Java 中的 java.util.Observablejava.util.Observer 类,C# 中的事件和委托机制等。在实际开发中,可以充分利用这些现有功能,避免重复造轮子。

综上所述,观察者模式是一种实用且灵活的设计模式,它提供了一种有效的方式来解决对象间的通信和依赖问题。在实际开发中,应根据具体需求和场景选择合适的观察者模式实现方式,并注意避免可能存在的问题。

九 浏览器

1 谈谈你对dom事件机制的理解

DOM(文档对象模型,Document Object Model)事件机制是Web浏览器中实现交互的重要组成部分。在JavaScript中,DOM事件机制使得开发者可以在特定情况下响应用户的操作,如点击、滚动、键盘输入等。以下是对DOM事件机制的一些理解:

  1. 事件流:DOM事件机制分为三个阶段,分别是捕获阶段(capturing phase)、目标阶段(target phase)和冒泡阶段(bubbling phase)。事件首先会经过捕获阶段,沿着DOM树从上至下传递,直到到达事件的目标元素。然后进入目标阶段,触发目标元素上的事件处理程序。最后是冒泡阶段,事件沿着DOM树从下往上回传。
  2. 事件处理程序:开发者可以为DOM元素添加事件处理程序,用以响应特定的事件。事件处理程序可以使用两种方式进行绑定:一种是通过HTML属性直接在元素上绑定,如onclick;另一种是通过JavaScript为元素添加事件处理程序,如addEventListener()方法。
  3. 事件对象:当事件触发时,浏览器会自动创建一个事件对象,该对象包含了有关事件的详细信息,例如事件类型、目标元素、触发时间等。事件对象通常作为事件处理程序的第一个参数传递。
  4. 事件委托:事件委托是一种事件处理机制,通过将事件处理程序绑定到父元素,可以实现对子元素的事件监听。这样可以减少事件处理程序的数量,提高性能。事件委托利用了事件冒泡机制,使得在父元素上可以捕捉到子元素触发的事件。
  5. 阻止默认行为和阻止事件传播:在某些情况下,我们希望阻止事件的默认行为或阻止事件的传播。可以通过调用事件对象的preventDefault()方法来阻止默认行为,调用stopPropagation()方法来阻止事件的传播。

了解DOM事件机制对于前端开发者非常重要,因为它涉及到网页交互、用户体验等关键方面。掌握DOM事件机制有助于编写更高效、可维护的代码。

2 CSS加载会造成阻塞吗?

CSS 加载对于渲染流程来说是有阻塞性的。当浏览器遇到一个外部 CSS 文件时,它会发送一个请求去获取这个文件。在获取并解析完 CSS 文件之前,浏览器会阻塞渲染流程。这是因为 CSS 文件中可能包含对页面元素的样式信息,浏览器需要确保在渲染页面时使用正确的样式。

尽管 CSS 加载阻塞渲染,但它并不阻塞其他资源的下载(如 JavaScript、图片等)。这意味着浏览器可以并行下载其他资源,从而提高页面的加载性能。

为了避免 CSS 阻塞渲染,可以采取以下措施:

  1. 将 CSS 放在 <head> 标签中:将 <link> 标签放在 <head> 中,可以确保浏览器尽早发现并下载 CSS 文件,从而减少渲染阻塞的时间。
  2. 使用媒体查询:通过为 CSS 文件添加媒体查询,可以告诉浏览器只在特定条件下使用这个文件。这样,在不满足条件的情况下,浏览器不会阻塞渲染。例如:<link rel="stylesheet" href="print.css" media="print">
  3. 代码分割和按需加载:将 CSS 分为多个较小的文件,只在需要时加载。例如,可以将关键 CSS 内联到 HTML 文档中,以加速首屏渲染。其他非关键 CSS 可以在页面加载完成后异步加载。
  4. 优化和压缩 CSS:优化和压缩 CSS 代码,可以减小文件大小,从而加快下载速度。可以使用 CSS 预处理器(如 Sass、Less)或压缩工具(如 CSSNano、UglifyCSS)进行优化。

通过采用这些策略,可以降低 CSS 加载对页面渲染的阻塞性,从而提高用户体验。

3 谈谈你对跨域资源共享CORS的理解

跨域资源共享(Cross-Origin Resource Sharing,简称 CORS)是一种安全机制,允许一个网页的资源(例如 AJAX 请求、字体、图片等)从不同的源(域名、协议或端口)访问。由于同源策略(Same-origin policy)的限制,不同源的网页通常无法互相访问资源。CORS 通过在 HTTP 头中加入特定的字段,使得浏览器和服务器之间可以协商,从而允许跨域请求。

CORS 主要通过以下几种方式实现跨域访问:

  1. 简单请求:简单请求是指满足一定条件的跨域请求,包括使用以下 HTTP 方法之一:GET、HEAD、POST,并且 HTTP 头信息不超出以下字段:Accept、Accept-Language、Content-Language、Content-Type(限于 application/x-www-form-urlencoded、multipart/form-data、text/plain)。对于简单请求,浏览器会在请求中添加 Origin 头,指示请求来自哪个源。服务器接收到请求后,如果允许跨域访问,会在响应头中添加 Access-Control-Allow-Origin 字段。
  2. 预检请求:预检请求(Preflight request)是一种 CORS 机制,用于处理不满足简单请求条件的跨域请求。预检请求使用 OPTIONS 方法发送,浏览器会在请求头中包含 OriginAccess-Control-Request-MethodAccess-Control-Request-Headers 字段。服务器收到预检请求后,如果允许跨域访问,会在响应头中添加 Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers 等字段。预检请求成功后,浏览器才会发送实际的跨域请求。
  3. 携带身份凭证:CORS 还支持在跨域请求中携带身份凭证(如 Cookie),通过设置 withCredentials 属性为 true。在这种情况下,服务器需要在响应头中添加 Access-Control-Allow-Credentials: true 字段,以允许携带身份凭证的请求。同时,Access-Control-Allow-Origin 字段不能为通配符(*),必须指定具体的源。

总的来说,CORS 通过在 HTTP 头中添加特定的字段,使得浏览器和服务器之间可以协商允许跨域访问。这种机制确保了 Web 应用程序的安全性,同时允许跨域资源共享。在实际开发中,服务器端需要正确配置 CORS 相关的响应头,以支持跨域请求。

4 浏览器的主要组成部分是什么?

浏览器主要由以下几个组成部分:

  1. 用户界面(User Interface):用户界面包括地址栏、前进/后退按钮、书签菜单等。它是用户与浏览器进行交互的部分。
  2. 浏览器引擎(Browser Engine):浏览器引擎是浏览器的核心部分,负责在用户界面和渲染引擎之间协调操作。它接收用户界面的命令,然后将其传递给渲染引擎或其他浏览器组件。
  3. 渲染引擎(Rendering Engine):渲染引擎负责显示请求的内容,如 HTML、CSS 和图像等。它将这些资源解析为屏幕上可见的图形。主流的渲染引擎有 WebKit(Chrome、Safari 等浏览器使用)和 Gecko(Firefox 使用)。
  4. 网络(Networking):网络组件用于处理网络请求和响应,如 HTTP 请求,以获取所需的资源(HTML、CSS、JavaScript、图像等)。它还负责处理网络协议、DNS 查询等。
  5. JavaScript 解释器(JavaScript Interpreter):JavaScript 解释器负责解析和执行 JavaScript 代码。主流的 JavaScript 引擎有 V8(Chrome 和 Node.js 使用)、SpiderMonkey(Firefox 使用)和 JavaScriptCore(Safari 使用)。
  6. UI 后端(UI Backend):UI 后端负责绘制用户界面的基本组件,如窗口、按钮等。它使用操作系统的用户界面方法,以实现跨平台的用户界面功能。
  7. 数据存储(Data Storage):浏览器需要在本地存储数据,如 Cookie、localStorage 和 IndexedDB 等。这些数据存储技术使得 Web 应用可以在浏览器中持久化数据。

这些组件共同构成了现代浏览器,使得用户可以浏览网页并与之互动。各个浏览器可能在实现上有所不同,但它们的基本组成部分和功能是相似的。

5 谈谈你对SPA单页应用的理解

SPA(Single Page Application,单页应用)是一种 Web 应用开发模式,它在浏览器中加载一个 HTML 文件,然后通过 JavaScript 动态更新和操作 DOM 元素,以实现页面内容的改变。在这种模式下,所有的操作都在同一个页面中进行,不需要跳转到其他页面。这样可以避免多次向服务器请求不同的页面,从而提高用户体验。

以下是关于 SPA 的一些特点和优缺点:

优点:

  1. 快速响应:SPA 在第一次加载时,获取所有必要的资源(HTML、CSS、JavaScript),之后只需通过 AJAX 请求获取数据,无需重新加载整个页面。这使得 SPA 更快速响应,为用户提供了更流畅的体验。
  2. 前后端分离:SPA 通常与 RESTful API 结合使用,使得前端和后端可以独立开发和部署。这种分离有助于提高开发效率,降低维护成本。
  3. 易于开发和调试:由于 SPA 主要依赖于 JavaScript 进行页面更新和数据处理,开发者可以使用各种现代 JavaScript 开发工具和框架,如 React、Angular 和 Vue 等,以简化开发和调试过程。

缺点:

  1. SEO 问题:传统的搜索引擎爬虫可能无法正确解析和执行 JavaScript,导致 SPA 页面的内容无法被搜索引擎索引。虽然现代搜索引擎如 Google 在这方面有所改进,但仍需额外的优化以确保 SPA 的搜索引擎优化(SEO)效果。
  2. 初始加载时间:由于 SPA 在第一次加载时需要获取所有必要的资源,这可能导致初始加载时间较长。为解决这个问题,可以采用代码分割、懒加载等技术来优化加载性能。
  3. 安全问题:SPA 可能面临跨站脚本(XSS)攻击和其他安全威胁。开发者需要采取相应的安全措施,如对用户输入进行过滤和验证,确保 Web 应用的安全性。

总的来说,SPA 是一种流行的 Web 应用开发模式,它具有快速响应、前后端分离等优点,但同时也需要注意解决 SEO、性能和安全等方面的问题。

6 为什么JS执行时会阻塞页面加载

JavaScript 执行阻塞页面加载的原因是浏览器在解析和渲染 HTML 文档时遵循自上而下的顺序。当浏览器遇到一个 <script> 标签时(尤其是外部 JavaScript 文件),它会立即下载并执行这个脚本。由于 JavaScript 可能会修改 DOM 结构或操作 CSSOM(如添加、删除元素或更改样式),浏览器必须确保在执行 JavaScript 之前,先解析完 JavaScript 之前的 HTML 和 CSS。这样可以防止页面出现不一致的渲染效果。

因此,当浏览器遇到一个 JavaScript 脚本时,它会:

  1. 阻塞 HTML 解析。
  2. 如果有外部 JavaScript 文件,发送请求并等待下载完成。
  3. 执行 JavaScript 代码。
  4. 继续解析剩余的 HTML 文档。

JavaScript 执行阻塞页面加载可能会导致性能问题和较差的用户体验。为了解决这个问题,可以采用以下策略:

  1. 将脚本放在文档底部:将 <script> 标签放在文档的底部,紧邻 </body> 标签。这样可以确保在执行 JavaScript 之前,先解析完 HTML 和 CSS,从而减少阻塞时间。
  2. 使用 defer 属性:给 <script> 标签添加 defer 属性,可以告诉浏览器在下载脚本的同时继续解析 HTML。脚本将在文档解析完成后顺序执行。例如:<script src="example.js" defer></script>
  3. 使用 async 属性:给 <script> 标签添加 async 属性,可以使脚本异步下载和执行。这意味着脚本会在下载完成后立即执行,而不用等待其他脚本。请注意,这可能会导致脚本执行顺序发生变化,因此只适用于那些不依赖其他脚本的独立脚本。例如:<script src="example.js" async></script>

通过采用这些策略,可以减少 JavaScript 对页面加载的阻塞性,提高页面性能和用户体验。

7 说一说你对Cookie localStorage sessionStorage的理解

Cookie、localStorage 和 sessionStorage 都是在客户端存储数据的技术,但它们之间有一些关键的区别:

  1. Cookie
    • Cookie 最初是为了在客户端保存用户会话信息而设计的,它们可以在客户端和服务器之间进行传递。
    • Cookie 的大小限制为 4KB 左右,因此不适合存储大量数据。
    • Cookie 有一个有效期,可以设置为特定的时间长度。过期后,Cookie 会被自动删除。
    • Cookie 可能面临安全风险,如跨站请求伪造(CSRF)攻击。因此,在使用 Cookie 时需要采取一定的安全措施。
    • 因为每次 HTTP 请求都会携带 Cookie,所以频繁操作 Cookie 可能会影响性能。
  2. localStorage
    • localStorage 是 HTML5 引入的一种客户端存储技术,允许在用户的浏览器中存储较大量的数据(通常为 5-10MB,根据浏览器实现有所不同)。
    • localStorage 中存储的数据没有有效期,会一直保留,除非用户手动清除或者使用代码进行删除。
    • localStorage 只在客户端进行操作,不会与服务器进行通信。
    • localStorage 的数据存储在同一个域名下,不同域名之间的 localStorage 数据是隔离的。
  3. sessionStorage
    • sessionStorage 与 localStorage 非常相似,它们都是 HTML5 引入的客户端存储技术,具有相同的存储容量限制。
    • 与 localStorage 不同的是,sessionStorage 的数据仅在当前浏览器标签或窗口的生命周期内有效。当用户关闭标签或窗口时,sessionStorage 中的数据会被自动清除。
    • sessionStorage 同样只在客户端进行操作,不会与服务器进行通信。
    • sessionStorage 的数据同样存储在同一个域名下,不同域名之间的 sessionStorage 数据是隔离的。

总之,Cookie、localStorage 和 sessionStorage 都是客户端存储技术,但它们之间在数据有效期、存储容量和与服务器通信等方面有所不同。根据具体需求和场景,开发者可以选择合适的技术进行数据存储。

8 讲讲浏览器缓存

浏览器缓存是一种性能优化技术,通过将已请求的资源(如 HTML、CSS、JavaScript、图片等)存储在本地,以减少网络请求和数据传输。当用户再次访问相同的资源时,浏览器可以直接从缓存中获取,从而加快页面加载速度、降低服务器压力和减少网络流量消耗。

浏览器缓存主要分为以下几种类型:

  1. 强缓存: 强缓存是指浏览器在缓存期间内不会再向服务器发送任何请求,而是直接从本地缓存中获取资源。强缓存由以下两个 HTTP 响应头控制:
    • Expires:指定资源的过期时间。例如:Expires: Wed, 21 Oct 2023 07:28:00 GMT。但由于服务器时间和客户端时间可能存在差异,所以这个响应头不太准确。
    • Cache-Control:使用更现代且准确的方式控制缓存。例如:Cache-Control: max-age=3600,表示资源的缓存有效期为 3600 秒。其他可用的指令还包括 no-cacheno-storemust-revalidate 等。
  2. 协商缓存: 当强缓存失效后,浏览器会向服务器发送请求,以验证资源是否有更新。协商缓存通过以下 HTTP 响应头和请求头控制:
    • Last-ModifiedIf-Modified-Since:服务器返回资源时,通过 Last-Modified 响应头指定资源最后修改时间。浏览器下次请求时会通过 If-Modified-Since 请求头发送这个时间。服务器会比较这个时间和资源的实际修改时间,如果没有变化,则返回 304 Not Modified 状态码,浏览器将使用本地缓存;否则返回更新后的资源和新的 Last-Modified 时间。
    • ETagIf-None-MatchETag 是服务器为资源生成的唯一标识(通常是哈希值)。浏览器下次请求时会通过 If-None-Match 请求头发送这个标识。服务器会比较这个标识和资源的当前 ETag,如果相同,则返回 304 Not Modified 状态码;否则返回更新后的资源和新的 ETag
  3. 其他缓存技术: 除了上述浏览器缓存机制外,还有一些其他缓存技术,如 Service Workers、Memory Cache、IndexedDB 等。它们可以根据具体需求和场景进行灵活地缓存策略配置。

9 路由history和hash的区别?

路由 history 和 hash 是前端路由(Single Page Application, SPA)中常用的两种模式。它们的主要区别在于实现方式和 URL 的展示形式。

  1. Hash 模式
    • Hash 模式基于浏览器 URL 中的哈希(#)部分进行路由切换。当哈希值改变时,浏览器不会向服务器发送请求,而是触发 hashchange 事件。前端路由库会监听这个事件,并根据哈希值变化来更新视图。
    • URL 示例:https://example.com/#/page1
    • Hash 模式兼容性较好,适用于旧版本浏览器。
  2. History 模式
    • History 模式基于 HTML5 的 History API(如 pushStatereplaceStatepopstate 事件)实现。这些 API 允许在不重新加载页面的情况下,直接操作浏览器的历史记录和 URL。
    • URL 示例:https://example.com/page1
    • History 模式需要服务器的配合,因为在用户直接访问某个路由(如 https://example.com/page1)时,如果服务器没有对应的配置,可能会返回 404 错误。为了解决这个问题,服务器需要设置一个通配符路由,将所有未匹配到的路由都重定向到单页应用的入口 HTML 文件。

总之,Hash 模式和 History 模式是前端路由中的两种实现方式,具有不同的 URL 展示形式和浏览器兼容性。在选择路由模式时,需要根据项目需求、用户体验和服务器配置等因素进行权衡。

10 谈一谈跨域,同源策略,以及跨域解决方案

同源策略: 同源策略是一种安全机制,用于限制文档或脚本从不同来源的数据进行交互。当两个资源具有相同的协议(如 http 或 https)、域名和端口时,它们被认为是同源的。同源策略限制了从一个源加载的网页脚本与来自另一个源的资源进行交互的能力,以防止恶意行为,如跨站脚本攻击(XSS)或数据窃取。

跨域: 跨域是指当一个资源试图访问来自不同源的另一个资源时,由于同源策略的限制而无法进行的情况。例如,一个从 http://example-a.com 加载的网页试图通过 AJAX 请求 http://example-b.com 上的数据,这就是一个跨域请求。

跨域解决方案: 有多种方法可以绕过同源策略的限制,实现跨域请求。以下是一些常见的跨域解决方案:

  1. CORS(跨域资源共享): CORS 是一种官方推荐的跨域解决方案。它允许服务器通过设置响应头(如 Access-Control-Allow-Origin)来放宽对跨域请求的限制。浏览器将根据这些响应头决定是否允许跨域请求。
  2. JSONP(JSON with Padding): JSONP 利用了 <script> 标签的 src 属性不受同源策略限制的特性。它通过动态创建一个 <script> 标签,并将回调函数作为参数传递给服务器。服务器将请求数据包装在回调函数中,并将其作为响应返回。客户端脚本收到响应后,会立即执行回调函数以获取数据。
  3. 使用代理服务器: 可以通过代理服务器将跨域请求转发到目标服务器。这样,客户端与代理服务器之间的请求就是同源的,而代理服务器与目标服务器之间的请求则由代理服务器完成。常见的代理服务器实现方式有 Nginx 反向代理、Node.js 中间件代理等。
  4. 使用 iframe 和 postMessage: 可以使用 iframe 加载跨域页面,然后通过 window.postMessage 方法在不同源的窗口之间传递消息。这种方法需要跨域页面的配合,以便在接收到消息时执行相应操作。
  5. 使用 WebSockets: WebSockets 可以实现跨域通信,因为它们不受同源策略限制。通过建立一个 WebSocket 连接,客户端和服务器可以进行双向通信,实现跨域数据交换。
  6. 使用 CORS 代理: 如果目标服务器没有实现 CORS,而你又不想在自己的服务器上设置反向代理,可以使用第三方 CORS 代理服务。这些服务在请求目标服务器时为你添加 CORS 响应头,从而使浏览器允许跨域请求。但请注意,这可能会带来安全风险,因此不建议在生产环境中使用。
  7. 使用 document.domain: 当两个具有相同根域名但子域名不同的页面需要进行跨域通信时,可以将它们的 document.domain 设置为相同的值。这样,它们将被视为同源,可以进行通信。但此方法仅适用于具有相同根域名的情况。
  8. 使用 window.name: 可以利用 window.name 在同源和跨域的窗口之间传递数据。window.name 属性在窗口跳转时会保留其值,因此可以将数据存储在 window.name 中,然后通过跳转到同源页面来读取数据。但这种方法受到一些限制,只能传递字符串数据,且安全性和可靠性相对较低。

每种跨域解决方案都有其优缺点,需要根据实际场景和需求选择合适的方法。现代 Web 开发中,CORS 是最常用且推荐的解决方案。但在某些特殊场景或者兼容旧版浏览器时,可能需要考虑其他方案。在实现跨域时,始终要关注安全性和数据保护,确保不会引入安全漏洞。

11 前端如何进行seo优化

前端在进行搜索引擎优化(SEO)时,可以从以下几个方面进行优化:

  1. 合理的标题、描述和关键词: 确保每个页面都有独特且描述准确的<title>标签和<meta name="description">标签。这有助于搜索引擎理解页面内容,同时也为用户在搜索结果中显示更具吸引力的信息。
  2. 语义化的 HTML 结构: 使用语义化的 HTML 标签,如<header><footer><nav><section><article>等,以帮助搜索引擎更好地理解页面结构和内容。
  3. 良好的页面结构: 确保页面具有清晰的层次结构和导航,这有助于搜索引擎爬虫更容易地抓取和索引页面内容。
  4. 优化 URL 结构: 使用简洁且描述性的 URL,避免过长或包含不必要的参数。这有助于搜索引擎理解页面内容,并提高用户体验。
  5. 使用 header 标签: 使用<h1><h6>等 header 标签来组织页面内容,确保每个页面只有一个<h1>标签。这有助于搜索引擎理解页面的主题和重要性。
  6. 图片优化: 为所有图片添加alt属性,以便搜索引擎了解图片内容。同时,优化图片大小和格式,以提高页面加载速度。
  7. 内部链接和锚文本: 合理地使用内部链接和有意义的锚文本,以帮助搜索引擎更好地理解站点结构和内容之间的关联。
  8. 响应式设计: 确保网站适应不同设备和屏幕尺寸,提供良好的用户体验。搜索引擎倾向于对移动友好的网站给予更高的排名。
  9. 网站速度优化: 提高网站加载速度,减少页面渲染时间。这包括压缩资源、合并文件、优化图片、使用浏览器缓存等。搜索引擎会考虑网站速度作为排名的因素之一。
  10. 遵循 W3C 标准: 确保代码符合 W3C 标准,减少 HTML、CSS 和 JavaScript 的错误。这有助于搜索引擎更容易地抓取和解析页面内容。
  11. 生成 XML Sitemap: 为网站创建一个 XML Sitemap,并提交给搜索引擎。这有助于搜索引擎更有效地抓取和索引网站内容。
  12. 使用结构化数据: 使用结构化数据(如 Schema.org、JSON-LD、Microdata 等)来标注页面内容,有助于搜索引擎更准确地理解页面信息,并可能在搜索结果中显示为富文本摘要,提高点击率。
  13. 使用 robots.txt 控制爬虫访问: 合理设置 robots.txt 文件,指定搜索引擎爬虫可以访问和抓取的页面,避免爬虫抓取不相关或低质量的页面。
  14. 优化站内搜索: 提供高效、准确的站内搜索功能,有助于提高用户体验和用户停留时间,间接影响搜索排名。
  15. 社交媒体整合: 将网站内容与社交媒体平台整合,提高内容的曝光度和分享率,增加外部链接,有助于提高搜索排名。
  16. 网站安全: 使用 HTTPS 加密,保护用户数据和隐私。搜索引擎会将安全性作为排名因素之一。

总之,前端在进行 SEO 优化时,要关注页面结构、内容、用户体验和技术实现等多个方面。通过提高页面质量、提升用户体验和遵循搜索引擎的最佳实践,有助于提高网站在搜索结果中的排名和可见度。

12 SSR的实现原理?

服务器端渲染(Server Side Rendering,简称 SSR)是一种 web 应用的渲染方式,其实现原理是在服务器端将页面内容渲染为 HTML 字符串,然后将这些字符串发送到客户端,客户端接收到 HTML 后直接显示,无需等待 JavaScript 的解析、执行和渲染。

SSR 的核心实现原理可以概括为以下几个步骤:

  1. 请求处理:客户端发起请求时,请求首先到达服务器。
  2. 服务器渲染:服务器接收到请求后,根据路由和数据,将对应的页面内容渲染为 HTML 字符串。这一步通常涉及到模板引擎或服务器端 JavaScript 框架的使用,例如 React 的 ReactDOMServer.renderToString() 和 Vue 的 vue-server-renderer
  3. 数据注入:在渲染过程中,服务器还需要获取所需的数据,将其注入到 HTML 中。这可以通过内联脚本、JSON 格式或其他方式实现。数据注入的目的是在客户端 JavaScript 代码运行时能够获取到服务器端已经准备好的数据,避免重复请求。
  4. 生成完整 HTML:将渲染好的 HTML 字符串插入到 HTML 模板中,形成一个完整的 HTML 页面。这个页面包含了已经渲染好的内容以及必要的 JavaScript、CSS 等资源引用。
  5. 发送响应:服务器将完整的 HTML 页面作为响应发送给客户端。客户端接收到响应后,直接将 HTML 页面展示给用户。
  6. 客户端接管:在客户端,浏览器会解析和执行 JavaScript 代码,接管页面的交互和操作。这一步通常被称为 "hydration" 或 "激活",因为客户端 JavaScript 代码需要 "激活" 服务器端渲染的静态 HTML,使其具有动态功能。

通过 SSR,用户可以更快地看到页面的内容,因为无需等待客户端的 JavaScript 代码下载、解析、执行和渲染。此外,由于搜索引擎爬虫能够更好地解析服务器端渲染的 HTML,因此 SSR 对 SEO 也有一定的优势。然而,SSR 也会增加服务器的负担,因为服务器需要负责渲染页面。在实际应用中,可以根据项目需求和性能要求,选择合适的渲染方式。

13 谈谈浏览器的离线缓存与本地缓存的区别

浏览器的离线缓存和本地缓存都是用于在用户设备上存储数据以提高网页性能的技术,但它们的实现方式和使用场景略有不同。下面是它们之间的一些主要区别:

  1. 目的:

离线缓存:主要用于在用户离线时仍然可以访问网页。它可以在没有网络连接的情况下提供基本的功能,如查看已缓存页面的内容。

本地缓存:主要用于减少网络延迟,提高加载速度。通过在用户设备上缓存常用资源,如图片、脚本、样式等,可以在用户再次访问网站时直接从缓存中读取,降低服务器负担和提高用户体验。

  1. 存储类型:

离线缓存:使用HTML5的Application Cache(AppCache)和Service Worker技术实现。这些技术允许开发者为用户设备创建一个离线版本的网页,包括HTML、CSS、JavaScript等资源。

本地缓存:使用浏览器提供的缓存策略和存储机制实现,如HTTP缓存、Cookie、Web Storage(包括localStorage和sessionStorage)和IndexedDB等。

  1. 生命周期:

离线缓存:由开发者通过AppCache或Service Worker配置文件控制,包括缓存资源的更新和过期策略。

本地缓存:由浏览器或服务器通过HTTP头部字段(如Cache-Control、Expires等)控制,或通过脚本(如localStorage和IndexedDB)设置的存储时间限制。

  1. 适用场景:

离线缓存:适用于需要在无网络环境下访问的网页,例如离线阅读应用、PWA(Progressive Web Apps)等。

本地缓存:适用于任何需要提高网站性能和加载速度的场景,减少不必要的网络请求。

总之,离线缓存和本地缓存都是为了优化网页性能,它们各自针对不同的使用场景。离线缓存主要是为了在没有网络连接时继续访问网页,而本地缓存则是为了提高网页加载速度和降低服务器负担。

14 谈谈你对Shadow DOM的理解

Shadow DOM 是 Web Components 规范的一个重要组成部分,它提供了一种将 HTML、CSS 和 JavaScript 封装在独立、隔离的 DOM 结构中的方法,从而实现组件的样式和行为的封装。Shadow DOM 解决了全局样式污染的问题,使得开发者可以创建具有独立样式和逻辑的可重用组件。

Shadow DOM 的关键特点如下:

  1. 封装:Shadow DOM 允许将一组 DOM 元素和相关样式封装在一个独立的、隔离的 DOM 树中。这样,组件的样式不会影响到主文档,同样主文档的样式也不会影响到组件。通过这种封装,组件可以在不同的应用中重用,而无需担心样式污染和冲突。
  2. 隔离:Shadow DOM 的树结构是隔离的,意味着组件内的 DOM 元素和 JavaScript 逻辑与主文档是分开的。这有助于保护组件内的数据和方法,避免被外部访问和修改。
  3. 附件阴影树:通过 attachShadow 方法,可以在一个普通的 DOM 元素上创建一个 Shadow DOM。这个普通元素被称为 "Shadow Host",而创建的 Shadow DOM 被称为 "Shadow Tree"。Shadow Tree 与主文档的 DOM 树是并列的,互不干扰。
  4. 插槽(Slot):Shadow DOM 支持使用 <slot> 元素来分发(或投影)主文档中的内容。这使得组件可以定义可自定义的内容区域,让使用者在引入组件时提供所需的内容。
  5. 样式隔离:组件内的 CSS 样式只对 Shadow Tree 中的元素生效,不会影响到主文档。同时,主文档的 CSS 样式(除了 CSS 变量)也不会影响到组件。这样,组件的样式能够完全独立,避免了全局样式污染。

总之,Shadow DOM 提供了一种在 Web 开发中实现组件封装和样式隔离的机制。通过使用 Shadow DOM,开发者可以创建具有独立样式和行为的可重用组件,简化开发过程,提高组件的可维护性。

谈谈你对微前端的理解?

微前端(Micro Frontends)是一种架构模式,它的主要目标是将单一的,通常较大的前端应用程序(如单页面应用)拆分为多个较小的、独立的部分。这些独立的部分通常被称为"微应用"(micro apps)。

微前端的主要理念来源于微服务架构,后者在后端开发中已经变得非常流行。与微服务一样,微前端也强调团队的独立性和技术栈的多样性。每个微应用可以由不同的团队使用不同的技术栈来开发,然后再集成到一个统一的用户界面中。

以下是我对微前端的一些理解:

  1. 解耦:微前端的一个关键优点是解耦。每个微应用都是独立的,有自己的代码库、构建流程、开发团队和生命周期。这可以使每个团队更专注于自己的部分,降低开发复杂性,提高开发速度。

  2. 技术栈无关:每个微应用可以选择适合自己需求的技术栈,不必受限于整个应用的技术选择。这使得前端开发可以跟上技术的发展,逐步引入新的技术和工具,而无需进行大规模的重构。

  3. 并行开发:由于每个微应用都是独立的,所以可以并行开发,提高开发效率。也可以更灵活地调整开发资源和计划,因为每个团队的工作不会直接影响到其他团队。

  4. 独立部署:每个微应用可以独立部署,不需要重新部署整个应用。这可以大大减少部署带来的风险,并且可以更快地将新功能和修复推送到用户那里。

但是,微前端也不是没有挑战的。例如,微应用间的通信和协调、整体用户体验的一致性、性能问题(比如加载时间和资源占用)等,都需要在实施微前端架构时仔细考虑。

总的来说,我认为微前端是一个有前景的架构模式,它在前端开发中引入了微服务的思想,有助于解决大型、复杂的前端应用开发和维护的问题。但是,它也需要对前端架构和工程化有深入的理解,才能有效地实施和管理。

15 讲讲浏览器的进程和线程

浏览器的进程和线程是浏览器实现其功能的基础。它们之间的关系和任务分工对于理解浏览器的运行原理和性能优化至关重要。简单来说,进程是操作系统资源分配的最小单位,而线程是操作系统调度(CPU 利用率)的最小单位。

进程

进程是一个运行中的程序实例,它包含程序所需的所有资源。一个进程拥有独立的内存空间、全局变量、打开的文件和设备等。浏览器中的进程主要有以下几类:

  1. 浏览器主进程:负责协调浏览器的各个模块,包括用户界面、地址栏、书签栏等。它还负责管理浏览器的各个标签页进程和插件进程。
  2. 渲染进程:负责将网页内容渲染到屏幕上。每个标签页通常对应一个渲染进程(在某些情况下,标签页可能会共享一个渲染进程)。渲染进程包括 HTML、CSS 和 JavaScript 的解析、布局、渲染以及执行等任务。
  3. 插件进程:负责运行和管理浏览器插件(如 Flash)。
  4. 网络进程:负责处理网络请求,包括资源的下载、上传和缓存。

线程

线程是进程中的一个执行单元,它共享进程的资源,如内存空间和文件句柄。一个进程可以有多个线程,这些线程可以并发执行任务。在浏览器中,有以下几种主要的线程:

  1. 主线程:渲染进程的主要执行线程,负责解析和执行 JavaScript 代码、处理 DOM 事件、执行 CSS 动画等任务。主线程是单线程的,这意味着 JavaScript 的执行和页面渲染任务需要排队执行。
  2. Web Workers:一种可以在后台运行 JavaScript 代码的线程,它与主线程独立,不会阻塞主线程。Web Workers 可以用于执行耗时的计算任务,避免影响页面渲染。
  3. Service Workers:用于实现离线缓存、消息推送、后台同步等功能的独立线程。Service Workers 与 Web Workers 类似,但具有更多的 API 和能力。
  4. 渲染线程:负责将解析好的 HTML、CSS 和图像资源绘制到屏幕上。渲染线程与主线程是分离的,这意味着页面的渲染和 JavaScript 的执行是并行进行的。
  5. 合成线程:负责处理页面的合成和分层。当浏览器检测到可以使用 GPU 加速的动画时,合成线程会将这些动画分离出来并在 GPU 上独立运行。这可以避免主线程的阻塞,提高页面的性能。
  6. 网络线程:负责处理 HTTP 请求和响应,以及与服务器之间的通信。网络线程与主线程独立,以避免网络请求导致的阻塞。
  7. 定时器线程:负责处理 JavaScript 中的定时器任务(如 setTimeoutsetInterval)。这些任务会在定时器线程上排队执行,然后在指定的时间后将回调函数推送到主线程的任务队列中。
  8. 解析线程:负责解析 HTML 和 CSS。解析线程会将解析后的 DOM 树和 CSSOM 树合并为渲染树,并传递给渲染线程进行绘制。在某些情况下,解析线程和主线程可能会合并为一个线程,这取决于浏览器的具体实现。
  9. 文件线程:负责处理与文件系统的交互,如读取和写入操作。这些操作在文件线程上执行,避免阻塞主线程。

以上就是浏览器中的一些主要进程和线程。需要注意的是,不同浏览器的实现可能会有所不同,但它们的基本原理和任务分工是类似的。理解这些进程和线程之间的关系有助于我们更好地理解浏览器的运行原理,从而优化前端性能。

16 html解析过程

HTML(超文本标记语言)是用于构建和呈现网页内容的标准标记语言。浏览器解析HTML的过程包括以下几个步骤:

  1. 获取HTML文档:浏览器首先向服务器发送请求,获取HTML文档。服务器响应请求并返回HTML文件,通常是一个以.html.htm为扩展名的文件。
  2. 词法分析:浏览器开始对HTML文档进行词法分析,将其分解成各种符号(tokens),例如标签、属性和文本内容。词法分析的结果是一系列token,这些token有助于构建DOM树。
  3. 构建DOM树:浏览器将词法分析得到的tokens用于构建DOM(文档对象模型)树。DOM树是一种表示HTML文档结构的树形数据结构,其中每个节点代表页面上的一个元素、属性或文本内容。
  4. 解析CSS:浏览器会解析与HTML文档关联的CSS样式表,包括内联样式、内部样式和外部样式。解析CSS样式后,浏览器会生成CSSOM(CSS对象模型)树,这是一种表示CSS样式的树形结构。
  5. 构建渲染树:浏览器将DOM树和CSSOM树合并,生成渲染树。渲染树包含了页面上可见的所有元素及其样式信息。隐藏元素(如display: none;)不会包含在渲染树中。
  6. 布局(Layout):根据渲染树,浏览器计算每个元素在页面上的准确位置和大小。这个过程也被称为重排(reflow)。
  7. 绘制(Painting):布局完成后,浏览器开始将元素绘制到屏幕上。这个过程包括绘制文本、颜色、图片、边框等视觉效果。
  8. 合成(Compositing):在某些情况下,浏览器会将页面分成多个层进行绘制。最后,这些层会按照特定顺序合成为最终的页面视图。

在整个解析过程中,浏览器可能还需要处理JavaScript代码。JavaScript可以通过修改DOM树、CSSOM树和触发事件等方式影响页面的呈现。

17 说一说从输入URL到页面呈现发生了什么

从输入 URL 到页面呈现,经历了以下几个主要步骤:

  1. 地址解析:浏览器首先解析输入的 URL,提取协议、域名、端口和路径等信息。

  2. DNS 查询:浏览器通过 DNS 查询将域名解析为 IP 地址。如果浏览器或操作系统缓存中已有相应的 DNS 记录,将直接使用缓存的结果;否则,浏览器将发送请求到 DNS 服务器进行查询。

  3. 建立 TCP 连接:浏览器与目标服务器建立 TCP 连接,进行三次握手。这一步确保了数据传输的可靠性。

  4. 发送 HTTP 请求:浏览器构建 HTTP 请求报文,包含请求头(如 User-Agent、Accept 等)和请求体(如 POST 提交的表单数据),然后通过建立的 TCP 连接将请求报文发送给服务器。

  5. 服务器响应:服务器收到请求后,处理请求并生成响应报文,包含响应头(如 Content-Type、Content-Length 等)和响应体(如请求的 HTML 文件)。服务器通过 TCP 连接将响应报文发送回浏览器。

  6. 浏览器接收响应:浏览器接收并解析响应报文,提取状态码、响应头和响应体等信息。

  7. 解析 HTML:浏览器开始解析 HTML 文档,构建 DOM 树。遇到外部资源(如 CSS、JavaScript、图片等),浏览器会发起额外的请求获取这些资源。

  8. 解析 CSS:浏览器解析 CSS 文件,构建 CSSOM 树。CSSOM 树和 DOM 树会被合并为渲染树。

  9. 执行 JavaScript:浏览器解析并执行 JavaScript 代码。注意,JavaScript 的执行可能会修改 DOM 树和 CSSOM 树,从而影响渲染树的构建。

  10. 构建渲染树:浏览器将 DOM 树和 CSSOM 树合并为渲染树,包含每个可见元素的布局信息。

  11. 布局:浏览器根据渲染树计算每个元素的准确位置和大小,确定页面的布局。

  12. 绘制:浏览器遍历渲染树,将每个元素绘制到屏幕上。

  13. 合成:在某些情况下,浏览器会将页面分成多个层进行绘制。最后,这些层会按照特定顺序合成为最终的页面视图。

  14. 页面呈现:浏览器将渲染好的页面呈现给用户。

总之,从输入 URL 到页面呈现,浏览器经历了一系列复杂的过程。理解这些过程有助于我们优化前端性能,提高用户体验。

18 当前的前端渲染方式有哪些,谈谈你对它们的理解,并说说它们的优缺点是什么?

当前主要的前端渲染方式有三种:服务器端渲染(SSR)、客户端渲染(CSR)和预渲染(Prerendering)。下面分别阐述这三种渲染方式的特点及优缺点:

  1. 服务器端渲染(SSR)

    服务器端渲染指的是在服务器上将网页的 HTML、CSS 和 JavaScript 渲染成完整的 HTML 页面,然后将渲染后的页面发送给客户端。客户端接收到页面后直接展示,无需执行额外的 JavaScript。

    • 优点:
      1. 有利于 SEO,因为搜索引擎可以直接爬取完整的 HTML 页面。
      2. 首屏加载速度较快,因为用户无需等待 JavaScript 执行完毕就能看到页面内容。
    • 缺点:
      1. 服务器端压力较大,因为每次请求都需要服务器进行页面渲染。
      2. 用户交互复杂度受限,因为每次交互都可能需要重新渲染页面。
  2. 客户端渲染(CSR)

    客户端渲染指的是将 HTML、CSS 和 JavaScript 发送给客户端,然后在客户端执行 JavaScript 来生成页面内容。这种方式在现代前端框架(如 React、Vue 和 Angular)中非常常见。

    • 优点:
      1. 服务器压力较小,因为页面渲染工作由客户端完成。
      2. 用户交互体验较好,因为客户端可以实现动态的、无需刷新页面的交互。
    • 缺点:
      1. 首屏加载速度较慢,因为客户端需要等待 JavaScript 执行完毕才能看到页面内容。
      2. 不利于 SEO,因为搜索引擎在爬取页面时可能无法执行 JavaScript。
  3. 预渲染(Prerendering)

    预渲染是在构建过程中生成静态 HTML 文件,然后将这些静态文件部署到服务器。当用户请求页面时,服务器直接返回对应的静态 HTML 文件。这种方法适用于内容不经常变动的网站。

    • 优点:
      1. 首屏加载速度较快,因为用户无需等待 JavaScript 执行完毕就能看到页面内容。
      2. 有利于 SEO,因为搜索引擎可以直接爬取静态 HTML 页面。
      3. 服务器压力较小,因为页面在构建过程中已经渲染完成。
    • 缺点:
      1. 构建过程可能较慢,尤其是对于大型网站。
      2. 如果网站内容经常变动,需要频繁地重新构建和部署页面。

总结:

每种渲染方式都有其优缺点,具体选择哪种方式要根据实际项目需求进行权衡。以下是一些建议:

  • 如果 SEO 对项目至关重要,而且项目内容相对稳定,则可以考虑使用预渲染或服务器端渲染。
  • 如果项目需要提供丰富的用户交互体验,可以考虑使用客户端渲染,这样能够充分利用现代前端框架的优势。
  • 如果项目的内容动态性较强,可以考虑使用服务器端渲染或客户端渲染。但对于客户端渲染,可能需要考虑使用服务端渲染的同构应用或者采用一些服务器端渲染辅助的技术(如 prerender.io)来提高 SEO 效果。
  • 对于首屏加载速度有较高要求的项目,可以考虑使用服务器端渲染或预渲染。

实际项目中,可以根据需求灵活地将不同渲染方式结合使用,以达到最佳的性能和用户体验。例如,可以在服务器端渲染首屏内容以提高首屏加载速度,然后在客户端渲染后续的页面内容以提供更好的交互体验。

十 编码

用setTimeout实现setInterval

function customSetInterval(callback: () => void, delay: number): () => void {
    let timer;

    const interval = () => {
        callback();
        timer = setTimeout(interval, delay)
    }

    timer = setTimeout(interval, delay)

    // 返回清除定时器的函数
    return () => {
        clearTimeout(timer)
    }
}

// 示例用法
const callback = () => console.log("Hello, world!");
const delay = 1000; // 每隔1000毫秒执行一次
const clearIntervalFunc = customSetInterval(callback, delay);

// 用法:在需要的时候清除定时器
// clearIntervalFunc();

var实现let

function demo() {
    (function() {
        var x = "Hello, world!";
        console.log(x); // 输出 "Hello, world!"
    })();

    try {
        console.log(x); // 抛出 ReferenceError,因为x在这个作用域内未定义
    } catch (error) {
        console.error(error); // 输出错误信息
    }
}

demo();

实现所有的TypeScript Utility Types

// 1. Partial<T>
type Partial<T> = { [P in keyof T]?: T[P] };

// 2. Required<T>
type Required<T> = { [P in keyof T]-?: T[P] };

// 3. Readonly<T>
type Readonly<T> = { readonly [P in keyof T]: T[P] };

// 4. Pick<T, K>
type Pick<T, K extends keyof T> = { [P in K]: T[P] };

// 5. Omit<T, K>
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// 6. Exclude<T, U>
type Exclude<T, U> = T extends U ? never : T;

// 7. Extract<T, U>
type Extract<T, U> = T extends U ? T : never;

// 8. NonNullable<T>
type NonNullable<T> = Exclude<T, null | undefined>;

// 9. ReturnType<T>
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

// 10. InstanceType<T>
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

// 11. ThisParameterType<T>
type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;

// 12. OmitThisParameter<T>
type OmitThisParameter<T> = T extends (this: any, ...args: infer A) => infer R ? (...args: A) => R : T;

// 13. ThisType<T>
// `ThisType` is a marker utility type and can't be implemented like other utility types.

防抖debounce

function debounce(func: (...args: any[]) => void, wait: number): (...args: any[]) => void {
    let timeout: ReturnType<typeof setTimeout> | null = null;

    return (...args: any[]) => {
        if (timeout) {
            clearTimeout(timeout);
        }

        timeout = setTimeout(() => {
            func.apply(null, args);
        }, wait);
    };
}

节流throttle

function throttle(func: (...args: any[]) => void, limit: number): (...args: any[]) => void {
    let lastCall = 0; // 记录上次调用的时间戳

    return (...args: any[]) => {
        const now = Date.now(); // 获取当前时间戳

        // 如果当前时间与上次调用的时间差大于等于设定的限制时间,执行函数并更新上次调用时间
        if (now - lastCall >= limit) {
            func.apply(null, args);
            lastCall = now;
        }
    };
}

New

function customNew(constructorFn: Function, ...args: any[]): object {
    const obj = Object.create(constructorFn.prototype); // 创建一个新对象,并将其原型链设置为构造函数的prototype
    const result = constructorFn.apply(obj, args); // 调用构造函数并将this绑定到新创建的对象

    // 如果构造函数返回了一个对象,那么返回这个对象,否则返回创建的新对象
    return (typeof result === "object" && result !== null) ? result : obj;
}

// 示例用法
function Person(name: string, age: number) {
    this.name = name;
    this.age = age;
}

const alice = customNew(Person, "Alice", 30) as Person;
console.log(alice.name); // 输出 "Alice"
console.log(alice.age);  // 输出 30

数组去重

// First
const uniqueArray = (arr: any[]) => {
    return [...new Set(arr)]
}

// Second
const uniqueArray = (arr: any[]) => {
	const result = [];
  for (const item of arr) {
    if (result.indexOf(item) === -1) {
      result.push(item);
    }
  }
  return result;
}

// Third
const uniqueArray = (arr: any[]) => {
  return arr.filter((item, index) => {
    return arr.indexOf(item) === index
  })
}

console.log(uniqueArray([1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5])); // [1, 2, 3, 4, 5, 6]

实现正则切分千分位

function formatThousands(n: number): string {
  const reg = /\d{1,3}(?=(\d{3})+$)/g;
  const num = n.toString();
  const formattedNum = num.replace(reg, '$&,');
  return formattedNum;
}

// 测试
console.log(formatThousands(123456789)); // 输出: 123,456,789
console.log(formatThousands(1000000)); // 输出: 1,000,000
console.log(formatThousands(9876543210)); // 输出: 9,876,543,210

call

// 实现自定义call方法
Function.prototype.myCall = function (thisArg: any, ...args: any[]): any {
    const fn = this;
    const uniqueKey = Symbol("uniqueKey");
    thisArg[uniqueKey] = fn;

    const result = thisArg[uniqueKey](...args);
    delete thisArg[uniqueKey];

    return result;
};

const obj = {
    name: 'Alice'
}

// 示例用法
function greet(greeting: string, punctuation: string) {
    console.log(`${greeting}, ${this.name}${punctuation}`);
}

greet.myCall(obj, "Hello", "!"); // 输出 "Hello, Alice!"

apply

// 实现自定义apply方法
Function.prototype.myApply = function (thisArg: any, args: any[]): any {
    const fn = this;
    const uniqueKey = Symbol("uniqueKey");
    thisArg[uniqueKey] = fn;

    const result = thisArg[uniqueKey](...args);
    delete thisArg[uniqueKey];

    return result;
};

const obj = {
    name: 'Alice'
}

// 示例用法
function greet(greeting: string, punctuation: string) {
    console.log(`${greeting}, ${this.name}${punctuation}`);
}

greet.myApply(obj, ["Hi", "!"]); // 输出 "Hi, Alice!"

bind

// 实现自定义bind方法
Function.prototype.myBind = function (thisArg: any, ...args1: any[]): (...args2: any[]) => any {
    const fn = this;

    return function (...args2: any[]) {
        return fn.myApply(thisArg, args1.concat(args2));
    };
};

const obj = {
    name: 'Alice'
}

// 示例用法
function greet(greeting: string, punctuation: string) {
    console.log(`${greeting}, ${this.name}${punctuation}`);
}

const boundGreet = greet.myBind(obj, "Hey");
boundGreet("?"); // 输出 "Hey, Alice?"

深拷贝

function deepClone(obj: any, cache = new WeakMap()): any {
    if (obj === null || typeof obj !== "object") {
        return obj;
    }

    if (cache.has(obj)) {
        return cache.get(obj);
    }

    if (obj instanceof Date) {
        return new Date(obj.getTime());
    }

    if (obj instanceof Function) {
        return function(...args: any[]) {
            obj.apply(this, args)
        }
    }

    if (obj instanceof RegExp) {
        return new RegExp(obj);
    }

    if (obj instanceof Array) {
        const clonedArr: any[] = [];
        cache.set(obj, clonedArr);
        for (let i = 0; i < obj.length; ++i) {
            clonedArr[i] = deepClone(obj[i], cache);
        }
        return clonedArr;
    }

    const clonedObj: { [key: string]: any } = {};
    cache.set(obj, clonedObj);
    for (const key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            clonedObj[key] = deepClone(obj[key], cache);
        }
    }

    return clonedObj;
}

// 示例用法
const original: { [key: string]: any } = {
    name: "Alice",
    age: 30,
    dateOfBirth: new Date("1993-01-01"),
    preferences: {
        color: "blue",
        food: "pizza"
    },
    sum() {
        console.log(this.name + '-' + this.age);
    }
};
original.original = original
original.originalArr = [original, original]

const cloned = deepClone(original);
console.log(cloned); // 输出与 original 相同但不是同一个引用的对象

柯里化

function curry(fn: (...args: any[]) => any): (...args: any[]) => any {
    const arity = fn.length; // 获取原函数的参数个数

    function curried(...args: any[]): any {
        if (args.length >= arity) {
            return fn.apply(null, args);
        }

        return (...restArgs: any[]) => curried.apply(null, args.concat(restArgs));
    }

    return curried;
}

// 示例用法
function add(a: number, b: number, c: number): number {
    return a + b + c;
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 输出 6

es5和es6继承

// ES5继承(构造函数 + 原型链)
function AnimalES5(name: string) {
  this.name = name;
}

AnimalES5.prototype.sayName = function () {
  console.log("My name is " + this.name);
};

function DogES5(name: string, breed: string) {
  AnimalES5.call(this, name); // 调用父类构造函数
  this.breed = breed;
}

DogES5.prototype = Object.create(AnimalES5.prototype); // 设置原型链
DogES5.prototype.constructor = DogES5; // 修复构造函数

DogES5.prototype.sayBreed = function () {
  console.log("My breed is " + this.breed);
};

// 示例用法
const dogES5 = new DogES5("Max", "Golden Retriever");
dogES5.sayName(); // 输出 "My name is Max"
dogES5.sayBreed(); // 输出 "My breed is Golden Retriever"

// ES6继承(使用class和extends关键字)
class AnimalES6 {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayName() {
    console.log("My name is " + this.name);
  }
}

class DogES6 extends AnimalES6 {
  breed: string;
  constructor(name: string, breed: string) {
    super(name); // 调用父类构造函数
    this.breed = breed;
  }
  sayBreed() {
    console.log("My breed is " + this.breed);
  }
}
// 示例用法
const dogES6 = new DogES6("Max", "Golden Retriever");
dogES6.sayName(); // 输出 "My name is Max"
dogES6.sayBreed(); // 输出 "My breed is Golden Retriever"

instanceof

// 自定义实现 instanceof
function myInstanceOf(target: any, constructorFunc: Function): boolean {
  // 参数校验
  if (typeof target !== 'object' || target === null || typeof constructorFunc !== 'function') {
    return false;
  }

  // 获取目标对象的原型
  let targetProto = Object.getPrototypeOf(target);

  // 获取构造函数的原型
  const constructorProto = constructorFunc.prototype;

  // 遍历原型链,查找目标对象是否是构造函数的实例
  while (targetProto !== null) {
    if (targetProto === constructorProto) {
      return true;
    }
    targetProto = Object.getPrototypeOf(targetProto);
  }

  return false;
}

// 测试用例
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

const dog = new Dog();
const cat = new Cat();

console.log(myInstanceOf(dog, Dog)); // true
console.log(myInstanceOf(dog, Animal)); // true
console.log(myInstanceOf(cat, Dog)); // false
console.log(myInstanceOf(cat, Animal)); // true
console.log(myInstanceOf(123, Number)); // false

数组扁平化

// 自定义实现数组扁平化
function flattenArray(arr: any[]): any[] {
  const result: any[] = [];

  // 递归处理每个元素
  function processItem(item: any) {
    // 如果元素是数组,则递归处理
    if (Array.isArray(item)) {
      item.forEach(processItem);
    } else {
      // 如果元素不是数组,直接添加到结果数组中
      result.push(item);
    }
  }

  arr.forEach(processItem);
  return result;
}

// 测试用例
const nestedArray = [1, [2, [3, 4], 5, [6, [7, 8]]], 9, 10];

console.log(flattenArray(nestedArray)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

const nestedArray2 = [1, [2, 3], 4, [[5], 6, [7, [8, 9, [10]]]]];

console.log(flattenArray(nestedArray2)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

对象扁平化

function flattenObject(obj: { [key: string]: any }, prefix = ""): { [key: string]: any } {
    const flattened: { [key: string]: any } = {};

    for (const key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            const newKey = prefix ? `${prefix}.${key}` : key;

            if (typeof obj[key] === "object" && obj[key] !== null && !Array.isArray(obj[key])) {
                Object.assign(flattened, flattenObject(obj[key], newKey));
            } else {
                flattened[newKey] = obj[key];
            }
        }
    }

    return flattened;
}

// 示例用法
const nestedObj = {
    a: {
        b: {
            c: 1,
            d: {
                e: 2
            }
        },
        f: 3
    },
    g: {
        h: 4
    }
};

const flattenedObj = flattenObject(nestedObj);
console.log(flattenedObj);
// 输出 { 'a.b.c': 1, 'a.b.d.e': 2, 'a.f': 3, 'g.h': 4 }

JSON.parse

const myJSONParse = (target) => {
  return eval(`(${target})`);
};

// 测试用例
const jsonString = '{"name": "John", "age": 30, "city": "New York"}';
const parsedObject = myJSONParse(jsonString);
console.log(parsedObject); // { name: 'JOHN', age: 30, city: 'NEW YORK' }

EventEmitter事件触发器

class EventEmitter {
    private events: Map<string, Array<(...args: any[]) => void>>;

    constructor() {
        this.events = new Map(); // 存储事件名和对应的回调函数列表
    }

    // 添加事件监听
    on(event: string, listener: (...args: any[]) => void): void {
        if (!this.events.has(event)) {
            this.events.set(event, []);
        }
        this.events.get(event)!.push(listener);
    }

    // 移除事件监听
    off(event: string, listener: (...args: any[]) => void): void {
        const listeners = this.events.get(event);
        if (listeners) {
            const index = listeners.indexOf(listener);
            if (index !== -1) {
                listeners.splice(index, 1);
            }
        }
    }

    // 触发事件
    emit(event: string, ...args: any[]): void {
        const listeners = this.events.get(event);
        if (listeners) {
            listeners.forEach(listener => listener.apply(null, args));
        }
    }

    // 添加只执行一次的事件监听
    once(event: string, listener: (...args: any[]) => void): void {
        const wrappedListener = (...args: any[]) => {
            listener.apply(null, args);
            this.off(event, wrappedListener);
        };
        this.on(event, wrappedListener);
    }
}

// 示例用法
const eventEmitter = new EventEmitter();

function hello(name: string) {
    console.log(`Hello, ${name}!`);
}

eventEmitter.on("greet", hello);
eventEmitter.emit("greet", "Alice"); // 输出 "Hello, Alice!"

eventEmitter.off("greet", hello);
eventEmitter.emit("greet", "Bob"); // 不会输出,因为监听器已被移除

eventEmitter.once("welcome", hello);
eventEmitter.emit("welcome", "Carol"); // 输出 "Hello, Carol!"
eventEmitter.emit("welcome", "David"); // 不会输出,因为监听器只执行一次

async/await

function customAsync(generatorFn: (...args: any[]) => Generator) {
    return function (...args: any[]) {
        const generator = generatorFn.apply(null, args);

        function handle(result: IteratorResult<any>): Promise<any> {
            if (result.done) {
                return Promise.resolve(result.value);
            }

            return Promise.resolve(result.value)
                .then(value => handle(generator.next(value)))
                .catch(error => handle(generator.throw!(error)));
        }

        return handle(generator.next());
    };
}

// 示例用法
function* myGenerator() {
    const result1 = yield new Promise(resolve => setTimeout(() => resolve("First result"), 1000));
    console.log(result1);

    const result2 = yield new Promise(resolve => setTimeout(() => resolve("Second result"), 1000));
    console.log(result2);

    return "Done";
}

const myAsyncFunction = customAsync(myGenerator);
myAsyncFunction().then(result => console.log(result)); // 依次输出 "First result", "Second result", "Done"

正则获取url params

// 自定义实现获取 URL 参数
function getUrlParams(url: string): Record<string, string> {
  const params: Record<string, string> = {};
  const regex = /[?&]([^=&#]+)=([^&#]*)/g;
  let match: RegExpExecArray | null;

  // 使用正则表达式匹配 URL 参数
  while ((match = regex.exec(url)) !== null) {
    // 将匹配到的参数名称和值添加到结果对象中
    params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
  }

  return params;
}

// 测试用例
const testUrl1 = 'https://www.example.com/test?name=John&age=30&city=New%20York';
const result1 = getUrlParams(testUrl1);
console.log(result1); // { name: 'John', age: '30', city: 'New York' }

const testUrl2 = 'https://www.example.com/test?query=test&page=1&filter=active';
const result2 = getUrlParams(testUrl2);
console.log(result2); // { query: 'test', page: '1', filter: 'active' }

jsonp

function jsonp(url: string, params: { [key: string]: any }, callbackName: string): Promise<any> {
    return new Promise((resolve, reject) => {
        // 创建一个全局回调函数,用于接收请求返回的数据
        (window as any)[callbackName] = (data: any) => {
            delete (window as any)[callbackName]; // 请求完成后删除全局回调函数
            document.body.removeChild(script); // 移除script标签
            resolve(data); // 解析Promise,返回数据
        };

        // 将请求参数和回调函数名添加到URL
        const queryString = Object.entries(params)
            .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
            .join("&");
        const finalUrl = `${url}?${queryString}&callback=${callbackName}`;

        // 创建并插入一个script标签,用于发起JSONP请求
        const script = document.createElement("script");
        script.src = finalUrl;
        script.onerror = () => reject(new Error("JSONP request failed")); // 监听错误事件以处理请求失败的情况
        document.body.appendChild(script);
    });
}

// 示例用法
const url = "https://api.example.com/data";
const params = {
    userId: 123,
    accessToken: "abcdefgh"
};
const callbackName = "jsonpCallback";

jsonp(url, params, callbackName)
    .then(data => console.log(data))
    .catch(error => console.error(error));

JSON.stringify

function customJSONStringify(obj: any): string | undefined {
    const seenObjects: any[] = [];

    function stringify(value: any): string | undefined {
        if (typeof value === "number" || typeof value === "boolean" || value === null) {
            return String(value);
        }

        if (typeof value === "string") {
            return `"${value}"`;
        }

        if (typeof value === "undefined" || typeof value === "function" || value instanceof Symbol) {
            return undefined;
        }

        if (seenObjects.indexOf(value) !== -1) {
            throw new TypeError("Converting circular structure to JSON");
        }
        seenObjects.push(value);

        if (Array.isArray(value)) {
            const arr = value.map(item => stringify(item) ?? "null");
            return `[${arr.join(",")}]`;
        }

        const keys = Object.keys(value).filter(key => typeof value[key] !== "function" && typeof value[key] !== "undefined");
        const keyValuePairs = keys.map(key => `"${key}":${stringify(value[key]) ?? "null"}`);
        return `{${keyValuePairs.join(",")}}`;
    }

    return stringify(obj);
}

// 示例用法
const obj = {
    name: "Alice",
    age: 30,
    sayHello: function() {
        console.log("Hello");
    },
    preferences: {
        color: "blue",
        food: "pizza"
    }
};

console.log(customJSONStringify(obj)); // 输出 '{"name":"Alice","age":30,"preferences":{"color":"blue","food":"pizza"}}'

Promise

// 定义Promise的三种状态常量
enum PromiseStatus {
    Pending = "PENDING",
    Fulfilled = "FULFILLED",
    Rejected = "REJECTED"
}

class CustomPromise {
    status: PromiseStatus;
    value: any;
    reason: any;
    onFulfilledCallbacks: Array<(...args: any[]) => void>;
    onRejectedCallbacks: Array<(...args: any[]) => void>;

    constructor(executor: (resolve: (value?: any) => void, reject: (reason?: any) => void) => void) {
        this.status = PromiseStatus.Pending; // 初始状态为Pending
        this.value = null; // 存储成功时的值
        this.reason = null; // 存储失败时的原因
        this.onFulfilledCallbacks = []; // 存储成功时的回调函数
        this.onRejectedCallbacks = []; // 存储失败时的回调函数

        const resolve = (value?: any) => {
            if (this.status === PromiseStatus.Pending) {
                this.status = PromiseStatus.Fulfilled;
                this.value = value;
                this.onFulfilledCallbacks.forEach(callback => callback());
            }
        };

        const reject = (reason?: any) => {
            if (this.status === PromiseStatus.Pending) {
                this.status = PromiseStatus.Rejected;
                this.reason = reason;
                this.onRejectedCallbacks.forEach(callback => callback());
            }
        };

        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }

    then(onFulfilled?: (value: any) => any, onRejected?: (reason: any) => any): CustomPromise {
        onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
        onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason; };

        const promise = new CustomPromise((resolve, reject) => {
            const handleFulfilled = () => {
                try {
                    const result = onFulfilled!(this.value);
                    if (result === promise) {
                        throw new TypeError("Chaining cycle detected for promise");
                    }
                    if (result instanceof CustomPromise) {
                        result.then(resolve, reject);
                    } else {
                        resolve(result);
                    }
                } catch (error) {
                    reject(error);
                }
            };

            const handleRejected = () => {
                try {
                    const result = onRejected!(this.reason);
                    if (result === promise) {
                        throw new TypeError("Chaining cycle detected for promise");
                    }
                    if (result instanceof CustomPromise) {
                        result.then(resolve, reject);
                    } else {
                        resolve(result);
                    }
                } catch (error) {
                    reject(error);
                }
            };

            if (this.status === PromiseStatus.Fulfilled) {
                queueMicrotask(handleFulfilled);
            } else if (this.status === PromiseStatus.Rejected) {
                queueMicrotask(handleRejected);
            } else {
                this.onFulfilledCallbacks.push(() => queueMicrotask(handleFulfilled));
                this.onRejectedCallbacks.push(() => queueMicrotask(handleRejected));
            }
        });

        return promise;
    }

    catch(onRejected?: (reason: any) => any): CustomPromise {
        return this.then(undefined, onRejected);
    }
}

结尾 文章参考 github.com/qaz62482455…