总结
腾讯的面试进来便直接手撕了两个代码,不过题目都属于LeetCode上的,比较常见。后面就先考察了一些八股文,还有些项目和项目上的扩展问题。感觉很多面试官都喜欢基于项目进行扩展,包括追究底层原理或者进行新场景的问题,有什么解决方案。还是很喜欢问扩展类的,答起来也是很爽的。
总而言之,有些地方没有答上来,最后还是凉了。不过还是很开心,有这样一次很好的学习机会。
手撕代码
最长子序列
面试的时候 在dp数组初始化时想当然均赋值为0 导致后面输出结果有问题 不过面试官很好 提醒了下“dp数组设置的含义” 后面就改正过来了
function longer(nums) {
if (nums.length === 0) return [];
// 初始化dp数组,长度与nums相同,默认值为1(因为每个元素本身都可以看作是一个长度为1的递增子序列)
const dp = new Array(nums.length).fill(1);
// 用于记录最长递增子序列的构造过程
const prev = new Array(nums.length).fill(-1);
let maxLength = 1; // 最长递增子序列的长度
let endIndex = 0; // 最长递增子序列的最后一个元素的索引
for (let i = 1; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
// 如果当前元素大于前一个元素,并且以当前元素结尾的递增子序列长度比之前更长
if (nums[i] > nums[j] && dp[i] < dp[j] + 1) {
dp[i] = dp[j] + 1;
prev[i] = j; // 记录前一个元素的索引
}
}
// 更新最长递增子序列的长度和最后一个元素的索引
if (dp[i] > maxLength) {
maxLength = dp[i];
endIndex = i;
}
}
// 根据prev数组回溯构建最长递增子序列
const lis = [];
let k = endIndex;
while (k !== -1) {
lis.unshift(nums[k]); // 从后向前添加元素到lis数组中
k = prev[k];
}
return lis;
}
// 示例用法
const nums = [10, 9, 2, 5, 3, 7, 101, 18];
const lis = longer(nums);
console.log(lis); // 输出:[2, 3, 7, 101] 或者其他可能的递增子序列,只要它是最长的
版本号比较大小
面试的时候 由于传递的是字符串 导致'10'和‘9’的大小比较存在问题 后面测试时修正过来了。
function compareVersions(version1, version2) {
let v1 = version1.split(".");
let v2 = version2.split(".");
for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
// 如果某个版本号缺少部分,则视为0
// 必须number 字符串中10<9
let num1 = Number(v1[i]) || 0;
let num2 = Number(v2[i]) || 0;
// 比较数字
let diff = num1 - num2;
if (num1 > num2) return 1;
if (num1 < num2) return -1;
}
// 所有数字都相同,视为版本号相等
return 0;
}
// 示例
console.log(compareVersions("1.2.3", "1.2.3")); // 输出 0
console.log(compareVersions("1.10.3", "1.9.4")); // 输出 -1
console.log(compareVersions("1.2.3", "1.1.3")); // 输出 1
八股文
1 vue 双向绑定 v-model 原理是什么?
本质为语法糖 在输入框中使用 v-model 相当于同时设置了 v-bind:value 来绑定数据 并添加了 v-on:input 监听并处理数据变化
双向绑定的实现:当用户在输入框输入内容时 会触发 v-on:input 事件 vue 会捕获这个事件并更新绑定的数据属性。同时 vue 会监听数据变化 一旦数据变化 vue 会自动更新视图。
2 如何实现 input 中 v-model 方法?
<input id="myInput" type="text" />
<p id="displayText">Value:</p>
<script>
// 获取输入框和显示文本的元素
const input = document.getElementById("myInput");
const displayText = document.getElementById("displayText");
let currentValue = ""; // 初始化绑定的变量
// 更新显示的文本
function updateDisplay() {
displayText.textContent = "Value: " + currentValue;
}
// 监听输入框的输入事件
input.addEventListener("input", function (event) {
currentValue = event.target.value; // 更新数据
updateDisplay(); // 更新显示的文本
});
// 如果需要从数据变化出发更新输入框(模拟Vue的响应式)
// 可以想象有一个地方会改变currentValue的值
// 模拟一个数据改变的场景
setTimeout(() => {
currentValue = "Hello from timeout";
updateDisplay();
input.value = currentValue; // 更新输入框的值以反映数据变化
}, 700);
</script>
3 为什么不直接操作虚拟 dom 若直接确定修改真实 dom 会不会比虚拟 dom 快
不会更快 因为真实 DOM 的操作涉及到了浏览器的布局和渲染过程
- 减少重排与重绘:直接修改真实 DOM 可能导致不必要的重排(layout)和重绘(paint)虚拟 DOM 通过批量计算差异(diff 算法),仅对必要的部分进行最小化的真实 DOM 更新,从而减少重排和重绘。
- 批量更新:虚拟 DOM 允许你在一个批次中累积多个状态变化,然后一次性应用到真实 DOM 上,这样可以减少与 DOM 交互的次数,提高效率。
- 计算效率:虚拟 DOM 是轻量级的 JavaScript 对象结构,比真实的 DOM 树操作更高效。修改 JavaScript 对象的速度远快于修改真实 DOM
4 https 中 TLS(SSL) 具体是怎么加密的?
HTTPS 中的TLS加密,就是在客户端与服务器建立连接时,通过 TLS 握手协议安全地协商出一个对称加密密钥。这个过程中,服务器会提供一个证书证明其身份,客户端验证证书后,使用证书中的公钥加密并发送一个对称密钥给服务器;服务器用私钥解密得到该对称密钥。之后通过这个共享的对称密钥进行数据传输
- 对称密钥:加密和解密过程使用相同的密钥
- 非对称密钥:包含一对密钥:公钥(Public Key)和私钥(Private Key) 公钥用于加密 私钥用于解密
- 会话密钥:在一次会话或交流中临时使用的对称密钥
5 Web Worker 有了解吗?
【占坑】会出一期单独介绍
- 含义:Web Worker 是 HTML5 引入的一个 API,允许在浏览器后台独立于主线程运行 js 脚本
- 作用:可以执行繁重的计算任务(图像处理、音频处理、大数运算、离线计算等)保证页面的流畅性
- 使用步骤:
- 创建 worker 实例 参数为 worker 脚本的 URL 目的:启动一个后台任务
- 通过.postMessage() 实现主线程和 worker 之间的通信
- worker 接收消息 通过.onMessage()进行事件处理
- 处理完成后 通过.postMessage()将信息返回给主线程
- 主线程通过监听.onMessage()接收结果
- 最后 任务完成时 通过 terminate 关闭 worker
- 缺点:
- 不能直接访问 dom 不能使用浏览器默认的方法和属性(alert 等)
6 proxy 的监听原理有了解吗?
Proxy 可以创建目标对象的代理 来实现数据更新。Proxy 在访问(get)或修改(set)对象属性时 可以拦截这些操作并添加自定义处理逻辑 在数据变化时自动执行副作用函数 使得数据变化时进行视图更新 而无需手动监听每个属性
7 闭包是如何延长变量的生命周期
因为闭包是函数内部返回函数 内部的函数有权访问其自身作用域、外部函数作用域以及全局作用域中的变量
- 作用域链的形成:当外部函数返回内部函数时,内部函数(即闭包)会保留对外部函数作用域变量的引用
- 声明周期延长:正常情况下函数执行完后,局部变量会随着执行上下文的销毁而被回收。但闭包使得外部函数的局部变量在外部函数执行结束后仍然存活。只要闭包存在,这些变量就一直有效
8 XSS是什么 XSS如何预防?
XSS 跨站脚本攻击,是指浏览器中执行恶意脚本(无论是跨域还是同域),从而拿到用户的信息并进行操作。(攻击者想尽一切办法将恶意的 html 代码插入到网页中,当用户访问时,该代码就会被执行,从而达到攻击用户的目的)
- 出现场景:
- 窃取 Cookie。
- 监听用户行为(输入账号密码后直接发送到黑客服务器)。
- 修改 DOM 伪造登录表单。
- 在页面中生成浮窗广告。
- 类型:
- 存储型:攻击者通过可输入区域,将恶意代码永久存在数据库中(评论、私信等)
- 反射型:存在 url 上,用户需要主动打开 url 才可以(网站跳转、搜索)
- DOM 型:存在 url 上,同反射型,区别为取出和执行恶意代码由浏览器端完成
- 预防方法:
- 【宗旨】:防止攻击者提交恶意代码,防止浏览器执行恶意代码
- 过滤所有的非法字符
- 对链接跳转,进行内容检测
- 限制输入长度
- 输入内容加密
9 小程序双线程架构有了解吗?
【占坑】 不太了解
项目
10 大型文件和多文件上传如何实现?
【大型文件上传】
整体过程:定义切片原则(Math.ceil取整) ,利用file.slice进行切片,将切片的整体hash名、文件以对象的形式添加到切片数组中。循环遍历这个数组调接口进行文件上传,当全部文件上传后调用接口合并上传的文件。
获取文件的hash值和后缀:利用FileReader的readAsArrayBuffer方法获取文件的buffer数据,利用spark-md5的ppendChild方法获取hash值(SparkMD5.ArrayBuffer( ) )文件名则为hash值与后缀的拼接
断点续传:定义数组用来接收已经上传的片段。在上传函数中进行判断,如果已上传的数组中包含切片的hash名则调用完成的函数(进度条样式+调用接口合并切片,传值:hash和总数量) 并return
【多文件上传】
选择文件:将获取文件的伪数组集合转为数组Array from,并循环遍历数组给每个文件添加唯一标识的自定义属性(随机数*时间.toString(16) 16进制),利用innerHTML设置前端显示情况。利用事件委托(parentNode父节点)、filter实现删除文件功能。
上传文件:循环发送上传请求,利用axios的onUploadProgress中的e.loaded已上传量、e.total需传的总量来监听进度,最后利用Promise.all设置所有文件上传成功的提示。
11 大文件如何实现断点续传 大文件和多文件上传失败的时候怎么做?
【占坑】 会整理出全部的方法 单独出一期
12 如何实现秒传?
- 秒传:如果服务器上已经存在相同的文件,则不需要重新上传整个文件,而是直接完成上传过程,或者仅上传少量的校验数据以验证文件是否一致。
- 文件哈希值计算:客户端在上传文件之前,首先计算文件的哈希值(如 MD5、SHA-1 等)。哈希值是根据文件内容生成的唯一标识符,只要文件内容相同,哈希值就相同。(使用 JavaScript 库(如 SparkMD5、crypto-js 等)在客户端计算文件的哈希值)
- 检查哈希值是否存在:客户端将计算出的哈希值发送到服务器 服务器在数据库中或文件系统中查找该哈希值是否已存在
- 文件秒传:如果服务器找到与客户端提供的哈希值匹配的文件,服务器会告诉客户端文件已经存在,无需再次上
- 文件上传:如果服务器没有找到匹配的哈希值,客户端需要上传整个文件,服务器接收文件,计算文件的哈希值,并将其与文件名、文件路径等信息一起存储在数据库中
- 错误处理:如果在哈希值计算、文件上传或服务器响应过程中发生错误,客户端需进行处理
- 优化性能:
- 对于大型文件,计算哈希值可能需要一些时间。为了提高用户体验,可以在用户选择文件后立即开始计算哈希值,并在上传过程中显示进度。
- 如果服务器经常需要处理大量上传请求,可能需要考虑使用缓存或分布式存储来加速哈希值的查找过程。