收集一点面试题。说实话我是感觉没啥用的大部分记住了以后就会忘记除非用的时候再想起,也是没办法。
大致的面试方向让GPT列了一下,用于对照有没有缺的:
路线表:
第一部分:前端基础(The Core Fundamentals)
1. JavaScript (重中之重)
核心概念:
- 数据类型:基本类型 vs 引用类型,
nullvsundefined,Symbol和BigInt。 - 类型转换:隐式转换规则(特别是
==和+),显式转换方法。 - 作用域 (Scope) 与作用域链:全局作用域、函数作用域、块级作用域 (
let/const)。 - 闭包 (Closure):是什么、为什么、应用场景(防抖、节流、模块化)、可能导致的内存泄漏。
this指向:不同场景下(全局、函数、对象方法、箭头函数、call/apply/bind)的this指向问题。- 原型 (Prototype) 与原型链:
__proto__、prototype、constructor的关系,继承的实现方式。 - 执行上下文 (Execution Context) 与调用栈 (Call Stack)。
- 数据类型:基本类型 vs 引用类型,
异步编程:
- 事件循环 (Event Loop):宏任务 (Macrotask) 与微任务 (Microtask) 的区别和执行顺序(非常高频的考点)。
- Promise:三种状态、
then/catch/finally,Promise.all/race/any的用法和区别。 async/await:语法糖的本质,如何处理错误(try...catch)。
ES6+ 新特性:
let/constvsvar的区别。- 箭头函数及其
this。 - 解构赋值、模板字符串、默认参数。
Map/SetvsObject/Array。- 模块化 (
import/export)。 Class语法糖。
手写代码题(高频):
- 防抖 (Debounce) 和节流 (Throttle)。
- 深拷贝 (Deep Clone)。
- 实现
call/apply/bind。 - 实现一个
Promise或Promise.all。 - 数组扁平化、去重。
- 实现一个事件发布/订阅模型 (Event Emitter)。
2. CSS (Cascading Style Sheets)
- 核心概念:
- 盒子模型 (Box Model):标准盒模型 vs IE 盒模型 (
box-sizing)。 - 选择器与优先级:
!important > 内联 > ID > 类/属性/伪类 > 标签 > 通配符。 - BFC (块级格式化上下文):是什么、触发条件、应用场景(清除浮动、防止 margin 塌陷)。
- 层叠上下文 (Stacking Context):
z-index的工作原理。
- 盒子模型 (Box Model):标准盒模型 vs IE 盒模型 (
- 布局 (Layout):
- Flexbox 布局:主轴、交叉轴、
flex-direction,justify-content,align-items,flex属性。 - Grid 布局:二维布局,
grid-template-columns/rows,grid-gap,grid-area。 - 定位 (Positioning):
static,relative,absolute,fixed,sticky的区别和用途。 - 浮动 (Float):以及如何清除浮动。
- Flexbox 布局:主轴、交叉轴、
- 进阶与工程化:
- CSS 动画:
transition和animation的区别。 - 预处理器:Sass/Less/Stylus(变量、Mixin、嵌套等)。
- CSS Modules / CSS-in-JS:解决 CSS 全局污染和作用域问题。
- 响应式设计:媒体查询 (
@media)。
- CSS 动画:
3. HTML (HyperText Markup Language)
- 语义化标签:
header,footer,nav,section,article,aside的使用场景,好处是什么(SEO、可访问性)。 - HTML5 新特性:
canvas,video,audio, 新的表单类型。 - DOCTYPE 的作用:触发标准模式。
- 可访问性 (Accessibility, a11y):
alt属性,aria-*属性的角色。 - SEO (搜索引擎优化):
title,meta标签的优化。
第二部分:框架与库 (Frameworks & Libraries)
通常要求至少精通一个主流框架。
- React / Vue (选择一个深入)
- 核心思想:声明式 UI、组件化、数据驱动视图。
- Virtual DOM:是什么,为什么需要它,Diff 算法(非常重要,需要理解其 key 的作用和 diff 过程)。
- 组件生命周期:创建、更新、销毁各个阶段的钩子函数。
- 组件通信:父子、子父、兄弟、跨级通信的各种方式。
- 状态管理:
- React:
useState,useReducer,Context API,以及 Redux / Zustand / MobX 的原理和使用。 - Vue:
props,$emit, EventBus, Vuex / Pinia 的原理和使用。
- React:
- React Hooks vs. Vue Composition API:它们的优势和解决了什么问题。
- 原理深挖:
- React: Fiber 架构、Scheduler、合成事件。
- Vue: 响应式原理(
Object.definePropertyvsProxy)。
第三部分:工程化与工具链 (Engineering & Tooling)
这部分体现你的工程能力和开发效率。
- 构建工具:
- Webpack:核心概念(Entry, Output, Loader, Plugin, Mode),工作流程,常见优化(代码分割、Tree Shaking、打包速度优化)。
- Vite:与 Webpack 的区别,为什么快(ESM、esbuild)。
- 包管理器:
npm/yarn/pnpm的区别,package.json和package-lock.json的作用。 - 代码规范与质量:ESLint (代码检查), Prettier (代码格式化), TypeScript (类型系统)。
- Git 版本控制:常用命令 (
mergevsrebase,cherry-pick,reset),分支管理策略 (Git Flow)。 - 前端测试:单元测试 (Jest/Vitest), E2E 测试 (Cypress/Playwright)。
第四部分:浏览器与网络 (Browser & Network)
这部分是高级和资深岗位的分水岭。
- 浏览器渲染原理:
- 关键渲染路径 (Critical Rendering Path):从输入 URL到页面展示的全过程(DNS 查询 -> TCP 握手 -> HTTP 请求 -> 解析 HTML -> 构建 DOM 树 -> 构建 CSSOM 树 -> 构建渲染树 -> 布局 -> 绘制)。
- 重排 (Reflow/Layout) 与 重绘 (Repaint):是什么,区别,如何减少。
- 网络协议:
- HTTP/HTTPS:状态码 (200, 301, 304, 403, 404, 500 等),请求方法 (GET/POST 区别),Headers (Cache-Control, ETag, Cookie, Content-Type 等)。
- HTTP 缓存:强缓存和协商缓存的机制和区别。
- 跨域 (CORS):原因和解决方案(JSONP, CORS, Nginx 反向代理)。
- TCP/IP:三次握手、四次挥手。
- 浏览器存储:
Cookie/localStorage/sessionStorage/IndexedDB的区别和应用场景。 - 安全性:XSS (跨站脚本攻击), CSRF (跨站请求伪造) 的原理和防范。
第五部分:性能优化 (Performance Optimization)
衡量一个前端工程师能力的重要标准。
- 加载性能优化:
- 资源压缩与合并:JS/CSS/HTML 压缩,图片压缩(WebP 格式)。
- 代码分割 (Code Splitting) 和 按需加载 (Lazy Loading)。
- Tree Shaking:移除无用代码。
- 使用 CDN。
- 渲染性能优化:
- 减少重排和重绘。
- 使用
requestAnimationFrame处理动画。 - 长列表的虚拟滚动 (Virtual Scrolling)。
- 性能监控与分析工具:
- Chrome DevTools (Lighthouse, Performance, Network 面板)。
window.performanceAPI。
第六部分:算法、数据结构与设计模式
大厂尤其看重这部分。
- 数据结构:数组、链表、栈、队列、哈希表、树(特别是二叉树)。要能理解其概念并知道在 JS 中如何实现。
- 算法:
- 排序算法(冒泡、快排)。
- 查找算法(二分查找)。
- 树的遍历(深度优先 DFS, 广度优先 BFS),这在处理 DOM 树时很常见。
- LeetCode 上的简单和中等难度的题目,特别是字符串和数组相关的。
- 设计模式:单例模式、工厂模式、观察者模式(发布-订阅)、装饰器模式等在前端中的应用。
在浏览器中输入地址回车后会发生什么
第一阶段:浏览器处理
URL 解析
缓存检查
强制缓存和协商缓存
第二阶段:网络连接
- DNS 域名解析
域名转ip地址
4.建立 TCP 连接
- 建立 TLS 连接 (仅限 HTTPS)
第三阶段:服务器处理
发送 HTTP 请求
服务器处理请求并响应
第四阶段:浏览器渲染
解析资源,构建 DOM/CSSOM
构建渲染树
布局
绘制
合成
项目中的图片懒加载怎么实现
方法一:浏览器原生懒加载 (loading="lazy") - 推荐首选
经我的实验验证,确实有用,但是条件很宽松,离视口很远的话才会触发懒加载不是不在视口就懒加载,而且听说网络条件好也会直接加载。缺点不好控制细节行为。
方法二:使用 JavaScript IntersectionObserver API - 最灵活、最推荐的自定义方案
不要将真实的图片地址放在 src 中,而是放在一个自定义的 data-src 属性里。src 可以放一个占位图,或者留空。监听视口轮到图片就挂上。
方法三:基于 scroll 事件监听 + getBoundingClientRect() - 传统方法
[] == ![] 为什么是 true****?
解析(这是隐式转换规则(特别是 == 和 +)与 显式转换方法的问题):
![]:首先计算!运算符。[]是一个对象,在布尔上下文中是true。所以![]就是false。- 表达式变为:
[] == false。 - 根据规则
boolean == 任何类型,将boolean转换为number。false转换为0。 - 表达式变为:
[] == 0。 - 根据规则
对象 == number,将对象[]通过ToPrimitive转换为原始值。提示是number。- 调用
[].valueOf(),返回[]本身(不是原始值)。 - 继续调用
[].toString(),返回''(空字符串)。
- 调用
- 表达式变为:
'' == 0。 - 根据规则
string == number,将string转换为number。''转换为0。 - 表达式变为:
0 == 0。 - 结果:
true。
闭包 (Closure)是什么
当一个函数能够记住并访问它所在的词法作用域时,就产生了闭包,即使函数是在其当前词法作用域之外执行的。
当你(函数)离开房间(词法作用域)去别的地方执行时,这个背包让你依然能访问到房间里的那些物品(变量)。
function makeCounter() {
let count = 0; // count 位于 makeCounter 的词法作用域内
// 这个内部函数就是一个闭包
return function() {
count++; // 它可以访问并修改外部作用域的 count 变量
return count;
};
}调用函数后,返回匿名函数,虽然理论要销毁 count 这个变量,因为匿名函数和原来词法作用域里的变量仍然有联系引用,通过垃圾回收机制不去回收联系的变量,这样变量就被封闭住了。
优点:
1.提供私有变量,实现数据封装和隐藏 2.让变量的状态得以持久化
应用场景:
防抖节流模块化
缺点:内存泄漏,因为不怎么回收
执行上下文 (Execution Context) 与调用栈 (Call Stack)
JavaScript 代码执行的底层机制:执行上下文 和 调用栈。 **执行上下文:**每当一段代码准备执行时,JS 引擎都会为它创建一个执行上下文。
执行上下文主要有三种:1.全局执行上下文 2.函数执行上下文 3.Eval函数执行上下文
创建阶段:
1.创建词法环境
- 它记录了当前作用域内的所有变量、函数声明以及对外部环境的引用(这就是作用域链的基础)。
- 对于
let和const声明的变量,它们会被创建但不会被初始化,处于“未初始化”状态。这就是为什么在声明前访问它们会报错(暂时性死区,TDZ)。 - 对于
function声明,它会被完整创建并初始化,这就是为什么函数声明可以在定义之前被调用(函数提升)。
tips:
let 和 const 定义的函数(即函数表达式)不会被“完整地”提升,但它们声明的变量会被提升进入“暂时性死区”。
不可以在声明之前调用!
2.确定 this 的指向
执行阶段 在准备工作完成后,代码开始一行一行地执行。
深拷贝浅拷贝
核心:其实就是能不能完全把对象嵌套的属性也给复制过去的问题。
浅拷贝(只能拷贝一层 更深的还是引用的地址 不是完全拷贝):
1.Object.assign()
let originalObj = {
name: '张三',
age: 25,
hobbies: ['读书', '游泳']
};
// 使用 Object.assign 进行浅拷贝
let shallowCopy = Object.assign({}, originalObj);
// 修改原始类型的值
shallowCopy.name = '李四';
console.log(originalObj.name); // '张三' (不受影响)
// 修改引用类型的值
shallowCopy.hobbies.push('跑步');
console.log(originalObj.hobbies); // ['读书', '游泳', '跑步'] (被影响了!)2.扩展运算符 ...
3.Array.prototype.slice() 或 Array.from()
深拷贝(完全拷贝,更深的嵌套属性此刻也不是原引用了独立起来了):
1.JSON.parse(JSON.stringify(obj)) (简单但有缺陷)
2.递归函数 (最根本的解决方案)
3.现代浏览器内置方法 structuredClone() (推荐)
其实还得最后看polyfill一下 这个有点太新了貌似
4.使用第三方库 (如 Lodash)
cloneDeep
// 需要先安装 lodash: npm install lodash
import { cloneDeep } from 'lodash';
let originalObj = { /* ... */ };
let deepCopy = cloneDeep(originalObj);总结与对比
| 方法 | 类型 | 能否处理嵌套对象 | 优点 | 缺点 |
|---|---|---|---|---|
Object.assign() | 浅拷贝 | 否 | 简单 | 嵌套对象会共享引用 |
扩展运算符 ... | 浅拷贝 | 否 | 语法简洁,ES6 标准 | 嵌套对象会共享引用 |
JSON.parse/stringify | 深拷贝 | 能 | 简单快捷 | 类型丢失多,无法处理函数、循环引用 |
| 递归函数 | 深拷贝 | 能 | 可自定义,理解原理 | 实现复杂,需考虑各种边界情况 |
structuredClone() | 深拷贝 | 能 | 原生API,强大,性能好 | 无法拷贝函数、原型链 |
Lodash cloneDeep | 深拷贝 | 能 | 最健壮,功能最全 | 增加项目体积 |
MAP/SET
| 特性 | Set | Map |
|---|---|---|
| 是否存键值对 | ❌ 只有值 | ✅ key-value |
| key 是否能是对象 | — | ✅ 任意类型 |
| value 是否唯一 | ✅ 自动去重 | ❌ 可重复 |
| 是否按插入顺序遍历 | ✅ | ✅ |
| 性能(查找/删除) | 快 | 快 |
| 常用用途 | 去重、集合运算 | 字典、缓存、映射 |
WeakMap
- key 必须是对象
- 对象会被自动垃圾回收
- 用于缓存、存储私有数据
WeakSet
- 值必须是对象
- 热门用途:存储对象是否处理过
前端算法解析
指南:
💡 第一梯队:必考基础(保命题)
这一类最常见,考的是你对 JS 语言特性的熟悉程度。如果不写出来,面试官会觉得基础不牢。
- 防抖 (Debounce) & 节流 (Throttle)
- 作用: 限制事件触发频率(点太快了不行)。
- 状态: ✅ 你已经看过了。
- 深拷贝 (Deep Clone)
- 作用: 复制一个对象,彻底切断与原对象的联系。
- 状态: ✅ 你已经看过了。
- 数组扁平化 (Array Flatten)
- 作用: 把多维数组变成一维数组。
- 状态: ✅ 你已经看过了。
- 手写
call、apply、bind- 作用: 手动改变函数执行时的
this指向。 - 难度: ⭐⭐(逻辑和
new有点像)。
- 作用: 手动改变函数执行时的
- 数组去重 (Array Unique)
- 作用: 数组里重复的数字去掉。
- 难度: ⭐(非常简单,通常用
Set一行代码解决,但面试官可能会让你用老办法写)。
🚀 第二梯队:工程实战(加分题)
这一类考的是“解决实际问题的能力”,大厂特别喜欢考。
- 发布订阅模式 (Event Emitter) 🔥 极高频
- 描述: 实现
on(订阅)、emit(发布)、off(取消)。类似于 Vue 的$on和$emit。 - 场景: 组件通信。
- 描述: 实现
- 解析 URL 参数 (Parse URL)
- 描述: 把网址后面的
?name=jack&age=18变成对象{ name: 'jack', age: '18' }。 - 场景: 页面跳转传参处理。
- 描述: 把网址后面的
- 手写 Promise.all
- 描述: 给你一堆请求,等它们全部完成了再返回;如果有一个失败了就直接报错。
- 场景: 同时加载多个接口数据。
- 图片懒加载 (Lazy Load)
- 描述: 图片滚到屏幕里了再加载。
- 场景: 性能优化。
- 列表转树结构 (List to Tree)
- 描述: 后端给你一个平铺的数组(带 parentId),你要把它转成树形结构(children)。
- 场景: 菜单栏、权限树。
🧠 第三梯队:算法与进阶(大厂核心题)
如果你面的是字节、阿里、腾讯,或者高级岗位,这一类是拉开差距的关键。
- 并发控制 (Scheduler)
- 状态: ✅ 你已经看过了(那个银行柜台排队的题)。
- 手写 Promise 完整版
- 描述: 从零实现一个 Promise Class。
- 难度: ⭐⭐⭐⭐⭐(非常繁琐,建议最后再看,性价比不高)。
- LRU 缓存算法
- 描述: 内存有限,新数据来了,把“最久没用过”的老数据删掉。
- 场景: 浏览器的缓存机制、Vue 的
<keep-alive>原理。
- 柯里化 (Currying)
- 描述: 把
add(1, 2, 3)变成add(1)(2)(3)的形式。 - 场景: 函数式编程。
- 描述: 把
1.防抖
function debounce(fn,delay){
let timer=null;
return function(...args){
const context=this;
if(timer){
clearTimeout(timer)
}
setTimeout(() => {
fn.apply(context,args)
}, delay);
}
}主要几个模块,定时器,this,和回调
其中this一直很难懂 但是我发现有点懂了
const input = document.querySelector('input');
input.addEventListener('input', debounce(function() {
console.log(this); // 我们希望这里是 input 这个 DOM 元素
console.log(this.value); // 希望能直接拿到输入框的值
}, 300));
//其中本来的监听回调现在是有两层函数一层debounce一层定时函数里的函数普通函数this是运行时候确认的,这里有个标准不带防抖版
document.querySelector("button").addEventListener("click", function () {
console.log(this); //正常输出元素
});但是防抖现在是相当于**回调的上一层(是正常时候的唯一函数层)**这时候是有正确this的 回调是没有的 所以必须存储一下 我们真正执行的函数层被防抖包裹了一层实际上是里层获取不到外层本来能得到的this了
2.扁平化数组
let arr = [[23, 32321], [2313], 23];
const flatten = (arr) => {
return arr.reduce((prev, curr) => {
const next = Array.isArray(curr) ? flatten(curr) : curr;
return prev.concat(next);
}, []);
};
console.log(flatten(arr));
//output:[ 23, 32321, 2313, 23 ]3.深拷贝
// map 用来记录已经拷贝过的对象,防止死循环(比如 A 里面引用了 A)。
function deepClone(obj, map = new WeakMap()) {
// 1. 基础类型出口。
// 如果是 null 或者不是对象(是数字、字符串),不用拷贝,直接返回。
if (obj === null || typeof obj !== 'object') return obj;
// 2. 查表(防循环引用)。
// 看看 map 记录本里,这个 obj 是不是之前已经拷贝过了?
// 如果拷贝过,直接把上次拷贝的结果拿出来,不要再递归了。
if (map.has(obj)) return map.get(obj);
// 3. 创建新容器。
// 如果 obj 是数组,就 new Array(),是对象就 new Object()。
// obj.constructor 就能自动找到它是数组还是对象。
const newObj = new obj.constructor();
// 4. 记录备案。
// 把原对象(obj)和新对象(newObj)的关系记下来,防止下次再遇到死循环。
map.set(obj, newObj);
// 5. 遍历属性。
// 把 obj 里的每一个 key 都拿出来。
for (const key in obj) {
// 只拷贝对象自己的属性,不拷贝原型链上的。
if (obj.hasOwnProperty(key)) {
// 6. 递归关键!
// 属性的值 (obj[key]) 可能还是个对象,所以要再次调用 deepClone。
// 把结果赋值给新对象。
newObj[key] = deepClone(obj[key], map);
}
}
return newObj;
}MAP的作用:防止“循环引用”导致的死循环。
暗黑模式 CSS 方案
这个我一时半会竟然想不到 想到的最简单的CSS文件切换
核心方案:CSS 变量 + 类名切换
这是目前最主流、最推荐的方案。它将颜色主题与组件逻辑解耦,维护成本极低,切换性能也非常高(因为只是修改一个根元素的属性)。
1. 定义颜色变量
在 :root(代表文档根元素 <html>)中定义默认(亮色)主题的颜色。
/* :root 中的变量是全局默认值(亮色主题) */
:root {
--bg-color: #ffffff;
--text-color: #333333;
--card-bg-color: #f4f4f4;
--border-color: #dddddd;
--primary-color: #007bff;
}2. 定义暗黑主题的颜色
使用一个属性选择器(如 [data-theme='dark'])来覆盖默认变量,定义暗黑主题的颜色。
/* 当 html 标签有 data-theme="dark" 属性时,应用这些变量值 */
[data-theme='dark'] {
--bg-color: #121212;
--text-color: #e0e0e0;
--card-bg-color: #1e1e1e;
--border-color: #333333;
--primary-color: #4dabf7;
}3. 在组件中使用变量
在所有需要根据主题变化颜色的地方,使用 var() 函数来引用变量。
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease; /* 添加平滑过渡效果 */
}
.card {
background-color: var(--card-bg-color);
border: 1px solid var(--border-color);
}
.button-primary {
background-color: var(--primary-color);
}4. JavaScript 切换逻辑
编写一个简单的 JS 函数来切换 <html> 元素的 data-theme 属性。
- CSS 硬件加速:切换主题只是改变一个 HTML 属性,浏览器会高效地重新计算样式并触发重绘,这个过程非常快。
- 避免 JS 操作样式:我们不需要用 JS 遍历所有元素并逐个修改
style,这避免了昂贵的 DOM 操作和大量的回流/重绘。 - CSS 变量的优势:CSS 变量的更新是“继承式”的,修改根部的变量,所有后代引用该变量的地方都会自动更新,效率极高。
性能优化100000条数据
通常指在前端渲染一个包含10万个项目的列表或表格。直接渲染10万个 DOM 元素是绝对不可行的,会导致页面瞬间卡死。核心思想是:永远不要渲染你看不见的东西。
核心策略:虚拟滚动
- 计算可见区域:容器有一个固定的高度,滚动时,计算出当前滚动条位置对应的应该显示的数据项的起始索引(
startIndex)和结束索引(endIndex)。 - 动态渲染:只渲染
startIndex到endIndex之间的数据项。 - 撑开容器:为了保持滚动条的正确比例,容器内部会有一个总高度为
itemHeight * totalCount的“占位元素”。可见的列表项通过transform: translateY()来定位到正确的位置。 - 分页
- 原理:最简单粗暴的方法。将数据分成多页,用户通过点击页码来加载不同页的数据。
- 优点:实现简单,服务器和前端压力都小。
- 缺点:用户体验不连贯,无法快速浏览所有数据。
- 无限滚动
- 原理:用户滚动到底部时,通过 AJAX 加载下一页的数据,并追加到现有列表中。
- 优点:用户体验流畅,符合现代 Web 习惯。
- 缺点:如果一直滚动,DOM 节点会越来越多,最终还是会变慢。(因此,对于超大数据,无限滚动必须结合虚拟滚动)。
- Web Worker
- 场景:当对这10万条数据进行复杂计算时(如搜索、过滤、排序),会阻塞主线程,导致页面卡顿。
- 解决方案:将这些计算密集型任务放到 Web Worker 中执行。Worker 在后台线程中运行,计算完成后将结果通过
postMessage发回主线程,主线程再进行渲染。这样 UI 就不会被阻塞。