在六年前,我还是一个初出茅庐的小前端,面试了某大厂,但是结果不是很理想,所以这算是一篇完整的某大厂面凉经。
当时就想写这篇文章了,当时面完的整体感觉就是很羞耻,「和面试官同龄,但是各种问题回答的都不尽如人意,被面试官嫌弃了」,所以一直想写一个很全面的面试总结,但是由于种种原因,一直拖到了现在,时过境迁,我也从当初的面试者变成了现在的面试官,对这些题目也有了不同的理解,再看这场面试也有了不同的心境。
首先说明一下,手写模板引擎确实是面试中遇到的问题,但是我没有写出来。于是一直想把这个知识点完完整整的总结出来,这也是这篇文章拖了这么久的主要原因------太难写了......不过这篇文章重点在于记录面经,所以关于手写模板引擎 可以移步这篇文章------> 《鸽了六年的某大厂面试题:手写 Vue 模板编译》
笔试题
当年还是在线下面试,到了之后是有一份统一的笔试题,40 分钟。
简答题 - 1、原型链继承实现原理
这个之前我写过,【前端面试】同学,你会手写代码吗? 所以我直接写了出来。
js
function Sup() {}
function Sub() { Sup.call(this) }
Object.setPrototypeOf(Sub.prototype, Sup.prototype)
没想到的是,面试官问我为什么要用 Object.setPrototypeOf
,我当时没有回答出来,因为在我想法里,这有什么好问的,面试官不是很满意。
现在回想,我这里的错过了一个表现自己的机会,因为那个时候我把《你不知道的JavaScript》看了3遍,什么原型链什么继承学得清清楚楚,好不容易问到了一个能回答的问题却没有把握住。
事实上,面试中每一个问题都是表现自己的机会。既然面试问了为什么,潜意识是在问"还有什么方案?为什么选择了这个方案?有什么区别呢?"
JS继承除了上述方案有下面几种方案:
js
function Sup() {}
function Sub() { Sup.call(this); }
// 方案1 - 父类实例作为子类原型
Sub.prototype = new Sup();
Sub.prototype.constructor = Sub;
// 方案2 - 寄生组合继承
function F() {}
F.prototype = Sup.prototype;
Sub.prototype = new F();
Sub.prototype.constructor = Sub;
// 方案3 - Object.create
Sub.prototype = Object.create(Sup.prototype);
Sub.prototype.constructor = Sub;
// 方案 4 - ES6 class 继承
class Sup {}
class Sub extends Sup {
constructor() { super(); }
}
- 方案 1 是古早实现方案,把父类实例作为子类原型,它缺点是会额外调用一次父类构造函数,而且子类原型上会有父类的实例属性,而不仅仅是方法。
- 方案 2 使用临时空构造函数,将其原型指向父类原型,然后创建其实例作为子类原型。这避免了调用父类构造函数,是 ES5 之前的最佳实践。不过它会完全替换
Sub.prototype
,所以应该先设置继承关系,再添加子类方法。 - 方案 3 通过
Object.create(Sup.prototype)
创建一个新对象作为子类原型,本质上实现了与方案 2 相同的效果,但代码更简洁。同样会替换整个Sub.prototype
对象。 - 与方案 2、方案 3 相比, 使用
Object.setPrototypeOf
在不替换整个原型对象的情况下改变继承关系,会保留原型上已有的属性和方法。但是由于修改已存在对象的[[Prototype]]
属于 JavaScript 的昂贵操作,比起前者来说,性能较差。 - 方案 4 是通过 ES6 class 实现继承,是现代 JavaScript 中最佳实践。它不仅语法简洁,还自动处理了继承的各种细节,包括构造函数调用顺序、原型链设置、静态方法继承等。
对于这道题给我的经验教训就是,面试时切记当哑巴,学会举一反三,学会扩展,会多少说多少,你多说一点你会的,就少给面试官一点问你不会的问题的机会。
简答题 - 2、attribute 和 property 的区别
这个我当时真的不了解,于是随便说了下,property
是 html
定义的属性,attribute
是所有用户定义的属性。面试官点头表示过。
不过这个并不准确,查了下 《DOM 中 Property 和 Attribute 的区别》 这篇文章讲的还蛮详细的。
简答题 - 3、混杂模式和标准模式的区别
从我开始做前端,我写得就是 <!DOCTYPE html>
这个问题应该比较老了,我只知道混杂模式就是不写 DOCTYPE
。和 AI 请教了一下:
浏览器渲染页面有两种模式:混杂模式(Quirks Mode)和标准模式(Standards Mode)。这两种模式在页面渲染、CSS 处理和 JavaScript 行为等方面有显著差异。
- 混杂模式(Quirks Mode)也称为"怪异模式"或"兼容模式",为了向后兼容早期网页而存在,模拟旧浏览器(主要是 IE5 和 IE6)的行为。
- 标准模式(Standards Mode):遵循 W3C 标准规范渲染页面,行为更加一致和可预测。在 HTML 文档开头添加正确的 DOCTYPE 声明:
<!DOCTYPE html>
。
在现代 Web 开发中,标准模式是唯一推荐的选择,混杂模式仅用于维护非常古老的网站。
编程题 - 1、路径总和
给你二叉树的根节点
root
和一个表示目标和的整数targetSum
。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和targetSum
。如果存在,返回true
;否则,返回false
。
一道简单题,我写起来很快,递归实现。
js
var hasPathSum = function(root, targetSum, currentSum = 0) {
if (!root) return false;
currentSum += root.val;
if (currentSum === targetSum && !root.left && !root.right) return true;
if (hasPathSum(root.left, targetSum, currentSum)) return true;
if (hasPathSum(root.right, targetSum, currentSum)) return true;
return false;
};
然后面试官说,那如果有一个节点的和已经超过答案了呢?我第一反应是剪枝,不过题目也没有说只有正数,没办法剪枝。。。说了之后面试官不是很满意,然后又问我,left 和 right 是什么,我:
ts
interface Node {
val: number;
left: Node;
right: Node;
}
但是面试官又好像要提示我是说数组= = 反正面试官也不是很满意,这题就这么过了。
说实话到今天我也没理解面试官到底啥意思,算法题其实是我的强项,尤其那个时候我刚毕业不久,在 LeetCode 可以秒 Hard,可惜面试过程中没有体现出来。
编程题 - 2、并发请求
实现一个函数,请求一组
url
,并指定最大并发数,请求全部完成后执行回调。 函数接口:parallel(urls, max, callback)
。可以直接使用函数fetch
。
当时写的比较复杂,回来后又重新写了一遍,不过思路是一样的。
js
function parallel(urls, maxParallel, callback) {
let length = urls.length;
let index = 0;
let result = new Array(length);
let finishNumber = 0;
if (maxParallel > length) maxParallel = length;
for (let i = 0; i < maxParallel; i++) loopFetch();
function loopFetch() {
if (index >= length) return;
let i = index++;
fetch(urls[i]).then(res => {
result[i] = res;
++finishNumber === length ? callback(result) : loopFetch();
});
}
}
这题很经典,不算难,但是想写得优雅还是不太容易的。当时 fetch
写的是回调函数而不是 Promise
,面试官问可以写成 Promise
吗?我说可以但都一样的。面试官说,你可以用 Promise
自带的一些 API, 我说 Promise.all()
吗?面试官点头。我说,但是那样也没办法保证并发是 max
啊?
面试官很无奈,但是没说什么。(emm 到现在我也不知道那天的面试官到底想说什么。
面试题
Vue 模板编译
笔试题看完,接下来就是面试题了。
- 面试官:"模板有了解过吗?"
- 我:"了解过一些"
- 面试官:"那你就写一个处理 vue 的
v-bind
,{{ }}
的代码" - 我:????(黑人问号)
我承认我很菜,那个时候,完全没看过 Vue 的源码。但是在之前,我在看 underscore 源码的时候,模板那一块有认认真真地看一遍,所以面试的时候,思路完全在 underscore 的实现逻辑上。
可惜,看懂是一件事,会写是另一件事。所以我当时愣在那里了。现在想想,确实是理解不够,不然也不会完全不知道说什么。我对正则的掌握也不够熟练,所以我完全写不出。
良久......
- 面试官:不一定要写出来,你说个思路。
- 我:使用正则表达式解析...... 然后......额......然后......
- 面试官:你知道什么是 AST 吗?
- 我:知道
- 面试官:AST 有什么好处?
- 我:哈?

六年后的我,重新再回到那个场景,我会怎么回答?
首先,面试官问这个问题,不可能是要求你现场手写出来的(当然,这个面试官估计面试经验也不够多,提问引导性不够,太宽泛)。
关于 {{}}
和 v-bind
解析的重点就是:正则表达式。
我们先处理插值语法,其实很简单,首先匹配两个 {
,然后匹配至少一个不为 }
的字符,最后匹配两个 }
。
js
const interpolationRegex = /\{\{\s*([^}]+?)\s*\}\}/g;
const template = '<h1>{{ message }}</h1>'
interpolationRegex.exec(template)
// ['{{ message }}', 'message', index: 4, input: '<h1>{{ message }}</h1>', groups: undefined]
这样通过正则,我们就提取出了插值中的变量。然后在替换变量就可以生成最终结果:
js
function compileTemplate(template, data) {
return template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (match, expression) => {
const value = data[expression];
return value !== undefined ? String(value) : '';
});
}
// 使用示例
const data = {
message: 'Hello Cookie!',
};
const template = `<div><h1>{{ message }}</h1></div>`;
console.log(compileTemplate(template, data)); // Hello Cookie!
然后是 v-bind
语法,这个的处理就会复杂一些,因为需要解析属性,然后再进行替换。具体代码如下:
js
/**
* 处理 v-bind 指令的主函数
* @param {string} template - HTML 模板字符串
* @param {object} data - 数据对象
* @returns {string} 处理后的 HTML 字符串
*/
function processVBind(template, data) {
// 使用正则表达式匹配所有的 HTML 标签(包括开始标签和自闭合标签)
// <([^>]+)> 匹配 < 和 > 之间的所有内容
return template.replace(/<([^>]+)>/g, (match, tagContent) => {
// match: 完整匹配的标签,如 "<div class='test'>"
// tagContent: 标签内容,如 "div class='test'"
return '<' + processAttributes(tagContent, data) + '>'
})
}
/**
* 处理单个标签内的所有属性
* @param {string} tagContent - 标签内容(不包括 < >)
* @param {object} data - 数据对象
* @returns {string} 处理后的标签内容
*/
function processAttributes(tagContent, data) {
// 正则表达式解释:
// (v-bind:|:) - 匹配 "v-bind:" 或 ":" (分组1)
// (\w+) - 匹配属性名,如 href、class、title (分组2)
// =["'] - 匹配等号和引号开始
// ([^"']+) - 匹配引号内的表达式内容 (分组3)
// ["'] - 匹配引号结束
return tagContent.replace(
/(v-bind:|:)(\w+)=["']([^"']+)["']/g,
(match, prefix, attrName, expression) => {
// match: 完整匹配,如 'v-bind:href="url"' 或 ':class="buttonClass"'
// prefix: 'v-bind:' 或 ':'
// attrName: 属性名,如 'href', 'class', 'title'
// expression: 表达式,如 'url', 'buttonClass', 'user.name'
// 从数据对象中获取表达式的值
const value = data[expression]
return `${attrName}="${value}"`
}
)
}
// 使用示例
const data = {
url: 'https://juejin.cn/user/3263006241480605',
title: 'cookie'
}
const template = `<a v-bind:href="url" :title="title">链接</a>`
console.log(processVBind(template, data))
// <a href="https://juejin.cn/user/3263006241480605" title="cookie">链接</a>
Vue 双向绑定
关于Vue的响应式和双向绑定有太多太多文章写了,我以为我看懂了,实际上我没懂。
那个时候,Vue3 还没出来,所以 Vue 只有 Object.defineProperty
实现响应式方案。通过 Object.defineProperty()
重新定义对象属性的 getter
和 setter
,实现数据劫持。
上面这个表述大部分人都会说,我也这么说的。就像我曾经面试过一个什么都不懂的实习生,ta 也答出来了。当我深入提问的时候,我发现 ta 什么都不知道。就像当时的我。
有一种几年前的子弹正中眉心的荒唐感。
- 为什么重新定义
getter
和setter
可以实现数据劫持? - 什么时候去进行依赖收集?
- 这样实现的响应式有什么局限性?
- 数组的响应式是如何实现的?通过下标设置数据可以被响应吗?
- 对于嵌套对象,Vue 是如何处理的?
computed
和watch
的响应式原理有什么区别?
我不记得面试官后面都问了什么,只记得我回答得一塌糊涂。
HTTP
在发现我对 Vue 的掌握趋近于零后,面试官转为提问 HTTP 相关知识点。
不过这两个问题我......都没有回答上来。不管怎么样,现在再回答一次吧。
说一下 HTTP 的长连接
HTTP 长连接(HTTP Persistent Connection)是指在一个 TCP 连接上可以传输多个 HTTP 请求和响应,而不需要为每个请求都建立新的连接。
在 HTTP/1.0 时代,默认使用短连接,每个请求都需要建立新的 TCP 连接,而 TCP 的连接需要三次握手和四次挥手,所以效率低下,存在大量连接建立和断开的开销。
所以在 HTTP/1.1 默认开启了长连接,通过 Connection: keep-alive
头部控制。开启长连接可以在同一 TCP 连接上发送多个请求。
服务端推送
传统的 HTTP 请求是由客户端发送一个 Request,服务端返回对应 Response。而服务端推送是指服务器主动向客户端发送数据的技术,而不需要客户端先发起请求。常见的业务场景如新消息提醒。
这个我之前总结过,大家想仔细了解可以看这篇文章 《# 一文了解服务端推送》
常用的服务端推送技术,包括轮询、长轮询、websocket、server-sent-event(SSE)
- 轮询:就是不断向服务端发送请求,这样当服务端有新消息立刻就可以获取到,一种伪服务端推送。
- 长轮询:在轮询的基础上,设置一个比较长的超时时间,比如30秒,实时性比短轮询更好,资源消耗比较大。
- WebSocket:WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
- SSE(Sever-Sent Event):是一种能让浏览器通过 HTTP 连接自动收到服务器端推送的技术,EventSource 是 浏览器提供的对应 API。通过 EventSource 实例打开与 HTTP 服务器的持久连接,该服务器以文本/事件流格式发送事件,连接会保持打开状态,直到服务端或客户端主动关闭。
- HTTP/2 Server Push: 服务器可以主动推送客户端可能需要的资源。
前端优化
在发现我对 HTTP 也一窍不通后,面试官又抛出了新的问题:前端优化。
这个问题谁都能答上一两句,但是未必能让面试官满意,最好可以全面的体系化地回答。
是不是眼花缭乱了哈哈,这下面试还会答不出来吗?
最后是优化效果,一般在回答面试官都比较关注优化效果,最好用数据来体现,比如:"首屏加载时间减少50%","包体积减少40%","整体用户体验评分提升20%"等。
前端安全
接下来面试官又抛出了新的问题:XSS 和 XSRF。
什么是 XSS 攻击?
这个问题,真的不难,但是......我真的不会。
Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。
三种 XSS 攻击流程:
存储型XSS攻击
反射型XSS攻击
DOM型XSS攻击
参考
后端
最后,在发现我什么都不会的时候,面试官垂死挣扎
面试官:"我看你简历上写了你还做后端,那我问你一些后端问题吧。"
其实我也做了不少后端的东西,比如我独自搭建了一个后端项目,包括数据库的创建等。我有一些需求是前后端一起写的,Java的接口、数据库操作、单测都写过,还有 Kafka、mongoDB 等,甚至我们还用了 Neo4j。
负载均衡
可惜我也找不到为什么,面试官张口就是我不知道的东西。我没用过 NGINX 我也不知道什么是负载均衡。
其实这个问题很简单,后端一般有多台机器,负载均衡这个高大上的说法,其实就是问,怎么让用户的请求平均分布在每台机器上。
常见算法:
- 轮询:按顺序依次分配请求给每台服务器
- 加权轮询:根据服务器性能设置权重,性能强的服务器处理更多请求
- 最少连接:将请求分配给当前连接数最少的服务器
- IP哈希:根据客户端 IP 计算哈希值,确保同一客户端总是访问同一台服务器
- 最短响应时间:选择响应时间最短的服务器处理请求
高并发
面试官还想问我高并发,但是作为一个前端,我不了解这些,也不太想去了解了。
如何同时处理大量用户的同时请求?加机器。
说点有的没的
面试结束了,面试官对我很不满意。
我也有点忧伤,我以为我复习了很久,可是这场面试让我发现我好像什么都不会。
面试官最后问我,你是几几年,我一时没搞懂,但还是回答了。
面试官:"我和你同龄"。
其实我挺惨的,校招是以 Java 的身份入职的,领导因为组里缺一个前端让我转前端。我的不自信让我没有拒绝,可是组里所有的人都是做后端的,我没有任何人可以请教,我只能一遍一遍的看书,花钱买网课,看一些面经。我以为我复习几个月差不多了。
可是还是差得太远了。
六年过去了,现在想到这句话,我还是很忧伤。
好像我们的人生永远在一条跑道上,只知道要不停地向前跑,却不知道终点在哪。我曾经以为高考是终点,可是那不过是一个休息站。我们还是要不停地不停地跑,走错一步,就被人拉下了,停下一会,就再也赶不上了。不敢裸辞不敢Gap。
可是好像也追不上别人。
希望有一天能和自己和解吧。