2025-2026 大厂前端经典面试题库(京东、字节、华为、荣耀)

8 阅读14分钟

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' 是历史遗留 bug
  • typeof 无法区分数组和对象
  • 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 2Vue 3
实现方式Object.definePropertyProxy
数组监听需要重写数组方法原生支持
对象新增属性需要 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 2Vue 3 (Options API)Vue 3 (Composition API)说明
beforeCreatebeforeCreatesetup()实例创建前
createdcreatedsetup()实例创建后
beforeMountbeforeMountonBeforeMount挂载前
mountedmountedonMounted挂载完成
beforeUpdatebeforeUpdateonBeforeUpdate更新前
updatedupdatedonUpdated更新完成
beforeDestroybeforeUnmountonBeforeUnmount销毁前
destroyedunmountedonUnmounted销毁完成
activatedactivatedonActivatedkeep-alive 激活
deactivateddeactivatedonDeactivatedkeep-alive 停用

考点:

  • Vue 3 移除了 beforeCreatecreated,统一使用 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 算法优化策略:

  1. 只比较同一层级:不会跨层级比较
  2. Key 的作用:通过 key 识别节点,减少不必要的 DOM 操作
  3. 双端比较:Vue 2 使用双端比较算法
  4. 最长递增子序列:Vue 3 使用 LIS 算法进一步优化
  5. 静态提升:静态节点不会参与 diff
  6. 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>

区别对比:

特性computedwatch
缓存
惰性求值
返回值必须返回不需要
副作用不推荐推荐
异步操作不支持支持
依赖追踪自动追踪需要指定

考点:

  • 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>&copy; 2026 Company</p>
</footer>

语义化的好处:

  1. SEO 优化:搜索引擎更好理解页面结构
  2. 可访问性:屏幕阅读器更好识别内容
  3. 代码可读性:开发者更容易理解页面结构
  4. 样式与结构分离:便于维护和重构

考点:

  • 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 的特性:

  1. 内部元素的 margin 不会与外部重叠
  2. 计算高度时,浮动元素也会计算
  3. 不会与浮动元素重叠
  4. 独立的渲染区域,内部元素不受外部影响

考点:

  • 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 优化策略:

  1. mode: 生产模式自动启用优化
  2. entry: 提取公共库为单独 chunk
  3. output: 使用 contenthash 实现长效缓存
  4. loader: 开启 cacheDirectory
  5. plugin: 压缩代码、提取 CSS
  6. optimization: 代码分割、Tree Shaking
  7. cache: 文件系统缓存加速构建
  8. 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 对比:

特性WebpackVite
开发启动慢(需打包)快(按需加载)
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

架构设计要点:

  1. Monorepo 管理:使用 pnpm workspace
  2. 组件独立:每个组件独立打包
  3. 主题系统:CSS 变量 + SCSS 混入
  4. TypeScript:类型安全
  5. 单元测试:Vitest + Vue Test Utils
  6. 文档生成:VitePress
  7. 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

总结

面试准备建议

  1. 基础扎实:深入理解 JS、CSS、HTML 基础
  2. 框架原理:掌握 Vue 3 响应式、虚拟 DOM、生命周期
  3. 算法能力:常见算法和数据结构
  4. 工程化:Webpack、Vite、性能优化
  5. 项目经验:能够讲解项目难点和解决方案

面试技巧

  1. 先说思路,再写代码
  2. 考虑边界情况
  3. 优化代码性能
  4. 主动沟通,不懂就问

祝面试顺利!🎉


整理者:前端技术爱好者
更新日期:2026 年 3 月
参考来源:各大厂面试真题 + 技术博客