前言
鄙人大三在读,也是从大三才开始接触前端,起步算比较晚了,但是不想错过今年的春招,所以还是鼓足了勇气尝试投了下大厂。那么其实在面腾讯之前我也面过其他公司,发现如腾讯之类的大厂在实习生面试这块效率非常高,平均半小时面完一场技术面。
面试题整体来说难度并不高,但是本人面试经验还是太少,许多问题回答的不是很好,可以看出来一点:大厂面试很多时候并不是为了考倒候选人,而是为了发掘候选人身上的闪光点和潜力。从我的体验来看,有的问题回答的不好似乎并不会立刻把你"枪毙",如果你在某些板块研究的比较深入,你的答案足够有深度,同样能得到面试官的认可。
本文旨在分享体验和对面试答案的思考,如有错误请批评指正。
面试题
那么我们进入正题:
国际惯例先来段自我介绍,我在自我介绍重点说明了我对AI领域比较感兴趣。果然面试官第一个问题就是让我谈谈对AI的理解。
Q:你认为AI对前端有什么帮助?
这个问题我首先想到的是huggingface社区上比较火的Transformers.jsTransformers.js - Hugging Face 机器学习平台,那么有transformer+ webgpu + webworker前端就可以轻松完成在浏览器端直接运行15亿参数的DeepSeek-R1模型,这应该也算是AI为前端带来的一个新的可能吧。 我再一个提的就是AI编辑器了,那cursor谁用谁知道,确实强;不过字节推出的Trae也是后来居上,虽然就我的体验来看还是cursor好用一点,这里就不过多赘述了~
Q:大模型的分类和聚类有了解过吗?
这个其实触及知识盲区了,附上DeepSeek的结果:
1. 分类(Classification)
定义 :
分类是监督学习任务,目标是将输入数据分配到预定义的类别中(例如文本分类、图像识别)。
大模型的应用方式:
-
微调(Fine-tuning) :
利用预训练大模型(如BERT、RoBERTa)作为基础,在特定任务(如情感分析、新闻分类)上进行微调。
- 流程:在预训练模型后添加分类层(如全连接层),用带标签的数据训练整个模型。
- 优势:充分利用预训练模型的语义理解能力,适应具体任务。
- 示例:用BERT微调实现电影评论的情感二分类(正面/负面)。
-
Prompt-based Learning :
通过设计提示(Prompt)引导大模型直接生成分类结果,减少对标注数据的依赖。
- 示例:输入"这篇文章的情感是____",让模型生成"正面"或"负面"。
-
Few-shot/Zero-shot分类 :
利用大模型的泛化能力直接分类,无需微调或仅需极少量样本。
- 示例:GPT-3通过上下文学习(In-context Learning)直接分类未知领域的文本。
2. 聚类(Clustering)
定义 :
聚类是无监督学习任务,目标是将数据划分为内在的组别(无预定义标签),如用户分群、主题发现。
大模型的应用方式:
-
特征提取 :
利用大模型生成高质量的特征表示(Embedding),再用传统聚类算法(如K-means、DBSCAN)分组。
- 示例:用BERT提取文本嵌入,再通过K-means聚类新闻文章的主题。
-
对比学习增强聚类 :
结合对比学习(Contrastive Learning)优化特征空间,提升聚类的可分性。
- 示例:SimCSE通过对比学习增强句子嵌入,提升聚类效果。
-
端到端聚类模型 :
将聚类目标融入大模型训练(如DeepCluster),但大模型通常更侧重特征提取而非直接聚类。
3. 大模型在分类与聚类中的优势
-
语义理解能力:
- 大模型(如LLM)能捕捉复杂语义,解决传统模型难以处理的模糊边界问题。
-
少样本学习:
- 通过Prompt或上下文学习,降低对标注数据的依赖。
-
跨模态能力:
- 多模态大模型(如CLIP)可统一处理文本、图像等跨模态分类/聚类任务。
4. 典型应用场景
-
分类:
- 文本分类(垃圾邮件检测、意图识别)
- 图像分类(医学影像诊断、商品分类)
- 多模态分类(图文匹配、视频内容审核)
-
聚类:
- 用户画像分群(电商、社交网络)
- 文档主题发现(科研论文、新闻聚合)
- 异常检测(金融欺诈、工业质检)
总结
- 分类更适合有明确标签的场景,依赖大模型的微调或Prompt工程;
- 聚类更依赖大模型的特征提取能力,需与传统算法结合;
- 两者均可通过大模型提升效果,但需根据任务需求选择合适方法(如数据量、标注成本)。
Q:断点续传。常考题了,大厂面试一般会从你简历里的项目找到提问的点然后考察你对这块的开发经验和理解的深度。
断点续传顾名思义即断点 + 续传,所以第一步先实现"断点",也就是暂停上传。
原理是使用 XMLHttpRequest 的 abort
方法,可以取消一个 xhr 请求的发送,为此我们需要将上传每个切片的 xhr 对象保存起来。
由于当文件切片上传后,服务端会建立一个文件夹存储所有上传的切片,所以每次前端上传前可以调用一个接口,服务端将已上传的切片的切片名返回,前端再跳过这些已经上传切片,这样就实现了"续传"的效果。
关键点在于前端/服务端需要记住
已上传的切片,这样下次上传就可以跳过之前已上传的部分。一开始我是在前端使用localStorage记录已上传的切片hash,但是做好了发现有个问题,如果换了个浏览器就失去了记忆的效果,所以还是结合服务端来做更完善,服务端保存已上传的切片hash,前端每次上传前向服务端获取已上传的切片。
然后就是怎么生成文件和切片的 hash,我开始采取的做法是使用文件名 + 切片下标作为切片 hash,但是后面意识到这样做文件名一旦修改就失去了效果,而事实上只要文件内容不变,hash 就不应该变化,所以正确的做法是根据文件内容生成 hash,所以用到一个库叫 spark-md5
,md5加密我是在学习浏览器缓存的时候要需要计算Etag值的时候了解到的,它可以根据文件内容计算出文件的 hash 值
然后如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会引起 UI 的阻塞
,导致页面假死状态,这里可以使用 web-worker 在 worker 线程计算 hash,这样用户仍可以在主界面正常的交互。
总的来讲就是:
使用 spark-md5 根据文件内容算出文件 hash,通过 hash 可以判断服务端是否已经上传该文件,从而直接提示用户上传成功(秒传),通过 XMLHttpRequest 的 abort 方法暂停切片的上传,上传前服务端返回已经上传的切片名,前端跳过这些切片的上传.
Q:egg和express的区别
Express.js 是 Node.JS 诞生之初,最早出现的一款框架,由于发展历史悠久插件生态强大,现在依然很流行。
随着ECMAScript的发展,推出了generator yield 语法,JS向同步方式写异步代码迈出了一步,KOA也随之诞生。
KOA 是一个可定制的框架,开发者可以根据自己的需要,定制各种机制,比如多进程处理、路由处理、上下文 context 的处理、异常处理等,非常灵活。Egg.js 就是在 KOA 基础上,做了各种比较成熟的中间件和模块,可以说是在 KOA 框架基础上的最佳实践,用以满足开发者开箱即用的特性。
区别
-
Express
- 需自行实现日志、错误处理、安全策略(如 CSRF、XSS)等,或依赖第三方库。
- 依赖第三方中间件(如
body-parser
、cors
),需要手动集成和配置。 - 无强制结构 :开发者需自行设计目录(如
routes/
、controllers/
、models/
)。
-
Egg
-
内置企业级支持:
- 日志分级(DEBUG/INFO/ERROR)。
- 安全机制(默认开启 CSRF、XSS 防护)。
- 进程管理(多进程模型,如 Agent Worker 机制)。
-
内置插件机制 :官方提供高质量插件(如
egg-mongoose
、egg-validate
),开箱即用。 -
插件可复用配置,支持按需加载(如不同环境启用不同插件)。
-
约定目录结构 :例如
app/controller
(控制器)、app/service
(业务逻辑)、config/
(配置)等。
-
Express 是基于 callback 来处理中间件的,而 KOA/egg 则是基于 await/async;
在异步执行中间件时,Express 并非严格按照洋葱模型执行中间件,而 KOA/egg 则是严格遵循的;
这个表应该还算直观
维度 | Express | Egg |
---|---|---|
定位 | 轻量、灵活 | 企业级、规范化 |
架构 | 中间件线性执行 | 洋葱模型(Koa 核心) |
学习成本 | 低 | 中(需学习约定) |
适用阶段 | 入门/小型项目 | 中大型团队项目 |
生态 | 海量中间件(碎片化) | 高质量官方插件(标准化) |
Q:讲讲洋葱模型
洋葱模型是其中间件(Middleware)执行流程的核心机制,形象地描述了请求从外到内、响应从内到外的双向处理过程。
核心机制:
- 中间件执行顺序
中间件像洋葱的层一样包裹,请求(Request
)从外层中间件逐层向内传递,响应(Response
)则从最内层中间件逐层向外返回。 - 双向控制权
每个中间件可以通过await next()
暂停自身代码,将控制权交给下一个中间件。待后续中间件全部执行完毕后,再"回溯"执行当前中间件剩余的代码。
Q:XSS攻击
我简历中的某个项目设计"发帖"操作,那么到这里可以看出来大厂面试你需要对写进简历的所有内容都有深入研究。
Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。为了和 CSS 区分,这里把攻击的第一个字母改成了 X,于是叫做 XSS。
XSS 的本质是:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。
而由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,或者利用这些信息冒充用户向网站发起攻击者定义的请求。
在部分情况下,由于输入的限制,注入的恶意脚本比较短。但可以通过引入外部的脚本,并由浏览器执行,来完成比较复杂的攻击策略。
XSS 分类
根据攻击的来源,XSS 攻击可分为存储型、反射型和 DOM 型三种。
存储型 XSS
存储型 XSS 的攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中。
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
反射型 XSS
反射型 XSS 的攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。
由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。
DOM 型 XSS
DOM 型 XSS 的攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL。
- 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。
XSS 攻击的预防
通过前面的介绍可以得知,XSS 攻击有两大要素:
- 攻击者提交恶意代码。
- 浏览器执行恶意代码。
所以预防的关键是"防止浏览器执行恶意代码"来防范 XSS。这部分分为两类:
- 防止 HTML 中出现注入。
- 防止 JavaScript 执行时,执行恶意代码。
预防存储型和反射型 XSS 攻击
存储型和反射型 XSS 都是在服务端取出恶意代码后,插入到响应 HTML 里的,攻击者刻意编写的"数据"被内嵌到"代码"中,被浏览器所执行。
预防这两种漏洞,有两种常见做法:
- 改成纯前端渲染,把代码和数据分隔开。
- 对 HTML 做充分转义。
纯前端渲染
纯前端渲染的过程:
- 浏览器先加载一个静态 HTML,此 HTML 中不包含任何跟业务相关的数据。
- 然后浏览器执行 HTML 中的 JavaScript。
- JavaScript 通过 Ajax 加载业务数据,调用 DOM API 更新到页面上。
在纯前端渲染中,我们会明确的告诉浏览器:下面要设置的内容是文本(.innerText
),还是属性(.setAttribute
),还是样式(.style
)等等。浏览器不会被轻易的被欺骗,执行预期外的代码了。
但纯前端渲染还需注意避免 DOM 型 XSS 漏洞(例如 onload
事件和 href
中的 javascript:xxx
等,请参考下文"预防 DOM 型 XSS 攻击"部分)。
在很多内部、管理系统中,采用纯前端渲染是非常合适的。但对于性能要求高,或有 SEO 需求的页面,我们仍然要面对拼接 HTML 的问题。
转义 HTML
如果拼接 HTML 是必要的,就需要采用合适的转义库,对 HTML 模板各处插入点进行充分的转义。
常用的模板引擎,如 doT.js、ejs、FreeMarker 等,对于 HTML 转义通常只有一个规则,就是把 & < > " ' /
这几个字符转义掉,确实能起到一定的 XSS 防护作用,但并不完善,最好还需要使用一些库来对这个方案进行扩充。
预防 DOM 型 XSS 攻击
DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。
在使用 .innerHTML
、.outerHTML
、document.write()
时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent
、.setAttribute()
等。
如果用 Vue/React 技术栈,并且不使用 v-html
/dangerouslySetInnerHTML
功能,就在前端 render 阶段避免 innerHTML
、outerHTML
的 XSS 隐患。
DOM 中的内联事件监听器,如 location
、onclick
、onerror
、onload
、onmouseover
等,<a>
标签的 href
属性,JavaScript 的 eval()
、setTimeout()
、setInterval()
等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。
XSS 的检测
- 使用通用 XSS 攻击字符串手动检测 XSS 漏洞。
css
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
它能够检测到存在于 HTML 属性、HTML 文字内容、HTML 注释、跳转链接、内联 JavaScript 字符串、内联 CSS 样式表等多种上下文中的 XSS 漏洞,也能检测 eval()
、setTimeout()
、setInterval()
、Function()
、innerHTML
、document.write()
等 DOM 型 XSS 漏洞,并且能绕过一些 XSS 过滤器。
小明只要在网站的各输入框中提交这个字符串,或者把它拼接到 URL 参数上,就可以进行检测了。
perl
http://xxx/search?keyword=jaVasCript%3A%2F*-%2F*%60%2F*%60%2F*%27%2F*%22%2F**%2F(%2F*%20*%2FoNcliCk%3Dalert()%20)%2F%2F%250D%250A%250d%250a%2F%2F%3C%2FstYle%2F%3C%2FtitLe%2F%3C%2FteXtarEa%2F%3C%2FscRipt%2F--!%3E%3CsVg%2F%3CsVg%2FoNloAd%3Dalert()%2F%2F%3E%3E
- 使用扫描工具自动检测 XSS 漏洞。例如 Arachni、Mozilla HTTP Observatory、w3af 等。
整体的 XSS 防范是非常复杂和繁琐的,我们不仅需要在全部需要转义的位置,对数据进行对应的转义。而且要防止多余和错误的转义,避免正常的用户输入出现乱码。
总结
虽然很难通过技术手段完全避免 XSS,但我们可以总结以下原则减少漏洞的产生:
- 利用模板引擎 开启模板引擎自带的 HTML 转义功能。例如: 在 ejs 中,尽量使用
<%= data %>
而不是<%- data %>
; 在 doT.js 中,尽量使用{{! data }
而不是{{= data }
; 在 FreeMarker 中,确保引擎版本高于 2.3.24,并且选择正确的freemarker.core.OutputFormat
。 - 避免内联事件 尽量不要使用
onLoad="onload('{{data}}')"
、onClick="go('{{action}}')"
这种拼接内联事件的写法。在 JavaScript 中通过.addEventlistener()
事件绑定会更安全。 - 避免拼接 HTML 前端采用拼接 HTML 的方法比较危险,如果框架允许,使用
createElement
、setAttribute
之类的方法实现。或者采用比较成熟的渲染框架,如 Vue/React 等。 - 时刻保持警惕 在插入位置为 DOM 属性、链接等位置时,要打起精神,严加防范。
- 增加攻击难度,降低攻击后果 通过 CSP、输入长度配置、接口安全措施等方法,增加攻击的难度,降低攻击的后果。
- 主动检测和发现 可使用 XSS 攻击字符串和自动扫描工具寻找潜在的 XSS 漏洞。
Q:TCP的三次握手
到这里面试已经接近尾声了,看得出来面试官着急面下一位同学。。。看来我还是得多沉淀沉淀才能留住面试官的心。 那么我在这里的回答并不亮眼,这里直接附上高分回答吧。
刚开始客户端处于 closed 的状态,服务端处于 listen 状态。然后
1、第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 SN(c) 。此时客户端处于 SYN_Send 状态。
2、第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 *SYN_REVD *的状态。
3、第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 establised 状态。
4、服务器收到 ACK 报文之后,也处于 establised 状态,此时,双方以建立起了链接。
三次握手的作用
三次握手的作用也是有好多的,多记住几个,保证不亏。例如:
1、确认双方的接受能力、发送能力是否正常。
2、指定自己的初始化序列号,为后面的可靠传送做准备。
单单这样还不足以应付三次握手,面试官可能还会问一些其他的问题,例如:
1、(ISN)是固定的吗
三次握手的一个重要功能是客户端和服务端交换ISN(Initial Sequence Number), 以便让对方知道接下来接收数据的时候如何按序列号组装数据。
如果ISN是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。
2、什么是半连接队列
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列 。当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
这里在补充一点关于SYN-ACK 重传次数的问题: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超 过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s, 2s, 4s, 8s, ....
3、三次握手过程中可以携带数据吗
很多人可能会认为三次握手都不能携带数据,其实第三次握手的时候,是可以携带数据的。也就是说,第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。
为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手可以放数据的话,其中一个简单的原因就是会让服务器更加容易受到攻击了。
而对于第三次的话,此时客户端已经处于 established 状态,也就是说,对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据页没啥毛病。
结语
手写题我会收集最近的面试单独再开一篇文章,表面上看大厂面试出的题目没有特别难,但是往往只有亮眼的回答才能从众多候选人中脱颖而出,加油各位!