2026前端面试题(初、中、高级)

11 阅读24分钟

声明:本文内容仅代表个人对前端知识体系的理解与复盘,受限于个人认知水平,若有错误或表述不严谨的地方,恳请各位大佬批评指正,共同进步~

初级/基础题

这部分主要考察对前端基础知识(HTML/CSS/JS)的掌握程度,以及常用框架的基本使用。

HTML&&CSS

Q:HTML行内元素和块级元素的区别

A:

  • 块级元素 (Block-level):  如 divph1-h6。它们独占一行,可以设置宽度、高度、内外边距。默认宽度是其父容器的100%。
  • 行内元素 (Inline):  如 spanastrong。它们不独占一行,与其他行内元素并排显示。设置宽度和高度无效,其尺寸由内容撑开。垂直方向的内外边距可能不会按预期生效。
  • 行内块元素 (Inline-block):  如 imginput。结合了两者特点,既能在同一行显示,又能设置宽高和内外边距

Q:CSS伪类与伪元素

A:

  • 伪类 (Pseudo-class):  用于定义元素的特殊状态,以单冒号 : 开头。例如 :hover (鼠标悬停)、:focus (获得焦点)、:nth-child(n) (选择第n个子元素)。它选择的是“文档树中已存在的元素”。
  • 伪元素 (Pseudo-element):  用于创建不在文档树中的虚拟元素,以双冒号 :: 开头(CSS3规范,但单冒号也兼容)。例如 ::before 和 ::after,常配合 content 属性在元素前后插入内容。它创建的是“文档树中不存在的元素”

Q:什么是外边距合并?如何解决

A:

  • 现象:  当两个或多个垂直方向相邻的块级元素的外边距相遇时,它们会合并成一个外边距,其大小为两者中的较大值,而不是相加。

  • 解决思路:  破坏它们“相邻”的条件。

    1. 触发BFC:  给其中一个元素的父容器设置 overflow: hiddendisplay: flow-root 等,使其成为一个独立的渲染区域。
    2. 使用内边距或边框:  用 padding 或 border 代替 margin
    3. 使用空标签或浮动:  在两个元素间插入一个空的、设置了 clear: both 的元素,或让其中一个元素浮动。

Q:解释一下BFC(块级格式化上下文),BFC脱离文档流了吗?

A:

  • 定义:  BFC是一个独立的渲染区域,内部的元素布局不受外部影响,反之亦然。

  • 触发条件:  float 不为 noneposition 为 absolute 或 fixeddisplay 为 inline-blocktable-cellflex 等、overflow 不为 visible

  • 作用:

    1. 防止外边距合并。
    2. 清除内部浮动(父元素触发BFC后,计算高度时会包含浮动的子元素)。
    3. 阻止元素被浮动元素覆盖。
  • 是否脱离文档流:  没有。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)。 - 数据结构:  MapSet。 - 异步编程:  Promiseasync/await。 - 模块化:  import / export。 - 类:  class 语法糖。

Q:深浅拷贝:如何实现一个深拷贝

A:

  • 浅拷贝:  只复制对象的引用,新旧对象共享同一块内存。Object.assign(), 展开运算符 ... 都是浅拷贝。

    • 深拷贝:  递归地复制对象的所有层级,新旧对象完全独立。

    • 简单方式:  JSON.parse(JSON.stringify(obj))。缺点:无法处理函数、undefinedSymbol、循环引用、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...ofsome() 或 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):

    1. 从数组中选择一个“基准”元素。
    2. 将数组分为两部分,比基准小的放左边,比基准大的放右边。
    3. 对左右两部分递归执行上述步骤。
    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: -
复杂数据类型 (引用类型):  包括 ObjectArrayFunctionDateRegExp 等。它们在栈中存储的是指向堆内存的地址。function 是一种特殊的对象,属于复杂数据类型。

  • 类型判定:

    1. typeof:  适用于基本类型判断,但对 null 和引用类型(除 function 外)判断不准确。typeof null 为 'object'
    2. instanceof:  判断一个实例是否属于某个构造函数。[] instanceof Array 为 true
    3. 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特性:

    1. 组合式API (Composition API):  引入 setup 函数,逻辑组织更灵活,解决了Options API在大型组件中逻辑分散的问题。
    2. 响应式系统重构:  使用 Proxy 替代 Object.defineProperty,性能更好,能监听数组和对象属性的增删。
    3. 性能提升:  虚拟DOM重写、Tree-shaking支持更好、打包体积更小。
    4. 更好的TypeScript支持

Q:v-if 与 v-show 的区别

A:

  • 手段不同:  v-if 是真正的条件渲染,会根据表达式的值在DOM中销毁或重建元素及事件监听器。v-show 只是简单地切换元素的CSS display 属性。
  • 编译过程不同:  v-if 有局部编译/卸载的过程,切换时有更高的开销。v-show 在任何条件下都会被编译并保留在DOM中,初始渲染开销更高。
  • 使用场景:  v-if 适用于运行时条件很少改变的场景。v-show 适用于需要频繁切换的场景。

Q:React父子组件如何通信 / 父组件如何调用子组件方法

A: -

  • 父传子:  通过 props 传递数据或函数。

  • 子传父:  父组件将一个函数作为 prop 传递给子组件,子组件在需要时调用该函数,并将数据作为参数传入。

  • 父调子方法:

    • Class组件:  使用 ref 获取子组件实例,然后调用其方法。
    • Function组件:  使用 useImperativeHandle Hook 配合 forwardRef 来暴露子组件的方法给父组件。

Q:React生命周期 / class和hooks分别是如何实现生命周期的

A:

  • Class组件生命周期:  constructor -> static getDerivedStateFromProps -> render -> componentDidMount -> componentDidUpdate -> componentWillUnmount

  • Hooks实现:  使用 useEffect Hook 来模拟生命周期。

    • componentDidMountuseEffect(() => { ... }, [])
    • componentDidUpdateuseEffect(() => { ... }) (无依赖数组) 或 useEffect(() => { ... }, [dep])
    • componentWillUnmountuseEffect(() => { return () => { ... } }, []) (返回一个清理函数)

Q:受控组件和非受控组件

A:

  • 受控组件 (Controlled Component):  表单数据由React组件的 state 管理。每次输入变化都会触发 onChange 事件更新 state,从而重新渲染组件。优点是数据来源单一,易于验证和操作。
  • 非受控组件 (Uncontrolled Component):  表单数据由DOM自身管理。通过 useRef 来获取DOM节点的值。优点是写法简单,适合简单的表单集成。

Q:如何使用React Hooks实现之前的生命周期 / React Hooks相对于class组件的优势

A:

  • 实现生命周期:  见上文 useEffect 的用法。

  • Hooks优势:

    1. 逻辑复用:  自定义Hook可以轻松提取和复用状态逻辑,避免了HOC和Render Props的嵌套地狱。
    2. 代码更简洁:  解决了Class组件中 this 指向混乱的问题,相关逻辑可以聚合在一起,而不是分散在各个生命周期方法中。
    3. 学习成本更低:  对于新手来说,函数组件比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 来管理表单项的值。

  • 自定义组件需要遵循这个约定:

    1. 接收 value prop 来显示当前值。
    2. 在内部状态变化时,调用 onChange prop 将新值通知给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等):

    1. URL参数:  通过路由跳转时携带参数 (/page?id=123)。
    2. 本地存储:  使用 localStorage 或 sessionStorage 存储共享数据。
    3. 发布订阅模式:  自己实现一个简单的Event Bus。

网络与浏览器基础

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:

    1. 多路复用:  解决了队头阻塞,可以在一个TCP连接上并行发送多个请求和响应。
    2. 头部压缩:  使用HPACK算法压缩请求头,减少传输体积。
    3. 服务端推送:  服务器可以主动向客户端推送资源。

Q:如何解决跨域问题(CORS原理及配置)

A:

  • 同源策略:  浏览器的一种安全机制,限制从一个源加载的文档或脚本与来自另一个源的资源进行交互。
  • CORS (跨域资源共享):  一种W3C标准,允许服务器声明哪些源可以通过浏览器访问其资源。
  • 原理:  浏览器在发起跨域请求时,会自动在请求头中添加 Origin 字段。服务器在响应头中设置 Access-Control-Allow-Origin 来告知浏览器是否允许该源的访问。
  • 配置:  后端服务器设置

Q:输入URL点击回车后的发生过程。

A:

  • 网络请求阶段

    1. DNS解析:浏览器先查缓存(浏览器/系统/Hosts),若无则向DNS服务器发起递归查询,将域名转换为IP地址。
    2. 建立连接:基于IP地址,通过TCP三次握手建立可靠连接;若是HTTPS,还需进行TLS握手协商加密密钥。
    3. 发送请求:浏览器构建HTTP请求报文发送给服务器。
    4. 接收响应:服务器处理请求并返回HTTP响应报文(含状态码如200/304及资源内容)。
  • 渲染构建阶段

    1. 解析构建:浏览器解析HTML生成DOM树,解析CSS生成CSSOM树。
    2. 生成渲染树:结合DOM和CSSOM,剔除不可见元素(如display: none),生成Render Tree。
    3. 布局:计算各节点在屏幕上的确切位置和大小。
    4. 绘制与合成:将各图层绘制到位图,最后由GPU合成并显示在屏幕上。
  • 脚本执行阶段

    • 在解析HTML过程中遇到<script>标签时,会暂停DOM构建,下载并执行JS代码(除非标记了asyncdefer),这可能会修改DOM或CSSOM从而触发重新渲染。
  • 缓存与断开

    • 浏览器根据响应头(Cache-Control等)决定是否缓存资源;页面加载完成后,根据Connection头决定是否断开TCP连接

中级

这部分考察对技术底层原理的理解、工程化能力以及解决实际问题的能力

JavaScript 进阶

Q:事件循环(Event Loop):宏任务与微任务(为什么会有事件循环?)

A:

  • 事件循环

    1. 同步优先:先执行完当前调用栈中的所有同步代码。
    2. 微任务清空:同步代码执行完毕后,立刻清空当前的微任务队列。如果在执行微任务时又产生了新的微任务,也会在当前轮次一并执行。
    3. 渲染与下一轮宏任务:微任务清空后,浏览器可能会进行 UI 渲染,随后从宏任务队列中取出一个宏任务执行,重复上述过程。
  • 宏任务 (MacroTask) :通常由宿主环境发起,包括 setTimeoutsetInterval、I/O 操作、UI 渲染等。

  • 微任务 (MicroTask) :通常由 JS 引擎发起,优先级更高,包括 Promise.then/catch/finallyqueueMicrotaskMutationObserver 等

    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 的内部实现本质上是一个带有状态机的发布订阅模式:

  1. 三种状态Pending(等待态)、Fulfilled(成功态)、Rejected(失败态)。状态一旦改变就不可逆,确保了结果的确定性。
  2. 回调队列:内部维护了两个队列(成功回调队列和失败回调队列)。当调用 .then() 时,如果状态未定,就将回调函数推入对应队列;当状态改变时,依次执行队列中的回调。
  3. 异步执行:Promise 的回调会被放入微任务队列,确保在当前同步代码执行完毕后再执行。

关于 Promise 的取消:
原生的 Promise 一旦创建就会立即执行,且无法中途取消。但我们可以通过以下几种方式模拟“取消”行为:

  1. AbortController(推荐) :现代浏览器原生支持的 API,常用于中止 fetch 请求。
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal }); // 需要取消时调用 
controller.abort();
  1. 自定义封装:通过一个外部标志位(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)机制,目的是找出不再使用的变量并释放其占用的内存。

常见的回收算法:

  1. 引用计数法:记录每个对象被引用的次数,当引用数为 0 时回收。缺点是无法解决“循环引用”导致的内存泄漏问题(即 A 引用 B,B 引用 A,但外部已经不再使用它们)。

  2. 标记-清除法(Mark-and-Sweep) :现代浏览器的主流算法。

    • 标记阶段:从“根对象”(如全局对象 window、当前执行栈的局部变量)出发,递归遍历所有可达的对象并打上标记。
    • 清除阶段:遍历堆内存,凡是没被打上标记的对象,就视为垃圾并进行清理。这种方法完美解决了循环引用的问题。

V8 引擎的优化(分代回收):
为了提升效率,V8 引擎将内存分为新生代老生代

  • 新生代:存放存活时间短的小对象。采用 Scavenge 算法(复制算法),将存活对象复制到另一块空间,然后清空原空间,效率极高。
  • 老生代:存放存活时间长的大对象。采用标记-清除和标记-整理(Compact,防止内存碎片化)相结合的算法。

Webpack与工程化

Q:Webpack打包机制与原理 / 编译过程

A: Webpack 是一个现代 JavaScript 应用程序的静态模块打包器。其核心原理可以概括为:一切皆模块

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数。
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
  3. 确定入口:根据配置中的 entry 找出所有的入口文件。
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,递归本步骤直到所有入口依赖的文件都经过了处理。
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

Q:Webpack的load和plugin有什么区别?常用的有哪些

A:

  • 区别

    • Loader (转换器) :本质是一个函数。Webpack 默认只能处理 JS/JSON 文件,Loader 的作用是让 Webpack 能够处理其他类型的文件(如 CSS, Vue, TS, 图片等),并将它们转换为有效的模块。它运行在打包前的转换阶段。
    • Plugin (扩展器) :基于事件流框架 Tapable。Plugin 的作用是扩展 Webpack 的功能,它可以介入打包的整个生命周期(如优化、压缩、定义环境变量等)。它运行在打包全过程
  • 常用 Loaderbabel-loader (转译ES6+), css-loaderstyle-loaderless-loaderurl-loader/file-loader (处理图片)。

  • 常用 PluginHtmlWebpackPlugin (生成HTML), MiniCssExtractPlugin (提取CSS), DefinePlugin (定义环境变量), CleanWebpackPlugin (清理目录)。

Q:Webpack如何分包 / 利用什么机制分包

A: 主要利用 Code Splitting (代码分割)  机制,核心配置是 optimization.splitChunks

  1. 多入口打包:配置多个 entry,自动分离公共代码。
  2. 动态导入 (Dynamic Imports) :使用 import() 语法,Webpack 会自动将其分割为一个独立的 Chunk,实现按需加载。
  3. 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:

  • 缓存方式

    1. 内存缓存:开发模式下默认开启。
    2. 文件系统缓存 (持久化缓存) :配置 cache: { type: 'filesystem' },将编译结果写入硬盘,重启构建时直接读取。
    3. 快照 (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 协议

    1. 物料区:提供可拖拽的组件。
    2. 画布区:接收拖拽事件,记录组件的位置和层级。
    3. 属性区:修改选中组件的 Props。
    4. 渲染引擎:将生成的 JSON Schema 递归渲染为真实的 DOM。

React&Vue原理

Q:React Hooks内部实现原理 / 自定义一个Hooks

A:

  • 原理:React 内部维护了一个链表结构。每个 Function Component 都有一个对应的 Fiber 节点,Fiber 上有一个 memoizedState 属性指向第一个 Hook 对象。每个 Hook 对象包含 memoizedState (保存 state)、baseStatequeue (更新队列) 和 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:

  1. 调用 setState 或 useState 的 setter。
  2. React 创建一个新的 Fiber 树(WorkInProgress)。
  3. Reconciler (Diff) :对比新旧 Fiber 树,标记出变化的节点(Placement, Update, Deletion)。
  4. 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。
  • 优化思路

    1. 网络层:HTTP2、CDN、Gzip/Brotli 压缩、DNS 预解析。
    2. 构建层:代码分割、Tree Shaking、图片格式优化 (WebP)。
    3. 渲染层:减少重排重绘、虚拟列表(长列表)、防抖节流、SSR (服务端渲染)。
    4. 感知层:骨架屏、懒加载。

Q:项目打包从200M优化到120M做了哪些事情

A:

  1. 分析:使用 webpack-bundle-analyzer 分析包体积,发现大文件主要是 Moment.js、Echarts 和一些重复的 lodash。
  2. 替换:用 dayjs 替换 moment.js(体积从几百K降到几K)。
  3. 外部化:将 React/Vue/AntD 等基础库通过 CDN 引入,配置 externals 不打包进 bundle。
  4. 按需引入:配置 AntD/Echarts 的按需加载插件。
  5. 压缩:开启 Gzip 压缩,配置图片压缩。
  6. 去重:使用 IgnorePlugin 忽略不必要的语言包(如 moment 的 locale)。

Q:CDN基本概念、配置及缓存策略(CDN服务器宕机怎么办)

A:

  • 概念:内容分发网络,将源站内容分发到全球各地的边缘节点,用户就近获取内容。

  • 策略:静态资源(JS/CSS/Img)设置较长的强缓存(如 1 年),文件名带 Hash;HTML 文件设置协商缓存或不缓存。

  • 宕机处理

    1. 健康检查:CDN 厂商会自动监测节点健康状态。
    2. 回源:如果边缘节点宕机,流量会调度到其他可用节点或直接回源站获取。
    3. 多 CDN 容灾:大型项目会接入多家 CDN 厂商,通过 DNS 智能解析切换流量。

Q:微信原生小程序性能指标

A:

  • 首屏时间:从打开小程序到第一屏完全渲染的时间。
  • setData 耗时setData 是通信桥梁,频繁或大数据量的 setData 会导致卡顿。
  • 页面路径深度:避免过深的嵌套。
  • 优化手段:分包加载、图片懒加载、避免频繁的 setData、使用 wxs 处理手势交互(减少逻辑层与渲染层通信)。

Q:一个页面的按钮权限如何控制

A:

  1. 后端返回权限表:登录时,后端返回当前用户的权限列表(如 ['btn:add', 'btn:delete'])。

  2. 全局存储:将权限列表存入 Vuex/Pinia 或 Redux/Context。

  3. 封装指令/组件

    • Vue 指令v-auth="'btn:add'",在指令的 mounted 钩子中判断当前按钮标识是否在权限列表中,不在则 el.parentNode.removeChild(el)
    • React 组件<Auth code="btn:add"><Button>删除</Button></Auth>,组件内部判断无权限则返回 null

高级

这部分考察架构视野、复杂系统设计能力及团队管理思维

微前端架构

Q:为什么要使用微前端?能解决什么痛点?前期做了哪些调研

A:

  • 核心痛点

    1. 巨石应用维护难:随着业务迭代,单体应用代码量巨大,编译慢、构建慢,且牵一发而动全身。
    2. 技术栈锁定:老项目可能基于 jQuery 或老旧框架,无法直接升级,但又需要开发新功能。
    3. 多团队协作冲突:多个团队在一个仓库开发,容易产生代码冲突,部署流程耦合严重。
  • 调研方向:通常会对比 iframe(隔离好但通信难、状态不共享)、npm 分包(技术栈必须统一)、Web Components(兼容性问题)以及 qiankun/single-spa 方案。最终选择 qiankun 通常是因为其完善的沙箱机制和 HTML Entry 模式。

Q:Qiankun的特性是什么?有没有看过qiankun的底层原理

A:

  • 特性

    1. HTML Entry:直接通过 URL 加载子应用,像 iframe 一样简单,但体验更好。
    2. 样式隔离:支持 strictStyleIsolation (Shadow DOM) 和 experimentalStyleIsolation (Scoped CSS 类似实现)。
    3. JS 沙箱:确保子应用的全局变量不会污染主应用。
    4. 资源预加载:利用浏览器空闲时间加载子应用资源。
  • 底层原理

    • 路由劫持:重写 window.history.pushState/replaceState,监听路由变化来匹配子应用。

    • 入口解析:使用 import-html-entry 库 fetch 子应用的 HTML,解析出 script 和 link 标签。

    • 沙箱机制

      • Legacy Sandbox:基于 Proxy 的单例沙箱(适用于单实例)。
      • Proxy Sandbox:基于 Proxy 的多例沙箱(适用于多实例并存),在激活时记录变更,卸载时还原 window 对象。

Q:Qiankun配置微应用入口后,entry是如何实现应用的路由的

A: Qiankun 启动时会调用 start(),内部会进行路由劫持。

  1. 监听变化:它监听了浏览器的 popstate 事件,并重写了 history.pushState 和 history.replaceState 方法。
  2. 匹配规则:当路由发生变化时,Qiankun 会遍历注册的所有微应用(registerMicroApps),根据配置的 activeRule(通常是路径前缀)判断当前路由属于哪个微应用。
  3. 加载执行:如果匹配到新的微应用,就通过 import-html-entry 请求该应用的 HTML,提取 JS/CSS 并在沙箱环境中执行;如果匹配到旧应用离开,则触发卸载逻辑。

Q:微前端中微任务注入权限如何控制

A:

  • 控制思路

    1. 主应用鉴权:在主应用的 registerMicroApps 的 beforeLoad 钩子中进行全局权限校验。如果用户无权限访问某模块,直接拦截加载并跳转 403 页面。
    2. Props 传递:主应用将用户的权限列表(如按钮权限、菜单权限)通过 props 传递给子应用。
    3. 子应用消费:子应用在初始化时接收这些权限数据,结合自身的路由守卫或组件级指令(如 Vue 的 v-auth)来控制页面元素的显示隐藏。

Q:有没有看过 single-spa 的内部实现方式

A:

  1. 应用注册:提供一个 registerApplication 函数,接收应用名称、加载函数、激活函数。

  2. 路由监听:内部监听 hashchange 和 popstate 事件。

  3. 生命周期流转:当事件触发时,它会检查所有注册的应用。

    • 如果应用从“未激活”变为“激活”,则依次调用其 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 })。
    解决

    1. 开启 qiankun 的 experimentalStyleIsolation: true,它会给子应用的样式自动加上类似 [data-qiankun="app1"] .btn 的选择器前缀(类似于 Shadow DOM 的效果)。
    2. 团队内部强制推行 CSS Modules 或 Scoped CSS,从源头避免全局样式。
  • 坑二:全局变量污染
    现象:子应用在 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;

Q:什么是 BFF 层?它解决了什么问题?

A:

BFF (Backend for Frontend) ,即“服务于前端的后端”。它是介于前端和传统后端(微服务)之间的一个中间层,通常由 Node.js 实现。

  • 解决的问题

    1. 接口聚合:移动端或 PC 端的一个页面往往需要展示来自多个微服务的数据(如用户信息+订单列表+推荐商品)。如果没有 BFF,前端需要发 3 个 HTTP 请求;有了 BFF,前端只发 1 个请求,BFF 在服务端并行调用 3 个微服务,组装好数据后一次性返回给前端。
    2. 数据裁剪:后端微服务返回的字段可能非常多且通用,而前端只需要其中几个字段。BFF 可以负责过滤多余数据,减少网络传输体积。
    3. 适配多端:PC 端和 App 端需要的数据格式可能不同。BFF 可以为不同的端提供定制化的接口,而不需要让底层微服务去迁就前端。

Q:Node.js 中的 Stream(流)有什么作用?有哪些应用场景?

A: 核心作用:在处理大数据时,避免将其一次性全部加载到内存中,从而防止内存溢出(OOM)。Stream 就像自来水管一样,数据是一块一块(chunk)流动的,处理完一块再接收下一块。

  • 应用场景

    1. 大文件上传/下载:用户上传一个 2GB 的视频,如果用普通方式读取会直接撑爆服务器内存。使用 Stream 可以一边读取文件,一边写入磁盘或转发给云存储(OSS/S3)。
    2. 视频流媒体播放:在线看视频时,并不需要把整部电影下载下来。服务器通过 Stream 将视频切片,源源不断地推送到前端播放器。
    3. 日志分析:读取几个 G 的服务器日志文件,通过 Stream 逐行解析,统计关键词出现的次数。
    4. 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:

  1. 监控与度量:先接入 APM(如 Sentry, Prometheus),确定瓶颈在哪里(是数据库慢、网络延迟还是前端渲染卡顿)。
  2. 前端层:CDN 加速、HTTP2、资源压缩、骨架屏、SSR 改造。
  3. 网关层 (BFF) :使用 Node.js 做聚合层,减少客户端请求次数,裁剪多余数据字段。
  4. 服务层:引入 Redis 缓存热点数据,数据库读写分离,消息队列(Kafka/RabbitMQ)削峰填谷。
  5. 基础设施:容器化部署,自动扩缩容。

Q:如何保证一个项目从开发到上线没有bug?讲讲你的思路

A:

  1. 开发阶段:强制 TypeScript 类型检查,ESLint/Prettier 规范代码,编写单元测试(Jest/Vitest)。
  2. 提交阶段:Git Hooks (Husky) 拦截不规范的提交,CI 流水线自动运行测试用例。
  3. 测试阶段:Code Review 机制,QA 介入集成测试,自动化 UI 测试(Cypress/Playwright)。
  4. 发布阶段:灰度发布(金丝雀发布),先开放给 5% 的用户,观察日志无异常后再全量推。
  5. 线上兜底:完善的错误监控系统(Sentry),一旦报错立即报警。

Q:一般项目从开发到上线,你认为需要具备几个环境才是合理的

A: 通常至少需要 3-4 个环境

  1. 开发环境 (Dev) :开发人员本地或联调环境,不稳定,随时变动。
  2. 测试环境 (Test/QA) :供测试人员验证功能,相对稳定,每次发版前冻结。
  3. 预发布环境 (Staging/Pre-prod)最关键的环境。数据和配置尽可能模拟生产环境,用于上线前的最后一次演练和回归。
  4. 生产环境 (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)。

  • 实战流程

    1. 编写 Dockerfile:指定基础镜像 (node:alpine),复制代码,安装依赖,暴露端口,定义启动命令。
    2. 构建镜像:docker build -t my-app .
    3. 运行容器:docker run -d -p 3000:3000 my-app
    4. 编排:配合 docker-compose.yml 一键启动前端、后端、Redis、MySQL 等多个服务。

TypeScript 深度

Q:TS在项目中的定位

A: TS 不仅仅是“带类型的 JS”,它的核心价值在于:

  1. 静态检查:在编译阶段发现潜在的类型错误,减少运行时 Crash。
  2. 活文档:类型定义即文档。新人入职看 Interface 就能知道数据结构,无需翻阅冗长的 Wiki。
  3. 智能提示:极大地提升了 IDE 的代码补全和重构能力(如重命名变量、查找引用)。
  4. 架构约束:通过严格的类型系统,强制规范团队的代码结构和数据流向。

Q:TS @ 符号的使用(如Angular中的装饰器是用来实现什么的)

A:

@ 符号代表 装饰器 (Decorator) ,它是一种实验性提案(但在 Angular/NestJS 中广泛使用,TS 5.0+ 已正式支持)。

  • 本质:它是一个高阶函数,用于在不修改原有类/方法代码的情况下,动态地添加元数据或修改行为(AOP 面向切面编程思想)。

  • 常见用途

    • 类装饰器:如 @Component,用于注册组件元数据。
    • 方法装饰器:如 @Get('/api'),用于定义路由映射;或 @Debounce(300) 实现防抖。
    • 属性装饰器:如 @Input(),用于标记输入属性。

Q:TS中的一些冷门/高阶知识点

A:

  1. 协变与逆变:理解函数参数类型的兼容性规则(默认是双向协变,开启 strictFunctionTypes 后参数变为逆变)。
  2. 条件类型与 inferT extends U ? X : Y,配合 infer 可以提取类型中的部分信息(如提取 Promise 的返回值类型 type Unpack<T> = T extends Promise<infer R> ? R : T;)。
  3. 映射类型keyof 和 in 关键字,结合 readonly 或 ? 修饰符,可以快速生成新类型(如 Partial<T>Pick<T, K>)。
  4. 声明合并: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:严格检查 bindcallapply 的参数类型。
  • 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; }