进阶篇题目列表
一、JS基础
1 类型及检测方式
JavaScript 是一种动态弱类型语言,共有七种基本数据类型和一种引用类型。理解类型及其检测方式对于编写健壮的程序至关重要。
面试回答:请解释 JavaScript 的数据类型及检测方法
JavaScript 中的数据类型分为两大类:原始类型(Primitive Types)和对象类型(Object Type)。
一、核心类型分类
| 类型 | 示例 | 特性 |
|---|---|---|
String | "hello" | 字符串类型,不可变 |
Number | 42, 3.14 | 双精度浮点数,无整型独立类型 |
Boolean | true, false | 布尔值 |
Null | null | 表示“空值”,typeof 返回 "object"(历史 bug) |
Undefined | undefined | 未赋值变量或属性 |
Symbol (ES6) | Symbol('key') | 唯一且不可变的值,常用于对象键 |
BigInt (ES2020) | 123n | 任意精度整数 |
Object | {}, [], function(){} | 引用类型,包括普通对象、数组、函数等 |
二、类型检测方法详解
| 检测方式 | 语法 | 返回值 | 优点 | 缺点 | 典型用途 |
|---|---|---|---|---|---|
typeof | typeof value | 字符串("string", "number", "boolean", "undefined", "symbol", "bigint", "object", "function") | 快速判断基础类型 | 无法区分 null 和对象;对数组返回 "object" | 判断基本类型 |
instanceof | obj instanceof Constructor | Boolean | 可检测自定义构造函数实例 | 仅适用于对象;跨 iframe 会失效 | 检查对象是否由某构造器创建 |
Object.prototype.toString.call() | Object.prototype.toString.call(value) | [object Type] 形式字符串 | 可准确识别所有内置类型 | 写法略繁琐 | 精确类型判断 |
constructor | value.constructor === Type | 构造器引用 | 直观 | 易被重写;null/undefined 报错 | 辅助判断 |
Array.isArray() | Array.isArray(arr) | Boolean | 专用于数组判断,可靠 | 仅限数组 | 判断数组 |
三、实际应用场景
- 在表单验证中判断输入类型,防止非法数据进入系统。
- 工具库(如 Lodash)中实现
isString,isArray等辅助函数时,需精确判断类型。 - 序列化/反序列化逻辑中根据类型采取不同处理策略。
四、常见误区
- 认为
typeof null === "null"—— 实际是"object",这是 JavaScript 的历史遗留 bug。 - 使用
== null来判断null或undefined虽然可行,但推荐显式使用=== null || === undefined提高可读性。 - 在跨域环境中使用
instanceof导致失败,因为不同全局环境下的构造函数不是同一个引用。
五、最佳实践
- 检测基础类型优先使用
typeof。 - 检测
null使用value === null。 - 检测数组必须使用
Array.isArray()。 - 高精度类型判断统一使用
Object.prototype.toString.call()。
// 示例代码
console.log(typeof null); // "object"
console.log(Array.isArray([])); // true
console.log(Object.prototype.toString.call(new Date())); // "[object Date]"
2 This
this 是 JavaScript 中最常被误解的关键字之一,其指向在运行时决定,取决于函数的调用方式。
面试回答:请解释 JavaScript 中 this 的绑定规则
this 的绑定规则遵循以下四个优先级从高到低的原则:
一、new 绑定 当使用 new 操作符调用函数时,this 指向新创建的对象。
function Person(name) {
this.name = name;
}
const p = new Person('Alice');
console.log(p.name); // Alice
二、显式绑定 通过 call, apply, bind 方法强制指定 this 值。
function greet() {
console.log(`Hello, ${this.name}`);
}
greet.call({ name: 'Bob' }); // Hello, Bob
三、隐式绑定 当函数作为对象的方法被调用时,this 指向该对象。
const obj = {
name: 'Charlie',
greet() {
console.log(this.name);
}
};
obj.greet(); // Charlie
四、默认绑定 独立函数调用时,非严格模式下 this 指向全局对象(浏览器中为 window),严格模式下为 undefined。
function fn() { console.log(this); }
fn(); // 非严格模式:window,严格模式:undefined
箭头函数的特殊性 箭头函数没有自己的 this,它会捕获其外层作用域的 this 值,并且不能通过 call/apply/bind 修改。
const obj = {
name: 'Outer',
regularFn() {
setTimeout(() => {
console.log(this.name); // Outer,继承外层 this
}, 100);
}
};
obj.regularFn();
实际应用
- React 类组件中需手动绑定事件处理器的
this。 - 防止回调函数中
this丢失(使用箭头函数或 bind)。 - 创建私有作用域避免污染全局。
常见误区
- 认为函数定义位置决定
this—— 实际是调用方式决定。 - 忽视箭头函数的词法
this特性。 - 在
setTimeout或事件监听中忘记绑定this。
最佳实践
- 使用箭头函数避免
this丢失问题。 - 在类中使用属性初始化语法绑定方法:
onClick = () => {}。 - 显式使用
bind确保上下文正确。 - 使用 ESLint 规则检测潜在的
this错误。
3 apply/call/bind 原理
这三个方法用于改变函数执行时的 this 指向,是实现函数式编程和面向对象编程灵活交互的重要工具。
面试回答:请解释 apply、call、bind 的区别与实现原理
一、方法定义与区别
| 方法 | 语法 | 是否立即执行 | 参数传递方式 | 主要用途 |
|---|---|---|---|---|
call | fn.call(thisArg, arg1, arg2, ...) | 是 | 逐个传参 | 临时改变上下文并调用 |
apply | fn.apply(thisArg, [argsArray]) | 是 | 数组形式 | 数组参数调用(如 Math.max) |
bind | fn.bind(thisArg, arg1, arg2, ...) | 否,返回新函数 | 支持柯里化 | 创建具有固定上下文的函数副本 |
二、手写实现(Polyfill)
// call 实现
Function.prototype.myCall = function(context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('not a function');
}
context = context || globalThis;
const key = Symbol('temp');
context[key] = this;
const result = context[key](...args);
delete context[key];
return result;
};
// apply 实现
Function.prototype.myApply = function(context, args = []) {
if (typeof this !== 'function') {
throw new TypeError('not a function');
}
context = context || globalThis;
const key = Symbol('temp');
context[key] = this;
const result = context[key](...args);
delete context[key];
return result;
};
// bind 实现(支持 new 调用)
Function.prototype.myBind = function(context, ...boundArgs) {
if (typeof this !== 'function') {
throw new TypeError('not a function');
}
const fn = this;
const boundFn = function(...args) {
if (new.target) {
return new fn(...boundArgs, ...args);
}
return fn.apply(context, [...boundArgs, ...args]);
};
boundFn.prototype = Object.create(fn.prototype);
return boundFn;
};
三、实际应用场景
- 数组方法借用:
Array.prototype.slice.call(arguments)将类数组转为数组。 - 函数柯里化:
const add5 = add.bind(null, 5)固定部分参数。 - 事件处理器绑定:
button.addEventListener('click', handler.bind(obj))。 - 实现继承:
Parent.call(this, ...args)在子类构造函数中调用父类。
四、性能考虑
bind会创建新函数,频繁调用可能影响内存。call/apply比直接调用稍慢,但在现代引擎中优化良好。- 箭头函数在某些场景下可替代
bind。
五、常见误区
- 认为
bind会立即执行函数 —— 它只是返回一个新函数。 - 忽视
bind后函数的prototype仍指向原函数。 - 在循环中滥用
bind导致性能问题。
六、最佳实践
- 优先使用箭头函数解决
this问题。 - 将
bind提升到模块顶层或构造函数中执行一次。 - 使用
apply处理大量参数的函数调用。 - 注意
bind与new的兼容性处理。
4 变量提升
变量提升(Hoisting)是 JavaScript 解释器在编译阶段将变量和函数声明移动到其作用域顶部的行为。
面试回答:请解释 JavaScript 中的变量提升机制
一、提升机制详解
var 声明:
- 声明会被提升到作用域顶部。
- 初始化(赋值)保留在原位置。
- 提升后值为
undefined。
function 声明:
- 整个函数声明(包括函数体)被提升。
- 可在声明前调用。
let/const 声明:
- 存在暂时性死区(Temporal Dead Zone, TDZ)。
- 声明会被提升,但不可访问直到执行到声明语句。
- 在声明前访问会抛出
ReferenceError。
class 声明:
- 类似
let/const,存在 TDZ。 - 必须先声明后使用。
- 类似
二、示例对比
// var 提升
console.log(a); // undefined
var a = 1;
// function 提升
foo(); // "Hello"
function foo() {
console.log("Hello");
}
// let/const TDZ
console.log(b); // ReferenceError!
let b = 2;
// class TDZ
const instance = new MyClass(); // ReferenceError!
class MyClass {}
三、提升顺序(同一作用域内)
- 函数声明(完整提升)
- 变量声明(仅声明提升)
> 若同名,函数声明优先于变量声明。
console.log(foo); // [Function: foo]
var foo = 1;
function foo() {}
console.log(foo); // 1
四、实际影响与陷阱
- 容易导致意外的
undefined值。 - 函数表达式不会被完整提升:
bar(); // TypeError: not a function var bar = function() {}; - 模块化开发中减少全局变量可降低提升带来的复杂性。
五、最佳实践
- 使用
let/const替代var,避免 TDZ 问题。 - 声明前置:将变量和函数声明放在作用域顶部。
- 启用 ESLint 规则禁止在声明前使用变量。
- 理解提升机制有助于阅读旧代码。
5 执行上下文
执行上下文(Execution Context)是 JavaScript 代码执行的环境抽象,决定了变量查找、this 绑定等行为。
面试回答:请解释 JavaScript 的执行上下文及其生命周期
一、执行上下文的类型
| 类型 | 触发条件 | 示例 |
|---|---|---|
| 全局执行上下文 | 页面加载时创建 | 浏览器中的 window |
| 函数执行上下文 | 函数被调用时创建 | function foo(){} 调用时 |
| Eval 执行上下文 | eval() 函数执行时 | 已不推荐使用 |
二、执行上下文的生命周期
创建阶段(Creation Phase)
- 创建词法环境(Lexical Environment)
- 创建变量环境(Variable Environment)
- 设置
this绑定 - (函数上下文)创建
arguments对象
执行阶段(Execution Phase)
- 逐行执行代码
- 变量赋值、函数调用等
销毁阶段(Cleanup Phase)
- 上下文出栈
- 内存回收(受垃圾回收机制影响)
三、内部组件结构
ExecutionContext = {
LexicalEnvironment: { /* 用于存储 let/const 声明 */ },
VariableEnvironment: { /* 用于存储 var 声明 */ },
ThisBinding: /* this 的值 */
}
四、词法环境(Lexical Environment)结构
- 环境记录(Environment Record):存储变量和函数声明
- 外部环境引用(Outer Environment Reference):指向外层作用域,形成作用域链
五、示例分析
let x = 1;
function outer() {
let y = 2;
function inner() {
let z = 3;
console.log(x + y + z);
}
inner();
}
outer();
/*
调用 inner 时的执行上下文:
LexicalEnvironment: { z: 3 }
Outer: 指向 outer 的 LexicalEnvironment
VariableEnvironment: 同 LexicalEnvironment(此处无 var)
ThisBinding: 取决于调用方式
*/
六、与作用域链的关系 执行上下文的 Outer Environment Reference 构成了作用域链,用于变量查找。
七、实际意义
- 理解闭包的实现原理。
- 分析变量查找过程。
- 调试作用域相关问题。
八、最佳实践
- 避免过多嵌套函数导致作用域链过长。
- 使用
const/let减少变量环境与词法环境的分离。 - 理解执行上下文栈(调用栈)有助于调试异步代码。
6 作用域
作用域(Scope)决定了变量和函数的可访问性,是控制代码可见性的核心机制。
面试回答:请解释 JavaScript 的作用域类型及作用域链
一、作用域类型
| 类型 | 特征 | 示例 |
|---|---|---|
| 全局作用域 | 变量在任何地方都可访问 | var globalVar = 1; |
| 函数作用域 | 变量在函数内部可访问 | function fn(){ var funcVar = 2; } |
| 块级作用域(ES6) | 变量在 {} 内部可访问 | if(true){ let blockVar = 3; } |
二、作用域链(Scope Chain) 当查找变量时,JavaScript 引擎会从当前作用域开始,逐层向外查找,直到全局作用域。
let global = 'global';
function outer() {
let outerVar = 'outer';
function inner() {
let innerVar = 'inner';
console.log(global); // ✅ 找到
console.log(outerVar); // ✅ 找到
console.log(innerVar); // ✅ 找到
}
inner();
}
outer();
三、词法作用域 vs 动态作用域
- 词法作用域(静态作用域):作用域在代码书写时确定,JavaScript 采用此模型。
- 动态作用域:作用域在运行时确定,如 Bash 脚本。
let x = 'global';
function foo() {
console.log(x); // 词法作用域:'global'
}
function bar() {
let x = 'local';
foo(); // 即使在 bar 中调用,foo 仍访问全局 x
}
bar(); // 输出 'global'
四、块级作用域的意义
let/const提供真正的块级作用域。- 避免
var的变量提升和函数内全域问题。 - 支持
for循环中正确的闭包行为。
// var 的问题
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出 3, 3, 3
}
// let 的解决方案
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100); // 输出 0, 1, 2
}
五、实际应用
- 模块模式实现私有变量。
- 避免命名冲突。
- 控制变量生命周期。
六、常见误区
- 认为
if/for块能限制var的作用域。 - 忽视 IIFE(立即执行函数)曾是实现私有作用域的主要手段。
- 混淆作用域与执行上下文的概念。
七、最佳实践
- 优先使用
let/const创建块级作用域。 - 将变量声明靠近使用位置。
- 使用 IIFE 或模块封装私有逻辑。
- 理解作用域链对性能的影响(深层查找较慢)。
7 闭包
闭包(Closure)是指函数能够访问其词法作用域之外的变量,即使在其外部函数已经执行完毕之后。
面试回答:什么是闭包?它的应用场景和注意事项是什么?
一、核心定义 当一个内部函数引用了其外部函数的变量,并且该内部函数在其外部函数的作用域之外被调用时,就形成了闭包。
二、形成条件
- 存在嵌套函数。
- 内部函数引用外部函数的变量。
- 内部函数在其外部函数执行结束后仍可被访问。
三、基础示例
function outer() {
let count = 0;
return function inner() {
count++;
console.log(count);
};
}
const counter = outer();
counter(); // 1
counter(); // 2
// count 变量被保留在内存中,未被垃圾回收
四、实现原理
- 内部函数的 Environment 内部槽保存了对外部词法环境的引用。
- 即使外部函数调用结束,其变量环境仍被内部函数引用,因此不会被回收。
- 这构成了一个持久的作用域链。
五、实际应用场景
- 模块模式:创建私有变量和方法
const Counter = (function() {
let privateCount = 0;
return {
increment() { privateCount++; },
getCount() { return privateCount; }
};
})();
- 函数柯里化
function add(a) {
return function(b) {
return a + b;
};
}
const add5 = add(5);
add5(3); // 8
- 事件处理器与回调
for (let i = 0; i < buttons.length; i++) {
buttons[i].onclick = (function(index) {
return function() {
console.log('Button', index, 'clicked');
};
})(i);
}
- 防抖与节流(后续详述)
六、内存管理注意事项
- 闭包会阻止外部变量被回收,可能导致内存泄漏。
- 应及时解除不必要的引用。
- 使用 Chrome DevTools 分析内存快照。
七、常见误区
- 认为只有返回函数才算闭包 —— 实际上任何能访问外部变量的函数都是闭包。
- 忽视循环中的闭包问题(ES5 中常见)。
- 认为闭包一定会导致内存泄漏 —— 合理使用是安全的。
八、最佳实践
- 利用闭包实现数据封装和信息隐藏。
- 避免在大型数据结构上创建长期存在的闭包。
- 使用 WeakMap/WeakSet 存储关联数据以避免强引用。
- 理解现代 JS 引擎对闭包的优化。
8 New的原理
new 操作符用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
面试回答:请解释 new 操作符的执行过程及其原理
一、new 操作的四个步骤
创建一个全新的对象
- 创建一个空的普通对象
{}。
- 创建一个空的普通对象
设置原型链接
- 将这个新对象的
Prototype(即__proto__)链接到构造函数的prototype属性。
- 将这个新对象的
绑定 this 并执行构造函数
- 将构造函数的
this绑定到这个新对象上,并执行构造函数体。
- 将构造函数的
返回结果
- 如果构造函数没有显式返回一个对象,则返回这个新对象;
- 否则返回该对象。
二、手动实现 new 操作
function myNew(Constructor, ...args) {
// 1. 创建新对象
const obj = Object.create(Constructor.prototype);
// 2. 绑定 this 并执行构造函数
const result = Constructor.apply(obj, args);
// 3. 返回结果
if (result && (typeof result === 'object' || typeof result === 'function')) {
return result;
}
return obj;
}
// 使用示例
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
};
const p = myNew(Person, 'Alice');
p.sayName(); // Alice
三、与普通函数调用的区别
| 方面 | new 调用 | 普通调用 |
|---|---|---|
this 绑定 | 新创建的对象 | 调用者或全局对象 |
| 返回值 | 默认返回新对象 | 返回函数显式返回值 |
| 原型链 | 设置 __proto__ 指向 Constructor.prototype | 无特殊设置 |
| 构造函数性质 | 期望作为构造器使用 | 一般函数 |
四、特殊情况处理
- 构造函数显式返回原始值:忽略返回值,仍返回新对象。
- 构造函数显式返回对象:返回该对象,新对象被丢弃。
- 箭头函数不能作为构造函数(无
prototype属性)。
function Test() {
this.value = 1;
return { custom: true }; // 返回对象
}
const t = new Test();
console.log(t); // { custom: true }
function Test2() {
this.value = 1;
return 'string'; // 返回原始值
}
const t2 = new Test2();
console.log(t2); // Test2 { value: 1 }
五、实际应用
- 创建自定义对象类型。
- 实现继承链。
- 设计模式中的工厂模式配合使用。
六、常见误区
- 认为
new只是调用构造函数 —— 忽略了原型链设置。 - 忽视构造函数返回对象时的行为。
- 在 ES6 class 中误解
new的作用。
七、最佳实践
- 构造函数首字母大写以示区分。
- 使用
instanceof检查实例类型。 - 在构造函数中验证
new.target确保正确调用。 - 优先使用
class语法糖(更清晰)。
9 原型/原型链
原型(Prototype)和原型链(Prototype Chain)是 JavaScript 实现继承和属性查找的核心机制。
面试回答:请解释 JavaScript 的原型和原型链机制
一、核心概念
- 原型(Prototype):每个函数都有一个
prototype属性,它是一个对象,包含可以被实例共享的属性和方法。 - 原型链:当访问一个对象的属性时,如果该对象自身没有这个属性,就会沿着
__proto__链向上查找,直到找到该属性或到达null。
二、关键属性与关系
| 属性 | 说明 |
|---|---|
prototype | 函数特有的属性,指向原型对象 |
__proto__ | 对象特有的属性(现已标准化为 Object.getPrototypeOf()),指向其构造函数的 prototype |
constructor | 原型对象上的属性,默认指向构造函数 |
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
};
const p = new Person('Alice');
// 关系验证
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
三、原型链查找过程
p.sayName();
// 1. 查找 p 自身是否有 sayName -> 无
// 2. 查找 p.__proto__ (即 Person.prototype) -> 有,执行
四、instanceof 原理instanceof 通过检查构造函数的 prototype 是否出现在对象的原型链上来判断类型。
p instanceof Person;
// 检查 Person.prototype 是否在 p 的原型链上
五、ES6 Class 的等价表示
class Person {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
// 等价于上面的构造函数+原型方式
六、实际应用
- 方法共享:避免每个实例都复制方法。
- 继承实现的基础。
- 动态扩展对象能力(如给 Array.prototype 添加方法)。
七、常见误区
- 混淆
prototype和__proto__。 - 认为所有对象都有
prototype—— 只有函数有。 - 修改原型会影响所有实例(包括未来的实例)。
- 原型链过长影响性能。
八、最佳实践
- 使用
Object.create(null)创建无原型链的对象(如 Map 模拟)。 - 避免修改内置对象原型(可能导致冲突)。
- 使用
hasOwnProperty区分自有属性和继承属性。 - 理解现代继承模式(如
class和extends)。
10 继承
继承是面向对象编程的重要特性,允许子类复用父类的属性和方法。JavaScript 通过原型链实现继承。
面试回答:请列举 JavaScript 中的继承方式并说明其优缺点
一、主要继承方式
| 序号 | 方式 | 实现要点 | 优点 | 缺点 | 是否推荐 |
|---|---|---|---|---|---|
| 1 | 原型链继承 | Child.prototype = new Parent() | 简单直观 | 父类引用类型被共享;无法向父类传参 | ❌ |
| 2 | 构造函数继承 | Parent.call(this) | 可传参;隔离引用类型 | 方法不能复用 | ❌ |
| 3 | 组合继承 | 原型链 + 构造函数 | 兼顾两者优点 | 父构造函数调用两次 | ⚠️ |
| 4 | 寄生组合继承 | 组合 + Object.create | 最佳方案,ES6前推荐 | 实现稍复杂 | ✅ |
| 5 | ES6 Class 继承 | class Child extends Parent | 语法清晰,官方标准 | 需环境支持 | ✅✅ |
二、各方式代码实现
1. 原型链继承
function Parent() {
this.colors = ['red'];
}
Parent.prototype.getName = function() { return 'parent'; };
function Child() {}
Child.prototype = new Parent(); // 核心
const c1 = new Child();
c1.colors.push('blue');
const c2 = new Child();
console.log(c2.colors); // ['red', 'blue'] —— 引用类型共享问题
2. 构造函数继承
function Child(name) {
Parent.call(this); // 核心
this.name = name;
}
// 无法继承 Parent.prototype 上的方法
3. 组合继承
function Child(name) {
Parent.call(this); // 第一次调用 Parent
this.name = name;
}
Child.prototype = new Parent(); // 第二次调用 Parent
Child.prototype.constructor = Child;
// 存在两次调用问题
4. 寄生组合继承(最优)
function inherit(Child, Parent) {
// 创建父类原型的副本,避免第二次调用构造函数
const prototype = Object.create(Parent.prototype);
prototype.constructor = Child;
Child.prototype = prototype;
}
function Child(name) {
Parent.call(this); // 只在此处调用
this.name = name;
}
inherit(Child, Parent);
5. ES6 Class 继承
class Parent {
constructor(name) {
this.name = name;
}
getName() { return this.name; }
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
getAge() { return this.age; }
}
三、super 关键字
- 在构造函数中调用
super()初始化父类。 - 在方法中调用
super.method()访问父类方法。
四、实际应用
- UI 组件库中的基础组件扩展。
- 工具类的功能增强。
- 框架中的插件系统。
五、常见误区
- 认为
extends只是语法糖 —— 背后仍是原型链。 - 忽视
super()必须在this之前调用。 - 在静态方法中错误使用
super。
六、最佳实践
- 优先使用
class语法。 - 使用寄生组合继承实现兼容性方案。
- 理解 Babel 如何转换
class代码。 - 避免多重继承(JavaScript 不支持)。
11 面向对象
JavaScript 是基于原型的面向对象语言,虽无传统类的概念,但可通过多种方式实现 OOP 特性。
面试回答:请说明 JavaScript 中如何实现面向对象的四大特性
一、OOP 四大特性在 JS 中的体现
| 特性 | JavaScript 实现方式 |
|---|---|
| 封装 | 闭包、模块模式、私有字段(#) |
| 继承 | 原型链、class extends |
| 多态 | 动态类型、方法重写、接口模拟 |
| 抽象 | 抽象类模拟、接口规范约定 |
二、封装 通过闭包或私有字段实现信息隐藏。
// 闭包方式
function createCounter() {
let count = 0; // 私有变量
return {
increment() { count++; },
getCount() { return count; }
};
}
// ES2022 私有字段
class Counter {
#count = 0; // 私有字段
increment() { this.#count++; }
getCount() { return this.#count; }
}
三、继承 详见上一节“继承”。
四、多态 同一个接口在不同实例中有不同实现。
class Animal {
speak() {
throw new Error('Must override speak');
}
}
class Dog extends Animal {
speak() { return 'Woof!'; }
}
class Cat extends Animal {
speak() { return 'Meow!'; }
}
function makeAnimalSpeak(animal) {
console.log(animal.speak()); // 多态调用
}
makeAnimalSpeak(new Dog()); // Woof!
makeAnimalSpeak(new Cat()); // Meow!
五、抽象 JavaScript 无原生抽象类,但可通过约定或工具模拟。
// 抽象类模拟
class AbstractCollection {
add(item) {
throw new Error('Abstract method not implemented');
}
remove(item) {
throw new Error('Abstract method not implemented');
}
}
class ArrayCollection extends AbstractCollection {
constructor() {
super();
this.items = [];
}
add(item) { this.items.push(item); }
remove(item) { this.items = this.items.filter(i => i !== item); }
}
六、工厂模式 创建对象的替代方案,隐藏具体构造细节。
function createUser(type, name) {
switch(type) {
case 'admin':
return new AdminUser(name);
case 'guest':
return new GuestUser(name);
default:
throw new Error('Unknown type');
}
}
七、实际应用
- 复杂状态管理系统的架构。
- 组件化开发。
- 设计模式的实现基础。
八、常见误区
- 认为 JS 不支持 OOP —— 实际支持,只是方式不同。
- 过度使用继承导致脆弱基类问题。
- 忽视组合优于继承的原则。
九、最佳实践
- 使用
class语法提高代码可读性。 - 优先使用组合而非继承。
- 利用 TypeScript 实现更严格的 OOP 约束。
- 理解原型与类的关系。
12 事件机制
JavaScript 的事件机制是实现用户交互和异步编程的基础,主要包括事件捕获、目标阶段和事件冒泡。
面试回答:请解释 JavaScript 的事件流、事件委托及事件对象
一、事件流三阶段
- 捕获阶段(Capture Phase):事件从
window开始,逐级向下传播到目标元素。 - 目标阶段(Target Phase):事件到达目标元素。
- 冒泡阶段(Bubble Phase):事件从目标元素逐级向上传播回
window。
<div id="outer">
<div id="inner"></div>
</div>
document.getElementById('outer').addEventListener('click', () => {
console.log('outer captured');
}, true); // true 表示捕获阶段
document.getElementById('inner').addEventListener('click', () => {
console.log('inner target');
});
document.getElementById('outer').addEventListener('click', () => {
console.log('outer bubbled');
}); // 默认冒泡阶段
点击 inner 时输出:
outer captured
inner target
outer bubbled
二、事件监听方法
// 传统方式(不推荐)
element.onclick = function(e) {};
// 现代方式
element.addEventListener('click', handler, options);
options 参数:
capture: 是否在捕获阶段触发once: 是否只触发一次passive: 是否为被动监听器(无法调用preventDefault)signal: AbortSignal,用于取消监听
三、事件对象(Event Object) 在事件处理器中接收的 event 参数包含丰富信息:
type: 事件类型target: 触发事件的元素(最初的目标)currentTarget: 当前绑定事件处理程序的元素bubbles: 是否冒泡cancelable: 是否可取消stopPropagation(): 阻止事件继续传播stopImmediatePropagation(): 阻止同级其他监听器执行preventDefault(): 阻止默认行为
四、事件委托(Event Delegation) 利用事件冒泡,将事件监听器绑定到父元素,处理子元素的事件。
// 优点:动态元素自动生效,减少内存占用
document.getElementById('list').addEventListener('click', function(e) {
if (e.target.tagName === 'LI') {
console.log('Item clicked:', e.target.textContent);
}
});
五、自定义事件
// 创建
const event = new CustomEvent('myevent', {
detail: { message: 'Hello' },
bubbles: true
});
// 分发
element.dispatchEvent(event);
// 监听
element.addEventListener('myevent', function(e) {
console.log(e.detail.message);
});
六、实际应用
- 表单验证。
- 动态内容交互。
- SPA 路由切换。
- 游戏开发中的键盘/鼠标控制。
七、常见误区
- 混淆
target和currentTarget。 - 忘记移除事件监听器导致内存泄漏。
- 在循环中直接绑定匿名函数(无法解绑)。
八、最佳实践
- 使用事件委托优化性能。
- 合理使用
passive提升滚动性能。 - 使用
AbortController管理监听器生命周期。 - 避免过度阻止默认行为。
13 模块化
模块化是组织和管理代码的重要方式,解决了命名空间污染、依赖管理等问题。
面试回答:请说明 JavaScript 模块化的发展历程及 ES6 Module 的特性
一、模块化发展史
| 阶段 | 方案 | 特点 |
|---|---|---|
| 无模块化 | 全局变量 | 命名冲突,依赖混乱 |
| IIFE | (function(){})() | 创建私有作用域 |
| AMD | RequireJS | 异步加载,浏览器友好 |
| CMD | SeaJS | 接近 CommonJS,按需加载 |
| CommonJS | Node.js | 同步加载,服务器端 |
| UMD | 通用模块定义 | 兼容 AMD/CMD/全局 |
| ES6 Module | import/export | 官方标准,静态分析 |
二、ES6 Module 详解
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export default function multiply(a, b) { return a * b; }
// main.js
import multiply, { PI, add } from './math.js';
import * as math from './math.js';
三、特性
- 静态分析:
import必须在顶层,便于工具优化。 - 只读引用:导入的绑定是只读的。
- 单例模式:模块只会执行一次,多次导入共享同一实例。
- 严格模式:模块代码自动启用严格模式。
- Top-level await:支持顶层
await。
四、与其他模块系统的对比
| 特性 | ES6 Module | CommonJS |
|---|---|---|
| 加载方式 | 静态(编译时) | 动态(运行时) |
| 导出 | 值的引用 | 值的拷贝 |
| 加载性能 | 支持 tree-shaking | 不支持 |
| 环境 | 浏览器/Node.js | 主要在 Node.js |
| 语法 | import/export | require/module.exports |
五、实际应用
- 前端项目拆分功能模块。
- 库的发布(支持多种格式)。
- 微前端架构中的独立模块。
六、常见误区
- 认为
import是异步的 —— 实际是同步的静态解析。 - 在条件语句中使用
import—— 语法错误。 - 混淆
default和命名导出。
七、最佳实践
- 使用 ES6 Module 作为首选。
- 合理设计模块边界。
- 配合打包工具(Webpack/Rollup)使用。
- 发布库时提供多种模块格式。
14 Iterator迭代器
Iterator 是 ES6 引入的一种统一的接口机制,用于遍历各种数据结构。
面试回答:请解释 Iterator 和可迭代协议的原理及应用
一、Iterator 协议 一个对象如果具有 next() 方法,且该方法返回 { done: Boolean, value: any } 形式的对象,则称为可迭代对象。
const myIterable = {
data: [1, 2, 3],
index: 0,
next() {
if (this.index < this.data.length) {
return { done: false, value: this.data[this.index++] };
} else {
return { done: true, value: undefined };
}
}
};
// 使用
while (true) {
const result = myIterable.next();
if (result.done) break;
console.log(result.value);
}
二、可迭代协议(Iterable Protocol) 对象部署 Symbol.iterator 方法,返回一个迭代器。
const myIterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.data.length) {
return { done: false, value: this.data[index++] };
} else {
return { done: true, value: undefined };
}
}
};
}
};
// 现在可以用 for...of 遍历
for (const item of myIterable) {
console.log(item);
}
三、原生支持可迭代的对象
- Array
- String
- Map
- Set
- arguments
- NodeList (部分)
- TypedArray
四、迭代器消费方式
for...of循环- 扩展运算符
... Array.from()- 解构赋值
yield*
const arr = [1, 2, 3];
console.log([...arr]); // [1, 2, 3]
const [a, b] = arr; // 解构
五、自定义迭代行为
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current < end) {
return { done: false, value: current++ };
} else {
return { done: true, value: undefined };
}
}
};
}
}
for (const n of new Range(1, 5)) {
console.log(n); // 1,2,3,4
}
六、实际应用
- 实现自定义数据结构的遍历。
- 懒加载大数据集。
- 生成器函数的基础。
七、常见误区
- 认为所有对象都可被
for...of遍历 —— 必须实现Symbol.iterator。 - 混淆迭代器和生成器。
- 忘记返回
{ done, value }的完整结构。
八、最佳实践
- 为自定义集合类实现
Symbol.iterator。 - 利用迭代器实现惰性求值。
- 理解
for...of与for...in的区别。
15 Promise
Promise 是 ES6 引入的异步编程解决方案,用于处理回调地狱问题。
面试回答:请解释 Promise 的状态、方法及其实现原理
一、Promise 状态
- pending:初始状态。
- fulfilled:操作成功完成。
- rejected:操作失败。
> 状态只能从 pending -> fulfilled 或 pending -> rejected,不可逆。
二、基本用法
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve('Success!');
} else {
reject(new Error('Failed!'));
}
}, 1000);
});
promise
.then(value => {
console.log(value); // Success!
return value.toUpperCase();
})
.then(value => console.log(value)) // SUCCESS!
.catch(error => console.error(error))
.finally(() => console.log('Cleanup'));
三、关键方法
| 方法 | 说明 |
|---|---|
Promise.resolve(value) | 返回已解决的 Promise |
Promise.reject(reason) | 返回已拒绝的 Promise |
Promise.all(iterable) | 所有 Promise 成功才成功,任一失败则失败 |
Promise.race(iterable) | 第一个完成的 Promise 决定结果 |
Promise.allSettled(iterable) | 等待所有完成,无论成功失败 |
Promise.any(iterable) | 第一个成功的 Promise,全失败则拒绝 |
四、Promise.all 示例
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = new Promise((resolve) => setTimeout(resolve, 100, 3));
Promise.all([p1, p2, p3]).then(values => {
console.log(values); // [1, 2, 3]
});
五、错误处理
then的第二个参数或catch处理错误。- 未捕获的 Promise 错误会触发
unhandledrejection事件。 catch后可继续链式调用。
promise
.then(successHandler)
.catch(errorHandler)
.then(nextStep); // 无论成功失败都会执行
六、实现一个简易 Promise
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
// 处理穿透
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };
return new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
const result = onFulfilled(this.value);
resolve(result);
} catch (error) {
reject(error);
}
});
}
if (this.state === 'rejected') {
setTimeout(() => {
try {
const result = onRejected(this.reason);
resolve(result);
} catch (error) {
reject(error);
}
});
}
if (this.state === 'pending') {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const result = onFulfilled(this.value);
resolve(result);
} catch (error) {
reject(error);
}
});
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const result = onRejected(this.reason);
resolve(result);
} catch (error) {
reject(error);
}
});
});
}
});
}
}
七、实际应用
- 封装 Ajax 请求。
- 并行加载资源。
- 异步流程控制。
八、常见误区
- 认为
then中的return会中断链式调用 —— 实际会传递值。 - 忘记处理错误导致静默失败。
- 在
then中忘记returnPromise 导致无法链式调用。
九、最佳实践
- 使用
async/await简化 Promise 使用。 - 合理使用
Promise.all并行化。 - 捕获所有可能的异常。
- 使用
finally进行清理。
16 Generator
Generator 是 ES6 引入的一种特殊函数,可以暂停和恢复执行,是实现协程的基础。
面试回答:请解释 Generator 的语法、特性及应用场景
一、基本语法
function* generator() {
yield 'first';
yield 'second';
return 'result';
}
const gen = generator();
console.log(gen.next()); // { value: 'first', done: false }
console.log(gen.next()); // { value: 'second', done: false }
console.log(gen.next()); // { value: 'result', done: true }
二、关键特性
function*定义生成器函数。yield暂停执行并返回值。next()恢复执行,可传参。return()提前结束。throw()抛出异常。
三、传参示例
function* echo() {
const input = yield 'ready';
yield `You said: ${input}`;
}
const gen = echo();
console.log(gen.next().value); // 'ready'
console.log(gen.next('hello').value); // 'You said: hello'
四、与 Iterator 的关系 生成器函数返回一个生成器对象,该对象既是迭代器也是可迭代对象。
function* numbers() {
yield 1;
yield 2;
}
const gen = numbers();
// gen 是迭代器
gen.next(); // { value: 1, done: false }
// gen 也是可迭代对象
for (const n of gen) {
console.log(n); // 2 (因为上次已消耗 1)
}
五、yield* 委托 用于将另一个生成器或可迭代对象的值委托给当前生成器。
function* g1() {
yield 1;
yield 2;
}
function* g2() {
yield* g1();
yield 3;
}
[...g2()]; // [1, 2, 3]
六、实际应用
- 实现无限序列。
- 异步流程控制(早于 async/await)。
- 状态机。
- 懒加载数据。
七、异步控制示例(早期方案)
function request(url) {
return new Promise((resolve, reject) => {
// 模拟请求
setTimeout(() => resolve(`Data from ${url}`), 1000);
});
}
function* main() {
const data1 = yield request('/api/user');
const data2 = yield request('/api/posts');
console.log(data1, data2);
}
// 需要一个 runner 函数来驱动
function run(generator) {
const it = generator();
function go(result) {
if (result.done) return;
result.value.then(
value => go(it.next(value)),
error => go(it.throw(error))
);
}
go(it.next());
}
run(main);
八、常见误区
- 认为
yield可以在普通函数中使用 —— 只能在生成器函数中。 - 忽视
next()返回的对象结构。 - 在
for...of中使用生成器时忘记重新创建实例。
九、最佳实践
- 现代开发中优先使用
async/await。 - 使用生成器实现复杂的迭代逻辑。
- 理解 Babel 如何转换生成器。
- 避免过度复杂的生成器逻辑。
17 async/await
async/await 是 ES2017 引入的异步编程语法糖,基于 Promise 实现,使异步代码看起来像同步代码。
面试回答:请解释 async/await 的工作原理及最佳实践
一、基本用法
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch failed:', error);
throw error;
}
}
// 使用
fetchData().then(data => console.log(data));
二、核心规则
async函数总是返回一个 Promise。await只能在async函数内部使用。await后可以是任意值,但通常为 Promise。await会暂停函数执行,等待 Promise 完成。
三、并发控制
// 串行(等待前一个完成)
const user = await fetchUser();
const posts = await fetchPosts(user.id);
// 并行(同时发起)
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts(1)
]);
// 动态并行
async function fetchMultiple(urls) {
const promises = urls.map(url => fetch(url));
const responses = await Promise.all(promises);
return Promise.all(responses.map(r => r.json()));
}
四、错误处理
- 使用
try...catch捕获错误。 - 未捕获的错误会使返回的 Promise 被拒绝。
- 可以
catch处理局部错误。
async function riskyOperation() {
try {
const result = await mightFail();
return process(result);
} catch (error) {
// 处理错误,函数仍可返回正常值
return fallbackValue;
}
}
五、与 Promise 的关系async/await 是 Promise 的语法糖,以下两种写法等价:
// Promise 链
function fetchUser() {
return fetch('/api/user')
.then(response => response.json())
.then(data => data.name);
}
// async/await
async function fetchUser() {
const response = await fetch('/api/user');
const data = await response.json();
return data.name;
}
六、性能考虑
await会阻塞后续代码,合理安排并发。- 避免在循环中
await(除非需要串行)。 - 使用
Promise.all实现并行。
// 反模式:串行请求
for (const url of urls) {
const data = await fetch(url); // 等待每一个
}
// 正确:并行请求
const promises = urls.map(url => fetch(url));
const responses = await Promise.all(promises);
七、实际应用
- API 数据获取。
- 文件读写操作。
- 复杂的异步流程编排。
- 服务端渲染数据预取。
八、常见误区
- 在
forEach中使用await—— 无效,因为forEach回调不是async。 - 忘记处理错误导致崩溃。
- 认为
await会阻塞整个程序 —— 只阻塞当前async函数。
九、最佳实践
- 使用
try...catch包裹可能出错的await。 - 合理使用
Promise.all提升性能。 - 为
async函数命名以明确其异步性质。 - 配合 TypeScript 提供更好的类型推断。
18 事件循环
事件循环(Event Loop)是 JavaScript 实现异步非阻塞 I/O 的核心机制,协调宏任务和微任务的执行。
面试回答:请解释 JavaScript 的事件循环机制及浏览器与 Node.js 的差异
一、运行时模型 JavaScript 引擎包含:
- 调用栈(Call Stack):执行同步代码。
- 堆(Heap):存储对象。
- 队列(Queue):存放回调函数。
- 宏任务队列(Task Queue):setTimeout, setInterval, setImmediate, I/O, UI rendering。
- 微任务队列(Microtask Queue):Promise, queueMicrotask, MutationObserver。
二、事件循环算法
- 执行调用栈中的所有同步代码。
- 执行微任务队列中的所有任务(清空队列)。
- 执行一个宏任务(来自任务队列)。
- 重复步骤 2-3。
> 注意:每轮循环中,微任务队列会被完全清空。
三、示例分析
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('script end');
// 输出顺序:
// script start
// script end
// promise1
// promise2
// setTimeout
四、Node.js 中的事件循环 Node.js 有更复杂的循环阶段:
- timers
- pending callbacks
- idle, prepare
- poll
- check (
setImmediate) - close callbacks
> 浏览器与 Node.js 在 process.nextTick 和 setImmediate 的优先级上有差异。
五、queueMicrotask 方法 ES2017 引入,用于将函数加入微任务队列。
queueMicrotask(() => {
console.log('microtask via queueMicrotask');
});
六、实际应用
- 理解异步代码执行顺序。
- 实现延迟执行(优于
setTimeout(fn, 0))。 - 避免长时间运行的同步代码阻塞事件循环。
七、常见误区
- 认为
setTimeout(fn, 0)会立即执行 —— 至少等待一轮事件循环。 - 忽视微任务的高优先级。
- 在微任务中递归调度导致主线程饥饿。
八、最佳实践
- 使用
Promise或queueMicrotask实现高优先级异步操作。 - 避免在事件循环中执行耗时同步任务。
- 使用
requestIdleCallback处理低优先级任务。 - 理解不同环境下的事件循环差异。
19 垃圾回收
垃圾回收(Garbage Collection)是 JavaScript 自动管理内存的机制,释放不再使用的对象所占用的内存。
面试回答:请解释 JavaScript 的垃圾回收机制及常见算法
一、内存管理概述 JavaScript 具有自动垃圾回收机制,开发者无需手动分配和释放内存。当对象不再被引用时,GC 会自动回收其内存。
二、可达性(Reachability) GC 的核心概念是“可达性”:从根对象(如全局对象、调用栈中的变量)出发,能直接或间接访问到的对象被认为是“可达”的,其余对象将被回收。
三、常见垃圾回收算法
标记-清除(Mark-and-Sweep)
- 标记阶段:从根对象开始遍历,标记所有可达对象。
- 清除阶段:扫描整个堆,回收未被标记的对象。
- 优点:简单有效,主流 V8 引擎使用。
- 缺点:会产生内存碎片。
引用计数(Reference Counting)
- 每个对象维护一个引用计数,当计数为 0 时立即回收。
- 缺点:无法处理循环引用问题。
let obj1 = {}; let obj2 = {}; obj1.ref = obj2; obj2.ref = obj1; // 循环引用,计数永不为 0
分代回收(Generational Collection)
- 基于“弱代假说”:大多数对象生命周期很短。
- 内存分为新生代(Young Generation)和老生代(Old Generation)。
- 新生代使用 Scavenge 算法(如 Cheney 算法),速度快。
- 老生代使用 Mark-Sweep 和 Mark-Compact 算法。
四、V8 引擎的 GC 实现
- 新生代:使用 Scavenge 算法,将内存分为 From 和 To 空间,存活对象复制到 To 空间,然后交换角色。
- 老生代:使用 Mark-Sweep 和 Mark-Compact(解决碎片问题)。
- 增量标记:将标记过程分解为小片段,减少卡顿。
- 并发标记:在单独线程中标记对象,减少主线程阻塞。
五、实际应用
- 避免创建大量短期对象,减少 GC 频率。
- 及时解除不必要的引用(如事件监听器、定时器)。
- 使用 WeakMap/WeakSet 存储关联数据,避免阻止回收。
六、常见误区
- 认为
delete操作会立即回收内存 —— 它只是断开引用,GC 何时回收不确定。 - 忽视闭包可能导致的内存泄漏。
- 试图手动触发 GC(如
--expose-gc)在生产环境不现实。
七、最佳实践
- 使用 Chrome DevTools Memory 面板分析内存快照。
- 识别并修复内存泄漏。
- 避免在循环中创建函数(产生闭包)。
- 使用对象池管理高频创建/销毁的对象。
20 内存泄露
内存泄露指程序中已分配的内存由于某种原因无法被释放,导致可用内存逐渐减少。
面试回答:请列举 JavaScript 中常见的内存泄露场景及解决方案
一、常见内存泄露场景
意外的全局变量
function leak() { leakVariable = 'I am global now'; // 忘记 var/let/const }解决方案:使用严格模式
'use strict',避免隐式全局变量。未清理的定时器或回调函数
setInterval(() => { // 如果 timer 一直存在,callback 无法被回收 }, 1000);解决方案:在适当时候调用
clearInterval/clearTimeout。脱离 DOM 的引用
const element = document.getElementById('myDiv'); // 即使从 DOM 移除,只要 JS 有引用,就不会被回收解决方案:将引用置为
null。闭包引起的泄露
function outer() { const hugeData = new Array(1000000).fill('*'); return function inner() { console.log(hugeData.length); // hugeData 被闭包引用 }; } // 如果 inner 一直被引用,hugeData 无法回收解决方案:避免在闭包中引用大型对象,或在不需要时解除引用。
事件监听器未移除
element.addEventListener('click', handler); // 移除 element 后,若不移除监听器,handler 仍被引用解决方案:使用
removeEventListener,或在现代框架中依赖自动清理。循环引用(在 IE 中尤其严重)
let obj1 = {}; let obj2 = {}; obj1.ref = obj2; obj2.ref = obj1;解决方案:现代浏览器的 GC 能处理循环引用,但仍应避免。
二、检测工具
- Chrome DevTools: Memory 面板,拍摄堆快照(Heap Snapshot)、记录内存分配时间线(Record Allocation Timeline)。
- Performance 面板:监控内存使用趋势。
三、最佳实践
- 使用 WeakMap/WeakSet 存储辅助数据。
- 在组件卸载时(如 React 的 useEffect cleanup)清理副作用。
- 避免在全局作用域存储大量数据。
- 定期审查代码中的长生命周期对象。
21 深浅拷贝
深拷贝和浅拷贝是处理对象复制时的两种策略。
面试回答:请解释深拷贝与浅拷贝的区别及实现方式
一、概念区别
- 浅拷贝:只复制对象的第一层属性。对于引用类型,复制的是地址,因此源对象和副本共享同一引用。
- 深拷贝:递归复制对象的所有层级,包括嵌套的对象和数组,副本与源对象完全独立。
二、浅拷贝实现方式
// 1. Object.assign()
const shallow1 = Object.assign({}, original);
// 2. 展开运算符
const shallow2 = { ...original };
// 3. Array.prototype.slice() / concat()
const shallowArr = arr.slice();
三、深拷贝实现方式
JSON 方法(局限性大)
const deepCopy = JSON.parse(JSON.stringify(original));优点:简单。 缺点:无法处理函数、undefined、Symbol、循环引用、Date 对象变为字符串等。
递归实现(基础版)
function deepClone(obj) { if (obj === null || typeof obj !== 'object') return obj; if (obj instanceof Date) return new Date(obj); if (obj instanceof Array) return obj.map(item => deepClone(item)); if (typeof obj === 'object') { const cloned = {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = deepClone(obj[key]); } } return cloned; } return obj; }递归实现(完善版,处理循环引用)
function deepClone(obj, hash = new WeakMap()) { if (obj === null || typeof obj !== 'object') return obj; if (hash.has(obj)) return hash.get(obj); // 处理循环引用 let cloned; if (obj instanceof Date) { cloned = new Date(obj); } else if (obj instanceof Array) { cloned = obj.map(item => deepClone(item, hash)); } else if (obj instanceof RegExp) { cloned = new RegExp(obj); } else { cloned = Object.create(Object.getPrototypeOf(obj)); } hash.set(obj, cloned); // 记录已克隆对象 for (let key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = deepClone(obj[key], hash); } } return cloned; }使用第三方库
- Lodash:
_.cloneDeep() - jQuery:
$.extend(true, {}, obj)
- Lodash:
四、实际应用
- 在 Redux 中更新 state 时避免直接修改原对象。
- 配置对象的默认值合并。
- 防止函数参数被意外修改。
五、常见误区
- 认为展开运算符
...是深拷贝 —— 它是浅拷贝。 - 忽视循环引用导致的栈溢出。
- 未处理特殊对象(Date, RegExp, Function)。
六、最佳实践
- 根据需求选择合适的方法。
- 复杂场景使用成熟的库(如 Lodash)。
- 注意性能,深拷贝大型对象可能很慢。
22 节流与防抖
节流(Throttle)和防抖(Debounce)是优化高频事件处理的常用技巧。
面试回答:请解释节流与防抖的原理及实现
一、防抖(Debounce) 在事件被触发 n 秒后才执行回调,如果在这 n 秒内又被触发,则重新计时。
应用场景:
- 搜索框输入联想(避免每次输入都请求)。
- 窗口大小调整(resize)。
- 按钮重复点击。
实现方式:
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 使用
const debouncedResize = debounce(handleResize, 300);
window.addEventListener('resize', debouncedResize);
二、节流(Throttle) 规定一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次,只有一次生效。
应用场景:
- 滚动事件(scroll)监听。
- 游戏中的射击频率限制。
- Canvas 绘图。
实现方式(定时器版):
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
实现方式(时间戳版):
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function(...args) {
const context = this;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
三、区别总结
| 特性 | 防抖 | 节流 |
|---|---|---|
| 执行时机 | 事件停止触发后执行 | 固定频率执行 |
| 执行次数 | 可能只执行一次 | 在持续触发时定期执行 |
| 适用场景 | 输入校验、搜索 | 滚动、动画 |
四、最佳实践
- 根据业务需求选择合适的策略。
- 使用 Lodash 的
_.debounce和_.throttle。 - 注意
this和参数的传递。
23 Proxy代理
Proxy 是 ES6 引入的强大特性,用于定义基本操作的自定义行为(如属性查找、赋值、枚举等)。
面试回答:请解释 Proxy 的作用、语法及实际应用
一、基本语法
const proxy = new Proxy(target, handler);
target: 要代理的目标对象。handler: 一个对象,包含一系列 trap(陷阱)函数,定义拦截行为。
二、常用 Trap
| Trap | 触发时机 |
|---|---|
get(target, propKey, receiver) | 读取属性 |
set(target, propKey, value, receiver) | 设置属性 |
has(target, propKey) | in 操作符 |
deleteProperty(target, propKey) | delete 操作 |
ownKeys(target) | Object.getOwnPropertyNames() / Object.keys() |
apply(target, thisArg, argumentsList) | 函数调用 |
construct(target, argumentsList, newTarget) | new 操作 |
三、应用示例
数据绑定与响应式
const observed = new Proxy(data, { set(target, key, value) { target[key] = value; updateView(); // 视图更新 return true; } });属性验证
const validatedObj = new Proxy({}, { set(target, key, value) { if (key === 'age') { if (!Number.isInteger(value)) { throw new TypeError('Age must be an integer'); } } target[key] = value; return true; } });日志记录
const loggedObj = new Proxy(obj, { get(target, key) { console.log(`GET ${key}`); return target[key]; }, set(target, key, value) { console.log(`SET ${key} = ${value}`); target[key] = value; return true; } });负索引数组
function createNegativeArray(arr) { return new Proxy(arr, { get(target, propKey, receiver) { const index = Number(propKey); if (index < 0) { return target[target.length + index]; } return Reflect.get(target, propKey, receiver); } }); } const arr = createNegativeArray([1, 2, 3]); console.log(arr[-1]); // 3
四、与 Object.defineProperty 的区别
defineProperty只能劫持已有属性,Proxy可以劫持整个对象的所有操作。Proxy能监听数组变化、对象新增/删除属性。Proxy功能更强大,但兼容性稍差(IE 不支持)。
五、实际应用
- Vue 3 的响应式系统基于 Proxy 实现。
- 实现 ORM 框架。
- 创建调试工具。
- 实现懒加载。
六、最佳实践
- 注意性能,Proxy 有一定开销。
- 处理好边界情况(如 Symbol 属性)。
- 使用
Reflect方法确保默认行为。
24 Ajax
Ajax(Asynchronous JavaScript and XML)是一种在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页的技术。
面试回答:请解释 Ajax 的原理、实现方式及现代替代方案
一、核心原理 通过 JavaScript 的 XMLHttpRequest 对象或 fetch API 向服务器发送异步请求,接收响应数据(通常是 JSON),然后使用 DOM 操作更新页面局部内容。
二、XMLHttpRequest 实现
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
}
};
xhr.send();
三、fetch API(现代方式)
fetch('/api/data')
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
四、async/await 版本
async function fetchData() {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Network response was not ok');
const data = await response.json();
return data;
} catch (error) {
console.error('Error:', error);
}
}
五、Axios(第三方库)
axios.get('/api/data')
.then(response => console.log(response.data))
.catch(error => console.error(error));
六、CORS(跨域资源共享) 现代 Ajax 请求受同源策略限制。CORS 是一种 W3C 标准,通过服务器设置响应头(如 Access-Control-Allow-Origin)来允许跨域请求。
七、实际应用
- 表单提交无刷新。
- 无限滚动加载更多内容。
- 实时搜索建议。
- 单页应用(SPA)数据获取。
八、常见问题
- 处理网络错误和超时。
- 序列化和反序列化数据(JSON)。
- CSRF 攻击防护(使用 Token)。
九、最佳实践
- 使用
fetch或 Axios 替代传统的 XHR。 - 处理错误和加载状态。
- 使用 AbortController 取消请求。
- 避免在循环中发起大量请求。
25 深入数组
数组是 JavaScript 中最常用的数据结构之一,了解其高级方法和特性对开发至关重要。
面试回答:请介绍 JavaScript 数组的常用方法及性能优化
一、变异方法(Mutating Methods) 直接修改原数组:
push(),pop()shift(),unshift()splice()sort(),reverse()
二、非变异方法(Non-Mutating Methods) 返回新数组,不修改原数组:
concat()slice()filter()map()flatMap()reduce(),reduceRight()join()toString()
三、遍历方法
forEach():执行函数,无返回值。every():全部满足条件返回 true。some():至少一个满足返回 true。find()/findIndex():返回第一个匹配的元素或索引。includes():判断是否包含某元素。
四、索引方法
indexOf()/lastIndexOf():返回索引。keys(),values(),entries():返回迭代器。
五、静态方法
Array.from():类数组转数组。Array.of():创建数组。Array.isArray():判断是否为数组。
六、性能优化建议
- 避免频繁的 push/pop 在数组头部:
shift/unshift时间复杂度 O(n),考虑使用双端队列或length操作。 - 使用
for循环代替forEach获取更好性能(尤其在大量数据时)。 map/filter等链式调用可能产生中间数组,考虑使用reduce合并操作。- 稀疏数组:避免创建非常大的稀疏数组,浪费内存。
七、实际应用
- 数据过滤与转换。
- 列表渲染。
- 统计计算(
reduce)。 - 数组去重。
八、最佳实践
- 优先使用函数式编程方法(
map,filter,reduce)提高代码可读性。 - 在 React 等框架中,避免直接修改数组,使用不可变更新。
- 了解各方法的时间复杂度。
二、HTML
1 meta 标签:自动刷新/跳转
<meta> 标签提供关于 HTML 文档的元数据,可用于实现页面自动刷新或跳转。
面试回答:如何使用 meta 标签实现页面自动刷新和跳转?
一、自动刷新 使用 http-equiv="refresh" 和 content 属性。
<!-- 5秒后刷新页面 -->
<meta http-equiv="refresh" content="5">
<!-- 5秒后刷新到指定URL -->
<meta http-equiv="refresh" content="5;url=https://example.com">
二、页面跳转
<!-- 立即跳转 -->
<meta http-equiv="refresh" content="0;url=https://example.com">
<!-- 3秒后跳转 -->
<meta http-equiv="refresh" content="3;url=https://example.com">
三、注意事项
- 用户体验:自动跳转可能干扰用户,应谨慎使用。
- SEO:搜索引擎可能不友好对待 meta refresh,推荐使用 HTTP 3xx 重定向。
- 替代方案:JavaScript 实现跳转更灵活。
setTimeout(() => { window.location.href = 'https://example.com'; }, 3000);
四、其他 meta 应用
- 字符集:
<meta charset="UTF-8"> - 视口:
<meta name="viewport" content="width=device-width, initial-scale=1.0"> - SEO 描述:
<meta name="description" content="...">
五、最佳实践
- 优先使用服务器端重定向(HTTP 301/302)。
- 如需客户端跳转,使用 JavaScript 提供更好的控制。
- 避免在重要内容页面使用自动刷新。
2 viewport
viewport 元标签用于控制移动端浏览器的布局视口。
面试回答:请解释 viewport 的作用及常用配置
一、viewport 的重要性 在移动设备上,浏览器通常会模拟一个较宽的视口(如 980px),然后缩放页面以适应屏幕。这会导致桌面网站在手机上显示过小。viewport 元标签让开发者可以控制这个虚拟视口。
二、基本语法
<meta name="viewport" content="width=device-width, initial-scale=1.0">
三、常用属性
| 属性 | 说明 | 示例 |
|---|---|---|
width | 设置视口宽度 | width=device-width(设备宽度) |
height | 设置视口高度 | height=device-height |
initial-scale | 初始缩放比例 | initial-scale=1.0 |
minimum-scale | 最小缩放比例 | minimum-scale=0.5 |
maximum-scale | 最大缩放比例 | maximum-scale=2.0 |
user-scalable | 是否允许用户缩放 | user-scalable=yes/no |
四、典型配置
<!-- 响应式设计必备 -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 禁止缩放(不推荐,影响可访问性) -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
五、实际应用
- 确保网页在移动设备上正确缩放。
- 配合 CSS 媒体查询实现响应式布局。
- 避免横向滚动条。
六、最佳实践
- 总是包含 viewport 标签。
- 使用
device-width适配不同设备。 - 避免禁用用户缩放,尊重用户偏好。
3 性能优化
前端性能优化是提升用户体验的关键环节。
面试回答:请列举前端性能优化的主要策略
一、加载优化
- 减少 HTTP 请求:合并 CSS/JS 文件,使用雪碧图,内联小资源。
- 资源压缩:Gzip/Brotli 压缩文本资源。
- CDN 加速:静态资源部署到 CDN。
- DNS 预解析:
<link rel="dns-prefetch" href="//example.com">。 - 预加载关键资源:
<link rel="preload">。 - 懒加载:图片、组件延迟加载。
二、渲染优化
- 关键渲染路径优化:减少关键资源数量,缩短关键路径长度。
- CSS 放在头部:避免 FOUC(Flash of Unstyled Content)。
- JS 放在尾部或使用
async/defer:避免阻塞渲染。 - 避免使用
@import:会阻塞页面渲染。 - 减少重排(Reflow)和重绘(Repaint):
- 避免频繁读取布局信息(如
offsetTop)。 - 使用
transform和opacity实现动画(触发合成层)。 - 批量修改 DOM(DocumentFragment)。
- 避免频繁读取布局信息(如
三、脚本优化
- 防抖与节流:优化高频事件处理。
- Web Workers:将耗时任务放到后台线程。
- 代码分割:按路由或功能拆分代码。
- Tree Shaking:移除未使用的代码。
四、缓存策略
- 强缓存:
Cache-Control: max-age=3600。 - 协商缓存:
ETag/Last-Modified。 - Service Worker:实现离线缓存。
五、其他
- 图片优化:WebP 格式,响应式图片(
srcset),懒加载。 - 字体优化:
font-display: swap,预加载字体。 - HTTP/2:多路复用,头部压缩。
- 预渲染/SSR:提升首屏速度。
六、监测工具
- Lighthouse
- WebPageTest
- Chrome DevTools Performance 面板
七、最佳实践
- 以用户为中心,关注 FCP、LCP、CLS 等 Core Web Vitals 指标。
- 持续监控性能。
- 建立性能预算。
4 如何高效操作DOM
高效的 DOM 操作对性能至关重要。
面试回答:有哪些高效操作 DOM 的方法?
一、减少 DOM 访问
- 将 DOM 查询结果缓存到变量。
- 批量读取属性,再批量更新。
二、使用 DocumentFragment 在内存中创建节点片段,一次性插入 DOM,减少重排。
const fragment = document.createDocumentFragment();
for (let i = 0; i < items.length; i++) {
const li = document.createElement('li');
li.textContent = items[i];
fragment.appendChild(li);
}
list.appendChild(fragment);
三、离线 DOM 操作
- 将元素从 DOM 中移除,操作完成后再插入。
- 使用
display: none临时隐藏元素。
四、使用 CSS 类而非内联样式
- 更易于维护。
- 避免频繁的样式计算。
// 推荐
element.classList.add('active');
// 不推荐
element.style.color = 'red';
element.style.fontSize = '16px';
五、避免强制同步布局 不要在修改样式后立即读取布局属性,会触发强制重排。
// 反模式
element.style.height = '200px';
console.log(element.offsetHeight); // 强制重排
// 正确
element.style.height = '200px';
// ... 其他修改
// 最后一次性读取
console.log(element.offsetHeight);
六、使用事件委托 减少事件监听器数量。
// 优化前:为每个按钮绑定
buttons.forEach(btn => btn.addEventListener('click', handler));
// 优化后:委托给父容器
container.addEventListener('click', e => {
if (e.target.matches('button')) {
handler(e);
}
});
七、使用 requestAnimationFrame 在下一个重绘周期前执行 DOM 更新。
function update() {
// 更新 DOM
requestAnimationFrame(update);
}
requestAnimationFrame(update);
八、最佳实践
- 优先使用虚拟 DOM 框架(React/Vue)。
- 关注重排和重绘成本。
- 使用性能分析工具定位瓶颈。
三、CSS基础
1 盒模型
CSS 盒模型是布局的基础,每个元素都被视为一个矩形盒子。
面试回答:请解释 CSS 盒模型及 box-sizing 属性
一、盒模型组成 每个盒子由四部分组成(从内到外):
- content:内容区域,
width/height默认设置于此。 - padding:内边距,背景色会延伸至此。
- border:边框。
- margin:外边距,透明,不接受背景。
二、标准盒模型(content-box)
width/height= 内容区域的宽高。- 总宽度 =
width+padding-left+padding-right+border-left+border-right+margin-left+margin-right。 - 这是默认行为。
三、IE 盒模型(border-box)
width/height= 内容 + padding + border 的总和。- 总宽度 =
width+margin-left+margin-right。 - 更符合直觉。
四、box-sizing 属性
/* 默认值 */
.box1 {
box-sizing: content-box;
}
/* 推荐:统一使用 border-box */
.box2 {
box-sizing: border-box;
}
/* 继承父元素 */
.box3 {
box-sizing: inherit;
}
五、实际应用
- 重置全局盒模型:
*, *::before, *::after { box-sizing: border-box; } - 精确控制元素尺寸。
六、最佳实践
- 在项目开始时统一设置
box-sizing: border-box。 - 理解不同盒模型对布局的影响。
2 BFC
BFC(Block Formatting Context)是 CSS 中的一个渲染区域,有其独特的布局规则。
面试回答:什么是 BFC?如何创建 BFC 及其应用场景
一、BFC 定义 BFC 是一个独立的渲染区域,内部的元素布局不受外部影响,反之亦然。
二、BFC 的特性
- 内部的 Box 垂直排列。
- Box 垂直方向的距离由 margin 决定,属于同一个 BFC 的相邻 Box 的 margin 会发生重叠。
- 每个元素的 margin box 的左边,与包含块 border box 的左边相接触(从左到右),即使浮动也是如此。
- BFC 的区域不会与 float box 重叠。
- 计算 BFC 的高度时,浮动元素也参与计算。
- BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然。
三、如何创建 BFC 满足下列条件之一即可:
- 根元素(
<html>) float的值不为noneposition为absolute或fixeddisplay为inline-block,table-cell,table-caption,flex,inline-flex,grid,inline-gridoverflow的值不为visible
四、应用场景
防止 margin 重叠
.parent { overflow: hidden; /* 创建 BFC */ } .child { margin-top: 20px; }清除浮动
.clearfix { overflow: hidden; /* 创建 BFC,包含浮动子元素 */ }避免文字环绕浮动元素
.sidebar { float: right; width: 200px; } .main-content { overflow: hidden; /* 创建 BFC,不与 sidebar 重叠 */ }
五、最佳实践
- 理解 BFC 是解决特定布局问题的有力工具。
- 现代布局(Flex/Grid)减少了对 BFC 的依赖,但仍有价值。
3 层叠上下文
层叠上下文(Stacking Context)决定了元素在 Z 轴上的堆叠顺序。
面试回答:请解释 CSS 层叠上下文的创建及 z-index 的工作原理
一、层叠上下文定义 层叠上下文是 HTML 元素的三维概念,沿 Z 轴(垂直于屏幕)排列。每个层叠上下文有其独立的堆叠顺序。
二、如何创建层叠上下文 以下情况会创建新的层叠上下文:
- 根元素(
<html>) position不为static且z-index值不为autoopacity小于 1transform不为nonefilter不为nonewill-change指定了创建层叠上下文的属性perspective不为noneclip-path使用了 URL 引用的剪切路径或不是noneisolation设置为isolate-webkit-overflow-scrolling设置为touch
三、堆叠顺序(从下到上) 在一个层叠上下文中,元素按以下顺序堆叠:
- 背景和边框(Background and borders)
- 负 z-index 的子元素(按 z-index 升序)
- Block-level boxes(非定位元素)
- Floats
- Inline-level boxes
- z-index: auto / 0 的定位元素
- 正 z-index 的子元素(按 z-index 升序)
四、z-index 的工作原理
z-index只在定位元素(position不为static)上生效。z-index的值可以是auto、数字、inherit、initial。- 在同一个层叠上下文中,
z-index值越大,越靠上。 - 不同层叠上下文中,父级的层叠上下文优先级更高。
五、实际应用
- 模态框(Modal)置于顶层。
- 下拉菜单的层级控制。
- 轮播图的切换效果。
六、常见误区
- 认为
z-index在任何元素上都有效 —— 必须配合position使用。 - 忽视层叠上下文的创建条件。
七、最佳实践
- 使用最小的
z-index值满足需求。 - 避免滥用高
z-index(如 9999)。 - 理解层叠上下文的嵌套影响。
4 左右居中方案
实现元素水平居中有多种方法。
面试回答:请列举实现元素左右居中的方法
一、行内元素或行内块元素
- text-align: center(父元素)
.parent { text-align: center; } .child { display: inline; /* or inline-block */ }
二、块级元素
margin: 0 auto
.block-center { width: 200px; margin: 0 auto; }绝对定位 + transform
.abs-center { position: absolute; left: 50%; transform: translateX(-50%); }Flexbox
.flex-center { display: flex; justify-content: center; }Grid
.grid-center { display: grid; justify-content: center; }
三、未知宽度的块级元素
Flexbox(推荐)
.flex-center { display: flex; justify-content: center; }Grid
.grid-center { display: grid; justify-content: center; }table-cell + text-align
.table-center { display: table-cell; text-align: center; width: 100%; /* 或固定宽度 */ } .inner { display: inline-block; }
四、最佳实践
- 现代布局首选 Flexbox 或 Grid。
- 兼容性要求高时使用
margin: 0 auto(需知宽度)。 - 避免使用
float实现居中。
5 上下垂直居中方案
垂直居中比水平居中更具挑战性。
面试回答:请列举实现元素上下垂直居中的方法
一、单行文本垂直居中
- line-height = height
.single-line { height: 50px; line-height: 50px; }
二、已知高度的块级元素
绝对定位 + margin 负值
.abs-known { position: absolute; top: 50%; height: 100px; margin-top: -50px; /* 高度的一半 */ }绝对定位 + transform
.abs-transform { position: absolute; top: 50%; transform: translateY(-50%); }
三、未知高度的块级元素
Flexbox(强烈推荐)
.flex-center { display: flex; align-items: center; justify-content: center; /* 水平居中 */ }Grid
.grid-center { display: grid; align-items: center; justify-content: center; }table-cell + vertical-align
.table-cell { display: table-cell; vertical-align: middle; height: 200px; /* 父元素需有高度 */ }绝对定位 + transform(未知高度)
.abs-transform-unknown { position: absolute; top: 50%; transform: translateY(-50%); }
四、多行文本垂直居中
Flexbox
.flex-multi { display: flex; align-items: center; height: 200px; }table-cell
.table-multi { display: table-cell; vertical-align: middle; height: 200px; }
五、最佳实践
- Flexbox 是最通用、最简单的方案。
- Grid 适合复杂布局。
- 避免使用
vertical-align除非在 table-cell 上下文中。
6 选择器权重计算方式
CSS 选择器的权重决定了样式的优先级。
面试回答:请解释 CSS 选择器的权重计算规则
一、权重计算方法 权重由四个等级组成,从左到右依次为:
- 内联样式(Inline styles):1000
- ID 选择器(#id):100
- 类选择器(.class)、属性选择器([attr])、伪类(:hover):10
- 元素选择器(div)、伪元素(::before):1
二、计算示例
| 选择器 | 权重计算 | 总值 |
|---|---|---|
div | 0,0,0,1 | 1 |
.nav | 0,0,1,0 | 10 |
#header | 0,1,0,0 | 100 |
div#header | 0,1,0,1 | 101 |
.nav ul li | 0,0,1,2 | 12 |
body div#content .info:hover | 0,1,2,2 | 122 |
<p style="color: red;"> | 1,0,0,0 | 1000 |
三、其他规则
- 通用选择器(
*)、后代选择器()、兄弟选择器(+,~)不增加权重。 - !important 声明具有最高优先级,会覆盖内联样式,但应避免使用。
- 继承的样式没有权重,优先级最低。
四、实际应用
- 使用更具体的选择器提高优先级。
- 避免使用
!important。 - 使用 BEM 等命名规范减少权重竞争。
五、最佳实践
- 优先使用类选择器,避免过度使用 ID。
- 保持选择器简洁。
- 使用 CSS Modules 或 CSS-in-JS 避免全局冲突。
7 清除浮动
浮动(Float)曾是主要的布局方式,清除浮动是必要的。
面试回答:有哪些清除浮动的方法?
一、为什么需要清除浮动 浮动元素脱离文档流,其父元素无法感知其高度,导致父元素高度塌陷。
二、清除浮动的方法
clear: both
.clearfix::after { content: ""; display: block; clear: both; }创建 BFC
.bfc-parent { overflow: hidden; /* 或 auto */ }使用 Flexbox/Grid
.modern-parent { display: flex; /* 或 grid */ }空 div 法(不推荐)
<div class="float-container"> <div style="float: left;"></div> <div style="float: right;"></div> <div style="clear: both;"></div> </div>
三、最佳实践
- 现代布局使用 Flexbox 或 Grid,无需清除浮动。
- 旧项目使用
clearfix类。 - 避免空 div 法。
8 左边定宽,右边自适应方案
经典的两栏布局。
面试回答:如何实现左边定宽、右边自适应的布局?
一、Flexbox(推荐)
.container {
display: flex;
}
.left {
width: 200px; /* 定宽 */
}
.right {
flex: 1; /* 自适应剩余空间 */
}
二、Grid
.container {
display: grid;
grid-template-columns: 200px 1fr;
}
三、浮动
.left {
float: left;
width: 200px;
}
.right {
margin-left: 200px; /* 留出左边宽度 */
}
四、绝对定位
.container {
position: relative;
}
.left {
position: absolute;
left: 0;
width: 200px;
}
.right {
margin-left: 200px;
}
五、Table
.container {
display: table;
width: 100%;
}
.left {
display: table-cell;
width: 200px;
}
.right {
display: table-cell;
}
六、最佳实践
- 优先使用 Flexbox。
- Grid 适合更复杂的需求。
- 避免使用浮动和绝对定位。
9 左右两边定宽,中间自适应
三栏布局的经典问题。
面试回答:如何实现左右两边定宽、中间自适应的圣杯布局或双飞翼布局?
一、Flexbox(推荐)
.container {
display: flex;
}
.left {
width: 200px;
}
.center {
flex: 1; /* 自适应 */
}
.right {
width: 200px;
}
二、Grid
.container {
display: grid;
grid-template-columns: 200px 1fr 200px;
}
三、浮动(双飞翼布局)
<div class="container">
<div class="center"></div>
<div class="left"></div>
<div class="right"></div>
</div>
.container {
padding: 0 200px; /* 为左右留出空间 */
}
.center {
float: left;
width: 100%;
}
.left {
float: left;
width: 200px;
margin-left: -100%;
position: relative;
left: -200px;
}
.right {
float: left;
width: 200px;
margin-left: -200px;
}
四、绝对定位
.container {
position: relative;
}
.left {
position: absolute;
left: 0;
width: 200px;
}
.right {
position: absolute;
right: 0;
width: 200px;
}
.center {
margin: 0 200px;
}
五、最佳实践
- Flexbox 和 Grid 是最佳选择。
- 双飞翼布局是经典,但复杂。
- 避免使用负 margin 技巧。
10 CSS动画和过渡
CSS 提供了强大的动画和过渡能力。
面试回答:请解释 CSS transition 和 animation 的区别及用法
一、Transition(过渡) 在两个状态之间平滑地改变属性值。
.button {
background-color: blue;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: red;
}
transition-property: 属性名。transition-duration: 持续时间。transition-timing-function: 时间函数(ease, linear, cubic-bezier)。transition-delay: 延迟。
二、Animation(动画) 更复杂的动画,可以定义关键帧。
@keyframes slideIn {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
.element {
animation: slideIn 0.5s ease-out;
}
@keyframes: 定义关键帧。animation-name: 动画名称。animation-duration: 持续时间。animation-timing-function: 时间函数。animation-delay: 延迟。animation-iteration-count: 次数(infinite)。animation-direction: 方向。animation-fill-mode: 动画前后状态。animation-play-state: 暂停/运行。
三、区别
| 特性 | Transition | Animation |
|---|---|---|
| 触发方式 | 状态变化(如 hover) | 自动播放或通过类名控制 |
| 复杂度 | 简单,两个状态 | 复杂,多关键帧 |
| 控制 | 有限 | 精细控制(暂停、反向等) |
| 适用场景 | 简单悬停效果 | 复杂交互动画 |
四、性能优化
- 使用
transform和opacity,它们触发合成层,性能最好。 - 避免动画
top/left/width/height,会触发重排。
五、最佳实践
- 简单效果用
transition。 - 复杂动画用
animation。 - 结合 JavaScript 控制动画。
11 CSS3的新特性
CSS3 引入了许多新特性。
面试回答:请列举 CSS3 的主要新特性
一、选择器
- 属性选择器增强:
[att^="val"],[att$="val"],[att*="val"] - 伪类:
:nth-child(),:nth-of-type(),:not(),:focus-within - 伪元素:
::before,::after,::first-line,::selection
二、视觉效果
- 圆角:
border-radius - 阴影:
box-shadow,text-shadow - 渐变:
linear-gradient,radial-gradient - 透明度:
rgba(),hsla(),opacity - 背景:
background-size,background-origin,background-clip, 多背景
三、变形与动画
- 2D/3D Transform:
rotate,scale,translate,skew,matrix - Transitions
- Animations
四、布局
- Flexbox
- Grid Layout
- Multi-column layout
五、字体与文本
@font-face自定义字体text-overflowword-wrap,word-break
六、媒体查询
@media查询设备特性,实现响应式设计。
七、其他
calc()函数filter滤镜mask遮罩clip-path剪切路径will-change提示浏览器优化
八、最佳实践
- 使用 Autoprefixer 处理浏览器前缀。
- 关注浏览器兼容性。
- 逐步增强,优雅降级。
12 列举几个css中可继承和不可继承的元素
CSS 属性的继承性是样式传播的基础。
面试回答:请列举 CSS 中可继承和不可继承的属性
一、可继承的属性 通常与文本相关的属性具有继承性:
colorfont-family,font-size,font-weight,font-styleline-heighttext-aligntext-indentvisibilitycursorletter-spacing,word-spacinglist-style(及其子属性)
二、不可继承的属性 大多数盒模型、定位、背景相关的属性不继承:
width,height,margin,paddingborderdisplayposition,top,right,bottom,leftfloat,clearbackground(及其子属性)overflowz-indexbox-shadow
三、注意
visibility和cursor可继承,但overflow不可继承。border不可继承,但border-collapse和border-spacing在表格中可继承。- 使用
inherit关键字可以强制继承。
四、最佳实践
- 了解继承性有助于减少 CSS 代码量。
- 使用
initial或unset控制继承。
四、浏览器
1 浏览器架构
现代浏览器是复杂的软件系统。
面试回答:请简述现代浏览器的主要架构组件
现代浏览器主要由以下组件构成:
- 用户界面(User Interface):地址栏、前进/后退按钮、书签菜单等。
- 浏览器引擎(Browser Engine):在用户界面和渲染引擎之间传递指令。
- 渲染引擎(Rendering Engine):负责显示请求的内容(HTML、CSS、图像等)。如 Blink (Chrome)、WebKit (Safari)、Gecko (Firefox)。
- 网络(Networking):处理网络请求(HTTP/HTTPS)。
- JavaScript 引擎:解析和执行 JavaScript。如 V8 (Chrome)、SpiderMonkey (Firefox)、JavaScriptCore (Safari)。
- 数据存储(Data Storage):Cookie、LocalStorage、IndexedDB 等。
- UI 后端(UI Backend):绘制基础控件(组合框、窗口等),通常使用操作系统的 UI 接口。
- 显示控制器(Display Compositor):合成各个图层,生成最终画面。
多进程架构(Chrome)
- Browser Process:主进程,管理 UI、网络、存储等。
- Renderer Processes:渲染进程,每个 Tab 通常一个,运行渲染引擎和 JS 引擎,沙箱化。
- GPU Process:处理 GPU 任务。
- Plugin Processes:运行插件。
- Utility Processes:其他辅助任务。
优势:稳定性(一个页面崩溃不影响其他)、安全性(沙箱)、性能(并行)。
2 JavaScript单线程模型
JavaScript 为何是单线程的?
面试回答:为什么 JavaScript 是单线程的?
JavaScript 设计为单线程,主要原因如下:
- 简化编程模型:避免多线程带来的复杂性,如同步、锁、竞态条件等问题。
- 与 DOM 的互斥性:DOM 操作是非线程安全的。如果允许多线程操作 DOM,需要复杂的同步机制,会严重影响性能和开发体验。
- 事件驱动模型:通过事件循环(Event Loop)处理异步操作,既保证了单线程的简单性,又能实现非阻塞 I/O。
补充机制:
- Web Workers:在后台线程运行脚本,不阻塞主线程,但不能访问 DOM。
- SharedArrayBuffer:允许多线程共享内存,需配合
Atomics使用。
结论:单线程是 JavaScript 的核心设计决策,通过事件循环和 Web APIs 实现了高效的异步处理能力。
3 Chrome 打开一个页面需要启动多少进程?分别有哪些进程?
Chrome 采用多进程架构。
面试回答:Chrome 打开一个页面会启动哪些进程?
打开一个页面时,Chrome 通常会启动以下进程:
Browser Process (1个):主进程,负责:
- 浏览器 UI(地址栏、书签等)
- 网络请求
- 存储管理
- 创建/管理 Renderer 进程
Renderer Process (1个或多个):渲染进程,负责:
- 解析 HTML/CSS
- 执行 JavaScript
- 计算布局
- 绘制页面
- 每个 Tab 通常一个独立的 Renderer 进程,实现沙箱隔离。
GPU Process (1个):处理:
- 3D CSS
- WebGL
- 硬件加速的 UI 合成
网络进程 (1个):处理所有网络请求。
存储进程 (1个):管理磁盘存储(IndexedDB 等)。
特殊情况:
- 如果页面使用了
iframe,且cross-origin-isolated,可能会创建新的 Renderer 进程。 - 使用了特定插件(如 Flash)会启动 Plugin Process。
优势:
- 稳定性:一个页面崩溃不会导致整个浏览器崩溃。
- 安全性:沙箱机制限制恶意代码。
- 性能:并行处理,充分利用多核 CPU。
4 渲染机制
浏览器如何将 HTML、CSS、JS 转换为可视页面?
面试回答:请描述浏览器的渲染机制
浏览器的渲染机制是一个复杂的过程,主要包括以下步骤:
解析 HTML,构建 DOM 树
- 字节流 -> 字符串 -> Tokens -> Nodes -> DOM Tree。
解析 CSS,构建 CSSOM 树
- 类似 DOM 构建,生成 CSS Object Model。
合并 DOM 和 CSSOM,生成 Render Tree(渲染树)
- Render Tree 只包含需要渲染的节点(如不含
display: none的元素)。 - 每个节点包含样式信息。
- Render Tree 只包含需要渲染的节点(如不含
Layout(布局/重排)
- 计算每个节点在视口内的几何信息(位置、大小)。
- 从根节点开始,递归计算。
Paint(绘制/重绘)
- 将 Render Tree 的每个节点转换为屏幕上的像素。
- 通常划分为多个图层(Layers),分别绘制。
Composite(合成)
- 将各个图层按照正确的顺序合成,生成最终图像。
- 利用 GPU 加速,特别是对于
transform和opacity动画。
关键点:
- 重排(Reflow):Layout 阶段,成本高。
- 重绘(Repaint):Paint 阶段,成本较低。
- 合成(Compositing):成本最低,优先使用。
优化:
- 减少重排重绘。
- 使用
transform和opacity实现动画。 - 合理使用
will-change提示浏览器。
5 缓存机制
浏览器缓存是性能优化的核心。
面试回答:请解释浏览器的缓存机制
浏览器缓存分为强缓存和协商缓存。
一、强缓存(Force Cache) 不向服务器发送请求,直接使用本地缓存。
- Expires:HTTP/1.0,指定过期时间(绝对时间)。
Expires: Wed, 21 Oct 2025 07:28:00 GMT - Cache-Control:HTTP/1.1,更强大。
Cache-Control: max-age=3600 // 3600秒内有效 Cache-Control: no-cache // 不用强缓存,但可协商 Cache-Control: no-store // 不缓存 Cache-Control: public/private
二、协商缓存(Conditional Request) 向服务器发送请求,询问资源是否更新。
Last-Modified / If-Modified-Since
- 服务器返回
Last-Modified时间。 - 再次请求时,浏览器发送
If-Modified-Since。 - 服务器比较,未修改则返回 304 Not Modified。
- 服务器返回
ETag / If-None-Match
- 服务器为资源生成唯一标识 ETag。
- 再次请求时,浏览器发送
If-None-Match。 - 服务器比较 ETag,相同则返回 304。
优先级:Cache-Control > Expires;ETag > Last-Modified。
最佳实践:
- 静态资源使用
max-age配合版本号(如app.js?v=1.2.3)。 - 动态资源使用协商缓存。
- 使用 Service Worker 实现更复杂的缓存策略。
6 浏览器存储
前端有多种数据存储方式。
面试回答:请比较 Cookie、LocalStorage、SessionStorage、IndexedDB
| 特性 | Cookie | LocalStorage | SessionStorage | IndexedDB |
|---|---|---|---|---|
| 容量 | ~4KB | ~5MB | ~5MB | ~50% 磁盘空间 |
| 有效期 | 可设置过期时间 | 永久 | 会话期(关闭标签页清除) | 永久 |
| 发送到服务器 | 是(每次请求) | 否 | 否 | 否 |
| API | document.cookie | setItem/getItem | setItem/getItem | 异步 API |
| 数据类型 | 字符串 | 字符串 | 字符串 | 对象、二进制 |
| 作用域 | 同源,可设 path/domain | 同源 | 同源同标签页 | 同源 |
| 安全性 | 可设 HttpOnly, Secure | 仅客户端 | 仅客户端 | 仅客户端 |
应用场景:
- Cookie:身份认证(Token)、个性化设置。
- LocalStorage:用户偏好、持久化数据。
- SessionStorage:临时数据(如表单草稿)。
- IndexedDB:离线应用、大量结构化数据。
最佳实践:
- 避免在 Cookie 中存储敏感信息(HttpOnly)。
- 使用
try-catch处理存储异常。 - 关注隐私政策。
7 跨域方案
同源策略限制了跨域请求。
面试回答:有哪些解决跨域问题的方案?
一、CORS(Cross-Origin Resource Sharing) W3C 标准,最推荐。
- 简单请求:浏览器自动添加
Origin头,服务器返回Access-Control-Allow-Origin。 - 预检请求(Preflight):非简单请求(如 PUT、带自定义头)先发 OPTIONS 请求。
二、JSONP 利用 <script> 标签不受同源策略限制。
function handleResponse(data) {
console.log(data);
}
const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleResponse';
document.head.appendChild(script);
缺点:只支持 GET,安全性差。
三、代理服务器
- 开发环境:Webpack Dev Server 的
proxy。 - 生产环境:Nginx 反向代理。
四、PostMessage 用于不同窗口(iframe、window.open)间的通信。
// 父窗口
iframe.contentWindow.postMessage('data', 'https://target.com');
// 子窗口
window.addEventListener('message', (event) => {
if (event.origin !== 'https://parent.com') return;
console.log(event.data);
});
五、WebSocket 不受同源策略限制。
六、Node.js 中间层 用 Node.js 作为代理服务器转发请求。
七、最佳实践
- 生产环境首选 CORS。
- 开发环境使用代理。
- 避免 JSONP。
8 XSS 和 CSRF
前端安全两大威胁。
面试回答:请解释 XSS 和 CSRF 攻击及防御措施
一、XSS(跨站脚本攻击) 攻击者注入恶意脚本,在受害者浏览器执行。
类型:
- 存储型:恶意脚本存储在服务器(如评论)。
- 反射型:恶意脚本通过 URL 参数反射回来。
- DOM 型:通过 DOM 操作注入。
防御:
- 输入过滤:对用户输入进行转义(HTML Entity Encoding)。
- 输出编码:在输出到 HTML 时编码。
- Content Security Policy (CSP):限制可执行脚本的来源。
- HttpOnly Cookie:防止 JavaScript 访问 Cookie。
二、CSRF(跨站请求伪造) 攻击者诱导用户在已登录状态下执行非预期操作。
例子:用户登录银行后,访问恶意网站,自动提交转账请求。
防御:
- SameSite Cookie:
SameSite=Lax/Strict,防止跨站携带 Cookie。 - CSRF Token:服务器生成随机 Token,客户端提交时带上。
- 检查 Referer/Header:验证请求来源。
- 双重提交 Cookie:将 Token 同时放在 Cookie 和请求体中。
最佳实践:
- 服务器端做好输入验证和输出编码。
- 前端使用框架的安全特性(如 React 的自动转义)。
- 启用 CSP。
9 Service Worker
Service Worker 是一种可编程的网络代理。
面试回答:什么是 Service Worker?它的应用场景是什么?
一、定义 Service Worker 是一个在浏览器后台独立于网页运行的脚本,可拦截网络请求,实现离线缓存等功能。
二、特点
- 独立线程:不阻塞主线程。
- 可编程网络代理:可拦截和处理 fetch 请求。
- 生命周期:注册、安装、激活、终止。
- 作用域:绑定到特定路径。
- HTTPS:生产环境必须,开发环境 localhost 除外。
三、生命周期
- 注册:
navigator.serviceWorker.register('/sw.js') - 安装:
install事件,缓存静态资源。 - 激活:
activate事件,清理旧缓存。 - 运行:
fetch事件,拦截请求。
四、代码示例
// sw.js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/',
'/styles/main.css',
'/script/main.js'
]);
})
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys.filter(key => key !== 'v1')
.map(key => caches.delete(key))
);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
五、应用场景
- 离线应用:PWA 的核心。
- 资源缓存:提升加载速度。
- 消息推送:Push API。
- 后台同步:Sync API。
- 网络请求代理:实现自定义缓存策略。
六、最佳实践
- 处理好版本更新。
- 提供回退机制。
- 监控 Service Worker 状态。
10 DOM 节点操作
高效的 DOM 操作是前端性能的关键。
面试回答:有哪些常用的 DOM 节点操作方法?
一、创建节点
document.createElement(tagName)document.createTextNode(text)document.createDocumentFragment()
二、查询节点
document.getElementById(id)document.getElementsByTagName(tag)document.getElementsByClassName(class)document.querySelector(selector)document.querySelectorAll(selector)
三、添加节点
parentNode.appendChild(child)parentNode.insertBefore(newNode, referenceNode)
四、删除节点
parentNode.removeChild(child)child.remove()(现代)
五、替换节点
parentNode.replaceChild(newNode, oldNode)
六、修改节点
element.innerHTML/textContentelement.setAttribute(name, value)element.removeAttribute(name)element.className/classList
七、遍历节点
node.parentNodenode.childNodesnode.firstChild/lastChildnode.nextSibling/previousSibling
八、最佳实践
- 使用
querySelector系列方法。 - 使用
classList操作类名。 - 批量操作使用
DocumentFragment。 - 避免频繁的 DOM 访问。
11 掌握页面的加载过程
理解页面加载全过程对优化至关重要。
面试回答:请描述从输入 URL 到页面展示的完整过程
一、URL 解析
- 浏览器解析 URL,提取协议、域名、端口、路径等。
二、DNS 查询
- 将域名解析为 IP 地址。
- 浏览器缓存 -> 系统缓存 -> 路由器 -> ISP DNS -> 根 DNS。
三、建立 TCP 连接
- 三次握手。
四、发送 HTTP 请求
- 浏览器构建 HTTP 请求报文,通过 TCP 发送。
五、服务器处理请求
- 服务器收到请求,处理(可能查询数据库),生成响应。
六、接收 HTTP 响应
- 浏览器接收响应报文(HTML 内容)。
七、关闭 TCP 连接
- 四次挥手(如果 Connection: close)。
- HTTP/1.1 Keep-Alive 默认保持连接。
八、解析 HTML,构建 DOM 树
九、解析 CSS,构建 CSSOM 树
十、合并 DOM 和 CSSOM,生成 Render Tree
十一、Layout(布局)
十二、Paint(绘制)
十三、Composite(合成)
十四、JavaScript 执行
- 解析、编译、执行 JS。
- 可能修改 DOM/CSSOM,触发重排重绘。
十五、加载子资源
- 图片、CSS、JS、字体等,重复上述过程。
十六、页面渲染完成
DOMContentLoaded:DOM 构建完成。load:所有资源加载完成。
12 从输入URL到页面展示过程
同上一题。
13 渲染引擎什么情况下才会为特定的节点创建新的图层
图层(Layer)是合成的前提。
面试回答:渲染引擎在什么情况下会为节点创建新图层?
为了实现高效的合成(Compositing),渲染引擎会为以下情况的节点创建新图层:
- 根元素(
<html>) - 有明确合成属性的元素:
opacity小于 1transform不为nonefilter不为nonebackface-visibility为hiddenwill-change指定了transform、opacity等
- 有 3D 上下文的元素:
transform-style: preserve-3d - 视频元素(
<video>) - Canvas 元素
- 有 CSS 动画或过渡的元素
- 有
position: fixed的元素 - 有
overflow: scroll的元素(某些情况下) - 有
mix-blend-mode不为normal的元素 - 有
isolation为isolate的元素
优势:将这些元素提升到独立图层,其动画(如 transform、opacity)可以在 GPU 上独立合成,不触发重排重绘,性能极高。
注意:过多图层会增加内存和合成开销,应合理使用 will-change。
14 定时器与requestAnimationFrame、requestIdleCallback
不同的时间调度 API。
面试回答:请比较 setTimeout、setInterval、requestAnimationFrame、requestIdleCallback
一、setTimeout / setInterval
- 用途:延时或周期性执行代码。
- 精度:不精确,受事件循环和系统负载影响。
- 问题:
setInterval可能堆积回调。
二、requestAnimationFrame (rAF)
- 用途:为动画而生,在下一次重绘前执行。
- 执行时机:约 60fps(16.6ms 间隔),与显示器刷新率同步。
- 优势:性能好,自动节流(页面不可见时暂停)。
- 用法:
function animate() { // 更新动画 requestAnimationFrame(animate); } requestAnimationFrame(animate);
三、requestIdleCallback (rIC)
- 用途:在浏览器空闲时期执行低优先级任务。
- 执行时机:帧末,如果还有空闲时间。
- 优势:避免阻塞高优先级任务(如动画、输入)。
- 用法:
requestIdleCallback(deadline => { while (deadline.timeRemaining() > 0 && tasks.length > 0) { performTask(tasks.pop()); } });
四、比较
| API | 优先级 | 典型用途 |
|---|---|---|
| rAF | 高 | 动画、视觉更新 |
| setTimeout/setInterval | 中 | 一般定时任务 |
| rIC | 低 | 日志上报、预加载、非关键计算 |
最佳实践:
- 动画用
rAF。 - 高频事件用
setTimeout防抖/节流。 - 低优先级任务用
rIC。
五、框架通识
框架通识问题。
1 MVC/MVVM 模式
面试回答:请解释 MVC 和 MVVM 模式
MVC (Model-View-Controller)
- Model:数据和业务逻辑。
- View:用户界面。
- Controller:处理用户输入,协调 Model 和 View。
- 数据流:View -> Controller -> Model -> View (主动更新)。
MVVM (Model-View-ViewModel)
- Model:同上。
- View:同上。
- ViewModel:一个观察者,暴露数据和命令,自动同步 View 和 Model。
- 数据流:双向绑定,View 和 Model 通过 ViewModel 自动同步。
在前端框架中的应用:
- Angular:典型的 MVVM。
- Vue:MVVM 的变种,通过响应式系统实现数据绑定。
- React:更接近 MVVM,但强调单向数据流,需手动 setState。
优势:分离关注点,提高可维护性。
2 虚拟DOM的优势
面试回答:虚拟 DOM 有什么优势?
- 性能优化:通过 Diff 算法计算出最小变更,批量更新真实 DOM,减少昂贵的 DOM 操作。
- 跨平台:虚拟 DOM 是 JavaScript 对象,可渲染到不同环境(Web、Native、Canvas)。
- 声明式编程:开发者只需描述 UI 状态,框架负责更新。
- 可预测性:Diff 过程可控,便于调试和测试。
代价:增加了内存和计算开销,但对于复杂 UI,收益远大于成本。
3 前端路由原理
面试回答:前端路由是如何实现的?
一、Hash 路由
- 利用 URL 的
#后面的部分。 - 改变
location.hash不会刷新页面。 - 监听
hashchange事件。 - 优点:兼容性好。
- 缺点:URL 不美观。
二、History 路由
- 使用 HTML5 History API:
pushState、replaceState、popstate事件。 - 可以改变 URL 而不刷新页面。
- 优点:URL 美观。
- 缺点:需要服务器配置,避免 404。
三、实现原理
// Hash Router
window.addEventListener('hashchange', () => {
const path = location.hash.slice(1) || '/';
renderComponent(path);
});
// History Router
function navigate(path) {
history.pushState(null, '', path);
renderComponent(path);
}
window.addEventListener('popstate', () => {
renderComponent(location.pathname);
});
最佳实践:现代框架(React Router, Vue Router)都支持两种模式。
4 状态管理必要性
面试回答:为什么需要状态管理?
随着应用复杂度增加,组件间状态共享变得困难。
问题:
- Prop Drilling:状态需要通过多层组件传递。
- 状态分散:相同状态分布在多个组件。
- 难以维护:状态变更逻辑分散。
解决方案:
- 全局状态管理库:Redux、Vuex、Pinia。
- Context API(React)、Provide/Inject(Vue)。
优势:
- 单一数据源:状态集中管理。
- 可预测性:状态变更通过明确的 Action。
- 可调试:时间旅行调试。
- 可测试:纯函数 reducer。
最佳实践:并非所有应用都需要,小型应用可使用组件状态或 Context。
六、Vue
1 Vue 响应式原理
面试回答:请解释 Vue 2 和 Vue 3 的响应式原理
Vue 2
- 核心:
Object.defineProperty - 过程:
- 遍历
data对象的每个属性。 - 使用
Object.defineProperty将其转换为 getter/setter。 - 在 getter 中收集依赖(Watcher)。
- 在 setter 中通知依赖更新。
- 遍历
- 缺点:
- 无法检测属性的添加或删除。
- 数组索引修改、
length修改无法检测。 - 需要
Vue.set/$set。
Vue 3
- 核心:
Proxy - 过程:
- 使用
new Proxy(target, handler)代理整个对象。 get拦截中收集依赖。set、deleteProperty等拦截中触发更新。
- 使用
- 优点:
- 能监听对象/数组的添加、删除。
- 性能更好(不需要递归遍历)。
- 代码更简洁。
共同点:都使用发布-订阅模式,核心是依赖收集和派发更新。
2 发布订阅模式和观察者模式
面试回答:发布订阅模式和观察者模式的区别
观察者模式 (Observer Pattern)
- 定义:一对多的依赖关系,当一个对象(Subject)的状态改变时,所有依赖它的对象(Observer)都会得到通知。
- 实现:Subject 持有 Observer 列表,直接调用 Observer 的更新方法。
- 耦合度:较高,Subject 和 Observer 知道彼此。
发布订阅模式 (Publish-Subscribe Pattern)
- 定义:发布者(Publisher)将消息发布到事件通道(Event Channel),订阅者(Subscriber)从通道接收感兴趣的消息。
- 实现:通过一个中介(Event Bus/Message Broker)解耦发布者和订阅者。
- 耦合度:很低,发布者和订阅者互不知情。
在 Vue 中的应用:
- Vue 的响应式系统更接近观察者模式:Dep (Subject) 和 Watcher (Observer)。
- Vue 的事件总线(
$emit/$on)是发布订阅模式。
总结:发布订阅模式是观察者模式的变体,通过引入事件通道实现了更高的解耦。
3 为什么使用 Virtual DOM
面试回答:Vue 为什么要使用 Virtual DOM?
一、性能
- 直接操作真实 DOM 非常昂贵。
- Virtual DOM 是轻量的 JavaScript 对象,创建和修改成本低。
- 通过 Diff 算法计算出最小变更,批量更新真实 DOM,减少重排重绘。
二、跨平台
- Virtual DOM 可以渲染到不同环境(Web、Native、SSR)。
- Vue 可以用于 Weex、Uni-app 等跨平台框架。
三、声明式编程
- 开发者只需描述 UI 应该是什么样子(模板或 JSX)。
- 框架负责高效的更新过程。
四、可维护性
- 将复杂的 DOM 操作抽象为 Virtual DOM 的 diff 和 patch。
- 代码更清晰,易于调试。
权衡:增加了内存和计算开销,但对于复杂应用,整体性能更好。
4 VDOM:三个 part
面试回答:Virtual DOM 的三个核心部分是什么?
VDOM Tree (vnode):用 JavaScript 对象描述 DOM 结构。
{ tag: 'div', data: { class: 'container' }, children: [ { tag: 'span', text: 'Hello' } ] }Diff Algorithm (diff):比较新旧 VDOM 树,找出差异。
- Vue 采用同层比较,不跨层级移动。
- 通过 key 优化列表渲染。
Patch Function (patch):将差异应用到真实 DOM 上。
- 创建、删除、更新节点。
- 批量操作,减少重排。
流程:render() -> VDOM Tree -> diff(oldVdom, newVdom) -> patch(diff) -> Real DOM
5 vue 和 react技术选型
面试回答:Vue 和 React 如何选型?
选择 Vue 的场景:
- 快速开发:模板语法直观,学习曲线平缓。
- 渐进式:可逐步集成到现有项目。
- 团队规模小:约定优于配置,减少分歧。
- 企业级应用:Vue CLI、Vuex、Vue Router 生态成熟。
- 中文社区:国内资料丰富。
选择 React 的场景:
- 复杂大型应用:灵活性高,生态庞大。
- 跨平台:React Native、Electron。
- 创新项目:拥抱最新技术(Hooks、Concurrent Mode)。
- 团队技术强:需要更多架构决策。
- 国际化团队:全球社区活跃。
结论:两者都很优秀,选择应基于项目需求、团队熟悉度和技术栈。
6 nextTick
面试回答:Vue 的 nextTick 原理是什么?
一、问题 Vue 的数据更新是异步的。当你修改数据时,DOM 不会立即更新。
this.message = 'new';
console.log(this.$el.textContent); // 旧值
二、nextTick 的作用 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用它,获取更新后的 DOM。
this.message = 'new';
this.$nextTick(() => {
console.log(this.$el.textContent); // 新值
});
三、实现原理 利用 JavaScript 的事件循环机制,将回调函数延迟到“微任务”或“宏任务”队列。
- 优先使用 Promise.then()(微任务)
- 其次 MutationObserver(微任务)
- 再次 setImmediate(宏任务)
- 最后 setTimeout(fn, 0)(宏任务)
四、源码简化
const callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
let timerFunc;
if (typeof Promise !== 'undefined') {
timerFunc = () => {
Promise.resolve().then(flushCallbacks);
};
} else if (typeof MutationObserver !== 'undefined') {
// ... MutationObserver polyfill
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb) {
callbacks.push(cb);
if (!pending) {
pending = true;
timerFunc();
}
}
五、最佳实践
- 在数据更新后需要操作 DOM 时使用。
this.$nextTick或Vue.nextTick。
7 生命周期
面试回答:请描述 Vue 2 和 Vue 3 的生命周期钩子
Vue 2 生命周期
| 钩子 | 触发时机 |
|---|---|
beforeCreate | 实例初始化后,数据观测和事件配置之前 |
created | 实例创建完成,数据已观测,但 $el 未创建 |
beforeMount | 挂载开始,模板编译完成,但未挂载到 DOM |
mounted | 挂载完成,$el 被替换 |
beforeUpdate | 数据更新时,虚拟 DOM 重新渲染前 |
updated | 虚拟 DOM 重新渲染后 |
beforeDestroy | 实例销毁前 |
destroyed | 实例销毁后 |
activated | keep-alive 组件激活时 |
deactivated | keep-alive 组件停用时 |
Vue 3 Composition API (setup)
| 钩子 | 组合式 API |
|---|---|
beforeCreate / created | setup() |
beforeMount | onBeforeMount() |
mounted | onMounted() |
beforeUpdate | onBeforeUpdate() |
updated | onUpdated() |
beforeUnmount | onBeforeUnmount() |
unmounted | onUnmounted() |
activated | onActivated() |
deactivated | onDeactivated() |
最佳实践:
created/setup:发起网络请求。mounted:访问 DOM、第三方库初始化。beforeDestroy/onBeforeUnmount:清理定时器、事件监听器。
8 vue-router
面试回答:vue-router 的实现原理是什么?
一、核心
- 路由模式:Hash 模式、History 模式。
- 响应式:路由状态(
$route)是响应式的,路由变化时组件能自动更新。
二、Hash 模式
- 利用
location.hash和hashchange事件。 - 改变 hash 不会刷新页面。
- 优点:兼容性好。
- 缺点:URL 丑。
三、History 模式
- 使用
history.pushState()和popstate事件。 - 优点:URL 美观。
- 缺点:需要服务器配置(fallback 404 到 index.html)。
四、实现原理
class VueRouter {
constructor(options) {
this.mode = options.mode || 'hash';
this.routes = options.routes || [];
this.history = new HistoryMode(this);
}
init() {
if (this.mode === 'hash') {
window.addEventListener('hashchange', () => {
this.history.current = location.hash.slice(1) || '/';
});
} else {
window.addEventListener('popstate', () => {
this.history.current = location.pathname;
});
}
}
}
五、导航守卫
- 全局:
beforeEach,afterEach - 路由独享:
beforeEnter - 组件内:
beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave
六、最佳实践
- 使用 History 模式配合服务器配置。
- 合理使用导航守卫做权限校验。
- 路由懒加载。
9 vuex
面试回答:Vuex 的核心概念和工作原理
一、核心概念
- State:单一状态树,存储所有应用层级的状态。
- Getters:从 state 派生出状态,类似计算属性。
- Mutations:唯一修改 state 的地方,必须是同步函数。
- Actions:提交 mutations,可以包含异步操作。
- Modules:将 store 分割成模块。
二、工作原理
- 单向数据流:View -> Actions -> Mutations -> State -> View.
- 响应式:State 是响应式的,当 state 改变时,依赖它的组件会更新。
三、代码示例
const store = new Vuex.Store({
state: {
count: 0
},
getters: {
doubleCount: state => state.count * 2
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment');
}, 1000);
}
}
});
四、Vue 3 + Pinia
- Pinia 是新一代状态管理库,更简洁,基于 Composition API。
五、最佳实践
- 不要滥用 Vuex,小项目用 Provide/Inject 或 props。
- Mutation 必须同步。
- Action 处理异步。
- 使用模块化组织大型应用。
10 vue3带来的新特性/亮点
面试回答:Vue 3 有哪些新特性和改进?
- Composition API:更灵活的逻辑复用和代码组织。
- Performance:更快的渲染和更新。
- Tree-shaking:支持按需引入,减小打包体积。
- Better TypeScript Support:用 TS 重写,类型推断更好。
- Multiple v-models:一个组件可有多个 v-model。
- Teleport:将组件渲染到 DOM 树外的任意位置(如 Modal)。
- Fragments:组件可有多个根节点。
- Suspense:异步组件的加载状态管理。
- Global API Changes:从全局 API 改为可组合的函数。
- Proxy-based reactivity:更好的响应式系统。
Impact:提高了开发效率和应用性能,更适合大型项目。
11 Compositon api
面试回答:Composition API 解决了什么问题?
一、Options API 的痛点
- 逻辑分散:一个功能的逻辑分散在
data、methods、computed、watch等选项中。 - 复用困难:Mixin 可能导致命名冲突,缺乏类型支持。
二、Composition API 的优势
- 逻辑聚合:将同一个功能的代码组织在一起。
- 更好的复用:通过自定义 Hook (
useXXX) 复用逻辑。 - TypeScript 支持:类型推断更友好。
- 灵活的代码组织:不受选项限制。
三、代码示例
<script setup>
import { ref, computed, watch } from 'vue';
// 逻辑聚合
const count = ref(0);
const double = computed(() => count.value * 2);
watch(count, (newVal) => {
console.log('count changed:', newVal);
});
function increment() {
count.value++;
}
</script>
四、自定义 Hook
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0);
const y = ref(0);
function update(e) {
x.value = e.clientX;
y.value = e.clientY;
}
onMounted(() => {
window.addEventListener('mousemove', update);
});
onUnmounted(() => {
window.removeEventListener('mousemove', update);
});
return { x, y };
}
五、最佳实践
- 优先使用 Composition API。
- 创建可复用的自定义 Hook。
- 结合 TypeScript 使用。
12 computed 的实现原理
面试回答:Vue 的 computed 是如何实现的?
一、核心 Computed 是基于响应式系统和脏检查(Dirty Checking)实现的惰性求值。
二、实现原理
- 依赖收集:当访问
computed属性时,它会作为依赖被收集。 - 缓存:计算结果被缓存,直到其依赖发生变化。
- 派发更新:当依赖变化时,标记为“脏”,下次访问时重新计算。
三、源码简化
class ComputedRefImpl {
private _value;
private _dirty = true;
private effect;
constructor(getter) {
this.effect = new ReactiveEffect(getter, () => {
// 调度器:标记为脏,不立即执行
if (!this._dirty) {
this._dirty = true;
}
});
}
get value() {
// 依赖收集
track(this, TrackOpTypes.GET, 'value');
// 惰性求值
if (this._dirty) {
this._value = this.effect.run();
this._dirty = false;
}
return this._value;
}
}
四、与 watch 的区别
computed是惰性求值,有缓存,有返回值。watch是立即执行,无缓存,用于执行副作用。
五、最佳实践
- 用
computed替代模板中的复杂表达式。 - 避免在
computed中有副作用。
13 watch 的理解
面试回答:Vue 的 watch 和 watchEffect 有什么区别?
一、watch
- 惰性:默认在创建时不立即执行,除非
immediate: true。 - 精确性:需要指定侦听的来源(getter、ref、reactive)。
- 返回值:返回一个 stop 函数,用于停止侦听。
- 新旧值:可以获取变化前后的值。
const count = ref(0);
const stop = watch(count, (newVal, oldVal) => {
console.log(newVal, oldVal);
});
// stop(); // 停止侦听
二、watchEffect
- 立即执行:创建时立即执行一次。
- 自动追踪:自动追踪回调中使用的响应式数据,作为依赖。
- 无新旧值:不提供旧值。
const count = ref(0);
watchEffect(() => {
console.log(count.value); // 自动追踪 count
});
三、选择
- watch:需要知道旧值,或侦听特定来源。
- watchEffect:简单场景,自动追踪依赖。
四、最佳实践
- 用
watch处理复杂逻辑。 - 用
watchEffect处理简单副作用。
14 vue 渲染过程
面试回答:Vue 的完整渲染过程是怎样的?
- 初始化:创建 Vue 实例,合并选项。
- 响应式化:遍历
data,将其转换为响应式(Vue 2:defineProperty,Vue 3:Proxy)。 - 编译(仅非 Runtime 版本):将模板编译为
render函数。 - 挂载:调用
$mount。 - 首次渲染:
- 执行
render函数,生成 VDOM 树。 patch函数将 VDOM 转换为真实 DOM,插入页面。
- 执行
- 更新:
- 数据变化,触发 setter。
- 通知依赖(Watcher)更新。
- Watcher 重新执行
render函数,生成新 VDOM。 - Diff 新旧 VDOM。
- Patch 差异到真实 DOM。
- 销毁:调用
$destroy,清理事件、子组件、Watcher 等。
关键:render -> VDOM -> diff -> patch。
15 说一说keep-alive实现原理
面试回答:keep-alive 的实现原理是什么?
一、作用<keep-alive> 包裹的组件在切换时不会被销毁,而是被缓存,保留其状态。
二、实现原理
- 缓存机制:内部维护一个
cache对象,存储组件实例。 - LRU 策略:通过
max属性限制缓存数量,移除最近最少使用的组件。 - 组件切换:
- 进入:如果缓存中存在,直接激活(
activated钩子)。 - 离开:不销毁,而是缓存(
deactivated钩子)。
- 进入:如果缓存中存在,直接激活(
三、源码简化
export default {
name: 'KeepAlive',
abstract: true, // 抽象组件,不会渲染到 DOM
created() {
this.cache = Object.create(null);
},
mounted() {
// 监听缓存组件的 insert 钩子
},
updated() {
// 更新缓存
},
render() {
const slot = this.$slots.default;
const vnode = getFirstComponentChild(slot);
const key = vnode.key == null
? vnode.componentOptions.Ctor.cid + (vnode.componentOptions.tag || '')
: vnode.key;
if (this.cache[key]) {
// 命中缓存,直接使用
vnode.componentInstance = this.cache[key].componentInstance;
vnode.elm = this.cache[key].elm;
} else {
// 未命中,缓存
this.cache[key] = vnode;
// LRU 逻辑...
}
return vnode;
},
destroyed() {
// 清理缓存
for (const key in this.cache) {
pruneCacheEntry(this.cache, key);
}
}
};
四、最佳实践
- 用于需要保留状态的组件(如表单、列表)。
- 设置
max防止内存泄漏。 - 使用
include/exclude精确控制缓存。
16 为什么访问data属性不需要带data
面试回答:为什么在模板中可以直接访问 data 里的属性?
因为在 Vue 实例初始化时,会将 data 对象的属性代理到实例上。
一、实现原理
- 数据代理:通过
Object.defineProperty将data的属性代理到vm上。 - 访问时:
vm.a实际访问的是vm._data.a。
二、Vue 2 代码示意
function proxy(vm, source, key) {
Object.defineProperty(vm, key, {
get() {
return vm[source][key];
},
set(newVal) {
vm[source][key] = newVal;
}
});
}
// 在 initState 中
const data = vm.$options.data;
for (const key in data) {
proxy(vm, '_data', key);
}
三、好处
- 模板中使用
{{ a }}比{{ _data.a }}更简洁。 this.a在 methods 中也能直接访问。
四、注意
data中不能有与 Vue 实例属性(如$data,$el)同名的属性。
17 template预编译是什么
面试回答:Vue 的模板预编译是什么?
一、问题 在浏览器中直接使用模板(<template>)需要在运行时编译,消耗性能。
二、解决方案:预编译 在构建时(如 Webpack)将 Vue 模板提前编译为 render 函数。
三、好处
- 性能:运行时无需编译,启动更快。
- 体积:可以使用 Runtime-only 版本的 Vue,更小。
- 错误提示:编译时就能发现模板错误。
四、实现
- vue-loader:Webpack loader,将
.vue文件中的<template>编译为render函数。 - vue-template-compiler:编译模板的包。
五、代码示例
<!-- 模板 -->
<template>
<div>{{ msg }}</div>
</template>
// 编译后
render: function() {
with(this) {
return createElement('div', [msg])
}
}
六、最佳实践
- 项目中总是使用预编译。
- 使用 Vue CLI 或 Vite 等工具自动处理。
18 介绍一下Vue中的Diff算法
面试回答:Vue 的 Diff 算法是如何工作的?
Vue 的 Diff 算法基于同层比较和双端比较,旨在高效地更新列表。
一、假设
- 跨层级移动很少:只比较同层节点。
- 列表项有唯一 key:通过 key 识别节点。
二、Diff 过程
- 同层比较:只比较新旧 VDOM 树的同一层级。
- 双端比较(Vue 2.0+):
- 定义四个指针:旧头、旧尾、新头、新尾。
- 比较
oldHeadvsnewHead,oldTailvsnewTail,oldHeadvsnewTail,oldTailvsnewHead。 - 根据 key 和标签匹配,移动指针。
- 未匹配的节点进行创建或删除。
三、key 的重要性
key帮助 Diff 算法识别节点的身份。- 没有
key或使用index作为key会导致错误的复用。
四、与 React 的区别
- Vue 默认尝试就地更新,React 要求必须有
key。 - Vue 的双端比较在某些场景下更高效。
五、最佳实践
- 列表渲染时务必使用
key。 key应稳定、可预测、唯一(避免Math.random())。
19 说说Vue2.0和Vue3.0有什么区别
面试回答:Vue 2 和 Vue 3 的主要区别是什么?
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式 | Object.defineProperty | Proxy |
| API 风格 | Options API | Composition API |
| 构建工具 | Vue CLI | Vite (推荐) |
| TypeScript | 支持 | 重写,更好支持 |
| Tree-shaking | 不支持 | 支持 |
| 性能 | 较好 | 更好(更快的 diff) |
| 体积 | 较大 | 更小(按需引入) |
| Fragment | 不支持 | 支持(多根节点) |
| Teleport | 无 | 支持 |
| Suspense | 无 | 支持 |
| 全局 API | Vue.* | createApp |
| 状态管理 | Vuex | Pinia (推荐) |
总结:Vue 3 是一次重大升级,提供了更好的性能、更小的体积、更强的 TypeScript 支持和更灵活的代码组织方式。
七、React
0 对虚拟DOM的理解
面试回答:你对虚拟 DOM 的理解是什么?
虚拟 DOM(Virtual DOM)是真实 DOM 的轻量级 JavaScript 对象表示。
核心思想:
- 状态 -> UI:UI 是状态的函数。
- 不可变:每次状态变化,生成一个新的 VDOM 树。
- Diff & Patch:通过高效的 Diff 算法比较新旧 VDOM 树,计算出最小变更集,然后批量更新真实 DOM。
优势:
- 性能:减少昂贵的真实 DOM 操作。
- 跨平台:VDOM 可以渲染到不同宿主环境(Web、Native、Canvas)。
- 声明式:开发者只需描述 UI 应该是什么,框架负责如何更新。
- 可测试性:VDOM 是普通对象,易于单元测试。
代价:增加了内存和计算开销,但对于复杂 UI,总体性能更好。
1 谈谈你对React的理解
面试回答:谈谈你对 React 的理解
React 是一个用于构建用户界面的声明式、组件化、基于虚拟 DOM 的 JavaScript 库。
核心理念:
- 组件化:UI 被拆分为独立、可复用的组件。
- 单向数据流:数据从父组件流向子组件(props),通过回调函数向上传递事件。
- 状态驱动视图:UI 是状态的函数,状态变化,UI 自动更新。
- 虚拟 DOM:高效的更新机制。
- 函数式编程:鼓励使用纯函数和不可变数据。
生态系统:
- React DOM:渲染到 Web。
- React Native:渲染到移动原生。
- 状态管理:Redux、Context API。
- 路由:React Router。
- 构建工具:Create React App、Next.js。
优势:灵活、强大、社区庞大、跨平台。
挑战:学习曲线陡峭,需要做更多架构决策。
2 如何避免React生命周期中的坑
面试回答:React 类组件生命周期有哪些常见坑?如何避免?
一、常见坑
- componentWillReceiveProps:容易滥用,逻辑复杂。
- componentWillUpdate:不适合做副作用。
- setState 异步:连续调用可能合并,
this.state不是最新。 - 内存泄漏:未清理定时器、事件监听器。
- 错误边界:未捕获所有错误。
二、避免方法
- 使用 Hooks:函数组件和 Hooks 更简单、更安全。
- useEffect:取代大部分生命周期。
- useRef:获取最新的 state/props。
- 清理副作用:在
useEffect的返回函数中清理。 - 错误边界:使用
componentDidCatch捕获子组件错误。
三、React 16+ 新生命周期
getDerivedStateFromProps:取代componentWillReceiveProps。getSnapshotBeforeUpdate:在 DOM 更新前捕获信息。
四、最佳实践
- 优先使用函数组件和 Hooks。
- 使用 ESLint 插件检测潜在问题。
- 合理使用错误边界。
3 React Fiber架构
面试回答:React Fiber 是什么?它解决了什么问题?
一、问题 React 15 的渲染是同步的,一旦开始,就不能中断。对于复杂组件树,可能导致主线程长时间阻塞,页面卡顿。
二、Fiber 的解决方案 Fiber 是 React 16 引入的可中断、可分片的渲染架构。
核心思想:
- 将组件树分解为 Fiber 节点:每个 Fiber 节点代表一个工作单元。
- 增量渲染:在每一帧中,只完成一部分工作,然后检查是否有更高优先级的任务(如用户输入)。
- 优先级调度:高优先级更新(如用户输入)可以打断低优先级更新(如动画)。
- 双缓冲:维护两棵 Fiber 树(current 和 workInProgress),完成后再切换。
三、好处
- 响应性:高优先级任务能及时响应。
- 可中断:渲染过程可暂停。
- 并发模式(Concurrent Mode):实验性功能,如
startTransition、useDeferredValue。
四、对开发者的影响
- 更流畅的用户体验。
- 新的 API:
useTransition、useDeferredValue。 - 需要关注更新的优先级。
4 createElement过程
面试回答:React.createElement 的作用和过程是什么?
一、作用createElement 是 JSX 编译后的产物,用于创建 React 元素(React Element)。
二、JSX 到 createElement
// JSX
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
// 编译后
React.createElement(
MyButton,
{ color: 'blue', shadowSize: 2 },
'Click Me'
);
三、createElement 函数
function createElement(type, config, children) {
const props = { ...config };
// 处理 children
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
props.children = Array.prototype.slice.call(arguments, 2);
}
return {
$$typeof: REACT_ELEMENT_TYPE,
type,
key: config.key || null,
ref: config.ref || null,
props
};
}
四、React Element
- 是一个普通的 JavaScript 对象,描述了应该渲染什么。
- 不是真实的 DOM 节点。
- 包含
type、props、key、ref。
五、后续
ReactDOM.render接收 React Element,将其转换为真实 DOM。
5 调和阶段 setState内部干了什么
面试回答:React 的调和阶段,setState 内部发生了什么?
一、setState 触发
this.setState({ count: this.state.count + 1 });
二、内部过程
- 合并状态:将新状态与旧状态合并(浅合并)。
- 标记更新:标记组件为“需要更新”。
- 调度更新:将更新加入调度队列。
- Fiber Reconciliation:
- 从根节点开始,遍历组件树。
- 对于每个组件,调用
render方法,生成新的 React Element。 - 比较新旧 VDOM(Diff 算法)。
- 计算出需要对真实 DOM 进行的操作(增删改)。
- Commit 阶段:将计算出的 DOM 操作批量应用到真实 DOM。
三、批处理(Batching)
- 多个
setState调用可能被合并为一次更新。 - 在事件处理函数中是批处理的。
- 在
setTimeout或原生事件中不是批处理的。
四、性能
- 调和是 React 最昂贵的操作。
- 通过 Fiber 架构优化。
6 setState
面试回答:setState 是同步还是异步的?
setState 的表现是异步的,但其内部机制更复杂。
一、异步表现
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 旧值
二、批处理
- 在 React 事件处理函数中,多个
setState会被合并为一次更新。 - 避免不必要的渲染。
三、同步情况
- 在
setTimeout、原生事件、Promise回调中,setState是同步的。 - 因为不在 React 的控制范围内。
四、获取更新后的值
- 回调函数:
setState(updater, callback) - useEffect(函数组件)
- componentDidUpdate(类组件)
五、函数式 setState
this.setState((prevState, props) => ({
count: prevState.count + 1
}));
- 确保基于最新状态。
六、最佳实践
- 使用函数式
setState处理异步更新。 - 不要依赖
this.state立即更新。
7 setState原理分析
面试回答:请深入分析 setState 的原理
一、源码层面
setState是Component类上的方法。- 调用
this.updater.enqueueSetState(this, partialState)。
二、Updater
enqueueSetState将更新放入队列。- 触发调度。
三、事务机制(旧)
- React 15 使用事务(Transaction)包装生命周期和事件处理。
- 在
close阶段批量更新。
四、Fiber 调度(新)
- 更新被调度到
Scheduler。 Scheduler根据优先级执行。
五、批处理实现
- 维护一个
isBatchingUpdates标志。 - 在 React 事件中,标志为
true,setState只是排队。 - 事件结束,标志为
false,执行批处理。
六、总结setState 的“异步”本质上是 React 为了优化性能而实现的批处理机制。
8 React事务机制
面试回答:React 的事务机制是什么?
一、定义 事务(Transaction)是 React 15 中的一种机制,用于在函数执行前后执行一些“wrapper”(包装器)。
二、目的
- 批处理:将多个
setState合并。 - 错误边界:捕获生命周期中的错误。
- 性能监控。
三、原理
- initialize:执行所有 wrapper 的 initialize 方法。
- perform:执行核心函数。
- close:执行所有 wrapper 的 close 方法。
四、例子
const transaction = new Transaction([FLUSH_BATCHED_UPDATES]);
transaction.perform(coreFn);
五、现状
- React 16+ 用 Fiber 调度取代了事务机制。
- 事务不再是公开的 API。
9 React组件和渲染更新过程
面试回答:React 的组件和渲染更新过程是怎样的?
一、初始化
- 创建 React 元素(
createElement)。 ReactDOM.render(element, container)。- 调和(Reconciler)遍历元素树,创建 Fiber 节点。
- 渲染(Renderer)将 Fiber 节点转换为真实 DOM。
二、更新
- 状态变化(
setState、useState)。 - 标记组件为 dirty。
- 调度更新(Scheduler)。
- 调和阶段(Reconciliation):
- 生成新的 VDOM。
- Diff 算法比较新旧 VDOM。
- 计算出 DOM 操作。
- 提交阶段(Commit):
- 执行 DOM 操作。
- 调用生命周期(
componentDidUpdate、useEffect)。
三、卸载
- 组件被移除。
- 调用
componentWillUnmount/useEffectcleanup。 - 清理事件、定时器。
关键:state/props change -> re-render -> diff -> commit.
10 如何解释 React 的渲染流程
同上一题。
11 diff算法是怎么运作
面试回答:React 的 Diff 算法是如何运作的?
React 的 Diff 算法基于两个假设,使其能够在 O(n) 时间内完成:
- 不同类型元素产生不同的树:如果元素类型不同,直接销毁旧树,创建新树。
- 列表项有稳定的 key:通过
key属性识别列表项的身份。
算法步骤:
- 同层比较:只比较同一层级的节点,不跨层级。
- 树的比较:
- 类型不同:销毁旧节点及其子树,创建新节点。
- 类型相同(DOM 元素):更新属性,递归比较子节点。
- 类型相同(组件):判断是否为同一类型组件,是则更新 props,否则重新挂载。
- 列表的比较:
- 没有
key:按索引比较,可能导致错误的复用。 - 有
key:React 会维护一个key到节点的映射,尽量复用节点。
- 没有
举例:
// 旧
<ul>
<li>A</li>
<li>B</li>
</ul>
// 新
<ul>
<li>C</li>
<li>A</li>
<li>B</li>
</ul>
- 没有
key:A->C, B->A, 新增 B。错误! - 有
key:新增 C,A 和 B 复用。正确!
最佳实践:始终为列表项提供稳定、唯一的 key。
12 合成事件原理
面试回答:React 的合成事件(Synthetic Event)是什么?
一、定义 合成事件是 React 实现的一套跨浏览器的事件系统,是对原生事件的包装。
二、为什么需要
- 浏览器兼容性:抹平不同浏览器事件 API 的差异。
- 事件委托:将所有事件绑定到
document,提升性能。 - 性能:避免为每个元素绑定原生事件。
三、工作原理
- 事件委托:React 在
document上绑定一个事件处理器。 - 事件池:为了性能,React 会重用事件对象,清空其属性。
- 异步访问:在
setTimeout中访问事件属性可能为null,需调用event.persist()。
四、事件流
- React 事件是冒泡的。
- 捕获事件以
onXXXCapture命名(如onClickCapture)。
五、与原生事件的区别
- React 事件名是驼峰命名(
onClick)。 - 事件对象是
SyntheticEvent,不是原生事件。
六、最佳实践
- 使用 React 事件。
- 避免混用原生事件。
- 如需异步访问,调用
persist()。
13 JSX语法糖本质
面试回答:JSX 的本质是什么?
JSX 是 JavaScript 的语法扩展,其本质是 React.createElement() 的语法糖。
一、编译过程
// JSX
<MyButton color="blue">Click Me</MyButton>
// 编译后
React.createElement(
MyButton,
{ color: 'blue' },
'Click Me'
);
二、规则
- 首字母大写:认为是 React 组件。
- 首字母小写:认为是 HTML 标签。
- 必须有一个根元素。
三、Babel 转换
@babel/preset-react将 JSX 转换为createElement调用。
四、优势
- 语法直观,类似 HTML。
- 在 JavaScript 中编写 UI。
- 类型检查(TSX)。
五、注意
- JSX 不是字符串,也不是 HTML。
- 需要编译。
14 为什么 React 元素有一个 typeof 属性
面试回答:为什么 React 元素有一个 $$typeof 属性?
$$typeof 属性是 React 用来标识一个对象是 React 元素的符号。
一、目的
- 安全:防止 XSS 攻击。
- 类型检查:在
createElement和cloneElement时快速验证。
二、实现
$$typeof的值是一个唯一的 Symbol。- 例如:
Symbol.for('react.element')。
三、为什么用 Symbol
- 无法被伪造。
- 即使攻击者构造了一个带有
$$typeof的对象,也无法拥有相同的 Symbol。
四、示例
const element = {
$$typeof: Symbol.for('react.element'),
type: 'div',
props: { children: 'Hello' }
};
五、重要性
- 是 React 内部安全机制的一部分。
15 Virtual DOM 的工作原理是什么
同“对虚拟DOM的理解”。
16 React有哪些优化性能的手段
面试回答:React 有哪些性能优化手段?
- 使用生产版本:Development 版本有额外检查。
- 代码分割:
React.lazy+Suspense按需加载。 - Memoization:
React.memo:记忆组件,避免不必要的重渲染。useMemo:记忆计算结果。useCallback:记忆函数。
- 避免在渲染中创建函数/对象:在组件外或
useMemo/useCallback中创建。 - 列表优化:为
map提供稳定、唯一的key。 - 使用 Fragment:避免不必要的包裹 div。
- 懒加载:
React.lazy。 - 服务端渲染(SSR):Next.js。
- 并发模式:
startTransition、useDeferredValue处理紧急和非紧急更新。 - 分析工具:React DevTools Profiler。
最佳实践:先测量,再优化。不要过早优化。
17 Redux实现原理解析
面试回答:Redux 的实现原理是什么?
Redux 是一个可预测的状态管理容器。
核心原则:
- 单一数据源:整个应用的状态存储在单一的 store 中。
- State 是只读的:不能直接修改,只能通过 dispatch action。
- 使用纯函数(Reducer)来执行修改:Reducer 根据当前 state 和 action,返回新的 state。
三大组成部分:
- Store:通过
createStore(reducer)创建。 - Action:描述“发生了什么”的普通对象,必须有
type字段。 - Reducer:纯函数
(state, action) => newState。
工作流程:
- 调用
store.dispatch(action)。 - Store 调用传入的 Reducer 函数。
- Reducer 返回新的 state。
- Store 保存新的 state。
- 触发所有订阅的监听器。
中间件(Middleware):
- 如
redux-thunk,允许 action creator 返回函数,处理异步。 - 位于
dispatch和 Reducer 之间。
Connect:
- React-Redux 的
connect函数,将 store 的 state 和 dispatch 注入组件。
Hook:useSelector、useDispatch。
18 谈谈你对状态管理的理解
面试回答:谈谈你对前端状态管理的理解
状态管理是管理应用中数据流动和共享的机制。
状态类型:
- UI 状态:加载、错误、表单。
- 业务状态:用户信息、购物车。
- 服务器状态:缓存、同步。
管理方式:
- 组件状态:
useState、this.state。适合局部状态。 - Context API:跨层级传递状态。适合中等复杂度。
- 全局状态库:Redux、MobX、Vuex。适合复杂应用。
核心挑战:
- 共享:多个组件需要同一份数据。
- 一致性:数据变更时,所有依赖同步更新。
- 可预测性:状态变更可追溯、可调试。
趋势:
- 原子化状态:Jotai、Recoil。
- 服务即状态:TanStack Query、RTK Query。
- Zustand:简单、灵活。
选择:根据应用复杂度选择,避免过度设计。
19 connect组件原理分析
面试回答:React-Redux 的 connect 原理是什么?
connect 是一个高阶组件(HOC),用于连接 React 组件和 Redux store。
一、签名
connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)
二、原理
- 订阅 store:
connect内部调用store.subscribe。 - 获取 state:在
mapStateToProps中读取 store 的 state。 - 分发 action:在
mapDispatchToProps中包装dispatch。 - 合并 props:将
mapStateToProps、mapDispatchToProps和组件自身的props合并。 - 注入 props:将合并后的 props 传递给被包裹的组件。
- 性能优化:使用
shallowEqual比较 props,避免不必要的重渲染。
三、代码简化
function connect(mapStateToProps, mapDispatchToProps) {
return function(WrappedComponent) {
class Connect extends Component {
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
this.forceUpdate();
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const stateProps = mapStateToProps
? mapStateToProps(store.getState(), this.props)
: {};
const dispatchProps = mapDispatchToProps
? mapDispatchToProps(store.dispatch, this.props)
: { dispatch: store.dispatch };
const mergedProps = { ...this.props, ...stateProps, ...dispatchProps };
return <WrappedComponent {...mergedProps} />;
}
}
return Connect;
};
}
四、现状
connect仍然可用。- 推荐使用 Hook:
useSelector和useDispatch,更简洁。
20 React Hooks
面试回答:React Hooks 解决了什么问题?有哪些核心 Hook?
一、解决的问题
- 逻辑复用:类组件的
render props和HOC有嵌套地狱问题。 - 复杂组件:生命周期中混杂不同逻辑。
- 类组件心智负担:
this、绑定、生命周期理解。
二、核心 Hook
- useState:状态管理。
- useEffect:副作用(数据获取、订阅、手动 DOM 操作)。
- useContext:访问 Context。
- useReducer:复杂 state 逻辑。
- useCallback:记忆函数。
- useMemo:记忆计算结果。
- useRef:访问 DOM 或保存可变值。
三、规则
- 只在最顶层使用 Hook。
- 只在 React 函数组件或自定义 Hook 中调用。
四、自定义 Hook
- 以
use开头的函数。 - 封装可复用的逻辑。
五、优势
- 更简单的逻辑复用。
- 更好的代码组织。
- 函数组件成为首选。
21 受控组件和非受控组件
面试回答:受控组件和非受控组件的区别?
受控组件 (Controlled Components)
- 定义:表单元素的值由 React 组件的 state 控制。
- 实现:通过
value(或checked)属性设置值,通过onChange事件更新 state。 - 优点:数据单一来源,易于验证和转换。
- 缺点:需要为每个元素编写事件处理器。
function Form() {
const [name, setName] = useState('');
return (
<input value={name} onChange={(e) => setName(e.target.value)} />
);
}
非受控组件 (Uncontrolled Components)
- 定义:表单元素的值由 DOM 自身存储。
- 实现:使用
ref获取 DOM 元素的值。 - 优点:集成简单,性能好(无需实时更新 state)。
- 缺点:数据分散,不易验证。
function Form() {
const inputRef = useRef();
const handleSubmit = () => {
console.log(inputRef.current.value);
};
return (
<input ref={inputRef} />
);
}
选择:
- 受控:需要实时验证、动态控制、复杂逻辑。
- 非受控:简单表单、文件上传、性能敏感。
22 如何避免ajax数据请求重新获取
面试回答:如何避免组件重新渲染时重复发起 AJAX 请求?
一、问题 在 useEffect 中发起请求,如果组件因状态变化而重渲染,useEffect 可能再次执行。
二、解决方案
依赖数组为空:确保
useEffect只在挂载时执行。useEffect(() => { fetchData(); }, []); // 空依赖避免在依赖中包含对象/函数:使用
useMemo/useCallback。const memoizedCallback = useCallback(() => { fetchData(); }, []); useEffect(() => { memoizedCallback(); }, [memoizedCallback]);使用 ref:存储请求状态。
const hasFetched = useRef(false); useEffect(() => { if (!hasFetched.current) { fetchData(); hasFetched.current = true; } }, []);状态管理库:将数据放在 Redux、Zustand 等全局状态中,避免组件重复请求。
服务即状态:使用 TanStack Query、SWR 等库,它们自动处理缓存、去重。
三、最佳实践
- 优先使用专门的数据请求库(SWR、TanStack Query)。
- 合理使用
useEffect依赖。
23 组件之间通信
面试回答:React 组件之间有哪些通信方式?
- 父组件 -> 子组件:通过
props传递数据。 - 子组件 -> 父组件:通过
props传递回调函数。 - 兄弟组件:
- 通过共同的父组件中转(提升状态)。
- 使用 Context API。
- 使用全局状态管理(Redux)。
- 任意组件:
- Event Bus:自定义事件系统(不推荐,难维护)。
- 状态管理库:Redux、Zustand。
- Context API。
最佳实践:
- 优先使用
props和回调。 - 中等复杂度用 Context。
- 复杂应用用状态管理库。
24 类组件与函数组件有什么区别呢?
面试回答:类组件和函数组件的区别?
| 特性 | 类组件 | 函数组件 |
|---|---|---|
| 语法 | ES6 Class | 函数 |
| 状态 | this.state / setState | useState |
| 生命周期 | componentDidMount 等 | useEffect |
| this | 有 this | 无 this |
| 性能 | 稍重(实例化) | 轻量 |
| 逻辑复用 | HOC、Render Props | Hooks |
| 未来 | 支持,但不推荐 | 推荐 |
结论:函数组件 + Hooks 是现代 React 开发的首选。
25 如何设计React组件
面试回答:如何设计一个好的 React 组件?
- 单一职责:一个组件只做一件事。
- 可复用:设计为通用组件。
- 可组合:支持嵌套和扩展。
- Props 设计:
- 明确的 API。
- 使用
PropTypes或 TypeScript。 - 合理的默认值。
- 状态提升:将共享状态提到共同的父组件。
- 使用 Hooks:封装逻辑。
- 性能:合理使用
React.memo、useMemo、useCallback。 - 无障碍:支持 ARIA。
- 测试:易于单元测试。
原则:DRY、KISS、YAGNI。
26 组件的协同及(不)可控组件
同“受控组件和非受控组件”。
27 React-Router 的实现原理及工作方式分别是什么
面试回答:React-Router 的实现原理是什么?
React-Router 是一个声明式的路由库。
一、核心组件
BrowserRouter/HashRouter:路由模式。Route:定义路由规则。Link:声明式导航。Switch/Routes:只渲染第一个匹配的 Route。Redirect/Navigate:重定向。
二、实现原理
- 路由模式:
- HashRouter:基于
hashchange事件。 - BrowserRouter:基于
history.pushState和popstate。
- HashRouter:基于
- 上下文(Context):
Router组件通过 Context 提供location、history。 - Route 匹配:
Route组件订阅location变化,根据path匹配。 - 渲染:匹配时渲染
component、render或children。
三、动态路由
useParams:获取动态参数。useLocation:获取 location。useHistory/useNavigate:编程式导航。
四、懒加载
React.lazy+Suspense。
五、最佳实践
- 使用
Routes替代Switch。 - 使用
useNavigate进行编程式导航。 - 合理使用
exact。
28 React 17 带来了哪些改变
面试回答:React 17 有哪些新特性?
React 17 是一个“无新功能”的版本,主要目的是使升级更容易。
主要变化:
- 事件委托变更:事件不再绑定到
document,而是绑定到渲染容器(root)。方便多版本共存。 - 新的 JSX 转换:
import {jsx} from 'react/jsx-runtime',不再需要React在作用域内。支持Fragment语法。 - 废弃生命周期:
componentWillMount、componentWillReceiveProps、componentWillUpdate被标记为 UNSAFE。 - 更平滑的错误处理:错误边界捕获范围更精确。
影响:
- 降低了升级到 React 18 的难度。
- JSX 转换让代码更简洁。
八、性能
1 DNS 预解析
面试回答:什么是 DNS 预解析?如何使用?
一、定义 DNS 预解析(DNS Prefetching)是浏览器在后台提前解析域名,为后续请求节省时间。
二、使用方法
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//cdn.example.com">
三、优势
- 减少 DNS 查询延迟。
- 提升资源加载速度。
四、注意
- 只对跨域资源有效。
- 不要预解析太多,可能浪费带宽。
五、最佳实践
- 为关键的第三方资源(字体、CDN)预解析。
- 结合
preconnect使用。
2 缓存
同“浏览器缓存机制”。
3 使用 HTTP / 2.0
面试回答:HTTP/2 有哪些特性?如何利用?
一、HTTP/2 特性
- 二进制分帧:将消息分解为帧,交错传输。
- 多路复用:一个连接上并行传输多个请求/响应,解决队头阻塞。
- 头部压缩(HPACK):减少头部大小。
- 服务器推送:服务器主动推送资源。
- 优先级和依赖:客户端可指定请求优先级。
二、利用方式
- 开启 HTTPS:HTTP/2 通常需要 TLS。
- 合并资源:HTTP/2 下,合并文件(如 bundle)的收益减少,甚至可能有害。
- 利用服务器推送:谨慎使用,可能造成资源浪费。
- 合理设置优先级。
三、最佳实践
- 优先升级到 HTTP/2。
- 评估资源合并策略。
- 监控服务器推送的效果。
4 预加载
面试回答:预加载(preload)和预获取(prefetch)的区别?
一、rel="preload"
- 目的:告诉浏览器立即加载某个关键资源。
- 资源类型:
as="style"、as="script"、as="font"、as="image"等。 - 用法:
<link rel="preload" href="/critical.css" as="style"> <link rel="preload" href="/hero.jpg" as="image"> - 特点:高优先级,立即下载,不执行。
二、rel="prefetch"
- 目的:告诉浏览器在空闲时预取可能需要的资源(通常是下一页)。
- 用法:
<link rel="prefetch" href="/next-page.html"> - 特点:低优先级,空闲时下载,不执行。
三、区别
| 特性 | preload | prefetch |
|---|---|---|
| 优先级 | 高 | 低 |
| 触发时机 | 立即 | 空闲时 |
| 用途 | 关键资源 | 下一页资源 |
| 资源类型 | 指定 as | 通常是文档 |
四、最佳实践
preload用于 above-the-fold 资源。prefetch用于用户可能访问的下一页。
5 预渲染
面试回答:什么是预渲染?有哪些方式?
一、定义 预渲染是在用户访问前,预先生成页面的 HTML。
二、方式
- 静态站点生成(SSG):构建时生成所有页面(如 Gatsby、Next.js)。
- 服务端渲染(SSR):请求时在服务器生成 HTML(如 Next.js SSR)。
- Prerendering:使用爬虫访问并缓存页面(如 Prerender.io)。
三、优势
- SEO:搜索引擎能看到完整内容。
- 首屏性能:用户看到 HTML 更快。
- 社交分享:卡片信息正确。
四、最佳实践
- SSG 适合内容相对静态的网站。
- SSR 适合动态内容。
- 避免滥用,增加服务器压力。
6 懒执行与懒加载
面试回答:懒加载和懒执行的区别?
懒加载(Lazy Loading)
- 资源:图片、组件、路由。
- 目的:延迟加载非关键资源,减少初始负载。
- 实现:
- 图片:
loading="lazy"。 - 组件:
React.lazy。 - 路由:
<Route lazy />(React Router 6.4+)。
- 图片:
懒执行(Lazy Execution)
- 代码:函数、计算。
- 目的:延迟执行耗时操作,直到需要时。
- 实现:
setTimeout/requestIdleCallback。- 条件渲染。
联系:都是一种延迟策略,优化性能。
最佳实践:
- 图片懒加载。
- 路由懒加载。
- 非关键计算懒执行。
7 文件优化
面试回答:如何优化前端文件?
- 代码压缩:UglifyJS、Terser。
- Tree Shaking:移除未使用代码。
- 代码分割:按路由、按组件拆分。
- Gzip/Brotli 压缩:服务器开启。
- 图片优化:
- 选择合适格式(WebP、AVIF)。
- 压缩(TinyPNG)。
- 响应式图片(
srcset)。
- 字体优化:
font-display: swap。- 预加载。
- 子集化。
- 移除未使用 CSS:PurgeCSS。
- 使用 CDN。
工具:Webpack、Vite、Rollup。
8 其他
面试回答:还有哪些性能优化手段?
- 关键渲染路径优化:减少关键资源,缩短关键路径。
- WebPacked 体积分析:webpack-bundle-analyzer。
- CDN:静态资源加速。
- HTTP/2:多路复用。
- Service Worker:离线缓存。
- 骨架屏:改善加载感知。
- SSR/SSG:提升首屏。
- 监控:RUM(真实用户监控)。
9 如何根据chrome的timing优化
面试回答:如何根据 Chrome DevTools Timing 优化?
一、Performance 面板
- Loading:解析 HTML、CSS、执行 JS。
- Scripting:执行 JS。
- Rendering:样式计算、布局。
- Painting:绘制。
- System:其他。
- Idle:空闲。
二、优化策略
- 长 Scripting:优化 JS,代码分割。
- 长 Rendering:减少重排重绘,使用
transform。 - 长 Painting:简化 CSS,避免复杂选择器。
- 长 Loading:压缩资源,预加载关键资源。
三、Lighthouse
- 提供具体的优化建议。
10 移动端优化
面试回答:移动端有哪些性能优化点?
- Touch 事件:避免
click300ms 延迟,使用fastclick或touch-action: manipulation。 - 图片:使用
srcset、sizes提供合适分辨率。 - 字体:避免 FOIT/FOUT,
font-display: swap。 - 视口:正确设置
viewport。 - 手势:支持 pinch-to-zoom。
- 硬件加速:
transform: translateZ(0)或will-change。 - 网络:检测网络状况,降级体验。
- 内存:避免内存泄漏,及时释放资源。
- 骨架屏:改善加载体验。
- PWA:离线能力。
九、工程化
1 介绍一下 webpack 的构建流程
面试回答:Webpack 的构建流程是怎样的?
- 初始化:读取配置文件(
webpack.config.js),创建 Compiler 实例。 - 编译:Compiler 触发
run钩子。 - 创建 Compilation:代表一次资源版本构建。
- 入口起点:从
entry配置开始,递归解析依赖。 - Loader 处理:对每个模块应用相应的 Loader 链(如
css-loader、babel-loader)。 - AST 分析:解析模块代码为 AST,分析
import/require。 - 依赖图:构建模块依赖图。
- 代码生成:将模块转换为可执行的 Bundle 代码。
- 优化:Tree Shaking、代码分割、压缩。
- 输出:将生成的文件写入
output.path。
核心:entry -> loaders -> dependencies -> chunks -> bundles.
2 介绍 Loader
面试回答:Webpack Loader 是什么?
一、定义 Loader 是一个转换器,用于对模块的源代码进行预处理。
二、作用
- 让 Webpack 能够处理非 JavaScript 文件(如 CSS、图片、TypeScript)。
- 将这些文件转换为有效的模块。
三、执行顺序
- 从右到左,从下到上。
use: ['style-loader', 'css-loader']:先css-loader,再style-loader。
四、常用 Loader
babel-loader:转换 ES6+ 语法。ts-loader:处理 TypeScript。css-loader:处理 CSS 中的@import、url()。style-loader:将 CSS 插入 DOM。file-loader:将文件输出到构建目录。url-loader:将小文件转为 Data URL。
五、编写 Loader
- 函数形式:
module.exports = function(source) { return source; }。 - 支持同步/异步。
3 介绍 plugin
面试回答:Webpack Plugin 是什么?
一、定义 Plugin 是用于扩展 Webpack 功能的机制,可以监听构建生命周期的钩子。
二、与 Loader 的区别
- Loader:处理单个文件,转换源代码。
- Plugin:执行更广泛的任务,如打包优化、资源管理、注入环境变量。
三、工作原理
- 基于事件流(Tapable)。
- 通过
apply(compiler)方法接入。
四、常用 Plugin
HtmlWebpackPlugin:生成 HTML 文件。MiniCssExtractPlugin:提取 CSS 到单独文件。DefinePlugin:定义全局常量。CleanWebpackPlugin:清理输出目录。CompressionPlugin:生成 Gzip 文件。
五、编写 Plugin
class MyPlugin {
apply(compiler) {
compiler.hooks.emit.tap('MyPlugin', compilation => {
// 在输出 asset 前执行
});
}
}
4 webpack 热更新实现原理
面试回答:Webpack 热更新(HMR)的原理是什么?
一、目的 在不刷新页面的情况下,更新修改的模块。
二、实现原理
- Webpack Dev Server (WDS):启动一个本地服务器和 WebSocket 服务。
- 监听文件变化:WDS 监听文件系统。
- 增量编译:文件变化,WDS 通知 Webpack 只编译受影响的模块。
- 生成补丁:Webpack 生成包含更新模块的
hot-update.js和hot-update.json。 - 推送更新:WDS 通过 WebSocket 将更新推送给浏览器。
- 浏览器接收:HMR Runtime 接收更新。
- 检查更新:Runtime 请求
hot-update.json,获取需要更新的 chunk。 - 加载模块:动态加载
hot-update.js。 - 应用更新:调用
module.hot.accept回调,替换模块。 - 重新渲染:框架(如 React Hot Loader)更新 UI。
三、关键
module.hotAPI。- WebSocket 实时通信。
- 框架需要支持 HMR。
5 webpack 层面如何做性能优化
面试回答:Webpack 构建性能如何优化?
- 缩小文件搜索范围:
resolve.modules:减少模块查找路径。resolve.extensions:减少尝试的后缀。test/include/exclude:精确匹配文件。
- 多进程/多实例:
thread-loader:多进程处理。HappyPack(旧)。
- DLL:将第三方库提前打包,避免重复构建。
- 缓存:
cache.type = 'filesystem'。babel-loader的cacheDirectory。
- Tree Shaking:
optimization.usedExports。 - Code Splitting:
splitChunks。 - 分析工具:
webpack-bundle-analyzer。 - 升级到 Webpack 5:持久缓存、Module Federation。
6 介绍一下 Tree Shaking
面试回答:什么是 Tree Shaking?
一、定义 Tree Shaking 是一种通过静态分析,移除 ES6 模块中未使用代码的优化技术。
二、原理
- 静态分析:ES6
import/export是静态的,可以在编译时分析。 - 标记未使用:标记未被引用的
export。 - 移除:在压缩阶段(如 Terser)移除死代码。
三、前提
- ES6 模块语法(
import/export)。 - 使用
mode: 'production',启用压缩。 - 模块必须是“无副作用”(
sideEffects: falsein package.json)。
四、示例
// utils.js
export function unused() { /* dead code */ }
export function used() { return 'hello'; }
// main.js
import { used } from './utils';
console.log(used());
unused函数会被移除。
五、注意
- CommonJS (
require) 不支持 Tree Shaking。 - 动态导入 (
import()) 不支持。
六、最佳实践
- 使用 ES6 模块。
- 配置
sideEffects。
7 介绍一下 webpack scope hosting
面试回答:什么是 Scope Hoisting?
一、定义 Scope Hoisting(作用域提升)是 Webpack 的一项优化,将模块的代码尽可能地合并到一个函数中,减少闭包开销。
二、原理
- 默认:每个模块被包装在一个闭包中。
/* 0 */ (function(module, __webpack_exports__, __webpack_require__) { // module code }), /* 1 */ (function(module, __webpack_exports__, __webpack_require__) { // another module }) - Scope Hoisting:将多个模块的代码合并到一个作用域。
/* 0 */ (function() { // module 0 code // module 1 code (inlined) })
三、好处
- 更小的包体积:减少闭包包装代码。
- 更快的执行:减少函数调用开销。
四、前提
- ES6 模块。
optimization.concatenateModules: true(Webpack 4+ 默认开启)。
五、最佳实践
- 确保使用 ES6 模块。
- 启用生产模式。
8 Webpack Proxy工作原理?为什么能解决跨域
面试回答:Webpack Dev Server 的 Proxy 原理?
一、工作原理
- 开发时,前端应用运行在
localhost:3000。 - API 请求
localhost:3000/api/users。 - Webpack Dev Server 拦截此请求。
- 根据
proxy配置,将请求转发到目标服务器(如http://api.example.com)。 - 目标服务器返回响应。
- Dev Server 将响应返回给浏览器。
二、为什么能解决跨域
- 同源:浏览器发起的请求是到
localhost:3000(前端同源)。 - 转发:跨域发生在 Dev Server(服务器端),不受浏览器同源策略限制。
- 结果:浏览器认为是同源请求,无跨域问题。
三、配置示例
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
}
}
}
};
四、注意
- 仅用于开发环境。
- 生产环境需服务器配置反向代理。
9 介绍一下 babel原理
面试回答:Babel 的工作原理是什么?
Babel 是一个 JavaScript 编译器,用于将高版本 JS 语法转换为向后兼容的版本。
三阶段编译:
- 解析(Parse):
- 词法分析:将源码字符串分解为 tokens(词法单元)。
- 语法分析:将 tokens 转换为 AST(抽象语法树)。
- 转换(Transform):
- 遍历 AST,应用插件(Plugins)。
- 插件修改 AST(如将箭头函数转为普通函数)。
- 生成(Generate):
- 将修改后的 AST 重新生成为目标代码字符串。
核心概念:
- Presets:预设插件集合(如
@babel/preset-env)。 - Plugins:单个转换功能(如
@babel/plugin-transform-arrow-functions)。 - Polyfill:通过
core-js提供缺失的 API(如Promise)。
配置:.babelrc 或 babel.config.js。
10 介绍一下Rollup
面试回答:Rollup 是什么?和 Webpack 的区别?
一、Rollup
- 一个 JavaScript 模块打包器。
- 专注于打包库(Library)。
二、特点
- Tree Shaking:非常激进,移除死代码效果好。
- ES6 模块优先:天然支持,输出更干净的代码。
- 输出格式:支持 IIFE、CommonJS、UMD、ES Module。
- 简单配置:通常比 Webpack 简单。
三、与 Webpack 的区别
| 特性 | Rollup | Webpack |
|---|---|---|
| 定位 | 库 | 应用 |
| Tree Shaking | 更好 | 好 |
| 配置 | 简单 | 复杂 |
| 功能 | 聚焦 | 全面 |
| 热更新 | 弱 | 强 |
| Loader/Plugin | 少 | 多 |
四、适用场景
- 打包开源库(如 Vue、React)。
- 需要极致 Tree Shaking 的项目。
五、最佳实践
- 库开发用 Rollup。
- 应用开发用 Webpack 或 Vite。
十、HTTP
HTTP状态码
面试回答:常见的 HTTP 状态码有哪些?
一、1xx (Informational)
100 Continue:继续。101 Switching Protocols:协议切换。
二、2xx (Success)
200 OK:成功。201 Created:创建成功。204 No Content:成功,无内容。
三、3xx (Redirection)
301 Moved Permanently:永久重定向。302 Found:临时重定向。304 Not Modified:协商缓存,未修改。
四、4xx (Client Error)
400 Bad Request:请求错误。401 Unauthorized:未授权。403 Forbidden:禁止访问。404 Not Found:未找到。405 Method Not Allowed:方法不允许。
五、5xx (Server Error)
500 Internal Server Error:服务器内部错误。502 Bad Gateway:网关错误。503 Service Unavailable:服务不可用。504 Gateway Timeout:网关超时。
1 HTTP前生今世
面试回答:HTTP 协议的发展历程?
- HTTP/0.9:1991,极简,只有 GET。
- HTTP/1.0:1996,引入 Header、状态码、POST。
- HTTP/1.1:1997/1999,持久连接、管道化、缓存控制。
- HTTP/2:2015,二进制分帧、多路复用、头部压缩。
- HTTP/3:2022,基于 QUIC 协议,解决队头阻塞。
2 HTTP世界全览
同“HTTP分层”。
3 HTTP分层
面试回答:HTTP 协议的分层结构?
HTTP 基于 TCP/IP 模型:
- 应用层:HTTP 协议本身。
- 传输层:TCP(HTTP/1.x, HTTP/2 over TCP)或 UDP(HTTP/3 over QUIC)。
- 网络层:IP。
- 链路层:物理传输。
4 HTTP报文是什么样子的
面试回答:HTTP 报文的结构?
一、请求报文
请求行: GET /index.html HTTP/1.1
请求头: Host: www.example.com
User-Agent: ...
Accept: text/html
...
空行
请求体: (可选,如 POST 数据)
二、响应报文
状态行: HTTP/1.1 200 OK
响应头: Server: Apache
Content-Type: text/html
Content-Length: 1374
...
空行
响应体: <!DOCTYPE html>...
5 HTTP之URL
面试回答:URL 的组成部分?
https://www.example.com:8080/path/to/page?param=value#section
- Scheme:
https(协议) - Authority:
- User Info:
username:password@(不常用) - Host:
www.example.com(主机) - Port:
:8080(端口)
- User Info:
- Path:
/path/to/page(路径) - Query:
?param=value(查询参数) - Fragment:
#section(片段标识符)
6 HTTP实体数据
面试回答:HTTP 实体数据的类型?
实体数据(Entity Body)是请求或响应的实际内容。
- 类型:由
Content-Type头指定。text/plaintext/htmlapplication/jsonapplication/x-www-form-urlencodedmultipart/form-data
- 编码:由
Content-Encoding头指定(gzip、br)。 - 长度:
Content-Length头。
7 谈一谈HTTP协议优缺点
面试回答:HTTP 协议的优缺点?
优点:
- 简单、灵活。
- 无状态,易于扩展。
- 明文,易于调试。
- 广泛支持。
缺点:
- 明文传输:不安全,需 HTTPS。
- 无状态:需要 Cookie/Session 管理状态。
- 队头阻塞(HTTP/1.1):一个请求阻塞后续。
- 冗余头部:每次请求都发送。
演进:HTTP/2、HTTP/3 解决了部分缺点。
8 说一说HTTP 的请求方法
面试回答:HTTP 请求方法有哪些?
GET:获取资源。幂等,可缓存。POST:创建资源。非幂等。PUT:更新资源(全量)。幂等。PATCH:更新资源(部分)。幂等。DELETE:删除资源。幂等。HEAD:获取头部,无 body。OPTIONS:预检请求,获取服务器支持的方法。CONNECT:建立隧道,用于 HTTPS。
9 谈一谈GET 和 POST 的区别
面试回答:GET 和 POST 的主要区别?
| 特性 | GET | POST |
|---|---|---|
| 参数位置 | URL 查询字符串 | 请求体 |
| 长度限制 | 有(URL 长度限制) | 无(理论上) |
| 缓存 | 可以 | 不可以 |
| 幂等性 | 是 | 否 |
| 安全性 | 幂等,不应修改服务器状态 | 可修改 |
| 书签 | 可以 | 不可以 |
| 历史 | 有 | 无 |
| 编码类型 | application/x-www-form-urlencoded | 可多种(form-data, json) |
本质:RESTful 中,GET 用于安全、幂等的读取,POST 用于不幂等的创建。
10 谈一谈队头阻塞问题
面试回答:什么是队头阻塞?如何解决?
一、HTTP/1.1 队头阻塞
- 现象:浏览器对同一域名有连接数限制(通常 6-8)。
- 问题:如果一个大文件请求阻塞,后续请求需排队。
- 解决:域名分片(domain sharding)。
二、HTTP/2 队头阻塞
- 现象:虽然多路复用,但底层 TCP 传输时,一个包丢失会导致整个连接阻塞。
- 解决:HTTP/3 使用 QUIC(基于 UDP),解决了传输层队头阻塞。
11 谈一谈HTTP数据传输
面试回答:HTTP 数据传输的方式?
- 短连接:HTTP/1.0 默认,每次请求后关闭连接。
- 持久连接:HTTP/1.1
Connection: keep-alive,复用连接。 - 管道化:HTTP/1.1,一次发送多个请求,但响应必须按序返回。
- 多路复用:HTTP/2,一个连接上并行传输多个请求/响应。
- 流式传输:服务器推送、Chunked Transfer Encoding。
12 cookie 和 session
面试回答:Cookie 和 Session 的区别?
Cookie
- 存储在客户端。
- 每次请求自动携带。
- 容量小(~4KB)。
- 可设置过期时间。
- 可被 JS 读取(除非 HttpOnly)。
Session
- 存储在服务器端。
- 通过 Cookie 中的 Session ID 关联。
- 安全性高(数据在服务端)。
- 依赖 Cookie 传输 ID。
- 服务器有存储压力。
关系:通常用 Cookie 存储 Session ID。
13 介绍一下HTTPS和HTTP区别
面试回答:HTTPS 和 HTTP 的区别?
| 特性 | HTTP | HTTPS |
|---|---|---|
| 安全性 | 明文 | 加密 |
| 端口 | 80 | 443 |
| 证书 | 无 | 需要 SSL/TLS 证书 |
| 性能 | 快 | 稍慢(加密开销) |
| SEO | 差 | 好(Google 排名因素) |
| 协议 | 应用层 | 应用层 + SSL/TLS 层 |
核心:HTTPS = HTTP + SSL/TLS。
14 HTTPS握手过程
面试回答:HTTPS 的握手过程是怎样的?
- Client Hello:客户端发送支持的加密套件、随机数。
- Server Hello:服务器选择加密套件、发送随机数、证书。
- Certificate Verify:客户端验证证书(CA 链)。
- Client Key Exchange:客户端生成预主密钥,用公钥加密发送。
- Server Decrypt:服务器用私钥解密,得到预主密钥。
- Master Secret:双方用随机数和预主密钥生成主密钥。
- Change Cipher Spec:双方通知切换到加密通信。
- Finished:发送加密的完成消息,验证握手成功。
后续:使用主密钥对称加密通信。
15 介绍一个HTTPS工作原理
同“HTTPS握手过程”。
16 SSL 连接断开后如何恢复
面试回答:SSL 会话恢复?
为了减少握手开销,SSL/TLS 支持会话恢复。
两种方式:
- Session ID:
- 服务器为会话分配 ID,缓存会话参数。
- 客户端在
Client Hello中发送 ID。 - 服务器查找缓存,若存在,复用主密钥。
- Session Tickets:
- 服务器将会话参数加密为 ticket,发送给客户端。
- 客户端在下次
Client Hello中发送 ticket。 - 服务器解密 ticket,恢复会话。
优势:避免完整的非对称加密,加快连接。
17 谈一谈你对HTTP/2理解
面试回答:对 HTTP/2 的理解?
HTTP/2 是 HTTP/1.1 的重大升级。
核心特性:
- 二进制分帧:将报文分解为帧,交错传输。
- 多路复用:一个连接上并行传输多个请求/响应,解决 HTTP/1.1 队头阻塞。
- 头部压缩:HPACK 算法,减少头部大小。
- 服务器推送:服务器主动推送资源。
- 优先级和依赖:客户端可指定请求优先级。
对前端的影响:
- 域名分片收益减少。
- 资源合并策略需重新评估。
- 更关注服务器推送的使用。
18 HTTP3
面试回答:HTTP/3 的主要改进?
HTTP/3 的最大改进是传输层协议。
问题:HTTP/2 的多路复用在 TCP 层仍有队头阻塞(一个包丢失,整个连接阻塞)。
解决方案:HTTP/3 基于 QUIC 协议。
- QUIC:基于 UDP,实现可靠传输。
- 多路复用在 QUIC 层:一个流阻塞不影响其他流。
- 0-RTT:更快的连接建立。
- 内置加密:TLS 1.3 集成。
优势:更好的移动网络性能,更低的延迟。
19 HTTP/1.0 HTTP1.1 HTTP2.0版本之间的差异
面试回答:HTTP/1.0, 1.1, 2.0 的主要差异?
| 特性 | HTTP/1.0 | HTTP/1.1 | HTTP/2 |
|---|---|---|---|
| 连接 | 短连接 | 长连接 (keep-alive) | 长连接 |
| 管线化 | 无 | 有(但不实用) | 多路复用 |
| 头部 | 无缓存头 | Cache-Control, ETag | HPACK 压缩 |
| 数据格式 | 文本 | 文本 | 二进制分帧 |
| 服务器推送 | 无 | 无 | 有 |
| 优先级 | 无 | 无 | 有 |
20 DNS如何工作的
面试回答:DNS 的工作原理?
- 浏览器缓存:检查是否有缓存。
- 操作系统缓存:
hosts文件或 DNS Client 缓存。 - 路由器/ISP DNS:向本地 DNS 服务器查询。
- 根域名服务器:
.com的权威服务器地址。 - 顶级域名服务器:
example.com的权威服务器地址。 - 权威域名服务器:返回
www.example.com的 IP。 - 缓存结果:各级缓存。
递归查询:本地 DNS 服务器替客户端完成全部查询。 迭代查询:本地 DNS 服务器只返回下一步地址。
21 短轮询、长轮询和 WebSocket 间的区别
面试回答:三种实时通信方式的区别?
| 方式 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 短轮询 | 客户端定时发送请求 | 简单 | 延迟高,浪费资源 | 低频更新 |
| 长轮询 | 客户端发请求,服务器有数据才返回 | 实时性较好 | 服务器压力大,连接占用 | 中等实时性 |
| WebSocket | 双向持久连接 | 实时性最好,高效 | 复杂,需要支持 | 高频实时(聊天、游戏) |
22 说一说正向代理和反向代理
面试回答:正向代理和反向代理的区别?
正向代理 (Forward Proxy)
- 位置:客户端侧。
- 目的:代表客户端访问目标服务器。
- 效果:隐藏客户端真实 IP。
- 例子:翻墙代理、公司上网代理。
反向代理 (Reverse Proxy)
- 位置:服务器侧。
- 目的:代表服务器接收客户端请求。
- 效果:隐藏服务器真实 IP,负载均衡。
- 例子:Nginx、CDN。
共同点:都作为中间人。
23 介绍一下Connection:keep-alive
面试回答:Connection: keep-alive 的作用?
- HTTP/1.0:默认短连接。
Connection: keep-alive启用持久连接。 - HTTP/1.1:默认持久连接。
Connection: close关闭。
作用:
- 复用 TCP 连接,避免重复握手。
- 减少延迟,提高性能。
注意:连接不会永远保持,有超时时间。
24 http/https 协议总结
综合以上。
25 TCP为什么要三次握手
面试回答:TCP 三次握手的原因?
- SYN:客户端 -> 服务器,同步序列号,表示“我想连接”。
- SYN-ACK:服务器 -> 客户端,确认 SYN,并发送自己的 SYN。
- ACK:客户端 -> 服务器,确认服务器的 SYN。
原因:
- 确认双方的发送和接收能力。
- 防止已失效的连接请求突然传送到服务器(历史连接)。
四次挥手:因为 TCP 是全双工,两端都要关闭。
26 为什么要有 WebSocket
面试回答:为什么需要 WebSocket?
HTTP 是请求-响应模式,不适合实时双向通信。
问题:
- 轮询效率低,延迟高。
- 服务器无法主动推送。
解决方案:WebSocket
- 全双工:客户端和服务器可随时互相发送数据。
- 持久连接:一次握手,长期保持。
- 低延迟:数据直达。
- 高效:头部小。
应用:聊天室、在线游戏、实时股票。
27 UDP和TCP有什么区别
面试回答:UDP 和 TCP 的区别?
| 特性 | UDP | TCP |
|---|---|---|
| 连接 | 无连接 | 面向连接 |
| 可靠性 | 不可靠 | 可靠(重传、确认) |
| 有序性 | 无序 | 有序 |
| 速度 | 快 | 慢 |
| 头部 | 8字节 | 20字节起 |
| 流量控制 | 无 | 有 |
| 拥塞控制 | 无 | 有 |
| 适用 | 视频、语音、DNS | 网页、邮件、文件 |
十一、9种前端常见的设计模式
1 外观模式
面试回答:外观模式(Facade Pattern)?
一、定义 为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,使得子系统更容易使用。
二、例子
// 复杂的相机设置
const camera = {
openShutter() {},
adjustFocus() {},
flashOn() {},
takePhoto() {}
};
// 外观
const CameraFacade = {
takePortrait() {
camera.adjustFocus();
camera.flashOn();
camera.takePhoto();
},
takeLandscape() {
camera.openShutter();
camera.takePhoto();
}
};
CameraFacade.takePortrait(); // 简单调用
三、应用:jQuery 的 $ API。
2 代理模式
面试回答:代理模式(Proxy Pattern)?
一、定义 为一个对象提供一个替身或占位符,以控制对该对象的访问。
二、类型
- 远程代理:访问远程对象(如 RPC)。
- 虚拟代理:延迟创建开销大的对象。
- 保护代理:控制访问权限。
三、例子:Vue 3 的 Proxy 实现响应式。
3 工厂模式
面试回答:工厂模式(Factory Pattern)?
一、定义 创建对象的接口,让子类决定实例化哪个类。
二、简单工厂
function createProduct(type) {
switch(type) {
case 'book': return new Book();
case 'pen': return new Pen();
}
}
三、应用:React.createElement。
4 单例模式
面试回答:单例模式(Singleton Pattern)?
一、定义 确保一个类只有一个实例,并提供一个全局访问点。
二、实现
class Singleton {
static instance;
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
}
}
三、应用:全局状态管理、日志记录器。
5 策略模式
面试回答:策略模式(Strategy Pattern)?
一、定义 定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换。
二、例子
const strategies = {
'discount10': price => price * 0.9,
'discount20': price => price * 0.8
};
function calculatePrice(price, strategy) {
return strategies[strategy](price);
}
三、应用:表单验证。
6 迭代器模式
面试回答:迭代器模式(Iterator Pattern)?
一、定义 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
二、例子:JavaScript 的 Symbol.iterator。
三、应用:遍历自定义数据结构。
7 观察者模式
面试回答:观察者模式(Observer Pattern)?
一、定义 定义对象间的一对多依赖关系,当一个对象改变状态,所有依赖它的对象都会收到通知。
二、例子:Vue 的 Dep-Watcher。
三、与发布订阅区别:观察者模式中,目标和观察者是紧耦合的。
8 中介者模式
面试回答:中介者模式(Mediator Pattern)?
一、定义 用一个中介对象来封装一系列对象之间的交互,使对象之间不需要显式地相互引用。
二、例子:
const mediator = {
users: [],
notify(sender, event) {
this.users.forEach(user => {
if (user !== sender) {
user.receive(event);
}
});
}
};
三、应用:聊天室。
9 访问者模式
面试回答:访问者模式(Visitor Pattern)?
一、定义 表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
二、例子:AST 遍历。
三、应用:编译器、静态分析工具。
十二、综合问题
前端常见面试流程
面试回答:前端面试通常有哪些流程?
- 简历筛选:HR 或技术主管初筛。
- 技术初面:基础知识、项目经验。
- 技术终面:深入原理、系统设计。
- 交叉面:其他团队面试。
- HR 面:薪资、职业规划。
- Offer:发放录用通知。
准备:复习基础,梳理项目,准备作品集。
面试一定要问这几个问题
面试回答:面试时应该问面试官哪些问题?
- 团队:团队规模、技术栈、成员背景。
- 项目:当前主要项目、技术挑战。
- 发展:新人培训、晋升机制。
- 文化:工作氛围、加班情况。
- 技术:技术决策流程、新技术采纳。
避免:直接问薪资福利(留到 HR 面)。
经历
面试回答:如何讲述你的工作经历?
使用 STAR 法则:
- Situation:当时的情况。
- Task:你的任务。
- Action:你采取的行动。
- Result:取得的结果(量化)。
重点:突出你的贡献和技术深度。
项目相关
面试回答:如何介绍你的项目?
- 项目背景:解决什么问题。
- 技术选型:为什么选这些技术。
- 你的角色:具体负责哪部分。
- 技术难点:遇到的挑战和解决方案。
- 成果:上线效果、性能指标。
准备:1-2 个重点项目。
项目难点问题分析
面试回答:你在项目中遇到的最大挑战?
- 选择合适案例:真实、有技术含量。
- 描述问题:清晰界定挑战。
- 分析原因:根本原因。
- 解决方案:你的思路和实施。
- 结果:问题解决,带来什么好处。
示例:性能优化、复杂交互、跨团队协作。
项目流程相关面试题
面试回答:你们项目的开发流程是怎样的?
- 需求评审:产品、设计、开发讨论。
- 技术方案:设计文档,技术选型。
- 开发:编码,Code Review。
- 测试:单元测试,集成测试。
- 上线:灰度发布,监控。
- 迭代:收集反馈,优化。
敏捷:Scrum,每日站会。
把握投递简历的黄金时间段
面试回答:什么时候投递简历最好?
- 年后:2月-3月,“金三银四”。
- 毕业季:6月-7月。
- 跳槽季:9月-10月,“金九银十”。
- 避开:春节前后,年底。
建议:提前准备,主动投递。
把握面试时的关键点
面试回答:面试时要注意哪些关键点?
- 第一印象:着装、准时。
- 沟通:清晰、自信、诚实。
- 倾听:理解问题再回答。
- 结构化:逻辑清晰,分点陈述。
- 互动:适时提问。
- 态度:积极、热情。
避免:不懂装懂。
工作交接流程 & 福利衔接
面试回答:离职交接和入职福利?
交接:
- 文档整理。
- 代码注释。
- 知识转移。
- 工作移交。
福利衔接:
- 五险一金:尽快办理转入。
- 商业保险:咨询 HR。
- 年假:按比例计算。
建议:遵守劳动合同,平稳过渡。
十三、人事面
第一个要点: 你是否胜任这份工作?
面试回答:你为什么能胜任这份工作?
- 技能匹配:强调你的技术栈与职位要求相符。
- 项目经验:举例说明你做过类似的项目。
- 学习能力:快速掌握新技术的能力。
- 解决问题:过去成功解决难题的经历。
用事实和数据说话。
第二个要点:你是怎样的人?
面试回答:你如何评价自己?
- 性格:乐观、责任心强、细心。
- 工作风格:严谨、追求完美、善于沟通。
- 兴趣:技术热情,业余学习。
- 价值观:诚信、团队合作。
真诚,避免空洞形容词。
第三个要点:你是否适合这个企业?
面试回答:你为什么想加入我们公司?
- 公司认可:欣赏公司的产品、技术、文化。
- 发展空间:看重公司的成长机会。
- 团队匹配:认同团队的工作方式。
- 个人契合:职业规划与公司方向一致。
提前研究公司。
第四个要点:聘用你,公司需付出多少?
面试回答:你的期望薪资是多少?
- 市场调研:了解行业平均水平。
- 结合自身:根据经验、能力定价。
- 表达弹性:说明期望范围,愿意根据整体待遇协商。
- 关注整体:提及对福利、发展机会的重视。
避免过早谈论,通常在 HR 面谈。