【前端八股第一弹】

1.说说你对模块化的理解:

模块化的实用性及场景(对背景的理解,对知识的串联化及相关性搜索整合能力):对于开发者来说,使用模块化有利于代码的开发和维护,便于后续上线部署,项目性能优化需要模块化来进行代码整理和归类,工程化中模块化在整个CI/CD以及Docker容器化中存在大用。模块化的核心价值在于封装隔离。它不仅利于代码维护,更是Tree Shaking、懒加载、CICD 并行构建、Docker 分层缓存 的基础。例如,在 CI 中,只有 package-lock.json 变化时才重新 npm install 层,这依赖于模块的依赖树可哈希化。

通用来说,模块化有以下几种方案:CommonJS,AMD,ESModule,CMD。

详述:

CJS:设计用于服务器 。重点:同步加载,因为文件在本地磁盘。require运行时加载,module.exports导出的值是浅拷贝,一旦导出,模块内部变化不影响已加载的值,且支持动态require(在if条件里),故CJS不适用于哪些场景:

需要状态同步的场景 :由于 CJS 导出的是值的浅拷贝,若模块内部状态(如计数器、配置对象)在加载后被修改,其他模块无法感知。这在微前端共享运行时状态热更新 (HMR) 中会导致不一致。

浏览器环境(无打包工具时):同步加载会阻塞页面渲染。

循环依赖复杂的项目 :CJS 对循环依赖的处理通过返回已执行部分的半完成对象,容易引发未定义错误。

AMD:设计用于浏览器。异步加载,依赖前置(先定义所有依赖再执行回调),适合网页首屏优化。

CMD:国内规范。异步加载,依赖就近(用到时才 require),语法接近 CommonJS(有点类似金丝雀发布的逻辑)。

ESModule:规范标准。编译时静态加载(这里和CJS的动态加载区分开),支持Tree Shaking(tree shaking的前提是:必须是静态加载,构建工具可以在编译时 确定哪些导出被使用,哪些是"副作用"的。CommonJS 的 require 是运行时表达式(如 require(condition ? 'a' : 'b')),无法静态分析)。

2.前端跨页面通信方法:

场景:同源、非同源。

同源场景:电商网站的"购物车"tab 与商品详情页同步数量;后台管理系统的"全局通知已读"同步。

非同源场景:主站内嵌第三方客服 iframe,需传递用户 token 或操作行为。

方法:

同源:

Brodcast Channel API:创建一个广播频道,同源下的所有标签页均可监听和发送信息。简单、实时,但数据量小(不适合传递大文件),且没有持久化

Service Worker:利用其作为代理,多个页面通过postMessage与其通信,再由他转发(联系实时通信)(联系PWA离线缓存)。

LocalStorage/StorageEvent:一个页面修改localStorage,其他同源页面会触发window.onstorage事件,适合简单状态同步。

SharedWorker:共享线程,多页面共享一个Worker上下文。可以维持复杂内存状态(如 WebSocket 连接池),适合多人协作(类似飞书文档),页面关闭后 worker 仍可存活。

postMessage+window.open:通过window.open拿到子窗口句柄,或通过window.opener反向通信。

非同源(有限制):

postMessage:通过iframe嵌入或者window.open,配合targetOrigin白名单进行跨域通。非同源通信时提到 iframe,可联系微前端的 沙箱隔离<iframe sandbox="..."> 可以限制表单提交、脚本执行。在微前端中,iframe 是最彻底的隔离方案,但会导致子应用路由同步困难(需要 postMessage 代理历史记录)。

3.JS脚本延迟加载的方式:

思考什么情况下需要JS脚本延迟加载。

前因后果:回顾浏览器渲染的过程:JS引擎执行JS代码、渲染引擎执行:解析HTML、解析CSS、生成DOM树、布局、绘制、渲染。前因:HTML 解析器遇到 JS<script src="...">(无 defer/async)时,会暂停 DOM 构建,下载并执行 JS,之后才恢复解析。渲染(paint)被阻塞的条件:CSSOM 未就绪 + JS 查询样式。后果:需要实现JS脚本延迟加载。

方式:

defer属性:<script defer src="">。下载与HTML解析并行,但在DOMContentLoaded事件之前、DOM构建完成之后按顺序执行。保证执行顺序。

async属性:<script async src="">下载完成之后立即暂停HTML解析,执行脚本。执行顺序不定,谁先下载完谁先执行。适合独立第三方脚本(Google Analytics、百度统计、广告脚本。它们不依赖 DOM 结构,也不依赖于其他脚本。)。

动态创建script标签:document.creatElment("script"),设置src后插入DOM。可控性最高,支持onload回调。插入 DOM 前设置 script.async = false 可以强行保证执行顺序(利用 onload 链式调用)。注意内存与全局污染:动态脚本通常无法被 defer 管理,需手动清理。

ESModule默认延迟:<script type="module"> 默认具有 defer 行为。

4.怎么理解ES6中的Generator?使用场景有哪些?

理解:Generator是可中断的生成器函数function*。调用它不立即执行,而是返回一个迭代器。调用next()执行到下一个yield并暂停,调用throw()向内部抛出错误,调用return()终止函数。

使用场景:

异步流程控制(红绿灯):替代回调地狱(在async/await普及前,如co库。)

实现迭代器:手写[Symbol.iterator] 很繁琐,function* 自动生成。

状态机:不使用外部变量管理状态,通过yield在不同状态之间流转。

javascript 复制代码
function* stateMachine() {
  while (true) {
    yield 'stateA';
    yield 'stateB';
    yield 'stateC';
  }
}
// 每次 next().value 自动流转,无需外部变量

按需数据生成:生成无限序列(如斐波那契数列),每次next()才计算下一项,节省内存。

javascript 复制代码
function* fibonacci() {
  let [prev, curr] = [0, 1];
  while (true) { yield curr; [prev, curr] = [curr, prev + curr]; }
}
const gen = fibonacci();
gen.next().value; // 1
// 不会像数组一样一次性生成所有项

4.1说说你对Iterator、Generator和Async/Await的理解。

这三者构成了JS异步编程和迭代协议的进化链。

Generator :生成器函数 function*。它是Iterator 的生产者 。执行它返回一个迭代器,但它的重点在于 yield 可以暂停函数执行并向外返回值,且可以接受外部传入的值(next(value))。这让函数具备了"双向通信"和"分段执行"的能力。

Iterator :迭代器协议。只要对象实现了 [Symbol.iterator] 方法,且该方法返回一个包含 next() 方法的对象(该方法返回 { value, done }),就可以用 for...of 遍历。本质是"数据消费的标准化接口"。

Async/Await :它是Generator + Promise 的语法糖。async 函数相当于 function*await 相当于 yield。但它自动封装了 Promise,并将 next().value 的链式调用自动化,隐藏了迭代器的控制逻辑,专注于异步流程的同步化书写。

关系:Iterator 定义规则 -> Generator 实现规则并支持暂停 -> Async/Await 利用该机制解决异步回调地狱。

5.导致页面加载白屏时间长的原因及优化。

白屏:指从输入URL到开始渲染第一个像素的耗时时长。

原因:

DNS解析慢。

服务器响应慢TTFB:后端接口耗时,服务端渲染逻辑重。

资源阻塞渲染:首屏的同步CSS,CSS构建CSSOM树会阻塞同步渲染;同步JS未加defer,JS执行阻塞DOM构建和渲染。

HTML体积过大或DOM嵌套过深。

关键渲染路径CRP过长:CRP 指从 HTML/ CSS 到像素的过程:构建 DOM → 构建 CSSOM → 合并为渲染树 → 布局 → 绘制。白屏时间长意味着 CSSOM 或 DOM 构建过慢

优化策略:

网络层:使用 preconnect:提前与第三方源建立连接(如 CDN、API 域名)。使用CDN、HTTP/2、预解析DNS(<link rel="dns-prefetch">)。

资源层:内联关键CSS(Critical CSS),非关键CSS异步加载;JS加defer;使用资源提示<link rel="preload">。避免阻塞CSS:将非首屏 CSS 标记为 media="print" 后再改为 all

服务端:SSR服务端渲染使用流式渲染(Streaming),边生成边输出,而不是生成完整HTML再响应。Node.js 中 res.write('<html>...</html>') 逐步发送,让浏览器尽早接收并渲染部分 DOM。

骨架屏:先展示展位,减少用户感知的白屏。

6.微前端中的应用隔离是什么?一般是如何实现?

应用隔离指的是多个独立的前端子应用在同一个主应用(基座)中运行时互不干扰。

实现:

样式隔离:

CSS Module/BEM:约定式命名。局限:只能解决命名冲突,无法隔离全局样式 (如 body { background } 或第三方 UI 库的全局重置)。需要使用 CSS-in-JSShadow DOM 补充。。

Shadow DOM:将子应用包裹在iframe框架中(无界)或者Web Components的ShadowRoot中(乾坤)。CSS样式完全隔离,但全局DOM交互受限。

JS隔离(沙箱):

快照沙箱:激活子应用前,保存window上的原有全局属性,卸载时恢复(适用于单实例,性能一般)。

Proxy代理沙箱(Legacy/Modern):通过new Proxy(window)给每一个子应用设置一个fakewindow对象,拦截全局变量的get/set属性,对window的修改被记录在代理层,互不影响。乾坤采用。

Legacy 沙箱 (qiankun 早期):通过 deleterestore 快照,性能差,不支持多实例同时激活。

Modern 沙箱 :每个子应用独立 proxyWindow,对 windowset 存入 fakeWindowget 优先返回 fakeWindow 上的值,否则返回原生 window 的只读版本。

<iframe>:最原始最彻底的隔离,但通信成本高,DOM结构臃肿。

路由隔离:

基座路由匹配 :监听 popstatehashchange,根据路径前缀(如 /app1/*)动态挂载/卸载子应用。

子应用路由独立 :子应用内部可使用自己的 BrowserRouter,但需设置 basename 为基座分配的路径。

冲突风险 :若两个子应用使用相同的 window.location 变更方式(如 pushState),需要路由劫持或统一交由基座管理。

7.JS对象的底层数据结构是什么?

本质是哈希表。但在现代引擎如V8中,为了性能优化和内存优化,采用了更复杂的混合结构:

字典模式:纯粹的哈希表。属性名作为建,通过哈希函数计算存储位置。是和属性频繁增删的对象。

隐藏类:V8的核心优化。对于相同属性名和顺序的对象,复用同一个隐藏类。隐藏类记录了属性的偏移量,访问属性时直接按偏移量查找,速度接近数组。这解释了为什么不要随意打乱对象属性顺序。

偏移量是属性值在对象内存中的相对地址。例如,隐藏类记录了 name 属性在对象起始地址 +8 字节处,age 在 +16 字节处。访问时直接通过偏移量寻址,省去哈希查找的哈希计算和桶遍历。

内联属性:存储空间直接分配在对象本身(更快),而超出部分存储在单独的属性数组中。内联关键CSS属性能优化性能(更快)的原因:内联属性直接存储在对象头部固定大小的空间(V8 通常为 4 个属性),访问是常数时间。额外属性存储在独立的 properties 数组中(类似哈希表)。

8.浏览器和Node中的事件循环有什么区别?

核心区别在于宏任务(MacroTask)与微任务(MicroTask)的宿主实现不同

浏览器

宏任务:setTimeoutsetInterval、I/O、UI 渲染。

微任务:Promise.thenMutationObserver

机制:执行宏任务,每个宏任务执行完 -> 清空全部微任务 -> 必要时尝试 UI 渲染 -> 取下一个宏任务。

Node.js:

阶段更复杂:timer(setTimeout)、pending callbacks、idle/prepare、poll(I/O 回调)、check(setImmediate)、close。

关键区别:

特性 浏览器 Node.js (≤10) Node.js (≥11)
宏任务优先级 setTimeout 等队列顺序 多阶段固定顺序:timers → poll → check 趋向浏览器
setImmediate 不存在 check 阶段执行,在 poll 之后 同左
process.nextTick 不存在 任意阶段间立即执行,优先于微任务 同左
微任务清空时机 每个宏任务后 每个阶段切换前 每个宏任务后(对齐浏览器)

setImmediate 与 setTimeout(..., 0) 对比:

在 poll 阶段,若 timer 未到期,先执行 setImmediate

在 I/O 回调内部,setImmediate 总是先于 setTimeout(..., 0)

在主模块中,执行顺序取决于性能(计时器延迟 1ms 可能已过)。process.nextTick 不属于上述任何阶段,它会在当前阶段结束之前、下一个阶段开始之前执行(优先级甚至高于微任务)。

Node 11+ 变化 :为了对齐浏览器,Promise 微任务的行为发生了变化:在执行一个宏任务后,会立即清空微任务队列(以前是阶段切换时清空)。

9.版本号排序:

语义化版本规范 :major.minor.patch[-prerelease]。排序逻辑:先比较major,大者版本新;相等则比较minor,再比较patch;若有预发布标识(如-alpha.1、-beta.2):正式版本(无标识)大于任何预发布版本;预发布版本之间按标识符字典顺序比较。

在生产环境中,建议使用现成的库:semver。

10.哪些原因会导致JS里this指向混乱?

说明this指向分为哪几种情况以及取决于什么:普通函数的this指向取决于调用时的上下文,箭头函数的this由定义时的位置决定(且不能通过call/bind改变 )。常见this指向被调用者,apply\bind\call可以改变this的指向。

常见混乱场景:

默认绑定丢失;

回调函数:setTimeout(obj.greet, 1000) -> this 指向 window。因为 setTimeout 将函数引用剥离了对象。

事件处理:button.onclick = obj.method。普通函数执行时 this 指向 button,而非 obj

内部函数:this在对象方法的内部函数中。

箭头函数特殊性。

很多混乱源于混合使用普通函数和箭头函数,搞不清捕获时机。牢记 this 的四种绑定规则:默认绑定、隐式绑定(obj.fn())、显式绑定(call/apply/bind)、new 绑定。箭头函数优先级最高(无视上述规则)。

11.举例说明你对尾递归的理解及应用场景。

理解:尾递归是递归的一种特殊形式,指函数的最后一个动作是调用自己本身,且该调用的结果直接被返回(不再参与其他运算)。

尾调用优化要求:严格模式(use strict),函数调用是最后一步动作,没有闭包引用外层变量。

应用场景:累加器模式:数组求和、阶乘、斐波那契;树形结构遍历:深度优先遍历深层嵌套的AST或DOM树,防止递归深度过大爆栈;状态机:用于处理连续状态转换(游戏循环),在V8引擎(Node/Chrome)中已实现尾递归优化。

深度优先遍历爆栈的原因:嵌套深度10万层的DOM或AST(如超大JSON解析),普通递归会创建10万个栈帧;尾递归只复用一个栈帧。

12.怎么使用JS实现拖曳功能?

核心依赖:mousedown、mousemove、mouseup事件。

重点:坐标计算;边界控制;防止选中;事件绑定(防止鼠标捕获丢失:在document上绑定mousemove,而不是element);释放。

13.如果使用Math.random()计算中奖概率会有什么问题?

根本问题:伪随机性与不均匀分布。

非真随机:Math.random()通常使用梅森旋转算法等伪随机数生成器,对于绝大多数抽奖业务足够,但在高安全性场景(如开奖公证,金融系统)下,其周期有限且可预测。

精度与浮点数:返回 [0, 1) 的双精度浮点数。严格离散化时(如 Math.floor(Math.random() * N)),由于浮点数精度限制和 2^52 的尾数范围,当 N 很大(如大于 2^32)或不是 2 的幂时,会导致微小的概率偏差(某些值被选中的概率略高或略低)。

不可测试/不可回放:随机性是隐式的,导致单元测试无法mock固定结果。无法复现用户中奖的路径进行bug调试。

安全风险:若用于涉及金钱或敏感权限的抽奖,攻击者可预测随机数种子。

改进方案:使用crypto.getRandomValues()配合取模修正,剔除超出范围的值(Web)或crypto.randomBytes(Node.js)获取接近真随机的值。

14.相比于npm\yarn,pnpm的优势是什么?

优势:节省磁盘空间与提升安装速度(硬链接和符号链接)。

npm/yarn的问题(平铺结构):使用非扁平化的node_modules结构或者幽灵依赖。虽然yarn使用平铺解决了深层依赖问题,但如果你安装了100个项目,同一个包(如lodash)会在磁盘上有100份副本。

pnpm的解决方案:

内容寻址存储:所有包文件都存放在全局统一的.pnpm-store中。项目安装时,通过硬链接指向全局存储。一个包在磁盘上只存在一份。

非平铺结构:创建了嵌套的node_modules,严格遵守Node.js模块解析规则,但通过符号链接巧妙地组织依赖关系,从而解决了"幽灵依赖"问题。极大的节省了CI\CD流水线的磁盘空间,安装速度比传统缓存机制更快(因为硬链接几乎不复制文件)。

补充:幽灵依赖的危害:项目中写有import "lodash",但是package.json中未声明,实际因为某个依赖安装了lodash被平埔到顶层而能运行。当那个依赖升级不再依赖lodash时,项目会突然报错。

注意:pnpm通过强制链接只能访问显式声明的依赖。硬链接是指多个文件指向同一磁盘inode,删除一个不影响原数据(节省空间);符号链接类似快捷方式,指向目标路径。

相关推荐
道里2 小时前
花了 5 万刀用 AI 写代码之后,这是我的全部经验
前端·人工智能
Royzst2 小时前
xml知识点
java·服务器·前端
IT_陈寒3 小时前
React useEffect闭包陷阱差点把我整失业了
前端·人工智能·后端
努力努力再努力wz3 小时前
【Qt入门系列】:按钮组件全解析:从 QAbstractButton 到快捷键事件、单选与复选机制
c语言·开发语言·数据结构·c++·git·qt·github
kyriewen3 小时前
推行AI写代码一年后,Code Review变成了新的加班理由
前端·ai编程·cursor
前端环境观察室4 小时前
给 Agent Browser Workflow 加一层可观测性:Trace、Snapshot 和 Review Queue
前端
skywalk81634 小时前
言知(Yanzhi)系统提升建议报告和完工报告 by AutoCoder
开发语言·编程
yunn_4 小时前
单例模式两种实现方法
开发语言·c++·单例模式
我材不敲代码4 小时前
Python基础:列表详解、增删改查及常用高阶操作
开发语言·windows·python
柒瑞4 小时前
Superpowers结合Claude code浅实战
前端