声明:本文内容仅代表个人对前端知识体系的理解与复盘,受限于个人认知水平,若有错误或表述不严谨的地方,恳请各位大佬批评指正,共同进步~
初级/基础题
这部分主要考察对前端基础知识(HTML/CSS/JS)的掌握程度,以及常用框架的基本使用。
HTML&&CSS
Q:HTML行内元素和块级元素的区别
A:
- 块级元素 (Block-level): 如
div,p,h1-h6。它们独占一行,可以设置宽度、高度、内外边距。默认宽度是其父容器的100%。 - 行内元素 (Inline): 如
span,a,strong。它们不独占一行,与其他行内元素并排显示。设置宽度和高度无效,其尺寸由内容撑开。垂直方向的内外边距可能不会按预期生效。 - 行内块元素 (Inline-block): 如
img,input。结合了两者特点,既能在同一行显示,又能设置宽高和内外边距
Q:CSS伪类与伪元素
A:
- 伪类 (Pseudo-class): 用于定义元素的特殊状态,以单冒号
:开头。例如:hover(鼠标悬停)、:focus(获得焦点)、:nth-child(n)(选择第n个子元素)。它选择的是“文档树中已存在的元素”。 - 伪元素 (Pseudo-element): 用于创建不在文档树中的虚拟元素,以双冒号
::开头(CSS3规范,但单冒号也兼容)。例如::before和::after,常配合content属性在元素前后插入内容。它创建的是“文档树中不存在的元素”
Q:什么是外边距合并?如何解决
A:
-
现象: 当两个或多个垂直方向相邻的块级元素的外边距相遇时,它们会合并成一个外边距,其大小为两者中的较大值,而不是相加。
-
解决思路: 破坏它们“相邻”的条件。
- 触发BFC: 给其中一个元素的父容器设置
overflow: hidden、display: flow-root等,使其成为一个独立的渲染区域。 - 使用内边距或边框: 用
padding或border代替margin。 - 使用空标签或浮动: 在两个元素间插入一个空的、设置了
clear: both的元素,或让其中一个元素浮动。
- 触发BFC: 给其中一个元素的父容器设置
Q:解释一下BFC(块级格式化上下文),BFC脱离文档流了吗?
A:
-
定义: BFC是一个独立的渲染区域,内部的元素布局不受外部影响,反之亦然。
-
触发条件:
float不为none、position为absolute或fixed、display为inline-block、table-cell、flex等、overflow不为visible。 -
作用:
- 防止外边距合并。
- 清除内部浮动(父元素触发BFC后,计算高度时会包含浮动的子元素)。
- 阻止元素被浮动元素覆盖。
-
是否脱离文档流: 没有。BFC内的元素仍在标准文档流中,只是它们的布局规则在一个独立的上下文中进行。这与
position: absolute/fixed导致的完全脱离文档流是不同的概念。
Q:栅栏格和flex布局的区别
A:
- Flex布局 (弹性盒子): 一维布局模型。通过设置父容器
display: flex,可以轻松实现子元素的水平/垂直居中、等分、对齐和顺序调整。核心属性包括justify-content(主轴对齐)、align-items(交叉轴对齐) 等。 - 栅格布局 (Grid System): 通常指基于百分比和浮动(或Flex)实现的12列或24列布局系统,常见于Bootstrap等框架。它是一种二维布局思想,将页面划分为行和列。CSS Grid Layout (
display: grid) 是原生的二维布局方案,比Flex更强大,适合复杂的页面整体布局。
Q:如何实现一个导航栏的显示隐藏(根据分辨率)/ 变窄
A: 使用媒体查询(@media)。当屏幕变窄时,将横向菜单改为汉堡菜单(点击弹出)
这是响应式设计的典型应用,主要通过 CSS媒体查询 (@media) 实现
/* 默认样式:宽屏下横向导航 */
.nav {
display: flex;
}
/* 窄屏下(如小于768px)变为汉堡菜单 */
@media (max-width: 768px) {
.nav { display: none; } /* 隐藏原导航 */
.hamburger-menu { display: block; } /* 显示汉堡按钮 */
}
Q:CSS实现动画 vs JS来实现动画
A: CSS性能好,利用GPU加速,适合简单动效;JS控制力强,适合复杂交互和逻辑联动
Q:如何实现移动端适配问题?rem和vw有啥区别
A: rem基于根元素字体大小,需配合JS动态计算;vw/vh基于视口宽度/高度,原生支持,无需JS计算。
Q:如何实现一个换肤功能(如切换2个不同的css文件)
A:
-
方法一:切换CSS文件。 准备多套CSS文件,通过JS动态修改
<link>标签的href属性来切换主题。 -
方法二:CSS变量。 定义不同主题的CSS变量,通过JS切换
<html>或<body>上的data-theme属性,利用CSS变量的继承特性实现主题切换。/* 定义变量 */ :root { --bg-color: #fff; --text-color: #333; } [data-theme="dark"] { --bg-color: #333; --text-color: #fff; } /* 使用变量 */ body { background: var(--bg-color); color: var(--text-color); }
JavaScript 基础
Q:ES6+ 用过哪些特性 / 有哪些特性
A: let/const、箭头函数、模板字符串、解构赋值、Promise、Class、Module等
- 变量声明: let (块级作用域)、const (常量)。
- 函数: 箭头函数 (简化写法,无自己的this)、默认参数、剩余参数 (...args)。
- 字符串: 模板字符串 ( Hello $ {name} )。
- 解构赋值: 从数组或对象中提取值 (const { a, b } = obj)。
- 数据结构: Map, Set。
- 异步编程: Promise, async/await。
- 模块化: import / export。
- 类: class 语法糖。
Q:深浅拷贝:如何实现一个深拷贝
A:
-
浅拷贝: 只复制对象的引用,新旧对象共享同一块内存。
Object.assign(), 展开运算符...都是浅拷贝。-
深拷贝: 递归地复制对象的所有层级,新旧对象完全独立。
-
简单方式:
JSON.parse(JSON.stringify(obj))。缺点:无法处理函数、undefined、Symbol、循环引用、Date/RegExp等特殊对象。 -
手写递归实现: 核心是遍历对象属性,对引用类型递归调用拷贝函数,并用
WeakMap解决循环引用问题。
-
Q:如何实现一个闭包 (P)
A:
-
定义: 一个函数能够记住并访问它的词法作用域,即使这个函数在其词法作用域之外执行。
-
实现: 在一个函数内部定义另一个函数,并返回这个内部函数。
-
用途: 数据私有化、防抖节流、模块化。
function createCounter() { let count = 0; // 私有变量 return function() { return ++count; // 内部函数可以访问外部函数的变量 }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2
Q:如何实现一个forEach?forEach循环返回的是个什么?如何跳出循环 P
A:
- 实现:
forEach是数组原型上的方法,接收一个回调函数。 - 返回值:
undefined。 - 跳出循环:
forEach无法 通过break或return提前终止。它会遍历完所有元素。如需提前终止,应使用for...of、some()或every()
Q:实现一个节流函数(及Hooks写法)
A:
-
概念: 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。常用于搜索框输入、窗口resize等高频事件。
-
JS实现 (时间戳版):
function throttle(func, delay) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= delay) { func.apply(this, args); lastTime = now; } }; } -
React Hooks写法: 结合
useRef和useCallback。import { useRef, useCallback } from 'react'; function useThrottle(fn, delay) { const lastTimeRef = useRef(0); return useCallback((...args) => { const now = Date.now(); if (now - lastTimeRef.current >= delay) { fn(...args); lastTimeRef.current = now; } }, [fn, delay]); }
Q:常见算法手写:实现一个快排 / 求最长相同字符前缀 (['ABCDEFG','ABC123','AB78DSA'])
A:
-
快速排序 (Quick Sort):
- 从数组中选择一个“基准”元素。
- 将数组分为两部分,比基准小的放左边,比基准大的放右边。
- 对左右两部分递归执行上述步骤。
function quickSort(arr) { if (arr.length <= 1) return arr; const pivotIndex = Math.floor(arr.length / 2); const pivot = arr.splice(pivotIndex, 1)[0]; const left = [], right = []; for (let i = 0; i < arr.length; i++) { if (arr[i] < pivot) left.push(arr[i]); else right.push(arr[i]); } return [...quickSort(left), pivot, ...quickSort(right)]; } -
最长公共前缀:
function longestCommonPrefix(strs) {
if (!strs || strs.length === 0) return '';
let prefix = strs[0];
for (let i = 1; i < strs.length; i++) {
while (strs[i].indexOf(prefix) !== 0) {
prefix = prefix.substring(0, prefix.length - 1);
if (prefix === '') return '';
}
}
return prefix;
}
Q:说说复杂数据类型,function是复杂数据类型吗?如何判定类型
A: -
复杂数据类型 (引用类型): 包括 Object, Array, Function, Date, RegExp 等。它们在栈中存储的是指向堆内存的地址。function 是一种特殊的对象,属于复杂数据类型。
-
类型判定:
typeof: 适用于基本类型判断,但对null和引用类型(除function外)判断不准确。typeof null为'object'。instanceof: 判断一个实例是否属于某个构造函数。[] instanceof Array为true。Object.prototype.toString.call(): 最准确的方法。例如Object.prototype.toString.call([])返回'[object Array]'。
Vue&&React基础
Q:Vue生命周期 / Vue3有哪些特性
A:
-
Vue2生命周期:
beforeCreate->created->beforeMount->mounted->beforeUpdate->updated->beforeDestroy->destroyed。 -
Vue3特性:
- 组合式API (Composition API): 引入
setup函数,逻辑组织更灵活,解决了Options API在大型组件中逻辑分散的问题。 - 响应式系统重构: 使用
Proxy替代Object.defineProperty,性能更好,能监听数组和对象属性的增删。 - 性能提升: 虚拟DOM重写、Tree-shaking支持更好、打包体积更小。
- 更好的TypeScript支持
- 组合式API (Composition API): 引入
Q:v-if 与 v-show 的区别
A:
- 手段不同:
v-if是真正的条件渲染,会根据表达式的值在DOM中销毁或重建元素及事件监听器。v-show只是简单地切换元素的CSSdisplay属性。 - 编译过程不同:
v-if有局部编译/卸载的过程,切换时有更高的开销。v-show在任何条件下都会被编译并保留在DOM中,初始渲染开销更高。 - 使用场景:
v-if适用于运行时条件很少改变的场景。v-show适用于需要频繁切换的场景。
Q:React父子组件如何通信 / 父组件如何调用子组件方法
A: -
-
父传子: 通过
props传递数据或函数。 -
子传父: 父组件将一个函数作为
prop传递给子组件,子组件在需要时调用该函数,并将数据作为参数传入。 -
父调子方法:
- Class组件: 使用
ref获取子组件实例,然后调用其方法。 - Function组件: 使用
useImperativeHandleHook 配合forwardRef来暴露子组件的方法给父组件。
- Class组件: 使用
Q:React生命周期 / class和hooks分别是如何实现生命周期的
A:
-
Class组件生命周期:
constructor->static getDerivedStateFromProps->render->componentDidMount->componentDidUpdate->componentWillUnmount。 -
Hooks实现: 使用
useEffectHook 来模拟生命周期。componentDidMount:useEffect(() => { ... }, [])componentDidUpdate:useEffect(() => { ... })(无依赖数组) 或useEffect(() => { ... }, [dep])componentWillUnmount:useEffect(() => { return () => { ... } }, [])(返回一个清理函数)
Q:受控组件和非受控组件
A:
- 受控组件 (Controlled Component): 表单数据由React组件的
state管理。每次输入变化都会触发onChange事件更新state,从而重新渲染组件。优点是数据来源单一,易于验证和操作。 - 非受控组件 (Uncontrolled Component): 表单数据由DOM自身管理。通过
useRef来获取DOM节点的值。优点是写法简单,适合简单的表单集成。
Q:如何使用React Hooks实现之前的生命周期 / React Hooks相对于class组件的优势
A:
-
实现生命周期: 见上文
useEffect的用法。 -
Hooks优势:
- 逻辑复用: 自定义Hook可以轻松提取和复用状态逻辑,避免了HOC和Render Props的嵌套地狱。
- 代码更简洁: 解决了Class组件中
this指向混乱的问题,相关逻辑可以聚合在一起,而不是分散在各个生命周期方法中。 - 学习成本更低: 对于新手来说,函数组件比Class组件更容易理解。
Q:纯函数组件的好处 / class方法和纯函数有什么不一样
A:
- 纯函数组件 (Pure Function Component): 相同的输入(props)总是得到相同的输出(JSX),且没有副作用。
- 好处: 易于测试、易于推理、便于性能优化(如
React.memo)。 - 与Class组件区别: Class组件是ES6的类,有自身的生命周期和状态(
this.state),需要通过extends React.Component创建。纯函数组件就是一个接收props返回JSX的普通函数。
Q:antd form表单自定义组件
A:
-
Ant Design的Form组件通过
value和onChange来管理表单项的值。 -
自定义组件需要遵循这个约定:
- 接收
valueprop 来显示当前值。 - 在内部状态变化时,调用
onChangeprop 将新值通知给Form。
function MyCustomInput({ value, onChange }) { const handleChange = (e) => { onChange && onChange(e.target.value); }; return <input value={value} onChange={handleChange} />; } // 使用时 <Form.Item name="custom" label="自定义"> <MyCustomInput /> </Form.Item> - 接收
Q:React Context 是什么 / 抛开三方插件,如何实现跨页面通信
A:
-
Context: 提供了一种在组件树中传递数据的方式,无需手动一层层传递
props。适用于全局数据,如用户信息、主题、语言等。 -
跨页面通信 (不使用Redux等):
- URL参数: 通过路由跳转时携带参数 (
/page?id=123)。 - 本地存储: 使用
localStorage或sessionStorage存储共享数据。 - 发布订阅模式: 自己实现一个简单的Event Bus。
- URL参数: 通过路由跳转时携带参数 (
网络与浏览器基础
Q:浏览器强缓存和协商缓存 / 状态码是多少
A:
- 强缓存: 浏览器直接从本地缓存读取资源,不向服务器发送请求。由HTTP响应头
Cache-Control(如max-age=3600) 或Expires控制。命中强缓存时,状态码为200 (from memory cache)或200 (from disk cache)。 - 协商缓存: 当强缓存失效后,浏览器会向服务器发送请求,询问资源是否有更新。服务器通过
ETag(资源唯一标识) 或Last-Modified(最后修改时间) 来判断。如果资源未变,返回304 Not Modified,浏览器继续使用本地缓存;如果变了,则返回200和新资源。
Q:HTTP 1.0、1.1 和 2.0 有什么区别。
A:
-
HTTP/1.0: 每次请求都需要建立一个新的TCP连接,效率低。
-
HTTP/1.1: 引入了持久连接 (Keep-Alive),默认复用TCP连接。但存在“队头阻塞”问题,即一个连接上必须等前一个请求响应后才能发送下一个。
-
HTTP/2.0:
- 多路复用: 解决了队头阻塞,可以在一个TCP连接上并行发送多个请求和响应。
- 头部压缩: 使用HPACK算法压缩请求头,减少传输体积。
- 服务端推送: 服务器可以主动向客户端推送资源。
Q:如何解决跨域问题(CORS原理及配置)
A:
- 同源策略: 浏览器的一种安全机制,限制从一个源加载的文档或脚本与来自另一个源的资源进行交互。
- CORS (跨域资源共享): 一种W3C标准,允许服务器声明哪些源可以通过浏览器访问其资源。
- 原理: 浏览器在发起跨域请求时,会自动在请求头中添加
Origin字段。服务器在响应头中设置Access-Control-Allow-Origin来告知浏览器是否允许该源的访问。 - 配置: 后端服务器设置
Q:输入URL点击回车后的发生过程。
A:
-
网络请求阶段
- DNS解析:浏览器先查缓存(浏览器/系统/Hosts),若无则向DNS服务器发起递归查询,将域名转换为IP地址。
- 建立连接:基于IP地址,通过TCP三次握手建立可靠连接;若是HTTPS,还需进行TLS握手协商加密密钥。
- 发送请求:浏览器构建HTTP请求报文发送给服务器。
- 接收响应:服务器处理请求并返回HTTP响应报文(含状态码如200/304及资源内容)。
-
渲染构建阶段
- 解析构建:浏览器解析HTML生成DOM树,解析CSS生成CSSOM树。
- 生成渲染树:结合DOM和CSSOM,剔除不可见元素(如
display: none),生成Render Tree。 - 布局:计算各节点在屏幕上的确切位置和大小。
- 绘制与合成:将各图层绘制到位图,最后由GPU合成并显示在屏幕上。
-
脚本执行阶段
- 在解析HTML过程中遇到
<script>标签时,会暂停DOM构建,下载并执行JS代码(除非标记了async或defer),这可能会修改DOM或CSSOM从而触发重新渲染。
- 在解析HTML过程中遇到
-
缓存与断开
- 浏览器根据响应头(Cache-Control等)决定是否缓存资源;页面加载完成后,根据Connection头决定是否断开TCP连接
中级
这部分考察对技术底层原理的理解、工程化能力以及解决实际问题的能力
JavaScript 进阶
Q:事件循环(Event Loop):宏任务与微任务(为什么会有事件循环?)
A:
-
事件循环:
- 同步优先:先执行完当前调用栈中的所有同步代码。
- 微任务清空:同步代码执行完毕后,立刻清空当前的微任务队列。如果在执行微任务时又产生了新的微任务,也会在当前轮次一并执行。
- 渲染与下一轮宏任务:微任务清空后,浏览器可能会进行 UI 渲染,随后从宏任务队列中取出一个宏任务执行,重复上述过程。
-
宏任务 (MacroTask) :通常由宿主环境发起,包括
setTimeout、setInterval、I/O 操作、UI 渲染等。 -
微任务 (MicroTask) :通常由 JS 引擎发起,优先级更高,包括
Promise.then/catch/finally、queueMicrotask、MutationObserver等
console.log("1"); // 同步
setTimeout(() => console.log("2"), 0); // 宏任务
Promise.resolve().then(() => console.log("3")); // 微任务
console.log("5"); // 同步
// 最终输出顺序:1 -> 5 -> 3 -> 2
Q:Promise内部是如何实现的?Promise能取消吗?
A:
Promise 的内部实现本质上是一个带有状态机的发布订阅模式:
- 三种状态:
Pending(等待态)、Fulfilled(成功态)、Rejected(失败态)。状态一旦改变就不可逆,确保了结果的确定性。 - 回调队列:内部维护了两个队列(成功回调队列和失败回调队列)。当调用
.then()时,如果状态未定,就将回调函数推入对应队列;当状态改变时,依次执行队列中的回调。 - 异步执行:Promise 的回调会被放入微任务队列,确保在当前同步代码执行完毕后再执行。
关于 Promise 的取消:
原生的 Promise 一旦创建就会立即执行,且无法中途取消。但我们可以通过以下几种方式模拟“取消”行为:
- AbortController(推荐) :现代浏览器原生支持的 API,常用于中止
fetch请求。
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal }); // 需要取消时调用
controller.abort();
- 自定义封装:通过一个外部标志位(flag)来控制是否忽略 Promise 的结果。
let canceled = false;
myPromise.then(res => { if (!canceled) { /* 处理结果 */ } }); // 取消逻辑
canceled = true;
Q:Generator函数内部是如何实现的?如何通过Generator实现一个async/await方法
A:
-
Generator 的内部实现:
Generator 函数(function*)本质上是一个状态机。调用它不会立即执行函数体,而是返回一个遍历器对象。其核心是yield关键字,它能让函数在执行过程中暂停,保存当前的执行上下文(包括变量、指令指针等),并在下次调用next()时从暂停的位置恢复执行。 -
通过 Generator 实现 async/await:
async/await其实就是 Generator 函数的语法糖。我们可以通过编写一个自动执行器(Runner) 来模拟await的效果。这个执行器会自动调用next(),并判断产出的值是否是 Promise,如果是,就在 Promise 的.then()中继续调用next()。// 简单的自动执行器实现 function runGenerator(genFunc) { // 简单的自动执行器实现 function runGenerator(genFunc) { const generator = genFunc(); function handle(result) { if (result.done) return result.value; // 结束 // 将 yield 后面的值统一转为 Promise return Promise.resolve(result.value) .then(res => handle(generator.next(res))) // 成功后继续执行下一步 .catch(err => handle(generator.throw(err))); // 错误处理 } return handle(generator.next()); }
Q:CommonJS的模块导入导出和ES6的导入导出有什么区别
A:
| 特性 | CommonJS (CJS) | ES6 Module (ESM) |
|---|---|---|
| 加载时机 | 运行时加载。代码执行到 require() 时才去读取并执行模块。 | 编译时(静态)加载。在代码解析阶段就确定了依赖关系,支持 Tree Shaking。 |
| 输出值的本质 | 值的拷贝。导出的是内存中值的副本,模块内部后续的变化不会影响已导入的值。 | 值的实时引用。导出的是接口的引用,模块内部变量变化,外部导入的值也会同步更新。 |
| 动态性 | 动态路径,require() 可以在代码任意位置调用。 | 静态结构,import 必须位于文件顶层(除非使用动态 import())。 |
Q:JS垃圾回收机制,如何回收
A: JavaScript 拥有自动垃圾回收(GC)机制,目的是找出不再使用的变量并释放其占用的内存。
常见的回收算法:
-
引用计数法:记录每个对象被引用的次数,当引用数为 0 时回收。缺点是无法解决“循环引用”导致的内存泄漏问题(即 A 引用 B,B 引用 A,但外部已经不再使用它们)。
-
标记-清除法(Mark-and-Sweep) :现代浏览器的主流算法。
- 标记阶段:从“根对象”(如全局对象
window、当前执行栈的局部变量)出发,递归遍历所有可达的对象并打上标记。 - 清除阶段:遍历堆内存,凡是没被打上标记的对象,就视为垃圾并进行清理。这种方法完美解决了循环引用的问题。
- 标记阶段:从“根对象”(如全局对象
V8 引擎的优化(分代回收):
为了提升效率,V8 引擎将内存分为新生代和老生代:
- 新生代:存放存活时间短的小对象。采用 Scavenge 算法(复制算法),将存活对象复制到另一块空间,然后清空原空间,效率极高。
- 老生代:存放存活时间长的大对象。采用标记-清除和标记-整理(Compact,防止内存碎片化)相结合的算法。
Webpack与工程化
Q:Webpack打包机制与原理 / 编译过程
A: Webpack 是一个现代 JavaScript 应用程序的静态模块打包器。其核心原理可以概括为:一切皆模块。
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数。
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的
run方法开始执行编译。 - 确定入口:根据配置中的
entry找出所有的入口文件。 - 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,递归本步骤直到所有入口依赖的文件都经过了处理。
- 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表。
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
Q:Webpack的load和plugin有什么区别?常用的有哪些
A:
-
区别:
- Loader (转换器) :本质是一个函数。Webpack 默认只能处理 JS/JSON 文件,Loader 的作用是让 Webpack 能够处理其他类型的文件(如 CSS, Vue, TS, 图片等),并将它们转换为有效的模块。它运行在打包前的转换阶段。
- Plugin (扩展器) :基于事件流框架 Tapable。Plugin 的作用是扩展 Webpack 的功能,它可以介入打包的整个生命周期(如优化、压缩、定义环境变量等)。它运行在打包全过程。
-
常用 Loader:
babel-loader(转译ES6+),css-loader,style-loader,less-loader,url-loader/file-loader(处理图片)。 -
常用 Plugin:
HtmlWebpackPlugin(生成HTML),MiniCssExtractPlugin(提取CSS),DefinePlugin(定义环境变量),CleanWebpackPlugin(清理目录)。
Q:Webpack如何分包 / 利用什么机制分包
A:
主要利用 Code Splitting (代码分割) 机制,核心配置是 optimization.splitChunks。
- 多入口打包:配置多个
entry,自动分离公共代码。 - 动态导入 (Dynamic Imports) :使用
import()语法,Webpack 会自动将其分割为一个独立的 Chunk,实现按需加载。 - SplitChunksPlugin:这是 Webpack4/5 内置的插件。通过配置
cacheGroups,可以将node_modules中的第三方库提取为vendor,将公共业务代码提取为common,避免重复打包。
Q:Webpack如何优化包体积和构建时间
A:
-
优化包体积:
- Tree Shaking:利用 ES6 Module 的静态特性,剔除未引用的代码(需 production 模式)。
- 压缩代码:使用
TerserPlugin压缩 JS,CssMinimizerPlugin压缩 CSS。 - 图片压缩:使用
image-webpack-loader。 - 按需加载:路由懒加载、组件懒加载。
- CDN 引入:将 React/Vue 等大库通过 CDN 引入,不打包进 bundle。
-
优化构建时间:
- 缓存:开启
babel-loader的cacheDirectory,使用 Webpack5 的持久化缓存 (cache: { type: 'filesystem' })。 - 缩小范围:配置
include/exclude限制 Loader 的处理范围,配置resolve.extensions减少文件查找。 - 多线程:使用
thread-loader开启多进程打包(注意启动开销)。 - DLL (过时) :虽然 Webpack5 不推荐,但老项目可用
DllPlugin预编译第三方库。
- 缓存:开启
Q:Webpack5缓存有哪几种方式 / 模块联邦简单介绍
A:
-
缓存方式:
- 内存缓存:开发模式下默认开启。
- 文件系统缓存 (持久化缓存) :配置
cache: { type: 'filesystem' },将编译结果写入硬盘,重启构建时直接读取。 - 快照 (Snapshots) :通过文件的时间戳或哈希值判断文件是否变化。
-
模块联邦 (Module Federation) :
Webpack5 的新特性,允许一个 JS 应用动态地从另一个 JS 应用加载代码。它解决了微前端架构中代码共享和依赖复用的难题,实现了真正的去中心化部署。
Q:Vite与Webpack的区别 / 前端打包工具Vite
A:
-
开发环境:
- Webpack:全量打包。启动服务器时需要先分析依赖、编译打包,项目越大启动越慢。
- Vite:基于浏览器原生 ESM。启动时不打包,直接启动服务器,当浏览器请求某个模块时再进行编译(按需编译),速度极快。
-
生产环境:
- Webpack:成熟稳定,生态丰富。
- Vite:使用 Rollup 进行打包,以获得更好的 Tree-shaking 效果和更小的体积。
Q:如何实现前端自动化 / 做过可视化拖拽生成页面吗
A:
-
前端自动化:通常指 CI/CD(持续集成/持续部署)。流程包括:代码提交 -> Git Hook 触发 -> Jenkins/GitLab CI 拉取代码 -> 安装依赖 -> 运行测试 -> Webpack/Vite 打包 -> 上传至服务器/CDN -> 通知上线。
-
可视化拖拽:核心是Schema 协议。
- 物料区:提供可拖拽的组件。
- 画布区:接收拖拽事件,记录组件的位置和层级。
- 属性区:修改选中组件的 Props。
- 渲染引擎:将生成的 JSON Schema 递归渲染为真实的 DOM。
React&Vue原理
Q:React Hooks内部实现原理 / 自定义一个Hooks
A:
-
原理:React 内部维护了一个链表结构。每个 Function Component 都有一个对应的 Fiber 节点,Fiber 上有一个
memoizedState属性指向第一个 Hook 对象。每个 Hook 对象包含memoizedState(保存 state)、baseState、queue(更新队列) 和next(指向下一个 Hook)。- 为什么不能写在条件语句中? 因为 React 依靠 Hook 的调用顺序来对应链表中的节点。如果顺序乱了,状态就会错位。
-
自定义 Hook:本质上就是一个以
use开头的普通函数,内部可以调用其他 Hook。它是逻辑复用的手段,而非状态复用的手段。
Q:Redux的数据运转及原理、三大原则、connect机制
A:
- 运转:View 发起 Action -> Dispatch 给 Store -> Reducer 根据 Action 类型计算新 State -> Store 更新并通知 View 重新渲染。
- 三大原则:单一数据源;State 是只读的(必须通过 Action 修改);使用纯函数 Reducer 进行修改。
- Connect 机制:
connect(mapStateToProps, mapDispatchToProps)是一个高阶组件。它订阅了 Store 的变化,当 Store 更新时,它会对比之前的 Props,如果有变化,就强制包裹的子组件更新。
Q:高阶组件(HOC)的使用场景与实现
A:
- 场景:权限控制、日志打点、代码复用(如统一处理 loading 状态)。
- 实现:一个函数,接收一个组件作为参数,返回一个新的组件。
const withAuth = (WrappedComponent) => {
return (props) => {
if (!isLoggedIn) return <Login />;
return <WrappedComponent {...props} />;
};
};
Q:PureComponent 与 React.memo(手写实现memo效果)
A:
-
PureComponent:用于 Class 组件,自动对
props和state进行浅比较,决定是否需要render。 -
React.memo:用于函数组件,作用相同。它包裹组件后,只有 props 变化时才会重新渲染。
-
手写 memo 效果:
function memo(Component) { return function MemoizedComponent(props) { // 这里需要 useRef 保存旧 props 进行比较,简化版逻辑如下 return <Component {...props} />; }; }
Q:React中状态的更新是如何触发视图渲染的
A:
- 调用
setState或useState的 setter。 - React 创建一个新的 Fiber 树(WorkInProgress)。
- Reconciler (Diff) :对比新旧 Fiber 树,标记出变化的节点(Placement, Update, Deletion)。
- Commit:遍历带有副作用标记的 Fiber 节点,同步执行 DOM 操作,更新视图。
Q:JSX为什么能在JS里写HTML代码?最后是怎么处理和编译的
A:
- 本质:JSX 既不是字符串也不是 HTML,它是
React.createElement(component, props, ...children)的语法糖。 - 编译:Babel 会将 JSX 语法转换成标准的 JS 函数调用。例如
<div className="app">Hi</div>会被编译为React.createElement('div', {className: 'app'}, 'Hi')。这个函数执行后返回一个虚拟 DOM 对象(Virtual DOM)。
Q:Vue中双向数据绑定是如何实现的 / html中指令(如v-model)是如何实现的
A:
-
Vue2:使用
Object.defineProperty劫持 data 中所有属性的 getter/setter。- Getter:收集依赖(Watcher)。
- Setter:数据变化时,通知 Watcher 更新视图。
- 缺点:无法检测对象属性的添加/删除,无法监听数组下标变化。
-
Vue3:使用 ES6 的
Proxy代理整个对象。可以直接监听对象和数组的变化,性能更好。 -
v-model 实现:它是一个语法糖。
- 在输入框上,
v-model="val"等价于:value="val" @input="val = $event.target.value"。 - 在组件上,它利用 props 传递 value,利用 emit 触发 input (Vue2) 或 update:modelValue (Vue3) 事件。
- 在输入框上,
性能优化与实战
Q:拿到一个项目如何做性能优化?有哪些指标参考
A:
-
指标 (Web Vitals) :
- LCP (最大内容绘制) :衡量加载速度,目标 < 2.5s。
- FID (首次输入延迟) :衡量交互性,目标 < 100ms。
- CLS (累积布局偏移) :衡量视觉稳定性,目标 < 0.1。
-
优化思路:
- 网络层:HTTP2、CDN、Gzip/Brotli 压缩、DNS 预解析。
- 构建层:代码分割、Tree Shaking、图片格式优化 (WebP)。
- 渲染层:减少重排重绘、虚拟列表(长列表)、防抖节流、SSR (服务端渲染)。
- 感知层:骨架屏、懒加载。
Q:项目打包从200M优化到120M做了哪些事情
A:
- 分析:使用
webpack-bundle-analyzer分析包体积,发现大文件主要是 Moment.js、Echarts 和一些重复的 lodash。 - 替换:用
dayjs替换moment.js(体积从几百K降到几K)。 - 外部化:将 React/Vue/AntD 等基础库通过 CDN 引入,配置
externals不打包进 bundle。 - 按需引入:配置 AntD/Echarts 的按需加载插件。
- 压缩:开启 Gzip 压缩,配置图片压缩。
- 去重:使用
IgnorePlugin忽略不必要的语言包(如 moment 的 locale)。
Q:CDN基本概念、配置及缓存策略(CDN服务器宕机怎么办)
A:
-
概念:内容分发网络,将源站内容分发到全球各地的边缘节点,用户就近获取内容。
-
策略:静态资源(JS/CSS/Img)设置较长的强缓存(如 1 年),文件名带 Hash;HTML 文件设置协商缓存或不缓存。
-
宕机处理:
- 健康检查:CDN 厂商会自动监测节点健康状态。
- 回源:如果边缘节点宕机,流量会调度到其他可用节点或直接回源站获取。
- 多 CDN 容灾:大型项目会接入多家 CDN 厂商,通过 DNS 智能解析切换流量。
Q:微信原生小程序性能指标
A:
- 首屏时间:从打开小程序到第一屏完全渲染的时间。
- setData 耗时:
setData是通信桥梁,频繁或大数据量的setData会导致卡顿。 - 页面路径深度:避免过深的嵌套。
- 优化手段:分包加载、图片懒加载、避免频繁的
setData、使用wxs处理手势交互(减少逻辑层与渲染层通信)。
Q:一个页面的按钮权限如何控制
A:
-
后端返回权限表:登录时,后端返回当前用户的权限列表(如
['btn:add', 'btn:delete'])。 -
全局存储:将权限列表存入 Vuex/Pinia 或 Redux/Context。
-
封装指令/组件:
- Vue 指令:
v-auth="'btn:add'",在指令的mounted钩子中判断当前按钮标识是否在权限列表中,不在则el.parentNode.removeChild(el)。 - React 组件:
<Auth code="btn:add"><Button>删除</Button></Auth>,组件内部判断无权限则返回null。
- Vue 指令:
高级
这部分考察架构视野、复杂系统设计能力及团队管理思维
微前端架构
Q:为什么要使用微前端?能解决什么痛点?前期做了哪些调研
A:
-
核心痛点:
- 巨石应用维护难:随着业务迭代,单体应用代码量巨大,编译慢、构建慢,且牵一发而动全身。
- 技术栈锁定:老项目可能基于 jQuery 或老旧框架,无法直接升级,但又需要开发新功能。
- 多团队协作冲突:多个团队在一个仓库开发,容易产生代码冲突,部署流程耦合严重。
-
调研方向:通常会对比
iframe(隔离好但通信难、状态不共享)、npm 分包(技术栈必须统一)、Web Components(兼容性问题)以及qiankun/single-spa方案。最终选择 qiankun 通常是因为其完善的沙箱机制和 HTML Entry 模式。
Q:Qiankun的特性是什么?有没有看过qiankun的底层原理
A:
-
特性:
- HTML Entry:直接通过 URL 加载子应用,像 iframe 一样简单,但体验更好。
- 样式隔离:支持
strictStyleIsolation(Shadow DOM) 和experimentalStyleIsolation(Scoped CSS 类似实现)。 - JS 沙箱:确保子应用的全局变量不会污染主应用。
- 资源预加载:利用浏览器空闲时间加载子应用资源。
-
底层原理:
-
路由劫持:重写
window.history.pushState/replaceState,监听路由变化来匹配子应用。 -
入口解析:使用
import-html-entry库 fetch 子应用的 HTML,解析出 script 和 link 标签。 -
沙箱机制:
- Legacy Sandbox:基于 Proxy 的单例沙箱(适用于单实例)。
- Proxy Sandbox:基于 Proxy 的多例沙箱(适用于多实例并存),在激活时记录变更,卸载时还原
window对象。
-
Q:Qiankun配置微应用入口后,entry是如何实现应用的路由的
A:
Qiankun 启动时会调用 start(),内部会进行路由劫持。
- 监听变化:它监听了浏览器的
popstate事件,并重写了history.pushState和history.replaceState方法。 - 匹配规则:当路由发生变化时,Qiankun 会遍历注册的所有微应用(
registerMicroApps),根据配置的activeRule(通常是路径前缀)判断当前路由属于哪个微应用。 - 加载执行:如果匹配到新的微应用,就通过
import-html-entry请求该应用的 HTML,提取 JS/CSS 并在沙箱环境中执行;如果匹配到旧应用离开,则触发卸载逻辑。
Q:微前端中微任务注入权限如何控制
A:
-
控制思路:
- 主应用鉴权:在主应用的
registerMicroApps的beforeLoad钩子中进行全局权限校验。如果用户无权限访问某模块,直接拦截加载并跳转 403 页面。 - Props 传递:主应用将用户的权限列表(如按钮权限、菜单权限)通过
props传递给子应用。 - 子应用消费:子应用在初始化时接收这些权限数据,结合自身的路由守卫或组件级指令(如 Vue 的
v-auth)来控制页面元素的显示隐藏。
- 主应用鉴权:在主应用的
Q:有没有看过 single-spa 的内部实现方式
A:
-
应用注册:提供一个
registerApplication函数,接收应用名称、加载函数、激活函数。 -
路由监听:内部监听
hashchange和popstate事件。 -
生命周期流转:当事件触发时,它会检查所有注册的应用。
- 如果应用从“未激活”变为“激活”,则依次调用其
load->bootstrap->mount。 - 如果应用从“激活”变为“未激活”,则调用其
unmount。 - Single-spa 本身不处理具体的 HTML 解析和沙箱,这些是 qiankun 在其基础上封装实现的。
- 如果应用从“未激活”变为“激活”,则依次调用其
Q:微前端应用中,子应用之间如何进行通信?
A: 核心原则: 微前端提倡解耦,因此强烈不推荐子应用之间直接互相调用或共享全局变量。最佳实践是“中心化通信”,即由主应用(基座)作为中间人进行状态分发。
- 方案一:基于 Props 的状态下发(最常用)
主应用维护一个全局状态(如 Redux、Pinia 或简单的 Observable),当状态变化时,通过 qiankun 的 props 属性将数据传递给所有子应用。
// 主应用注册微应用 registerMicroApps([ { name: 'app1', entry: '//localhost:7100', container: '#container', activeRule: '/app1', // 传递全局通信方法或状态 props: { globalState, onGlobalStateChange }, } ]); - 方案二:自定义事件 (CustomEvent)
利用浏览器原生的window.dispatchEvent和window.addEventListener。子应用A触发一个全局事件,子应用B监听该事件。这种方式简单,但缺乏类型约束,且容易产生命名冲突。 - 方案三:URL 参数传递
对于简单的页面跳转带参,直接通过路由 query 参数传递是最安全且符合 RESTful 规范的方式。
Q:如何解决微前端中的公共依赖重复加载问题?
A: 如果每个子应用都把 React/Vue 打包进去,会导致用户浏览器重复下载相同的库,严重影响性能。
-
方案一:Webpack Externals + CDN(传统方案)
在主应用的 HTML 中通过<script>标签引入 React/Vue 等基础库的 CDN 资源,并将它们挂载到window上。然后在所有子应用的 Webpack 配置中设置externals,告诉打包器:“遇到这些库不要去 node_modules 找,直接使用全局变量”。// 子应用 webpack.config.js module.exports = { externals: { react: 'React', 'react-dom': 'ReactDOM' } }; -
方案二:Module Federation(模块联邦,现代方案)
Webpack 5 的原生能力。可以将主应用配置为“提供者(Provider)”,将 React 等库共享出去;子应用配置为“消费者(Consumer)”,在运行时动态拉取主应用提供的依赖。这实现了真正的按需加载和版本去重。
Q:微前端落地过程中遇到的最大坑是什么?怎么解决的?
A:
-
坑一:样式冲突(全局污染)
现象:子应用 A 的 CSS 覆盖了子应用 B 或主应用的样式(比如都写了.btn { color: red })。
解决:- 开启 qiankun 的
experimentalStyleIsolation: true,它会给子应用的样式自动加上类似[data-qiankun="app1"] .btn的选择器前缀(类似于 Shadow DOM 的效果)。 - 团队内部强制推行 CSS Modules 或 Scoped CSS,从源头避免全局样式。
- 开启 qiankun 的
-
坑二:全局变量污染
现象:子应用在window上挂载了同名变量,导致其他应用崩溃。
解决:严格依赖 qiankun 的 JS 沙箱(Proxy Sandbox)。开发时严禁手动向window挂载业务数据,所有状态必须放在应用内部的状态管理器中。 -
坑三:路由死循环或404
现象:刷新子应用页面时,主应用找不到对应的路由。
解决:确保 Nginx 将所有未知路径都回退到主应用的index.html,并且主应用的路由配置要能正确匹配并激活对应的子应用activeRule。
Q: 什么是基座应用(主应用)?它主要负责什么?
A: 基座应用是整个微前端架构的“骨架”和“大脑”。它的职责非常明确:
- 布局框架:提供整个系统统一的顶部导航栏(Header)、侧边菜单(Sidebar)和底部版权信息(Footer)。
- 用户鉴权:统一处理用户的登录、注销、Token 刷新。子应用不需要关心用户是否登录,只需要信任主应用下发的权限信息。
- 路由分发:监听 URL 变化,决定当前应该加载哪个子应用,或者显示哪个公共页面(如登录页、404页)。
- 公共资源管理:加载公共的基础库、全局样式、错误监控 SDK 等。
复杂系统设计与Node.js
Q:Node.js 是单线程的,如何处理高并发请求??
A: 很多人误以为单线程就代表性能差,其实不然。Node.js 的高并发能力源于其事件驱动和非阻塞 I/O模型。
- 主线程(Event Loop) :Node.js 只有一个主线程,它非常轻量,只负责调度。当遇到 I/O 操作(如读文件、查数据库、发网络请求)时,它不会傻傻地等待结果,而是把这个任务交给操作系统底层的线程池(libuv 提供) 去处理,然后自己立刻去处理下一个请求。
- 异步回调:当底层线程池完成了耗时的 I/O 任务后,会把结果和一个回调函数放入任务队列。Event Loop 会在合适的时机取出这个回调,放回主线程执行。
- 结论:因为主线程 never block(从不阻塞),所以它能以极少的内存开销,同时维持成千上万个 TCP 连接,非常适合 I/O 密集型的 Web 服务。
Q:设计一个秒杀系统,核心要注意什么?
A: 秒杀系统的核心挑战在于:瞬间极大的流量(高并发) 与有限的库存(数据一致性) 之间的矛盾。我们需要层层设卡,保护后端数据库不被打死。
-
第一层:前端限流
- 秒杀按钮点击后立即置灰(防抖),禁止重复提交。
- 通过验证码(滑块/图形)拦截一部分机器脚本请求。
-
第二层:网关层限流与黑名单
- 使用 Nginx 或 API Gateway 限制单个 IP 的请求频率(如每秒最多 10 次)。
- 识别恶意 IP 加入黑名单,直接拒绝访问。
-
第三层:Redis 预减库存(关键)
- 数据库是磁盘 I/O,扛不住高并发写。秒杀开始前,先把商品库存加载到 Redis 缓存中。
- 请求到来时,先在 Redis 中执行原子递减操作(
DECR)。如果返回值小于 0,说明库存已空,直接返回“抢光了”,根本不去碰数据库。
-
第四层:消息队列异步下单
- Redis 扣减成功的请求,不要直接写数据库,而是发送一条消息到 MQ(如 Kafka/RabbitMQ)。
- 后端服务监听 MQ,按照自己的处理能力慢慢消费消息,从容地向数据库写入订单。这起到了削峰填谷的作用。
-
第五层:数据库乐观锁
- 最后一步写入 DB 时,使用 SQL 条件防止超卖:
UPDATE stock SET num = num - 1 WHERE id = 1 AND num > 0;
- 最后一步写入 DB 时,使用 SQL 条件防止超卖:
Q:什么是 BFF 层?它解决了什么问题?
A:
BFF (Backend for Frontend) ,即“服务于前端的后端”。它是介于前端和传统后端(微服务)之间的一个中间层,通常由 Node.js 实现。
-
解决的问题:
- 接口聚合:移动端或 PC 端的一个页面往往需要展示来自多个微服务的数据(如用户信息+订单列表+推荐商品)。如果没有 BFF,前端需要发 3 个 HTTP 请求;有了 BFF,前端只发 1 个请求,BFF 在服务端并行调用 3 个微服务,组装好数据后一次性返回给前端。
- 数据裁剪:后端微服务返回的字段可能非常多且通用,而前端只需要其中几个字段。BFF 可以负责过滤多余数据,减少网络传输体积。
- 适配多端:PC 端和 App 端需要的数据格式可能不同。BFF 可以为不同的端提供定制化的接口,而不需要让底层微服务去迁就前端。
Q:Node.js 中的 Stream(流)有什么作用?有哪些应用场景?
A: 核心作用:在处理大数据时,避免将其一次性全部加载到内存中,从而防止内存溢出(OOM)。Stream 就像自来水管一样,数据是一块一块(chunk)流动的,处理完一块再接收下一块。
-
应用场景:
- 大文件上传/下载:用户上传一个 2GB 的视频,如果用普通方式读取会直接撑爆服务器内存。使用 Stream 可以一边读取文件,一边写入磁盘或转发给云存储(OSS/S3)。
- 视频流媒体播放:在线看视频时,并不需要把整部电影下载下来。服务器通过 Stream 将视频切片,源源不断地推送到前端播放器。
- 日志分析:读取几个 G 的服务器日志文件,通过 Stream 逐行解析,统计关键词出现的次数。
- HTTP 代理:在 BFF 层做请求转发时,可以直接将上游服务的 Response Stream 管道(pipe)到下游客户端的 Response 中,无需在中间层缓存数据。
// 简单的流式文件复制示例
const fs = require('fs');
const readStream = fs.createReadStream('input.txt');
const writeStream = fs.createWriteStream('output.txt');
readStream.pipe(writeStream); // 管道机制,自动处理数据流动
Q:从拿到一个大型项目开始,如何做全链路优化
A:
- 监控与度量:先接入 APM(如 Sentry, Prometheus),确定瓶颈在哪里(是数据库慢、网络延迟还是前端渲染卡顿)。
- 前端层:CDN 加速、HTTP2、资源压缩、骨架屏、SSR 改造。
- 网关层 (BFF) :使用 Node.js 做聚合层,减少客户端请求次数,裁剪多余数据字段。
- 服务层:引入 Redis 缓存热点数据,数据库读写分离,消息队列(Kafka/RabbitMQ)削峰填谷。
- 基础设施:容器化部署,自动扩缩容。
Q:如何保证一个项目从开发到上线没有bug?讲讲你的思路
A:
- 开发阶段:强制 TypeScript 类型检查,ESLint/Prettier 规范代码,编写单元测试(Jest/Vitest)。
- 提交阶段:Git Hooks (Husky) 拦截不规范的提交,CI 流水线自动运行测试用例。
- 测试阶段:Code Review 机制,QA 介入集成测试,自动化 UI 测试(Cypress/Playwright)。
- 发布阶段:灰度发布(金丝雀发布),先开放给 5% 的用户,观察日志无异常后再全量推。
- 线上兜底:完善的错误监控系统(Sentry),一旦报错立即报警。
Q:一般项目从开发到上线,你认为需要具备几个环境才是合理的
A: 通常至少需要 3-4 个环境:
- 开发环境 (Dev) :开发人员本地或联调环境,不稳定,随时变动。
- 测试环境 (Test/QA) :供测试人员验证功能,相对稳定,每次发版前冻结。
- 预发布环境 (Staging/Pre-prod) :最关键的环境。数据和配置尽可能模拟生产环境,用于上线前的最后一次演练和回归。
- 生产环境 (Prod) :真实用户访问的环境,严格控制权限,禁止随意登录操作。
Q:Node.js 用过哪些东西
A:
- Web 框架:Express (轻量), Koa (洋葱模型), NestJS (企业级,依赖注入)。
- 中间件:处理日志 (Morgan)、鉴权 (JWT)、跨域 (Cors)、参数校验。
- 工具类:FS (文件流处理)、Path、Crypto (加密)、Child_Process (调用 Shell 脚本)。
- ORM/ODM:TypeORM, Prisma, Mongoose。
- 场景:BFF 层聚合接口、SSR 服务端渲染 (Next.js/Nuxt.js)、CLI 脚手架开发、服务端定时任务。
Q:使用过Docker部署吗
A:
-
基本概念:镜像 (Image)、容器 (Container)、仓库 (Repository)。
-
实战流程:
- 编写
Dockerfile:指定基础镜像 (node:alpine),复制代码,安装依赖,暴露端口,定义启动命令。 - 构建镜像:
docker build -t my-app . - 运行容器:
docker run -d -p 3000:3000 my-app - 编排:配合
docker-compose.yml一键启动前端、后端、Redis、MySQL 等多个服务。
- 编写
TypeScript 深度
Q:TS在项目中的定位
A: TS 不仅仅是“带类型的 JS”,它的核心价值在于:
- 静态检查:在编译阶段发现潜在的类型错误,减少运行时 Crash。
- 活文档:类型定义即文档。新人入职看 Interface 就能知道数据结构,无需翻阅冗长的 Wiki。
- 智能提示:极大地提升了 IDE 的代码补全和重构能力(如重命名变量、查找引用)。
- 架构约束:通过严格的类型系统,强制规范团队的代码结构和数据流向。
Q:TS @ 符号的使用(如Angular中的装饰器是用来实现什么的)
A:
@ 符号代表 装饰器 (Decorator) ,它是一种实验性提案(但在 Angular/NestJS 中广泛使用,TS 5.0+ 已正式支持)。
-
本质:它是一个高阶函数,用于在不修改原有类/方法代码的情况下,动态地添加元数据或修改行为(AOP 面向切面编程思想)。
-
常见用途:
- 类装饰器:如
@Component,用于注册组件元数据。 - 方法装饰器:如
@Get('/api'),用于定义路由映射;或@Debounce(300)实现防抖。 - 属性装饰器:如
@Input(),用于标记输入属性。
- 类装饰器:如
Q:TS中的一些冷门/高阶知识点
A:
- 协变与逆变:理解函数参数类型的兼容性规则(默认是双向协变,开启
strictFunctionTypes后参数变为逆变)。 - 条件类型与 infer:
T extends U ? X : Y,配合infer可以提取类型中的部分信息(如提取 Promise 的返回值类型type Unpack<T> = T extends Promise<infer R> ? R : T;)。 - 映射类型:
keyof和in关键字,结合readonly或?修饰符,可以快速生成新类型(如Partial<T>,Pick<T, K>)。 - 声明合并:Interface 可以重复定义并自动合并,而 Type Alias 不行。常用于扩展第三方库的类型定义。
Q:interface 和 type 的区别是什么?****
A:
| 特性 | Interface (接口) | Type Alias (类型别名) | |
|---|---|---|---|
| 核心用途 | 定义对象的结构(Shape),强调契约和规范。 | 给任何类型起个名字,强调类型的复用和组合。 | |
| 扩展方式 | 支持声明合并。如果定义了两个同名的 interface,TS 会自动把它们合并。 | 不支持合并,同名会报错。 | |
| 被继承 | 可以被 class 使用 implements 关键字实现。 | 不能被 class 实现。 | |
| 表达能力 | 只能描述对象、函数签名。 | 极其强大,可描述基本类型、联合类型(` | )、元组、交叉类型(&`)等。 |
Q:什么是泛型(Generics)?在什么场景下使用?
A:
通俗理解:泛型就是“类型的变量”。我们在定义函数或类的时候,先不指定具体的类型(如 string 或 number),而是用一个占位符(通常是 <T>)代替。等到真正使用这个函数时,再传入具体的类型。
核心价值:在保证类型安全的前提下,极大提升代码的复用性。
经典场景:封装通用的 HTTP 请求函数。
// 不使用泛型:只能返回 any,丢失了类型提示
function get(url: string): any { ... }
// 使用泛型:调用时可以指定返回的数据结构
interface User { id: number; name: string; }
function get<T>(url: string): Promise<T> {
return fetch(url).then(res => res.json());
}
// 使用时,T 被推导或指定为 User,res 就有完美的智能提示
get<User>('/api/user').then(res => {
console.log(res.name); // TS 知道 res 有 name 属性
});
Q:tsconfig.json 中 strict 选项包含哪些内容?
A:
"strict": true 是 TypeScript 的灵魂开关。开启它意味着开启了全家桶级别的严格检查,虽然刚开始写代码会觉得繁琐,但能规避掉 80% 的低级错误。它主要包含以下子选项:
noImplicitAny(最重要):禁止隐式的any类型。如果一个变量或参数你没有标注类型,且 TS 无法推断出来,它就会报错。这强制你写出类型明确的代码。strictNullChecks(最重要):严格的空值检查。null和undefined不再能赋值给任意类型。例如let a: string = null会报错,你必须写成string | null。这能有效防止线上常见的Cannot read property of undefined报错。strictFunctionTypes:对函数参数的类型进行更严格的逆变检查。strictBindCallApply:严格检查bind,call,apply的参数类型。noImplicitThis:当this的类型为any时报错。
Q:如何利用 TS 实现一个深拷贝的类型推导?
A: 这是一个非常高阶的类型体操题目。核心思路是利用递归条件类型和 infer 推断。我们需要判断当前类型 T 是不是一个对象,如果是,就遍历它的每一个 Key,并对 Value 再次调用深拷贝类型。
// 基础类型(包括函数)直接返回原类型
type Primitive = string | number | boolean | bigint | symbol | undefined | null | Function;
// 深拷贝类型推导
type DeepClone<T> = T extends Primitive
? T // 如果是基础类型,直接返回
: T extends Array<infer U>
? Array<DeepClone<U>> // 如果是数组,递归处理每一项
: T extends object
? { [K in keyof T]: DeepClone<T[K]> } // 如果是对象,递归处理每一个属性
: T;
// 测试
interface Original {
a: number;
b: { c: string };
d: Date;
}
type Cloned = DeepClone<Original>;
// Cloned 的类型结构将会是:
// { a: number; b: { c: string }; d: Date; }