Skip to main content

豆神集团前端

前端基础

字符串反转

//方法一:使用内置函数(最简单)
function reverseString(str) {
return str.split("").reverse().join("")
}
//方法二:使用循环手动拼接

function reverseString(str) {
let result = ""
for (let i = str.length - 1; i >= 0; i--) {
result += str[i]
}
return result
}

//方法三:使用 ES6 解构 + reduce
function reverseString(str) {
return [...str].reduce((acc, char) => char + acc, "")
}

数组去重

// 使用 Set(最推荐)
function uniqueArray(arr) {
return [...new Set(arr)]
}
//使用 filter + indexOf
function uniqueArray(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index)
}

// 手动用 Map 或 Object 去重
function uniqueArray(arr) {
const seen = {}
const result = []
for (let item of arr) {
if (!seen[item]) {
seen[item] = true
result.push(item)
}
}
return result
}

数组排序

  1. 冒泡排序

    冒泡排序的过程,就是从第一个元素开始,重复比较相邻的两个项,若第一项比第二项更大,则交换两者的位置;反之不动。

function betterBubbleSort(arr) {
const len = arr.length
for (let i = 0; i < len; i++) {
// 注意差别在这行,我们对内层循环的范围作了限制
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
;[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
return arr
}
  1. 选择排序

    选择排序的关键字是“最小值”:循环遍历数组,每次都找出当前范围内的最小值,把它放在当前范围的头部;然后缩小排序范围,继续重复以上操作,直至数组完全有序为止。

function selectSort(arr)  {
// 缓存数组长度
const len = arr.length
// 定义 minIndex,缓存当前区间最小值的索引,注意是索引
let minIndex
// i 是当前排序区间的起点
for(let i = 0; i < len - 1; i++) {
// 初始化 minIndex 为当前区间第一个元素
minIndex = i
// i、j分别定义当前区间的上下界,i是左边界,j是右边界
for(let j = i; j < len; j++) {
// 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j
if(arr[j] < arr[minIndex]) {
minIndex = j
}
}
// 如果 minIndex 对应元素不是目前的头部元素,则交换两者
if(minIndex !== i) {
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
}
}
return arr
}


  1. 插入排序
  • 插入排序的核心思想是“找到元素在它前面那个序列中的正确位置”。
  • 具体来说,插入排序所有的操作都基于一个这样的前提:当前元素前面的序列是有序的。基于这个前提,从后往前去寻找当前元素在前面那个序列里的正确位置。
function insertSort(arr) {
// 缓存数组长度
const len = arr.length
// temp 用来保存当前需要插入的元素
let temp
// i用于标识每次被插入的元素的索引
for(let i = 1;i < len; i++) {
// j用于帮助 temp 寻找自己应该有的定位
let j = i
temp = arr[i]
// 判断 j 前面一个元素是否比 temp 大
while(j > 0 && arr[j-1] > temp) {
// 如果是,则将 j 前面的一个元素后移一位,为 temp 让出位置
arr[j] = arr[j-1]
j--
}
// 循环让位,最后得到的 j 就是 temp 的正确索引
arr[j] = temp
}
return arr
}
  1. 归并排序
  • 分解子问题:将需要被排序的数组从中间分割为两半,然后再将分割出来的每个子数组各分割为两半,重复以上操作,直到单个子数组只有一个元素为止。
  • 求解每个子问题:从粒度最小的子数组开始,两两合并、确保每次合并出来的数组都是有序的。(这里的“子问题”指的就是对每个子数组进行排序)。 合并子问题的解,得出大问题的解:当数组被合并至原有的规模时,就得到了一个完全排序的数组
function mergeSort(arr) {
const len = arr.length
// 处理边界情况
if(len <= 1) {
return arr
}
// 计算分割点
const mid = Math.floor(len / 2)
// 递归分割左子数组,然后合并为有序数组
const leftArr = mergeSort(arr.slice(0, mid))
// 递归分割右子数组,然后合并为有序数组
const rightArr = mergeSort(arr.slice(mid,len))
// 合并左右两个有序数组
arr = mergeArr(leftArr, rightArr)
// 返回合并后的结果
return arr
}

function mergeArr(arr1, arr2) {
// 初始化两个指针,分别指向 arr1 和 arr2
let i = 0, j = 0
// 初始化结果数组
const res = []
// 缓存arr1的长度
const len1 = arr1.length
// 缓存arr2的长度
const len2 = arr2.length
// 合并两个子数组
while(i < len1 && j < len2) {
if(arr1[i] < arr2[j]) {
res.push(arr1[i])
i++
} else {
res.push(arr2[j])
j++
}
}
// 若其中一个子数组首先被合并完全,则直接拼接另一个子数组的剩余部分
if(i<len1) {
return res.concat(arr1.slice(i))
} else {
return res.concat(arr2.slice(j))
}
}

5 快排

  • 快速排序会将原始的数组筛选成较小和较大的两个子数组,然后递归地排序两个子数组。

二分搜索

二分搜索是一种在有序数组中查找目标值的高效算法,它每次都把查找范围折半,时间复杂度是 O(log n)

数组 必须是有序的

目标是查找某个元素的位置,或者判断它是否存在

http 状态吗

1xx(信息性状态码):表示服务器已接收到客户端请求,正在进一步处理中。

  • 100 Continue:表示服务器已经接收到请求头,并且客户端应该继续发送请求体。
  • 101 Switching Protocols: 升级协议 websocker
  • 102 Processing: 这个状态码表示服务器已经收到请求并且正在处理,但是响应还没有准备好发送。

2xx(成功状态码):表示请求已经被服务器接收、理解、并成功处理了。

  • 200 OK:表示请求已成功,请求所希望的响应头或数据体将随此响应返回。
  • 201 Created:表示请求已经被成功处理,并创建了新的资源。
  • 204 No Content:表示请求已经成功处理,但响应报文中不包含实体的主体部分
  • 206 范围请求

3xx(重定向状态码):表示需要客户端进一步操作才能完成请求。

  • 301 Moved Permanently:永久重定向
  • 302 Found:临时重定向 302 状态码则明确表示客户端应该使用新的 URL 发送一个 GET 请求。
  • 304 Not Modified:协商缓存
  • 307 临时 post

4xx(客户端错误状态码):表示客户端发送的请求有错误。

  • 400 Bad Request:表示客户端发送的请求有语法错误,服务器无法识别。

  • 401 Unauthorized:表示客户端请求未经授权,需要通过登录等方式进行身份验证

  • 403 Forbidden:表示客户端请求被拒绝,服务器理解请求但拒绝执行该请求。

  • 404 Not Found:表示服务器无法根据客户端的请求找到资源

  • 405 请求方法不允许

  • 413 Payload Too Large

    • 上传的文件/请求体太大。

    • 适用于上传场景,服务器限制了请求体大小。

    • 429 Too Many Requests

    • 请求太频繁,触发限流(rate limit)。

  • 常用于接口防刷设计,比如验证码、登录。

5xx(服务器错误状态码):表示服务器处理请求出错。

  • 500 Internal Server Error:表示服务器内部错误,无法完成请求。
  • 502 Bad Gateway:表示服务器作为网关或代理角色时,从上游服务器接收到无效的响应。网关错误,可能是 nginx 代理的服务挂了
  • 503 Service Unavailable:表示服务器暂时无法提供服务,可能是由于过载或者维护。
  • 504 网关超时,后端接口未及时响应

requestAnimationFrame

requestAnimationFrame 是浏览器提供的专门用于动画帧更新的 API,它会在下一次浏览器重绘前执行回调,通常每秒约 60 次(60fps)。

用于每一帧前渲染前执行,节能 + 流畅动画

帧率 = 屏幕刷新率(60FPS → 每 16.67ms)

浏览器优化调度(如合并帧、跳帧)

function render() {
// 执行动画逻辑,比如更新 DOM、canvas 绘图
requestAnimationFrame(render)
}
requestAnimationFrame(render)

requestIdleCallback(callback) 是浏览器提供的一个 在空闲时间执行回调函数的 API,适合执行不紧急、后台类的任务,比如预加载、日志上报、缓存清理等。

requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
doSomeTask(tasks.shift())
}
})
对比项requestAnimationFramerequestIdleCallback
触发时机下一帧绘制前浏览器空闲时(非关键任务期间)
使用场景动画更新、视觉渲染非关键任务、后台计算、预加载
是否每帧触发不一定(取决于主线程空闲情况)
浏览器支持✅ 所有浏览器⚠️ Chrome/Edge 支持,Safari 不支持

✅ 所以:动画推荐用 requestAnimationFrame,而不是 setTimeout/Interval。

执行

→ JS 执行
→ requestAnimationFrame 回调
→ 样式计算
→ 布局(Layout)
→ 绘制(Paint)
→ 合成(Composite)
→ 屏幕刷新

对比

requestAnimationFrame 用于绘制前的高频动画setTimeout(fn, 16) 用于近似模拟帧间调用,但精度和调度差; requestIdleCallback 是在浏览器空闲时低优先级执行任务

特性/函数requestAnimationFramesetTimeout(fn, 16)requestIdleCallback
触发时机浏览器下一帧重绘前(通常 16.6ms)固定时间后触发(不一定准时)浏览器空闲时
精度高,对齐屏幕刷新不稳定,受限于线程和最小延迟不保证执行,有 deadline 控制
适合场景动画、视觉更新、过渡效果简单延迟、节流、debounce 等非关键任务、日志、预加载、缓存清理
是否暂停(失焦/后台)✅ 自动暂停(省资源)❌ 继续运行✅ 空闲时才调度
参数回调(timestamp)回调回调(deadline) 包含剩余时间等信息
浏览器兼容性全兼容全兼容❌ Safari 不支持(可降级处理)
项目requestAnimationFramesetTimeout(fn, 16)requestIdleCallback
流畅性✅ 优❌ 可能丢帧❌ 非视觉类任务
稳定性✅ 高❌ 调度不稳定⚠️ 不确定是否执行
资源消耗低(前后台自调度)中(持续触发)最低
推荐用途动画、滚动、进度条等fallback、延时日志、预加载、缓存任务

能保证 60fps 吗

requestAnimationFrame 理论上是基于浏览器屏幕刷新率执行的,通常是 60fps,即每 16.67ms 调用一次回调函数。但它不能保证固定帧率,会受到主线程压力、后台标签页、硬件性能等多因素影响。如果一帧计算或绘制超过 16ms,就可能掉帧,导致动画卡顿。为了性能优化,我们通常结合节流、分帧等技术提高流畅度

Babel 的作用

  • Babel 能够转译 ECMAScript 2015+ 的代码,使它在旧的浏览器或者环境中也能够运行

  • 工作过程分为三个部分

    • Parse(解析) 将源代码转换成抽象语法树,树上有很多的estree 节点
    • Transform(转换) 对抽象语法树进行转换
    • Generate(代码生成) 将上一步经过转换过的抽象语法树生成新的代码
  • @babel/parser 可以把源码转换成 AST

  • @babel/traverse用于对 AST 的遍历,维护了整棵树的状态,并且负责替换、移除和添加节点

  • @babel/generate 可以把 AST 生成源码,同时生成 sourcemap

Babel 是前端工程中最核心的转译工具之一,它负责将我们编写的高阶 JS(比如 ES6、TS、JSX)转为浏览器兼容的 ES5 代码。项目中我通常结合 Webpack 配置 babel-loader 和 preset-env,确保代码在主流浏览器中都能运行。此外 Babel 还支持插件机制,我在 React 项目中用过像 transform-runtime 来优化 helper 复用,也配置过装饰器插件支持 MobX 的语法糖。

继承

一、原型链继承

·

重点: 让新实例的原型等于父类的实例。

优点:

  1. 实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。

缺点:

  1. 新实例无法向父类构造函数传参。

  2. 继承单一。(只能继承一个父类构造函数)

  3. 所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原性也会被修改!)

  4. 要想为子类原型新增属性和方法,必须要在new SuperType()这样的语句之后执行

    function SuperType(name) {
    this.name = name
    }
    function SubType() {
    // 继承 SuperType 并传参
    SuperType.call(this, "Nicholas")
    // 实例属性
    this.age = 29
    }
    let instance = new SubType()
    console.log(instance.name) // "Nicholas";
    console.log(instance.age) // 29

二、借用构造函数继承

重点: 用.call()和.apply()将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))

特点:

  1. 只继承了父类构造函数的属性,没有继承父类原型的属性。
  2. 解决了原型链继承缺点 1、2、3。
  3. 可以继承多个构造函数属性(call 多个)。
  4. 在子实例中可向父实例传参。
  5. 解决了引用值问题

缺点:

  1. 只能继承父类构造函数的属性。
  2. 无法实现构造函数的复用。
  3. 每个新实例都有父类构造函数的副本,臃肿。
function SuperType(name) {
this.name = name
}
function SubType() {
// 继承 SuperType 并传参
SuperType.call(this, "Nicholas")
// 实例属性
this.age = 29
}
let instance = new SubType()
console.log(instance.name) // "Nicholas";
console.log(instance.age) // 29

三、组合继承(组合原型链继承和借用构造函数继承)(常用)

重点: 结合了两种模式的优点,传参和复用

function SuperType(name) {
this.name = name
this.colors = ["red", "blue", "green"]
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name) //// 第一次调用 SuperType()
this.age = age
}
// 继承方法
SubType.prototype = new SuperType() // 第二次调用 SuperType()
SubType.prototype.sayAge = function () {
console.log(this.age)
}
let instance1 = new SubType("Nicholas", 29)
console.log("instance1=>", instance1)
instance1.colors.push("black")
console.log(instance1.colors) // "red,blue,green,black"
instance1.sayName() // "Nicholas";
instance1.sayAge() // 29
let instance2 = new SubType("Greg", 27)
console.log(instance2.colors) // "red,blue,green"
instance2.sayName() // "Greg";
instance2.sayAge() // 27

特点:

  1. 可以继承父类原型上的属性,可以传参,可复用。
  2. 每个新实例引入的构造函数属性是私有的。

缺点: 组合继承其实也存在效率问题。最主要的效率问题就是 父类构造函数始终会被调用两次 :一次在是创建子类原型时调用,另一次是在子类构造函数中调用

四、原型式继承

重点: 用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。

//核心代码
function object(o) {
function F() {}
F.prototype = o
return new F()
}

let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"],
}
let anotherPerson = object(person)
anotherPerson.name = "Greg"
anotherPerson.friends.push("Rob")
let yetAnotherPerson = object(person)
yetAnotherPerson.name = "Linda"
yetAnotherPerson.friends.push("Barbie")
console.log(person.friends) // "Shelby,Court,Van,Rob,Barbie"

特点: 类似于复制一个对象,用函数来包装。

缺点:

  1. 所有实例都会继承原型上的属性。
  2. 无法实现复用。(新实例属性都是后面添加的)

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住,属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的

webpack 及 vite 的区别

Webpack 和 Vite 的核心差异在于构建理念不同:Webpack 是打包导向的构建工具,它将所有模块打包为 bundle 再启动;而 Vite 是基于浏览器原生 ESM 的按需加载,它在开发时不打包,直接通过 HTTP 按需加载模块,启动速度非常快。

在开发体验上,Vite 秒启动,HMR 更快,调试体验也更接近源码;Webpack 在大项目中插件生态成熟,构建灵活性高。

我在 Vue3 项目中主要使用 Vite,结合 vite-plugin-vue 和 unplugin 系列插件,开发体验很好;但如果遇到需要高度定制的场景,比如微前端、Module Federation,我们也会选择 Webpack。

宏任务微任务

主线程开始

执行同步代码

执行完一个宏任务(比如 setTimeout)

立即执行所有微任务(Promise.then、MutationObserver)

DOM更新 → requestAnimationFrame

下一轮宏任务

javaScript 是单线程的,执行顺序由事件循环(Event Loop)控制。一个完整的执行周期叫一个宏任务(macro task),比如 setTimeout、主线程代码等。而微任务(micro task)是在每个宏任务执行后立即执行的任务,如 Promise.thenMutationObserver

微任务执行优先级高于宏任务。每次宏任务执行完后,都会清空微任务队列,再开始下一轮宏任务。比如在 setTimeoutPromise 混用时,Promise.then 总是比 setTimeout 更早执行。

this 指向

① 默认绑定(独立函数调用)

function show() {
console.log(this);
}
show(); // 非严格模式:window,严格模式:undefined



② 隐式绑定(通过对象调用)
const obj = {
name: 'Vue',
say() {
console.log(this.name);
}
};
obj.say(); // this → obj


③ 显式绑定(call / apply / bind)
function show() {
console.log(this.name);
}
const obj = { name: 'React' };
show.call(obj); // this → obj
show.apply(obj); // this → obj
const bound = show.bind(obj);
bound(); // this → obj

new 构造函数绑定(构造函数中)
function User(name) {
this.name = name;
}
const u = new User('张三');
console.log(u.name); // this → 新创建的对象

⑤ 箭头函数(没有自己的 this
const obj = {
name: 'Vue',
say: () => {
console.log(this.name);
}
};
obj.say(); // this → 外层作用域(通常是 window)

// 正确用法:
const obj2 = {
name: 'React',
say() {
const inner = () => {
console.log(this.name);
}
inner(); // this → obj2(继承自外层函数)
}
};
obj2.say(); // React


中级面试题

hybrid 双向通信方式,Electron 通信方式

1.hybrid

  • 常见方式平台说明
    window.webkit.messageHandlers.xxx.postMessage()iOSiOS WKWebView 标准通信方式
    window.Android.xxx()AndroidAndroid WebView 中暴露原生接口
    URL Scheme通用JS 构造一个特殊 URL,原生拦截跳转
    prompt 拦截Android/iOSJS 调用 prompt,Native 拦截其调用
    JSBridge 框架通用自定义桥接封装(如 WeixinJSBridge)

    JSBridge 框架

import dsBridge from "dsbridge"
/**
* 跳本地订单中心-订单详情页
* @param args { "orderNo": String }
*/
export function viewOrder(data: IOrdering) {
return dsBridge.call("rt-default.viewOrder", JSON.stringify(data))
}

/**
* 跳本地订单中心-订单详情页
* @param args { "orderNo": String }
*/
export function refreshPage(callback: Fn) {
dsBridge.register("depoist.refreshPage", (args: any[]) => {
callback(args)
})
}
  1. Electron
// 渲染进程 renderer.js
const { ipcRenderer } = require("electron")
ipcRenderer.send("show-alert", "Hello from renderer")

// 主进程 main.js
const { ipcMain } = require("electron")
ipcMain.on("show-alert", (event, arg) => {
console.log(arg) // "Hello from renderer"
})

safri/安卓 webview 的内核分别是什

OS 上所有 WebView 和 Safari 浏览器统一使用 WebKit 内核,这是 Apple 的平台限制。Android 上则使用 Chromium(Blink 引擎)作为 WebView 的渲染内核,从 Android 5.0 起 WebView 可独立更新,和 Chrome 共用内核。

这也解释了为什么我们在做 H5 页面兼容时,iOS WebView 和 Safari 表现基本一致,而 Android 各家 WebView 则可能因系统版本或厂商定制而有所差异。

iframe

推荐方法:postMessage(标准安全、跨域支持)

无论 iframe 是否同源,postMessage 都是推荐的通信方式。

👈 主页面发送消息给 iframe:
const iframe = document.getElementById('myIframe');
iframe.contentWindow.postMessage({ type: 'ping', data: 123 }, '*');

👉 iframe 接收消息:
window.addEventListener('message', (event) => {
console.log('收到主页面消息:', event.data);
});


👉 iframe 向主页面发送消息:
window.parent.postMessage({ type: 'pong', msg: 'hello from iframe' }, '*');
👈 主页面监听:
window.addEventListener('message', (event) => {
console.log('收到 iframe 消息:', event.data);
});

✅ 安全建议
event.origin 校验来源:

if (event.origin === 'https://trusted.com') { ... }
targetOrigin 参数不要写 '*',最好写明目标来源


其他 iframe 通信方式(不推荐/受限)

方法限制
同源直接调用(iframe.contentWindow.fn)只能同源
location.hash + 轮询低效、不安全
localStorage + storage 事件不适合 iframe 场景
BroadcastChannel现代浏览器支持,适合多个窗口间通信

iframe 与主页面可以通过 postMessage 实现双向通信,支持跨域、性能高、安全性强。主页面可以使用 iframe.contentWindow.postMessage(),iframe 可以使用 window.parent.postMessage()

而 WebView 是移动 App 中嵌入网页的一种容器,属于原生控件。它不受浏览器同源限制,但默认无法访问原生能力,需要通过 JSBridge 来实现 Web ↔ Native 通信,常用于混合应用开发场景。

WebView 和 iframe 的区别

项目iframeWebView
本质浏览器中嵌套网页原生 App 内嵌网页
所在环境浏览器移动 App(iOS/Android)
安全限制受同源策略限制原生控制权限,如禁 JS、禁调试
能力范围浏览器能访问的功能可以通过 JSBridge 访问原生能力(摄像头、定位等)
与主页面通信postMessage、调用父窗口JSBridge 通信:Web ↔ Native
多进程/线程同一个浏览器上下文原生独立进程或线程(iOS/Android)
是否能访问 DOM可以访问嵌套内容(同源)可以访问页面 DOM,但不能访问宿主 App 的 DOM/UI
场景复杂页面嵌套/广告/沙盒化页面混合 App、UniApp、微信小程序 WebView 页等

发布订阅

class EventBus {
constructor() {
this.events = {} // 存储事件与回调列表
}

// 订阅
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
}

// 发布
emit(event, ...args) {
if (this.events[event]) {
this.events[event].forEach((cb) => cb(...args))
}
}

// 取消订阅
off(event, callback) {
if (!this.events[event]) return
this.events[event] = this.events[event].filter((cb) => cb !== callback)
}

// 只执行一次
once(event, callback) {
const wrapper = (...args) => {
callback(...args)
this.off(event, wrapper)
}
this.on(event, wrapper)
}
}

应用场景

组件通信(非父子组件)

自定义事件(如全局事件总线)

日志、埋点上报

状态更新通知(如 Redux middleware)

微前端子应用间通信

四、现成方案有哪些?

名称类型简介
mitt轻量库仅 200 行,零依赖,高性能
EventEmitter3类 Node 的实现支持命名空间、once、性能好
Vue EventBusVue2 全局通信方案已被 Vue3 Composition API 替代
RxJS响应式编程支持流控制、操作符,学习曲线高
PubSubJS轻量发布订阅库支持同步/异步
TinyEmitter非常小的实现仅几百字节,用于小项目

什么是原子性

原子性(Atomicity)**指的是**一个操作要么全部完成,要么完全不做中间不会被中断或分割

虽然 JS 是单线程的,但也有“原子性”问题,比如:

  • localStorage 并非原子操作(多个 Tab 同时读写可能冲突)
  • 异步 Promise 不能打断(它是原子执行的)

前端状态管理 使用批量更新机制(如 React 批量 setState)

vue 项目核心文件夹

├── src/                  # 核心源码目录
│ ├── assets/ # 静态资源(图片、字体等)
│ ├── components/ # 通用组件(基础 UI 组件)
│ ├── views/ # 页面级组件(对应路由)
│ ├── layouts/ # 布局组件(如侧边栏、导航栏)
│ ├── router/ # 路由配置(Vue Router)
│ ├── store/ # 状态管理(Pinia / Vuex)
│ ├── composables/ # 可复用的组合函数(Composition API)
│ ├── directives/ # 自定义指令
│ ├── utils/ # 工具函数库(如格式化、校验等)
│ ├── api/ # API 请求封装(Axios 封装、接口管理)
│ ├── hooks/ # 与 composables 类似,用于封装逻辑
│ ├── types/ # TypeScript 类型定义
│ ├── locales/ # 国际化资源(i18n)
│ ├── styles/ # 全局样式(如 Tailwind、变量、mixin)
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件(初始化应用)
├── public/ # 公共静态资源目录(不经 Webpack/Vite 处理)
├── vite.config.ts # Vite 配置文件
├── tsconfig.json # TypeScript 配置
└── package.json # 依赖和脚本管理

还有一些枚举enum 常量constantion

vue 双向绑定原理

Vue 3 的响应式核心是:通过 Proxy 拦截对象属性访问,实现 get收集依赖set触发依赖更新。依赖通过 effect 函数注册,形成响应链。

  1. 创建响应式对象:reactive
import { reactive } from "vue"

const state = reactive({ count: 0 })

// packages/reactivity/src/reactive.ts
export function reactive(target) {
return createReactiveObject(target, false, mutableHandlers)
}

内部通过 new Proxy(target, handler) 创建代理对象

所有的 get/set 都会走进 mutableHandlers

  1. 访问属性时:get → track
// reactive object 被访问时
get(target, key, receiver) {
track(target, key) // 收集依赖
return Reflect.get(target, key, receiver)
}

track() 做了什么?

targetMap = WeakMap<target, Map<key, Set<effect>>>
  • 使用 WeakMap 按对象存储响应属性
  • 每个属性 key 有一个依赖 Set,存储用到它的 effect 函数

⚠️ 所有访问响应式数据的代码,必须包在 effect(fn) 内部,Vue 才能知道这个函数依赖了哪些响应式数据。

  1. 修改属性时:set → trigger
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发依赖
return result
}

trigger() 做了什么?

  • 读取 targetMap[target][key] 中的依赖 Set
  • 遍历执行所有副作用函数(effect)
depsMap.get(key).forEach((effect) => effect.run())
  1. effect 收集与响应更新(核心机制)
effect(() => {
console.log(state.count) // 访问会收集依赖
})
  • effect() 会创建一个 ReactiveEffect 实例
  • 运行 fn() 时,自动触发 gettrack() 收集
  • 数据变化时 → trigger() → 找到相关 effect → scheduler() 中重新执行

✅ 这就是响应式依赖追踪系统,不再使用 Vue 2 中的 Dep/Watcher!

Vue 3 的双向数据绑定是建立在全新的响应式系统之上的,核心是通过 Proxy 拦截对象的 get/set 操作,实现依赖收集和派发。Vue 内部维护了一个 targetMap(WeakMap 嵌套结构)来映射每个属性到它依赖的副作用函数(effect)。当我们使用 reactive 创建响应式对象后,访问属性会触发 track 收集依赖,修改属性会触发 trigger 通知相关副作用重新执行,最终更新 DOM。

对于 v-model,它本质是一个语法糖,等价于 :value + @input,依赖的正是这个响应式机制。相比 Vue 2 的 defineProperty + Dep/Watcher,Vue 3 的 Proxy 机制更加灵活、支持动态属性和懒代理,性能也更优。

vue 代码中,为什么不建议使用 querySelector

Vue 是一个响应式框架,推荐以“数据驱动视图”的方式开发,直接使用 document.querySelector 属于命令式操作,会破坏响应式机制、组件封装性和生命周期管理。取而代之,应通过 ref 获取 DOM 元素,配合生命周期钩子和响应式数据来控制行为,能更安全、更可维护。

你知道哪些 Vue3 新特性?

  • Composition API

    • 组合式 API (响应式 API ref()、reactive(),生命周期钩子onMounted()、onUnmounted(),依赖注入inject()、provide())
  • SFC Composition API Syntax Sugar (<script setup>)

    • 单文件组合式 API 语法糖(setup 语法糖)
    • 让代码更简洁,性能更好(不需要借助代理对象)。
  • Teleport

    • 类似于 React 中的 Portal 传送门组件,指定将组件渲染到某个容器中。

    • 经常用于处理弹窗组件和模态框组件。

      <button @click="open = true">打开模态框</button>
      <Teleport to="body">
      <div v-if="open" class="modal">
      <button @click="open = false">关闭</button>
      </div>
      </Teleport>
  • Fragments

    • Fragment(片段)Vue3 中允许组件中包含多个节点。无需引入额外的 DOM 元素。
  • Emits Component Option

    • Vue3 中默认绑定的事件会被绑定到根元素上。通过 Emits 属性可将事件从attrs 中移除。
  • createRenderer API from @vue/runtime-core to create custom renderers

    • 提供自定义渲染器,可以在非 DOM 环境中使用 Vue 的运行时。
  • SFC State-driven CSS Variables (v-bind in <style>)

    • 在 css 中使用 v-bind 绑定样式
    background: v-bind(color);
  • SFC <style scoped> can now include global rules or rules that target only slotted content

    • 在作用域样式中可以包含全局规则或只针对插槽内容的规则
    /* 跨组件修改组件内样式 */
    .parent :deep(h1) {
    color: red;
    }
    /* 控制全局样式 */
    :global(.root) {
    width: 100px;
    height: 100px;
    background: yellow;
    }
    /* 控制插槽内容的样式 */
    :slotted(.child) {
    color: red;
    }
  • Suspense experimental

    • 主要的作用优雅地处理异步组件的加载状态

    vue

    <Suspense>
    <template #default>
    <!-- 可以配合async setup使用 -->
    <AsyncComponent></AsyncComponent>
    </template>
    <template #fallback>
    正在加载异步组件...
    </template>
    </Suspense>

vue2 和 vue3 的区别

  • 性能优化(更快):
    • 使用了Proxy替代 Object.defineProperty 实现响应式。(为什么?defineProperty 需要对属性进行递归重写添加gettersetter 性能差,同时新增属性和删除属性时无法监控变化,需要$set、$delete 方法。此方法对数组劫持性能差,同时不支持 map 和 set 的数据结构。)
    • 模板编译优化。给动态节点增添 PatchFlag 标记;对静态节点进行静态提升;对事件进行缓存处理等。
    • Diff 算法优化,全量 diff 算法中采用最长递增子序列减少节点的移动。在非全量 diff 算法中只比较动态节点,通过 PatchFlag 标记更新动态的部分。
  • 体积优化(更小):
    • Vue3 移除了不常用的 API
      • 移除 inline-template (Vue2 中就不推荐使用)
      • $on、$off、$once (如果有需要可以采用 mitt 库来实现)
      • 删除过滤器 (可以通过计算属性或者方法来实现)
      • $children移除 (可以通过provide,inject方法构建$children)
      • 移除.sync .native)修饰符 (.sync通过 v-model:xxx实现,.native为 Vue3 中的默认行为) 以及不在支持 keycode 作为v-on修饰符(@keyup.13 不在支持)
      • 移除全局 API。Vue.component、Vue.use、Vue.directive (将这些 api 挂载到实例上)
    • 通过构建工具 Tree-shaking 机制实现按需引入,减少用户打包后体积。
  • 支持自定义渲染器:
    • 用户可以自定义渲染 API 达到跨平台的目的。扩展能力更强,无需改造 Vue 源码。
  • TypeScript 支持:
    • Vue3 源码采用 Typescript 来进行重写 , 对 Ts 的支持更加友好。
  • 源码结构变化:
    • Vue3 源码采用 monorepo 方式进行管理,将模块拆分到 package 目录中,解耦后可单独使用。

如何看待 Composition API 和 Options API?

  • 在 Vue2 中采用的是 OptionsAPI, 用户提供的 data,props,methods,computed,watch 等属性 (用户编写复杂业务逻辑会出现反复横跳问题)
  • Vue2 中所有的属性都是通过this访问,this存在指向明确问题
  • Vue2 中很多未使用方法或属性依旧会被打包,并且所有全局 API 都在 Vue 对象上公开Composition API 对 tree-shaking 更加友好,代码也更容易压缩。
  • 组件逻辑共享问题, Vue2 采用 mixins 实现组件之间的逻辑共享; 但是会有数据来源不明确,命名冲突等问题。 Vue3 采用 CompositionAPI 提取公共逻辑非常方便

Vue 中如何进行依赖收集?

vue2

  • 每个属性都拥有自己的dep属性,存放他所依赖的 watcher,当属性变化后会通知自己对应的 watcher 去更新
  • 默认在初始化时会调用 render 函数,此时会触发属性依赖收集 dep.depend
  • 当属性发生修改时会触发watcher更新 dep.notify()

Vue3

  • Vue3中会通过Map结构将属性和effect映射起来。
  • 默认在初始化时会调用 render 函数,此时会触发属性依赖收集track
  • 当属性发生修改时会找到对应的effect列表依次执行trigger

Vue 中如何检测数组变化?

1.1 Vue2 中采用重写数组方法的方式

  • 数组考虑性能原因没有用defineProperty对数组的每一项进行拦截,而是选择重写数组(push,shift,pop,splice,unshift,sort,reverse)方法。数组中如果是对象数据类型也会进行递归劫持

    1.2 Vue3 直接采用的是 Proxy

  • 在 Vue 3.x 中,直接使用 Proxy 实现了更高效精确的数组变化检测,通过 Proxy,Vue 可以捕获到数组索引和长度的变化,不再需要重写数组的方法。这是 Vue 3.x 在性能方面的一个重要改进(但是由于代理问题,还需要对部分检测方法进行重写)。

requestAnimationFrame 的帧率是多少?能保证 60fps 吗

vue3 从数据到页面的流程

Reactive/Ref
↓(get
track 依赖收集

用户触发 set

trigger → effect

scheduler 推入 queue
↓(微任务)
执行 job → render → patch

更新 DOM

Vue3 在 setup 中使用 reactive 创建数据时,会通过 Proxy 劫持 get/set。组件首次渲染时会访问响应式数据,触发 track() 建立依赖。之后当数据发生改变时,set 触发 trigger(),会调度副作用函数重新执行,这个副作用函数由 Vue 内部的 effect() 包裹,其中带有 scheduler,会异步推入任务队列中。任务 flush 时,会执行组件的 render 函数生成新的虚拟 DOM,最后通过 patch() 对比并更新真实 DOM。

高级

有没有做过前端兼容性方面的工作

  1. CSS 兼容性
  • 使用 autoprefixer 自动添加前缀(如 -webkit--ms-
  • 使用 postcss-preset-env 限定语法版本
  • 遇到 flex, grid, position: sticky 等行为不一致时,使用 fallback 或 polyfill
  • 样式兼容性调试会使用 Can I use 和 DevTools 模拟器

📍 真实例子:

在适配 iOS Safari 时,发现 vh 单位在软键盘弹出时会被压缩,于是改用 window.innerHeight + JS 设置高度,解决移动端跳动问题。

JS API 兼容性

  • 使用 core-js + babel-polyfill 处理 Promise, Array.prototype.includes, async/await 等新特性兼容
  • 根据目标浏览器配置 browserslist,自动注入兼容转换
  • 避免使用不兼容的 API(如 IE 不支持的 classList.replace()

真实例子:

某次在使用 Object.fromEntries() 时,IE 报错,查阅发现是 ES2019 的 API,使用 polyfill 替代。

DOM 行为兼容

  • 使用事件代理替代不支持的事件(如 inputchange 差异)
  • 检查事件冒泡行为差异(IE 有些事件不冒泡)
  • Safari 对 preventDefault() 有时候需要放在 touchstart 而不是 click
  1. 移动端兼容性
  • Android 微信浏览器对 position: fixed 支持不一致
  • iOS 滚动穿透用 touch-action, overflow: hidden, preventDefault 联合解决
  • Viewport 单位兼容:vh/vw + 真实视口 + resize 监听
  1. WebView/小程序兼容性
  • WebView 版本碎片化严重,常需要使用 JS Bridge 检测版本
  • 使用自定义 UA 判断(或 JSBridge 能力探测)
  • 针对低版本 WebView 做能力降级(如取消动画)

开启严格模式,它的作用

开启 JavaScript 的 严格模式(Strict Mode) 是一种“更严格、更安全的 JS 运行模式”,它可以帮助开发者减少 bug、禁止一些不合理用法、增强安全性,并对 ES6+ 的模块化、Class 体系奠定基础。

"use strict"

// ❌ 未声明变量直接赋值会报错
x = 10 // ReferenceError: x is not defined

// ❌ 不允许删除变量
var foo = 1
delete foo // SyntaxError

// ❌ 函数参数不能重名
function test(a, a) {} // SyntaxError

// ✅ this 不再指向 window
function showThis() {
console.log(this) // undefined
}
showThis()

其他

如何穿透遭罩层

  1. 页面上有一个按钮,按钮上层还有层遭罩层,如何点击穿透遮罩,点 击到按钮

给遮罩层设置 pointer-events: none;,这样遮罩层本身不会响应鼠标事件,点击事件会直接传递到底下的

元素永远不会成为鼠标事件的target。但是,当其后代元素的pointer-events属性指定其他值时,鼠标事件可以指向后代元素,在这种情况下,鼠标事件将在捕获或冒泡阶段触发父元素的事件侦听器。

如何实现路由守卫

Vue Router 如何实现路由守卫,根据条件拦截某些路由跳转,并重定向

// router/index.js 或 main.js
router.beforeEach((to, from, next) => {
// 举例:根据登录状态决定是否允许访问某些路由
const isLoggedIn = false; // 你的登录判断逻辑

if (to.meta.requiresAuth && !isLoggedIn) {
// 如果目标路由需要登录,且没登录,重定向到登录页
next({ path: '/login' });
} else {
// 允许访问
next();
}
});


const routes = [
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true },
},
{
path: '/login',
component: Login,
},
];

to:即将要进入的路由对象

from:当前导航正要离开的路由

next:必须调用,否则页面不会跳转

调用 next() 表示放行
调用 next('/login')next({ path: '/login' }) 表示重定向

当 URL 的查询參数(query 參数)本身是一个网络地址要注意什么?

查询参数的值中包含特殊字符(如 :/?& 等)会破坏 URL 的结构,导致解析错误。

需要使用 encodeURIComponent 对整个网络地址进行编码,确保它能作为一个整体安全地放入查询参数中。

在后端或前端获取查询参数时,要用 decodeURIComponent 还原原始的网络地址。

SHA256

如果您指的是 SHA-256(有时也简称为 H256),那它是常用的哈希算法之一,具有不可逆、定长输出、抗篡改等特点。前端主要用于对密码或数据做签名、摘要等,比如登录前对用户密码哈希处理、API 签名校验、JWT 的 HS256 签名算法等。我常用 js-sha256 或 crypto.subtle 来实现。

Docker 是什么?

Docker 是一个开源的容器化平台,允许你将应用和它所依赖的环境打包到一个“容器”中,从而实现应用的一次构建,到处运行。

  • 主要功能
    • 打包应用及其依赖(代码、库、环境变量、配置文件等)
    • 在任何支持 Docker 的环境中快速启动应用容器
    • 资源隔离和高效利用(比传统虚拟机轻量)
  • 应用场景
    • 快速部署和发布应用
    • 多环境一致性(开发、测试、生产)
    • 微服务架构
    • 持续集成与交付(CI/CD)
  • 特点
    • 轻量级虚拟化
    • 镜像(Image)和容器(Container)管理
    • 丰富的生态和社区支持(Docker Hub 等)

什么是 WebRTC 和 webRTM

WebRTC (Web Real-Time Communication):

  1. WebRTC(Web Real-Time Communication) 是一套由 W3C 和 IETF 标准化的技术,允许 浏览器之间或原生应用之间进行实时音视频通信和数据传输不需要安装任何插件
  2. 主要功能包括:
    • 音视频实时传输
    • 点对点数据传输
    • 音视频设备访问
    • 屏幕共享
  3. 使用场景:
    • 视频会议
    • 在线教育
    • 远程协助
    • 实时游戏
  4. 核心优势:
    • 无需插件,浏览器原生支持
    • 低延迟
    • 安全性高(强制加密)
    • 开源免费

WebRTM (Web Real-Time Messaging):

  1. WebRTM 是一个实时消息传输协议和系统
  2. 主要功能:
    • 实时消息推送
    • 在线状态同步
    • 消息队列
    • 消息持久化
  3. 使用场景:
    • 即时通讯
    • 实时通知
    • 在线协作
    • 实时数据同步
  4. 核心优势:
    • 轻量级
    • 可扩展性强
    • 支持多种传输协议
    • 消息可靠性保证

主要区别:

  1. 用途不同:
    • WebRTC 主要用于音视频通信和大量数据的点对点传输
    • WebRTM 主要用于文本消息和小数据包的实时传输
  2. 架构不同:
    • WebRTC 是点对点架构(P2P)
    • WebRTM 通常是客户端-服务器架构
  3. 数据传输:
    • WebRTC 适合大流量、低延迟的音视频数据
    • WebRTM 适合小数据包的高频率传输

这两种技术经常会结合使用,比如在视频会议系统中:

  • WebRTC 负责音视频传输
  • WebRTM 负责信令传输和在线状态同步

🧩 WebRTC 能干嘛?

  • 实时音视频通信:如音视频通话、会议系统(Zoom、腾讯会议、Google Meet)
  • 实时数据通道RTCDataChannel):用于 P2P 文件传输、游戏同步等
  • 屏幕共享:如远程协作、投屏等

🛠 WebRTC 的核心组成

  1. getUserMedia() 获取本地设备的音视频流(麦克风、摄像头、屏幕)
  2. RTCPeerConnection 建立浏览器之间的 P2P 连接,传输音视频流
  3. RTCDataChannel 传输任意数据(例如聊天消息、文件)
  4. 信令(Signaling) 用于协商连接(如交换 SDP、ICE candidate),不是 WebRTC 标准的一部分,常用 WebSocket、自定义协议实现

WebRTC:浏览器之间可以直接视频通话的技术

getUserMedia:调摄像头

PeerConnection:让两个浏览器能说话

offer / answer:像谈恋爱要先表白,另一方回应,然后建立连接

🌐 简单连接流程图


用户A 用户B
┃ ┃
┃--getUserMedia()-->┃
┃<--音视频流--------┃
┃ ┃
┃--Signaling通道---->┃ (交换SDP/ICE)
┃<--Signaling通道----┃
┃--建立RTCPeerConnection-->┃
┃<---- P2P 音视频通信 ----┃
let localStream
let localVideo = document.getElementById("localVideo")
let remoteVideo = document.getElementById("remoteVideo")

let pc1, pc2 // 两个“用户”的浏览器 Peer

async function start() {
// 1. 获取本地摄像头
localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false })
localVideo.srcObject = localStream

// 2. 创建两个 PeerConnection(本地模拟两人)
pc1 = new RTCPeerConnection()
pc2 = new RTCPeerConnection()

// 3. 把本地视频流加到 pc1
localStream.getTracks().forEach((track) => {
pc1.addTrack(track, localStream)
})

// 4. 设置 pc2 接收到媒体流时显示
pc2.ontrack = (event) => {
remoteVideo.srcObject = event.streams[0]
}

// 5. 模拟信令交换
const offer = await pc1.createOffer()
await pc1.setLocalDescription(offer)
await pc2.setRemoteDescription(pc1.localDescription)

const answer = await pc2.createAnswer()
await pc2.setLocalDescription(answer)
await pc1.setRemoteDescription(pc2.localDescription)
}

📦 应用举例:

应用WebRTC 用法
腾讯会议实时音视频传输
腾讯文档协作用 RTCDataChannel 做协同数据传输
文件 P2P 共享使用 WebRTC + DataChannel 传输大文件
游戏同步状态传输低延迟通信,配合 WebSocket 做同步

git stash

# 1. 保存当前所有修改(不想提交,但要切换分支)
git stash

# 2. 查看 stash 列表
git stash list
# 输出示例:
# stash@{0}: WIP on dev: 8372d7e 修改了登录功能

# 3. 应用并清除最近一次
git stash pop

# 或只应用,不清除
git stash apply

# 4. 删除某一条 stash
git stash drop stash@{0}

继承

  1. class A 继承自 class B,A 和 B 有一个同名的方法:getName,那么 A 的实例调用 getName 是否会执行 B 的 getName
JavaScript 中的类继承基于原型链,当子类与父类有同名方法时,子类方法会“覆盖”父类方法。方法查找遵循最近优先原则,也就是“先查自己,再往原型链上查”,所以 A 的实例调用 getName() 时,只会执行 A 自己的版本。
// 如何调用父类的方法?
你可以手动调用 super.getName() 来执行 B 的方法:

项目

前端渲染 3d 数字人,有什么方案/思路

类型技术栈特点适用场景
Three.js + GLTF 模型WebGL 底层渲染,广泛应用性能强大,社区成熟,支持骨骼动画、灯光、贴图多数 Web3D 项目、虚拟人展示
Babylon.js高性能 3D 引擎更偏游戏/场景渲染,支持 PBR游戏式交互、高保真数字人
Unity WebGL 发布Unity 构建后嵌入网页高还原度,但包大,加载慢高精度数字人、多人互动
WebGPU(新标准)+ PlayCanvas新一代 GPU 接口,性能更高前沿技术,适配较少设备高端设备、实验性产品
Sora + WebRTC 视频生成通过云渲染 + 视频流推送不依赖前端算力,画质佳AI 数字人直播/讲解
iframe + 云渲染嵌入从云端实时生成画面流能加载超高模型,支持交互重场景数字人
2.5D Live2D轻量动画,适合卡通风类似 VTuber 风格虚拟客服、轻交互展示

从开发到上线,如何做好分支管理

1. 从 develop 拉 feature 分支 → 开发 → 本地自测 ✅
2. 合并到 develop(提交 PR / MR)→ 触发集成环境 CI/CD
3. 多个功能完成后,创建 release 分支 → 联调测试(UAT)
4. 验收无误后,release 合并到 main & develop
5. main 触发生产部署
6. 线上问题,用 hotfix 处理 → 合并 main & develop

Node 能否利用多核处理器?

child_process 模块(更灵活)

用于手动创建子进程(子任务),适用于:

  • 执行 CPU 密集任务
  • 执行异步脚本、爬虫、图像处理等

const { fork } = require('child_process');

const childCount = 3; // 启动3个子进程
const children = [];

for (let i = 0; i < childCount; i++) {
const child = fork('./child.js');

child.on('message', (msg) => {
console.log(`Master received from child ${i}:`, msg);
});

child.on('exit', (code) => {
console.log(`Child ${i} exited with code ${code}`);
});

// 发送消息给子进程启动工作
child.send(`Hello child ${i}`);

children.push(child);
}

// child.js
process.on('message', (msg) => {
console.log(`Child received message: ${msg}`);
// 模拟耗时任务
setTimeout(() => {
process.send(`Child done with message: ${msg}`);
process.exit(0);
}, 1000 + Math.random() * 2000);
});

📌 特点:

  • 子进程间通过 send 通信(基于 IPC 通道)
  • 更适合业务子任务处理,而不是服务监听