2025-2026 大厂前端经典面试题库(京东、荣耀、字节、华为)
汇总京东、荣耀、字节跳动、华为等一线互联网公司前端面试真题,涵盖 Vue、JavaScript、HTML、CSS 核心考点
整理时间: 2026 年 3 月
适用岗位: 前端开发工程师、高级前端工程师
技术栈: Vue 3 + JavaScript + HTML + CSS
目录
一、JavaScript 基础与进阶
1.1 数据类型与类型转换
Q1: JavaScript 有哪些数据类型?如何判断数据类型?
答案:
// 7 种原始类型
// String, Number, Boolean, Null, Undefined, Symbol, BigInt
// 1 种引用类型
// Object (包含 Array, Function, Date, RegExp 等)
// 判断方法
console.log(typeof 'hello'); // 'string'
console.log(typeof 42); // 'number'
console.log(typeof true); // 'boolean'
console.log(typeof undefined); // 'undefined'
console.log(typeof null); // 'object' ⚠️ 注意
console.log(typeof {}); // 'object'
console.log(typeof []); // 'object'
console.log(typeof function(){}); // 'function'
// 准确判断使用 Object.prototype.toString
function getType(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
console.log(getType(null)); // 'null'
console.log(getType([])); // 'array'
console.log(getType(new Date())); // 'date'
考点:
typeof null返回'object'是历史遗留 bugtypeof无法区分数组和对象Object.prototype.toString是最准确的类型判断方法
Q2: 深拷贝和浅拷贝的区别?如何实现深拷贝?
答案:
// 浅拷贝:只复制引用,修改会影响原对象
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1); // 浅拷贝
obj2.b.c = 999;
console.log(obj1.b.c); // 999,原对象被修改
// 深拷贝:递归复制所有层级
function deepClone(obj, map = new WeakMap()) {
// 处理原始类型和 null
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理循环引用
if (map.has(obj)) {
return map.get(obj);
}
// 处理 Date
if (obj instanceof Date) {
return new Date(obj);
}
// 处理 RegExp
if (obj instanceof RegExp) {
return new RegExp(obj);
}
// 处理 Array
if (Array.isArray(obj)) {
const arr = [];
map.set(obj, arr);
for (let i = 0; i < obj.length; i++) {
arr[i] = deepClone(obj[i], map);
}
return arr;
}
// 处理 Object
const cloned = {};
map.set(obj, cloned);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key], map);
}
}
return cloned;
}
// 使用 JSON.stringify 的简单方法(有局限性)
const obj3 = JSON.parse(JSON.stringify(obj1));
// 局限性:无法处理函数、Symbol、循环引用、Date 等
考点:
- 浅拷贝只复制第一层
- 深拷贝需要递归处理所有层级
- 循环引用需要使用 WeakMap 或 Map 记录
- JSON 方法简单但有局限性
1.2 闭包与作用域
Q3: 什么是闭包?闭包的应用场景有哪些?
答案:
// 闭包定义:函数能够访问其词法作用域外的变量
function outer() {
const a = 10;
function inner() {
console.log(a); // 访问外部变量 a,形成闭包
}
return inner;
}
const closureFn = outer();
closureFn(); // 10,即使 outer 已执行完毕
// 应用场景 1:数据私有化
function createCounter() {
let count = 0;
return {
increment() { count++; },
decrement() { count--; },
getCount() { return count; }
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined,无法直接访问
// 应用场景 2:函数柯里化
function curry(fn, ...args) {
return args.length >= fn.length
? fn(...args)
: (...moreArgs) => curry(fn, ...args, ...moreArgs);
}
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
// 应用场景 3:防抖节流
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 应用场景 4:模块模式(ES6 之前)
const Module = (function() {
let privateVar = 'private';
function privateMethod() {
console.log(privateVar);
}
return {
publicMethod() {
privateMethod();
}
};
})();
考点:
- 闭包是函数 + 词法环境
- 闭包会导致内存泄漏(需要手动释放)
- 应用场景:数据私有化、柯里化、防抖节流、模块化
1.3 异步编程
Q4: Promise、async/await 的实现原理?
答案:
// 手写 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(cb => cb());
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(cb => cb());
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };
const promise2 = new MyPromise((resolve, reject) => {
const fulfilledTask = () => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
};
const rejectedTask = () => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
};
if (this.state === 'fulfilled') {
fulfilledTask();
} else if (this.state === 'rejected') {
rejectedTask();
} else {
this.onFulfilledCallbacks.push(fulfilledTask);
this.onRejectedCallbacks.push(rejectedTask);
}
});
return promise2;
}
}
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected'));
}
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
let called = false;
try {
const then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, r => {
if (called) return;
called = true;
reject(r);
});
} else {
resolve(x);
}
} catch (error) {
if (called) return;
called = true;
reject(error);
}
} else {
resolve(x);
}
}
// async/await 是 Promise 的语法糖
async function asyncFn() {
const result = await new Promise(resolve => {
setTimeout(() => resolve('done'), 1000);
});
console.log(result);
}
// 编译后类似
function asyncFn() {
return new Promise((resolve, reject) => {
new Promise(resolve => {
setTimeout(() => resolve('done'), 1000);
}).then(result => {
console.log(result);
resolve();
}).catch(reject);
});
}
考点:
- Promise 有三种状态:pending、fulfilled、rejected
- 状态一旦改变就不可逆
- then 方法返回新的 Promise,支持链式调用
- async/await 是 Promise 的语法糖,内部通过 generator 实现
1.4 原型与继承
Q5: JavaScript 的继承方式有哪些?ES6 class 继承的本质是什么?
答案:
// 1. 原型链继承
function Parent() {
this.name = 'parent';
}
Parent.prototype.getName = function() {
return this.name;
};
function Child() {}
Child.prototype = new Parent(); // 原型链继承
const child1 = new Child();
console.log(child1.getName()); // 'parent'
// 问题:所有实例共享父类实例属性
// 2. 构造函数继承
function Child2() {
Parent.call(this); // 借用构造函数
this.age = 18;
}
const child2 = new Child2();
console.log(child2.name); // 'parent'
console.log(child2.getName); // undefined,无法继承原型方法
// 3. 组合继承(原型链 + 构造函数)
function Child3() {
Parent.call(this);
this.age = 18;
}
Child3.prototype = new Parent();
Child3.prototype.constructor = Child3;
const child3 = new Child3();
console.log(child3.getName()); // 'parent'
// 问题:调用两次父类构造函数
// 4. 寄生组合继承(最优解)
function Child4() {
Parent.call(this);
this.age = 18;
}
Child4.prototype = Object.create(Parent.prototype);
Child4.prototype.constructor = Child4;
// 5. ES6 class 继承
class Parent5 {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Child5 extends Parent5 {
constructor(name, age) {
super(name); // 必须调用 super()
this.age = age;
}
}
const child5 = new Child5('child', 18);
console.log(child5.getName()); // 'child'
// ES6 class 继承本质
// class 只是语法糖,本质还是原型链继承
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function');
}
}
function _inherits(subClass, superClass) {
if (typeof superClass !== 'function' && superClass !== null) {
throw new TypeError('Super expression must either be null or a function');
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
考点:
- 原型链继承、构造函数继承、组合继承、寄生组合继承
- ES6 class 是语法糖,本质是原型链继承
- 继承的核心是
__proto__和prototype链 super()必须在子类构造函数中首先调用
二、Vue 3 框架深度
2.1 响应式原理
Q6: Vue 3 的响应式原理是什么?与 Vue 2 有什么区别?
答案:
// Vue 2: Object.defineProperty
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify();
}
});
}
// Vue 2 的问题:
// 1. 无法检测数组索引和长度的变化
// 2. 无法检测对象属性的添加/删除
// 3. 必须递归遍历所有属性
// Vue 3: Proxy
function reactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
track(target, key); // 收集依赖
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 触发更新
}
return result;
}
});
}
// 依赖收集
let activeEffect = null;
const targetMap = new WeakMap();
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(activeEffect);
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => {
effect();
});
}
}
// effect 函数
function effect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
// 使用
const state = reactive({ count: 0 });
effect(() => {
console.log(`count is: ${state.count}`);
});
state.count++; // 触发更新
区别对比:
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 实现方式 | Object.defineProperty | Proxy |
| 数组监听 | 需要重写数组方法 | 原生支持 |
| 对象新增属性 | 需要 Vue.set | 原生支持 |
| 性能 | 初始化慢(递归) | 惰性代理 |
| 内存占用 | 较高 | 较低 |
考点:
- Proxy 可以监听对象所有操作
- WeakMap 防止内存泄漏
- effect 函数用于收集依赖
- track 和 trigger 实现依赖收集和触发更新
2.2 组件通信
Q7: Vue 3 中父子组件、兄弟组件、跨层级组件如何通信?
答案:
<!-- 1. 父子组件通信 -->
<!-- Parent.vue -->
<template>
<Child :message="parentMsg" @update="handleUpdate" />
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const parentMsg = ref('Hello from parent');
function handleUpdate(newVal) {
parentMsg.value = newVal;
}
</script>
<!-- Child.vue -->
<template>
<div>
<p>{{ message }}</p>
<button @click="sendUpdate">Update Parent</button>
</div>
</template>
<script setup>
const props = defineProps({
message: String
});
const emit = defineEmits(['update']);
function sendUpdate() {
emit('update', 'Updated from child');
}
</script>
<!-- 2. v-model 双向绑定 -->
<!-- Parent.vue -->
<template>
<Child v-model="value" />
</template>
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const value = ref('initial value');
</script>
<!-- Child.vue -->
<template>
<input :value="modelValue" @input="onInput" />
</template>
<script setup>
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
function onInput(e) {
emit('update:modelValue', e.target.value);
}
</script>
<!-- 3. provide/inject 跨层级通信 -->
<!-- Root.vue -->
<script setup>
import { provide, ref } from 'vue';
import Child from './Child.vue';
const theme = ref('dark');
const updateTheme = (newTheme) => {
theme.value = newTheme;
};
provide('theme', { theme, updateTheme });
</script>
<!-- DeepChild.vue -->
<script setup>
import { inject } from 'vue';
const { theme, updateTheme } = inject('theme');
function changeTheme() {
updateTheme('light');
}
</script>
<!-- 4. mitt 事件总线(兄弟组件通信) -->
// eventBus.js
import mitt from 'mitt';
const emitter = mitt();
export default emitter;
// ComponentA.vue
<script setup>
import emitter from './eventBus';
function sendMessage() {
emitter.emit('custom-event', { data: 'Hello from A' });
}
</script>
// ComponentB.vue
<script setup>
import { onMounted } from 'vue';
import emitter from './eventBus';
onMounted(() => {
emitter.on('custom-event', (data) => {
console.log(data);
});
});
</script>
<!-- 5. Pinia 状态管理 -->
// store/user.js
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
name: 'John',
age: 30
}),
getters: {
doubleAge: (state) => state.age * 2
},
actions: {
incrementAge() {
this.age++;
}
}
});
// Component.vue
<script setup>
import { useUserStore } from './store/user';
const userStore = useUserStore();
console.log(userStore.name);
userStore.incrementAge();
</script>
考点:
- props/emit 用于父子通信
- v-model 是 props + emit 的语法糖
- provide/inject 用于跨层级通信
- mitt 用于兄弟组件通信
- Pinia 是 Vue 3 推荐的状态管理方案
2.3 生命周期
Q8: Vue 3 生命周期有哪些?与 Vue 2 有什么区别?
答案:
<script setup>
import { onMounted, onUpdated, onUnmounted } from 'vue';
// Vue 3 Composition API 生命周期
onMounted(() => {
console.log('组件挂载完成');
});
onUpdated(() => {
console.log('组件更新完成');
});
onUnmounted(() => {
console.log('组件卸载');
});
// 其他生命周期
import {
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount,
onErrorCaptured,
onActivated,
onDeactivated
} from 'vue';
</script>
生命周期对比:
| Vue 2 | Vue 3 (Options API) | Vue 3 (Composition API) | 说明 |
|---|---|---|---|
| beforeCreate | beforeCreate | setup() | 实例创建前 |
| created | created | setup() | 实例创建后 |
| beforeMount | beforeMount | onBeforeMount | 挂载前 |
| mounted | mounted | onMounted | 挂载完成 |
| beforeUpdate | beforeUpdate | onBeforeUpdate | 更新前 |
| updated | updated | onUpdated | 更新完成 |
| beforeDestroy | beforeUnmount | onBeforeUnmount | 销毁前 |
| destroyed | unmounted | onUnmounted | 销毁完成 |
| activated | activated | onActivated | keep-alive 激活 |
| deactivated | deactivated | onDeactivated | keep-alive 停用 |
考点:
- Vue 3 移除了
beforeCreate和created,统一使用setup() - Vue 3 将
destroy改为unmount - Composition API 生命周期函数需要手动导入
- 生命周期函数可以多次调用
2.4 虚拟 DOM 与 Diff 算法
Q9: Vue 的虚拟 DOM 是什么?Diff 算法的优化策略有哪些?
答案:
// 虚拟 DOM 是用 JavaScript 对象描述 DOM 结构
const vnode = {
tag: 'div',
props: {
id: 'app',
class: 'container'
},
children: [
{
tag: 'p',
props: { class: 'text' },
children: ['Hello World']
}
]
};
// Diff 算法优化策略
function patch(n1, n2, container) {
// 1. 如果新旧节点不同,直接替换
if (n1.tag !== n2.tag) {
container.replaceChild(createElement(n2), n1.el);
return;
}
// 2. 如果是文本节点,直接更新
if (typeof n2.children === 'string') {
if (n1.children !== n2.children) {
n1.el.textContent = n2.children;
}
return;
}
// 3. 比较属性
patchProps(n1.props, n2.props, n1.el);
// 4. 比较子节点
patchChildren(n1, n2, n1.el);
}
// 子节点 Diff 优化
function patchChildren(n1, n2, container) {
const c1 = n1.children;
const c2 = n2.children;
const l1 = c1.length;
const l2 = c2.length;
// 1. 新节点为空,卸载旧节点
if (l2 === 0) {
unmountChildren(c1);
return;
}
// 2. 旧节点为空,挂载新节点
if (l1 === 0) {
mountChildren(c2, container);
return;
}
// 3. 双端比较(Vue 2)
let i = 0;
let e1 = l1 - 1;
let e2 = l2 - 1;
// 从前往后比较
while (i <= e1 && i <= e2 && isSameVNodeType(c1[i], c2[i])) {
patch(c1[i], c2[i], container);
i++;
}
// 从后往前比较
while (i <= e1 && i <= e2 && isSameVNodeType(c1[e1], c2[e2])) {
patch(c1[e1], c2[e2], container);
e1--;
e2--;
}
// 4. Vue 3 最长递增子序列算法
const keyToNewIndexMap = new Map();
for (let i = 0; i <= e2; i++) {
keyToNewIndexMap.set(c2[i].key, i);
}
const newIndexToOldIndexMap = new Array(l2).fill(0);
for (let i = i; i <= e1; i++) {
const oldChild = c1[i];
const newIndex = keyToNewIndexMap.get(oldChild.key);
if (newIndex === undefined) {
unmount(oldChild);
} else {
newIndexToOldIndexMap[newIndex] = i + 1;
patch(oldChild, c2[newIndex], container);
}
}
// 计算最长递增子序列
const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap);
// 移动节点
for (let i = increasingNewIndexSequence.length - 1; i >= 0; i--) {
const newIndex = increasingNewIndexSequence[i];
const newChild = c2[newIndex];
const anchor = newIndex + 1 < l2 ? c2[newIndex + 1].el : null;
if (newIndexToOldIndexMap[newIndex] === 0) {
mount(newChild, container, anchor);
} else {
move(newChild, container, anchor);
}
}
}
function getSequence(arr) {
const p = arr.slice();
const result = [0];
let i, j, u, v, c;
const len = arr.length;
for (i = 0; i < len; i++) {
const arrI = arr[i];
if (arrI !== 0) {
j = result[result.length - 1];
if (arr[j] < arrI) {
p[i] = j;
result.push(i);
continue;
}
u = 0;
v = result.length - 1;
while (u < v) {
c = (u + v) >> 1;
if (arr[result[c]] < arrI) {
u = c + 1;
} else {
v = c;
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
u = result.length;
v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}
Diff 算法优化策略:
- 只比较同一层级:不会跨层级比较
- Key 的作用:通过 key 识别节点,减少不必要的 DOM 操作
- 双端比较:Vue 2 使用双端比较算法
- 最长递增子序列:Vue 3 使用 LIS 算法进一步优化
- 静态提升:静态节点不会参与 diff
- Patch Flags:标记节点变化类型,只更新变化的部分
考点:
- 虚拟 DOM 用 JS 对象描述 DOM
- Diff 算法只比较同层级的节点
- Key 的作用是帮助 diff 算法识别节点
- Vue 3 使用最长递增子序列算法优化 diff 性能
2.5 Computed 与 Watch
Q10: computed 和 watch 的区别?如何选择?
答案:
<script setup>
import { ref, computed, watch, watchEffect } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
// computed: 计算属性
// 特点:有缓存、惰性求值、必须返回值
const fullName = computed(() => {
console.log('computed called');
return `${firstName.value} ${lastName.value}`;
});
// 访问 fullName 多次,只会计算一次
console.log(fullName.value);
console.log(fullName.value);
// 可写的 computed
const fullNamed = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(value) {
[firstName.value, lastName.value] = value.split(' ');
}
});
fullNamed.value = 'Jane Smith';
console.log(firstName.value); // 'Jane'
// watch: 侦听器
// 特点:无缓存、立即执行、可执行副作用
watch(
() => fullName.value,
(newVal, oldVal) => {
console.log(`fullName changed from ${oldVal} to ${newVal}`);
},
{ immediate: true, deep: true }
);
// watchEffect: 自动追踪依赖的 watch
watchEffect(() => {
console.log(`fullName is: ${fullName.value}`);
});
// watch vs computed 选择
// 1. 需要缓存、派生数据 → computed
const doubledCount = computed(() => count.value * 2);
// 2. 需要执行副作用(异步请求、DOM 操作) → watch
watch(count, (newVal) => {
fetch(`/api/data?count=${newVal}`);
});
// 3. 不需要返回值,只是侦听变化 → watch
watch(count, (newVal) => {
console.log(`Count changed to ${newVal}`);
});
</script>
区别对比:
| 特性 | computed | watch |
|---|---|---|
| 缓存 | 有 | 无 |
| 惰性求值 | 是 | 否 |
| 返回值 | 必须返回 | 不需要 |
| 副作用 | 不推荐 | 推荐 |
| 异步操作 | 不支持 | 支持 |
| 依赖追踪 | 自动追踪 | 需要指定 |
考点:
- computed 有缓存,适合计算派生数据
- watch 适合执行副作用
- watchEffect 自动追踪依赖,不需要指定监听源
- computed 不能有副作用,watch 可以
三、HTML 与 CSS
3.1 HTML 语义化
Q11: 什么是 HTML 语义化?为什么需要语义化?
答案:
<!-- ❌ 非语义化 -->
<div class="header">
<div class="logo">Logo</div>
<div class="nav">
<div class="nav-item">Home</div>
<div class="nav-item">About</div>
</div>
</div>
<div class="content">
<div class="article-title">Title</div>
<div class="article-content">Content</div>
</div>
<!-- ✅ 语义化 -->
<header>
<nav>
<div class="logo">Logo</div>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Title</h1>
<p>Content</p>
</article>
</main>
<aside>
<h2>Related</h2>
<ul>
<li><a href="/related1">Related 1</a></li>
<li><a href="/related2">Related 2</a></li>
</ul>
</aside>
<footer>
<p>© 2026 Company</p>
</footer>
语义化的好处:
- SEO 优化:搜索引擎更好理解页面结构
- 可访问性:屏幕阅读器更好识别内容
- 代码可读性:开发者更容易理解页面结构
- 样式与结构分离:便于维护和重构
考点:
- header、nav、main、article、aside、footer 等语义标签
- 标题标签 h1-h6 的正确使用
- 表单元素的 label 关联
- 图片的 alt 属性
3.2 CSS 盒模型
Q12: 标准盒模型和 IE 盒模型的区别?如何设置?
答案:
/* 标准盒模型 (content-box) */
.box {
box-sizing: content-box;
width: 200px;
padding: 20px;
border: 5px solid #000;
margin: 10px;
/* 实际宽度 = 200 + 20*2 + 5*2 = 250px */
}
/* IE 盒模型 (border-box) */
.box {
box-sizing: border-box;
width: 200px;
padding: 20px;
border: 5px solid #000;
margin: 10px;
/* 实际宽度 = 200px (content = 200 - 20*2 - 5*2 = 150px) */
}
/* 全局设置 */
* {
box-sizing: border-box;
}
盒模型组成部分:
┌─────────────────────────────────┐
│ margin (外边距) │
│ ┌─────────────────────────┐ │
│ │ border (边框) │ │
│ │ ┌───────────────────┐ │ │
│ │ │ padding (内边距) │ │ │
│ │ │ ┌─────────────┐ │ │ │
│ │ │ │ content │ │ │ │
│ │ │ │ (内容) │ │ │ │
│ │ │ └─────────────┘ │ │ │
│ │ └───────────────────┘ │ │
│ └─────────────────────────┘ │
└─────────────────────────────────┘
考点:
- content-box: width 不包含 padding 和 border
- border-box: width 包含 padding 和 border
- 推荐全局设置
box-sizing: border-box
3.3 BFC
Q13: 什么是 BFC?如何触发 BFC?BFC 的应用场景有哪些?
答案:
/* BFC (Block Formatting Context) 块级格式化上下文 */
/* 是一个独立的渲染区域,内部元素的布局不受外部影响 */
/* 触发 BFC 的方式 */
.bfc {
/* 1. float 不为 none */
float: left;
/* 2. position 为 absolute 或 fixed */
position: absolute;
/* 3. overflow 不为 visible */
overflow: hidden;
/* 4. display 为 inline-block、table-cell、flex、grid */
display: flex;
/* 5. contain 为 layout、paint 或 strict */
contain: layout;
}
/* 应用场景 1: 清除浮动 */
.clearfix {
overflow: hidden; /* 触发 BFC 清除浮动 */
}
/* 应用场景 2: 防止 margin 重叠 */
.container {
overflow: hidden; /* 触发 BFC */
}
.child {
margin: 20px; /* 不会与外部元素 margin 重叠 */
}
/* 应用场景 3: 两栏布局 */
.left {
float: left;
width: 200px;
}
.right {
overflow: hidden; /* 触发 BFC,不与浮动元素重叠 */
}
/* 应用场景 4: 防止元素被浮动元素覆盖 */
.float {
float: left;
}
.bfc {
overflow: hidden; /* 触发 BFC,自适应宽度 */
}
BFC 的特性:
- 内部元素的 margin 不会与外部重叠
- 计算高度时,浮动元素也会计算
- 不会与浮动元素重叠
- 独立的渲染区域,内部元素不受外部影响
考点:
- BFC 是独立的渲染区域
- 常用触发方式:overflow: hidden、float、position、display: flex
- 应用场景:清除浮动、防止 margin 重叠、两栏布局
3.4 Flex 布局
Q14: Flex 布局的常用属性有哪些?如何实现水平垂直居中?
答案:
/* Flex 容器属性 */
.container {
display: flex;
/* 方向 */
flex-direction: row; /* row | row-reverse | column | column-reverse */
/* 换行 */
flex-wrap: nowrap; /* nowrap | wrap | wrap-reverse */
/* 主轴对齐 */
justify-content: flex-start; /* flex-start | flex-end | center | space-between | space-around | space-evenly */
/* 交叉轴对齐 */
align-items: stretch; /* stretch | flex-start | flex-end | center | baseline */
/* 多行对齐 */
align-content: stretch; /* stretch | flex-start | flex-end | center | space-between | space-around | space-evenly */
}
/* Flex 项目属性 */
.item {
/* 放大比例 */
flex-grow: 0; /* 默认 0,不放大 */
/* 缩小比例 */
flex-shrink: 1; /* 默认 1,可以缩小 */
/* 初始大小 */
flex-basis: auto; /* auto | 具体值 */
/* 简写 */
flex: 0 1 auto; /* flex-grow flex-shrink flex-basis */
}
/* 水平垂直居中 */
/* 方法 1: Flex */
.center {
display: flex;
justify-content: center;
align-items: center;
}
/* 方法 2: Grid */
.center {
display: grid;
place-items: center;
}
/* 方法 3: 绝对定位 + transform */
.center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* 三栏布局(圣杯布局/双飞翼布局) */
.container {
display: flex;
}
.left {
width: 200px;
flex-shrink: 0;
}
.right {
width: 200px;
flex-shrink: 0;
}
.center {
flex: 1; /* 占据剩余空间 */
}
考点:
- flex-direction 决定主轴方向
- justify-content 控制主轴对齐
- align-items 控制交叉轴对齐
- flex: 1 是 flex: 1 1 0% 的简写
3.5 Grid 布局
Q15: Grid 布局的基本用法?如何实现响应式布局?
答案:
/* Grid 基础 */
.container {
display: grid;
/* 定义列 */
grid-template-columns: 200px 1fr 200px; /* 三列:固定、自适应、固定 */
grid-template-columns: repeat(3, 1fr); /* 三列等宽 */
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); /* 响应式 */
/* 定义行 */
grid-template-rows: 100px auto;
/* 间距 */
gap: 20px;
column-gap: 20px;
row-gap: 20px;
/* 对齐 */
justify-items: center; /* 水平对齐 */
align-items: center; /* 垂直对齐 */
justify-content: center; /* 网格容器对齐 */
align-content: center;
}
/* Grid 项目 */
.item {
/* 指定位置 */
grid-column: 1 / 3; /* 从第 1 条线到第 3 条线 */
grid-row: 1 / 2;
/* 简写 */
grid-area: 1 / 1 / 2 / 3; /* row-start / col-start / row-end / col-end */
/* 跨列跨行 */
grid-column: span 2; /* 跨 2 列 */
grid-row: span 2;
}
/* 响应式布局 */
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
/* 媒体查询调整 */
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr; /* 单列 */
}
}
/* 经典布局:圣杯布局 */
.container {
display: grid;
grid-template-areas:
"header header header"
"nav main aside"
"footer footer footer";
grid-template-columns: 200px 1fr 200px;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
.header { grid-area: header; }
.nav { grid-area: nav; }
.main { grid-area: main; }
.aside { grid-area: aside; }
.footer { grid-area: footer; }
考点:
- grid-template-columns 定义列
- repeat() 函数简化重复定义
- auto-fit + minmax() 实现响应式
- grid-area 命名区域,便于布局
四、性能优化与工程化
4.1 性能优化
Q16: 前端性能优化有哪些策略?
答案:
// 1. 网络优化
// - 减少 HTTP 请求
// - 使用 CDN
// - 开启 Gzip 压缩
// - 使用 HTTP/2
// - 预加载关键资源
<link rel="preload" href="critical.css" as="style">
<link rel="prefetch" href="next-page.js">
// 2. 资源优化
// - 图片优化:WebP、懒加载、响应式图片
<img src="image.webp" loading="lazy" srcset="image-400w.webp 400w, image-800w.webp 800w">
// - 代码分割
// Webpack
const Home = () => import('./views/Home.vue');
// Vite
const Home = defineAsyncComponent(() => import('./views/Home.vue'));
// - Tree Shaking
// 只打包使用的代码
import { debounce } from 'lodash-es'; // 使用 lodash-es 而不是 lodash
// 3. 渲染优化
// - 虚拟列表
<virtual-list :items="largeList" :item-height="50" />
// - 防抖节流
import { debounce, throttle } from 'lodash-es';
const handleScroll = debounce(() => {
console.log('scroll');
}, 300);
const handleResize = throttle(() => {
console.log('resize');
}, 300);
// - 避免不必要的响应式
// Vue 3
const asRef = shallowRef({}); // 浅层响应式
const computed = computed(() => {}); // 计算属性缓存
// 4. 缓存策略
// - HTTP 缓存
// Cache-Control: max-age=31536000
// - Service Worker 缓存
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
// - LocalStorage / IndexedDB
localStorage.setItem('key', 'value');
// 5. 首屏优化
// - 骨架屏
<Skeleton />
// - 关键 CSS 内联
<style>
/* 关键 CSS */
</style>
// - 异步加载非关键资源
<script defer src="non-critical.js"></script>
// 6. 代码优化
// - 避免重排重绘
// 使用 transform 和 opacity 代替 top/left
.element {
transform: translateX(100px);
opacity: 0.5;
}
// - 使用 requestAnimationFrame
function animate() {
requestAnimationFrame(animate);
// 动画逻辑
}
requestAnimationFrame(animate);
// - 使用 Web Worker 处理复杂计算
const worker = new Worker('worker.js');
worker.postMessage({ data: largeData });
worker.onmessage = (e) => {
console.log(e.data);
};
性能优化清单:
| 优化方向 | 具体措施 |
|---|---|
| 网络层 | CDN、HTTP/2、Gzip、预加载 |
| 资源层 | 图片优化、代码分割、Tree Shaking |
| 渲染层 | 虚拟列表、防抖节流、避免重排 |
| 缓存层 | HTTP 缓存、Service Worker、LocalStorage |
| 代码层 | 算法优化、Web Worker、requestAnimationFrame |
考点:
- 性能优化从网络、资源、渲染、缓存多方面入手
- 首屏加载速度是最重要的指标
- 避免重排重绘是渲染优化的关键
4.2 Webpack 优化
Q17: Webpack 有哪些优化手段?
答案:
// webpack.config.js
const path = require('path');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
// 1. mode 模式
mode: 'production', // production | development
// 2. 入口优化
entry: {
main: './src/main.js',
vendor: ['vue', 'vue-router', 'pinia'] // 提取公共库
},
// 3. 输出优化
output: {
filename: '[name].[contenthash:8].js', // 内容哈希
chunkFilename: '[name].[contenthash:8].js',
path: path.resolve(__dirname, 'dist')
},
// 4. Loader 优化
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true // 开启缓存
}
}
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
},
{
test: /\.(png|jpg|jpeg|gif|webp)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024 // 小于 10KB 转 base64
}
}
}
]
},
// 5. Plugin 优化
plugins: [
// 代码分割
new BundleAnalyzerPlugin(),
// 压缩 CSS
new CssMinimizerPlugin(),
// 压缩 JS
new TerserPlugin({
parallel: true, // 多进程压缩
terserOptions: {
compress: {
drop_console: true // 删除 console
}
}
}),
// 提取 CSS
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css'
})
],
// 6. 优化配置
optimization: {
// 代码分割
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10
},
common: {
name: 'common',
minChunks: 2,
priority: 5
}
}
},
// 运行时代码提取
runtimeChunk: {
name: 'runtime'
},
// Tree Shaking
usedExports: true,
sideEffects: false
},
// 7. 缓存优化
cache: {
type: 'filesystem' // 文件系统缓存
},
// 8. 并行处理
parallelism: os.cpus().length - 1
};
Webpack 优化策略:
- mode: 生产模式自动启用优化
- entry: 提取公共库为单独 chunk
- output: 使用 contenthash 实现长效缓存
- loader: 开启 cacheDirectory
- plugin: 压缩代码、提取 CSS
- optimization: 代码分割、Tree Shaking
- cache: 文件系统缓存加速构建
- parallelism: 多进程构建
考点:
- splitChunks 实现代码分割
- contenthash 实现长效缓存
- Tree Shaking 删除未使用的代码
- 多进程构建加速
4.3 Vite 优势
Q18: Vite 相比 Webpack 有什么优势?
答案:
// Vite 的优势
// 1. 开发服务器启动快
// Webpack: 需要打包整个应用
// Vite: 使用 esbuild 预构建,原生 ESM 按需加载
// 2. HMR 更新快
// Webpack: 重新打包整个模块
// Vite: 只更新变化的模块,浏览器重新请求
// 3. 配置简单
// Webpack: 复杂的 loader 和 plugin 配置
// Vite: 约定优于配置,开箱即用
// 4. 构建性能
// Vite 使用 Rollup + esbuild
// esbuild 是 Go 编写,比 Webpack (JS) 快 10-100 倍
// Vite 配置示例
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
export default defineConfig({
plugins: [vue()],
// 路径别名
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
// 开发服务器配置
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
},
// 构建配置
build: {
target: 'es2015',
outDir: 'dist',
assetsDir: 'assets',
// 代码分割
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'element-plus': ['element-plus']
}
}
},
// 压缩
minify: 'terser',
terserOptions: {
compress: {
drop_console: true
}
},
// chunk 大小警告
chunkSizeWarningLimit: 1000
},
// CSS 配置
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
}
}
});
Vite vs Webpack 对比:
| 特性 | Webpack | Vite |
|---|---|---|
| 开发启动 | 慢(需打包) | 快(按需加载) |
| HMR | 较慢 | 极快 |
| 配置复杂度 | 高 | 低 |
| 构建速度 | 中等 | 快 |
| 生态 | 成熟 | 快速发展 |
| 兼容性 | 更好 | 需要现代浏览器 |
考点:
- Vite 使用原生 ESM,开发体验好
- esbuild 提供极速构建
- Rollup 提供优化的生产构建
- Vite 是前端构建工具的未来趋势
五、算法与数据结构
5.1 常见算法
Q19: 实现防抖和节流函数
答案:
// 防抖:延迟执行,重复触发会重置计时器
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 使用
const handleInput = debounce((e) => {
console.log('Input:', e.target.value);
}, 300);
input.addEventListener('input', handleInput);
// 节流:固定时间间隔执行
function throttle(fn, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 使用
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
}, 100);
window.addEventListener('scroll', handleScroll);
// 节流(时间戳 + 定时器版本)
function throttleAdvanced(fn, delay) {
let timer = null;
let lastTime = 0;
return function(...args) {
const now = Date.now();
const remaining = delay - (now - lastTime);
clearTimeout(timer);
if (remaining <= 0) {
fn.apply(this, args);
lastTime = now;
} else {
timer = setTimeout(() => {
fn.apply(this, args);
lastTime = Date.now();
}, remaining);
}
};
}
考点:
- 防抖:延迟执行,适合输入框搜索
- 节流:固定频率执行,适合滚动、resize
- 考虑立即执行和最后一次执行
Q20: 实现深拷贝函数
答案:
function deepClone(obj, map = new WeakMap()) {
// 处理原始类型和 null
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理循环引用
if (map.has(obj)) {
return map.get(obj);
}
// 处理 Date
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// 处理 RegExp
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// 处理 Map
if (obj instanceof Map) {
const cloned = new Map();
map.set(obj, cloned);
obj.forEach((value, key) => {
cloned.set(deepClone(key, map), deepClone(value, map));
});
return cloned;
}
// 处理 Set
if (obj instanceof Set) {
const cloned = new Set();
map.set(obj, cloned);
obj.forEach(value => {
cloned.add(deepClone(value, map));
});
return cloned;
}
// 处理 Array
if (Array.isArray(obj)) {
const cloned = [];
map.set(obj, cloned);
for (let i = 0; i < obj.length; i++) {
cloned[i] = deepClone(obj[i], map);
}
return cloned;
}
// 处理 Object
const cloned = {};
map.set(obj, cloned);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key], map);
}
}
// 处理 Symbol
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (const key of symbolKeys) {
cloned[key] = deepClone(obj[key], map);
}
return cloned;
}
// 测试
const obj = {
a: 1,
b: { c: 2 },
d: [3, 4],
e: new Date(),
f: /test/g,
g: new Map([['key', 'value']]),
h: new Set([1, 2, 3])
};
obj.self = obj; // 循环引用
const cloned = deepClone(obj);
console.log(cloned);
考点:
- 使用 WeakMap 处理循环引用
- 处理各种内置对象:Date、RegExp、Map、Set
- 处理 Symbol 属性
- 递归复制所有层级
Q21: 实现数组去重
答案:
// 方法 1: Set
function unique1(arr) {
return [...new Set(arr)];
}
// 方法 2: filter + indexOf
function unique2(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index);
}
// 方法 3: filter + includes
function unique3(arr) {
return arr.filter((item, index) => !arr.slice(0, index).includes(item));
}
// 方法 4: Map
function unique4(arr) {
const map = new Map();
return arr.filter(item => !map.has(item) && map.set(item, true));
}
// 方法 5: 对象去重(根据某个属性)
function uniqueBy(arr, key) {
const map = new Map();
return arr.filter(item => {
const k = item[key];
return !map.has(k) && map.set(k, true);
});
}
// 测试
const arr = [1, 2, 2, 3, 4, 4, 5];
console.log(unique1(arr)); // [1, 2, 3, 4, 5]
const users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
{ id: 1, name: 'John' }
];
console.log(uniqueBy(users, 'id')); // [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
考点:
- Set 是最简单的方法
- filter + indexOf 适合简单场景
- Map 方法性能最好
- 对象去重需要根据 key 判断
5.2 数据结构
Q22: 实现一个 LRU 缓存
答案:
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) {
return -1;
}
// 更新使用顺序
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
// 删除最久未使用的(第一个)
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}
// 测试
const lru = new LRUCache(2);
lru.put(1, 'a');
lru.put(2, 'b');
console.log(lru.get(1)); // 'a'
lru.put(3, 'c'); // 删除 key 2
console.log(lru.get(2)); // -1
console.log(lru.get(3)); // 'c'
console.log(lru.get(1)); // 'a'
考点:
- 使用 Map 保持插入顺序
- get 操作需要更新使用顺序
- put 操作需要检查容量并删除最久未使用的
六、场景题与设计题
6.1 场景题
Q23: 如何实现一个虚拟列表?
答案:
<template>
<div class="virtual-list" @scroll="handleScroll" ref="container">
<div class="phantom" :style="{ height: `${totalHeight}px` }"></div>
<div class="content" :style="{ transform: `translateY(${offsetY}px)` }">
<div
v-for="item in visibleItems"
:key="item.id"
class="item"
:style="{ height: `${itemHeight}px` }"
>
{{ item.content }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
const props = defineProps({
items: Array,
itemHeight: {
type: Number,
default: 50
},
containerHeight: {
type: Number,
default: 500
}
});
const container = ref(null);
const scrollTop = ref(0);
const totalHeight = computed(() => props.items.length * props.itemHeight);
const visibleCount = computed(() => Math.ceil(props.containerHeight / props.itemHeight));
const startIndex = computed(() => Math.floor(scrollTop.value / props.itemHeight));
const endIndex = computed(() => Math.min(startIndex.value + visibleCount.value + 2, props.items.length));
const offsetY = computed(() => startIndex.value * props.itemHeight);
const visibleItems = computed(() => {
return props.items.slice(startIndex.value, endIndex.value).map((item, index) => ({
...item,
id: startIndex.value + index
}));
});
function handleScroll(e) {
scrollTop.value = e.target.scrollTop;
}
onMounted(() => {
if (container.value) {
container.value.style.height = `${props.containerHeight}px`;
}
});
</script>
<style scoped>
.virtual-list {
overflow-y: auto;
position: relative;
}
.phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.content {
position: absolute;
left: 0;
right: 0;
top: 0;
}
.item {
box-sizing: border-box;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
padding: 0 16px;
}
</style>
考点:
- 只渲染可视区域的元素
- 使用 phantom 元素撑开滚动容器
- 根据滚动位置计算可见元素
- translateY 实现视窗定位
Q24: 如何实现一个图片懒加载?
答案:
// 方法 1: Intersection Observer
class LazyLoad {
constructor(options = {}) {
this.defaults = {
selector: 'img[data-src]',
rootMargin: '0px',
threshold: 0.1,
...options
};
this.init();
}
init() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const src = img.dataset.src;
if (src) {
img.src = src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
}
});
}, {
rootMargin: this.defaults.rootMargin,
threshold: this.defaults.threshold
});
const images = document.querySelectorAll(this.defaults.selector);
images.forEach(img => observer.observe(img));
}
}
// 使用
new LazyLoad({
selector: 'img[data-src]',
rootMargin: '100px'
});
// 方法 2: Vue 指令
const lazyLoadDirective = {
mounted(el, binding) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const src = binding.value;
if (src) {
img.src = src;
observer.unobserve(img);
}
}
});
});
observer.observe(el);
el._observer = observer;
},
unmounted(el) {
if (el._observer) {
el._observer.disconnect();
}
}
};
// 使用
<img v-lazy-load="imageUrl" />
考点:
- 使用 Intersection Observer API
- rootMargin 提前加载
- 加载后取消观察
- Vue 指令封装
6.2 设计题
Q25: 设计一个组件库的架构
答案:
my-component-lib/
├── packages/
│ ├── components/ # 组件库
│ │ ├── button/
│ │ │ ├── src/
│ │ │ │ ├── Button.vue
│ │ │ │ └── index.ts
│ │ │ └── package.json
│ │ ├── input/
│ │ └── ...
│ ├── theme/ # 主题系统
│ │ ├── src/
│ │ │ ├── variables.scss
│ │ │ └── index.ts
│ │ └── package.json
│ └── utils/ # 工具函数
│ └── package.json
├── docs/ # 文档
├── examples/ # 示例
├── scripts/ # 构建脚本
├── .eslintrc.js
├── .prettierrc.js
├── tsconfig.json
├── vite.config.ts
└── package.json
架构设计要点:
- Monorepo 管理:使用 pnpm workspace
- 组件独立:每个组件独立打包
- 主题系统:CSS 变量 + SCSS 混入
- TypeScript:类型安全
- 单元测试:Vitest + Vue Test Utils
- 文档生成:VitePress
- CI/CD:GitHub Actions
考点:
- Monorepo 架构便于管理
- 组件独立打包按需引入
- 主题系统支持定制
- 完整的测试和文档
七、字节跳动特色题
Q26: 手写 Promise.all
答案:
Promise.myAll = function(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('Argument must be an array'));
}
const results = [];
let completedCount = 0;
const length = promises.length;
if (length === 0) {
return resolve(results);
}
promises.forEach((promise, index) => {
Promise.resolve(promise).then(
value => {
results[index] = value;
completedCount++;
if (completedCount === length) {
resolve(results);
}
},
error => {
reject(error);
}
);
});
});
};
// 测试
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);
Promise.myAll([p1, p2, p3]).then(console.log); // [1, 2, 3]
考点:
- 处理非数组输入
- 处理空数组
- 保持结果顺序
- 一个失败全部失败
Q27: 手写 EventEmitter
答案:
class EventEmitter {
constructor() {
this.events = {};
}
on(event, listener) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(listener);
return this;
}
once(event, listener) {
const onceWrapper = (...args) => {
this.off(event, onceWrapper);
listener(...args);
};
this.on(event, onceWrapper);
return this;
}
off(event, listener) {
if (!this.events[event]) {
return this;
}
if (!listener) {
delete this.events[event];
return this;
}
this.events[event] = this.events[event].filter(l => l !== listener);
return this;
}
emit(event, ...args) {
if (!this.events[event]) {
return false;
}
this.events[event].forEach(listener => {
listener(...args);
});
return true;
}
}
// 测试
const emitter = new EventEmitter();
emitter.on('test', (data) => {
console.log('Listener 1:', data);
});
emitter.once('test', (data) => {
console.log('Listener 2:', data);
});
emitter.emit('test', 'Hello'); // 两个 listener 都执行
emitter.emit('test', 'World'); // 只有 listener 1 执行
考点:
- on 订阅事件
- once 只执行一次
- off 取消订阅
- emit 触发事件
八、华为荣耀特色题
Q28: 手写 new 操作符
答案:
function myNew(constructor, ...args) {
// 1. 创建新对象
const obj = Object.create(constructor.prototype);
// 2. 执行构造函数,绑定 this
const result = constructor.apply(obj, args);
// 3. 返回对象
return result instanceof Object ? result : obj;
}
// 测试
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
const person = myNew(Person, 'John', 30);
console.log(person.name); // 'John'
person.sayHello(); // 'Hello, I'm John'
考点:
- 创建新对象并继承原型
- 绑定 this 执行构造函数
- 处理构造函数返回值
Q29: 手写 instanceof
答案:
function myInstanceof(left, right) {
const prototype = right.prototype;
left = Object.getPrototypeOf(left);
while (true) {
if (left === null || left === undefined) {
return false;
}
if (left === prototype) {
return true;
}
left = Object.getPrototypeOf(left);
}
}
// 测试
console.log(myInstanceof([], Array)); // true
console.log(myInstanceof({}, Object)); // true
console.log(myInstanceof([], Object)); // true
console.log(myInstanceof({}, Array)); // false
考点:
- 获取原型链
- 遍历原型链
- 判断是否等于构造函数的 prototype
总结
面试准备建议
- 基础扎实:深入理解 JS、CSS、HTML 基础
- 框架原理:掌握 Vue 3 响应式、虚拟 DOM、生命周期
- 算法能力:常见算法和数据结构
- 工程化:Webpack、Vite、性能优化
- 项目经验:能够讲解项目难点和解决方案
面试技巧
- 先说思路,再写代码
- 考虑边界情况
- 优化代码性能
- 主动沟通,不懂就问
祝面试顺利!🎉
整理者:前端技术爱好者
更新日期:2026 年 3 月
参考来源:各大厂面试真题 + 技术博客