什么是 JS 闭包?
JS 闭包是 JavaScript 中一种特殊的函数现象,指当一个内部函数引用了外部函数的变量或参数,且内部函数在外部函数作用域之外被调用时,外部函数的作用域不会被销毁,内部函数依然能访问这些变量的机制。
闭包的形成需要满足三个条件:一是存在函数嵌套(内部函数定义在外部函数内部);二是内部函数引用了外部函数的变量或参数;三是外部函数被调用后,内部函数被返回或在其他作用域中被使用。例如:
function outer() {
let count = 0;
return function inner() {
count++;
return count;
};
}
const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
这里,inner
函数引用了 outer
函数的 count
变量,outer
调用后返回 inner
并赋值给 counter
,即使 outer
执行完毕,count
仍能被 counter
访问,形成闭包。
闭包的核心作用包括:实现变量私有化(避免全局污染)、保存函数状态(如计数器)、模块化开发(通过闭包封装私有方法和变量)。常见应用场景有防抖节流、函数柯里化、模块化工具(如早期没有 export
时,用闭包暴露接口)。
但闭包也可能引发问题:由于外部函数作用域未被销毁,过度使用会导致内存占用增加,甚至内存泄漏(需手动将引用设为 null
释放)。
关键点:闭包本质是作用域的保留,而非“函数包裹函数”;其核心价值是突破作用域限制访问变量。
面试加分点:能结合实际场景说明闭包的应用(如防抖函数实现),并指出内存管理注意事项。
记忆法:用“嵌套引用未释放”记忆——函数嵌套+内部引用外部变量+外部函数执行后内部函数仍可访问,三者缺一不可。
事件捕获和事件冒泡的区别是什么?
事件捕获和事件冒泡是 DOM 事件流中两个相反的传播阶段,共同构成了事件从触发到处理的完整流程(DOM2 级事件流包括:事件捕获阶段→目标阶段→事件冒泡阶段)。
事件捕获指事件从文档根节点(document
)向目标元素传播的过程,即“自上而下”触发。例如,点击一个嵌套在 div
中的 button
,捕获阶段事件会依次触发 document
→html
→body
→div
→button
。其作用是让上层元素提前捕获事件,可用于拦截事件(如阻止事件到达目标元素)。
事件冒泡指事件从目标元素向文档根节点传播的过程,即“自下而上”触发。同样点击上述 button
,冒泡阶段事件会依次触发 button
→div
→body
→html
→document
。其作用是让多个元素共享事件处理逻辑(如事件委托)。
两者的核心区别如下:
维度 | 事件捕获 | 事件冒泡 |
---|---|---|
传播方向 | 从根节点到目标元素 | 从目标元素到根节点 |
执行顺序 | 先于冒泡阶段执行 | 后于捕获阶段执行 |
绑定方式 | addEventListener 第三个参数为 true | addEventListener 第三个参数为 false (默认) |
典型应用 | 事件拦截、提前处理 | 事件委托、批量处理 |
代码示例:
<div id="parent">
<button id="child">点击</button>
</div>
<script>
const parent = document.getElementById('parent');
const child = document.getElementById('child');
// 捕获阶段触发
parent.addEventListener('click', () => console.log('parent 捕获'), true);
// 冒泡阶段触发
parent.addEventListener('click', () => console.log('parent 冒泡'), false);
child.addEventListener('click', () => console.log('child 点击'));
</script>
点击按钮后,输出顺序为:parent 捕获
→child 点击
→parent 冒泡
。
关键点:事件流的三个阶段不可分割,捕获和冒泡是传播方向的相反过程;目标元素本身的事件不分捕获和冒泡,触发时机相同。
面试加分点:能说明 stopPropagation()
可阻止事件继续传播(同时阻止捕获和冒泡),stopImmediatePropagation()
还能阻止同一元素后续事件触发。
记忆法:用“捕获从上到下,冒泡从下到上”记忆方向,结合“先捕获后冒泡”记忆执行顺序。
如何实现数组去重(尽量最优解)?
数组去重是前端常见需求,需根据场景选择合适方法,以下是多种实现方式及最优解分析:
-
利用 Set 数据结构
ES6 新增的Set
是无序且唯一的集合,可直接将数组转为Set
再转回数组,一行代码实现去重:const unique = arr => [...new Set(arr)];
优点:简洁高效(时间复杂度 O(n)),能处理基本数据类型(Number、String、Boolean 等);缺点:无法去重对象(因对象引用不同),不支持 IE11 及以下。
-
filter + indexOf
利用filter
筛选元素,保留数组中首次出现的元素:const unique = arr => arr.filter((item, index) => arr.indexOf(item) === index);
原理:
indexOf
返回元素首次出现的索引,若与当前索引一致则保留。优点:兼容性好;缺点:时间复杂度 O(n²)(indexOf
需遍历数组),对NaN
不友好(indexOf
无法识别NaN
)。 -
reduce + 对象键值对
用对象存储已出现的元素,避免重复:const unique = arr => { const obj = {}; return arr.reduce((prev, cur) => { if (!obj[cur]) { obj[cur] = true; prev.push(cur); } return prev; }, []); };
优点:时间复杂度 O(n),效率高;缺点:会将所有元素转为字符串(如
1
和'1'
被视为相同),需额外处理类型差异。 -
Map 数据结构
类似Set
,但可保留插入顺序,且能区分undefined
和null
:const unique = arr => { const map = new Map(); return arr.filter(item => !map.has(item) && map.set(item, true)); };
优点:支持
NaN
去重(Map
视NaN
为相等),保留顺序;缺点:较Set
代码稍长。
最优解:在大多数现代浏览器环境中,Set
方法是最优解——代码简洁、效率高(O(n)),且能处理大部分基本类型去重场景。若需兼容旧环境或处理复杂类型(如对象),需结合 JSON.stringify
或自定义比较函数,但会增加复杂度。
关键点:去重方法的选择需考虑数据类型(基本类型/引用类型)、兼容性、效率要求。
面试加分点:能分析不同方法的时间复杂度,说明 Set
为何高效(基于哈希表实现),并指出对象去重的特殊处理方式。
记忆法:用“Set 最简洁,对象去重需类型”记忆——Set
是基本类型去重的首选,处理对象时需注意类型和引用的差异。
给定三个数 1、2、5,找出和为 100 的所有组合
该问题本质是求解非负整数解的组合问题,设 1、2、5 的个数分别为 x、y、z,需满足方程:x + 2y + 5z = 100(x, y, z ≥ 0 且为整数)。
求解思路:通过固定较大数的个数,逐步推导较小数的可能值,减少变量维度。具体步骤如下:
- 确定 z 的取值范围:5z ≤ 100 → z 最大为 20(5×20=100),故 z 可取值 0 到 20。
- 对每个 z,确定 y 的取值范围:当 z 固定后,剩余和为 100 - 5z,此时 2y ≤ 100 - 5z → y 最大为 Math.floor((100 - 5z)/2),y 可取值 0 到该最大值。
- 计算 x 的值:x = 100 - 5z - 2y,因 x 非负,无需额外限制。
据此,可通过嵌套循环实现所有组合的枚举:
function findCombinations() {
const combinations = [];
// z 从 0 到 20
for (let z = 0; 5 * z <= 100; z++) {
const remaining = 100 - 5 * z;
// y 从 0 到 remaining/2 的整数部分
for (let y = 0; 2 * y <= remaining; y++) {
const x = remaining - 2 * y;
combinations.push({ x, y, z });
}
}
return combinations;
}
// 示例:打印前 3 组和最后 1 组
const result = findCombinations();
console.log(result.slice(0, 3)); // [{x:100,y:0,z:0}, {x:98,y:1,z:0}, {x:96,y:2,z:0}]
console.log(result[result.length - 1]); // {x:0,y:0,z:20}
上述代码中,外层循环遍历 z 的可能值,内层循环遍历 y 的可能值,最后计算 x,确保所有组合满足和为 100。
关键点:通过固定较大数(5)的个数减少变量,降低问题复杂度;利用数学关系确定变量范围,避免无效计算。
面试加分点:能说明算法时间复杂度为 O(z_max × y_max),其中 z_max=20,y_max 随 z 减小而增大,最坏情况约为 O(20×50)=O(1000),效率极高;还可优化为仅计算非零组合(如排除 x=0,y=0,z=20 等)。
记忆法:用“先定大数 z,再算中数 y,最后求 x”记忆——按数值从大到小确定变量,逐步简化计算。
遍历对象有哪些方法?
JavaScript 中遍历对象的方法多样,各有不同的适用场景,主要分为以下几类:
-
for...in 循环
遍历对象自身及原型链上的可枚举属性(不包括 Symbol 属性):const obj = { a: 1, b: 2 }; for (const key in obj) { // 通常需用 hasOwnProperty 过滤原型链属性 if (obj.hasOwnProperty(key)) { console.log(key, obj[key]); // a 1, b 2 } }
特点:兼容性好,但需手动排除原型属性,不适合遍历 Symbol 键。
-
Object.keys()
返回对象自身可枚举属性的键名数组(不包括原型链和 Symbol 属性):const keys = Object.keys(obj); // ['a', 'b'] keys.forEach(key => console.log(key, obj[key]));
特点:只遍历自身属性,返回数组便于使用数组方法(如
forEach
)。 -
Object.values()
返回对象自身可枚举属性的键值数组(与Object.keys()
对应):const values = Object.values(obj); // [1, 2] values.forEach(value => console.log(value));
特点:直接获取值,无需通过键名访问。
-
Object.entries()
返回对象自身可枚举属性的 [键名, 键值] 数组:const entries = Object.entries(obj); // [['a', 1], ['b', 2]] entries.forEach(([key, value]) => console.log(key, value));
特点:适合需要同时使用键和值的场景,可配合
Map
转换(new Map(entries)
)。 -
Object.getOwnPropertyNames()
返回对象自身所有属性的键名数组(包括不可枚举属性,但不包括 Symbol 属性):const obj = { a: 1 }; Object.defineProperty(obj, 'b', { value: 2, enumerable: false }); Object.getOwnPropertyNames(obj); // ['a', 'b']
特点:能获取不可枚举属性(如
length
对数组),但仍排除 Symbol 键。 -
Reflect.ownKeys()
返回对象自身所有属性的键名数组(包括可枚举、不可枚举、Symbol 属性,不包括原型链):const sym = Symbol('c'); const obj = { a: 1, [sym]: 3 }; Object.defineProperty(obj, 'b', { value: 2, enumerable: false }); Reflect.ownKeys(obj); // ['a', 'b', Symbol(c)]
特点:最全面的自身属性遍历方法,包含所有类型的键。
不同方法的核心差异如下:
方法 | 遍历范围 | 包含不可枚举 | 包含 Symbol | 包含原型链 |
---|---|---|---|---|
for...in | 自身+原型链 | 否 | 否 | 是 |
Object.keys() | 自身 | 否 | 否 | 否 |
Object.values() | 自身 | 否 | 否 | 否 |
Object.entries() | 自身 | 否 | 否 | 否 |
Object.getOwnPropertyNames() | 自身 | 是 | 否 | 否 |
Reflect.ownKeys() | 自身 | 是 | 是 | 否 |
关键点:区分“自身属性”与“原型链属性”、“可枚举”与“不可枚举”、“字符串键”与“Symbol 键”是选择遍历方法的核心依据。
面试加分点:能说明 for...in
遍历数组的风险(会遍历数组的非索引属性),以及 Reflect.ownKeys()
在 Proxy 拦截中的应用。
记忆法:用“forin 含原型,keys 值 entries,ownKeys 全属性”记忆——for...in
包含原型链,keys/values/entries
处理可枚举自身属性,ownKeys
涵盖所有自身属性。
原型链上的属性能否被遍历到?如果能,如何做到不遍历原型链上的属性;如果不能,请说明原因。
原型链上的属性能被部分遍历方法遍历到,具体取决于遍历方式。在JavaScript中,对象的属性分为自身属性和原型链上的继承属性,而遍历是否包含原型链属性,由遍历方法的特性决定。
最典型的会遍历原型链属性的方法是for...in
循环。for...in
的设计初衷是遍历对象的“可枚举属性”,包括对象自身的可枚举属性和其原型链上所有可枚举属性。例如:
function Person() {
this.name = '张三';
}
Person.prototype.age = 20; // 原型链上的可枚举属性
const p = new Person();
for (const key in p) {
console.log(key); // 输出 'name'、'age'(包含原型链属性)
}
这里age
是原型链上的属性,被for...in
遍历到了。
但并非所有遍历方法都会遍历原型链。例如Object.keys()
、Object.values()
、Object.entries()
仅遍历对象自身的可枚举属性,不会涉及原型链;Object.getOwnPropertyNames()
则遍历对象自身的所有属性(包括不可枚举属性,但不包括Symbol属性),同样不涉及原型链。
若使用for...in
时想不遍历原型链上的属性,需配合hasOwnProperty()
方法过滤。hasOwnProperty()
是对象自身的方法,用于判断某个属性是否为对象自身的属性(而非原型链继承的)。修改上面的例子:
for (const key in p) {
if (p.hasOwnProperty(key)) { // 仅保留自身属性
console.log(key); // 只输出 'name'
}
}
关键点与面试加分点:
- 明确
for...in
是唯一会遍历原型链可枚举属性的常见方法,其他如Object.keys()
等仅遍历自身属性。 - 解释
hasOwnProperty()
的作用时,可提到它本身也是原型链上的方法(继承自Object.prototype
),但不会被for...in
遍历(因为它是不可枚举的)。 - 若能举例说明“可枚举性”对遍历的影响(如
Object.defineProperty
定义不可枚举属性时,for...in
和Object.keys()
都不会遍历),会更加分。
记忆法:可记为“for...in
链上走,hasOwn
挡门口”——for...in
会遍历原型链,用hasOwnProperty
可过滤掉原型链属性。
对 JS 深浅拷贝的理解是什么?具体有哪些实现方法?
JavaScript中,深浅拷贝的核心区别在于是否复制引用类型的深层结构。
- 浅拷贝:仅复制对象的表层属性。如果属性是基本类型(如数字、字符串),会复制其值;但如果属性是引用类型(如对象、数组),则仅复制引用地址,新旧对象的该属性会指向同一块内存。因此,修改其中一个对象的深层引用类型属性,会影响另一个对象。
- 深拷贝:完全复制对象的所有层级结构。无论是基本类型还是引用类型,都会创建独立的副本,新旧对象互不影响,引用类型属性指向不同的内存地址。
浅拷贝的实现方法:
- Object.assign():用于将源对象的可枚举属性复制到目标对象,仅拷贝表层。
const obj = { a: 1, b: { c: 2 } }; const shallowCopy = Object.assign({}, obj); shallowCopy.b.c = 3; console.log(obj.b.c); // 输出 3(深层引用被修改)
- 扩展运算符(...):数组和对象都可使用,原理与
Object.assign()
类似。const arr = [1, [2, 3]]; const shallowCopyArr = [...arr]; shallowCopyArr[1][0] = 4; console.log(arr[1][0]); // 输出 4(深层数组被修改)
- 数组的slice()和concat():这两个方法返回新数组,但仅对表层元素复制,深层引用类型仍共享。
深拷贝的实现方法:
- JSON.parse(JSON.stringify()):通过将对象转为JSON字符串再解析,实现深拷贝。但存在局限性:无法复制函数、Symbol、循环引用的对象,且会忽略
undefined
和不可枚举属性。const obj = { a: 1, b: { c: 2 }, fn: () => {} }; const deepCopy = JSON.parse(JSON.stringify(obj)); console.log(deepCopy.fn); // 输出 undefined(函数被忽略)
- 递归实现:手动遍历对象,对基本类型直接复制,对引用类型递归调用拷贝方法,可处理函数、循环引用等场景(需额外处理循环引用)。
function deepClone(obj, map = new Map()) { if (obj === null || typeof obj !== 'object') return obj; if (map.has(obj)) return map.get(obj); // 处理循环引用 let cloneObj = Array.isArray(obj) ? [] : {}; map.set(obj, cloneObj); for (const key in obj) { if (obj.hasOwnProperty(key)) { cloneObj[key] = deepClone(obj[key], map); // 递归拷贝 } } return cloneObj; }
- 第三方库:如lodash的
_.cloneDeep()
,成熟且能处理各种边缘情况,实际开发中常用。
关键点与面试加分点:
- 清晰区分深浅拷贝的核心差异(是否复制深层引用类型)。
- 说明各方法的局限性(如
JSON
方法不支持函数,递归需处理循环引用)。 - 结合使用场景分析:浅拷贝适用于简单结构且无需修改深层数据的场景;深拷贝适用于需要完全独立数据的场景(如状态管理中的对象复制)。
记忆法:可记为“浅拷皮,深拷骨”——浅拷贝只复制表面结构,深拷贝复制所有层级(包括“骨头”般的深层结构)。
在 switch 语句中,当 case 1 命中后,还会进入下一个 case 吗?
在switch语句中,当case 1
命中后,默认会继续进入下一个case,这种现象称为“case穿透”。其原因是switch语句采用“fall through”(贯穿)机制:一旦某个case条件匹配,会执行该case对应的代码块,且在遇到break
语句或switch语句结束前,会继续执行后续case的代码,无论后续case条件是否匹配。
例如:
const num = 1;
switch (num) {
case 1:
console.log('命中case 1');
case 2:
console.log('进入case 2');
default:
console.log('进入default');
}
// 输出:
// 命中case 1
// 进入case 2
// 进入default
这里case 1
命中后,由于没有break
,程序继续执行case 2
和default
的代码。
若要避免穿透,需在case代码块末尾添加break
语句,强制退出switch结构:
switch (num) {
case 1:
console.log('命中case 1');
break; // 终止后续执行
case 2:
console.log('进入case 2');
break;
default:
console.log('进入default');
}
// 仅输出:命中case 1
此外,default
分支的位置不影响穿透机制。若default
在中间且没有break
,同样会继续执行后续case:
switch (num) {
case 1:
console.log('case 1');
break;
default:
console.log('default');
case 2:
console.log('case 2');
}
// 当num=3时,输出:
// default
// case 2(default无break,穿透到case 2)
特殊场景:利用穿透实现多case共享逻辑
穿透并非总是需要避免,有时可刻意利用它实现多个case执行同一代码。例如:
const day = 6;
switch (day) {
case 6:
case 0: // 6和0共享同一逻辑
console.log('周末');
break;
default:
console.log('工作日');
}
这里case 6
命中后,因无break
,会穿透到case 0
,最终执行“周末”的逻辑。
关键点与面试加分点:
- 明确“穿透”是switch的默认行为,根源是缺少
break
。 - 能举例说明穿透的应用场景(如多case共享逻辑),体现对语法的灵活理解。
- 提及
default
的穿透特性,说明其位置不影响执行顺序。
记忆法:可记为“无break则穿,有break则断”——没有break时case会穿透执行,有break时才会中断。
什么是防抖和节流?请结合具体场景说明(例如输入框的非立即版防抖、点赞的立即版防抖等)。
防抖和节流是JavaScript中用于控制函数执行频率的两种优化技术,核心目的是避免函数因频繁触发而导致的性能问题,但两者的实现逻辑和适用场景不同。
防抖(Debounce)
防抖的核心逻辑是:当函数被触发后,延迟n秒执行;若n秒内再次触发,则重新计时,最终只执行最后一次触发后的函数。即“多次触发,只执行最后一次”。
根据执行时机,防抖可分为:
-
非立即版防抖:触发后不立即执行,等待n秒后执行;若期间再次触发,重新计时。
适用场景:输入框搜索提示。用户输入过程中,不需要每次输入都发送请求,而是等用户停止输入一段时间(如500ms)后再发送,减少请求次数。
代码示例:function debounceNonImmediate(fn, delay) { let timer = null; return function(...args) { clearTimeout(timer); // 每次触发都清除上一次的定时器 timer = setTimeout(() => { fn.apply(this, args); // 延迟后执行 }, delay); }; } // 使用:输入框搜索 const input = document.querySelector('input'); input.oninput = debounceNonImmediate((e) => { console.log('搜索:', e.target.value); }, 500);
-
立即版防抖:触发后立即执行函数,且n秒内再次触发不执行。
适用场景:点赞功能。用户快速多次点击点赞按钮时,只执行第一次点击(防止重复点赞),n秒后才可再次触发。
代码示例:function debounceImmediate(fn, delay) { let timer = null; return function(...args) { if (!timer) { // 若定时器不存在,说明可执行 fn.apply(this, args); timer = setTimeout(() => { timer = null; // 延迟后重置定时器,允许再次执行 }, delay); } }; } // 使用:点赞 const likeBtn = document.querySelector('.like'); likeBtn.onclick = debounceImmediate(() => { console.log('点赞成功'); }, 1000); // 1秒内只能点赞一次
节流(Throttle)
节流的核心逻辑是:保证函数在n秒内最多只执行一次,无论触发多少次,都按固定频率执行。即“多次触发,固定间隔执行一次”。
适用场景:滚动加载(如监听scroll
事件加载更多内容)、窗口resize事件。例如,页面滚动时,不需要每次滚动都计算位置,而是每隔200ms计算一次,避免频繁触发导致的性能损耗。
代码示例(时间戳版节流):
function throttle(fn, interval) {
let lastTime = 0; // 记录上一次执行时间
return function(...args) {
const now = Date.now();
// 若当前时间与上次执行时间的间隔 >= 设定间隔,则执行
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now; // 更新上次执行时间
}
};
}
// 使用:滚动加载
window.onscroll = throttle(() => {
console.log('滚动位置:', window.scrollY);
}, 200); // 每200ms执行一次
防抖与节流的核心区别
- 防抖:聚焦于“最后一次触发”,适合需要等待用户操作结束后再执行的场景(如搜索、表单提交)。
- 节流:聚焦于“固定频率执行”,适合需要持续反馈但又要控制频率的场景(如滚动、拖拽)。
关键点与面试加分点:
- 能清晰区分两者的触发逻辑(防抖是“最后一次”,节流是“固定间隔”)。
- 结合具体场景说明不同版本的应用(如非立即防抖用于搜索,立即防抖用于点赞),体现实际开发经验。
- 若能手写核心实现代码,并说明定时器或时间戳的选择依据,会更加分。
记忆法:可记为“防抖等最后,节流按节奏”——防抖等待最后一次触发后执行,节流按固定节奏间隔执行。
JS 中的 ceil 函数有什么作用?你还用过哪些 JS 内置函数?
ceil 函数的作用
ceil
是JavaScript中Math
对象的内置函数,全称为“ceiling”(天花板),作用是对一个数字进行向上取整,返回大于或等于该数字的最小整数。
其语法为Math.ceil(x)
,其中x
为需要处理的数字。例如:
Math.ceil(2.1)
返回3
(大于2.1的最小整数);Math.ceil(2.9)
返回3
;Math.ceil(-2.1)
返回-2
(大于-2.1的最小整数是-2);Math.ceil(5)
返回5
(整数本身)。
需要注意的是,ceil
仅处理数字类型,若传入非数字(如字符串),会先尝试转换为数字,转换失败则返回NaN
(例如Math.ceil('abc')
返回NaN
)。
其他常用的 JS 内置函数
JavaScript内置函数丰富,可按功能分类如下:
-
数字处理相关
parseInt(x, radix)
:将字符串解析为整数,radix
指定基数(2-36)。例如parseInt('123', 10)
返回123
,parseInt('11', 2)
返回3
(二进制转十进制)。parseFloat(x)
:解析字符串为浮点数,例如parseFloat('3.14')
返回3.14
。Math.floor(x)
:向下取整(与ceil
相反),例如Math.floor(2.9)
返回2
。Math.round(x)
:四舍五入取整,例如Math.round(2.5)
返回3
,Math.round(2.4)
返回2
。
-
字符串处理相关
split(separator)
:将字符串按分隔符分割为数组,例如'a,b,c'.split(',')
返回['a','b','c']
。indexOf(searchValue)
:查找子字符串首次出现的索引,未找到返回-1
,例如'hello'.indexOf('l')
返回2
。includes(searchValue)
:判断字符串是否包含子字符串,返回布尔值,例如'hello'.includes('ll')
返回true
。toUpperCase()
/toLowerCase()
:转换字符串为全大写/全小写,例如'Hello'.toUpperCase()
返回'HELLO'
。
-
数组处理相关
map(callback)
:对数组每个元素执行回调,返回新数组,例如[1,2,3].map(x => x*2)
返回[2,4,6]
。filter(callback)
:筛选符合条件的元素,返回新数组,例如[1,2,3].filter(x => x>1)
返回[2,3]
。reduce(callback, initialValue)
:累加数组元素,例如[1,2,3].reduce((a,b) => a+b, 0)
返回6
。forEach(callback)
:遍历数组,无返回值,例如[1,2].forEach(x => console.log(x))
输出1
、2
。
-
对象处理相关
Object.keys(obj)
:返回对象自身可枚举属性的键名数组,例如Object.keys({a:1, b:2})
返回['a','b']
。Object.values(obj)
:返回对象自身可枚举属性的键值数组,例如Object.values({a:1})
返回[1]
。Object.assign(target, ...sources)
:合并对象,将源对象属性复制到目标对象(浅拷贝)。
-
日期与定时器相关
Date.now()
:返回当前时间的时间戳(毫秒数),例如Date.now()
返回1620000000000
(示例值)。setTimeout(callback, delay)
:延迟指定毫秒后执行回调,返回定时器ID。clearTimeout(id)
:清除指定ID的定时器,阻止回调执行。
关键点与面试加分点:
- 准确说明
ceil
的向上取整特性,包括对负数的处理。 - 按类别列举内置函数,并结合示例说明作用,体现对API的熟悉度。
- 提及函数的边缘情况(如
parseInt
的基数影响、ceil
对非数字的处理),展示细节掌握。
记忆法:可记为“ceil向上顶,内置分类型”——ceil
函数的作用是向上取整(像“顶到天花板”),其他内置函数可按数字、字符串、数组等类型分类记忆。
import 引入模块时,有哪些区别(例如默认导入与命名导入等)?
在 JavaScript 的模块化系统中,import
用于引入其他模块导出的内容,其导入方式因模块导出方式的不同而存在区别,主要包括默认导入、命名导入、整体导入以及混合导入等,不同方式在语法和使用场景上有明确差异。
默认导入针对模块的“默认导出”(使用 export default
导出),语法为 import 自定义名称 from '模块路径'
。默认导出的模块在导入时可以自定义名称,无需与导出时的名称一致,且每个模块只能有一个默认导出。例如,若模块 utils.js
中通过 export default function add(a, b) { return a + b }
导出,则导入时可写为 import sum from './utils.js'
,这里 sum
是自定义的名称,替代了原导出的 add
。
命名导入针对模块的“命名导出”(使用 export
直接导出变量、函数等),语法为 import { 导出名称 } from '模块路径'
。命名导入必须使用与导出时完全一致的名称,若需自定义名称,可使用 as
关键字重命名,例如 import { multiply as mul } from './utils.js'
。一个模块可以有多个命名导出,导入时可按需选择部分导出内容,如 import { add, subtract } from './math.js'
。
整体导入适用于需要导入模块所有导出内容的场景,语法为 import * as 模块别名 from '模块路径'
,此时模块的所有导出(包括默认导出和命名导出)会被封装到一个对象中,通过“模块别名.导出名称”访问。例如 import * as MathUtils from './math.js'
,则可通过 MathUtils.add()
调用命名导出的 add
函数,通过 MathUtils.default()
调用默认导出的内容。
混合导入指同时导入默认导出和命名导出,语法为 import 默认名称, { 命名导出1, 命名导出2 } from '模块路径'
。例如模块同时有默认导出 function log() {}
和命名导出 const version = '1.0'
,则导入时可写为 import logger, { version } from './tools.js'
。
此外,还存在“仅执行模块”的导入方式,即 import '模块路径'
,这种方式不导入任何内容,仅执行模块中的代码(如初始化操作)。
关键点与面试加分点:需明确默认导出与命名导出的对应导入规则,理解 as
关键字的作用,以及整体导入的使用场景。能区分 ES6 模块与 CommonJS 模块的导入差异(如 ES6 是静态导入,编译时确定;CommonJS 是动态导入,运行时确定)可加分。
记忆法:“默认无括号,命名带括号;整体用星号,混合分两边”——默认导入无需大括号,命名导入必须用大括号,整体导入用 * as
,混合导入则分开默认和命名部分。
了解 tree-shaking 吗?请简要说明。
tree-shaking 是前端模块化打包工具(如 Webpack、Rollup)中用于消除未使用代码(死代码)的优化技术,其目的是减小打包后文件的体积,提升项目性能。该术语源于“摇树”的比喻——就像摇晃树木使枯叶掉落,tree-shaking 会“摇掉”代码中未被引用的部分。
tree-shaking 的工作原理基于 ES6 模块的静态分析特性。ES6 模块采用静态导入/导出(import
/export
),导入和导出语句只能出现在模块顶层,且导入的模块路径和名称在编译时即可确定(无法在运行时动态修改)。打包工具通过分析模块间的依赖关系,识别出哪些导出内容未被任何地方导入或使用,进而在打包时将这些未使用的代码剔除。
tree-shaking 的生效需要满足几个条件:
- 使用 ES6 模块系统:CommonJS 模块采用动态导入(
require
可在条件语句中使用),无法在编译时确定依赖关系,因此不支持 tree-shaking。 - 开启生产环境模式:多数打包工具(如 Webpack)在开发环境中为了保留调试信息,默认不启用 tree-shaking;只有在生产环境(
mode: 'production'
)下,才会结合代码压缩(如 Terser)执行 tree-shaking。 - 代码无副作用:若模块中的代码存在副作用(如修改全局变量、执行 DOM 操作等),即使未被导入,打包工具也可能保留该代码,避免破坏程序运行。可通过
package.json
中的sideEffects
字段声明模块是否有副作用(如sideEffects: false
表示所有模块无副作用)。
实际使用中,tree-shaking 能有效清除未使用的函数、变量、组件等。例如,若模块 utils.js
导出 add
和 subtract
两个函数,而项目中仅使用 add
,则打包后 subtract
会被 tree-shaking 移除。
关键点与面试加分点:需理解 tree-shaking 依赖 ES6 模块的静态特性,清楚其与 CommonJS 模块的兼容性问题,以及 sideEffects
字段的作用。能举例说明如何在项目中配置以确保 tree-shaking 生效(如设置 mode: 'production'
、声明 sideEffects
)可加分。
记忆法:“静态模块是基础,生产环境才触发,无副作用才移除——摇树摇掉枯叶(未用代码)”——通过“静态模块”“生产环境”“无副作用”三个关键词记忆 tree-shaking 的核心条件和作用。
如何理解 CSS 中的 “脱离文档流”?
CSS 中的“文档流”指元素在页面中按照默认规则排列的顺序和方式,即元素从左到右、从上到下依次布局,块级元素独占一行,行内元素并排显示,每个元素会占据一定的空间,影响后续元素的位置。而“脱离文档流”则是指元素不再遵循这种默认的排列规则,从正常的文档流中“脱离”出来,不再占据原来的空间,周围的元素会重新调整布局以填补其留下的空位。
脱离文档流的元素会改变自身与其他元素的布局关系,常见的脱离方式有以下三种:
-
浮动(float):当元素设置
float: left
或float: right
时(float: none
为默认值,不脱离),元素会向左或向右浮动,直到碰到容器的边缘、其他浮动元素或非浮动元素的边缘。浮动元素会脱离文档流,导致其父元素可能出现“高度塌陷”(即父元素无法被浮动的子元素撑开高度),同时后续的非浮动元素会围绕浮动元素排列。例如,两个块级元素原本上下排列,若第一个元素设置float: left
,则第二个元素会向上移动,占据第一个元素原来的位置,但其内容会避开浮动元素。 -
绝对定位(position: absolute):设置
position: absolute
的元素会脱离文档流,其位置相对于最近的非static
定位(即position
为relative
、absolute
、fixed
或sticky
)的祖先元素进行偏移;若没有这样的祖先,则相对于初始包含块(通常是<html>
元素或视口)定位。绝对定位的元素不占据原来的空间,周围的元素会忽略它的存在,直接填充其位置。 -
固定定位(position: fixed):
position: fixed
的元素同样脱离文档流,其位置相对于视口(浏览器窗口)定位,即使页面滚动,元素也会保持在固定位置。固定定位元素不占据文档流空间,周围元素的布局不受其影响。
需要注意的是,脱离文档流的元素虽然不影响正常文档流中元素的布局,但可能会影响其他脱离文档流的元素(如多个浮动元素会按顺序排列)。此外,脱离文档流的元素仍会在页面中显示,只是布局规则改变。
关键点与面试加分点:需明确脱离文档流的定义、三种实现方式及其对周围元素的影响,尤其要理解浮动导致的父元素高度塌陷问题(为后续“清除浮动”知识点铺垫)。能结合具体场景说明何时需要让元素脱离文档流(如导航栏固定、图片浮动排版)可加分。
记忆法:“浮动飘两边,绝对找祖先,固定粘视口,出流不占位”——概括三种脱离文档流的方式及其定位特点,核心是“脱离后不再占据原来的位置”。
如何清除浮动?
清除浮动是为了解决“浮动元素导致父元素高度塌陷”的问题。当父元素的子元素设置浮动后,父元素若未设置固定高度,会因无法感知浮动子元素的高度而高度为 0,进而影响后续元素的布局。清除浮动的本质是让父元素重新包裹住浮动的子元素,恢复其正常高度。常见的清除浮动方法如下:
- 父元素添加 overflow: hidden:给父元素设置
overflow: hidden
(或auto
、scroll
),可触发父元素的 BFC(块级格式化上下文)。BFC 是一个独立的渲染区域,其内部元素的布局不会影响外部元素,且 BFC 会包含内部的浮动元素,从而使父元素能被浮动子元素撑开高度。示例代码:
.parent {
overflow: hidden; /* 触发 BFC,清除浮动 */
}
.child {
float: left;
width: 100px;
height: 100px;
}
优点是简单快捷,缺点是可能会隐藏超出父元素的内容(如子元素部分内容溢出时),且 scroll
会显示滚动条,影响美观。
- 使用 after 伪元素(clearfix 方法):通过给父元素添加
after
伪元素,利用clear: both
清除浮动。clear: both
表示元素的左右两侧不允许有浮动元素,从而迫使父元素延伸到浮动元素的底部。示例代码:
.parent::after {
content: ""; /* 伪元素必须有 content */
display: block; /* 使伪元素成为块级元素 */
clear: both; /* 清除左右浮动 */
visibility: hidden; /* 隐藏伪元素本身 */
height: 0; /* 避免伪元素占据空间 */
}
.parent {
*zoom: 1; /* 兼容 IE6/7,触发 hasLayout */
}
.child {
float: left;
}
这是最常用的方法,优点是兼容性好(包括旧版 IE),不影响其他布局,也不会隐藏内容,是企业开发中的首选方案。
-
父元素设置浮动:给父元素也设置
float: left
或float: right
,使其脱离文档流,此时父元素会包裹住内部的浮动子元素。但这种方法不推荐,因为父元素浮动后会影响其自身在文档流中的布局,可能导致父元素的兄弟元素位置错乱,形成“连锁反应”。 -
在浮动元素后添加空元素并设置 clear: both:在所有浮动子元素的末尾添加一个空的块级元素(如
<div class="clear"></div>
),并设置clear: both
。示例代码:
.clear {
clear: both;
}
优点是简单直观,缺点是会添加冗余的 HTML 元素,不符合语义化要求,不推荐在大型项目中使用。
关键点与面试加分点:需理解清除浮动的核心目的是解决父元素高度塌陷,掌握 clearfix
方法的实现原理及其兼容性处理,能对比不同方法的优缺点并说明适用场景(如 overflow: hidden
适用于简单场景,clearfix
适用于复杂场景)可加分。
记忆法:“伪类清除最通用,overflow 触发 BFC,父浮连锁不可取,空元素冗余不推荐”——通过口诀记忆四种方法的优先级和特点,重点记住 clearfix
方法的通用性。
display: none、overflow: hidden、visibility: hidden 三者之间的区别是什么?
display: none
、overflow: hidden
、visibility: hidden
是 CSS 中用于控制元素可见性或内容显示范围的属性,三者在元素可见性、空间占据、布局影响及事件响应等方面存在显著区别,具体如下:
特性 | display: none | visibility: hidden | overflow: hidden |
---|---|---|---|
元素可见性 | 完全不可见,从页面中消失 | 不可见,但元素仍存在于页面中 | 元素本身可见,超出容器的部分不可见 |
是否占据空间 | 不占据空间,周围元素会填补其位置 | 占据空间,位置保持不变 | 元素本身占据空间,超出部分不额外占空间 |
对布局的影响 | 会导致周围元素重新布局 | 不影响周围元素布局 | 仅隐藏超出部分,不影响元素本身及周围布局 |
事件响应 | 无法触发任何事件(元素不存在) | 无法触发事件(元素不可见) | 可见部分可触发事件,超出部分不可 |
子元素继承性 | 子元素也会被隐藏(无法单独显示) | 可通过设置子元素 visibility: visible 使其可见 | 子元素超出部分会被隐藏,不受子元素自身设置影响 |
过渡动画兼容性 | 不支持过渡动画(显示/隐藏时无过渡) | 支持过渡动画(可见性变化有过渡) | 不直接控制可见性,过渡动画针对元素本身 |
具体来说:
display: none
会彻底移除元素,使其在页面中完全消失,既不可见也不占据空间,相当于元素从 DOM 中被临时移除。例如,设置display: none
的按钮会从页面中消失,其原来的位置会被后续元素占据,且点击该位置不会触发按钮的点击事件。visibility: hidden
仅隐藏元素的视觉效果,元素仍保持原来的尺寸和位置,占据文档流空间。例如,设置visibility: hidden
的段落不可见,但下方的文字不会上移填补其位置,且点击该区域不会触发段落的事件。若子元素设置visibility: visible
,则子元素可在隐藏的父元素中显示(如父元素visibility: hidden
,子元素visibility: visible
时,子元素可见)。overflow: hidden
用于控制元素内容超出容器时的显示方式,它不会隐藏元素本身,仅将超出容器边界的内容裁剪掉。例如,设置overflow: hidden
的div,若内部文字过多超出div高度,超出部分会被隐藏,而div本身可见且占据原有空间,点击可见部分的文字可触发事件。
关键点与面试加分点:需准确区分三者在“空间占据”和“可见性”上的核心差异,理解 visibility: hidden
的继承特性和 overflow: hidden
的内容裁剪本质。能结合场景说明使用场景(如 display: none
用于条件性隐藏元素,visibility: hidden
用于保留位置的临时隐藏,overflow: hidden
用于处理内容溢出)可加分。
记忆法:“none全消失(不可见、不占位),hidden占位置(不可见、占位),overflow藏超出(元素可见、裁超出)”——通过口诀概括三者的核心区别,重点记忆空间占据和可见性的差异。
对 CSS 盒子模型的理解是什么?正常盒模型和怪异盒模型有什么区别?
CSS 盒子模型是 CSS 布局的核心概念,它将页面中的所有元素都视为一个“盒子”,每个盒子由四个部分组成,从内到外依次是:内容区(content)、内边距(padding)、边框(border)、外边距(margin)。这些部分共同决定了元素在页面中占据的空间大小和与其他元素的位置关系。
- 内容区(content):盒子的核心区域,用于显示文本、图像等内容,其大小可通过
width
和height
属性设置。 - 内边距(padding):内容区与边框之间的空白区域,可通过
padding-top
、padding-right
等属性设置,会影响盒子的总大小,且背景色会延伸到内边距区域。 - 边框(border):包围内边距和内容区的线条,可通过
border-width
、border-color
等属性设置,同样会增加盒子的总大小。 - 外边距(margin):边框外部的空白区域,用于分隔当前盒子与其他盒子,可通过
margin-top
、margin-right
等属性设置,不影响盒子自身大小,仅影响与其他元素的间距。
正常盒模型(W3C 标准盒模型)和怪异盒模型(IE 盒模型)的核心区别在于 盒子总宽度和高度的计算方式:
-
正常盒模型(默认):盒子的
width
和height
仅对应内容区(content)的宽度和高度。总宽度 = width + padding-left + padding-right + border-left-width + border-right-width;总高度 = height + padding-top + padding-bottom + border-top-width + border-bottom-width。例如:.box { width: 100px; height: 100px; padding: 10px; border: 5px solid black; }
该盒子的总宽度为 100 + 10 + 10 + 5 + 5 = 130px,总高度同理为 130px。
-
怪异盒模型(IE 盒模型):盒子的
width
和height
包含内容区、内边距和边框。总宽度 = width(已包含 content + padding + border);总高度 = height(已包含 content + padding + border)。上述例子若设置为怪异盒模型:.box { box-sizing: border-box; /* 触发怪异盒模型 */ width: 100px; height: 100px; padding: 10px; border: 5px solid black; }
该盒子的总宽度固定为 100px,其中内容区宽度为 100 - 10 - 10 - 5 - 5 = 70px。
通过 box-sizing
属性可切换盒模型:box-sizing: content-box
为正常盒模型(默认),box-sizing: border-box
为怪异盒模型。实际开发中,常用 border-box
简化布局计算,避免因内边距和边框导致盒子总大小超出预期。
关键点与面试加分点:需明确盒子模型的四部分组成,清晰区分两种盒模型在宽高计算上的差异,理解 box-sizing
的作用及实际应用场景(如响应式布局中统一盒模型计算方式)。
记忆法:“标准盒,算内容;怪异盒,含边距(padding + border)”——正常盒模型的宽高仅算内容区,怪异盒模型的宽高包含内容、内边距和边框。
你熟悉哪些布局方式(如 flex)?如何实现元素垂直居中?若有三个盒子,如何布局?
前端常用的布局方式多样,适用于不同场景,主要包括以下几种:
-
流式布局(Normal Flow):元素默认的布局方式,块级元素从上到下独占一行,行内元素从左到右排列,通过
margin
、padding
调整间距。简单直观,但复杂布局需配合其他方式。 -
浮动布局(Float Layout):通过
float: left/right
使元素脱离文档流并向左/右浮动,常用于早期的多列布局(如左侧导航+右侧内容)。需注意清除浮动避免父元素高度塌陷。 -
Flex 布局(弹性布局):基于弹性容器和弹性项的一维布局模型,通过
display: flex
定义弹性容器,可轻松实现元素的对齐、分布和重排。适用于导航栏、卡片列表等,优势是灵活响应不同屏幕尺寸。 -
Grid 布局(网格布局):二维布局模型,通过
display: grid
定义网格容器,将页面划分为行和列,精确控制元素在网格中的位置。适用于整体页面布局(如头部、主体、侧边栏、底部的排列)。 -
定位布局(Position Layout):通过
position
属性(relative
、absolute
、fixed
、sticky
)控制元素位置,absolute
和fixed
脱离文档流,适用于弹窗、导航固定等场景。 -
表格布局(Table Layout):通过
display: table
等属性模拟表格结构,适用于需要行列对齐的场景(如表单布局),但语义化较差,逐渐被 Flex/Grid 替代。
元素垂直居中的实现方法:
-
Flex 布局:父元素设
display: flex; align-items: center
(单行垂直居中),配合justify-content: center
可同时水平居中:.parent { display: flex; align-items: center; /* 垂直居中 */ justify-content: center; /* 水平居中 */ height: 300px; } .child { width: 100px; height: 100px; }
-
Grid 布局:父元素设
display: grid; place-items: center
(同时垂直和水平居中):.parent { display: grid; place-items: center; /* 等价于 align-items: center + justify-items: center */ height: 300px; }
-
定位 + transform:子元素绝对定位,通过
top: 50%
移动到父元素垂直中点,再用transform: translateY(-50%)
微调自身一半高度:.parent { position: relative; height: 300px; } .child { position: absolute; top: 50%; transform: translateY(-50%); /* 垂直居中 */ }
-
表格布局:父元素设
display: table-cell; vertical-align: middle
:.parent { display: table-cell; vertical-align: middle; /* 垂直居中 */ height: 300px; }
三个盒子的布局示例:
-
水平排列且均等分布(Flex):
.container { display: flex; gap: 20px; /* 盒子间距 */ justify-content: space-between; /* 两端对齐,中间自动分配空间 */ padding: 20px; } .box { width: 100px; height: 100px; }
-
三行一列(垂直排列,Grid):
.container { display: grid; grid-template-rows: repeat(3, 100px); /* 3行,每行高100px */ gap: 10px; }
-
两列布局(第一列1个,第二列2个垂直排列):
.container { display: grid; grid-template-columns: 1fr 1fr; /* 两列等宽 */ grid-template-rows: 1fr 1fr; /* 两行等高 */ gap: 10px; } .box:first-child { grid-row: 1 / 3; /* 第一盒占两行 */ }
关键点与面试加分点:需熟悉不同布局的适用场景(如 Flex 适合一维,Grid 适合二维),掌握垂直居中的多种方法并能说明优缺点(如 Flex 简单但兼容性需考虑 IE),布局示例能体现灵活性。
记忆法:“Flex 一维灵,Grid 二维精,定位脱流准,垂直居中多方案(Flex/Grid/定位)”——按布局维度和核心特点记忆,垂直居中记住最常用的 Flex 和 Grid 方法。
你最熟悉哪个前端框架(Vue、React、Angular)?请说明该框架的原理、生命周期函数、组件传值方式等。
我最熟悉的前端框架是 Vue,它是一个渐进式 JavaScript 框架,核心特点是“渐进式”(可按需引入功能模块)、“数据驱动”(通过数据变化自动更新视图)和“组件化”(将页面拆分为独立可复用的组件)。
Vue 的核心原理
Vue 的核心是 响应式系统 和 虚拟 DOM:
- 响应式系统:Vue 会将数据(
data
中的属性)转为响应式数据。Vue2 中通过Object.defineProperty
劫持数据的getter
和setter
,当数据被访问时收集依赖(Watcher
),当数据修改时触发setter
,通知依赖更新视图;Vue3 则改用Proxy
代理整个对象,支持监听数组索引、新增属性等Object.defineProperty
无法实现的功能,且性能更优。 - 虚拟 DOM(VNode):Vue 将真实 DOM 抽象为 JavaScript 对象(虚拟节点),数据更新时先对比新旧虚拟 DOM 的差异(diff 算法),只更新变化的部分到真实 DOM,减少 DOM 操作次数,提升性能。
- 模板编译:Vue 的模板(
<template>
)会被编译为渲染函数(render
),执行后生成虚拟 DOM,最终渲染为真实 DOM。
生命周期函数
Vue 的生命周期指组件从创建到销毁的全过程,不同阶段有对应的钩子函数,用于执行特定逻辑:
-
Vue2 生命周期:
- 创建阶段:
beforeCreate
(实例初始化后,数据和方法未挂载)→created
(数据和方法已挂载,未挂载到 DOM),可在此阶段发起初始化请求。 - 挂载阶段:
beforeMount
(模板编译完成,未渲染到页面)→mounted
(组件已挂载到 DOM),可在此阶段操作 DOM。 - 更新阶段:
beforeUpdate
(数据更新,DOM 未更新)→updated
(DOM 已更新),可处理更新后的 DOM 逻辑。 - 销毁阶段:
beforeDestroy
(组件即将销毁,资源未释放)→destroyed
(组件已销毁,资源已释放),可在此阶段清除定时器、解绑事件。
- 创建阶段:
-
Vue3 生命周期:
Vue3 推荐使用 Composition API 中的生命周期钩子(需从vue
导入),功能与 Vue2 对应但名称前缀多为on
:onBeforeMount
、onMounted
、onBeforeUpdate
、onUpdated
、onBeforeUnmount
、onUnmounted
等,且通过setup
函数替代beforeCreate
和created
(setup
执行时机在两者之间)。
组件传值方式
-
父组件 → 子组件:通过
props
传递。父组件在子组件标签上绑定属性,子组件通过props
声明接收(Vue3 中在defineProps
中声明)。<!-- 父组件 --> <Child :message="parentMsg" /> <!-- 子组件(Vue3) --> <script setup> const props = defineProps({ message: String }); console.log(props.message); </script>
-
子组件 → 父组件:通过
$emit
(Vue2)或emit
(Vue3)触发自定义事件。子组件触发事件并传递数据,父组件监听事件接收数据。<!-- 子组件(Vue3) --> <script setup> const emit = defineEmits(['sendData']); emit('sendData', '子组件数据'); </script> <!-- 父组件 --> <Child @sendData="handleData" />
-
兄弟组件传值:
- 小型项目:使用事件总线(
eventBus
),通过一个全局 Vue 实例的$on
和$emit
传递事件。 - 大型项目:使用状态管理工具(Vuex 或 Pinia),兄弟组件通过读取/修改共享状态实现通信。
- 小型项目:使用事件总线(
-
跨级组件传值:通过
provide
(提供数据)和inject
(注入数据),父组件提供数据,任意层级的子组件可直接注入使用,无需逐层传递。<!-- 祖先组件 --> <script setup> provide('theme', 'dark'); </script> <!-- 深层子组件 --> <script setup> const theme = inject('theme'); </script>
-
状态管理工具:Vuex(Vue2 常用)或 Pinia(Vue3 推荐),用于管理全局共享状态(如用户信息、购物车数据),任意组件可通过
store
读取或修改状态。
关键点与面试加分点:需清晰说明响应式原理的版本差异(Vue2 vs Vue3),准确描述生命周期各阶段的作用,掌握不同场景下的组件传值方案并能说明优缺点(如 props
适合父子间简单传值,Pinia 适合全局状态)。
记忆法:“响应式是核心,虚拟 DOM 提性能;生命周期分阶段(创建、挂载、更新、销毁),组件传值按关系(父子、兄弟、跨级)”——简化核心原理和传值逻辑的记忆。
Vue 中 computed 和 watch 的区别是什么?
Vue 中的 computed
(计算属性)和 watch
(监听器)都是用于处理数据变化的工具,但两者在设计理念、使用场景和特性上有显著区别,具体如下:
核心特性与原理
-
computed(计算属性):
是基于依赖的响应式数据(如data
、props
中的属性)衍生出的新数据,本质是带有缓存的函数。当依赖的响应式数据发生变化时,计算属性会自动重新计算;若依赖未变,多次访问计算属性会直接返回缓存的结果,不会重复计算。<script setup> import { ref, computed } from 'vue'; const firstName = ref('张'); const lastName = ref('三'); // 计算属性:依赖 firstName 和 lastName const fullName = computed(() => `${firstName.value}${lastName.value}`); </script>
上述代码中,
fullName
依赖firstName
和lastName
,只有当这两个值变化时,fullName
才会重新计算。 -
watch(监听器):
用于监听指定的响应式数据(或计算属性),当被监听的数据变化时,执行自定义的回调函数,可用于处理异步操作或复杂的业务逻辑,无缓存机制,每次数据变化都会触发回调。<script setup> import { ref, watch } from 'vue'; const count = ref(0); // 监听 count 变化 watch(count, (newVal, oldVal) => { console.log(`count 从 ${oldVal} 变为 ${newVal}`); // 可执行异步操作,如请求接口 setTimeout(() => { console.log('异步处理'); }, 1000); }); </script>
关键区别
维度 | computed | watch |
---|---|---|
本质 | 基于依赖的计算结果,有缓存 | 监听数据变化的回调,无缓存 |
依赖 | 自动追踪响应式依赖,依赖变化则更新 | 需手动指定监听的数据源 |
返回值 | 必须有返回值(衍生数据) | 无返回值,用于执行逻辑 |
适用场景 | 简单的衍生数据计算(如拼接、过滤) | 复杂逻辑(如异步操作、数据变化后的副作用) |
执行时机 | 依赖变化时同步计算 | 可配置同步/异步(默认异步更新队列) |
是否支持深度监听 | 自动支持(依赖深层数据时) | 需手动开启 deep: true 监听对象深层变化 |
使用场景
-
computed 适合:
- 从现有数据中衍生新数据(如全名 = 姓 + 名,总价 = 单价 × 数量)。
- 需要缓存计算结果以避免重复计算(如复杂的列表过滤)。
- 模板中需要多次使用的复杂表达式(提升可读性)。
-
watch 适合:
- 数据变化后执行异步操作(如根据搜索关键词请求接口)。
- 数据变化时执行副作用(如修改其他数据、操作 DOM、清除定时器)。
- 监听对象深层属性的变化(需配置
deep: true
)。
关键点与面试加分点:需明确 computed
的缓存特性和依赖追踪机制,watch
的手动监听和异步支持,能结合具体场景说明两者的选择依据(如简单计算用 computed
,异步逻辑用 watch
)。此外,提及 Vue3 中 watch
与 watchEffect
的区别(watchEffect
自动追踪依赖,无需指定监听源)可加分。
记忆法:“computed 算结果(有缓存,依赖驱动),watch 看变化(无缓存,手动监听)”——通过核心功能和特性区分两者。
你熟悉 Vue 吗?请谈谈对 Vue 的理解(可结合 Vue3 相关内容)。
我对 Vue 有较深入的了解,它是一款渐进式 JavaScript 框架,由尤雨溪创建,核心目标是让开发者能够灵活、高效地构建用户界面。Vue 的“渐进式”体现在其功能模块(如响应式、组件化、路由、状态管理)可按需引入,既适合小型项目快速开发,也能通过生态系统支持大型应用。
Vue 的核心特点
-
数据驱动视图:Vue 最核心的特性是响应式系统,开发者只需关注数据变化,无需手动操作 DOM。当数据(
data
中的属性)更新时,Vue 会自动触发视图重新渲染,这一过程通过“数据劫持”(Vue2 用Object.defineProperty
,Vue3 用Proxy
)实现,极大简化了状态管理。 -
组件化开发:Vue 将页面拆分为独立、可复用的组件(Component),每个组件包含模板(
template
)、逻辑(script
)和样式(style
),实现了“关注点分离”。组件可嵌套组合,形成复杂页面,且支持全局注册(全项目可用)和局部注册(仅当前组件可用),提升了代码复用性和维护性。 -
模板系统:Vue 提供了声明式模板语法,允许开发者在 HTML 中嵌入 Vue 指令(如
v-if
、v-for
、v-bind
)和表达式,无需手动编写 DOM 操作代码。模板会被编译为高效的渲染函数,兼顾了开发效率和运行性能。 -
轻量与灵活:Vue 核心库仅关注视图层,体积小巧(Vue3 核心包约 10KB gzip 后),且与其他库(如 jQuery)或现有项目兼容性良好,可逐步迁移使用。
Vue3 的重要改进
Vue3 是 Vue 的重大更新,在性能、开发体验和功能上有显著提升:
-
Composition API:替代了 Vue2 的 Options API(
data
、methods
、computed
等选项),允许开发者按逻辑关注点组织代码(如将表单相关逻辑抽为一个函数),而非按选项类型划分,更适合大型项目。核心 API 包括setup
(入口函数)、ref
/reactive
(响应式数据)、computed
、watch
等。 -
更高效的响应式系统:使用
Proxy
替代Object.defineProperty
,解决了 Vue2 中无法监听数组索引、对象新增属性等问题,且拦截方式更高效,性能提升约 55%。 -
更好的 TypeScript 支持:Vue3 源码使用 TypeScript 重写,提供完善的类型定义,开发者在使用 TypeScript 时可获得更好的类型推断和 IDE 支持,减少类型错误。
-
新特性与优化:
- Fragment:组件可拥有多个根节点,无需外层包裹
<div>
。 - Teleport:允许将组件内容渲染到 DOM 中的指定位置(如将弹窗渲染到
<body>
下,避免样式嵌套问题)。 - Suspense:用于优雅处理异步组件的加载状态(如显示加载动画直到组件加载完成)。
- 性能优化:编译阶段优化虚拟 DOM diff 算法,减少运行时开销;静态节点提升,避免重复渲染。
- Fragment:组件可拥有多个根节点,无需外层包裹
Vue 生态系统
Vue 的生态丰富,可与其他工具无缝集成:
- Vue Router:官方路由工具,实现组件间的路由跳转、参数传递和路由守卫(如登录验证)。
- Pinia:Vue3 推荐的状态管理工具(替代 Vuex),简化了状态定义和修改逻辑,支持 TypeScript,且无需嵌套的
mutations
和actions
。 - Vue Test Utils:官方测试工具,用于编写组件单元测试。
- Vite:基于 ES 模块的前端构建工具,启动快、热更新迅速,是 Vue3 项目的推荐构建工具。
适用场景
Vue 适用于各种规模的 Web 应用开发:
- 小型项目:利用核心库快速实现交互功能(如表单验证、动态列表)。
- 中型项目:通过组件化和 Vue Router 构建多页面应用(如管理后台)。
- 大型项目:结合 Pinia 管理全局状态,使用 TypeScript 保证代码质量,通过 Vite 提升开发效率(如电商平台)。
关键点与面试加分点:需理解 Vue 的渐进式理念和数据驱动核心,清晰说明 Vue3 相对于 Vue2 的改进(尤其是 Composition API 和响应式系统),能结合生态工具说明 Vue 在实际项目中的应用。
记忆法:“渐进式框架,数据驱动强;Vue3 更灵活,组合 API 优”——概括 Vue 的核心特点和 Vue3 的主要优势。
Vue3 和 Vue2 的区别有哪些?
Vue3 作为 Vue 的重大更新,在核心架构、API 设计、性能和功能上与 Vue2 有显著差异,这些差异使其更适合大型项目和现代前端开发需求,具体区别如下:
核心 API 不同
Vue2 采用 Options API,将组件逻辑按选项(data
、methods
、computed
、watch
等)划分,代码组织依赖选项类型。例如:
// Vue2 组件
export default {
data() { return { count: 0 } },
methods: { increment() { this.count++ } },
computed: { doubleCount() { return this.count * 2 } }
}
这种方式在逻辑复杂时,相关代码可能分散在不同选项中,维护难度增加。
Vue3 推荐使用 Composition API,允许按逻辑关注点组织代码(如将表单处理、数据请求等逻辑抽为独立函数),通过 setup
函数(或 <script setup>
语法糖)整合。例如:
<!-- Vue3 组件 -->
<script setup>
import { ref, computed } from 'vue';
// 计数相关逻辑
const count = ref(0);
const increment = () => { count.value++ };
const doubleCount = computed(() => count.value * 2);
</script>
Composition API 支持逻辑复用(通过自定义 Hooks),更适合大型项目。
响应式系统优化
Vue2 的响应式基于 Object.defineProperty
,存在局限性:无法监听数组索引变化、对象新增/删除属性,需通过 Vue.set
或 this.$set
手动触发更新;初始化时需递归遍历数据,性能开销大。
Vue3 改用 Proxy
实现响应式,解决了 Vue2 的缺陷:
- 天然支持监听数组索引、对象新增/删除属性,无需手动调用
set
; - 懒代理机制:仅在访问数据时才代理,而非初始化时递归遍历,提升初始化性能;
- 支持拦截更多操作(如
deleteProperty
),响应式能力更全面。
性能提升
- 编译时优化:Vue3 的模板编译器会对静态节点(如固定文本)进行标记,diff 阶段直接跳过;对动态节点进行分组,减少对比范围,虚拟 DOM 更新性能提升约 55%。
- 更小的体积:Vue3 采用 Tree-shaking 优化,未使用的 API 会被打包工具剔除,核心包体积比 Vue2 减少约 40%(gzip 后约 10KB)。
- 事件缓存:模板中的事件处理函数(如
@click="handleClick"
)会被缓存,避免每次渲染重新创建函数,减少垃圾回收压力。
新特性与语法改进
-
多根节点(Fragment):Vue2 组件要求单一根节点,Vue3 支持多根节点,无需外层包裹
<div>
:<!-- Vue3 合法 --> <template> <h1>标题</h1> <p>内容</p> </template>
-
Teleport:允许将组件内容渲染到 DOM 中的指定位置(如将弹窗渲染到
<body>
下),解决样式嵌套问题:<teleport to="body"> <div class="modal">弹窗内容</div> </teleport>
-
Suspense:优雅处理异步组件加载状态,在组件加载完成前显示 fallback 内容:
<Suspense> <template #default><AsyncComponent /></template> <template #fallback>加载中...</template> </Suspense>
-
更好的 TypeScript 支持:Vue3 源码用 TypeScript 编写,提供完善的类型定义,
<script setup>
配合 TypeScript 时类型推断更精准,开发体验更优。
生命周期与生态变化
- 生命周期钩子:Vue3 生命周期钩子前缀改为
on
(如onMounted
替代mounted
),且setup
替代了beforeCreate
和created
。 - 状态管理:Vue3 推荐使用 Pinia 替代 Vuex,Pinia 简化了 API(无
mutations
,直接通过actions
修改状态),支持 TypeScript,且更轻量。 - 路由:Vue Router 4(适配 Vue3)支持 Composition API(如
useRouter
、useRoute
),路由配置和导航守卫更灵活。
关键点与面试加分点:需重点说明 Composition API 对逻辑组织的优化、响应式系统的底层差异(Proxy
vs Object.defineProperty
),以及性能提升的具体体现。能结合实际项目说明 Vue3 如何解决 Vue2 的痛点(如大型项目逻辑复用)可加分。
记忆法:“API 变组合,响应式用 Proxy,性能更优特性多(多根、Teleport、Suspense)”——概括 Vue3 与 Vue2 的核心差异。
React 中如何实现父子组件传参?多层组件间如何传参?
在 React 中,组件间传参是构建复杂应用的基础,不同层级的组件有不同的传参方式,具体如下:
父子组件传参
父子组件传参是最基础的传参方式,分为“父传子”和“子传父”两种方向:
-
父组件向子组件传参(props 传递)
父组件通过在子组件标签上绑定属性(props)传递数据,子组件通过接收props
参数获取数据。props 可以是基本类型、对象、函数等,且子组件不能直接修改 props(单向数据流)。
示例:// 父组件 function Parent() { const name = "React"; const handleClick = () => console.log("父组件方法被调用"); return <Child title={name} onClick={handleClick} />; } // 子组件 function Child(props) { return ( <div> <p>接收父组件数据:{props.title}</p> <button onClick={props.onClick}>调用父组件方法</button> </div> ); }
子组件也可通过解构赋值简化 props 使用:
function Child({ title, onClick }) { ... }
。 -
子组件向父组件传参(回调函数)
父组件传递一个回调函数给子组件,子组件通过调用该函数并传入参数,实现向父组件传递数据。本质是“父组件提供方法,子组件调用并传参”。
示例:// 父组件 function Parent() { const [childData, setChildData] = React.useState(""); const handleChildData = (data) => { setChildData(data); // 接收子组件数据 }; return ( <div> <p>子组件传来的数据:{childData}</p> <Child sendData={handleChildData} /> </div> ); } // 子组件 function Child({ sendData }) { const [inputValue, setInputValue] = React.useState(""); return ( <div> <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} /> <button onClick={() => sendData(inputValue)}>发送给父组件</button> </div> ); }
多层组件间传参
当组件层级较深(如祖父→父→子→孙),直接通过 props 逐层传递(称为“props drilling”)会导致代码冗余且维护困难,此时需采用更高效的方式:
-
Context API
Context 用于跨层级共享数据,适用于中小型项目。通过createContext
创建上下文,Provider
提供数据,Consumer
或useContext
接收数据,无需逐层传递 props。
示例:// 创建上下文 const ThemeContext = React.createContext(); // 顶层组件(提供数据) function App() { const theme = "dark"; return ( <ThemeContext.Provider value={theme}> <Parent /> </ThemeContext.Provider> ); } // 中间组件(无需传递 props) function Parent() { return <Child />; } // 深层子组件(接收数据) function Child() { const theme = React.useContext(ThemeContext); // 通过 useContext 获取 return <p>当前主题:{theme}</p>; }
注意:Context 适合共享“全局配置”类数据(如主题、用户信息),频繁更新的数据使用 Context 可能导致不必要的重渲染。
-
状态管理库
大型项目中,推荐使用 Redux、MobX、Zustand 等状态管理库,通过全局 store 存储数据,任意层级组件可直接读取或修改数据,解决多层级传参问题。以 Redux 为例:- 定义全局 store 存储共享状态;
- 组件通过
useSelector
读取 store 中的数据; - 通过
useDispatch
分发 action 修改 store 中的数据,所有依赖该数据的组件会自动更新。
-
其他方式
- 自定义事件总线(Event Bus):通过发布-订阅模式(如
events
库),组件间通过事件名通信,适合简单场景,但大型项目易导致逻辑混乱。 - React Query/SWR:用于共享异步数据(如接口请求结果),通过缓存机制使多个组件获取同一份数据,避免重复请求和多层传递。
- 自定义事件总线(Event Bus):通过发布-订阅模式(如
关键点与面试加分点:需明确父子传参的核心是 props 和回调函数,理解单向数据流的原则;多层传参需说明 Context 的适用场景和局限性,以及状态管理库的优势。能对比不同方案的优缺点(如 Context 适合低频更新数据,Redux 适合复杂状态)可加分。
记忆法:“父子用 props,子父靠回调;多层用 Context,大型靠 Redux”——按组件层级和项目规模记忆传参方式。
如何理解 React 的路由管理?
React 的路由管理是指通过 React Router 库实现单页应用(SPA)中不同页面(组件)的切换,核心是在不刷新页面的情况下,根据 URL 路径显示对应的组件,提升用户体验。React Router 是 React 生态中官方推荐的路由解决方案,目前主流版本为 v6,其设计围绕“声明式路由”理念,通过组件化方式定义路由规则。
核心概念与组件
-
Router 容器
路由系统的根容器,用于管理路由状态和历史记录。常用的有:BrowserRouter
:使用 HTML5 History API(pushState
、replaceState
),URL 格式为/path
(如http://example.com/home
),需后端配置支持刷新页面。HashRouter
:使用 URL 中的哈希(#
)部分,URL 格式为/#/path
(如http://example.com/#/home
),无需后端配置,但 SEO 不友好。
应用中需将所有路由相关组件包裹在 Router 容器中:
import { BrowserRouter as Router } from 'react-router-dom'; function App() { return ( <Router> {/* 路由规则和组件 */} </Router> ); }
-
Routes 与 Route
Routes
:类似 Vue Router 中的<RouterView>
,用于包裹Route
组件,匹配当前 URL 并渲染对应的Route
组件(仅渲染第一个匹配的Route
)。Route
:定义路径与组件的映射关系,通过path
属性指定 URL 路径,element
属性指定对应组件。
示例:
import { Routes, Route } from 'react-router-dom'; import Home from './Home'; import About from './About'; function Main() { return ( <Routes> <Route path="/" element={<Home />} /> {/* 根路径匹配 Home */} <Route path="/about" element={<About />} /> {/* /about 匹配 About */} </Routes> ); }
-
导航组件
Link
:用于页面内导航,类似<a>
标签,但不会触发页面刷新,通过to
属性指定目标路径:import { Link } from 'react-router-dom'; function Nav() { return ( <nav> <Link to="/">首页</Link> <Link to="/about">关于</Link> </nav> ); }
NavLink
:Link
的增强版,可通过isActive
属性判断当前是否匹配,用于高亮显示当前路由。
-
路由参数与查询字符串
- 动态路由参数:通过
path="/user/:id"
定义参数,使用useParams
钩子获取:// 定义路由 <Route path="/user/:id" element={<User />} /> // User 组件中获取参数 import { useParams } from 'react-router-dom'; function User() { const { id } = useParams(); // id 为 URL 中的参数值 return <p>用户 ID:{id}</p>; }
- 查询字符串:通过
useSearchParams
钩子处理 URL 中的查询参数(如?name=react
):import { useSearchParams } from 'react-router-dom'; function Search() { const [searchParams, setSearchParams] = useSearchParams(); const name = searchParams.get('name'); // 获取 name 参数 return <p>搜索关键词:{name}</p>; }
- 动态路由参数:通过
高级特性
-
嵌套路由
用于实现页面布局中的“公共部分+动态内容”(如侧边栏+主内容),通过Outlet
组件渲染子路由对应的组件:// 父路由定义 <Route path="/dashboard" element={<Dashboard />}> <Route index element={<DashboardHome />} /> {/* 默认子路由 */} <Route path="settings" element={<DashboardSettings />} /> </Route> // Dashboard 组件(包含 Outlet) function Dashboard() { return ( <div> <h1>仪表盘</h1> <Outlet /> {/* 渲染子路由组件 */} </div> ); }
-
路由守卫与重定向
- 路由守卫:通过
useEffect
或自定义组件实现权限控制(如未登录跳转登录页):function PrivateRoute({ element }) { const isLogin = useAuth(); // 自定义钩子判断登录状态 return isLogin ? element : <Navigate to="/login" />; } // 使用 <Route path="/profile" element={<PrivateRoute element={<Profile />} />} />
- 重定向:使用
Navigate
组件实现页面跳转,replace
属性控制是否替换历史记录:<Route path="/old-path" element={<Navigate to="/new-path" replace />} />
- 路由守卫:通过
-
编程式导航
通过useNavigate
钩子实现代码触发的导航(如表单提交后跳转):import { useNavigate } from 'react-router-dom'; function Login() { const navigate = useNavigate(); const handleLogin = () => { // 登录逻辑... navigate('/home'); // 跳转至首页 // navigate(-1); // 后退一页 }; return <button onClick={handleLogin}>登录</button>; }
关键点与面试加分点:需掌握 React Router v6 的核心组件(Routes
、Route
、Link
)和钩子(useParams
、useNavigate
),理解嵌套路由和动态参数的使用,能说明 BrowserRouter
与 HashRouter
的区别及后端配置需求。提及 v6 相比 v5 的变化(如 Routes
替代 Switch
、element
属性)可加分。
记忆法:“Router 作容器,Routes 管匹配,Route 定映射,Link 来导航,参数用 Params”——概括路由管理的核心组件和功能。
对 React 的 DOM 树有什么理解?
React 中的“DOM 树”通常指真实 DOM 树与虚拟 DOM(Virtual DOM)树的结合,其中虚拟 DOM 是 React 实现高效渲染的核心机制。理解 React 的 DOM 树,需要从真实 DOM 的问题、虚拟 DOM 的作用、React 的渲染流程三个层面展开:
真实 DOM 与性能瓶颈
真实 DOM 是浏览器用于呈现页面的树形结构,每个节点对应 HTML 元素(如 <div>
、<p>
)。但直接操作真实 DOM 存在性能问题:
- 操作成本高:DOM 节点包含大量属性和方法(如样式、事件、布局信息),修改 DOM 会触发浏览器的重排(Reflow)或重绘(Repaint),重排会重新计算元素位置和大小,代价极高。
- 频繁更新低效:前端应用常需频繁更新数据(如列表刷新、表单输入),若每次更新都直接操作真实 DOM,会导致大量冗余计算,引发页面卡顿。
虚拟 DOM 的引入与作用
为解决真实 DOM 的性能问题,React 引入 虚拟 DOM(Virtual DOM),它是对真实 DOM 的轻量级 JavaScript 对象模拟,仅包含渲染所需的核心信息(如标签名、属性、子节点)。例如,一个真实 <div>
元素对应的虚拟 DOM 可能是:
const vNode = {
type: 'div', // 标签类型
props: { className: 'container' }, // 属性
children: [ // 子节点
{ type: 'p', props: null, children: 'Hello React' }
]
};
虚拟 DOM 的核心作用是 减少真实 DOM 操作:
- 缓存当前状态:React 会将当前虚拟 DOM 树缓存起来。
- 计算差异(Diff 算法):当数据变化时,React 生成新的虚拟 DOM 树,通过 Diff 算法对比新旧虚拟 DOM 树,找出需要更新的最小部分(“最小差异集”)。
- 批量更新真实 DOM:React 只将差异部分更新到真实 DOM,避免全量重绘,降低性能消耗。
React 的 DOM 树更新流程
React 中 DOM 树的更新是一个“协调(Reconciliation)→ 提交(Commit)”的过程:
-
协调阶段(Reconciliation)
该阶段在内存中进行,不操作真实 DOM。React 会:- 根据新的状态生成新的虚拟 DOM 树;
- 使用 Diff 算法对比新旧虚拟 DOM 树,确定哪些节点需要更新、新增或删除。
Diff 算法的优化策略: - 同层比较:只对比同一层级的节点,不跨层级比较(因 DOM 树跨层级操作极少,可降低复杂度);
- key 优化:列表渲染时,为每个子节点设置唯一
key
,帮助 React 识别节点是否复用,避免误删或重复创建(若无key
,默认按索引对比,可能导致状态错乱)。
-
提交阶段(Commit)
该阶段将协调阶段计算出的差异应用到真实 DOM:- 对需要新增的节点,调用
createElement
创建真实 DOM 并插入; - 对需要更新的节点,仅修改变化的属性(如
className
、style
); - 对需要删除的节点,调用
removeChild
移除。
同时,React 会在此时执行生命周期钩子(如componentDidMount
、useEffect
的回调)和事件处理。
- 对需要新增的节点,调用
React Fiber 与 DOM 树更新
React 16 引入 Fiber 架构,进一步优化了 DOM 树更新的性能。Fiber 将虚拟 DOM 树拆分为一个个独立的“工作单元”(Fiber 节点),允许更新过程被中断、暂停、恢复或终止:
- 当浏览器有更高优先级任务(如用户输入)时,React 可暂停当前 Fiber 的更新,优先处理用户操作,避免页面卡顿;
- 任务完成后,React 从暂停处继续执行剩余更新,确保 UI 响应性。
关键点与面试加分点:需明确虚拟 DOM 是真实 DOM 的 JavaScript 模拟,理解 Diff 算法的优化策略(同层比较、key 的作用),以及 Fiber 架构如何提升更新的灵活性。能说明“虚拟 DOM 并非总是比直接操作 DOM 快”(简单场景下直接操作可能更快,复杂场景下虚拟 DOM 优势明显)可加分。
记忆法:“虚拟 DOM 是副本,Diff 找差异,批量更真实,Fiber 可中断”——概括 React DOM 树的核心机制和优化点。
常用的 Git 命令有哪些?例如:如何合并两个 commit、如何与远程分支建立连接、merge 和 rebase 的区别;
Git 是前端开发中常用的版本控制工具,掌握其核心命令是协作开发的基础,以下是常用命令及关键场景的详细说明:
基础操作命令
-
初始化与克隆
git init
:在当前目录初始化一个新的 Git 仓库(创建.git
文件夹)。git clone <远程仓库地址>
:克隆远程仓库到本地(如git clone https://github.com/example/repo.git
)。
-
暂存与提交
git add <文件路径>
:将工作区的文件添加到暂存区(如git add index.js
添加单个文件,git add .
添加所有修改)。git commit -m "提交信息"
:将暂存区的文件提交到本地仓库,-m
指定提交说明(如git commit -m "fix: 修复登录bug"
)。git status
:查看工作区、暂存区的文件状态(哪些文件被修改、未跟踪)。git diff
:查看工作区与暂存区的差异(未add
的修改);git diff --cached
查看暂存区与本地仓库的差异(已add
但未commit
的修改)。
-
分支操作
git branch
:列出本地所有分支,当前分支前有*
标记。git branch <分支名>
:创建新分支(如git branch feature/login
)。git checkout <分支名>
:切换到指定分支(Git 2.23+ 推荐git switch <分支名>
,更直观)。git checkout -b <分支名>
:创建并切换到新分支(等价于git branch
+git checkout
)。git branch -d <分支名>
:删除本地分支(-D
强制删除未合并的分支)。
-
远程仓库交互
git remote
:列出已配置的远程仓库(通常默认名为origin
)。git remote add <远程名> <远程地址>
:与远程分支建立连接(如git remote add origin https://github.com/example/repo.git
)。git pull <远程名> <分支名>
:拉取远程分支的更新并合并到当前本地分支(如git pull origin main
)。git push <远程名> <分支名>
:将本地分支的提交推送到远程仓库(如git push origin feature/login
;首次推送新分支需加-u
关联,git push -u origin feature/login
)。
关键场景命令
-
合并两个 commit
用于将多个连续的提交合并为一个,使提交历史更简洁(通常在push
前整理本地提交),需使用交互式变基(git rebase -i
):- 执行
git rebase -i HEAD~n
(n
为需要合并的提交数量,如合并最近 2 个 commit 则用git rebase -i HEAD~2
)。 - 编辑器中会列出这
n
个 commit,将除第一个外的pick
改为squash
(或s
,表示将该 commit 合并到前一个)。 - 保存退出后,编辑器会打开新窗口,输入合并后的提交信息,完成合并。
示例:合并最近 2 个 commit:
git rebase -i HEAD~2 # 编辑界面将第一个 commit 保留 pick,第二个改为 squash # 保存后输入新的提交信息
- 执行
-
与远程分支建立连接
除了初始化时用git remote add
,若本地分支需关联已存在的远程分支,可使用:git branch --set-upstream-to=<远程名>/<远程分支名> <本地分支名>
(如git branch --set-upstream-to=origin/main main
,将本地main
关联远程main
)。- 后续可直接用
git pull
或git push
,无需指定远程和分支。
-
merge 和 rebase 的区别
两者均用于合并分支,但合并方式和提交历史不同:- git merge <源分支>:将源分支的提交合并到当前分支,会创建一个新的“合并
git操作失误时如何退回(如退回到上一个版本的命令)
git操作失误后的退回方式,需根据失误发生的阶段(工作区、暂存区、本地仓库、远程仓库)具体分析,不同场景对应不同命令,核心是通过版本控制的历史记录或状态管理实现回退。
在工作区失误(如修改了文件但未执行git add)时,可使用git checkout -- <文件名>
丢弃工作区的修改,让文件回到最近一次git add或git commit时的状态。例如,误改了index.js,执行git checkout -- index.js
即可恢复。
若已执行git add将修改加入暂存区,想撤销暂存,需用git reset HEAD <文件名>
,该命令会将暂存区的修改退回到工作区。比如,对index.js执行了git add后发现错误,可执行git reset HEAD index.js
,之后再用git checkout丢弃工作区修改。
当修改已提交到本地仓库(即执行了git commit),需回退到之前的版本,此时用git reset
命令。git reset --hard HEAD^
可回退到上一个版本(HEAD^表示上一个版本,HEAD^^表示上两个版本,也可用HEAD~n表示前n个版本);若知道目标版本的commit哈希值,可直接指定,如git reset --hard a1b2c3d
(a1b2c3d为目标版本的哈希前缀)。需注意,--hard
会彻底丢弃当前工作区和暂存区的修改,慎用;若想保留工作区修改,可使用--soft
,仅移动HEAD指针,暂存区和工作区不变。
若失误已推送到远程仓库,此时不能直接用git reset回退(会导致本地与远程版本不一致),而应使用git revert <commit哈希>
创建一个新的commit来抵消之前的错误commit,这样既保留了历史记录,又能在远程仓库生效。例如,错误提交的哈希是a1b2c3d,执行git revert a1b2c3d
,编辑提交信息后推送,远程仓库就会恢复正确状态。
此外,若不慎删除了分支或丢失了commit,可通过git reflog
查看本地所有操作记录(包括已删除的commit),找到目标commit的哈希后,用git checkout <哈希>
或git branch 新分支名 <哈希>
恢复。
面试关键点:需明确不同阶段的回退命令及--hard
、--soft
的区别,强调远程仓库回退用revert的原因(避免历史记录冲突)。记忆法可采用“阶段对应法”:工作区用checkout,暂存区用reset HEAD,本地仓库用reset --hard,远程仓库用revert,按操作阶段依次记忆。
Git 的实现原理是什么
Git的实现原理基于分布式版本控制思想,核心是通过高效的数据存储、哈希算法和指针管理,实现对文件版本的追踪、分支管理和协同开发。
从数据结构看,Git的核心是“对象库”(.git/objects),所有版本信息都以对象形式存储,包括四种类型:blob对象(存储单个文件的内容,不含文件名等元信息)、tree对象(存储目录结构,记录blob对象或其他tree对象的引用及文件名、权限等)、commit对象(记录一次提交的元信息,包括指向的tree对象、父commit引用、作者、提交时间、提交信息等)、tag对象(给特定commit打上标签,如版本号v1.0)。这些对象都通过SHA-1哈希算法生成唯一的40位哈希值作为标识,确保内容唯一且不可篡改。
Git的版本控制并非存储文件的差异,而是存储文件的完整快照。每次提交(commit)时,Git会为当前工作区的所有文件创建blob对象,通过tree对象组织目录结构,再用commit对象关联tree和父commit,形成一条链式的提交历史。这种快照式存储虽可能占用更多空间,但Git会通过压缩和增量存储优化(如相同内容的blob对象只存一次),保证效率。
分支的实现本质是指向commit对象的可变指针。默认的master/main分支就是一个指向最新commit的指针,创建新分支(如git branch dev
)只是新增一个指向相同commit的指针,切换分支(git checkout dev
或git switch dev
)则是让HEAD指针指向新分支。分支合并时,Git会找到两个分支的最近共同祖先,通过三方合并算法处理差异,生成新的commit。
工作区、暂存区(index)、本地仓库、远程仓库构成了Git的工作流程。工作区是本地编辑的文件目录;暂存区是一个临时区域(.git/index),用于存放即将提交的文件快照,通过git add
将工作区修改添加到暂存区;本地仓库存储所有commit和对象,通过git commit
将暂存区内容提交到本地仓库;远程仓库是共享的代码仓库,通过git push
/git pull
实现本地与远程的同步。
面试关键点:需说明四种对象的作用、快照式存储的特点、分支的指针本质。记忆法可采用“对象-指针-区域”三要素法:对象存储数据,指针管理版本和分支,区域划分工作流程,三者结合构成Git的核心原理。
git commit 的具体作用是什么
git commit 是Git中用于将暂存区的修改提交到本地仓库的核心命令,其本质是创建一个“提交对象”(commit object),将当前暂存区的状态永久记录到版本历史中,形成可追溯的版本节点。
具体作用可从以下几方面展开:首先,它会将暂存区(通过git add添加的修改)的内容打包成快照。Git会为暂存区中的每个文件生成对应的blob对象,通过tree对象组织目录结构,再创建一个commit对象关联该tree对象,同时记录父提交(上一次commit的哈希值)、作者信息(姓名和邮箱)、提交时间、提交说明等元信息。这一过程让当前项目状态被“固化”,成为版本历史中的一个节点。
其次,git commit 会更新当前分支的指针。分支本质是指向commit对象的指针,每次提交后,当前分支(如master)的指针会自动移动到新创建的commit对象上,确保分支始终指向最新的提交。
再者,它为版本回溯和协作提供了基础。每个commit都有唯一的SHA-1哈希值,可通过该哈希精准定位到对应版本,便于后续回退、比较(如git diff <commit1> <commit2>
)或分支合并。同时,提交历史记录了修改的目的和上下文(通过提交信息),帮助开发者理解代码变更的原因。
git commit 有多个常用选项:-m
直接添加提交信息,如git commit -m "修复登录bug"
;-am
可跳过git add,直接将已跟踪文件的修改添加到暂存区并提交(未跟踪的新文件不行),如git commit -am "更新首页样式"
;--amend
用于修改最近一次提交(仅本地未推送的提交),会替换上一次commit,适用于补充提交信息或修正小错误,如git commit --amend -m "修正:修复登录bug"
。
需注意,git commit 仅作用于本地仓库,不会影响远程仓库,需通过git push
将提交推送到远程。此外,提交后的数据是不可变的,若需修改历史,需用git rebase或git filter-branch等命令,但修改已推送的提交可能导致协作冲突,需谨慎。
面试关键点:需强调commit创建快照、更新分支指针、支持版本追溯的作用,以及-m
、--amend
等选项的使用场景。记忆法可联想“拍照存档”:暂存区是“待拍的场景”,commit是“按下快门”,生成的照片(commit对象)存入相册(本地仓库),且相册按拍摄顺序(提交历史)排列,方便回看。
你用过哪些不常见的 Git 命令?
在日常开发中,除了git add、git commit、git push等基础命令,一些不常见的Git命令能解决特定场景的问题,提升效率,以下是一些实用的不常见命令及使用场景。
git stash
及相关命令用于暂存工作区和暂存区的修改,适用于需要切换分支但不想提交当前修改的场景。git stash
会将当前修改保存到一个临时栈中,工作区恢复到干净状态;git stash save "备注信息"
可添加备注,便于区分不同暂存;git stash list
查看所有暂存记录;git stash pop
应用最近一次暂存并删除该记录;git stash apply stash@{n}
应用指定暂存(n为列表中的索引)且不删除记录;git stash drop stash@{n}
删除指定暂存。例如,开发中突然需要修复紧急bug,可执行git stash save "首页开发中"
,切换到修复分支,完成后用git stash pop
恢复之前的工作。
git reflog
用于查看本地仓库的所有操作记录(包括已删除的分支、重置的commit等),是恢复误操作的重要工具。Git的commit历史可能因reset、rebase等操作丢失,但reflog会记录HEAD指针的所有移动,默认保留90天。例如,误执行git reset --hard
删除了重要commit,可通过git reflog
找到该commit的哈希值,再用git checkout <哈希>
恢复。
git cherry-pick <commit哈希>
用于将其他分支的特定commit复制到当前分支,适用于仅需要某分支的部分修改,而非整个分支合并的场景。例如,dev分支有一个修复bug的commit(哈希a1b2c3d),想将其应用到master分支,可在master分支执行git cherry-pick a1b2c3d
,该commit的修改会被复制到master并创建新commit。若有冲突,解决后执行git cherry-pick --continue
即可。
git bisect
用于二分法查找引入bug的commit,适用于已知某个版本正常、当前版本有bug,但不确定具体哪个commit引入问题的场景。使用步骤:git bisect start
开始二分查找;git bisect bad
标记当前版本为有bug;git bisect good <已知正常版本的哈希或标签>
标记已知正常版本;Git会自动切换到中间版本,测试该版本是否有bug,有则执行git bisect bad
,无则执行git bisect good
,重复此过程,最终定位到首个引入bug的commit,完成后用git bisect reset
退出。
git submodule
用于管理项目的子模块(如依赖的第三方库或共用组件),适用于多个项目共享同一模块且需独立版本控制的场景。git submodule add <子模块仓库地址> <路径>
添加子模块;git submodule init
初始化子模块;git submodule update
拉取子模块的代码;git submodule foreach git pull
批量更新所有子模块。
面试关键点:需说明命令的具体作用和适用场景,体现对Git深入使用的理解。记忆法可按功能分类:暂存类(stash系列)、恢复类(reflog)、挑选类(cherry-pick)、查找类(bisect)、子模块类(submodule),按场景需求联想对应的命令。
Webpack 的常见配置项有哪些?如何修改打包路径?
Webpack 作为前端模块打包工具,通过配置文件(通常是webpack.config.js)定义打包规则,常见配置项覆盖入口、出口、模块处理、插件、开发工具等核心环节,修改打包路径主要通过输出(output)配置实现。
常见配置项及其作用如下:
- entry:指定打包入口文件,告诉Webpack从哪个文件开始构建依赖树。可配置为字符串(单入口)、数组(多入口合并为一个chunk)或对象(多入口,每个入口生成一个chunk)。例如:
entry: {
main: './src/index.js', // 主入口
vendor: './src/vendor.js' // 第三方库入口
}
- output:定义打包后的文件输出位置和命名规则,是修改打包路径的核心配置。包含path(输出目录的绝对路径)、filename(输出文件名,可使用占位符如[name](入口名)、[hash](打包哈希)、[contenthash](文件内容哈希))、publicPath(静态资源的公共路径,用于CDN或开发服务器)等。例如:
output: {
path: path.resolve(__dirname, 'dist'), // 输出到项目根目录的dist文件夹
filename: 'js/[name].[contenthash].js', // 输出到dist/js目录,文件名含入口名和内容哈希
publicPath: '/assets/' // 静态资源引用路径前缀
}
- module:配置模块处理规则,通过rules定义不同类型文件的加载器(loader)。Webpack默认只能处理JS和JSON文件,需通过loader处理CSS、图片、TS等文件。例如:
module: {
rules: [
{ test: /\.css$/, use: ['style-loader', 'css-loader'] }, // 处理CSS文件
{ test: /\.(png|jpg)$/, type: 'asset/resource', generator: { filename: 'images/[hash][ext]' } } // 处理图片,输出到images目录
]
}
- plugins:配置插件(plugin),用于扩展Webpack功能,如打包优化、环境变量注入等。常见插件有HtmlWebpackPlugin(生成HTML并自动引入打包后的JS/CSS)、CleanWebpackPlugin(打包前清理dist目录)、DefinePlugin(注入环境变量)等。例如:
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
new CleanWebpackPlugin()
]
-
mode:指定打包模式(development/production/none),不同模式会启用默认优化配置。development模式保留注释、不压缩代码,开启source map;production模式自动压缩代码、 tree-shaking 等。
-
devServer:配置开发服务器,支持热更新、代理等功能,方便开发调试。例如:
devServer: {
static: './dist', // 服务器根目录
hot: true, // 热更新
proxy: { '/api': { target: 'http://localhost:3000' } } // 接口代理
}
修改打包路径主要通过output的path和filename实现。path需设置为绝对路径(通常用path.resolve处理),决定打包文件的根目录;filename可通过目录前缀(如'js/[name].js')将文件输出到子目录;对于图片等资源,可在module.rules的generator.filename中配置子目录(如上述的'images/[hash][ext]')。
面试关键点:需掌握entry、output、module、plugins的核心配置,明确output.path和filename对打包路径的影响,提及contenthash在缓存优化中的作用。记忆法可采用“入口-出口-处理-插件-模式”五部曲:entry定义起点,output控制输出(包括路径),module处理文件,plugins扩展功能,mode决定环境,按流程记忆核心配置项。
什么是 Nginx 的正向代理和反向代理?
Nginx 的正向代理和反向代理是两种不同的代理模式,核心区别在于代理的对象和应用场景,理解这两者是前端部署和网络配置中的重要知识点。
正向代理是代理客户端的模式。当客户端无法直接访问目标服务器(如受网络限制、跨域或需要隐藏客户端身份)时,会通过正向代理服务器间接请求目标服务器。此时,代理服务器知道目标服务器的地址,而目标服务器不知道实际的客户端,只知道代理服务器。典型场景包括:科学上网(VPN 本质是正向代理)、企业内网客户端访问外部互联网。例如,在 Nginx 中配置正向代理,需指定 resolver(DNS 解析)和代理目标,配置示例:
server {
listen 8080;
resolver 8.8.8.8; # DNS 服务器
location / {
proxy_pass http://$http_host$request_uri; # 代理客户端请求到目标服务器
}
}
反向代理是代理服务器端的模式。客户端直接向反向代理服务器发送请求,代理服务器会将请求转发到后端的多个实际服务器(如应用服务器集群),客户端不知道实际处理请求的后端服务器。此时,代理服务器隐藏了后端服务器的地址,用于负载均衡、动静分离、统一入口等场景。典型场景包括:网站集群的负载均衡(将请求分发到不同后端服务器)、将静态资源请求转发到 CDN、隐藏后端服务地址提高安全性。例如,Nginx 反向代理配置(负载均衡场景):
upstream backend_servers {
server 192.168.1.100:3000;
server 192.168.1.101:3000;
}
server {
listen 80;
location / {
proxy_pass http://backend_servers; # 代理到后端服务器集群
}
}
两者的核心区别可通过表格对比:
维度 | 正向代理 | 反向代理 |
---|---|---|
代理对象 | 客户端 | 后端服务器 |
客户端知情 | 知道目标服务器地址 | 不知道实际后端服务器 |
核心作用 | 突破访问限制、隐藏客户端 | 负载均衡、保护后端服务 |
典型场景 | VPN、内网访问外网 | 网站集群、动静分离 |
面试加分点:能说明两者在网络分层中的位置(均工作在应用层),以及反向代理在 HTTPS 配置中的作用(代理服务器处理 SSL 握手,减轻后端压力)。
记忆法:用 “方向 + 对象” 记忆法。正向代理 “正向” 指向客户端,代理客户端去 “找” 目标服务器;反向代理 “反向” 指向服务器,代理服务器来 “接” 客户端的请求。
跨域的解决方法有哪些?
跨域是指浏览器因 “同源策略” 限制,阻止不同源(协议、域名、端口任一不同)的页面之间进行资源交互的现象。解决跨域的方法多样,需根据场景选择,以下是常见方案:
-
CORS(跨域资源共享):服务端配置的解决方案,通过设置响应头允许跨域请求,是目前最主流的方法。服务端需在响应中添加
Access-Control-Allow-Origin
等头信息,例如:http
Access-Control-Allow-Origin: https://example.com # 允许指定源访问 Access-Control-Allow-Methods: GET, POST, PUT # 允许的请求方法 Access-Control-Allow-Credentials: true # 允许携带Cookie
优点:支持所有 HTTP 方法,安全性高(可精确控制允许的源);缺点:需服务端配合,复杂请求(如 PUT、带自定义头)会触发预检请求(OPTIONS),可能增加请求次数。
-
JSONP:利用
<script>
标签不受同源策略限制的特性,通过动态创建 script 标签请求跨域资源,服务端返回一段调用客户端预设函数的 JS 代码。示例:// 客户端定义回调函数 function handleData(data) { console.log(data); } // 创建script标签 const script = document.createElement('script'); script.src = 'https://api.example.com/data?callback=handleData'; document.body.appendChild(script);
服务端返回:
handleData({ "name": "test" })
。优点:兼容性好(支持老旧浏览器);缺点:仅支持 GET 方法,存在 XSS 风险(依赖服务端返回安全代码)。 -
代理服务器:通过同源的代理服务器转发请求,避免浏览器直接跨域。常见于开发环境,如 Webpack DevServer 配置代理:
// webpack.config.js module.exports = { devServer: { proxy: { '/api': { target: 'https://api.example.com', // 目标跨域服务器 changeOrigin: true, // 伪装请求源为目标服务器域名 pathRewrite: { '^/api': '' } // 去除请求路径中的/api前缀 } } } };
生产环境可使用 Nginx 代理,配置类似。优点:前端无需修改代码,适合开发和生产;缺点:需额外配置代理服务。
-
document.domain:适用于主域相同、子域不同的场景(如
a.example.com
和b.example.com
),通过设置document.domain = 'example.com'
使两者同源。缺点:仅限主域相同的情况,且无法跨协议或端口。 -
WebSocket:WebSocket 协议本身不限制同源,可通过
new WebSocket('wss://cross-domain.example.com')
直接建立跨域连接,适用于实时通信场景(如聊天、实时数据更新)。 -
iframe 相关方案:如
window.postMessage
,允许不同源的 iframe 或窗口之间传递数据,示例:// 父窗口向子iframe发送消息 const iframe = document.getElementById('myIframe'); iframe.contentWindow.postMessage('hello', 'https://child.example.com'); // 子窗口监听消息 window.addEventListener('message', (e) => { if (e.origin === 'https://parent.example.com') { console.log(e.data); // 接收消息 } });
面试加分点:能说明 CORS 预检请求的触发条件(如请求方法为 PUT/DELETE、带自定义头、Content-Type 为 application/json 等),以及各种方案的安全性对比(如 CORS 比 JSONP 更安全)。
记忆法:按 “服务端控制” 和 “客户端技巧” 分类记忆。服务端控制类包括 CORS、代理服务器;客户端技巧类包括 JSONP、document.domain、postMessage、WebSocket,便于梳理不同场景下的选择逻辑。
平时如何学习前端?看过哪些相关书籍?
前端技术迭代快,高效的学习方法和系统的知识体系至关重要,我的学习路径可分为四个维度,结合实践与理论形成闭环。
-
理论基础:从官方文档和经典书籍入手
前端学习的核心是夯实基础,官方文档是最权威的资料。例如,学习 JavaScript 时精读 MDN 文档,理解原型链、异步编程等核心概念;学习 CSS 时参考 W3C 规范,掌握盒模型、布局规则等底层逻辑。书籍方面,基础阶段必读《JavaScript 高级程序设计(第 4 版)》(俗称 “红宝书”),它系统讲解了 JS 语法、DOM、BOM 及 ES6+ 特性,是深入理解 JS 的基石;CSS 领域推荐《CSS 揭秘》,通过 47 个实例掌握 CSS 进阶技巧;计算机基础补充《深入浅出计算机网络》,理解 HTTP、TCP 等协议原理。 -
实践练习:项目驱动 + 源码分析
理论需通过实践落地。初期可复现经典效果(如轮播图、下拉菜单),理解 DOM 操作和事件机制;中期开发完整项目(如电商网站、管理系统),整合框架(Vue/React)、路由、状态管理等技术;后期尝试源码分析,例如阅读 Vue3 的响应式原理源码,理解 Proxy 如何实现数据劫持,或分析 React 的 Fiber 架构,掌握虚拟 DOM 渲染逻辑。实践中遇到问题时,优先通过 Stack Overflow、掘金社区搜索解决方案,培养独立调试能力。 -
技术跟进:关注生态与行业动态
前端技术更新快,需保持对新技术的敏感度。定期查看 GitHub Trends 了解热门工具(如 Vite 替代 Webpack);关注前端会议(如 Google I/O、VueConf)的演讲视频,掌握框架演进方向(如 React Server Components、Vue3 的 Composition API);订阅技术公众号(如 “前端早读课”“字节前端”),获取行业实践案例和最佳实践。 -
规范与工程化:培养专业开发习惯
专业前端需注重代码质量,学习使用 ESLint、Prettier 规范代码风格,通过 Git 进行版本管理,掌握 Webpack/Vite 构建优化。阅读《代码整洁之道》培养代码可读性意识,参考 Airbnb 前端规范等业界标准,形成规范化开发习惯。
面试加分点:能结合具体项目说明学习成果(如 “通过开发管理系统掌握了 Vuex 模块化设计”),或分享源码分析的收获(如 “分析 React Hooks 源码后理解了依赖数组的作用”),体现主动学习能力。
记忆法:用 “地基 - 建筑 - 装修 - 维护” 类比法记忆。理论基础是地基,实践项目是建筑,技术跟进是装修(保持更新),规范工程化是维护(保证稳定),四者缺一不可。
新启动一个项目时,需要进行哪些配置?
新启动项目的配置直接影响开发效率和后期维护,需从初始化、规范、环境、功能等多维度系统规划,以下是关键配置步骤:
-
项目初始化与技术栈选型
首先确定技术栈:根据项目需求选择框架(如 Vue3+Vite 适合中小型项目,React+Next.js 适合 SEO 需求)、包管理工具(npm/yarn/pnpm,推荐 pnpm 因速度快且节省空间)。初始化项目:使用框架脚手架(如npm create vue@latest
或create-react-app
)自动生成基础结构,减少手动配置成本。生成package.json
后,明确项目名称、版本、入口文件等信息,并添加scripts
脚本(如dev
启动开发服务、build
打包生产环境)。 -
环境配置:区分开发 / 生产环境
不同环境(开发、测试、生产)的 API 地址、密钥等配置不同,需通过环境变量管理。使用.env
文件系列:.env.development
存放开发环境变量(如VITE_API_URL=http://localhost:3000
),.env.production
存放生产环境变量(如VITE_API_URL=https://api.example.com
),并在.gitignore
中排除敏感配置文件。框架中通过import.meta.env
(Vite)或process.env
(Webpack)访问变量,避免硬编码。 -
代码规范与质量控制
统一代码风格是团队协作的基础。配置 ESLint 检查语法错误和代码风格,结合框架插件(如eslint-plugin-vue
)定制规则(如禁止未使用的变量、强制单引号);使用 Prettier 自动格式化代码,通过eslint-config-prettier
解决 ESLint 与 Prettier 的规则冲突;配置 Husky 和 lint-staged,在提交代码前自动执行 ESLint 和 Prettier 检查,阻止不规范代码提交,示例:// package.json "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "*.{js,ts,vue}": ["eslint --fix", "prettier --write"] }
-
工程化配置:构建与优化
构建工具配置直接影响开发体验和打包性能。以 Vite 为例,配置vite.config.js
:设置resolve.alias
简化路径引用(如@
指向src
目录),配置server.proxy
解决开发跨域,设置build.outDir
指定打包输出目录;生产环境开启terser
压缩代码、rollupOptions
拆分公共库(如 Vue、React)。此外,配置index.html
作为入口,设置 favicon 和元数据(如 title、description)。 -
基础功能配置
集成核心功能模块:路由管理(Vue Router/React Router),配置路由规则和导航守卫;状态管理(Pinia/Redux),根据项目复杂度选择是否使用;API 请求封装,基于 Axios 封装请求拦截器(添加 token)和响应拦截器(统一错误处理);样式解决方案,选择 CSS Modules、Less/Sass 或 UI 库(Element Plus/Ant Design),并配置全局样式变量。 -
额外加分配置
进阶配置提升项目质量:添加单元测试(Jest+Vue Test Utils)确保核心功能稳定;配置 CI/CD 流程(如 GitHub Actions)实现自动部署;使用commitlint
规范提交信息(如feat: 新增登录功能
),便于版本管理和 Changelog 生成。
记忆法:按 “初始化 - 规范 - 构建 - 功能” 四步流程记忆。先搭骨架(初始化),再定规矩(规范),接着调工具(构建),最后填内容(功能),逐步推进确保配置全面。