Skip to content

概念卡 · 我的说法 ​

← 回题面卡默题总览

按概念整理,不按 题原文。写到自己能讲顺为止。

锚点请用英文 id(标题可写中文)。题面卡链接写成 /mine/explain#reflow-repaint 这种,不要用带 / 的中文 hash。

本页分段:浏览器与网络 · HTML · CSS · JavaScript · Vue 基础 · Vue3 基础 · UniApp · 架构 · 进阶 · 工程 · 实践 · 构建 · 追问 · 项目 · 实战


浏览器与网络 ​

搜索引擎 / SEO ​

概念 · 搜索引擎与 SEO

浏览器SEO

一句话
搜索引擎靠爬虫抓页面、建索引、再给用户排序;SEO(搜索引擎优化)就是让页面更容易被抓到、看懂、排到前面。

搜索引擎是怎么工作的
三步:

  1. 抓取:爬虫像简化版浏览器去请求你的网址,拿 HTML,顺着链接继续爬。不想被抓的用 robots.txt(爬虫协议)挡。
  2. 索引:把标题、正文、结构拆开存进库,相当于给网页做目录。
  3. 排名:用户搜索时排出先后,主要看三块——相关度(搜的词和页面内容匹不匹配)、站点权威(这个站靠不靠谱、有没有分量,比如别的正规站会不会链过来;这项前端几乎改不了)、页面体验(快不快、是不是 HTTPS、手机好不好用)。

和浏览器有什么关系
用户打开 Chrome 看到的是 JS 跑完之后的画面;爬虫优先吃的是服务器返回的那份 HTML。SPA(单页应用)如果首包只有一个空的 #app,人能看见内容,爬虫可能看不见。所以公开要收录的页,前端会用 SSR(服务端渲染)/ SSG(静态站点生成)/ 预渲染,先把正文写进 HTML。

Vue / React SPA 为什么不利于 SEO?怎么解?
不利于 SEO,是因为典型 SPA 走 CSR(客户端渲染):服务器先给空壳(#app + JS),正文要等浏览器跑完 JS 才有。人能看见,爬虫首包里没字,就很难索引。别赌「爬虫会跑 JS」,百度更弱。

怎么解:核心是让爬虫第一次拿到的 HTML 里就有正文,不只是 SSR。

  • SSR(服务端渲染):每次请求服务器当场渲成带正文的 HTML。适合内容常变、要个性化的页。
  • SSG(静态站点生成):构建时就把每页 HTML 生成好。适合官网、文档、博客这种不怎么变的页。
  • 预渲染:还是原来的 SPA,打包时用无头浏览器把公开路由打开,把渲好的 HTML 存下来。已有 Vite + Vue 官网改动最小:路由用 History(不要 /#/about),列出首页 / 关于 / 产品这些公开路径做预渲染即可。后台、登录页不用做。

官网内容相对静态,优先 SSG 或预渲染,一般不必上 SSR。空壳不先解决,再堆 title / description 也救不了。

前端 SEO 做什么

  • 让它看见:SSR(服务端渲染)/ SSG(静态站点生成)/ 预渲染,语义化 HTML,正文不要只放图片或 canvas(画布)里
  • 让它看懂:title、description、一个 h1、图片 alt(替代文本)、canonical(规范网址)
  • 让它找得到:sitemap(站点地图)、站内链接、robots.txt(爬虫协议)别误拦
  • 体验也算分:HTTPS、移动端、LCP(最大内容绘制)/ INP(交互响应)/ CLS(布局偏移)这类速度指标

怎么记
在我们讲SEO的时候我们要清楚它的前置条件,就是浏览器的搜索引擎以怎么工作,SEO也可以叫搜索引擎优化是浏览器搜索引擎方面的优化,让我们的站点展示权重更高,用户更容易看到我们的站点。

  • 我们先讲搜索引擎的工作原理 搜索引擎的工作整体分三步
  1. 抓取:搜索引擎爬虫类似简化版的浏览器来访问我们的站点
  2. 索引:把标题、正文来拆解类似做一个目录
  3. 排名:按照用户搜索的相关度、站点靠不靠谱、页面好不好用排出先后。 当我们理解了搜索引擎我们就需要来做SEO,通过上述,我们知道搜索引擎会先抓取html。
  • 以我们的vue项目为例,多数都是spa(单页应用),单页应用我们人看到的html是js加载完成的界面但是爬虫不是,爬虫抓的是服务端返回的html,在这种单页应用中多数最开始只是挂载了个#app这个时候搜索引擎看不到内容所以会以为是个空壳站点。这个时候就是我们第一个优化点,让爬虫能够看到的站点。怎么优化呢,就是让爬虫第一次就能看到我们站点的内容,现在主流的话就是SSR(服务端渲染) / SSG(静态站点生成) / 预渲染。这三种方式。我简单说一下三种方式的区别
  • 服务端渲染:每次请求服务器就会返回一个带正文的html
  • 静态站点生成: 构建的时候把每个页面的html生成好
  • 预渲染:还是一个单页应用不过打包的时候会把公用的路由打开把渲染好的html存下来

通过这三种手段可以让搜索引擎更好的完成它的第一步工作,具体使用那种方式可以根据项目的情况来挑选技术框架。 接下来就是到第二步索引了,如何让搜索引擎更好的索引呢,这个时候我们HTMl5的语义化标签就发挥了不可或缺的重大贡献,通过语义化标签可以让搜索引擎更好的去做索引的工作。当我们优化完抓取和索引之后就到第三不排名,我们在第三不中能做的不是很多主要还是在好不好用。 ← 回题面卡

XSS / CSRF ​

概念 · XSS 与 CSRF

浏览器安全

一句话
XSS(跨站脚本)是在你的页面里跑别人的脚本;CSRF(跨站请求伪造)是借你已登录的身份去发你没点过的请求。一个防「脚本被执行」,一个防「请求不是你本意」。

XSS 是什么
攻击者往页面注入恶意 JS 代码,浏览器执行了这段恶意脚本

三种常见类型:

  • 存储型:恶意内容写进服务端(评论、资料),别人打开页面就中招
  • 反射型:恶意内容挂在 URL 参数里,服务端原样塞回页面后执行
  • DOM 型:不经过服务端,前端用 JS 把不可信数据写进 DOM(如 innerHTML / v-html)后执行

怎么防:用户内容当数据不当代码——输出转义;少用会按 HTML 解析的 API;Cookie 加 HttpOnly(禁止 JS 读 Cookie);配 CSP(内容安全策略)限制脚本来源。

CSRF 是什么
冒充用户身份发起请求,利用浏览器自动带上 cookie 的特性。拿不到 Cookie 内容,只是「借身份办事」。

怎么防:敏感请求带 CSRF Token(跨站请求令牌,攻击站读不到);校验 Origin / Referer(来源);Cookie 设 SameSite(同站策略,限制跨站自动带 Cookie);关键操作再加密码 / 短信。

两者差在哪

  • XSS:在页面里跑恶意脚本 → 偷数据、改页面
  • CSRF:借登录态发请求 → 在你不知情下做敏感操作
  • HttpOnly 能挡 XSS 读 Cookie,挡不住 CSRF(CSRF 不需要读 Cookie,浏览器会自己带)

怎么记
先说 XSS。可以理解成别人把一段 JS 塞进你的站点,浏览器把它当正常代码跑了。比如评论里写了一段脚本,或者前端用 v-html / innerHTML 把用户输入原样插进页面,打开这个页的人就会中招,Cookie、页面内容都可能被偷、被改。常见分三种:存到服务器里的叫存储型,挂在 URL 里反射回来的叫反射型,不经过后端、前端自己把脏数据写进 DOM 的叫 DOM 型。怎么防呢,核心就是用户内容当数据、不当代码:输出要转义,少用会按 HTML 解析的接口,Cookie 加上 HttpOnly(JS 就读不到),再配个 CSP(内容安全策略)限制脚本从哪来。

再说 CSRF。它不是在你页面里跑脚本,而是用你的身份去办事。你已经登录了目标站,Cookie 浏览器会自动带,攻击者诱你点开另一个页面或链接,就向目标站发出改资料、转账这种请求。他通常拿不到 Cookie 内容,只是借登录态。怎么防:敏感请求带 CSRF Token(攻击站读不到这个令牌),校验请求是不是从本站来的(Origin / Referer),Cookie 设 SameSite(同站策略,跨站就别自动带),关键操作再加密码。

最后对比一句:XSS 是在你站点里跑别人的脚本,CSRF 是用你的身份发你没点过的请求。HttpOnly 能挡 XSS 偷 Cookie,挡不住 CSRF,因为 CSRF 根本不需要读 Cookie,浏览器自己会带。

← 回题面卡

Hash / History 路由 ​

概念 · 前端路由 Hash 与 History

浏览器路由

一句话
两种都是 SPA(单页应用)前端路由,切页不刷新整页;差在 URL 怎么改、刷新时找不找服务器。实现上都是:拦住跳转 → 改地址栏 → 按 path 换组件。

实现原理
不是服务器再发一份 HTML,是 JS 自己干三步:

  1. 点链接时拦住浏览器默认跳转(preventDefault)
  2. 只改 URL:Hash 改 location.hash;History 用 history.pushState(只改地址栏和历史栈,不发请求、不刷新)
  3. 拿当前 path 去路由表匹配组件,渲到 router-view

前进后退:Hash 听 hashchange;History 听 popstate。注意 pushState 不会自动触发 popstate,所以点菜单改完 URL 要自己立刻渲染,点返回才走 popstate。

Hash 是什么
地址是 https://a.com/#/about。# 后面的片段浏览器不会发给服务器。服务器只看到站点根路径,前端自己听地址变化,把 about 页渲出来。部署省事,刷新也不会 404。坏处是带 #,搜索引擎几乎不当独立页面,官网要收录别用它。

History 是什么
地址是 https://a.com/about。用浏览器的 History API(pushState 改路径、点后退走 popstate),看起来像真页面。利于 SEO,也能做预渲染。但用户刷新或把链接贴到新标签打开时,浏览器会向服务器要 /about 这个文件,没有就 404。所以 Nginx 要配:找不到静态文件就回退到 index.html,再交给前端路由。

怎么选

  • 后台、不需要被搜到:Hash 也能用
  • 官网、要收录、要预渲染:用 History,并配好服务器回退

怎么记
Hash模式和history模式是前端的路由。Hash模式的话它表现出来的是http://XXX.com/#/home它在地址前加了一个#,history的表现则像一个正常的地址http://XXX.com/home。Hash模式是通过hashchange 监听变化然后前端自己渲染出来,history是通过浏览器的history API来实现的。hash模式部署方便但是带#,搜索引擎不会将它当作独立的页面。history的话需要找服务器要文件,不带#,对SEO相对更友好一些。

← 回题面卡

HTTP / HTTPS ​

概念 · HTTP 和 HTTPS 的区别

网络

一句话
http和https都是网络传输的协议

HTTP / HTTPS 是什么

  • http是通过tcp协议来进行的网络连接 明文易被窃听篡改
  • https可以理解为http+TLS加密,TLS协议通过鉴权和加密来防止中间人窃听篡改

为什么要用 HTTPS(防窃听 / 防篡改 / 防中间人)
核心就是安全。HTTP 像明信片:路上谁都能拆开看、改两笔。洗澡被人盯着、八卦被当事人旁听,尴尬;钱扔大路上喊「别拿」——人一走开,钱还是不是你的?HTTPS 就是给数据加信封、盖戳:别人看不懂、改不了,还能确认「这真是这家站」,数据有归属,不是谁路过都能用。

代价(TLS 握手等)

  1. 额外的网络开销:当TCP三次握手走完后还要走一个TLS握手
  2. CPU的运算开销:证书校验、加密、解密请求报文 CPU 占用会比纯 HTTP 更高
  3. 部署和维护的成本更高:需要去购买申请证书到期后还需要续签更新
  4. CDN、Nginx的反向代理需要配置证书增加运维的工作量

怎么记

  • 都是传数据的协议,路都走 TCP
  • HTTP = 明信片(明文,谁都能看、能改)
  • HTTPS = HTTP + TLS 信封盖戳(加密 + 鉴权,防窃听 / 篡改 / 中间人)
  • 更安全也更贵:多一次 TLS 握手、加解密吃 CPU、证书要买要续、CDN / Nginx 还得配证书

← 回题面卡

TCP / UDP ​

概念 · TCP 和 UDP

网络

一句话
都是传输层协议:TCP 先握手再可靠地传;UDP 拿起就发,快但可能丢。

是什么 / 区别

  • TCP:面向连接,先三次握手再建;有确认、重传、保序,可靠但慢一点;连接是全双工——两边可以同时发、同时收
  • UDP:无连接,直接发;可能丢、乱序、重复,头开销小、更快(没有「建一条全双工通道」这回事,谁想发就发)
  • 场景:网页 / 接口 / HTTPS / WebSocket 走 TCP;DNS、直播、游戏状态常走 UDP

网页为啥走 TCP 不走 UDP
网页、接口、登录这些丢一包就可能白屏、半截 JSON、钱扣了页面却报错。TCP 保证「发了就能到、顺序也对」;UDP 图快,丢了就丢了,适合直播掉几帧还能看、游戏状态下一帧再补。

三次握手干什么
双方确认「我能发、你能收」,避免对着一个已经失效的请求白建连接。

  1. 客户端 → SYN:我想连
  2. 服务端 → SYN + ACK:收到了,我也想连
  3. 客户端 → ACK:确认,开始传数据
    (HTTPS 在这之后还要再做一次 TLS 握手。)

四次挥手干什么
正因为 TCP 是全双工,两个方向各管各的:我这边发完了,你那边可能还在发,所以关是半关,要挥四次。

  1. 主动方 → FIN:我发完了(只关我→你)
  2. 对方 → ACK:知道了
  3. 对方发完 → FIN:我也发完了(再关你→我)
  4. 主动方 → ACK:断开

怎么记

  • 打电话先接通再说话 = TCP;广播喊一声不管听见没 = UDP
  • TCP 全双工 = 电话两边能同时讲;关电话也要两边各自说完(所以四次挥手)
  • 网页不能丢包 → 走 TCP;DNS / 直播 / 游戏 → 常走 UDP
  • 三次握手:确认双方都能收发
  • 四次挥手:全双工半关,另一边可能还有货要发

← 回题面卡

WebSocket ​

概念 · WebSocket

网络

一句话
长连接、全双工:连上之后两边随时都能推,不用前端一遍遍轮询。

和 HTTP、TCP 什么关系

  • 应用层协议,底下还是走 TCP(wss 再套一层 TLS),不是 UDP,也不是替代 TCP
  • 记三层:TCP 负责可靠传输 → 先用 HTTP 做一次升级握手 → 之后这条 TCP 上跑的是 WebSocket 帧
  • HTTP 多半是一问一答(客户端问,服务端才答);WebSocket 是全双工,服务端也能主动推

怎么建连
路还是同一条 TCP,只是上面先用 HTTP「借一下通道」,再换成 WebSocket 说话。

  1. 浏览器先发一个看起来很像普通 HTTP 的请求,但多带两句话:
    • Upgrade: websocket → 我想换协议,别再用一问一答的 HTTP 了,换成 WebSocket
    • Connection: Upgrade → 这次连接是专门用来「升级协议」的(配合上面那句)
  2. 服务端如果同意,回 101 Switching Protocols(协议切换成功)。
  3. 从这一刻起,这条 TCP 不再当普通 HTTP 用,改成双方用 WebSocket 帧互发;连上后服务端也能主动推。

前端你写的只是 new WebSocket('wss://...'),浏览器会自动帮你发上面那个带 Upgrade 的请求。业务里常见还要做心跳、断线重连;跨域也看 Origin,服务端要放行。

适用场景
聊天、通知、协作、行情这类要实时推的;普通拉页面、CRUD 接口还是用 HTTP 更合适。

怎么记
WebSocket 是一种全双工、长连接的通讯方式。底层还是 TCP,只是在 TCP 之上做了一次「说话方式」的升级:先用 TCP 把路接通,再发一条意思大概是「我想改用 WebSocket 说话」的请求;服务端同意之后,这条连接就真正建成 WebSocket,两边随时都能推消息。

← 回题面卡

强缓存 / 协商缓存 ​

概念 · 强缓存与协商缓存

浏览器网络

一句话
强缓存和协商缓存都是浏览器的缓存机制,当浏览器拿资源的时候会先看有没有可用的缓存,我们先说一下强缓存

强缓存是什么(还发不发请求)
当浏览器命中强缓存的时候,就不会再发起网络请求,直接使用缓存数据,这个缓存数据依靠响应头来告诉浏览器能用多久。实现的方式主要有两种

  1. 目前主流是通过**Cache-control:max-age=60(秒)**来控制
  2. 以前是使用Expires来设置绝对过期时间
  • 两种方式同时设置以Cache-control设置为准

协商缓存是什么(怎么问、返回啥)
协商缓存是当强缓存失效之后,浏览器会再次发起请求,这次请求会带一个上次缓存的标识,然后去让服务器判断资源是否有变化,没有变化返回304直接使用旧的,如果有变化返回200并返回新的数据。这个标识有两种形式

  1. 使用ETag内容指纹
  2. Last-Modified上次修改时间
  • 两种形式区别就是内容指纹根据内容变化判断,相比时间判断内容指纹精细。

怎么记
当我们浏览器请求资源的时候会先看强缓存,强缓存没有过期的话直接用,不去询问服务器。如果强缓存过期就去用协商缓存带上内容指纹去询问服务器,服务器返回304说明缓存没变,我们就直接用缓存,如果返回200说明缓存有变化同时也会返回新的数据,我们使用新的并将缓存更新。

← 回题面卡

浏览器存储 ​

概念 · Cookie / localStorage / sessionStorage

浏览器存储

一句话
上述三个都是浏览器存储API,只是有各自的存储方式和使用方式

对比维度(大小 / 生命周期 / 作用域 / 设置方式)

API存储大小生命周期作用域设置方式
Cookie4KB左右可设置过期时间通过域名+地址控制主要由后端设置
localStorage5MB永久存储,需手动清除同源,可跨标签使用通过前端设置
sessionStorage5MB当前标签页关闭时销毁同源,不可跨标签使用由前端设置

怎么记
Cookie、localStorage、sessionStorage三个都是浏览器本地存储的手段。主要是有一些区别,下面我从我了解的四个方面来简单介绍一下他们之间的区别,分别是设置方式、存储大小、生命周期、作用域。首先我们先说一下Cookie,它主要是由后端来设置的,内存大小差不多是4MB左右,过期时间可以设置,如果不设置的话默认关闭标签的时候清空,域名+地址一样的话我们就可以使用。然后就是localStorage和sessionStorage它们两个几乎一样,都是由前端设置,内存在5KB左右,区别就是生命周期,localStorage只要不手动清理就会一直存在,sessionStorage是当关闭标签的时候就会清空,作用域方面localStorage可以跨标签使用但是sessionStorage不能跨标签用。

← 回题面卡

浏览器页面渲染管线 ​

概念 · 浏览器页面渲染管线

浏览器渲染

一句话
浏览器从输入完地址点击回车后到页面加载完成所做的一系列操作

都做了什么
URL--DNS解析为IP--建立TCP连接--发送http请求--响应--解析HTML构建一个DOM树--解析CSS--构建CSS表--生成渲染树--重排--重绘--合成

关键点
重排/重绘

怎么记

  • 当我们输入一个地址点击回车后
  • 浏览器首先会将地址进行解析获取IP
  • 拿到IP之后发送http请求
  • 然后等有响应结果之后去解析响应结果构建一个完整的DOM树
  • 再解析CSS构建一个CSS的样式表
  • 然后生成一个渲染树
  • 经过重排、重绘、合成将完整的界面渲染出来

← 回题面卡

重排 · 重绘 · 合成 ​

概念 · 重排 / 重绘 / 合成

浏览器渲染

重排/重绘/合成分别是什么呢

  • 重排:元素的几何信息发生改变浏览器需要重新计算元素位置
  • 重绘:元素的样式信息发生变化浏览器需要重新绘制元素样式
  • 合成:元素使用 transform、opacity,直接在 GPU 图层做变换

有什么区别?

  • 它们的核心区别就是重排会重新计算布局,重绘的话只是绘制样式不计算布局。
  • 重排会触发重绘但是重绘不会引发重排
  • 合成的话就是直接在GPU图层做变换,不会触发重排、重绘相比是性能最优

如何优化?

  • 避免频繁操作DOM触发重排
  • 避免频繁读写元素的布局属性如 offsetWidth、clientHeight也会触发重排
  • 避免短时间内大量的去修改颜色,阴影这类会触发重绘的操作
  • 动画类优先使用transform/opacity避免使用top/left
  • 开启GPU分层:will-change: transform; 提示浏览器提前做图层提升(不要滥用)

TIP

will-change: transform;原理是提前告知浏览器这个元素接下来会做一些动画然后在GPU来分配图层避免一开始卡顿

适用场景

  • 已知马上要进行动画的元素

  • 大量连续transform的元素

  • 首帧明显卡顿的情况

  • 注:不可以滥用will-change: transform;尤其禁用给通配符添加。因为每一个元素都会占用一块 GPU 显存,会出现显存占用飙升,低端设备掉帧、闪退等性能问题

怎么记

  • 重绘是指我们修改元素的样式信息color、background的时候会触发重绘
  • 重排是当我们修改元素的宽高边距等位置信息的时候会触发重排
  • 合成就是指使用transform、opacity这些直接在GPU做操作不经历重排重绘性能最优

TIP

  • 重排的时候一定会触发重绘,重绘的时候不一定会触发重排

← 回题面卡 :::

同源策略 ​

概念 · 同源策略

浏览器安全

一句话
浏览器规定:不同「源」的页面,不能随便读对方的数据。

是什么
源 = 协议 + 域名 + 端口。三者全一样才叫同源;差一个就不同源。

为什么要有
防止恶意站点偷你在别的网站上的 Cookie、页面内容、本地存储等,保护用户。

它限制什么

  • 不能随便读不同源的 DOM / Cookie / localStorage
  • 不能随便用 JS 读跨域 AJAX/Fetch 的响应(请求往往发出去了,响应被浏览器拦住不给页面脚本)

它不限制什么

  • 后端调后端、Postman 调接口:没有这套浏览器限制
  • <img> / <script> / <link> 这类标签加载资源通常仍可跨域(这也是 JSONP 思路的来源)

怎么记
同源像「同一栋楼同一门牌」;不同门牌就不能直接翻对方抽屉。跨域问题,根子多半先问到同源策略。

← 回题面卡

跨域 ​

概念 · 跨域

浏览器网络

一句话
跨域不是请求发不出去,而是浏览器按同源策略拦住了「前端 JS 读响应」。

是什么
当前页的源,和要请求的接口源,协议 / 域名 / 端口任一不同,就是跨域。

和同源策略的关系
同源策略是规则;跨域是触发规则后你遇到的现象。先有策略,才有「跨域怎么解」。

常见误解
以为接口没打到服务器——多数情况服务器收到了,浏览器不让页面脚本读结果。

怎么记
先问「是不是不同源」→ 再问「用 CORS / 代理 / Nginx 哪一种解」。解法见百科与题面卡追问。

← 回题面卡

HTML ​

H5 新增特性 ​

概念 · HTML5 新增了什么

HTML

一句话
H5 不是某一个标签,是一整套能力:语义化、表单、音视频、画布、本地存储,再加上一批浏览器 API。

都有哪些(按类说)

  • 语义化标签:header、nav、main、article、footer 等,细节见 HTML 语义化
  • 表单增强:新类型 email / tel / date / color,新属性 placeholder、required、pattern
  • 多媒体:video、audio,不再靠 Flash
  • 图形:canvas(JS 画)、svg(矢量)
  • 本地存储:localStorage、sessionStorage(Cookie 更早就有,不是 H5 新增)
  • 常用 API:File(FileReader)、拖拽、地理位置、WebWorker(后台线程)、WebSocket(长连接)
  • 其他:data-* 自定义属性,details / summary 折叠

怎么记
别从第一个标签往下背,按块说:结构(语义化)、表单、音视频、能画图、能本地存、还有文件拖拽定位这些 API。提一句 Cookie 不是 H5 的,存储只说 localStorage / sessionStorage。语义化如果追问,再展开下一张卡。

← 回题面卡

HTML 语义化 ​

概念 · HTML 语义化标签

HTML

一句话
标签本身就说明这块是什么内容(导航、正文、页脚),不是只靠 class 或样式。

是什么
以前页面大量用 div / span,机器不知道哪是头、哪是导航。语义化就是换成有含义的标签,告诉浏览器和搜索引擎:这块内容是什么类型。默认样式几乎不变,变的是含义。

常用有哪些
页面结构:header、nav、main(一页最好一个)、article、section、aside、footer。
还有 h1~h6、p、列表、video / audio、figure + figcaption。
对比:<div class="header"> 标签没含义;<header> 一看就是页头。

好处

  • SEO(搜索引擎优化):爬虫更容易分清导航、正文、页脚,利于索引;整条线见 搜索引擎 / SEO
  • 无障碍:读屏软件能跳到导航、主内容
  • 可维护:看标签就懂结构,少靠 class 猜
    不等于不用 div:布局嵌套仍大量用 div,有明确业务含义的区域才优先用语义标签。

怎么记
语义化可以理解成:别让整页都是没有含义的盒子。div 只是箱子,看不出来里面是导航还是文章;换成 header、nav、main、footer 这些标签,人和搜索引擎都能看懂这块是什么。常用的记页面骨架就行:头、导航、主内容、文章、侧边、页脚,再加上标题 h1。好处 说三句:对 SEO 友好、读屏能用、代码好维护。注意不是禁用 div,没语义的布局照样用 div。

← 回题面卡

CSS ​

CSS3 新特性 ​

概念 · CSS3 新增了什么

CSS

一句话
CSS3 也是一整套能力, 按块说:更好看(圆角阴影渐变)、能动(transform / 过渡动画)、更好排(flex / grid / 媒体查询)。

都有哪些(按类说)

  • 视觉:border-radius、box-shadow、text-shadow、opacity、rgba
  • 背景 / 渐变:background-size、linear-gradient
  • 变换:transform 的位移、旋转、缩放;动画优先改它和 opacity,少动宽高、top / left
  • 过渡 / 动画:transition 有状态变化才播;animation 能循环、多关键帧
  • 布局:flex(弹性布局,一维)、grid(网格布局,二维)、@media(媒体查询,按屏宽切样式)
  • 其他:box-sizing: border-box、calc()、CSS 变量 --主色、@font-face、filter

怎么记
和 H5 一样别从单个属性背。先说变好看:圆角、阴影、透明、渐变。再说能动:transform 变位置形状,transition 做 hover 这种过渡,animation 做 loading 这种自己会播的。最后说更好排:flex 管一行一列,grid 管格子,媒体查询做响应式。补一句开发里几乎都会开 border-box,宽高用 calc,主题色用 CSS 变量。flex 和动画如果追问,再按百科展开。

← 回题面卡

CSS 优先级 ​

概念 · CSS 选择器权重

CSS

一句话
多条 CSS 打到同一个元素时,比谁更具体:行内最狠,然后 id,再 class,最后标签。

怎么算
记四位 (行内, id, class, 标签),从左往右比,左边大的直接赢,不进位(不是 11 个 class 顶 1 个 id)。

  • 行内:标签上的 style=""
  • id:#app
  • class / 属性选择器 / 伪类:.box、[type]、:hover 算同一档
  • 标签 / 伪元素:div、::before

例子:#app .item div = 1 个 id + 1 个 class + 1 个标签 → (0, 1, 1, 1)。

还要记住

  • 权重一样:谁写在后面谁生效
  • !important 抬到最高档;两边都加,再回头比选择器权重
  • 通配符 *、继承来的样式:最低,很容易被盖掉
  • 能用 class 就别堆 id,少写 !important,后面才好改

怎么记
CSS的优先级正常分了四个权重可以想象成四个档位:最高档位是行内样式然后是ID再然后是class(属性选择器、hover)也在这一档位再然后就是标签选择器和伪元素。这是它的四个档位比如我们给一个元素设置宽在行内设置了10px,id设置了20px,class设置了30px,标签选择器设置了40px。这个时候这个元素是10px。因为行内的优先级最搞然后我们给标签选择器的40px加上!important那这个元素的宽度就变成了40px,因为!important会将它的优先级强行提升到最高,如果都加的话还是按照优先级排序来。

← 回题面卡

盒模型 ​

概念 · 标准盒模型与怪异盒模型

CSS

一句话
盒子由内容、内边距、边框、外边距组成;两种模型差在:你写的 width 到底包不包括 padding 和 border。

标准盒模型 content-box(浏览器默认)
width / height 只表示内容区。
实际占位宽度 = width + padding + border + margin。
加 padding、border 会把盒子撑大。

怪异盒模型 border-box(常叫 IE 盒模型)
width / height 包含 内容 + padding + border。
实际占位宽度 = width + margin。
padding、border 在设定的宽度里往里扣,外轮廓更好控。
注意:两种模型里 margin 都不算进 width。

怎么切换
box-sizing: content-box / border-box。项目里常见全局:

css
*, *::before, *::after { box-sizing: border-box; }

怎么记
盒模型主要讲盒子的宽高是怎么算的。我们先不管哪种模型,从一个小白角度看:一个盒子占出去的地方 = 你写的宽/高 + 内边距 + 边框 + 外边距。四层都在:内容、padding、border、margin。

然后浏览器默认用的是标准盒模型(content-box)。它的计算方式是:你写的 width / height 只等于内容区。所以你再加 padding、border,盒子会往外撑大,实际占位还是那句:内容宽 + 内边距 + 边框 + 外边距。

还有一种叫怪异盒模型(border-box,也叫 IE 盒模型)。它是:你写的 width 已经包含了内容 + padding + border,这三层在这个宽度里面往里扣,外圈不会被撑破。实际占位就变成:你写的宽 + 外边距。注意两种模型里 margin 都不算进 width。

两种用 box-sizing 切换。浏览器默认是 content-box,项目里几乎都会全局改成 border-box,写 200px 就是外轮廓 200px,布局不容易被 padding 撑破。

← 回题面卡

BFC ​

概念 · 块级格式化上下文

CSS

一句话
BFC(块级格式化上下文)是一块独立的布局小房间:里面怎么排,不跟外面串门。它不是新标签,是一套排版规则。

是什么
浏览器给块级盒子排版时,会划一块区域,里面的盒子按这套规则摆。默认大家往往待在同一个大房间里,所以 margin、浮动会互相影响。你给某个元素「开 BFC」,就是给它单独开一间房。

为什么会有那些问题
问题不是 BFC 制造的,是还没开独立房间时,普通流里的两条规则:

  1. 垂直 margin 合并:同一 BFC 里,相邻块的上下外边距会叠成一个(取较大的那个)。规范故意这样:两个段落不要空隙翻倍。副作用是父没有 padding / border 拦住时,子的 margin-top 会漏到父外面,你以为给子往下推,结果整块父一起动。
  2. float 脱离普通流:浮动元素不占流里的位置。父算高度只看还在流里的孩子,浮动子不算,父高度可能是 0。后面的块级盒子仍按满宽排,背景钻到 float 底下,只有文字会绕开。

它解决什么
给父或旁边的盒子开 BFC 之后:

  • 里面的 margin 关在房间里,不再和外面合并(解父子 margin 合并)
  • 房间算高度会把浮动子孙包进来(解高度塌陷,等于清浮动)
  • 这个房间不会和旁边的 float 重叠(解挡住、做两栏)

怎么开( 记常用的)
overflow 改成 hidden / auto(默认 visible 不开);更干净用 display: flow-root。float、position: absolute / fixed、inline-block、flex / grid 容器也会开。

怎么记
BFC 主要是解决float高度塌陷的问题,当我们在一个空间中放置了多个块级元素,同一个环境里,上下margin会合并成一个,为了不让这个间距翻倍,但是父子之间会吧子元素的margin漏出去这个时候我们给父元素的BFC打开就会发现子元素的margin不会超出父元素不然当元素多了的时候会出现不知名的边距。还有就是当我们使用float的时候,因为有浮动我们元素会离开普通流这个时候父元素的高度就会塌陷,这个时候父元素打开BFC就会告诉浏览器,这个元素内的浮动也算高度从而撑开高度。还有一种就是做两栏布局的时候这个时候。比如一个元素下有两个元素左侧开启float这个时候右侧会占满整个宽度并且会钻入左侧浮动下,我们给右侧元素加上BFC就会发现他的宽度是铺满右侧剩余宽度而不是整个父级的宽度。

← 回题面卡

position ​

概念 · position 五个值

CSS

一句话
position 管这个盒子还在不在正常文档流里、挪的时候相对谁算。五个值:static、relative、absolute、fixed、sticky。

五个值

  • static:默认,正常排,top / left 没用。
  • relative:相对自己原来的位置挪,原来那块坑还占着,后面元素不会顶上来。常用来微调,或给里面的 absolute 当参照(定位祖先)。
  • absolute:脱流,原来位置空出来。相对最近的非 static 祖先(relative / absolute / fixed / sticky);找不到就相对整页。弹层、角标、相对某个盒子精确定位。
  • fixed:脱流,相对浏览器视口钉住,页面滚它还在。吸顶导航、回到顶部。祖先有 transform 等时,可能变成相对那个祖先,不再对视口。
  • sticky:没滚到阈值时像 relative,滚到了像 fixed。必须写 top / left 等才看得出吸顶。表头、侧栏钉住。

怎么记
position 有5个值分别是static(默认)、relative(相对)、absolute(绝对)、fixed(固定)、sticky(吸顶)这五种定位又分两大类分别是脱离文档流和不脱离文档流,默认和相对是不脱离文档流的,绝对和固定这两种脱离文档流,吸顶稍微特殊点他介于相对和固定之间当滚动到达设置的阈值的话它会变成固定没到的话类似相对。还有就是绝对定位的一个布局,比如父相子绝这个方式,因为绝对定位会向上找非默认定位的祖先如果没有就相对整页。还有就是固定定位如果祖先有transform的话可能会相对transform这个祖先而不是视口。

← 回题面卡

flex / grid / flex:1 ​

概念 · flex、grid 与 flex: 1

CSS

一句话
一行一列用 flex(弹性布局);又要行又要列的格子用 grid(网格布局)。flex: 1 是子项简写,用来吃掉剩余空间或均分。

flex 怎么用
写在父上 display: flex。容器:flex-direction 定主轴(默认横排),justify-content 管主轴对齐,align-items 管交叉轴对齐,flex-wrap 换行,gap 间距。
居中最常用:justify-content: center + align-items: center。主轴改成 column 以后,justify 变管垂直、align 变管水平,别记反。
子项:flex-grow 有剩就涨,flex-shrink 不够就缩,flex-basis 分配前的基础尺寸,flex 是这三个的简写。

flex: 1 是什么
完整是 flex-grow | flex-shrink | flex-basis。flex: 1 一般当成 1 1 0%:能涨能缩,基础尺寸先按 0 算,再平分剩余。多个兄弟都写 flex: 1 就是均分;左右固定、中间自适应也常给中间写它。
和 flex: auto(1 1 auto)不一样:auto 先按内容宽度算基础,再分剩余;1 更像忽略内容宽度、直接平分。

grid 怎么用
display: grid,用 grid-template-columns 分列(如 repeat(3, 1fr)),gap 间距,子项 grid-column: span 2 跨格。仪表盘、卡片墙这种二维格子用它;一维导航、一排按钮继续 flex。

怎么记
flex 和 grid 是 CSS 里两种布局方式。平时用 flex 更多,一行一列、导航、居中都靠它;grid 更适合二维格子,比如表格感的卡片墙、仪表盘。flex 分写在父上和写在子上:父上常用 flex-direction、justify-content、align-items、gap,管主轴方向和对齐;子上有 flex-grow、flex-shrink、flex-basis,日常很少拆开写,要占满剩余空间直接给子项 flex: 1 就行。

← 回题面卡

移动端适配 ​

概念 · 移动端适配与单位

CSS

一句话
适配就是让不同屏幕上看起来比例差不多。H5 常用 rem 或 vw 做等比缩放,也可用 @media 分档;小程序 / UniApp 常用 rpx。

常见方案

  • rem:改 html 的 font-size,整页按比例缩放。设计稿(如 375)配合 flexible 或 postcss-pxtorem,把 px 转 rem。
  • vw / vh:100vw = 屏宽,直接跟视口走,不用改根字号。字号过大过小要设 max / min。
  • rpx:不是标准 CSS,微信等规定屏宽 = 750rpx,写起来像 px,运行时换算。
  • 媒体查询 @media:按屏宽切两套样式,适合改布局而不是单纯缩放。

单位相对谁

单位相对谁
px固定像素,不跟屏幕变
%多数相对父元素对应尺寸
em父元素字号,嵌套会层层放大,适配少用
rem根元素 html 字号,改一处整页跟着变
vw / vh视口宽 / 高
rpx小程序约定,相对屏宽

怎么记
适配先问在哪个端。H5 要么改根字号用 rem 整体缩放,要么用 vw 直接跟屏幕走;小程序别硬套 rem,用 rpx。单位就记相对谁:px 写死,% 看父级,em 看父字号容易套娃,rem 只看根字号所以适合适配,vw 看屏幕。em 和 rem 差一个字:e 是父亲,r 是根。

← 回题面卡

Less / SCSS ​

概念 · 预处理器、变量、嵌套、mixin

CSS进阶

一句话
构建期把 Less/SCSS 编成 CSS;变量、嵌套、mixin 提效,换肤仍靠 CSS 变量。

怎么记
Less、SCSS(Sass 的 CSS 语法)是 CSS 预处理器,构建期 Loader 或 Vite 插件编成 CSS,浏览器不能直接跑。多了变量、嵌套、mixin(混合宏)、运算。和 CSS 变量 --var 不同:预处理器变量编译期定死;--var 运行时可改,换肤常用后者。

展开对照

← 回题面卡

JavaScript ​

什么是 JS / 单线程 ​

概念 · JavaScript 与单线程

JavaScript

一句话
JS 是前端三剑客之一:HTML 管结构,CSS 管样式,JS 让网页能被操作。它是单线程的,正在做这件事时不能同时做另一件。

是什么
和 HTML、CSS 一起组成网页核心。HTML 搭骨架,CSS 画样子,JS 负责点击、改内容、发请求这些可操作性。

单线程 / 同步
单线程简单说:JS 在处理这个事情的时候,不可以去处理其他事情。当前正在执行的就是同步任务,排着队做完一件再做下一件。

好处
顺序清楚,好推理;不会两段代码同时改同一个页面,避免状态打架。

坏处
遇到慢的事(比如死等一个请求)会把后面全堵住,页面卡死、点不了。

怎么解决:EventLoop(事件循环)
耗时的先异步出去,JS 继续跑同步;做完了回调排队,等栈空了再执行。流程:同步跑完 → 清空微任务(Promise.then)→ 取一个宏任务(setTimeout)→ 再清微任务,一直转。

怎么记
JS 是前端三剑客之一,和 HTML、CSS 共同组成网页的核心:HTML 负责结构,CSS 负责样式,JS 赋予网页可操作性。JS 是单线程语言,简单说就是它在处理这件事的时候不能去处理别的事,这就是同步任务。好处是顺序清楚、不会同时触发多个事件导致打架;不好的是慢任务会把页面卡死。怎么解决靠 EventLoop:把耗时的丢出去异步做,做完再排队回来,先把同步跑完,再清微任务,再拿一个宏任务。

← 回题面卡

EventLoop ​

概念 · 事件循环

JavaScript

一句话
JavaScript 是单线程语言;为了不让请求、定时器卡住页面,浏览器宿主环境提供了 EventLoop(事件循环)来处理异步任务。

任务怎么分
JS 的任务分同步和异步;异步回调进队列后又分成宏任务和微任务。

  • 同步任务跑在调用栈(执行栈),栈是后进先出
  • 异步任务不直接进调用栈,放到对应任务队列等调度
  • 整段 script 本身就是浏览器提供的第一个宏任务

一轮怎么转

  1. 从宏任务队列取出一个宏任务,推进主线程调用栈
  2. 这个宏任务里:同步任务直接在调用栈执行;异步交给浏览器后台线程,完成后再把回调放入宏任务或微任务队列,不阻塞主线程
  3. 当前宏任务里同步全部跑完、调用栈清空后,立刻清空全部微任务(微任务里新产生的微任务本轮一并执行)
  4. 微任务清完后,浏览器进行 UI 渲染
  5. 进入下一轮:再取下一个宏任务,重复上面流程

完整一轮:取一个宏任务 → 执行内部同步代码 → 清空全部微任务 → UI 渲染 → 下一个宏任务。

js
console.log(1)
setTimeout(() => console.log(2))
Promise.resolve().then(() => console.log(3))
console.log(4)
// 1 4 3 2

怎么记
JavaScript 是单线程语言,同一时间只能处理一件事,如果所有任务都同步执行,遇到网络请求、定时器这类耗时操作就会阻塞页面。为了解决单线程阻塞,浏览器宿主环境提供了事件循环(EventLoop)机制,用来处理异步任务。

JS 的任务分为同步任务和异步任务,异步任务又分为宏任务和微任务。

  • 同步任务运行在调用栈(执行栈);
  • 异步任务不会直接进入调用栈,会被放到对应的任务队列等待调度。 整个 script 脚本本身就是浏览器提供的第一个宏任务,事件循环就是一轮一轮循环执行:
  1. 首先从宏任务队列取出一个宏任务,推入主线程调用栈执行;
  2. 在执行这个宏任务的过程中,遇到同步任务就直接在调用栈执行;遇到异步任务,交给浏览器其他后台线程处理,处理完成后,把回调放入宏任务或者微任务队列排队,不会阻塞主线程;
  3. 等到当前这个宏任务里面所有同步代码全部执行完毕,调用栈清空之后,立刻去清空全部微任务队列,把所有微任务依次执行完;执行微任务过程中新产生的微任务,也会在本轮一并执行;
  4. 微任务全部执行完成后,浏览器会进行 UI 渲染;
  5. 取下一个宏任务,重复上面整套流程。

整套流程下来就是事件循环机制

← 回题面卡

var / let / const ​

概念 · 变量声明

JavaScript

一句话
先进执行上下文:先处理声明,再一行一行执行。三种都是声明方式。var 函数作用域、顶层挂 window;let / const 块级、不挂 window。let 能再赋值,const 不能换绑。

主线怎么比

varletconst
作用域函数块块
顶层挂 window会不会不会
重复声明可以不行不行
再赋值可以可以不行
声明前访问undefined报错(TDZ)报错(TDZ)

可展开:JS 怎么执行
EventLoop 管异步怎么排队。当前这段同步代码是:进入一个 script 或函数时先创建执行上下文,先处理声明,再从第一行一行一行往下执行。提升发生在前一步,TDZ 发生在后一步还没走到赋值那一行。

可展开:变量提升
就是执行上下文先处理声明那一步。var 登记并初始化成 undefined,所以后面一行一行跑时,声明前访问不报错。let / const 也占坑,但不初始化。

可展开:暂时性死区(TDZ)
代码是一行一行往下执行的。还没执行到 let / const 那一行时,这个名字不能读、不能写,一碰就 ReferenceError。所以 console.log(a); let a = 1 报的不是 undefined,是指针还没走到声明那一行。

可展开:const 怎么赋值
声明时必须给值,const x; 不行。后面不能换绑(x = 2、obj = {} 都报错)。如果绑的是对象 / 数组,改内部可以:obj.n = 1、arr.push(1)。真要整份锁死再用 Object.freeze。

js
const obj = { n: 1 }
obj.n = 2   // 可以
obj = {}    // 报错,换绑不行

怎么记
var、let、const 都是 JS 声明变量的方式。先说作用域:var 是函数作用域,let 和 const 都是块级作用域。let 和 const 的区别是 let 声明的是变量,const 声明的是常量——也就是 var、let 的值可以再赋值,const 不行。然后再说 var 和 let:var 在 script 顶层声明会变成全局变量,还会挂到 window 上;let 在顶层也能访问,但不会挂到 window。var 可以重复声明,let 不可以。var 可以在声明前访问,值是 undefined;let 一用就直接抛错。

然后就是变量提升和暂时性死区,这是因为js执行上下文是分为了两步。第一步会先去处理变量然后再去一行一行的执行。

变量提升是因为第一步,var在生命的时候就初始化为了undefined,let和const会占坑但是不会去初始化,所以用var是undefined用let和const会报错的原因。

暂时性死区的话就是因为第二步,因为代码是一行一行执行的,执行到使用let的部分因为名字已经占坑了但是没有初始化那这个变量就不能用,提前使用就报错了。

const不是保证它声明的常量值不能改而是这个常量的内存指针指向不能改。也就是说基础数据类型它如果使用const的话就不能改了如果是引用数据类型在不改内存指针指向的情况下是可以改的比如数组的push或者使用obj.a=***这种方式是可以的。

← 回题面卡

函数写法 ​

概念 · 五种函数写法

JavaScript

一句话
函数常见五种写法:声明、表达式、箭头函数、new Function、对象 / class 方法。

五种

  1. 函数声明(函数提升):function fn() {},写在后面也能先调用
  2. 函数表达式(变量接收函数):const fn = function () {},赋上才能调
  3. 箭头函数(ES6):本质是表达式,没有自己的 this,不能 new
  4. new Function:字符串造函数,极少用,不形成闭包
  5. 对象方法 / class 方法:挂在对象或原型上;class 必须 new,底层还是原型
js
function fn() {}
const expr = function () {}
const arr = () => {}
const add = new Function('a', 'b', 'return a + b')
const obj = { say() {} }
class A { say() {} }

怎么记
JS 声明函数常见有五种写法。先说函数声明,就是 function 名字括号这种,它有函数提升,也就是写在后面也能先调用。然后再说函数表达式,它是用一个变量去接收一个函数,赋上才能调,没有函数提升。

然后就是 ES6 的箭头函数,本质也是函数表达式,只不过写成箭头。它没有自己的 this,也不能 new。

第四种是 new Function,用字符串去拼一个函数,这个极少用。

第五种是对象方法和 class 类方法,就是写在对象里面或者写在 class 里面的函数。class 的方法是挂在原型上的,必须 new,底层还是原型。

← 回题面卡

面向对象 ​

概念 · class 与面向对象

JavaScript

先别背三要素
你已经会写一个对象:const obj = { name: 'tom' }。要是页面上有 100 条狗,每条都要有自己的名字,还都能叫,一条条手写对象会疯。面向对象就是:先做一套模具,再用 new 按模具批量做出实例。

JS 这套模具底层就是你刚背过的原型:函数身上的 prototype 当模板。class 不是另一套语言,只是把「函数 + prototype」写成更好看的样子。

第一步:用 class 当模具

js
class Dog {
  // new Dog('旺财') 时会自动跑这里
  // this 就是这次 new 出来的那条狗
  constructor(name) {
    this.name = name // 这条狗自己的名字,每条狗各存各的
  }
  // 写在 class 里的方法,不会复制到每条狗身上
  // 实际挂在 Dog.prototype 上,所有狗共享这一份
  bark() {
    return this.name + ':旺旺'
  }
}

const a = new Dog('旺财')
const b = new Dog('来福')
a.bark() // 旺财:旺旺
b.bark() // 来福:旺旺
a.name === b.name // false,数据是各人各的
a.bark === b.bark // true,方法是同一份

new Dog('旺财') 可以对照你已经会的原型 / new:

  1. 先建一个空对象
  2. 把它的 __proto__ 指到 Dog.prototype(模具)
  3. 把这个空对象当成 this,去跑 constructor,把 name 填上
  4. 把这个对象返回给你,就是 a

所以:名字在实例自己身上,叫的方法在原型上。这就是常说的封装——数据和方法收在一套模具里,外面用 a.bark(),不用管内部怎么存。

必须 new。写成 Dog('旺财') 当普通函数调会报错。判断是不是这套模具做出来的:a instanceof Dog。

第二步:继承,子模具接着用爸的能力

猫也要名字、也要叫,还要多一个品种。不要把 Dog 再抄一遍,用 extends 接着用。

js
class Cat extends Dog {
  constructor(name, color) {
    // super(...) 就是:先跑爸的 constructor
    // 必须写在 this.xxx 前面,不然爸还没把模具接好,this 不能用
    super(name)
    this.color = color // 猫自己多出来的数据
  }
  bark() {
    // 猫把「叫」改成自己的实现,名字还是用爸给的 this.name
    return this.name + ':喵喵'
  }
}

const c = new Cat('橘坐', '橘色')
c.name  // 橘坐   这是爸 constructor 填上的
c.color // 橘色   这是自己填的
c.bark() // 橘坐:喵喵  用的是猫自己的 bark
c instanceof Cat // true
c instanceof Dog // true,顺着原型链,猫也算狗那套模具做出来的

原型链就是:c → Cat.prototype(有猫的 bark)→ Dog.prototype(有狗的别的方法)→ Object.prototype → null。找 bark 时自己这层有,就用猫的,不会再用狗的。

第三步:多态,同样叫 bark,各叫各的

js
function makeSound(animal) {
  // 不管你传狗还是猫,只要有 bark 就能调
  return animal.bark()
}
makeSound(a) // 旺财:旺旺
makeSound(c) // 橘坐:喵喵

同一个动作名 bark,不同模具自己实现。JS 没有 Java 那种接口,长得像就能用(鸭子类型)。

和 call 那段怎么对上
a.bark() 里的 this 是 a,因为是 a 调的。本质和 Dog.prototype.bark.call(a) 一样:方法在原型上,this 指到当前这条狗。

怎么记
面向对象就是先做模具再 new 出实例。JS 的模具底层是原型,class 只是写法好看。constructor 里 this.xxx 是这个实例自己的数据;class 里写的方法挂在 prototype 上大家共享。子类要用爸的能力就 extends,constructor 里先 super 再写自己的。不同类可以都有一个 bark,各写各的,这就是多态。必须 new。

← 回题面卡

ES6 新特性 ​

概念 · 解构、模板字符串、数组对象新方法

JavaScript

一句话
问 ES6 按块报:let / const、箭头函数、class、解构、模板字符串、展开运算符,再加上数组 / 字符串 / 对象一批新方法。

解构:把结构拆开赋给变量

js
const arr = [1, 2, 3]
const [a, b] = arr          // a=1 b=2,按位置拆
const [first, ...rest] = arr // rest=[2,3],剩下的收进数组

const obj = { name: 'tom', age: 18 }
const { name } = obj                 // 按名字拆,name='tom'
const { name: userName } = obj       // 重命名
const { age = 0, city = '北京' } = obj // city 没有才用默认值

模板字符串:反引号,能插值、能换行

js
const n = 'tom'
const s = `你好,${n}`  // 等价于 '你好,' + n

展开 ...:拷贝、合并(浅拷贝,不是深拷贝)

js
const arr2 = [...arr, 4]           // [1,2,3,4]
const obj2 = { ...obj, age: 20 }   // { name:'tom', age:20 },后者覆盖

数组最常考三个:映射、过滤、汇总

js
[1, 2, 3].map((x) => x * 2)        // [2,4,6] 新数组,原数组不动
[1, 2, 3].filter((x) => x > 1)     // [2,3]
[1, 2, 3].reduce((sum, x) => sum + x, 0) // 6

字符串 / 对象
字符串:includes、startsWith、trim、padStart。对象:Object.keys(obj) 拿键名,Object.assign(a, b) 浅合并(会改 a)。

箭头函数、let / const、class 已经单独成题,这里报个名、被追问再转过去。

怎么记
ES6 就是给 JS 补了一堆更好写的语法。先说变量,以前用 var,现在用 let 和 const,它们是块级作用域。然后再说函数,箭头函数没有自己的 this,定义的时候外层是谁就是谁。class 看起来像面向对象,底层还是原型。

然后就是解构赋值,把数组或者对象拆开赋给变量:数组按位置拆,对象按名字拆。模板字符串用反引号包起来,里面能插值也能换行,不用再加号拼接。三个点是展开运算符,数组对象都能拷贝、合并,不过是浅拷贝,不是深拷贝。

数组操作最常用三个:map 映射成新数组,filter 过滤,reduce 汇总成一个值。字符串常用 includes 判断包不包含。对象常用 Object.keys 拿所有键,或者三个点展开合并。

← 回题面卡

箭头函数 / 普通函数 ​

概念 · 箭头函数和普通函数

JavaScript

一句话
普通函数 this 看谁调用谁;箭头函数没有自己的 this,定义时就定死,也不能 new。

怎么比

普通函数箭头函数
this谁调用谁,能被 call 改看定义时外层,改不了
new可以不行
arguments有没有

怎么记
箭头函数和普通函数最大的区别是 this。普通函数的 this 是看谁调用它就指向谁,也可以用 call、apply、bind 去改。箭头函数没有自己的 this,它是看定义的时候外层是谁,这个定死了,call 也改不了。

然后再说两点:箭头函数不能 new,也没有 arguments,要用剩余参数。所以对象方法别随便写成箭头,this 容易不是这个对象;回调里面倒是适合用箭头,能把外层的 this 保住。

普通函数改 this 就这三下,箭头函数对这三下都没反应:

js
const obj = { name: 'tom' } // 想把 this 指到这个对象上
function say(from) {
  // this.name 看 this 是谁;from 是函数自己的参数
  console.log(this.name, from)
}

// call(this指向谁, 函数第1个参数, 函数第2个参数, ...)
// 第一个参数 obj = 这次调用的 this
// 后面的 'call' = 传给 say 的 from,一个一个挨着传
// 立刻执行,打印 tom call
say.call(obj, 'call')

// apply(this指向谁, [函数参数组成的数组])
// 第一个参数同样是 this
// 第二个参数必须是数组,数组里每一项对应 say 的参数
// 立刻执行,打印 tom apply
say.apply(obj, ['apply'])

// bind(this指向谁, 可预设的函数参数, ...)
// 参数含义和 call 一样,但不立刻执行
// 返回一个绑好 this 的新函数,以后再调
const fn = say.bind(obj, 'bind')
fn() // 这时才执行,打印 tom bind

const arr = () => console.log(this.name)
arr.call(obj) // 箭头函数没有自己的 this,call 的第一个参数没用,还是外层 this

← 回题面卡

JS 数据类型 ​

概念 · 基本类型与引用类型

JavaScript

一句话
JS 数据类型分两大类:基本类型变量里直接存值,赋值互不影响;引用类型变量里存地址,可能共看同一份对象。

有哪些

  • 基本类型:string、number、boolean、null、undefined、symbol(现在还可加 bigint, 说 7 种也行)
  • 引用类型:主要是 object(对象、数组、日期、正则都算);函数本质也是对象,只是 typeof 返回 'function'

怎么区别

基本类型引用类型
存什么值本身地址(指向堆里的对象)
赋值拷一份值,互不影响拷地址,可能指向同一对象
比较=== 比值=== 比是不是同一个地址

怎么记
JS 的数据类型主要分两类,一个基础数据类型还有一个引用数据类型,基础数据类型有,字符串、数字、布尔、null、undefined还有一个ES6增加的symbol类型。引用数据类型的话是函数、数组、对象。

基础数据类型中要注意null和undefined。undefined代表声明了但是没有赋值,null表示这是一个空值。

引用数据中函数本身也是对象不过typeof的时候会单独返会一个function类型。

基础数据类型和引用数据类型最大的区别是记存的什么,基础类型存的是值,引用类型存的是指针指向。举个例子

js
let a=1
let b=a
b=2
console.log(a) // 还是会输出1

let obj={name:'张三'}
let obj1=obj
obj1.name='李四'
console.log(obj.name) //输出会变成李四 因为内存指针指向的是同一个

← 回题面卡

隐式转换与比较 ​

概念 · 隐式转换、相等比较、类型判断

JavaScript

一句话
隐式转换是引擎自己转类型。比较分两块:比相不相等用 == / ===;比是什么类型用 typeof / instanceof / toString.call。instanceof 比的是原型链,不是 typeof 那个字符串。

隐式转换
没手写 Number() / String(),运算需要时引擎自己转。常见坑:==、+(有字符串就拼接)、if 转布尔、对象转原始值走 valueOf / toString。

比相不相等

  • == 先隐式转换再比
  • === 不转,类型和值都要一样
  • 基本类型 === 比值;引用类型 === 比是不是同一个地址
    业务默认 ===,要转就显式写,别背 == 规则表。

比较类型

干什么坑
typeof看基本类型null 是 'object';数组也是 'object'
instanceof沿原型链找构造函数跨 iframe 可能不准
toString.call最稳的类型字符串[object Array]、[object Null]

比较原型
a instanceof Ctor:从 a 的原型往上走,看有没有 Ctor.prototype。所以 [] instanceof Array、[] instanceof Object 都是 true。认数组更稳用 Array.isArray。

假值 / null
假值:0、空串、null、undefined、NaN、false。空数组空对象是真。undefined 是还没有,null 是主动说空。

怎么记
比较和转换经常一起考。隐式转换就是你没写 Number、String,引擎自己转,常见是 ==、加号、还有 if 转布尔。业务比较用 ===,需要转就显式写。

比较分两块。一块是比两个值相不相等:== 会先隐式转换再比,=== 类型和值都要一样;基本类型比值,引用类型比是不是同一个地址。另一块是比它是什么类型:typeof 看基本类型,但 null 会骗你说是 object;instanceof 是沿着原型链看右边的 prototype 在不在左边身上;要精细区分用 Object.prototype.toString.call。

假值就记 0、空串、null、undefined、NaN。空数组空对象反而是真。undefined 是还没有,null 是你主动说空。

← 回题面卡

原型 / 原型链 ​

概念 · 原型对象与原型链

JavaScript

一句话
函数的 prototype 是给实例用的模板;对象通过原型指向这份模板。自身没有的属性就沿原型一层层往上找,直到 null,这条链就是原型链。

三个词

谁有干什么
prototype函数原型对象,方法挂这里,实例共享
__proto__对象指向它的原型,一般等于构造函数的 prototype
原型链—自身没有就往上找,尽头是 null

三角关系
p.__proto__ === Person.prototype → Person.prototype.__proto__ === Object.prototype → Object.prototype.__proto__ === null。

怎么找属性
先看实例自己,没有就沿原型往上:构造函数 prototype → Object.prototype → null。p.say() 往往不在 p 上,是在 Person.prototype 上找到的。

可展开

  • instanceof:沿左边原型链,看右边 prototype 在不在
  • new:建对象、挂原型、当 this 执行构造、再决定返回谁
  • class / extends:语法糖,底层还是这套原型

怎么记
当我们声明一个函数的时候,它身上有一个 prototype,也就是原型对象,给后面 new 出来的实例当模板,方法挂在这个 prototype 上大家共享。然后就是对象的 proto,也就是原型,它指向这个模板——后面 new 出来的对象的 proto 等于函数的 prototype。原型链的话就是当我们找某个属性的时候,先看自己有没有,没有就顺着 proto 往上找,找到就用,没有就继续找,找到 Object.prototype,如果还没有再往上就是 null。这整个查找的链路就是原型链。

← 回题面卡

闭包 / 内存泄漏 / GC ​

概念 · 闭包、垃圾回收与内存泄漏

JavaScript

一句话
闭包是里面的函数还能用外面的变量。JS 自动回收内存,还有引用就不回收。内存泄漏就是该回收的还被挂着。

闭包

js
function outer() {
  let count = 0
  return function inner() {
    count++
    return count
  }
}
const fn = outer()
fn() // 1
fn() // 2  ← outer 都跑完了,count 还在,被 inner 记着

外面改不了 count,只有 fn 能加,这就是数据私有。好处是保存状态;坏处是 count 回收不了,fn 不用了要写成 fn = null,把引用断掉。

垃圾回收
JS 自己回收,不用手动 free。核心:还有引用就不回收。现在主流是标记清除——从全局、调用栈这些根出发,能碰到的留下,碰不到的当垃圾回收。以前的引用计数搞不定两个对象互相引用,基本不用了。

内存泄漏
该回收的还被引用挂着,积少成多页面会卡。常见:漏写 let 变成全局、定时器没 clear、DOM 从页面删了变量还指着它、事件没解绑、闭包里捏着很大的对象。解决就是对着把引用清掉。排查去 Chrome Memory 拍堆快照,看哪些对象只增不减。

怎么记
闭包简单理解就是函数嵌套函数,内部函数使用外部函数的变量,再把内部函数返回。好处就是数据私有化,隔离作用域,避免造成全局污染,早期没有ES6的使用闭包来实现模块化。缺点就是如果闭包函数一直存在,外层函数变量就会常驻内存。闭包不会被垃圾回收机制给回收掉。解决方式就是需要将使用完的闭包置空如fn=null切断引用避免常驻内存也可以被垃圾回收回收掉。

然后就是垃圾回收。JS 是自动回收的,核心就一句话:还有引用就不回收。现在主流是标记清除,从全局、调用栈这些根出发,能碰到的留下,碰不到的回收。以前的引用计数搞不定循环引用,基本不用了。

内存泄漏就是该回收的还被引用挂着,一直放不掉,页面会越来越卡。常见就是意外变成全局、定时器没清、DOM 删了变量还挂着、事件没解绑、闭包里捏着大对象。解决就是对着把引用清掉。排查用 Chrome 的 Memory 拍堆快照对比。

← 回题面卡

事件捕获 / 冒泡 ​

概念 · 捕获、冒泡、委托

JavaScript

一句话
点一下会先从外往里捕获,到目标,再从里往外冒泡。委托是把子元素的事件绑到父级,靠冒泡统一处理。

三阶段
外层父节点 → 往里走到你点的那个节点 → 再往外回去。默认 addEventListener 听的是冒泡;第三个参数传 true 才听捕获。

两下别混

  • stopPropagation:后面别再传了
  • preventDefault:别做浏览器默认动作(跳转、提交)

委托
列表不要给每个 li 绑一次,绑在 ul 上,点谁冒泡上来再看 e.target。监听少,后来动态加进去的 li 也能点到。

怎么记
事件捕获和冒泡说的是点一下这件事在 DOM 树上怎么走。先从外往里走一遍,这叫捕获;走到你真正点的那个节点,这叫目标;再从里往外走回去,这叫冒泡。我们平时 addEventListener 默认听的是冒泡。第三个参数传 true 才听捕获。

stopPropagation 是拦住别再往下传了,preventDefault 是别做浏览器自己的默认动作,这两件不是一回事。

然后就是事件委托,就是利用冒泡,把子元素的事件绑到父级统一处理。列表很常用,不用每个 li 都绑一次,后来动态插进来的也能点到。

← 回题面卡

Vue 基础 ​

Vue 是什么 ​

概念 · Vue、jQuery、React

Vue2基础

一句话
Vue 是渐进式前端框架,改数据页面就变。和 jQuery 比是声明式、有组件;和 React 比都是组件加虚拟 DOM,Vue 用模板、官方全家桶更齐。

Vue 是啥
用来做用户界面。你写模板和数据,它把两边绑上,数据变了视图跟着变,不用自己去选节点改内容。渐进式就是:可以先在一页里用一点,也可以加上路由、Vuex 做成完整单页。

和 jQuery
jQuery 是库,你自己 $('#xx').text('hi') 去改 DOM,命令式。Vue 是框架,改 data 就行,声明式。小页面改改 DOM 用 jQuery 够;页面有状态、要拆组件,用 Vue。

和 React
两边都组件化、都有虚拟 DOM。Vue 模板接近 HTML,路由和 Vuex 官方就有,表单常用 v-model。React 用 JSX,路由和状态库自己选,更灵活。没有绝对谁强,看团队和场景。

怎么记
Vue 是一套渐进式的前端框架,用来做用户界面。核心是数据驱动,你改数据,页面跟着变,不用自己去操作 DOM。

然后对比 jQuery。jQuery 是你自己选节点、自己改内容;Vue 是写模板和数据,它帮你更新视图。jQuery 适合小页面,做大了状态会乱;Vue 有组件、生命周期,适合中大型页面。

再对比 React。两边都是组件化、都有虚拟 DOM。差别是 Vue 用模板,更像写 HTML,官方有路由和 Vuex;React 用 JSX,生态更自由,路由状态库要自己选。Vue 表单双向绑定方便,上手相对快;React 更灵活。没有绝对谁更好,看团队和场景。Vue 的优势就是渐进式、上手快、官方全家桶齐、国内用的人多。

← 回题面卡

双向数据绑定 ​

概念 · defineProperty、Dep、Watcher、v-model

Vue2基础

先别背 Dep / Watcher
就当页面上有一个输入框,绑的是 data 里的 msg:

js
data() {
  return { msg: '你好' }
}
// 模板:<input v-model="msg">

你要同时做到两件事:代码里改 msg,输入框上的字跟着变;你在输入框里打字,msg 也跟着变。这就是双向绑定。

第一步:组件一创建,先把 msg 包一层
Vue2 用 Object.defineProperty 给已经有的每个属性加 getter / setter。以后不是直接碰那个字符串,而是:读 msg 走 getter,改 msg 走 setter。

第二步:第一次渲染,记下谁在用 msg
模板要画输入框,Watcher(可以理解成「这个输入框的监视员」)去读 msg,于是触发 getter。getter 里 Vue 就把这个 Watcher 记进 msg 的名单 Dep。等于输入框跟 Vue 说:我在用 msg,它变了叫我。

第三步:你改数据,名单里的人去改页面
代码里写 this.msg = '换了',走 setter。setter 翻出刚才那张名单,通知 Watcher 去更新视图。输入框上的字就变成「换了」。这是 数据 → 视图。

第四步:你打字,再写回 msg
v-model 拆开就是 :value="msg" 加上 @input。键盘一敲,input 事件把输入框里的字赋给 this.msg。msg 一改,又走第三步的 setter,别的用到 msg 的地方也会更新。这是 视图 → 数据。所以 v-model 不是另一套原理,就是绑值 + 听输入。

这套包法的坑
defineProperty 只包创建时已经有的属性。后来 this.age = 18 加字段,没有 getter/setter,改了页面不动,要用 Vue.set。数组用 arr[0] = x 或改 length 同样侦测不到,所以 Vue 把 push / splice 重写了一遍。嵌套对象还得一层层递归去包。

怎么记
Vue2 双向绑定是数据劫持加发布订阅。拿输入框绑 msg 来说。

劫持就是 Object.defineProperty,给 msg 加上 getter 和 setter。发布订阅就是:谁用了 msg,记到 msg 自己的名单里,msg 变了再通知名单里的 Watcher 去改页面。

输入框用 v-model 绑 msg。页面第一次渲染的时候会去读 msg,触发 getter,就把这个 Watcher 记进 msg 的名单。之后你改 msg,走 setter,名单里的 Watcher 被通知,去改输入框上的字。

← 回题面卡

Vue 生命周期 ​

概念 · 创建、挂载、更新、销毁

Vue2基础

一句话
组件从生到死四个阶段:创建、挂载、更新、销毁。面试常说:created 拿数据,mounted 碰 DOM,销毁前做清理。

四段

  • 创建:beforeCreate 几乎空的;created 有 data / methods,能发请求,还没有真实 DOM
  • 挂载:beforeMount 少用;mounted 已经上页面,可以 $refs、量尺寸、初始化图表
  • 更新:数据变了走 beforeUpdate → updated,平时少用,updated 里别再改数据
  • 销毁:beforeDestroy 清定时器、解绑、取消请求;destroyed 已经拆掉了

怎么记
生命周期就是组件从创建到销毁,框架给你几个时间点让你插手。我按四个阶段说:创建、挂载、更新、销毁。

先说创建。beforeCreate 的时候实例刚有,data 还没有,基本不用。created 的时候 data、methods 都有了,可以发请求、做初始化,但页面还没挂上去,碰不到真实 DOM。

然后再说挂载。beforeMount 马上要挂了,少用。mounted 是已经渲染到页面上了,这时候可以拿 DOM、初始化第三方库。请求也可以在这儿发,但不碰 DOM 的话放 created 更早一点。

然后就是更新。数据变了会 beforeUpdate,DOM 改完是 updated,这两个平时少用,updated 里再改数据容易循环。

最后销毁。beforeDestroy 里清定时器、解绑事件、取消请求。destroyed 已经拆掉了,收尾用。

如果组件被 keep-alive 缓存,切走不会销毁,走 deactivated,再进来走 activated。

← 回题面卡

常用指令 ​

概念 · 指令、v-if / v-show、computed / watch / methods

Vue2基础

一句话
指令是模板里的 v-。显隐看 v-if 和 v-show;算值用 computed,干事用 watch,点击用 methods。

常用指令

  • v-bind / : 绑属性
  • v-on / @ 绑事件
  • v-model 双向(值 + 输入)
  • v-for 循环,要配 key
  • v-if / v-show 显隐
  • 再补:v-html 插 HTML、v-slot 插槽、v-once 只渲染一次

v-if 和 v-show
v-if 条件为假就不渲染,切换等于销毁再建,贵。v-show 节点一直在,只改 display,切起来便宜。很少出现、或切换少用 if;频繁切用 show。

computed、watch、methods

  • methods:调用才跑,没有缓存,适合点击、提交
  • computed:由别的数据算出一个值,有缓存,依赖不变不重算,必须有返回值,适合给模板展示
  • watch:盯某个数据变了去做副作用(请求、写 storage),不求返回值

怎么记
常用指令我先报几个:v-bind 绑属性,v-on 绑事件,v-model 双向绑定,v-for 循环,v-if 和 v-show 做显隐。还有 v-html 插 HTML,v-slot 写插槽。

v-if 和 v-show 的区别是:v-if 条件为假的时候根本不渲染,切换会销毁再创建;v-show 一直在 DOM 里,靠 CSS 的 display 切换。所以频繁切换用 v-show,不常出现的用 v-if。

然后就是 computed、watch、methods。methods 是方法,点一次算一次,适合点击事件。computed 是计算属性,有缓存,依赖不变就不重算,适合从一个数据算出另一个给页面用,必须有返回值。watch 是侦听,数据变了去干副作用,比如发请求,它不求返回值。能用 computed 算出来的,就别用 watch。

← 回题面卡

组件传值 ​

概念 · props、emit、provide/inject、bus、Vuex

Vue2基础

一句话
按关系选通道:父子 props / emit,跨多层 provide / inject,全局 Vuex。

父子
父给子数据用 props。子要改、要通知父,用 $emit 抛事件,父来听。不要在子里直接改 props。

兄弟 / 跨多层 / 全局
兄弟找共同的父中转,或用 event bus。隔很多层用 provide / inject。很多页都要用的状态放 Vuex。

少用
$refs 父调子方法。$attrs / $listeners 把没声明的属性和事件继续往下传。

怎么记
组件传值按关系说,别一上来报一串名字。

先说父子,这个最常用。父传子用 props,子传父用 emit 抛事件,父来听。数据是单向的,子不要直接改 props,要改就告诉父去改。

兄弟组件没有直达的路,可以找共同的父中转一下,或者用 event bus。bus 小项目能用,大了不好追,容易乱。

隔了很多层,一层层 props 往下递太烦,用 provide 和 inject,祖先提供,后代注入。

很多页面都要的数据,比如用户信息,走 Vuex。

再补两个常被问的:父可以用 refs 拿到子组件去调方法;中间层还可以用 attrs 和 listeners 做透传。

← 回题面卡

路由传值 ​

概念 · query、params、$route

Vue2基础

一句话
跳页面带数据:query 挂问号后面,刷新还在;params 走路径动态段,没配进 path 刷新会丢。

query
/user?id=1。push 时带 query,页里 this.$route.query.id 取。写在地址栏里,刷新还在。

params
/user/1,路由得配 /user/:id。push 常用 name + params,页里 this.$route.params.id 取。只靠 name 传、path 里没有 :id,刷新就没了。

坑
密码、token 别放 URL。中文要编码。重要 id 要么写进 path,要么进页再请求。

怎么记
路由传值和组件传值不是一回事,这是跳页面带着数据走。Vue2 里常用两种:query 和 params。

query 就是挂在网址问号后面,比如 /user?id=1。跳的时候带 query,到了新页用 this.$route.query 去取。它写在地址栏里,刷新还在。

params 是走路径里的动态段,比如 /user/1,路由得配 /user/:id。跳的时候常用 name 加 params,到了用 this.$route.params 取。坑就是:你只靠 name 传、path 里没写 :id,刷新一刷新参数就丢了,因为它根本没写进 URL。

敏感信息别放地址栏。重要的 id 要么配进 path,要么进页面再请求一次。

← 回题面卡

Vuex ​

概念 · 五要素、持久化

Vue2基础

一句话
Vuex 管全局状态。state 存,getter 算,mutation 同步改,action 异步,module 拆。刷新会空,登录态要存本地再灌回。

五要素

  • state:数据放这儿
  • getter:像计算属性,从 state 算出衍生值
  • mutation:同步改 state,用 commit
  • action:发请求这类异步,完了再 commit,用 dispatch
  • module:仓库大了按业务拆

持久化
刷新内存清空。token、用户信息放 localStorage,启动时读出来 commit 回 store。列表详情进页再请求。也可以用 vuex-persistedstate,别整个 store 都塞本地。

怎么记
Vuex 是 Vue2 做全局状态管理的。五要素我按这条链路说:数据放 state 里,要从里面算出一个值用 getter,改数据必须走 mutation,而且得同步,因为调试工具要拍每次改之前改之后的快照。请求这种异步放 action 里,完了再 commit mutation。项目大了用 module 拆开。

然后就是持久化。Vuex 在内存里,一刷新就空了。登录 token、用户信息这类会存 localStorage,应用一启动再灌回 store。列表、详情这种业务数据,进页面重新请求就行,不要什么都往本地塞。也可以用 vuex-persistedstate 这种插件,选模块存,别把整个仓库都持久化。

← 回题面卡

Vue3 基础 ​

Vue3 双向绑定 ​

概念 · Proxy、ref、reactive、v-model

Vue3基础

一句话
还是数据变视图变、输入写回数据。底层从 defineProperty 换成 Proxy;组合式用 ref / reactive。

Proxy
代理整个对象,新增、删除属性能侦到,数组下标也好使。用到嵌套再代理,初始化更省。

组合式
ref 万能,脚本要 .value。reactive 给对象。v-model 还是绑值加事件;写在组件上默认 modelValue。

怎么记
Vue3 双向绑定还是两件事:数据变页面变,输入再写回数据。v-model 还是绑值加事件。底层从 defineProperty 换成 Proxy,增删、数组更好用。组合式用 ref / reactive;简单值用 ref,脚本要 .value。

← 回题面卡

Vue3 生命周期 ​

概念 · setup、onMounted、unmount

Vue3基础

一句话
四段还在。销毁改名 unmount。组合式在 setup 里写 onMounted。

选项式
created / mounted 还在。销毁是 beforeUnmount / unmounted。

组合式
setup 最早跑,没有 this,相当于 beforeCreate 加 created。DOM 用 onMounted,清理用 onBeforeUnmount。

怎么记
生命周期还是创建、挂载、更新、销毁。选项式大体一样,就是销毁改名叫 beforeUnmount 和 unmounted。

组合式在 setup 或者 script setup 里写。setup 最早执行,没有 this,这段就相当于 beforeCreate 加 created,发请求可以放这儿。要碰 DOM 写 onMounted。卸载前清定时器写 onBeforeUnmount。keep-alive 还是 onActivated、onDeactivated。

← 回题面卡

Vue3 组件传值 ​

概念 · defineProps、defineEmits、Pinia

Vue3基础

一句话
按关系没变。变的是写法:defineProps / defineEmits,全局换 Pinia。

怎么记
组件传值按关系还是父子、兄弟、跨多层、全局,和 Vue2 一样。

变的是写法。script setup 里父传子 defineProps,子传父 defineEmits。父要调子方法,子得 defineExpose,不然 refs 拿不到。Vue3 实例没有 $on $off 了,不能当事件总线。全局状态换成 Pinia。

再补两个:组件上的 v-model 默认是 modelValue;Vue2 的 $listeners 并进 $attrs 了。

← 回题面卡

Pinia ​

概念 · defineStore、action 改 state、持久化

Vue3基础

一句话
Vue3 用 Pinia 管全局状态。没有 mutation,action 里直接改 state。持久化和 Vuex 一个思路。

怎么记
Pinia 是 Vue3 官方推荐的状态库,对标 Vue2 的 Vuex。比 Vuex 简单:没有 mutation 这一层,action 里直接改 state。一个 store 就是一个模块,TS 也好写。

用法是 defineStore 定义,组件里 useUserStore 就能读写。

持久化和 Vuex 一样。刷新内存会空,token、用户信息存 localStorage,启动再灌回。也可以用 pinia-plugin-persistedstate。列表详情进页再请求,别整个仓库都塞本地。

← 回题面卡

UniApp ​

UniApp 是什么 ​

概念 · 多端编译、条件编译

UniApp

一句话
一套 Vue 代码,编译成 H5、小程序、App。端差异用条件编译切开。

怎么记
UniApp 就是写一套接近 Vue 的代码,编译器按目标平台转成各自能跑的东西:H5 是网页,微信是小程序包,App 是原生壳加前端资源。公共业务复用,各端不一样的 API、样式,用条件编译。#ifdef 后面跟平台名,是这个端才编进去;#ifndef 是不是这个端才保留。别在运行时再 if 平台,编译期就砍掉,包也更小。

← 回题面卡

UniApp 生命周期 ​

概念 · onLaunch、onLoad、onShow、onReady

UniApp

一句话
应用、页面、组件三套钩子。页面进出看 Uni 的 onLoad / onShow;组件里仍用 Vue 的 mounted。

怎么记
UniApp 生命周期不要和 Vue 混成一套。应用级有 onLaunch、onShow、onHide。页面级小程序常用 onLoad、onShow、onReady、onUnload。组件内部还是 Vue 的 created、mounted。

第一次进一个页:onLoad 接路由参数、发首次请求,只跑一次;接着 onShow 页面显示了;然后 onReady 首次渲染完,才能查节点、量高度。之后你跳走再回来,页面没销毁的话,多半只再走 onShow,不会再 onLoad。所以刷新数据放 onShow,接参放 onLoad。

← 回题面卡

UniApp 路由 ​

概念 · navigateTo、switchTab、传参

UniApp

一句话
五种跳转分清。url 只传短参数;tab 页不能靠 url 传参。

怎么记
跳转就这几个:navigateTo 往栈上压一页,能返回;redirectTo 换掉当前页;reLaunch 清空栈重新打开;switchTab 切底部 tab;navigateBack 返回。

传参一般拼在 url 上,下个页 onLoad 的 options 里取,中文要编码。坑有三个:switchTab 不能带 url 参数,要放 store 或 storage;参数太长会被截断,复杂数据只传个 id;页面栈大概十层,一直 navigateTo 会跳不动,该 redirect 就 redirect。页面之间要回传,可以用 eventChannel。

← 回题面卡

UniApp 分包 ​

概念 · 主包上限、subPackages、preloadRule

UniApp

一句话
主包有体积上限。非首屏拆到分包,进页再下载,启动更快。

怎么记
小程序主包超了就传不上去,启动也慢。分包就是把不是一打开就要的页面拆出去,先下主包进首页,点到分包页再下那一块。配置写在 pages.json 的 subPackages 里,root 是分包根目录。首页和 tabBar 必须留在主包。还可以配 preloadRule,在首页空闲时预下载某个分包。独立分包更隔离,不能直接引用主包私有文件。

← 回题面卡

rpx 与体积 ​

概念 · rpx、主包体积、权限降级

UniApp

一句话
样式优先 rpx。体积靠分包和砍资源。权限要写失败回调和降级。

怎么记
px 是写死的,不同屏幕会看起来不一样。rpx 按设计稿宽 750 换算,小程序和 UniApp 最常用。vw 是屏幕宽度的百分之一。多端样式优先统一 rpx。

体积大了就:分包减主包、图片压缩或放 CDN、UI 库按需引入、条件编译把别的端代码砍掉,再用构建分析看是谁占地方。

定位、相册这类权限,先 getSetting,调用要写 fail;用户拒绝了说明原因,引导去 openSetting,同时准备降级,比如手动选点,别让页面直接挂。

← 回题面卡

架构 · 进阶 ​

技术选型(按场景) ​

概念 · 场景形态、CSR/SSR、举例

架构

一句话
选型先看场景(官网 / H5 / 中后台 / 小程序…),再看要不要 SEO 出 HTML,最后才挑框架。

怎么记
技术选型先看场景:官网、中后台、H5、小程序,约束不同选法就不同。

官网要 SEO(Search Engine Optimization,搜索引擎优化),爬虫吃的是服务器返回的 HTML,常用 Next 或 Nuxt 做 SSR(Server-Side Rendering,服务端渲染)/ SSG(Static Site Generation,静态站点生成),让 HTML 里先有正文;纯客户端 SPA(Single Page Application,单页应用)首包常是空 #app,不适合当官网默认方案。中后台不在乎 SEO,Vue 或 React 加组件库做 CSR(Client-Side Rendering,客户端渲染)就行。微信活动页一般只在微信里打开、不靠外搜,SEO 不是重点;更看适配、包体、微信 JSSDK(JavaScript Software Development Kit,网页 JS 开发工具包)。小程序看 uni 还是原生;多团队巨石再谈微前端。

同形态再补 Vue / React;场景先定,框架后选。

← 回题面卡

Vue2 vs Vue3 ​

概念 · Proxy、组合式、Pinia、unmount

架构

一句话
指令那套差不多。差在响应式、组合式、销毁钩子名字、状态库;新项目倾向 3。

怎么记
Vue2 和 Vue3 指令、v-if / v-show、路由传参这些基础用法差不多,差在取舍。

响应式:Vue2 是 Object.defineProperty,Vue3 换成 Proxy,增删属性和数组更好用。组织代码:选项式到组合式。销毁钩子改名 unmount。状态库 Vuex 换 Pinia。还能提多根、Teleport、TypeScript(类型系统)、tree-shake(摇树优化)。

新项目倾向 3:响应式坑少、编译打包更好、组合式更友好、官方生态都在 3 上。老项目能跑就不必硬换。

展开对照

← 回题面卡

Vuex vs Pinia ​

概念 · 没有 mutation、defineStore

架构

一句话
Pinia 对标 Vuex。少了 mutation,action 里直接改 state,一个 store 一个模块。

怎么记
Vuex 和 Pinia 都是管全局状态的。Vue2 常用 Vuex,Vue3 官方推 Pinia,对标 Vuex、写法更简单。

Vuex 改 state 必须走同步的 mutation,异步放 action 再 commit,模块要嵌进一个大 store。Pinia 没有 mutation,action 里直接改;一个 defineStore 就是一个独立 store,TS 也好写。

持久化两边一样:刷新内存空,登录态存 localStorage 再灌回,列表详情进页再请求。Vue3 用 Pinia 是因为官方推、样板少。状态很少,props / provide 够用也可以先不上。

展开对照

← 回题面卡

虚拟 DOM ​

概念 · Virtual DOM、diff、patch

架构

一句话
用 JS 对象描述真实 DOM。数据变了先 diff,再批量改真实 DOM,少直接硬改节点。

怎么记
虚拟 DOM(Virtual DOM,虚拟文档对象树)用 JS 对象描述真实 DOM(Document Object Model,文档对象模型)。数据一变,先在内存里 diff(差异对比),再 patch(打补丁)到真实 DOM。

比直接改真实 DOM:一改常带重排重绘,贵;虚拟 DOM 在内存里算差异、合并更新,少瞎改。同一套描述还能跨端渲染。不是绝对更快,复杂页面、频繁更新时更划算。

展开对照

← 回题面卡

Vite vs Webpack ​

概念 · ESM、esbuild、Rollup

架构

一句话
Webpack 开发常先打包再启动;Vite 开发靠原生 ESM + esbuild,生产用 Rollup。

怎么记
Webpack 开发常先打包再启动,项目一大就慢。Vite 开发不先全量打包:源码走浏览器原生 ESM(ECMAScript Modules,ECMAScript 模块),按需请求;第三方依赖用 esbuild 预构建成 ESM,冷启动和热更新都快。生产打包 Vite 交给 Rollup。记:开发快靠 ESM + esbuild,产物靠 Rollup。老 Webpack 能跑不必硬换。

展开对照

← 回题面卡

工程 · 实践 ​

性能优化 ​

概念 · 加载、渲染、打包、运行

工程

一句话
四块清单:加载、渲染、打包、运行;先量再优化。

怎么记
性能优化分四块。加载:路由懒加载、图片压缩、CDN(Content Delivery Network,内容分发网络)、HTTP 缓存、骨架屏。渲染:少重排重绘、动画走 transform/opacity、虚拟列表。打包:代码拆包、Tree-shaking(树摇)、压缩、分析体积。运行:防抖节流、清监听和定时器。先量再优化,用 Lighthouse 或打包分析找瓶颈。

展开对照

← 回题面卡

懒加载 ​

概念 · 动态 import、异步组件

工程

一句话
路由 / 组件按需 import,减小首包。

怎么记
懒加载别把全站 JS 打进首包。路由写 () => import(...),组件用 defineAsyncComponent,用到再下载 chunk(代码分块)。首屏更小;小页不必硬拆,关键路由可 prefetch(预加载)。

← 回题面卡

虚拟列表 ​

概念 · 可视区渲染、scrollTop

工程

一句话
只渲染看得见的那一段 DOM,用总高度撑滚动条。

怎么记
卡是因为 DOM(Document Object Model,文档对象模型)节点太多。虚拟列表只渲染可视区:容器固定高度,按 scrollTop(滚动距离)算起始下标,只建一屏节点,用总高度撑滚动条,滚动时换数据。还可分页、稳定 key、图片懒加载。

← 回题面卡

Axios 封装 ​

概念 · 拦截器、业务码、401

工程

一句话
create 实例 + 请求塞 token + 响应拆业务码。

怎么记
axios.create 出实例,统一 baseURL 和超时。请求拦截写 token(令牌),可加 loading。响应拦截先看 HTTP 状态,再看业务 code,成功 return data,失败统一提示再 reject。401(未授权)跳登录或无感刷新。业务页只调封装方法。

← 回题面卡

token 无感刷新 ​

概念 · refresh、排队重试

工程

一句话
只刷新一次,其它 401 排队,换票后重试。

怎么记
access token(访问令牌)过期后,用 refresh token(刷新令牌)换新票。同时只刷新一次,其它 401 请求排队,换成功再带新 token 重试。refresh 接口别走死循环;refresh 也失效就清登录态。

← 回题面卡

构建 · 追问 ​

先建立地图:Webpack 打包 = 从入口找依赖 → Loader 翻译各类文件 → Plugin 加工整次构建 → 输出 bundle。Babel 管 JS 新语法怎么转成旧环境能跑的;模块化管文件怎么互相 import;Tree-shaking 靠 ESM 删掉没用到的代码;HMR 管开发时改代码不整页刷新。建议按下面顺序看。

Webpack 流程 ​

概念 · entry、依赖图、bundle

构建追问

是什么
Webpack 是打包工具:把你写的很多 .js、.vue、.css 文件,从入口文件(entry,一般是 main.js)出发,顺着 import 一路找依赖,拼成浏览器能加载的少量文件(bundle,打包文件)。

一句话
入口 → 依赖图 → Loader 转 → Plugin 加工 → 出包。

怎么记
可以把 Webpack 想成工厂流水线。入口是原材料入口,Webpack 顺着 import 画出一张依赖图(谁引用了谁)。每个文件过 Loader(加载器)翻译成 JS 能处理的模块,Plugin(插件)在整条流水线的节点上做注入 HTML、压缩之类的事,最后产出 bundle(打包文件)给浏览器下载。

记五词:入口 → 依赖图 → 转译 → 加工 → 出包。后面 Loader、Plugin、Tree-shaking 都挂在这条线上。

← 回题面卡

Loader / Plugin ​

概念 · 转文件 vs 管流程

构建追问

是什么
Webpack 只原生认识 JS。.vue、.scss、图片、TypeScript 它看不懂,需要 Loader(加载器)——按文件类型配置的「翻译官」:读入这个文件,输出 JS 模块。
Plugin(插件)不管单文件,而是挂在整次构建的生命周期上,做打包、生成 index.html、压缩、拷贝资源等「流程级」的事。

一句话
Loader 转文件,Plugin 管流程。

怎么记
Loader 和 Plugin 别混。Loader 是一种文件一个翻译官:.vue 用 vue-loader,新语法 JS 用 babel-loader,样式用 css-loader,Less/SCSS 还要先 less-loader / sass-loader 编成 CSS。Plugin 是包工头:比如 HtmlWebpackPlugin 把 JS 插进 HTML,DefinePlugin 注入环境变量,压缩插件负责瘦身。

口诀:Loader 转文件,Plugin 管流程。面试举例:babel-loader 转 JS 语法,css-loader 处理样式;Plugin 做 HTML 注入和压缩。

← 回题面卡

Tree-shaking ​

概念 · ESM、sideEffects

构建追问

是什么
Tree-shaking(树摇):打包时把 import 进来但实际没用到的导出删掉,减小体积。像摇树把枯叶摇掉。
能摇的前提:打包器在编译期就能算清「谁用了谁」——ESM(ECMAScript Modules,ECMAScript 模块)的 import / export 写在文件顶部,可以静态分析。CJS(CommonJS,CommonJS 模块规范)的 require() 可能运行时才决定引哪个文件,打包器算不准,就很难摇干净。

一句话
摇掉未使用的导出;ESM 才能静态分析。

怎么记
工具库常常 export 很多函数,你只 import { debounce },Tree-shaking ideally 只把 debounce 打进包,别的不要。
为什么 ESM 行、CJS 不行:import { a } from './x' 编译期就定了;require(path + name) 运行时才知引谁。
生产环境还要开压缩,并在 package.json 配 sideEffects(副作用声明):告诉打包器哪些文件删了会影响全局(比如引入了 polyfill 的文件不能乱删)。

← 回题面卡

HMR ​

概念 · WebSocket、换模块

构建追问

是什么
HMR(Hot Module Replacement,热模块替换):开发时你改了代码,不用整页 F5 刷新,只替换改动的那几个模块;页面状态(比如表单里已填的字)尽量保留。
流程:保存文件 → 开发服务器重新编译改动模块 → 通过 WebSocket(全双工通信,浏览器和开发服长连接)通知浏览器 → 浏览器用新模块替换旧的。

一句话
改代码只换相关模块,不整页刷。

怎么记
没有 HMR 时,改一行 CSS 也要整页刷新,表单状态全丢。HMR 是「热补丁」:改哪个模块换哪个。
Vite 开发靠原生 ESM(ECMAScript 模块),浏览器直接请求模块,改一个文件就更新一条请求,所以快。
Webpack 开发要自己注入 HMR runtime(热更新运行时),通过 WebSocket 收通知再换模块。面试说到「WebSocket 通知、换模块、尽量保状态」就够。

← 回题面卡

Babel ​

概念 · 语法降级、preset-env

构建追问

是什么
Babel(JS 语法转译器)不是打包器,常和 Webpack / Vite 一起用。作用:把新 JS 语法(箭头函数、可选链…)和 JSX(JavaScript XML,在 JS 里写 HTML 标签)转成旧浏览器也能跑的代码。
@babel/preset-env:按你在 browserslist 里配置的目标浏览器,按需决定转哪些语法、引哪些 polyfill(兼容垫片,补上旧浏览器缺的 API),避免「全部转成 ES5 + 塞满垫片」导致包巨大。

一句话
新语法转旧环境能跑的;preset-env 按目标浏览器按需转。

怎么记
你写 const、?.、JSX,Chrome 新版本能跑,老 WebView 可能不行。Babel 在构建阶段先翻译成兼容写法,再交给打包器。
Loader 里常见 babel-loader:文件进 Webpack 流水线时过一遍 Babel。
preset-env 就是「按目标浏览器清单自动选转换规则」,比手动一个个配 plugin 省事。面试:Babel 管语法降级,Webpack/Vite 管打包,分工不同。

← 回题面卡

模块化演进 ​

概念 · IIFE、CJS、ESM

构建追问

是什么
早期 JS 没有 import,所有代码塞一个文件。后来大家想办法拆文件、避免变量污染,一路演进到今天的 ESM(ECMAScript Modules,ECMAScript 模块,import / export)。

阶段干什么一句话
IIFE(Immediately Invoked Function Expression,立即执行函数)用 (function(){ ... })() 包一层,变量关在自己作用域里自封装,没有官方标准
CJS(CommonJS)Node 的 require() / module.exports,运行时才加载服务端常用
AMD(Asynchronous Module Definition,异步模块定义)浏览器里异步 require,代表 RequireJS配置重,现在少见了
ESM语言标准 import / export,编译期就能分析依赖现在前端工程默认

一句话
IIFE → CJS → AMD → ESM;现在默认 ESM。

怎么记
模块化解决两件事:怎么拆文件、怎么不互相污染变量。
现在 Vue / Vite 项目写 import xxx from 'yyy',就是 ESM。它和 Tree-shaking、Vite 开发快都有关系——ESM 静态、打包器能算依赖。
CJS 的 require 在 Node、老工具链里还很多,但前端新工程以 ESM 为主。面试按时间线讲四代,最后落「现在用 ESM,能 Tree-shaking,Vite 开发也靠它」。

← 回题面卡

项目 · 实战 ​

自由发挥用。uni-app 不是我选型。百科 → 项目笔记。

1 分钟项目介绍 ​

概念 · 飞啾小程序:后台多、讲小程序;App 全、小程序轻履约再延展

项目

一句话
后台做过不少;面试讲飞啾小程序——后做、比 App 瘦,先下单核销,再延俱乐部/志愿者入驻。

怎么记
偏后台业务多,代表项目说飞啾小程序。小程序后做、版本差 App 一截,形态也不一样:App 要完善(下单、用户入驻、商家入驻等);小程序早期只管下单 + 核销。后来才慢慢加俱乐部入驻、志愿者入驻,仍比 App 收敛。技术细节等追问再展开。

← 回题面卡

难点总览 ​

概念 · 高德合规链路 / 登录 / 列表

项目

一句话
高德可先讲:合规踩坑 → SDK → 加 ESM 导出;再登录、列表。

怎么记
高德(先讲、不吹超难):开放文档接口先跑通选点,但合规不熟;对照微信后小程序不允许那种接法,改官方 SDK。SDK bundle 撞 Vite ESM;二次改造加 ESM 导出、不动内部。卡久是只换 import;想通改导出才顺。登录、列表见专卡。

← 回题面卡

登录回跳 ​

概念 · 意图、栈、Composable

项目

一句话
解决回哪去、返回不撞登录;页面手势 + Composable 副作用。

怎么记
为了:登录后回对页,栈干净。当前页办事 navigateBack;去另一页记 pendingUrl 再 redirectTo。封装:页面管手势,Composable 管 code/token/用户信息/广播,业务 withLogin。pendingUrl 只内存。

← 回题面卡

高德接入 ​

概念 · 合规弯路、SDK、bundle vs ESM、加导出、排查方向

项目

一句话
先做出能跑但不能上的接口版 → 合规逼着换 SDK → bundle 撞 Vite ESM → 加导出不改内部;卡久在 import。

怎么记
说难不算硬核:不是算法,是合规不熟 + 构建形态,外加排查偏了。

选点接高德,我先按开放平台文档的接口做——文档全、联调快,功能能跑,但没先确认微信小程序允不允许。补看规范后发现小程序端不允许那种通用接口接法(偏 Web/通用),只能推倒改官方 SDK。

换 SDK 后又过不了构建:SDK 按 bundle 整包导出,项目 Vite 默认 ESM(import/export),Vite 按 ESM 吃包,SDK 导出对不上。做法是二次改造、向上兼容:不动 SDK 内部,补一种 ESM 认可的导出,让工程能正常 import——改的是「包怎么被认出来」,不是改地图能力。

真正耗时的不是方案本身,是我一度默认问题在引用方式,换各种 import 硬拧。想通冲突在导出形态之后才一下顺了。真机授权、域名追问再补。完整口播见百科该节末尾。

← 回题面卡

列表滚动假白 ​

概念 · 假白原因、横跳原因、scroll-view

项目

一句话
假白=整页滚高度没收干净;横跳=硬纠偏;正解=页内 scroll-view。

怎么记
假白:筛选后列表变矮,iOS 整页可滚高度没收干净。横跳:锁高度或狂 pageScrollTo 和手指抢控制权。解决:外层不滚,一屏高 scroll-view 内滚。不是虚拟列表。

← 回题面卡

登录态与用户信息分发 ​

概念 · token 存储、广播分发、401

项目

一句话
token 本地存;用户信息进全局再广播;意图只内存。

怎么记
token 本地存,请求头统一带。用户信息登录后拉一次进 store,成功/401 广播,各页一起刷新或清空。pendingUrl 不持久化。401 清态再走登录门控。

← 回题面卡

体验优化对比 ​

概念 · 列表、登录、高德

项目

一句话
列表内滚、登录换页加锁、高德 ESM 适配 + 防抖。

怎么记
列表假白横跳 → scroll-view。登录撞栈 → redirectTo + 锁。高德构建加导出;运行防抖 requestId、卸 map。

← 回题面卡