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-JS 或 Shadow DOM 补充。。
Shadow DOM:将子应用包裹在iframe框架中(无界)或者Web Components的ShadowRoot中(乾坤)。CSS样式完全隔离,但全局DOM交互受限。
JS隔离(沙箱):
快照沙箱:激活子应用前,保存window上的原有全局属性,卸载时恢复(适用于单实例,性能一般)。
Proxy代理沙箱(Legacy/Modern):通过new Proxy(window)给每一个子应用设置一个fakewindow对象,拦截全局变量的get/set属性,对window的修改被记录在代理层,互不影响。乾坤采用。
Legacy 沙箱 (qiankun 早期):通过
delete和restore快照,性能差,不支持多实例同时激活。Modern 沙箱 :每个子应用独立
proxyWindow,对window的set存入fakeWindow,get优先返回fakeWindow上的值,否则返回原生window的只读版本。
<iframe>:最原始最彻底的隔离,但通信成本高,DOM结构臃肿。
路由隔离:
基座路由匹配 :监听
popstate或hashchange,根据路径前缀(如/app1/*)动态挂载/卸载子应用。子应用路由独立 :子应用内部可使用自己的
BrowserRouter,但需设置basename为基座分配的路径。冲突风险 :若两个子应用使用相同的
window.location变更方式(如pushState),需要路由劫持或统一交由基座管理。
7.JS对象的底层数据结构是什么?
本质是哈希表。但在现代引擎如V8中,为了性能优化和内存优化,采用了更复杂的混合结构:
字典模式:纯粹的哈希表。属性名作为建,通过哈希函数计算存储位置。是和属性频繁增删的对象。
隐藏类:V8的核心优化。对于相同属性名和顺序的对象,复用同一个隐藏类。隐藏类记录了属性的偏移量,访问属性时直接按偏移量查找,速度接近数组。这解释了为什么不要随意打乱对象属性顺序。
偏移量是属性值在对象内存中的相对地址。例如,隐藏类记录了
name属性在对象起始地址 +8 字节处,age在 +16 字节处。访问时直接通过偏移量寻址,省去哈希查找的哈希计算和桶遍历。
内联属性:存储空间直接分配在对象本身(更快),而超出部分存储在单独的属性数组中。内联关键CSS属性能优化性能(更快)的原因:内联属性直接存储在对象头部固定大小的空间(V8 通常为 4 个属性),访问是常数时间。额外属性存储在独立的 properties 数组中(类似哈希表)。
8.浏览器和Node中的事件循环有什么区别?
核心区别在于宏任务(MacroTask)与微任务(MicroTask)的宿主实现不同。
浏览器:
宏任务:setTimeout、setInterval、I/O、UI 渲染。
微任务:Promise.then、MutationObserver。
机制:执行宏任务,每个宏任务执行完 -> 清空全部微任务 -> 必要时尝试 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,删除一个不影响原数据(节省空间);符号链接类似快捷方式,指向目标路径。