鸽了六年的某大厂面试题:你会手写一个模板引擎吗?

在六年前,我还是一个初出茅庐的小前端,面试了某大厂,但是结果不是很理想,所以这算是一篇完整的某大厂凉经。

当时就想写这篇文章了,当时面完的整体感觉就是很羞耻,「和面试官同龄,但是各种问题回答的都不尽如人意,被面试官嫌弃了」,所以一直想写一个很全面的面试总结,但是由于种种原因,一直拖到了现在,时过境迁,我也从当初的面试者变成了现在的面试官,对这些题目也有了不同的理解,再看这场面试也有了不同的心境。

首先说明一下,手写模板引擎确实是面试中遇到的问题,但是我没有写出来。于是一直想把这个知识点完完整整的总结出来,这也是这篇文章拖了这么久的主要原因------太难写了......不过这篇文章重点在于记录面经,所以关于手写模板引擎 可以移步这篇文章------> 《鸽了六年的某大厂面试题:手写 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 的区别

这个我当时真的不了解,于是随便说了下,propertyhtml 定义的属性,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() 重新定义对象属性的 gettersetter,实现数据劫持。

上面这个表述大部分人都会说,我也这么说的。就像我曾经面试过一个什么都不懂的实习生,ta 也答出来了。当我深入提问的时候,我发现 ta 什么都不知道。就像当时的我。

有一种几年前的子弹正中眉心的荒唐感。

  • 为什么重新定义 gettersetter 可以实现数据劫持?
  • 什么时候去进行依赖收集?
  • 这样实现的响应式有什么局限性?
  • 数组的响应式是如何实现的?通过下标设置数据可以被响应吗?
  • 对于嵌套对象,Vue 是如何处理的?
  • computedwatch 的响应式原理有什么区别?

我不记得面试官后面都问了什么,只记得我回答得一塌糊涂。

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 也一窍不通后,面试官又抛出了新的问题:前端优化。

这个问题谁都能答上一两句,但是未必能让面试官满意,最好可以全面的体系化地回答。

mindmap root((前端性能优化)) 资源加载优化 代码分割 懒加载 预加载策略 CDN加速 HTTP/2优化 构建优化 Tree Shaking 代码压缩 图片优化 Bundle分析 依赖优化 缓存策略 浏览器缓存 Service Worker 本地存储 接口缓存 版本控制 渲染性能 虚拟滚动 防抖节流 React优化 DOM操作优化 CSS优化 内存优化 内存泄漏防护 组件卸载 大对象处理 图片内存管理 网络优化 请求合并 接口优化 数据压缩 请求优先级 离线策略 用户体验 骨架屏 渐进式加载 错误边界 加载状态 响应式设计 监控分析 性能监控 错误监控 用户行为分析 性能埋点 A/B测试

是不是眼花缭乱了哈哈,这下面试还会答不出来吗?

最后是优化效果,一般在回答面试官都比较关注优化效果,最好用数据来体现,比如:"首屏加载时间减少50%","包体积减少40%","整体用户体验评分提升20%"等。

前端安全

接下来面试官又抛出了新的问题:XSS 和 XSRF。

什么是 XSS 攻击?

这个问题,真的不难,但是......我真的不会。

Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。

三种 XSS 攻击流程:

存储型XSS攻击
sequenceDiagram participant 攻击者 participant 网站服务器 participant 数据库 participant 受害者 participant 浏览器 participant 攻击者服务器 Note over 攻击者,攻击者服务器: 存储型XSS攻击流程 攻击者->>网站服务器: 提交恶意脚本(评论/帖子) 网站服务器->>数据库: 存储恶意内容 数据库-->>网站服务器: 存储成功 网站服务器-->>攻击者: 提交成功 Note over 受害者,攻击者服务器: 受害者访问阶段 受害者->>网站服务器: 访问包含恶意内容的页面 网站服务器->>数据库: 查询页面内容 数据库-->>网站服务器: 返回含恶意脚本的内容 网站服务器-->>浏览器: 返回页面(含恶意脚本) 浏览器->>浏览器: 执行恶意脚本 浏览器->>攻击者服务器: 发送Cookie/敏感信息 攻击者服务器-->>浏览器: 确认接收
反射型XSS攻击
sequenceDiagram participant 攻击者 participant 受害者 participant 浏览器 participant 网站服务器 participant 攻击者服务器 Note over 攻击者,攻击者服务器: 反射型XSS攻击流程 攻击者->>攻击者: 构造恶意URL 攻击者->>受害者: 发送恶意链接(邮件/社交) 受害者->>浏览器: 点击恶意链接 浏览器->>网站服务器: 请求含恶意参数的URL 网站服务器->>网站服务器: 处理请求参数 网站服务器-->>浏览器: 返回页面(恶意脚本在响应中) 浏览器->>浏览器: 执行恶意脚本 浏览器->>攻击者服务器: 发送Cookie/敏感信息 攻击者服务器-->>攻击者: 通知攻击成功
DOM型XSS攻击
sequenceDiagram participant 攻击者 participant 受害者 participant 浏览器 participant 网站服务器 participant JavaScript引擎 participant 攻击者服务器 Note over 攻击者,攻击者服务器: DOM型XSS攻击流程 攻击者->>攻击者: 构造恶意URL片段 攻击者->>受害者: 发送恶意链接 受害者->>浏览器: 点击链接访问页面 浏览器->>网站服务器: 请求页面(不含恶意参数) 网站服务器-->>浏览器: 返回正常页面 浏览器->>JavaScript引擎: 加载页面JavaScript JavaScript引擎->>JavaScript引擎: 处理URL片段/DOM操作 JavaScript引擎->>浏览器: 动态修改DOM(插入恶意脚本) 浏览器->>浏览器: 执行恶意脚本 浏览器->>攻击者服务器: 发送Cookie/敏感信息

参考

后端

最后,在发现我什么都不会的时候,面试官垂死挣扎

面试官:"我看你简历上写了你还做后端,那我问你一些后端问题吧。"

其实我也做了不少后端的东西,比如我独自搭建了一个后端项目,包括数据库的创建等。我有一些需求是前后端一起写的,Java的接口、数据库操作、单测都写过,还有 Kafka、mongoDB 等,甚至我们还用了 Neo4j。

负载均衡

可惜我也找不到为什么,面试官张口就是我不知道的东西。我没用过 NGINX 我也不知道什么是负载均衡。

其实这个问题很简单,后端一般有多台机器,负载均衡这个高大上的说法,其实就是问,怎么让用户的请求平均分布在每台机器上。

常见算法:

  • 轮询:按顺序依次分配请求给每台服务器
  • 加权轮询:根据服务器性能设置权重,性能强的服务器处理更多请求
  • 最少连接:将请求分配给当前连接数最少的服务器
  • IP哈希:根据客户端 IP 计算哈希值,确保同一客户端总是访问同一台服务器
  • 最短响应时间:选择响应时间最短的服务器处理请求

高并发

面试官还想问我高并发,但是作为一个前端,我不了解这些,也不太想去了解了。

如何同时处理大量用户的同时请求?加机器。

说点有的没的

面试结束了,面试官对我很不满意。

我也有点忧伤,我以为我复习了很久,可是这场面试让我发现我好像什么都不会。

面试官最后问我,你是几几年,我一时没搞懂,但还是回答了。

面试官:"我和你同龄"。

其实我挺惨的,校招是以 Java 的身份入职的,领导因为组里缺一个前端让我转前端。我的不自信让我没有拒绝,可是组里所有的人都是做后端的,我没有任何人可以请教,我只能一遍一遍的看书,花钱买网课,看一些面经。我以为我复习几个月差不多了。

可是还是差得太远了。

六年过去了,现在想到这句话,我还是很忧伤。

好像我们的人生永远在一条跑道上,只知道要不停地向前跑,却不知道终点在哪。我曾经以为高考是终点,可是那不过是一个休息站。我们还是要不停地不停地跑,走错一步,就被人拉下了,停下一会,就再也赶不上了。不敢裸辞不敢Gap。

可是好像也追不上别人。

希望有一天能和自己和解吧。

相关推荐
用户3802258598245 分钟前
vue3源码解析:diff算法之patchChildren函数分析
前端·vue.js
烛阴11 分钟前
XPath 进阶:掌握高级选择器与路径表达式
前端·javascript
小鱼小鱼干14 分钟前
【JS/Vue3】关于Vue引用透传
前端
JavaDog程序狗16 分钟前
【前端】HTML+JS 实现超燃小球分裂全过程
前端
独立开阀者_FwtCoder21 分钟前
URL地址末尾加不加 "/" 有什么区别
前端·javascript·github
独立开阀者_FwtCoder24 分钟前
Vue3 新特性:原来watch 也能“暂停”和“恢复”了!
前端·javascript·github
snakeshe101025 分钟前
优化 Mini React:实现组件级别的精准更新
前端
前端小盆友25 分钟前
从零实现一个GPT 【React + Express】--- 【2】实现对话流和停止生成
前端·gpt·react.js
京东云开发者31 分钟前
行云前端重构之路:从单体应用到 Monorepo 的血泪史
前端
whale fall33 分钟前
npm install安装不成功(node:32388)怎么解决?
前端·npm·node.js