数组去重的方法有哪些?
在 JavaScript 中,数组去重是一个常见的操作,有多种方法可以实现这一目标。每种方法都有其适用场景和性能特点,下面将详细介绍几种主要的去重方法。
使用 Set 数据结构
Set 是 ES6 引入的一种新数据结构,它类似于数组,但成员的值都是唯一的,没有重复的值。利用这一特性,可以非常简洁地实现数组去重。
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]
这种方法的优点是代码简洁,性能高效,时间复杂度为 O (n)。缺点是它只能对基本数据类型(如数字、字符串、布尔值)进行去重,对于引用类型(如对象、数组)无法正确去重,因为 Set 判断元素是否重复是基于值的比较,而引用类型比较的是引用地址。
使用 filter () 方法和 indexOf ()
利用数组的 filter () 方法结合 indexOf () 可以实现去重。filter () 方法会创建一个新数组,其中包含所有通过测试的元素。对于每个元素,如果它在数组中第一次出现的位置等于当前索引,则保留该元素。
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.filter((item, index) => {
return arr.indexOf(item) === index;
});
console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]
这种方法的优点是兼容性好,在旧版本的浏览器中也能正常工作。缺点是时间复杂度较高,为 O (n²),因为对于每个元素都需要调用 indexOf () 方法在数组中查找,当数组较大时性能较差。
使用 reduce () 方法
reduce () 方法可以对数组中的每个元素执行一个 reducer 函数,并将结果汇总为单个值。可以利用 reduce () 方法来构建一个新数组,在构建过程中检查元素是否已经存在。
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = arr.reduce((acc, current) => {
if (!acc.includes(current)) {
acc.push(current);
}
return acc;
}, []);
console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]
这种方法的优点是代码简洁,逻辑清晰。缺点同样是时间复杂度较高,为 O (n²),因为在每次迭代中都需要调用 includes () 方法来检查元素是否存在。
处理对象数组去重
当数组中的元素是对象时,上述方法无法直接去重,因为对象是引用类型。此时需要根据对象的某个属性值来进行去重。
const arr = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' }
];
const uniqueArr = [...new Map(arr.map(item => [item.id, item])).values()];
console.log(uniqueArr);
// 输出: [
// { id: 1, name: 'Alice' },
// { id: 2, name: 'Bob' }
// ]
这种方法利用 Map 的键唯一性,将对象的某个属性值作为键,对象本身作为值,从而实现根据属性值去重。
如何判断一个值是数组还是对象?
在 JavaScript 中,判断一个值是数组还是对象是一个常见的需求,因为数组和普通对象在某些操作上有不同的行为。JavaScript 提供了多种方式来进行判断,每种方式都有其优缺点和适用场景。
使用 typeof 操作符
typeof 操作符返回一个表示数据类型的字符串。然而,对于数组和普通对象,typeof 操作符返回的都是 "object",因此无法区分它们。
const arr = [1, 2, 3];
const obj = { a: 1, b: 2 };
console.log(typeof arr); // 输出: "object"
console.log(typeof obj); // 输出: "object"
由此可见,typeof 操作符不能用于区分数组和对象。
使用 instanceof 操作符
instanceof 操作符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。通过判断一个值是否是 Array 的实例,可以确定它是否为数组。
const arr = [1, 2, 3];
const obj = { a: 1, b: 2 };
console.log(arr instanceof Array); // 输出: true
console.log(obj instanceof Array); // 输出: false
然而,instanceof 操作符有一些局限性。在不同的 iframe 或 window 环境中创建的数组,可能无法通过 instanceof 正确判断,因为它们的原型链不同。
使用 Array.isArray () 方法
Array.isArray () 是 ES5.1 引入的一个静态方法,用于判断一个值是否为数组。这是判断数组最直接和可靠的方法。
const arr = [1, 2, 3];
const obj = { a: 1, b: 2 };
console.log(Array.isArray(arr)); // 输出: true
console.log(Array.isArray(obj)); // 输出: false
Array.isArray () 方法的优点是简单、直接、可靠,并且能正确处理跨 iframe 或 window 环境的数组。
使用 Object.prototype.toString.call ()
每个对象都有一个内部属性 [[Class]],它表示对象的类型。可以通过 Object.prototype.toString.call () 方法来获取这个内部属性的值,从而判断对象的类型。
const arr = [1, 2, 3];
const obj = { a: 1, b: 2 };
console.log(Object.prototype.toString.call(arr)); // 输出: "[object Array]"
console.log(Object.prototype.toString.call(obj)); // 输出: "[object Object]"
这种方法的优点是非常准确,能够区分各种类型的对象,包括内置对象和自定义对象。缺点是代码稍微复杂一些,需要调用 toString () 方法并通过 call () 来改变 this 指向。
使用 constructor 属性
每个对象都有一个 constructor 属性,它指向创建该对象的构造函数。可以通过检查 constructor 属性来判断一个值是否为数组。
const arr = [1, 2, 3];
const obj = { a: 1, b: 2 };
console.log(arr.constructor === Array); // 输出: true
console.log(obj.constructor === Array); // 输出: false
然而,这种方法也有局限性。如果对象的 constructor 属性被修改,或者在不同的环境中创建的对象,可能会导致判断不准确。
解释 JavaScript 事件循环机制(Event Loop)
JavaScript 事件循环机制是 JavaScript 运行时环境的核心组成部分,它负责处理异步操作,使得 JavaScript 能够在单线程的情况下处理并发任务。理解事件循环机制对于编写高效、无阻塞的 JavaScript 代码至关重要。
JavaScript 的单线程特性
JavaScript 是单线程的,这意味着同一时间只能执行一个任务。这种设计主要是为了避免在浏览器环境中操作 DOM 时出现竞态条件。如果 JavaScript 是多线程的,多个线程同时修改 DOM 可能会导致页面状态不一致。
然而,单线程也带来了一个问题:如果有一个耗时的操作(如网络请求、文件读取),整个程序会被阻塞,用户界面会变得卡顿甚至无响应。为了解决这个问题,JavaScript 引入了异步编程模型。
异步操作与回调函数
JavaScript 中的异步操作(如 setTimeout、Promise、fetch API 等)允许程序在执行耗时操作时不会阻塞主线程。当异步操作完成后,会通过回调函数通知主线程。
例如,使用 setTimeout 设置一个定时器:
console.log('开始');
setTimeout(() => {
console.log('定时器回调执行');
}, 1000);
console.log('结束');
这段代码的执行顺序是:先打印 "开始",然后设置定时器,接着打印 "结束",最后在 1 秒后执行定时器的回调函数并打印 "定时器回调执行"。
事件循环的基本原理
JavaScript 事件循环机制的核心是由以下几个部分组成:
-
调用栈(Call Stack):用于执行同步代码。当执行一个函数时,会将该函数压入调用栈;函数执行完毕后,会从调用栈中弹出。
-
任务队列(Task Queue):也称为回调队列,用于存放异步操作完成后的回调函数。任务队列分为宏任务队列(MacroTask Queue)和微任务队列(MicroTask Queue)。
-
事件循环(Event Loop):不断从任务队列中取出任务并放入调用栈执行。
宏任务和微任务
JavaScript 中的异步任务分为宏任务和微任务:
-
宏任务:包括 setTimeout、setInterval、setImmediate(Node.js 环境)、requestAnimationFrame(浏览器环境)、I/O 操作、UI 渲染等。
-
微任务:包括 Promise.then ()、MutationObserver、process.nextTick(Node.js 环境)等。
事件循环的执行顺序是:先执行调用栈中的所有同步代码,然后检查微任务队列,如果有微任务,则依次执行所有微任务,直到微任务队列为空;接着从宏任务队列中取出一个宏任务执行,执行完毕后,再次检查微任务队列,重复上述过程。
事件循环的工作流程
事件循环的工作流程可以概括为以下几个步骤:
-
执行调用栈中的所有同步代码。
-
当调用栈为空时,检查微任务队列。如果微任务队列中有任务,则依次执行所有微任务,直到微任务队列为空。
-
从宏任务队列中取出一个宏任务执行。
-
宏任务执行完毕后,再次检查微任务队列,执行所有微任务。
-
重复步骤 3 和 4,不断循环。
这种机制确保了异步任务能够在合适的时机执行,同时不会阻塞主线程,从而实现了 JavaScript 的非阻塞特性。
什么是闭包?闭包的应用场景有哪些?
闭包是 JavaScript 中一个非常重要且强大的特性,它允许函数访问并操作其外部函数作用域中的变量,即使该外部函数已经执行完毕。闭包的存在使得 JavaScript 中的函数具有了 "记忆性",能够保留其创建时的环境。
闭包的定义与原理
闭包的形成需要满足以下两个条件:
-
函数嵌套:内部函数定义在外部函数内部。
-
内部函数引用外部函数作用域中的变量。
当内部函数被外部函数返回时,它会捕获并保留外部函数的作用域链,即使外部函数已经执行完毕,内部函数仍然可以访问外部函数作用域中的变量。
function outer() {
const x = 10;
function inner() {
console.log(x); // 内部函数引用了外部函数的变量x
}
return inner; // 返回内部函数
}
const closure = outer(); // 外部函数执行完毕
closure(); // 输出: 10
在这个例子中,inner 函数是一个闭包,它捕获了外部函数 outer 的变量 x。当 outer 函数执行完毕后,变量 x 并没有被销毁,而是被闭包 inner 保留了下来。
闭包的应用场景
闭包在 JavaScript 中有许多实际应用场景,下面介绍几个常见的场景。
实现私有变量和方法
闭包可以用来实现私有变量和方法,这是 JavaScript 中模拟类的私有成员的一种常见方式。
function createCounter() {
let count = 0; // 私有变量
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.getCount()); // 输出: 0
counter.increment();
console.log(counter.getCount()); // 输出: 1
counter.decrement();
console.log(counter.getCount()); // 输出: 0
在这个例子中,count 变量是私有的,外部无法直接访问或修改它,只能通过返回的对象中的方法来操作。
函数柯里化
函数柯里化是指将一个多参数函数转换为一系列单参数函数的过程。闭包可以用来实现函数柯里化。
function add(a, b) {
return a + b;
}
// 柯里化后的add函数
function curriedAdd(a) {
return function(b) {
return a + b;
};
}
const add5 = curriedAdd(5);
console.log(add5(3)); // 输出: 8
console.log(add5(7)); // 输出: 12
在这个例子中,curriedAdd 函数返回了一个闭包,该闭包捕获了参数 a 的值。每次调用闭包时,都会使用捕获的 a 值和传入的参数 b 进行计算。
事件处理与回调函数
闭包在事件处理和回调函数中也非常有用,可以用来保存事件处理函数的状态。
function createButton() {
const button = document.createElement('button');
button.textContent = '点击';
let count = 0;
button.addEventListener('click', function() {
count++;
button.textContent = `点击了 ${count} 次`;
});
return button;
}
document.body.appendChild(createButton());
在这个例子中,事件处理函数是一个闭包,它捕获了 count 变量。每次点击按钮时,count 的值都会增加,并更新按钮的文本内容。
模块化设计
闭包可以用来实现模块化设计,将相关的变量和函数封装在一个闭包中,避免全局变量污染。
const MyModule = (function() {
// 私有变量和函数
const privateVariable = '私有变量';
function privateFunction() {
return '私有函数';
}
// 公开的接口
return {
publicMethod() {
return `访问 ${privateVariable} 和 ${privateFunction()}`;
}
};
})();
console.log(MyModule.publicMethod()); // 输出: "访问 私有变量 和 私有函数"
在这个例子中,立即执行函数表达式(IIFE)返回了一个对象,该对象包含了公开的方法。这些方法可以访问闭包中的私有变量和函数,而外部无法直接访问这些私有成员。
手写深拷贝函数,需考虑数组、对象、循环引用等场景。
深拷贝是 JavaScript 中一个重要的概念,它指的是创建一个新对象或数组,其内容与原对象或数组完全相同,但在内存中占据不同的空间。这意味着修改新对象不会影响原对象,反之亦然。实现一个完整的深拷贝函数需要考虑多种场景,包括基本数据类型、对象、数组、循环引用等。
基础版本的深拷贝函数
下面是一个基础版本的深拷贝函数,它可以处理基本数据类型、普通对象和数组:
function deepClone(target) {
// 处理基本数据类型(包括null)
if (typeof target !== 'object' || target === null) {
return target;
}
// 处理数组
if (Array.isArray(target)) {
const clone = [];
for (let i = 0; i < target.length; i++) {
clone[i] = deepClone(target[i]);
}
return clone;
}
// 处理普通对象
const clone = {};
for (const key in target) {
if (target.hasOwnProperty(key)) {
clone[key] = deepClone(target[key]);
}
}
return clone;
}
这个函数的工作原理是:首先检查目标是否为基本数据类型,如果是则直接返回;然后检查是否为数组,如果是则创建一个新数组,并递归地对每个元素进行深拷贝;最后处理普通对象,创建一个新对象,并递归地对每个属性值进行深拷贝。
处理循环引用
基础版本的深拷贝函数存在一个问题,就是无法处理循环引用。循环引用指的是对象之间相互引用,形成一个闭环。如果不处理循环引用,深拷贝函数会陷入无限递归,最终导致栈溢出错误。
为了处理循环引用,可以使用一个 WeakMap 来记录已经拷贝过的对象。在递归拷贝之前,先检查对象是否已经被拷贝过,如果是则直接返回之前拷贝的对象,避免无限递归。
function deepClone(target, map = new WeakMap()) {
// 处理基本数据类型(包括null)
if (typeof target !== 'object' || target === null) {
return target;
}
// 检查是否已经拷贝过
if (map.has(target)) {
return map.get(target);
}
// 处理数组
if (Array.isArray(target)) {
const clone = [];
map.set(target, clone); // 记录已经拷贝的数组
for (let i = 0; i < target.length; i++) {
clone[i] = deepClone(target[i], map);
}
return clone;
}
// 处理普通对象
const clone = {};
map.set(target, clone); // 记录已经拷贝的对象
for (const key in target) {
if (target.hasOwnProperty(key)) {
clone[key] = deepClone(target[key], map);
}
}
return clone;
}
在这个改进版本中,我们添加了一个 WeakMap 参数map
,用于记录已经拷贝过的对象。在处理对象或数组之前,先检查map
中是否已经存在该对象,如果存在则直接返回对应的拷贝对象。这样就避免了循环引用导致的无限递归问题。
处理特殊对象类型
除了普通对象和数组,JavaScript 中还有许多特殊类型的对象,如 Date、RegExp、Set、Map 等。完整的深拷贝函数还需要处理这些特殊类型的对象。
function deepClone(target, map = new WeakMap()) {
// 处理基本数据类型(包括null)
if (typeof target !== 'object' || target === null) {
return target;
}
// 处理特殊对象类型
if (target instanceof Date) {
return new Date(target.getTime());
}
if (target instanceof RegExp) {
return new RegExp(target);
}
// 处理Set
if (target instanceof Set) {
const clone = new Set();
target.forEach(value => {
clone.add(deepClone(value, map));
});
return clone;
}
// 处理Map
if (target instanceof Map) {
const clone = new Map();
target.forEach((value, key) => {
clone.set(key, deepClone(value, map));
});
return clone;
}
// 检查是否已经拷贝过
if (map.has(target)) {
return map.get(target);
}
// 处理数组
if (Array.isArray(target)) {
const clone = [];
map.set(target, clone);
for (let i = 0; i < target.length; i++) {
clone[i] = deepClone(target[i], map);
}
return clone;
}
// 处理普通对象
const clone = {};
map.set(target, clone);
for (const key in target) {
if (target.hasOwnProperty(key)) {
clone[key] = deepClone(target[key], map);
}
}
return clone;
}
在这个最终版本中,我们添加了对 Date、RegExp、Set 和 Map 等特殊对象类型的处理。对于每种特殊对象类型,我们都使用相应的构造函数创建一个新对象,并复制原对象的内容。同时,仍然保留了对循环引用的处理,确保函数在面对复杂对象结构时也能正常工作。
通过这个深拷贝函数,我们可以安全地复制包含各种数据类型和复杂结构的对象,而不用担心修改新对象会影响原对象。
箭头函数和普通函数的区别有哪些?箭头函数能否使用 new 操作符?为什么?
箭头函数与普通函数在语法和行为上存在多方面差异,这些差异源于它们不同的设计目标和适用场景。
首先是语法简洁性 。箭头函数采用更紧凑的语法格式,省略了function
关键字,通过=>
定义函数体。当参数仅有一个时,括号可以省略;若函数体为表达式,还能省略大括号并隐式返回结果。例如:
// 普通函数
function add(a, b) { return a + b; }
// 箭头函数
const add = (a, b) => a + b;
而普通函数需完整书写function
关键字、参数列表和大括号,语法结构更为传统。
其次是 this 指向的差异 。普通函数的this
在调用时动态绑定,取决于函数的调用上下文 ------ 作为对象方法调用时指向对象,普通函数调用时指向全局对象(浏览器中为window
,严格模式下为undefined
),通过call
/apply
/bind
方法可显式改变其指向。箭头函数则不具备自身的this
,其内部的this
继承自外层作用域,在定义时就已确定,无法通过call
/apply
/bind
修改。这一特性使其在处理回调函数或嵌套函数时能更便捷地保留外层作用域的this
,避免传统函数中常见的this
指向混乱问题。
关于参数处理 ,普通函数支持arguments
对象,可获取所有传入参数,还能通过rest参数(...args)
收集剩余参数。箭头函数不支持arguments
对象,但可使用rest参数
,其内部的arguments
会引用外层作用域的arguments
。例如:
function普通函数() { console.log(arguments); }
const箭头函数 = () => { console.log(arguments); };
// 箭头函数内的arguments指向外层作用域(若有),否则报错
在构造函数方面 ,箭头函数不能作为构造函数使用,无法通过new
操作符创建实例。这是因为箭头函数没有prototype
属性,也不存在constructor
方法。当对箭头函数使用new
时会抛出错误,而普通函数作为构造函数时,this
会指向新创建的实例,且可通过prototype
添加原型方法。
此外,箭头函数不支持super
和new.target
。在继承场景中,普通函数可通过super
调用父类方法,new.target
可用于检测函数是否作为构造函数被调用;箭头函数由于没有自身的this
和构造函数行为,无法使用这些特性。
总结来看,箭头函数的设计初衷是为了简化简单函数的书写,尤其适用于需要保持this
指向一致的回调函数、对象方法以外的场景。而普通函数在需要动态this
绑定、作为构造函数或使用arguments
对象时更为合适。
列举 ES6 的新特性(如 let/const、箭头函数、Promise、Proxy、Map/Set、解构赋值等)
ES6(ECMAScript 2015)带来了众多重要特性,显著提升了 JavaScript 的语法表现力和开发效率,以下是核心特性的详细说明:
1. 块级作用域声明:let 和 const
let
和const
是 ES6 引入的新变量声明方式,用于替代传统的var
。let
声明的变量具有块级作用域(如if
语句、for
循环体),避免了var
的函数作用域导致的变量提升和意外覆盖问题。const
用于声明常量,一旦赋值便不可更改,其作用域同样为块级,且要求声明时必须初始化。例如:
{
let a = 1;
const b = 2;
a = 3; // 允许
b = 4; // 报错,常量不可重新赋值
}
2. 箭头函数(Arrow Functions)
箭头函数提供了更简洁的函数定义语法,省略了function
关键字,通过=>
符号连接参数和函数体。其核心特点包括:没有自身的this
,this
继承自外层作用域;不支持arguments
对象,可使用rest参数(...args)
;不能作为构造函数,无法通过new
创建实例。箭头函数适用于需要固定this
指向的回调函数,如数组的map
、filter
方法回调。
3. 解构赋值(Destructuring Assignment)
解构赋值允许从数组或对象中提取值,并将其赋值给变量。数组解构按位置匹配,对象解构按键名匹配,可嵌套使用并设置默认值。例如:
// 数组解构
const [a, b, c = 3] = [1, 2]; // a=1, b=2, c=3
// 对象解构
const { name, age = 18 } = { name: "Alice" }; // name="Alice", age=18
4. Promise 对象
Promise 是处理异步操作的标准化解决方案,用于替代传统的回调函数,避免 "回调地狱"。它代表一个异步操作的最终状态(成功fulfilled
或失败rejected
),通过then()
方法处理成功结果,catch()
方法处理错误。Promise 支持链式调用,可组合多个异步操作。例如:
fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Error:", error));
5. 类(Class)语法
ES6 引入基于class
关键字的面向对象编程语法,简化了原型链继承的写法。class
包含构造方法constructor
和原型方法,通过extends
关键字实现继承,通过super
调用父类方法。例如:
class Animal {
constructor(name) { this.name = name; }
speak() { return "Sound"; }
}
class Dog extends Animal {
speak() { return `${super.speak()} Woof!`; }
}
6. 模块系统(Modules)
ES6 正式支持原生模块系统,通过export
导出模块内容,import
导入其他模块。模块具有独立的作用域,避免全局变量污染。例如:
// module.js
export const name = "Module";
export function greet() { return "Hello"; }
// app.js
import { name, greet } from "./module.js";
console.log(greet(), name); // 输出 "Hello Module"
7. 迭代器(Iterator)和可迭代对象(Iterable)
迭代器是一种接口,用于遍历数据结构中的元素,通过Symbol.iterator
方法实现。数组、字符串、Set、Map 等内置类型均为可迭代对象,可通过for...of
循环遍历。自定义对象也可实现迭代器接口,使其支持for...of
遍历。
8. Proxy 和 Reflect
Proxy
用于创建对象的代理,可拦截对象的各种操作(如属性读取、赋值、函数调用等),实现数据响应式、权限控制等功能。Reflect
是一个内置对象,提供了与Proxy
拦截方法对应的默认行为,用于替代某些操作的默认语法(如delete
、apply
等)。
9. Map 和 Set 数据结构
- Map :键值对集合,键可以是任意类型(包括对象),通过
get
、set
、has
等方法操作,适合需要灵活键类型的场景。 - Set :唯一值集合,自动去重,适合存储不重复的数据,可通过
add
、delete
、has
等方法操作。
10. 模板字符串(Template Literals)
模板字符串使用反引号(`````)包裹,支持变量插值(${变量}
)和多行文本,替代了传统字符串拼接的繁琐方式。例如:
const name = "Bob";
const age = 25;
const message = `Name: ${name}, Age: ${age}`; // 输出 "Name: Bob, Age: 25"
11. Rest 参数和扩展运算符
-
Rest 参数(...args) :用于函数参数中,将多余的参数收集为数组,替代传统的
arguments
对象。function sum(...nums) { return nums.reduce((a, b) => a + b, 0); }
-
扩展运算符(...) :用于展开数组或对象,例如合并数组、函数调用时传递参数等。
const arr1 = [1, 2], arr2 = [3, 4]; const merged = [...arr1, ...arr2]; // [1, 2, 3, 4]
12. Symbol 原始数据类型
Symbol 是一种唯一的标识符,用于创建对象的唯一属性键,避免属性名冲突。通过Symbol()
创建,例如:
const key = Symbol("unique");
const obj = { [key]: "value" };
这些特性共同构成了 ES6 的核心内容,推动 JavaScript 向更现代、更高效的编程语言演进,广泛应用于前端开发的各个领域。
Proxy 对象的作用是什么?除了在 Vue3 中使用,还有哪些实际应用场景?
Proxy 是 ES6 引入的强大特性,用于创建一个对象的代理,允许通过自定义 "陷阱"(trap)拦截并自定义对象的基本操作(如属性访问、赋值、枚举、函数调用等)。其核心作用是在不直接修改原对象的前提下,对对象的行为进行一层抽象和控制,实现数据劫持、权限控制、日志记录等功能。
Proxy 的主要作用包括:
-
数据响应式(Reactivity)
这是 Proxy 最典型的应用场景,Vue3 正是基于 Proxy 实现了响应式系统。通过拦截对象的属性读取(
get
陷阱)和赋值(set
陷阱),可以自动追踪依赖并触发更新,相比 Vue2 使用的Object.defineProperty
,Proxy 支持对数组和对象的深层次响应式处理,且无需预先知道所有属性。 -
访问控制与权限管理
通过拦截
get
、set
、deleteProperty
等陷阱,可以限制对对象属性的访问或修改。例如,创建一个只读代理,禁止修改某些属性:const data = { name: "Alice", age: 25 }; const handler = { set(target, key, value) { if (key === "age" && value < 0) { throw new Error("Age cannot be negative"); } return Reflect.set(target, key, value); } }; const proxy = new Proxy(data, handler); proxy.age = -5; // 抛出错误
-
日志记录与调试
拦截对象操作并添加日志,可用于追踪对象的使用情况,辅助调试。例如,记录每次属性访问和修改的时间、值等信息:
const handler = { get(target, key) { console.log(`Accessing property: ${key}`); return Reflect.get(target, key); }, set(target, key, value) { console.log(`Updating ${key} from ${target[key]} to ${value}`); return Reflect.set(target, key, value); } }; const proxy = new Proxy(obj, handler); proxy.name = "Bob"; // 输出更新日志
-
惰性加载(Lazy Loading)
在访问对象属性时动态加载数据,避免提前请求或计算资源。例如,当访问某个属性时才从服务器获取数据:
const resource = { data: null, loadData() { this.data = fetch("https://api.example.com/data").then(res => res.json()); } }; const handler = { get(target, key) { if (key === "data" && target.data === null) { target.loadData(); } return Reflect.get(target, key); } }; const proxy = new Proxy(resource, handler); // 首次访问data时触发加载 proxy.data.then(data => console.log(data));
-
函数参数校验
拦截函数调用(
apply
陷阱),对传入的参数进行校验,确保符合预期格式。例如,限制函数参数必须为数字:const add = (a, b) => a + b; const handler = { apply(target, thisArg, args) { if (!args.every(arg => typeof arg === "number")) { throw new Error("Parameters must be numbers"); } return target.apply(thisArg, args); } }; const proxy = new Proxy(add, handler); proxy("1", 2); // 抛出错误
-
对象序列化与转换
在读取对象属性时自动进行格式转换,例如将日期对象转换为字符串:
const data = { birthDate: new Date(2000, 0, 1) }; const handler = { get(target, key) { const value = Reflect.get(target, key); if (value instanceof Date) { return value.toISOString(); } return value; } }; const proxy = new Proxy(data, handler); console.log(proxy.birthDate); // 输出日期字符串 "2000-01-01T00:00:00.000Z"
-
代理模式实现
通过 Proxy 创建代理对象,实现设计模式中的代理模式,例如虚拟代理(延迟创建昂贵对象)、缓存代理(缓存函数调用结果)等。
其他实际应用场景举例:
- 数组操作拦截 :监控数组的
push
、pop
等方法调用,统计数组变化次数或限制数组长度。 - 只读代理:创建不可修改的对象副本,防止数据被意外篡改。
- 自定义迭代器 :通过拦截
Symbol.iterator
陷阱,为非可迭代对象添加迭代器支持。 - 性能监控:在对象方法调用前后添加性能统计代码,分析方法执行耗时。
Proxy 的灵活性使其在需要动态控制对象行为的场景中具有广泛应用。需要注意的是,Proxy 的兼容性需结合实际项目需求,且过度使用可能带来一定的性能开销,需根据场景权衡选择。
Map 和 Object 的区别是什么?各自的适用场景有哪些?
Map 和 Object 都是用于存储键值对的数据结构,但在设计理念、功能特性和适用场景上存在显著差异。以下从多个维度对比分析两者的区别,并说明各自的适用场景。
一、键的类型与限制
-
Object:
-
键只能是字符串或 Symbol 类型(非字符串类型会被自动转换为字符串)。例如,使用数字作为键时会被转为字符串
"1"
,使用对象作为键时会被转为"[object Object]"
。 -
示例:
const obj = { }; obj[1] = "one"; // 键为字符串"1" obj[{ a: 1 }] = "value"; // 键为字符串"[object Object]"
-
-
Map:
-
键可以是任意类型(包括对象、函数、原始值等),且键的相等性基于
SameValueZero
算法(与===
类似,但NaN
等于自身)。 -
示例:
const map = new Map(); map.set(1, "one"); // 键为数字1 const keyObj = { a: 1 }; map.set(keyObj, "value"); // 键为对象引用
-
二、数据初始化与遍历顺序
-
Object:
- 属性没有内置的顺序(ES6 之后规范要求对象属性按定义顺序遍历,但早期环境可能不保证)。
- 初始化属性需逐个添加,或通过对象字面量一次性定义。
- 遍历方式包括
for...in
(遍历自身及原型链属性,需配合hasOwnProperty
过滤)、Object.keys()
(自身可枚举字符串键)、Object.getOwnPropertySymbols()
(自身 Symbol 键)、Object.values()
/Object.entries()
等。
-
Map:
- 键值对按插入顺序排列,遍历时遵循插入顺序。
- 可通过构造函数接收一个包含键值对的数组(如
[[key1, val1], [key2, val2]]
)进行初始化。 - 遍历方式包括
for...of
(直接遍历键值对)、map.keys()
(键)、map.values()
(值)、map.entries()
(键值对),且默认迭代器为entries()
。
三、属性与方法
-
Object:
- 自身拥有原型属性(如
toString
、hasOwnProperty
等),可能与用户定义的键冲突。 - 操作方法需通过
Object
对象的静态方法实现,例如:Object.hasOwn(obj, key)
:判断是否包含指定键(ES2022 新增,替代hasOwnProperty
)。delete obj.key
:删除属性。obj.key = value
:添加 / 修改属性。
- 自身拥有原型属性(如
-
Map:
- 自身方法集中在实例上,包括:
set(key, value)
:设置键值对,返回 Map 实例,支持链式调用。get(key)
:获取对应值,键不存在时返回undefined
。has(key)
:判断是否存在键。delete(key)
:删除键值对,返回布尔值表示是否成功。clear()
:清空所有键值对。
- 没有原型属性干扰,键名更安全。
- 自身方法集中在实例上,包括:
四、内存与性能
-
Object:
- 适合存储少量键值对,尤其是键为字符串且需与 JSON 兼容的场景(JSON 仅支持字符串键)。
- 对于大量动态键,可能因属性扩散(dictionary mode)导致性能下降。
-
Map:
- 更适合存储大量动态键(如对象键),内部实现为哈希表,插入、删除、查询的平均时间复杂度为 O (1)。
- 在频繁增删操作的场景中性能优于 Object,尤其当键为对象时无需转换为字符串,减少了隐式类型转换的开销。
五、适用场景对比
场景 | Object 适用 | Map 适用 |
---|---|---|
键为字符串 / Symbol | 推荐使用,符合传统对象用法,且可直接通过字面量初始化。 | 也可使用,但初始化更繁琐,适合需要按顺序遍历的场景。 |
键为任意类型(如对象) | 不推荐,键会被转为字符串,无法通过对象引用精确匹配。 | 强烈推荐,支持对象作为键,按引用相等性匹配。 |
需要保持插入顺序 | ES6 + 环境下可行,但遍历需注意顺序保证(部分旧环境不支持)。 | 推荐,内置顺序保证,遍历顺序与插入一致。 |
频繁增删操作 | 性能可能较低,尤其当键为动态字符串时。 | 性能更优,内部哈希表优化了增删操作。 |
与 JSON 交互 | 必须使用,因为 JSON 仅支持字符串键和基本类型值。 | 不适用,需先转换为 Object(如Object.fromEntries(map) )。 |
简单配置项存储 | 推荐,通过字面量简洁明了。 | 可选,但可能过度设计。 |
复杂数据结构(如缓存) | 若键为字符串且需顺序无关,可使用;若键为对象或需顺序,不适用。 | 推荐,尤其适合以对象为键的缓存场景(如 DOM 节点作为键关联数据)。 |
Map 和 Set 的常用方法及使用场景
Map 和 Set 是 ES6 引入的两种集合类型,分别用于键值对存储和唯一值集合。它们提供了高效的数据操作方法,适用于不同的业务场景。以下详细介绍两者的常用方法及典型应用场景。
一、Map 的常用方法及使用场景
Map 是键值对的有序集合,键可以是任意类型,支持按插入顺序遍历。
常用方法
-
new Map([iterable])
-
作用:创建 Map 实例,可选参数为包含键值对的可迭代对象(如数组数组)。
-
示例 :
const map = new Map([["a", 1], ["b", 2]]); // 初始化为{a:1, b:2}
-
-
set(key, value)
-
作用:设置键值对,返回 Map 实例,支持链式调用。
-
示例 :
map.set("c", 3).set("d", 4); // 链式添加多个键值对
-
-
get(key)
-
作用 :获取指定键的值,若键不存在则返回
undefined
。 -
示例 :
console.log(map.get("a")); // 输出1 console.log(map.get("e")); // 输出undefined
-
-
has(key)
-
作用:判断是否存在指定键,返回布尔值。
-
示例 :
console.log(map.has("b")); // 输出true
-
-
delete(key)
-
作用:删除指定键值对,返回布尔值表示是否成功。
-
示例 :
map.delete("c"); // 删除键"c"
-
-
clear()
-
作用:清空 Map 中的所有键值对。
-
示例 :
map.clear(); // 清空所有数据
-
-
size
-
作用:获取 Map 中键值对的数量(只读属性)。
-
示例 :
console.log(map.size); // 输出当前键值对个数
-
-
遍历方法
-
keys()
:返回键的迭代器。 -
values()
:返回值的迭代器。 -
entries()
:返回键值对的迭代器(默认迭代器)。 -
for...of
遍历 :for (const [key, value] of map) { console.log(key, value); // 按插入顺序输出键值对 }
-
使用场景
-
键为对象的场景
需要以对象(如 DOM 元素、函数、其他对象)作为键时,Map 是最佳选择,因为 Object 会将非字符串键转为字符串,无法精确匹配对象引用。
示例:为每个 DOM 元素关联自定义数据:const button = document.querySelector("button"); const dataMap = new Map(); dataMap.set(button, { count: 0 }); button.addEventListener("click", () => { const data = dataMap.get(button); data.count++; });
-
需要保持插入顺序的键值对存储
当数据顺序至关重要时(如撤销 / 重做历史记录),Map 的有序性可确保遍历顺序与操作顺序一致。
-
频繁增删的大规模数据
Map 内部基于哈希表实现,增删操作的平均时间复杂度为 O (1),性能优于 Object,适合处理大量动态数据。
-
链式操作场景
set
方法返回 Map 实例,支持链式调用,可简化代码结构:map.set("a", 1).set("b", 2).set("c", 3);
二、Set 的常用方法及使用场景
Set 是唯一值的集合,自动去重,值可以是任意类型。
常用方法
-
new Set([iterable])
-
作用:创建 Set 实例,可选参数为可迭代对象,自动去重。
-
示例 :
const set = new Set([1, 2, 2, 3]); // 初始化为{1, 2, 3}
-
-
add(value)
-
作用:添加值到 Set,返回 Set 实例,支持链式调用。
-
示例 :
set.add(4).add(5); // 链式添加多个值
-
-
has(value)
-
作用:判断是否存在指定值,返回布尔值。
-
示例 :
console.log(set.has(3)); // 输出true
-
-
delete(value)
-
作用:删除指定值,返回布尔值表示是否成功。
-
示例 :
set.delete(2); // 删除值2
-
-
clear()
-
作用:清空 Set 中的所有值。
-
示例 :
set.clear(); // 清空所有数据
-
-
size
-
作用:获取 Set 中值的数量(只读属性)。
-
示例 :
console.log(set.size); // 输出当前值的个数
-
-
遍历方法
-
keys()
/values()
:返回值的迭代器(两者行为一致,因为 Set 中值即键)。 -
entries()
:返回包含值和自身的键值对迭代器(如[value, value]
)。 -
for...of
遍历 :for (const value of set) { console.log(value); // 按插入顺序输出值 }
-
使用场景
-
数组去重
利用 Set 自动去重的特性,快速实现数组去重,尤其适合处理复杂类型(如对象需注意:Set 判断对象相等基于引用,而非内容)。
示例:const arr = [1, 2, 2, 3, { a: 1 }, { a: 1 }]; const uniqueArr = [...new Set(arr)]; // 结果:[1, 2, 3, {a:1}, {a:1}](对象因引用不同未去重)
-
快速判断成员是否存在
需要频繁检查某个值是否在集合中时,Set 的
has
方法性能优于数组的includes
(时间复杂度为 O (1) vs O (n))。
示例:用户权限校验:const allowedUsers = new Set(["admin", "editor"]); function checkPermission(user) { return allowedUsers.has(user); }
-
交集、并集、差集运算
通过遍历 Set 实现集合运算,适用于数据筛选场景。
-
交集 :
const setA = new Set([1, 2, 3]); const setB = new Set([3, 4, 5]); const intersection = new Set([...setA].filter(x => setB.has(x))); // {3}
-
并集 :
const union = new Set([...setA, ...setB]); // {1,2,3,4,5}
-
-
存储临时状态或标记
需要记录已访问的节点、已处理的任务等场景,利用 Set 的唯一性避免重复操作。
示例:DFS 遍历图结构时标记已访问节点:const visited = new Set(); function dfs(node) { if (visited.has(node)) return; visited.add(node); // 处理节点... node.children.forEach(child => dfs(child)); }
-
类数组转换为集合
将参数对象、DOM 节点列表等类数组转换为 Set,方便操作:
const args = new Set(arguments); // 将函数参数转为Set const elements = new Set(document.querySelectorAll("div")); // DOM节点集合
总结对比
类型 | 核心特性 | 典型场景 | 关键方法 |
---|---|---|---|
Map | 键值对,键为任意类型,有序 | 对象键存储、有序数据、频繁增删、链式操作 | set /get /has /for...of |
Set | 唯一值集合,自动去重 | 数组去重、成员校验、集合运算、临时标记 | add /has /delete /size |
在实际开发中,若需要存储键值对且键为复杂类型,选择 Map;若只需存储唯一值或进行集合操作,选择 Set。两者结合使用可实现更复杂的数据管理,例如用 Map 的键为 Set 来存储多个关联的唯一值集合。
如何判断一个对象是否为空对象(需考虑原型链属性)?
判断一个对象是否为空对象需要综合考虑自身属性 和原型链属性 的情况,同时要明确 "空" 的定义:通常指对象既没有自身可枚举属性,也不继承原型链上的可枚举属性(若有特殊需求,可能仅需判断自身属性)。以下是具体方法及适用场景:
1. 仅判断自身可枚举属性(不考虑原型链)
最常用的方式是通过Object.keys()
获取对象自身的可枚举属性数组,若长度为 0 则为空。该方法不会遍历原型链,适用于仅需检查对象自身是否有属性的场景。
function isEmptyObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]' && Object.keys(obj).length === 0;
}
注意 :需先通过toString
判断是否为对象(排除null
、数组等),否则Object.keys(null)
会报错。
2. 判断自身所有属性(包括不可枚举属性)
若需检测对象自身的所有属性 (包括Object.defineProperty
定义的不可枚举属性),可使用Object.getOwnPropertyNames()
或Object.getOwnPropertySymbols()
,前者获取字符串键名,后者获取 Symbol 键名。
function isEmptyObjectDeep(obj) {
if (Object.prototype.toString.call(obj) !== '[object Object]') return true;
const ownProps = Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj));
return ownProps.length === 0;
}
场景:适用于需要严格检查对象自身是否存在任何属性(如框架内部对数据模型的初始化校验)。
3. 排除原型链可枚举属性
若需确保对象既无自身属性,也不继承原型链上的可枚举属性(例如自定义原型对象的场景),需手动遍历原型链。
function isEmptyObjectStrict(obj) {
if (Object.prototype.toString.call(obj) !== '[object Object]') return true;
let current = obj;
while (current !== null) {
if (Object.keys(current).length > 0) return false; // 自身有属性则非空
current = Object.getPrototypeOf(current); // 遍历原型链
}
return true;
}
原理 :通过Object.getPrototypeOf
逐层向上查找原型对象,若任意一层存在可枚举属性,则返回false
。该方法适用于严格隔离原型链影响的场景(如模拟 "纯净" 对象)。
4. 区分数组和对象
需注意:数组[]
通过Object.keys
判断时长度为 0,但数组本身有原型链属性(如push
、pop
)。若需求中 "空对象" 需排除数组,可在判断前增加类型校验:
function isEmptyObjectExcludingArray(obj) {
return Object.prototype.toString.call(obj) === '[object Object]' && Object.keys(obj).length === 0;
}
调用isEmptyObjectExcludingArray([])
会返回false
,因数组类型不满足[object Object]
。
5. 特殊场景:原型链污染攻击检测
在安全场景中,可能需要检测对象是否被原型链污染(即原型链上新增了可枚举属性)。此时可通过hasOwnProperty
结合for...in
遍历:
function hasPrototypePollution(obj) {
for (const key in obj) {
if (!obj.hasOwnProperty(key)) { // key来自原型链
return true;
}
}
return false;
}
该方法可检测原型链是否存在可枚举属性,但无法区分属性是否为对象自身所有,需结合具体需求使用。
解释 Promise 的作用及常用方法(如 then、catch、all、race 等)?
Promise 的作用 在于解决 JavaScript 异步编程中的 "回调地狱" 问题,通过链式调用 和统一的错误处理机制,让异步代码更易读、可维护。它表示一个异步操作的最终状态(成功 / 失败),并允许在状态变更时执行相应的回调函数。以下从核心概念、常用方法及应用场景展开说明:
一、Promise 的核心概念
-
三种状态
- pending(进行中):初始状态,异步操作未完成或未失败。
- fulfilled (已成功):异步操作完成,结果可用(通过
resolve
触发)。 - rejected (已失败):异步操作失败,原因可捕获(通过
reject
触发)。
状态一旦变更(pending
→fulfilled
或pending
→rejected
),便不可逆转,确保回调执行的确定性。
-
链式调用的本质
then
/catch
方法返回一个新的 Promise ,允许连续调用(如promise.then().then()
),避免层层嵌套回调。每个then
可接收两个参数:成功回调(onFulfilled
)和失败回调(onRejected
),后者可省略,失败会沿链向上传递。
二、常用方法及实战示例
1. then(onFulfilled, onRejected)
作用:注册成功或失败的回调函数,返回新 Promise。
-
成功场景 :获取异步操作结果并处理。
fetch('https://api.example.com/data') .then(response => response.json()) // 解析JSON .then(data => console.log('数据:', data)); // 处理数据
-
失败处理 :第二个参数或后续
catch
捕获错误。fetch('https://api.example.com/invalid') .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('请求失败:', error)); // 统一处理错误
2. catch(onRejected)
作用 :等价于then(undefined, onRejected)
,专门处理链中的错误,提升代码可读性。
asyncFunction()
.then(result => process(result))
.catch(error => {
logError(error); // 记录错误日志
return defaultResult; // 可返回新值让后续then接收
});
注意 :catch
会捕获前面所有未处理的错误 ,包括同步代码中的错误(如then
回调内的JSON.parse
抛错)。
3. Promise.all(iterable)
作用 :接收一个 Promise 数组,所有 Promise 都成功时 ,返回包含所有结果的数组;任意一个失败时,立即返回失败原因(第一个拒绝的 Promise 的理由)。
const promise1 = fetch('https://api1.com');
const promise2 = fetch('https://api2.com');
Promise.all([promise1, promise2])
.then(responses => responses.map(res => res.json()))
.then(datas => console.log('全部数据:', datas))
.catch(error => console.error('任一请求失败:', error));
场景:并行获取多个不相关的异步数据,需等待全部完成后再处理(如页面初始化时加载多个模块的数据)。
4. Promise.race(iterable)
作用 :接收一个 Promise 数组,第一个状态变更的 Promise 的结果(成功或失败)会成为最终结果。
const timeout = new Promise((_, reject) => {
setTimeout(() => reject('请求超时'), 5000);
});
const fetchData = fetch('https://api.example.com');
Promise.race([fetchData, timeout])
.then(response => response.json())
.catch(error => console.error('超时或失败:', error));
场景:设置请求超时机制,或在多个并行请求中取最快返回的结果(如负载均衡)。
5. Promise.allSettled(iterable)
(ES2020 新增)
作用 :等待所有 Promise 完成(无论成功或失败),返回一个数组,每个元素包含status
(fulfilled
/rejected
)和value
/reason
。
const promises = [Promise.resolve(1), Promise.reject('出错')];
Promise.allSettled(promises)
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('成功值:', result.value);
} else {
console.error('失败原因:', result.reason);
}
});
});
场景:需要确保所有异步操作执行完毕(如批量上传文件,无论成功与否都记录状态)。
6. Promise.resolve(value)
& Promise.reject(reason)
-
Promise.resolve
:将现有值 / Promise 转换为已成功的 Promise,用于统一异步接口。function getValue() { return Promise.resolve(42); // 等价于 new Promise(resolve => resolve(42)) }
-
Promise.reject
:创建一个已拒绝的 Promise,用于快速抛出错误。
三、与事件循环的关系
Promise 的回调(then/catch
)属于微任务(Microtask),会在当前宏任务(如同步代码、定时器回调)执行完毕后,下一个宏任务之前立即执行。这确保了异步操作的结果能以可预测的顺序处理,避免阻塞主线程。
四、最佳实践建议
- 统一错误处理 :在 Promise 链末尾添加
catch
,避免未捕获的错误导致程序崩溃。 - 避免内存泄漏 :若
then
中返回 Promise,需确保后续链有catch
处理,否则错误会被忽略。 - 合理使用并行操作 :
all
适用于强依赖所有结果的场景,race
用于取最快结果,allSettled
用于容忍部分失败的批量操作。
通过 Promise,开发者能以同步代码的思维组织异步逻辑,结合 async/await 语法糖(本质是 Promise 的语法封装),进一步提升异步代码的可读性和维护性,成为现代前端开发中处理异步操作的核心工具。
一道 Promise 代码的输出结果分析题(需结合事件循环机制)?
以下通过具体代码示例分析 Promise 与事件循环(Event Loop)的交互逻辑,重点理解 ** 宏任务(Macrotask)和微任务(Microtask)** 的执行顺序。
示例代码
console.log('start');
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => {
console.log('promise1 in timer1');
});
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
setTimeout(() => {
console.log('timer2 in promise1');
}, 0);
});
setTimeout(() => {
console.log('timer2');
Promise.resolve().then(() => {
console.log('promise2 in timer2');
});
}, 0);
console.log('end');
输出结果分析
按事件循环的执行规则,代码执行流程可分为以下阶段:
1. 同步代码执行(宏任务初始阶段)
console.log('start')
→ 输出:start
console.log('end')
→ 输出:end
- 定时器回调 :两个
setTimeout
(延迟 0ms)将回调函数加入宏任务队列。 - Promise 微任务 :第一个
Promise.resolve().then
回调加入微任务队列。
当前状态:
- 宏任务队列:
[timer1回调, timer2回调]
- 微任务队列:
[promise1回调]
2. 处理微任务队列
同步代码执行完毕后,事件循环会先清空微任务队列中的所有任务:
- 执行
promise1回调
:console.log('promise1')
→ 输出:promise1
- 回调中又创建一个
setTimeout
,其回调timer2 in promise1
加入宏任务队列末尾。
- 此时微任务队列已空,事件循环进入下一个宏任务。
当前状态:
- 宏任务队列:
[timer1回调, timer2回调, timer2 in promise1回调]
- 微任务队列:
[]
3. 执行第一个宏任务(timer1 回调)
- 执行
timer1回调
:console.log('timer1')
→ 输出:timer1
- 回调中创建
Promise.resolve().then
,其回调promise1 in timer1
加入微任务队列。
- 宏任务执行完毕后,事件循环再次处理微任务队列:
- 执行
promise1 in timer1回调
→ 输出:promise1 in timer1
- 执行
- 微任务队列再次清空。
当前状态:
- 宏任务队列:
[timer2回调, timer2 in promise1回调]
- 微任务队列:
[]
4. 执行第二个宏任务(timer2 回调)
- 执行
timer2回调
:console.log('timer2')
→ 输出:timer2
- 回调中创建
Promise.resolve().then
,其回调promise2 in timer2
加入微任务队列。
- 宏任务执行完毕后,处理微任务队列:
- 执行
promise2 in timer2回调
→ 输出:promise2 in timer2
- 执行
- 微任务队列再次清空。
当前状态:
- 宏任务队列:
[timer2 in promise1回调]
- 微任务队列:
[]
5. 执行第三个宏任务(timer2 in promise1 回调)
- 执行
timer2 in promise1回调
:console.log('timer2 in promise1')
→ 输出:timer2 in promise1
- 该回调无其他异步操作,宏任务队列清空。
最终输出顺序
start
end
promise1
timer1
promise1 in timer1
timer2
promise2 in timer2
timer2 in promise1
关键规则总结
-
宏任务与微任务的优先级:
- 宏任务包括:
setTimeout
、setInterval
、setImmediate
(Node.js)、I/O 操作、UI 渲染等。 - 微任务包括:
Promise.then/catch/finally
、MutationObserver
、process.nextTick
(Node.js)。 - 每完成一个宏任务,会立即处理所有微任务,再取下一个宏任务。
- 宏任务包括:
-
同一类型任务的执行顺序:
- 微任务按加入队列的顺序执行(如示例中先加入
promise1
回调,后加入timer1
中的微任务)。 - 宏任务同样按队列顺序执行(如示例中两个
setTimeout
按代码顺序加入队列,先执行timer1
再执行timer2
)。
- 微任务按加入队列的顺序执行(如示例中先加入
-
嵌套异步操作的影响:
- 在微任务(如
Promise.then
)中创建的宏任务(如setTimeout
),会被加入宏任务队列末尾,等待当前微任务队列处理完毕后,由后续宏任务处理。
- 在微任务(如
常见误区辨析
- 误区 1 :认为
setTimeout(0)
会立即执行。
实际上,0ms
是操作系统允许的最小延迟,回调仍需等待当前宏任务和微任务处理完毕才会执行。 - 误区 2 :认为 Promise 回调会在
setTimeout
之前立即执行。
正确逻辑是:同步代码→微任务队列→宏任务队列,因此 Promise 回调(微任务)会在当前宏任务(同步代码)结束后优先于setTimeout
(宏任务)执行。
通过此类示例可深入理解事件循环机制,在实际开发中合理安排异步操作顺序,避免因任务优先级导致的逻辑错误或性能问题(如微任务过多阻塞 UI 渲染)。
对函数式编程的理解(如纯函数、数据不可变、高阶函数等)?
函数式编程(Functional Programming,简称 FP)是一种以函数为核心 的编程范式,强调通过函数的组合和变换处理数据,而非依赖对象状态或命令式操作。其核心概念包括纯函数 、数据不可变 、高阶函数 、函数组合等,以下从核心思想、关键概念及实际应用展开说明:
一、核心思想:声明式编程与数据抽象
函数式编程倡导声明式编程风格 ,即描述 "做什么" 而非 "如何做",通过函数的组合简化逻辑复杂度。例如,相较于命令式的循环遍历数组,FP 更倾向于使用map
、filter
等高阶函数处理数据:
// 命令式:关注过程
const numbers = [1, 2, 3];
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
// 函数式:关注结果
const doubled = numbers.map(n => n * 2);
这种方式将具体实现封装在函数中,代码更简洁且易于推理。
二、关键概念解析
1. 纯函数(Pure Function)
定义:满足以下条件的函数:
-
相同输入必有相同输出(无副作用);
-
不修改外部状态 (输入参数不可变)。
示例:// 纯函数:输入输出确定,无副作用
function add(a, b) {
return a + b;
}// 非纯函数:依赖外部变量,输出不确定
let count = 0;
function impureAdd(b) {
return count++ + b; // 修改外部状态,副作用
}
作用:
- 可预测性:便于测试和调试;
- 可组合性:纯函数可作为 "积木" 自由组合(如
compose
、pipe
); - 并行安全:无共享状态,适合多线程或异步场景。
2. 数据不可变(Immutable Data)
禁止直接修改原始数据,如需变更需返回新数据。常见实现方式包括:
-
使用
展开运算符
(...
)创建新数组 / 对象:const arr = [1, 2, 3]; const newArr = [...arr, 4]; // 新建数组,原数组不变
-
使用 Immutable.js 等库实现持久化数据结构。
优势: -
避免副作用:防止意外修改导致的逻辑错误;
-
简化状态管理:如 Redux 通过不可变数据确保状态可追踪。
3. 高阶函数(Higher-Order Function)
接收函数作为参数或返回函数的函数,是 FP 的核心工具。常见类型包括:
-
函数作为参数 :
map
、filter
、reduce
等数组方法;const numbers = [1, 2, 3]; const evenNumbers = numbers.filter(n => n % 2 === 0); // filter接收函数参数
-
函数作为返回值 :柯里化(Currying)函数;
function add(a) { return function(b) { return a + b; }; } const add5 = add(5); // 返回函数,可延迟调用 add5(3); // 8
作用:
- 抽象通用逻辑:如防抖函数
debounce(fn)
接收函数并返回包装后的函数; - 实现函数组合:通过
compose
将多个函数串联执行(如compose(f, g, h)(x) = f(g(h(x)))
)。
4. 函数组合(Function Composition)
将多个函数组合成一个新函数,前一个函数的输出作为后一个函数的输入,分为compose
(从右向左)和pipe
(从左向右):
// 从右向左组合:compose(f, g, h)(x) = f(g(h(x)))
function compose(...fns) {
return x => fns.reverse().reduce((acc, fn) => fn(acc), x);
}
// 示例:先转大写,再添加前缀,最后输出
const process = compose(
str => `结果:${str}`,
str => str.toUpperCase(),
str => str.trim()
);
process(' hello '); // "结果:HELLO"
优势:将复杂逻辑拆解为简单函数,提高代码复用性和可维护性。
三、与面向对象编程(OOP)的对比
特性 | 函数式编程 | 面向对象编程 |
---|---|---|
核心组织单元 | 函数(无状态) | 对象(包含状态和方法) |
数据操作 | 不可变,返回新数据 | 可变,修改对象状态 |
副作用处理 | 显式控制,尽量避免 | 隐式存在,依赖对象生命周期 |
并发性支持 | 天然安全(无共享状态) | 需通过锁等机制保证安全 |
四、实际应用场景
- 状态管理 :如 Redux 遵循 FP 原则,通过纯函数
reducer
处理状态变更,确保可预测性; - 异步处理 :通过
Promise
组合、async/await
(本质是函数组合)处理异步逻辑; - 数组 / 集合操作 :使用
map
、filter
、reduce
等高阶函数替代命令式循环; - 工具库开发:Lodash、Ramda 等库提供大量 FP 工具函数,简化数据处理;
- 性能优化 :不可变数据配合浅比较(如 React 的
useState
依赖项检查),减少不必要的重新渲染。
五、优缺点分析
- 优点 :
- 代码更简洁、可测试性强;
- 易于并行处理和推理逻辑;
- 避免共享状态带来的竞态条件。
- 缺点 :
- 学习曲线较陡(需理解柯里化、函子等概念);
- 频繁创建新数据可能带来内存开销(现代 JS 引擎已优化);
- 复杂业务场景下可能过度嵌套函数组合。
六、如何在 JavaScript 中实践 FP
- 优先使用纯函数:避免修改参数,用返回值替代赋值;
- 拥抱不可变数据 :使用
const
声明变量,避免push
、splice
等 mutable 方法; - 利用高阶函数 :用
map/filter/reduce
替代循环,用柯里化封装通用逻辑; - 使用 FP 工具库 :如 Ramda 提供
pipe
、compose
、curry
等函数,降低手动实现成本。
函数式编程并非完全替代面向对象编程,而是作为一种编程思维,帮助开发者以更抽象、更可靠的方式组织代码,尤其在处理复杂数据变换和异步流程时优势显著。
防抖(Debounce)和节流(Throttle)的区别是什么?各自的使用场景有哪些?
防抖和节流是前端性能优化中处理高频事件 (如窗口 Resize、输入框输入、按钮快速点击)的核心技术,二者通过延迟执行函数减少回调次数,但实现逻辑和应用场景有显著差异。
一、核心原理对比
特性 | 防抖(Debounce) | 节流(Throttle) |
---|---|---|
触发时机 | 在事件触发后,等待指定时间内无后续事件才执行 | 按固定时间间隔周期性执行事件处理函数 |
核心逻辑 | 使用定时器,若期间再次触发则重置定时器 | 使用定时器或时间戳,控制函数执行频率 |
执行次数 | 事件停止触发后执行一次 | 事件持续触发时按间隔执行多次 |
典型场景 | 输入框实时搜索、按钮防重复点击 | 窗口 Resize、滚动事件、canvas 画笔实时绘制 |
二、防抖(Debounce)的实现与场景
1. 基本实现(定时器版)
function debounce(fn, delay = 300) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer); // 清除前一个定时器
timer = setTimeout(() => {
fn.apply(this, args); // 延迟delay ms后执行
timer = null; // 重置定时器
}, delay);
};
}
关键点 :每次触发事件都重置定时器,确保只有最后一次触发且间隔超过delay
时才执行函数。
2. 立即执行版(leading edge)
function debounce(fn, delay = 300, immediate = false) {
let timer = null;
return function(...args) {
const context = this;
if (immediate && !timer) { // 首次触发时立即执行
fn.apply(context, args);
timer = setTimeout(() => {
timer = null; // 清空定时器,允许下次立即执行
}, delay);
} else if (!immediate) { // 非立即执行模式,等待延迟
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
}
};
}
区别 :通过immediate
参数控制是否在首次触发时立即执行,适用于需要 "先执行、再防抖" 的场景(如搜索按钮点击)。
3. 典型应用场景
-
输入框实时搜索 :用户输入时实时请求接口,但避免每秒发起数十次请求。例如,用户停止输入 300ms 后再触发搜索:
const searchInput = document.getElementById('search'); searchInput.addEventListener('input', debounce((e) => { fetch(`/api/search?q=${e.target.value}`); }, 300));
-
按钮防重复点击 :防止用户快速点击提交按钮导致多次请求,确保点击间隔超过指定时间:
submitButton.addEventListener('click', debounce(() => { // 提交表单逻辑 }, 1000, true)); // immediate=true,点击立即执行,1秒内无法再次触发
-
窗口 Resize 后的布局调整 :避免 Resize 过程中频繁计算布局,仅在 Resize 停止后执行一次:
window.addEventListener('resize', debounce(() => { calculateLayout(); }, 200));
三、节流(Throttle)的实现与场景
1. 时间戳版(leading edge)
function throttle(fn, limit = 300) {
let lastCallTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastCallTime >= limit) { // 超过间隔时间则执行
fn.apply(this, args);
lastCallTime = now; // 更新最后执行时间
}
};
}
特点 :首次触发时立即执行,之后按limit
间隔执行,事件结束后若剩余时间不足间隔则不再执行。
2. 定时器版(trailing edge)
function throttle(fn, limit = 300) {
let timer = null;
return function(...args) {
const context = this;
if (!timer) { // 首次触发时设置定时器
timer = setTimeout(() => {
fn.apply(context, args);
timer = null; // 执行后清空定时器,允许下次触发
}, limit);
}
};
}
特点 :首次触发不立即执行,等待limit
时间后执行,事件持续触发时按间隔执行,结束后会执行最后一次触发的回调(与时间戳版互补)。
3. 混合版(同时支持 leading 和 trailing)
function throttle(fn, limit = 300) {
let lastCallTime = 0;
let timer = null;
return function(...args) {
const now = Date.now();
const context = this;
if (now - lastCallTime >= limit) { // 处理leading edge
fn.apply(context, args);
lastCallTime = now;
} else if (!timer) { // 处理trailing edge
timer = setTimeout(() => {
fn.apply(context, args);
lastCallTime = Date.now();
timer = null;
}, limit - (now - lastCallTime)); // 计算剩余时间
}
};
}
优势:兼顾立即执行和结束后执行,确保事件开始和结束时都能触发回调。
4. 典型应用场景
-
滚动事件处理 :如无限滚动加载数据时,避免每秒触发数十次
scroll
事件:window.addEventListener('scroll', throttle(() => { if (isBottomReached()) loadMoreData(); }, 200));
-
canvas 画笔实时绘制 :用户拖动鼠标时实时绘制路径,通过节流控制绘制频率,避免帧率下降:
canvas.addEventListener('mousemove', throttle((e) => { drawPoint(e.clientX, e.clientY); }, 50)); // 每秒20次绘制
-
高频点击统计 :记录用户点击次数时,按固定间隔上报数据(如每秒一次),而非每次点击都上报:
button.addEventListener('click', throttle(() => { sendClickStatistics(); }, 1000));
四、选择策略与注意事项
- 根据业务需求选择 :
- 若需要 "等待操作停止后执行"(如输入完成后搜索),用防抖;
- 若需要 "控制操作频率"(如持续拖动时实时反馈),用节流。
- 参数调优 :
- 防抖的
delay
需平衡用户体验与性能(如搜索场景建议 300-500ms); - 节流的
limit
需根据操作频率和计算复杂度调整(如动画相关操作建议 16ms/60fps)。
- 防抖的
- 内存泄漏风险 :
- 防抖函数中若引用 DOM 元素,需在组件卸载时手动清除定时器(如 React 中使用
useEffect
的清理函数)。
- 防抖函数中若引用 DOM 元素,需在组件卸载时手动清除定时器(如 React 中使用
手写防抖和节流函数,并举例说明调用后的执行结果
防抖(Debounce)和节流(Throttle)是前端性能优化的重要技术,用于处理高频触发的事件(如滚动、输入、点击)。以下通过代码实现并结合示例说明其执行逻辑。
防抖函数实现
防抖的核心是合并多次触发为一次,通过定时器延迟执行,期间若再次触发则重置定时器。
function debounce(fn, delay = 300) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
// 示例:模拟搜索输入
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
console.log(`搜索: ${e.target.value}`);
}, 500));
执行结果:
- 当用户连续输入
hello
(每 100ms 输入一个字符),期间不会触发搜索。 - 输入完成后停止 500ms,才会执行一次搜索操作,输出
搜索: hello
。
节流函数实现
节流的核心是控制执行频率,按固定间隔执行函数,通过时间戳或定时器实现。
// 时间戳版(立即执行)
function throttle(fn, limit = 300) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= limit) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 示例:窗口滚动事件
window.addEventListener('scroll', throttle(() => {
console.log('滚动位置:', window.scrollY);
}, 200));
执行结果:
- 当用户持续滚动页面时,无论滚动速度多快,函数每 200ms 执行一次。
- 若滚动持续 1 秒,将触发约 5 次输出(1000ms / 200ms = 5)。
对比与选择策略
场景 | 防抖(Debounce) | 节流(Throttle) |
---|---|---|
搜索输入实时联想 | ✅ 停止输入后触发一次 | ❌ 按间隔触发可能显示不全 |
按钮防重复点击 | ✅ 避免短时间内多次触发 | ❌ 仍会按间隔执行多次 |
窗口 Resize 事件 | ✅ 只在调整完成后计算布局 | ✅ 按间隔更新布局(需权衡) |
滚动加载更多数据 | ❌ 可能错过临界点 | ✅ 持续滚动时按频率加载 |
在实际开发中,若需合并高频操作为一次有效执行 ,选择防抖;若需控制操作的最大频率,选择节流。两者结合使用(如先节流收集数据,再防抖批量提交)可应对更复杂的场景。
元素垂直居中对齐的方式有哪些?
在 CSS 中实现元素垂直居中是常见需求,可通过多种布局模型实现。以下介绍主流方案及适用场景,重点解析 Flex 布局中justify-content
与align-items
的区别。
一、行内元素垂直居中
适用于文本、图片等行内元素(inline/inline-block):
-
单行文本 :通过
line-height
等于容器高度实现。.container { height: 100px; line-height: 100px; /* 文本垂直居中 */ }
-
多行文本 :结合
display: table-cell
与vertical-align: middle
。.container { display: table-cell; height: 100px; vertical-align: middle; /* 多行文本垂直居中 */ }
二、Flex 布局(现代方案)
通过display: flex
或display: inline-flex
实现,需区分主轴与交叉轴:
.container {
display: flex;
justify-content: center; /* 主轴(水平)居中 */
align-items: center; /* 交叉轴(垂直)居中 */
}
关键点:
justify-content
:控制主轴 方向的对齐方式(默认水平方向),可选值包括flex-start
、center
、flex-end
、space-between
等。align-items
:控制交叉轴 方向的对齐方式(默认垂直方向),可选值包括flex-start
、center
、flex-end
、stretch
等。
三、绝对定位方案
适用于固定宽高的元素:
.container {
position: relative;
}
.element {
position: absolute;
top: 50%;
left: 50%;
width: 100px;
height: 100px;
margin-top: -50px; /* 向上偏移自身高度的一半 */
margin-left: -50px; /* 向左偏移自身宽度的一半 */
}
优化版 :使用transform: translate(-50%, -50%)
,无需提前知道元素尺寸。
.element {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* 自动计算偏移 */
}
四、Grid 布局(二维居中)
通过place-items
简写属性同时控制水平和垂直方向:
.container {
display: grid;
place-items: center; /* 等价于 justify-items: center + align-items: center */
}
五、对比与选择策略
方案 | 优点 | 缺点 |
---|---|---|
Flex 布局 | 简洁、响应式、兼容性好 | 需考虑主轴方向 |
绝对定位 + transform | 无需提前知道元素尺寸 | 依赖父元素定位 |
Grid 布局 | 二维居中能力更强 | 兼容性略差(IE 不支持) |
行内元素法 | 简单直接 | 仅适用于行内元素 |
在实际开发中,优先使用 Flex 布局(如导航栏、卡片组件),若需精确控制二维空间(如弹窗居中)则选择 Grid。绝对定位方案适用于需要脱离文档流的场景,但需注意父元素的定位属性。
简述 position 属性的取值及默认值
CSS 的position
属性用于控制元素的定位方式,其取值决定了元素在文档流中的表现形式。以下是各值的详细说明及典型场景:
1. static
(默认值)
元素按正常文档流布局,top
、left
、right
、bottom
属性无效。
.element {
position: static; /* 默认值,无需显式声明 */
}
特点:元素的位置由 HTML 结构决定,无法通过定位属性调整。
2. relative
(相对定位)
元素相对于其正常位置进行定位,仍占据原文档流空间。
.element {
position: relative;
top: 20px; /* 向下偏移20px */
left: 10px; /* 向右偏移10px */
}
典型场景:
- 作为
absolute
子元素的定位容器。 - 微调元素位置(如图标与文本的对齐)。
3. absolute
(绝对定位)
元素相对于最近的已定位祖先元素 (即position
值不为static
的元素)定位,脱离正常文档流。
.container {
position: relative; /* 作为参考容器 */
}
.element {
position: absolute;
top: 0;
right: 0; /* 右上角定位 */
}
特点:
- 若没有已定位的祖先元素,则相对于初始包含块(通常是浏览器窗口)。
- 宽度默认由内容决定,除非显式设置。
4. fixed
(固定定位)
元素相对于浏览器视口定位,滚动时位置保持不变。
.element {
position: fixed;
bottom: 20px;
right: 20px; /* 右下角固定位置 */
}
典型场景:
- 悬浮按钮(如返回顶部)。
- 固定导航栏。
5. sticky
(粘性定位)
元素在滚动时初始按正常文档流布局 ,到达指定位置后变为fixed
定位。
.header {
position: sticky;
top: 0; /* 滚动到顶部时固定 */
}
特点:
- 需指定
top
、left
、right
、bottom
中的至少一个值。 - 兼容性略差(IE/Edge 15 及以下不支持)。
6. 对比与应用场景
值 | 参考对象 | 是否脱离文档流 | 滚动时表现 |
---|---|---|---|
static |
正常文档流 | 否 | 随文档流滚动 |
relative |
自身正常位置 | 否 | 随文档流滚动 |
absolute |
最近已定位祖先 | 是 | 随祖先元素滚动 |
fixed |
浏览器视口 | 是 | 固定不动 |
sticky |
正常文档流 | 否(滚动到临界点后是) | 临界点前随流滚动,之后固定 |
在实际开发中,relative
常作为定位容器,absolute
用于精确定位元素(如下拉菜单),fixed
用于全局交互元素,sticky
则适合需要临时固定的内容(如表格表头)。合理使用定位属性可构建复杂的页面布局,同时避免脱离文档流导致的布局塌陷问题。
relative 和 absolute 定位分别相对于谁进行定位?
在 CSS 中,position: relative
和position: absolute
是两种常用的定位方式,其定位基准和行为差异直接影响页面布局。以下通过对比解析两者的核心区别及应用场景。
relative(相对定位)的定位基准
relative
定位的元素相对于其正常文档流中的位置进行偏移,仍占据原空间。
.box {
position: relative;
top: 20px;
left: 30px; /* 相对于原位置向右下偏移 */
}
关键点:
- 无论是否设置偏移值,元素始终保留在文档流中,不会影响其他元素的布局。
- 偏移方向由
top
、left
、right
、bottom
控制(正值表示向相反方向偏移)。
典型场景:
- 微调元素位置(如图标与文本的对齐)。
- 作为
absolute
子元素的定位容器(自身无需偏移)。
absolute(绝对定位)的定位基准
absolute
定位的元素相对于最近的已定位祖先元素 (即position
值不为static
的元素)进行定位,完全脱离文档流。
<div class="container"> <!-- 需设置position: relative/absolute/fixed/sticky -->
<div class="box"> <!-- 绝对定位元素 -->
</div>
.container {
position: relative; /* 作为参考容器 */
}
.box {
position: absolute;
top: 10px;
right: 10px; /* 相对于.container右上角偏移 */
}
关键点:
- 若没有已定位的祖先元素,则相对于初始包含块 (通常是浏览器窗口或
<html>
元素)。 - 宽度默认由内容决定,除非显式设置(如
width: 100%
)。
典型场景:
- 悬浮层(如下拉菜单、提示框)。
- 绝对定位的广告组件。
- 响应式布局中的元素重叠效果。
对比与注意事项
特性 | relative | absolute |
---|---|---|
参考对象 | 自身正常位置 | 最近的已定位祖先元素 |
文档流占用 | 保留 | 脱离 |
对兄弟元素的影响 | 无 | 可能导致布局塌陷 |
默认宽度 | 由内容或容器决定 | 由内容决定 |
常见用途 | 微调位置、作为定位容器 | 创建浮动元素、覆盖效果 |
实战案例:相对定位容器 + 绝对定位子元素
<div class="card">
<img src="image.jpg" alt="Product">
<div class="badge">NEW</div> <!-- 绝对定位的标签 -->
</div>
.card {
position: relative; /* 容器需相对定位 */
width: 200px;
height: 200px;
}
.badge {
position: absolute;
top: 10px;
right: 10px; /* 相对于.card右上角定位 */
background: red;
}
常见误区
- 未设置参考容器 :若直接对元素使用
absolute
而不指定已定位的祖先元素,元素会相对于视口定位,导致布局错乱。 - 过度使用绝对定位:脱离文档流可能导致父元素高度塌陷,需谨慎处理。
- 混淆偏移方向 :
top: 10px
表示向下偏移,而非向上(与直觉相反)。
理解relative
和absolute
的定位基准是构建复杂布局的基础,合理组合两者可实现如模态框、导航菜单、卡片徽章等常见 UI 组件,同时避免布局失控。
实现两栏布局(左侧宽度不固定,右侧自适应)的方法有哪些?
两栏布局是前端常见需求,其中左侧宽度由内容决定(不固定),右侧自适应剩余空间。以下介绍主流实现方案及适用场景,结合代码示例说明。
一、浮动(Float)布局
利用float
使左侧元素脱离文档流,右侧通过margin-left
让出空间。
<div class="container">
<div class="left">左侧内容</div>
<div class="right">右侧自适应内容</div>
</div>
.left {
float: left;
}
.right {
margin-left: 100px; /* 需与左侧宽度匹配 */
}
局限性:需预先知道左侧宽度,否则右侧会换行。
二、Flex 布局(推荐)
通过flex
实现自动分配空间,右侧使用flex: 1
占满剩余宽度。
.container {
display: flex;
}
.left {
/* 宽度由内容决定 */
}
.right {
flex: 1; /* 自适应剩余空间 */
}
优势:
- 简洁高效,无需关心左侧实际宽度。
- 支持响应式设计(如通过
media query
调整为垂直布局)。
三、Grid 布局
通过grid-template-columns
定义列宽,右侧使用1fr
表示剩余空间。
.container {
display: grid;
grid-template-columns: auto 1fr; /* 左侧自动,右侧占满剩余 */
}
优势:
- 二维布局能力更强,支持多行列控制。
- 代码更简洁,无需额外设置。
四、绝对定位 + 负边距
左侧绝对定位,右侧通过负边距抵消左侧宽度。
.container {
position: relative;
}
.left {
position: absolute;
left: 0;
top: 0;
}
.right {
margin-left: 100px; /* 需与左侧宽度匹配 */
}
局限性:需预先知道左侧宽度,且父容器需设置高度。
五、表格布局
通过display: table-cell
实现类似表格的布局。
.container {
display: table;
width: 100%;
}
.left, .right {
display: table-cell;
}
.right {
width: 100%; /* 强制右侧占满剩余空间 */
}
兼容性:IE8 + 支持,但语义不够现代。
六、对比与选择策略
方案 | 优点 | 缺点 | 兼容性 |
---|---|---|---|
Flex 布局 | 代码简洁,自动适应宽度 | 需考虑浏览器兼容性 | IE10+ |
Grid 布局 | 二维控制能力更强 | 兼容性略差(IE 不支持) | 现代浏览器 |
浮动布局 | 兼容性好 | 需固定左侧宽度 | 全兼容 |
表格布局 | 无需固定宽度 | 语义不明确 | IE8+ |
七、响应式优化
在小屏幕设备上,可通过媒体查询将两栏转为垂直布局:
@media (max-width: 768px) {
.container {
flex-direction: column; /* Flex布局转为垂直 */
}
.left, .right {
width: 100%; /* 宽度恢复为100% */
}
}
八、实战案例:导航栏 + 内容区
<div class="app">
<nav class="sidebar">
<!-- 导航菜单,宽度由内容决定 -->
</nav>
<main class="content">
<!-- 主要内容,自适应宽度 -->
</main>
</div>
.app {
display: flex;
height: 100vh;
}
.sidebar {
width: 200px; /* 或由内容决定 */
}
.content {
flex: 1; /* 占满剩余空间 */
overflow: auto; /* 内容过多时显示滚动条 */
}
在实际开发中,优先使用 Flex 布局(兼容性好且简洁),若需更复杂的二维控制则选择 Grid。浮动和表格布局适用于兼容性要求高的场景,但需注意其局限性。响应式设计应作为必备优化,确保在不同设备上均有良好表现。
如何让一行内的三个元素等间隔排列?
实现一行内三个元素等间隔排列的核心是利用 CSS 布局属性控制元素间距和对齐方式,常见方法包括Flex 布局 、Grid 布局 和传统盒模型 + text-align等,不同场景下需结合兼容性和需求选择。
1. Flex 布局(推荐)
Flex 布局是最灵活的方案,通过justify-content
属性控制主轴方向的间隔,配合space-between
或space-around
实现不同效果:
justify-content: space-between
:元素两端对齐,中间间隔相等(两侧无间隔)。justify-content: space-around
:元素周围间隔相等(两侧间隔为中间的一半)。justify-content: space-evenly
(CSS3 新增):所有间隔完全相等(包括两侧)。
示例代码:
.container {
display: flex;
justify-content: space-between; /* 或 space-around/space-evenly */
width: 80%; /* 容器宽度,避免撑满整行 */
margin: 0 auto; /* 居中容器 */
}
.item {
padding: 10px 20px;
background: #f0f0f0;
}
效果说明 :三个.item
元素在容器内水平等间隔排列,space-between
适用于两侧需要贴边的场景,space-around
则适合元素周围均匀留白的需求。
2. Grid 布局(适用于二维场景)
Grid 布局通过网格轨道分配空间,利用justify-items
控制单元格水平对齐,配合grid-template-columns
定义列宽:
.container {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 三列等宽 */
gap: 20px; /* 列间距 */
width: 80%;
margin: 0 auto;
}
关键点 :1fr
表示等比例分配剩余空间,gap
属性设置列与列之间的间隔,实现视觉上的等间隔效果。此方法适合需要同时控制行列间隔的复杂布局。
3. 传统盒模型 + text-align(兼容 IE)
通过给父元素设置text-align: justify
(两端对齐),并在最后一个子元素后添加伪元素模拟换行,使前三个元素均匀分布:
.container {
width: 80%;
margin: 0 auto;
text-align: justify;
/* 兼容IE的hack */
font-size: 0; /* 消除行内元素间的空格 */
}
.item {
display: inline-block;
padding: 10px 20px;
background: #f0f0f0;
font-size: 16px; /* 恢复字体大小 */
}
.container::after {
content: '';
display: inline-block;
width: 100%;
}
原理 :text-align: justify
会使行内元素两端对齐,但仅当内容超过一行时生效,因此需要伪元素撑开容器强制对齐。此方法兼容性好,但代码复杂度较高。
4. 绝对定位 + 计算(适用于固定宽度元素)
若元素宽度固定,可通过绝对定位计算每个元素的位置:
.container {
position: relative;
width: 80%;
height: 100px;
margin: 0 auto;
}
.item {
position: absolute;
width: 100px;
height: 100px;
background: #f0f0f0;
}
.item:nth-child(1) { left: 0; }
.item:nth-child(2) { left: 50%; transform: translateX(-50%); } /* 居中 */
.item:nth-child(3) { right: 0; }
限制:元素宽度需已知,且无法自适应内容变化,适合固定尺寸的场景。
关键对比
方法 | 兼容性 | 灵活性 | 适用场景 |
---|---|---|---|
Flex 布局 | 现代浏览器 | 高(自适应) | 大多数场景,推荐使用 |
Grid 布局 | 现代浏览器 | 高(二维) | 需要行列对齐的复杂布局 |
传统盒模型 | IE8+ | 中 | 兼容旧版浏览器 |
绝对定位 | 全兼容 | 低(固定宽) | 已知元素尺寸的简单布局 |
注意事项
- 空白节点问题 :行内元素(如
inline-block
)之间的空格会被解析为字符间距,可通过父元素font-size: 0
消除,子元素再恢复字体大小。 - 垂直对齐 :若需元素垂直居中,可配合
align-items: center
(Flex 布局)或vertical-align: middle
(行内元素)使用。 - 响应式设计:结合媒体查询调整容器宽度或布局方式,使排列在不同屏幕尺寸下保持合理。
通过上述方法,可根据具体需求选择最适合的等间隔排列方案,其中 Flex 布局因简洁性和强大功能成为主流选择。
实现三列布局,元素一行显示,多余元素自动折行,且折行后保持相同纵列对齐。
实现三列布局并保证折行后纵列对齐,需借助 CSS 的弹性布局(Flex)或网格布局(Grid),结合换行属性和轨道对齐功能。以下是具体方案及原理分析:
方案一:Flex 布局(推荐)
Flex 布局通过flex-wrap
允许元素换行,并利用flex-basis
控制列宽基准,配合align-content
调整换行后纵列对齐。
核心思路:
- 设置父容器为 Flex 容器,启用换行(
flex-wrap: wrap
)。 - 定义子元素的基准宽度(如
flex-basis: 33.33%
),确保每行最多三列。 - 通过
justify-content
控制行内水平对齐,align-content
控制换行后纵列垂直对齐(如space-between
避免列间距变化)。
代码示例:
.container {
display: flex;
flex-wrap: wrap;
justify-content: flex-start; /* 行内左对齐 */
align-content: flex-start; /* 换行后纵列顶部对齐 */
width: 100%;
gap: 10px; /* 元素间距 */
}
.item {
flex-basis: calc(33.33% - 20px); /* 减去两倍间距,避免超出容器 */
max-width: calc(33.33% - 20px); /* 限制最大宽度 */
padding: 20px;
background: #f0f0f0;
box-sizing: border-box; /* 包含padding计算宽度 */
}
效果解析:
- 当容器宽度足够时,每行显示三列,元素宽度由
flex-basis
计算得出(33.33%
减去左右间距)。 - 容器宽度不足时,多余元素自动换行,新行与前一行保持相同列数和宽度,实现纵列对齐。
gap
属性统一元素间距,避免换行后间距错位。
进阶优化:等间距对齐
若需每行元素左右两端对齐(类似 justify-content: space-between),可结合伪元素模拟弹性空间:
.container {
justify-content: space-between; /* 行内两端对齐 */
}
.container::after {
content: '';
flex-grow: 1; /* 占据剩余空间,确保最后一行不足三列时仍对齐 */
}
此技巧可使最后一行不足三列的元素仍保持两端对齐,避免左对齐导致的纵列错位。
方案二:Grid 布局(二维对齐更精准)
Grid 布局通过grid-template-columns
定义固定列数,grid-auto-rows
控制行高,自动换行时列轨道会严格对齐。
核心思路:
- 父容器设为 Grid 布局,定义三列等宽轨道(
grid-template-columns: repeat(3, 1fr)
)。 - 启用自动换行(隐式网格),通过
gap
设置行列间距。 - 利用
justify-items
和align-items
控制单元格内容对齐。
代码示例:
.container {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 三列等宽 */
gap: 10px; /* 行列间距 */
width: 100%;
}
.item {
padding: 20px;
background: #f0f0f0;
}
优势:
- Grid 布局天然支持二维对齐,换行后列轨道会严格继承第一行的宽度,无需额外处理。
1fr
单位会自动分配剩余空间,适应不同内容高度的元素。- 缺点是 IE 不支持,需配合 Polyfill 或放弃旧版浏览器兼容。
方案三:浮动布局(兼容旧版浏览器)
利用浮动和清除机制实现三列布局,但需手动计算宽度并处理高度塌陷问题,适合需要兼容 IE8 + 的场景。
核心思路:
- 子元素设置浮动(
float: left
),宽度设为33.33%
并减去间距。 - 父容器添加
overflow: hidden
清除浮动影响。 - 通过
margin
或padding
控制元素间距。
代码示例:
.container {
width: 100%;
overflow: hidden; /* 清除浮动 */
}
.item {
float: left;
width: calc(33.33% - 20px);
margin: 0 10px 20px; /* 左右间距10px,底部间距20px */
padding: 20px;
background: #f0f0f0;
}
局限性:
- 浮动布局无法自动感知容器宽度变化,需手动计算宽度与间距的关系。
- 换行后元素可能因高度不同导致纵列错位,需额外处理(如等高布局
flex-shrink: 0
)。
关键对比与适用场景
布局方式 | 兼容性 | 纵列对齐精度 | 代码复杂度 | 动态内容适应性 |
---|---|---|---|---|
Flex 布局 | 现代浏览器 | 高(需辅助技巧) | 中 | 优(自适应宽度) |
Grid 布局 | 现代浏览器 | 极高 | 低 | 优(二维对齐) |
浮动布局 | IE8+ | 中(需手动调整) | 高 | 差(固定宽度) |
注意事项
-
盒模型处理 :使用
box-sizing: border-box
确保padding
和border
不影响元素计算宽度。 -
间距一致性 :通过
gap
(Flex/Grid)或统一margin
值避免间距错乱。 -
响应式设计 :结合媒体查询,在小屏幕下调整为两列或单列布局,提升移动端体验:
@media (max-width: 768px) { .item { flex-basis: calc(50% - 20px); } /* 两列布局 */ }
通过 Flex 或 Grid 布局,可高效实现三列自动换行且纵列对齐的效果,其中 Grid 布局因二维特性更适合复杂场景,而 Flex 布局在兼容性和灵活性之间取得平衡,是现代项目的首选方案。
用 CSS 实现九宫格布局,要求鼠标悬停时边框变为 1px 红色实线。
实现九宫格布局并添加悬停边框效果,可结合Grid 布局 或Flex 布局 快速生成网格结构,再通过伪类选择器:hover
动态修改边框样式。以下是具体实现步骤和代码示例:
方案一:Grid 布局(推荐)
Grid 布局是最适合二维网格的方案,通过grid-template-columns
和grid-template-rows
定义三行三列结构,配合gap
设置单元格间距。
核心步骤:
- 创建父容器并设为 Grid 布局,定义 3×3 网格轨道。
- 子元素默认填充网格单元格,通过
padding
或background
区分格子。 - 使用
:hover
选择器为子元素添加红色边框,注意边框会覆盖原有间距,需通过box-sizing
或调整gap
避免布局偏移。
代码示例:
/* 基础布局 */
.grid-container {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 三列等宽 */
grid-template-rows: repeat(3, 100px); /* 三行等高,可改为auto适应内容 */
gap: 5px; /* 单元格间距,需与边框宽度协调 */
width: 320px; /* 总宽度 = 3×100px + 2×5px×2(左右间距) */
margin: 20px auto;
background: #f0f0f0; /* 父容器背景,衬托间距 */
}
.grid-item {
background: #fff;
display: flex; /* 方便内容居中 */
align-items: center;
justify-content: center;
font-size: 1.2em;
transition: border 0.3s ease; /* 添加过渡效果 */
box-sizing: border-box; /* 使边框不影响尺寸 */
border: 1px solid #e0e0e0; /* 默认边框 */
}
/* 悬停效果 */
.grid-item:hover {
border-color: #ff0000; /* 红色边框 */
border-width: 1px;
border-style: solid;
/* 可选:提升层级避免遮挡 */
z-index: 1;
}
效果解析:
repeat(3, 1fr)
生成三列等宽轨道,gap: 5px
设置单元格之间的间距,父容器背景色显示间距效果。- 子元素默认边框为浅灰色,悬停时变为红色,
transition
属性使颜色变化平滑。 box-sizing: border-box
确保边框宽度包含在元素尺寸内,避免悬停时布局跳动。
优化点 :若希望悬停时边框覆盖间距(即相邻格子边框合并),可移除gap
并通过父容器border-collapse
模拟,但会增加复杂度,建议保留间距以清晰区分单元格。
方案二:Flex 布局 + 嵌套容器
通过 Flex 布局实现行级排列,每行包含三个子元素,配合flex-wrap: wrap
换行生成九宫格。
代码示例:
.flex-container {
display: flex;
flex-wrap: wrap;
width: 320px;
margin: 20px auto;
}
.flex-item {
width: 100px;
height: 100px;
margin: 5px; /* 模拟间距 */
background: #fff;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #e0e0e0;
transition: border 0.3s ease;
box-sizing: border-box;
}
.flex-item:hover {
border-color: #ff0000;
}
局限性:
- 间距通过
margin
实现,悬停时边框会覆盖margin
,导致相邻元素间距缩小(视觉上边框叠加)。 - 需手动计算宽度和间距(如
width: 100px + 2×5px margin
),灵活性低于 Grid 布局。
方案三:传统定位 + 浮动(兼容旧版浏览器)
利用浮动和清除浮动实现九宫格,适合需要兼容 IE8 + 的场景,但代码复杂度较高:
.float-container {
width: 320px;
margin: 20px auto;
}
.float-item {
float: left;
width: 100px;
height: 100px;
margin: 5px;
background: #fff;
border: 1px solid #e0e0e0;
transition: border 0.3s ease;
box-sizing: border-box;
}
.float-container::after {
content: '';
display: block;
clear: both;
}
.float-item:hover { border-color: red; }
注意事项:
- 需手动清除浮动(
::after
伪元素),避免父容器高度塌陷。 - 浮动布局下元素换行可能因高度不一致导致对齐问题,本例中固定高度可避免此问题。
关键细节处理
-
边框过渡效果 :通过
transition
属性为border-color
添加渐变动画,提升交互体验:transition: border-color 0.2s, border-width 0.2s; /* 若改变边框宽度 */
-
层级控制 :悬停时通过
z-index
提升元素层级,避免被相邻元素遮挡(尤其在边框变粗时)。 -
响应式适配 :可通过媒体查询调整网格尺寸,例如在小屏幕下改为两列布局:
@media (max-width: 480px) { .grid-container { grid-template-columns: repeat(2, 1fr); } }
行内元素(inline)和行内块元素(inline-block)的区别是什么?行内元素能否设置内边距(padding)?如何实现行内元素的垂直对齐?
行内元素与行内块元素的区别源于 CSS 盒模型对元素的渲染规则,两者在布局特性、样式支持和对齐方式上有显著差异。以下是详细对比及实践方法:
一、行内元素(inline)与行内块元素(inline-block)的核心区别
特性 | 行内元素(inline) | 行内块元素(inline-block) |
---|---|---|
布局表现 | 多个元素在同一行排列,不会自动换行(需br 标签) |
同样支持一行排列,但可设置宽高,多余元素自动换行 |
宽高属性 | 无法通过width /height 设置尺寸,由内容撑开 |
支持width /height ,也可由内容撑开 |
盒模型支持 | 左右margin /padding 有效,上下方向无效(不影响行高) |
上下左右margin /padding 均有效,会影响布局 |
边框与背景 | 背景和边框会应用,但上下边框不影响行间距 | 边框和背景完全包裹元素,占据物理空间 |
子元素兼容性 | 通常只能包含文本或行内元素,嵌套块级元素可能导致渲染异常 | 可包含块级元素,渲染规则与块级元素一致 |
典型元素:
- 行内元素:
span
、a
、strong
、em
等。 - 行内块元素:
img
、input
、button
、textarea
等(默认样式为inline-block
)。
二、行内元素能否设置内边距(padding)?
行内元素的padding
属性是有效的,但表现与块级元素不同:
- 左右方向 :
padding-left
/padding-right
会增加元素水平空间,导致相邻元素间距扩大,且背景色会填充左右内边距区域。 - 上下方向 :
padding-top
/padding-bottom
会增加元素上下内边距,但不会影响行高或布局,即相邻行的行内元素不会因上下内边距而改变位置,背景色会覆盖行高范围(可能与相邻行重叠)。
示例代码:
.inline-element {
display: inline;
padding: 20px 40px; /* 上下20px,左右40px */
background: #f0f0f0;
border: 1px solid #333;
}
效果说明:
- 左右内边距使元素水平区域扩大,相邻行内元素会被推开。
- 上下内边距虽然增加了元素视觉高度,但不会改变行高(行高由
line-height
或字体大小决定),可能导致背景色超出文本行范围。
三、如何实现行内元素的垂直对齐?
行内元素的垂直对齐基于基线对齐(baseline) ,基线是字体底部的假想线(如字母x
的底部)。可通过以下属性调整对齐方式:
1. vertical-align
属性(核心方法)
该属性控制行内元素相对于父元素基线或行内其他元素的垂直位置,常见取值:
baseline
(默认值):元素基线与父元素基线对齐(如文本与图片底部不对齐的问题即源于此)。top
:元素顶部与行内最高元素的顶部对齐。middle
:元素中点与父元素基线向上偏移0.5em
的位置对齐(近似垂直居中)。bottom
:元素底部与行内最低元素的底部对齐。sub
/super
:下标 / 上标对齐,用于文本排版。px/em
等数值 :相对于基线偏移指定距离(如vertical-align: 2px
使元素上移 2px)。
示例:图片与文本垂直居中对齐
<span>文本</span>
<img src="example.jpg" style="vertical-align: middle;">
问题场景 :当行内元素包含不同高度的子元素(如图片和文本)时,默认baseline
对齐会导致图片底部与文本基线对齐,视觉上图片偏下,此时vertical-align: middle
可改善对齐效果。
2. 父元素设置line-height
通过调整父元素的line-height
使其等于自身高度,可使单行内的行内元素垂直居中(仅适用于单行文本):
.parent {
height: 80px;
line-height: 80px;
}
原理 :行高等于容器高度时,文本行的基线会垂直居中,行内元素若为文本则自动居中;若为非文本元素(如span
包裹的块级元素),需配合vertical-align: middle
。
3. 转换为行内块元素并使用 Flex 布局
将行内元素转为inline-block
,并对父元素使用 Flex 布局实现垂直居中,此方法适用于多行或复杂内容:
.parent {
display: inline-flex; /* 行内Flex容器 */
align-items: center; /* 垂直居中 */
height: 80px;
}
.child {
display: inline-block;
}
优势 :无需依赖基线对齐,直接通过 Flex 的align-items
实现视觉居中,兼容性好且灵活。
四、常见问题与解决方案
-
行内块元素间的空格问题
多个
inline-block
元素的 HTML 代码间若存在空格(换行 / 制表符),会被解析为字符间距,导致元素间出现缝隙。
解决方法:-
父元素设置
font-size: 0
,子元素恢复字体大小:.parent { font-size: 0; } .child { font-size: 16px; }
-
移除 HTML 代码中的空格: 预览
<span class="child"></span><span class="child"></span>
-
-
行内元素背景溢出问题
行内元素的上下
padding
会使背景色超出文本行范围,可能与相邻行重叠。
解决方法:- 避免对行内元素设置过大的上下
padding
,或改用line-height
调整行高。 - 转换为
inline-block
元素,此时上下padding
会占据物理空间,避免重叠。
- 避免对行内元素设置过大的上下
-
基线对齐与边框的冲突
行内元素若包含边框(如
img
标签默认有边框),基线对齐可能导致元素底部与边框底边不一致。
解决方法 :明确设置vertical-align: bottom
使元素底部与边框底边对齐。
Vue2 和 Vue3 的主要区别有哪些?
Vue3(Vue 3.x)是 Vue.js 的重大升级版本,相比 Vue2(2.x)在性能、语法、生态和底层实现上有显著改进。以下是核心区别的详细对比:
一、响应式系统:Proxy 替代 Object.defineProperty
-
Vue2 :
使用
Object.defineProperty
劫持对象的get
和set
方法实现响应式,但存在以下局限:- 无法监听数组索引修改 (如
arr[0] = newVal
)和对象新增 / 删除属性 (需手动调用Vue.set
或this.$set
)。 - 深度响应式需递归遍历嵌套对象,性能开销较大,尤其在处理大型数据时。
- 无法监听数组索引修改 (如
-
Vue3 :
基于 ES6 的
Proxy
重构响应式系统,优势包括:- 原生支持数组索引和对象属性的增删 ,无需额外 API(如直接修改
arr[0]
或新增obj.newProp
即可触发更新)。 - 懒响应式:仅在访问属性时建立响应式联系,避免无效代理,提升初始化性能。
- 更好的内存管理 :通过
ReactiveMap
和WeakMap
实现响应式对象的缓存与释放,减少内存泄漏。
- 原生支持数组索引和对象属性的增删 ,无需额外 API(如直接修改
代码对比:
// Vue2:需手动处理数组索引更新
this.items.splice(0, 1, newItem); // 替代 arr[0] = newItem
// Vue3:直接修改索引即可响应
this.items[0] = newItem; // 自动触发视图更新
二、组件 API:组合式 API(Composition API)的引入
-
Vue2 :
采用选项式 API(Options API) ,逻辑按功能模块(如
data
、methods
、watch
)分块,适合中小型项目,但在处理复杂逻辑时会导致代码碎片化(如多个watch
和computed
分散在不同选项中)。 -
Vue3 :
新增组合式 API(Composition API) ,通过函数(如
setup
、ref
、reactive
)将相关逻辑组合在一起,优势包括:- 逻辑复用更灵活 :通过自定义 Hook 函数(如
useMousePosition
)复用逻辑,避免 Vue2 中 Mixin 的命名冲突和性能问题。 - 更好的类型推导:TypeScript 支持更友好,函数参数和返回值类型可直接推断。
- 减少模板依赖 :部分逻辑可在
setup
中直接处理,降低模板复杂度。
- 逻辑复用更灵活 :通过自定义 Hook 函数(如
示例:组合式 API 实现计数器
// Vue3
import { ref, computed } from 'vue';
export default {
setup() {
const count = ref(0);
const double = computed(() => count.value * 2);
const increment = () => count.value++;
return { count, double, increment };
}
};
对比 Vue2 选项式 API:
// Vue2
export default {
data() { return { count: 0 }; },
computed: { double() { return this.count * 2; } },
methods: { increment() { this.count++; } }
};
三、组件生命周期钩子:setup 替代 beforeCreate 和 created
-
Vue2 :
生命周期钩子包括
beforeCreate
、created
、beforeMount
、mounted
等,在选项中直接定义。 -
Vue3:
-
setup
函数替代beforeCreate
和created
,作为组件逻辑的入口,在响应式数据初始化前执行。 -
其他生命周期钩子需在
setup
中通过onXX
函数注册(如onMounted
、onUnmounted
),更贴近函数式编程风格:import { onMounted } from 'vue'; setup() { onMounted(() => { console.log('组件挂载完成'); }); }
-
四、性能优化:编译优化与 Tree-shaking
-
静态提升(Static Hoisting)
Vue3 编译器会自动将模板中的静态节点(如无响应式数据的 HTML)提升为常量,避免重复渲染,减少运行时开销。
预览
<!-- Vue3编译后会将静态文本提升,仅动态节点保留响应式 --> <div> <span>静态文本</span> <span>{{ dynamicText }}</span> </div>
-
片段(Fragments)与 Teleport
-
Vue2 组件模板必须有一个根节点,Vue3 支持多根节点(片段),无需额外包裹
div
:预览
<!-- Vue3合法模板 --> <header></header> <main></main> <footer></footer>
-
Teleport
组件允许将子节点渲染到 DOM 树的其他位置(如模态框挂载到body
下),解决层级嵌套问题。
-
-
Tree-shaking 支持
Vue3 的 API 采用 ES 模块导出(如
import { ref } from 'vue'
),打包工具可直接移除未使用的代码(如仅使用ref
时,未使用的reactive
会被摇树优化掉),减小打包体积。
五、其他重要变化
特性 | Vue2 | Vue3 |
---|---|---|
全局 API | 通过Vue.xxx 调用(如Vue.component ) |
改为应用实例方法(如app.component ) |
自定义指令钩子 | bind 、inserted 等 |
统一为beforeMount 、mounted 等,与组件生命周期对齐 |
异步组件 | 通过() => import('Component.vue') 定义 |
支持defineAsyncComponent 函数,参数更灵活 |
过渡动画 | vue-transition 组件 |
内置Transition 和TransitionGroup ,支持更复杂的状态过渡 |
TypeScript 支持 | 需额外配置声明文件 | 内置类型定义,组合式 API 天然适配 TS |
六、生态与工具链
-
Vue Router 4.x :
适配 Vue3,支持组合式 API,路由守卫改为在
setup
中通过useRoute
、useRouter
钩子调用。 -
Vuex 4.x :
兼容 Vue3,同时推出轻量级状态管理方案
Pinia
,基于组合式 API 设计,逐步替代 Vuex。 -
开发工具 :
Vue3 支持 Vite 快速构建(原生 ES 模块,冷启动速度比 Webpack 快数倍),搭配 HMR(热更新)提升开发体验。
升级建议与兼容性
- 新项目:优先使用 Vue3,充分利用组合式 API 和性能优化特性。
- 旧项目迁移 :可通过
vue-compat
插件在 Vue3 中兼容 Vue2 的选项式 API,逐步重构为组合式逻辑。 - 注意点 :Vue3 移除了
$attrs
的继承行为(需显式通过inheritAttrs: false
关闭),且v-model
在组件上的用法改为modelValue
和update:modelValue
(多模型支持)。
Vue3 通过响应式系统升级、组合式 API 和编译优化,显著提升了开发效率和应用性能,是现代前端项目的理想选择。
常用的 Vue 父子组件通信方式有哪些?
在 Vue 中,父子组件通信是组件化开发的核心场景之一,不同的通信需求可选择不同的方式实现。以下是常用的通信方法及特点:
1. props 与 $emit:基础单向数据流
-
props :父组件通过标签属性向子组件传递数据,子组件通过
props
选项声明接收的字段。这是单向绑定,父组件数据更新会触发子组件重新渲染,但子组件无法直接修改 props,需通过事件通知父组件。<!-- 父组件 --> <ChildComponent :msg="parentMsg" /> <!-- 子组件 --> export default { props: { msg: String } }
-
**:子组件通过emit (' 事件名 ', 数据)
触发自定义事件,父组件在子组件标签上用
@事件名 ="处理函数"` 监听。<!-- 子组件 --> this.$emit('update', newVal) <!-- 父组件 --> <ChildComponent @update="handleUpdate" />
2. 自定义事件与 v-model:双向绑定简化
-
通过
props
和$emit
结合实现双向绑定,Vue 提供v-model
语法糖。子组件需触发input
事件并传递新值,父组件自动更新绑定的变量。<!-- 父组件 --> <ChildComponent v-model="inputValue" /> <!-- 子组件 --> export default { props: { modelValue: String }, methods: { changeValue(e) { this.$emit('input', e.target.value) } } }
3. refs 与 parent/children:直接访问实例
- refs :父组件通过
ref
为子组件添加引用(如<ChildComponent ref="child" />
),然后通过this.$refs.child
直接访问子组件实例,调用其方法或属性。需注意这打破了单向数据流原则,仅适用于特殊场景。 - parent/children :子组件通过
this.$parent
访问父组件实例,父组件通过this.$children
访问所有子组件实例。但$children
是数组且顺序不固定,需谨慎使用。
4. Provide 与 Inject:跨层级通信(祖先与后代)
-
适用于深层嵌套组件间通信,避免逐层传递 props。祖先组件通过
provide
选项暴露数据或方法,后代组件通过inject
选项直接注入使用,无需在中间层级逐层传递。<!-- 祖先组件 --> export default { provide() { return { theme: this.theme, changeTheme: () => { /* ... */ } } } } <!-- 后代组件 --> export default { inject: ['theme', 'changeTheme'] }
注意 :
provide/inject
是响应式的,若要保持响应性,需暴露ref
或reactive
对象。
5. 事件总线(Event Bus):非父子组件通信
-
创建一个全局的事件中心(如
eventBus
实例),组件通过eventBus.$on('事件名', 回调)
监听事件,通过eventBus.$emit('事件名', 数据)
触发事件。适用于非父子关系的组件通信,但在 Vue3 中更推荐使用 Composition API 配合全局状态管理(如 Pinia)。// 全局事件总线 const eventBus = new Vue() // 组件 A 触发事件 eventBus.$emit('data-change', newData) // 组件 B 监听事件 eventBus.$on('data-change', (data) => { /* ... */ })
6. 全局状态管理(Vuex/Pinia):复杂状态共享
- 当多个组件需要共享状态时,使用 Vuex(Vue2)或 Pinia(Vue3)进行集中管理。状态存储在全局 store 中,组件通过
mapState
映射状态,通过 mutations/actions 修改状态。适用于中大型项目,避免组件间多层级通信的复杂性。
选择建议
- 简单单向数据传递:优先使用 props/$emit 或 v-model。
- 跨层级通信:祖先与后代组件用 provide/inject,非父子组件用事件总线或全局状态管理。
- 复杂状态逻辑:使用 Vuex/Pinia 实现集中式管理。
- 避免滥用 refs/$parent:直接操作组件实例会降低代码可维护性,仅在必要时使用(如操作 DOM 元素)。
通过合理选择通信方式,可确保组件间数据流动清晰,提升代码的可维护性和可测试性。
简述 Vue3 的生命周期钩子函数(如 setup、onMounted、onUnmounted 等)
Vue3 基于 Composition API 重构了生命周期机制,钩子函数的使用方式与 Vue2 的选项式 API 有所不同。以下是 Vue3 中主要生命周期钩子的作用、触发时机及使用场景:
1. setup:组合式 API 的入口
- 触发时机:在组件创建之前,beforeCreate 和 created 钩子之前执行,是 Composition API 的起点。
- 作用 :
- 初始化响应式状态(通过
ref
或reactive
)。 - 注册生命周期钩子、事件监听等。
- 返回对象或函数,供模板或其他钩子使用。
- 初始化响应式状态(通过
- 注意 :
-
无法访问
this
(组件实例未创建),需通过参数获取 props 和 context。 -
若使用
setup
,则不再需要选项式的data
、methods
等选项。export default {
setup(props, context) {
// 响应式状态
const count = ref(0)
// 生命周期钩子需在 setup 内调用
onMounted(() => {
console.log('组件挂载完成')
})
return { count }
}
}
-
2. 挂载阶段钩子
- onBeforeMount :组件即将挂载到 DOM 前触发,对应 Vue2 的
beforeMount
。 - onMounted:组件挂载到 DOM 后触发,可在此访问真实 DOM,发送异步请求等。
3. 更新阶段钩子
- onBeforeUpdate :数据更新导致组件重新渲染前触发,对应 Vue2 的
beforeUpdate
。 - onUpdated:组件重新渲染完成后触发,此时 DOM 已更新,可进行 DOM 操作(注意避免在此期间修改响应式数据,可能引发无限循环)。
4. 卸载阶段钩子
- onBeforeUnmount :组件即将卸载前触发,对应 Vue2 的
beforeDestroy
。 - onUnmounted:组件卸载后触发,用于清理副作用(如清除定时器、解绑事件监听等)。
5. 错误处理钩子
-
onErrorCaptured :捕获组件内的错误(包括子组件错误),可用于错误日志上报。
setup() { onErrorCaptured((error, instance, info) => { console.error('捕获到错误:', error, info) return true // 阻止错误继续向上传播 }) }
6. 与 Vue2 钩子的对应关系
Vue2 选项式钩子 | Vue3 组合式钩子(需在 setup 中调用) |
---|---|
beforeCreate | 无(setup 替代其初始化逻辑) |
created | 无(setup 替代其初始化逻辑) |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
errorCaptured | onErrorCaptured |
使用注意事项
-
组合式 API 的灵活性 :在
setup
中可按需引入生命周期钩子,逻辑按功能分组,而非按生命周期阶段排列,提升代码可读性。 -
副作用清理 :在
onUnmounted
中清除setup
中创建的定时器、事件监听器等,避免内存泄漏。例如:setup() { const timer = setInterval(() => { /* ... */ }, 1000) onUnmounted(() => clearInterval(timer)) }
-
避免重复调用 :生命周期钩子需在
setup
内直接调用,不要在函数或条件语句中嵌套调用,确保每个钩子只注册一次。
Vue3 的生命周期机制更贴合组合式开发模式,通过将逻辑拆分为独立的钩子函数,使代码结构更清晰,尤其适合复杂组件的逻辑复用和测试。
描述父组件和子组件的完整生命周期执行流程
在 Vue 中,组件的生命周期执行顺序与组件嵌套关系密切相关。父组件与子组件的生命周期钩子会按照特定顺序触发,理解这一流程有助于调试和处理组件间的交互逻辑。以下是完整的执行流程分析(以 Vue3 为例,结合选项式和组合式 API):
一、组件初始化阶段(挂载前)
-
父组件创建
- 触发 beforeCreate (选项式 API,仅 Vue2 存在)或进入
setup
(组合式 API,Vue2/3 均适用)。 - 在
setup
中初始化响应式状态、注册生命周期钩子(如onBeforeMount
)。
- 触发 beforeCreate (选项式 API,仅 Vue2 存在)或进入
-
子组件创建
- 父组件在模板中调用子组件时,子组件进入创建阶段:
- 触发子组件的 beforeCreate 或
setup
。 - 子组件在
setup
中完成状态初始化和钩子注册。
- 触发子组件的 beforeCreate 或
- 父组件在模板中调用子组件时,子组件进入创建阶段:
-
父组件 props 传递
- 父组件将 props 数据传递给子组件,子组件接收并校验 props。
-
父组件 beforeMount
- 触发父组件的 onBeforeMount (组合式)或
beforeMount
(选项式),此时组件尚未挂载到 DOM。
- 触发父组件的 onBeforeMount (组合式)或
-
子组件 beforeMount
- 触发子组件的 onBeforeMount 或
beforeMount
,父子组件均处于 "即将挂载" 状态。
- 触发子组件的 onBeforeMount 或
二、组件挂载阶段(插入 DOM)
-
子组件先挂载
-
子组件的虚拟 DOM 先渲染为真实 DOM,触发 onMounted (组合式)或
mounted
(选项式)。 -
此时子组件已可访问真实 DOM,例如:
// 子组件 setup() { onMounted(() => { console.log('子组件 mounted') // 先输出 }) }
-
-
父组件后挂载
-
父组件的虚拟 DOM 渲染完成,包含子组件的 DOM 结构,触发父组件的 onMounted 或
mounted
。 -
父组件此时可访问包含子组件的完整 DOM 树:
// 父组件 setup() { onMounted(() => { console.log('父组件 mounted') // 后输出 }) }
总结顺序:
父 beforeCreate → 父 setup → 子 beforeCreate → 子 setup → 父 beforeMount → 子 beforeMount → 子 mounted → 父 mounted
-
三、组件更新阶段(数据变化时)
当父组件或子组件的响应式数据变化时,会触发重新渲染,流程如下:
-
父组件数据更新
- 父组件状态变更,触发 onBeforeUpdate (组合式)或
beforeUpdate
(选项式)。
- 父组件状态变更,触发 onBeforeUpdate (组合式)或
-
子组件接收更新后的 props
- 父组件传递的 props 变化,子组件触发 onBeforeUpdate 或
beforeUpdate
。
- 父组件传递的 props 变化,子组件触发 onBeforeUpdate 或
-
子组件先更新
- 子组件虚拟 DOM 重新渲染,触发 onUpdated 或
updated
。
- 子组件虚拟 DOM 重新渲染,触发 onUpdated 或
-
父组件后更新
-
父组件虚拟 DOM 重新渲染完成,触发 onUpdated 或
updated
。
总结顺序:父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated
-
四、组件卸载阶段(移除 DOM)
当父组件或子组件被卸载(如通过 v-if
控制显示 / 隐藏)时:
-
父组件触发卸载
- 父组件进入卸载流程,触发 onBeforeUnmount 或
beforeUnmount
。
- 父组件进入卸载流程,触发 onBeforeUnmount 或
-
子组件先卸载
-
子组件的所有生命周期钩子优先执行清理逻辑,触发 onUnmounted 或
destroyed
。 -
例如,子组件中清除定时器:
setup() { const timer = setInterval(() => { /* ... */ }, 1000) onUnmounted(() => clearInterval(timer)) // 子组件先执行 }
-
-
父组件后卸载
-
父组件完成卸载,触发 onUnmounted 或
destroyed
,此时父子组件的 DOM 已被移除。
总结顺序:父 beforeUnmount → 子 beforeUnmount → 子 unmounted → 父 unmounted
-
五、特殊场景:子组件内异步组件或动态组件
- 若子组件是异步组件(通过
defineAsyncComponent
创建)或动态组件(使用<component :is="currentComponent"
),其生命周期钩子会在组件实际加载或切换时触发,顺序遵循上述规则。 - 例如,异步子组件的
mounted
会在其加载完成并插入 DOM 后触发,晚于父组件的mounted
。
关键记忆点
- 挂载 / 卸载顺序 :子组件先于父组件完成挂载和卸载,即 "先子后父"。
- 更新顺序 :父组件先触发更新钩子,子组件后执行,但子组件实际更新完成早于父组件(虚拟 DOM diff 从子节点开始)。
- 组合式 API 与选项式 API 共存 :在 Vue3 中,若同时使用选项式和组合式钩子,组合式钩子(如
onMounted
)会在选项式钩子(如mounted
)之前触发。
理解这些流程有助于在开发中合理安排逻辑,例如在父组件 mounted
中访问子组件状态时,需确保子组件已挂载完成,或通过 ref
引用配合 onMounted
回调实现安全访问。
watch 和 computed 在数据变化时的区别是什么?
在 Vue 中,watch
和 computed
是响应式系统的重要工具,用于处理数据变化后的逻辑,但两者的设计理念和使用场景有显著差异。以下从多个维度对比分析:
一、本质区别:被动监听 vs 主动计算
-
computed:
- 基于依赖的缓存计算 :内部维护一个缓存,只有当依赖的响应式数据(如
data
、props
中的属性)发生变化时,才会重新计算结果。 - 自动更新视图:计算结果会被模板或其他响应式函数依赖,当结果变化时自动触发视图更新。
- 适用于复杂逻辑的实时计算,例如表单联动、数据格式化等。
- 基于依赖的缓存计算 :内部维护一个缓存,只有当依赖的响应式数据(如
-
watch:
- 被动监听数据变化 :监听特定数据(如
data
、props
、computed
的值)的变化,当数据变化时执行回调函数。 - 可执行异步操作或副作用:如发送网络请求、修改非响应式数据、操作 DOM 等。
- 适用于需要 "观察" 数据变化并执行特定逻辑的场景,例如搜索框防抖、状态变更通知等。
- 被动监听数据变化 :监听特定数据(如
二、触发时机与执行方式
特性 | computed | watch |
---|---|---|
触发条件 | 依赖数据变化时自动重新计算 | 监听的数据变化时触发回调 |
首次执行 | 首次渲染时触发(需被模板引用) | 需配置 immediate: true 才会在初始化时执行 |
执行次数 | 依赖不变则复用缓存结果,仅执行一次 | 每次监听数据变化均执行 |
返回值 | 必须返回计算结果(用于响应式依赖) | 无强制返回值(可执行任意逻辑) |
异步支持 | 不支持(同步计算) | 支持(回调中可使用 async/await) |
示例对比:
<!-- computed 示例:实时计算总价 -->
<template>
<div>
单价:{{ price }} 元<br>
数量:{{ count }} 件<br>
总价:{{ totalPrice }} 元
</div>
</template>
<script>
export default {
data() {
return { price: 100, count: 2 }
},
computed: {
totalPrice() {
console.log('计算总价') // 仅在 price 或 count 变化时触发
return this.price * this.count
}
}
}
</script>
<!-- watch 示例:监听搜索词并发起请求 -->
<template>
<input v-model="searchKey" />
</template>
<script>
export default {
data() {
return { searchKey: '' }
},
watch: {
searchKey(newVal, oldVal) {
if (newVal.trim()) {
this.fetchData(newVal) // 异步请求
}
},
immediate: true // 初始化时执行一次
},
methods: {
async fetchData(key) {
const result = await axios.get(`/api/search?key=${key}`)
this.results = result.data
}
}
}
</script>
三、依赖管理与性能影响
-
computed 的依赖收集 :
在计算函数中访问的响应式数据会被自动收集为依赖,例如
totalPrice
依赖price
和count
。当且仅当这两个值变化时,才会重新计算totalPrice
,避免无效渲染,提升性能。 -
watch 的依赖显式声明 :
监听的数据源可以是单个属性(如
searchKey
),也可以是复杂表达式(如'obj.a + obj.b'
),但需注意:- 监听对象属性时,需使用深度监听(
deep: true
)才能捕获对象内部变化。 - 监听数组时,默认只能捕获数组引用的变化,无法检测元素新增 / 删除(需通过
deep
或特定方法触发)。
- 监听对象属性时,需使用深度监听(
深度监听示例:
watch: {
obj: {
handler(newVal, oldVal) {
// 监听对象内部属性变化
},
deep: true // 开启深度监听
}
}
四、适用场景总结
场景描述 | 推荐使用 | 原因 |
---|---|---|
复杂数据的实时计算(如过滤、求和) | computed | 自动缓存,避免重复计算,响应式依赖清晰 |
数据变化时触发异步操作(如请求) | watch | 支持异步逻辑,可访问新旧值 |
数据变化时执行多步操作或副作用 | watch | 可执行任意逻辑,如修改非响应式变量、操作 DOM |
初始化时执行一次逻辑 | watch(配 immediate) | computed 需被引用才会执行,watch 可通过配置直接初始化执行 |
监听对象 / 数组的深层变化 | watch(配 deep) | computed 无法直接处理深层依赖,需手动拆解为多个属性 |
五、注意事项
-
避免滥用 computed :若计算逻辑中包含异步操作或副作用(如修改 DOM),会导致逻辑混乱,此时应使用
watch
。 -
watch 的性能优化 :
- 监听复杂对象时,优先拆解为多个简单属性监听,避免
deep: true
带来的性能开销。 - 使用
debounce
或throttle
优化高频触发的监听(如搜索框输入)。
- 监听复杂对象时,优先拆解为多个简单属性监听,避免
-
组合使用场景 :
有时需要结合两者,例如先用computed
处理数据转换,再用watch
监听转换后的结果并执行副作用:computed: { formattedValue() { return this.rawValue.toUpperCase() } }, watch: { formattedValue(newVal) { this.logToConsole(newVal) } }
什么是 Vue3 的组合式 API(Composition API)?列举常用的组合式 API(如 ref、reactive、watchEffect 等)
Vue3 的 ** 组合式 API(Composition API)** 是一种基于函数的组件开发模式,允许开发者通过组合不同的逻辑函数来组织组件代码,而非依赖传统的选项式 API(如 data
、methods
、computed
等选项)。它解决了选项式 API 中逻辑复用困难、组件代码碎片化等问题,使代码更易维护、测试和复用。
一、核心思想:逻辑组合与复用
- 按功能分组:将相关逻辑(如数据获取、DOM 操作、状态管理)封装为独立的函数(称为 "组合函数"),可在不同组件中重复使用,避免选项式 API 中同一功能逻辑分散在多个选项中的问题。
- 响应式系统底层统一 :通过
ref
和reactive
创建响应式数据,watch
和watchEffect
实现依赖监听,使逻辑更贴近原生 JavaScript。 - 无 this 上下文依赖 :组合函数中无需依赖组件实例
this
,降低了代码的隐性依赖,提升可读性。
二、常用组合式 API 及功能
以下是 Vue3 中最常用的组合式 API,按功能分类说明:
1. 响应式数据创建
-
ref
:创建响应式引用-
用于创建单个响应式变量,可存储任意类型(包括基本类型和对象)。
-
基本类型需通过
.value
访问,对象类型会被自动转为reactive
代理。import { ref } from 'vue'
const count = ref(0)
count.value++ // 修改值
-
-
reactive
:创建响应式对象-
用于将普通对象转为响应式代理,适用于复杂数据结构(如对象、数组)。
-
内部基于 ES6 Proxy 实现,可监听对象属性的新增、删除和修改。
import { reactive } from 'vue'
const state = reactive({
user: { name: 'Alice' },
list: [1, 2, 3]
})
state.user.name = 'Bob' // 响应式更新
-
-
readonly
:创建只读响应式数据-
接收
ref
或reactive
对象,返回一个只读的代理,禁止修改原始数据。const original = ref(10)
const readonlyVal = readonly(original)
// readonlyVal.value = 20 // 报错:无法修改只读属性
-
2. 依赖监听与副作用
-
watch
:显式监听响应式数据-
监听单个或多个响应式数据源(
ref
、reactive
属性、计算值等),支持深度监听对象 / 数组。 -
回调函数接收新值和旧值,可用于执行异步操作或副作用。
import { watch, ref } from 'vue'
const searchKey = ref('')
watch(searchKey, (newVal, oldVal) => {
if (newVal.trim()) {
fetchData(newVal) // 异步请求
}
}, { immediate: true }) // 初始化时执行
-
-
watchEffect
:自动追踪依赖的副作用-
无需显式声明监听源,回调函数中使用的响应式数据会被自动收集依赖,依赖变化时重新执行回调。
-
适用于执行与响应式数据相关的副作用(如 DOM 更新、定时器、日志输出)。
import { watchEffect, ref } from 'vue'
const count = ref(0)
watchEffect(() => {
console.log('Count is:', count.value) // 依赖 count.value,自动追踪
})
count.value = 1 // 触发回调
-
-
watchPostEffect
:延迟执行副作用-
与
watchEffect
类似,但回调会在组件更新完成后执行,确保能访问最新 DOM。watchPostEffect(() => {
// 这里可以安全地操作 DOM
})
-
3. 计算属性与依赖收集
computed
:创建响应式计算属性-
与选项式 API 中的
computed
功能一致,返回一个只读的ref
对象,依赖变化时自动重新计算。import { ref, computed } from 'vue'
const a = ref(1)
const b = ref(2)
const sum = computed(() => a.value + b.value) // 依赖 a 和 b
-
4. 生命周期钩子
在组合式 API 中,生命周期钩子通过独立函数引入,需在 setup
或组合函数中调用:
-
onBeforeMount
:组件挂载前触发。 -
onMounted
:组件挂载后触发(可访问 DOM)。 -
onBeforeUpdate
:组件更新前触发。 -
onUpdated
:组件更新后触发(DOM 已更新)。 -
onBeforeUnmount
:组件卸载前触发。 -
onUnmounted
:组件卸载后触发(清理副作用)。import { onMounted, ref } from 'vue'
setup() {
const timer = setInterval(() => { /* ... */ }, 1000)
onUnmounted(() => clearInterval(timer)) // 卸载时清理定时器
}
5. 依赖注入与上下文
-
provide
和inject
:跨层级组件通信,替代选项式 API 中的provide/inject
。// 父组件 import { provide, ref } from 'vue' provide('theme', ref('light')) // 子组件 import { inject } from 'vue' const theme = inject('theme')
-
useContext
:获取当前组件的上下文(如attrs
、slots
、emit
),替代选项式 API 中的this.$attrs
等。import { useContext } from 'vue' const { emit, slots } = useContext()
6. 其他实用工具
-
toRef
:为响应式对象属性创建独立 ref从
reactive
对象中提取属性并转为ref
,保持与原始对象的响应式关联:const state = reactive({ name: 'Alice' }) const nameRef = toRef(state, 'name') nameRef.value = 'Bob' // 会同步修改 state.name
-
toRefs
:批量转换对象属性为 refs将
reactive
对象的所有属性转为独立的ref
,方便解构赋值:const state = reactive({ x: 0, y: 0 }) const { x, y } = toRefs(state) // x 和 y 都是 ref 对象
-
shallowRef
/shallowReactive
:浅响应式仅对第一层属性进行响应式代理,适用于性能优化或无需深层监听的场景:
const shallowState = shallowReactive({ obj: { a: 1 } }) shallowState.obj.a = 2 // 不会触发响应式更新
三、组合式 API 的优势
- 逻辑复用更灵活 :通过组合函数(如
useFetch
、useTimer
)封装可复用逻辑,避免选项式 API 中 Mixin 的命名冲突和隐性依赖问题。 - 代码组织更清晰:按功能(如数据获取、表单验证)分组代码,而非按生命周期阶段排列,提升可读性。
- 更好的 Tree-shaking :按需引入 API(如仅使用
ref
和watch
),减少打包体积。 - 支持 TypeScript:类型推断更友好,组合函数可显式声明参数和返回值类型。
四、与选项式 API 的对比
特性 | 组合式 API | 选项式 API |
---|---|---|
代码组织方式 | 函数式,按功能分组 | 对象式,按选项分组 |
逻辑复用 | 组合函数(可直接导入) | Mixin / 组件继承(易冲突) |
this 依赖 | 无(避免隐性上下文问题) | 依赖组件实例 this |
类型支持 | 优秀(TS 友好) | 较弱(需额外声明) |
大型组件维护 | 更易拆分和测试 | 逻辑分散在多个选项中 |
Vue3 同时支持组合式 API 和选项式 API,但组合式 API 是官方推荐的先进模式,尤其适合中大型项目和逻辑复杂的组件开发。通过合理使用组合式 API,可显著提升开发效率和代码质量。
在 Vue3 中如何自定义一个 v-model 指令?
在 Vue3 中自定义 v-model
指令需结合组件的 modelValue
props 和 update:modelValue
事件实现双向绑定。原生 v-model
本质是语法糖,会展开为接收 modelValue
并监听 update:modelValue
事件的形式,因此自定义指令需遵循这一模式。
核心步骤如下:
- 定义子组件 :声明
modelValue
作为 props 接收父组件值,并通过defineEmits
定义update:modelValue
事件用于更新父组件数据。 - 绑定交互元素 :在子组件模板中,将
modelValue
绑定到表单元素(如input
)的value
属性,并在元素事件(如input
事件)中触发update:modelValue
事件,传递新值。 - 父组件使用 :通过
v-model
指令绑定子组件,此时 Vue 会自动将指令的值作为modelValue
传入子组件,并监听更新事件。
示例代码 :
子组件(CustomInput.vue)
<template>
<input :value="modelValue" @input="handleInput" />
</template>
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const handleInput = (e) => {
emit('update:modelValue', e.target.value)
}
</script>
父组件使用
<template>
<CustomInput v-model="parentValue" />
<p>父组件值:{{ parentValue }}</p>
</template>
<script setup>
import { ref } from 'vue'
const parentValue = ref('初始值')
</script>
进阶场景:多个 v-model 绑定
若需在组件中定义多个 v-model
绑定,可通过 model
选项指定不同的 prop 和事件名。例如:
<template>
<input :value="name" @input="updateName" />
<input :value="age" @input="updateAge" />
</template>
<script setup>
const props = defineProps({
name: String,
age: Number
})
const emit = defineEmits(['update:name', 'update:age'])
const updateName = (e) => {
emit('update:name', e.target.value)
}
const updateAge = (e) => {
emit('update:age', e.target.value)
}
</script>
父组件中可分别绑定:
<MultiModelComponent
v-model:name="userName"
v-model:age="userAge"
/>
注意事项:
- 自定义
v-model
本质是通过 props 和事件实现双向通信,需确保数据流动单向(父 → 子通过 props,子 → 父通过事件)。 - 若子组件需要处理非原始值(如对象),需注意引用类型的响应式问题,可结合
watch
或toRefs
保持响应性。 - 避免在子组件中直接修改
modelValue
原值,应通过事件触发更新,以遵循 Vue 的单向数据流原则。
通过这种方式,自定义 v-model
指令可灵活适配各种组件交互场景,如自定义表单组件、数据展示组件等,提升代码复用性和组件封装能力。
项目中如何封装 Axios?请说明封装的思路和步骤。
在前端项目中封装 Axios 的核心目标是统一处理请求 / 响应逻辑、简化重复代码、增强可维护性和扩展性。以下是具体的封装思路和步骤:
一、基础封装:设置全局配置与拦截器
-
创建 Axios 实例
通过
axios.create()
创建独立实例,避免污染全局配置。例如:import axios from 'axios' const service = axios.create({ baseURL: import.meta.env.VITE_APP_API_BASE, // 环境变量配置接口前缀 timeout: 5000, // 请求超时时间 withCredentials: true // 跨域时携带 cookie })
-
请求拦截器:统一处理请求头
在请求发送前添加公共参数、Token 等,例如:
service.interceptors.request.use( (config) => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` // 添加 Token 到请求头 } // 统一添加时间戳防止缓存(可选) if (config.method === 'get') { config.params = { ...config.params, timestamp: Date.now() } } return config }, (error) => { return Promise.reject(error) // 处理请求错误 } )
-
响应拦截器:统一处理响应结果
解析响应数据、处理错误状态码(如 401 未认证、500 服务器错误):
service.interceptors.response.use( (response) => { const { data } = response // 假设后端返回格式:{ code: number, data: any, message: string } if (data.code === 200) { return data.data // 正常响应返回业务数据 } else { // 处理业务错误(如提示用户) console.error('业务错误:', data.message) return Promise.reject(new Error(data.message)) } }, (error) => { // 处理网络错误或状态码非 2xx 的情况 const status = error.response?.status switch (status) { case 401: // 跳转到登录页 router.push('/login') break case 404: console.error('接口不存在') break default: console.error('网络请求失败:', error.message) } return Promise.reject(error) } )
二、模块化封装:按业务拆分接口
将不同业务的接口分文件管理,例如在 src/api
目录下创建 user.js
、order.js
等文件:
// src/api/user.js
import service from '@/utils/axios'
// 登录接口
export const login = (data) => {
return service.post('/user/login', data)
}
// 获取用户信息
export const getUserInfo = () => {
return service.get('/user/info')
}
优势:
- 接口按功能分组,方便查找和维护;
- 可单独 mock 某模块接口,便于单元测试;
- 避免单个文件过于臃肿。
三、高级封装:支持请求配置扩展
为满足复杂场景(如上传文件、取消请求等),可在基础封装上添加参数扩展:
-
支持自定义配置
在请求函数中接收第三个参数
config
,合并到 Axios 配置中:export const uploadFile = (data, config = {}) => { return service.post('/file/upload', data, { headers: { 'Content-Type': 'multipart/form-data' }, ...config // 允许传入自定义配置覆盖默认值 }) }
-
请求取消(AbortController)
使用浏览器原生
AbortController
取消未完成的请求,避免组件卸载后请求回调导致的内存泄漏:export const fetchDataWithCancel = () => { const controller = new AbortController() service.get('/data', { signal: controller.signal }) .then((res) => { /* 处理结果 */ }) return { controller } // 返回取消控制器供外部调用 } // 使用时 const { controller } = fetchDataWithCancel() // 取消请求 controller.abort()
四、集成到 Vue 项目(以 Vue3 为例)
-
全局挂载
在
main.js
中挂载封装好的service
到app.config.globalProperties
,方便组件中直接使用:import { createApp } from 'vue' import App from './App.vue' import service from './utils/axios' const app = createApp(App) app.config.globalProperties.$http = service app.mount('#app')
-
组件中使用
<script setup> import { getCurrentInstance } from 'vue' const { appContext } = getCurrentInstance() const $http = appContext.config.globalProperties.$http // 调用接口 const fetchUser = async () => { try { const data = await $http.get('/user/info') console.log('用户信息:', data) } catch (error) { console.error('请求失败:', error) } } </script>
封装的核心优势
- 统一错误处理:避免每个接口重复编写错误处理逻辑,提升代码一致性;
- 便捷的环境切换 :通过
baseURL
配置不同环境(开发、测试、生产)的接口地址,配合.env
文件管理; - 可扩展性强:通过拦截器轻松添加日志记录、请求加密、参数校验等功能;
- 组件解耦:接口调用逻辑与组件分离,便于复用和维护。
通过以上步骤,可构建一个健壮、灵活的 Axios 封装方案,适应大型项目的复杂需求。
React 和 Vue 的主要区别有哪些?(可结合数据驱动、组件模型、生态等方面)
React 与 Vue 作为主流前端框架,在设计理念和实现方式上存在显著差异,以下从核心机制、组件模型、生态等维度对比分析:
一、数据驱动:响应式原理的差异
-
React :基于 状态(State)驱动视图 ,采用 不可变数据(Immutable Data) 和 虚拟 DOM(Virtual DOM) 机制。
- 状态更新需通过
setState
或 Hooks(如useState
)触发,且每次更新会生成新的虚拟 DOM 树,通过 diff 算法 对比差异后批量更新真实 DOM。 - 优点:数据流动清晰(单向数据流),便于追踪状态变化;虚拟 DOM 抽象层使跨平台(如 React Native)成为可能。
- 缺点:频繁状态更新可能导致不必要的重新渲染,需通过
useMemo
、useCallback
等优化性能。
- 状态更新需通过
-
Vue :基于 响应式系统(Reactive System) ,通过 Proxy(Vue3)或 Object.defineProperty(Vue2) 劫持数据变化,精准更新依赖组件。
- 状态直接修改响应式数据(如
this.count = 1
)即可触发视图更新,无需手动调用更新函数。 - 优点:细粒度响应式更新,性能优化更精准;模板语法直观,接近原生 HTML。
- 缺点:响应式依赖收集在复杂场景下可能存在边界问题(如动态添加对象属性需手动处理)。
- 状态直接修改响应式数据(如
二、组件模型:声明方式与逻辑组织
-
React:
- 组件声明 :以 函数组件(Functional Component) 为主(推荐使用 Hooks),类组件(Class Component)逐渐被淘汰。
- 逻辑复用 :通过 自定义 Hooks (如
useFetch
、useForm
)实现逻辑共享,灵活性高但学习成本较高。 - 模板语法 :使用 JSX(JavaScript XML),将 HTML 与 JavaScript 深度融合,可直接在模板中编写表达式和逻辑。
-
Vue:
- 组件声明 :采用 单文件组件(.vue) 格式,包含
<template>
(模板)、<script>
(逻辑)、<style>
(样式),结构清晰易上手。 - 逻辑复用 :通过 Mixin (Vue2)或 组合式 API(Composition API,Vue3) 实现逻辑抽离,组合式 API 更贴合函数式编程风格。
- 模板语法 :使用 模板表达式(Mustache) 和 指令(Directives) (如
v-if
、v-for
),语法简洁,对 HTML 开发者更友好。
- 组件声明 :采用 单文件组件(.vue) 格式,包含
三、生态系统:工具链与社区支持
-
React:
- 核心生态 :依赖 npm 生态 ,主力库包括
react-router
(路由)、redux
/zustand
(状态管理)、react-dom
(DOM 操作)。 - 工具链 :官方推荐 Create React App(CRA) 初始化项目,搭配
Webpack
/Vite
构建,调试工具有 React DevTools。 - 社区与就业:生态成熟,适合大型团队开发复杂应用(如企业级后台、电商平台),但需学习较多周边库(如状态管理、路由)。
- 核心生态 :依赖 npm 生态 ,主力库包括
-
Vue:
- 核心生态 :官方提供 Vue CLI 和 Vite 快速搭建项目,配套库包括
vue-router
(路由)、pinia
(状态管理)、vuex
(Vue2 主流)。 - 工具链 :单文件组件支持热更新和样式作用域(
scoped
),生态更轻量,入门门槛低。 - 社区与就业 :国内社区活跃,适合中小项目快速开发,也可通过 Nuxt.js 构建 SSR 应用(如博客、营销网站)。
- 核心生态 :官方提供 Vue CLI 和 Vite 快速搭建项目,配套库包括
四、性能与优化
-
React:
- 虚拟 DOM 的 diff 算法在复杂列表渲染时可能存在性能瓶颈,需通过
key
属性优化列表 diff,或使用react-window
等库实现虚拟列表。 - 函数组件的 memoization(记忆化)依赖
useMemo
和useCallback
,需手动优化避免不必要的重新渲染。
- 虚拟 DOM 的 diff 算法在复杂列表渲染时可能存在性能瓶颈,需通过
-
Vue:
- 响应式系统自动追踪依赖,组件更新粒度更细,在数据驱动的简单场景下性能更优。
- Vue3 引入 Proxy 响应式 和 Composition API,进一步提升了大型应用的可维护性和性能表现。
五、适用场景
-
选择 React 的场景:
- 构建大型单页应用(SPA)或需要复杂状态管理的项目(如社交平台、仪表盘);
- 跨平台开发(如同时开发 Web 和移动端应用);
- 团队熟悉函数式编程,或需要使用前沿技术(如 Server Components)。
-
选择 Vue 的场景:
- 快速开发中小型项目(如企业官网、内部管理系统);
- 团队中有较多 HTML/CSS 开发者,希望降低学习成本;
- 需要与现有 jQuery 或旧项目集成,或使用 SSR(如 Nuxt.js 构建 SEO 友好型网站)。
简述 React 的 Hooks 机制(如 useState、useEffect、useContext 等)。
React 的 Hooks 是 React 16.8 引入的新特性,允许在函数组件中使用状态和副作用等功能,彻底改变了函数组件的开发模式。以下从核心 Hooks 原理、使用规则及实践场景展开说明:
一、核心 Hooks 解析
1. useState:状态管理的核心
-
作用 :为函数组件添加状态,替代类组件的
this.state
和setState
。 -
语法 :
const [state, setState] = useState(initialState)
state
是当前状态值,setState
是更新状态的函数。initialState
可以是任意类型(如数值、对象、数组),首次渲染后不可修改(仅在初始化时生效)。
-
更新机制:
- 调用
setState
会触发组件重新渲染,并将新状态值传入下一次渲染的组件函数中。 - 若新状态与旧状态 ** 浅比较(shallow comparison)** 相等,React 会跳过渲染以优化性能。
- 调用
-
示例:
import { useState } from 'react' function Counter() { const [count, setCount] = useState(0) return ( <div> <p>计数:{count}</p> <button onClick={() => setCount(count + 1)}>+1</button> </div> ) }
2. useEffect:副作用操作的入口
-
作用 :处理组件渲染后的副作用(如数据请求、DOM 操作、订阅 / 取消订阅),替代类组件的
componentDidMount
、componentDidUpdate
和componentWillUnmount
。 -
语法 :
useEffect(effect, dependencies?)
effect
是副作用函数,可返回一个清理函数(用于清除副作用,如取消订阅、清除定时器)。dependencies
是依赖数组,决定effect
的触发时机:- 空数组(
[]
):仅在组件挂载时执行一次(类似componentDidMount
); - 包含状态或 props(如
[count]
):当依赖项变化时执行(类似componentDidUpdate
); - 不传入依赖数组(不推荐):每次组件渲染后都会执行(性能隐患)。
- 空数组(
-
执行流程:
- 组件首次挂载后,执行
effect
; - 依赖项变化时,先执行上一次
effect
的清理函数(若有),再执行新的effect
; - 组件卸载时,执行清理函数(若有)。
- 组件首次挂载后,执行
-
示例:模拟数据请求
function UserInfo({ userId }) { const [user, setUser] = useState(null) useEffect(() => { // 挂载时发送请求 fetch(`/api/users/${userId}`) .then(res => res.json()) .then(data => setUser(data)) // 清理函数:组件卸载时取消未完成的请求(需配合 AbortController) return () => { // 此处可添加取消请求的逻辑 } }, [userId]) // 仅当 userId 变化时重新请求 return user ? <div>用户:{user.name}</div> : <p>加载中...</p> }
3. useContext:跨组件层级的数据传递
-
作用 :替代类组件的
contextType
或Consumer
组件,实现跨层级组件的数据共享(无需逐层传递 props)。 -
使用步骤:
-
创建 Context 对象:
const ThemeContext = createContext(defaultValue)
-
在顶层组件提供数据:
<ThemeContext.Provider value={theme}> <App /> </ThemeContext.Provider>
-
在任意子组件中消费数据:
function Button() { const theme = useContext(ThemeContext) return <button style={{ color: theme.color }}>按钮</button> }
-
-
注意事项:
useContext
的参数是createContext
返回的对象,消费时会订阅该 Context 的变化;- 当
Provider
的value
引用变化时,所有消费该 Context 的组件都会重新渲染,需通过useMemo
优化value
的稳定性。
二、Hooks 的规则(必须严格遵守)
-
只能在函数组件或自定义 Hooks 中调用
- 禁止在循环、条件判断或普通函数中调用 Hooks,确保 Hooks 的调用顺序在每次渲染时一致。
-
优先使用数组进行依赖管理
useEffect
、useMemo
等 Hooks 的依赖数组需包含所有在副作用中使用的 props 或 state,避免出现 "过时闭包" 问题(即副作用中访问的状态不是最新值)。
-
自定义 Hooks 需以 use 开头
- 遵循命名约定(如
useFetch
、useForm
),便于 React 识别和校验 Hooks 的调用规则。
- 遵循命名约定(如
三、常见自定义 Hooks 场景
-
数据请求封装
function useFetch(url) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) useEffect(() => { fetch(url) .then(res => res.json()) .then(data => { setData(data) setLoading(false) }) }, [url]) return { data, loading } }
-
表单处理
function useForm(initialState) { const [form, setForm] = useState(initialState) const handleChange = (e) => { const { name, value } = e.target setForm(prev => ({ ...prev, [name]: value })) } return { form, handleChange } }
-
窗口尺寸监听
function useWindowSize() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight }) useEffect(() => { const handleResize = () => { setSize({ width: window.innerWidth, height: window.innerHeight }) } window.addEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize) }, []) // 仅在挂载时添加监听 return size }
四、Hooks 的优势与挑战
-
优势:
- 逻辑复用更灵活:通过自定义 Hooks 抽离重复逻辑,避免 Mixin 的命名冲突问题;
- 组件更简洁:函数组件无类组件的生命周期钩子分散问题,逻辑按副作用类型组织;
- 性能优化更精准:依赖数组明确控制副作用触发时机,减少不必要的重新渲染。
-
挑战:
- 闭包陷阱:Hooks 的函数作用域可能导致回调函数中捕获的状态不是最新值,需正确管理依赖数组;
- 调试难度:复杂自定义 Hooks 的逻辑嵌套可能增加调试成本,需借助 React DevTools 的 Hooks 面板追踪状态变化。
Hooks 机制彻底释放了函数组件的潜力,使其成为 React 开发的主流模式。理解 Hooks 的原理和规则,是掌握现代 React 开发的核心关键。
React 函数组件和类组件(Class Component)的区别是什么?函数组件有哪些改进?
在 React 中,函数组件和类组件是两种不同的组件声明方式,随着版本迭代,函数组件逐渐成为主流。以下从核心特性、开发模式、性能等方面对比两者差异,并分析函数组件的改进点:
一、本质区别:组件的定义方式
-
类组件(Class Component)
-
基于 ES6
class
语法定义,继承自React.Component
。 -
需要通过
this.state
管理状态,通过this.setState()
更新状态。 -
包含生命周期钩子函数(如
componentDidMount
、shouldComponentUpdate
)。 -
示例:
class Counter extends React.Component { state = { count: 0 } handleClick = () => { this.setState({ count: this.state.count + 1 }) } render() { return ( <div> <p>计数:{this.state.count}</p> <button onClick={this.handleClick}>+1</button> </div> ) } }
-
-
函数组件(Functional Component)
-
本质是 JavaScript 函数,接收
props
并返回 JSX。 -
早期(React 16.8 前)仅用于无状态组件(Stateless Component),无法管理状态或副作用。
-
React 16.8 引入 Hooks 后,函数组件可通过
useState
、useEffect
等 Hooks 实现类组件的所有功能。 -
示例(使用 Hooks):
import { useState } from 'react' function Counter() { const [count, setCount] = useState(0) return ( <div> <p>计数:{count}</p> <button onClick={() => setCount(count + 1)}>+1</button> </div> ) }
-
二、核心特性对比
特性 | 类组件 | 函数组件(含 Hooks) |
---|---|---|
状态管理 | 通过 this.state 和 setState 管理 |
通过 useState 钩子函数管理 |
生命周期 | 包含 mount 、update 、unmount 钩子 |
通过 useEffect 统一处理副作用 |
逻辑复用 | 通过 Mixin、HOC(高阶组件)实现 | 通过自定义 Hooks 实现,更灵活轻量 |
性能优化 | shouldComponentUpdate 、PureComponent |
React.memo 、useMemo 、useCallback |
代码可读性 | 逻辑按生命周期钩子拆分,可能较分散 | 逻辑按副作用类型组织,更集中 |
this 指向问题 | 需手动绑定 this (如箭头函数) |
无 this 上下文,函数作用域更清晰 |
跨平台支持 | 依赖 react-dom |
更易适配 React Native 等非 DOM 环境 |
三、函数组件的主要改进
1. 抛弃 this
,避免上下文混乱
类组件中 this
指向容易因绑定问题引发 bug(如事件处理函数中 this
丢失),而函数组件完全不存在 this
上下文,所有逻辑均在函数作用域内处理,代码更简洁可靠。
2. 逻辑复用更高效:自定义 Hooks
类组件通过 Mixin 或 HOC 复用逻辑时,可能导致组件层级嵌套过深、命名冲突等问题(如 "嵌套地狱")。函数组件通过自定义 Hooks(如 useForm
、useFetch
)可轻松抽离逻辑,且逻辑复用更灵活,无组件包裹的层级负担。
示例:Hooks 复用表单逻辑
// 自定义 Hook
function useForm(initialState) {
const [form, setForm] = useState(initialState)
const handleChange = (e) => {
const { name, value } = e.target
setForm(prev => ({ ...prev, [name]: value }))
}
return { form, handleChange }
}
// 组件中使用
function UserForm() {
const { form, handleChange } = useForm({ name: '', email: '' })
return (
<form>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
</form>
)
}
3. 更细粒度的性能优化
- 类组件依赖
shouldComponentUpdate
或PureComponent
进行浅比较优化,需手动实现。 - 函数组件通过
React.memo
包裹组件,结合useMemo
、useCallback
缓存计算结果或回调函数,避免子组件不必要的重新渲染,优化更精准。
示例:使用 useMemo
缓存计算
function List({ items }) {
const expensiveValue = useMemo(() => {
// 复杂计算逻辑
return items.reduce((acc, cur) => acc + cur.value, 0)
}, [items]) // 仅当 items 变化时重新计算
return <div>计算结果:{expensiveValue}</div>
}
4. 简化生命周期管理
类组件的生命周期钩子(如 componentDidMount
、componentDidUpdate
)需分不同钩子处理逻辑,而函数组件通过 useEffect
统一处理副作用,并通过依赖数组灵活控制执行时机(挂载时、更新时、卸载时),逻辑更集中。
示例:替代类组件生命周期
类组件钩子 | 函数组件实现 |
---|---|
componentDidMount |
useEffect(effect, []) |
componentDidUpdate |
useEffect(effect, [deps...]) |
componentWillUnmount |
useEffect(() => () => cleanup) |
5. 更好的 TypeScript 支持
函数组件的 props 和状态类型声明更简洁,无需像类组件那样绑定 this
的类型(如 this.setState
的类型推断),结合 TypeScript 可提供更友好的类型提示。
6. 体积更小,性能更优
函数组件无需创建类实例,减少了内存开销;Hooks 的实现机制(如 useState
基于数组索引)比类组件的 setState
更高效,尤其在大型应用中性能优势更明显。
四、类组件的现状与迁移建议
-
现状:React 官方仍支持类组件,但不再推荐新功能开发使用,社区逐渐向函数组件迁移。
-
迁移场景:
- 复杂状态逻辑:可通过
useReducer
钩子替代类组件的this.state
和自定义更新逻辑; - 生命周期钩子:通过
useEffect
组合实现; - 代码迁移工具:使用
react-codemod
自动将类组件转换为函数组件(需手动处理this
相关逻辑)。
- 复杂状态逻辑:可通过
-
何时保留类组件:
- 维护旧项目中的类组件代码,无重构需求;
- 使用仅类组件支持的特性(如
getChildContext
,已废弃)。
解释 BFC(块级格式化上下文)的概念及应用场景
BFC(Block Formatting Context,块级格式化上下文)是 CSS 中一个独立的渲染区域,规定了内部元素如何布局,以及与外部元素的相互作用。它具有以下特性:内部盒子会在垂直方向排列,盒子垂直方向的间距由 margin 决定(同一 BFC 中相邻块级元素的垂直 margin 会重叠),BFC 的区域不会与浮动元素的盒子重叠,计算 BFC 高度时会包含浮动元素,BFC 是页面上的一个隔离容器,容器内部元素不会影响外部元素。
创建 BFC 的方式 包括:将元素的 display
设置为 table-cell
、table-caption
、inline-block
或 flex
;设置 overflow
为 auto
、scroll
或 hidden
(非 visible
);设置 float
为非 none
的值;设置 position
为 absolute
或 fixed
。
应用场景主要有以下几个方面:
- 解决 margin 重叠问题:当两个相邻块级元素处于同一 BFC 时,它们的垂直 margin 会重叠。通过为其中一个元素创建新的 BFC,可避免这种重叠。例如,两个段落元素上下排列,若都在同一个 BFC 中,它们的 margin 会合并,给其中一个段落包裹一个创建了 BFC 的容器,就能使 margin 正常显示。
- 清除浮动影响 :传统清除浮动需使用额外标签并设置
clear: both
,而利用 BFC 的特性,给父元素创建 BFC(如设置overflow: auto
),父元素就能包含浮动子元素,从而计算高度,解决高度塌陷问题。 - 避免与浮动元素重叠:当一个元素设置浮动后,相邻的块级元素会围绕它排列。若希望相邻元素不与浮动元素重叠,可将其放入新的 BFC 中,这样该元素的内容就不会进入浮动元素的区域。比如侧边栏浮动,主内容区域创建 BFC 后,主内容的文本就不会紧贴侧边栏,而是正常显示在右侧。
- 实现多列布局:利用 BFC 不与浮动元素重叠的特性,可实现简单的多列布局。例如,左侧元素浮动,右侧元素创建 BFC,两者可并列显示,互不影响。
图片性能优化的方法有哪些?(如压缩、WebP 格式、雪碧图、懒加载等)
图片性能优化是前端性能优化的重要环节,可从图片体积、加载方式、格式选择等多方面入手,以下是常见的优化方法:
1. 图片压缩
通过工具减少图片文件大小,同时尽量保持视觉质量。分为有损压缩 和无损压缩:
- 有损压缩:删除图像中冗余数据,会损失部分细节,适用于照片等对细节要求不高的场景。常用工具有 Photoshop、TinyPNG、Squoosh 等。例如,将一张 2MB 的 JPEG 照片通过 TinyPNG 压缩后,可能降至 500KB 左右,肉眼难以察觉画质明显下降。
- 无损压缩:通过算法优化文件编码,不损失像素数据,适合图标、线条图等对细节敏感的图片。工具如 ImageOptim、PNGGauntlet。
2. 选择合适的图片格式
不同格式适用于不同场景,合理选择可平衡画质与体积:
- JPEG(JPG):适合色彩丰富的照片,支持高压缩比,但不支持透明背景。
- PNG:支持透明背景,适合图标、图形,但文件体积通常大于 JPEG。其中 PNG-8 适合简单图形,PNG-24 适合复杂透明图像。
- WebP:谷歌开发的现代格式,同等画质下体积比 JPEG/PNG 小 30% 以上,支持有损和无损压缩、透明背景及动画。但需注意兼容性,可通过渐进式增强(先提供 JPEG/PNG,再检测支持 WebP 后替换)解决。
- AVIF:新一代图片格式,压缩效率优于 WebP,但目前浏览器支持度较低,可作为未来优化方向。
- SVG:矢量图,无限缩放不失真,体积小,适合图标、图表。可通过图标字体(如 Font Awesome)或直接嵌入 HTML 使用。
3. 雪碧图(CSS Sprites)
将多张小型图标合并为一张大图,通过 CSS 背景定位显示具体图标。减少 HTTP 请求次数,提升页面加载速度,尤其适合移动端项目。例如,将导航栏的多个图标合并为雪碧图,浏览器只需加载一次图片,通过 background-position
属性显示不同图标。但需注意,雪碧图维护成本较高,新增或修改图标需重新制作图片。
4. 懒加载(延迟加载)
非关键图片(如页面底部的图片)在用户滚动到可视区域时再加载,减少初始加载时的请求数量,加快首屏渲染。实现方式包括:
- 原生属性 :给图片标签添加
loading="lazy"
(浏览器支持有限)。 - JavaScript 监听滚动事件 :通过计算元素与视口的位置关系,判断是否加载图片。例如,使用
getBoundingClientRect()
方法获取元素位置,当进入视口附近时,将img
标签的data-src
属性值赋给src
。 - Intersection Observer API:更高效的方式,自动监听元素是否进入可视区域,触发回调函数加载图片,避免频繁滚动事件监听带来的性能开销。
5. 响应式图片(自适应图片)
根据设备屏幕尺寸和分辨率加载不同尺寸的图片,避免大尺寸图片在小屏幕设备上浪费流量。通过 srcset
和 sizes
属性实现,例如:
<img
src="small.jpg"
srcset="small.jpg 480w, medium.jpg 768w, large.jpg 1024w"
sizes="(max-width: 480px) 480px, (max-width: 768px) 768px, 1024px"
alt="响应式图片"
>
浏览器会根据当前视口宽度和设备像素比,选择最合适的图片加载。
6. 优化图片分辨率
避免使用分辨率远高于显示区域的图片。例如,在设计稿中某图片显示尺寸为 300px×200px,就无需上传 1200px×800px 的原图,可提前缩放至合适尺寸再上传。
7. 使用 CDN 加速
将图片存储在 CDN(内容分发网络)上,利用全球节点加速图片加载,减少服务器压力,尤其适合国际化项目。
8. 字体图标替代简单图形
对于纯色图标,使用字体图标(如 Iconfont)替代位图,字体文件体积小且可通过 CSS 灵活控制颜色、大小等样式,减少图片请求。
懒加载的实现方式有哪些?(如计算元素位置、IntersectionObserver API)
懒加载(Lazy Loading)是一种延迟加载非关键资源的技术,通过在需要时(如用户滚动到元素可视区域)再加载资源,减少初始加载时间,提升页面性能。以下是常见的实现方式及其原理和特点:
1. 通过滚动事件监听计算元素位置
原理 :利用 JavaScript 监听浏览器的 scroll
事件,在滚动过程中计算目标元素是否进入视口(viewport),若进入则触发加载操作。
关键步骤:
- 获取目标元素的位置:通过
getBoundingClientRect()
方法获取元素相对于视口的位置信息(包含top
、bottom
、left
、right
等属性)。 - 判断元素是否可见:设置一个阈值(如距离视口顶部或底部一定距离时提前加载),当元素的
top
值小于视口高度加上阈值,且bottom
值大于 0 时,认为元素进入可视区域。 - 替换图片地址:将
img
标签的data-src
(或自定义属性)值赋给src
,触发图片加载。
代码示例:
<img src="placeholder.jpg" data-src="real-image.jpg" class="lazy">
const lazyImages = document.querySelectorAll('img.lazy');
const threshold = 200; // 提前 200 像素加载
function loadLazyImage(img) {
const rect = img.getBoundingClientRect();
if (rect.bottom >= -threshold && rect.top <= window.innerHeight + threshold) {
if (img.src === 'placeholder.jpg') { // 避免重复加载
img.src = img.dataset.src;
img.classList.remove('lazy'); // 移除懒加载类,防止再次触发
}
}
}
function handleScroll() {
lazyImages.forEach(img => loadLazyImage(img));
}
// 监听滚动事件
window.addEventListener('scroll', handleScroll);
// 初始化时检查(处理页面初始可见的元素)
handleScroll();
优缺点:
- 优点:兼容性好,可支持低版本浏览器。
- 缺点 :频繁触发
scroll
事件可能导致性能问题(需通过debounce
优化);手动计算位置逻辑较繁琐,且可能因页面动态变化(如元素位置改变)导致判断不准确。
2. 使用 Intersection Observer API
原理 :Intersection Observer 是浏览器原生提供的 API,用于异步观察目标元素与祖先元素(或视口)的相交情况。通过创建观察者实例,监听元素的可见性变化,当元素进入或离开可视区域时触发回调函数,无需手动计算位置或监听滚动事件。
关键步骤:
- 创建观察者实例:通过
new IntersectionObserver(callback, options)
初始化,callback
为元素可见性变化时的回调函数,options
可配置阈值、根元素等。 - 注册目标元素:使用
observer.observe(element)
将目标元素加入观察列表。 - 在回调函数中处理加载逻辑:当元素可见性满足条件(如进入视口)时,加载资源并停止观察(避免重复触发)。
代码示例:
<img src="placeholder.jpg" data-src="real-image.jpg" class="lazy">
const lazyImages = document.querySelectorAll('img.lazy');
const observerOptions = {
rootMargin: '0px 0px 200px 0px', // 根元素(视口)外扩展 200px 作为触发区域
threshold: 0.1 // 元素可见区域占比超过 10% 时触发
};
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) { // 元素进入可视区域
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
observer.unobserve(img); // 停止观察,避免重复加载
}
});
}, observerOptions);
// 注册所有懒加载图片
lazyImages.forEach(img => imageObserver.observe(img));
优缺点:
- 优点 :性能更优,异步非阻塞,浏览器自动优化回调频率,无需手动处理滚动事件;配置灵活,可通过
rootMargin
和threshold
精准控制触发条件。 - 缺点:兼容性有限(需浏览器支持 Intersection Observer,可通过 polyfill 解决)。
3. 原生 loading="lazy"
属性
原理 :HTML5 新增的 loading
属性,为图片和 iframe 提供原生懒加载支持。将 loading
设置为 lazy
时,浏览器会在元素接近视口时再加载,减少初始请求。
使用方式:
<img src="image.jpg" loading="lazy" alt="懒加载图片" />
优缺点:
- 优点:代码极简,无需额外 JavaScript 逻辑。
- 缺点:兼容性较差(仅部分现代浏览器支持,如 Chrome 77+),且控制粒度较粗,无法自定义阈值或加载逻辑。
4. 框架 / 库的封装方案
在 Vue、React 等框架中,可使用现成的库或插件实现懒加载,简化开发流程:
-
Vue 中使用
vue-lazyload
:通过指令v-lazy
绑定图片地址,自动处理加载逻辑。预览
<img v-lazy="imageUrl" placeholder="placeholder.jpg" />
-
React 中使用
react-lazyload
:包裹图片组件,监听滚动事件并触发加载。import LazyLoad from 'react-lazyload'; <LazyLoad> <img src="placeholder.jpg" data-src="real-image.jpg" /> </LazyLoad>
选择建议
- 现代项目优先使用 Intersection Observer API:性能最佳,配置灵活,配合 polyfill 可兼容低版本浏览器。
- 兼容性要求高的项目 :使用滚动事件监听 + debounce 优化,或引入成熟的懒加载库(如
lozad.js
)。 - 简单场景 :若浏览器兼容性允许,可直接使用原生
loading="lazy"
属性。
懒加载的核心是按需加载,需根据项目需求和浏览器兼容性选择合适的方案,同时注意处理加载失败、占位图样式等细节,提升用户体验。
路由懒加载的实现方式(如使用 import () 动态导入)
路由懒加载(也称为代码拆分)是前端路由优化的重要手段,通过将路由对应的组件代码延迟加载,避免初始加载时一次性加载所有组件,减少打包后的文件体积,加快首屏渲染速度。以下是常见的实现方式及其原理和应用场景:
1. 使用 ES6 的 import () 动态导入(推荐)
原理 :import()
是 ES6 提出的动态导入语法,允许在运行时异步加载模块。在路由配置中,将组件的导入语句从静态 import
改为动态 import()
,打包工具(如 Webpack、Vite)会自动将对应的组件代码拆分为独立的 chunk 文件,仅在路由切换到该组件时加载。
示例(以 Vue Router 为例):
// 传统静态导入(全部组件一次性加载)
// import Home from './views/Home.vue';
// import About from './views/About.vue';
// 路由懒加载(动态导入)
const router = new VueRouter({
routes: [
{
path: '/',
name: 'Home',
component: () => import('./views/Home.vue') // 动态导入组件
},
{
path: '/about',
name: 'About',
component: () => import('./views/About.vue')
}
]
});
打包效果 :构建后会生成 Home.[hash].js
、About.[hash].js
等独立文件,首次访问页面时仅加载主 bundle 文件,切换到 /about
路由时,浏览器会自动请求 About.[hash].js
文件并渲染组件。
在 React 中使用(React Router):
import { Route, Routes } from 'react-router-dom';
function App() {
return (
<Routes>
<Route path="/" element={<LazyLoadComponent componentName="Home" />} />
<Route path="/about" element={<LazyLoadComponent componentName="About" />} />
</Routes>
);
}
// 封装懒加载组件
const LazyLoadComponent = ({ componentName }) => {
const [Component, setComponent] = useState(null);
useEffect(() => {
import(`./views/${componentName}.js`)
.then(module => setComponent(module.default));
}, [componentName]);
return Component ? <Component /> : <Spinner />; // 加载中显示 Loading 组件
};
2. 使用 Webpack 的 require.ensure(历史方案,逐步淘汰)
原理 :Webpack 提供的 require.ensure
方法可手动指定代码拆分点,在指定的回调函数中异步加载模块。该方法通过设置 webpackPrefetch
或 webpackPreload
注释可优化加载时机(如预获取资源)。
示例(Vue Router):
const router = new VueRouter({
routes: [
{
path: '/about',
name: 'About',
component: resolve => {
require.ensure([], () => {
resolve(require('./views/About.vue'));
}, 'about-chunk'); // 自定义 chunk 名称
}
}
]
});
注意 :require.ensure
是 Webpack 特有的语法,兼容性和可读性不如 import()
,现代项目中已逐步被动态导入取代。
3. 结合路由配置的异步组件(框架特性)
部分框架(如 Vue)支持将路由组件定义为异步组件,框架内部会处理动态导入逻辑:
// Vue 2 中使用异步组件
const router = new VueRouter({
routes: [
{
path: '/about',
component: () => ({
// 异步组件配置
component: import('./views/About.vue'),
loading: LoadingComponent, // 加载中显示的组件
error: ErrorComponent, // 加载失败显示的组件
delay: 200, // 延迟显示 loading 的时间
timeout: 3000 // 加载超时时间
})
}
]
});
4. 预加载(Prefetch)优化用户体验
为进一步提升用户体验,可结合 import()
的 webpackPrefetch
注释,告知浏览器在空闲时提前加载后续可能需要的路由组件代码:
const About = () => import(/* webpackPrefetch: true */ './views/About.vue');
原理:浏览器会在主线程空闲且网络空闲时,提前获取该 chunk 文件并缓存,当用户切换到该路由时可直接使用缓存,减少加载延迟。注意避免滥用预加载,以免浪费流量和资源。
实现路由懒加载的关键步骤
-
修改路由配置 :将静态导入的组件改为动态导入(
import()
或框架支持的异步组件写法)。 -
配置打包工具 :Webpack、Vite 等工具会自动处理代码拆分,无需额外配置。若需自定义 chunk 名称,可在动态导入语句中添加注释:
const About = () => import(/* webpackChunkName: "about-page" */ './views/About.vue');
-
处理加载状态:在组件加载过程中显示 Loading 提示,避免页面空白,提升用户体验。
-
优化网络请求:结合 CDN 部署拆分后的 chunk 文件,利用浏览器缓存和并行加载进一步提升性能。
适用场景
-
大型单页应用(SPA):当项目包含多个路由页面时,懒加载可显著减少初始加载时间。
-
非首屏关键路由:对于用户可能不会立即访问的页面(如 "关于我们"、"用户中心" 等),优先应用懒加载。
-
按需加载第三方库 :除路由组件外,对于 lodash、Chart.js 等大型库,也可使用动态导入按需加载:
const loadChart = async () => { const Chart = await import('chart.js'); // 使用 Chart 库创建图表 };
通过路由懒加载,可有效优化首屏加载性能,尤其在移动端或网络环境较差的场景中效果显著。实际开发中需结合项目规模、框架特性和用户行为分析,合理拆分代码,平衡加载速度与开发复杂度。
Webpack 能否将某路由下的所有子路由一起打包?如何配置?
Webpack 可以将某路由下的所有子路由组件打包到同一个 Chunk 中,这种优化方式称为 "按路由层级打包" 或 "分块打包",有助于减少 HTTP 请求次数,提升页面加载性能。以下是具体实现思路和配置方法:
核心原理
Webpack 通过动态导入语句(如 import()
)拆分代码时,默认会为每个动态导入的组件生成独立的 Chunk。若希望将同一父路由下的所有子路由组件合并到同一个 Chunk 中,需通过 命名 Chunk (Named Chunks)或 手动分组 的方式,告诉 Webpack 哪些模块应合并打包。
实现方式一:使用魔法注释命名 Chunk(推荐)
在动态导入语句中添加 webpackChunkName
注释,为同一父路由下的子路由指定相同的 Chunk 名称,Webpack 会将这些模块打包到同一个文件中。
示例场景:假设路由结构如下:
- 父路由 /dashboard
- 子路由 /dashboard/overview
- 子路由 /dashboard/settings
- 子路由 /dashboard/logs
配置步骤:
-
在路由配置中,为每个子路由的动态导入语句添加相同的 Chunk 名称:
// 父路由无关组件(单独打包) const Dashboard = () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue'); // 子路由组件,指定相同的Chunk名称"dashboard-child" const Overview = () => import(/* webpackChunkName: "dashboard-child" */ './views/Overview.vue'); const Settings = () => import(/* webpackChunkName: "dashboard-child" */ './views/Settings.vue'); const Logs = () => import(/* webpackChunkName: "dashboard-child" */ './views/Logs.vue');
-
Webpack 构建后,会生成
dashboard.[hash].js
(父组件)和dashboard-child.[hash].js
(所有子路由组件合并后的 Chunk)。 -
当访问
/dashboard
或其子路由时,浏览器会先加载dashboard.[hash].js
,再按需加载dashboard-child.[hash].js
(若子路由未被访问,则无需加载)。
关键注释说明:
webpackChunkName: "name"
:指定 Chunk 的名称,同名 Chunk 会被合并。- 名称中可使用
~
分隔符,实现层级分组(如dashboard~child
会生成dashboard-child.[hash].js
)。
实现方式二:通过 Webpack 配置手动分组(高级)
若项目未使用动态导入(如仍用 require.ensure
),或需更精细的控制,可通过 Webpack 的 optimization.splitChunks
配置手动分组。
示例配置(webpack.config.js):
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
// 定义一个名为"dashboard-child"的分组
dashboardChild: {
test: (module) => {
// 匹配所有属于/dashboard子路由的组件路径
return module.resource && module.resource.includes('/views/dashboard/');
},
name: 'dashboard-child',
chunks: 'all',
enforce: true
}
}
}
}
};
配置说明:
test
:通过正则或函数匹配需要合并的模块路径(如/views/dashboard/
目录下的组件)。name
:指定分组名称,Webpack 会将匹配的模块打包到名为dashboard-child.[hash].js
的 Chunk 中。chunks: 'all'
:表示从所有 Chunk 中提取符合条件的模块。enforce: true
:强制将匹配的模块拆分到该分组,即使未被其他模块引用。
实现方式三:利用目录结构自动分组(适用于约定式路由)
若项目采用约定式路由(如根据文件目录自动生成路由),可利用 Webpack 的 require.context
或动态导入结合目录匹配,自动将同一路由目录下的组件打包到同一 Chunk。
示例(Vue CLI + 约定式路由):
// 在路由入口文件中,动态导入/dashboard目录下的所有组件
const files = require.context('./views/dashboard', true, /\.vue$/);
files.keys().forEach(key => {
const componentName = key.split('/').pop().replace(/\.vue$/, '');
const routePath = `/${componentName.toLowerCase()}`;
routes.push({
path: routePath,
component: () => import(`./views/dashboard/${componentName}.vue`),
// 为同一目录下的组件指定相同的Chunk名称
meta: { chunkName: 'dashboard-child' }
});
});
配合 Webpack 魔法注释:
component: () => import(/* webpackChunkName: "dashboard-child" */ `./views/dashboard/${componentName}.vue`)
注意事项
- Chunk 粒度控制:合并子路由 Chunk 时需避免过度打包,若某子路由组件体积过大,合并后可能导致单个 Chunk 加载时间过长,建议按路由层级合理拆分(如父路由一个 Chunk,子路由按模块功能分组)。
- 缓存策略 :Chunk 名称中包含哈希值(如
dashboard-child.[hash].js
),修改子路由组件时仅会更新对应 Chunk 的哈希值,不影响其他 Chunk 的缓存。 - 兼容性:魔法注释需 Webpack 2+ 支持,若使用旧版本需升级或改用其他方式。
- 按需加载时机:合并后的 Chunk 仍为异步加载,仅在访问父路由或子路由时触发下载,不会影响首屏性能。
打包效果验证
构建完成后,可通过 Webpack 的 stats.json
或可视化工具(如 webpack-bundle-analyzer
)查看 Chunk 分组情况:
npm run build -- --stats
# 或安装分析工具
npm install webpack-bundle-analyzer --save-dev
在分析报告中搜索 dashboard-child
,确认所有子路由组件是否包含在同一个 Chunk 中。
通过以上方法,可灵活控制 Webpack 将某路由下的子组件打包到同一 Chunk,减少 HTTP 请求数量,提升路由切换时的加载速度。实际应用中需结合项目结构和路由设计,选择最适合的分组策略,平衡性能优化与代码可维护性。
为 objectType 对象编写一个 TypeScript 接口(需明确属性类型)
在 TypeScript 中,接口(Interface)用于定义对象的形状,明确属性的类型、可选性及函数类型等。为 objectType
对象编写接口时,需根据实际需求指定每个属性的类型,包括基本类型(如字符串、数字、布尔值)、复杂类型(如数组、对象)、函数类型或联合类型等。以下是不同场景的示例:
基础属性接口
若对象包含姓名(字符串)、年龄(数字)、是否激活(布尔值),接口可定义为:
interface ObjectType {
name: string; // 必选字符串属性
age: number; // 必选数字属性
isActive?: boolean; // 可选布尔值属性,问号表示可选
}
包含数组或对象的接口
若对象包含一个存储用户信息的数组(每个元素为对象)或嵌套对象,可进一步定义:
interface User { // 嵌套对象的接口
userId: string;
email: string;
}
interface ObjectType {
users: User[]; // 数组类型,元素为 User 接口类型
settings: { // 直接定义嵌套对象的结构
theme: 'light' | 'dark'; // 字面量联合类型,限定取值
fontSize: number;
};
}
包含函数的接口
若对象包含方法(如获取用户信息的函数),需定义函数类型:
interface ObjectType {
getUser: (id: number) => User; // 函数类型,参数为数字,返回值为 User 类型
logMessage: (msg: string) => void; // 返回值为 void 的函数
}
可选属性与只读属性
通过 ?
标记可选属性,通过 readonly
标记只读属性(初始化后不可修改):
interface ObjectType {
readonly id: string; // 只读字符串属性,创建后不可更改
data?: any[]; // 可选数组属性
}
索引签名(动态属性)
若对象包含不确定名称的属性,可通过索引签名定义动态类型:
interface ObjectType {
[key: string]: string | number; // 键为字符串,值为字符串或数字的动态属性
fixedProp: boolean; // 同时包含固定属性
}
接口继承
若需复用已有接口的属性,可通过 extends
关键字继承:
interface BaseType {
baseProp: string;
}
interface ObjectType extends BaseType { // 继承 BaseType 的属性
extendedProp: number;
}
注意事项
- 接口名称通常以大写字母开头,符合驼峰命名法。
- 未标记为可选的属性在创建对象时必须存在,否则会触发 TypeScript 类型检查错误。
- 函数类型需严格匹配参数类型和返回值类型,包括参数数量和类型顺序。
使用 TypeScript 定义一个函数接口或泛型类型
TypeScript 中定义函数接口或泛型类型可增强代码的类型安全性和复用性,适用于函数参数、返回值或复杂逻辑的类型约束。以下分场景说明:
一、函数接口(Function Interface)
函数接口用于定义函数的参数类型和返回值类型,可通过接口明确函数形状。
示例 1:普通函数接口
定义一个加法函数接口,要求接收两个数字参数,返回数字:
interface AddFunction {
(a: number, b: number): number; // 函数签名,参数和返回值类型
}
const add: AddFunction = (x, y) => x + y; // 正确,符合接口定义
// const add: AddFunction = (x: string, y: string) => x + y; // 错误,参数类型不匹配
示例 2:带可选参数的函数接口
若函数参数可选,需在参数名后加 ?
:
interface GreetFunction {
(name?: string, isFormal?: boolean): string; // 可选参数
}
const greet: GreetFunction = (name = 'Guest', isFormal = false) => {
return isFormal ? `Hello, ${name}` : `Hi, ${name}`;
};
示例 3:带默认参数的函数接口
接口中可定义参数默认值(需配合函数实现):
interface MultiplyFunction {
(a: number, b: number = 1): number; // b 有默认值 1
}
const multiply: MultiplyFunction = (x, y) => x * y;
multiply(2); // 有效,y 取默认值 1,返回 2
二、泛型类型(Generic Types)
泛型通过 <T>
占位符定义类型变量,使函数或类型可适应多种数据类型,避免重复定义类似逻辑。
示例 1:泛型函数
定义一个返回数组最后一个元素的函数,支持任意类型数组:
function getLastItem<T>(arr: T[]): T | undefined { // T 为数组元素类型
return arr[arr.length - 1];
}
const numbers = [1, 2, 3];
const lastNum = getLastItem(numbers); // 推断为 number 类型
const strings = ['a', 'b', 'c'];
const lastStr = getLastItem(strings); // 推断为 string 类型
示例 2:泛型接口
将泛型函数封装为接口,明确泛型参数的位置:
interface GetItemFunction {
<T>(arr: T[]): T | undefined; // 泛型参数在接口名称后声明
}
const getItem: GetItemFunction = (arr) => arr[arr.length - 1];
示例 3:泛型类
定义一个栈(Stack)类,支持存储任意类型数据:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const numberStack = new Stack<number>(); // 实例化为数字栈
numberStack.push(10);
const poppedNum = numberStack.pop(); // 类型为 number
const stringStack = new Stack<string>(); // 实例化为字符串栈
stringStack.push('hello');
const poppedStr = stringStack.pop(); // 类型为 string
示例 4:泛型约束(Generic Constraints)
限制泛型类型必须包含特定属性,例如要求类型具有 length
属性(如字符串、数组):
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): T { // T 必须继承 HasLength 接口
console.log('Length:', arg.length);
return arg;
}
logLength('abc'); // 有效,string 有 length 属性
logLength([1, 2, 3]); // 有效,array 有 length 属性
// logLength(123); // 错误,number 没有 length 属性
三、泛型与函数接口结合
定义一个处理泛型数据的函数接口,适用于多种类型操作:
interface Processor {
<T, U>(data: T, converter: (item: T) => U): U; // 双泛型参数
}
const process: Processor = (data, converter) => converter(data);
// 示例调用:将数字转为字符串
const result = process(42, (num) => num.toString()); // 类型推导为 string
关键总结
- 函数接口通过
(参数: 类型) => 返回值类型
定义函数签名,确保函数实现符合类型约束。 - 泛型通过
<T>
动态指定类型,提升代码复用性,避免类型 "硬编码"。 - 泛型约束(
extends
)可限制泛型的类型范围,结合接口实现更精准的类型检查。
Cookie 和 Token 的区别是什么?各自的使用场景有哪些?
Cookie 和 Token 是 Web 开发中用于状态管理和身份验证的重要机制,但设计理念、存储方式和应用场景差异显著。以下从技术特性和实际应用两方面对比分析:
一、核心区别
维度 | Cookie | Token |
---|---|---|
本质 | 存储于客户端的键值对数据(HTTP 协议标准) | 服务端生成的令牌(通常为 JWT 或自定义字符串) |
存储位置 | 浏览器内存(会话级)或磁盘(持久化) | 客户端(如 localStorage、sessionStorage、内存) |
数据格式 | 简单字符串(键值对,用分号分隔) | 结构化数据(如 JWT 包含头部、载荷、签名) |
自动传输 | 每次 HTTP 请求自动携带(受限域和路径) | 需手动在请求头中添加(如 Authorization 字段) |
安全性 | 较低(易受 XSS、CSRF 攻击,需配合 HttpOnly) | 较高(无状态,不依赖客户端存储特性) |
时效性 | 可设置过期时间(秒级至数年) | 通常短期有效(需定期刷新,防止令牌泄露) |
数据大小限制 | 单个 Cookie 不超过 4KB,每个域名限制约 20 个 | 无固定限制(建议控制在合理范围,避免影响性能) |
二、关键特性对比
-
Cookie 的局限性
- 自动传输的双刃剑:浏览器自动携带 Cookie 节省开发成本,但会增加请求头冗余(如静态资源请求也会携带 Cookie),影响性能。
- 安全风险 :
- 若未设置
HttpOnly
,易被 XSS 攻击窃取; - 需配合
SameSite
属性防范 CSRF 攻击(如设置为strict
或lax
)。
- 若未设置
- 存储限制:数据量小,不适合存储复杂信息。
-
Token 的优势
- 无状态设计:服务端无需存储会话状态,可横向扩展(如分布式架构),适合高并发场景。
- 灵活的验证方式:可通过签名(如 JWT 的 HMAC 算法)验证令牌合法性,无需查询数据库。
- 跨域友好:不依赖浏览器原生 Cookie 机制,可在 AJAX 请求中手动携带,适用于前后端分离项目。
三、使用场景
Cookie 的典型应用
-
会话管理(传统服务端渲染项目)
- 场景:用户登录后,服务端生成
sessionId
存储于 Cookie,后续请求通过sessionId
查找服务端会话数据。 - 示例:PHP 的
session_start()
、Java 的HttpSession
机制。
- 场景:用户登录后,服务端生成
-
个性化配置
- 存储用户偏好(如语言、主题),每次请求自动携带以返回定制化内容。
-
跨站点请求伪造(CSRF)防护
- 通过 Cookie 存储 CSRF Token,与表单提交的 Token 对比验证(需配合
SameSite
属性)。
- 通过 Cookie 存储 CSRF Token,与表单提交的 Token 对比验证(需配合
注意:Cookie 不适合存储敏感信息(如密码),且在移动端 SDK 中支持较差。
Token 的典型应用
-
前后端分离项目的身份验证
- 场景:用户登录后,服务端返回 JWT Token,前端存储于
localStorage
或内存,每次请求在Authorization
头中携带Bearer ${token}
。 - 优势:无状态,适合 RESTful API 和微服务架构。
- 场景:用户登录后,服务端返回 JWT Token,前端存储于
-
第三方服务鉴权(OAuth 2.0)
- 如微信、GitHub 登录,通过 Access Token 访问用户资源,避免直接暴露用户凭证。
-
临时权限发放
- 生成短期有效的 Token 用于文件下载、密码重置等场景,到期后自动失效。
-
跨域通信
- 在非同源请求中(如前端调用后端 API),手动传递 Token 避免浏览器同源策略限制。
四、实践建议
-
选择 Cookie 的情况:
- 需要兼容老旧浏览器(如 IE 6-10 对 localStorage 支持有限);
- 需利用浏览器自动发送机制(如简单的会话管理)。
-
选择 Token 的情况:
- 前后端分离架构或移动端开发;
- 需支持分布式部署或微服务;
- 对安全性要求较高(如避免 CSRF 风险)。
-
混合使用场景:
- 用 Cookie 存储 CSRF Token,用 Token 进行身份验证(如 Django、Ruby on Rails 的默认方案)。
什么是 JWT(JSON Web Token)?简述其工作原理和应用场景。
JWT(JSON Web Token)是一种基于 JSON 的开放标准(RFC 7519),用于在网络通信中安全地传输信息。其设计目标是实现无状态身份验证和数据交换,广泛应用于前后端分离、分布式系统和第三方登录等场景。
一、JWT 的结构
JWT 由三部分组成,通过 .
分隔,格式为:
header.payload.signature
-
Header(头部)
-
描述令牌的元数据,包含两部分:
typ
:令牌类型,固定为JWT
;alg
:签名算法,常用HS256
(HMAC-SHA256)或RS256
(RSA-SHA256)。
-
示例 :
json
{ "typ": "JWT", "alg": "HS256" }
-
头部会被 Base64Url 编码(非加密),生成第一部分字符串。
-
-
Payload(载荷)
-
存储实际数据(Claims,声明),分为三类:
- 注册声明 :预定义字段(如
iss
- 签发者,exp
- 过期时间,sub
- 主题); - 公共声明:自定义字段(如用户 ID、角色);
- 私有声明:用于双方约定的自定义数据(非标准,需避免冲突)。
- 注册声明 :预定义字段(如
-
示例 :
json
{ "sub": "123456", "name": "John Doe", "admin": true, "exp": 1689345600 // 过期时间(Unix 时间戳) }
-
载荷同样经过 Base64Url 编码,生成第二部分字符串。
-
-
Signature(签名)
-
用于验证令牌的完整性和合法性,生成方式:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret // 服务端私有的密钥(HS256 算法)或私钥(RS256 算法) )
-
签名结果经 Base64Url 编码后成为第三部分字符串。
-
二、工作原理
-
签发令牌(Login)
- 用户登录时,客户端向服务端发送凭证(如用户名 / 密码)。
- 服务端验证通过后,根据用户信息生成 JWT,包含有效期等声明,通过响应返回给客户端。
-
携带令牌(Request)
- 客户端收到 JWT 后,存储于
localStorage
、sessionStorage
或内存中。 - 后续每次请求(如访问 API)时,在请求头中添加
Authorization: Bearer <JWT>
。
- 客户端收到 JWT 后,存储于
-
验证令牌(Validation)
- 服务端接收请求,提取 JWT 并拆解为三部分。
- 用相同算法和密钥对头部和载荷重新计算签名,与令牌中的签名对比:
- 一致则验证通过,解析载荷获取用户信息;
- 不一致或过期则返回 401 未授权错误。
-
无状态特性
- 服务端无需存储会话数据,每次请求完全依赖 JWT 自身携带的信息,支持水平扩展和微服务架构。
三、核心优势
- 轻量级:数据格式为 JSON,体积小,传输效率高。
- 自包含:载荷携带用户权限等必要信息,避免频繁查询数据库。
- 跨语言兼容:基于标准协议,可在多种后端语言(如 Node.js、Python、Java)中统一实现。
- 安全可靠:通过签名防止数据篡改,配合 HTTPS 可避免令牌在传输中被窃取。
四、应用场景
-
身份验证(最核心场景)
- 前后端分离项目中,替代传统的 Cookie + Session 机制,实现无状态登录。
- 示例:用户登录后,前端携带 JWT 访问
/api/user
接口,服务端验证后返回用户信息。
-
单点登录(SSO)
- 多个子系统共享一个 JWT 签发中心,用户登录后可凭同一令牌访问所有子系统(需注意跨域问题)。
-
权限管理
-
在载荷中存储用户角色(如
admin
、user
),服务端根据角色控制接口访问权限。// 示例:验证用户是否有权限访问管理员接口
const token = request.headers.authorization.split(' ')[1];
const payload = verifyToken(token); // 解析并验证令牌
if (!payload.admin) {
throw new Error('无管理员权限');
}
-
-
数据传递(非敏感信息)
-
例如在 URL 中传递临时令牌(如密码重置链接),避免明文传输敏感数据。
https://example.com/reset-password?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
-
-
第三方服务集成
- 在 OAuth 2.0 中,作为 Access Token 访问用户资源(如微信开放平台获取用户头像)。
五、注意事项
- 令牌安全存储 :避免将 JWT 存储于 Cookie(易受 XSS 攻击),优先使用
HttpOnly
标记的 Cookie 或内存存储(如 Vuex、Redux)。 - 有效期控制:设置较短的过期时间(如 15 分钟),配合刷新令牌(Refresh Token)机制延长会话。
- 签名算法选择 :
HS256
:适用于单体应用(密钥仅服务端持有);RS256
:适用于分布式系统(公钥公开,私钥仅签发中心持有)。
- 防止 CSRF 攻击 :在使用 Cookie 存储 JWT 时,需结合
SameSite
属性和 CSRF Token 双重防护。
项目中 UI 设计的流程是怎样的?是否独立完成过 UI 设计?
在前端项目中,UI 设计流程通常与产品需求、交互逻辑紧密结合,需经历从需求分析到视觉落地的多阶段协作。以下是标准流程的详细说明,以及关于独立设计能力的阐述:
一、UI 设计核心流程
1. 需求分析与目标定义
- 明确业务目标:与产品经理、后端开发团队沟通,理解项目定位(如企业官网、电商平台、管理系统)和用户群体(如 C 端消费者、B 端企业用户)。
- 拆解功能模块:根据需求文档梳理页面结构(如首页、列表页、详情页)和交互场景(如表单提交、数据可视化)。
- 关键输出:功能清单、用户画像、页面流程图。
示例:若开发一款电商 APP,需优先考虑商品浏览、购物车、支付等核心流程的易用性,针对年轻用户群体设计轻量化视觉风格。
2. 交互设计(低保真原型)
- 绘制线框图 :使用 Figma、Axure 或 Sketch 工具创建低保真原型,聚焦页面布局、信息层级和操作逻辑。
- 确定导航栏位置(顶部或底部)、按钮交互状态(默认 / 点击 / 禁用)、列表项展示方式(图文混排或纯文本)。
- 交互逻辑说明:标注页面跳转规则(如点击按钮打开模态窗)、异常状态处理(如网络错误提示)。
- 关键输出:可交互原型图、交互说明文档。
示例:在管理系统中,数据表格需支持筛选、排序、分页功能,线框图需明确筛选条件的触发位置和展示形式。
3. 视觉设计(高保真设计)
- 建立设计系统 :
- 基础样式:定义品牌色(主色、辅助色)、字体规范(标题 / 正文的字号、字重、行高)、按钮尺寸(大 / 中 / 小);
- 组件库:设计通用组件(输入框、下拉菜单、模态框),确保交互一致性;
- 图标体系:选择或绘制符合产品调性的图标(如线性图标、填充图标)。
- 页面视觉落地 :
- 将线框图转化为高保真设计稿,注重色彩对比、留白比例、动效预览(如按钮点击反馈动画);
- 适配多端设备(如 PC 端 1920px 分辨率、移动端 iPhone 14 尺寸),考虑响应式布局规则。
- 关键输出:视觉设计稿(标注版)、组件库文件、设计规范文档。
示例:在官网首页设计中,主按钮采用品牌色 #2B6CB0,hover 状态透明度降低 10%,点击时添加轻微缩放动画,增强操作反馈。
4. 开发协作与交付
- 切图与资源输出 :
- 使用 Figma 的插件自动导出图片资源(PNG、SVG),确保视网膜屏适配(2x、3x 图);
- 标注元素尺寸、间距、字体样式(如标题 font-size: 24px,line-height: 32px)、阴影参数(box-shadow: 0 2px 4px rgba (0,0,0,0.1))。
- 前端对接 :
- 与开发团队沟通交互细节(如列表滚动加载的触发距离),解答样式实现疑问(如 CSS 弹性布局的兼容性处理);
- 跟踪开发进度,进行视觉走查,确保页面还原度(如按钮圆角半径、输入框边框颜色是否与设计稿一致)。
- 关键输出:切图压缩包、标注链接(如 Zeplin 或蓝湖)、动效开发说明(如使用 CSS3 animation 或 Lottie)。
5. 测试与迭代优化
- 可用性测试:邀请真实用户体验产品,收集反馈(如按钮位置不便于单手操作、配色导致视觉疲劳)。
- 数据驱动优化:分析页面热力图、用户点击流,调整高流量页面的布局(如将核心功能按钮上移至首屏)。
- 版本迭代:根据业务需求更新设计(如节日活动主题换肤),维护组件库的扩展性(如新增加载中状态的骨架屏)。
二、独立完成 UI 设计的能力
是否具备独立设计经验?
-
前端开发视角的设计能力 :
多数前端开发者具备基础视觉感知能力,可使用 Figma 完成简单页面布局(如官网单页、表单页面),但复杂交互(如数据大屏可视化、3D 动效)需依赖专业 UI/UX 设计师。
- 优势:熟悉前端实现逻辑,设计时会考虑技术可行性(如避免使用 CSS 不支持的滤镜效果);
- 局限:缺乏用户体验理论支撑(如尼尔森可用性原则)、色彩搭配专业性(如未系统学习色轮理论)。
-
协作场景下的角色定位 :
在中小型团队中,前端开发者可能承担部分轻量化设计任务(如调整现有组件样式、适配移动端界面),但大型项目仍需专业设计师主导全流程。
- 示例:独立开发个人博客时,可自主设计页面结构和配色;参与企业级项目时,需基于现有设计系统开发组件,不涉及从零构建视觉体系。
在项目中使用过哪些 Vue 生命周期钩子?在 created 钩子中如何访问 DOM 元素?
Vue 的生命周期钩子是组件实例从创建到销毁的各个阶段中自动执行的函数,贯穿组件的整个生命周期。项目中常用的钩子函数涵盖组件创建阶段 、挂载阶段 、更新阶段 和卸载阶段,不同阶段适用于不同的业务场景。
常用的 Vue 生命周期钩子
-
beforeCreate
在组件实例初始化之后、数据观测(data observer)和事件配置之前调用。此时组件的
data
和methods
尚未初始化,无法访问组件状态,通常用于插件初始化或执行与组件状态无关的操作。 -
created
组件实例创建完成后调用,此时
data
和methods
已初始化,可以访问组件的响应式数据和方法,但组件尚未挂载到 DOM 上(即$el
不存在)。这一阶段常用于数据获取 (如调用 API 加载初始数据)、事件监听 或非 DOM 依赖的逻辑预处理。 -
beforeMount
在组件即将挂载到 DOM 前调用,此时
$el
已生成(通过template
或render
函数编译),但尚未插入真实 DOM。可用于在渲染前对$el
进行最后的修改,或在服务端渲染(SSR)场景中做特殊处理。 -
mounted
组件挂载到真实 DOM 后调用,此时可以通过
$refs
或原生 DOM API 访问 DOM 元素,适合执行依赖 DOM 的操作 (如初始化第三方库、设置滚动监听、获取元素尺寸等)。需要注意的是,若组件存在异步更新或嵌套子组件,mounted
会在所有子组件挂载完成后触发,因此若需操作子组件的 DOM,可能需要结合$nextTick
。 -
beforeUpdate
组件数据更新之前调用,此时数据已发生变化,但 DOM 尚未更新。可用于在更新前获取现有 DOM 状态,或执行一些与状态变更相关的预处理逻辑。
-
updated
组件数据更新且 DOM 重新渲染完成后调用,此时可以访问更新后的 DOM。需注意避免在此钩子中修改状态,否则会触发新一轮的更新循环。常见场景包括基于新 DOM 结构的重新计算或动画触发。
-
beforeUnmount
组件即将卸载前调用,用于清理副作用(如移除事件监听、取消定时器、销毁第三方实例等),避免内存泄漏。
-
unmounted
组件卸载完成后调用,此时组件实例已被销毁,所有子组件也已卸载,通常无需在此阶段执行操作,但可用于执行最终的清理工作。
此外,Vue 还提供了错误处理钩子 (如 errorCaptured
)和服务端渲染钩子 (如 ssrRendered
),适用于特定场景。
在 created 钩子中如何访问 DOM 元素?
created 钩子中无法直接访问 DOM 元素 ,因为此时组件尚未挂载到 DOM 上,$el
属性尚未生成。若强行通过 document.querySelector
等方法直接操作 DOM,可能会因 DOM 未渲染而导致错误或无效操作。
若业务需求需要在数据初始化后立即操作 DOM,可通过以下方式实现:
-
使用 mounted 钩子
将 DOM 操作逻辑转移到
mounted
钩子中,此时组件已挂载,$refs
和 DOM 元素均已可用。<template> <div ref="targetDiv">示例文本</div> </template> <script> export default { data() { return { /* 数据 */ }; }, created() { // 此处无法通过 $refs 访问 DOM // 若需预处理数据,可在此处执行 }, mounted() { const div = this.$refs.targetDiv; // 正确方式:在 mounted 中访问 console.log(div.textContent); } }; </script>
-
结合 Vue 的 若需在数据更新后立即获取更新后的(如在中触发了数据变更),可通过nextTick` 延迟到下一个 DOM 更新周期执行。
<script> export default { created() { this.fetchData(); // 假设 fetchData 会修改响应式数据 }, methods: { async fetchData() { const data = await api.getData(); this.data = data; // 修改数据后,DOM 尚未更新 // 使用 $nextTick 确保 DOM 已更新 this.$nextTick(() => { const div = document.querySelector('.target'); // 在此处执行 DOM 操作 }); } } }; </script>
$nextTick
的原理是将回调函数延迟到 Vue 的异步更新队列之后执行,确保此时 DOM 已完成渲染。 -
避免在 created 中依赖 DOM
组件的生命周期设计决定了
created
阶段应专注于数据逻辑和非 DOM 操作。若业务逻辑强依赖 DOM,需调整架构,例如将逻辑封装到自定义指令、组合式函数(Vue 3 的 Composition API)或单独的 DOM 操作服务中,通过依赖注入的方式在合适的生命周期阶段调用。