2025面试题大全(4)

305 阅读1小时+

1. 说说Fiber的含义与数据结构

Fiber的含义: "Fiber"在React中是一个核心概念,它代表了一种新的 reconciliation(协调)算法的架构。在React 16之前,React使用的是递归的方式来进行组件的渲染和更新,这种方式在复杂的应用中可能会导致性能问题,因为它会阻塞主线程。Fiber架构的引入就是为了解决这些问题,它将渲染工作分解为多个小任务,这些任务可以在合适的时候中断和恢复,从而不会长时间阻塞主线程。 "Fiber"这个词本身也有“纤维”的意思,形象地表示了这种新的架构是细粒度、可中断的。 Fiber的数据结构: 每个React组件在渲染过程中都会对应一个Fiber节点,Fiber节点是一个包含多个属性的对象,这些属性存储了关于组件的状态、依赖关系、副作用等信息。以下是Fiber节点的一些重要属性:

  1. type:表示组件的类型,如函数组件、类组件、宿主组件(如div、span等)等。
  2. tag:表示Fiber节点的类型,如FunctionComponent、ClassComponent、HostComponent等。
  3. stateNode:对于类组件,指向组件的实例;对于宿主组件,指向对应的DOM节点。
  4. props:存储传递给组件的属性。
  5. memoizedState:存储组件的state。
  6. return:指向父Fiber节点。
  7. child:指向第一个子Fiber节点。
  8. sibling:指向下一个兄弟Fiber节点。
  9. alternate:指向当前Fiber节点在另一次渲染时对应的Fiber节点,用于实现双缓冲技术。
  10. pendingProps:存储新的props,待处理。
  11. memoizedProps:存储上一次渲染时使用的props。
  12. updateQueue:存储更新队列,用于处理state和props的更新。
  13. effectTag:表示Fiber节点需要执行的副作用类型,如 Placement(插入)、Update(更新)、Deletion(删除)等。
  14. nextEffect:指向下一个有副作用的Fiber节点。 这些属性共同构成了Fiber节点的数据结构,使得React能够更细粒度地控制组件的渲染和更新过程。 Fiber架构通过这种数据结构和相应的工作循环机制,实现了增量渲染、任务调度、优先级分配、中断和恢复等一系列功能,从而大大提高了React应用的性能和响应性。

2. 说说 React render 阶段的执行过程

React的render阶段是组件渲染更新的重要环节,主要负责生成新的Fiber树(也称为“工作树”或“alternate树”),这个树反映了应用即将更新的状态。render阶段的执行过程可以分为以下几个主要步骤:

  1. 开始阶段(Begin Phase)
    • React会为当前渲染的组件创建一个Fiber节点,并初始化一些基本属性。
    • 设置工作循环的上下文,包括当前的工作Fiber节点、根节点等。
  2. 渲染阶段(Render Phase)
    • 调和(Reconciliation):React会遍历旧的Fiber树(当前树),并根据新的虚拟DOM(React元素)生成新的Fiber树。这个过程称为“调和”或“协调”。
      • 对于每个组件,React会调用其渲染方法(如render或函数组件本身)来获取新的子元素。
      • React会根据新的子元素和旧的Fiber节点进行比较,决定如何更新Fiber树。这个过程包括创建新的Fiber节点、复用旧的Fiber节点或删除不再需要的Fiber节点。
      • 在这个过程中,React还会为每个Fiber节点标记副作用(effect tag),如Placement(插入)、Update(更新)、Deletion(删除)等,以便在后续的提交阶段执行相应的DOM操作。
  3. 完成阶段(Complete Phase)
    • 当React完成一个Fiber节点的所有子节点的调和后,会进入该节点的完成阶段。
    • 在完成阶段,React会处理一些收尾工作,如收集副作用、更新节点的状态等。
    • 完成阶段的处理会从子节点向上冒泡,直到根节点。
  4. 中断与恢复
    • Fiber架构的一个重要特性是能够中断和恢复渲染过程。如果React在渲染过程中发现没有足够的时间完成当前任务,它会将控制权交回给浏览器,让浏览器处理其他任务,如用户交互、渲染等。当主线程空闲时,React会恢复渲染过程。
  5. 生成副作用列表
    • 在整个render阶段,React会构建一个副作用列表(effect list),这个列表包含了所有需要执行副作用的Fiber节点。副作用列表会在后续的提交阶段使用,以执行实际的DOM更新。 render阶段完成后,React会进入提交阶段(Commit Phase),在这个阶段,React会根据副作用列表执行实际的DOM操作,如插入、更新和删除节点。提交阶段是同步执行的,以确保DOM状态的完整性。 需要注意的是,render阶段的工作是在内存中进行的,不会直接操作DOM,这样可以确保在执行实际的DOM更新之前,所有的变更都已经计算完毕,从而提高性能和减少页面重绘和回流。

3. 说说 React commit 阶段的执行过程

React的commit阶段是组件渲染更新的最后一步,主要负责将render阶段生成的副作用应用到实际的DOM上。这个过程是同步执行的,以确保UI的稳定性和一致性。commit阶段可以分为以下几个主要步骤:

  1. _beforeMutation阶段
    • 在这个阶段,React会执行一些DOM变更前的准备工作,例如:
      • 调用getSnapshotBeforeUpdate生命周期方法(在类组件中)。
      • 执行useEffect回调函数中返回的清理函数(在函数组件中)。
      • 保持屏幕上的内容不变,直到所有DOM变更准备就绪。
  2. mutation阶段
    • 这个阶段是commit阶段的核心,React会根据render阶段收集的副作用列表(effect list)执行实际的DOM操作。这些操作包括:
      • 插入(Placement):将新的DOM节点插入到相应的位置。
      • 更新(Update):更新DOM节点的属性,如样式、属性等。
      • 删除(Deletion):移除不再需要的DOM节点。
      • 替换(Replacement):替换现有的DOM节点。
    • 除了DOM操作,React还会更新refs,确保它们指向正确的实例。
  3. layout阶段
    • 在这个阶段,React会执行一些DOM变更后的工作,例如:
      • 调用componentDidMountcomponentDidUpdate生命周期方法(在类组件中)。
      • 调用useLayoutEffect的回调函数(在函数组件中)。 -/layout阶段是同步执行的,因为它可能会读取DOM布局并作出相应的调整。
  4. _afterPaint阶段
    • 这个阶段是commit阶段的最后一步,它是在浏览器完成绘制之后执行的。在这个阶段,React会执行以下操作:
      • 调用useEffect的回调函数(在函数组件中),这些回调函数不依赖于DOM的布局。
      • 清理一些内部状态,如重置悬挂状态( suspense)。 在整个commit阶段,React会确保所有副作用都按顺序执行,并且保持DOM的稳定性和一致性。由于commit阶段是同步的,它可能会阻塞主线程,因此React会在render阶段尽可能减少需要在此阶段执行的工作,以减少对性能的影响。 需要注意的是,commit阶段的操作是直接对DOM进行的,因此任何在commit阶段执行的代码都应该避免进行大量的计算或者重新触发新的渲染,以避免造成性能问题。

4. React 中,fiber 是如何实现时间切片的?

在React中,Fiber架构通过时间切片(Time Slicing)来实现任务的异步执行,从而避免长时间占用主线程,提高应用的响应性和性能。时间切片的实现主要依赖于以下机制:

  1. 任务分解
    • Fiber将整个渲染过程分解为许多小的任务单元,每个任务单元对应一个Fiber节点。这样,React可以将大的渲染任务分解为多个小任务,每个小任务都可以独立执行和暂停。
  2. 请求Idle时间
    • React使用requestIdleCallbackAPI(在浏览器不支持的情况下,使用polyfill或自定义实现)来请求浏览器在主线程空闲时执行任务。requestIdleCallback允许开发者指定一个回调函数,该函数将在浏览器空闲时被调用,并且可以设置超时时间,以确保回调函数在一定时间内被执行。
  3. 工作循环(Work Loop)
    • Fiber通过工作循环来管理任务的执行。工作循环会不断地从任务队列中取出任务并执行,每个任务执行完成后,会检查剩余的时间是否足够继续执行下一个任务。如果时间不足,React会将控制权交回给浏览器,让浏览器处理其他高优先级任务,如用户输入、动画等。
  4. 中断与恢复
    • 在执行任务的过程中,如果遇到时间不足或者有更高优先级的任务需要处理,React可以中断当前任务的执行,并在稍后恢复。每个Fiber节点都会保存其执行状态,以便在恢复时能够从上次中断的地方继续执行。
  5. 优先级调度
    • React为不同的任务分配不同的优先级,高优先级的任务(如用户交互)会优先执行。优先级调度确保了重要的任务能够得到及时处理,而低优先级的任务可以在浏览器空闲时执行。
  6. 协作式多任务
    • Fiber的时间切片机制是一种协作式多任务处理方式。React与浏览器协作,确保在执行JavaScript任务的同时,浏览器仍有足够的时间处理其他任务,如渲染、绘制和执行其他脚本。 通过这些机制,React的Fiber架构实现了时间切片,使得长时间运行的渲染任务可以被分解为多个小任务,这些任务可以在浏览器的空闲时间分散执行,从而避免了长时间占用主线程,提高了应用的响应性和性能。 需要注意的是,React的时间切片机制主要是为了优化大量计算的渲染过程,对于DOM操作等无法分割的任务,React仍然会在commit阶段同步执行,以确保DOM的稳定性和一致性。

5. script 预加载方式有哪些,这些加载方式有何区别?

<script>标签的预加载方式主要有以下几种,每种方式都有其特点和适用场景:

  1. 同步加载(Sync)
    • 使用方式:<script src="script.js"></script>
    • 特点:浏览器会暂停HTML解析,直到脚本完全下载并执行完毕后再继续解析HTML。
    • 适用场景:适用于脚本非常重要,且需要立即执行的情况。
  2. 异步加载(Async)
    • 使用方式:<script src="script.js" async></script>
    • 特点:脚本会在下载时异步进行,不会阻塞HTML解析。脚本下载完成后会暂停HTML解析并执行脚本,执行完毕后继续HTML解析。
    • 适用场景:适用于不依赖于其他脚本,且不阻塞页面解析的脚本。
  3. 延迟加载(Defer)
    • 使用方式:<script src="script.js" defer></script>
    • 特点:脚本会在HTML解析完成后,DOM内容完全就绪之前执行。多个带有defer属性的脚本会按照它们在文档中出现的顺序执行。
    • 适用场景:适用于依赖于DOM或其他脚本,且不急于立即执行的脚本。
  4. 动态加载(Dynamic Import)
    • 使用方式:通过JavaScript代码动态创建<script>标签,例如:var script = document.createElement('script'); script.src = 'script.js'; document.body.appendChild(script);
    • 特点:可以控制脚本加载的时机,且不阻塞HTML解析。脚本执行时机取决于动态加载的代码。
    • 适用场景:适用于按需加载或条件加载的脚本。
  5. 预加载(Preload)
    • 使用方式:<link rel="preload" href="script.js" as="script">
    • 特点:提前加载脚本文件,但不执行。可以在需要时再通过其他方式执行脚本。
    • 适用场景:适用于提前加载重要资源,但暂时不执行的情况。
  6. 模块加载(Modules)
    • 使用方式:<script type="module" src="module.js"></script>
    • 特点:支持ES6模块的加载方式,默认具有异步加载行为,类似于defer。可以配合import语句使用。
    • 适用场景:适用于使用ES6模块编写的脚本。 区别总结
  • 同步加载会阻塞HTML解析,适用于立即执行的脚本。
  • 异步加载不会阻塞HTML解析,但执行时会暂停解析,适用于独立脚本。
  • 延迟加载在HTML解析完成后执行,保持脚本执行顺序,适用于依赖DOM的脚本。
  • 动态加载提供灵活的加载控制,适用于按需加载。
  • 预加载提前加载资源,但不执行,适用于提前准备重要资源。
  • 模块加载支持ES6模块,具有异步特性,适用于模块化脚本。 选择合适的加载方式取决于脚本的重要性、依赖关系以及页面性能要求。

6. cookie 构成部分有哪些

Cookie是由服务器发送到客户端(通常是浏览器)并存储在客户端的一种小型文本文件,用于在客户端和服务器之间传递信息。一个Cookie通常包含以下构成部分:

  1. 名称(Name)
    • Cookie的名称,用于唯一标识一个Cookie。
  2. 值(Value)
    • Cookie的值,存储实际的数据,通常经过URL编码。
  3. 域(Domain)
    • 指定Cookie有效的域,即允许访问Cookie的域名。如果未设置,默认为创建Cookie的域名。
  4. 路径(Path)
    • 指定Cookie有效的路径,即允许访问Cookie的URL路径。如果未设置,默认为创建Cookie的路径。
  5. 过期时间(Expires)/ 最大存活时间(Max-Age)
    • Expires属性指定Cookie的过期日期,格式为UTC时间。
    • Max-Age属性指定Cookie的最大存活时间,以秒为单位。两者只需设置一个,用于控制Cookie的生存期。
  6. 安全标志(Secure)
    • 指定Cookie是否仅通过安全的HTTPS连接发送。如果设置为Secure,则Cookie只在HTTPS连接中传输。
  7. HttpOnly标志(HttpOnly)
    • 指定Cookie是否仅通过HTTP协议访问,防止客户端脚本语言(如JavaScript)访问Cookie,从而减少XSS攻击的风险。
  8. SameSite属性(SameSite)
    • 控制Cookie是否随着跨站请求一起发送。可以设置为StrictLaxNone
    • Strict:Cookie仅在请求来自同一站点时发送。
    • Lax:Cookie在用户从外部站点导航到目标站点时发送,但在跨站POST请求中不发送。
    • None:Cookie在任何情况下都会发送,但需要同时设置Secure属性。 例如,一个典型的Cookie字符串可能如下所示:
Set-Cookie: name=value; Expires=Wed, 21 Oct 2025 07:28:00 GMT; Domain=example.com; Path=/; Secure; HttpOnly; SameSite=Lax

在这个例子中,name是Cookie的名称,value是Cookie的值,其他部分分别是过期时间、域、路径、安全标志、HttpOnly标志和SameSite属性。

7. 怎么实现扫码登录?

扫码登录是一种常见的登录方式,它利用二维码技术和移动设备来实现快速、便捷的登录。以下是实现扫码登录的一般步骤:

服务器端

  1. 生成二维码
    • 服务器生成一个唯一的二维码,包含一个临时登录凭证(如UUID)。
  2. 存储登录凭证
    • 将临时登录凭证存储在服务器端,通常与一个状态(如未扫描、已扫描、已确认)关联。
  3. 轮询或WebSocket
    • 提供一个接口供客户端轮询查询二维码状态,或者使用WebSocket实时推送状态变化。
  4. 确认登录
    • 当移动设备扫描二维码并确认登录后,服务器将登录凭证与用户账户关联,并更新状态。
  5. 发放登录令牌
    • 确认登录后,服务器向客户端发放登录令牌(如JWT),用于后续的认证。

移动设备端

  1. 扫描二维码
    • 用户使用移动设备上的扫码功能扫描二维码。
  2. 解析二维码
    • 解析出二维码中的临时登录凭证。
  3. 发送确认信息
    • 向服务器发送确认信息,包括临时登录凭证和用户身份信息。
  4. 接收登录状态
    • 接收服务器返回的登录状态,确认登录成功。

客户端(通常为网页端)

  1. 显示二维码
    • 展示服务器生成的二维码。
  2. 轮询或WebSocket
    • 通过轮询或WebSocket连接查询二维码状态。
  3. 处理登录结果
    • 接收到登录成功的状态后,存储登录令牌,并跳转到登录后的页面。

实现细节

  • 安全性:确保所有通信都通过HTTPS进行,防止中间人攻击。
  • 二维码有效期:设置二维码的有效期,过期后需要重新生成。
  • 用户身份验证:在移动设备端确认登录时,需要验证用户身份,防止未授权登录。
  • 错误处理:处理各种异常情况,如二维码过期、网络错误等。

技术选型

  • 二维码生成:可以使用开源库如qrcode生成二维码。
  • 实时通信:可以使用WebSocket或轮询API实现实时状态更新。
  • 认证机制:可以使用OAuth、JWT等认证机制发放和管理登录令牌。 通过以上步骤和技术选型,可以实现一个基本的扫码登录功能。根据具体需求,还可以进一步优化和扩展功能。

8. DNS 协议了解多少?

DNS(Domain Name System,域名系统)是一种分布式数据库,用于将域名(如www.example.com)转换为IP地址(如192.0.2.1)。DNS协议是互联网的基础协议之一,它使得用户可以通过易于记忆的域名来访问网站,而不需要记住复杂的IP地址。 以下是关于DNS协议的一些关键点:

DNS层次结构

  • 根域名服务器:位于DNS层次结构的顶部,负责指向顶级域名服务器。
  • 顶级域名服务器:负责特定顶级域名(如.com、.org、.net等)的解析。
  • 权威域名服务器:负责特定域名的解析,如example.com的权威服务器。
  • 递归解析器:通常由ISP提供,负责向用户设备提供DNS解析服务。

DNS解析过程

  1. 递归查询:用户设备向递归解析器发送查询请求。
  2. 根域名查询:递归解析器向根域名服务器查询顶级域名服务器。
  3. 顶级域名查询:递归解析器向顶级域名服务器查询权威域名服务器。
  4. 权威域名查询:递归解析器向权威域名服务器查询具体的IP地址。
  5. 返回结果:递归解析器将查询结果返回给用户设备。

DNS记录类型

  • A记录:将域名映射到IPv4地址。
  • AAAA记录:将域名映射到IPv6地址。
  • CNAME记录:将域名映射到另一个域名(别名)。
  • MX记录:指定接收电子邮件的邮件服务器。
  • NS记录:指定负责域名的权威域名服务器。
  • TXT记录:可以包含任意文本,常用于验证域名所有权或实现SPF等。

DNS缓存

  • 本地缓存:用户设备和服务器的本地缓存,用于减少DNS查询时间。
  • 递归解析器缓存:递归解析器也会缓存查询结果,以提高解析效率。

DNS安全

  • DNS劫持:攻击者篡改DNS解析结果,将用户导向恶意网站。
  • DNS欺骗:攻击者伪造DNS响应,误导用户设备。
  • DNSSEC:一种安全扩展,用于验证DNS解析结果的合法性。

DNS优化

  • EDNS:扩展DNS协议,支持更大的UDP包和更多的选项。
  • DNS负载均衡:通过DNS解析实现流量分配,提高网站可用性。

DNS工具

  • dig:命令行工具,用于查询DNS记录。
  • nslookup:另一种命令行工具,用于查询DNS记录。
  • Wireshark:网络协议分析工具,可以捕获和分析DNS流量。

DNS发展趋势

  • DoH(DNS over HTTPS):通过HTTPS加密DNS查询,提高隐私和安全性。
  • DoT(DNS over TLS):通过TLS加密DNS查询,同样提高隐私和安全性。 DNS协议是互联网的基石之一,了解其工作原理和安全性对于网络管理和安全至关重要。随着互联网的发展,DNS协议也在不断演进,以适应新的需求和挑战。

9. TCP/IP 是如何保证数据包传输有序可靠的?

TCP/IP协议栈中的TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP通过以下机制保证数据包传输的有序性和可靠性:

1. 三次握手(Three-way Handshake)

  • 建立连接:在数据传输之前,TCP通过三次握手建立连接,确保双方准备好进行数据传输。

2. 序号和确认应答(Sequence Numbers and Acknowledgments)

  • 序号:每个TCP段(segment)都有一个序号,用于标识数据在字节流中的位置。
  • 确认应答:接收方会发送确认应答(ACK),告知发送方已成功接收到的数据序号。

3. 数据重传重共鸣和重复文化内涵

  • **喜好好评。

4. 重传策略

社交媒体网络,确保策略,提升吸引了大量观众。

5. 流量控制和拥塞控制

  • 流量控制:通过滑动窗口机制,接收方可以控制发送方的发送速率,以避免缓冲区溢出。
  • 拥塞控制:TCP通过减少发送窗口大小来响应网络拥塞,如慢启动、拥塞避免、快速重传和快速恢复算法。

6. 错误检测和纠正

  • 校验和:每个TCP段都包含一个校验和,用于检测数据在传输过程中是否出现错误。
  • 超时重传:如果发送方在预定的超时时间内没有收到确认应答,它会重传未被确认的数据段。

7. 有序数据传输

  • 序列号:确保数据按照发送的顺序到达接收方。
  • 重组:接收方根据序列号重新组装数据段,确保数据的有序性。

8. 保持连接状态

  • 持续连接:TCP连接在数据传输过程中保持打开状态,直到通过四次挥手(Four-way Handshake)正常关闭。

9. 可靠性保证

  • 重传机制:对于丢失或错误的数据段,TCP会进行重传,直到接收方正确接收。 通过这些机制,TCP能够提供一种可靠的服务,确保数据包按照发送的顺序到达目的地,且没有丢失或错误。这种可靠性使得TCP成为许多应用层协议(如HTTP、HTTPS、FTP、SMTP等)的首选传输协议。然而,这种可靠性也带来了额外的开销,如延迟和带宽消耗,因此在某些对实时性要求较高的应用中(如视频会议或在线游戏),可能会选择使用UDP(用户数据报协议)等不可靠的传输协议。

10. JavaScript 对象的底层数据结构是什么?

JavaScript对象的底层数据结构通常是哈希表(也称为散列表)。哈希表是一种数据结构,它通过键(Key)来快速访问存储在其中的值(Value)。在JavaScript中,对象属性的存储和访问就是基于哈希表实现的。

哈希表的工作原理:

  1. 键的哈希化:当向对象中添加一个属性时,JavaScript引擎会使用一个哈希函数将属性的键(字符串)转换为一个哈希值(通常是一个整数)。
  2. 存储值:哈希值被用来确定属性值在内存中的存储位置。
  3. 快速访问:当需要访问对象的某个属性时,JavaScript引擎会再次使用相同的哈希函数来计算键的哈希值,并直接定位到对应的存储位置,从而实现快速访问。

哈希表的优势:

  • 快速访问:哈希表提供了平均情况下常数时间复杂度(O(1))的访问速度,这意味着无论哈希表中有多少数据,访问任何一个特定元素的时间都大致相同。
  • 灵活的键:JavaScript对象的键可以是字符串或符号(Symbol),哈希表可以有效地处理这些不同类型的键。

注意事项:

  • 哈希冲突:不同的键可能会产生相同的哈希值,这称为哈希冲突。JavaScript引擎会使用各种策略来解决哈希冲突,如链表法或开放地址法。
  • 性能变化:虽然平均情况下哈希表的访问速度非常快,但在最坏情况下(如大量哈希冲突时),访问速度可能会下降到线性时间复杂度(O(n))。

其他数据结构:

除了哈希表,JavaScript引擎也可能使用其他数据结构来实现对象,例如:

  • 有序哈希表:为了保持属性的插入顺序,一些JavaScript引擎使用了有序哈希表。
  • 隐藏类:对于频繁访问的对象,JavaScript引擎可能会使用隐藏类(Hidden Classes)来优化性能。隐藏类是一种基于对象属性结构的数据结构,它可以提高属性访问的速度。 总的来说,哈希表是JavaScript对象最常用的底层数据结构,但具体的实现细节可能因JavaScript引擎而异。

11. JavaScript 中的变量在内存中的具体存储形式是什么

在JavaScript中,变量的存储形式取决于它们的类型。JavaScript是一种高级语言,其底层的内存管理由JavaScript引擎(如V8、SpiderMonkey等)负责。以下是不同类型变量在内存中的大致存储形式:

1. 基本类型(Primitive Types)

  • 数字(Numbers):通常以双精度64位浮点数(Double)的形式存储。
  • 字符串(Strings):字符串在内存中通常以字符数组的形式存储,并且是不可变的。一旦创建,字符串的内容就不能改变,改变字符串实际上是创建了一个新的字符串。
  • 布尔值(Booleans):通常以一个位(bit)的形式存储,但实际上在许多JavaScript引擎中,布尔值可能被存储为1字节(8位),以便于对齐和快速访问。
  • null:通常表示为机器码中的零值或特定的位模式。
  • undefined:通常表示为特定的位模式,用于表示变量已声明但未初始化。

2. 引用类型(Reference Types)

  • 对象(Objects)数组(Arrays)、**函数(Functions)**等:这些类型在内存中通过引用来存储。引用是一个指向内存中实际对象位置的指针。对象本身存储在堆(Heap)中,而栈(Stack)中存储的是指向这些对象的引用。

内存分配

  • 栈(Stack):用于存储基本类型值和引用类型的引用。栈内存的管理是自动的,由JavaScript引擎在函数调用时分配和释放。
  • 堆(Heap):用于存储引用类型的实际数据。堆内存的管理也是由JavaScript引擎负责,但相对于栈来说,堆的内存分配和回收更为复杂,通常涉及到垃圾回收机制。

示例

let num = 42;        // 数字,存储在栈中
let str = "hello";   // 字符串,存储在栈中(引用指向堆中的字符串数据)
let bool = true;     // 布尔值,存储在栈中
let obj = { key: "value" }; // 对象,引用存储在栈中,对象数据存储在堆中

在这个示例中,numstrboolobj 的引用都存储在栈中。对于 strobj,它们的具体数据(字符串内容和对象属性)存储在堆中。

注意事项

  • 内存管理:JavaScript开发者通常不需要关心具体的内存管理细节,因为JavaScript引擎会自动处理内存的分配和回收。
  • 性能考虑:了解变量的存储形式有助于理解性能优化的一些基本原则,例如避免不必要的对象创建以减少垃圾回收的压力。 总的来说,JavaScript中的变量在内存中的存储形式取决于它们的类型,基本类型直接存储值,而引用类型存储的是指向堆中实际数据的引用。

12. npm script 生命周期有哪些?

npm 脚本(npm scripts)是定义在 package.json 文件中的脚本,它们可以简化开发流程中的常见任务,如构建、测试、启动开发服务器等。npm 脚本有自己的生命周期,包括以下阶段:

1. 检测阶段(Preparation Phase)

  • prepublish:在 npm publish 之前运行,也可以在 npm install 时运行(如果包是本地开发的)。
  • prepare:在 npm install 之后,npm publish 之前运行。
  • preinstall:在 npm install 安装包之前运行。
  • install:在 npm install 安装包之后运行。
  • postinstall:在 npm install 安装包之后运行,在 install 脚本之后。
  • preuninstall:在卸载包之前运行。
  • uninstall:在卸载包时运行。
  • postuninstall:在卸载包之后运行。

2. 版本管理阶段(Versioning Phase)

  • preversion:在运行 npm version 命令之前运行。
  • version:在运行 npm version 命令时运行,用于自定义版本号。
  • postversion:在运行 npm version 命令之后运行。

3. 测试阶段(Testing Phase)

  • pretest:在运行 npm test 命令之前运行。
  • test:运行 npm test 命令时执行。
  • posttest:在运行 npm test 命令之后运行。

4. 打包阶段(Building Phase)

  • prepack:在打包包文件之前运行。
  • pack:在打包包文件时运行。
  • postpack:在打包包文件之后运行。

5. 发布阶段(Publishing Phase)

  • publish:在运行 npm publish 命令时运行。
  • postpublish:在运行 npm publish 命令之后运行。

6. 其他常用脚本

  • start:运行 npm start 时执行,通常用于启动开发服务器。
  • stop:运行 npm stop 时执行,用于停止服务。
  • restart:运行 npm restart 时执行,实际上是先运行 stop 脚本,然后运行 start 脚本。
  • lint:运行代码风格检查。
  • build:运行构建过程。

钩子脚本(Hook Scripts)

npm 脚本还支持钩子,可以在特定命令前后执行。钩子脚本以 prepost 为前缀,例如 prestartpoststart 等。

示例

package.json 中定义脚本:

{
  "scripts": {
    "prestart": "echo Starting the development server...",
    "start": "node server.js",
    "poststart": "echo Development server is running.",
    "pretest": "eslint src/",
    "test": "mocha tests/",
    "posttest": "echo Tests completed.",
    "prepublish": "npm run build",
    "build": "webpack"
  }
}

在这个示例中,当运行 npm start 时,会先执行 prestart 脚本,然后执行 start 脚本,最后执行 poststart 脚本。 了解 npm 脚本的生命周期可以帮助开发者更好地组织和管理开发流程中的各种任务。

13. 说说你对 createPortal 的了解

createPortal 是一个在 React 应用中用于将子节点渲染到父组件以外的 DOM 节点中的函数。这个功能在 React 16 的“门户”(Portals)API 中引入。createPortal 的主要用途包括创建模态框(Modal)、悬浮层(Tooltip)、弹出窗口(Popover)等,这些组件需要渲染在 DOM 的顶层,以避免被其他组件的样式或布局影响。

基本用法

import React from 'react';
import ReactDOM from 'react-dom';
function PortalComponent() {
  return ReactDOM.createPortal(
    <div>这是一个门户组件</div>,
    document.getElementById('portal-root') // 指定渲染的 DOM 节点
  );
}
function App() {
  return (
    <div>
      <h1>主应用</h1>
      <PortalComponent />
    </div>
  );
}
ReactDOM.render(<App />, document.getElementById('root'));

在这个例子中,PortalComponent 会被渲染到 idportal-root 的 DOM 节点中,而不是 App 组件的内部。

主要特点

  1. 解耦组件与位置createPortal 允许组件与其在 DOM 树中的位置解耦,使得组件可以渲染在任意指定的 DOM 节点中。
  2. 事件冒泡:尽管门户组件被渲染到不同的 DOM 节点,但事件仍然会按照正常的冒泡机制在 React 组件树中传递。
  3. 样式隔离:门户组件的样式与其父组件隔离,因为它们被渲染在不同的 DOM 节点中。
  4. 性能优化:在某些情况下,使用门户可以减少主组件树的重新渲染,从而提高性能。

使用场景

  • 模态框(Modal):模态框通常需要覆盖整个屏幕,使用 createPortal 可以确保模态框渲染在顶层,不受其他组件影响。
  • 悬浮层(Tooltip):悬浮层需要跟随鼠标或特定元素显示,使用 createPortal 可以确保其位置和显示的独立性。
  • 弹出窗口(Popover):弹出窗口类似于模态框,但通常用于显示额外的信息或操作,使用 createPortal 可以更好地控制其位置和显示。

注意事项

  • DOM 节点存在性:确保传递给 createPortal 的 DOM 节点在组件渲染时存在。
  • 服务端渲染(SSR)createPortal 不适用于服务端渲染,因为服务端没有 DOM。 createPortal 为 React 应用提供了一种灵活的组件渲染方式,使得开发者可以更自由地控制组件的渲染位置和方式。

14. 说说你对 eval 的理解

eval 是 JavaScript 中一个强大的全局函数,它可以将字符串当作代码来执行。具体来说,eval 函数接收一个字符串作为参数,并尝试将其解析为有效的 JavaScript 代码并执行。

基本用法

eval("console.log('Hello, world!');"); // 输出: Hello, world!

在这个例子中,eval 接收一个字符串,该字符串包含了 JavaScript 代码,eval 会执行这段代码,结果是在控制台输出 "Hello, world!"。

特点

  1. 动态执行代码eval 可以在运行时动态地执行代码,这为编写某些类型的程序提供了灵活性。
  2. 访问作用域eval 执行的代码可以访问其所在作用域中的变量。
    let x = 10;
    eval("console.log(x);"); // 输出: 10
    
  3. 返回值eval 会返回最后一个表达式的结果。
    let result = eval("let y = 20; y * 2;");
    console.log(result); // 输出: 40
    

安全性问题

  • 执行任意代码eval 可以执行任意字符串作为代码,这可能导致安全漏洞,尤其是当执行的字符串来自不可信的源时。
  • XSS 攻击:如果网站允许用户输入的数据通过 eval 执行,那么可能会遭受跨站脚本(XSS)攻击。

性能问题

  • 优化困难:JavaScript 引擎难以对 eval 执行的代码进行优化,因为代码是在运行时动态解析的。
  • 影响性能:频繁使用 eval 可能会降低程序的性能。

使用场景

尽管 eval 有安全和性能上的问题,但在某些特定场景下,它仍然是有用的:

  • 动态代码执行:当需要根据用户输入或配置动态执行代码时。
  • 代码解析器:在实现简单的代码解析器或沙盒环境时。
  • 调试工具:在开发调试工具时,允许动态执行和测试代码。

替代方案

由于 eval 的安全性和性能问题,通常建议使用以下替代方案:

  • 函数:将代码封装在函数中,而不是使用 eval
  • 新的 Function 构造器new Function 可以创建一个新函数,但其作用域是全局的,不访问局部变量。
  • 模板引擎:对于动态生成 HTML 的场景,使用模板引擎而不是 eval

结论

eval 是一个强大的工具,但应谨慎使用。在大多数情况下,应避免使用 eval,以减少安全风险和提高程序性能。当确实需要动态执行代码时,应考虑使用更安全的替代方案。

15. 说说对 new Function 的理解

new Function 是 JavaScript 中的一种构造函数,用于创建一个新的函数对象。与普通函数定义方式不同,new Function 允许在运行时动态地创建函数,其参数是一系列字符串,最后一个是函数体,前面的都是函数的参数。

基本用法

let myFunction = new Function('a', 'b', 'return a + b;');
console.log(myFunction(1, 2)); // 输出: 3

在这个例子中,new Function 接收三个参数:'a'、'b' 和 'return a + b;'。前两个参数是函数的参数名,最后一个参数是函数体。执行 myFunction(1, 2) 时,会返回 3。

特点

  1. 动态创建函数new Function 允许在运行时动态地创建函数,这对于某些需要动态生成代码的场景非常有用。
  2. 作用域new Function 创建的函数在其自身的作用域中执行,不继承创建它的函数的作用域。这意味着它无法访问局部变量,只能访问全局变量。
    let x = 10;
    let myFunction = new Function('return x;');
    console.log(myFunction()); // 输出: undefined,因为无法访问局部变量 x
    
  3. 安全性:与 eval 相比,new Function 在安全性方面略有优势,因为它不直接在当前作用域中执行代码,但仍需谨慎使用,避免执行不可信的代码。
  4. 性能:与普通函数相比,new Function 创建的函数在性能上可能略逊一筹,因为它们是在运行时解析的。

使用场景

  • 动态代码执行:当需要根据用户输入或配置动态生成和执行函数时。
  • 沙盒环境:在创建沙盒环境时,可以使用 new Function 来隔离代码执行环境。
  • 代码加载:在某些情况下,可以通过 new Function 来加载和执行外部代码。

替代方案

  • 普通函数:在大多数情况下,使用普通函数定义更为清晰和高效。
  • 箭头函数:对于简单的函数,可以使用箭头函数来简化语法。
  • 模板引擎:对于动态生成 HTML 的场景,使用模板引擎而不是 new Function

结论

new Function 是一个强大的工具,允许在运行时动态创建函数。然而,由于其安全性和性能问题,应谨慎使用。在大多数情况下,建议使用更安全、更高效的替代方案。当确实需要动态创建函数时,应确保执行的代码是可信的,并注意性能影响。

16. Javascript 数组中有哪些方法可以改变自身,哪些不可以?

在 JavaScript 中,数组方法可以分为两类:一类是会改变数组自身的方法,另一类是不会改变数组自身的方法。以下是这两类方法的详细列表:

会改变数组自身的方法

  1. 添加元素
    • push(): 在数组末尾添加一个或多个元素,并返回新的长度。
    • unshift(): 在数组开头添加一个或多个元素,并返回新的长度。
  2. 删除元素
    • pop(): 删除数组的最后一个元素,并返回该元素。
    • shift(): 删除数组的第一个元素,并返回该元素。
    • splice(): 通过删除或替换现有元素或者原地添加新的元素来修改数组。
  3. 排序
    • sort(): 对数组元素进行排序。
    • reverse(): 颠倒数组中元素的顺序。
  4. 修改
    • fill(): 用一个固定值填充数组中从起始索引到终止索引内的全部元素。
    • copyWithin(): 浅复制数组的一部分到同一数组中的另一个位置,并返回它,不会改变原数组的长度。

不会改变数组自身的方法

  1. 访问元素
    • concat(): 合并两个或多个数组。此方法返回一个新数组,不改变原数组。
    • slice(): 提取数组的一部分,并返回一个新数组,原数组不变。
  2. 搜索元素
    • indexOf(): 返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。
    • lastIndexOf(): 返回在数组中可以找到一个给定元素的最后一个索引,如果不存在,则返回-1。
    • includes(): 判断数组是否包含某个值,返回布尔值。
    • find(): 找出第一个符合条件的元素。
    • findIndex(): 找出第一个符合条件的元素的索引。
  3. 遍历数组
    • forEach(): 对数组的每个元素执行一次提供的函数。
    • map(): 创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后的返回值。
    • filter(): 创建一个新数组,包含通过所提供函数实现的测试的所有元素。
    • reduce(): 对数组元素计算总和,返回一个单一值。
    • reduceRight(): 从数组的末尾开始,对数组元素计算总和,返回一个单一值。
  4. 字符串相关
    • join(): 将数组元素连接成一个字符串。
    • toString(): 将数组转换为字符串,并返回结果。
    • toLocaleString(): 返回一个表示数组元素的本地化字符串。
  5. 其他
    • every(): 测试一个数组内的所有元素是否都能通过某个指定函数的测试。返回一个布尔值。
    • some(): 测试数组中的元素是否至少有一个满足条件。返回一个布尔值。
    • keys(): 返回一个包含数组中每个索引键的Array Iterator对象。
    • values(): 返回一个包含数组中每个值的Array Iterator对象。
    • entries(): 返回一个包含数组中每个索引键及其对应值的Array Iterator对象。

注意事项

  • 不可变性:对于不改变数组自身的方法,它们通常返回一个新的数组或值,而原数组保持不变。这种不可变性是函数式编程的一个重要概念。
  • 性能考虑:改变数组自身的方法通常在原数组上操作,可能会更高效。而不改变数组自身的方法可能会产生新的数组,这在处理大量数据时需要考虑性能影响。 了解这些方法的特点和区别,可以帮助你更有效地使用 JavaScript 数组。

17. 如何做 promise 缓存?上一次调用函数的 promise 没有返回, 那么下一次调用函数依然返回上一个 promise

要实现Promise缓存,即当函数被多次调用时,只要第一次调用的Promise没有解决(resolved或rejected),后续的调用都应该返回同一个Promise对象,可以通过以下步骤实现:

  1. 创建一个缓存对象来存储Promise。
  2. 在函数调用时,检查缓存中是否已经有一个pending状态的Promise。
  3. 如果有,直接返回这个Promise。
  4. 如果没有,创建一个新的Promise,执行异步操作,并将这个Promise存储在缓存中。
  5. 当Promise解决后,从缓存中移除它。 以下是一个简单的实现示例:
function promiseCache(func) {
  const cache = {};
  return function(...args) {
    // 使用函数名和参数作为缓存的键
    const key = func.name + JSON.stringify(args);
    // 检查缓存中是否有pending的Promise
    if (cache[key]) {
      return cache[key];
    }
    // 创建一个新的Promise并存储在缓存中
    const promise = func(...args).finally(() => {
      // 当Promise解决后,从缓存中移除
      delete cache[key];
    });
    cache[key] = promise;
    return promise;
  };
}
// 示例函数,返回一个Promise
function fetchData(params) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`Data for ${params}`);
    }, 2000);
  });
}
// 使用promiseCache包装fetchData函数
const cachedFetchData = promiseCache(fetchData);
// 调用函数
cachedFetchData('param1').then(console.log); // 输出: Data for param1
cachedFetchData('param1').then(console.log); // 立即输出: Data for param1,没有等待2秒
// 不同参数的调用会创建新的Promise
cachedFetchData('param2').then(console.log); // 输出: Data for param2

在这个示例中,promiseCache是一个高阶函数,它接收一个返回Promise的函数func,并返回一个新的函数。这个新函数会检查缓存,如果已经有了一个pending的Promise,就返回它;否则,创建一个新的Promise,执行原函数,并存储在缓存中。 这种方法可以确保对于相同的参数,只要第一个Promise还没有解决,后续的调用都会返回同一个Promise,从而实现缓存效果。当Promise解决后,它会从缓存中移除,以便后续的调用可以创建新的Promise。

18. 怎么在前端页面中添加水印?

在前端页面中添加水印可以通过多种方式实现,以下是几种常见的方法:

1. 使用CSS背景图水印

通过CSS将水印作为背景图添加到页面元素上,可以设置背景图的透明度以达到水印效果。

<div class="watermark-container">
  <!-- 页面内容 -->
</div>
.watermark-container {
  background-image: url('watermark.png');
  background-repeat: repeat;
  background-position: center;
}

2. 使用SVG水印

SVG可以创建文本水印,并且可以通过CSS控制样式。

<div class="watermark-svg">
  <!-- 页面内容 -->
</div>
.watermark-svg::after {
  content: '';
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><text x='10' y='20' font-family='Verdana' font-size='20' fill='rgba(255,255,255,0.5)'>Watermark</text></svg>");
  background-repeat: repeat;
  opacity: 0.5;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  position: absolute;
  z-index: 1000;
}

3. 使用JavaScript动态生成水印

可以使用JavaScript动态地在页面上生成水印,这样可以更灵活地控制水印的内容和样式。

<div id="watermark-container">
  <!-- 页面内容 -->
</div>
function addWatermark(container, text) {
  const watermarkDiv = document.createElement('div');
  watermarkDiv.style.position = 'absolute';
  watermarkDiv.style.top = '0';
  watermarkDiv.style.left = '0';
  watermarkDiv.style.right = '0';
  watermarkDiv.style.bottom = '0';
  watermarkDiv.style.opacity = '0.5';
  watermarkDiv.style.pointerEvents = 'none';
  watermarkDiv.style.display = 'flex';
  watermarkDiv.style.alignItems = 'center';
  watermarkDiv.style.justifyContent = 'center';
  watermarkDiv.style.fontSize = '20px';
  watermarkDiv.style.color = 'white';
  watermarkDiv.style.zIndex = '1000';
  watermarkDiv.textContent = text;
  container.appendChild(watermarkDiv);
}
// 使用示例
addWatermark(document.getElementById('watermark-container'), 'Watermark');

4. 使用Canvas生成水印

可以使用Canvas绘制水印,并将其作为背景图添加到页面元素上。

<div id="watermark-container">
  <!-- 页面内容 -->
</div>
function createWatermark(text) {
  const canvas = document.createElement('canvas');
  canvas.width = 200;
  canvas.height = 200;
  const ctx = canvas.getContext('2d');
  ctx.font = '20px Verdana';
  ctx.fillStyle = 'rgba(255,255,255,0.5)';
  ctx.fillText(text, 10, 20);
  return canvas.toDataURL();
}
function addWatermark(container, text) {
  const watermarkUrl = createWatermark(text);
  container.style.backgroundImage = `url(${watermarkUrl})`;
  container.style.backgroundRepeat = 'repeat';
}
// 使用示例
addWatermark(document.getElementById('watermark-container'), 'Watermark');

注意事项

  • 水印应具有一定的透明度,以免影响页面内容的阅读。
  • 水印应使用绝对定位,避免影响页面布局。
  • 可以设置pointer-events: none;以避免水印影响页面交互。
  • 考虑水印的防删除措施,例如定时刷新水印,以防止用户通过开发者工具删除水印。 根据具体需求选择合适的方法实现水印功能。

19. CSS 伪类和伪元素有哪些,它们的区别和实际应用

CSS伪类和伪元素是CSS选择器的一部分,它们允许我们为元素的特定状态或部分设置样式。以下是常见的伪类和伪元素,以及它们的区别和实际应用:

常见CSS伪类

  1. :link - 未访问的链接
  2. :visited - 已访问的链接
  3. :hover - 鼠标悬停时的元素
  4. :active - 被激活的元素(如点击时)
  5. :focus - 获得焦点的元素(如输入框)
  6. :nth-child(n) - 匹配父元素中的第n个子元素
  7. :nth-last-child(n) - 匹配父元素中的倒数第n个子元素
  8. :first-child - 匹配父元素中的第一个子元素
  9. :last-child - 匹配父元素中的最后一个子元素
  10. :only-child - 匹配父元素中唯一的子元素
  11. :empty - 匹配没有子元素的元素
  12. :not(selector) - 匹配不符合括号内选择器的元素
  13. :disabled - 匹配禁用的表单元素
  14. :enabled - 匹配启用的表单元素
  15. :checked - 匹配选中的表单元素(如单选按钮或复选框)

常见CSS伪元素

  1. ::before - 在元素内容前插入内容
  2. ::after - 在元素内容后插入内容
  3. ::first-letter - 匹配元素的首字母
  4. ::first-line - 匹配元素的首行
  5. ::selection - 匹配用户选中的内容
  6. ::placeholder - 匹配输入框的占位符文本
区别
  • 伪类用于定义元素的特殊状态,例如链接的访问状态、鼠标悬停、获得焦点等。
  • 伪元素用于选择元素的特定部分,例如第一个字母、第一行、内容的前后等,并且可以插入内容。

实际应用

伪类应用
  • 为未访问和已访问的链接设置不同的颜色:
a:link { color: blue; }
a:visited { color: purple; }
  • 为按钮在鼠标悬停时改变背景颜色:
button:hover { background-color: #ccc; }
  • 为获得焦点的输入框设置边框:
input:focus { border: 1px solid #000; }
伪元素应用
  • 在元素前添加图标:
.element::before {
  content: '🔴';
  margin-right: 8px;
}
  • 为段落的第一行设置不同的字体大小:
p::first-line {
  font-size: 1.2em;
}
  • 为选中的文本设置背景颜色:
::selection {
  background-color: yellow;
}
  • 为输入框的占位符设置样式:
input::placeholder {
  color: #999;
}

在使用伪类和伪元素时,需要注意它们的功能和适用场景,以确保正确地应用它们来增强网页的样式和用户体验。

20. 如何防止 CSS 阻塞渲染?

CSS阻塞渲染是指浏览器在解析HTML时遇到<link rel="stylesheet">标签,会暂停HTML的解析直到CSS文件下载并解析完毕。这可能会导致页面显示延迟。为了防止CSS阻塞渲染,可以采取以下几种策略:

1. 使用媒体查询

<link>标签添加媒体查询,这样浏览器会先解析HTML,只有当媒体查询条件满足时,才会下载和解析CSS文件。

<link rel="stylesheet" href="style.css" media="print">

在这个例子中,style.css只会在打印页面时加载,不会阻塞正常的页面渲染。

2. 使用rel="preload"

使用<link rel="preload">预加载CSS文件,但不会立即应用它,这样可以不阻塞渲染。

<link rel="preload" href="style.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="style.css"></noscript>

在这段代码中,CSS文件被预加载,一旦加载完成,通过JavaScript将其rel属性改为stylesheet来应用样式。<noscript>标签确保在禁用JavaScript的情况下仍能加载CSS。

3. 将CSS内联到HTML中

将关键的CSS直接内联到HTML的<head>中,这样可以避免额外的CSS文件请求,从而减少阻塞。

<style>
  /* 关键CSS样式 */
  body { margin: 0; }
  /* 更多样式 */
</style>

4. 使用asyncdefer属性

虽然asyncdefer属性通常用于JavaScript标签,但某些浏览器也支持将这些属性用于CSS链接。然而,这并不是标准做法,兼容性有限。

<link rel="stylesheet" href="style.css" async>

5. 优化CSS交付

  • 压缩CSS文件:减少文件大小,加快下载速度。
  • 使用CDN:利用内容分发网络(CDN)加快CSS文件的传输速度。
  • 减少CSS文件数量:合并多个CSS文件,减少HTTP请求。

6. 使用JavaScript动态加载CSS

通过JavaScript在页面加载后动态加载CSS文件。

<script>
  function loadCSS(href) {
    var link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = href;
    document.head.appendChild(link);
  }
  loadCSS('style.css');
</script>

7. 设置HTTP缓存头

通过设置合适的HTTP缓存头,可以确保CSS文件在用户首次访问后缓存,减少后续访问时的加载时间。

8. 使用font-display: swap属性

对于Web字体,使用font-display: swap属性可以减少字体加载时的阻塞。

@font-face {
  font-family: 'MyFont';
  src: url('myfont.woff2') format('woff2');
  font-display: swap;
}

通过实施这些策略,可以有效地减少CSS阻塞渲染的问题,提高页面的加载速度和用户体验。

21. 如何将JavaScript代码解析成抽象语法树(AST)?

将JavaScript代码解析成抽象语法树(AST)通常需要使用专门的解析器。以下是一些常用的JavaScript解析器以及如何使用它们来生成AST的步骤:

1. 使用Acorn

Acorn是一个小巧且快速的JavaScript解析器。

// 引入Acorn
const acorn = require("acorn");
// 要解析的JavaScript代码
const code = `function add(a, b) { return a + b; }`;
// 解析代码并生成AST
const ast = acorn.parse(code, { ecmaVersion: 2020 });
// 输出AST
console.log(ast);

2. 使用Esprima

Esprima是一个流行的JavaScript解析器,它能够将代码解析成AST。

// 引入Esprima
const esprima = require("esprima");
// 要解析的JavaScript代码
const code = `function add(a, b) { return a + b; }`;
// 解析代码并生成AST
const ast = esprima.parseScript(code, { jsx: true });
// 输出AST
console.log(ast);

3. 使用Babel

Babel是一个广泛使用的JavaScript编译器,它也提供了将代码解析成AST的功能。

// 引入Babel解析器
const babelParser = require("@babel/parser");
// 要解析的JavaScript代码
const code = `function add(a, b) { return a + b; }`;
// 解析代码并生成AST
const ast = babelParser.parse(code, {
  sourceType: "module",
  plugins: ["jsx"]
});
// 输出AST
console.log(ast);

4. 使用Espree

Espree是一个基于Esprima的解析器,它是ESLint的默认解析器。

// 引入Espree
const espree = require("espree");
// 要解析的JavaScript代码
const code = `function add(a, b) { return a + b; }`;
// 解析代码并生成AST
const ast = espree.parse(code, {
  ecmaVersion: 2020,
  sourceType: "module"
});
// 输出AST
console.log(ast);

5. 使用JavaScript内置的解析器

现代浏览器提供了内置的解析器,如Parser对象,可以用来解析JavaScript代码并生成AST。

// 要解析的JavaScript代码
const code = `function add(a, b) { return a + b; }`;
// 使用浏览器内置的解析器解析代码并生成AST
const ast = Parser.parse(code, { sourceType: "module" });
// 输出AST
console.log(ast);

请注意,Parser对象并不是所有浏览器都支持,而且它的API可能会随着浏览器的更新而变化。

步骤总结:

  1. 选择一个JavaScript解析器,如Acorn、Esprima、Babel或Espree。
  2. 引入解析器库。
  3. 编写要解析的JavaScript代码字符串。
  4. 调用解析器的解析函数,传入代码字符串和配置选项。
  5. 解析函数会返回一个AST对象。
  6. 可以使用console.log或其他方法来查看和操作AST。 通过这些步骤,你可以将JavaScript代码解析成AST,然后进行进一步的分析、转换或优化。

22. base64 的编码原理是什么?

Base64是一种基于64个可打印字符来表示二进制数据的方法。它的编码原理如下:

1. 数据划分

将输入的数据每3个字节(即24位)划分为一组。如果数据长度不是3的倍数,则在数据的末尾填充1个或2个0字节,以确保数据长度是3的倍数。

2. 二进制转换

将每组数据中的每个字节转换为二进制形式,即8位二进制数。

3. 分割二进制位

将每组24位二进制数划分为4个6位的二进制数。

4. 查表转换

将每个6位的二进制数转换为对应的Base64字符。Base64字符集包括A-Z、a-z、0-9、+和/,共64个字符。具体映射关系如下:

  • 0-25对应A-Z
  • 26-51对应a-z
  • 52-61对应0-9
  • 62对应+
  • 63对应/

5. 补充等号

如果原始数据长度不是3的倍数,则在编码后的数据末尾添加等号(=)作为填充字符。具体规则如下:

  • 如果原始数据长度模3余1,则添加两个等号(==)
  • 如果原始数据长度模3余2,则添加一个等号(=)

示例

假设我们要对字符串"Man"进行Base64编码:

  1. 将"Man"转换为二进制:01001101 01000001 01001110
  2. 划分为4个6位二进制数:010011 010100 000101 001110
  3. 查表转换:19 20 5 14 -> T W F O
  4. 因为原始数据长度是3的倍数,所以不需要添加等号。 因此,"Man"的Base64编码是TWFu

注意事项

  • Base64编码后的数据长度是原始数据长度的4/3倍。
  • Base64编码主要用于在文本中表示二进制数据,如电子邮件、存储或传输编码等。
  • Base64编码不是一种安全的加密方法,它只是一种数据表示方式。 通过以上步骤,我们可以理解Base64的编码原理,并手动对数据进行Base64编码。在实际应用中,通常使用编程语言提供的库或函数来实现Base64编码和解码。

23. 如何优化大规模 dom 操作的场景?

优化大规模DOM操作的场景通常涉及减少重绘(repaint)和重排(reflow)的次数,以及使用更高效的数据结构和算法。以下是一些常用的优化策略:

1. 批量操作

  • 使用DocumentFragment:在内存中创建一个文档片段,将所有更改应用到这个片段上,然后一次性将片段添加到DOM中。
  • 批量插入:如果需要插入多个元素,可以先创建一个包含所有元素的字符串,然后一次性插入。

2. 减少重排

  • 避免频繁修改样式:尽量一次性修改样式,或者使用类切换来改变样式。
  • 使用transform和opacity进行动画:这两个属性的变化不会触发重排,只有重绘。
  • 脱离文档流:使用position: absoluteposition: fixed使元素脱离文档流,减少对其他元素的影响。
  • 使用虚拟DOM:如React或Vue等框架,它们通过虚拟DOM来减少实际DOM的操作。

3. 事件委托

  • 利用事件冒泡:将事件监听器添加到父元素上,而不是每个子元素上,减少事件监听器的数量。

4. 使用高效的选择器

  • 避免使用深层次或复杂的选择器:它们会导致浏览器进行更多的计算。
  • 缓存DOM引用:如果需要多次访问同一个DOM元素,应该将其引用存储在变量中。

5. 分批处理

  • 分批更新:如果需要更新大量元素,可以将其分批进行,避免长时间阻塞UI线程。

6. Web Workers

  • 使用Web Workers:对于复杂的数据处理,可以使用Web Workers在后台线程中进行,避免阻塞主线程。

7. 优化算法

  • 减少DOM操作的数量:通过优化算法和数据结构,减少不必要的DOM操作。

8. 使用requestAnimationFrame

  • 平滑动画:对于动画效果,使用requestAnimationFrame来确保在合适的时机进行DOM更新。

9. 避免不必要的DOM操作

  • 条件判断:在操作DOM之前,进行必要的条件判断,避免执行无用的操作。

10. 利用现代前端框架

  • 使用React、Vue等框架:这些框架内置了多种优化机制,可以自动处理很多性能问题。

示例代码

// 使用DocumentFragment批量插入元素
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const newElement = document.createElement('div');
  newElement.textContent = 'New Element';
  fragment.appendChild(newElement);
}
document.body.appendChild(fragment);
// 使用requestAnimationFrame优化动画
function animate() {
  // 更新DOM
  requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

通过结合这些策略,可以显著提高大规模DOM操作的性能,提升用户体验。

24. react native 工作原理是什么?

React Native是React的一个版本,专门为原生应用(如iOS和Android应用)设计。其工作原理与React DOM和虚拟DOM的工作原理相似,但针对原生平台进行了优化和调整。以下是React Native的工作原理:

1. 虚拟DOM

React Native同样使用虚拟DOM(也称为Fiber架构)来减少直接操作原生视图(如UIView或Android View)的次数。这种架构允许React Native在内存中维护一个表示应用状态的虚拟视图树,然后在这个树上进行更改,最后一次性将更改应用到原生视图。

2. 批量操作

React Native优化了批量的视图操作,可以一次性更新多个视图,减少对原生视图的重复操作,提高性能。

3. 原生模块

React Native使用原生模块(如RCTView、RCTText等)来表示原生视图,这些模块是React Native组件与原生平台视图之间的桥梁。

4. 事件系统

React Native有一个优化的事件系统,可以减少不必要的事件分发,提高性能。

5. 原生渲染

React Native直接使用原生渲染API(如iOS的UIKit或Android的View系统)进行渲染,而不是Web的DOM。

6. 时间切片

React Native支持时间切片(Time Slicing),允许在多个帧之间分布工作,减少每帧的工作量,提高性能。

7. 动画和过渡

React Native支持复杂的动画和过渡效果,可以创建平滑的动画。

8. 热更新

React Native支持热更新,允许在运行时更新应用,而无需重启。

示例代码

// 创建原生视图
const view = React.createElement(RCTView);
view.props = { ... };
// 更新原生视图
view.updateProps({ ... });
// 使用原生模块
<RCTView style={{ ... }} />;
<RCTText>Some Text</RCTText>;
// 使用时间切片
requestAnimationFrame(() => {
  // 更新视图
  view.updateProps({ ... });
});

通过这些原理和优化,React Native可以为原生应用提供流畅和高效的渲染体验。

25. 一直在 window 上面挂内容(数据,方法等等),是否有什么风险?

在全局的window对象上挂载内容(如数据、方法等)确实存在一些风险和潜在问题,主要包括以下几点:

1. 命名冲突

  • 风险:如果不同库或组件使用了相同的变量名,会导致命名冲突,从而覆盖原有的属性或方法,引发错误。
  • 示例:两个库都定义了window.myFunction,后者会覆盖前者的定义。

2. 全局污染

  • 风险:过多的全局变量会导致全局命名空间污染,使得代码难以维护,也增加了调试的难度。
  • 示例:在一个大型应用中,如果每个模块都向window添加变量,最终会导致window对象变得非常庞大和复杂。

3. 安全性问题

  • 风险:全局变量可以被页面中的任何脚本访问,包括恶意脚本,这可能导致数据泄露或被篡改。
  • 示例:如果敏感信息被存储在window对象上,恶意脚本可以轻易读取这些信息。

4. 可测试性降低

  • 风险:依赖于全局变量的代码通常更难进行单元测试,因为需要模拟全局环境。
  • 示例:测试函数时,需要额外设置和清理window上的变量,增加了测试的复杂性。

5. 模块化和封装性差

  • 风险:使用全局变量违背了模块化和封装的原则,使得代码的复用性和可维护性降低。
  • 示例:一个模块依赖于window上的变量,当这个模块需要在不同的环境中运行时,可能会出现问题。

6. 跨域问题

  • 风险:在跨域的情况下,不同源之间的全局变量可能不可见,导致问题。
  • 示例:如果一个iframe中的脚本尝试访问父窗口的window变量,可能会因为同源策略而被阻止。

7. 性能影响

  • 风险:频繁地读写全局变量可能会对性能产生负面影响,尤其是在变量复杂或数量较多时。
  • 示例:在循环中频繁访问window上的大数据结构,可能会导致性能瓶颈。

best practices

为了减少这些风险,可以考虑以下最佳实践:

  • 使用模块化:通过ES6模块、CommonJS或其他模块系统来组织代码,避免使用全局变量。
  • 封装:将相关的数据和函数封装在对象或类中,减少全局暴露。
  • 命名空间:如果必须使用全局变量,可以使用命名空间来减少冲突,例如window.myApp.myFunction
  • 使用局部变量:尽可能使用局部变量,减少对全局变量的依赖。
  • 避免存储敏感信息:不要在window对象上存储敏感信息,如用户数据、密码等。 总之,虽然在window上挂载内容有时可以简化开发,但应该谨慎使用,以避免上述风险。

26. React 中的 createContext 和 useContext 分别有什么用?

在React中,createContextuseContext是两个用于状态管理的钩子,它们通常一起使用,以实现跨组件的数据传递。下面分别解释它们的用途:

createContext

createContext是一个函数,用于创建一个Context对象。Context提供了一种在组件树中传递数据而不必手动传递props的方法。 用途:

  1. 创建共享状态:通过createContext创建一个Context,可以在组件树的任何层级共享状态。
  2. 避免逐层传递:在没有Context的情况下,状态需要通过props逐层传递,而使用Context可以避免这种情况。
  3. 定义默认值createContext可以接受一个默认值作为参数,当组件没有匹配的Provider时,将使用这个默认值。 示例:
const MyContext = React.createContext(defaultValue);

useContext

useContext是一个钩子,用于在函数组件中接收Context的值。 用途:

  1. 消费Context:在函数组件中,使用useContext可以轻松地消费Context提供的值。
  2. 简化代码:相比类组件中的contextTypeConsumer组件,useContext使代码更加简洁。
  3. 响应式更新:当Context的值发生变化时,使用useContext的组件会自动重新渲染。 示例:
const value = useContext(MyContext);

结合使用

通常,createContextuseContext一起使用,以实现状态的共享和消费。 示例:

// 创建Context
const MyContext = React.createContext();
// Provider组件,用于提供状态
function MyProvider({ children }) {
  const [state, setState] = useState(initialState);
  return <MyContext.Provider value={{ state, setState }}>{children}</MyContext.Provider>;
}
// Consumer组件,用于消费状态
function MyConsumer() {
  const { state, setState } = useContext(MyContext);
  // 使用state和setState
}

在这个例子中:

  • MyContext是通过createContext创建的Context对象。
  • MyProvider是一个组件,它使用MyContext.Provider来提供状态。
  • MyConsumer是一个组件,它使用useContext(MyContext)来消费状态。 通过这种方式,状态可以在组件树中高效地传递,而无需手动通过每个组件的props传递。

27. Object.prototype.hasOwnProperty() 作用是什么?

Object.prototype.hasOwnProperty() 是 JavaScript 中 Object 构造函数原型上的一个方法,用于检查一个对象是否拥有某个特定的自身属性(即直接在对象上定义的属性,而不是通过原型链继承的属性)。 作用:

  1. 检查自身属性:判断一个对象是否有一个名为propertyName的自身属性。
  2. 忽略原型链hasOwnProperty()方法不会检查对象的原型链,只会检查对象本身。
  3. 返回布尔值:如果对象拥有该属性,则返回true;否则返回false语法:
object.hasOwnProperty(propertyName);
  • object:需要检查的对象。
  • propertyName:需要检查的属性名称,可以是字符串或符号(Symbol)。 示例:
const obj = {
  name: 'Alice',
  age: 25
};
console.log(obj.hasOwnProperty('name')); // true
console.log(obj.hasOwnProperty('age'));  // true
console.log(obj.hasOwnProperty('toString')); // false,toString是继承自Object的原型方法

注意事项:

  • hasOwnProperty()方法不会检查对象的原型链,这意味着如果属性是继承自原型链上的,hasOwnProperty()会返回false
  • 在使用for...in循环遍历对象的属性时,通常使用hasOwnProperty()来过滤掉那些从原型链上继承的属性。 示例:
const obj = {
  name: 'Alice'
};
for (const key in obj) {
  if (obj.hasOwnProperty(key)) {
    console.log(key); // 只会输出'name'
  }
}

在这个例子中,hasOwnProperty()确保了只有对象自身的属性被遍历,而继承自原型链的属性被忽略了。

28. npm 中的“幽灵依赖”是什么?

在npm(Node Package Manager)中,“幽灵依赖”(Phantom Dependencies)指的是那些虽然没有被明确列在项目的package.json文件中的依赖,但仍然存在于项目的node_modules目录中,并且可以被项目代码所引用的依赖包。 幽灵依赖的产生原因:

  1. 间接依赖:当一个包被安装时,它的依赖也会被安装到node_modules目录中。如果这些间接依赖没有被其他包所需要,它们就成为了幽灵依赖。
  2. 开发时的临时安装:开发者在开发过程中可能临时安装了一些包,但忘记将它们添加到package.json中。
  3. 版本冲突解决:npm在解决版本冲突时,可能会安装一些额外的包,这些包不一定在package.json中列出。 幽灵依赖的问题:
  4. 不可控性:由于幽灵依赖没有在package.json中明确列出,它们的存在和版本都可能不可控,这可能导致构建结果在不同环境中的不一致。
  5. 安全性风险:未列出的依赖可能没有经过安全审查,存在安全风险。
  6. 依赖混乱:幽灵依赖可能导致依赖关系变得复杂和混乱,难以管理和维护。 如何避免幽灵依赖:
  7. 明确列出所有依赖:在package.json中明确列出所有直接和间接依赖。
  8. 使用npm ls检查依赖:使用npm ls命令检查项目中实际安装的依赖,并与package.json中的依赖列表进行对比。
  9. 使用npm shrinkwrapnpm shrinkwrap命令会生成一个npm-shrinkwrap.json文件,锁定项目中所有依赖的版本,包括间接依赖,从而避免幽灵依赖的产生。
  10. 使用package-lock.json:从npm 5开始,默认会生成一个package-lock.json文件,该文件记录了项目中所有依赖的精确版本,有助于避免幽灵依赖。 注意:虽然package-lock.jsonnpm-shrinkwrap.json都可以帮助锁定依赖版本,但它们的作用范围不同。package-lock.json主要用于确保团队开发时依赖的一致性,而npm-shrinkwrap.json则用于确保项目在生产环境中的依赖一致性。 通过以上方法,可以有效地管理和控制项目中的依赖,避免幽灵依赖带来的问题。

29. vue 是如何识别和解析指令的?

Vue.js 是一个用于构建用户界面的渐进式JavaScript框架,它通过指令来扩展HTML的功能,使得我们可以通过声明式的方式将数据绑定到DOM。Vue识别和解析指令的过程主要涉及以下几个步骤:

  1. 模板编译
    • 当你创建一个Vue实例时,会提供一个模板(通常是HTML结构),Vue会使用其编译器将这个模板编译成渲染函数。
    • 编译过程中,Vue会解析模板中的指令(以v-开头的特殊属性,如v-ifv-for等)。
  2. 指令识别
    • 在解析模板时,Vue会识别出所有的指令属性。
    • 指令可以是一个单词(如v-ifv-show),也可以是动态的(如v-bind:titlev-on:click)。
  3. 指令解析
    • 对于每个识别出的指令,Vue会解析其含义并转换为相应的JavaScript代码。
    • 例如,v-if="condition"会被转换为条件渲染的代码,v-for="item in items"会被转换为循环渲染的代码。
  4. 创建虚拟DOM
    • 编译后的渲染函数会返回虚拟DOM(Virtual DOM),这是一个轻量级的JavaScript对象,代表了真实的DOM结构。
    • 虚拟DOM中包含了指令的信息,这些信息会在后续的渲染过程中被使用。
  5. 数据绑定
    • Vue会根据指令将数据绑定到虚拟DOM上。
    • 例如,v-model指令会创建双向数据绑定,v-bind指令会创建单向数据绑定。
  6. 渲染和更新
    • 当数据发生变化时,Vue会重新执行渲染函数,生成新的虚拟DOM。
    • 然后,Vue会比较新旧虚拟DOM的差异,并应用到真实的DOM上,这个过程称为“渲染”或“更新”。
  7. 指令钩子函数
    • 一些指令(如v-onv-bind等)有对应的钩子函数,这些函数会在特定的生命周期阶段被调用。
    • 例如,v-on指令的钩子函数会在事件触发时被调用。 Vue的指令系统是其核心特性之一,通过指令,Vue能够以声明式的方式处理DOM的更新,使得开发者可以更专注于业务逻辑而不是繁琐的DOM操作。指令的识别和解析是Vue内部复杂流程的一部分,但对外提供了简单直观的API,使得开发者可以轻松地使用这些功能。

30. 说说对 node 子进程的了解

Node.js 的子进程(Child Processes)模块允许开发者从当前 Node.js 进程中衍生出新的子进程。子进程可以用于执行系统命令、运行脚本、处理并行任务等。Node.js 提供了多种方式来创建和管理子进程,以下是几种常见的方法:

  1. spawn
    • spawn 方法用于启动一个新进程,并返回一个 ChildProcess 实例。
    • 它适用于需要流式数据传输的场景,比如执行一个命令并实时获取输出。
    • spawn 方法接受命令名和参数数组,以及可选的配置对象。
  2. exec
    • exec 方法用于执行一个命令并缓冲输出,当输出较大时可能会导致内存问题。
    • 它返回一个 ChildProcess 实例,并通过回调函数提供执行结果。
    • exec 方法接受命令字符串和可选的回调函数及配置对象。
  3. execFile
    • execFile 类似于 exec,但它直接执行指定的文件,而不是通过 shell。
    • 这可以提高性能,因为它避免了额外的 shell 解释过程。
    • execFile 接受文件路径、参数数组、可选的回调函数及配置对象。
  4. fork
    • forkspawn 的一个特例,专门用于衍生新的 Node.js 进程。
    • 它允许父进程与子进程之间通过 IPC(Inter-Process Communication)进行通信。
    • fork 方法接受模块文件路径和可选的参数数组及配置对象。
  5. pty
    • pty 模块(需要单独安装)可以创建伪终端,允许 Node.js 应用程序模拟终端环境。
    • 这对于需要交互式命令行界面的应用非常有用。 子进程模块还提供了一系列事件和方法,用于管理子进程的生命周期和交互,例如:
  • 事件exitcloseerrormessage 等。
  • 方法killsenddisconnect 等。 使用子进程时,需要注意以下几点:
  • 性能:创建和管理子进程是有开销的,应避免不必要的子进程创建。
  • 安全性:执行外部命令时,应避免注入攻击,确保传递给子进程的参数是安全的。
  • 资源管理:子进程会占用系统资源,应确保子进程在完成任务后正确退出。 子进程是 Node.js 中实现并行处理和系统交互的重要工具,合理使用可以大大扩展 Node.js 应用的能力。

31. 说说你对 Source Map 的了解

Source Map 是一种提供源代码到编译后代码之间映射的技术。它通常用于将压缩、转换或编译后的代码(如 JavaScript、CSS 等)映射回原始源代码,以便于调试和错误追踪。以下是关于 Source Map 的一些关键点:

工作原理

  1. 生成 Source Map
    • 在代码压缩、转换或编译的过程中,工具(如 Webpack、Babel、UglifyJS 等)会生成一个 Source Map 文件。
    • 这个文件包含了源代码与编译后代码之间的映射关系。
  2. 使用 Source Map
    • 浏览器或开发工具(如 Chrome DevTools)会根据 Source Map 文件将编译后的代码映射回原始源代码。
    • 当发生错误或断点时,开发者可以看到原始源代码中的位置,而不是编译后的代码。

优点

  • 调试友好:允许开发者直接调试原始源代码,而不是难以阅读的编译后代码。
  • 错误追踪:错误信息会指向原始源代码中的正确位置,便于定位和修复问题。
  • 支持多种语言:不仅限于 JavaScript,还支持 CSS、TypeScript、CoffeeScript 等。

应用场景

  • 前端开发:在开发过程中,使用 Source Map 可以方便地调试压缩或转换后的 JavaScript 和 CSS 文件。
  • 构建工具:Webpack、Babel 等构建工具支持生成 Source Map,以方便开发者在开发模式下调试。
  • 生产环境:虽然生产环境通常不包含 Source Map,但在需要调试生产环境问题时,可以临时启用。

注意事项

  • 安全性:Source Map 可能会暴露源代码结构,因此在生产环境中应谨慎使用。
  • 性能:生成和使用 Source Map 会增加构建时间和调试时的性能开销。
  • 版本控制:确保 Source Map 与对应的编译后代码版本一致,以避免映射错误。

示例

// 原始源代码:index.js
function add(a, b) {
  return a + b;
}
// 编译后代码:index.min.js
function add(a,b){return a+b;}
// Source Map 示例(简化版)
{
  "version": 3,
  "file": "index.min.js",
  "sources": ["index.js"],
  "names": ["add", "a", "b"],
  "mappings": "AAAA,CAAC,CAAC,GAAG,CAAC,CAAC"
}

在这个示例中,Source Map 提供了 index.min.js 中每个字符与 index.js 中对应位置的映射关系。

总结

Source Map 是现代前端开发中不可或缺的一部分,它极大地提高了开发效率和调试体验。然而,也需要注意其安全性和性能影响,合理地在开发和使用过程中进行管理。

32. 哪些原因会导致js里this指向混乱?

在JavaScript中,this的指向是根据函数的调用方式来确定的,而不是在函数定义时。这种灵活性有时会导致this指向混乱,以下是一些常见的原因:

  1. 全局上下文中的this
    • 在全局执行上下文中(即不在任何函数内部),this通常指向全局对象(在浏览器中是window,在Node.js中是global)。
  2. 函数调用
    • 普通函数调用时,this指向全局对象。在严格模式下,this会指向undefined
  3. 方法调用
    • 当函数作为对象的方法被调用时,this指向该对象。
  4. 构造函数
    • 使用new关键字调用函数时,this指向新创建的对象。
  5. 间接调用
    • 使用callapplybind方法可以显式指定this的值。
  6. 箭头函数
    • 箭头函数不绑定自己的this,它会捕获其所在上下文的this值。 以下是一些导致this指向混乱的具体场景:

1. 普通函数内部的this

function foo() {
  console.log(this); // 在非严格模式下指向全局对象,严格模式下指向undefined
}
foo();

2. 方法内部的this

const obj = {
  method: function() {
    console.log(this); // 指向obj对象
  }
};
obj.method();

3. 函数作为回调

const obj = {
  method: function() {
    setTimeout(function() {
      console.log(this); // 指向全局对象,因为setTimeout中的函数是普通函数调用
    }, 1000);
  }
};
obj.method();

4. 箭头函数

const obj = {
  method: function() {
    setTimeout(() => {
      console.log(this); // 指向obj对象,因为箭头函数捕获了method函数的this
    }, 1000);
  }
};
obj.method();

5. 构造函数

function Foo() {
  this.bar = 'bar';
}
const foo = new Foo();
console.log(foo.bar); // 'bar',这里的this指向新创建的foo对象

6. 间接调用

function foo() {
  console.log(this);
}
const obj = { bar: 'bar' };
foo.call(obj); // 显式将this指向obj

7. DOM事件处理函数

document.getElementById('myButton').addEventListener('click', function() {
  console.log(this); // 指向触发事件的元素,即myButton
});

8. 在对象字面量中定义函数

const obj = {
  foo: 'bar',
  method: function() {
    console.log(this.foo); // 正确,this指向obj
  },
  wrongMethod: () => {
    console.log(this.foo); // 可能导致错误,this不指向obj,而是指向外层上下文的this
  }
};
obj.method();
obj.wrongMethod();

避免混乱的方法

  • 使用箭头函数:在需要保持this指向的场合,使用箭头函数可以避免this指向改变。
  • 显式绑定this:使用callapplybind方法显式指定this的值。
  • 避免在全局作用域中定义函数:尽量将函数定义为对象的方法或使用模块化。
  • 理解上下文:在编写代码时,始终注意函数的调用方式和上下文。 通过理解这些场景和规则,可以更好地控制this的指向,避免混乱。

33. 使用 react-router 跳转时,如何将参数传递给下一个页面?

在使用 react-router 进行页面跳转时,可以通过以下几种方式传递参数给下一个页面:

1. 使用 URL 参数(Params)

通过在路由路径中定义参数,可以在组件中通过 useParams 钩子获取这些参数。 定义路由:

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
<Router>
  <Switch>
    <Route path="/details/:id" component={DetailsPage} />
  </Switch>
</Router>

获取参数:

import { useParams } from 'react-router-dom';
function DetailsPage() {
  const { id } = useParams();
  // 使用 id 参数
}

跳转时传递参数:

import { useHistory } from 'react-router-dom';
function SomeComponent() {
  const history = useHistory();
  const goToDetails = (id) => {
    history.push(`/details/${id}`);
  };
  return <button onClick={() => goToDetails('123')}>Go to Details</button>;
}

2. 使用查询字符串(Query Strings)

通过在 URL 中添加查询字符串,可以在组件中通过 useLocation 钩子获取这些参数。 跳转时传递参数:

import { useHistory } from 'react-router-dom';
function SomeComponent() {
  const history = useHistory();
  const goToDetails = () => {
    history.push('/details?id=123&name=John');
  };
  return <button onClick={goToDetails}>Go to Details</button>;
}

获取参数:

import { useLocation } from 'react-router-dom';
function DetailsPage() {
  const location = useLocation();
  const queryParams = new URLSearchParams(location.search);
  const id = queryParams.get('id');
  const name = queryParams.get('name');
  // 使用 id 和 name 参数
}

3. 使用状态对象(State)

通过 history.push 方法传递一个状态对象,可以在组件中通过 useHistory 钩子获取这个状态。 跳转时传递参数:

import { useHistory } from 'react-router-dom';
function SomeComponent() {
  const history = useHistory();
  const goToDetails = () => {
    history.push('/details', { id: '123', name: 'John' });
  };
  return <button onClick={goToDetails}>Go to Details</button>;
}

获取参数:

import { useHistory } from 'react-router-dom';
function DetailsPage() {
  const history = useHistory();
  const { id, name } = history.location.state || {};
  // 使用 id 和 name 参数
}

4. 使用上下文(Context)或全局状态管理

如果参数需要在多个组件之间共享,可以使用 React 的上下文(Context)或者全局状态管理库(如 Redux)来传递参数。 使用 Context:

import { createContext, useContext } from 'react';
const UserContext = createContext(null);
// 在顶层组件提供 Context
<UserContext.Provider value={{ id: '123', name: 'John' }}>
  <Router>
    <Switch>
      <Route path="/details" component={DetailsPage} />
    </Switch>
  </Router>
</UserContext.Provider>
// 在 DetailsPage 组件中获取 Context
function DetailsPage() {
  const user = useContext(UserContext);
  // 使用 user.id 和 user.name
}

选择哪种方式取决于你的具体需求和偏好。URL 参数和查询字符串更适合于那些不需要敏感信息的场景,而状态对象和上下文更适合于在组件间传递复杂或敏感数据。

34. vue3 相比较于 vue2,在编译阶段有哪些改进?

Vue 3 在编译阶段相对于 Vue 2 进行了多项改进,以提高性能、可维护性和开发体验。以下是一些主要的改进:

  1. 性能优化
    • 更快的编译速度:Vue 3 的编译器进行了重构,以实现更快的编译速度。
    • 更小的打包体积:通过tree-shaking和更好的代码分割,Vue 3 的打包体积更小。
  2. 编译器架构
    • 模块化设计:Vue 3 的编译器采用了模块化设计,使得代码更加模块化和可维护。
    • 可扩展性:编译器插件系统使得开发者可以更轻松地扩展和自定义编译过程。
  3. 静态树提升(Static Tree Hoisting)
    • Vue 3 可以识别并提升静态树,从而减少不必要的渲染和重渲染,提高性能。
  4. 静态属性提升(Static Props Hoisting)
    • 类似于静态树提升,Vue 3 还可以提升静态属性,进一步减少运行时的负担。
  5. 更好的类型推断
    • Vue 3 在编译阶段提供了更好的类型推断,使得与 TypeScript 的集成更加顺畅。
  6. 编译时警告
    • Vue 3 在编译阶段可以提供更多的警告和错误信息,帮助开发者更快地发现问题。
  7. 支持更多的语言特性
    • Vue 3 的编译器支持更多的现代 JavaScript 语言特性,如可选链(Optional Chaining)、空值合并运算符(Nullish Coalescing)等。
  8. 更好的错误处理
    • Vue 3 在编译阶段进行了更好的错误处理,提供了更清晰的错误信息和堆栈跟踪。
  9. 自定义渲染器
    • Vue 3 引入了自定义渲染器的概念,允许开发者使用不同的渲染策略,如 DOM、SVG、Canvas 等。
  10. 更高效的更新算法
    • Vue 3 引入了更高效的更新算法,如 Proxy-based reactivity,减少了不必要的虚拟 DOM 更新。
  11. 更好的SSR支持
    • Vue 3 对服务器端渲染(SSR)进行了优化,提高了 SSR 的性能和可扩展性。
  12. 组件初始化性能提升
    • Vue 3 通过优化组件的初始化过程,减少了组件创建和挂载的时间。 这些改进使得 Vue 3 在编译阶段更加高效、灵活和强大,为开发者提供了更好的开发体验和更优化的应用程序性能。

35. HTTP Header 中有哪些信息?

HTTP 头部(HTTP Header)是 HTTP 请求和响应的一部分,它们包含了关于请求或响应的元数据。HTTP 头部可以分为几种类型:通用头部、请求头部、响应头部和实体头部。以下是一些常见的 HTTP 头部信息:

通用头部(General Headers)

  • Cache-Control:指定请求/响应的缓存机制。
  • Connection:控制客户端和服务器之间的连接策略,如保持连接(Keep-Alive)。
  • Date:创建报文的日期和时间。
  • Pragma:包含实现特定的指令,通常用于控制缓存(已被 Cache-Control 取代)。
  • Trailer:指示在报文主体后发送的头部字段。
  • Transfer-Encoding:指示报文主体的传输编码方式,如chunked。
  • Upgrade:用于升级到其他协议。
  • Via:追踪报文经过的代理服务器。
  • Warning:关于报文可能的问题的警告。

请求头部(Request Headers)

  • Accept:指定客户端能够接收的内容类型。
  • Accept-Charset:指定客户端能够接收的字符集。
  • Accept-Encoding:指定客户端能够接收的内容编码。
  • Accept-Language:指定客户端能够接收的语言。
  • Authorization:用于提供请求的认证信息。
  • Cookie:发送到服务器的cookie。
  • Expect:指示服务器需要的特定行为。
  • From:请求发送者的电子邮件地址。
  • Host:指定请求的服务器域名和端口号。
  • If-Match:用于条件请求,如果匹配则执行请求。
  • If-Modified-Since:用于条件请求,如果自指定日期以来文档被修改则执行请求。
  • If-None-Match:用于条件请求,如果匹配则不执行请求。
  • If-Range:用于条件请求,如果范围有效则执行请求。
  • If-Unmodified-Since:用于条件请求,如果自指定日期以来文档未被修改则执行请求。
  • Max-Forwards:限制请求可以经过的代理服务器数量。
  • Proxy-Authorization:用于提供代理服务器的认证信息。
  • Range:请求实体的部分内容。
  • Referer:表示请求发起方的来源地址。
  • TE:指定传输编码的优先级。
  • User-Agent:包含发送请求的用户的代理信息。

响应头部(Response Headers)

  • Accept-Ranges:指示服务器是否接受范围请求。
  • Age:响应对象在代理缓存中存在的时长。
  • ETag:资源的实体标签,用于缓存验证。
  • Location:用于重定向,表示新的资源位置。
  • Proxy-Authenticate:代理服务器请求客户端的认证信息。
  • Retry-After:指示客户端多久后可以再次发送请求。
  • Server:包含服务器软件的信息。
  • Vary:告诉缓存服务器如何判断缓存的有效性。
  • WWW-Authenticate:用于HTTP认证的认证信息。

实体头部(Entity Headers)

  • Allow:指定资源支持的所有HTTP请求方法。
  • Content-Encoding:实体内容的编码方式。
  • Content-Language:实体内容使用的语言。
  • Content-Length:实体内容的长度。
  • Content-Location:实体内容的位置。
  • Content-MD5:实体内容的MD5摘要,用于检查完整性。
  • Content-Range:实体内容的部分范围。
  • Content-Type:实体内容的媒体类型。
  • Expires:实体内容的过期时间。
  • Last-Modified:实体内容最后一次被修改的时间。 这些头部信息可以提供关于请求和响应的详细信息,帮助客户端和服务器更好地处理HTTP通信。需要注意的是,并不是每个HTTP请求或响应都会包含所有这些头部,它们可以根据需要进行添加。此外,还有一些自定义的头部字段,可以根据特定的应用需求进行定义。

36. 304 状态码用于什么场景, 好处和坏处分别是什么?

304 状态码的场景: 304 状态码(Not Modified)用于 HTTP 条件请求中,表示客户端发送的请求内容与服务器上的资源相比没有发生变化。这种情况下,服务器不会返回资源的内容,而是告诉客户端可以使用本地缓存的版本。 常见场景包括:

  1. 缓存验证:客户端通过 If-Modified-SinceIf-None-Match 头部发送上次获取资源的时间或实体标签(ETag),服务器检查资源是否在此时间后修改或ETag是否匹配。
  2. 减少带宽使用:当资源未发生变化时,避免重复传输相同的内容。
  3. 提高加载速度:客户端可以直接使用本地缓存,减少等待服务器响应的时间。 好处:
  4. 节省带宽:由于不需要重新传输未更改的资源,可以显著减少网络带宽的使用。
  5. 提高性能:减少数据传输量,加快页面加载速度,提升用户体验。
  6. 减轻服务器负载:服务器不需要处理完整的请求和响应周期,减少了计算和存储资源的消耗。
  7. 缓存一致性:通过ETag和最后修改时间等机制,确保客户端使用的缓存版本与服务器上的资源保持一致。 坏处:
  8. 缓存失效问题:如果缓存策略设置不当或资源更新后未能正确更新缓存标识,可能导致客户端使用过时的资源。
  9. 复杂度增加:实现条件请求和缓存机制需要额外的开发和管理成本。
  10. 首次访问延迟:对于首次访问或缓存失效的情况,客户端需要等待完整的请求响应周期,可能存在延迟。
  11. 兼容性问题:某些老旧的浏览器或代理服务器可能不完全支持HTTP缓存机制,导致不一致的行为。 总的来说,304 状态码在正确使用的情况下可以带来显著的性能提升和带宽节省,但需要注意缓存策略的管理和潜在的问题。

37. 说说对 XMLHttpRequest 对象的了解

XMLHttpRequest对象是浏览器提供的一个API,用于在后台与服务器交换数据。这是实现AJAX(Asynchronous JavaScript and XML)技术的核心对象,使得网页能够异步更新,即在不需要重新加载整个页面的情况下,与服务器进行数据交互并更新部分网页内容。 主要特点:

  1. 异步通信:可以在不干扰用户操作的情况下,在后台发送和接收数据。
  2. 兼容性:广泛支持各种浏览器,包括老旧的IE版本。
  3. 灵活性:可以发送不同类型的数据(如文本、JSON、XML等),并处理各种响应类型。 基本用法:
  4. 创建对象
    var xhr = new XMLHttpRequest();
    
  5. 配置请求
    • 指定请求方法(GET、POST等)和URL:
      xhr.open('GET', 'https://example.com/api/data', true);
      
    • 设置请求头(如果需要):
      xhr.setRequestHeader('Content-Type', 'application/json');
      
  6. 发送请求
    • 对于GET请求,可以直接发送:
      xhr.send();
      
    • 对于POST请求,需要传递数据:
      xhr.send(JSON.stringify({ key: 'value' }));
      
  7. 处理响应
    • 监听readystatechange事件,该事件在请求状态发生变化时触发:
      xhr.onreadystatechange = function() {
        if (xhr.readyState === 4 && xhr.status === 200) {
          console.log(xhr.responseText);
        }
      };
      
    • readyState属性表示请求的状态,值为4表示请求已完成。
    • status属性表示HTTP状态码,值为200表示成功。 高级用法:
  8. 设置超时
    xhr.timeout = 3000; // 设置超时时间(毫秒)
    xhr.ontimeout = function() {
      console.log('请求超时');
    };
    
  9. 处理不同类型的响应
    • 通过responseType属性指定响应类型,如'json'、'blob'等。
    • 使用response属性获取响应数据。
  10. 上传进度
    • 监听upload对象的progress事件,可以获取上传进度。 局限性:
  11. 不兼容新API:随着Fetch API的推出,XMLHttpRequest的一些局限性变得明显,如语法复杂、不支持Promise等。
  12. 跨域问题:需要服务器端支持CORS(跨源资源共享)才能实现跨域请求。 尽管有这些局限性,XMLHttpRequest仍然在一些老旧的项目或特定场景下被使用。然而,对于新项目,通常推荐使用更现代、更易用的Fetch API。

38. 说说对数字证书的了解

数字证书是一种用于在互联网上验证身份、确保数据传输安全性的电子文件。它由权威的证书颁发机构(CA,Certificate Authority)发行,用于证明某个公钥属于特定的个体、组织或设备。 主要组成部分:

  1. 证书持有者信息:包括名称、组织、国家等。
  2. 公钥:用于加密数据或验证签名。
  3. 有效期:证书的有效起始和终止日期。
  4. 颁发机构信息:颁发证书的CA的名称和数字签名。
  5. 序列号:用于唯一标识证书。
  6. 签名算法:用于生成和验证数字签名的算法。 工作原理:
  7. 生成密钥对:首先,用户生成一对密钥,包括公钥和私钥。
  8. 申请证书:用户将公钥和其他相关信息提交给CA,申请数字证书。
  9. 验证信息:CA验证用户信息的真实性和合法性。
  10. 颁发证书:验证通过后,CA使用自己的私钥对用户信息及其公钥进行数字签名,生成数字证书。
  11. 使用证书:用户可以将数字证书用于身份验证、数据加密、数字签名等场景。 主要用途:
  12. 身份验证:证明通信双方的身份,防止中间人攻击。
  13. 数据加密:使用公钥加密数据,只有持有对应私钥的接收者才能解密。
  14. 数字签名:使用私钥对数据进行签名,接收者使用公钥验证签名,确保数据完整性和来源真实性。
  15. SSL/TLS:在HTTPS协议中,用于建立安全的数据传输通道。 类型:
  16. SSL证书:用于网站安全,实现HTTPS加密。
  17. 代码签名证书:用于验证软件发布者的身份和代码的完整性。
  18. 电子邮件证书:用于加密和签名电子邮件。
  19. 客户端证书:用于客户端身份验证。 优点:
  20. 安全性:通过加密和数字签名技术,确保数据传输的安全性和完整性。
  21. 可信度:由权威CA颁发,具有很高的可信度。
  22. 便捷性:易于部署和使用,广泛应用于各种网络安全场景。 局限性:
  23. 依赖CA:如果CA的私钥被泄露,可能会影响所有颁发的证书的安全性。
  24. 管理复杂:需要定期更新和维护证书,管理较为复杂。
  25. 成本:一些高级证书可能需要支付较高的费用。 总之,数字证书是网络安全的重要组成部分,广泛应用于各种需要验证身份和确保数据安全的场景。随着网络安全需求的不断增加,数字证书的重要性也越来越突出。

39. TCP 粘包(TCP packet sticking)是什么?

TCP粘包是指在TCP数据传输过程中,发送方发送的若干个数据包到接收方时,接收方可能会将它们合并为一个数据包进行处理的现象。这种现象通常发生在TCP连接中,因为TCP是一个基于字节流的协议,它并不保证发送的数据包会按照发送时的边界到达接收方。 粘包的原因:

  1. TCP缓冲区:TCP发送方和接收方都有缓冲区,发送方将数据发送到缓冲区,接收方从缓冲区读取数据。如果发送方发送的数据小于缓冲区大小,TCP可能会等待更多的数据到来后再一起发送,这就导致了粘包。
  2. Nagle算法:TCP使用Nagle算法来减少小数据包的发送,以提高网络效率。Nagle算法会合并小的数据包,等到一定条件满足后再发送,这也可能导致粘包。
  3. 接收方读取方式:接收方在读取数据时,可能会一次性读取多个数据包的内容,而不是按照发送方的数据包边界来读取。 粘包的影响: 粘包可能会导致接收方在处理数据时出现困难,因为接收方可能无法准确判断数据包的边界,从而无法正确解析数据。 解决粘包的方法:
  4. 发送方控制:发送方可以在数据包之间插入特定的分隔符,或者在每个数据包前添加长度字段,指示数据包的长度。
  5. 接收方控制:接收方可以根据发送方提供的分隔符或长度字段来解析数据包。
  6. 使用TCP_NODELAY选项:在TCP连接中设置TCP_NODELAY选项可以禁用Nagle算法,减少粘包的可能性。
  7. 使用更高级的协议:例如,使用HTTP、WebSocket等应用层协议,这些协议在TCP之上定义了自己的数据格式和边界,可以有效避免粘包问题。 示例: 假设客户端向服务器发送了两个字符串"hello"和"world",由于粘包,服务器可能收到一个合并后的字符串"hello world"。为了解决这个问题,客户端可以在发送每个字符串后发送一个特殊的分隔符,如"\n",服务器则根据这个分隔符来解析收到的数据。 注意: 粘包并不是TCP协议的错误,而是其设计特性的一部分。在实际应用中,开发者需要根据具体情况选择合适的策略来处理粘包问题。

40. token过期后,页面如何实现无感刷新?

实现Token过期后的无感刷新通常涉及到前端和后端的配合。以下是一种常见的实现方式:

后端设计

  1. Token和Refresh Token
    • 后端在用户登录时返回两个Token:Access Token和Refresh Token。
    • Access Token具有较短的有效期,用于访问受保护的资源。
    • Refresh Token具有较长的有效期,用于在Access Token过期后获取新的Access Token。
  2. Token过期响应
    • 当Access Token过期时,后端应返回特定的错误码(如401 Unauthorized)和错误信息,指示客户端需要刷新Token。

前端设计

  1. 拦截器/中间件
    • 前端使用拦截器或中间件拦截所有的HTTP请求。
    • 拦截器检查响应状态码和错误信息,判断是否因为Token过期。
  2. 刷新Token
    • 如果检测到Token过期,前端使用存储的Refresh Token向特定的后端接口发送请求,以获取新的Access Token。
    • 获取新的Access Token后,更新存储的Token,并重新发起原始请求。
  3. 无感刷新
    • 在刷新Token的过程中,前端可以显示一个加载指示器或遮罩层,以提供用户反馈。
    • 刷新Token的操作应尽可能快速,以减少用户感知的延迟。
    • 刷新完成后,自动重新发起原始请求,用户无需手动刷新页面或重新登录。

示例流程

  1. 用户发起请求 -> Access Token过期 -> 后端返回401错误。
  2. 前端拦截器捕获401错误 -> 发起刷新Token请求(使用Refresh Token)。
  3. 后端验证Refresh Token -> 发放新的Access Token。
  4. 前端更新Access Token -> 重新发起原始请求。
  5. 用户看到的结果是请求成功,无需额外操作。

注意事项

  • 安全性:确保Refresh Token的安全存储和传输,避免泄露。
  • 并发请求:处理多个并发请求时,避免重复刷新Token。
  • 错误处理:考虑Refresh Token也过期或无效的情况,此时可能需要用户重新登录。
  • 用户体验:尽量减少刷新Token对用户的影响,提供良好的用户体验。 通过上述设计,可以实现Token过期后的无感刷新,提升用户的使用体验。

41. 什么是 HTML 文档的预解析?

HTML文档的预解析(也称为预加载或预取)是浏览器的一种优化技术,它可以在解析HTML文档的过程中,提前加载和解析文档中可能需要的资源,以减少后续资源加载的延迟,提高页面加载速度和用户体验。 预解析可以包括以下几种类型:

  1. DNS预解析(DNS Prefetching)
    • 浏览器提前解析文档中提到的域名,以便在后续请求时能够更快地连接到这些域名。
    • 通过<link rel="dns-prefetch" href="://example.com">实现。
  2. TCP预连接(TCP Preconnection)
    • 浏览器提前建立与特定域名的TCP连接,包括DNS解析、TCP握手和TLS协商(如果使用HTTPS)。
    • 通过<link rel="preconnect" href="://example.com">实现。
  3. 资源预加载(Resource Prefetching)
    • 浏览器提前加载文档中可能需要的资源,如脚本、样式表、图片等。
    • 通过<link rel="prefetch" href="resource.js">实现。
  4. 子资源预加载(Subresource Prefetching)
    • 类似于资源预加载,但更具体地指明预加载的是子资源,如脚本或样式表中的特定文件。
    • 通过<link rel="subresource" href="resource.js">实现(注意:subresource是一个非标准的rel值,可能不被所有浏览器支持)。
  5. 链接预渲染(Link Prerendering)
    • 浏览器提前渲染页面中的链接,以便在用户点击时能够立即显示。
    • 通过<link rel="prerender" href="next-page.html">实现。 预解析的好处包括:
  • 减少延迟:通过提前完成DNS解析、TCP连接和资源加载,可以减少用户请求资源时的延迟。
  • 提高性能:提前加载资源可以减少页面渲染时间,提高页面加载速度。
  • 提升用户体验:用户感受到的页面加载速度更快,体验更流畅。 然而,预解析也需要谨慎使用,因为如果不当地预加载大量资源,可能会导致不必要的网络流量和资源浪费,甚至可能影响页面的整体性能。因此,建议根据页面的实际需求和资源的重要性来合理使用预解析技术。

42. PostCSS 是什么,有什么作用?

PostCSS 是一个用 JavaScript 工具和插件转换 CSS 代码的工具。它本身不提供具体的样式处理功能,而是通过插件系统来扩展其功能。PostCSS 的主要作用包括:

  1. CSS预处理器
    • 类似于Sass、Less等预处理器,PostCSS可以通过插件支持变量、嵌套、混合(mixins)、函数等高级功能。
  2. 自动添加浏览器前缀
    • 通过插件如autoprefixer,PostCSS可以自动为CSS规则添加所需的浏览器前缀,以确保兼容性。
  3. CSS模块
    • 支持将CSS模块化,避免全局污染,提高样式的可维护性。
  4. 压缩和优化CSS
    • 通过插件如cssnano,PostCSS可以压缩CSS代码,删除冗余和无效的代码,减少文件大小。
  5. 支持未来CSS语法
    • PostCSS可以通过插件支持尚未广泛实现的CSS新特性,允许开发者使用未来的CSS语法。
  6. 自定义插件
    • 开发者可以编写自定义插件来满足特定的需求,如样式转换、代码检查等。
  7. 与构建工具集成
    • PostCSS可以与Webpack、Gulp、Grunt等构建工具集成,作为CSS处理流程的一部分。
  8. 样式检查和校验
    • 通过插件如stylelint,PostCSS可以检查CSS代码的风格和质量,确保代码的一致性。 PostCSS 的优势在于其灵活性和可扩展性。它不强制使用特定的语法或功能,而是允许开发者根据项目需求选择和配置插件。这种模块化的 approach 使得 PostCSS 成为现代前端开发中非常受欢迎的CSS处理工具。

43. html 元素节点上, 有多个 class 名称,这几个class 名称对应的样式渲染优先级是如何的?

在HTML元素节点上,如果有多个class名称,这些class对应的样式渲染优先级是由CSS的特异性(specificity)和声明顺序(source order)决定的。 特异性: 特异性是CSS选择器的一个衡量标准,用于确定当多个规则应用于同一元素时,哪个规则最终生效。特异性由四个部分组成,可以表示为一个四位数(a, b, c, d):

  • a:如果声明是在style属性中(内联样式),则为1,否则为0。
  • b:ID选择器的数量。
  • c:类选择器、属性选择器、伪类选择器的数量。
  • d:元素选择器和伪元素选择器的数量。 优先级规则
  1. 内联样式(inline style)的特异性最高,因为a的值为1。
  2. ID选择器的特异性高于类选择器,因为b的值大于c。
  3. 类选择器的特异性高于元素选择器,因为c的值大于d。
  4. 如果特异性相同,则后定义的规则会覆盖先定义的规则。 多个class的优先级: 当同一个元素上有多个class时,这些class的特异性是相加的。例如:
<div class="class1 class2"></div>
.class1 { color: red; } /* 特异性为 (0, 0, 1, 0) */
.class2 { color: blue; } /* 特异性为 (0, 0, 1, 0) */

在这个例子中,.class1.class2的特异性相同,因此哪个样式最终生效取决于它们在CSS中的声明顺序。如果.class2.class1之后定义,那么文本颜色将为蓝色。 示例

<div id="myId" class="class1 class2"></div>
#myId { color: red; } /* 特异性为 (0, 1, 0, 0) */
.class1 { color: blue; } /* 特异性为 (0, 0, 1, 0) */
.class2 { color: green; } /* 特异性为 (0, 0, 1, 0) */

在这个例子中,#myId的特异性最高,因此文本颜色将为红色,即使.class1.class2在CSS中定义在#myId之后。 注意

  • !important声明可以覆盖任何其他声明,但不建议滥用,因为它会破坏CSS的层叠规则。
  • 特异性计算不涉及通配选择器(*)、子选择器(>)、相邻兄弟选择器(+)和兄弟选择器(~),因为这些选择器不增加特异性。 总之,多个class的样式渲染优先级是由它们的特异性和在CSS中的声明顺序共同决定的。在编写CSS时,应尽量保持选择器的简洁和特异性的一致性,以避免不必要的复杂性。

44. 如何避免全局样式污染?

避免全局样式污染是前端开发中的一个重要考虑,尤其是在大型项目中。以下是一些常用的方法来避免全局样式污染:

  1. 使用CSS模块(CSS Modules): CSS模块是一种将CSS类名局部化的技术,通过编译过程生成唯一的类名,从而避免全局冲突。
    /* styles.module.css */
    .myClass {
      color: red;
    }
    
    import styles from './styles.module.css';
    <div className={styles.myClass}>Hello, World!</div>
    
  2. 使用预处理器(如Sass、Less)的命名约定: 通过使用更具体的前缀或BEM(Block Element Modifier)命名方法来命名类名,减少冲突的可能性。
    .my-component__element--modifier {
      color: red;
    }
    
  3. 使用CSS-in-JS库: CSS-in-JS是一种将CSS直接写在JavaScript中的技术,可以生成局部作用域的样式。
    import styled from 'styled-components';
    const MyDiv = styled.div`
      color: red;
    `;
    
  4. 使用Shadow DOM: Web组件的Shadow DOM提供了一种封装样式的方法,使得组件内部的样式不会影响到外部。
    const shadowRoot = element.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `<style>:host { color: red; }</style><div>Hello, World!</div>`;
    
  5. 使用命名空间: 为项目或组件的样式定义一个独特的命名空间,所有类名都以此命名空间为前缀。
    .my-project__myClass {
      color: red;
    }
    
  6. 使用ID选择器: 虽然不推荐过度使用ID选择器,但在某些情况下,它们可以提供更高的特异性,减少冲突。
    #myUniqueElement {
      color: red;
    }
    
  7. 使用组件库或UI框架: 许多现代UI框架和组件库(如React Bootstrap、Ant Design等)已经考虑了样式隔离的问题,使用它们可以减少样式冲突。
  8. 避免使用通配符选择器: 通配符选择器(*)会影响所有元素,应尽量避免使用,以减少全局样式的影响。
  9. 使用!important谨慎!important会覆盖其他所有样式,应谨慎使用,避免造成难以追踪的样式问题。
  10. 模块化组件: 将组件设计为自包含的,每个组件只负责自己的样式,不依赖于外部样式。 通过结合以上方法,可以有效地避免全局样式污染,使得样式更加可维护和可扩展。在实际开发中,根据项目需求和团队约定选择合适的方法。

45. web 应用中如何对静态资源加载失败的场景做降级处理?

在Web应用中,对静态资源加载失败的场景进行降级处理是提高应用健壮性和用户体验的重要手段。以下是一些常见的降级处理策略:

  1. 备用资源: 为关键资源提供备用版本,例如,如果高分辨率的图片加载失败,可以自动切换到低分辨率的图片或占位符。
    <img src="high-res-image.jpg" onerror="this.onerror=null; this.src='low-res-image.jpg'">
    
  2. 占位符: 对于图片、视频等媒体资源,可以在加载失败时显示一个预定义的占位符。
    <img src="image.jpg" onerror="this.onerror=null; this.src='placeholder.png'">
    
  3. 错误处理样式: 为加载失败的资源设置特定的样式,例如,为失败的图片设置背景颜色或边框。
    img加载失败 {
      background-color: #f0f0f0;
      border: 1px solid #ccc;
    }
    
  4. 重试机制: 实现自动重试机制,当资源加载失败时,自动尝试重新加载。
    function retryLoad(url, attempts) {
      return new Promise((resolve, reject) => {
        function attempt() {
          fetch(url).then(resolve).catch(() => {
            if (--attempts > 0) {
              setTimeout(attempt, 2000); // 重试间隔
            } else {
              reject();
            }
          });
        }
        attempt();
      });
    }
    
  5. 服务端降级: 在服务端实现降级逻辑,当检测到资源服务器不可用时,自动提供备用资源或降级内容。
  6. 缓存策略: 利用浏览器缓存或服务端缓存,确保即使在资源服务器不可用时,用户仍可以使用缓存的资源。
  7. 监控和报警: 实现资源加载监控,当检测到大量加载失败时,触发报警,以便及时处理。
  8. 用户提示: 当资源加载失败时,向用户显示友好的错误信息,提供刷新页面或联系支持的选项。
    <div id="resource-error" style="display:none;">资源加载失败,请<a href="javascript:location.reload()">刷新页面</a>或联系支持。</div>
    <script>
    document.getElementById('some-resource').onerror = function() {
      document.getElementById('resource-error').style.display = 'block';
    };
    </script>
    
  9. 使用CDN: 利用CDN(内容分发网络)提高资源加载的可靠性和速度,CDN通常具备自动降级和备份机制。
  10. 前端路由降级: 对于单页应用(SPA),可以在前端路由层面实现降级,当关键资源加载失败时,重定向到备用页面或错误页面。
  11. 模块化加载: 使用模块化加载(如AMD、CommonJS、ES6 Modules),可以实现对单个模块的加载失败处理,而不会影响整个应用的运行。 通过结合以上策略,可以根据具体需求和场景实现对静态资源加载失败的有效降级处理,提高Web应用的稳定性和用户体验。

46. html 中前缀为 data- 开头的元素属性是什么?

在HTML中,前缀为data-开头的元素属性被称为自定义数据属性(Custom Data Attributes)。这些属性允许我们在HTML元素上存储额外的信息,这些信息可以由JavaScript访问和使用,而不会干扰元素的其它属性或DOM API。 自定义数据属性的语法如下:

<element data-key="value">

其中data-key是属性的名称,value是属性的值。你可以根据需要定义任何数量的自定义数据属性。 例如:

<div id="myElement" data-user="12345" data-role="admin"></div>

在JavaScript中,可以使用dataset属性来访问这些自定义数据属性:

var element = document.getElementById('myElement');
var userId = element.dataset.user; // "12345"
var userRole = element.dataset.role; // "admin"

自定义数据属性是HTML5引入的一项功能,它们为前端开发者提供了一种在元素上附加数据的灵活方式,而不需要使用非标准的属性或额外的DOM元素。这些属性对于实现一些特定的功能,如数据绑定、组件状态管理等非常有用。

47. 判断数组的方式有哪些?

在JavaScript中,判断一个变量是否为数组有多种方式,以下是几种常用的方法:

  1. 使用instanceof操作符
    var arr = [1, 2, 3];
    console.log(arr instanceof Array); // true
    
    这种方法可以检测变量是否是数组,并且它还考虑了变量的上下文(即它是在哪个窗口或框架中定义的)。
  2. 使用Array.isArray()方法
    var arr = [1, 2, 3];
    console.log(Array.isArray(arr)); // true
    
    Array.isArray()是ES5中引入的方法,它返回一个布尔值,表示传入的参数是否是一个数组。这个方法不会考虑变量的上下文,因此在多框架或多窗口环境中更为可靠。
  3. 使用Object.prototype.toString.call()
    var arr = [1, 2, 3];
    console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true
    
    这种方法使用Object.prototype.toString方法来检测变量的内部[[Class]]属性,它对于任何对象都是有效的,并且不受变量上下文的影响。
  4. 使用constructor属性
    var arr = [1, 2, 3];
    console.log(arr.constructor === Array); // true
    
    这种方法检查变量的constructor属性是否指向Array构造函数。然而,如果变量是在不同的上下文中创建的,这个方法可能会失败。
  5. 使用typeof操作符(不推荐)
    var arr = [1, 2, 3];
    console.log(typeof arr); // "object"
    
    typeof操作符对于数组会返回"object",因此它不能用来区分数组和普通对象。这个方法不是用来判断数组的好方法。
  6. 使用Array.prototype的方法(不推荐)
    var arr = [1, 2, 3];
    console.log(typeof arr.push === 'function'); // true
    
    这种方法检查变量是否具有数组原型上的方法,如push。然而,这并不是一个可靠的检测方法,因为其他对象也可能有名为push的方法。 在大多数情况下,推荐使用Array.isArray()方法来判断一个变量是否为数组,因为它是最直接和最可靠的方法。如果需要兼容旧版浏览器,可以考虑使用Object.prototype.toString.call()方法。

48. typeof null 的结果是什么,为什么?

在JavaScript中,typeof null的结果是 "object"。这个结果是由于JavaScript的历史遗留问题造成的。 原因解释:

  1. 历史背景:在JavaScript的早期版本中,值被存储为32位字。这32位中的前3位表示类型的标签,其余的位表示实际的值。对于对象,类型标签是000。而null被设计为一个空指针,也就是所有位都是0,包括类型标签。因此,null的类型标签与对象的类型标签相同,都是000。
  2. typeof操作符typeof操作符会检查值的类型标签。由于null的类型标签与对象相同,typeof操作符错误地将其识别为"object"。
  3. 无法更改:这个行为已经成为JavaScript语言的一部分,为了保持向后兼容性,这个错误没有被修复。修复它可能会导致现有的代码出现问题。 如何正确检测null: 由于typeof null返回"object",你不能使用typeof来检测null。相反,你应该使用严格等于操作符===来检测null:
if (value === null) {
  // value 是 null
}

这种方法是检测null值的推荐方式。

49. js 函数参数有默认值时,如果传递的参数是 undefined 那么会被默认值赋值吗?

是的,在JavaScript中,当函数参数有默认值时,如果传递的参数是undefined,那么该参数会被赋予其默认值。 示例代码

function func(a = 10, b = 20) {
  console.log(a, b);
}
func(undefined, 30); // 输出:10 30
func(null, undefined); // 输出:null 20

解释

  1. 默认参数值:在函数func中,参数ab都有默认值,分别是10和20。
  2. 传递undefined:当调用func(undefined, 30)时,第一个参数是undefined,因此a被赋予默认值10。第二个参数是30,所以b的值是30。
  3. 传递null:当调用func(null, undefined)时,第一个参数是nullnull不是undefined,所以a的值是null。第二个参数是undefined,因此b被赋予默认值20。 注意:只有当参数的值严格等于undefined时,默认值才会生效。任何其他假值(如null0''(空字符串)等)都不会触发默认值。

50. 什么是伪数组(类数组)?

**伪数组(类数组)**指的是一个对象,它具有以下特征:

  1. 具有索引属性:对象具有以数字为键的属性,这些属性从0开始递增,类似于数组的索引。
  2. 具有length属性:对象具有一个length属性,表示对象中索引属性的数量。
  3. 没有数组的方法:伪数组没有数组原型上的方法,如pushpopmap等。 示例
const fakeArray = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};
console.log(fakeArray[0]); // 输出:'a'
console.log(fakeArray.length); // 输出:3

常见的伪数组

  • arguments对象:函数内部的一个特殊对象,包含了函数调用时传入的所有参数。
  • DOM元素集合:如document.getElementsByTagName()返回的HTMLCollection。 伪数组与数组的区别
  • 原型不同:伪数组没有数组原型上的方法,而数组具有。
  • 创建方式不同:数组是通过[]Array()创建的,而伪数组通常是其他对象(如函数的arguments对象)。 转换伪数组为真数组: 可以使用以下方法将伪数组转换为真数组:
  • Array.from():ES6引入的方法,可以轻松将伪数组转换为数组。
  • 扩展运算符...:使用扩展运算符可以将伪数组转换为数组。 示例
// 使用Array.from()
const realArray = Array.from(fakeArray);
// 使用扩展运算符
const realArray2 = [...fakeArray];

总结:伪数组是一种具有数组特征但不是真正数组的对象。了解伪数组有助于更好地处理某些JavaScript对象和API。