← 已是第一轮 · 返回本次面经 · 已是最后一轮 →
本轮共 21 道题。答案默认折叠,便于先自行作答。
1. 深拷贝浅拷贝有什么区别?怎么实现深拷贝?
题库原题:深拷贝浅拷贝有什么区别?怎么实现深拷贝?
题目要点
JS数据类型分别基本数据类型和引用数据类型,基本数据类型保存的是值,引用类型保存的是引用地址(this指针)。
浅拷贝共用一个引用地址,深拷贝会创建新的内存地址。
浅拷贝方法
- 直接对象复制
- Object.assign
深拷贝
- JSON.stringify转为字符串再JSON.parse
- 深度递归遍历
参考答案
一、数据类型存储
前面文章我们讲到,JavaScript中存在两大数据类型:
- 基本类型
- 引用类型
基本类型数据保存在在栈内存中
引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中
二、浅拷贝
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址
下面简单实现一个浅拷贝
function shallowClone(obj) {
const newObj = {};
for(let prop in obj) {
if(obj.hasOwnProperty(prop)){
newObj[prop] = obj[prop];
}
}
return newObj;
}
在JavaScript中,存在浅拷贝的现象有:
Object.assignArray.prototype.slice(),Array.prototype.concat()- 使用拓展运算符实现的复制
Object.assign
var obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'fx',
name2: 'xka'
},
love: function () {
console.log('fx is a great girl')
}
}
var newObj = Object.assign({}, obj);
slice()
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.slice(0)
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
concat()
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
拓展运算符
const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
三、深拷贝
深拷贝开辟一个新的栈,两个对象的属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝方式有:
_.cloneDeep()
jQuery.extend()
JSON.stringify()
手写循环递归
_.cloneDeep()
const _ = require('lodash');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
jQuery.extend()
const $ = require('jquery');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
JSON.stringify()
const obj2=JSON.parse(JSON.stringify(obj1));
但是这种方式存在弊端,会忽略undefined、symbol和函数
const obj = {
name: 'A',
name1: undefined,
name3: function() {},
name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}
循环递归
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
四、区别
下面首先借助两张图,可以更加清晰看到浅拷贝与深拷贝的区别

从上图发现,浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象
// 浅拷贝
const obj1 = {
name : 'init',
arr : [1,[2,3],4],
};
const obj3=shallowClone(obj1) // 一个浅拷贝方法
obj3.name = "update";
obj3.arr[1] = [5,6,7] ; // 新旧对象还是共享同一块内存
console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3',obj3) // obj3 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
// 深拷贝
const obj1 = {
name : 'init',
arr : [1,[2,3],4],
};
const obj4=deepClone(obj1) // 一个深拷贝方法
obj4.name = "update";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存
console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
小结
前提为拷贝类型为引用类型的情况下:
- 浅拷贝是复制内存中的地址,拷贝前后的对象,因为引用类型共享了同一块内存,修改会相互影响。
- 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址
2. 如何用JSON.parse(JSON.stringify())实现深拷贝?
题目要点
JSON.parse(JSON.stringify()) 适合 结构简单、数据类型基础 的对象深拷贝,如果涉及函数、Date、Map 等复杂对象,就需要手写递归拷贝或用 lodash.cloneDeep() 这样的库。
参考答案
用 JSON.parse(JSON.stringify(obj)) 实现深拷贝的原理和过程是这样的:
序列化
JSON.stringify(obj)会把一个对象序列化成 JSON 字符串。- 这个过程中,所有可枚举的属性会被转换为字符串格式。
反序列化
JSON.parse(...)会把字符串重新解析成一个新的对象。- 这样一来,新的对象和原来的对象在内存中是完全独立的,没有引用关系。
所以:
const obj = {
name: 'Tom',
age: 20,
info: {
city: 'Beijing'
}
};
const copy = JSON.parse(JSON.stringify(obj));
copy.info.city = 'Shanghai';
console.log(obj.info.city); // 'Beijing'
console.log(copy.info.city); // 'Shanghai'
可以看到,修改 copy 不会影响原对象,从而实现了“深拷贝”。
优点
- 语法简单,一行搞定。
- 能够处理嵌套对象和数组的场景。
缺点(局限性)
无法拷贝函数:函数会丢失(因为
JSON只支持数据)。const obj = { fn: () => {} }; JSON.parse(JSON.stringify(obj)); // { }无法拷贝
undefined、Symbol、BigInt等非 JSON 支持的类型。const obj = { a: undefined, b: Symbol('x') }; console.log(JSON.parse(JSON.stringify(obj))); // {}无法正确处理特殊对象:如
Date会变成字符串,RegExp变成空对象,Map/Set丢失。const obj = { d: new Date(), r: /abc/ }; console.log(JSON.parse(JSON.stringify(obj))); // { d: "2025-08-28T08:00:00.000Z", r: {} }有循环引用会报错:
const obj = {}; obj.self = obj; JSON.parse(JSON.stringify(obj)); // 报错
3. 如何实现WebSocket的断线重连机制?
题目要点
重连时是否保留消息队列
- 可以在断开时缓存未发送的消息,等重连后再补发。
避免服务端踢出
- 需要和服务端约定心跳协议(如
ping/pong),否则可能会被网关/代理关闭连接。
- 需要和服务端约定心跳协议(如
区分“主动关闭”与“异常关闭”
- 主动关闭时不需要重连。
移动端环境
- 在 App/浏览器后台时可能会被系统挂起,需要特别处理。
参考答案
实现 WebSocket 的断线重连机制,一般需要在客户端(浏览器/前端)做一些健壮性处理。核心思路就是:
捕获异常/断开事件
- 监听
onclose、onerror,在连接关闭或出错时,启动重连逻辑。 - 可选:在
onmessage里做心跳检测,发现服务端长时间未响应也触发重连。
- 监听
指数退避/固定间隔重连
避免频繁重连给服务器造成压力。
典型策略:
- 每次重连延迟
delay = Math.min(maxDelay, baseDelay * 2^attempt) - 或者使用固定间隔(如 5 秒)。
- 每次重连延迟
心跳机制(保持长连接)
- 定时发送心跳包(如
ping),服务端回应(如pong)。 - 如果心跳超时未响应,则主动关闭并重连。
- 定时发送心跳包(如
示例实现(JavaScript)
class ReconnectWebSocket {
constructor(url, options = {}) {
this.url = url;
this.ws = null;
this.lockReconnect = false; // 防止重复重连
this.reconnectDelay = options.reconnectDelay || 2000; // 初始重连间隔
this.maxDelay = options.maxDelay || 30000; // 最大重连间隔
this.heartbeatInterval = options.heartbeatInterval || 10000; // 心跳间隔
this.heartbeatTimer = null;
this.reconnectTimer = null;
this.createWebSocket();
}
createWebSocket() {
try {
this.ws = new WebSocket(this.url);
this.initEventHandlers();
} catch (e) {
this.reconnect();
}
}
initEventHandlers() {
this.ws.onopen = () => {
console.log("WebSocket 连接成功");
this.startHeartbeat();
};
this.ws.onmessage = (msg) => {
console.log("收到消息:", msg.data);
// 服务端 pong 响应时,重置心跳
if (msg.data === "pong") {
this.resetHeartbeat();
}
};
this.ws.onerror = () => {
console.log("WebSocket 出错");
this.reconnect();
};
this.ws.onclose = () => {
console.log("WebSocket 关闭");
this.reconnect();
};
}
reconnect() {
if (this.lockReconnect) return;
this.lockReconnect = true;
console.log("准备重连...");
this.reconnectTimer && clearTimeout(this.reconnectTimer);
this.reconnectTimer = setTimeout(() => {
this.createWebSocket();
this.lockReconnect = false;
// 增加延迟,避免频繁重连
this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxDelay);
}, this.reconnectDelay);
}
startHeartbeat() {
this.heartbeatTimer && clearInterval(this.heartbeatTimer);
this.heartbeatTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
console.log("发送心跳 ping");
this.ws.send("ping");
}
}, this.heartbeatInterval);
}
resetHeartbeat() {
console.log("收到 pong,心跳正常");
}
send(msg) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(msg);
} else {
console.log("连接未建立,消息丢弃");
}
}
}
// 使用示例
const ws = new ReconnectWebSocket("wss://example.com/socket", {
reconnectDelay: 2000,
maxDelay: 30000,
heartbeatInterval: 10000,
});
4. 如何通过虚拟列表解决小程序中长列表渲染卡顿问题?
题目要点
小程序的 渲染层(WebView) 与 逻辑层(JS) 之间通信开销大。一次性渲染过多节点会阻塞页面。
参考答案
1. 问题原因
- 小程序的 渲染层(WebView) 与 逻辑层(JS) 之间通信开销大。一次性渲染过多节点会阻塞页面。
- 长列表滚动过程中,DOM 节点太多会导致渲染性能下降。
2. 虚拟列表的基本原理
虚拟列表的核心是:
- 只渲染可见区域的数据(比如屏幕高度能显示 10 条,就渲染 10~15 条);
- 通过占位容器保持滚动条正确高度,避免影响用户滚动体验;
- 在滚动事件中计算可见范围,动态替换需要渲染的那一段数据。
总结:渲染 1000 条 → 实际只渲染几十条,用户感受不到差异,但性能提升明显。
3. 在小程序中的实现方式
小程序官方提供了 <scroll-view> 和 <movable-view> 等组件,但直接放长列表性能差。常见做法:
方法一:手写虚拟列表
计算可见区域
- 根据容器高度 / 每项高度,算出可显示多少条。
- 滚动时通过
scrollTop算出当前应该显示的起始索引。
渲染可见数据
visibleData = list.slice(startIndex, endIndex)- 在 WXML 里只绑定这部分数据。
占位高度
- 给整个列表一个
padding-top(表示前面隐藏的部分高度)+padding-bottom(表示后面未渲染的部分高度),保持滚动条长度。
- 给整个列表一个
示例(伪代码):
<scroll-view
scroll-y
style="height: 100vh;"
bindscroll="onScroll">
<view style="height: {{paddingTop}}px;"></view>
<block wx:for="{{visibleData}}" wx:key="id">
<view class="item">{{item.text}}</view>
</block>
<view style="height: {{paddingBottom}}px;"></view>
</scroll-view>
Page({
data: {
list: [], // 原始数据
visibleData: [],
itemHeight: 50,
containerHeight: 600,
startIndex: 0,
endIndex: 0,
paddingTop: 0,
paddingBottom: 0
},
onLoad() {
const list = new Array(1000).fill(0).map((_, i) => ({ id: i, text: `item-${i}` }));
this.setData({ list });
this.updateVisible(0);
},
onScroll(e) {
const scrollTop = e.detail.scrollTop;
this.updateVisible(scrollTop);
},
updateVisible(scrollTop) {
const { itemHeight, list, containerHeight } = this.data;
const visibleCount = Math.ceil(containerHeight / itemHeight) + 5; // 多渲染几条,避免空白
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = startIndex + visibleCount;
this.setData({
visibleData: list.slice(startIndex, endIndex),
paddingTop: startIndex * itemHeight,
paddingBottom: (list.length - endIndex) * itemHeight
});
}
});
方法二:使用官方/第三方组件
微信小程序官方提供的
wx.createSelectorQuery+scroll-view可以实现,但要自己写逻辑。有现成的 第三方虚拟列表库:
- taro / uni-app 等框架也有现成的 VirtualList 组件。
4. 注意事项
- 固定高度项更容易实现(计算快)。如果是动态高度,可以用懒加载 +
IntersectionObserver或在滚动时动态计算高度,但实现复杂。 - 提前渲染 buffer 区域(上下多渲染几条,避免快速滚动出现空白)。
- 避免频繁 setData,可以用
requestAnimationFrame节流。
5. 对于虚拟滚动的场景,如何动态计算列表项高度?如何优化滚动时的白屏问题?
题目要点
动态高度计算:用 DOM 测量 + 前缀和数组;未测量项先给预估值再修正。
白屏优化:
- 增加上下 buffer 区域
- 滚动计算节流(或
requestAnimationFrame) - 占位符保持滚动条连贯
参考答案
虚拟列表如果每一项高度是固定的,计算可见区非常简单;但在实际业务中(例如电商商品列表、动态内容流),列表项高度经常不一致,这就带来了两个核心问题:
- 如何动态计算每一项的高度
- 滚动快速时如何避免白屏
一、动态计算列表项高度
当列表项高度不固定时,不能只用“索引 × 固定高度”推算可见范围,需要实时统计高度。常见方法有:
1. 渲染后测量 DOM 高度
- 在小程序里,可以用
wx.createSelectorQuery();在 Web/React 中可以用getBoundingClientRect()。 - 每次列表项渲染完成后记录该项的实际高度。
- 用一个数组
heights[]存储每项高度,用prefixSum[](高度前缀和)快速查找任意索引的累计高度。
这样就能在滚动时通过二分查找 scrollTop 对应的起始索引。
2. 逐步懒计算
- 如果一开始不知道全部项的高度,可以只计算“第一次渲染的部分”。
- 随着用户滚动,动态测量并填充剩余项的高度。
- 对未测量的项,可以先给一个 预估高度,等渲染后再修正。
这样首屏渲染更快,避免卡顿。
二、优化滚动时的白屏问题
白屏的本质原因是:
- 渲染区域计算不够快,用户滚动过快时,列表内容还没替换好。
解决办法:
1. 缓冲区(buffer)策略
- 除了渲染“正好可见区域”外,再上下额外多渲染几条(如
+5 ~ +10)。 - 用户快速滚动时还能看到 buffer 中的数据,避免瞬间空白。
- 代价是多渲染少量元素,但整体性能仍然远优于全量渲染。
2. 节流 / requestAnimationFrame
- 滚动事件非常频繁(几十次/秒),不能每次都重新计算和
setData。 - 最佳做法是用 节流(throttle) 或
requestAnimationFrame来控制更新频率。 - 这样避免过度重排,提高流畅度。
3. 占位符容器
- 即使当前渲染的可见数据没准备好,也可以先渲染一个等高的空容器,保持滚动条正常,用户不会看到抖动或错位。
- 数据回来后替换真实内容,用户感知不到白屏。
三、综合实现思路(伪代码)
class VirtualList {
constructor(containerHeight) {
this.containerHeight = containerHeight;
this.itemHeights = []; // 每项真实高度
this.prefixSum = [0]; // 高度前缀和,prefixSum[i] = sum(0..i-1)
}
// 更新某项高度
updateHeight(index, height) {
this.itemHeights[index] = height;
this.prefixSum[index + 1] = this.prefixSum[index] + height;
}
// 通过 scrollTop 找到可见起点
findStartIndex(scrollTop) {
// 二分查找 scrollTop 所在的区间
let left = 0, right = this.prefixSum.length - 1;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (this.prefixSum[mid] <= scrollTop) {
left = mid + 1;
} else {
right = mid;
}
}
return Math.max(0, left - 1);
}
}
配合滚动事件:
- 获取
scrollTop。 - 通过前缀和 + 二分查找确定起始索引。
- 向下渲染可见区域 + buffer。
6. 判断数组的方式有哪些?
题库原题:判断数组的方式有哪些?
题目要点
- 推荐方法:使用
Array.isArray(),这是最简单、最可靠的方式来检查一个值是否为数组。 - 其他方法:
instanceof和Object.prototype.toString.call()也很有效,但有一些特定的局限性。constructor和isPrototypeOf()方法不如Array.isArray()可靠,且在某些情况下可能会出现问题。
参考答案
判断一个值是否为数组的方式有多种:
1. Array.isArray() 方法
- 描述:这是 ES5 引入的标准方法,推荐用于检查一个值是否为数组。
- 语法:
Array.isArray(value) - 示例:
console.log(Array.isArray([1, 2, 3])); // true console.log(Array.isArray('hello')); // false
2. instanceof 操作符
- 描述:使用
instanceof操作符来判断对象是否是Array的实例。 - 语法:
value instanceof Array - 示例:
console.log([1, 2, 3] instanceof Array); // true console.log('hello' instanceof Array); // false
3. Object.prototype.toString.call() 方法
- 描述:使用
Object.prototype.toString.call()可以准确判断一个对象的类型,包括数组。它返回[object Array]对于数组,其他类型则返回不同的结果。 - 语法:
Object.prototype.toString.call(value) - 示例:
console.log(Object.prototype.toString.call([1, 2, 3])); // [object Array] console.log(Object.prototype.toString.call('hello')); // [object String]
4. constructor 属性
- 描述:检查
constructor属性是否为Array。这种方法不如Array.isArray()可靠,因为constructor可以被改变。 - 语法:
value.constructor === Array - 示例:
console.log([1, 2, 3].constructor === Array); // true console.log('hello'.constructor === Array); // false
5. Array.prototype.isPrototypeOf() 方法
- 描述:检查数组的
prototype是否在目标对象的prototype链上。这种方法也可以用来判断一个对象是否为数组。 - 语法:
Array.prototype.isPrototypeOf(value) - 示例:
console.log(Array.prototype.isPrototypeOf([1, 2, 3])); // true console.log(Array.prototype.isPrototypeOf('hello')); // false
6. 使用 constructor 属性和原型链
- 描述:结合
constructor属性和原型链检查。这个方法有一定的局限性,不推荐使用。 - 示例:
function isArray(value) { return value && typeof value === 'object' && value.constructor === Array; } console.log(isArray([1, 2, 3])); // true console.log(isArray('hello')); // false
7. 如何用Promise.all实现并发请求?如何处理部分请求失败?
题目要点
- 只要全部成功才继续 → 用
Promise.all - 要拿到每个请求的完整结果(成功 + 失败) → 用
Promise.allSettled - 需要容错或兜底值 → 在每个 Promise 上加 catch
参考答案
1. 用 Promise.all 实现并发请求
Promise.all 的特性是:等待所有 Promise 全部成功,才会进入 .then;如果有一个失败,会立刻进入 .catch。
例子:
function fetchData(url) {
return fetch(url).then(res => res.json());
}
Promise.all([
fetchData('/api/user'),
fetchData('/api/order'),
fetchData('/api/message'),
]).then(([user, order, message]) => {
console.log('全部成功:', user, order, message);
}).catch(error => {
console.error('有请求失败:', error);
});
这样就能同时发起多个请求(并发),等结果全部回来再统一处理。
2. 如何处理 部分请求失败
方案 A: Promise.allSettled(推荐)
- 它不会短路,所有 Promise 都会执行完。
- 返回结果包含
status: "fulfilled"或"rejected"。
Promise.allSettled([
fetchData('/api/user'),
fetchData('/api/order'),
fetchData('/api/message'),
]).then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('成功:', result.value);
} else {
console.error('失败:', result.reason);
}
});
});
这样就能做到“部分成功也能拿到”。
方案 B: 手动封装 catch,让 Promise.all 不被短路
给每个请求加上 .catch,保证它不会抛错,而是返回一个标记:
function safeFetch(promise) {
return promise
.then(res => ({ status: 'fulfilled', value: res }))
.catch(err => ({ status: 'rejected', reason: err }));
}
Promise.all([
safeFetch(fetchData('/api/user')),
safeFetch(fetchData('/api/order')),
safeFetch(fetchData('/api/message')),
]).then(results => {
console.log(results);
});
输出结构和 allSettled 一样,但可以兼容旧浏览器(没有 allSettled 的情况)。
方案 C: 按需兜底(部分失败时用默认值)
比如请求失败时,给个默认数据继续用:
Promise.all([
fetchData('/api/user').catch(() => ({ name: '游客' })),
fetchData('/api/order').catch(() => []),
fetchData('/api/message').catch(() => []),
]).then(([user, order, message]) => {
console.log('即使失败也有默认值:', user, order, message);
});
8. 以下代码输出什么?解释原因
题库原题:以下代码输出什么?解释原因
const obj = {
objName: "百度",
print: () => console.log(this.objName),
};
obj.print();
题目要点
- 箭头函数没有自己的
this,它继承自定义时的上下文。 - 在对象中定义箭头函数作为方法时,
this不会指向该对象,而是外层作用域。 - 因此这段代码输出
undefined,而不是"百度"。
参考答案
这段代码的执行结果是:
undefined
原因解析
箭头函数的
this特性- 箭头函数不会创建自己的
this,它的this来自 词法作用域,即它定义时所在的上下文。 - 在这段代码里,
print是用箭头函数定义的,所以它的this不是obj,而是定义它时所在的环境。
- 箭头函数不会创建自己的
当前环境的
this这段代码运行在模块或脚本的顶层环境:
- 在严格模式下,顶层的
this是undefined。 - 在非严格模式下,顶层的
this是window(浏览器)或global(Node.js)。
- 在严格模式下,顶层的
不论是哪种情况,顶层对象上都没有
objName属性,因此this.objName是undefined。
为什么不是 “百度”
如果把
print定义为普通函数:print: function() { console.log(this.objName); }那么调用
obj.print()时,this会绑定到obj,输出就是"百度"。但是箭头函数丢失了这种绑定特性,它固定死了
this,因此取不到obj上的objName。
9. 如何通过bind或箭头函数固定this指向?
题目要点
| 特点 | bind | 箭头函数 |
|---|---|---|
| 绑定时机 | 调用 bind 时固定 | 定义时词法绑定 |
| 返回结果 | 返回一个新的函数 | 不生成新的 this |
| 使用场景 | 需要显式传递 this 或预设参数 | 需要保持外层作用域的 this |
| 是否可作为构造函数 | 可以 | 不可以 |
参考答案
一、问题背景
在 JavaScript 中,this 的指向不是在函数定义时确定的,而是 在函数执行时,由调用方式决定。
因此,很多情况下我们需要“固定”this,保证函数执行时内部的 this 始终指向期望的对象。
二、使用 bind 固定 this
Function.prototype.bind 会返回一个新的函数,无论后续如何调用,内部的 this 都会固定为指定对象。
示例
const obj = {
name: "Alice",
sayHi: function() {
console.log("Hi, I am " + this.name);
}
};
const fn = obj.sayHi;
fn(); // 输出: Hi, I am undefined (this丢失)
const boundFn = obj.sayHi.bind(obj);
boundFn(); // 输出: Hi, I am Alice
特点
bind返回一个新的函数,不会立即执行。绑定的
this一旦确定,无法再被更改。还可以 预设参数,形成偏函数。
function add(a, b) { return a + b; } const add5 = add.bind(null, 5); console.log(add5(3)); // 8
三、使用 箭头函数 固定 this
箭头函数没有自己的 this,它的 this 来自定义时所在的词法作用域。
示例
const obj = {
name: "Alice",
sayHi: function() {
const inner = () => {
console.log("Hi, I am " + this.name);
};
inner();
}
};
obj.sayHi(); // 输出: Hi, I am Alice
特点
- 箭头函数不会创建自己的
this,直接用外层函数的this。 - 不能用作构造函数(不能
new)。 - 常用于 回调函数 或 事件监听器,避免丢失
this。
10. 实现 Function.prototype.bind
题目要点
核心考查:实现 Function.prototype.bind的概念、原理与实际应用。
参考答案
Function.prototype.bind = function(context, ...args) {
if (typeof this !== 'function') {
throw new Error("Type Error");
}
// 保存this的值
var self = this;
return function F() {
// 考虑new的情况
if(this instanceof F) {
return new self(...args, ...arguments)
}
return self.apply(context, [...args, ...arguments])
}
}
11. 获取DOM元素的方法
题目要点
- 性能最佳:
getElementById - 灵活强大:
querySelector / querySelectorAll - 适合批量元素:
getElementsByClassName / getElementsByTagName
参考答案
1. 通过 ID 获取
const el = document.getElementById("myId");
- 返回单个元素(或
null)。 - 效率最高,推荐在唯一 ID 的情况下使用。
2. 通过 类名 获取
const els = document.getElementsByClassName("myClass");
- 返回 HTMLCollection(动态集合)。
- 可以通过下标访问,如
els[0]。
3. 通过 标签名 获取
const els = document.getElementsByTagName("div");
- 返回所有指定标签的元素集合。
4. 通过 选择器 获取
const el = document.querySelector(".myClass"); // 返回第一个匹配元素
const els = document.querySelectorAll(".myClass"); // 返回所有匹配元素(NodeList)
querySelector更加灵活,支持复杂选择器(如#id > .class span)。querySelectorAll返回 静态 NodeList,不会随 DOM 改变而更新。
5. 通过 表单元素的 name 属性 获取
const els = document.getElementsByName("username");
- 返回 NodeList,一般用于表单元素(如
<input name="username">)。
6. 特殊 DOM 获取方式
document.body获取<body>元素document.documentElement获取<html>根元素document.forms获取页面所有<form>document.images获取页面所有<img>document.links获取所有<a>(带href的)
7. 通过 父子关系 获取
const parent = document.getElementById("container");
const children = parent.children; // HTMLCollection 子元素
const first = parent.firstElementChild;
const last = parent.lastElementChild;
12. 在Vue中,为什么推荐使用ref而非直接操作DOM?
题目要点
在 Vue 中推荐使用 ref 而非直接操作 DOM,是为了 保持响应式编程思想、提高代码可维护性、避免全局污染、增强组件化支持,以及兼容 SSR 和未来扩展。
直接操作 DOM 只能作为「最后手段」,通常用于操作第三方库或必须访问底层 DOM 的场景。
参考答案
1. 保持响应式编程思想
- Vue 的核心理念是 数据驱动视图。
- 如果你频繁通过
document.querySelector、getElementById等方式直接操作 DOM,就等于绕过了 Vue 的响应式系统,破坏了「数据变化 → 视图自动更新」的逻辑。 ref是 Vue 提供的 在响应式体系内获取 DOM 或组件实例的方式,不会违背 Vue 的设计哲学。
2. 保证可维护性和可读性
- 使用
ref获取 DOM 元素或子组件时,逻辑清晰,绑定关系一目了然:
<template>
<input ref="inputEl" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
const inputEl = ref(null)
onMounted(() => {
inputEl.value.focus()
})
</script>
而如果用
document.querySelector('input'),就会出现以下问题:- 不直观,不知道选中的是不是自己想要的节点。
- 如果 DOM 结构变化,选择器很容易失效。
3. 避免全局作用域污染
- 原生 DOM 操作往往是「全局查询」的,比如
document.querySelector。 - 当页面很大时,可能会选中不该选的 DOM,造成隐蔽的 bug。
ref是 组件作用域内的绑定,不会误伤其它组件。
4. 更好地支持组件和生命周期
ref不仅能绑定 DOM,还能绑定 组件实例:
<ChildComponent ref="child" />
onMounted(() => {
console.log(child.value.someMethod())
})
- 这种方式比手动查找 DOM、再找对应组件要安全得多。
- 而且
ref在 Vue 的生命周期中是有保证的(例如在onMounted之后才能访问),不会出现 DOM 还没挂载就被访问的情况。
5. 利于服务端渲染(SSR)和未来扩展
- Vue 在 SSR 或虚拟 DOM 渲染时,不依赖真实 DOM,而是依赖
ref提供的抽象。 - 直接操作 DOM 会导致 不可移植,在非浏览器环境下不可用,而
ref更加通用。
13. Vue2中v-model的实现原理
题库原题:Vue2中v-model的实现原理
题目要点
本质:
v-model= 数据绑定 + 事件监听(语法糖)核心机制:
Object.defineProperty劫持数据,实现响应式- 指令编译器绑定
value/checked和事件 - Watcher/Dep 通知视图更新
在组件上:约定
value+input事件即可实现双向绑定
参考答案
在 Vue 2 中,v-model 的本质是 语法糖,它本质上是对 value(或 checked)属性绑定 + input 事件监听 的封装。理解它的实现原理,需要从 指令编译机制 和 双向绑定逻辑 入手。
1. v-model 的核心原理
假设在模板中写:
<input v-model="msg">
编译后的大致效果等价于:
<input
:value="msg"
@input="msg = $event.target.value"
/>
分析:
:value="msg"- 将 Vue 实例中的
msg属性绑定到 input 的 value 上。 - 当
msg发生变化时,视图自动更新(单向绑定)。
- 将 Vue 实例中的
@input="msg = $event.target.value"- 监听用户输入事件,将 input 的值同步回 Vue 实例的数据。
- 完成从视图到数据的更新,实现双向绑定。
2. 内部实现机制
2.1 数据劫持(双向绑定)
- Vue 2 利用
Object.defineProperty对data中的属性进行 getter/setter 劫持。 - 当数据发生变化时,会触发 Watcher 更新视图。
- 当用户输入触发
input事件时,通过事件回调修改数据,setter 被触发,视图更新。
2.2 指令编译
Vue 编译模板时,会检测到
v-model指令:- 对 文本输入框 (
input[type=text]),绑定value+input - 对 复选框 (
input[type=checkbox]),绑定checked+change - 对 单选框 (
input[type=radio]),绑定checked+change - 对
<select>,绑定value+change
- 对 文本输入框 (
这些规则都是在 Vue 的
v-model指令编译器里实现的。
2.3 Watcher 和 Dep
- 每个绑定的数据属性都有一个 依赖收集器(Dep)
- 当数据被访问时,收集依赖(Watcher)
- 当数据被修改时,Dep 通知所有 Watcher 更新视图
3. 自定义组件上的 v-model
在 Vue 2 中,v-model 也可以用于组件。原理是:
<my-input v-model="msg" />
- 编译后等价于:
<my-input
:value="msg"
@input="msg = $event"
/>
组件需要:
- 接收
value属性 - 在内部触发
this.$emit('input', newValue)
- 接收
Vue 通过这个约定,实现组件上的双向绑定。
14. Vue3中v-model的改进是什么?如何用defineModel简化代码?
题目要点
Vue 3 改进点:
- 支持多
v-model(v-model:propName) - 不再固定
value,绑定任意 prop - 基于 Proxy 的响应式系统
- 支持多
defineModel 优势:
- 减少重复代码(无需手动定义 props/emits)
- 直接操作变量即可触发双向绑定
- 与
<script setup>组合式 API 自然结合
参考答案
在 Vue 3 中,v-model 做了显著改进,相比 Vue 2,更加灵活和可配置,同时减少了模板中重复代码。
1. Vue 3 中 v-model 的改进
1.1 支持 多 v-model
- Vue 2 组件只能通过单个
v-model绑定value+input。 - Vue 3 支持给组件定义多个
v-model,可以绑定不同的属性和事件。
<MyComponent
v-model:title="pageTitle"
v-model:count="itemCount"
/>
对应组件内部:
defineProps(['title', 'count'])
const emit = defineEmits(['update:title', 'update:count'])
function updateTitle(newTitle) {
emit('update:title', newTitle)
}
1.2 指定绑定的 prop 名称
- Vue 3 不再固定
value属性,v-model:propName可以绑定任意 prop,并生成update:propName事件。 - 语法更直观,避免只能用
value的局限。
2. v-model 的原理变化
Vue 3 不再依赖
Object.defineProperty,而是用 Proxy 实现响应式。v-model的双向绑定原理依然是:- 数据 → 视图:绑定 prop
- 视图 → 数据:通过触发
update:propName事件,父组件更新数据
<!-- 父组件 -->
<MyInput v-model="msg" />
<!-- 子组件 -->
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
function onInput(e) {
emit('update:modelValue', e.target.value)
}
</script>
3. 使用 defineModel 简化代码
defineModel 是 Vue 3.3+ 提供的 组合式 API,用于在 <script setup> 中声明 v-model 绑定属性和事件,减少手动编写 defineProps + defineEmits 的模板。
示例
传统方式
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
function onInput(e) {
emit('update:modelValue', e.target.value)
}
</script>
使用 defineModel
<script setup>
const modelValue = defineModel() // 自动生成 props + emits
function onInput(e) {
modelValue.value = e.target.value // 自动触发 update:modelValue
}
</script>
特点
- 自动生成
props+emits,简化模板和代码 - 可直接使用
ref风格修改值,内部会触发update:modelValue - 支持默认值、类型声明和验证
15. 为什么Vue要求在使用v-for时提供唯一key?不设置key会导致什么问题?
题目要点
key 是虚拟 DOM 更新的标识
- 唯一且稳定的
key能保证节点正确复用
- 唯一且稳定的
未设置或使用索引作为 key 的风险
- 数据复用错误 → 组件状态错乱
- 性能下降 → DOM 被无谓销毁和重建
最佳实践
- 优先使用数据唯一标识(如
id)作为key - 仅当列表项是静态、不会变更时,索引可用作临时 key
- 优先使用数据唯一标识(如
参考答案
在 Vue 中,v-for 循环列表时要求提供唯一 key,这是因为 key 用于 虚拟 DOM 的差异比较和高效更新,直接影响渲染性能和正确性。
1. key 的作用
标识每个节点的唯一性
- Vue 在渲染列表时,会对比前后两次虚拟 DOM(VNode)树。
key告诉 Vue:这个节点对应同一个数据项,能安全复用 DOM 元素,而不是全部销毁重建。
优化 DOM 更新
- 当列表项顺序发生变化或新增/删除时,Vue 使用
key来 最小化 DOM 操作,只移动或更新真正变化的节点。 - 如果没有
key,Vue 会采用默认的 就地复用(index 对应复用),可能导致不必要的 DOM 更新。
- 当列表项顺序发生变化或新增/删除时,Vue 使用
2. 不设置 key 的问题
假设有一个列表:
<div v-for="(item, index) in items" :key="index">
{{ item.text }}
</div>
问题示例
节点复用错误
如果列表顺序变化或删除/插入元素,Vue 会根据索引复用 DOM,而不是对应数据。
结果可能出现:
- 输入框内容被错误复用
- 组件状态(如折叠、选中)被错误继承
性能浪费
- 没有
key时,Vue 可能需要 销毁并重建整个列表,而不是只移动或更新必要节点。 - 大列表更新时,会明显卡顿。
- 没有
示例
<template>
<div v-for="(item, index) in items" :key="index">
<input v-model="item.text" />
</div>
</template>
操作:
- 删除列表中间一项
- 结果:后面的输入框值可能错乱,因为 Vue 用索引复用 DOM,而不是复用对应的数据项
正确做法:
<div v-for="item in items" :key="item.id">
<input v-model="item.text" />
</div>
item.id唯一标识每个元素- Vue 能准确复用 DOM,输入框内容不会错乱
16. 如何避免用数组索引作为key?
题目要点
首选数据自带唯一 ID 作为 key
没有 ID 时,初始化时生成唯一标识
不要用数组索引,除非列表完全静态
稳定且唯一的 key 可以保证:
- 虚拟 DOM 高效更新
- 表单或组件状态不会错乱
参考答案
在 Vue 中,避免用数组索引作为 key,主要是为了 确保虚拟 DOM 正确复用节点,避免状态错乱。
1. 使用数据唯一标识
- 每条数据如果有 唯一 ID 或其他天然唯一字段,应当直接使用它作为
key。
<template>
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
</template>
优点:
- 节点复用准确,DOM 更新最小化
- 表单或组件状态不会错乱
2. 生成唯一标识(没有 id 时)
如果数据本身没有唯一 ID,可以在获取或初始化数据时生成唯一标识,例如:
2.1 使用 uuid 或类似库
import { v4 as uuidv4 } from 'uuid'
items.forEach(item => {
item._uid = uuidv4()
})
模板中:
<div v-for="item in items" :key="item._uid">
{{ item.name }}
</div>
2.2 使用组合索引和内容(谨慎)
<div v-for="item in items" :key="item.name + item.type">
{{ item.name }}
</div>
- 适合数据内容天然唯一的场景
- 不保证稳定性,如果内容变化,仍可能触发 DOM 重建
3. 避免使用索引的场景
当列表可能 增删或排序 时,绝对不能使用索引,否则会导致:
- 输入框值错乱
- 组件状态错位
只有列表是 静态且不会变化 时,索引作为 key 才可接受。
17. Vue2中为什么需要$set方法?如何用它解决对象新增属性的响应性问题?
题目要点
- 问题来源:Vue 2 的响应式依赖
Object.defineProperty,只能劫持已有属性 - 解决方案:
$set/Vue.set - 作用:新增属性或修改数组索引时,保证响应式生效并更新视图
- 使用场景:对象动态添加字段、数组通过索引修改元素
参考答案
在 Vue 2 中,需要 $set 的核心原因是 对象属性的新增在 Vue 2 中无法被自动侦测,即响应式系统的局限性。
1. Vue 2 响应式原理回顾
- Vue 2 使用
Object.defineProperty对data对象的每个属性进行 getter/setter 劫持,实现响应式。 - 但 只能劫持已存在的属性。
- 因此,对对象新增属性时,Vue 无法侦测到变化,也无法触发视图更新。
示例:
data() {
return {
user: { name: 'Alice' }
}
},
mounted() {
this.user.age = 18 // ❌ 无法触发视图更新
}
2. $set 的作用
Vue 提供
$set(或全局Vue.set)来显式添加响应式属性。它的作用是:
- 在对象或数组上新增属性
- 保证该属性是 响应式的
- 自动触发视图更新
示例
this.$set(this.user, 'age', 18) // ✅ 响应式,视图会更新
等价于:
Vue.set(this.user, 'age', 18)
3. $set 对数组的作用
- Vue 2 数组在某些操作(如通过索引直接赋值
arr[index] = value)也不会触发视图更新。 - 通过
$set可以解决:
this.$set(this.items, 1, 'newValue') // 正确更新视图
4. 使用场景总结
- 对象新增属性
this.$set(obj, 'newProp', value)
- 数组通过索引修改元素
this.$set(arr, index, newValue)
- 保证响应式系统能够侦测到变化
- 不用
$set,新增或修改可能导致视图不更新
5. Vue 3 的变化
- Vue 3 使用 Proxy 实现响应式,不再存在
$set的限制 - 对象新增属性、数组索引赋值都可以自动触发视图更新
18. Vue3的响应式方案如何规避这一问题?
题目要点
问题来源:Vue 2 只能劫持已有属性,新增属性不响应
Vue 3 解决方案:
- 使用 Proxy 拦截对象和数组所有操作
- 新增属性或修改数组索引都能触发视图更新
优势:
- 简化开发,无需
$set - 兼容动态添加字段或动态数组操作
- 更稳定、更高效的响应式系统
- 简化开发,无需
参考答案
在 Vue 3 中,对象新增属性不再需要 $set,这是因为 Vue 3 彻底重写了响应式系统,从 Object.defineProperty 改为 Proxy,从根本上解决了 Vue 2 的局限性。
1. Vue 3 响应式原理
- Vue 3 使用
Proxy包裹对象或数组,实现响应式:
const state = reactive({ user: { name: 'Alice' } })
Proxy 拦截所有操作,包括:
- 读取属性 (
get) - 设置属性 (
set) - 删除属性 (
deleteProperty) - 数组索引修改
- 读取属性 (
因为
set拦截器会捕获 新增属性,所以无需像 Vue 2 那样提前定义或使用$set。
2. 对象新增属性也能响应式
示例
import { reactive } from 'vue'
const state = reactive({ user: { name: 'Alice' } })
state.user.age = 18 // 自动响应,视图会更新
- 这里直接给
user对象新增age属性,视图会立即响应更新。 - 不再需要
Vue.set或$set。
3. 数组索引修改也能响应式
const arr = reactive([1, 2, 3])
arr[1] = 20 // 自动触发更新
arr.push(4) // 同样响应式
- Proxy 拦截数组的
set和常用方法(push,pop,splice等),保证响应式更新。
19. 小程序如何实现rpx到px的转换?如何适配不同屏幕密度的设备(如Retina屏)?
题目要点
- rpx → px:根据屏幕宽度按比例转换
- 跨屏适配:rpx 自动适配宽度,但涉及绘制或图片时需考虑
devicePixelRatio - 优势:使用 rpx 可以让布局在各种屏幕上保持一致,开发成本低
参考答案
在小程序开发中,为了实现 界面自适应,官方提供了 rpx 单位,而最终在渲染时需要转换为实际像素(px),同时需要兼顾不同屏幕密度的设备(如 Retina 屏)。
1. rpx 的概念
rpx(responsive pixel)是微信小程序特有的单位。设计初衷:统一不同屏幕宽度的布局,使页面在各种设备上都能按比例显示。
转换规则:
- 以 iPhone 6/7/8 宽度 750rpx 作为设计稿参考宽度
- 小程序会根据设备屏幕宽度自动计算比例
示例:
- 设备屏幕宽度:375px
- 1rpx = 375 / 750 = 0.5px
2. rpx → px 转换公式
px = rpx * (屏幕宽度 / 750)
小程序官方 API
const systemInfo = wx.getSystemInfoSync()
const screenWidth = systemInfo.screenWidth // 单位 px
function rpxToPx(rpx) {
return rpx * screenWidth / 750
}
- 可以在 JS 中动态计算尺寸
- 结合
wx.createSelectorQuery()获取元素实际宽度,实现动态布局
3. 适配高密度屏(Retina、2K、4K 屏)
Retina 屏 的特征是物理像素比(
devicePixelRatio)大于 1微信小程序渲染引擎会自动处理 设备像素比,所以在大部分情况下:
- 使用 rpx → px 转换后,页面显示正常
- 不必手动乘
devicePixelRatio
如果涉及 Canvas 绘制或图片显示:
- 需要手动乘上
devicePixelRatio,保证在高分屏上清晰
- 需要手动乘上
const dpr = wx.getSystemInfoSync().pixelRatio
const canvasWidth = rpxToPx(300) * dpr
const canvasHeight = rpxToPx(200) * dpr
- 这样绘制的 Canvas 在 Retina 屏上不会模糊
4. 实践建议
布局元素:
- 使用
rpx单位,自动适配不同屏幕宽度 - 避免直接使用 px
- 使用
图片和 Canvas:
- 对图片可用多分辨率资源(@2x、@3x)
- 对 Canvas 绘制手动乘
devicePixelRatio
字体适配:
- 小程序的
rpx也可以用在font-size,保证文字大小随屏幕缩放
- 小程序的
20. 如何在小程序中实现一套代码兼容Web和移动端?
题目要点
- 选择跨端框架(Taro/Uni-app)统一开发语言和组件
- 封装跨平台 API,屏蔽 wx/uni/fetch 差异
- 条件编译解决平台差异渲染
- 样式单位适配,rpx 或 rem/vw
- 组件复用与封装,针对不同平台做必要分支
参考答案
在小程序开发中,如果希望 同一套代码既能运行在小程序环境,又能运行在 Web/移动端浏览器,核心思路是 跨端抽象、条件编译和适配平台 API。
1. 使用跨端框架
1.1 Taro
Taro 是凹凸实验室开源的 多端统一开发框架
核心特点:
- 编写 React/Vue 风格的组件
- 支持编译到:微信小程序、支付宝小程序、字节小程序、H5(Web)、React Native
- 提供跨端 API 封装(网络请求、存储、路由等)
示例:
import Taro from '@tarojs/taro'
Taro.request({ url: '/api/data' }).then(res => console.log(res))
- 编译后会自动替换为小程序
wx.request或浏览器fetch
1.2 Uni-app
- 使用 Vue 语法,支持多端编译
- 提供
uni全局 API,统一封装跨端差异
2. 条件编译
在小程序和 Web 环境中,有些 API 或组件差异明显,需要使用条件编译:
<template>
<!-- #ifdef H5 -->
<div>这是 Web 端内容</div>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view>这是微信小程序端内容</view>
<!-- #endif -->
</template>
- 可针对不同平台渲染不同元素
- 在 JS 中也可以判断:
if (process.env.TARO_ENV === 'h5') {
console.log('Web 环境')
} else if (process.env.TARO_ENV === 'weapp') {
console.log('小程序环境')
}
3. 封装跨平台 API
- 对于网络请求、存储、路由等操作,建议统一封装接口,屏蔽平台差异:
// api.js
import Taro from '@tarojs/taro'
export function request(url, options) {
if (process.env.TARO_ENV === 'h5') {
return fetch(url, options).then(res => res.json())
} else {
return Taro.request({ url, ...options })
}
}
- 上层组件无需关心当前平台
4. 样式适配
使用 响应式单位:
- rpx 在小程序端自动适配屏幕
- H5 可使用 vw/vh 或 rem 替代
可以通过 预处理器 或 跨端框架 做统一转换
5. 组件适配
- 公共组件尽量用跨端框架提供的组件库
- 针对特定平台差异,使用条件编译或封装多版本组件
<template>
<common-button v-if="isWeapp" />
<button v-else>Web按钮</button>
</template>
21. 项目中如何选择路由模式?如何解决history模式的404问题?
题目要点
Hash 模式:简单、兼容性好,无需服务器配置
History 模式:
- URL 更美观,SEO 更友好
- 需要服务器 URL 重写 以避免刷新 404
选择依据:
- 服务器可控且 SEO 重要 → History
- 部署在静态服务器或兼容性要求高 → Hash
参考答案
Vue 或 React 等 SPA 项目通常有两种路由模式:hash 模式 和 history 模式。
1. 路由模式对比
| 特性 | Hash 模式 | History 模式 |
|---|---|---|
| URL 形式 | /#/home | /home |
| 页面刷新 | 无需服务器配置,直接刷新不会报 404 | 直接刷新需要服务器支持,否则会 404 |
| SEO | 不友好,锚点形式 | 相对友好,可配合服务端渲染 |
| 浏览器支持 | 所有现代浏览器 | 现代浏览器支持(IE9+) |
选择建议
Hash 模式:
- 项目部署在 静态服务器,无法修改服务器配置
- 对 SEO 要求不高
- 使用简单、兼容性好
History 模式:
- 项目对 SEO 有要求
- 可在服务器做 URL 重写
- URL 美观,无
#
2. History 模式的 404 问题
- SPA 项目在 History 模式下,页面刷新或直接访问
/about时,浏览器向服务器请求该路径。 - 如果服务器没有配置 URL 重写,返回的不是 index.html,而是 404。
解决方法
2.1 Nginx 配置
server {
listen 80;
server_name example.com;
root /usr/share/nginx/html;
location / {
try_files $uri /index.html;
}
}
try_files $uri /index.html;会优先查找静态资源,如果不存在,则返回index.html- SPA 前端接管路由,实现页面渲染
2.2 Apache 配置(.htaccess)
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
2.3 静态托管(如 GitHub Pages / Netlify)
- GitHub Pages: 在项目根目录添加
404.html并重定向到index.html - Netlify: 配置
_redirects文件
/* /index.html 200
</details>
---
← 已是第一轮 · [返回本次面经](/LastStand/posts/frontend/interview-experiences/experience-73/) · 已是最后一轮 →