得物前端面试题及参考答案(精选50道题)

发布于:2025-05-31 ⋅ 阅读:(19) ⋅ 点赞:(0)

浏览器强制缓存和协商缓存的机制及区别

浏览器缓存机制用于减少网络请求、提升页面加载性能,强制缓存和协商缓存是其中两种核心策略。

强制缓存的机制:当浏览器请求资源时,首先检查该资源在本地缓存中的有效期。有效期由响应头中的Cache-Control(优先级更高)和Expires字段控制。例如,Cache-Control: max-age=31536000表示资源在31536000秒内有效,期间浏览器直接从本地读取缓存,无需向服务器发送请求。若缓存未过期,浏览器会在请求头中携带If-None-Match(对应资源的ETag)或If-Modified-Since(对应资源的最后修改时间),但此时服务器不会实际处理请求,而是直接返回304状态码,告知浏览器使用缓存。

协商缓存的机制:当强制缓存失效(如超过max-age时间),浏览器会向服务器发送请求,验证资源是否更新。服务器通过请求头中的If-None-Match与资源当前的ETag对比,或通过If-Modified-SinceLast-Modified对比。若内容未变,返回304状态码,浏览器使用本地缓存;若内容更新,则返回200状态码及新资源内容,浏览器更新缓存。

两者的核心区别

  • 是否需要与服务器交互:强制缓存期间无需向服务器发送请求,协商缓存必须通过服务器验证。
  • 控制字段不同:强制缓存依赖Cache-ControlExpires,协商缓存依赖ETag/If-None-MatchLast-Modified/If-Modified-Since
  • 状态码差异:强制缓存生效时浏览器直接使用缓存,无状态码返回;协商缓存生效时服务器返回304状态码。
  • 适用场景:强制缓存适合不常更新的静态资源(如图片、CSS、JS),协商缓存适合更新较频繁的资源(如用户动态数据)。

实际应用中,两者常结合使用:首次请求时服务器返回强制缓存规则,过期后通过协商缓存判断是否需要更新,以平衡性能和数据时效性。

HTTP 和 HTTPS 的区别与优缺点

HTTP(超文本传输协议)和HTTPS(安全超文本传输协议)是Web通信的核心协议,主要区别体现在安全性、传输机制和应用场景上。

核心区别

  1. 安全性

    • HTTP传输明文数据,未加密,易被中间人攻击、窃取或篡改。
    • HTTPS在HTTP基础上叠加SSL/TLS加密层,对数据进行加密传输,确保数据完整性和机密性。通信过程通过数字证书(CA认证)验证服务器身份,防止伪造。
  2. 端口与默认地址

    • HTTP默认使用80端口,URL以http://开头。
    • HTTPS默认使用443端口,URL以https://开头。
  3. 握手过程

    • HTTP直接建立TCP连接后传输数据。
    • HTTPS在TCP连接基础上增加TLS握手阶段:客户端与服务器协商加密算法、交换公钥,通过私钥验证身份,最终建立安全连接。

优缺点对比

维度 HTTP HTTPS
优点 协议简单,传输效率高,服务器资源消耗少。 数据加密传输,防篡改、防窃取,身份验证可靠,受浏览器信任(Chrome标记HTTP为“不安全”)。
缺点 安全性差,不适合传输敏感数据(如密码、支付信息)。 握手过程增加延迟(约300-500ms),服务器需消耗额外资源处理加密,证书需要付费购买和维护。

应用场景

  • HTTP适用于对安全性要求低的场景(如公开新闻网站、静态资源加载),或需要极致性能的内部系统。
  • HTTPS是互联网主流选择,尤其适用于电商平台、金融服务、用户登录等涉及敏感数据的场景。此外,现代浏览器对HTTPS支持更优(如HTTP/2仅在HTTPS上部署),且搜索引擎(如Google)会优先索引HTTPS页面,提升SEO排名。

发展趋势:随着“HTTPS Everywhere”倡议的推进,越来越多的网站迁移至HTTPS,HTTP仅在特定 legacy 系统中保留。开发者需注意,HTTPS的性能问题可通过优化TLS版本(如使用TLS 1.3减少握手延迟)、启用HTTP/2多路复用等方式缓解。

了解 HTTP/2.0 和 HTTP/3.0 吗?各自的特点是什么?

HTTP/2.0 和 HTTP/3.0 是HTTP协议的两代重要升级,旨在解决性能瓶颈,适应现代Web应用需求。

HTTP/2.0 的特点

  1. 二进制分帧(Binary Framing)
    将HTTP消息分解为二进制帧(如数据帧、头部帧),取代HTTP/1.x的文本格式,解析更高效,避免歧义。

  2. 多路复用(Multiplexing)
    多个请求和响应可在同一个TCP连接中并行传输,解决HTTP/1.x的“队头阻塞”问题(单个请求阻塞会影响其他请求)。例如,浏览器只需建立一个TCP连接即可加载页面所有资源(HTML、CSS、JS、图片等),减少连接开销。

  3. 头部压缩(HPACK)
    对重复的请求头(如User-AgentCookie)进行压缩,通过索引表存储常用字段,减少传输数据量。据测算,头部压缩可使数据量减少50%-90%。

  4. 服务器推送(Server Push)
    服务器可主动向客户端推送关联资源(如HTML引用的CSS文件),无需客户端额外请求,提前加载资源,提升首屏渲染速度。

  5. 流量控制
    允许客户端控制接收数据的速率,避免因接收缓冲区满导致的数据丢失,增强传输稳定性。

HTTP/3.0 的特点
HTTP/3.0 基于QUIC协议(Quick UDP Internet Connections),旨在解决HTTP/2.0依赖TCP带来的局限性:

  1. 基于UDP协议
    摒弃TCP,改用UDP传输数据,避免TCP协议栈的复杂机制(如慢启动、拥塞控制),降低延迟。QUIC在UDP之上实现了可靠传输、流量控制、加密等功能,兼具UDP的速度和TCP的可靠性。

  2. 减少握手延迟

    • 首次连接时,QUIC通过1-RTT(Round-Trip Time)完成TLS加密握手和连接建立(HTTP/2.0需2-RTT)。
    • 后续连接可通过“连接迁移”机制,利用本地缓存的会话密钥快速恢复连接,无需重新握手。
  3. 队头阻塞优化
    TCP中单个数据包丢失会导致整个连接阻塞,而QUIC将数据划分为独立的“流”(Stream),单个流的阻塞不影响其他流,彻底解决队头阻塞问题。

  4. 集成加密与多路复用
    QUIC默认启用TLS 1.3加密,所有传输数据均加密处理,同时天然支持多路复用,避免HTTP/2.0在TCP层的潜在阻塞问题。

  5. 连接迁移(Connection Migration)
    当客户端网络环境变化(如从Wi-Fi切换到移动网络),只需更新IP地址,无需重新建立连接,保持业务连续性。

TCP 和 UDP 的区别

TCP(传输控制协议)和UDP(用户数据报协议)是TCP/IP协议栈中两种核心传输层协议,设计目标和应用场景差异显著。

1. 连接性与可靠性

  • TCP:面向连接的协议,通信前需通过“三次握手”建立连接(客户端发送SYN→服务器返回SYN+ACK→客户端发送ACK),确保双方就绪。传输过程中通过序列号、确认应答(ACK)、超时重传、流量控制(滑动窗口)和拥塞控制(慢启动、拥塞避免)机制,保证数据无丢失、无重复、按序到达,可靠性极高。
  • UDP:无连接协议,发送数据前无需建立连接,直接将数据包(称为Datagram)发送至目标地址。不保证数据到达、不处理重复或乱序,可靠性由应用层负责,协议本身仅提供“尽力而为”的传输。

2. 传输效率与延迟

  • TCP:因需要维护连接状态和复杂的可靠性机制,协议头部开销较大(20字节固定头部),传输延迟较高,尤其在网络拥塞时会触发退避机制,进一步降低速度。
  • UDP:头部简单(8字节固定头部),无连接建立和维护开销,传输速度快、延迟低,适合对实时性要求高的场景。

3. 数据传输方式

  • TCP:流式传输,数据无边界,发送方连续写入的字节流会被分割为多个数据包,接收方需根据序列号重组为连续的数据流,适用于大量数据的可靠传输。
  • UDP:数据报传输,每个Datagram是独立的,包含完整的源地址和目标地址,接收方读取时需按包接收,存在丢包可能,适合小数据量、非连续的传输。

4. 应用场景

协议 典型应用 原因
TCP 网页浏览(HTTP/HTTPS)、文件传输(FTP)、电子邮件(SMTP/POP3)、远程登录(SSH) 需要确保数据完整到达,允许一定延迟但不可接受丢失。
UDP 实时通信(视频会议、直播、在线游戏)、DNS查询、SNMP监控 优先保证低延迟和实时性,可容忍少量丢包(如视频画面短暂卡顿可通过帧率调整弥补)。

5. 资源占用与复杂性

  • TCP:服务器需为每个连接维护状态(如连接队列、滑动窗口),占用较多内存和CPU资源,实现复杂度高。
  • UDP:无连接状态维护,服务器可同时处理大量客户端请求,资源占用少,实现简单。

队头阻塞(Head-of-Line Blocking)的定义及在 HTTP 中的表现

队头阻塞的定义
在计算机网络中,队头阻塞指的是在队列传输的场景下,排在队列头部的请求或数据单元阻塞,导致后续请求或数据单元无法被处理或传输的现象。即使后续请求与头部请求无关,也会因队列的顺序性被迫等待,从而降低整体传输效率。

在HTTP中的表现
HTTP协议的队头阻塞问题随版本演进呈现不同特点:

1. HTTP/1.x 中的显著阻塞
HTTP/1.x(包括1.0和1.1)基于TCP连接传输数据,且默认行为会导致双重队头阻塞:

  • TCP层阻塞:TCP是面向连接的流式协议,数据包按顺序传输。若某个数据包在传输中丢失或延迟,接收方需等待该包重传完成后才能处理后续数据包,即使后续数据包已正确到达。例如,浏览器请求一个HTML页面及其引用的CSS、JS文件时,若HTML的某个数据包丢失,整个TCP连接会被阻塞,导致CSS、JS文件无法提前加载。
  • HTTP请求队列阻塞:在HTTP/1.x中,浏览器为减少TCP连接开销,通常开启“长连接”(Connection: keep-alive),但同一连接上的多个请求需按顺序发送。若第一个请求(如获取HTML)因服务器处理缓慢而阻塞,后续请求(如获取图片)会被阻塞在队列中,无法并行处理。例如,一个页面依赖多个JavaScript文件,若第一个JS文件加载缓慢,后续JS文件的请求会被阻塞,导致页面渲染延迟。

2. HTTP/2.0 的部分优化
HTTP/2.0通过多路复用机制缓解了HTTP请求队列的阻塞:多个请求和响应被分解为二进制帧,在同一个TCP连接中交错传输,服务器可并行处理不同请求的帧,客户端按帧重组数据。例如,HTML、CSS、JS的请求帧可混合传输,服务器无需等待前一个请求处理完成即可返回后续请求的响应。
TCP层的队头阻塞依然存在:若某个帧对应的数据包丢失,整个TCP连接会暂停,等待重传,导致所有依赖该连接的HTTP请求被阻塞。例如,视频流和文本数据共享一个TCP连接时,视频数据包的丢失会阻塞文本数据的传输。

3. HTTP/3.0 的彻底解决
HTTP/3.0基于QUIC协议(基于UDP),通过以下方式解决队头阻塞:

  • 流级并行:QUIC将数据划分为多个独立的“流”(Stream),每个流有独立的序列号,单个流的数据包丢失不会影响其他流的传输。例如,网页的HTML和图片可在不同流中传输,HTML流的阻塞不影响图片流的加载。
  • 无TCP连接阻塞:UDP本身无连接概念,QUIC通过加密和可靠传输机制实现应用层的“连接”,避免TCP协议栈的阻塞问题。

影响与应对策略
队头阻塞会显著降低页面加载速度,尤其在高延迟或不稳定网络环境中。在HTTP/1.x时代,常用解决方案包括:

  • 域名分片:将资源分散到多个域名下(如img1.example.comimg2.example.com),利用浏览器对不同域名的TCP连接并发限制(通常每个域名允许6-8个连接),实现并行加载。
  • 内联资源:将小文件(如CSS、JS)直接嵌入HTML中,减少HTTP请求数量。
  • 压缩资源:降低单个数据包的大小,减少TCP层阻塞概率。

随着HTTP/2.0和HTTP/3.0的普及,队头阻塞问题逐步得到缓解,但开发者仍需根据业务场景选择合适的协议版本,并结合缓存、CDN等技术进一步优化性能。

ETag 和 Last-Modified 的优缺点及使用场景

ETag 是服务器生成的资源唯一标识,通常基于文件内容哈希、修改时间等生成,用于精准判断资源是否变更;Last-Modified 是资源最后一次修改的时间戳,通过时间差异判断是否需要更新。

ETag 的优点 包括准确性高,能感知文件内容的细微变化(如字节级修改),避免因时间戳精度不足(如秒级)导致的误判;支持断点续传场景,可通过 If-Range 头结合 ETag 实现分片下载的续传。缺点 是生成需要消耗服务器资源(计算哈希值),且分布式系统中多节点生成规则需统一,否则可能导致缓存验证失效。其 使用场景 包括对资源变更敏感的场景(如版本管理严格的静态资源)、文件内容频繁小幅修改的场景(如图像压缩后的再次保存),以及需要支持断点续传的大文件下载场景。

Last-Modified 的优点 是实现简单,服务器只需记录文件修改时间,无需额外计算;浏览器兼容广泛,几乎所有客户端都支持。缺点 是精度有限(多数系统仅支持秒级),若文件在同一秒内多次修改则无法识别;此外,文件内容未变但修改时间变化(如编辑后撤销)会导致缓存失效,产生不必要的请求。其 使用场景 包括静态资源更新频率较低的场景(如网站图标、公共样式表)、对性能敏感但变更检测精度要求不高的场景,以及不支持 ETag 的老旧客户端环境。

实际应用中,两者常结合使用:服务器优先返回 ETag,客户端同时发送 If-None-Match(ETag)和 If-Modified-Since(Last-Modified),服务器先验证 ETag,若匹配再验证时间戳,以提升缓存验证的可靠性。

CDN 回源的概念及原理

CDN 回源 指当 CDN 节点缓存中不存在用户请求的资源,或资源已过期、需要更新时,节点向源服务器(如网站的原始服务器)发起请求获取资源的过程。这一机制是 CDN 实现内容分发和加速的重要补充,确保用户始终能获取到最新或未被缓存的内容。

其 原理 可分为以下环节:

  1. 用户请求发起:用户通过域名访问资源,DNS 解析将请求导向离用户最近的 CDN 节点。
  2. 节点缓存检查:CDN 节点先检查本地是否有该资源的缓存。若存在且未过期,直接返回给用户;若不存在或已过期,则触发回源。
  3. 回源请求发送:节点向源服务器发送请求,请求头中可能携带缓存策略相关字段(如 If-Modified-SinceIf-None-Match),以验证资源是否需要更新。
  4. 源服务器响应:源服务器收到请求后,根据请求头判断是否返回最新资源。若资源未变更,返回 304 Not Modified;若需更新,返回完整资源内容及新的缓存控制头(如 Cache-ControlExpires)。
  5. 节点缓存与响应用户:CDN 节点获取资源后,按配置规则缓存到本地,并将资源返回给用户。后续相同请求可直接从节点缓存响应,减少对源服务器的压力。

回源的触发场景 包括:首次请求某资源(节点无缓存)、缓存过期(根据 Cache-Control 的 max-age 或 Expires 时间判断)、强制刷新请求(用户按 Ctrl+F5,发送 Cache-Control: no-cache 头)、节点缓存被主动清理(如管理员手动刷新 CDN 缓存)。

影响回源的因素 包括缓存策略配置(如源站设置的 Cache-Control 时长过短会导致频繁回源)、资源更新频率(动态内容易触发回源)、CDN 节点数量与分布(节点少或分布不均可能导致部分节点缓存命中率低)。优化回源可通过合理设置缓存时间、启用资源压缩(减少回源传输数据量)、使用边缘计算(在节点直接处理部分请求,避免回源)等方式实现,以降低源服务器负载并提升用户访问速度。

同源策略是什么?如何解决跨域问题?

同源策略 是浏览器的安全机制,用于限制不同源的文档或脚本之间的交互,防止恶意网站窃取用户数据。“同源”指 协议(protocol)、域名(domain)、端口(port) 三者完全相同,例如 http://example.com:80 与 https://example.com:8080 因协议或端口不同而不同源。

该策略限制的行为包括:

  • DOM 访问限制:不同源的页面无法直接操作对方的 DOM 元素。
  • 数据交互限制:XMLHttpRequest、Fetch 等接口无法直接请求不同源的资源(会触发跨域请求阻止)。
  • Cookie 隔离:不同源的页面无法读取对方的 Cookie、LocalStorage 等数据。

跨域问题的解决方案 需结合具体场景选择,常见方法如下:

1. CORS(跨域资源共享)

原理:通过服务器返回的响应头告知浏览器允许哪些源、方法、字段参与跨域请求。
关键响应头

  • Access-Control-Allow-Origin:指定允许的源(如 * 表示所有源,或具体域名)。
  • Access-Control-Allow-Methods:允许的 HTTP 方法(如 GET, POST)。
  • Access-Control-Allow-Headers:允许的请求头字段(如自定义头 X-Custom-Header)。
    适用场景:前后端分离项目,需服务器端配合配置,支持复杂请求(如带自定义头的 POST 请求)。
2. JSONP(JSON with Padding)

原理:利用 <script> 标签不受同源策略限制的特性,动态插入脚本标签请求跨域数据,服务器返回包裹在回调函数中的 JSON 数据。
示例

<script>  
  function handleData(data) {  
    console.log(data);  
  }  
</script>  
<script src="http://api.example.com/data?callback=handleData"></script>  

限制:仅支持 GET 方法,存在 XSS 风险(需确保回调函数名不被篡改),现代项目中逐渐被 CORS 替代。

3. 代理服务器(反向代理)

原理:在前端项目中配置一个与前端同源的代理服务器,将跨域请求转发到目标服务器,规避浏览器的同源限制。
实现(以 Webpack 为例):

// webpack.config.js  
devServer: {  
  proxy: 'http://api.example.com' // 将 `/api` 路径请求转发到目标服务器  
}  

适用场景:开发环境或需统一处理跨域的场景,需前端和服务器端配合,但无需修改浏览器行为。

4. PostMessage(跨文档消息传递)

原理:通过 window.postMessage() 方法在不同源的窗口间安全地传递消息,接收方通过 message 事件监听并验证来源。
示例

// 源 A 窗口发送消息  
otherWindow.postMessage('Hello from source A', 'http://target.example.com');  

// 源 B 窗口接收消息  
window.addEventListener('message', (event) => {  
  if (event.origin === 'http://source.example.com') {  
    console.log(event.data);  
  }  
});  

适用场景:窗口间通信(如主窗口与 iframe),需双方页面主动配合。

5. WebSocket

原理:WebSocket 协议通过 ws:// 或 wss:// 建立长连接,其跨域限制由浏览器单独处理,默认允许跨域通信,但需服务器支持。
关键:建立连接时需服务器返回 101 Switching Protocols 响应,后续通信不受同源策略限制。

6. Nginx 反向代理

原理:在服务器端配置 Nginx,将不同源的请求转发到同一域名下,实现路径重写。例如将 http://example.com/api 转发到 http://api.target.com,使前端请求与自身同源。

server {  
  listen 80;  
  server_name example.com;  
  location /api {  
    proxy_pass http://api.target.com;  
    proxy_set_header Host $host;  
  }  
}  

选择策略:优先使用 CORS(兼容性好、支持全方法),开发环境可用代理服务器简化配置,跨窗口通信选 PostMessage, legacy 项目可保留 JSONP。需注意,所有跨域方案均需服务器端配合,前端不可单独解决跨域限制。

前端如何实现懒加载?有哪些常见方式?

前端懒加载 指延迟加载非关键资源或组件,待需要时再加载,以减少初始加载时间、提升页面性能。常见实现方式按场景可分为 图片懒加载路由懒加载组件懒加载 和 数据懒加载,原理和实现各有差异。

一、图片懒加载

核心原理:将图片的 src 属性替换为占位符(如 data-src),待图片进入视口可见区域时,再将占位符内容赋值给 src,触发真实加载。
实现方式

  1. Intersection Observer API(推荐)
    利用浏览器原生 API 监听元素是否进入视口,兼容性较好(IE 不支持,需 polyfill)。

    预览

    <img src="placeholder.jpg" data-src="real-image.jpg" class="lazy">  
    
     
    const observer = new IntersectionObserver((entries) => {  
      entries.forEach(entry => {  
        if (entry.isIntersecting) {  
          const img = entry.target;  
          img.src = img.dataset.src;  
          observer.unobserve(img); // 加载后停止观察  
        }  
      });  
    });  
    document.querySelectorAll('.lazy').forEach(img => observer.observe(img));  
    
  2. 滚动事件监听(传统方式)
    通过 scroll 事件计算元素与视口的位置关系,性能较低(频繁触发回调),需配合 debounce 优化。
    function checkLazyImages() {  
      const lazyImages = document.querySelectorAll('img[loading="lazy"], .lazy');  
      lazyImages.forEach(img => {  
        if (isInViewport(img)) {  
          img.src = img.dataset.src;  
          img.classList.remove('lazy');  
        }  
      });  
    }  
    function isInViewport(el) {  
      const rect = el.getBoundingClientRect();  
      return rect.top <= window.innerHeight && rect.bottom >= 0;  
    }  
    window.addEventListener('scroll', checkLazyImages);  
    
  3. 原生 loading="lazy" 属性
    浏览器自带属性(Chrome 77+ 支持),简单易用,但兼容性有限:

    预览

    <img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy">  
    
二、路由懒加载(单页应用,如 Vue/React)

原理:将路由对应的组件分割成独立的代码块,初始加载时不请求,仅在路由切换时加载对应组件。
实现方式

  • Vue 中使用 import()(ES6 动态导入):
    const Home = () => import('./views/Home.vue');  
    const About = () => import('./views/About.vue');  
    const routes = [  
      { path: '/', component: Home },  
      { path: '/about', component: About }  
    ];  
    
  • React 中使用 React.lazy + Suspense
    const Home = React.lazy(() => import('./views/Home'));  
    const About = React.lazy(() => import('./views/About'));  
    function App() {  
      return (  
        <Router>  
          <Suspense fallback={<Spinner />}>  
            <Switch>  
              <Route path="/" component={Home} />  
              <Route path="/about" component={About} />  
            </Switch>  
          </Suspense>  
        </Router>  
      );  
    }  
    
三、组件懒加载(非路由组件)

场景:页面中某些不常用组件(如弹窗、图表)延迟加载,减少首屏 JS 体积。
实现方式

  • 动态导入 + 状态控制(以 Vue 为例):
    <template>  
      <button @click="showComponent = true">加载组件</button>  
      <div v-if="showComponent">  
        <LazyComponent />  
      </div>  
    </template>  
    <script>  
    export default {  
      data() { return { showComponent: false }; },  
      components: {  
        LazyComponent: () => import('./LazyComponent.vue')  
      }  
    };  
    </script>  
    
  • React 中使用 React.lazy + 条件渲染
    const LazyComponent = React.lazy(() => import('./LazyComponent'));  
    function App() {  
      const [isLoaded, setIsLoaded] = useState(false);  
      return (  
        <button onClick={() => setIsLoaded(true)}>加载组件</button>  
        {isLoaded && (  
          <Suspense fallback={<p>Loading...</p>}>  
            <LazyComponent />  
          </Suspense>  
        )}  
      );  
    }  
    
四、数据懒加载(分页加载、无限滚动)

原理:初始加载时仅获取当前页数据,用户滚动或点击时再加载后续数据,减少初始请求量。
实现方式

  • 无限滚动(结合 Intersection Observer)
    const observer = new IntersectionObserver(([entry]) => {  
      if (entry.isIntersecting && !isLoading) {  
        loadMoreData(); // 发送请求获取下一页数据  
      }  
    });  
    observer.observe(document.querySelector('.load-more-trigger'));  
    
  • 分页按钮点击加载
    let page = 1;  
    function loadPage() {  
      fetch(`/api/data?page=${page}`)  
        .then(res => res.json())  
        .then(data => appendToDOM(data));  
      page++;  
    }  
    
五、其他懒加载场景
  • 视频懒加载:类似图片懒加载,使用 video 标签的 poster 属性作为占位图,进入视口后再加载视频源。
  • JS/CSS 资源懒加载:通过 <script defer><link rel="preload"> 等标签控制加载时机,或动态插入 <script> 标签延迟执行非关键脚本。

优化建议

  • 优先使用浏览器原生能力(如 loading="lazy"、Intersection Observer),减少自定义逻辑。
  • 对懒加载元素设置合理的占位符(如低分辨率模糊图),提升用户体验。
  • 避免过度使用懒加载,关键资源(如首屏内容)仍需直接加载,确保核心体验流畅。

如何让多个 div 元素排成一排?尽可能列举多种方法

让多个 div 元素水平排列是前端布局的基础需求,可通过不同 CSS 特性实现,适用场景包括导航栏、卡片列表、弹性布局等。以下是常见方法及详细实现:

一、浮动布局(float

原理:通过 float: left 或 float: right 使元素脱离文档流,相邻浮动元素会沿水平方向排列。
注意点:需处理父元素高度塌陷问题(可通过 overflow: hidden 或 clearfix 解决)。
示例

<div class="container">  
  <div class="box">1</div>  
  <div class="box">2</div>  
  <div class="box">3</div>  
</div>  

.box {  
  float: left;  
  width: 100px;  
  height: 100px;  
  margin-right: 10px;  
  background: #f0f0f0;  
}  
.container {  
  overflow: hidden; /* 清除浮动 */  
}  

适用场景:兼容旧版浏览器(如 IE6+),但需手动处理布局依赖和回流问题。

二、内联块元素(display: inline-block

原理:将元素转为内联块级元素,使其像文字一样水平排列,保留块级元素的宽高特性。
注意点:元素间会产生默认间距(由 HTML 代码中的换行或空格导致),可通过设置父元素 font-size: 0 消除,子元素再恢复字体大小。
示例

.container {  
  font-size: 0; /* 消除子元素间距 */  
}  
.box {  
  display: inline-block;  
  width: 100px;  
  height: 100px;  
  margin-right: 10px;  
  background: #f0f0f0;  
  font-size: 16px; /* 恢复子元素字体大小 */  
  vertical-align: top; /* 解决基线对齐导致的底部空隙 */  
}  

适用场景:需兼容不支持 Flexbox 的浏览器,或元素高度不一致时需控制垂直对齐。

三、Flex 弹性布局(display: flex

原理:通过父元素设置 display: flex,子元素自动成为弹性项,沿主轴(默认水平方向)排列。
核心属性

  • flex-direction: row(默认水平排列)
  • justify-content:控制子元素水平对齐(如 space-between 两端对齐)
  • align-items:控制子元素垂直对齐(如 center 垂直居中)
    示例

.container {  
  display: flex;  
  gap: 10px; /* 替代 margin,自动处理间距 */  
}  
.box {  
  width: 100px;  
  height: 100px;  
  background: #f0f0f0;  
}  

优势:轻松实现等高布局、自动换行(flex-wrap: wrap)、灵活的对齐方式,是现代布局的首选方案。
兼容性:需注意 IE11 对 gap 属性支持有限,可通过 margin 替代或添加前缀。

四、Grid 网格布局(display: grid

原理:将父元素视为网格容器,子元素作为网格项,通过 grid-template-columns 指定列布局,实现水平排列。
示例

.container {  
  display: grid;  
  grid-template-columns: repeat(3, 100px); /* 3列,每列100px */  
  column-gap: 10px; /* 列间距 */  
}  
.box {  
  height: 100px;  
  background: #f0f0f0;  
}  

扩展场景:若列数不固定,可使用 grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)) 实现响应式布局。
适用场景:二维布局(如多行多列卡片),或需精确控制行列对齐的场景。

五、绝对定位(position: absolute

原理:将父元素设置为 position: relative,子元素通过 left/right 和 margin 定位到水平方向。
示例

.container {  
  position: relative;  
  width: 350px; /* 固定父容器宽度 */  
}  
.box {  
  position: absolute;  
  width: 100px;  
  height: 100px;  
  background: #f0f0f0;  
}  
.box:nth-child(1) { left: 0; }  
.box:nth-child(2) { left: 110px; } /* 间距10px */  
.box:nth-child(3) { left: 220px; }  

局限性:需手动计算每个子元素的位置,不适合动态数量的元素,维护成本高,仅适用于固定布局场景。

六、表格布局(display: table-cell

原理:将父元素设为 display: table,子元素设为 display: table-cell,模拟表格单元格的水平排列特性。
示例

.container {  
  display: table;  
  width: 100%;  
}  
.box {  
  display: table-cell;  
  width: 100px;  
  height: 100px;  
  margin-right: 10px;  
  background: #f0f0f0;  
  /* 消除单元格间距 */  
  border-spacing: 10px;  
}  

注意点margin 对 table-cell 元素垂直方向无效,水平间距需通过 border-spacing 或 padding 实现,兼容性一般,现代项目中较少使用。

七、CSS 框架(如 Bootstrap)

原理:基于 Flex 布局封装的栅格系统,通过类名快速实现水平排列。
示例(Bootstrap 5):

<div class="container">  
  <div class="row g-3">  
    <div class="col-4">1</div>  
    <div class="col-4">2</div>  
    <div class="col-4">3</div>  
  </div>  
</div>  

row 类启用 Flex 布局,g-3 设置间距,col-4 定义每列占 4 个栅格单位(12 列栅格系统)。

方法对比与选择建议
方法 兼容性 动态适应性 布局控制复杂度 推荐场景
Flex 布局 现代浏览器佳 高(自动换行) 低(属性直观) 主流项目、响应式布局
Grid 布局 现代浏览器佳 高(二维控制) 中(需理解网格) 复杂网格、多行列布局
浮动布局 全浏览器兼容 低(需手动清除) 高(依赖清除浮动) 旧项目兼容、简单水平排列
内联块元素 较广兼容 中(需处理间距) 中(消除间距) 文字与块混合布局

优先选择:Flex 布局(语法简洁、功能全面)或 Grid 布局(二维场景)。若需兼容 IE9 以下,可使用浮动或内联块元素,并配合工具库(如 Autoprefixer)处理前缀。动态增减元素时,Flex 的 flex-grow 和 flex-shrink 属性可自动调整空间分配,提升布局灵活性。

如何让大小不一的 div 实现水平居中对齐?使用inline-block需要注意什么?

让大小不一的 div 实现水平居中对齐,可通过不同 CSS 特性结合容器属性来实现。** Flex 布局是常用方案,给容器设置 display: flex; justify-content: center;,子元素无需设置宽度即可在容器内水平居中,且能适应不同尺寸,尤其适合动态内容场景。 Grid 布局**同样高效,容器设为 display: grid; justify-content: center;,子元素会在网格中水平居中,适合二维布局需求。

若使用  inline-block 模式,需先给子元素设置 display: inline-block;,再给容器设置 text-align: center;,利用文本居中特性实现块级元素对齐。但此方法存在诸多注意事项:

  1. 空白间隙问题:HTML 代码中标签间的空格、换行符会被解析为空白间隙,导致子元素间出现额外间距。可通过移除 HTML 标签间的空格、给容器设置 font-size: 0; 再给子元素恢复字体大小(如 font-size: 16px;)来解决。
  2. 垂直对齐基准inline-block 元素默认以基线(baseline)对齐,可能导致大小不一的元素垂直方向不对齐。可通过设置 vertical-align: middle; 调整对齐基准,或给子元素设置固定高度避免基线差异。
  3. 宽度计算影响:子元素的宽度会包含内边距(padding)和边框(border),若需精确控制布局,需配合 box-sizing: border-box; 确保宽度计算符合预期。
  4. 兼容性限制:旧版 IE 浏览器对 inline-block 的支持存在差异,需添加 *display: inline; *zoom: 1; 等 hack 代码兼容 IE6/7。

实际应用中,应优先选择 Flex 或 Grid 布局,因其更简洁且无需处理额外细节,而 inline-block 可作为兼容性 fallback 方案使用。

实现一个正方形,距离浏览器左右边距各 50px(用 CSS 实现)

实现固定边距的正方形,可结合 CSS 盒模型与定位属性,根据场景选择不同方法:

方法一:利用 padding 保持宽高比
.square {  
  position: relative;  
  margin: 0 50px; /* 左右边距 50px */  
  width: calc(100% - 100px); /* 容器宽度自适应,减去左右边距总和 */  
  padding-top: 100%; /* 通过 padding-top 实现宽高相等,形成正方形 */  
  background: #f00; /* 背景色便于观察 */  
}  

原理:利用 padding-top 的百分比值相对于容器宽度计算的特性,当 width 与 padding-top 百分比相同时,容器会呈现正方形。margin 直接控制左右边距,适合需要自适应宽度的场景。

方法二:固定宽高 + 定位
.square {  
  position: absolute;  
  top: 0; /* 可选,根据垂直布局需求调整 */  
  left: 50px;  
  right: 50px;  
  width: calc(100% - 100px);  
  height: calc(100% - 100px); /* 若需占满视口高度,可结合 height: 100vh */  
  background: #f00;  
}  

原理:通过绝对定位(position: absolute)将元素脱离文档流,利用 left 和 right 同时设置边距,width 计算为 100% - 左右边距总和,配合相同的 height 形成正方形。适用于固定定位或全屏布局场景。

方法三:CSS Grid + minmax
.container {  
  display: grid;  
  grid-template-columns: minmax(50px, 1fr) minmax(auto, 200px) minmax(50px, 1fr); /* 左右边距 + 正方形主体 */  
}  
.square {  
  grid-column: 2; /* 占据中间列 */  
  width: 200px;  
  height: 200px; /* 固定宽高 */  
  background: #f00;  
}  

原理:利用 Grid 布局的列轨道(grid-template-columns)定义左右边距列(minmax(50px, 1fr))和正方形列(minmax(auto, 200px)),通过 grid-column 指定正方形位置。适合复杂布局中嵌入固定边距元素。

关键点说明
  • 边距计算:左右边距各 50px,总边距为 100px,需从元素宽度中扣除(如 width: calc(100% - 100px))。
  • 宽高一致性:无论使用 padding 还是固定 width/height,确保两者数值相同或比例为 1:1。
  • 定位与布局模式:根据需求选择文档流内布局(如 Flex、Grid)或脱离文档流(如绝对定位),避免布局冲突。

实际场景中,若需正方形随视口缩放,优先使用方法一;若需固定尺寸且位置固定,可采用方法二或三。

如何实现上下固定、中间自适应撑满的三栏布局?列举多种方法

上下固定、中间自适应的三栏布局是前端常见需求,可通过不同 CSS 技术实现,核心在于固定上下栏高度,中间栏自动填充剩余空间。以下是几种主流方案:

方法一:Flex 布局(推荐)
.container {  
  display: flex;  
  flex-direction: column;  
  min-height: 100vh; /* 占满视口高度 */  
}  
.header, .footer {  
  height: 60px; /* 固定上下栏高度 */  
  background: #f0f0f0;  
}  
.content {  
  flex: 1; /* 中间栏自动填充剩余空间 */  
  overflow: auto; /* 内容超出时显示滚动条 */  
  background: #fff;  
}  

原理:容器设为 Flex 垂直布局(flex-direction: column),中间栏通过 flex: 1 占据所有可用空间,上下栏高度固定。此方法代码简洁,兼容性良好(IE11+),支持子元素垂直居中、响应式等特性。

方法二:Grid 布局
.container {  
  display: grid;  
  grid-template-rows: 60px 1fr 60px; /* 定义上中下三行,中间为自适应行 */  
  min-height: 100vh;  
}  
.header, .footer {  
  background: #f0f0f0;  
}  
.content {  
  background: #fff;  
  overflow: auto;  
}  

原理:利用 Grid 布局的行轨道(grid-template-rows)直接指定上下栏高度(60px)和中间栏自适应(1fr),无需额外属性即可实现填充。适合二维布局场景,语法直观,但 IE 不支持 Grid。

方法三:绝对定位 + 负边距
.container {  
  position: relative;  
  min-height: 100vh;  
}  
.header, .footer {  
  position: absolute;  
  left: 0;  
  right: 0;  
  height: 60px;  
  background: #f0f0f0;  
}  
.header {  
  top: 0;  
}  
.footer {  
  bottom: 0;  
}  
.content {  
  margin: 60px 0 calc(60px + env(safe-area-inset-bottom)); /* 预留上下栏空间,适配移动端安全区域 */  
  background: #fff;  
  overflow: auto;  
}  

原理:上下栏使用绝对定位(position: absolute)固定在视口顶部和底部,中间栏通过 margin 预留上下栏高度,确保内容不被遮挡。需注意处理移动端安全区域(如 env(safe-area-inset-bottom)),避免底部内容被遮挡。

方法四:传统盒模型 + 浮动
.header, .footer {  
  height: 60px;  
  background: #f0f0f0;  
}  
.content {  
  padding: 60px 0; /* 上下内边距预留空间 */  
  min-height: calc(100vh - 120px); /* 计算剩余高度 */  
  background: #fff;  
  overflow: auto;  
}  

原理:通过给中间栏设置 padding 预留上下栏空间,结合 min-height 计算视口剩余高度(100vh - 上下栏高度总和),确保中间内容撑满容器。此方法兼容性好,但需手动计算高度,不够灵活。

方法五:Sticky Footer 模式(适用于内容不足时底部栏固定)
html {  
  height: 100%;  
}  
body {  
  display: flex;  
  flex-direction: column;  
  min-height: 100%;  
  margin: 0;  
}  
.header {  
  height: 60px;  
  background: #f0f0f0;  
}  
.content {  
  flex: 1;  
  background: #fff;  
  overflow: auto;  
}  
.footer {  
  height: 60px;  
  background: #f0f0f0;  
}  

原理:利用 flex: 1 让中间内容填充剩余空间,当内容不足时,底部栏仍固定在视口底部;内容超过时,中间栏自动滚动。此方法是 Flex 布局的变种,适合通用型三栏布局。

方案对比
方法 兼容性 代码复杂度 响应式支持 适用场景
Flex 布局 IE11+、现代浏览器 通用场景,推荐使用
Grid 布局 现代浏览器 复杂二维布局
绝对定位 IE6+ 一般 固定定位需求
传统盒模型 全兼容 一般 兼容性优先场景

选择建议:优先使用 Flex 或 Grid 布局,代码简洁且功能强大;若需兼容旧版浏览器,可采用绝对定位或传统盒模型方案。注意处理中间栏的 overflow 属性,避免内容溢出导致布局错乱。

CSS 如何设置字符间距?

CSS 中设置字符间距主要通过 letter-spacing 属性,该属性用于控制文本中字符之间的水平间距,可接受正、负数值或百分比,数值越大间距越宽,负值则会让字符重叠。其语法规则为:

selector {  
  letter-spacing: normal | <length>; /* normal 为默认值,等效于 0 */  
}  
具体用法与场景
  1. 固定数值设置

    • 使用像素(px)、em、rem 等单位精确控制间距。例如:
      .title {  
        letter-spacing: 2px; /* 字符间距增加 2px */  
      }  
      .compact {  
        letter-spacing: -0.5px; /* 字符间距缩小 0.5px,适用于紧凑型排版 */  
      }  
      
    • 场景:标题文字为增强视觉效果扩大间距,或小字体为提升可读性缩小间距。
  2. 百分比设置

    • 百分比相对于元素的字体大小(font-size)计算,例如 letter-spacing: 10% 表示间距为字体大小的 10%。
      .responsive-text {  
        font-size: 20px;  
        letter-spacing: 5%; /* 间距为 1px(20px × 5%) */  
      }  
      
    • 场景:响应式设计中,需根据字体大小动态调整间距,确保不同屏幕尺寸下视觉一致性。
  3. 全局继承特性

    • letter-spacing 具有继承性,子元素会继承父元素的字符间距设置。若需覆盖,可在子元素中重新定义。
      .parent {  
        letter-spacing: 1px;  
      }  
      .child {  
        letter-spacing: normal; /* 清除继承的间距 */  
      }  
      
  4. 与单词间距的区别

    • letter-spacing:控制单个字符(包括字母、汉字、标点)之间的间距,对所有字符生效。
    • word-spacing:控制单词(以空格分隔的文本块)之间的间距,仅对英文单词有效,对中文无效(中文无单词概念)。
    • 示例对比:
      /* 字符间距:每个字母/汉字间距增加 */  
      .letter-example {  
        letter-spacing: 2px;  
      }  
      /* 单词间距:仅英文单词间空格扩大 */  
      .word-example {  
        word-spacing: 1em;  
      }  
      
注意事项
  1. 兼容性letter-spacing 在现代浏览器和 IE6+ 均支持,但旧版浏览器对负值的渲染可能存在差异,需测试验证。
  2. 文本对齐影响:当元素设置 text-align: justify(两端对齐)时,letter-spacing 可能与浏览器自动调整的字间距产生叠加效果,导致布局不稳定,需谨慎组合使用。
  3. 性能考虑:大规模文本设置过密或过疏的字符间距可能影响可读性,尤其在小字体场景下(如正文),建议保持默认值(normal)或轻微调整(±0.5px)。
  4. 应用场景限制
    • 适合标题、标语等短文本的视觉优化;
    • 不建议用于大段正文,可能降低阅读效率;
    • 中文场景中,字符间距调整需结合字体特性,部分字体本身字间距较大,过度调整会显得松散。
实践示例
  • 标题优化
    h1 {  
      font-size: 48px;  
      letter-spacing: 1px; /* 提升标题层次感 */  
      text-transform: uppercase; /* 配合大写字母增强视觉效果 */  
    }  
    
  • 紧凑型按钮文字
    .btn {  
      font-size: 14px;  
      letter-spacing: -0.3px; /* 缩小间距使文字更紧凑 */  
      padding: 8px 16px;  
    }  
    

通过合理使用 letter-spacing,可优化文本的视觉呈现,提升界面设计的精致感,但需结合具体场景和字体特性调整,避免过度使用影响可读性。

如何隐藏页面元素?列举多种 CSS 方法

在 CSS 中隐藏页面元素有多种方式,不同方法的原理和适用场景差异显著,需根据需求选择合适方案。以下是常见隐藏方法及其特点:

1. display: none;

原理:将元素从文档流中完全移除,不占据任何空间,浏览器不渲染其内容,也无法触发事件(如点击)。
特点

  • 隐藏彻底,不影响布局,但元素的 DOM 结构仍存在于代码中;
  • 动态切换时(如配合 JavaScript)会导致回流(reflow),影响性能;
  • 无法通过过渡动画(transition)渐变隐藏,因为元素脱离文档流后不参与渲染过程。
    场景:非交互性元素的永久隐藏(如加载时的临时占位符),或根据条件动态显示/隐藏的复杂组件。
2. visibility: hidden;

原理:元素仍在文档流中占据空间,但内容不可见,可触发鼠标事件(如 :hover),但无法响应点击。
特点

  • 占位空间保留,避免隐藏后布局重排;
  • 支持过渡动画(如 visibility: visible/hidden 切换),但动画效果仅控制显示状态,元素本身不产生渐变过程;
  • 子元素继承该属性,若需子元素可见,需单独设置 visibility: visible;
    场景:临时隐藏元素且需保留布局空间(如表格中的某一列),或配合动画实现元素的显隐切换。
3. opacity: 0;

原理:元素透明度设为 0,视觉上不可见,但仍占据空间并响应鼠标事件(如点击、滚动)。
特点

  • 支持过渡动画(opacity 从 1 到 0 渐变),实现平滑隐藏效果;
  • 子元素会继承透明度,若需子元素可见,需单独设置 opacity: 1;
  • 元素仍在文档流中,隐藏后可能遮挡底层内容(需配合 z-index 调整层级)。
    场景:需要动画效果的元素隐藏(如模态框退出动画),或临时隐藏但需保持交互功能的元素。
4. position: absolute; clip: rect(0, 0, 0, 0);(旧版方法)

原理:通过绝对定位将元素移出可视区域,并用 clip 属性将其裁剪为 0 大小。
特点

  • 元素脱离文档流,不占据空间;
  • 仅适用于块级元素,且 clip 属性需配合 position: absolute 使用;
  • 现代浏览器已推荐使用 clip-path 替代,旧版语法(clip: rect())兼容性有限。
    示例

.hidden {  
  position: absolute;  
  clip: rect(0, 0, 0, 0);  
  width: 1px;  
  height: 1px;  
  overflow: hidden;  
}  

场景:无障碍设计中隐藏屏幕阅读器文本(如供搜索但不显示的内容),但更推荐使用 clip-path: inset(100%);

5. clip-path: inset(100%);

原理:使用 CSS 形状裁剪(clip-path)将元素从可视区域完全裁剪掉,不占据空间。
特点

  • 支持复杂形状裁剪,inset(100%) 表示元素完全超出裁剪区域;
  • 元素仍在文档流中,但视觉上不可见,不响应交互;
  • 现代浏览器支持良好,IE 不兼容。
    示例

.hidden {  
  clip-path: inset(100%);  
}  

场景:替代旧版 clip 方法,用于隐藏需保留 DOM 结构但不显示的元素。

6. overflow: hidden; 配合定位

原理:父元素设置 overflow: hidden;,子元素通过绝对定位或负边距移出父容器可视区域。
特点

  • 子元素脱离父容器视线,但仍在文档流中(若未脱离文档流则可能影响布局);
  • 需确保子元素超出父容器边界,且父容器有固定尺寸。
    示例

.parent {  
  width: 200px;  
  height: 200px;  
  overflow: hidden;  
}  
.child {  
  position: relative;  
  left: 300px; /* 移出父容器可视区域 */  
}  

场景:滑动菜单隐藏在容器外,通过动画切换位置显示/隐藏。

7. transform: scale(0);

原理:通过缩放变换将元素尺寸缩小为 0,视觉上不可见,但仍占据原始空间(根据 transform-origin 定位)。
特点

  • 支持过渡动画(transform 缩放过程),实现元素的渐变消失;
  • 元素仍可响应鼠标事件(点击缩放为 0 的元素等同于点击其原始位置);
  • 子元素会继承缩放属性,需注意层级关系(z-index)避免遮挡。
    示例

.hidden {  
  transform: scale(0);  
  transition: transform 0.3s ease;  
}  

场景:需要动画效果且需保留交互逻辑的元素隐藏(如折叠面板的内容)。

方案对比
方法 占据空间 响应事件 支持动画 兼容性 典型场景
display: none; 全兼容 动态显示/隐藏组件
visibility: hidden; 部分 全兼容 保留布局的临时隐藏
opacity: 0; 全兼容 带过渡效果的隐藏
clip-path 现代浏览器 复杂形状裁剪隐藏
transform: scale(0) 现代浏览器 动画驱动的元素收缩隐藏
注意事项
  • 无障碍性:屏幕阅读器可能无法识别 display: none; 或 clip-path 隐藏的内容,需配合 ARIA 标签(如 aria-hidden="true")明确告知辅助技术。
  • 性能影响display: none; 切换会触发大规模回流,而 opacity 和 transform 切换仅触发合成层更新,性能更优。
  • 层级控制:隐藏元素若涉及 z-index,需确保其层级不会影响其他可见元素(如 opacity: 0 的元素可能覆盖底层内容)。

选择隐藏方法时,需综合考虑是否保留布局空间、是否需要交互、动画需求及兼容性,优先使用现代 CSS 特性(如 opacitytransform)实现高效优雅的隐藏效果。

img标签的alt和title属性的区别及作用


img标签的alttitle属性都是用于描述图片的辅助性信息,但二者的功能和应用场景存在明显差异。

alt属性的作用与特点
alt属性是图片的替代文本,属于无障碍设计的核心要素。当图片因网络问题、路径错误或浏览器不支持等原因无法显示时,页面会显示alt属性的内容,帮助用户理解图片的含义。此外,屏幕阅读器(如用于视障用户的工具)会读取alt文本,确保内容可访问性。因此,alt属性具有功能性和必要性,尤其是在图片承载关键信息(如图表、Logo)时必须正确设置。若图片仅用于装饰(无实际信息价值),可将alt属性值留空(即alt=""),避免屏幕阅读器冗余读取。

title属性的作用与特点
title属性是图片的提示文本,当用户将鼠标悬停在图片上时,会以浮动提示框的形式显示内容。它主要用于提供额外的补充信息或细节说明,例如图片的作者、拍摄时间或更详细的描述。与alt不同,title属性并非必需,且其功能依赖于用户主动触发(鼠标悬停),在移动端或触摸设备上可能无法生效。此外,title属性还可用于其他HTML元素(如链接、按钮),而altimg标签的专用属性。

两者的核心区别

维度 alt属性 title属性
功能定位 图片的替代内容,确保无障碍访问 鼠标悬停时的提示信息,补充说明
必要性 关键图片必须设置,装饰性图片可留空 非必需,根据需求选择是否添加
触发条件 图片无法显示时自动呈现 鼠标悬停时被动触发
适用场景 所有图片(尤其功能性图片) 需额外信息提示的场景(如装饰性图片)
屏幕阅读器 会读取内容 通常不读取(部分浏览器可能支持)

使用建议

  • 对于图表、Logo、产品图等功能性图片,必须设置alt属性,内容应简洁准确地描述图片核心信息。
  • 对于装饰性图片,设置alt=""以避免无障碍工具误读,同时可根据需要添加title属性提供额外说明。
  • 避免重复设置alttitle的相同内容,除非确实需要双重信息(如复杂图片的详细描述)。
  • 注意alt属性的值不宜过长,建议控制在1-2句话内,确保屏幕阅读器高效传达信息。

JavaScript 基础数据类型有哪些?


在JavaScript中,基础数据类型(Primitive Types)是直接存储值的数据类型,与引用数据类型(如对象、数组)不同,它们的变量直接保存数据本身而非内存地址。ECMAScript规范定义了以下7种基础数据类型,每种类型在存储方式、取值范围和使用场景上各有特点。

1. Undefined

表示变量已声明但未赋值的状态。当声明一个变量未初始化时,其默认值为undefined。例如:

let a;  
console.log(a); // 输出: undefined  

需注意,undefined是一个原始值,与未声明变量不同——访问未声明的变量会抛出ReferenceError,而访问值为undefined的变量不会报错。

2. Null

表示一个空值有意赋值为空的对象引用。它是JavaScript中唯一属于null类型的值,通常用于初始化变量或表示预期中的空值(如等待加载的数据)。例如:

let element = null; // 表示变量计划指向DOM元素,但当前无引用  

nullundefined的区别在于:null是开发者主动赋值的空值,而undefined是系统默认的未赋值状态。在类型判断中,typeof null会返回"object"(这是早期JavaScript的设计缺陷)。

3. Boolean

包含两个字面值:truefalse,用于表示逻辑真或假。Boolean类型常用于条件判断(如if语句、循环条件)或标识状态(如isLoadinghasPermission)。例如:

const isLoggedIn = true;  
if (isLoggedIn) { /* 执行登录状态逻辑 */ }  

需要注意,其他数据类型可通过类型转换隐式转为Boolean值。例如,0""nullundefinedNaN转换为false,其余值(如非零数字、非空字符串)转换为true

4. Number

用于表示整数和浮点数,包含正数、负数和NaN(Not a Number,表示无效数值)。JavaScript采用IEEE 754双精度浮点标准,因此存在精度限制(如0.1 + 0.2 !== 0.3)。常见的数值操作包括算术运算、类型转换和数值方法(如parseInt()Math.sqrt())。

  • 特殊值Infinity(正无穷)、-Infinity(负无穷)、NaN
  • 类型判断:可通过typeof检测是否为number,但需注意NaNtypeof也为"number",需通过isNaN()Number.isNaN()进一步判断。

5. String

用于表示文本数据,由Unicode字符序列组成,可通过单引号('')、双引号("")或模板字面量()定义。字符串是不可变的,即一旦创建,其内容无法修改,但可以通过方法(如concat()slice())返回新字符串。例如:

const name = "Alice";  
const message = `Hello, ${name}!`; // 模板字面量支持变量插值  

字符串的常用操作包括拼接、截取、查找子串和正则匹配等。

6. Symbol

ES6引入的原始数据类型,表示唯一的标识符。每个Symbol值都是唯一的,即使使用相同描述符创建,也不会相等。其主要作用是为对象添加唯一的属性键,避免属性名冲突(如在类的私有属性中使用)。例如:

const key = Symbol("uniqueKey");  
const obj = { [key]: "value" };  

Symbol类型的值不能通过对象的for...in循环枚举,也不能转为字符串(除非显式调用toString()),因此适合作为私有属性的标识。

7. BigInt

ES10引入的原始数据类型,用于表示任意精度的整数,解决了Number类型因精度限制无法准确表示大整数的问题(如超过2^53的整数会丢失精度)。BigInt类型通过在数值末尾添加n后缀定义,例如:

const largeNum = 12345678901234567890123n;  
console.log(largeNum + 1n); // 输出: 12345678901234567890124n  

BigInt与普通Number不能直接进行混合运算,需显式转换类型(如通过Number()BigInt()函数)。

闭包的概念、作用及应用场景
闭包是JavaScript中一个重要且基础的概念,其本质是函数与词法环境的组合。理解闭包的原理和应用场景,对掌握JavaScript的作用域、内存管理及高级编程技巧至关重要。

闭包的概念

当一个函数内部定义的嵌套函数(内部函数)引用了外层函数的变量或参数时,就形成了闭包。即使外层函数执行完毕,嵌套函数依然可以访问外层函数作用域中的变量,因为这些变量会被封闭在嵌套函数的环境中,不会被垃圾回收机制释放。
例如:

function outer() {  
  let count = 0;  
  function inner() {  
    count++; // 引用外层函数的变量count  
    console.log(count);  
  }  
  return inner; // 返回内层函数  
}  
const counter = outer();  
counter(); // 输出: 1(此时outer函数已执行完毕,但count仍被inner引用)  
counter(); // 输出: 2(count的值被保留)  

在这个例子中,inner函数形成了闭包,它持有对outer函数作用域中count变量的引用,因此每次调用counter时,count会持续累加,而非重置为初始值。

闭包的核心作用

1. 实现变量私有化

闭包允许将变量封装在函数内部,通过返回的嵌套函数控制对变量的访问,从而实现私有作用域。外部代码无法直接访问或修改函数内部的变量,只能通过嵌套函数提供的接口(如方法)操作变量,这是JavaScript中实现封装的重要方式。

function createCounter() {  
  let privateCount = 0;  
  return {  
    increment() { privateCount++; },  
    getCount() { return privateCount; }  
  };  
}  
const counter = createCounter();  
counter.increment(); // 调用接口修改私有变量  
console.log(counter.getCount()); // 输出: 1(无法直接访问privateCount)  
2. 保存状态

闭包可以“记住”外层函数执行时的变量状态。当外层函数多次调用时,每次调用会创建独立的闭包环境,互不干扰。例如,在循环中使用闭包保存迭代变量的值:

for (let i = 0; i < 3; i++) {  
  setTimeout(() => {  
    console.log(i); // 输出: 0, 1, 2(闭包保存了每次循环的i值)  
  }, 100);  
}  

若将let改为var,由于var的函数作用域特性,闭包会共享同一个i变量,最终输出3次3

3. 函数柯里化(Currying)

闭包可用于将多参数函数转换为单参数函数链,逐步接收参数并返回新函数,直到所有参数收集完毕后执行计算。这有助于提高函数的复用性和灵活性。

function add(a) {  
  return function(b) {  
    return a + b;  
  };  
}  
const add5 = add(5); // 闭包保存a=5  
console.log(add5(3)); // 输出: 8(相当于5+3)  

闭包的常见应用场景

1. 模块模式(Module Pattern)

通过闭包封装私有变量和方法,暴露公共接口,实现类似面向对象的模块封装。例如:

const myModule = (function() {  
  let privateData = "secret";  
  function privateMethod() { /* 私有方法 */ }  
  return {  
    publicMethod() {  
      // 访问私有变量或方法  
      console.log(privateData);  
    }  
  };  
})();  
myModule.publicMethod(); // 输出: "secret"(可访问公共方法,但无法直接访问privateData)  
2. 事件处理函数

在事件监听器中,闭包可用于保存事件处理逻辑所需的上下文或变量。例如,动态创建按钮时为每个按钮绑定不同的点击事件:

function createButtons() {  
  for (let i = 0; i < 3; i++) {  
    const btn = document.createElement("button");  
    btn.textContent = `Button ${i}`;  
    btn.addEventListener("click", () => {  
      alert(`You clicked button ${i}`); // 闭包保存i的值  
    });  
    document.body.appendChild(btn);  
  }  
}  
createButtons();  
3. 防抖与节流函数

在前端性能优化中,闭包常用于实现防抖(Debounce)和节流(Throttle)函数,通过保存定时器ID或上次执行时间等状态,控制函数的执行频率。

function debounce(func, delay) {  
  let timeoutId;  
  return function() {  
    const context = this;  
    const args = arguments;  
    clearTimeout(timeoutId);  
    timeoutId = setTimeout(() => {  
      func.apply(context, args); // 闭包保存timeoutId和参数  
    }, delay);  
  };  
}  
// 使用:监听窗口Resize事件并防抖  
window.addEventListener("resize", debounce(() => {  
  console.log("Resized");  
}, 300));  
4. 回调函数与异步操作

在异步回调中,闭包可用于保留回调函数执行时所需的变量状态。例如,在Ajax请求的回调中访问当前循环的索引值:

for (let i = 0; i < 5; i++) {  
  fetch(`/api/data/${i}`).then(response => {  
    console.log(`Data for ${i}:`, response); // 闭包正确捕获i的值  
  });  
}  
闭包的注意事项
  • 内存管理:闭包会持有外层变量的引用,若滥用可能导致内存泄漏(如长时间持有大型对象的引用)。需确保不再使用的闭包被正确释放(如将引用置为null)。
  • 性能影响:过度使用闭包可能增加函数调用的开销,尤其是在循环或高频操作中,需权衡封装需求与性能损耗。
  • 作用域链查找:闭包访问变量时需沿着作用域链逐层查找,若嵌套层级过深,可能影响变量访问速度,建议保持作用域链简洁。

闭包是JavaScript的核心特性之一,合理使用可实现强大的封装和状态管理,但需注意其潜在的内存和性能问题,确保代码的健壮性和效率。

原型和原型链的原理
在JavaScript中,原型(Prototype)和原型链(Prototype Chain)是实现基于原型的继承的核心机制,也是理解对象属性和方法继承的关键。以下从原理、机制和应用场景展开说明。

原型的基本概念
1. 对象的原型([[Prototype]]

每个对象(除了null)都有一个内置的原型对象[[Prototype]],也称为__proto__,非标准属性),指向其原型。原型对象本身也是一个普通对象,包含可被当前对象继承的属性和方法。

  • 当访问对象的属性或方法时,若当前对象不存在该属性,JavaScript会自动到其原型对象中查找,这一过程称为原型链查找
  • 原型的作用是实现对象之间的属性共享,避免重复定义相同的方法。
2. 函数的prototype属性

函数对象(包括构造函数)拥有一个显式的prototype属性,该属性指向一个对象,称为函数的原型对象。当使用new关键字调用构造函数创建实例时,实例的[[Prototype]]会自动指向构造函数的prototype对象。

function Person(name) {  
  this.name = name;  
}  
// 构造函数的prototype属性指向原型对象  
Person.prototype.sayHello = function() {  
  return `Hello, ${this.name}`;  
};  
// 创建实例  
const alice = new Person("Alice");  
console.log(alice.sayHello()); // 输出: "Hello, Alice"(通过原型链查找方法)  

在上述例子中,alice实例的[[Prototype]]指向Person.prototype,因此可以访问sayHello方法。

3. 原型对象的constructor属性

每个原型对象默认包含一个constructor属性,指向创建它的构造函数。例如,Person.prototype.constructor === Persontrue。这一属性常用于反向引用构造函数,尤其是在对象创建后需要判断类型时。

原型链的工作原理
1. 原型链的构成

原型链是由多个对象的原型依次串联形成的链式结构。每个对象的[[Prototype]]指向其原型对象,而原型对象本身也有自己的[[Prototype]],直至指向Object.prototypeObject.prototype[[Prototype]]null,作为原型链的终点。

const obj = {};  
console.log(obj.__proto__ === Object.prototype); // true  
console.log(Object.prototype.__proto__); // null  
2. 属性查找过程

当访问对象的属性(如obj.prop)时,JavaScript执行以下步骤:

  1. 先在当前对象本身查找是否存在该属性。
  2. 若不存在,沿着原型链向上查找原型对象,直到找到该属性或到达null
  3. 若始终未找到,返回undefined

const animal = { type: "mammal" };  
const dog = { breed: "husky" };  
dog.__proto__ = animal; // 设置原型链:dog → animal → Object.prototype → null  
console.log(dog.type); // 输出: "mammal"(从animal原型中查找)  
console.log(dog.toString()); // 输出: "[object Object]"(从Object.prototype中查找)  
3. 属性赋值与原型链

对对象的属性赋值时,若属性不存在于当前对象,会直接在当前对象上创建该属性,而非修改原型链中的属性。只有当使用delete操作符删除对象自身属性时,才会影响后续的原型链查找。

dog.type = "canine"; // 在dog对象上创建type属性  
console.log(dog.type); // 输出: "canine"(优先使用自身属性)  
delete dog.type; // 删除自身属性  
console.log(dog.type); // 输出: "mammal"(重新从原型链查找)  

原型链的应用:继承

原型链是JavaScript实现继承的主要方式之一,通过将子类的原型指向父类的实例,使子类可以继承父类的属性和方法。

// 父类构造函数  
function Animal(type) {  
  this.type = type;  
}  
Animal.prototype.getInfo = function() {  
  return `Type: ${this.type}`;  
};  
// 子类构造函数  
function Dog(breed, type) {  
  Animal.call(this, type); // 借用构造函数初始化父类属性  
  this.breed = breed;  
}  
// 设置原型链:Dog.prototype.__proto__ = Animal.prototype  
Dog.prototype = Object.create(Animal.prototype);  
Dog.prototype.constructor = Dog; // 修复constructor指向  
// 创建子类实例  
const husky = new Dog("Husky", "mammal");  
console.log(husky.getInfo()); // 输出: "Type: mammal"(继承自Animal原型)  
console.log(husky.breed); // 输出: "Husky"(子类自身属性)  

上述代码中,Dog通过原型链继承了AnimalgetInfo方法,同时通过构造函数借用(call)初始化了父类属性,实现了属性和方法的双重继承。

原型与原型链的注意事项

1. 原型污染风险

原型链是共享的,若修改原型对象的属性,会影响所有继承该原型的对象。例如:

Object.prototype.foo = "bar"; // 污染全局原型  
const obj1 = {};  
const obj2 = {};  
console.log(obj1.foo); // 输出: "bar"(所有对象都继承了该属性)  

这种操作可能导致不可预期的副作用,尤其是在第三方库中,需避免直接修改原生对象的原型。

2. 性能考量

原型链查找会带来一定的性能开销,尤其是在多层级原型链中查找属性时。对于高频访问的属性,建议直接定义在对象本身,而非通过原型链继承。

3. 类型判断

使用instanceof操作符可判断对象是否属于某个构造函数的实例,其原理是检查对象的原型链中是否存在构造函数的原型对象:

console.log(husky instanceof Dog); // true  
console.log(husky instanceof Animal); // true(原型链包含Animal.prototype)  

typeof操作符无法准确区分对象的具体类型(如数组、普通对象均返回"object"),需结合Object.prototype.toString方法或instanceof进行判断。

4. ES6类与原型的关系

ES6的class语法糖本质上仍是基于原型的继承,其底层实现与构造函数+原型链一致。例如:

class Animal {  
  constructor(type) { this.type = type; }  
  getInfo() { return `Type: ${this.type}`; }  
}  
// 等价于传统构造函数+原型的写法  

classextends关键字会自动处理原型链的设置,使代码更简洁,但底层原理与传统原型继承相同。

总结

原型和原型链是JavaScript实现继承和属性共享的核心机制,其关键点在于:

  • 每个对象都有原型,指向其继承的对象。
  • 属性查找沿原型链向上进行,直至null
  • 构造函数的prototype属性用于定义实例共享的方法。
  • 原型链继承通过设置原型指向实现,但需注意原型污染和性能问题。
    理解原型链的原理有助于深入掌握JavaScript的对象系统,并在开发中合理利用继承机制,避免常见陷阱。

箭头函数与普通函数的区别
箭头函数(Arrow Functions)是ES6引入的新特性,其语法简洁,与普通函数(传统函数表达式或函数声明)在行为和功能上有显著差异。以下从多个维度对比两者的核心区别。

1. 语法形式

普通函数使用function关键字定义,可通过函数声明或函数表达式创建,支持命名参数和函数体包裹的多条语句。例如:

// 函数声明  
function regularFunc(a, b) {  
  return a + b;  
}  
// 函数表达式  
const regularFuncExpr = function(a, b) {  
  return a * b;  
};  

箭头函数使用箭头符号=>定义,语法更简洁,参数列表和函数体的写法灵活。例如:

// 单参数、单表达式  
const arrowFunc1 = x => x * 2;  
// 多参数、块级作用域  
const arrowFunc2 = (a, b) => {  
  const result = a + b;  
  return result;  
};  
// 无参数时需用括号包裹  
const arrowFunc3 = () => console.log("Hello");  

2. this指向

普通函数this在运行时动态绑定,取决于函数的调用方式:

  • 作为对象方法调用时,this指向调用者对象。
  • 作为普通函数调用时,this指向全局对象(浏览器中为window,严格模式下为undefined)。
  • 使用callapplybind可显式绑定this

const obj = {  
  name: "Alice",  
  greet: function() {  
    console.log(this.name); // 输出: "Alice"(this指向obj)  
  }  
};  
obj.greet();  

箭头函数this在定义时静态绑定,指向其词法作用域(即外层作用域)中的this,且无法通过callapplybind修改。常见应用场景是在回调函数中保持外层this的一致性:

const obj = {  
  name: "Alice",  
  delayGreet: function() {  
    setTimeout(() => {  
      console.log(this.name); // 输出: "Alice"(箭头函数捕获外层的this)  
    }, 100);  
  }  
};  
obj.delayGreet();  

若在箭头函数中使用this,需确保外层作用域存在合法的this值。例如,在全局作用域中,箭头函数的this指向window(浏览器环境)。

3. 构造函数与new关键字

普通函数可作为构造函数,通过new关键字创建实例。此时,函数内部的this指向新创建的实例,且默认返回该实例(除非显式返回其他对象)。

function Person(name) {  
  this.name = name;  
}  
const alice = new Person("Alice");  
console.log(alice.name); // 输出: "Alice"  

箭头函数不能作为构造函数,无法使用new关键字调用。尝试使用new调用会抛出错误,因为箭头函数没有prototype属性,也不支持创建实例时的this绑定。

const ArrowPerson = (name) => { this.name = name; };  
const instance = new ArrowPerson("Bob"); // 报错: ArrowPerson is not a constructor  

4. arguments对象与剩余参数

普通函数内部自动生成arguments对象,包含调用时传入的所有参数。arguments是类数组对象,可通过索引访问参数,但无法直接使用数组方法(需转换为数组)。

function sum() {  
  let total = 0;  
  for (let i = 0; i < arguments.length; i++) {  
    total += arguments[i];  
  }  
  return total;  
}  
console.log(sum(1, 2, 3)); // 输出: 6  

箭头函数不具备arguments对象,需通过剩余参数(Rest Parameters)...args获取参数数组,或直接使用参数名访问。剩余参数是真正的数组,可直接调用数组方法。

const arrowSum = (...args) => {  
  return args.reduce((acc, cur) => acc + cur, 0);  
};  
console.log(arrowSum(1, 2, 3)); // 输出: 6  

若在箭头函数中使用arguments,会引用外层作用域中的arguments对象(如果有的话)。例如:

function outer() {  
  const inner = () => {  
    console.log(arguments[0]); // 引用outer的arguments对象  
  };  
  inner();  
}  
outer(10); // 输出: 10  

5. 原型(prototype)属性

普通函数作为构造函数时,拥有显式的prototype属性,指向其原型对象,用于定义实例共享的方法。

function RegularFunc() {}  
console.log(RegularFunc.prototype); // 输出: { constructor: RegularFunc }  

箭头函数没有prototype属性,因此无法通过箭头函数为实例添加共享方法。访问其prototype属性会返回undefined

const ArrowFunc = () => {};  
console.log(ArrowFunc.prototype); // 输出: undefined  

6. 函数名与调试

普通函数的函数名可通过name属性获取:

  • 函数声明的name为定义时的名称。
  • 函数表达式的name为赋值时的变量名(ES6规范要求,部分旧浏览器可能返回"anonymous")。

function namedFunc() {}  
const exprFunc = function() {};  
console.log(namedFunc.name); // 输出: "namedFunc"  
console.log(exprFunc.name); // 输出: "exprFunc"  

箭头函数name属性返回定义时的箭头函数表达式,通常为"() => ..."形式,或根据上下文推断(如对象属性中的箭头函数可能显示为"[objectMethod]")。

const arrow = () => {};  
console.log(arrow.name); // 输出: "arrow"(ES6规范,变量名作为函数名)  

7. 适用场景对比

场景 普通函数 箭头函数
需要动态绑定this 适用(如对象方法、事件处理函数) 不适用(this固定为词法作用域的值)
作为构造函数创建实例 必须使用普通函数 不可用(会报错)
需要arguments对象 适用(或使用剩余参数) 建议使用剩余参数...args
定义原型方法 必须使用普通函数(通过prototype 不可用(无prototype属性)
简洁的回调函数 可选(但this绑定需注意) 推荐(自动继承外层this
模块导出或命名函数 适用(清晰的函数名便于调试) 可选(需注意函数名的可读性)

img标签的alt和title属性的区别及作用

<img>标签的alttitle属性都是用于描述图片的辅助性信息,但两者的功能和使用场景有明显差异。

alt属性的作用与特点
alt属性是图片的替换文本,主要用于当图片因网络问题、路径错误或用户使用屏幕阅读器时,替代图片显示或朗读给用户。它是无障碍访问(WCAG标准)的重要组成部分,直接影响网页的可访问性。例如,若图片无法加载,浏览器会在图片位置显示alt文本;对于视障用户,屏幕阅读器会读取alt内容以描述图片。
alt属性的内容应简洁且准确,概括图片的核心信息或功能。若图片仅用于装饰(无实际意义),可将alt属性设为空(alt=""),避免屏幕阅读器冗余读取。需注意,搜索引擎会将alt文本作为图片内容的索引,因此合理设置alt也有助于SEO优化。

title属性的作用与特点
title属性是图片的提示文本,当用户将鼠标悬停在图片上时,会以悬浮框形式显示文本内容。它主要用于提供额外的补充信息或细节说明,例如图片的作者、版权信息或更详细的描述。
title属性的使用场景更偏向于增强用户体验,而非必需的无障碍支持。但需注意,移动端设备通常不支持鼠标悬停事件,因此title文本在手机端可能无法显示。此外,过度使用title可能导致页面信息冗余,影响简洁性。

两者的核心区别

维度 alt属性 title属性
功能 图片的替代内容,用于无障碍访问和SEO 鼠标悬停时的提示信息,补充细节
显示条件 图片加载失败或使用屏幕阅读器时显示 鼠标悬停时显示(仅桌面端)
必要性 无障碍要求的必需属性(功能性图片) 可选属性,非必需
内容要求 简洁概括图片核心信息,装饰性图片可留空 可包含详细说明或额外信息

实际应用建议

  • 对于功能性图片(如按钮图标、图表),必须设置alt属性,确保用户理解其用途。

    预览

    <img src="search-icon.png" alt="搜索" title="点击进行站内搜索">  
    
  • 对于装饰性图片alt设为空,title可根据需要决定是否添加。

    预览

    <img src="bg-decor.png" alt="" title="页面底部装饰图案">  
    
  • 避免两者内容重复,alt注重核心信息,title侧重扩展说明。

总之,合理使用alttitle属性可提升网页的可访问性和用户体验,但需根据场景区分功能,确保信息传达的准确性和有效性。

JavaScript 基础数据类型有哪些?

在JavaScript中,数据类型分为原始数据类型(Primitive Types)和引用数据类型(Reference Types)。其中,原始数据类型是直接存储值的简单数据段,而引用数据类型存储的是对数据对象的引用地址。以下详细介绍原始数据类型及其特点,并简要说明引用数据类型。

原始数据类型(共7种)
  1. Undefined
    当变量声明但未初始化时,其默认值为undefined。它表示“未定义”的状态,例如:

    let age;  
    console.log(age); // 输出: undefined  
    
     

    注意,undefined是一个独立的数据类型,可通过typeof运算符判断:

    typeof undefined; // 输出: "undefined"  
    
  2. Null
    null是一个特殊的值,表示“空值”或“无对象引用”。它通常用于显式说明某个变量不指向任何对象,例如:

    let element = null; // 表示变量element当前不引用任何DOM元素  
    
     

    typeof null会返回"object"(这是JavaScript早期设计的历史遗留问题),判断null时需通过严格相等运算符(===):

    element === null; // 正确判断null的方式  
    
  3. Boolean
    布尔类型包含两个值:truefalse,用于表示逻辑判断的结果。例如:

    const isReady = true;  
    const hasError = false;  
    
     

    非布尔值可通过Boolean()函数转换为布尔类型,例如空字符串、0nullundefinedNaN会转换为false,其余为true

  4. Number
    数字类型用于表示整数和浮点数,采用64位双精度浮点格式。JavaScript中没有单独的整数类型,所有数字均以浮点形式存储,例如:

    const count = 10;       // 整数  
    const pi = 3.14;        // 浮点数  
    const infinity = Infinity; // 无穷大  
    const nan = NaN;        // 非数值(Not a Number)  
    
     

    NaN是一个特殊值,表示“非有效数字”,其特点是与任何值(包括自身)比较都返回false,需通过isNaN()函数判断:

    isNaN(nan); // 输出: true  
    
  5. String
    字符串类型用于表示文本数据,由Unicode字符序列组成,可使用单引号('')、双引号("")或模板字面量()定义,例如:

    const name = 'Alice';  
    const message = "Hello, World!";  
    const template = `用户年龄:${age}`; // 模板字面量支持变量插值  
    
     

    字符串是不可变的,对字符串的操作会返回新的字符串实例,而非修改原字符串。

  6. BigInt
    BigInt是ES2020引入的新类型,用于表示任意精度的整数,解决了Number类型无法精确表示大于2^53的整数的问题。通过在数字末尾添加n来定义BigInt,例如:

    const largeNum = 12345678901234567890123n;  
    typeof largeNum; // 输出: "bigint"  
    
  7. Symbol
    Symbol是ES6引入的原始数据类型,用于创建唯一的标识符。每个Symbol实例都是唯一的,即使参数相同,例如:

    const id1 = Symbol('user');  
    const id2 = Symbol('user');  
    id1 === id2; // 输出: false  
    
     

    Symbol常用于对象属性的唯一键,避免属性名冲突,例如:

    const key = Symbol('secret');  
    const obj = { [key]: 'value' };  
    
引用数据类型(1种)

Object
引用数据类型仅Object一种,用于存储键值对集合或复杂数据结构。常见的对象类型包括:

  • 普通对象{ name: 'Bob', age: 30 }
  • 数组[1, 2, 3](本质是Array对象)
  • 函数function fn() {}(本质是Function对象)
  • 日期对象new Date()
  • 正则对象/abc/gi

引用数据类型的值存储在堆内存中,变量保存的是对象的引用地址。当多个变量引用同一个对象时,修改其中一个变量会影响其他变量,例如:

const obj1 = { x: 10 };  
const obj2 = obj1;  
obj2.x = 20;  
console.log(obj1.x); // 输出: 20(obj1和obj2引用同一对象)  
类型判断的注意事项
  • typeof运算符:可判断原始数据类型(除null)和函数类型,但对对象类型(如数组、普通对象)统一返回"object"
    typeof 'hello'; // "string"  
    typeof [1, 2]; // "object"  
    typeof function() {}; // "function"  
    
  • instanceof运算符:用于判断引用类型是否为某构造函数的实例,例如:
    [1, 2] instanceof Array; // true  
    new Date() instanceof Object; // true  
    
  • Object.prototype.toString.call():最准确的类型判断方法,可识别所有原始类型和引用类型,例如:
    Object.prototype.toString.call(null); // "[object Null]"  
    Object.prototype.toString.call(NaN); // "[object Number]"  
    

理解JavaScript的数据类型及其特性是编写健壮代码的基础,尤其是原始类型的不可变性和引用类型的按引用传递机制,是解决常见编程问题的关键。

闭包的概念、作用及应用场景

闭包(Closure)是JavaScript中一个重要且基础的概念,其核心在于函数对外部变量的引用机制。理解闭包的原理和应用场景,对掌握JavaScript的作用域、内存管理及异步编程至关重要。

闭包的概念

闭包是指嵌套函数对其父作用域变量的引用,即使父函数已执行完毕,嵌套函数依然可以访问并操作父作用域中的变量。换句话说,闭包使得函数可以“记住”其定义时的环境,即使函数在其他地方被调用,依然能访问定义时所在作用域的变量。

闭包的形成需满足两个条件:

  1. 函数嵌套(内层函数嵌套在父函数中)。
  2. 内层函数引用了父函数的变量(参数、局部变量等)。

例如:

function outer() {  
  let count = 0; // 父作用域变量  
  function inner() {  
    count++; // 内层函数引用父作用域的count变量  
    console.log(count);  
  }  
  return inner; // 返回内层函数,此时inner形成闭包  
}  

const counter = outer();  
counter(); // 输出: 1(此时outer函数已执行完毕,但count仍被inner引用)  
counter(); // 输出: 2(闭包保留了count的状态)  

在上述例子中,inner函数被返回后,虽然outer函数的执行上下文已销毁,但inner通过闭包维持了对count的引用,使得count的状态得以保留。

闭包的作用

闭包的核心作用体现在以下三个方面:

  1. 变量私有化(封装)
    闭包可以将变量限制在函数内部,避免全局作用域污染,同时通过返回的函数提供对内部变量的访问接口,实现“私有变量”的效果。例如:

    function createCounter() {  
      let count = 0;  
      return {  
        increment: () => count++,  
        getCount: () => count  
      };  
    }  
    
    const counter = createCounter();  
    counter.increment(); // 内部count变为1  
    console.log(counter.getCount()); // 输出: 1(只能通过返回的方法访问count)  
    // 无法直接访问count:counter.count会报错  
    
     

    这里的count是私有的,外部只能通过incrementgetCount方法操作,体现了封装性。

  2. 保存状态(记忆功能)
    闭包会记住父作用域变量的最新值,适用于需要保留状态的场景。例如,在事件处理函数中,闭包可保存循环变量的当前值:

    for (let i = 0; i < 3; i++) {  
      setTimeout(() => {  
        console.log(i); // 输出: 0, 1, 2(闭包保存了let声明的i的当前值)  
      }, 100);  
    }  
    
     

    若将let改为var,由于var的函数作用域特性,闭包会引用同一个全局变量i,最终输出三个3

  3. 函数柯里化(Currying)
    闭包可用于将多参数函数转换为单参数函数链,逐步接收参数并返回新函数,直到所有参数收集完毕后执行计算。例如:

    function add(x) {  
      return function(y) {  
        return x + y;  
      };  
    }  
    
    const add5 = add(5);  
    console.log(add5(3)); // 输出: 8(闭包保存了x=5)  
    
     

    柯里化通过闭包缓存已接收的参数,简化函数调用方式,提高代码复用性。

闭包的应用场景
  1. 模块模式(Module Pattern)
    利用闭包实现私有属性和公共方法,模拟面向对象的封装特性,例如:

    const module = (function() {  
      let privateData = 'secret';  
      return {  
        getPrivateData: () => privateData,  
        setPrivateData: (data) => privateData = data  
      };  
    })();  
    
    console.log(module.getPrivateData()); // 输出: "secret"  
    module.setPrivateData('new secret');  
    
  2. 防抖与节流函数
    在事件处理中,闭包用于保存定时器ID或上次执行时间,实现防抖(Debounce)或节流(Throttle)效果,例如防抖函数的基本实现:

    function debounce(fn, delay) {  
      let timer = null;  
      return function() {  
        const context = this;  
        const args = arguments;  
        clearTimeout(timer);  
        timer = setTimeout(() => {  
          fn.apply(context, args);  
        }, delay);  
      };  
    }  
    
     

    这里的timer变量通过闭包被保存在返回的函数中,确保多次触发事件时定时器能正确清除和重新设置。

  3. 回调函数中的状态保留
    在异步操作(如定时器、AJAX)中,闭包可保留当前迭代的变量值,避免异步回调中出现变量值不符合预期的问题。例如:

    for (let i = 0; i < 3; i++) {  
      fetch(`/api/data/${i}`).then(response => {  
        console.log(`数据${i}加载完成`); // 正确输出i的当前值  
      });  
    }  
    
     

    let声明的i在闭包中保留了每次循环的当前值,若用var则会因作用域问题导致所有回调输出3

  4. 生成器函数(Generator)与迭代器
    闭包可用于实现自定义迭代器,通过保存迭代状态(如当前索引),控制数据的遍历过程:

    function createIterator(arr) {  
      let index = 0;  
      return {  
        next: () => index < arr.length ? { value: arr[index++], done: false } : { done: true }  
      };  
    }  
    
    const iterator = createIterator([1, 2, 3]);  
    console.log(iterator.next().value); // 1  
    console.log(iterator.next().value); // 2  
    
闭包的注意事项
  • 内存管理:闭包会导致父作用域变量无法被垃圾回收,若滥用可能引发内存泄漏。例如,在事件监听器中使用闭包时,需确保在不需要时移除监听器,避免引用链残留。
  • 性能影响:过度使用闭包可能增加函数调用的开销,尤其是在循环中创建大量闭包时,需权衡封装需求与性能损耗。
  • 作用域链查找:闭包访问父作用域变量时,会沿着作用域链逐层查找,若父作用域层级较深,可能影响访问速度。

原型和原型链的原理

原型(Prototype)和原型链(Prototype Chain)是JavaScript实现继承的核心机制,理解其原理有助于深入掌握对象之间的属性继承关系。

原型的基本概念

在JavaScript中,每个函数都有一个prototype属性(称为“显式原型”),它是一个对象,默认包含一个constructor属性,指向函数本身。而每个对象(除了null)都有一个__proto__属性(称为“隐式原型”),指向其构造函数的prototype对象。

例如,通过构造函数创建对象时:

function Person(name) {  
  this.name = name;  
}  
// Person的显式原型prototype默认包含constructor属性  
Person.prototype.constructor === Person; // true  

const alice = new Person('Alice');  
// alice的隐式原型__proto__指向Person.prototype  
alice.__proto__ === Person.prototype; // true  

原型的作用
当访问对象的属性或方法时,若对象本身不存在该属性,JavaScript会自动查找对象的__proto__指向的原型对象,若原型对象中存在该属性,则返回该属性值。这种机制使得多个对象可以共享原型对象中的属性和方法,避免重复定义,实现代码复用。

原型链的形成

原型链是由多个原型对象串联起来的链式结构。每个对象的__proto__指向其构造函数的原型对象,而原型对象本身也是一个对象,它的__proto__会指向其构造函数的原型对象,以此类推,直到Object.prototypeObject.prototype__proto__null,作为原型链的终点。

原型链的层级结构示例

// 定义构造函数  
function Animal() {  
  this.type = 'animal';  
}  
Animal.prototype.speak = function() { /* ... */ };  

function Dog(name) {  
  Animal.call(this); // 借用构造函数实现属性继承  
  this.name = name;  
}  
// 设置Dog的原型为Animal的实例,实现原型链继承  
Dog.prototype = Object.create(Animal.prototype);  
Dog.prototype.constructor = Dog; // 修正constructor指向  

const dog = new Dog('Buddy');  

此时,dog的原型链如下:

dog.__proto__ → Dog.prototype(构造函数Dog的原型)  
Dog.prototype.__proto__ → Animal.prototype(构造函数Animal的原型)  
Animal.prototype.__proto__ → Object.prototype(所有对象的原型)  
Object.prototype.__proto__ → null(原型链终点)  
属性查找与原型链

当访问对象的属性时,JavaScript会按照以下顺序查找:

  1. 对象自身属性:首先检查对象本身是否拥有该属性(包括自定义属性和通过prototype添加的实例属性)。
  2. 原型对象:若对象自身没有该属性,沿着原型链向上查找原型对象(__proto__指向的对象)。
  3. 原型链上层对象:依次查找原型对象的原型,直到Object.prototype,若仍未找到则返回undefined

示例:属性查找过程

const obj = { x: 1 };  
obj.__proto__.y = 2; // 向Object.prototype添加属性y  
console.log(obj.x); // 1(对象自身属性)  
console.log(obj.y); // 2(从Object.prototype继承)  
console.log(obj.z); // undefined(原型链中不存在)  
原型链的特点与注意事项
  1. 原型链的继承性
    原型链实现了“继承”效果,子类对象可以访问父类原型中的属性和方法。例如,所有对象都继承自Object.prototype,因此都拥有toString()hasOwnProperty()等方法。

  2. 原型属性的共享性
    原型对象中的属性被所有实例共享,修改原型属性会影响所有实例。例如:

    function Car() {}  
    Car.prototype.color = 'red';  
    const car1 = new Car();  
    const car2 = new Car();  
    car1.color; // 'red'  
    car2.color; // 'red'  
    Car.prototype.color = 'blue';  
    car1.color; // 'blue'(所有实例的color属性均改变)  
    
     

    若实例对象自身定义了与原型同名的属性,则会覆盖原型属性(属性遮蔽)。

  3. 原型链与构造函数的关系

    • 构造函数的prototype属性指向原型对象。
    • 原型对象的constructor属性默认指向构造函数,可通过手动赋值修正(如继承时)。
    • 对象的constructor属性指向其构造函数,本质是通过原型链查找得到:
      dog.constructor === Dog; // true(实际查找路径:dog → Dog.prototype.constructor)  
      
  4. 原型链的终点
    Object.prototype.__proto__ === null,表明原型链在此终止,所有对象的原型链最终都指向Object.prototypenull除外)。

原型链的应用与限制

应用场景

  • 实现继承:通过原型链关联构造函数,实现属性和方法的继承(如上述Dog继承Animal的示例)。
  • 扩展内置对象:通过修改内置对象的原型(如Array.prototype),为所有实例添加自定义方法:
    Array.prototype.myFilter = function(callback) {  
      // 自定义数组过滤方法  
    };  
    

注意事项

  • 避免污染原型:修改原生对象的原型(如Object.prototype)可能导致全局作用域污染,引发兼容性问题。
  • 原型链过长的性能问题:属性查找需遍历原型链,层级过深会影响访问速度,应尽量避免多层继承。
  • 原型链与instanceof运算符instanceof通过检查对象的原型链是否包含某个构造函数的原型,判断是否为该类型的实例:
    dog instanceof Dog; // true  
    dog instanceof Animal; // true  
    dog instanceof Object; // true  
    
ES6类与原型链的关系

ES6的class语法糖本质上仍是基于原型链的继承,其内部实现与构造函数+原型的模式一致。例如:

class Animal {  
  constructor() {  
    this.type = 'animal';  
  }  
  speak() {}  
}  
// 等价于传统构造函数写法  
Animal.prototype.speak = function() {};  

子类通过extends关键字继承父类时,会自动设置子类原型的__proto__指向父类原型,实现原型链继承。

总之,原型和原型链是JavaScript实现继承和属性共享的核心机制,理解其原理有助于正确使用对象的属性继承、避免常见编程错误,并灵活运用原型链进行代码设计。

箭头函数与普通函数的区别

箭头函数(Arrow Function)是ES6引入的新特性,其语法简洁,改变了传统函数的作用域绑定方式。与普通函数相比,箭头函数在语法、this指向、原型、参数处理等方面有显著差异。

1. 语法形式

普通函数使用function关键字定义,可作为函数声明或表达式:

// 函数声明  
function regularFunc() {  
  // 函数体  
}  

// 函数表达式  
const regularFuncExpr = function() {  
  // 函数体  
};  

箭头函数使用箭头符号=>定义,语法更简洁,参数和函数体的写法灵活:

  • 无参数或多参数需用括号包裹:
    () => console.log('无参数');  
    (x, y) => x + y;  
    
  • 单参数可省略括号:
    x => x * 2;  
    
  • 函数体若为单表达式,可省略{}return(隐式返回结果):
    x => x * x; // 等价于 x => { return x * x; }  
    
  • 函数体若为多条语句,需用{}包裹,显式使用return
    (x, y) => {  
      const sum = x + y;  
      return sum * 2;  
    };  
    
2. this指向的差异

普通函数this在调用时动态绑定,取决于函数的调用方式:

  • 作为对象方法调用时,this指向调用者对象:
    const obj = {  
      name: 'Alice',  
      sayHi: function() {  
        console.log(this.name); // 输出: 'Alice'(this指向obj)  
      }  
    };  
    obj.sayHi();  
    
  • 作为普通函数调用时,this指向全局对象(浏览器中为window,严格模式下为undefined):
    function greet() {  
      console.log(this.name); // 若未在对象中调用,this指向window/undefined  
    }  
    greet();  
    
  • 使用call()apply()bind()可显式绑定this
    const obj2 = { name: 'Bob' };  
    greet.call(obj2); // this指向obj2,输出: 'Bob'  
    

箭头函数this在定义时绑定,指向其所在的词法作用域(静态作用域),且无法通过call()apply()bind()改变:

  • 若箭头函数在全局作用域中定义,this指向全局对象:
    const arrowFunc = () => {  
      console.log(this); // 浏览器中指向window  
    };  
    arrowFunc();  
    
  • 若在对象方法中使用箭头函数,this会继承外层作用域的this(通常为全局对象或函数作用域):
    const obj3 = {  
      name: 'Charlie',  
      sayHi: () => {  
        console.log(this.name); // this指向window,输出: undefined(非obj3)  
      }  
    };  
    obj3.sayHi(); // 此处箭头函数的this继承自外层(全局作用域)  
    
  • 常见用途:在回调函数中保持this指向外层作用域(如组件实例):
    class Component {  
      constructor() {  
        this.data = [];  
      }  
      fetchData() {  
        setTimeout(() => {  
          this.data.push('new data'); // 箭头函数的this指向Component实例  
        }, 1000);  
      }  
    }  
    
3. 原型(prototype)的有无

普通函数作为构造函数时,具有prototype属性,用于定义原型方法:

function Person(name) {  
  this.name = name;  
}  
Person.prototype.sayName = function() {  
  console.log(this.name);  
};  
const p = new Person('David');  
p.sayName(); // 输出: 'David'(通过原型链调用)  

箭头函数没有prototype属性,不能作为构造函数使用,无法通过new关键字实例化:

const ArrowPerson = (name) => {  
  this.name = name;  
};  
new ArrowPerson('Eve'); // 报错:ArrowPerson is not a constructor  
console.log(ArrowPerson.prototype); // 输出: undefined  
4. 参数处理与arguments对象

普通函数内部有默认的arguments对象,包含所有传入的参数:

function sum() {  
  let total = 0;  
  for (let i = 0; i < arguments.length; i++) {  
    total += arguments[i];  
  }  
  return total;  
}  
sum(1, 2, 3); // 输出: 6  

箭头函数不绑定arguments对象,需通过剩余参数(...rest)获取所有参数:

const arrowSum = (...args) => {  
  return args.reduce((acc, cur) => acc + cur, 0);  
};  
arrowSum(1, 2, 3); // 输出: 6  
// 尝试访问arguments会报错:  
const arrowFunc = () => {  
  console.log(arguments); // 报错:arguments is not defined  
};  
5. 作为构造函数的能力
  • 普通函数可作为构造函数,通过new关键字创建实例,内部this指向新创建的对象:

    function Car(brand) {  
      this.brand = brand;  
    }  
    const car = new Car('BMW');  
    console.log(car.brand); // 输出: 'BMW'  
    
  • 箭头函数不能作为构造函数,调用时会报错,因为其没有prototypeconstructor,且this指向定义时的作用域,而非新对象:

    const ElectricCar = (brand) => {  
      this.brand = brand;  
    };  
    new ElectricCar('Tesla'); // 报错:Cannot use 'new' with an arrow function  
    
6. super关键字的使用

在类的方法中,普通函数需显式绑定this,而箭头函数this自动继承自外层的类实例,可直接使用super

class Parent {  
  constructor() {  
    this.value = 10;  
  }  
  getValue() {  
    return this.value;  
  }  
}  

class Child extends Parent {  
  constructor() {  
    super();  
    // 普通函数需绑定this  
    this.getVal = function() {  
      return super.getValue(); // 正确,this指向Child实例  
    };  
    // 箭头函数无需绑定this  
    this.arrowGetVal = () => {  
      return super.getValue(); // 正确,this自动继承自Child实例  
    };  
  }  
}  
适用场景对比
场景 普通函数 箭头函数
对象方法定义 推荐使用(this指向对象实例) 不推荐(this指向外层作用域,非对象实例)
构造函数 必须使用 不可用
原型方法定义 必须使用 不可用(无prototype
回调函数(需保留外层this 需手动绑定this(如bind 推荐使用(自动继承外层this
简单函数表达式(如数组方法) 可选 推荐使用(语法简洁,无this绑定问题)

img标签的alt和title属性的区别及作用

<img>标签的alttitle属性在功能和使用场景上有明显区别,理解两者差异对无障碍开发和用户体验优化至关重要。

alt属性的核心作用是替代文本,用于当图片无法加载时(如网络错误、路径失效)或用户使用屏幕阅读器时,向用户传达图片的语义信息。它是无障碍访问(WCAG标准)的重要组成部分,搜索引擎也会通过alt文本理解图片内容,影响SEO效果。例如,商品详情页的图片alt应描述商品名称、型号等关键信息,而装饰性图片的alt可设为空(alt=""),避免冗余读屏。需注意,alt文本应简洁精准,避免堆砌关键词,且必须包含在标签内,不可省略。

title属性的作用是提示信息,当用户将鼠标悬停在图片上时显示悬浮提示,提供额外细节或解释。它更偏向于补充说明,而非必需内容。例如,图标按钮的title可标注功能名称(如“搜索”),帮助用户理解图标含义;地图截图的title可注明区域名称或坐标。但title的兼容性存在差异,部分移动端设备可能不显示,且屏幕阅读器对其支持不如alt完善,因此不能替代alt的语义功能。

两者的差异可通过表格进一步明确:

属性 必要性 显示时机 核心用途 无障碍支持
alt 必需 图片加载失败时 传达图片核心语义 屏幕阅读器读取
title 可选 鼠标悬停时 提供补充说明或交互提示 部分场景支持

实际开发中,应优先保证alt属性的正确性,根据需求选择性添加title。例如,新闻配图的alt需描述新闻事件,title可补充拍摄时间或摄影师信息;纯装饰性图片仅需alt="",无需title。需注意,若图片用于超链接,alt描述的是图片本身,而链接的语义需通过上下文或额外文本传达,不可依赖title属性。

JavaScript 基础数据类型有哪些?

JavaScript 的基础数据类型(Primitive Types)是直接存储值的数据类型,与引用数据类型(如对象、数组)不同,它们的值在变量中被直接保存,而非存储内存地址。ES6 规范定义了7种基础数据类型,每种类型在内存分配、赋值方式和操作特性上均有独特表现。

1. Undefined
表示变量已声明但未初始化的值。当声明变量未赋值时,默认值为undefined。例如:

let a;  
console.log(a); // 输出 undefined  

需注意,undefined与未声明变量不同——访问未声明变量会报错,而undefined是合法值,可通过typeof检测为"undefined"

2. Null
表示有意为空的值,通常用于初始化变量或标识对象引用的缺失。它是一个假值(falsy value),通过typeof检测会返回"object"(这是早期设计遗留的历史问题)。例如:

let obj = null;  
console.log(typeof obj); // 输出 "object"  

实际开发中,null常用于表示预期中的空对象,如DOM元素未找到时的返回值。

3. Boolean
仅有两个值:truefalse,用于逻辑判断。布尔值可通过逻辑运算符(如&&||)或类型转换生成。例如:

const isReady = true;  
console.log(!!"abc"); // 输出 true(字符串转换为布尔值)  

4. Number
包含整数和浮点数,采用IEEE 754双精度浮点格式存储。特殊值包括Infinity(无穷大)、-InfinityNaN(非数值)。例如:

const pi = 3.14;  
const num = NaN;  
console.log(num === NaN); // 输出 false(NaN与任何值都不相等,包括自身)  

需注意浮点运算的精度问题(如0.1 + 0.2 !== 0.3),处理金融计算时需使用库(如decimal.js)。

5. String
用于表示文本数据,由Unicode字符序列组成,用单引号、双引号或模板字面量(反引号)定义。字符串是不可变类型,对其操作会返回新字符串。例如:

const name = "Alice";  
const greeting = `Hello, ${name}`; // 模板字面量支持变量插值  
console.log(name.length); // 输出 5  

6. Symbol(ES6新增)
表示唯一的标识符,通过Symbol()函数创建。每个Symbol实例都是唯一的,即使参数相同。它常用于对象属性名,避免属性名冲突。例如:

const key = Symbol("id");  
const obj = { [key]: "value" };  
console.log(obj[key]); // 输出 "value"  

7. BigInt(ES10新增)
用于表示任意精度的整数,解决Number类型无法安全表示大于2^53的整数的问题。通过在数字末尾加n定义。例如:

const bigNum = 12345678901234567890n;  
console.log(bigNum + 1n); // 输出 12345678901234567891n  

类型检测方式

  • typeof:可区分大部分基础类型,但对null返回"object",对ArrayFunction等引用类型均返回"object"
  • instanceof:用于检测引用类型,对基础类型无效(需通过包装对象间接使用)。
  • Object.prototype.toString.call():最可靠的检测方法,能准确区分所有类型。例如:
    console.log(Object.prototype.toString.call(null)); // "[object Null]"  
    console.log(Object.prototype.toString.call(Symbol(1))); // "[object Symbol]"  
    

理解基础数据类型的特性是JavaScript编程的基石,尤其在类型转换、相等比较(如=====的区别)和内存管理中至关重要。例如,nullundefined虽都表示“无”,但前者是人为设空,后者是未初始化状态;SymbolBigInt的引入则解决了早期版本中对象属性冲突和大数精度的问题。

闭包的概念、作用及应用场景

闭包(Closure)是JavaScript中函数与词法作用域绑定的组合,允许函数访问并操作其词法作用域之外的变量。简单来说,当内部函数在外部被调用时,会记住定义时所在的词法作用域,即使外部函数已执行完毕,内部函数仍可访问外部函数的变量。闭包的核心在于“函数嵌套”和“作用域链的保持”,其机制与内存管理密切相关。

闭包的概念解析

闭包的形成需满足两个条件:

  1. 函数嵌套:内部函数在外部函数内定义。
  2. 内部函数引用外部变量:内部函数使用了外部函数作用域中的变量或参数。
    此时,外部函数执行完毕后,其作用域不会被垃圾回收机制销毁,因为内部函数的闭包仍持有对该作用域的引用。例如:

function outer() {  
  let count = 0;  
  function inner() {  
    count++; // 引用外部函数的变量count  
    console.log(count);  
  }  
  return inner; // 返回内部函数,形成闭包  
}  
const counter = outer();  
counter(); // 输出 1  
counter(); // 输出 2  

在上述示例中,inner函数形成闭包,每次调用counter时,都会访问并修改outer作用域中的count变量,体现了闭包对外部变量的持久引用。

闭包的主要作用
  1. 封装变量,实现数据私有化
    闭包可将变量限制在函数内部,外部无法直接访问,通过暴露特定方法间接操作变量,实现封装效果。这是JavaScript中实现“私有属性”的常见方式,例如模块模式(Module Pattern):

    const counterModule = (function() {  
      let count = 0;  
      return {  
        increment() { count++; },  
        getCount() { return count; }  
      };  
    })();  
    counterModule.increment();  
    console.log(counterModule.getCount()); // 输出 1(无法直接访问count)  
    
  2. 保存状态,实现记忆功能
    闭包能记住函数调用时的状态,常用于需要“记忆”前一次操作结果的场景。例如,防抖函数(Debounce)通过闭包保存定时器ID,避免频繁触发事件:

    function debounce(fn, delay) {  
      let timer = null;  
      return function() {  
        const context = this;  
        const args = arguments;  
        clearTimeout(timer);  
        timer = setTimeout(() => {  
          fn.apply(context, args);  
        }, delay);  
      };  
    }  
    
  3. 柯里化(Currying)和函数工厂
    通过闭包将多参数函数转换为单参数函数链,逐步处理参数。例如,实现一个加法函数工厂:

    function add(x) {  
      return function(y) {  
        return x + y;  
      };  
    }  
    const add5 = add(5);  
    console.log(add5(3)); // 输出 8(闭包保存了x=5)  
    
闭包的典型应用场景
  • 模块模式与私有属性:在类(Class)语法普及前,闭包是实现私有成员的主要方式,即使在ES6类中,闭包仍可用于补充私有字段(如WeakMap)。

  • 事件处理函数:在循环中绑定事件时,闭包可保存当前迭代变量的值,避免所有回调共享同一个变量引用。例如:

    for (let i = 0; i < 3; i++) {  
      button[i].addEventListener("click", function() {  
        console.log(i); // 正确输出0、1、2(let声明的i形成闭包)  
      });  
    }  
    
     

    (若用var声明i,需借助闭包函数或let块级作用域解决)

  • 缓存机制:通过闭包保存计算结果,避免重复运算。例如,记忆函数(Memoization):

    function memoize(fn) {  
      const cache = {};  
      return function(n) {  
        if (n in cache) return cache[n];  
        const result = fn(n);  
        cache[n] = result;  
        return result;  
      };  
    }  
    
  • 定时器与异步操作:在定时器回调中保持对外部变量的引用,例如动画计数器或异步数据处理。

闭包的注意事项
  • 内存泄漏风险:闭包会延长外部作用域的生命周期,若不当使用(如循环中创建大量闭包),可能导致内存占用过高。需及时释放不再使用的闭包引用(如设置为null)。
  • 变量作用域混淆:嵌套层级过深时,需注意变量的遮蔽问题(如内部函数与外部函数存在同名变量)。
  • 性能影响:过度使用闭包可能增加函数调用开销,尤其在高频操作场景中需谨慎评估。

闭包是JavaScript的核心特性之一,理解其原理有助于编写模块化、封装性强的代码,但需合理控制使用场景,避免滥用带来的性能和内存问题。

原型和原型链的原理

原型(Prototype)和原型链(Prototype Chain)是JavaScript实现继承的核心机制,基于对象间的引用关系形成层次化的属性访问路径。理解这一机制需从构造函数、原型对象和实例的关系入手。

原型的基本概念

每个函数(包括构造函数)都有一个prototype属性,指向该函数的原型对象(Prototype Object)。原型对象默认包含一个constructor属性,指向构造函数本身。当使用new关键字调用构造函数创建实例时,实例的[[Prototype]](内部属性,可通过__proto__访问)会指向构造函数的原型对象。例如:

function Person(name) {  
  this.name = name;  
}  
Person.prototype.age = 30; // 向原型对象添加属性  
const alice = new Person("Alice");  
console.log(alice.age); // 输出 30(通过原型链访问)  

在此示例中,alice实例本身没有age属性,JavaScript引擎会沿alice.__proto__(即Person.prototype)查找,找到后返回该值。

原型链的工作原理

当访问对象的属性或方法时,引擎先检查对象自身是否存在该属性:

  1. 若存在,直接返回;
  2. 若不存在,沿__proto__链向上查找,直到到达Object.prototype
  3. 若最终仍未找到,返回undefined
    原型链的终点是Object.prototype,其__proto__null,形成链式结构。例如:

function Animal() {  
  this.type = "animal";  
}  
Animal.prototype.speak = function() { console.log(""); };  

function Dog() {  
  Animal.call(this); // 借用构造函数继承属性  
  this.breed = "husky";  
}  
// 原型链继承:设置Dog.prototype为Animal.prototype的实例  
Dog.prototype = Object.create(Animal.prototype);  
Dog.prototype.constructor = Dog; // 修正constructor指向  

const dog = new Dog();  
console.log(dog.type); // 输出 "animal"(自身属性)  
console.log(dog.speak()); // 输出 ""(通过原型链访问Animal.prototype.speak)  
console.log(dog.toString()); // 输出 "[object Object]"(继承自Object.prototype)  

上述示例中,dog的原型链为:dog.__proto__ → Dog.prototype → Animal.prototype → Object.prototype → null

关键概念辨析
  • prototype vs __proto__

    • prototype是函数特有的属性,指向原型对象,用于定义构造函数创建的实例的共享属性和方法。
    • __proto__是对象的内部属性,指向其构造函数的原型对象,是原型链的实际载体。ES6规范建议使用Object.getPrototypeOf()替代__proto__获取原型。
  • 构造函数、实例、原型对象的关系

    构造函数(Person)  
      ↓ (prototype属性)  
    原型对象(Person.prototype)  
      ↓ (constructor属性)  
    构造函数 ←───── 原型对象.constructor  
      ↑ (new操作创建)  
    实例(alice)  
      ↓ (__proto__属性)  
    实例.__proto__ === 原型对象  
    
  • 原型链继承的优缺点

    • 优点:共享原型对象的方法和属性,减少内存占用;通过原型链实现多层继承。
    • 缺点:原型对象的引用类型属性会被所有实例共享,可能导致意外修改;创建子类型实例时,无法向父类型构造函数传递参数。
原型链的实际应用
  • 内置对象的继承关系
    JavaScript内置对象(如ArrayString)均通过原型链继承自Object。例如:

    const arr = [1, 2, 3];  
    console.log(arr instanceof Array); // true  
    console.log(arr instanceof Object); // true(Array.prototype.__proto__ === Object.prototype)  
    
     

    arr的原型链为:Array实例 → Array.prototype → Object.prototype → null

  • 自定义继承逻辑
    在ES6类语法中,class本质是原型链继承的语法糖。例如:

    class Animal {  
      constructor(type) { this.type = type; }  
      speak() { console.log(""); }  
    }  
    class Dog extends Animal {  
      constructor(breed) {  
        super("animal"); // 调用父类构造函数  
        this.breed = breed;  
      }  
    }  
    
     

    上述代码等价于通过原型链实现的继承,Dog.prototype.__proto__ === Animal.prototype

注意事项
  • 避免直接修改原型对象:若直接为构造函数的prototype赋值新对象,需手动修正constructor属性,否则会导致原型链断裂。
  • 属性遮蔽(Property Shadowing):若实例自身属性与原型属性同名,实例属性会覆盖原型属性,可通过delete操作删除实例属性以恢复原型链访问。
  • 性能影响:原型链过长会导致属性查找变慢,需合理设计继承层级,避免过深的链结构。

原型和原型链是JavaScript实现“基于原型的继承”(Prototype-based Inheritance)的核心,与类式继承(Class-based Inheritance)不同,其通过对象间的直接引用形成层次关系,灵活且轻量,但需注意共享属性的副作用和原型链的性能问题。

React.memo的作用与useEffect的区别

React.memo是用于优化组件性能的高阶组件,其核心作用是缓存函数组件的渲染结果,避免在props未发生变化时重复执行渲染过程。当组件的props没有改变时,React会跳过该组件的渲染,直接使用上一次的结果,这对于频繁更新的父组件中包含的子组件尤为重要,能有效减少不必要的重绘。需要注意的是,React.memo仅对props进行浅比较,若props中包含对象或函数,可能需要通过自定义比较函数(第二个参数)来控制是否重新渲染。

而useEffect是React提供的副作用钩子,用于处理组件渲染后的副作用操作,如数据获取、事件监听、定时器设置等。它的执行时机是在组件渲染完成后(浏览器完成布局和绘制之后),可以通过依赖项数组控制副作用的触发条件。若依赖项数组为空,副作用仅在组件挂载和卸载时执行;若依赖项发生变化,则会重新执行副作用。

两者的本质区别在于:React.memo关注组件渲染的性能优化,通过避免无变化的渲染提升效率;useEffect关注组件渲染后的副作用管理,解决数据同步和异步操作的问题。前者作用于渲染阶段,后者作用于副作用执行阶段,二者解决不同层面的问题,可结合使用以优化应用性能。

React 18 的主要更新内容

React 18引入了多项重要更新,核心变化围绕并发模式自动批处理展开,旨在提升应用的响应性和用户体验。以下是关键更新点:

  1. 并发渲染(Concurrent Rendering)
    并发模式是React 18的底层新特性,允许渲染过程被中断和恢复,优先处理紧急任务(如用户输入),避免长任务阻塞主线程。通过startTransition标记非紧急更新(如列表过滤),使界面在更新时仍保持交互性,解决了传统同步渲染可能导致的卡顿问题。

  2. 自动批处理(Automatic Batching)
    在React 18之前,只有在React事件处理函数中触发的状态更新会被自动批处理(合并为一次渲染),而在 setTimeout、Promise、原生事件等场景中需手动调用batchUpdate。新版本中,所有更新都会自动批处理,无论触发环境如何,减少了不必要的重绘次数,提升更新效率。

  3. 新的Root API:createRoot
    取代旧版的ReactDOM.render,使用createRoot创建根节点,支持并发模式和新特性。例如:

    import { createRoot } from 'react-dom/client';  
    const root = createRoot(document.getElementById('root'));  
    root.render(<App />);  
    
  4. 新的Hook:useTransition
    配合并发模式,用于区分紧急更新和非紧急更新。useTransition返回isPending状态,可在界面展示加载状态(如骨架屏),提升用户体验:

    const [isPending, startTransition] = useTransition();  
    const handleSearch = (value) => {  
      startTransition(() => setSearchValue(value));  
    };  
    
  5. 新的Hook:useDeferredValue
    延迟处理非紧急值,避免其变化导致的过度渲染。当父组件状态频繁更新时,子组件中依赖该状态的非紧急值可通过useDeferredValue延迟计算,保持界面流畅。

  6. SSR优化:流式渲染与悬念(Streaming SSR & Suspense)
    服务端渲染支持流式输出,配合Suspense组件实现渐进式渲染,先输出可见内容,异步加载的数据通过悬念状态逐步填充,提升首屏加载速度。

此外,React 18还包含一些辅助更新,如useId用于生成跨服务端和客户端的唯一ID,避免hydration冲突;useSyncExternalStore优化外部状态管理库(如Redux)的订阅机制等。这些变化共同推动React向更高效、更灵活的应用开发框架演进。

为什么不能在条件判断中调用 Hooks?Hooks 的规则是什么?

在React中,禁止在条件判断中调用Hooks,核心原因是为了保证Hook调用顺序的稳定性,这是由React内部的Hook链表机制决定的。

React通过维护一个Hook链表来管理组件中的Hook状态,每次组件渲染时,会按照Hook的调用顺序依次读取或更新对应的状态。如果Hook出现在条件判断中,可能导致不同渲染周期中Hook的调用顺序不一致。例如:

function Component() {  
  if (condition) {  
    useState(0); // 第一次渲染时调用  
  } else {  
    useState(1); // 第二次渲染时条件不成立,跳过调用  
  }  
  // 后续Hook调用顺序可能与之前不一致,导致状态错乱  
}  

上述代码中,两次渲染的Hook调用次数和顺序不同,会导致React无法正确匹配状态,引发不可预测的bug。因此,React强制要求Hook必须在组件顶层作用域按顺序调用,确保每次渲染时Hook的调用顺序一致,从而保证状态的正确绑定。

Hooks 的规则(即“Hook 规则”)

React通过两条规则确保Hook的正确使用,可概括为“两个必须”:

  1. 必须在顶层作用域调用Hook

    • 禁止在循环、条件判断或嵌套函数中调用Hook,只能在组件函数的最外层或自定义Hook的最外层调用。这样能保证Hook的调用顺序在每次渲染时保持一致,避免因逻辑分支导致顺序变化。
  2. 必须在React函数中调用Hook

    • 只能在React组件函数或自定义Hook中调用Hook,不能在普通JavaScript函数中调用(除非是自定义Hook内部)。这是为了确保Hook能正确访问React的状态管理机制,如组件的上下文和生命周期。

违反这两条规则会导致React无法正确管理Hook状态,出现“Invalid hook call”等错误。遵循规则的本质是让React能够通过固定的调用顺序建立Hook与状态的映射关系,从而实现状态的正确持久化和更新。

自定义Hook(如useMyHook)本质上是Hook的组合,同样需要遵守上述规则,即在其内部的顶层作用域调用其他Hook,确保顺序稳定。通过严格遵循规则,开发者可以安全地使用Hook实现组件逻辑复用和状态管理,同时避免底层机制的冲突。

Vue 的路由模式(hash和history)区别,通常如何选择?

Vue Router支持两种主要路由模式:hash模式和history模式,二者的核心区别在于URL的表现形式底层实现原理,适用于不同的应用场景。

1. hash模式(默认模式)
  • URL结构:URL中包含#符号,如http://example.com/#/home#后的部分为哈希值(hash)。
  • 原理:基于浏览器的hashchange事件,通过监听URL中哈希值的变化来切换路由。哈希值的变化不会触发浏览器重新请求服务器,而是由前端路由直接处理。
  • 优点
    • 兼容性强,支持所有主流浏览器(包括IE8+)。
    • 无需服务端配置,直接使用前端路由即可,适合快速开发或静态站点。
  • 缺点
    • URL中包含#符号,不够美观,可能影响用户体验或SEO(搜索引擎通常会忽略哈希值)。
    • 哈希值属于URL的锚点,与页面内跳转的锚点功能冲突,需额外处理。
2. history模式
  • URL结构:URL为标准的路径形式,如http://example.com/home,无#符号。
  • 原理:基于HTML5的History APIpushStatereplaceState方法),通过修改浏览器的历史记录来实现路由切换。路由变化时,URL会更新,但不会触发浏览器刷新,需前端通过popstate事件监听路由变化。
  • 优点
    • URL更简洁美观,符合传统Web开发习惯,有利于SEO(搜索引擎可正确解析URL路径)。
    • 支持更丰富的路由操作,如修改历史记录的状态对象。
  • 缺点
    • 兼容性依赖浏览器是否支持History API(现代浏览器基本支持,IE9及以下不支持)。
    • 需要服务端配合配置单页应用(SPA)的 fallback 路由,即当用户直接访问某一路径(如http://example.com/home)时,服务端需返回应用的首页(如index.html),否则会返回404错误。
选择策略
  • 优先使用history模式的场景
    • 应用需要良好的SEO,如内容型网站、博客等。
    • 团队有服务端配置能力,可处理SPA的路由 fallback(如在Node.js、Nginx中配置所有路径指向index.html)。
    • 使用现代浏览器,无需兼容老旧浏览器(如IE9及以下)。
  • 选择hash模式的场景
    • 快速原型开发或无需SEO的内部系统、工具类应用。
    • 无法配置服务端路由,或需要兼容低版本浏览器。
    • 避免与服务端路由冲突,如服务端已占用某些路径时,哈希值可作为前端路由的“隔离层”。
配置示例

在Vue Router中切换模式的方式如下:

// hash模式(默认,无需额外配置)  
const router = new VueRouter({  
  mode: 'hash',  
  routes: [...]  
});  

// history模式(需配置mode和服务端 fallback)  
const router = new VueRouter({  
  mode: 'history',  
  routes: [...]  
});  

服务端配置示例(以Nginx为例):

location / {  
  try_files $uri $uri/ /index.html;  
}  

Vue2 中如何通过Object.defineProperty实现双向数据绑定?数组的响应式如何处理?

在Vue2中,双向数据绑定的核心是通过数据劫持结合发布-订阅模式实现的,其中Object.defineProperty是实现数据劫持的关键方法。以下是具体实现原理和过程:

一、基于Object.defineProperty的双向数据绑定
  1. 数据劫持:将data对象转化为响应式对象
    Vue通过Observer类遍历数据对象的所有属性,使用Object.defineProperty为每个属性设置gettersetter,从而监听数据的读取和修改。例如:

    function Observer(data) {  
      if (!data || typeof data !== 'object') return;  
      Object.keys(data).forEach(key => {  
        this.defineReactive(data, key, data[key]);  
      });  
    }  
    Observer.prototype.defineReactive = function(data, key, value) {  
      const that = this;  
      // 递归处理嵌套对象  
      new Observer(value);  
      Object.defineProperty(data, key, {  
        enumerable: true,  
        configurable: true,  
        get() {  
          // 依赖收集:记录当前组件需要依赖该属性  
          // 例如将当前组件的watcher添加到属性的依赖列表中  
          track(key);  
          return value;  
        },  
        set(newVal) {  
          if (newVal === value) return;  
          value = newVal;  
          // 数据更新时通知所有依赖该属性的组件重新渲染  
          that.notify(key);  
        }  
      });  
    };  
    
  2. 发布-订阅模式:依赖收集与通知更新

    • 依赖收集(getter阶段):当组件渲染时读取数据属性(如{{ message }}),触发getter,此时将当前组件的Watcher(订阅者)添加到该属性的依赖列表(dep)中。
    • 通知更新(setter阶段):当数据属性被修改时,触发setter,遍历依赖列表中的所有Watcher,调用其update方法,触发组件重新渲染。
  3. 指令解析与视图更新
    Vue的编译器会将模板中的指令(如v-model{{ }})解析为对应的Watcher,建立视图与数据的关联。当数据变化时,Watcher会根据新值更新DOM,实现双向绑定。

二、数组的响应式处理

由于Object.defineProperty无法直接监听数组索引和长度的变化,Vue2对数组的响应式处理采用了重写数组原型方法的方式:

  1. 数组方法劫持
    Vue通过继承原生数组的prototype,重写了7个会改变数组自身的方法:pushpopshiftunshiftsplicesortreverse。例如:

    const arrayProto = Array.prototype;  
    export const arrayMethods = Object.create(arrayProto);  
    ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {  
      arrayMethods[method] = function(...args) {  
        // 调用原生方法获取返回值  
        const result = arrayProto[method].apply(this, args);  
        // 处理新增元素(如有),使其变为响应式  
        const ob = this.__ob__;  
        let inserted;  
        switch (method) {  
          case 'push':  
          case 'unshift':  
            inserted = args;  
            break;  
          case 'splice':  
            inserted = args.slice(2);  
            break;  
        }  
        if (inserted) ob.observeArray(inserted);  
        // 通知依赖更新  
        ob.dep.notify();  
        return result;  
      };  
    });  
    
  2. 响应式数组的特性

    • 通过重写的数组方法修改数组时(如arr.push(1)),会触发依赖通知,更新视图。
    • 直接通过索引修改数组(如arr[0] = 'new')或修改长度(如arr.length = 0不会触发响应式更新,因为未调用被劫持的方法。此时需使用Vue.set或数组的splice方法手动触发更新:
      // 正确方式:通过Vue.set或splice修改  
      Vue.set(arr, index, newValue);  
      arr.splice(index, 1, newValue);  
      
  3. 数组遍历的限制

    • 使用Object.keysfor...in遍历数组时,无法检测到索引的新增或删除,需通过Vue.set或重写的数组方法操作。
    • 数组的length属性同样通过重写的方法(如splicepush)触发更新,直接赋值arr.length = 0不会触发响应。
三、总结

Vue2的双向数据绑定通过Object.defineProperty劫持对象属性的读写,结合WatcherDep实现依赖收集与更新通知,而数组则通过重写原型方法实现响应式。这种机制在处理对象时较为灵活,但存在以下局限:

  • 无法监听对象属性的新增或删除(需通过Vue.setthis.$set处理)。
  • 数组索引和长度的直接修改无法触发更新,需使用特定方法。
    Vue3中引入了Proxy替代Object.defineProperty,解决了上述问题,实现了更全面的响应式机制。

Vue2 和 Vue3 的主要区别

Vue2 与 Vue3 的差异体现在多个技术维度,深刻影响着开发体验与应用性能。

响应式原理的革新是核心区别之一。Vue2 基于 Object.defineProperty 实现数据劫持,通过遍历对象属性为每个属性设置 getter/setter 来监听变化,但这种方式存在明显局限:无法直接监听数组索引变化,需通过重写数组原型方法(如 pushsplice)实现间接观测;对于对象新增或删除属性,需手动调用 Vue.set 或 this.$set 触发更新。而 Vue3 采用 Proxy 替代 Object.defineProperty,直接代理整个对象,无需预先知晓属性列表,可原生监听数组索引和对象属性的动态增减,响应式系统更高效且灵活。

组件通信与状态管理也有显著调整。Vue2 中父子组件通信依赖 props 和 $emit,非父子组件常借助中央事件总线(Event Bus)或 Vuex 实现状态共享;Vue3 则强化了组合式 API(Composition API),通过 setup 函数和 ref/reactive 组合管理状态,支持更灵活的逻辑复用,且内置的 provide/inject 机制在跨层级组件通信时比 Vue2 更稳定,减少了对全局状态管理库的依赖。

虚拟 DOM 与渲染机制的优化是 Vue3 的重要升级。Vue2 的虚拟 DOM 采用双循环对比算法,对节点的更新遍历效率较低;Vue3 引入 快速 diff 算法,通过静态标记(static)识别静态节点,避免重复渲染,同时支持碎片(Fragment)和 teleport 组件,使模板结构更灵活,无需强制包裹单一根节点。此外,Vue3 的渲染器采用 Proxy 代理的响应式依赖收集,能更精准追踪组件依赖,减少不必要的重新渲染。

TypeScript 支持方面,Vue2 对 TypeScript 的支持不够完善,组件声明需借助第三方库(如 vue-class-component),类型推断不够精准;Vue3 从底层设计上拥抱 TypeScript,setup 函数和组合式 API 天然适配类型系统,官方文档和示例均提供完整类型定义,极大提升了大型项目的开发体验。

性能与包体积的差异同样值得关注。Vue3 通过优化响应式系统、虚拟 DOM 算法和 Tree-shaking 支持,相比 Vue2 整体性能提升约 50%,初始化渲染速度更快,内存占用更少。同时,Vue3 的包体积更小,尤其在移除 IE11 支持后,代码可进一步精简,更适合现代前端项目。

生态与工具链方面,Vue3 全面支持 Vite 等新一代构建工具,利用 ES Modules 实现快速冷启动和热更新,而 Vue2 更多依赖 Webpack。在调试工具上,Vue3 的 DevTools 提供了更强大的组件树和响应式依赖追踪功能,便于开发者定位问题。

Vue 组件通信的方式(父子、非父子组件)

Vue 中组件通信的方式随组件关系和场景不同而变化,需结合具体需求选择合适方案。

父子组件通信是最常见的场景,主要通过以下方式实现:

  • props 与 $emit:父组件通过 props 向子组件传递数据,子组件通过 $emit 触发自定义事件向父组件传递回调或状态。例如,父组件定义 props: { list: Array } 传递数组,子组件通过 this.$emit('update', newData) 通知父组件数据更新。这种方式层级清晰,但多层级嵌套时需逐层传递,存在“透传”繁琐的问题。
  • 自定义事件修饰符:父组件可通过 @事件名.native 监听子组件的原生 DOM 事件,或使用 .sync 修饰符简化双向绑定语法(如 <child :title.sync="parentTitle" /> 等价于 <child :title="parentTitle" @update:title="parentTitle = $event" />)。
  • $refs:父组件通过 ref 标记获取子组件实例引用(如 <child ref="childRef" />),直接调用子组件方法或访问其属性。这种方式打破了单向数据流原则,仅适用于特殊场景(如操作子组件中的 DOM 元素),需谨慎使用。

非父子组件通信(如兄弟组件、跨层级组件)需借助更灵活的机制:

  • 中央事件总线(Event Bus):在 Vue2 中,可创建一个空 Vue 实例作为事件中心,各组件通过 $on 监听事件,$emit 触发事件。例如:
    // 创建事件总线  
    const eventBus = new Vue();  
    // 组件 A 触发事件  
    eventBus.$emit('custom-event', data);  
    // 组件 B 监听事件  
    eventBus.$on('custom-event', (data) => { /* 处理逻辑 */ });  
    

    但 Vue3 中已不推荐使用该模式,因其难以追踪事件流向,容易导致内存泄漏。
  • Vuex/Pinia 状态管理库:通过全局 store 集中管理状态,各组件通过 mapStatemapMutations 等辅助函数获取或修改状态。适用于中大型项目中复杂的状态共享场景,尤其适合跨多个层级或模块的状态同步。
  • provide/inject:这是 Vue 内置的依赖注入机制,祖先组件通过 provide 提供数据或方法,后代组件通过 inject 直接获取,无需逐层传递 props。例如:
    // 祖先组件  
    export default {  
      provide() {  
        return {  
          theme: this.currentTheme,  
          changeTheme: () => { /* 方法 */ }  
        };  
      }  
    };  
    // 后代组件  
    export default {  
      inject: ['theme', 'changeTheme']  
    };  
    

    该方式在 Vue3 组合式 API 中结合 setup 函数使用更便捷,但需注意 provide 的值若为响应式对象(如 ref 或 reactive),后代组件需解构后使用 .value 访问最新值。
  • attrs 与 listeners:父组件通过 v-bind="$attrs" 向下传递未声明的 props,子组件通过 inheritAttrs: false 配合 $attrs 接收,适用于透传属性到更底层组件的场景。v-on="$listeners" 则可传递事件监听,简化多层级事件传递。

跨组件通信的进阶方案还包括:

  • 自定义插件:封装全局方法或混入(Mixin),通过 Vue.use() 安装后,各组件可直接调用(如封装一个 $utils 对象提供通用方法)。
  • 消息订阅-发布模式(Pub/Sub):利用第三方库(如 mitt 或 tiny-emitter)实现轻量级事件通信,替代传统的事件总线,更适合现代前端项目的模块化需求。

选择通信方式时,需权衡组件耦合度、代码可维护性和项目规模:简单场景优先使用 props/$emit 或 attrs/listeners,复杂状态管理采用状态库,跨层级通信推荐 provide/inject,避免过度依赖全局模式导致逻辑混乱。

在 Vite 中如何设置插件的执行时机?

Vite 的插件系统基于 Rollup 的插件机制,插件执行时机通过 钩子函数(Hook) 和 插件配置顺序 共同控制。理解不同钩子的触发阶段及插件加载顺序,是精准控制插件行为的关键。

插件执行的生命周期钩子 分为构建阶段(Build)和开发阶段(Dev),主要钩子包括:

钩子名称 触发时机 适用场景
configResolved Vite 解析完配置后触发,此时已合并用户配置和环境变量 修改最终生效的配置(如动态调整别名、定义全局变量)
configureServer 开发服务器启动前触发,用于配置开发服务器(仅开发阶段) 添加中间件、修改服务器路由规则、处理代理配置等
resolveId 解析模块 ID 时触发(如导入语句中的路径转换) 自定义模块解析逻辑,例如处理非标准文件路径、虚拟模块(Virtual Module)
load 加载模块内容时触发,在 resolveId 之后 直接返回模块内容(如内存中的字符串),或指定跳过后续加载器(Loader)
transform 转换模块内容时触发(如代码编译、压缩、注入代码) 对 JS/TS/HTML/CSS 等文件内容进行转换,例如 Babel 转译、PostCSS 处理
buildStart 构建开始时触发(仅构建阶段) 初始化构建相关资源、清空输出目录
buildEnd 构建结束时触发(仅构建阶段) 处理构建结果、生成统计报告
closeBundle 构建打包完成,即将输出文件前触发(仅构建阶段) 修改最终生成的 bundle 文件内容,如注入哈希值、生成 manifest 文件

插件执行顺序的控制原则

  1. 插件配置顺序:在 vite.config.ts 中,插件数组的顺序决定了钩子的执行顺序。前置插件的钩子先执行,后置插件的钩子后执行。例如:

    typescript

    export default defineConfig({  
      plugins: [pluginA(), pluginB()], // pluginA 的钩子先于 pluginB 执行  
    });  
    
     

    这一规则适用于所有钩子,包括 resolveIdloadtransform 等。

  2. 钩子类型的优先级:不同类型的钩子有固定的执行阶段,顺序如下:

    • 插件注册阶段configResolved(全局配置解析完成)
    • 开发服务器阶段configureServer(仅开发模式)
    • 模块解析阶段resolveId → load → transform(按插件顺序依次调用)
    • 构建阶段buildStart → 模块处理(同上) → buildEnd → closeBundle
  3. 特殊钩子的执行范围

    • resolveId 和 load 钩子按模块逐个触发,每个模块的解析过程中,所有插件的 resolveId 依次执行,直到某个插件返回有效 ID;若所有插件均未处理,则进入 Vite 内置解析器。
    • transform 钩子可针对特定文件类型(如 *.js*.vue)触发,通过 enforce 选项控制插件是作为 预处理插件(pre) 还是 后处理插件(post) 执行。例如:

      typescript

      export default defineConfig({  
        plugins: [  
          {  
            name: 'pre-plugin',  
            enforce: 'pre', // 作为预处理插件,先于其他插件执行 transform  
            transform(code, id) { /* 处理逻辑 */ }  
          },  
          {  
            name: 'post-plugin',  
            enforce: 'post', // 作为后处理插件,晚于其他插件执行 transform  
            transform(code, id) { /* 处理逻辑 */ }  
          }  
        ]  
      });  
      

典型应用场景示例

  • 修改开发服务器配置:在 configureServer 中添加中间件,实现自定义路由响应:

    typescript

    import { defineConfig } from 'vite';  
    export default defineConfig({  
      plugins: [  
        {  
          name: 'dev-middleware',  
          configureServer(server) {  
            server.middlewares.use((req, res, next) => {  
              if (req.url === '/custom-path') {  
                res.end('Custom response from middleware');  
              } else {  
                next();  
              }  
            });  
          }  
        }  
      ]  
    });  
    
  • 在构建结束后生成文件:利用 buildEnd 钩子生成版本号文件:

    typescript

    import { writeFile } from 'fs/promises';  
    export default defineConfig({  
      plugins: [  
        {  
          name: 'generate-version',  
          async buildEnd() {  
            await writeFile('dist/VERSION', new Date().toISOString());  
          }  
        }  
      ]  
    });  
    
  • 处理虚拟模块:通过 resolveId 和 load 钩子创建不存在于文件系统的模块(如动态获取环境变量):

    typescript

    export default defineConfig({  
      plugins: [  
        {  
          name: 'virtual-env',  
          resolveId(id) {  
            if (id === 'virtual:env') {  
              return id; // 标识虚拟模块  
            }  
          },  
          load(id) {  
            if (id === 'virtual:env') {  
              return `export const env = ${JSON.stringify(process.env)}`;  
            }  
          }  
        }  
      ]  
    });  
    

需要注意的是,插件钩子的执行顺序可能影响最终构建结果。例如,预处理插件(enforce: 'pre')通常用于语法转译(如 TypeScript 转 JS),而后处理插件用于代码压缩或优化,若顺序颠倒可能导致压缩后的代码无法正确转译。此外,Vite 内置插件(如 @vitejs/plugin-vue)的钩子执行顺序早于用户自定义插件,因此自定义插件若需覆盖内置逻辑,需合理利用 enforce 选项或调整注册顺序。

项目中常用的性能优化方法(前端层面)

前端性能优化需从代码结构、资源加载、渲染机制、用户体验等多维度切入,结合项目实际场景选择合适策略。以下是实践中常见的优化手段:

一、减少资源加载耗时

  1. 静态资源压缩与缓存

    • 对 JS、CSS 文件启用 Gzip/Brotli 压缩,可使文件体积减少 60% 以上。在 Nginx 等服务器配置中开启压缩模块,并通过 Accept-Encoding 头协商客户端支持的压缩格式。
    • 利用 Cache-Control 强缓存策略,为不常变更的资源(如字体、图片、第三方库)设置长缓存时间(如 max-age=31536000),并通过文件名哈希(如 main.abc123.js)实现版本更新时的缓存失效。
  2. 按需加载与代码分割

    • 使用动态导入(import('./module.js'))实现路由组件或功能模块的按需加载,避免首次加载时下载全部代码。例如在 Vue/React 中配合路由懒加载:
      // Vue Router 示例  
      const Home = () => import('./views/Home.vue');  
      
    • 利用 Webpack/Vite 的 SplitChunksPlugin 自动拆分重复依赖,生成公共代码块(如 vendor.js),减少重复加载。
  3. 图片优化

    • 压缩图片体积:使用工具(如 ImageOptim、Squoosh)压缩图片,或采用 WebP/AVIF 等现代格式(同等画质下体积比 JPEG 小 30%+)。
    • 响应式图片:通过 srcset 和 sizes 属性提供不同分辨率的图片源,浏览器自动选择合适资源加载:

      预览

      <img src="small.jpg" srcset="medium.jpg 768w, large.jpg 1024w" sizes="(max-width: 768px) 100vw, 50vw">  
      
    • 占位图与懒加载:对长列表中的图片使用 loading="lazy" 延迟加载,或通过 CSS 渐变、低分辨率占位图(LQIP)提升首屏渲染速度。

二、优化渲染性能

  1. 减少 DOM 操作与重排重绘

    • 批量更新 DOM:将多次 DOM 修改合并为一次操作(如先修改元素属性,再添加到文档中),或使用 DocumentFragment 缓存节点。
    • 避免频繁访问会触发回流的属性(如 offsetWidthgetBoundingClientRect),若需多次读取,可先缓存值。
  2. 虚拟列表与分页

    • 对于大数据量列表,采用 虚拟列表(Virtual Scrolling) 技术,仅渲染可见区域内的项目,通过计算偏移量模拟滚动效果。例如 React 中的 react-window、Vue 中的 vue-virtual-scroller
    • 对表格类数据使用分页加载,每次请求少量数据(如每页 20 条),避免一次性渲染数千条记录导致浏览器卡顿。
  3. CSS 性能优化

    • 避免使用通配符选择器(*)、复杂选择器(如 div > span a),减少 CSS 匹配计算耗时。
    • 将动画元素脱离文档流,使用 will-change: transform 或 transform: translateZ(0) 开启硬件加速,减少重绘区域。

三、逻辑优化与内存管理

  1. 防抖与节流

    • 对高频触发的事件(如 resizescrollinput)使用防抖(Debounce)或节流(Throttle)函数,减少事件处理函数的执行频率。例如,搜索框输入时延迟 300ms 发起请求:
      function debounce(fn, delay) {  
        let timer;  
        return function (...args) {  
          clearTimeout(timer);  
          timer = setTimeout(() => fn.apply(this, args), delay);  
        };  
      }  
      inputElement.addEventListener('input', debounce(searchHandler, 300));  
      
  2. 避免内存泄漏

    • 手动移除事件监听器:在组件卸载时(如 Vue 的 beforeUnmount、React 的 useEffect 清理函数),清除通过 addEventListener 添加的监听器,避免闭包引用导致的内存无法释放。
    • 谨慎使用全局变量:避免在全局作用域中存储大量临时数据,及时释放不再使用的定时器(clearTimeout/clearInterval)和 DOM 引用。
  3. 优化条件渲染

    • 在 Vue/React 中,通过 v-if/v-else(Vue)或条件组件(React)实现按需渲染,避免渲染不可见的 DOM 节点。对于列表渲染,使用 key 属性确保虚拟 DOM diff 算法准确识别节点,减少不必要的更新。

四、网络与服务端优化

  1. CDN 加速

    • 将第三方库(如 Vue、React、Lodash)托管到 CDN,利用其全球节点降低资源加载延迟。例如在 HTML 中直接引入 CDN 版本:

      预览

      <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>  
      
  2. 服务端渲染(SSR)与静态站点生成(SSG)

    • 对于内容型网站(如博客、新闻站点),使用 SSR(如 Nuxt.js、Next.js)或 SSG 提前生成 HTML 页面,减少客户端首屏渲染时间,同时提升搜索引擎优化(SEO)效果。
  3. HTTP/2 与 HTTP/3 协议

    • 启用 HTTP/2 支持多路复用(Multiplexing)和头部压缩(HPACK),减少 TCP 连接数和请求延迟;若服务器支持,升级至 HTTP/3(基于 QUIC 协议),进一步降低延迟和丢包率。

五、工具与自动化优化

  1. ESLint 与 Prettier

    • 通过代码规范工具强制团队遵循性能最佳实践(如禁止使用 eval、避免不必要的嵌套循环),并自动格式化代码,提升可维护性。
  2. 性能分析工具

    • 使用浏览器开发者工具(如 Chrome DevTools 的 Performance 面板)录制性能日志,定位长任务(Long Task)、频繁重绘区域和内存泄漏点。
    • 借助 Lighthouse、WebPageTest 等工具生成性能报告,获取具体优化建议(如“消除 render-blocking JS/CSS”、“减少主线程工作”)。

实际优化中需遵循 “先测量后优化” 的原则,避免过度优化导致代码复杂度上升。例如,首屏加载优化优先处理阻塞渲染的资源,列表性能问题先通过虚拟滚动而非直接升级框架版本解决。同时,结合用户行为分析(如 Google Analytics)识别真正的性能瓶颈,确保优化投入产出比最大化。

打包优化的常见手段(如 Tree Shaking、代码分割等)

前端项目打包优化的核心目标是减少输出文件体积、提升加载速度,同时改善代码可维护性。以下是实践中常用的技术手段及其原理:

一、Tree Shaking(摇树优化)
Tree Shaking 基于 ES Modules 的静态结构特性,剔除代码中未被引用的部分(“死代码”),常用于 JS 和 CSS 优化。

  • 原理:ES Modules 的导入导出语句是静态的(编译时可分析),打包工具(如 Webpack、Vite)通过静态分析确定哪些模块、函数、变量未被使用,从而在打包时排除这些代码。
  • 使用条件
    • 项目需使用 ES Modules 语法(import/export),CommonJS(require/module.exports)因动态特性无法被 Tree Shaking 处理。
    • 打包工具需配置 sideEffects 属性告知是否存在副作用。例如,在 package.json 中声明:
      {  
        "sideEffects": [  
          "*.css", // 样式文件有副作用,需保留  
          "src/utils/*.js" // 明确有副作用的 JS 文件  
        ]  
      }  
      
  • 注意事项
    • 避免在代码中使用动态导入(import())或 eval,可能导致 Tree Shaking 失效。
    • 对 CSS 可配合 purgecss 实现样式级摇树,剔除未使用的 CSS 类(如在 Vite 中使用 @vitejs/plugin-purgecss 插件)。

二、代码分割(Code Splitting)
代码分割将打包后的代码拆分为多个 bundle,实现按需加载,减少首次加载的文件体积。

  • 动态导入(Dynamic Import)
    通过 import() 语法异步加载模块,打包工具自动生成独立的 chunk 文件。例如在路由场景中:

    // Vue Router 懒加载  
    const Home = () => import('./views/Home.vue');  
    const About = () => import('./views/About.vue');  
    
     

    打包后会生成 Home.[hash].jsAbout.[hash].js 等文件,仅在访问对应路由时加载。

  • SplitChunksPlugin(公共代码抽离)
    自动提取多个 chunk 中的公共依赖(如 Vue、React 等库),生成独立的 vendor chunk。以 Webpack 为例,默认配置下会将重复引用的第三方库拆分为 vendor.js,避免重复打包:

    // webpack.config.js  
    module.exports = {  
      optimization: {  
        splitChunks: {  
          chunks: 'all', // 对所有 chunk 应用分割  
          minSize: 20000, // 最小体积阈值(单位:字节)  
          maxSize: 500000 // 最大体积阈值,超过则进一步拆分  
        }  
      }  
    };  
    
  • 预渲染与预加载
    对关键路由组件使用 <link rel="preload"> 提前加载资源,或通过 prefetch 提示浏览器在空闲时预下载后续可能需要的 chunk:

    预览

    <link rel="preload" href="home.chunk.js" as="script">  
    <link rel="prefetch" href="about.chunk.js" as="script">  
    

三、缩小打包目标与剔除无效代码

  1. Target 配置
    在打包工具中指定目标环境(如 browserslist),剔除低版本浏览器不必要的 polyfill。例如,若项目无需支持 IE11,可在 Vite 中配置:

    typescript

    // vite.config.ts  
    export default defineConfig({  
      build: {  
        target: 'es2020' // 生成 ES2020 语法代码,体积更小  
      }  
    });  
    
  2. 剔除调试代码

    • 生产环境下自动移除 console.logdebugger 等调试语句,可通过 Webpack 的 terser-webpack-plugin 或 Vite 的 build.minify 配置实现。
    • 对 Vue/React 组件,生产环境会自动移除开发模式下的警告代码(如 vue.runtime.global.prod.js 已剔除警告逻辑)。

四、压缩与混淆

  1. JS/CSS 压缩

    • 打包工具默认启用压缩插件,如 Webpack 使用 Terser,Vite 使用 esbuild 或 terser。压缩过程包括代码丑化(变量名缩短)、删除无效代码、合并语句等。
    • 对 CSS 可使用 cssnano 插件,实现自动前缀补全、无效规则移除、值简写等优化(如将 margin: 10px 20px 10px 20px 简写为 margin: 10px 20px)。
  2. HTML 压缩
    移除 HTML 中的注释、空白符,合并属性(如 class="a b" 保持原样但压缩空白),可通过 Webpack 的 html-webpack-plugin 或 Vite 的内置插件实现。

五、缓存优化与版本管理

  1. 内容哈希(Content Hash)
    在文件名中加入内容哈希值(如 main.abc123.js),当文件内容变更时哈希值改变,确保浏览器能正确获取最新资源,同时利用强缓存(Cache-Control: max-age=31536000)提升重复访问速度。

  2. 长效缓存策略

    • 将第三方库(如 vue.runtime.global.prod.js)与业务代码分离,单独打包为 vendor.[hash].js,因其更新频率低,可设置更长的缓存时间。
    • 对静态资源(如字体、图片)使用 CDN 加速,并配置 immutable 缓存策略(Cache-Control: max-age=31536000, immutable),进一步减少重复请求。

六、使用现代打包工具与插件

  1. Vite 替代 Webpack
    Vite 基于 ES Modules 实现快速冷启动,开发阶段无需打包即可直接运行代码,构建阶段使用 Esbuild 进行 JS/CSS 压缩(速度比 Terser 快 20-30 倍),尤其适合中小型项目。

  2. 按需引入组件库
    对 Element UI、Ant Design 等组件库,通过 babel-plugin-import 等插件实现按需加载,避免打包整个库。例如,在 Vue 中按需引入 Button 组件:

    // 传统全量引入  
    import { Button } from 'element-ui';  
    // 按需引入(需配合 babel 插件)  
    const Button = require('element-ui/lib/button');  
    

七、WebAssembly(Wasm)
对于计算密集型任务(如视频解码、3D 渲染),将代码编译为 Wasm 格式,可大幅提升执行速度,同时减少 JS 代码体积。例如,使用 Rust 编写算法并编译为 Wasm,通过 JS 调用:

// 加载 Wasm 模块  
const go = new Go();  
WebAssembly.instantiateStreaming(fetch('app.wasm'), go.importObject).then((result) => {  
  go.run(result.instance);  
});  

优化效果对比

优化手段 典型场景 体积减少比例 加载速度提升
Tree Shaking 剔除未使用的工具函数 10%-30% 5%-15%
代码分割 大型单页应用路由模块 20%-40% 15%-25%
Gzip 压缩 文本类资源(JS/CSS/HTML) 40%-60% 20%-30%
按需引入组件库 使用 Element UI 等大型组件库 20%-50% 10%-20%

实际操作中需结合项目类型选择优化组合:单页应用(SPA)侧重代码分割与路由懒加载,营销型网站优先使用 SSG 与静态资源压缩,复杂应用可引入 Wasm 提升性能。同时,通过打包报告(如 Webpack 的 webpack-bundle-analyzer、Vite 的 --stats 选项)可视化分析文件体积构成,精准定位优化点,避免过度优化导致开发效率下降。

前端构建工具对比:Webpack 和 Vite 的优势与适用场景

Webpack 和 Vite 是前端开发中主流的构建工具,二者设计理念和适用场景差异显著。

Webpack 的优势体现在强大的生态整合与复杂场景适配能力。作为模块化打包工具,它通过 loader 和 plugin 机制实现对多种文件类型的处理,例如用 Babel 转译 ES6+ 语法、用 Sass-loader 处理样式文件,甚至能通过 asset module 支持图片、字体等资源的打包。其核心优势在于 打包优化能力,比如代码分割(Code Splitting)可将应用拆分为多个 bundle,实现按需加载;Tree Shaking 能移除未使用的代码,减小打包体积;还支持 Source Map 生成、热更新(HMR)等高级功能。此外,Webpack 的社区生态极为成熟,拥有大量现成的插件(如 HtmlWebpackPlugin、MiniCssExtractPlugin)和解决方案,适合开发大型单页应用(SPA)、复杂的企业级项目或需要深度定制构建流程的场景,例如需要兼容旧浏览器、进行多环境打包配置等。

Vite 的优势则聚焦于开发体验的革新与轻量场景的高效性。基于 ES 模块(ES Modules)原生支持,Vite 在开发阶段无需预先打包依赖,直接通过浏览器动态导入(import)方式加载第三方模块,避免了 Webpack 冗长的打包过程,实现 极速冷启动。其内置的 HMR 机制响应速度更快,尤其在 Vue、React 等框架中能精准更新组件,减少页面刷新带来的状态丢失。生产环境下,Vite 基于 Rollup 进行打包,同样支持 Tree Shaking 和代码分割,但配置更为简洁,开箱即用。Vite 更适合 中小型项目、快速原型开发或现代浏览器环境下的应用,尤其是使用 Vue 3、React 等新框架的项目,能显著提升开发效率。对于不需要复杂构建流程的场景,如静态网站生成(SSG)、单页应用的轻量化开发,Vite 的优势更为突出。

适用场景对比可通过以下维度理解:

  • 项目规模:Webpack 适合大型复杂项目,Vite 更适合中小型项目或轻量级应用。
  • 开发体验:Vite 在开发阶段启动速度和热更新效率上优势明显,Webpack 则需通过配置优化才能达到类似效果。
  • 兼容性需求:若项目需要兼容低版本浏览器(如 IE),Webpack 可通过 polyfill 等方案处理,而 Vite 更依赖现代浏览器的 ES Modules 支持。
  • 构建定制化:Webpack 的插件系统允许开发者深入干预打包流程(如自定义打包策略、优化资源加载顺序),Vite 则更强调简洁,定制化需通过插件或配置文件实现,但灵活性稍逊于 Webpack。

实际选择时,可根据项目需求权衡:若追求开发效率和现代技术栈,Vite 是更优解;若需要高度可控的构建流程或兼容复杂环境,Webpack 仍是可靠选择。随着前端技术向模块化和浏览器原生能力发展,Vite 的适用范围正逐渐扩大,但 Webpack 在生态成熟度和复杂场景处理上仍不可替代。

对 Webpack/Vite 配置的了解(如按需加载、路径修改等)

Webpack 配置要点

按需加载(Code Splitting) 是 Webpack 优化性能的核心手段之一,通过将代码拆分为多个 bundle,实现运行时按需加载。常见实现方式包括:

  • 动态导入(Dynamic Import):在代码中使用 import('./module.js') 语法,Webpack 会自动将该模块拆分为独立文件,适用于路由组件、弹窗组件等非首屏必需内容。
  • SplitChunksPlugin:通过配置 optimization.splitChunks,可将第三方依赖(如 lodash、React)或公共代码提取为共享 chunk,避免重复打包。例如:
    // webpack.config.js  
    module.exports = {  
      optimization: {  
        splitChunks: {  
          chunks: 'all',  
          minSize: 20000,  
          maxSize: 300000,  
        }  
      }  
    };  
    

    上述配置会将所有模块中超过 20KB 且小于 300KB 的代码块进行拆分,并优先提取公共依赖。

路径修改 通常通过 resolve 配置实现,用于简化模块导入路径。例如,将 src 目录设置为别名 @

module.exports = {  
  resolve: {  
    alias: {  
      '@': path.resolve(__dirname, 'src'),  
    }  
  }  
};  

这样在代码中可直接使用 import Component from '@/components/Component.vue',提升开发效率。

Vite 配置要点

Vite 的配置基于 vite.config.ts,语法更简洁,且内置对 ES Modules 的支持。
按需加载 在 Vite 中同样通过动态导入实现,原理与 Webpack 类似,但生产环境打包基于 Rollup,默认启用 Tree Shaking 和代码分割。对于第三方库,Vite 会自动将其拆分为独立 chunk,无需额外配置。

路径修改 通过 resolve.alias 实现,例如:

// vite.config.ts  
import { defineConfig } from 'vite';  
import path from 'path';  

export default defineConfig({  
  resolve: {  
    alias: {  
      '@': path.resolve(__dirname, 'src'),  
    }  
  }  
});  

此外,Vite 支持直接在 tsconfig.json 中配置路径别名(需启用 paths 和 baseUrl),无需在 Vite 配置中重复设置,这一点比 Webpack 更便捷。

其他常见配置对比
  • 环境变量:Webpack 使用 dotenv 或 DefinePlugin 注入环境变量,Vite 内置 import.meta.env,可通过 .env 文件直接配置。
  • 静态资源处理:Webpack 需通过 file-loaderurl-loader 等 loader 处理图片、字体,Vite 原生支持直接导入此类文件,并自动进行 base64 转换(小于特定阈值时)。
  • 开发服务器:Vite 的开发服务器基于 esbuild,启动速度极快,且支持实时编译;Webpack 需配置 webpack-dev-server 或 webpack-dev-middleware,启动速度相对较慢。
配置原则与最佳实践
  • 按需配置:Webpack 的灵活性意味着过度配置可能导致性能下降,应优先使用社区成熟插件(如 webpack-bundle-analyzer 分析打包体积),避免重复造轮子。
  • 拥抱原生能力:Vite 的设计理念是尽可能利用浏览器原生功能(如 ES Modules、Fetch),配置时应避免退回到传统打包思维,例如尽量使用动态导入而非同步加载大模块。
  • 性能优化优先级:在生产环境中,两者均需关注打包速度和产物体积。Webpack 可通过 thread-loader 开启多线程编译,Vite 则依赖 esbuild 的并行压缩能力,无需额外配置。

Babel 解析 JS 源码的过程,生成 AST 之前有哪些步骤?

Babel 作为 JavaScript 编译器,其核心功能是将高版本 JS 代码转换为向后兼容的低版本代码,这一过程基于对代码的解析、转换和生成。在生成抽象语法树(AST)之前,Babel 会执行 词法分析(Lexical Analysis) 和 语法分析(Syntactic Analysis) 两个关键步骤,这两个阶段共同构成了编译器的前端(Frontend)部分。

1. 词法分析(Lexical Analysis)

词法分析是编译器处理代码的第一步,其目标是将连续的字符流转换为有意义的 词法单元(Token)。Babel 使用 正则表达式 对输入的 JS 源码进行逐字符扫描,识别出关键字、标识符、操作符、字面量(如字符串、数字)、标点符号等基本单元。例如,对于代码 let name = "Alice";,词法分析会将其分解为以下 Token:

  • let:关键字(Keyword)
  • name:标识符(Identifier)
  • =:赋值操作符(Punctuator)
  • "Alice":字符串字面量(StringLiteral)
  • ;:分号(Punctuator)

词法分析过程中会忽略空格、换行符等无关字符,并处理注释(将其过滤或标记为特定 Token)。这一步骤的输出是一个有序的 Token 列表,为后续语法分析提供结构化的输入。

2. 语法分析(Syntactic Analysis)

语法分析阶段基于词法分析生成的 Token 列表,按照 JS 语言的语法规则(如 ECMA-262 规范)进行解析,构建 抽象语法树(AST)。语法分析器(Parser)会通过递归下降算法(Recursive Descent Parsing)对 Token 序列进行匹配,验证代码是否符合语法规范,并将 Token 组合成具有层级结构的节点。

以代码 const sum = (a, b) => a + b; 为例,语法分析过程会:

  • 识别 const 为声明关键字,创建变量声明节点(VariableDeclaration)。
  • 解析 sum 为标识符,作为变量名(id)。
  • 处理赋值表达式右侧的函数表达式,创建函数表达式节点(FunctionExpression),包含参数列表(Params)和函数体(Body)。
  • 函数体中的 a + b 会被解析为二元表达式节点(BinaryExpression),操作符为 +,左右操作数分别为标识符 a 和 b

语法分析过程中若发现语法错误(如缺少括号、关键字拼写错误),Babel 会抛出明确的错误信息,指出错误位置和类型。例如,代码 let x = 1 + 会因缺少右操作数触发语法错误。

生成 AST 后的流程

生成 AST 后,Babel 进入 转换阶段(Transformation),通过插件机制对 AST 进行遍历和修改。例如,将箭头函数转换为普通函数、添加 polyfill 等。最后通过 代码生成阶段(Code Generation) 将修改后的 AST 转换为目标代码。

关键概念与技术细节
  • Token 的组成:每个 Token 通常包含类型(Type)、值(Value)、位置信息(Start/End Index),例如字符串字面量 Token 的类型为 StringLiteral,值为具体字符串内容。
  • 语法规则的定义:Babel 使用 @babel/parser(基于 Acorn 改造)作为解析器,其语法规则通过 babylon 语法规范定义,支持最新的 JS 特性(如 ES6+、JSX、TypeScript)。
  • 错误处理机制:语法分析器在遇到不符合规则的 Token 序列时,会尝试通过“错误恢复”(Error Recovery)机制跳过错误部分,继续解析后续代码,以提供更友好的错误提示。
与其他工具的对比
  • ESLint 的解析差异:ESLint 同样会将代码解析为 AST,但更关注代码风格和规范检查,其解析器(如 @typescript-eslint/parser)支持更严格的类型校验,而 Babel 的解析器更侧重于语法转换。
  • TypeScript 编译器(TSC)的前端:TSC 的词法分析和语法分析阶段与 Babel 类似,但会额外进行类型检查,生成包含类型信息的 AST(称为“带有类型的 AST”或“Type AST”)。

实现 Koa2 洋葱模型的原理

Koa2 的洋葱模型是其中间件机制的核心特征,因中间件执行顺序呈现“先入后出、层层包裹”的洋葱状而得名。这一模型通过 异步函数的嵌套调用 和 koa-compose 库 实现,核心原理在于中间件的执行顺序控制和上下文(Context)的共享。

核心概念与执行流程
  1. 中间件的定义:Koa2 中间件是一个接受 ctx(上下文)和 next(下一个中间件函数)参数的异步函数。next 是一个函数,调用它会触发下一个中间件的执行;若不调用 next,则流程将终止于当前中间件。

  2. 洋葱模型的执行顺序

    • 当第一个中间件(如 middleware1)被调用时,它会先执行自身逻辑(例如打印日志),然后调用 next() 触发下一个中间件(middleware2)。
    • middleware2 执行类似逻辑,调用 next() 后触发 middleware3,依此类推,直到最后一个中间件。
    • 当最后一个中间件执行完毕(即不再调用 next()),流程会沿原路返回,依次执行各中间件中 next() 之后的逻辑。

    例如,三个中间件的执行顺序如下:

    middleware1 前半部分逻辑  
    ├─> middleware2 前半部分逻辑  
    │  ├─> middleware3 前半部分逻辑  
    │  └─> middleware3 后半部分逻辑  
    └─> middleware2 后半部分逻辑  
    └─> middleware1 后半部分逻辑  
    
     

    这种“先入后出”的机制使得中间件可以在请求处理的前后(如请求进入时记录开始时间,响应返回前计算耗时)添加逻辑,形成对请求的“包裹”效果。

关键实现:koa-compose 库

Koa2 洋葱模型的核心实现依赖 koa-compose 函数,该函数负责将多个中间件组合成一个可执行的函数链。其原理是通过递归和 Promise 链来管理中间件的执行顺序,具体步骤如下:

  1. 参数校验:确保所有中间件都是函数,且符合 Koa 中间件规范(接受 ctx 和 next 参数)。
  2. 创建中间件数组:将传入的中间件转换为数组,便于遍历。
  3. 递归调用中间件:定义一个内部函数 dispatch(i),表示从第 i 个中间件开始执行。当 i 等于中间件总数时,返回一个已决议的 Promise(Promise.resolve()),表示流程结束。
  4. Promise 链的建立:每个中间件的执行结果是一个 Promise,通过 await middleware(ctx, () => dispatch(i + 1)) 实现中间件的嵌套调用。其中,() => dispatch(i + 1) 作为当前中间件的 next 函数,用于触发下一个中间件。

以下是简化的 koa-compose 实现思路(非完整代码):

function compose(middlewares) {  
  if (!Array.isArray(middlewares)) {  
    throw new Error('Middlewares must be an array!');  
  }  
  for (const fn of middlewares) {  
    if (typeof fn !== 'function') {  
      throw new Error('Middlewares must be functions!');  
    }  
  }  
  return function (ctx) {  
    let index = 0;  
    function dispatch(i) {  
      if (i >= middlewares.length) return Promise.resolve();  
      const middleware = middlewares[i];  
      try {  
        return middleware(ctx, () => dispatch(i + 1)); // 调用当前中间件,并传入 next 函数  
      } catch (err) {  
        return Promise.reject(err);  
      }  
    }  
    return dispatch(0); // 从第一个中间件开始执行  
  };  
}  

在上述逻辑中,每个中间件通过 await next() 等待下一个中间件执行完毕,从而实现前半部分逻辑在 next() 之前执行,后半部分逻辑在 next() 之后执行,形成洋葱状的包裹效果。

上下文(ctx)的共享与控制

Koa2 的 ctx 对象在所有中间件中共享,中间件可以通过修改 ctx 的属性来传递数据(如设置 ctx.state.user 存储用户信息)。由于洋葱模型的执行顺序是线性的,ctx 的状态变化会按中间件的执行顺序依次传递,确保后续中间件能访问到之前中间件修改后的上下文。

应用场景与注意事项
  • 日志记录:在请求进入时记录开始时间(中间件前半部分),在响应返回前记录结束时间和耗时(中间件后半部分)。
  • 错误处理:通过在中间件链中添加错误捕获逻辑(如 try...catch),统一处理下游中间件抛出的异常。
  • 异步操作支持:中间件可通过 async/await 处理异步逻辑,确保 next() 调用前的异步操作完成(如数据库查询)。

注意事项

  • 中间件必须按顺序注册,顺序会直接影响执行结果(如身份验证中间件应在业务逻辑中间件之前)。
  • 若中间件未调用 next(),则后续中间件不会执行,可能导致流程中断(如身份验证失败时终止请求)。

总之,Koa2 的洋葱模型通过 Promise 链和中间件的有序调度,实现了请求处理逻辑的灵活组合与分层控制,使得开发者可以轻松地在请求的不同阶段(进入、处理、响应)添加功能模块,同时保持代码的模块化和可维护性。

一个无限长的列表,如何实现前两个和后两个元素设置为红色?(JS/CSS 思路)

处理无限长列表时,直接操作所有 DOM 元素会导致性能问题,因此需通过 虚拟滚动 或 动态计算可见区域 的方式优化。对于“前两个和后两个元素设置为红色”的需求,可结合列表的索引位置和 CSS 选择器实现,以下是具体思路:

方案一:基于索引的动态样式控制(适用于有限渲染或虚拟滚动场景)

假设列表通过 JavaScript 动态生成或渲染(如使用框架的列表渲染指令),每个列表项可通过索引值判断是否为前两个或后两个元素,从而添加对应的 CSS 类。

HTML 结构

预览

<ul id="list"></ul>  
JavaScript 逻辑
  1. 生成列表数据:假设数据为无限滚动的虚拟数据(如通过页码或滚动事件加载),每个元素包含索引信息。
  2. 渲染可见区域元素:仅渲染视口内的元素(虚拟滚动),或渲染固定数量的元素(如每页 100 条),避免全量渲染。
  3. 根据索引添加样式类:对索引为 01(前两个)和最后两个元素(如总长度减 1、总长度减 2)添加 red-item 类。

function renderList(data) {  
  const list = document.getElementById('list');  
  list.innerHTML = ''; // 清空原有内容  

  const total = data.length;  
  data.forEach((item, index) => {  
    const li = document.createElement('li');  
    li.textContent = item.content;  

    // 判断是否为前两个或后两个元素  
    if (index < 2 || index >= total - 2) {  
      li.classList.add('red-item');  
    }  

    list.appendChild(li);  
  });  
}  

// 模拟无限数据(如滚动加载时调用)  
const infiniteData = Array.from({ length: 1000 }, (_, i) => ({ content: `Item ${i}` }));  
renderList(infiniteData);  
CSS 样式
.red-item {  
  color: red;  
}  

关键点

  • 虚拟滚动优化:若列表为无限滚动,需结合滚动事件计算可见区域的起始和结束索引,仅渲染该范围内的元素。例如,当用户滚动到列表底部时,加载更多数据并重新计算索引,确保前两个和后两个元素始终对应实际可见的首尾元素。
  • 动态更新样式:当数据长度变化(如新增或删除元素)时,重新调用 renderList 方法,根据新的索引重新分配样式类。
方案二:纯 CSS 实现(适用于静态列表或已知长度的列表)

若列表长度固定或可通过 CSS 动态获取(如使用伪类选择器),可借助 CSS 的 :first-child:nth-child:last-child 等选择器定位目标元素。

HTML 结构

预览

<ul>  
  <li>Item 0</li>  
  <li>Item 1</li>  
  <li>Item 2</li>  
  <!-- 中间省略无限个列表项 -->  
  <li>Item N-2</li>  
  <li>Item N-1</li>  
</ul>  
CSS 样式
/* 前两个元素 */  
ul li:first-child,  
ul li:nth-child(2) {  
  color: red;  
}  

/* 后两个元素(需已知列表总长度,或使用 JavaScript 动态设置类名) */  
/* 方式一:假设列表总长度为 N,通过类名标识 */  
ul.long-list li:nth-last-child(1),  
ul.long-list li:nth-last-child(2) {  
  color: red;  
}  

/* 方式二:使用 JavaScript 动态添加类名到最后两个元素 */  
/* 例如,给最后两个 li 添加类名 `last-two` */  
.last-two {  
  color: red;  
}  

局限性

  • CSS 无法直接获取动态无限列表的总长度,因此“后两个元素”的选择需依赖 JavaScript 预先知道数据长度或动态添加类名。
  • 对于真正的无限滚动列表(数据无限加载),纯 CSS 方案不适用,需结合 JavaScript 动态计算索引。
方案三:框架层面的优化(如 React/Vue)

在 React 或 Vue 中,可通过列表渲染指令(如 mapv-for)结合条件判断动态添加样式类,同时利用框架的虚拟 DOM 机制优化渲染性能。

React 示例
function InfiniteList({ data }) {  
  return (  
    <ul>  
      {data.map((item, index) => (  
        <li  
          key={index}  
          style={{  
            color: index < 2 || index >= data.length - 2 ? 'red' : 'inherit'  
          }}  
        >  
          {item.content}  
        </li>  
      ))}  
    </ul>  
  );  
}  
Vue 示例
<template>  
  <ul>  
    <li  
      v-for="(item, index) in data"  
      :key="index"  
      :class="{ redItem: index < 2 || index >= data.length - 2 }"  
    >  
      {{ item.content }}  
    </li>  
  </ul>  
</template>  

<style>  
.redItem {  
  color: red;  
}  
</style>  

优势

  • 框架自动处理列表渲染和更新,无需手动操作 DOM。
  • 结合虚拟滚动库(如 React Virtualized、Vue Virtual Scroller),可进一步优化无限列表的性能,确保仅渲染可见区域内的元素,同时动态计算索引以应用样式。
性能优化关键点
  1. 避免全量渲染:对于无限长列表,必须采用虚拟滚动技术,仅渲染视口内的元素(通常为可见区域上下各额外渲染若干缓冲元素),减少 DOM 节点数量。
  2. 批量更新样式:通过一次性计算所有可见元素的索引并批量添加类名,避免频繁操作 DOM(如使用 requestAnimationFrame 优化更新时机)。
  3. 利用 CSS 硬件加速:若样式涉及复杂动画,可通过 transformopacity 等属性触发 GPU 渲染,提升性能。

扫码登录的实现流程(前端部分)

扫码登录的前端实现需结合后端接口与设备交互逻辑,核心流程围绕二维码生成、状态轮询和用户授权展开。首先,前端调用后端接口获取唯一标识(如uuid),用于关联用户设备与登录请求。后端生成包含该标识的二维码数据,前端通过canvasimg标签渲染二维码,同时启动定时器轮询后端接口,检查该uuid对应的扫码状态。

轮询过程中,若后端返回“已扫码未确认”状态,前端可提示用户确认登录;若状态变为“已确认”,后端会生成用户凭证(如token)并存储,前端接收到凭证后跳转至登录成功页面,同时清除定时器。需注意轮询间隔不宜过短(通常5-10秒),避免对服务器造成压力。此外,需处理网络异常、用户取消扫码等边界情况,例如超时未操作时停止轮询并提示用户刷新二维码。

在技术实现上,可使用WebSocket替代轮询以降低延迟,当用户扫码确认后,后端通过WebSocket主动推送消息给前端,提升实时性。前端还需适配不同设备的屏幕尺寸,确保二维码清晰显示,并提供“刷新二维码”按钮用于处理过期或异常情况。安全性方面,需对uuid进行加密传输,防止中间人攻击,同时限制二维码有效期(如5分钟未操作则失效)。

开发一个组件时需要考虑哪些共性问题?(如兼容性、可维护性等)

开发组件时需从功能、性能、可维护性和用户体验等多维度综合设计。兼容性方面,需适配不同浏览器(如处理flex布局在IE中的兼容问题)、设备尺寸(响应式设计)和操作系统(如移动端点击事件与PC端差异)。可通过特性检测(如Modernizr)或 polyfill 解决 API 差异。

可维护性与扩展性要求组件结构清晰,采用单一职责原则,将逻辑、样式和模板分离。使用 props 和事件系统实现组件间通信,避免内部状态耦合。命名规范需统一(如遵循 BEM 方法论),注释和文档齐全,方便团队协作。扩展性方面,提供插槽(slot)或配置项,允许用户自定义内容或行为,例如表格组件支持自定义列渲染函数。

性能优化需关注渲染效率,避免不必要的重新渲染。对于列表类组件,可采用虚拟滚动或react-window等库优化内存占用;使用 memoization 技术缓存计算结果,减少重复计算。样式方面,避免使用全局选择器,采用作用域样式(如 CSS Modules)防止样式污染。

可用性与体验包括合理的交互反馈(如加载状态、错误提示)、键盘导航支持(符合无障碍标准)和响应速度优化。例如,按钮组件需处理焦点状态,输入框组件应提供清晰的占位提示。同时,需考虑组件的体积,通过 Tree Shaking 剔除未使用的代码,或提供按需引入机制。

测试与调试方面,需编写单元测试覆盖主要功能路径,使用 Storybook 等工具可视化展示组件状态,方便调试。错误处理机制需完善,如通过ErrorBoundary捕获组件内部错误,避免应用崩溃。

事件委托的原理及应用,如何实现一个事件类Event?(JS 代码思路)

事件委托基于浏览器事件冒泡机制,将多个子元素的事件处理函数委托给父元素统一监听,通过判断事件目标(event.target)识别具体触发事件的子元素。其优势在于减少内存占用(只需绑定一次事件)、动态元素自动支持事件(无需重复绑定),适用于列表项点击、按钮组交互等场景。

应用场景包括:动态生成的列表项点击处理(如待办事项添加删除)、批量按钮操作(如多个删除按钮共用一个父级监听器)、全局手势监听(如页面级的键盘事件处理)。需注意避免在捕获阶段使用事件委托,以免影响事件冒泡路径。

实现一个事件类Event的核心思路如下:

  1. 事件绑定方法:接收事件类型、回调函数和可选的元素选择器,若提供选择器则使用事件委托,否则直接绑定事件。
  2. 事件触发方法:模拟事件触发,遍历对应事件类型的回调函数并执行,支持传递参数。
  3. 事件解绑方法:移除已绑定的回调函数或全部事件。

代码示例(简化版):

class Event {  
  constructor() {  
    this.events = {}; // 存储事件回调  
  }  

  // 绑定事件:on('click', selector, callback) 或 on('click', callback)  
  on(type, selectorOrCallback, callback?) {  
    if (typeof selectorOrCallback === 'function') {  
      callback = selectorOrCallback;  
      selectorOrCallback = null;  
    }  
    if (!this.events[type]) this.events[type] = [];  
    this.events[type].push({ selector: selectorOrCallback, callback });  
  }  

  // 触发事件:trigger('click', event)  
  trigger(type, event) {  
    event = event || window.event;  
    const handlers = this.events[type] || [];  
    handlers.forEach(handler => {  
      if (!handler.selector) {  
        handler.callback.call(this, event);  
      } else {  
        const target = event.target || event.srcElement;  
        if (target.matches(handler.selector)) {  
          handler.callback.call(this, event, target);  
        }  
      }  
    });  
  }  

  // 解绑事件:off('click', selector, callback) 或 off('click', callback)  
  off(type, selectorOrCallback, callback?) {  
    if (!this.events[type]) return;  
    let handlers = this.events[type];  
    if (!selectorOrCallback) {  
      delete this.events[type];  
      return;  
    }  
    handlers = handlers.filter(handler => {  
      if (typeof selectorOrCallback === 'function') {  
        return handler.callback !== selectorOrCallback;  
      }  
      return (handler.selector !== selectorOrCallback || handler.callback !== callback);  
    });  
    this.events[type] = handlers;  
  }  
}  

该类支持直接绑定事件和委托绑定,通过matches方法判断目标元素是否符合选择器,实现动态元素的事件处理。使用时可实例化Event对象,绑定到具体DOM元素或全局对象(如window)上。

睡眠函数(sleep)的 JS 代码实现(使用 Promise 或 setTimeout)

睡眠函数用于暂停代码执行一段时间,在异步操作中控制执行顺序。基于Promise的实现更符合现代异步编程模型,利用async/await可使代码结构更清晰。

基于 Promise 的实现

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

使用方式:

async function demo() {  
  console.log('开始');  
  await sleep(1000); // 暂停1秒  
  console.log('结束');  
}  
demo();  

该函数通过创建一个Promise实例,在指定毫秒数后调用resolve触发后续操作。async/await的语法糖使异步代码看起来像同步流程,提升可读性。

基于 setTimeout 的回调实现(非阻塞版本):

function sleep(ms, callback) {  
  setTimeout(callback, ms);  
}  
// 使用示例  
console.log('开始');  
sleep(1000, () => {  
  console.log('结束');  
});  

此方法通过回调函数实现延迟执行,但嵌套层级过深易导致“回调地狱”,适用于简单场景。

注意事项

  • JavaScript 是单线程事件循环机制,setTimeout属于宏任务,睡眠函数不会阻塞主线程,而是将回调加入事件队列等待执行。
  • 若需精确控制延迟(如动画帧同步),可使用requestAnimationFrame配合时间戳计算,但通常setTimeout已满足大多数场景需求。
  • 避免在同步代码中使用while循环配合Date.now()实现睡眠,这会阻塞主线程导致界面卡顿,例如:
    // 错误示例(阻塞主线程)  
    function sleep(ms) {  
      const start = Date.now();  
      while (Date.now() - start < ms) {}  
    }  
    

在请求中,如何保证先请求 A 接口再请求 B 接口,并获取 A 的数据?(用 Promise/Async/Await 实现)

保证接口请求顺序的核心是利用异步操作的链式调用,通过Promise的链式调用或async/await的同步写法实现依赖关系。以下是具体实现方式:

基于 Promise 链式调用

function requestA() {  
  return fetch('/api/a').then(res => res.json());  
}  

function requestB(dataFromA) {  
  return fetch(`/api/b?param=${dataFromA.id}`).then(res => res.json());  
}  

// 调用方式  
requestA()  
  .then(dataA => {  
    console.log('A接口数据:', dataA);  
    return requestB(dataA); // 将A的数据传递给B  
  })  
  .then(dataB => {  
    console.log('B接口数据:', dataB);  
  })  
  .catch(error => {  
    console.error('请求失败:', error);  
  });  

requestA返回Promise,调用then后在回调中执行requestB,确保requestBrequestA完成后启动。通过参数传递,将dataA注入requestB的请求逻辑。

基于 async/await 的同步写法

async function fetchDataSequentially() {  
  try {  
    const dataA = await requestA(); // 等待A接口完成  
    console.log('A接口数据:', dataA);  
    const dataB = await requestB(dataA); // 等待B接口完成,传入dataA  
    console.log('B接口数据:', dataB);  
    return { dataA, dataB };  
  } catch (error) {  
    console.error('请求失败:', error);  
    throw error; // 可选,向上层传递错误  
  }  
}  

// 调用  
fetchDataSequentially();  

async函数内部通过await关键字按顺序执行异步操作,代码结构更接近同步逻辑,易于理解和调试。错误处理通过try/catch统一捕获,避免多层catch嵌套。

处理并发请求中的顺序问题
若存在多个前置请求(如A1、A2需先于B执行),可使用Promise.all等待所有前置请求完成后再执行后续请求:

async function fetchMultiple() {  
  const [dataA1, dataA2] = await Promise.all([requestA1(), requestA2()]);  
  const dataB = await requestB({ ...dataA1, ...dataA2 });  
  return dataB;  
}  

关键要点

  • 确保每个接口函数返回Promise对象(可通过fetch原生支持或手动包装回调函数为Promise)。
  • 通过参数传递明确依赖关系,避免闭包或全局变量导致的隐性耦合。
  • 错误处理需覆盖每个可能失败的异步操作,建议在async函数中使用统一的try/catch,或在Promise链末尾添加catch处理全局错误。
  • 若A接口数据量较大,可考虑缓存dataA以避免重复请求(需结合业务场景判断)。

Cookie、LocalStorage、SessionStorage 的区别与使用场景

Cookie、LocalStorage 和 SessionStorage 是前端常用的存储机制,它们在生命周期、存储容量、数据传输方式和适用场景上有显著差异。

生命周期方面,Cookie 由服务器通过响应头设置,可通过 expires 或 max-age 指定过期时间,若未设置则随浏览器关闭失效;LocalStorage 存储持久化数据,除非手动删除或清除浏览器缓存,否则数据长期存在;SessionStorage 的数据仅在当前浏览器标签页或窗口存活,关闭页面后即被清除。

存储容量与数据类型,Cookie 的容量通常较小,单个域名下一般限制在 4KB 左右,且数据以字符串形式存储,需手动解析;LocalStorage 和 SessionStorage 的容量更大,多数浏览器支持 5MB 至 10MB,可存储字符串类型数据,对象或数组需通过 JSON.stringify 转换后存储。

数据传输机制是三者的核心区别:Cookie 会在浏览器发起 HTTP 请求时自动携带至服务器(同域名下),因此适合存储需要与服务端交互的状态信息(如会话令牌),但过多 Cookie 会增加请求头体积,影响性能;LocalStorage 和 SessionStorage 的数据仅存在于客户端,不会自动参与网络传输,适合纯前端场景的数据存储。

使用场景的选择需结合需求特性:

  • Cookie 适用于身份验证(存储 JWT 令牌)、个性化设置(如用户主题偏好)等需要跨请求共享的场景,但需注意隐私安全和存储上限。
  • LocalStorage 适合长期保存的非敏感数据,例如用户设置、购物车缓存、未完成的表单数据等,尤其在单页应用(SPA)中常用于持久化用户状态。
  • SessionStorage 适用于临时数据存储,如多步骤表单的中间状态、当前标签页的临时缓存,确保数据不跨页面泄漏。

此外,三者的 API 操作方式类似,均通过 setItemgetItemremoveItem 和 clear 方法管理数据。但需注意,LocalStorage 和 SessionStorage 的数据变化不会自动通知其他标签页,若需跨页面通信,需通过 window.addEventListener('storage', event) 监听存储事件。

Cookie 如何传输给后端?有哪些安全注意事项?

Cookie 的传输机制与安全控制是前端开发的重要知识点。当浏览器向服务器发起请求时(如 GETPOST),会自动将同域名下未过期的 Cookie 附加到请求头的 Cookie 字段中,服务器通过解析该字段获取客户端数据。这一过程无需前端手动干预,但需关注以下安全细节:

传输流程解析

  1. 创建 Cookie:服务器通过响应头 Set-Cookie 字段向浏览器写入 Cookie,例如:

    http

    Set-Cookie: sessionId=abc123; Expires=Thu, 31 Dec 2023 23:59:59 GMT; Path=/; Domain=example.com; Secure; HttpOnly
    
  2. 自动携带:后续浏览器对该域名的所有请求(如 AJAX、页面跳转)都会在请求头中包含 Cookie: sessionId=abc123 字段。
  3. 服务器处理:服务端通过请求解析 Cookie,验证用户身份或获取状态信息。

安全注意事项

  1. HttpOnly 标志:设置 HttpOnly 后,Cookie 无法通过前端 JavaScript 访问(如 document.cookie),可有效防御跨站脚本攻击(XSS),建议所有敏感 Cookie(如会话令牌)必须启用该属性。
  2. Secure 标志:标记 Secure 的 Cookie 仅在 HTTPS 连接下传输,避免明文传输导致的中间人攻击,生产环境应强制使用 HTTPS。
  3. SameSite 属性:通过 SameSite=Strict/Lax/None 控制 Cookie 的跨站携带行为:
    • Strict:仅允许同站请求携带,完全禁止跨站(如外链跳转),安全性最高但兼容性较差。
    • Lax:允许部分安全的跨站请求(如 GET 方法的外链跳转),平衡安全与可用性。
    • None:允许任意跨站携带,需配合 Secure 使用,适用于需要跨域共享 Cookie 的场景(如单点登录)。
  4. 过期时间控制:敏感 Cookie 应设置较短的过期时间(如 max-age=3600),避免长期存储增加泄漏风险;非必要 Cookie 可不设置过期时间,随浏览器关闭自动删除。
  5. 存储内容限制:禁止在 Cookie 中存储密码、信用卡信息等高度敏感数据,优先使用 HTTP 头部的 Authorization 字段传输令牌(如 Bearer Token)。
  6. Domain 和 Path 限制:通过 Domain 限定 Cookie 生效的域名范围(如 Domain=example.com 允许子域名访问),Path 限定路径范围(如 Path=/api 仅在 /api 路径下生效),缩小攻击面。

常见攻击与防御

  • XSS 攻击:通过前端 JS 代码窃取未设置 HttpOnly 的 Cookie,防御手段包括输入过滤、转义 HTML 内容、使用 CSP(内容安全策略)限制脚本来源。
  • CSRF 攻击:利用浏览器自动携带 Cookie 的特性,伪造跨站请求,防御方法包括验证 Referer 或 Origin 头、使用 CSRF Token 进行双重验证。

总之,Cookie 的安全配置需遵循 “最小权限” 原则,结合业务需求合理设置属性,同时依赖后端接口的安全策略(如令牌过期机制、请求签名),形成完整的安全防护体系。

实现一个带过期时间的 localStorage(需考虑数据读取时的过期判断)

实现带过期时间的 localStorage 需在原生 API 基础上封装存储逻辑,确保数据写入时记录过期时间,并在读取时验证有效性。以下是具体思路与代码实现:

核心原理
将数据与过期时间以对象形式存储为字符串,读取时解析并判断当前时间是否超过过期时间。若未过期,返回原始数据;若过期,自动删除并返回 null 或提示信息。

代码实现步骤

  1. 封装 setItemWithExpiry 方法:接收键名、数据、过期时间(毫秒),生成包含数据和过期时间的对象,转换为 JSON 字符串后存入 localStorage
  2. 封装 getItemWithExpiry 方法:读取键名对应的数据,若不存在则返回 null;若存在,解析对象并比较当前时间与过期时间,判断是否失效。
  3. 处理数据格式:确保存储的过期时间为数值类型,避免解析错误;读取时统一返回有效数据或 null,保持接口一致性。

示例代码

// 写入带过期时间的数据
function setItemWithExpiry(key, value, ttl) {
  const item = {
    value: value,
    expiry: Date.now() + ttl // 计算过期时间(当前时间+存活毫秒数)
  };
  localStorage.setItem(key, JSON.stringify(item));
}

// 读取并验证过期时间
function getItemWithExpiry(key) {
  const itemStr = localStorage.getItem(key);
  if (!itemStr) return null; // 数据不存在

  try {
    const item = JSON.parse(itemStr);
    const now = Date.now();
    // 判断是否过期
    if (now > item.expiry) {
      localStorage.removeItem(key); // 过期则删除
      return null;
    }
    return item.value; // 返回有效数据
  } catch (error) {
    console.error('解析 localStorage 数据失败:', error);
    localStorage.removeItem(key); // 解析错误时清理无效数据
    return null;
  }
}

使用示例

// 存储数据,设置 5 分钟过期(300000 毫秒)
setItemWithExpiry('userToken', 'abc123', 300000);

// 读取数据
const token = getItemWithExpiry('userToken');
if (token) {
  console.log('有效令牌:', token);
} else {
  console.log('令牌已过期或不存在');
}

边界情况处理

  • 过期时间为 0 或负数:可视为 “永久有效” 或直接拒绝存储,需在方法中添加校验逻辑,例如:
    if (ttl <= 0) throw new Error('过期时间必须为正整数(毫秒)');
    
  • 数据格式错误:若存储的非 JSON 字符串(如用户手动修改 localStorage),JSON.parse 会抛出错误,需通过 try-catch 捕获并清理无效数据。
  • 跨标签页更新:由于 localStorage 的 storage 事件在当前标签页修改时不会触发,若需监听其他标签页的过期清理操作,需结合 window.addEventListener('storage', ...) 实现全局监听。

与原生 API 的区别
该封装扩展了原生 localStorage 的功能,使其具备 “自动过期” 能力,适用于临时缓存场景(如登录令牌、临时用户数据)。相比直接使用原生存储,开发者无需在业务代码中重复编写过期判断逻辑,提升代码复用性和维护性。

注意事项

  • localStorage 的数据存储在浏览器本地,过期时间依赖客户端系统时间,若用户修改系统时间可能导致逻辑失效,敏感场景需结合服务端时间校验。
  • 存储容量限制与原生 localStorage 一致(通常为 5MB),大量过期数据可能占用存储空间,建议定期清理或使用 sessionStorage 替代临时数据。

回流(Reflow)和重绘(Repaint)的区别及优化

回流(Reflow)和重绘(Repaint)是浏览器渲染流程中的关键概念,理解两者的区别与触发条件对性能优化至关重要。

定义与区别

  • 回流(Reflow/Relayout):当元素的几何属性(如宽高、位置、边框尺寸)或布局相关属性(如 displayflexgrid)发生改变时,浏览器需要重新计算元素的几何位置和布局,更新文档流。这一过程会影响页面中多个元素甚至整个文档的结构,计算成本较高。
  • 重绘(Repaint):当元素的视觉属性(如颜色、背景、阴影)发生改变但不影响布局时,浏览器只需更新元素的外观,无需重新计算布局。重绘的成本通常低于回流,但大量重绘仍可能导致性能瓶颈。

触发条件对比

操作类型 触发回流 触发重绘
修改 width/height ✅(若视觉属性变化)
修改 color
修改 display ✅(元素脱离文档流) ✅(若元素可见性变化)
修改 visibility ❌(不影响布局) ✅(元素显示状态变化)
添加 / 删除 DOM ✅(可能影响周围元素布局)
滚动页面 ✅(视口内元素位置变化) ✅(元素进入 / 离开视口)

性能影响
回流会导致浏览器重新计算布局,涉及复杂的渲染树遍历和几何计算,尤其当操作影响大量元素或触发多层级布局嵌套时(如修改 body 的字体大小),性能消耗显著。重绘虽无需布局计算,但频繁更新多个元素的样式(如动画效果)可能导致浏览器渲染队列积压,出现卡顿。

优化策略

一、减少回流与重绘的触发频率
  1. 批量修改样式
    • 使用 class 类名一次性应用多个样式,避免逐行修改属性。
    // 反例:多次修改样式触发多次回流
    element.style.width = '100px';
    element.style.height = '200px';
    element.style.backgroundColor = 'red';
    
    // 正例:通过 class 批量修改
    element.className = 'active'; // CSS 中定义好所有样式
    
  2. 使用文档碎片(Document Fragment)
    批量操作 DOM 时,先将元素添加到 DocumentFragment 中,完成后一次性插入 DOM 树,避免多次回流。
    const fragment = document.createDocumentFragment();
    for (const item of data) {
      const div = document.createElement('div');
      div.textContent = item;
      fragment.appendChild(div);
    }
    container.appendChild(fragment); // 仅触发一次回流
    
二、分离读写操作

浏览器会优化同步的读写操作,但连续的 “读布局→改布局” 操作会强制触发回流。应将多次读操作合并,写操作合并后一次性执行。

// 反例:读写交叉触发多次回流
console.log(element.offsetWidth); // 读布局
element.style.width = '200px';    // 写布局
console.log(element.offsetHeight); // 再次读,触发回流

// 正例:先读后写
const width = element.offsetWidth;
const height = element.offsetHeight;
element.style.width = width + 10 + 'px';
element.style.height = height + 10 + 'px';
三、利用层叠上下文(Layer)

将频繁动画的元素提升为独立层(如设置 will-change: transform 或 transform: translateZ(0)),使其渲染在单独的图层中,避免影响其他元素的回流与重绘。

四、避免触发全局布局属性

某些属性(如 offsetWidthscrollHeightgetBoundingClientRect())会强制浏览器同步计算布局,应尽量减少使用。若需获取元素位置,可缓存结果或使用 requestAnimationFrame 延迟获取。

五、使用 CSS 3D 变换

对于动画效果,优先使用 transform 和 opacity 属性,这两个属性的修改不会触发回流,仅触发复合层的重绘,性能更优。

工具与调试

  • 浏览器开发者工具:Chrome DevTools 的 “Performance” 面板可录制渲染性能,标记回流与重绘的耗时操作;“Rendering” 标签页可勾选 “Paint flashing” 或 “Layout shift debugging”,直观查看重绘与回流区域。
  • CSS Triggers:查询哪些 CSS 属性会触发回流或重绘(如 csstriggers.com),辅助优化样式编写。

总结
回流与重绘是浏览器渲染的必要环节,无法完全避免,但可通过合理的代码组织和样式设计减少其触发频率与影响范围。关键原则是:批量操作 DOM、分离读写流程、利用图层隔离动画元素、优先使用低消耗的属性。在复杂列表或动画场景中,需结合虚拟滚动、CSS 硬件加速等技术进一步优化,确保页面流畅性。