外观
JavaScript 与 ES6
1. requestAnimationFrame 与 requestIdleCallback 在渲染优化中的执行时机差异?谁优先触发?
答案:
requestAnimationFrame(rAF)用于下一次绘制前必须完成的视觉更新。浏览器进入一次渲染机会时,会先执行 rAF 回调,随后进行样式计算、布局和绘制,因此动画应在 rAF 中读取/更新必要状态,并尽量避免耗时任务。
requestIdleCallback(rIC)用于非关键后台工作。浏览器完成高优先级任务和当前帧渲染后,只有预计仍有空闲时间时才会调用它;回调可通过 deadline.timeRemaining() 判断剩余预算。若主线程持续繁忙,它可能长时间不执行,除非设置 timeout,而 timeout 到期也不代表回调可无限耗时。
所以在同一个需要绘制的帧里,通常是 rAF 先于 rIC,原因是渲染工作优先;但两者不是简单、固定的一对一排序,是否形成空闲期由浏览器调度决定。rAF 适合动画和布局同步,rIC 适合低优先级预计算、分批处理,不能用来执行必须按时完成的业务。
2. 说说 Class 语法糖的底层实现
答案:
Class 仍建立在 JavaScript 的原型模型上。实例方法定义在 Class.prototype 上,静态方法定义在类构造器本身;extends 会同时连接实例原型链和构造器的静态继承链,super 负责调用父构造器或按父原型进行方法查找。
js
class User {
constructor(name) { this.name = name; }
say() { return this.name; }
static create(name) { return new User(name); }
}
Object.getPrototypeOf(new User('A')) === User.prototype; // true
Object.getPrototypeOf(User.prototype) === Object.prototype; // true但“语法糖”不能理解为与普通构造函数完全等价。类具有专门的内部语义:类体默认严格模式、类声明存在暂时性死区、只能用 new 调用、原型方法默认不可枚举;派生类必须在使用 this 前调用 super。私有字段、静态块和 super 也不是简单改写几行 prototype 赋值就能完整模拟的。
3. 如何使用 WeakMap 解决循环引用?
答案:
WeakMap 常用于递归克隆、序列化或遍历时记录“源对象到处理结果”的映射。再次遇到同一个对象时直接复用结果,就不会沿循环引用无限递归。
js
function deepClone(value, seen = new WeakMap()) {
if (value === null || typeof value !== 'object') return value;
if (seen.has(value)) return seen.get(value);
const copy = Array.isArray(value)
? []
: Object.create(Object.getPrototypeOf(value));
seen.set(value, copy); // 必须在递归子属性前登记
for (const key of Reflect.ownKeys(value)) {
copy[key] = deepClone(value[key], seen);
}
return copy;
}WeakMap 的键是弱引用,遍历结束后不会仅因记录表而阻止源对象被回收。它解决的是算法中的重复访问和无限递归,并不会自动拆除原对象图中的循环;只要循环中的对象仍可从根对象访问,它们仍然存活,整组都不可达时垃圾回收器可以一起回收。
4. 如何拦截arr[0] = 1这类操作
答案:
可以用 Proxy 的 set 捕获器拦截数组索引赋值。数组索引到达捕获器时是字符串属性名,因此要判断它是否为合法数组索引,并用 Reflect.set 保留数组更新 length、访问器等原生语义。
js
const source = [];
const arr = new Proxy(source, {
set(target, key, value, receiver) {
const index = Number(key);
const isIndex =
String(index) === key &&
Number.isInteger(index) &&
index >= 0 &&
index < 2 ** 32 - 1;
if (isIndex) {
console.log('写入索引', index, value);
if (typeof value !== 'number') {
throw new TypeError('数组元素必须是数字');
}
}
return Reflect.set(target, key, value, receiver);
}
});
arr[0] = 1;push、splice 等方法最终也会触发相应的索引和 length 写入。Proxy 只能拦截经过代理对象的操作;若外部仍持有 source 并直接修改它,则不会触发捕获器。
5. async function 返回的 Promise 状态何时变为 resolved?
答案:
调用 async function 会立即得到一个 Promise,函数体同步执行到第一个 await(或结束、抛错)为止。
- return 普通值:返回的 Promise 以该值 fulfilled。
- throw:返回的 Promise rejected。
- return Promise 或 thenable:外层 Promise 会“吸收”其状态,只有被返回对象最终 fulfilled/rejected 后才最终落定。
- 执行到 await:函数暂停;等待值落定后,后续代码通过微任务继续执行。
即使 async 函数直接 return 1,注册的 then 回调也不会在当前同步调用栈中执行,而是在微任务阶段执行。严格区分术语时,resolved 还可能表示“已绑定到另一个仍 pending 的 Promise”,这时它尚未 fulfilled。
6. 如何实现大文件分片上传与断点续传
答案:
完整方案需要前后端共同维护上传会话:
- 前端按固定大小用 Blob.slice 切片,计算文件标识(通常由文件元数据加内容摘要得到)和每片摘要。
- 上传前向服务端查询已存在的分片,只发送缺失部分;用并发池限制同时请求数,并对失败分片做有上限的重试。
- 服务端按 uploadId、chunkIndex 幂等保存分片,校验索引、大小和摘要,返回已接收列表。
- 全部分片完成后请求合并。服务端按顺序流式合并并校验最终文件摘要,合并成功后原子地标记完成并清理临时片。
- 浏览器在 IndexedDB 等位置保存 uploadId、分片大小和文件元数据。刷新后重新选择文件并校验身份,再从服务端状态续传。
“断点续传”的可信状态应以服务端为准,本地记录只是加速。还要处理同名文件、重复合并、过期会话、取消、限速、鉴权和配额。摘要计算也应分块或放入 Worker,避免一次性读入大文件并阻塞主线程。
7. 如何设计模块以确保它只初始化一次?
答案:
同一 JavaScript realm/模块加载器中,ESM 模块按解析后的模块标识只实例化并求值一次,因此模块顶层状态天然可作为单例:
js
let client;
export function getClient() {
return client ??= createClient();
}异步初始化要缓存 Promise,而不只是缓存最终结果,否则并发调用会重复启动初始化:
js
let initPromise;
export function init() {
return initPromise ??= connect()
.catch(error => {
initPromise = undefined; // 是否允许重试由业务决定
throw error;
});
}初始化函数仍应设计成幂等的。边界是:不同 iframe/Worker、不同 Node 加载上下文、解析为不同 URL 的模块(例如查询参数不同)可能拥有独立实例;开发环境的热更新也可能重新求值模块。需要跨进程或跨页面“仅一次”时,必须依靠服务端锁、数据库唯一约束或其他共享协调机制。
8. 虚拟滚动如何动态计算可视区域?无限滚动如何结合Intersection Observer优化性能?
答案:
虚拟滚动只渲染可视区附近的数据,并用顶部、底部占位空间保持总滚动高度。
等高项可直接计算:start = floor(scrollTop / itemHeight),end = ceil((scrollTop + viewportHeight) / itemHeight),再向两侧加入 overscan。变高项需要用 ResizeObserver 测量已渲染元素,把高度写入缓存;维护高度前缀和或 Fenwick Tree,通过二分查找由 scrollTop 定位首项。未测量项先用估计高度,实测后修正总高度,并对视口上方的高度差补偿 scrollTop,避免画面跳动。滚动处理应在 rAF 中合并更新。
无限滚动可在列表尾部放一个哨兵,用 IntersectionObserver 观察它。哨兵进入 rootMargin 提前区时请求下一页,并设置 loading/hasMore 防止重复请求;失败可重试,离开页面要 abort 请求和 disconnect observer。IntersectionObserver 负责低成本触发加载,虚拟列表负责控制 DOM 数量,两者职责不同但可以组合。
9. JSON.stringify 遇到 bigint 的类型的数据,会怎么处理?
答案:
默认情况下,只要序列化过程中遇到 BigInt,JSON.stringify 就会抛出 TypeError,因为 JSON 数据模型没有 BigInt 类型。
可以按协议显式转换,例如转成十进制字符串:
js
const json = JSON.stringify(
{ id: 9007199254740993n },
(_key, value) => typeof value === 'bigint' ? value.toString() : value
);
// {"id":"9007199254740993"}接收端必须知道该字段是大整数,再用 BigInt(value) 恢复。也可以为 BigInt.prototype 定义 toJSON,但修改全局原型会影响整个运行环境,通常不推荐。若强转为 Number,超过 Number.MAX_SAFE_INTEGER 的整数会丢失精度。
10. 如何在前端项目中实现动态路由?
答案:
动态路由通常是“登录后按权限获取路由描述,再把可信描述转换成前端路由记录”。
后端只返回约定好的 routeKey、path、权限和菜单元数据;前端维护 routeKey 到组件的白名单映射(Vite 可用 import.meta.glob 预生成),校验数据后调用路由器的 addRoute。导航守卫先恢复用户和权限、等待路由注册完成,再重放首次导航;退出登录或切换租户时调用 addRoute 返回的移除函数,避免旧权限路由残留。
不能把服务端给出的任意文件路径直接交给 import,这既可能无法被构建器静态分析,也会扩大攻击面。还应处理父子路由顺序、路由名冲突、404 路由最后注册、刷新时重新恢复、组件加载失败和后端权限校验。前端动态路由只改善界面和导航,真正的授权必须由服务端接口执行。
11. 如何通过bind或箭头函数固定this指向?
答案:
bind 适合把普通函数的 this 显式绑定到某个对象,它返回一个新函数:
js
const user = { name: 'Ada' };
function greet(prefix) {
return prefix + this.name;
}
const bound = greet.bind(user, 'Hi ');
bound(); // Hi Ada普通调用无法再用 call/apply 改掉 bound 的绑定;但把可构造函数用 new 调用时,new 创建的实例会取代 bind 提供的 thisArg。
箭头函数没有自己的 this,而是在定义时捕获外层 this:
js
class Timer {
start() {
setTimeout(() => console.log(this), 0); // this 是 Timer 实例
}
}箭头函数的 this 不能被 bind、call 或 apply 修改,也不能作为构造函数。对象字面量的方法若需要调用者对象作为 this,不应直接写成箭头函数,因为它捕获的是对象外层作用域的 this。
12. 以下代码输出什么?解释原因
js
const obj = {
objName: "百度",
print: () => console.log(this.objName),
};
obj.print();答案:
箭头函数不会因 obj.print() 这种成员调用而把 this 绑定到 obj;它捕获定义位置外层的 this。
在浏览器的传统非模块脚本中,顶层 this 是 window。若 window 上没有 objName,输出为:
text
undefined在 CommonJS 顶层,this 通常是 module.exports,同样通常输出 undefined。但在 ES Module 中,顶层 this 是 undefined,读取 this.objName 会直接抛出 TypeError,而不是输出 undefined。因此精确结果取决于代码运行方式。
若希望稳定输出“百度”,应使用普通方法:
js
const obj = {
objName: '百度',
print() {
console.log(this.objName);
}
};
obj.print(); // 百度13. 如何用Promise.all实现并发请求?如何处理部分请求失败?
答案:
先创建请求 Promise,再传给 Promise.all;请求会在 fetch 调用时启动,Promise.all 负责聚合,不负责真正调度网络并发。它按输入顺序返回结果,但任意一个请求拒绝就立即拒绝,其他已启动请求仍会继续。
js
const requests = urls.map(url =>
fetch(url).then(response => {
if (!response.ok) throw new Error(String(response.status));
return response.json();
})
);
const data = await Promise.all(requests);需要保留部分成功结果时,优先用 Promise.allSettled:
js
const results = await Promise.allSettled(requests);
const successes = results
.filter(item => item.status === 'fulfilled')
.map(item => item.value);
const failures = results.filter(item => item.status === 'rejected');也可以给每项单独 catch 并返回统一的成功/失败结构。大量 URL 不应一次全部启动,应加并发池;需要“失败即停止”时还要配合 AbortController,因为 Promise.all 的快速失败不会自动取消底层请求。
14. 下面代码的输出是什么?
js
const numbers = [1, 2, 3, 4, 5, 6];
numbers.forEach((num, index) => {
if (num === 3) {
console.log(`遇到数字 3,使用 return 跳过`);
return;
}
if (num === 5) {
console.log(`遇到数字 5,使用 return 跳过`);
return;
}
console.log(`处理数字: ${num}, 索引: ${index}`);
});答案:
输出依次是:
text
处理数字: 1, 索引: 0
处理数字: 2, 索引: 1
遇到数字 3,使用 return 跳过
处理数字: 4, 索引: 3
遇到数字 5,使用 return 跳过
处理数字: 6, 索引: 5forEach 会为数组中的每个现有元素调用一次回调。回调里的 return 只结束当前这一次回调,相当于本轮“跳过后续代码”,不会结束整个 forEach,所以数字 4、5、6 仍会继续处理。forEach 本身没有 break 能力;需要提前终止时应使用 for/for...of,或按语义使用 some、every、find。
15. 函数的原型和对象的原型分别是什么?它们之间有何关联?结合代码进行说明。
答案:
通常所说的“函数的原型”是构造函数的 prototype 属性;“对象的原型”是对象内部的 [[Prototype]] 链接,可用 Object.getPrototypeOf 读取。二者通过 new 建立关联:
js
function Person(name) {
this.name = name;
}
Person.prototype.say = function () {
return this.name;
};
const p = new Person('Ada');
Object.getPrototypeOf(p) === Person.prototype; // true
Person.prototype.constructor === Person; // true
Object.getPrototypeOf(Person.prototype) === Object.prototype; // true查找 p.say 时,先查 p 自身,再沿 p 的 [[Prototype]] 找到 Person.prototype。prototype 只是可构造函数上的普通属性,用作 new 创建实例时的原型候选;对象本身并不普遍拥有名为 prototype 的属性。
函数本身也是对象,所以 Person 还有自己的 [[Prototype]],通常是 Function.prototype。箭头函数不可构造,也没有默认的 prototype 属性。proto 是历史访问器,生产代码更适合使用 Object.getPrototypeOf/Object.setPrototypeOf。
16. JavaScript 中循环有哪些常见方法?它们在遍历不同数据结构时的适用情况如何?
答案:
常见方式及适用范围如下:
- for、while、do...while:通用、可精确控制索引,可用 break/continue,适合性能敏感或复杂控制流。
- for...of:遍历 iterable 的值,适合 Array、String、TypedArray、Set、Map、生成器等,可提前退出。
- for...in:遍历对象及其原型链上的可枚举字符串键,不适合直接遍历数组值;通常要配合 Object.hasOwn。
- Object.keys/values/entries:把普通对象的自有可枚举字符串属性转为数组后遍历;Symbol 键需用 Reflect.ownKeys 等 API。
- 数组的 forEach:执行副作用,不能正常 break;map/filter/reduce/some/every/find 分别适合映射、筛选、归并、短路判断和查找。
- Map/Set 可直接 for...of;Map 默认得到 [key, value],Set 默认得到 value。
选择时先看数据是否实现 iterable,再看是否需要索引、返回新集合或提前终止。不要用 map 只做副作用,也不要把 for...in 当作数组值循环。
17. 如何避免setInterval可能导致的定时器堆积问题?
答案:
setInterval 按时间尝试安排回调,不会等待上一次异步任务完成。即使回调很快返回,其中启动的请求仍可能未结束,于是请求、状态更新或任务会重叠;同步回调过长时,后续计时也会延迟。
更稳妥的方法是上一轮完成后再递归 setTimeout:
js
let stopped = false;
let timer;
async function poll() {
try {
await fetchAndUpdate();
} finally {
if (!stopped) timer = setTimeout(poll, 1000);
}
}
poll();
function stop() {
stopped = true;
clearTimeout(timer);
}还可以设置 running 锁跳过重入,并用 AbortController 在停止时取消未完成请求。组件卸载、页面隐藏或条件失效时必须 clearInterval/clearTimeout 并清理相关资源。递归 setTimeout 的间隔从本轮完成后计算,适合轮询;需要按墙钟时间对齐时则应根据当前时间计算下一次延迟,避免累计漂移。
18. 如何使用 WeakMap 封装私有属性?
答案:
把 WeakMap 放在模块闭包中,以实例为键保存私有状态,外部拿不到 WeakMap 就无法按常规属性访问这些数据:
js
const privateState = new WeakMap();
class Counter {
constructor(initial = 0) {
privateState.set(this, { value: initial });
}
increment() {
privateState.get(this).value += 1;
}
get value() {
return privateState.get(this).value;
}
}实例被回收后,对应状态不会仅因 WeakMap 而继续存活。方法若被脱离实例调用,privateState.get(this) 可能得到 undefined,必要时应校验接收者。
现代 JavaScript 更直接的方案是 #value 私有字段,它由语言强制私有并具有更好的语法和工具支持。WeakMap 更适合需要在多个类/函数间共享同一份隐藏状态、兼容旧语法或给外部对象附加不影响其生命周期的元数据。
19. 为什么 WeakMap 的键不能是基本类型?
答案:
弱键必须具有可独立追踪、可被垃圾回收的身份。字符串、数字、布尔值、null、undefined 和 BigInt 是按值使用的普通基本类型,同一个值可随时重新产生,没有适合作为弱引用目标的独立生命周期;若允许它们作为键,也无法实现“键不可达时自动消失”的语义。
需要补充现代 ECMAScript 的边界:WeakMap 键除了对象,也允许可被弱持有的 Symbol,即非全局注册的 Symbol;Symbol.for 创建的注册 Symbol 由全局注册表持有,不能作为弱键。
WeakMap 不可枚举,也不能可靠报告 size,因为垃圾回收发生时间不可观察。若键是字符串、数字等,应使用 Map;若键是对象或合适的 Symbol,且映射不应延长键的生命周期,才使用 WeakMap。
20. Map 的键是引用类型时,如何防止内存泄漏?
答案:
Map 会强引用键和值。只要 Map 本身可达,作为键的对象就不会被回收,因此长期存活的 Map 必须有明确的生命周期策略:
- 若映射只是“对象到附加信息”,不需要枚举和 size,改用 WeakMap。
- 业务资源结束时调用 delete,整体作废时调用 clear;组件卸载、连接关闭、缓存失效都应执行清理。
- 缓存应设置容量上限、TTL 或 LRU 淘汰,不能无限增长。
- 尽量把 Map 限制在较短作用域,不把临时映射挂到全局单例。
- 监听器、定时器或闭包若同时持有键对象,也要一起释放,否则换成 WeakMap 仍不能回收。
WeakRef 和 FinalizationRegistry 的回收时机不确定,只适合辅助型优化,不应作为正确性所依赖的清理机制。
21. 怎么理解 ESM 中的 export * from "a.js" 这种写法?
答案:
export * from './a.js' 是“星号再导出”:把 a.js 的命名导出转发为当前模块的命名导出,绑定仍是实时绑定;它不会把这些名字创建成当前模块正文可直接使用的局部变量。
有几个边界:
- default 不会被 export * 转发,需显式写 export { default } from './a.js'。
- 当前模块显式导出的同名项优先于星号再导出。
- 从多个模块星号再导出同名项会产生歧义,该名字不会成为可导入的导出;应显式写 export { name } from './a.js' 消除歧义。
- export * as ns from './a.js' 导出的是一个模块命名空间 ns,与普通的 export * 不同。
它常用于 barrel 模块统一出口,但大量星号再导出会隐藏依赖来源、增加命名冲突,也可能让循环依赖更难排查。
22. 如何理解 ES6 模块化方案的缓存机制?
答案:
ESM 加载器会把解析后的模块标识对应到一个模块记录。对同一记录的多次静态 import 或 dynamic import,只会实例化并求值一次,之后复用同一个模块命名空间和内部状态;导入项是实时只读绑定,因此能看到导出方后续对绑定的更新。
循环依赖时,加载器先构建和实例化整个模块图,再按规则求值。实时绑定能支持循环,但在某个导出初始化前访问它仍可能触发暂时性死区错误。
缓存边界取决于宿主:浏览器通常按规范化 URL 区分模块,查询参数不同可能得到不同实例;不同 iframe realm、Worker 或 Node 加载上下文也可能各自加载一份。开发服务器的热更新可以使模块重新求值。这个缓存没有标准的手动清除 API,因此不能把改查询参数当作通用生产缓存控制方案。
23. 使用 const 定义函数和直接用 function 声明有什么区别?
答案:
主要区别来自“函数声明”和“词法变量上的函数表达式”:
js
run(); // 可调用
function run() {}
work(); // ReferenceError:仍在暂时性死区
const work = function () {};function 声明在作用域实例化时就创建并初始化,通常可以在源码声明前调用;const 绑定是块级作用域,在执行到声明前处于暂时性死区,且之后不能重新赋值。const 只保证变量绑定不变,不会让函数对象内部状态不可变。
若 const 保存的是普通 function 表达式,它仍有动态 this、arguments,并且通常可被 new 调用;若保存箭头函数,则 this/arguments 取自外层,且不可构造。函数声明在块内的兼容行为受严格模式和历史网页语义影响,跨环境代码应避免依赖非严格模式下的特殊提升规则。
24. 说说你对高阶函数的理解
答案:
高阶函数是“接收函数作为参数”或“返回函数”的函数。函数在 JavaScript 中是一等值,所以可以像其他值一样传递和保存。
js
const withLogging = fn => (...args) => {
console.log('args:', args);
return fn(...args);
};
const add = withLogging((a, b) => a + b);
add(1, 2);map、filter、reduce 接收回调,是高阶函数;防抖、节流、缓存、权限校验等通常返回包装函数,也是高阶函数。它能把遍历策略或横切逻辑与具体业务分离,便于组合和复用。
边界是要正确转发 this、参数、返回值和异常;包装异步函数时还要保持 Promise 的拒绝语义。过度嵌套会增加调用层级和调试成本,闭包若长期引用大对象也可能延长其生命周期。
25. JS中本地对象、内置对象、宿主对象分别是什么,有什么区别?
答案:
这组术语主要来自较早版本的 ECMAScript 文档,现代规范更常直接讨论“ECMAScript 内置对象”和“宿主提供的对象”,不应把三者理解成严格并列的现代分类。
- 本地/原生对象(旧称 native object):语义由 ECMAScript 规范定义的对象,如 Object、Array、Function、Date、RegExp、Promise。
- 内置对象(built-in object):程序开始执行时由实现提供的规范对象,如全局的 Object、Math、JSON。按旧术语,它属于原生对象范围,而不是与原生对象完全分离的一类。
- 宿主对象(host object):由运行环境定义,用于接入环境能力。浏览器中的 window、document、fetch、HTMLElement,以及 Node.js 中的 process、Buffer 等都属于这一层。
核心区别是标准来源和可用环境:ECMAScript 内置对象具有跨合规 JS 引擎的语言规范;宿主 API 由 Web、Node.js 等平台规范和实现决定,不能假定在所有 JavaScript 环境中存在。
26. Js 中,有哪些方法可以退出循环
答案:
真正用于结束循环的是 break;在嵌套循环中可用带标签的 break 一次跳出指定外层循环:
js
outer:
for (const row of rows) {
for (const cell of row) {
if (cell === target) break outer;
}
}若循环位于函数中,return 会同时结束循环和整个函数;throw 会沿调用栈中断执行,直到被 catch,不应把异常当作普通循环控制。continue 只跳过本轮,不是退出循环。
forEach 没有 break 语义,回调中的 return 只结束当前回调。需要提前停止时改用 for/for...of,或按需求使用 some、every、find:它们可以根据回调结果短路。直接修改 length 等方式退出数组遍历副作用大且语义不清,不推荐。
27. get 请求的参数是否能够使用数组?
答案:
可以,但 URL 查询串本质上只有字符串,没有标准的“数组类型”。前后端必须约定编码方式,常见的是重复键:
js
const params = new URLSearchParams();
for (const tag of ['js', 'css']) params.append('tag', tag);
params.toString(); // tag=js&tag=css服务端再用“获取同名参数的全部值”解析。其他项目也会约定 tag[]=js&tag[]=css、tag=js,css 或把数组编码成 JSON;这些都不是自动通用的,逗号方案还要处理值本身含逗号的情况。
GET 参数会出现在 URL、历史记录和日志中,并受浏览器、代理及服务器的 URL 长度限制。短小的筛选数组适合查询串;很大或结构复杂的数据通常应改用 POST 请求体,但是否缓存、幂等仍应按接口语义设计。
28. JS 有哪些迭代器,该如何使用?
答案:
要区分 iterable 和 iterator:
- iterable 实现 Symbol.iterator 方法;该方法返回 iterator。
- iterator 实现 next(),每次返回 { value, done }。
- 同一个对象也可以同时实现二者。
Array、String、TypedArray、Map、Set、生成器,以及现代浏览器中的许多集合都可迭代。它们常提供 keys()、values()、entries();Map 默认迭代 [key, value],Set 默认迭代 value。
js
const iterator = [10, 20].entries();
iterator.next(); // { value: [0, 10], done: false }
for (const [index, value] of [10, 20].entries()) {
console.log(index, value);
}展开语法、数组解构、Array.from 和 for...of 都消费同步迭代协议。异步数据源实现 Symbol.asyncIterator,其 next 返回 Promise,用 for await...of 消费。普通对象默认不可迭代,需要先用 Object.entries,或自行实现 Symbol.iterator。
29. 如何使对象 iterable 化, 使其可以支持 for...of 迭代
答案:
给对象实现 Symbol.iterator,并让它返回符合迭代器协议的对象即可。用生成器写法最简洁:
js
const record = {
name: 'Ada',
age: 30,
*[Symbol.iterator]() {
for (const key of Object.keys(this)) {
yield [key, this[key]];
}
}
};
for (const [key, value] of record) {
console.log(key, value);
}Symbol.iterator 方法每次调用都应创建独立迭代器;生成器天然满足这一点。上例只遍历自有、可枚举的字符串键,不包含 Symbol 键,顺序遵循 Object.keys 的属性顺序规则。
还可以让迭代器只产出 value,具体协议由业务决定。遍历期间修改对象时,结果取决于实现是先快照键还是动态读取;上例调用 Object.keys 时已经取得键快照。若只偶尔遍历普通对象,直接 for...of Object.entries(obj) 通常更清晰。
30. ESM 与 CJS 的差异有哪些?
答案:
ESM 和 CJS 的核心差异是:
- ESM 使用 import/export,依赖结构大多可静态分析;CJS 使用 require/module.exports,在运行时执行。
- ESM 导入是实时只读绑定;CJS require 得到缓存的 exports 值/对象,后续读取对象属性可见修改,但解构出的普通变量不是实时绑定。
- ESM 模块体默认严格模式,顶层 this 是 undefined;CJS 在 Node 中由包装函数执行。
- ESM 支持异步模块图和顶层 await;CJS 的 require 是同步加载接口。
- 两者都会缓存已加载模块,但缓存键、循环依赖表现和加载器边界不同。
- ESM 更利于 tree shaking;CJS 的动态 require 使静态优化更困难。
Node 中启用方式、扩展名和 package.json 的 type 会影响解析。ESM 导入 CJS、CJS 动态 import ESM 存在互操作规则,但 default/named export 映射并不完全对称,库代码不应假定两套导出能无损互换。
31. 详细讲一下 Symbol 数据类型特征与实际使用案例?
答案:
Symbol 是基本类型,每次 Symbol(description) 都创建唯一值;description 只用于调试,不参与相等判断,不能用 new 调用。Symbol.for(key) 则通过全局 Symbol 注册表复用同一值,可用 Symbol.keyFor 查询注册键。
Symbol 可作为对象属性键,避免与普通字符串键冲突:
js
const id = Symbol('id');
const user = { [id]: 123, name: 'Ada' };
Object.keys(user); // ['name']
Reflect.ownKeys(user); // ['name', id]Symbol 键不会出现在 Object.keys、for...in 和常规 JSON.stringify 结果中,但这不等于安全私有;拿到对象后仍可通过 Object.getOwnPropertySymbols/Reflect.ownKeys 找到它。
典型用途包括库的非冲突元数据、用 Symbol 值表示唯一枚举,以及实现语言扩展协议,如 Symbol.iterator、Symbol.asyncIterator、Symbol.toPrimitive、Symbol.hasInstance。全局注册 Symbol 适合跨模块共享协议键;模块内部 Symbol 适合避免命名碰撞。
32. 说说你对 Promise 的了解?
答案:
Promise 表示一个异步结果,状态从 pending 单向变为 fulfilled 或 rejected,落定后不可再改变。构造器 executor 同步执行,但 then/catch/finally 注册的处理函数总在微任务中异步执行。
then 会返回新的 Promise:回调返回普通值时下一个 Promise fulfilled;返回 Promise/thenable 时会吸收其最终状态;抛出异常时下一个 Promise rejected。未提供相应处理器时,值或错误会沿链传递,因此可以集中 catch。finally 用于清理,通常不改变原结果,除非它自身抛错或返回拒绝的 Promise。
Promise.all、allSettled、race、any 用于不同的并发聚合语义。Promise 只封装结果,不会自动启动线程,也不能自动取消已经开始的 I/O;取消要由 AbortController 等底层机制协作。未及时处理的拒绝还可能触发宿主的 unhandledrejection/未处理拒绝报告。
33. postMessage 是如何解决跨域问题的?
答案:
postMessage 并没有关闭同源策略,而是浏览器提供的一条受控跨源消息通道:发送方只需持有目标 Window 引用(例如 iframe.contentWindow、window.opener),浏览器通过结构化克隆把消息交给目标窗口,双方无需直接读取彼此 DOM 或变量。
js
targetWindow.postMessage({ type: 'READY' }, 'https://example.com');
window.addEventListener('message', event => {
if (event.origin !== 'https://example.com') return;
if (event.source !== expectedWindow) return;
if (event.data?.type !== 'READY') return;
// 处理经过结构校验的数据
});安全关键是发送时使用精确 targetOrigin,接收时同时校验 origin、source 和消息结构;敏感场景还应设计随机 nonce 或会话关联。不要无条件使用 '*',尤其不能发送密钥。消息采用结构化克隆,也可转移 ArrayBuffer、MessagePort 等 transferable;它是通信机制,不会让跨域窗口获得任意访问权限。
34. js 对象可以使用 for...of 迭代吗?
答案:
普通对象默认没有 Symbol.iterator,因此不能直接 for...of,尝试迭代会抛出 TypeError。
遍历普通对象的自有可枚举字符串属性,通常写:
js
for (const [key, value] of Object.entries(obj)) {
console.log(key, value);
}若要遍历属性名也可使用 Object.keys;若还要包含不可枚举键和 Symbol 键,可使用 Reflect.ownKeys 后自行读取。for...in 虽能遍历对象,但会包含原型链上的可枚举字符串属性,通常需要 Object.hasOwn 过滤。
也可以给对象实现 Symbol.iterator,使其变成 iterable,但必须明确每轮产出的值及修改期间的行为。
35. 解释一下原型、构造函数、实例、原型链 之间的关系?
答案:
构造函数是可被 new 调用的函数;实例是 new 的结果;构造函数的 prototype 属性通常成为实例的 [[Prototype]];多个 [[Prototype]] 连起来就是原型链。
js
function Animal() {}
Animal.prototype.move = function () {};
const dog = new Animal();
Object.getPrototypeOf(dog) === Animal.prototype; // true
Animal.prototype.constructor === Animal; // true
Object.getPrototypeOf(Animal.prototype) === Object.prototype;
Object.getPrototypeOf(Object.prototype) === null;读取 dog.move 时先查实例自身,找不到就依次查 Animal.prototype、Object.prototype,直到 null。new 大致会创建这个原型关系,以新对象作为 this 调用构造函数;若构造函数显式返回对象,则该对象成为 new 的结果。
构造函数本身也是对象,它自己的原型通常是 Function.prototype,这与它用于实例的 prototype 属性是两条不同关系。instanceof 主要检查构造函数当前的 prototype 是否出现在对象原型链中,因此修改 prototype 后,旧实例的判断结果可能变化。
36. async/await 原理, 手写 async 函数?
答案:
从可观察语义看,await 会暂停当前 async 函数,等待表达式经 Promise.resolve 归一化后落定;fulfilled 值作为 await 结果继续执行,rejected 原因像 throw 一样在函数中抛出。async 函数始终返回 Promise。转译器常把它改写为生成器加 Promise 驱动器,但引擎不必按这种源码级方式实现。
核心驱动器可写为:
js
function run(generatorFn, ...args) {
const iterator = generatorFn(...args);
return new Promise((resolve, reject) => {
function step(method, value) {
let result;
try {
result = iterator[method](value);
} catch (error) {
reject(error);
return;
}
if (result.done) {
resolve(result.value);
return;
}
Promise.resolve(result.value).then(
value => step('next', value),
error => step('throw', error)
);
}
step('next');
});
}
function* task() {
const value = yield Promise.resolve(2);
return value * 3;
}
run(task).then(console.log); // 6每个 yield 只注册一次延续,时间复杂度为 O(k),k 是暂停点数量;额外状态空间 O(1),不计异步结果本身。深层同步递归不在此模型内。
37. 如何检测对象是否循环引用?
例如下面的场景, 已经出现循环引用了, 如何检测?
js
const foo = {
foo: "Foo",
bar: {
bar: "Bar",
},
};
foo.bar.baz = foo; // 循环引用!答案:
检测“当前递归路径”上是否再次遇到同一对象即可。不能只用一个永不删除的全局 seen 集合,否则两个属性共享同一子对象的无环 DAG 会被误判为循环。
js
function hasCycle(value, path = new WeakSet()) {
if (value === null || typeof value !== 'object') return false;
if (path.has(value)) return true;
path.add(value);
for (const key of Reflect.ownKeys(value)) {
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if ('value' in descriptor && hasCycle(descriptor.value, path)) {
return true;
}
}
path.delete(value);
return false;
}
console.log(hasCycle(foo)); // true这里读取属性描述符而不是直接访问属性,可避免无意执行 getter;因此只沿自有数据属性检查。算法时间复杂度为 O(V + E),递归栈和路径集合为 O(V)。极深对象可能导致调用栈溢出,可改用显式栈;Proxy、宿主对象及遍历期间发生的并发修改还需按业务限制处理。
38. promise.race、promise.all、promise.allSettled 有哪些区别?
答案:
三者都接收 iterable,并用 Promise.resolve 处理其中的普通值或 Promise:
- Promise.all:等待全部 fulfilled,结果按输入顺序排列;任意一个先 rejected 就立即拒绝。空输入得到 fulfilled 的空数组。
- Promise.allSettled:等待全部落定,返回同顺序的 { status, value } 或 { status, reason },自身不会因某一项失败而拒绝。空输入得到 fulfilled 的空数组。
- Promise.race:采用第一个落定项的状态和值,不论成功或失败;空输入返回一个永远 pending 的 Promise。
“立即拒绝/胜出”只影响聚合 Promise,其他已经开始的任务不会自动取消。需要停止 fetch 等底层操作时要配合 AbortController。若需求是“第一个成功”,应使用 Promise.any;它在全部失败时以 AggregateError 拒绝。
39. es5 和 es6 使用 new 关键字实例化对象的流程是一样的吗?
答案:
对 ES5 构造函数和 ES6 class 来说,new 的核心流程相同:确认目标可构造;创建新对象并把其 [[Prototype]] 指向构造器的 prototype(若不是对象则回退到默认原型);以新对象为 this 执行构造逻辑;构造器若显式返回对象则采用该对象,否则返回新实例。
但 class 增加了不能忽略的语义:
- class 构造器只能通过 new/Reflect.construct 调用,普通调用会抛错。
- 类体默认严格模式,原型方法默认不可枚举。
- 派生类在 super() 完成前没有可用的 this;super() 负责调用父类构造过程。
- new.target、私有字段初始化和字段定义都由类的构造语义处理。
所以 new 的抽象目标一致,但 ES6 class 不是简单把 ES5 构造函数原样换一种写法,尤其是继承和派生构造器的细节不同。
40. Promise then 第二个参数和 Promise.catch 的区别是什么?
答案:
catch(onRejected) 本质上等价于 then(undefined, onRejected),区别来自它在链中的位置。
js
promise.then(onFulfilled, onRejected);这里 onRejected 能处理 promise 本身的拒绝,但不能处理同一个 then 中 onFulfilled 抛出的错误,因为两者是同一级处理器,onFulfilled 的异常会拒绝 then 返回的新 Promise。
js
promise
.then(onFulfilled)
.catch(onRejected);这里 catch 挂在 then 返回的新 Promise 后面,因此既能处理原 promise 的拒绝,也能处理 onFulfilled 抛错或返回的拒绝 Promise,覆盖范围更大,通常更符合集中错误处理的需求。
无论哪种写法,处理器若正常返回会把链恢复为 fulfilled;若希望错误继续传播,应再次 throw 或返回 rejected Promise。
41. JS 内存泄露的问题该如何排查?
答案:
排查要先把“正常峰值”与“持续不可回收”区分开。建立可重复步骤:空闲后记录基线,重复进入/退出页面或执行操作,等待或手动触发 GC,再比较多轮后的存活对象数量和堆大小。若基线阶梯式上涨,才像泄漏。
浏览器 DevTools 中重点使用:
- Heap Snapshot 的 Comparison、Dominator Tree 和 Retainers,查谁在保持对象可达。
- Allocation instrumentation/timeline,定位持续分配的调用栈。
- Detached DOM tree、Event Listener、Performance 面板,检查脱离 DOM、监听器和长生命周期任务。
- 必要时用弱引用标记或实例计数辅助验证,不把非标准 memory 数值当唯一证据。
常见根因是未移除事件监听、定时器/观察器、闭包捕获大对象、无限增长的 Map/缓存、未结束的订阅、脱离 DOM、全局变量。修复后必须用同一复现流程重新拍快照,确认对象能从 GC Root 的保留链上断开且基线不再增长。Node.js 可用 --inspect、heap snapshot 和采样分析做同样的保留链检查。
42. 说说 axios 的拦截器原理及应用,并简单手写核心逻辑
答案:
Axios 把请求拦截器、真正发送请求的 dispatchRequest、响应拦截器组装成一条 Promise 链。请求拦截器通常后注册先执行(unshift),响应拦截器先注册先执行(push);每个拦截器都有 fulfilled/rejected 两个处理器,返回值或异常自然沿 Promise 链传递。
简化核心逻辑:
js
class InterceptorManager {
handlers = [];
use(fulfilled, rejected) {
this.handlers.push({ fulfilled, rejected });
return this.handlers.length - 1;
}
eject(id) {
if (this.handlers[id]) this.handlers[id] = null;
}
forEach(fn) {
this.handlers.forEach(item => item && fn(item));
}
}
class Client {
interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
request(config) {
const chain = [this.dispatchRequest.bind(this), undefined];
this.interceptors.request.forEach(item => {
chain.unshift(item.fulfilled, item.rejected);
});
this.interceptors.response.forEach(item => {
chain.push(item.fulfilled, item.rejected);
});
let promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
dispatchRequest(config) {
return fetch(config.url, config);
}
}常见应用是添加 token/traceId、统一序列化、错误转换、日志和刷新令牌。刷新令牌必须合并并发刷新并防止重试死循环;组件级拦截器还要 eject,避免重复注册和闭包泄漏。
43. WebWorker、SharedWorker 和 ServiceWorker 有哪些区别?
答案:
三者都在主线程之外运行且不能直接操作 DOM,但生命周期和作用域不同:
| 类型 | 共享范围 | 生命周期与主要用途 | 通信 |
|---|---|---|---|
| Web Worker(Dedicated Worker) | 仅创建它的页面/Worker | 随所有者使用,适合 CPU 计算、解析、编码 | postMessage |
| SharedWorker | 同源且匹配名称/脚本的多个页面 | 多标签页共享一个后台上下文,适合共享连接和状态协调 | MessagePort |
| Service Worker | 注册 scope 下的多个页面 | 事件驱动,可在没有页面时被唤醒或随时终止;拦截 fetch,支持离线缓存、推送、后台同步 | postMessage、clients API |
Service Worker 不是常驻线程,不能依赖全局变量永久保存状态,应使用 Cache Storage/IndexedDB;其安装、激活和控制页面有独立生命周期,通常要求 HTTPS。SharedWorker 不拦截网络,请求共享也不等于离线能力。大量计算通常用 Dedicated Worker,跨页面共享用 SharedWorker,网络代理和离线能力用 Service Worker。
44. Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?
答案:
Promise 构造函数接收的 executor 是同步执行的;resolve/reject 只改变 Promise 状态,不会立即同步调用 then 回调。then、catch、finally 的处理器都会作为微任务,在当前同步栈结束后执行,即使 Promise 在注册前已经落定。
js
console.log(1);
const p = new Promise(resolve => {
console.log(2);
resolve();
console.log(3);
});
p.then(() => console.log(4));
console.log(5);输出是 1、2、3、5、4。还要注意:then 方法本身是同步调用并立即返回一个新 Promise,异步的是传给 then 的处理函数。
45. promise 是否可以取消?
答案:
标准 Promise 一旦创建,没有通用的 cancel 方法,且落定后状态不可逆。更重要的是,Promise 只是结果容器;“取消 Promise”并不等于停止已经发出的网络请求、计时器或计算。
应让底层任务支持协作取消,例如 fetch 配合 AbortController:
js
const controller = new AbortController();
const request = fetch('/api/data', {
signal: controller.signal
});
controller.abort(); // fetch 以 AbortError 拒绝自定义任务也可接收 signal,监听 abort 后清理定时器、关闭连接并拒绝。Promise.race 与一个“取消 Promise”竞速只会让调用方提前得到结果,不会停止输掉竞速的任务,因此仍可能消耗资源或产生副作用。取消通常应被建模为一种明确错误/状态,并在 finally 中完成清理。
46. js 中对于超过 Number 最大值的数怎么处理?
答案:
要区分两个上限:
- Number.MAX_SAFE_INTEGER(2^53 - 1)是整数精确表示上限。超过它仍可能是有限 Number,但相邻整数不能保证区分。精确大整数应使用 BigInt,例如 9007199254740993n。
- Number.MAX_VALUE 是最大有限浮点数。运算超过它会得到 Infinity,之后已无法从该值恢复原精度。
BigInt 适合任意精度整数,但不能与 Number 直接混合算术,也不能表示小数,JSON.stringify 默认也不支持。金额或高精度小数应使用十进制定点方案(如最小货币单位的 BigInt)或可靠的 decimal 库。接收超大整数 JSON 时,必须让服务端以字符串传输;如果解析阶段已经变成不安全 Number,之后再转 BigInt 也无法找回丢失的位。
47. 全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?
答案:
在浏览器传统脚本的全局环境中,规范上的 Global Environment Record 由两部分组成:Object Record 对应全局对象属性,Declarative Record 保存 let、const、class 等全局词法绑定。后者属于全局词法环境,所以不是 window/globalThis 的属性。
js
let count = 1;
count; // 1:通过标识符解析词法绑定
globalThis.count; // undefined同一 realm 中后续的传统脚本可以按标识符 count 访问它,但没有标准的反射 API 能通过 globalThis['count'] 按字符串取得这个词法绑定。若需要动态按名访问,应显式放入对象、Map 或有意定义 globalThis 属性。
ES Module 又不同:顶层 let/const/function 都是模块作用域绑定,不会成为全局绑定,其他模块必须通过 export/import 获取。let/const 也不可重复声明,并在初始化前处于暂时性死区。
48. 为什么普通 for 循环的性能高于 forEach ?
答案:
普通 for 往往有更少的语义开销:索引、边界和控制流都在一个函数体内,不必为每个元素执行用户回调的调用语义,也可缓存 length、提前 break,并避免创建闭包。forEach 还要遵守跳过稀疏数组空位、传入 value/index/array、固定迭代范围等规范行为。
但“for 一定更快”并不成立。现代 JIT 可能内联简单回调,真实差异受引擎版本、数组元素类型、是否稀疏、回调复杂度和热身状态影响;当每轮业务工作较重时,循环形式的开销通常可以忽略。
因此性能敏感的热路径应在目标运行环境用真实数据基准测试,并确认优化没有破坏可读性。普通业务遍历优先选择语义清晰的方式;需要提前退出、精确控制或基准证明存在瓶颈时再选 for/for...of。
49. 数组里面有10万个数据,取第一个元素和第10万个元素的时间相差多少?
答案:
对正常的密集 JavaScript 数组,arr[0] 和 arr[99999] 都是按索引随机访问,平均时间复杂度都是 O(1),不会因为索引靠后就线性扫描前面的元素。单次访问的时间差通常小到没有业务意义,不能给出固定的纳秒差值。
引擎可能因 CPU 缓存、JIT、数组元素表示等产生微小测量波动。若数组变成超稀疏、频繁删除属性或包含特殊属性,底层可能退化为字典式存储,但索引访问仍不是按位置从头遍历,其常数开销由引擎实现决定。第 10 万个元素的零基索引是 99999。
50. [3, 15, 8, 29, 102, 22].sort(),结果是多少,为什么?
答案:
结果是:
js
[102, 15, 22, 29, 3, 8]Array.prototype.sort 在没有 compareFn 时,会把非 undefined 元素转成字符串,按 UTF-16 码元的字典序比较。因此 '102' 排在 '15' 前,'29' 排在 '3' 前,并不是按数值大小排序。
数值升序应写:
js
[3, 15, 8, 29, 102, 22].sort((a, b) => a - b);
// [3, 8, 15, 22, 29, 102]sort 会原地修改原数组;需要保留原数组可用 toSorted((a, b) => a - b) 或先复制再排序。
51. 箭头函数为何不能作为构造函数使用?
答案:
能被 new 调用的值必须具有内部 [[Construct]] 方法。箭头函数只有调用能力,没有 [[Construct]],也没有为实例准备的默认 prototype 属性,所以 new Arrow() 会抛出 TypeError。
这是其语义设计的一部分:箭头函数没有自己的 this、arguments、super 和 new.target,而是从定义处的词法环境捕获相关值。构造函数则需要由 new 创建实例,并在构造调用中绑定 this,这与箭头函数的词法 this 冲突。
给箭头函数手动添加一个名为 prototype 的普通属性,或对它调用 bind,都不会凭空获得 [[Construct]],仍然不能作为构造函数。
52. 对象取值中 a.b.c.d 和 a['b']['c']['d'] 有何区别?
答案:
在这些键都是固定且有效标识符时,a.b.c.d 与 a['b']['c']['d'] 访问的是相同属性,性能和原型链查找语义没有本质区别。
点语法的属性名写死在源码中,要求是合法的标识符名称;方括号接收运行时表达式,可用于动态键、含空格或连字符的字符串、数字和 Symbol:
js
obj[key]
obj['first-name']
obj[symbolKey]方括号中的非 Symbol 值会被转换为属性键,例如 obj[1] 等价于 obj['1']。两种写法都不会自动处理空值:如果中间的 a.b 为 null/undefined,继续访问都会抛错,应使用可选链 a?.b?.c?.d。a['b'] 不是比 a.b 更安全,只是更灵活。
53. JavaScript 对象的底层数据结构是什么?
答案:
在语言规范层面,对象是“属性集合 + [[Prototype]] + 若干内部槽”。每个属性有键(String 或 Symbol)和属性描述符;数据属性包含 value/writable,访问器属性包含 get/set,并有 enumerable/configurable。属性查找先查自身,再沿 [[Prototype]] 链进行。
具体内存结构不由 ECMAScript 规定。以常见引擎为例,形状稳定的对象通常使用 hidden class/shape 记录“属性名到槽位”的布局,值存放在对象内槽或属性数组中,便于 JIT 用固定偏移和 inline cache 快速访问。频繁增删属性、形状高度动态时,可能退化为字典/哈希式属性存储。整数索引属性通常又放在独立的 elements 存储中。
因此笼统说“JS 对象底层就是哈希表”不准确:引擎会根据对象形状和使用方式在多种表示之间转换,而且 V8、SpiderMonkey、JavaScriptCore 的细节不同。
54. JavaScript 中的变量在内存中的具体存储形式是什么
答案:
ECMAScript 只规定值、绑定和可观察行为,不规定变量必须放在“栈”还是“堆”。变量名对应执行上下文的环境记录中的绑定;对象赋值复制的是引用语义,基本值赋值复制值语义。
实现层面通常是:
- 调用帧和部分局部数据优先放在机器栈或寄存器中。
- 对象、函数、许多字符串和需要长期存活的环境位于垃圾回收堆。
- Number/Boolean 等基本值常用 tagged value、立即数或寄存器表示,但大字符串等可能仍由堆对象承载。
- 被闭包捕获且逃离当前调用的词法环境必须在调用结束后继续存活,通常会被提升到堆或等价结构。
- JIT 的逃逸分析、内联和标量替换还可能消除原本看似存在的分配。
所以“基本类型都在栈、引用类型都在堆”只是教学近似,不是可靠规则。判断内存问题应关注对象是否仍从 GC Root 可达,而不是仅凭变量类型推断位置。
55. 说说你对 eval 的理解
答案:
eval 会把字符串解析为 JavaScript 并执行,返回最后一个表达式的结果。直接调用 eval(code) 会在当前词法环境中执行,可能访问局部变量;间接调用(如 (0, eval)(code))在全局环境执行。严格模式还会限制 eval 对外层绑定的影响。
它主要有三类问题:
- 字符串来自不可信输入时会造成任意代码执行。
- 动态代码破坏静态分析、重构和 tree shaking,并使调试困难。
- 引擎难以优化可能被 eval 动态读写的作用域;严格 CSP 通常也会禁止 unsafe-eval。
解析 JSON 应使用 JSON.parse,按名字调用行为应使用对象/Map 映射,表达式需求应使用受限解析器。只有在代码沙箱、调试器等确实需要语言求值的底层场景才考虑 eval,而且 eval 本身不是安全沙箱;隔离不可信代码需要独立 realm/进程及能力限制。
56. 说说对 new Function 的理解
答案:
Function 构造器用字符串参数生成一个新的普通函数,最后一个参数是函数体,前面的参数是形参名;用不用 new 效果相同。
js
const add = new Function('a', 'b', 'return a + b');
add(1, 2); // 3与直接 eval 的关键区别是:new Function 创建的函数只在全局作用域中解析,不能闭包捕获创建位置的局部变量。函数体默认不是严格模式,即使外层是严格模式;需要在函数体内显式写 'use strict'。
它仍然是动态代码执行,具有注入风险、编译开销、调试和静态分析困难,并通常受 CSP 的 unsafe-eval 限制。不要用它解析 JSON 或执行用户输入。适用场景很少,主要是可信代码生成、模板/表达式引擎的内部实现,而且通常还应缓存编译结果并建立真正的隔离边界。
57. Javascript 数组中有哪些方法可以改变自身,哪些不可以?
答案:
会原地改变数组的方法主要有:
- push、pop、shift、unshift
- splice
- sort、reverse
- fill、copyWithin
它们可能修改元素、顺序或 length,并返回长度、删除项、数组自身等不同结果,不能把返回值一概当成新数组。
不改变原数组的方法包括 concat、slice、map、filter、flat、flatMap、reduce/reduceRight、forEach、some、every、find/findLast、findIndex/findLastIndex、includes、indexOf/lastIndexOf、join、at、keys/values/entries 等。ES2023 的 toSorted、toReversed、toSpliced 和 with 是对应的“复制后修改”方法,会返回新数组。
易混淆点是 splice 会修改,slice 不修改;sort/reverse 会修改。即使方法本身不改数组,回调仍可能主动修改原数组,返回的新数组也可能与原数组共享对象元素。
58. 一直在 window 上面挂内容(数据,方法等等),是否有什么风险?
答案:
有风险。window/globalThis 是长生命周期共享命名空间,持续挂载内容会带来:
- 名称与浏览器未来 API、第三方脚本或其他业务模块冲突,出现静默覆盖。
- 全局对象长期持有数据、DOM、闭包,使它们无法回收。
- 任意同页面脚本都可读取或篡改状态,扩大供应链/XSS 事件的影响。
- 隐式依赖导致初始化顺序、测试隔离和并发修改难以控制。
- 代码无法直接运行在 SSR、Worker、Node 等没有 window 的环境。
业务代码应优先使用 ESM 导入导出、模块闭包和有生命周期的状态容器。确实需要全局集成点时,应集中到一个明确命名空间,定义稳定接口、避免暴露敏感数据,并在生命周期结束时移除引用和监听器。仅把属性设为不可枚举并不能解决生命周期和安全问题。
59. Object.prototype.hasOwnProperty() 作用是什么?
答案:
hasOwnProperty 用于判断指定属性是否是对象“自身”的属性,而不是从原型链继承来的;即使属性值是 undefined,结果仍为 true,也支持 Symbol 属性键。
js
const obj = Object.create({ inherited: 1 });
obj.own = undefined;
obj.hasOwnProperty('own'); // true
obj.hasOwnProperty('inherited'); // false
'inherited' in obj; // true,in 会查原型链不能无条件写 obj.hasOwnProperty(key),因为对象可能覆盖这个方法,或由 Object.create(null) 创建而根本没有该方法。现代代码优先使用 Object.hasOwn(obj, key);兼容旧环境可用 Object.prototype.hasOwnProperty.call(obj, key)。它只判断归属,不判断属性是否可枚举,也不会校验属性值。
60. 哪些原因会导致js里this指向混乱?
答案:
this 的主要误区是把它当成函数定义位置的变量。普通函数的 this 由调用方式决定:
- obj.fn() 是隐式绑定;把 const fn = obj.fn 后再调用会丢失接收者。
- call/apply/bind 是显式绑定;绑定函数再作为回调可保持接收者。
- new 调用把 this 绑定为新实例,并优先于 bind 提供的 thisArg。
- 独立普通调用在严格模式下是 undefined,非严格脚本中可能是 globalThis。
- 箭头函数没有自己的 this,只捕获外层 this,call/apply/bind 无法修改。
- setTimeout、数组回调、事件系统等传递函数时,调用方决定普通回调的 this;不同 API 约定可能不同。
- 嵌套普通函数不会自动继承外层方法的 this。
排查时看实际调用点,而不是只看声明。类和 ES Module 默认严格模式,使“丢失 this”更容易直接报错。常用修复是保留成员调用、显式 bind,或在确实需要词法 this 的回调中使用箭头函数。
61. JS里的类就是构造函数的语法糖,这个说法是否正确?
答案:
这个说法“方向正确,但不完整”。class 没有引入另一套实例继承模型:实例方法仍位于 Class.prototype,静态成员位于类构造器,extends 最终也建立原型关系,所以理解底层时仍要掌握构造函数和原型链。
但 class 具有普通构造函数没有的专门语义:只能用 new 调用、类体默认严格模式、声明存在暂时性死区、原型方法不可枚举;派生构造器有 super/this 初始化规则,并支持私有字段、静态块等语法和内部槽。
因此可以把 class 称为“基于原型机制的更高层语法”,但不能认为把 class 机械替换成 function 加 prototype 后在所有边界上都完全等价。
62. 使用 async/await 时,是否有必要加 try catch?
答案:
不是每个 await 都必须包 try...catch。await 的 Promise 拒绝时会在当前 async 函数中抛出;若不捕获,async 函数返回的 Promise 会保持 rejected,由调用方继续处理。这种向上层传播通常是正确的。
应该捕获的情况是当前层能做有意义的事,例如恢复、转换为业务错误、补充上下文、降级或记录一次边界日志:
js
async function loadUser(id) {
try {
return await api.getUser(id);
} catch (error) {
throw new UserLoadError(id, { cause: error });
}
}资源清理更适合 finally。不要 catch 后只打印并吞掉错误,否则调用方会误以为成功;也不要在每层重复记录同一个异常。若只是想让调用方处理,可以直接 return api.getUser(id),但在需要当前函数的 try/catch/finally 覆盖该异步失败时必须 await。应用入口、事件处理器等 Promise 链终点应有明确的未处理错误策略。
63. 如何冻结一个 JS 对象?
答案:
Object.freeze(obj) 会让对象不可扩展,并把自身数据属性变为不可写、所有自身属性变为不可配置。严格模式下修改会抛 TypeError,非严格模式下多数写入静默失败。可用 Object.isFrozen 检查。
freeze 是浅冻结:嵌套对象仍可修改;访问器属性的 setter 也仍可能改变其他状态。需要深冻结可递归处理,并用 WeakSet 防止循环:
js
function deepFreeze(value, seen = new WeakSet()) {
if ((typeof value !== 'object' && typeof value !== 'function') || value === null) {
return value;
}
if (seen.has(value)) return value;
seen.add(value);
for (const key of Reflect.ownKeys(value)) {
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if ('value' in descriptor) deepFreeze(descriptor.value, seen);
}
return Object.freeze(value);
}时间复杂度 O(V + E),额外空间 O(V)。某些宿主对象或非空 TypedArray 不能按普通对象方式冻结;深冻结还不会冻结闭包、私有槽或通过 getter/setter 关联的外部状态。
64. 原生 js 如何进行监听路由的变化?
答案:
Hash 路由监听 hashchange;History 路由监听 popstate:
js
window.addEventListener('hashchange', () => {
render(location.hash);
});
window.addEventListener('popstate', event => {
render(location.pathname, event.state);
});popstate 主要在用户前进、后退导致活动历史记录变化时触发。history.pushState 和 replaceState 本身不会触发 popstate,所以应用内导航应集中封装,并在调用后主动通知路由层:
js
function navigate(url, state = null) {
history.pushState(state, '', url);
window.dispatchEvent(new Event('app:navigate'));
}
window.addEventListener('app:navigate', () => {
render(location.pathname, history.state);
});如果第三方代码也会直接调用 pushState/replaceState,可谨慎包装这两个方法统一派发自定义事件,但应保留原方法的 this、参数、返回值,且只包装一次。初始化页面时还要主动执行一次 render;History 模式刷新任意路径时,服务端必须回退到入口 HTML。
65. onpopstate 可以监听到 pushstate 的事件吗?
答案:
不能直接监听到。history.pushState() 和 history.replaceState() 都不会触发 popstate;popstate 通常在用户点击前进/后退,或调用 history.back()、forward()、go() 导致同一文档的活动历史记录项变化时触发。
如果应用需要在调用 pushState 后得到通知,应把路由跳转统一封装,或包装 pushState 后主动派发自定义事件:
js
const rawPushState = history.pushState;
history.pushState = function (...args) {
const result = rawPushState.apply(this, args);
window.dispatchEvent(new Event('app:navigate'));
return result;
};hashchange 只关注 URL 的片段标识变化,也不会因为 pushState 本身而触发。
66. 说说对 XMLHttpRequest 对象的了解
答案:
XMLHttpRequest(XHR)是浏览器提供的 HTTP 请求 API,是早期 AJAX 的核心实现。典型流程是创建实例、open()、设置请求头和 responseType、监听事件,再调用 send():
js
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users');
xhr.responseType = 'json';
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) console.log(xhr.response);
};
xhr.onerror = () => console.error('network error');
xhr.send();它用 readyState 表示请求阶段,并支持 load、error、timeout、abort、上传/下载进度、超时、凭据和取消请求。HTTP 4xx/5xx 通常会进入 load 而不是 error,因此仍要检查 status;跨域仍受 CORS 限制。与 fetch 相比,XHR 是事件驱动接口、原生支持进度,但 Promise 组合和流式处理体验较弱。
67. 判断数组的方式有哪些?
答案:
首选 Array.isArray(value),它语义明确,并且能正确识别来自 iframe 等不同 realm 的数组。
js
Array.isArray([]); // true其他方式及边界:
value instanceof Array:同一 realm 中通常可用,但跨 iframe 时可能失败,也会受原型链或Symbol.hasInstance影响。Object.prototype.toString.call(value) === '[object Array]':传统跨 realm 方案,但对象可通过Symbol.toStringTag伪装标签。- 检查原型、
constructor或是否有length都不可靠;类数组对象也可能满足这些条件。
因此业务代码一般直接使用 Array.isArray。
68. typeof null 的结果是什么,为什么?
答案:
结果是字符串 "object":
js
typeof null; // 'object'这是 JavaScript 早期实现遗留的兼容性问题:值曾使用类型标签表示,对象标签为 0,而 null 的位模式也被判成了对象。该行为后来无法修改,否则会破坏大量既有代码。
null 实际是一个独立的原始值,不是对象。判断它应使用 value === null,不能依赖 typeof。
69. js 函数参数有默认值时,如果传递的参数是 undefined 那么会被默认值赋值吗?
答案:
会。实参省略或显式传入 undefined 时,默认参数都会生效;传入 null、0、false 或空字符串则不会。
js
function f(value = 10) {
return value;
}
f(); // 10
f(undefined); // 10
f(null); // null默认值表达式在每次调用时求值,并且可以引用它左侧已经初始化的参数,不能引用仍处于暂时性死区的右侧参数。
70. 什么是伪数组(类数组)?
答案:
类数组对象是具有 length 和数字索引属性、外形像数组,但原型不是 Array.prototype 的对象,例如函数内的 arguments、DOM 的部分集合,以及 {0: 'a', 1: 'b', length: 2}。
它通常不能直接使用 map、filter 等数组方法,也不一定实现了迭代协议。可以转换为真正的数组:
js
const arr1 = Array.from(arrayLike);
const arr2 = Array.prototype.slice.call(arrayLike);展开语法 [...value] 只适用于实现了 Symbol.iterator 的值;仅有 length 的类数组不保证能展开。
71. escape、encodeURI、encodeURIComponent 有什么区别?
答案:
三者的用途和保留字符范围不同:
encodeURI用于编码完整 URI,会保留: / ? # & =等 URI 结构字符,因此不能安全地直接编码查询参数值。encodeURIComponent用于编码 URI 的单个组成部分,会编码上述分隔符,常用于路径片段或查询参数的键和值。escape是非标准的旧 API,按旧规则转义字符串,可能生成%uXXXX,不适合 URI,现代代码不应使用。
js
encodeURI('https://a.test/?q=中文&x=1');
encodeURIComponent('中文&x=1'); // %E4%B8%AD%E6%96%87%26x%3D1编码后应分别用 decodeURI 或 decodeURIComponent 解码。构造查询字符串时优先使用 URL 和 URLSearchParams,可避免手工处理分隔符。
72. use strict是什么,有什么用?
答案:
'use strict' 是严格模式指令。把它放在脚本或函数体开头,可让对应作用域采用更严格、错误更早暴露的语义;ES Module 和 class 主体本身就按严格模式执行。
主要变化包括:未声明变量赋值会报错;普通函数直接调用时 this 为 undefined,不再自动指向全局对象;禁止 with;删除不可配置属性、给只读属性赋值等静默失败会改为抛错;禁止重复参数名以及部分旧式八进制语法;eval 不会向外层泄漏声明。
严格模式不会让代码自动变快,也不会把所有潜在错误都变成异常;它主要提供更可预测、便于静态分析的语言语义。
73. JS 中的数组和函数在内存中是如何存储的?
答案:
ECMAScript 规范只定义行为,不规定必须使用“栈”或“堆”,具体布局由引擎决定。概念上,数组和函数都是对象,变量中保存的是指向对象的值;赋值或传参会复制这个引用值,而不是复制整个对象。
引擎通常会为数组维护元素存储区:稠密、同类型元素可能采用紧凑表示;出现大量空洞、超大索引或混合类型后,可能退化为字典式存储。函数对象除普通属性外,还关联可执行代码以及创建时捕获的词法环境,因此闭包可让外层变量在调用结束后继续存活。
这些都是引擎优化细节,不能在业务代码中假定数组一定连续存储,也不能简单断言“原始值都在栈、引用值都在堆”。
74. 普通函数动态参数 和 箭头函数的动态参数有什么区别?
答案:
普通函数拥有自己的 arguments 类数组对象,可以取得调用时传入的全部实参;箭头函数没有自己的 arguments,在箭头函数里访问该标识符会沿词法作用域查找外层函数的 arguments。
js
function outer() {
const arrow = (...args) => args;
return arrow(1, 2);
}现代代码更推荐剩余参数 ...args:它是真数组、名称明确,普通函数和箭头函数都能使用。arguments 只是类数组;在非严格模式的简单参数函数中,它还可能与形参存在历史上的联动,剩余参数没有这个问题。
75. 函数声明与函数表达式有什么区别
答案:
函数声明会在当前作用域实例化阶段被创建并初始化,因此通常可以在声明文本之前调用:
js
run();
function run() {}函数表达式是在表达式执行时才创建函数,能否提前访问取决于承接它的变量:var fn 会提升但初值是 undefined,提前调用会得到 TypeError;let/const fn 在初始化前处于暂时性死区,会得到 ReferenceError。
js
const run = function namedRun() {};函数表达式适合赋值、传参和条件创建。具名函数表达式的名称通常只在函数体内部可见,便于递归和调试。块级函数声明在严格模式下按块作用域处理,旧环境的非严格模式行为不宜依赖。
76. JS 创建对象的方式有哪些?
答案:
常见创建方式包括:
js
const a = { x: 1 }; // 对象字面量
const b = new Object({ x: 1 }); // Object 构造器
const c = Object.create(a); // 指定原型
function User(name) { this.name = name; }
const d = new User('A'); // 构造函数 + new
class Admin { constructor(name) { this.name = name; } }
const e = new Admin('B'); // class
const makeUser = name => ({ name });
const f = makeUser('C'); // 工厂函数
const g = JSON.parse('{"x":1}'); // 从 JSON 数据反序列化普通数据对象优先用字面量;需要精确控制原型时用 Object.create;需要共享行为和构造语义时使用 class 或构造函数。Object.create(null) 会创建没有 Object.prototype 的纯字典对象,因此没有 toString、hasOwnProperty 等继承方法。
77. hasOwnProperty 与 instanceof 有什么区别
答案:
两者判断的关系完全不同:
obj.hasOwnProperty(key)判断key是否是obj自身的属性,不查原型链。更稳妥的现代写法是Object.hasOwn(obj, key),它也适用于无原型对象或覆盖了hasOwnProperty的对象。value instanceof Constructor判断Constructor.prototype是否出现在value的原型链上,回答的是实例与构造器的关系。
js
Object.hasOwn({ x: 1 }, 'x'); // true
[] instanceof Array; // trueinstanceof 会受跨 iframe、手工修改原型和 Symbol.hasInstance 影响,判断数组应优先使用 Array.isArray。
78. 原型链的终点是什么?
答案:
原型链最终以 null 结束。对普通对象而言,通常是:
text
对象 -> Object.prototype -> null因此 Object.getPrototypeOf(Object.prototype) === null。查找属性时,引擎从对象自身开始沿原型链向上查找,走到 null 仍未找到就返回 undefined。
这不等于说每条链都必须经过 Object.prototype:Object.create(null) 创建的对象其原型直接是 null。
79. 引用类型有哪些,有什么特点
答案:
JavaScript 中除七种原始类型 string、number、bigint、boolean、symbol、undefined、null 外,其余都属于对象类型。常见对象包括普通对象、数组、函数、Date、RegExp、Map、Set、Promise、错误对象等;函数是可调用的特殊对象。
对象按身份比较:两个内容相同但独立创建的对象并不相等。变量赋值和函数传参复制的是引用值,所以多个变量可以指向同一对象并观察到其修改。JavaScript 仍然是按值传递,只是这个值可能是对象引用,函数不能通过重绑形参来改变调用方变量本身。
对象通常可变,但也可用 Object.freeze 等限制修改;“引用类型必然可变”并不准确。
80. 箭头函数解决了什么问题
答案:
箭头函数主要提供两点:更简洁的函数表达式,以及词法绑定的 this。它没有自己的 this,而是使用定义位置外层作用域的 this,因此特别适合需要保留上下文的回调。
js
class Timer {
start() {
setTimeout(() => console.log(this), 0);
}
}它同时没有自己的 arguments、super 和 new.target,不能用作构造函数,也不能作为生成器。正因为 this 无法通过 call、apply、bind 改变,箭头函数通常不适合需要动态接收调用者的对象方法或 DOM 事件处理器。
81. 副作用是什么概念
答案:
副作用是函数除计算并返回结果外,对外部可观察状态造成的影响。例如修改全局变量或入参对象、更新 DOM、写存储、发网络请求、输出日志、注册定时器或事件监听器。
js
function add(a, b) { return a + b; } // 无副作用
function addItem(list, item) { list.push(item); } // 修改入参,有副作用纯函数要求相同输入得到相同输出且没有副作用。读取当前时间、随机数或可变全局状态虽然不一定“写”外部状态,也会破坏确定性。副作用不是错误,实际应用离不开它;关键是把它集中在边界层、明确生命周期并便于测试和清理。
82. JS 中如何实现大对象深度对比
答案:
应先定义“相等”的语义。下面实现适用于大型普通对象、数组、Date 和 RegExp,比较可枚举的字符串与 Symbol 自有属性,并用显式栈避免深层递归爆栈;成对记忆还能处理循环引用:
js
function deepEqual(a, b) {
const stack = [[a, b]];
const seen = new WeakMap();
const enumerableKeys = obj =>
Reflect.ownKeys(obj).filter(key =>
Object.prototype.propertyIsEnumerable.call(obj, key)
);
while (stack.length) {
const [x, y] = stack.pop();
if (Object.is(x, y)) continue;
if (x === null || y === null || typeof x !== 'object' || typeof y !== 'object') {
return false;
}
if (Object.getPrototypeOf(x) !== Object.getPrototypeOf(y)) return false;
const paired = seen.get(x);
if (paired) {
if (paired !== y) return false;
continue;
}
seen.set(x, y);
if (x instanceof Date) {
if (x.getTime() !== y.getTime()) return false;
continue;
}
if (x instanceof RegExp) {
if (x.source !== y.source || x.flags !== y.flags) return false;
continue;
}
const xKeys = enumerableKeys(x);
const yKeys = enumerableKeys(y);
if (xKeys.length !== yKeys.length || !xKeys.every(k => Object.hasOwn(y, k))) {
return false;
}
for (const key of xKeys) stack.push([x[key], y[key]]);
}
return true;
}时间复杂度为 O(n),额外空间为 O(n)。Map、Set、TypedArray、属性描述符和跨 realm 对象需要按业务语义另行处理;生产项目通常优先使用经过验证的深比较库,并尽量通过版本号、不可变数据和结构共享避免频繁遍历整个大对象。
83. 什么是匿名函数?
答案:
匿名函数是源码中没有显式函数名的函数,例如:
js
const fn = function () {};
const handler = () => {};它常用于回调、事件监听和一次性逻辑。匿名函数不等于函数表达式:函数表达式也可以具名;立即执行函数也可以匿名或具名。
现代引擎经常会根据变量名或属性名推断函数的 name,所以上例中的 fn.name 可能是 "fn",但这不代表源码中存在可在函数体外引用的声明名。复杂递归或需要清晰堆栈时,使用具名函数通常更易调试。
84. splice 和 slice 有什么区别?
答案:
slice(start, end) 截取 [start, end) 范围并返回浅拷贝,不修改原数组;参数可以是负数。它也可用于字符串。
splice(start, deleteCount, ...items) 会从原数组删除、替换或插入元素,修改原数组,并返回被删除元素组成的数组。
js
const a = [1, 2, 3, 4];
a.slice(1, 3); // [2, 3],a 不变
a.splice(1, 2, 9); // 返回 [2, 3],a 变为 [1, 9, 4]两者都是浅层操作,嵌套对象仍与原数组共享引用。若需要不修改原数组地插入或删除,可组合 slice,或使用较新的 toSpliced()。
85. JavaScript 和 BOM、DOM 、ECMAScript、Nodejs 之间是什么关系
答案:
ECMAScript 是 JavaScript 语言的标准,定义语法、类型、对象模型、Promise、模块等核心能力。JavaScript 通常指 ECMAScript 语言加上宿主环境提供的 API。
在浏览器中,宿主主要提供:
- DOM:操作 HTML/XML 文档和节点,如
document、元素、事件。 - BOM:与浏览器窗口和导航环境相关的传统统称,如
window、location、history、navigator;它不是像 ECMAScript 那样单一的语言标准。
Node.js 是另一个 JavaScript 运行时,使用 ECMAScript,但提供文件系统、进程、网络等 Node API,默认没有浏览器 DOM。浏览器和 Node 也共享部分 Web API,例如 URL、定时器和逐渐普及的 fetch,但具体全局对象和能力仍取决于运行时。
86. JavaScript 是怎么做内存管理的?
答案:
JavaScript 引擎自动完成内存的分配与垃圾回收。创建对象、闭包、字符串等值时引擎分配内存;只要值仍能从根对象或当前执行上下文等位置到达,就会被视为存活。不可达对象随后由垃圾回收器回收。现代引擎通常组合标记清除、分代、增量和并发回收等策略,具体实现并非语言规范的一部分。
自动回收不等于不会泄漏。常见泄漏来源包括长期存活的全局容器或缓存、未移除的事件监听器和定时器、闭包意外保留大对象、脱离 DOM 的节点,以及无上限的 Map。处理方式是缩短引用生命周期、及时清理资源、限制缓存容量;对象关联的弱引用元数据可考虑 WeakMap/WeakSet。
WeakRef 和 FinalizationRegistry 的回收时机不确定,不应承担关键业务或资源释放逻辑。
87. JavaScript 中, 隐藏类是什么概念?
答案:
隐藏类是 V8 等引擎内部描述对象“形状”的优化结构,不是 ECMAScript 可直接访问的概念。形状通常记录对象有哪些属性、属性布局及其转换关系;多个以相同顺序创建相同属性的对象可以共享形状,属性访问再配合内联缓存,避免每次都做通用字典查找。
js
function Point(x, y) {
this.x = x;
this.y = y;
}如果对象以不同顺序添加属性、频繁增删属性,或同一调用点接收到很多不同形状,引擎可能产生更多隐藏类、让内联缓存变成多态甚至退化,影响优化。应优先保持对象结构稳定,但不要为隐藏类牺牲正确性;不同引擎的策略也不完全相同。
88. 实现一个函数, 计算两个日期之间的天数差
答案:
如果题意是比较两个“日历日期”相差多少天,应先把年月日映射到 UTC 日序,避免夏令时造成一天不是固定 24 小时:
js
function calendarDayDiff(left, right) {
const dayIndex = input => {
let year, month, day;
const match = typeof input === 'string' &&
/^(\d{4})-(\d{2})-(\d{2})$/.exec(input);
if (match) {
[, year, month, day] = match.map(Number);
const check = new Date(0);
check.setUTCHours(0, 0, 0, 0);
check.setUTCFullYear(year, month - 1, day);
if (check.getUTCFullYear() !== year ||
check.getUTCMonth() !== month - 1 ||
check.getUTCDate() !== day) {
throw new RangeError('Invalid date');
}
return check.getTime() / 86400000;
}
const date = input instanceof Date ? new Date(input) : new Date(input);
if (Number.isNaN(date.getTime())) throw new RangeError('Invalid date');
return Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) / 86400000;
};
return Math.abs(dayIndex(right) - dayIndex(left));
}
calendarDayDiff('2024-02-28', '2024-03-01'); // 2时间和空间复杂度均为 O(1)。这里返回绝对值;若需要表示先后方向,去掉 Math.abs。若题意是两个时间点实际相隔多少个 24 小时,应直接比较时间戳,再根据业务规定向上、向下或四舍五入。
89. 实现日期格式化 format 函数
答案:
下面实现支持常用的本地时间令牌,并校验日期:
js
function formatDate(input, pattern = 'YYYY-MM-DD HH:mm:ss') {
const date = input instanceof Date ? new Date(input) : new Date(input);
if (Number.isNaN(date.getTime())) throw new RangeError('Invalid date');
const pad = (value, length = 2) => String(value).padStart(length, '0');
const tokens = {
YYYY: pad(date.getFullYear(), 4),
MM: pad(date.getMonth() + 1),
DD: pad(date.getDate()),
HH: pad(date.getHours()),
mm: pad(date.getMinutes()),
ss: pad(date.getSeconds()),
SSS: pad(date.getMilliseconds(), 3)
};
return pattern.replace(/YYYY|SSS|MM|DD|HH|mm|ss/g, token => tokens[token]);
}
formatDate(new Date(2024, 0, 2, 3, 4, 5), 'YYYY/MM/DD HH:mm:ss');
// 2024/01/02 03:04:05时间复杂度为 O(p),p 是格式字符串长度,额外空间为 O(p)。该实现按运行环境本地时区格式化,也未定义字面量转义;涉及国际化、时区名称和语言习惯时,应优先使用 Intl.DateTimeFormat。
90. Object 对象有哪些场景 api ?
答案:
Object 的常用静态 API 可按场景归类:
- 创建与组合:
Object.create、Object.assign、对象展开语法。 - 枚举与转换:
Object.keys、values、entries、fromEntries。前三者只处理可枚举的字符串自有属性。 - 属性与描述符:
defineProperty、defineProperties、getOwnPropertyDescriptor(s)、getOwnPropertyNames、getOwnPropertySymbols、Reflect.ownKeys。 - 原型:
getPrototypeOf、setPrototypeOf。频繁修改现有对象原型通常不利于优化。 - 自有属性判断:
Object.hasOwn。 - 可变性控制:
preventExtensions、seal、freeze及对应的isExtensible、isSealed、isFrozen。这些操作默认都只是浅层。 - 比较与类型辅助:
Object.is、Object.prototype.toString.call(value)。
实例方法如 toString、valueOf 通常来自 Object.prototype;无原型对象并不具备这些方法。
91. 如何拦截 web 应用的请求
答案:
要先明确拦截范围:
- 应用代码层:统一请求客户端,并使用 Axios 拦截器;原生请求可包装
fetch,也可代理XMLHttpRequest,适合注入鉴权、日志和统一错误处理。只改写全局fetch不能覆盖已保存的引用、XHR、图片标签等其他请求通道。 - 浏览器网络层:同源且在作用域内的 Service Worker 可监听
fetch,决定转发、缓存或返回自定义响应,适合离线缓存和 Mock。 - 开发/部署层:使用开发服务器代理、反向代理、网关或服务端中间件,可统一处理浏览器之外的流量。
js
self.addEventListener('fetch', event => {
event.respondWith(caches.match(event.request).then(r => r || fetch(event.request)));
});浏览器安全模型不允许页面任意读取或篡改其他来源、其他应用的请求;跨域响应仍受 CORS 控制。测试场景可使用 Mock Service Worker 一类基于 Service Worker/Node 拦截器的工具。
92. for...of、for...in、for 循环, 三者有什么区别?
答案:
三者迭代的对象不同:
- 普通
for由开发者控制初始化、条件和步进,适合需要索引、倒序、跳步或精细控制的场景。 for...of消费可迭代协议,得到“值”。数组、字符串、Map、Set、生成器等都可使用,并支持break、continue;普通对象默认不可迭代。for...in枚举对象可枚举的字符串属性键,会包含原型链上的属性,不包含 Symbol;顺序虽有规范约束,但仍不应用于数组值遍历。
js
for (const value of array) {}
for (const key in object) {
if (Object.hasOwn(object, key)) {}
}数组用 for...in 还可能遍历自定义属性,键也是字符串,因此通常选普通 for、for...of 或数组迭代方法。
93. JS 代码放在 head 里和放在 body 里有什么区别?
答案:
传统外部脚本 <script src> 在 HTML 解析器遇到时会暂停解析,等待下载并执行。因此阻塞脚本放在 head 可能延迟页面 DOM 构建,且执行时后续元素尚不存在;放在 body 末尾可让前面的 DOM 先解析。
现代做法通常是把脚本保留在 head 并使用加载属性:
defer:并行下载,文档解析完成后按文档顺序执行,并在DOMContentLoaded前完成。async:并行下载,下载完立即执行,不保证顺序,适合相互独立的脚本。type="module":默认具有类似defer的行为,且按模块语义执行。
内联脚本没有 src 时,defer 不产生同样效果。最终选择应看脚本依赖顺序和首次渲染需求,而不是机械地规定只能放在某个标签中。
94. JS 的严格模式有什么作用?
答案:
严格模式让 JavaScript 使用更明确、更安全且便于优化的语义。脚本或函数顶部的 'use strict' 可启用它;ES Module 和 class 代码天然处于严格模式。
典型作用包括:
- 未声明变量赋值、给只读属性赋值、删除不可配置属性等行为从静默失败变为抛错。
- 普通函数直接调用的
this为undefined,不会自动装箱或指向全局对象。 - 禁止
with、重复形参和部分遗留语法。 eval创建的变量不会泄漏到外层作用域。- 保留部分标识符,为静态分析和未来语法演进减少歧义。
它不会改变闭包、事件循环等基本机制,也不是完整的安全沙箱。
95. JS 的严格模式为什么会禁用 with 语句?
答案:
with (obj) { x } 会在作用域链中临时插入对象,使 x 到底表示词法变量还是 obj.x 取决于对象运行时是否具有该属性。对象属性还可动态增删,代理对象也能改变查询结果,因此编译器和读代码的人都难以静态确定标识符绑定。
这会妨碍优化、重构和压缩:局部变量难以安全重命名,拼写错误也可能意外访问外层变量。严格模式要求词法绑定可预测,所以直接将 with 规定为语法错误。应改为显式属性访问或先解构需要的属性。
96. 说说你对 API IntersectionObserver 的理解,它有什么使用场景?
答案:
IntersectionObserver 异步观察目标元素与根元素或视口的交叉状态。浏览器批量计算交叉信息,并在跨过 threshold 阈值时回调,避免在高频 scroll 中反复手算布局。
js
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
loadImage(entry.target);
observer.unobserve(entry.target);
}
}
}, { root: null, rootMargin: '200px', threshold: 0 });
observer.observe(document.querySelector('img[data-src]'));常见场景有图片懒加载、无限滚动哨兵、曝光统计、进入视口动画和按可见性暂停媒体。rootMargin 可提前触发,threshold 可设多个比例。它的通知是异步且可能被合并,不适合像素级、每帧同步的位置跟踪;不再使用时应 unobserve 或 disconnect。
97. 怎么遍历 ES6 中的 Set 对象?
答案:
Set 实现了可迭代协议,默认按插入顺序遍历值:
js
const set = new Set(['a', 'b']);
for (const value of set) console.log(value);
set.forEach(value => console.log(value));
[...set.values()];
Array.from(set);set.keys() 与 set.values() 等价;为兼容 Map 接口,set.entries() 返回 [value, value]。forEach 的前两个回调参数也都是值。WeakSet 不可枚举、没有迭代器和 forEach,因为其成员可随垃圾回收消失。
98. 怎么遍历 ES6 中的 Map 对象?
答案:
Map 默认迭代器和 entries() 都按插入顺序返回 [key, value]:
js
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) console.log(key, value);
map.forEach((value, key) => console.log(key, value));
for (const key of map.keys()) {}
for (const value of map.values()) {}也可用 [...map] 或 Array.from(map) 转成键值对数组。Map.prototype.forEach 的参数顺序是“值、键”,与 for...of 解构顺序不同。WeakMap 不可遍历。
99. Proxy 和 Reflect 有什么关系?
答案:
Proxy 用来拦截对象的内部操作,例如读取、赋值、删除、函数调用和构造;Reflect 提供与这些内部操作相对应的函数式默认行为。两者经常配合:在 trap 中记录或校验后,用 Reflect 把操作正确地转发给目标对象。
js
const proxy = new Proxy(target, {
get(target, key, receiver) {
console.log('get', key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver);
}
});Reflect.get/set 的 receiver 能保留访问器中 this 的正确语义,Reflect.set/deleteProperty 等还返回布尔结果,便于 trap 原样返回。Reflect 可以单独使用,并不依赖 Proxy;Proxy 也必须遵守不可配置属性等语言不变量,否则会抛出 TypeError。
100. 如何自定义鼠标右键菜单?
答案:
监听目标区域的 contextmenu,阻止浏览器默认菜单,再显示自己的菜单:
js
const area = document.querySelector('.area');
const menu = document.querySelector('.context-menu');
area.addEventListener('contextmenu', event => {
event.preventDefault();
menu.hidden = false;
const x = Math.min(event.clientX, innerWidth - menu.offsetWidth);
const y = Math.min(event.clientY, innerHeight - menu.offsetHeight);
Object.assign(menu.style, { left: `${x}px`, top: `${y}px` });
menu.focus();
});
addEventListener('pointerdown', event => {
if (!menu.contains(event.target)) menu.hidden = true;
});
addEventListener('keydown', event => {
if (event.key === 'Escape') menu.hidden = true;
});菜单一般使用 position: fixed,并处理视口边缘、滚动、点击外部、Escape、焦点移动和键盘操作。网页只能替换页面内容区域中的上下文菜单,不能定制浏览器自身的标签页或地址栏菜单。
101. 如何减少项目里的 if-else
答案:
减少 if...else 的目标是降低分支复杂度,而不是消灭条件判断。常用方式有:
- 用提前返回处理非法状态和边界条件,减少嵌套。
- 对“键 -> 行为”使用对象、
Map或命令表。 - 对不同类型的稳定行为使用多态或策略模式。
- 把复杂布尔表达式提炼成有业务含义的谓词函数。
- 有限状态流使用状态机,避免多个布尔值组合出非法状态。
- 默认值只在语义正确时使用
??、可选链等表达式。
js
const handlers = new Map([
['create', createItem],
['delete', deleteItem]
]);
const handler = handlers.get(action);
if (!handler) throw new Error('Unsupported action');
return handler(payload);两三个清晰分支往往比过度抽象更容易维护;避免把条件藏进难懂的三元表达式或动态调用。
102. ajax 和 xhr 是什么关系?
答案:
AJAX 是一种在不整页刷新的情况下,由页面异步与服务器交换数据并更新界面的开发技术;XHR(XMLHttpRequest)是浏览器提供的具体请求 API。
早期 AJAX 基本依赖 XHR,所以两者常被混用,但它们不是同一层概念。现在 AJAX 请求也可以用 fetch、Axios 等实现;Axios 在浏览器里又可能通过 XHR 或 fetch 适配器发送。名称里的 XML 是历史遗留,XHR 可以传输 JSON、文本、Blob 等多种数据。
103. axios 请求的底层依赖是什么?
答案:
Axios 的底层不是固定依赖某一个请求 API,而是通过适配器屏蔽运行环境差异。现代 Axios 内置 xhr、http 和 fetch 适配器,并从当前环境可用的适配器中选择,也允许显式配置或自定义适配器。
- 浏览器中传统默认实现是
XMLHttpRequest;也可以显式选择fetch适配器。 - Node.js 中通常使用基于 Node HTTP 能力的
http适配器,也可在支持条件满足时选择fetch。
Axios 在适配器之上统一提供 Promise、请求/响应转换、拦截器、取消、超时等能力。因此笼统回答“Axios 底层就是 XHR”只适用于特定浏览器版本和适配器配置,不是完整结论。
104. documentFragment api 是什么, 有哪些使用场景?
答案:
DocumentFragment 是一种轻量的 DOM 节点容器,本身不在活动文档树中,因此不会直接渲染。把它插入 DOM 时,插入的是它的所有子节点,fragment 自身不会留下,并会在插入后变空。
js
const fragment = document.createDocumentFragment();
for (const text of ['A', 'B']) {
const li = document.createElement('li');
li.textContent = text;
fragment.append(li);
}
document.querySelector('ul').append(fragment);它适合离线组装一批节点、批量移动节点、承载 <template>.content,以及配合 Range.extractContents() 处理文档片段。这样可减少中间状态暴露在页面上的次数,但现代浏览器中它不保证比所有直接追加方式都快,性能仍应以测量为准。
105. 说说你对 MutationObserver 的理解
答案:
MutationObserver 用于异步观察 DOM 树变化,替代性能和语义较差的旧 Mutation Events。多个同步修改通常会被批量记录,在当前同步代码结束后的微任务检查点附近回调。
js
const observer = new MutationObserver(records => {
for (const record of records) console.log(record.type, record.target);
});
observer.observe(root, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class']
});
// observer.disconnect();可观察子节点增删、属性和文本变化,并通过 subtree、attributeOldValue、characterDataOldValue 等控制范围。回调中继续修改 DOM 可能产生新记录,应避免反馈循环;结束时调用 disconnect(),必要时用 takeRecords() 取出待处理记录。它观察的是 DOM 变化,不会监听普通 JavaScript 变量,也不能穿透未显式观察的封闭 Shadow DOM。
106. mouseEnter、mouseLeave、mouseOver、mouseOut 有什么区别?
答案:
核心区别是是否冒泡,以及进入子元素时会不会重复触发:
mouseover/mouseout会冒泡。指针在父元素与其子元素之间移动时,也会分别触发,适合事件委托,但可能产生很多回调。mouseenter/mouseleave不冒泡,并把元素及其后代视为一个整体;在同一元素的后代之间移动不会重新触发父元素的 enter/leave。
mouseout 和 mouseleave 的 relatedTarget 表示指针要进入的节点,mouseover 和 mouseenter 的 relatedTarget 表示来自哪个节点。触摸笔等统一输入场景可使用对应的 pointerover/out 与 pointerenter/leave。
107. 微前端架构中,一般是如何做到 JavaScript 隔离的?
答案:
微前端的 JavaScript 隔离通常有三个层级:
- iframe:每个子应用拥有独立的
window、全局变量和事件循环环境,隔离最强,但路由、弹窗、样式融合、通信和首屏体验成本较高。 - Proxy 沙箱:把子应用脚本放进包装作用域,用代理后的全局对象拦截属性读写;应用卸载或停用时恢复/丢弃变更。兼容性回退方案可能在激活前后对全局属性做快照和差异恢复。
- 工程约束:ES Module、模块联邦或独立 bundle 避免把模块变量挂到全局,并给全局键、事件、存储加命名空间。
仅代理 window 还不够。定时器、全局事件监听、DOM 节点、动态脚本、history 修改等副作用需要在生命周期中登记并清理。Proxy 沙箱主要防止应用间的意外污染,不是抵御恶意代码的安全边界;不可信代码应使用 iframe 等真正的隔离环境。
108. qiankun 是如何实现 JS 隔离的?
答案:
qiankun 会先获取子应用入口并控制脚本执行,再把脚本放到以沙箱代理对象为作用域的包装函数中。支持 Proxy 时,核心思路是通过 with (proxyWindow) 和 Proxy trap 拦截对子应用全局属性的读取、设置和删除,使新增或修改尽量落在子应用自己的代理环境中。
其实现会根据能力和模式使用不同沙箱:现代多实例场景以 Proxy 沙箱为主;旧环境可使用快照沙箱,在激活/失活时比较和恢复全局属性;旧的单实例模式也有记录全局变更再恢复的方案。qiankun 还会在生命周期中处理部分全局副作用和动态资源。
这种隔离用于降低子应用之间的全局污染,不是安全沙箱。能够逃逸包装作用域的代码、共享 DOM/API、未清理的监听器或定时器仍可能相互影响,因此子应用仍需遵守 mount/unmount 清理约定。
109. 不会冒泡的事件有哪些?
答案:
常见不冒泡事件包括:
- 焦点:
focus、blur;需要委托时可用会冒泡的focusin、focusout。 - 指针边界:
mouseenter、mouseleave、pointerenter、pointerleave;对应的mouseover/out、pointerover/out会冒泡。 - 资源与页面生命周期中的不少事件:元素上的
load、资源error、unload、beforeunload。 - 元素的
scroll以及多数媒体事件,如play、pause、ended。
准确性应以事件对象的 event.bubbles 为准,而且“不冒泡”和“不经过捕获阶段”不是一回事:很多不冒泡事件仍可在祖先的捕获监听器中观察到。事件是否跨 Shadow DOM 边界还由独立的 composed 属性决定。
110. 如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功?
答案:
数组解构读取的是可迭代协议,而普通对象默认不可迭代。给对象实现 Symbol.iterator 即可让原写法成功:
js
const obj = {
a: 1,
b: 2,
*[Symbol.iterator]() {
yield this.a;
yield this.b;
}
};
var [a, b] = obj;
console.log(a, b); // 1 2生成器返回的迭代器依次提供两个值,数组解构会调用它并消费前两项。这里时间和额外空间复杂度都是 O(1)。
如果不要求保持右侧对象原样,也可以写 var [a, b] = Object.values(obj),其顺序遵循对象自有可枚举字符串键的枚举顺序,时间和空间复杂度为 O(n);业务上若顺序必须稳定,最好显式列出属性而不是依赖通用枚举。
111. MessageChannel 是什么,有什么使用场景?
答案:
MessageChannel 创建一对相互连接的 MessagePort:向 port1 发送的数据由 port2 接收,反之亦然。消息异步投递,数据使用结构化克隆算法,也可携带可转移对象。
js
const channel = new MessageChannel();
channel.port1.onmessage = event => console.log(event.data);
channel.port2.postMessage({ type: 'ping' });常见用途包括主线程与 Worker 的专用双向通道、向 iframe 转交一个通信端口、在多个通信方之间建立点对点端口,以及把工作安排到当前调用栈和微任务之后的任务中。端口可通过 postMessage 的 transfer 列表移交给另一个上下文。
它不会自动发现任意标签页;跨独立 tab 通常更适合 BroadcastChannel 或 SharedWorker。结束时可调用 port.close(),使用 addEventListener('message', ...) 注册时还需调用 port.start()。
112. try...catch 可以捕获到异步代码中的错误吗?
答案:
try...catch 只能捕获其动态执行范围内同步抛出的错误,不能捕获稍后在独立任务中运行的回调错误:
js
try {
setTimeout(() => { throw new Error('boom'); }, 0);
} catch (error) {
// 捕获不到
}因为回调执行时外层 try 已经结束。Promise 拒绝应使用 .catch(),或在 async 函数中对被 await 的 Promise 使用 try...catch:
js
try {
await request();
} catch (error) {
console.error(error);
}如果只启动 Promise 而不 await 或 return,外层仍捕获不到它后续的拒绝。回调式 API 则应在回调内部捕获,或通过其约定的错误参数/事件处理。
113. 虚拟dom渲染到页面的时候,框架会做哪些处理?
答案:
首次挂载时,框架会把虚拟节点转换成真实 DOM,设置属性、样式和事件,并按层级插入容器。更新时通常经历:
- 根据新的状态重新产生虚拟树。
- 对新旧节点做协调:比较类型和
key,决定复用、移动、创建或卸载哪些节点。 - 生成并提交最小必要的宿主操作,更新文本、属性、样式、事件引用及子节点顺序。
- 在提交阶段处理 ref、生命周期钩子或 effect;某些框架会批量更新、调度优先级或把渲染阶段拆分。
服务端渲染场景还可能进行 hydration:复用已有 DOM 并补上事件和状态关联,而不是全部重建。具体 diff、事件委托和调度策略因 React、Vue 等框架而异;并非每次渲染都会重新绑定所有事件或遍历整个页面 DOM。
114. setTimeout 延时写成0,一般可以什么场景下使用?
答案:
setTimeout(fn, 0) 的含义是达到最小延迟后把回调排入定时器任务队列,不是立即执行。它一定要等当前调用栈结束,并且当前任务产生的微任务清空后才有机会执行;嵌套定时器、后台标签页和浏览器节流还会增加延迟。
适用场景包括把非关键逻辑推迟到当前同步流程之后、避免同步重入,或把较大计算拆成多批以给事件循环处理输入和渲染的机会。它不保证“DOM 一定已经绘制”;需要在下一帧绘制前更新动画应使用 requestAnimationFrame,需要空闲处理可考虑 requestIdleCallback,现代任务调度也可考虑 scheduler.postTask。
另外,setTimeout 创建的是任务(常称宏任务),不是微任务。
115. 描述下列代码的执行结果
js
foo(typeof a);
function foo(p) {
console.log(this);
console.log(p);
console.log(typeof b);
let b = 0;
}答案:
在非严格的浏览器经典脚本中,输出和异常依次为:
text
Window(全局对象)
undefined
ReferenceError: Cannot access 'b' before initialization这里第二行是字符串 "undefined" 的控制台显示。原因是函数声明 foo 已提升;对完全未声明的 a 使用 typeof 不会报错,而是得到字符串 "undefined" 并传给 p。foo(...) 是普通直接调用,非严格模式下 this 指向全局对象。
函数内的 let b 从进入作用域到执行声明前处于暂时性死区。typeof 对“未声明标识符”安全,但访问暂时性死区中的绑定仍会抛 ReferenceError,因此 let b = 0 不会执行。若代码在严格模式或 ES Module 中运行,第一行的 this 会是 undefined,其余过程相同。
116. 说说下面代码的输出结果
Promise.resolve().then(() => {
console.log(0)
return Promise.resolve(4)
}).then((res) => {
console.log(res)
})
Promise.resolve().then(() => {
console.log(1)
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(5)
}).then(() =>{
console.log(6)
})答案:
输出顺序是:
text
0
1
2
3
4
5
6两条链的首个 .then 先后进入微任务队列,所以先输出 0、1。第二条链的每个回调返回普通值 undefined,会逐级安排下一个 reaction,因此继续输出 2、3。
第一条链的回调返回一个已经 fulfilled 的 Promise。外层 .then 返回的 Promise 必须“采纳”这个 Promise 的状态,规范会通过额外的 Promise 解析/反应任务完成,而不是立刻把 4 交给下一层,所以打印 4 的回调排到了第二条链打印 3 之后。它执行后,第二条链已安排的后续回调再输出 5、6。
117. 如何让 Proxy 去监听基本数据类型?
答案:
不能直接代理基本类型。new Proxy(target, handler) 的 target 必须是对象,传入数字、字符串、布尔值、null 等都会抛 TypeError。要监听变化,需要把值放进对象属性(常称 ref/box),并要求所有读写都经过代理:
js
function observableValue(initial, onChange) {
return new Proxy({ value: initial }, {
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
const ok = Reflect.set(target, key, value, receiver);
if (ok && key === 'value' && !Object.is(oldValue, value)) {
onChange(value, oldValue);
}
return ok;
}
});
}
const count = observableValue(0, console.log);
count.value = 1;时间和额外空间复杂度均为 O(1)。普通变量 let n = 1; n = 2 的重新赋值无法被 Proxy 截获;必须通过包装对象、访问器、显式 setter 或响应式状态 API。
118. Proxy 能够监听到对象中的对象的引用吗?
答案:
Proxy 默认是浅层的。根代理能监听嵌套对象引用本身的读取或替换,例如 proxy.child = other;但 proxy.child.name = 'x' 修改的是原始 child 对象,若 child 没有被代理,根对象的 set trap 不会触发。
可在读取对象值时惰性创建并缓存嵌套代理:
js
function reactive(value, cache = new WeakMap()) {
if (value === null || typeof value !== 'object') return value;
if (cache.has(value)) return cache.get(value);
const proxy = new Proxy(value, {
get(target, key, receiver) {
return reactive(Reflect.get(target, key, receiver), cache);
},
set(target, key, next, receiver) {
const prev = Reflect.get(target, key, receiver);
const ok = Reflect.set(target, key, next, receiver);
if (ok && !Object.is(prev, next)) console.log('changed', key, next);
return ok;
}
});
cache.set(value, proxy);
return proxy;
}单次属性访问平均为 O(1),最多为每个访问到的对象创建一个代理,空间为 O(n)。若外部仍持有并直接修改原始嵌套对象,该修改也绕过代理,因此要统一暴露代理引用。
119. 需要在本地实现一个聊天室,多个tab页相互通信,不能用websocket,你会怎么做?
答案:
同源多个 tab 的本地聊天室首选 BroadcastChannel。所有页面加入同名频道即可广播结构化数据,不需要互相持有窗口引用:
js
const channel = new BroadcastChannel('local-chat');
channel.addEventListener('message', event => {
renderMessage(event.data);
});
function sendMessage(text) {
const message = { id: crypto.randomUUID(), text, time: Date.now() };
renderMessage(message); // 发送方不会收到自己的 message 事件
channel.postMessage(message);
}
addEventListener('pagehide', () => channel.close());它仅适用于允许互通的同源/同存储分区上下文,消息也不持久化。聊天记录可另存 IndexedDB,并用消息 ID 去重。兼容性方案是写入 localStorage 并在其他 tab 监听 storage(该事件不在写入页触发,且只能传字符串);需要中央状态或更复杂连接管理时可使用 SharedWorker。时间和空间开销与单条消息大小均为 O(m)。
120. 如果空数组调用reduce会发生什么?
答案:
空数组调用 reduce 时:
- 没有提供初始值会抛出
TypeError: Reduce of empty array with no initial value,因为不存在可作为首个累加器的元素。 - 提供初始值时不会调用回调,直接返回该初始值。
js
[].reduce((sum, n) => sum + n); // TypeError
[].reduce((sum, n) => sum + n, 0); // 0实践中建议始终提供符合结果类型的初始值,这也能让非空数组的处理规则保持一致,并改善 TypeScript 的类型推断。
121. 数组中的reduce方法有用过吗,说说它的具体用途?
答案:
reduce 把数组按顺序归约成一个结果。回调接收 accumulator、当前元素、索引和原数组,其返回值成为下一轮累加器:
js
const total = [1, 2, 3].reduce((sum, n) => sum + n, 0);
const byId = users.reduce((result, user) => {
result[user.id] = user;
return result;
}, {});除了求和,还可用于分组、计数、建立索引、扁平化和组合管道。遍历时间复杂度通常为 O(n),额外空间取决于累加器。应尽量提供初始值,否则空数组会报错且第一轮规则不同;稀疏数组的空槽会被跳过。
不要为了“一行代码”把复杂流程都塞进 reduce。需要提前退出、包含大量副作用或分支时,普通循环通常更清楚。
122. 改变this指向的方法有哪些?
答案:
显式指定普通函数 this 的主要方法是:
fn.call(thisArg, a, b):立即调用,参数逐个传入。fn.apply(thisArg, [a, b]):立即调用,参数以数组或类数组传入。fn.bind(thisArg, a):返回绑定了this和可选前置参数的新函数,不立即执行。
obj.fn() 会让 this 由调用点的接收者 obj 决定;new Fn() 会创建并绑定新实例,构造调用时绑定函数的 thisArg 会被忽略。箭头函数没有自己的 this,它从定义位置捕获外层 this,因此不能通过 call、apply 或 bind 改变。
严格模式下传给 call/apply 的 null、undefined 会原样作为 this;非严格函数可能把它们替换为全局对象,并对原始值装箱。
123. addEventListener 第三个参数
答案:
第三个参数可以是布尔值 useCapture,也可以是选项对象。布尔值 true 表示在捕获阶段监听,false 表示非捕获监听,默认是 false。推荐选项对象,可读性更好:
js
const controller = new AbortController();
button.addEventListener('click', handler, {
capture: false,
once: true,
passive: false,
signal: controller.signal
});capture:是否在捕获阶段处理。once:调用一次后自动移除。passive:声明监听器不会取消默认行为;若仍调用preventDefault(),调用会被忽略并通常产生警告。signal:对应AbortController.abort()时自动移除监听器。
调用 removeEventListener 时,类型、回调引用和 capture 必须匹配;once、passive 等不参与移除匹配。某些滚动相关事件的浏览器默认 passive 策略可能不同,需要取消默认行为时应显式配置。
124. 是否有使用过空值合并运算符(??),举几个可以使用的场景。
答案:
a ?? b 只在 a 为 null 或 undefined 时返回 b,会保留合法的 0、false、空字符串和 NaN。这与 || 对所有假值使用默认值不同。
js
const pageSize = options.pageSize ?? 20; // 0 会保留
const title = response.title ?? '未命名'; // '' 会保留
const city = user.address?.city ?? '未知';
settings.timeout ??= 3000;它具有短路行为:左侧非空时不会计算右侧。适合配置默认值、可选链结果兜底和只补齐缺失字段。语法上不能在没有括号的情况下直接与 && 或 || 混用,例如 a || b ?? c 是语法错误,应明确写成 (a || b) ?? c。
125. canvas 和 webgl 有什么区别?
答案:
canvas 是 HTML 位图画布元素;通常所说的 Canvas 2D 是 canvas.getContext('2d') 提供的即时模式 2D 绘图 API。WebGL 则是同一类 canvas 上可取得的另一种上下文,基于 OpenGL ES 思想,通过顶点/片元着色器和 GPU 管线绘制 2D 或 3D 图形。
Canvas 2D API 更直接,适合图表、签名、图片合成和中等规模 2D 动画;WebGL 能处理大量顶点、纹理、深度、混合和自定义特效,适合 3D、游戏和大规模可视化,但需要管理缓冲区、着色器、矩阵和 GPU 状态,开发复杂度更高。Three.js 等库可封装 WebGL。
两者绘制结果都是位图,不像 DOM/SVG 那样保留可逐节点操作的图形对象;命中测试、可访问性和高 DPI 缩放通常需要应用自行处理。Canvas 2D 也可能被浏览器硬件加速,因此不能简单概括为“Canvas 一定用 CPU”。
126. 说说下面代码的输出是什么?
js
function Foo(){
Foo.a = function(){
console.log(1);
}
this.a = function(){
console.log(2)
}
}
Foo.prototype.a = function(){
console.log(3);
}
Foo.a = function(){
console.log(4);
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();答案:
输出为:
text
4
2
1第一次 Foo.a() 调用的是构造函数对象上的静态属性,输出 4。执行 new Foo() 时,构造函数做了两件事:把 Foo.a 重写为输出 1 的函数;同时给新实例创建自有属性 obj.a,其函数输出 2。
因此 obj.a() 优先命中实例自有属性,遮蔽 Foo.prototype.a,输出 2;最后 Foo.a() 已在构造过程中被重写,输出 1。原型上的输出 3 的方法始终没有被调用;只有实例不存在同名自有属性时,属性查找才会走到它。
127. 说说下面代码的执行过程
js
var a=3;
function c(){
alert(a);
}
(function(){
var a=4;
c();
})();答案:
弹窗显示 3。
函数 c 在全局词法环境中定义,它对变量 a 的解析位置由定义时的词法作用域决定,而不是由调用位置决定。立即执行函数中的 var a = 4 只属于该函数作用域;虽然 c() 在这里被调用,但 c 的外层仍是全局作用域,所以读取全局的 a = 3。
这体现了 JavaScript 的词法作用域。若希望输出 4,可以把值作为参数传给 c,或在立即执行函数内部重新定义一个能闭包捕获局部 a 的函数。
128. generator 是怎么做到中断和恢复的?
答案:
调用生成器函数不会立即执行函数体,而是返回一个迭代器。第一次 next() 才开始运行;遇到 yield 时,引擎保存当前执行状态,包括指令位置、局部变量、参数和待处理的控制结构,然后把 { value, done: false } 交给调用者。下一次 next(value) 恢复该状态,传入的 value 会成为上一次 yield 表达式的结果,并继续运行到下一个 yield 或函数结束。
js
function* gen() {
const x = yield 1;
return x * 2;
}
const it = gen();
it.next(); // { value: 1, done: false }
it.next(5); // { value: 10, done: true }throw(error) 会在暂停的 yield 位置抛入异常,return(value) 请求提前结束并执行必要的 finally 清理。引擎可把生成器实现为可挂起执行上下文或内部状态机;转译器也常将其编译成显式状态机。生成器迭代器通常是一次性的,结束后不能从头恢复。
129. 为什么要区分宏任务和微任务?它们的执行优先级是什么?
答案:
区分宏任务和微任务,是为了在“进入下一轮事件”之前,给已经发生的异步结果一个及时、确定的收尾阶段。例如 Promise 的后续回调通常应紧接当前同步代码完成,而不是等到下一个定时器或用户事件之后。
浏览器中一次典型的执行顺序是:
- 执行一个宏任务(也称 task),例如初始脚本、定时器回调、I/O 或用户事件回调。
- 当前调用栈清空后执行微任务检查点,把微任务队列一直清空;微任务中新加入的微任务也会在本轮继续执行。
- 浏览器获得一次样式计算、布局和绘制的机会。
- 再选择下一个宏任务执行。
常见微任务有 Promise 的 then/catch/finally 回调、queueMicrotask 和 MutationObserver;常见宏任务有 script、setTimeout/setInterval、MessageChannel 消息和 DOM 事件。
因此通常说“微任务优先于下一个宏任务”,但不是微任务能抢占正在执行的同步代码。宏任务也不一定来自一个全局严格 FIFO 队列,浏览器可以在不同任务源之间调度。若微任务不断产生新微任务,会让渲染和后续任务长期得不到执行,形成微任务饥饿。
130. 导致 JavaScript 中 this 指向混乱的原因是什么?
答案:
混乱的根源是:普通函数的 this 主要由调用方式决定,而不是由函数写在哪里决定;箭头函数又采用完全不同的词法 this 规则。
普通函数可按以下规则判断:
- new 调用:this 指向新创建的实例。
- call、apply、bind:this 显式绑定到指定对象;bind 返回的绑定函数通常不能再被 call/apply 改写。
- obj.fn():this 指向点号前的 obj。
- 独立调用 fn():严格模式下是 undefined;非严格浏览器脚本中通常是全局对象。
- DOM 监听器等宿主 API 还可能按 API 约定设置 this。
常见问题是方法被解构或作为回调传递后,调用点变成 fn(),隐式绑定随之丢失;嵌套普通函数也不会自动继承外层方法的 this。箭头函数没有自己的 this、arguments、super 和 new.target,它捕获定义位置外层的 this,因此 call/apply/bind 不能改变它,也不能用作构造函数。
实践中应优先通过参数传递依赖;确实需要上下文时,保持 obj.fn() 的调用形式,或使用 bind、箭头回调。不能简单地认为“异步会改变 this”,真正变化的是回调最终采用了什么调用方式。
131. 前端路由 a -> b -> c这样前进,也可以返回 c -> b -> a,用什么数据结构来存比较高效
答案:
只要求按 a → b → c 前进,再按 c → b → a 后退时,使用栈最合适:访问新路由时入栈,后退时弹栈,入栈和出栈都是 O(1)。
若要完整支持浏览器式的“后退后还能前进”,应使用两个栈加一个当前项:
- 访问新页面:把 current 压入 backStack,清空 forwardStack,再更新 current。
- 后退:把 current 压入 forwardStack,从 backStack 弹出一项作为 current。
- 前进:把 current 压入 backStack,从 forwardStack 弹出一项作为 current。
例如当前为 c 时,backStack 是 [a, b];后退到 b 后,forwardStack 是 [c]。这种结构的单次前进、后退仍为 O(1),空间复杂度为 O(n)。实际浏览器还要处理 replaceState、跨文档导航、刷新持久化和历史长度限制,但核心数据结构就是栈式历史。
132. 如果要实现一个类似“谷歌图片”的系统,你会有哪些方面的考虑?
答案:
实现图片搜索系统需要同时考虑检索链路、图片处理和前端体验,而不只是做一个瀑布流页面。
搜索与数据链路
- 明确图片来源和版权授权;抓取时遵守站点规则,保存来源、作者、许可证等元数据。
- 对图片做去重、缩略图生成、格式转换、质量检测和内容审核。
- 建立文本标签、OCR、视觉向量、颜色、尺寸等索引,支持关键词检索、以图搜图、筛选、排序和安全搜索。
- 搜索服务要有纠错、联想、分页游标、缓存、限流、降级和可观测性;排序可综合相关性、质量、时效和多样性。
存储与分发
原图放对象存储,缩略图和多规格图片通过 CDN 分发;URL 应包含尺寸、格式或内容哈希,便于长期缓存和失效。上传链路需要校验 MIME、文件头、尺寸和恶意内容,不能只信扩展名。
前端体验与性能
结果页使用虚拟列表或分段渲染,首屏加载合适尺寸的缩略图,配合 srcset/sizes、WebP/AVIF、懒加载、占位比例和请求取消,避免布局偏移及无效下载。无限滚动要保留 URL 查询条件、滚动位置,并提供键盘操作、替代文本和详情预览。
安全与合规
需要防盗链、签名 URL、访问频控、举报和下架流程,并处理隐私、人脸、未成年人及地域合规。还要先和产品确认索引规模、更新时效、搜索延迟、可用性目标、版权边界和成本预算,因为这些指标会直接决定架构。
133. 前端的页面截图怎么实现?
答案:
实现方式取决于要截取的是“某个 DOM 区域”“当前标签页/屏幕”,还是“服务端生成的完整页面”。
- DOM 转图片:html2canvas、dom-to-image 一类库会读取 DOM 和样式并重绘到 Canvas,再用 canvas.toBlob() 导出。它适合业务卡片、海报,但不是浏览器真实像素级截图,复杂 CSS、视频、iframe、伪元素和字体可能存在差异。
- 屏幕捕获:navigator.mediaDevices.getDisplayMedia() 可在用户授权后捕获屏幕、窗口或标签页的视频流,再把某一帧绘制到 Canvas。它必须由用户交互触发,页面不能静默截图。
- 浏览器自动化:服务端使用 Playwright/Puppeteer 打开页面,等待字体、图片和接口稳定后调用 screenshot,适合长页面、批量任务和一致性要求高的场景。
- 浏览器自带能力或扩展:适合用户主动操作;扩展能获得更高权限,但必须声明权限并经过安装。
DOM 转 Canvas 时尤其要处理跨域图片:资源若没有正确的 CORS 响应,Canvas 会被污染,之后无法导出。还应等待 document.fonts.ready 和图片解码完成,固定截图区域尺寸,并考虑设备像素比、滚动容器、超长画布的内存上限及敏感信息脱敏。
134. 下面代码的输出是什么?
js
const obj = {
fn1: () => console.log(this),
fn2: function() {console.log(this)}
}
obj.fn1();
obj.fn2();
const x = new obj.fn1();
const y = new obj.fn2();答案:
在浏览器的经典 script 中,执行结果依次是:
text
Window 对象
obj 对象
Uncaught TypeError: obj.fn1 is not a constructor随后程序中断,new obj.fn2() 不会执行,y 也不会被赋值。
原因是 fn1 是箭头函数,没有自己的 this,它捕获定义处的顶层 this;在浏览器经典脚本中顶层 this 是 window,所以 obj.fn1() 的点调用不能改变它。fn2 是普通函数,obj.fn2() 触发隐式绑定,因此其中的 this 是 obj。
箭头函数没有 [[Construct]] 内部方法,也没有可供 new 使用的构造能力,所以 new obj.fn1() 直接抛出 TypeError。若单独执行 new obj.fn2(),其 this 会指向新实例,console.log 会输出该实例。
顶层 this 与运行环境有关:ES Module 顶层是 undefined,Node.js CommonJS 顶层通常是 module.exports。因此第一行的具体值不能脱离执行环境判断;后面两点不变。
135. 浏览器有哪几种缓存,各种缓存的优先级是什么样的?
答案:
浏览器中的“缓存”不是一个统一层级,先要区分页面缓存、请求缓存和业务存储。
- BFCache:保存整个页面及其 JS 堆和 DOM 状态,前进/后退命中时可直接恢复页面。
- Service Worker / Cache Storage:Service Worker 可拦截其作用域内的 fetch,由代码决定缓存优先、网络优先等策略;Cache Storage 本身不会自动参与请求。
- HTTP 缓存:浏览器可能把响应放在内存或磁盘中。Cache-Control: max-age、s-maxage、immutable 等控制新鲜度;未过期时直接复用,属于强缓存。过期后可带 If-None-Match/If-Modified-Since 请求,服务器返回 304 时复用缓存,属于协商缓存。ETag 一般比 Last-Modified 更精确,Cache-Control 通常优先于 Expires。
- 业务数据存储:localStorage、sessionStorage、IndexedDB 等由应用显式读写,不是 HTTP 缓存,也不存在“其他缓存未命中后自动使用”的优先级。
可概括一次导航或资源请求:若是历史导航且 BFCache 可用,先恢复页面;正常 fetch 若受 Service Worker 控制,先进入其 fetch 处理逻辑;没有拦截或处理器回退到网络流程时,浏览器再依据 HTTP 缓存的新鲜度使用内存/磁盘缓存、发条件请求或完整请求网络。
内存缓存与磁盘缓存的具体选择、预加载缓存等属于浏览器实现细节,不能依赖一个跨浏览器绝对固定的顺序。
136. Web Worker 是什么?
答案:
Web Worker 是浏览器提供的后台线程执行环境,用于把 CPU 密集型或可并行任务移出主线程,减少页面交互和渲染卡顿。
Worker 有独立的全局作用域和事件循环,不能直接访问 DOM、window 或主线程中的普通变量。主线程与 Worker 通常通过 postMessage/message 传递数据:普通对象使用结构化克隆;ArrayBuffer 等可转移对象可以转移所有权,避免大数据复制。Worker 内可以使用 fetch、定时器、IndexedDB 等允许的 Web API。
常见类型包括:
- Dedicated Worker:只服务于创建它的页面或脚本。
- Shared Worker:同源多个页面可共享。
- Service Worker:面向网络代理、离线缓存和后台事件,不是通用长时间计算线程。
适合图片处理、压缩、解析大文件、搜索计算等任务。边界是线程创建和消息传输本身有成本,共享状态需要额外设计,Worker 也可能被浏览器节流或终止;短小任务放入 Worker 反而可能更慢。它不会让算法自动并行,任务拆分、错误处理、终止和资源释放仍需业务代码负责。
137. 说说你对 webpack5 模块联邦的了解?
答案:
Webpack 5 的 Module Federation 允许多个独立构建、独立部署的应用在运行时互相加载模块。提供模块的一方称为 remote,通过 exposes 暴露组件或业务模块;消费方称为 host,通过 remotes 获取远程容器并动态加载。一个应用可以同时是 host 和 remote。
shared 用于声明 React、Vue 或设计系统等公共依赖。Webpack 会在运行时协商共享作用域和版本;singleton 可要求只保留一个实例,requiredVersion 用于约束兼容版本。它减少重复打包只是可能的结果,并非必然:版本不兼容、加载策略或配置不当仍可能产生多份依赖。
它常用于微前端、跨团队组件共享和渐进式拆分。优势是发布边界独立、无需在构建期把所有模块合并;代价是把一部分依赖和版本问题推迟到了运行时。落地时要重点处理:
- 远程入口不可用时的超时、重试、降级和监控。
- host/remote 的接口契约、类型发布和兼容策略。
- shared 依赖的版本、单例和初始化顺序。
- 远程代码的信任边界、CSP、供应链安全和缓存失效。
- CSS 隔离、路由、状态共享及重复请求。
因此 Module Federation 是运行时代码组合机制,不等于完整的微前端方案,也不应把频繁变化且无兼容承诺的内部模块直接暴露给所有应用。
138. Promise 的 finally 怎么实现的?
答案:
finally 的关键语义是:无论原 Promise 成功还是失败都执行回调;回调没有状态参数;回调完成后默认透传原值或原错误;若回调抛错或返回拒绝的 Promise,则用这个新错误覆盖原结果。
一个接近原生语义的简化实现如下:
js
Object.defineProperty(Promise.prototype, 'myFinally', {
configurable: true,
writable: true,
value: function (onFinally) {
const C = this.constructor;
if (typeof onFinally !== 'function') {
return this.then(onFinally, onFinally);
}
return this.then(
value =>
C.resolve(onFinally()).then(() => value),
reason =>
C.resolve(onFinally()).then(() => {
throw reason;
})
);
}
});C.resolve(...) 会吸收回调返回的 Promise 或 thenable,因此链会等待清理工作结束。例如原 Promise 成功,但 onFinally 抛错时,最终结果应是拒绝,而不是继续返回原成功值。
该实现自身只增加 O(1) 的同步时间和空间,实际完成时间取决于 onFinally 返回的异步任务。它适合作为原理展示;完整规范实现还要处理 Promise 子类的 SpeciesConstructor、内部品牌检查等细节,不建议在生产环境覆盖原生 Promise.prototype.finally。
139. Promise then 第二个参数和catch的区别是什么?
答案:
catch(onRejected) 在语义上等价于 then(undefined, onRejected),二者都返回一个新的 Promise。真正的区别来自它们在链中的位置。
js
promise.then(onFulfilled, onRejected);这里 onFulfilled 和 onRejected 是同一个 then 的两个分支。onRejected 能处理 promise 本身的拒绝,但不能处理同级 onFulfilled 内部抛出的异常,因为执行 onFulfilled 时,这次 then 返回的新 Promise 才刚产生拒绝。
js
promise
.then(onFulfilled)
.catch(onRejected);这里 catch 挂在 then 返回的新 Promise 上,所以既能处理原 promise 的拒绝,也能处理 onFulfilled 抛出的异常或其返回的拒绝 Promise。因而通常更推荐链尾 catch,错误传播范围更完整。
两种拒绝处理器如果正常 return,都会把后续链恢复为 fulfilled;若再次 throw 或返回拒绝 Promise,错误会继续向后传播。catch 自身抛出的错误仍需要后续的 catch 处理。
140. 下面代码的输出是什么?
js
var name = '123';
var obj = {
name: '456',
print: function() {
function a() {
console.log(this.name);
}
a();
}
}
obj.print();答案:
在浏览器的非严格经典脚本中输出:
text
123obj.print() 中的 this 是 obj,但 a 是以 a() 的形式独立调用的普通函数,它不会继承 print 的 this。非严格模式下,a 内的 this 被替换为全局对象 window;顶层 var name = '123' 会创建 window.name,因此输出 123。
边界需要说清楚:
- 若 a 处于严格模式,this 是 undefined,访问 this.name 会抛出 TypeError。
- ES Module 默认严格,也会抛错。
- Node.js CommonJS 中顶层 var 不会成为 global 的属性,结果也不能按浏览器 window 推断。
- 若希望继承外层 this,可把 a 改成箭头函数,或显式调用 a.call(this)。
141. 下面代码的输出是什么?
js
console.log(typeof typeof typeof null);
console.log(typeof console.log(1));答案:
完整输出顺序是:
text
string
1
undefined第一句从内向外计算:typeof null 得到字符串 'object';对任意字符串再做 typeof 都得到 'string',所以无论嵌套多少层,外层结果都是 'string'。
第二句会先求 typeof 的操作数,因此先执行 console.log(1),输出 1。console.log 的返回值是 undefined,随后 typeof undefined 得到字符串 'undefined',外层 console.log 再输出 undefined。
142. 怎么使用 Math.max、Math.min 获取数组中的最值?
答案:
Math.max 和 Math.min 接收的是一组参数,不直接接收数组。常用扩展运算符展开:
js
const numbers = [3, 5, 1, 8, 2];
const max = Math.max(...numbers); // 8
const min = Math.min(...numbers); // 1也可以使用 apply:
js
const max = Math.max.apply(null, numbers);
const min = Math.min.apply(null, numbers);两种方式的时间复杂度都是 O(n),但超大数组展开为函数参数可能超过引擎的参数数量上限,此时应遍历或 reduce:
js
function minMax(numbers) {
if (numbers.length === 0) {
throw new RangeError('数组不能为空');
}
let min = numbers[0];
let max = numbers[0];
for (let i = 1; i < numbers.length; i++) {
if (numbers[i] < min) min = numbers[i];
if (numbers[i] > max) max = numbers[i];
}
return { min, max };
}空数组调用 Math.max(...[]) 得到 -Infinity,Math.min(...[]) 得到 Infinity;数组中有 NaN 时结果会是 NaN,业务代码通常应提前校验。
143. 说说下面代码执行后的输出是什么?
js
var b = 10;
(function b(){
b = 20;
console.log(b);
})();答案:
在普通非严格脚本中,console.log(b) 输出的是函数 b 本身,不是 20;外层变量 b 仍然是 10。控制台可能显示为 ƒ b() 或 [Function: b]。
text
[Function: b](function b(){ ... }) 是命名函数表达式。函数名 b 会在函数自身的内部作用域创建一个只读绑定,用于递归和调试,并遮蔽外层的 var b。执行 b = 20 时找到的是这个内部绑定:非严格模式下赋值静默失败,所以紧接着读取 b 仍得到当前函数。
若代码在严格模式或 ES Module 中执行,给该只读绑定赋值会抛出 TypeError,console.log 不会执行。无论哪种情况,都不会把外层 b 改为 20。
144. 怎么实现虚拟列表?
答案:
虚拟列表的核心是让滚动容器保留完整列表的滚动高度,但 DOM 中只存在可视区附近的一小段元素。
固定高度列表中,设单项高度为 h、容器高度为 H、滚动距离为 scrollTop:
- start = floor(scrollTop / h)
- visibleCount = ceil(H / h)
- end = start + visibleCount
- 实际渲染区间再向前后增加 overscan,减少快速滚动时的白屏。
- 外层占位元素高度设为 itemCount × h;可见项容器通过 translateY(start × h) 移到正确位置。
滚动时用 requestAnimationFrame 合并高频更新,并保持稳定 key。图片和异步内容要预留尺寸,避免测量结果反复变化。
变高列表需要记录每项实测高度,维护前缀偏移量;根据 scrollTop 二分查找起始项,元素渲染后用 ResizeObserver 修正高度和后续偏移。高度变化时还要做滚动锚定,避免用户看到内容跳动。
还需处理窗口缩放、滚动到指定索引、列表增删、焦点与键盘导航、可访问性、吸顶项以及服务端渲染。固定高度定位为 O(1);变高列表配合前缀索引通常定位为 O(log n),DOM 数量约为 O(visibleCount + overscan)。
145. 如果要设计一个转盘组件,你会考虑哪些方面?有哪些是需要和业务方确认的技术细节?另外,如何从前端的角度进行防刷?
答案:
转盘组件应把“抽奖结果”和“播放动画”分开:结果必须由服务端决定并落库,前端只根据返回结果把转盘停到对应位置,不能在前端计算中奖概率或信任前端上报的奖品。
需要设计的组件能力包括奖项数量和角度映射、图片预加载、旋转圈数与缓动、重复点击锁、接口超时与重试、低性能降级、减少动态效果偏好、适配尺寸、音效开关以及结果弹窗。动画应先进入可持续旋转状态,服务端结果返回后再计算终止角度,避免接口延迟破坏体验。
需要和业务方确认:
- 活动起止时间、参与资格、每日/总次数、概率规则和保底规则。
- 奖品库存扣减时机、并发超卖策略、未中奖项、奖品发放及补偿流程。
- 点击到出结果的延迟目标、动画是否必须播放完、失败后次数是否返还。
- 登录要求、地域限制、风控等级、埋点口径和无障碍要求。
- 接口是否支持幂等键、活动版本号、服务端时间和可验证的结果编号。
前端只能提高攻击成本,不能独立完成防刷。可做按钮锁、请求中的状态机、一次性 nonce、幂等 requestId、验证码接入、设备风险信息采集和异常节奏上报,但这些值都可能被伪造。真正的防刷必须在服务端实施:鉴权、签名与时效校验、原子扣次数和库存、用户/IP/设备多维限流、幂等、风控模型、审计和告警。HTTPS 保护传输,前端混淆或所谓 JS 加密不能替代服务端校验。
146. async/await、generator、promise 这三者的关联和区别是什么?
答案:
三者都能参与异步流程控制,但抽象层次不同。
- Promise 表示一个未来结果,状态只能从 pending 变为 fulfilled 或 rejected。then/catch/finally 负责组合和传播结果,all、race、allSettled 等负责并发组合。Promise 构造器的 executor 在创建时立即执行,回调则进入微任务。
- Generator 是可暂停、可恢复的同步状态机。调用生成器函数只得到迭代器;每次 next/throw/return 才推进到下一个 yield。yield 出什么由生成器决定,它本身不会等待 Promise,也不会自动继续执行,需要运行器驱动。
- async/await 建立在 Promise 语义之上。async 函数调用后一定返回 Promise;await 会把值按 Promise/thenable 处理,暂停当前 async 函数,并在结果确定后通过微任务恢复。它不阻塞线程,其他任务仍可运行。
Generator 加一个自动运行器可以模拟 async/await,许多转译器也采用这种实现,所以常把 async/await 称为 Generator 加 Promise 的语法糖;但语言规范中的 async 函数是独立语义,不能简单等同。Generator 可以双向传值并由外部控制推进,适合惰性序列和状态机;async/await 更适合顺序异步流程和 try/catch。
还要注意 await 连写会串行执行。互不依赖的任务应先同时创建 Promise,再用 Promise.all 等待,避免无意中降低并发度。
147. mouseEnter 和 mouseOver 有什么区别?
答案:
两者都表示指针进入元素,但传播和触发范围不同:
- mouseover 会冒泡。指针进入目标或其后代时都会产生事件,因此从父元素移动到子元素、再移动到另一个子元素,父元素上的委托监听器可能多次收到 mouseover。
- mouseenter 不冒泡。它关注指针是否从元素边界外进入元素整体;在同一元素的后代之间移动,不会再次触发该父元素的 mouseenter。
两者的 relatedTarget 都表示指针来自哪个元素,可用于判断是否真正跨越了目标边界。需要事件委托时通常使用 mouseover,并结合 target、closest 和 relatedTarget 过滤;只关心进入某个组件整体时使用 mouseenter 更直接。
对应的离开事件分别是 mouseout 和 mouseleave。现代代码若还要统一鼠标、触控笔等指针设备,可考虑 pointerover/pointerenter。
148. 写出一个函数trans,将数字转换成汉语的输出,输入为不超过10000亿的数字。
js
trans(123456) —— 十二万三千四百五十六
trans(100010001)—— 一亿零一万零一答案:
可以从右向左每四位分组,组内使用个、十、百、千,组间使用万、亿、万亿;跨组缺位时只补一个“零”。
js
function trans(input) {
const MAX = 1_000_000_000_000n; // 10000 亿
const digits = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
const smallUnits = ['', '十', '百', '千'];
const groupUnits = ['', '万', '亿', '万亿'];
let text;
if (typeof input === 'number') {
if (!Number.isSafeInteger(input) || input < 0) {
throw new TypeError('只接受非负安全整数');
}
text = String(input);
} else if (typeof input === 'bigint') {
if (input < 0n) throw new TypeError('只接受非负整数');
text = String(input);
} else if (typeof input === 'string' && /^\d+$/.test(input.trim())) {
text = input.trim();
} else {
throw new TypeError('输入必须是非负整数或整数字符串');
}
text = text.replace(/^0+(?=\d)/, '');
if (BigInt(text) > MAX) {
throw new RangeError('输入不能超过 10000 亿');
}
if (text === '0') return '零';
function readGroup(value) {
const group = String(value).padStart(4, '0');
let result = '';
let pendingZero = false;
for (let i = 0; i < 4; i++) {
const digit = Number(group[i]);
if (digit === 0) {
if (result) pendingZero = true;
continue;
}
if (pendingZero) result += '零';
result += digits[digit] + smallUnits[3 - i];
pendingZero = false;
}
return result;
}
const groups = [];
for (let end = text.length; end > 0; end -= 4) {
groups.push(Number(text.slice(Math.max(0, end - 4), end)));
}
let result = '';
let skippedZeroGroup = false;
for (let i = groups.length - 1; i >= 0; i--) {
const group = groups[i];
if (group === 0) {
if (result) skippedZeroGroup = true;
continue;
}
if (result && (skippedZeroGroup || group < 1000)) {
result += '零';
}
result += readGroup(group) + groupUnits[i];
skippedZeroGroup = false;
}
return result.replace(/^一十/, '十');
}
console.log(trans(123456)); // 十二万三千四百五十六
console.log(trans(100010001)); // 一亿零一万零一设输入有 d 位,时间复杂度为 O(d),额外空间复杂度为 O(d)。实现限定非负整数;字符串输入可避免更大范围数字被科学计数法表示,不过本题上限仍处于安全整数范围内。
149. 以下等式是成立的吗:1_000_000 === 1000000 ?
答案:
成立,表达式结果为 true。
js
1_000_000 === 1000000 // true下划线是 ECMAScript 的数字分隔符,只用于提高源码可读性,解析后不参与数值本身,因此两边都是同一个 Number 值。它也可用于二进制、八进制、十六进制和 BigInt 字面量,例如 0b1010_0001、0xff_ff 和 1_000n。
数字分隔符只能出现在数字字面量允许的位置,不能连续使用,也不能放在数字开头、结尾或紧邻小数点。字符串中的下划线不会被 Number 自动忽略,例如 Number('1_000') 得到 NaN。
150. 说说对 requestIdleCallback 的理解
答案:
requestIdleCallback(callback, options) 用于把低优先级、可切片的工作安排到浏览器主线程相对空闲的时段。回调收到 IdleDeadline:
- timeRemaining() 返回本次空闲时段预计还剩多少毫秒。
- didTimeout 表示是否因为 options.timeout 到期而被强制调度。
典型做法是在 timeRemaining() 仍有余量时处理少量队列项,未完成则再次注册,而不是在一次回调里做完全部工作:
js
function runIdle(deadline) {
while (jobs.length && deadline.timeRemaining() > 1) {
jobs.shift()();
}
if (jobs.length) {
requestIdleCallback(runIdle, { timeout: 1000 });
}
}
requestIdleCallback(runIdle, { timeout: 1000 });它适合非关键统计、预计算和分批处理,不适合必须按时完成的业务逻辑、动画或直接影响交互的更新。空闲回调没有固定执行时间,后台页面可能被明显节流;timeout 虽能防止无限等待,但到期执行时也可能占用繁忙主线程。视觉更新应优先使用 requestAnimationFrame,重计算还可放进 Web Worker,并为不支持该 API 的环境提供可取消的降级调度。
151. 说说你对 ToPrimitive 的理解
答案:
ToPrimitive 是 ECMAScript 的内部抽象操作,用于把对象转换为原始值。原始值包括 undefined、null、boolean、number、bigint、string 和 symbol;若输入已经是原始值就直接返回。
对象转换时会带一个 hint:'string'、'number' 或 'default'。
- 若对象定义了 Symbol.toPrimitive,就先调用它并传入 hint;返回值必须是原始值,否则抛出 TypeError。
- 否则执行普通转换。hint 为 string 时通常先尝试 toString(),再尝试 valueOf();hint 为 number 时顺序相反。
- default 通常按 number 处理,但 Date 等内建对象有特殊行为。
js
const value = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') return 10;
if (hint === 'string') return 'ten';
return 'default';
}
};
Number(value); // 10
String(value); // 'ten'
value + 1; // 'default1'加法、关系比较、宽松相等以及显式 String/Number 转换等操作都可能触发 ToPrimitive。它只负责得到原始值,之后运算符还可能继续执行 ToString 或 ToNumber,所以对象参与 + 时尤其容易因 default hint 得到字符串拼接结果。
152. 页面加载的过程中,JS 文件是不是一定会阻塞 DOM 和 CSSOM 的构建?
答案:
不一定,取决于脚本类型、属性和执行时机。
- 普通
<script src="...">被 HTML 解析器遇到时,会暂停 DOM 解析,等待脚本下载并执行完再继续。 - defer 脚本并行下载,等 HTML 解析完成后按文档顺序执行,通常不会阻塞 DOM 的构建。
- async 脚本并行下载,但下载完成就尽快执行;若此时 HTML 尚未解析完,执行期间仍会暂停解析,而且多个 async 脚本不保证顺序。
- type="module" 的脚本默认具有类似 defer 的行为;动态导入也不会以传统方式阻塞解析。
- Worker 中的脚本不占用主线程解析 DOM,但也不能直接访问 DOM。
CSS 关系更细:外部样式表通常与 HTML 并行下载和解析;但位于脚本之前的阻塞样式表可能让该脚本延迟执行,因为脚本可能读取计算样式。若 HTML 解析器又正等待这个脚本,就会表现为 CSS 间接阻塞后续 DOM 构建。
因此“JS 一定阻塞 DOM 和 CSSOM”不准确。更准确的说法是:同步经典脚本会阻塞 HTML 解析;脚本执行占用主线程,并可能等待前置 CSSOM。DOM 修改造成的是后续样式失效、布局和绘制工作,不等同于脚本必然阻塞最初的 CSSOM 构建。
153. 说说你对轮询的理解
答案:
轮询是客户端周期性请求服务端,以接近实时地发现状态变化。普通短轮询每次立即返回,即使数据没变化也会产生请求;长轮询则由服务端挂起请求,直到有新数据或超时后返回,客户端收到响应后立即发起下一次请求。
前端实现普通轮询时,更推荐“请求结束后再 setTimeout”,而不是固定 setInterval。后者在网络变慢时可能让多次请求重叠并产生乱序结果。
js
function startPolling(request, interval = 5000) {
let stopped = false;
let timer;
async function poll() {
try {
await request();
} finally {
if (!stopped) timer = setTimeout(poll, interval);
}
}
poll();
return () => {
stopped = true;
clearTimeout(timer);
};
}生产实现还应有 AbortController、页面隐藏或卸载时停止、失败指数退避和随机抖动、最大重试次数、请求超时、响应版本号及乱序保护。ETag/If-None-Match 可减少无变化时的响应体。
轮询实现简单、兼容性好,但实时性和资源消耗互相制约。服务端单向高频推送可考虑 SSE,双向低延迟通信可考虑 WebSocket;具体选择还要看代理兼容、连接规模、断线重连和消息可靠性要求。
154. js函数有哪几种声明方式?有什么区别?
答案:
JavaScript 常见的函数定义形式有以下几类:
- 函数声明:
function fn() {}。声明和函数体会在作用域初始化阶段建立,因此可在源码位置之前调用。块级函数声明在现代规范中按块作用域理解,但旧环境存在兼容差异。 - 函数表达式:
const fn = function () {}或命名表达式const fn = function inner() {}。函数在表达式求值时创建;变量是否提升取决于 var/let/const。const 和 let 在初始化前处于暂时性死区,var 只提升声明并初始为 undefined。 - 箭头函数:
const fn = (x) => x * 2。语法简洁,词法捕获 this、arguments、super 和 new.target;没有 prototype,不能用 new,也不适合需要动态 this 的方法。 - 对象或类的方法:
{ fn() {} }、class A { fn() {} }。方法支持 super;类方法默认严格,且不能直接作为构造函数 new。 - Function 构造器:
new Function('a', 'return a * 2')。代码在全局作用域编译,不能捕获当前词法环境,并有性能和注入风险,通常不使用。
async function、function*、async function* 是上述能力的异步或生成器变体。所谓 IIFE 是函数表达式创建后立即调用,不是新的声明类型。函数声明与表达式最重要的差异是创建时机和提升;普通函数与箭头函数最重要的差异是 this/arguments 及构造能力。
155. 说说你对“立即执行函数”的理解
答案:
立即执行函数(IIFE)是一个函数表达式在创建后立刻被调用:
js
;(function (config) {
const privateValue = config.value;
console.log(privateValue);
})({ value: 42 });外层括号把 function 解析为表达式,末尾括号完成调用。也可写成 (function () {}());用 !、+ 等一元运算符也能转成表达式,但可读性通常较差。前导分号用于防止该语句与上一行因自动分号插入规则意外连接。
IIFE 的主要用途是创建函数作用域、隐藏临时变量、注入依赖并完成一次性初始化。在 let/const、块级作用域和 ES Module 普及前,它常用于模块封装;现在仍适合需要立即执行且不希望泄漏局部变量的代码,也可写成 async IIFE 来使用 await。
IIFE 返回什么取决于函数的 return。若内部函数或对象被返回,闭包仍可继续访问其局部变量,所以“执行完就一定释放全部变量”并不成立。IIFE 只提供作用域隔离,不是安全边界。
156. 如何把一个对象变成可迭代对象?
答案:
对象只要实现可迭代协议即可:它必须提供一个 Symbol.iterator 方法,该方法返回迭代器;迭代器的 next() 每次返回 { value, done }。
使用 Generator 实现最简洁:
js
const range = {
start: 1,
end: 4,
*[Symbol.iterator]() {
for (let value = this.start; value < this.end; value++) {
yield value;
}
}
};
console.log([...range]); // [1, 2, 3]
for (const value of range) {
console.log(value);
}Generator 对象同时实现迭代器协议和可迭代协议。上面的 Symbol.iterator 每次调用都会创建一个新 Generator,因此 range 可以重复遍历,两个并发遍历也不会共享游标。
若手写 next(),遍历结束后应持续返回 { value: undefined, done: true },并把 index 放在 Symbol.iterator 内部,避免所有遍历共享状态。迭代是惰性的:生成 n 个值的总时间复杂度为 O(n),除结果消费者自身占用外,迭代器额外空间可为 O(1)。展开运算符、for...of、Array.from、yield* 等都会消费可迭代对象;普通对象默认不可迭代。
157. 编程实现温度转换,已知温度转换的关系式是:华氏度=32+摄氏度×1.8,现在要求输入摄氏度,输出对应的华氏度,小数保留两位
答案:
js
function celsiusToFahrenheit(celsius) {
if (typeof celsius !== 'number' || !Number.isFinite(celsius)) {
throw new TypeError('摄氏温度必须是有限数值');
}
const fahrenheit = 32 + celsius * 1.8;
if (!Number.isFinite(fahrenheit)) {
throw new RangeError('计算结果超出可表示范围');
}
return fahrenheit.toFixed(2);
}
console.log(celsiusToFahrenheit(0)); // '32.00'
console.log(celsiusToFahrenheit(37)); // '98.60'
console.log(celsiusToFahrenheit(-40)); // '-40.00'toFixed(2) 会四舍五入到两位小数并返回字符串,这正适合直接展示;若调用方必须得到 Number,可再用 Number(...) 转换,但末尾的 0 会丢失。由于 JavaScript 使用二进制浮点数,某些十进制边界的舍入可能存在浮点误差;金融级或严格十进制定点场景应改用十进制库。时间和额外空间复杂度均为 O(1)。
158. 版本号排序
有一组版本号如下['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']。
现在需要对其进行排序,排序的结果为 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']
答案:
应按每一段的数值逐段比较,不能直接比较整个字符串,否则 0.302.1 会被错误地排在 0.5.1 后面。下面的实现还避免把超长版本段转换为 Number,从而没有安全整数精度问题:
js
function compareNumericPart(a, b) {
const left = a.replace(/^0+(?=\d)/, '');
const right = b.replace(/^0+(?=\d)/, '');
if (left.length !== right.length) {
return left.length > right.length ? 1 : -1;
}
if (left === right) return 0;
return left > right ? 1 : -1;
}
function compareVersionDesc(a, b) {
const left = a.split('.');
const right = b.split('.');
const length = Math.max(left.length, right.length);
for (let i = 0; i < length; i++) {
const result = compareNumericPart(left[i] ?? '0', right[i] ?? '0');
if (result !== 0) return -result;
}
return 0;
}
function sortVersionsDesc(versions) {
const valid = /^\d+(?:\.\d+)*$/;
if (!versions.every(version => valid.test(version))) {
throw new TypeError('版本号只能包含非负整数段');
}
return [...versions].sort(compareVersionDesc);
}
const versions = [
'0.1.1',
'2.3.3',
'0.302.1',
'4.2',
'4.3.5',
'4.3.4.5'
];
console.log(sortVersionsDesc(versions));
// ['4.3.5', '4.3.4.5', '4.2', '2.3.3', '0.302.1', '0.1.1']题干给出的期望结果漏掉了输入中的 4.2;排序不应删除元素,正确结果应包含它。该比较器把缺失段视为 0,所以 1、1.0 和 1.0.0 相等,并依赖现代稳定排序保持它们原有次序。它只处理纯数字版本段,不包含 SemVer 的 prerelease、build metadata 规则。
设有 n 个版本、每个版本最多 k 段、每段最多 m 个字符,排序时间复杂度约为 O(n log n × k × m),复制结果的额外空间为 O(n)。
159. 给某个资源的链接,如 https://www.baidu.com/index.html ,请实现一个方法,获取该资源的后缀,如 html
答案:
应只检查 URL 的 pathname,避免查询参数或域名中的点被误认为扩展名:
js
function getFileExtension(input) {
if (typeof input !== 'string') return '';
let pathname;
try {
pathname = new URL(input, 'https://example.invalid').pathname;
} catch {
return '';
}
const encodedName = pathname.slice(pathname.lastIndexOf('/') + 1);
let fileName;
try {
fileName = decodeURIComponent(encodedName);
} catch {
fileName = encodedName;
}
const dot = fileName.lastIndexOf('.');
if (dot <= 0 || dot === fileName.length - 1) return '';
return fileName.slice(dot + 1).toLowerCase();
}
getFileExtension('https://www.baidu.com/index.html'); // 'html'
getFileExtension('/assets/archive.tar.gz?download=true'); // 'gz'
getFileExtension('/assets/.gitignore'); // ''实现支持绝对 URL 和相对 URL,会忽略 query 与 hash;对 .gitignore 这类只有开头点号的文件和以点结尾的路径返回空字符串。对于 tar.gz 返回最后一级后缀 gz;若业务需要复合后缀,应维护 tar.gz 等允许列表。时间复杂度为 O(L),L 为 URL 长度,额外空间复杂度为 O(L)。
160. 说说你对 Iterator, Generator 和 Async/Await 的理解
答案:
Iterator
Iterator 是按需产生序列值的对象,核心是 next() 方法,每次返回 { value, done }。Iterable 则是实现了 Symbol.iterator 的对象,该方法能返回 Iterator。for...of、展开运算符和 Array.from 消费的是 Iterable 协议。Iterator 通常是有状态且一次性的,而 Iterable 可以在每次遍历时创建新 Iterator。
Generator
Generator 函数用 function* 声明,调用时不会立即执行函数体,而是返回 Generator 对象。外部通过 next(value)、throw(error)、return(value) 推进或终止它,函数在 yield 处暂停,并保留局部变量和执行位置。Generator 同时是 Iterator 和 Iterable,因此很适合实现惰性序列、树遍历和可控状态机。
js
function* count() {
yield 1;
yield 2;
return 3;
}
const iterator = count();
iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: 2, done: false }
iterator.next(); // { value: 3, done: true }for...of 只消费 done 为 false 的值,所以不会输出最终 return 的 3。yield* 可把迭代委托给另一个可迭代对象。
Async/Await
async 函数每次调用都返回 Promise。await 会把表达式结果按 Promise/thenable 处理,暂停当前 async 函数;结果确定后在微任务中恢复。暂停的是该函数,不是 JavaScript 线程,所以页面和其他任务仍能继续运行。异常可用 try/catch 处理,return 值成为返回 Promise 的兑现值。
三者的联系是:Generator 能方便地生成 Iterator;Generator 若 yield Promise,再由一个运行器自动调用 next/throw,可以实现类似 async/await 的流程,转译器常采用这一思路。但原生 async/await 的规范语义直接建立在 Promise 上,不要求开发者手动推进。
同步惰性序列使用 Iterator/Generator;普通异步流程使用 Promise 与 async/await;既要异步又要逐项产出时可使用 AsyncIterator 和 async function*,并通过 for await...of 消费。
161. Map 和 WeakMap 有什么区别?
答案:
Map 和 WeakMap 都保存键值对,但语义不同:
Map的键可以是任意 JavaScript 值,键和值都被强引用;只要Map还存在,其中的对象键通常就不能被回收。它有size,支持keys、values、entries和for...of遍历,也支持clear。WeakMap的键必须是可被垃圾回收的值。现代规范允许对象和未注册的Symbol,旧环境通常只支持对象;值可以是任意类型。它对键是弱引用:当一个对象键在别处已不可达时,对应条目可被垃圾回收。- 因为垃圾回收时机不可预测,
WeakMap没有size、clear和遍历 API,不能用它观察某个键何时被回收;只提供set、get、has、delete等按键操作。
Map 适合需要枚举、统计或长期保存的映射;WeakMap 适合给对象附加元数据、缓存与对象生命周期绑定的数据,或实现不暴露在对象属性上的私有状态。
162. 如何判断某个字符串长度(要求支持表情)?
答案:
要先明确“长度”指什么。str.length 统计的是 UTF-16 码元,因此普通汉字通常算 1,😀 会算 2。Array.from(str).length 或 [...str].length 统计 Unicode 码点,能把单个非 BMP 表情算作 1,但家庭表情、肤色修饰符、国旗等仍可能由多个码点组成。
如果要统计用户看到的字符,也就是字素簇,应使用 Intl.Segmenter:
js
function visibleLength(str, locale = 'zh') {
const segmenter = new Intl.Segmenter(locale, { granularity: 'grapheme' });
return [...segmenter.segment(String(str))].length;
}
console.log(visibleLength('A😀👨👩👧👦')); // 3不支持 Intl.Segmenter 的旧环境可引入 Unicode 字素分割库;退化为 Array.from 只能保证按码点统计,不能完整支持组合表情。
163. 说说你对模块化方案的理解,比如 CommonJS、AMD、CMD、ES Module 分别是什么?
答案:
模块化的目的,是把实现封装成独立作用域,通过明确的导入、导出管理依赖,从而支持复用、测试和构建优化。
- CommonJS:主要用于 Node.js,使用
require和module.exports。依赖在运行时同步加载,模块首次执行后会被缓存;导出的是module.exports当前提供的值。它适合服务端文件系统环境,但静态分析和 tree shaking 能力较弱。 - AMD:面向早期浏览器异步加载,代表实现是 RequireJS。通过
define(['dep'], factory)前置声明依赖,加载完成后执行工厂函数,解决浏览器中同步加载脚本会阻塞的问题。 - CMD:代表实现是 SeaJS,同样用于浏览器异步加载,强调依赖就近,在工厂函数中用到时再
require,模块通常延迟到需要时执行。AMD、CMD 目前多见于历史项目。 - ES Module:语言标准,使用
import、export。依赖关系可在解析阶段确定,导入是只读的实时绑定,模块默认严格模式且每个模块只执行一次。浏览器和 Node.js 都原生支持,构建工具可据此做 tree shaking、代码分割。
CommonJS 与 ESM 的加载时机、导出语义和循环依赖表现不同,混用时还要遵循具体运行时或构建工具的互操作规则,不能简单认为 module.exports 与 export default 完全等价。
164. 一个滚动公告组件,如何在鼠标滑入时停止播放,在鼠标离开时继续等待滑入时的剩余等待时间后播放?
答案:
不要只在移入时清除 setInterval,否则移出后只能重新等待一个完整周期。更合适的是用单次 setTimeout,记录本次触发的截止时间;暂停时用截止时间减去当前时间得到剩余时间,恢复时按该剩余时间重新调度。
js
const notice = document.querySelector('.notice');
const interval = 3000;
let timer = null;
let deadline = 0;
let remaining = interval;
let paused = false;
function showNext() {
console.log('切换到下一条公告');
}
function schedule(delay) {
clearTimeout(timer);
remaining = delay;
deadline = performance.now() + delay;
timer = setTimeout(() => {
timer = null;
showNext();
remaining = interval;
if (!paused) schedule(interval);
}, delay);
}
notice.addEventListener('pointerenter', () => {
if (paused) return;
paused = true;
remaining = Math.max(0, deadline - performance.now());
clearTimeout(timer);
timer = null;
});
notice.addEventListener('pointerleave', () => {
if (!paused) return;
paused = false;
schedule(remaining);
});
schedule(interval);performance.now() 是单调时钟,不受系统时间调整影响。若暂停期间允许用户手动切换,应在手动切换后把 remaining 重置为完整周期,并明确鼠标仍停留时是否继续计时。
165. 如果使用 Math.random() 计算中奖概率会有什么问题吗?
答案:
有问题,尤其是涉及奖金或可被攻击的抽奖。Math.random() 不是密码学安全随机数,规范也不保证具体算法和随机质量;生成器状态可能被推测,而且放在前端的中奖逻辑可以被篡改、重放或直接绕过。把随机数乘区间后取整、取模时,错误的映射方式还可能引入概率偏差。
可靠做法是:
- 抽奖在服务端完成,校验资格并以事务方式写入结果,前端只展示结果。
- 使用密码学安全随机源,例如浏览器的
crypto.getRandomValues,Node.js 的crypto.randomInt;从随机整数映射到非 2 的幂区间时采用库提供的无偏接口或拒绝采样,避免简单取模偏差。 - 为高价值抽奖保存活动版本、参与人、随机过程输入和最终结果等审计信息,并防止同一资格重复消费。
Math.random() 仍适合动画抖动、随机展示等不涉及安全和金钱的场景。
166. 怎么预防用户快速连续点击,造成数据多次提交?
答案:
前端应在第一次提交开始时立即加锁并禁用按钮,请求结束后在 finally 中恢复;提交场景通常不应只靠防抖,因为防抖只是改变触发时机,不能覆盖刷新、重试、多标签页或绕过页面直接调用接口。
js
let submitting = false;
async function handleSubmit(event) {
if (submitting) return;
submitting = true;
const button = event.currentTarget;
button.disabled = true;
try {
await fetch('/api/orders', { method: 'POST' });
} finally {
submitting = false;
button.disabled = false;
}
}真正的数据安全要由服务端保证:给一次业务操作生成幂等键,请求重试时复用同一个键;服务端用唯一约束或幂等记录保证同一键只执行一次并返回同一结果。按钮禁用、加载状态、节流只能改善交互,不能替代服务端幂等。
167. 相比于npm和yarn,pnpm的优势是什么?
答案:
pnpm 的主要优势来自内容寻址存储和链接式 node_modules 结构:
- 相同版本、相同内容的包文件集中存放在全局 store,各项目通过硬链接、reflink 或复制到虚拟 store,再用符号链接组织依赖关系,通常比每个项目各存一份更省磁盘。
- 已下载内容可跨项目复用,缓存命中时安装速度快;锁文件和 store 也便于离线或接近离线安装。
- 项目顶层通常只暴露自己声明的直接依赖,间接依赖位于
node_modules/.pnpm对应的依赖图中,能更早暴露未声明就直接引用的“幽灵依赖”。 - 原生支持 workspace、过滤执行和递归命令,管理 monorepo 比较顺手;
pnpm-lock.yaml保证可重复解析依赖。
边界是:实际速度取决于网络、缓存和文件系统;某些不兼容符号链接或错误依赖幽灵包的旧工具需要配置 hoist。npm、Yarn 也持续改进,因此不能把任何基准结果当成所有项目中的绝对结论。
168. 说说你对 pnpm 的了解
答案:
pnpm 是兼容 npm registry 和 package.json 生态的 Node.js 包管理器,常用命令与 npm 类似,例如 pnpm install、pnpm add、pnpm run。
它的核心结构分三层:全局内容寻址 store 按文件内容保存包;项目的 node_modules/.pnpm 是虚拟 store,记录具体版本和依赖关系;顶层 node_modules 再链接项目直接声明的依赖。这样既复用文件,又保留接近真实依赖图的隔离结构。不同 peer dependency 组合会形成不同的依赖实例,避免把不兼容的 peer 环境错误混在一起。
工程上通常提交 pnpm-lock.yaml,在 CI 使用冻结锁文件安装;monorepo 用 pnpm-workspace.yaml 声明工作区,通过 workspace: 协议引用内部包,并可用 --filter 只对部分包执行命令。
需要注意,store 是内容缓存,不等于所有项目共享一个可变包目录;项目删除不会破坏其他项目。遇到依赖只能在扁平 node_modules 下工作的旧工具时,可按需配置 hoist,但过度提升会削弱严格依赖检查的价值。
169. 如何判断页面是通过PC端还是移动端访问?
答案:
如果目的是适配布局,不应先判断“PC 还是移动端”,而应直接用响应式媒体查询和能力检测:宽度用 @media,输入能力可用 (pointer: coarse)、(hover: none)。平板、触屏笔记本、桌面模式手机使二分设备类型并不可靠。
业务确实需要粗略分类时,可优先读取 User-Agent Client Hints,再退化到 UA:
js
function isLikelyMobile() {
const mobile = navigator.userAgentData?.mobile;
if (typeof mobile === 'boolean') return mobile;
return /Android|iPhone|iPod|Mobile/i.test(navigator.userAgent);
}navigator.userAgentData 并非所有浏览器都支持,UA 可以被伪装,iPad 桌面模式也可能像桌面设备;屏幕宽度和触摸事件同样只能说明能力,不能证明设备类别。因此该结果只能用于非关键体验分流,不能用于权限、风控或安全判断。服务端渲染时还要避免直接访问 navigator。
170. 导致页面加载白屏时间长的原因有哪些,怎么进行优化?
答案:
白屏长通常发生在四段:网络与服务端响应慢;HTML 被重定向、DNS/TLS 或接口阻塞;关键 CSS、同步脚本阻塞解析和渲染;前端包过大、执行耗时、运行异常或 CSR 必须等接口后才有内容。字体、首屏大图、错误的资源路径、缓存失效和 hydration 失败也常见。
优化应先用瀑布图、Performance 面板和 RUM 指标定位,重点看 TTFB、FCP、LCP、长任务和 JS 异常,再对症处理:
- CDN、HTTP 缓存、压缩和合适的连接复用降低传输与 TTFB;必要时优化服务端查询和缓存。
- 缩小 JS/CSS,移除无用依赖,按路由或功能拆包;非关键脚本用
defer、模块脚本或延迟加载,避免长时间主线程任务。 - 内联少量首屏关键 CSS,延后非关键样式;谨慎预加载真正关键的字体、脚本和首图,避免抢占带宽。
- 图片使用正确尺寸、现代格式和首屏优先级,非首屏懒加载;字体做子集化并设置合理的
font-display。 - 对内容型页面采用 SSR/SSG 或流式渲染,让基础内容不必等全部 JS;CSR 至少提供稳定的壳、加载态和错误兜底。
- 建立资源加载失败、JS 异常和性能指标监控,防止某个接口或 chunk 失败导致永久白屏。
拆包和预加载都不是越多越好:碎片过多会增加调度开销,错误预加载会挤占真正关键资源。
171. 怎么使用 js 动态生成海报?
比如将当前页面生成一张海报,要求携带当前用户的登录信息。
答案:
常见方案有三种:按固定版式直接用 Canvas 绘制;把现有 DOM 通过 html2canvas 一类库转成 Canvas;在服务端用 Playwright/Puppeteer 打开专用海报页并截图。固定模板优先 Canvas,结果最可控;复杂页面或要求跨端一致时,服务端截图通常更稳定。
以 DOM 转图片为例:
js
async function createPoster(node) {
await document.fonts.ready;
const canvas = await html2canvas(node, {
scale: Math.min(window.devicePixelRatio || 1, 2),
useCORS: true,
backgroundColor: '#fff'
});
return new Promise((resolve, reject) => {
canvas.toBlob(
blob => blob ? resolve(blob) : reject(new Error('海报导出失败')),
'image/png'
);
});
}绘制前要等字体和图片加载完成;跨域图片必须由资源服务器返回允许的 CORS 响应,否则 Canvas 会被污染,无法调用 toBlob 或 toDataURL。还要限制输出像素和 DPR,避免移动端内存溢出。
“携带当前用户登录信息”不能理解为把 Cookie、Access Token 或用户密码写进海报或二维码。应先用当前会话向服务端申请一个与业务资源绑定、短时有效且权限受限的分享标识或签名链接,二维码只放这个分享链接;打开后由服务端再次鉴权或展示允许公开的内容。
172. 怎么把十进制的 0.2 转换成二进制?
答案:
十进制小数转二进制可反复乘 2,每次取整数部分:0.2 × 2 = 0.4 取 0,0.4 × 2 = 0.8 取 0,0.8 × 2 = 1.6 取 1,0.6 × 2 = 1.2 取 1,余数又回到 0.2。因此:
text
0.2₁₀ = 0.001100110011...₂即 0011 无限循环,不能用有限个二进制位精确表示。JavaScript 的 Number 使用 IEEE 754 双精度浮点数,只保存最接近它的有限近似值,所以:
js
console.log((0.2).toString(2));
// 0.001100110011001100110011001100110011001100110011001101这里输出的是当前 Number 近似值的二进制表示,不是数学上无限循环的小数本身,这也是 0.1 + 0.2 !== 0.3 的根源。
173. map和 filter 有什么区别?
答案:
map 用于逐项转换,filter 用于按条件筛选。两者都不修改原数组结构,并返回新数组,但回调中的副作用仍可能修改原对象或原数组。
arr.map(fn)把每个已存在的元素替换为fn的返回值,结果通常与原数组长度相同;稀疏数组的空槽会保留。arr.filter(predicate)只保留回调结果为真值的元素,结果长度不大于原数组,并会把结果压成连续索引。它不会自动复制元素,对象元素仍与原数组共享引用。
js
const values = [1, 2, 3, 4];
values.map(x => x * 2); // [2, 4, 6, 8]
values.filter(x => x % 2); // [1, 3]两者都不会等待 async 回调。异步映射通常写成 await Promise.all(arr.map(asyncFn));异步过滤则应先计算条件结果,再同步过滤。
174. map 和 forEach 有什么区别?
答案:
核心区别是返回值和用途:
map收集每次回调的返回值,创建并返回一个同长度的新数组,适合纯转换和后续链式处理。forEach只遍历并执行副作用,固定返回undefined,适合日志、写入外部状态等不需要结果数组的操作。
js
const source = [1, 2, 3];
const doubled = source.map(x => x * 2); // [2, 4, 6]
const result = source.forEach(x => console.log(x)); // undefinedforEach 不会自动修改原数组,map 也不是深拷贝;是否修改取决于回调做了什么。两者都会跳过稀疏数组的空槽,也都不能用普通 break 提前退出,需要提前退出时用 for...of、some 或 find。它们也不会等待 async 回调:并发映射应使用 Promise.all(arr.map(asyncFn)),严格串行则使用 for...of 配合 await。
175. 实现以下转换,合并连续的数字
[1,2,3,4,6,7,9,13,15]=>['1->4','6->7','9','13','15']
答案:
在输入为严格递增、无重复的整数数组时,可以一次扫描维护当前连续区间:
js
function mergeConsecutive(numbers) {
if (!Array.isArray(numbers)) {
throw new TypeError('参数必须是数组');
}
if (numbers.length === 0) return [];
for (let i = 0; i < numbers.length; i++) {
if (!Number.isInteger(numbers[i]) ||
(i > 0 && numbers[i] <= numbers[i - 1])) {
throw new TypeError('数组必须由严格递增的整数组成');
}
}
const result = [];
let start = numbers[0];
let end = start;
const append = () => {
result.push(start === end ? String(start) : `${start}->${end}`);
};
for (let i = 1; i < numbers.length; i++) {
const value = numbers[i];
if (value === end + 1) {
end = value;
} else {
append();
start = end = value;
}
}
append();
return result;
}
console.log(mergeConsecutive([1, 2, 3, 4, 6, 7, 9, 13, 15]));
// ['1->4', '6->7', '9', '13', '15']时间复杂度为 O(n),除返回结果外额外空间为 O(1)。若允许乱序或重复值,应先明确是保留原顺序还是排序去重;排序方案的时间复杂度会变为 O(n log n)。
176. JQuery中的$(document).ready与window.onload有什么区别?
答案:
$(document).ready(handler) 在 DOM 已解析、可以安全查询和绑定元素时执行,语义接近原生 DOMContentLoaded;它不等待图片、视频等外部资源全部加载。简写是 $(handler)。如果注册时 DOM 已经 ready,jQuery 也会安排处理函数执行。
window.onload 在文档及其依赖资源完成加载后触发,因此通常更晚,适合必须依赖图片固有尺寸等场景。
还有两个边界:多个 jQuery ready 处理函数都会执行;直接多次赋值 window.onload = fn 会覆盖前一次,但原生 window.addEventListener('load', fn) 可以注册多个监听器。旧版 jQuery 的 $(window).load(handler) 事件简写已被弃用,应写成 $(window).on('load', handler)。
177. 前端跨页面通信,你知道哪些方法?
答案:
要根据页面关系、是否同源和是否需要服务端中转来选:
- 同源标签页、窗口或 iframe 广播:
BroadcastChannel,接口直接,适合实时消息。 - 同源存储通知:一个页面写
localStorage,其他同源页面监听storage事件。它不在发起写入的当前页面触发,也不适合作为可靠消息队列。IndexedDB 更适合共享较大持久数据,但通常还需搭配通知机制。 - 已持有窗口引用的页面:
window.postMessage,可用于 iframe、window.open或 opener,支持跨源。发送时指定精确的targetOrigin,接收时校验event.origin、event.source和消息结构。 - 多页面共享后台上下文:
SharedWorker;需要离线、缓存或统一推送时也可由 Service Worker 通过clients.matchAll和postMessage转发。 - 页面之间没有直接浏览器关系或需要跨设备:通过服务端使用 WebSocket、SSE 或普通接口中转。
- 页面跳转时传少量状态:URL 参数、History state;Cookie 可随请求共享,但不应承载大量或敏感业务消息。
多数浏览器方案受同源策略、存储分区和隐私模式限制;跨源场景不能通过放宽校验来图省事。
178. 请对以下数组,根据 born 的值降序排列
js
const singers = [
{ name: 'Steven Tyler', band: 'Aerosmith', born: 1948 },
{ name: 'Karen Carpenter', band: 'The Carpenters', born: 1950 },
{ name: 'Kurt Cobain', band: 'Nirvana', born: 1967 },
{ name: 'Stevie Nicks', band: 'Fleetwood Mac', born: 1948 },
];答案:
用比较函数返回 b.born - a.born 即可。若不希望修改原数组,先复制再排序:
js
const singers = [
{ name: 'Steven Tyler', band: 'Aerosmith', born: 1948 },
{ name: 'Karen Carpenter', band: 'The Carpenters', born: 1950 },
{ name: 'Kurt Cobain', band: 'Nirvana', born: 1967 },
{ name: 'Stevie Nicks', band: 'Fleetwood Mac', born: 1948 },
];
const sorted = [...singers].sort((a, b) => b.born - a.born);
console.log(sorted);结果顺序是:Kurt Cobain (1967)、Karen Carpenter (1950)、Steven Tyler (1948)、Stevie Nicks (1948)。现代 ECMAScript 要求 sort 稳定,因此 born 相同时保持原有先后顺序。sort 会原地修改接收者;也可在支持的环境中使用 singers.toSorted(...)。一般排序时间复杂度为 O(n log n);边界上应保证 born 是有效数字,否则先定义缺失值的排序规则。
179. 遍历一个任意长度的list中的元素并依次创建异步任务,如何获取所有任务的执行结果?
答案:
如果任务彼此独立,可以按列表顺序创建 Promise,再统一等待:
js
async function runAll(list, createTask) {
const promises = list.map((item, index) => createTask(item, index));
return Promise.all(promises);
}map 会按索引顺序调用 createTask,这些异步任务通常并发执行;Promise.all 的结果顺序始终与输入顺序一致,而不是完成顺序。任一任务失败时,返回的 Promise 会立即拒绝,但其他任务不会自动取消。若需要保留每项成功或失败状态,改用:
js
async function runAllSettled(list, createTask) {
const promises = list.map((item, index) =>
Promise.resolve().then(() => createTask(item, index))
);
return Promise.allSettled(promises);
}对于真正“任意长度”的列表,不宜一次创建无限多网络请求,应增加并发上限或分批处理。创建和收集结果的时间开销为 O(n),结果及 Promise 列表占用 O(n) 空间。若任务必须严格依次执行,则应使用 for...of 加 await。
180. 如何顺序执行10个异步任务?
答案:
关键是传入“任务函数”而不是已经创建的 Promise;已经创建的 Promise 可能早已并发开始。用 for...of 在上一个任务完成后再调用下一个:
js
async function runSequentially(tasks, initialValue) {
const results = [];
let previous = initialValue;
for (const task of tasks) {
previous = await task(previous);
results.push(previous);
}
return results;
}
const tasks = Array.from({ length: 10 }, (_, index) => async previous => {
await new Promise(resolve => setTimeout(resolve, 100));
return `${previous}->任务${index + 1}`;
});
runSequentially(tasks, '开始').then(console.log);默认情况下某一步拒绝会停止后续任务;若要继续,应在循环内部单独 try...catch 并定义失败后传给下一步的值。总耗时约为各任务耗时之和,调度复杂度为 O(n);保存全部结果需要 O(n) 空间,只保留最后结果则可降为 O(1)。
181. 怎么把函数中的 arguments 转成数组?
答案:
arguments 是类数组对象。现代 JavaScript 可用以下方式转换:
js
function example() {
const a = Array.from(arguments);
const b = [...arguments];
const c = Array.prototype.slice.call(arguments);
return { a, b, c };
}新代码更推荐直接使用剩余参数,因为拿到的本来就是真数组:
js
function example(...args) {
return args;
}如果函数已有命名参数,function f(first, ...rest) 中的 rest 只包含剩余参数,而 arguments 包含全部实参。箭头函数没有自己的 arguments,应使用剩余参数,或访问外层普通函数的 arguments。
182. 如何获取页面的滚动距离值?
答案:
现代浏览器直接读取 window.scrollX 和 window.scrollY,分别表示页面横向、纵向滚动距离,单位为 CSS 像素,值可能是小数。兼容写法可使用空值合并而不是 ||:
js
function getPageScroll() {
const root = document.scrollingElement || document.documentElement;
return {
x: window.scrollX ?? window.pageXOffset ?? root.scrollLeft ?? 0,
y: window.scrollY ?? window.pageYOffset ?? root.scrollTop ?? 0
};
}获取某个滚动容器的距离则读该元素的 scrollLeft、scrollTop,不要与页面滚动混淆。监听滚动时应使用被动监听并按帧节流昂贵计算:
js
window.addEventListener('scroll', onScroll, { passive: true });在部分浏览器的回弹滚动中可能短暂出现负值;业务若不接受,应显式用 Math.max(0, value) 归一化。
183. 如何让Promise.all在抛出异常后依然有效
答案:
Promise.all 是 fail-fast:任一输入拒绝,组合 Promise 就拒绝,但其他任务仍会继续执行,只是成功结果不会由这个 Promise.all 返回。需要收集全部结果时首选 Promise.allSettled:
js
async function executeAll(taskFns) {
const promises = taskFns.map(task =>
Promise.resolve().then(task)
);
return Promise.allSettled(promises);
}
const results = await executeAll([
() => Promise.resolve('A'),
() => Promise.reject(new Error('B 失败')),
() => 'C'
]);
// 每项都是 { status: 'fulfilled', value }
// 或 { status: 'rejected', reason }Promise.resolve().then(task) 还能把任务函数同步抛出的异常转成拒绝结果。若必须继续使用 Promise.all,可给每项加 catch,把失败转换为普通值;但成功值和失败值应使用带 status 的结构区分,不能悄悄吞掉异常。结果顺序仍与输入顺序一致。
184. 实现一个数字转中文的方法
答案:
下面实现支持正负数、整数和小数,整数部分最多 16 位;为避免 Number 精度丢失,大整数和需要保留末尾零的小数应以字符串传入。
js
const CN_DIGITS = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
const SECTION_UNITS = ['千', '百', '十', ''];
const GROUP_UNITS = ['', '万', '亿', '万亿'];
function sectionToChinese(section) {
let text = '';
let pendingZero = false;
for (let i = 0; i < 4; i++) {
const base = 10 ** (3 - i);
const digit = Math.floor(section / base) % 10;
if (digit === 0) {
if (text) pendingZero = true;
continue;
}
if (pendingZero) text += '零';
pendingZero = false;
text += CN_DIGITS[digit] + SECTION_UNITS[i];
}
return text;
}
function numberToChinese(input) {
let raw = String(input).trim();
if (!/^[+-]?[0-9]+(?:[.][0-9]+)?$/.test(raw)) {
throw new TypeError('请输入普通十进制数字,不支持指数记法');
}
const negative = raw.startsWith('-');
raw = raw.replace(/^[+-]/, '');
let [integer, fraction] = raw.split('.');
integer = integer.replace(/^0+(?=[0-9])/, '');
if (integer.length > 16) throw new RangeError('整数部分最多 16 位');
const groups = [];
for (let end = integer.length; end > 0; end -= 4) {
groups.unshift(Number(integer.slice(Math.max(0, end - 4), end)));
}
let text = '';
let pendingZero = false;
groups.forEach((section, index) => {
const unitIndex = groups.length - 1 - index;
if (section === 0) {
if (text) pendingZero = true;
return;
}
if (text && (pendingZero || section < 1000)) text += '零';
text += sectionToChinese(section) + GROUP_UNITS[unitIndex];
pendingZero = false;
});
if (!text) text = '零';
text = text.replace(/^一十/, '十');
if (fraction) {
text += '点' + [...fraction].map(char => CN_DIGITS[Number(char)]).join('');
}
const nonZero = integer !== '0' || (fraction && /[1-9]/.test(fraction));
return negative && nonZero ? `负${text}` : text;
}
console.log(numberToChinese('10001')); // 一万零一
console.log(numberToChinese('-1203.04')); // 负一千二百零三点零四设输入数字串长度为 d,时间和输出空间复杂度均为 O(d)。更大的单位需要继续扩展分组单位,并先约定采用“万亿”还是“兆”等读法。
185. 用js实现二叉树的定义和基本操作
答案:
普通二叉树只约束每个节点最多有两个孩子;要实现有序插入、查找、最小值和删除,通常实现二叉搜索树。下面约定左子树值更小、右子树值更大,重复值不插入:
js
class TreeNode {
constructor(value) {
this.value = value;
this.left = null;
this.right = null;
}
}
class BinarySearchTree {
constructor() {
this.root = null;
}
insert(value) {
const node = new TreeNode(value);
if (!this.root) {
this.root = node;
return true;
}
let current = this.root;
while (true) {
if (value === current.value) return false;
const side = value < current.value ? 'left' : 'right';
if (!current[side]) {
current[side] = node;
return true;
}
current = current[side];
}
}
find(value) {
let current = this.root;
while (current) {
if (value === current.value) return current;
current = value < current.value ? current.left : current.right;
}
return null;
}
min(node = this.root) {
if (!node) return undefined;
while (node.left) node = node.left;
return node.value;
}
max(node = this.root) {
if (!node) return undefined;
while (node.right) node = node.right;
return node.value;
}
remove(value) {
const existed = this.find(value) !== null;
this.root = this._remove(this.root, value);
return existed;
}
_remove(node, value) {
if (!node) return null;
if (value < node.value) {
node.left = this._remove(node.left, value);
} else if (value > node.value) {
node.right = this._remove(node.right, value);
} else {
if (!node.left) return node.right;
if (!node.right) return node.left;
let successor = node.right;
while (successor.left) successor = successor.left;
node.value = successor.value;
node.right = this._remove(node.right, successor.value);
}
return node;
}
inOrder() {
const result = [];
const stack = [];
let current = this.root;
while (current || stack.length) {
while (current) {
stack.push(current);
current = current.left;
}
current = stack.pop();
result.push(current.value);
current = current.right;
}
return result;
}
}
const tree = new BinarySearchTree();
[5, 3, 7, 2, 4, 6, 8].forEach(value => tree.insert(value));
tree.remove(7);
console.log(tree.inOrder()); // [2, 3, 4, 5, 6, 8]单次操作复杂度为 O(h),h 是树高;平衡时约为 O(log n),按有序数据插入导致退化时最坏为 O(n)。需要稳定对数复杂度应使用 AVL 树或红黑树;重复值也应预先约定忽略、计数还是固定放到一侧。
186. 非递归遍历二叉树
答案:
非递归遍历用显式栈模拟调用栈。以下节点结构约定为 { value, left, right },实现不会修改原树:
js
function preorder(root) {
if (!root) return [];
const result = [];
const stack = [root];
while (stack.length) {
const node = stack.pop();
result.push(node.value);
if (node.right) stack.push(node.right);
if (node.left) stack.push(node.left);
}
return result;
}
function inorder(root) {
const result = [];
const stack = [];
let current = root;
while (current || stack.length) {
while (current) {
stack.push(current);
current = current.left;
}
current = stack.pop();
result.push(current.value);
current = current.right;
}
return result;
}
function postorder(root) {
const result = [];
const stack = [];
let current = root;
let lastVisited = null;
while (current || stack.length) {
if (current) {
stack.push(current);
current = current.left;
continue;
}
const top = stack[stack.length - 1];
if (top.right && lastVisited !== top.right) {
current = top.right;
} else {
result.push(top.value);
lastVisited = stack.pop();
}
}
return result;
}三种遍历都访问每个节点一次,时间复杂度为 O(n);不计结果数组,栈空间为 O(h),最坏退化树为 O(n)。前序必须先压右再压左;后序需要记录右子树是否已访问,否则会反复进入右子树。
187. 什么是同步和异步?
答案:
同步表示当前操作完成并返回后,调用栈才继续执行下一条语句;耗时同步代码会一直占用当前线程。异步表示操作先发起,结果稍后通过回调、Promise 或事件交付,当前调用栈可以先继续做其他工作。
浏览器中的 JavaScript 主线程通常一次只执行一个调用栈。定时器、网络、文件 I/O 等由宿主环境处理,完成后把任务加入队列;事件循环在调用栈清空后调度它们。当前任务结束时会先清空 Promise、queueMicrotask 等微任务,再取下一个定时器、事件等任务,所以 Promise.then 通常早于同轮可执行的 setTimeout。
async/await 是 Promise 的语法封装:执行到 await 时函数暂停,后续部分作为微任务恢复,但它不会把 CPU 密集循环自动移到后台。CPU 密集工作仍会阻塞页面,需要切片或交给 Web Worker;异步也不等于并行。
188. 说说你的ES6-ES12的了解
答案:
ES6 之后 ECMAScript 改为按年发布,ES6 到 ES12 分别对应 ES2015 到 ES2021。常见新增能力如下:
- ES2015(ES6):
let/const、箭头函数、模板字符串、解构、默认参数、rest/spread、类、模块、迭代器与生成器、Promise、Symbol、Map/Set/WeakMap/WeakSet、Proxy/Reflect。 - ES2016(ES7):
Array.prototype.includes、指数运算符**。 - ES2017(ES8):
async/await、Object.values/entries、Object.getOwnPropertyDescriptors、字符串padStart/padEnd、共享内存与 Atomics。 - ES2018(ES9):异步迭代、对象 rest/spread、
Promise.prototype.finally,以及正则的命名捕获组、后行断言、dotAll 和 Unicode 属性转义。 - ES2019(ES10):
flat/flatMap、Object.fromEntries、trimStart/trimEnd、可选 catch 绑定、Symbol.prototype.description,并要求数组排序稳定。 - ES2020(ES11):BigInt、可选链
?.、空值合并??、Promise.allSettled、globalThis、动态import()、import.meta、String.prototype.matchAll。 - ES2021(ES12):
String.prototype.replaceAll、Promise.any与AggregateError、逻辑赋值运算符、数字分隔符、WeakRef 与 FinalizationRegistry。
面试中还应区分语法转译与运行时能力:Babel 能改写部分新语法,但 Promise、Map 等 API 需要 polyfill;模块、WeakRef 等能力还受运行时和构建目标限制。
189. ['1','2','3'].map(parseInt) 的返回值是什么?
答案:
明确输出是:
js
[1, NaN, NaN]原因是 map 给回调传入 (当前元素, 索引, 原数组),而 parseInt 的前两个参数是 (字符串, radix),因此等价于:
js
parseInt('1', 0); // 1,radix 为 0 时按规则推断进制
parseInt('2', 1); // NaN,1 不是合法进制
parseInt('3', 2); // NaN,二进制中没有数字 3正确写法是显式固定进制,或直接转成数字:
js
['1', '2', '3'].map(value => parseInt(value, 10)); // [1, 2, 3]
['1', '2', '3'].map(Number); // [1, 2, 3]190. 怎么预防按钮的重复点击?
答案:
按钮触发异步操作时,应使用“请求进行中”状态而不是只禁用固定几百毫秒:
js
let inFlight = false;
async function onClick(event) {
if (inFlight) return;
inFlight = true;
const button = event.currentTarget;
button.disabled = true;
button.setAttribute('aria-busy', 'true');
try {
await doRequest();
} finally {
inFlight = false;
button.disabled = false;
button.removeAttribute('aria-busy');
}
}防抖适合搜索输入等只关心最后一次的场景;节流适合限制高频操作速率。提交、支付、创建订单等业务即使前端禁用按钮,仍可能因刷新、网络重试或直接调用接口而重复,所以服务端必须使用幂等键、唯一约束或状态机保证一次业务操作只生效一次。请求失败后的重试应复用原幂等键,而不是创建一笔新操作。
191. JavaScript 中如何取消请求
答案:
fetch 使用 AbortController 取消:
js
function cancellableFetch(url, options = {}) {
const controller = new AbortController();
const promise = fetch(url, {
...options,
signal: controller.signal
});
return {
promise,
cancel: () => controller.abort()
};
}
const request = cancellableFetch('/api/profile');
try {
const response = await request.promise;
console.log(await response.json());
} catch (error) {
if (error.name !== 'AbortError') throw error;
}
// 在不再需要结果时调用
request.cancel();原生 XMLHttpRequest 使用 xhr.abort();当前 Axios 也支持传入 AbortController.signal,旧的 CancelToken 已不建议使用。组件卸载、路由切换、搜索条件变化和超时都适合取消旧请求。
取消通常只表示客户端停止等待或读取响应,不保证服务端已经停止处理。写操作仍需幂等设计;如果底层请求无法取消,至少用请求序号或当前参数校验来忽略过期响应,防止旧结果覆盖新状态。
192. 怎么实现大型文件上传?
答案:
大文件上传通常采用“初始化会话、切片、有限并发上传、查询已上传分片、完成合并”的协议。浏览器用 File.slice 切片,服务端按 uploadId + index 幂等保存,最后校验总大小和文件摘要后按序合并;对象存储场景应优先使用其原生 multipart upload。
下面是一个可运行的浏览器端骨架,假设初始化接口返回 { uploadId, uploaded },其中 uploaded 是已完成分片下标:
js
async function asJson(response) {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
async function retry(task, times = 3) {
let lastError;
for (let attempt = 0; attempt < times; attempt++) {
try {
return await task();
} catch (error) {
lastError = error;
if (attempt + 1 < times) {
await new Promise(resolve => setTimeout(resolve, 500 * 2 ** attempt));
}
}
}
throw lastError;
}
async function uploadLargeFile(file, options = {}) {
const chunkSize = options.chunkSize ?? 5 * 1024 * 1024;
const concurrency = options.concurrency ?? 4;
const totalChunks = Math.ceil(file.size / chunkSize);
const session = await fetch('/upload/init', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: file.name,
size: file.size,
type: file.type,
chunkSize,
totalChunks
})
}).then(asJson);
const uploaded = new Set(session.uploaded ?? []);
const pending = Array.from(
{ length: totalChunks },
(_, index) => index
).filter(index => !uploaded.has(index));
let cursor = 0;
async function worker() {
while (cursor < pending.length) {
const index = pending[cursor++];
const chunk = file.slice(
index * chunkSize,
Math.min(file.size, (index + 1) * chunkSize)
);
await retry(async () => {
const form = new FormData();
form.append('uploadId', session.uploadId);
form.append('index', String(index));
form.append('chunk', chunk);
const response = await fetch('/upload/chunk', {
method: 'POST',
body: form
});
if (!response.ok) throw new Error(`分片 ${index} 上传失败`);
});
}
}
await Promise.all(
Array.from({ length: Math.min(concurrency, pending.length) }, worker)
);
return fetch('/upload/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploadId: session.uploadId })
}).then(asJson);
}服务端还必须校验登录用户、会话有效期、分片编号、大小和摘要,防止越权覆盖;合并应原子完成,并清理过期会话。断点续传时重新调用初始化或状态接口取得已上传分片。文件摘要计算是 O(n),上传总数据量也是 O(n);客户端峰值额外内存约为 O(并发数 × 分片大小)。对超大文件计算完整摘要应放进 Worker 并增量处理,避免阻塞主线程或一次读入全部文件。
193. 说说你对JS的模块化方案的了解
答案:
模块化是把程序按职责拆成有明确导入、导出和依赖关系的单元,从而隔离作用域、复用代码并便于测试和维护。
常见方案如下:
- 早期浏览器通过全局变量、命名空间和 IIFE 隔离代码,但依赖顺序靠手工维护,仍可能污染全局。
- AMD(RequireJS)强调依赖前置和异步加载;CMD(SeaJS)常用依赖就近。它们主要解决早期浏览器没有原生模块的问题。
- CommonJS 使用
require()和module.exports,模块在运行时加载并缓存,传统实现以同步加载为主,适合 Node.js。 - UMD 是兼容层,可同时适配 AMD、CommonJS 和全局变量环境。
- ES Module 使用静态的
import/export。导入导出关系可在执行前分析,导入是只读的实时绑定,模块默认严格模式,便于 tree shaking;import()可动态、异步加载。它是现代浏览器和 Node.js 的主流方案。
CommonJS 的依赖和值通常在运行时确定,导出的是 module.exports 对象;ESM 的静态结构和实时绑定则更利于编译期优化。工程中还需留意 Node.js 的包类型、文件扩展名及 CJS/ESM 互操作规则。
194. 遍历数组的方式有哪些?
答案:
数组可按需求用以下方式遍历:
for、while:可访问下标,也能用break、continue,适合需要精确控制流程的场景。for...of:遍历值;配合arr.entries()可同时得到下标和值。forEach:执行副作用,返回值始终是undefined,不能用普通的break提前结束。map、filter、reduce:分别用于映射、筛选和归并,应在确实需要其返回结果时使用。some、every、find、findIndex:适合带短路或查找语义的遍历。keys()、values()、entries():返回迭代器,可配合for...of。
不建议用 for...in 遍历数组,因为它遍历可枚举属性名,可能包含额外属性和继承属性,顺序语义也不是为数组值遍历设计的。还要注意稀疏数组:forEach、map 等会跳过空槽,而按下标读取或 for...of 会把空槽读成 undefined。遍历过程中修改长度或元素也可能影响后续访问,应尽量避免。
195. 说说你对 new.target 的理解
答案:
new.target 是函数或构造器内部的元属性,用来表示本次构造调用实际由哪个构造器通过 new 或 Reflect.construct 发起。普通调用函数时它是 undefined;通过 new Foo() 调用时通常是 Foo。
js
function User(name) {
if (!new.target) {
throw new TypeError('User 必须使用 new 调用');
}
this.name = name;
}在继承中,父类构造器里的 new.target 仍指向最初被实例化的子类,因此也可用于抽象基类检查:
js
class Shape {
constructor() {
if (new.target === Shape) {
throw new TypeError('Shape 不能直接实例化');
}
}
}箭头函数没有自己的 new.target,只能从外层函数词法获取;类构造器本身也不能脱离 new 普通调用。
196. arguments 这种类数组,如何遍历类数组?
答案:
arguments 有索引和 length,但不是普通数组。可以直接按下标遍历,也可以先转成数组:
js
function demo() {
for (let i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
for (const value of arguments) {
console.log(value);
}
const list1 = Array.from(arguments);
const list2 = [...arguments];
list1.forEach(console.log);
}现代 JavaScript 中 arguments 是可迭代对象,因此可使用 for...of 和展开语法;兼容较老环境时可用 Array.prototype.forEach.call(arguments, callback)。新代码更推荐剩余参数 function demo(...args) {},因为 args 就是真数组,语义也更明确。箭头函数没有自己的 arguments。
197. 连续 bind()多次,输出的值是什么?
js
var bar = function(){
console.log(this.x);
}
var foo = {
x:3
}
var sed = {
x:4
}
var func = bar.bind(foo).bind(sed);
func(); //?
var fiv = {
x:5
}
var func = bar.bind(foo).bind(sed).bind(fiv);
func(); //?答案:
两次调用都输出:
text
3
3第一次 bar.bind(foo) 已创建绑定函数,并把普通调用时的 this 固定为 foo。再对这个绑定函数调用 bind(sed) 或 bind(fiv),不能覆盖已有的 this 绑定,所以读取的始终是 foo.x。后续 bind 仍可继续预置参数。另一个边界是:绑定函数若通过 new 构造,绑定的 thisArg 会被忽略,因为此时 this 是新实例。
198. new fn与new fn()有什么区别吗?
答案:
在不传参数的情况下,new fn 与 new fn() 等价,都会创建实例并调用 fn,参数列表为空。实际代码通常写 new fn(),可读性更好,也便于以后添加参数。
差异主要出现在复杂表达式的运算符优先级中。例如 new Foo.bar() 构造的是 Foo.bar,而 new Foo().bar 是先构造 Foo,再访问实例的 bar。因此链式成员访问时应显式加括号,避免歧义。
199. Object与Map有什么区别?
答案:
Object 更适合表示字段固定的记录,Map 更适合表示运行时增删的键值映射。主要区别是:
- 对象键只能是字符串或 Symbol,其他值会被转换为字符串;
Map可用任意值作键,对象键按引用区分。 - 普通对象带原型,可能出现继承属性或特殊键冲突;
Map没有这类业务键冲突。无原型字典可用Object.create(null)。 Map直接提供size、set、get、has、delete、clear,并按插入顺序迭代。对象通常通过Object.keys/values/entries处理自有可枚举属性。Map对频繁增删和未知键集合的语义更清晰;对象更适合属性访问、结构化数据和 JSON 序列化。JSON.stringify默认不会把Map序列化为其条目。
二者都按引用保存对象值,选择时应以数据语义为主,不应假定所有引擎中某一种永远更快。
200. cookie 的有效时间设置为 0 会怎么样
答案:
如果设置的是 Max-Age=0,Cookie 会立即过期,浏览器会删除与该名称、Domain、Path 匹配的 Cookie。这也是前端删除可访问 Cookie 的常见方式:
js
document.cookie = 'token=; Max-Age=0; Path=/';删除时必须使用与原 Cookie 一致的 Path 和 Domain,否则可能只是创建或删除了另一个同名 Cookie。不要把“有效时间为 0”与“不设置过期属性”混淆:省略 Expires 和 Max-Age 通常会创建会话 Cookie,在会话结束时失效;某些封装库对 expires: 0 的解释也可能不同,应以其 API 文档和最终响应头为准。
201. postMessage 有哪些使用场景?
答案:
postMessage 用于不同执行上下文之间安全地传递可结构化克隆的数据,常见场景包括:
- 页面与跨域
iframe通信,例如嵌入式支付、登录或微前端。 - 当前窗口与
window.open()打开的窗口通信。 - 页面与 Web Worker、Service Worker 通信。
- 配合
MessageChannel建立独立的双向消息通道,并转移ArrayBuffer、MessagePort等可转移对象。
窗口通信时应指定准确的 targetOrigin,接收端必须校验 event.origin,必要时也校验 event.source 和消息结构:
js
otherWindow.postMessage({ type: 'READY' }, 'https://example.com');
window.addEventListener('message', (event) => {
if (event.origin !== 'https://example.com') return;
// 校验 event.data 后再处理
});它解决的是上下文通信,不是绕过同源策略发起任意跨域 HTTP 请求;敏感场景不要随意使用 '*'。
202. async/await 怎么进行错误处理?
答案:
最常见的做法是在 async 函数中用 try...catch...finally 捕获被 await 的 Promise 拒绝:
js
async function loadUser() {
try {
const user = await fetchUser();
return user;
} catch (error) {
console.error(error);
throw error; // 无法恢复时继续向调用方传播
} finally {
hideLoading();
}
}也可以在调用处处理返回的 Promise:
js
loadUser().then(renderUser).catch(showError);try...catch 只能捕获其作用域内真正被 await 的拒绝;如果只是启动 Promise 而没有 await 或 return,错误会脱离该调用链。并行任务可把 await Promise.all(...) 放进 try 中;若需要收集每项成功或失败结果,可用 Promise.allSettled(...)。捕获后如果既不抛出也不返回 rejected Promise,外层看到的是已兑现状态。
203. 将数组的length设置为0,取第一个元素会返回什么?
答案:
通常返回 undefined:
js
const arr = [10, 20];
arr.length = 0;
console.log(arr[0]); // undefined数组长度缩短时,会删除下标大于等于新长度的数组元素,因此第一个元素已不存在。这不会销毁仍被其他变量引用的对象,也不会删除数组上的非索引属性。极端情况下,如果待删除的数组元素被定义为不可配置属性,缩短 length 会失败;严格模式下会抛出 TypeError,数组长度也不会按预期变为 0。
204. CSS动画和JS实现的动画分别有哪些优缺点?
答案:
CSS 动画适合状态明确、时间轴较简单的过渡。它声明式、代码少,transform 和 opacity 等属性通常可由合成线程高效处理,即使主线程短暂繁忙也可能保持平滑;缺点是复杂编排、动态路径、逐帧计算和精细交互控制不够方便。
JavaScript 动画通常使用 requestAnimationFrame 或 Web Animations API。它能根据手势、物理模型和实时数据精确控制暂停、反向、时间线及多个对象联动;代价是代码和状态管理更复杂,逐帧回调占用主线程,写法不当容易掉帧。
性能不只取决于使用 CSS 还是 JS,更取决于动画属性和每帧工作量。应优先动画 transform、opacity,减少布局与绘制;JS 逐帧动画使用 requestAnimationFrame,不要用固定间隔的 setInterval 模拟帧率。Web Animations API 则兼具 JS 控制能力和浏览器原生时间线。
205. 前端实现动画有哪些方式?
答案:
前端动画主要有以下实现方式:
- CSS
transition:适合两个状态之间的简单过渡。 - CSS
@keyframes/animation:适合预定义的多关键帧和循环动画。 - Web Animations API:通过 JS 创建、组合、暂停和反向浏览器原生动画。
requestAnimationFrame:逐帧更新 DOM、SVG 或 Canvas,适合交互驱动和自定义运动模型。- SVG 动画:适合矢量图形的属性、路径和变形动画。
- Canvas 2D/WebGL:每帧重绘,适合大量对象、图表、粒子和游戏。
- 动画库:如 GSAP 等,用于时间线、缓动和复杂编排。
简单 UI 状态优先 CSS;需要可编程时间线可用 Web Animations API;大量图元或实时场景使用 Canvas/WebGL。无论采用哪种方式,都应避免频繁触发布局,逐帧逻辑使用 requestAnimationFrame。
206. 写一个返回数据类型的函数,要求自定义的类实例化的对象返回定义的类名
答案:
可以先处理原始类型,再利用 Object.prototype.toString 识别内建对象;普通对象或自定义实例再读取其原型构造器名:
js
function getType(value) {
if (value === null) return 'null';
const primitiveType = typeof value;
if (primitiveType !== 'object') return primitiveType;
const tag = Object.prototype.toString.call(value).slice(8, -1);
if (tag !== 'Object') return tag;
const proto = Object.getPrototypeOf(value);
const ctor = proto && proto.constructor;
return typeof ctor === 'function' && ctor.name ? ctor.name : 'Object';
}
class Person {}
getType(null); // 'null'
getType([]); // 'Array'
getType(new Date()); // 'Date'
getType(new Person()); // 'Person'
getType(Object.create(null)); // 'Object'时间复杂度和额外空间都是 O(1)。边界是 constructor 和 Symbol.toStringTag 都可被修改,构建工具也可能压缩类名,因此它适合诊断和一般业务分支,不适合作为安全校验或稳定持久化类型标识。
207. e.target 和 e.currentTarget 有什么区别?
答案:
e.target 是事件最初命中的目标节点;e.currentTarget 是当前正在执行该监听器的节点,通常等于注册监听器时的对象。
js
ul.addEventListener('click', (e) => {
console.log(e.target); // 实际点击的 li 或其子节点
console.log(e.currentTarget); // ul
});事件冒泡时 target 通常保持不变,而 currentTarget 会随事件经过不同监听节点而变化,所以事件委托用 target.closest(...) 定位业务元素,用 currentTarget 表示委托容器。浏览器中回调执行结束后再异步读取事件对象时,currentTarget 通常已是 null,需要提前保存。Shadow DOM 还可能对外部观察者重定向 target。
208. Map 和 Set 的用法以及区别
答案:
Map 保存键值对,Set 只保存不重复的值:
js
const map = new Map();
map.set('name', 'Ada');
map.set({ id: 1 }, '对象也可作键');
console.log(map.get('name'));
console.log(map.has('name'));
const set = new Set([1, 1, 2]);
set.add(3);
set.delete(1);
console.log([...set]); // [2, 3]二者都按插入顺序迭代,具有 size,并使用 SameValueZero 判断键或值是否相同,所以 NaN 与自身相同,+0 与 -0 相同;对象则按引用判断。Map 遍历项是 [key, value],适合字典、缓存和关联数据;Set 适合去重和成员判断。
WeakMap 和 WeakSet 只弱引用允许的对象键/值,不可枚举,适合附加私有元数据,但不能替代需要遍历或统计大小的集合。
209. const声明了数组,还能push元素吗,为什么?
答案:
可以。const 限制的是变量绑定不能重新赋值,并不会自动冻结绑定所指向的对象:
js
const list = [1];
list.push(2); // 合法,list 仍指向同一个数组
list = []; // TypeError:不能重新赋值如果希望阻止数组本身被修改,可以使用 Object.freeze(list);严格模式下再 push 会抛错。Object.freeze 是浅冻结,数组元素若是对象,其内部属性仍可修改,除非递归冻结或采用不可变数据策略。
210. 如何区分数组和对象?
答案:
首选 Array.isArray(value):
js
Array.isArray([]); // true
Array.isArray({}); // false它能正确识别来自不同 iframe/realm 的数组。value instanceof Array 依赖当前 realm 的 Array.prototype,跨窗口时可能误判,也可能被原型链伪造;typeof 对数组和普通对象都返回 'object'。兼容性兜底可用 Object.prototype.toString.call(value) === '[object Array]',但 Symbol.toStringTag 可影响某些标签判断,因此一般仍以 Array.isArray 为准。
211. async、await 实现原理
答案:
async 函数调用后一定返回 Promise:普通返回值会成为兑现值,抛出的异常会成为拒绝原因。执行到 await value 时,规范会把 value 按 Promise 语义吸收;当前函数暂停并把控制权交还调用者,待结果确定后,把后续代码作为微任务继续执行。若等待值拒绝,恢复时等价于在 await 位置抛出异常。
js
async function f() {
const value = await task();
return value + 1;
}在概念和转译实现上,它可理解为生成器加 Promise 执行器:生成器在 yield 处暂停,执行器用 Promise.resolve 等待结果,再调用 next 或 throw 恢复。但现代引擎直接实现 async/await 的规范语义,并不一定真的创建生成器。
连续 await 会形成串行依赖;互不依赖的任务应先启动,再用 Promise.all 一起等待。await 不会阻塞线程,只暂停当前异步函数。
212. 说说对 ES6 中rest参数的理解
答案:
剩余参数用 ...name 把未被前面形参接收的实参收集成一个真正的数组:
js
function sum(base, ...numbers) {
return numbers.reduce((total, n) => total + n, base);
}
sum(10, 1, 2, 3); // 16,numbers 为 [1, 2, 3]它必须是最后一个形参,不能设置默认值,也不能在其后再写逗号。与 arguments 相比,剩余参数只包含“剩余”的实参,是数组,可直接使用数组方法,并且能用于箭头函数;arguments 是类数组,还可能在非严格模式下与命名形参存在联动。
形参中的 ... 是收集,调用、数组或对象字面量中的 ... 是展开,二者方向相反。
213. 岛屿数量
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:
js
grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]输出: 1
示例 2:
输入:
js
grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]输出: 3
提示:
m == grid.lengthn == grid[i].length1 <= m, n <= 300grid[i][j]的值为'0'或'1'
js
/**
* @param {character[][]} grid
* @return {number}
*/
var numIslands = function(grid) {
};答案:
扫描网格,遇到尚未访问的陆地就把岛屿数加一,并用 DFS/BFS 把与它四向连通的陆地全部标记。下面用迭代 DFS,避免大网格递归过深:
js
/**
* @param {string[][]} grid
* @return {number}
*/
function numIslands(grid) {
if (!grid.length || !grid[0].length) return 0;
const rows = grid.length;
const cols = grid[0].length;
let count = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (grid[r][c] !== '1') continue;
count++;
grid[r][c] = '0';
const stack = [[r, c]];
while (stack.length) {
const [x, y] = stack.pop();
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
const nx = x + dx;
const ny = y + dy;
if (
nx >= 0 && nx < rows && ny >= 0 && ny < cols &&
grid[nx][ny] === '1'
) {
grid[nx][ny] = '0';
stack.push([nx, ny]);
}
}
}
}
}
return count;
}时间复杂度为 O(mn),最坏额外空间为 O(mn)。该实现会把访问过的陆地改成水;若不能修改输入,应额外使用 visited 集合或布尔数组。
214. 以下代码的输出是什么?
js
var name = 'window'
const obj = {
name: 'obj',
sayName:function() {
console.log(this.name)
},
}
obj.sayMyName = () => {
console.log(this.name)
}
const fn1 = obj.sayName
const fn2 = obj.sayMyName
fn1()
obj.sayName()
fn2()
obj.sayMyName()答案:
在浏览器的非模块经典脚本中,输出为:
text
window
obj
window
windowfn1() 是普通函数直接调用,非严格模式下 this 指向 window;obj.sayName() 的调用者是 obj,所以输出 obj。sayMyName 是箭头函数,没有自己的 this,它在创建时词法捕获顶层的 this,经典脚本中为 window;把它赋给变量或作为对象方法调用都不会改变该绑定。顶层 var name 同时成为 window.name。
运行环境是必要边界:ES Module 顶层 this 为 undefined,并且代码处于严格模式,此代码会在第一次 fn1() 读取 this.name 时抛出 TypeError;CommonJS 中箭头函数捕获的顶层对象也不是浏览器的 window。
215. 给一个dom同时绑定两个点击事件,一个用捕获,一个用冒泡,说下会执行几次事件,然后会先执行冒泡还是捕获?
答案:
一次点击会调用两个监听器各一次,共执行两次。捕获监听器先执行,冒泡监听器后执行。
js
el.addEventListener('click', captureHandler, true);
el.addEventListener('click', bubbleHandler, false);事件从外层祖先向目标传播时执行捕获监听器,到达目标后执行目标上的捕获监听器,再执行目标上的非捕获监听器,之后才从目标向祖先冒泡。若某个处理器调用 stopImmediatePropagation(),同一节点后续监听器也可能不再执行;stopPropagation() 主要阻止继续传播,不会取消同一节点上尚未执行的其他监听器。
216. promise.catch后面的.then还会执行吗?
答案:
通常会执行。catch 本质上是 then(undefined, onRejected),它返回一个新的 Promise。若错误被 catch 正常处理且回调没有再次抛错,返回的 Promise 会变为 fulfilled,后面的成功 then 就会执行:
js
Promise.reject('error')
.catch((error) => {
console.log(error);
return 'recovered';
})
.then((value) => console.log(value)); // recovered如果 catch 内抛出异常、返回 rejected Promise,或根本没有提供有效的拒绝处理函数,拒绝会继续向后传播,后续 then 的成功回调不会执行,而其拒绝回调或后面的 catch 会执行。
217. 如何确保你的构造函数只能被new调用,而不能被普通调用?
答案:
函数构造器可用 new.target 检查:
js
function User(name) {
if (!new.target) {
throw new TypeError('User 必须通过 new 调用');
}
this.name = name;
}
new User('Ada'); // 正常
User('Ada'); // TypeError如果使用 class,语言本身已经强制构造器只能通过 new 或 Reflect.construct 调用:直接执行 User() 会抛出 TypeError。旧代码也可用 this instanceof User,但它会受显式绑定 this 和原型链伪造影响,不如 new.target 准确。该检查只能约束构造器调用,无法阻止别人用 Object.create(User.prototype) 创建一个未执行构造逻辑的对象。
218. 如何获取到一个实例对象的原型对象?
答案:
使用标准方法 Object.getPrototypeOf,或等价的 Reflect.getPrototypeOf:
js
class User {}
const user = new User();
Object.getPrototypeOf(user) === User.prototype; // true
Reflect.getPrototypeOf(user) === User.prototype; // trueobj.__proto__ 是历史访问器,不建议作为正式 API。obj.constructor.prototype 也不可靠,因为实例可以有自己的 constructor 属性,构造器属性还可能被改写;获取对象当前真实的 [[Prototype]] 应使用上述标准方法。对于 Object.create(null) 创建的对象,结果是 null。
219. 下面代码会输出什么?
js
foo();
var foo;
function foo(){
console.log(1);
}
foo = function(){
console.log(2);
}答案:
输出:
text
1进入作用域的初始化阶段时,函数声明 function foo(){...} 会建立并初始化 foo 绑定;同名的 var foo 声明不会把它覆盖成 undefined。因此第一行调用的是函数声明,输出 1。最后的赋值才把 foo 改为输出 2 的函数表达式,但赋值之后没有再次调用,所以不会输出 2。
220. RESTful 接口规范是什么?
答案:
REST 是面向资源的架构风格,不是一份强制所有接口照抄的 URL 标准。常见接口约定是:
- 用资源名词建模 URI,如
/users/42/orders,避免把动作写成路径主体。 - 用 HTTP 方法表达操作:
GET查询、POST创建或提交处理、PUT整体替换、PATCH部分更新、DELETE删除。 - 正确使用状态码,如
200、201、204、400、401、403、404、409、422、500,错误体保持统一结构。 - 请求无状态,每次请求携带完成处理所需的信息;缓存通过标准缓存头描述。
- 明确幂等性:
GET、PUT、DELETE按语义应幂等,POST通常不保证;重试创建操作可配合幂等键。 - 列表接口统一分页、筛选、排序字段,使用正确的
Content-Type,并制定兼容的版本演进策略。
安全、鉴权、限流和审计仍需单独设计;仅把 URL 写得像资源并不等于系统满足 REST 约束。
221. ES5怎么实现继承
答案:
ES5 中较完整的做法是寄生组合继承:父构造器负责实例属性,原型链负责共享方法,避免直接执行父构造器来创建子类原型。
js
function Parent(name) {
this.name = name;
this.tags = [];
}
Parent.prototype.sayName = function () {
return this.name;
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = Object.create(Parent.prototype);
Object.defineProperty(Child.prototype, 'constructor', {
value: Child,
writable: true,
configurable: true
});
// 如需继承静态属性/方法:
Object.setPrototypeOf(Child, Parent);
var child = new Child('Ada', 18);
console.log(child.sayName()); // AdaParent.call 确保每个实例拥有独立属性,Object.create 建立原型继承且不会额外调用一次父构造器。旧环境没有 Object.setPrototypeOf 时,静态成员通常显式复制。
222. 请简述 == 的机制
答案:
== 使用抽象相等比较。大致规则是:
- 类型相同时按对应类型比较,
NaN与任何值都不相等,+0与-0相等,对象按引用比较。 null只与undefined(以及 Web 历史兼容的document.all特例)宽松相等。- 数字与字符串比较时,字符串转成数字。
- 布尔值先转成
0或1,再继续比较。 - 对象与原始值比较时,对象先通过 ToPrimitive 转成原始值,再按相应规则比较。
- BigInt 与字符串会尝试按 BigInt 解析;BigInt 与 Number 只有在数值可精确表示为同一数学值时才相等。Symbol 不会被隐式转成数字。
因此会出现:
js
0 == false // true
'' == 0 // true
null == undefined // true
[] == '' // true业务代码通常优先使用 ===,需要区分 NaN 或正负零时可使用 Object.is。不要仅凭“== 会把两边都转成数字”理解,因为 null/undefined、对象转原始值、BigInt 等都有专门分支。
223. 说说sourcemap的原理?
答案:
Source Map 是生成代码到原始源代码的位置映射。构建工具在压缩、转译或合并代码时生成 .map 文件,其中记录生成文件的行列、原文件、原行列以及可选符号名;mappings 通常用 Base64 VLQ 对相邻位置的增量进行压缩。生成文件通过 sourceMappingURL 注释或响应头关联映射文件。
开发者工具加载映射后,可把断点、调用栈和报错位置还原到 TypeScript、JSX 或压缩前源码。映射只影响调试展示,不参与程序执行,也不会把生产代码恢复成完全相同的源码结构。
常见模式包括内联 map、外部 map、只保留报错映射但不在产物中公开链接等。生产环境公开包含 sourcesContent 的 map 可能暴露源代码;可以将 map 私下上传到错误监控平台,同时避免对公网分发。
224. AST语法树是什么?
答案:
AST(抽象语法树)是源代码语法结构的树形表示。节点表示程序、声明、表达式、调用、标识符、字面量等语义结构,并携带子节点及位置信息。它通常省略括号、分号等不影响抽象语义的细节,这也是“抽象”与具体语法树/CST 的区别。
典型处理流程是:
text
源码 -> 词法分析得到 Token -> 语法分析得到 AST
-> 遍历/修改 AST -> 重新生成代码和 Source MapBabel 用 AST 做语法降级和插件转换,ESLint 用它匹配代码规则,Prettier 用语法结构重新排版,压缩器和打包器则据此做作用域分析、tree shaking 与代码生成。AST 的节点格式取决于语言和解析器,JavaScript 生态常以 ESTree 为基础,但不同工具仍可能有扩展字段。
225. flexible.js实现移动端适配的原理是什么?
答案:
flexible.js 的核心是根据设备视口宽度动态设置根元素 html 的 font-size,设计稿尺寸再换算成 rem。经典方案常把视口划分为 10 份,例如视口宽 375px 时令 1rem = 37.5px,于是页面尺寸会随视口等比例变化。它还会监听 resize、横竖屏变化等事件重新计算。
较早版本还会结合 devicePixelRatio 动态设置 viewport 缩放,并暴露 data-dpr,用于处理高 DPR 设备上的 1px 线和字体。其目标是让 CSS 像素布局按设计稿比例缩放,而不是提升图片实际分辨率。
边界是:纯等比缩放不适合所有屏幕,过宽设备通常需要最大宽度限制;浏览器缩放、系统字体和无障碍需求也不能只靠固定比例处理。现代项目常直接使用响应式布局、媒体/容器查询、vw、clamp() 等方案,是否使用 flexible 应由现有设计体系决定。
226. JavaScript中的 sort 方法是怎么实现的?
答案:
Array.prototype.sort 会原地修改数组并返回原数组。默认比较会把非 undefined 元素转成字符串,按 UTF-16 码元顺序排列,所以 [2, 10].sort() 得到 [10, 2];数值升序应传 (a, b) => a - b。比较函数返回负数、0、正数,分别表示 a 在前、保持等价顺序、b 在前。
ECMAScript 只规定可观察语义,不规定必须使用哪种排序算法。自 ES2019 起规范要求稳定排序;具体引擎可根据数组长度和元素类型采用 TimSort、插入排序或其他混合算法,因此不能把某个 V8 版本的实现当成语言保证,复杂度也不是规范承诺。
比较函数应保持纯粹、反对称和传递性,否则结果不可预测。undefined 和空槽会被排到末尾,其中空槽仍保持为空槽。需要保留原数组时可用 toSorted(),或在旧环境中先复制 [...arr].sort(compareFn)。
227. cookie、localStorage和sessionStorage 三者之间有什么区别
答案:
三者的关键区别是:
- Cookie:单条通常约 4KB,可设置过期时间、Domain、Path、
Secure、HttpOnly、SameSite。匹配请求时会自动随 HTTP 请求发送,因此适合服务端会话标识,但会增加请求开销。HttpOnlyCookie 不能被 JS 读取。 localStorage:按源存储,容量通常为数 MB,持久保存,除非用户或代码清除;同源标签页共享,修改可触发其他页面的storage事件。不会自动发送到服务器。sessionStorage:同样按源,但还按顶层浏览上下文/标签页隔离,通常在该标签页会话结束时清除;新窗口的初始复制行为还受打开方式影响。不会自动发送到服务器。
localStorage 和 sessionStorage 都是同步字符串键值 API,大量读写会阻塞主线程,配额也因浏览器和模式而异。二者只要脚本可访问,就可能被 XSS 读取;敏感会话凭证通常更适合设置了 HttpOnly、Secure 和合适 SameSite 的 Cookie,但仍需配合 CSRF/XSS 防护。
228. 使用js实现二分查找
答案:
前提是数组已按升序排列。每次比较中间元素,并把搜索区间缩小一半:
js
/**
* 找到目标值的任意一个下标,不存在则返回 -1。
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
function binarySearch(nums, target) {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
const mid = left + Math.floor((right - left) / 2);
const value = nums[mid];
if (value === target) return mid;
if (value < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}时间复杂度为 O(log n),额外空间为 O(1)。数组未排序时结果没有意义;存在重复值时该实现返回其中任意一个下标。若要求第一个或最后一个匹配位置,应在命中后继续向左或向右收缩边界。
229. 前端怎么实现跨域请求?
答案:
浏览器跨域请求的主流方案是 CORS:服务端针对允许的源返回 Access-Control-Allow-Origin 等响应头;非简单请求会先发送 OPTIONS 预检。携带 Cookie 时前端需设置 credentials: 'include',服务端允许凭证且 Allow-Origin 不能写 *。
其他方案包括:
- 同源后端代理或开发服务器代理:浏览器请求自己的域,服务端再转发目标接口。
- JSONP:利用
<script>不受普通 XHR 同源读取限制,只支持 GET,缺少现代错误处理与安全保障,属于遗留方案。 postMessage:用于页面、iframe、窗口之间跨源通信,不是通用 HTTP 请求代理。- 服务端到服务端请求:不受浏览器同源策略限制,但仍需做鉴权、超时和 SSRF 防护。
同源策略主要限制脚本读取跨源响应,不能靠前端设置 Access-Control-Allow-Origin 绕过;最终需要目标服务或受控代理配合。生产环境不应通过关闭浏览器安全策略解决。
230. 怎么实现图片懒加载?
答案:
优先使用原生懒加载:
html
<img src="photo.webp" loading="lazy" width="800" height="600" alt="...">需要更精确控制时,可把真实地址放在 data-src/data-srcset,用 IntersectionObserver 在图片接近视口时赋值:
js
const load = (img) => {
if (img.dataset.srcset) img.srcset = img.dataset.srcset;
if (img.dataset.src) img.src = img.dataset.src;
};
const observer = new IntersectionObserver((entries, io) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
load(entry.target);
io.unobserve(entry.target);
}
}, { rootMargin: '300px 0px' });
document.querySelectorAll('img[data-src]').forEach((img) => {
observer.observe(img);
});观察和加载处理总体为 O(n),浏览器负责高效计算相交关系。应预留 width/height 或 aspect-ratio 防止布局跳动;首屏关键图不要懒加载,可配合 fetchpriority="high"。老环境再退化为节流后的滚动位置检测。
231. 我现在有一个canvas,上面随机布着一些黑块,请实现方法,计算canvas上有多少个黑块。
答案:
把“黑块”定义为相互四向连通的黑色像素区域。先用 getImageData 判断每个像素是否为黑色,再对黑色像素做连通分量搜索:
js
/**
* @param {HTMLCanvasElement} canvas
* @param {number} threshold RGB 各通道不大于该值时视为黑色
* @return {number}
*/
function countBlackBlocks(canvas, threshold = 20) {
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const { width, height } = canvas;
const data = ctx.getImageData(0, 0, width, height).data;
const black = new Uint8Array(width * height);
for (let i = 0; i < width * height; i++) {
const p = i * 4;
black[i] = data[p + 3] > 0 &&
data[p] <= threshold &&
data[p + 1] <= threshold &&
data[p + 2] <= threshold ? 1 : 0;
}
let count = 0;
for (let start = 0; start < black.length; start++) {
if (!black[start]) continue;
count++;
black[start] = 0;
const stack = [start];
while (stack.length) {
const index = stack.pop();
const x = index % width;
const y = Math.floor(index / width);
const neighbors = [];
if (x > 0) neighbors.push(index - 1);
if (x + 1 < width) neighbors.push(index + 1);
if (y > 0) neighbors.push(index - width);
if (y + 1 < height) neighbors.push(index + width);
for (const next of neighbors) {
if (black[next]) {
black[next] = 0;
stack.push(next);
}
}
}
}
return count;
}时间和空间复杂度都是 O(width * height)。抗锯齿图片需要调整阈值,斜角也算连通时应加入四个对角邻居。若 Canvas 被无 CORS 授权的跨域图片污染,getImageData 会抛出安全异常。
232. 怎么解决canvas中获取跨域图片数据的问题?
答案:
Canvas 一旦绘制了未获 CORS 授权的跨域图片,就会变成 tainted canvas,之后调用 getImageData、toDataURL 或 toBlob 会抛出安全异常。正确做法是让图片服务器返回允许当前站点的 Access-Control-Allow-Origin,并在设置 src 之前声明跨域模式:
js
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
ctx.drawImage(img, 0, 0);
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
};
img.src = 'https://cdn.example.com/image.png';若必须携带凭证,可用 use-credentials,但服务端必须返回精确 Origin 并允许凭证。无法控制图片源时,应通过自己受控的后端下载并转发图片,同时做好地址白名单和 SSRF 防护。污染发生后无法通过前端代码“解除”,只能清空/重建绘图上下文并重新加载合规资源。
233. es5 中的类和es6中的class有什么区别?
答案:
ES5 通常用构造函数加原型模拟类;ES6 class 仍建立在原型机制上,但不仅是写法替换,还规定了更严格的语义:
class构造器不能普通调用,必须用new;ES5 构造函数本身可普通调用。- 类体始终处于严格模式。
- 类声明存在暂时性死区,不能像函数声明那样在声明前调用。
- 类原型方法默认不可枚举;直接给
Fn.prototype.method赋值创建的方法默认可枚举。 extends、super、静态方法、访问器等有标准语法,派生类必须在使用this前调用super()。- 现代 class 还支持实例字段、静态字段、私有字段和静态初始化块,这些并非简单的原型方法语法糖。
两者的实例方法通常都位于原型上,instanceof 和原型链模型一致。ES5 继承内建对象较困难,而 class extends 配合语言内部构造语义能正确处理许多内建类型。
234. 背包问题
有 N 件物品和一个容量是 V 的背包。每件物品有且只有一件。
第 i 件物品的体积是 v[i] ,价值是 w[i] 。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
示例 1:
输入: N = 3, V = 4, v = [4,2,3], w = [4,2,3]
输出: 4
解释: 只选第一件物品,可使价值最大。示例 2:
输入: N = 3, V = 5, v = [4,2,3], w = [4,2,3]
输出: 5
解释: 不选第一件物品,选择第二件和第三件物品,可使价值最大。答案:
这是 0/1 背包,每件物品最多选择一次。令 dp[j] 表示容量不超过 j 时的最大价值;处理每件物品时必须从大容量向小容量更新,防止同一件物品在本轮被重复使用。
js
/**
* @param {number} capacity
* @param {number[]} volumes
* @param {number[]} values
* @return {number}
*/
function knapsack01(capacity, volumes, values) {
if (volumes.length !== values.length) {
throw new TypeError('volumes 和 values 长度必须一致');
}
const dp = new Array(capacity + 1).fill(0);
for (let i = 0; i < volumes.length; i++) {
const volume = volumes[i];
const value = values[i];
for (let j = capacity; j >= volume; j--) {
dp[j] = Math.max(dp[j], dp[j - volume] + value);
}
}
return dp[capacity];
}
console.log(knapsack01(4, [4, 2, 3], [4, 2, 3])); // 4
console.log(knapsack01(5, [4, 2, 3], [4, 2, 3])); // 5时间复杂度为 O(NV),额外空间为 O(V)。容量和体积应为非负整数;若要还原具体选择了哪些物品,需要保存二维状态或额外记录决策。
235. 全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]示例 3:
输入:nums = [1]
输出:[[1]]提示:
- 1 <= nums.length <= 6
- -10 <= nums[i] <= 10
- nums 中的所有整数 互不相同
js
/**
* @param {number[]} nums
* @return {number[][]}
*/
var permute = function(nums) {
};答案:
用回溯固定当前位置:把每个尚未使用的元素交换到当前位置,递归处理下一位,返回后恢复数组。
js
/**
* @param {number[]} nums
* @return {number[][]}
*/
function permute(nums) {
const current = nums.slice();
const result = [];
function backtrack(index) {
if (index === current.length) {
result.push(current.slice());
return;
}
for (let i = index; i < current.length; i++) {
[current[index], current[i]] = [current[i], current[index]];
backtrack(index + 1);
[current[index], current[i]] = [current[i], current[index]];
}
}
backtrack(0);
return result;
}共有 n! 个结果,复制每个排列需要 O(n),所以时间复杂度为 O(n * n!),结果空间为 O(n * n!),递归栈为 O(n)。题目保证元素互不相同;若输入可能重复,需要在每一层用 Set 跳过相同值,否则会产生重复排列。空数组按该实现返回 [[]]。
236. ES6有哪些新特性?
答案:
ES6 即 ES2015,主要新增:
let、const、块级作用域和暂时性死区。- 箭头函数、默认参数、剩余参数和展开语法。
- 模板字符串、解构赋值、对象字面量增强和计算属性名。
class、extends、super。- 原生模块
import/export。 - Promise。
- 迭代器协议、
for...of和生成器。 Symbol。Map、Set、WeakMap、WeakSet。Proxy、Reflect。- 新的内建方法、二进制/八进制字面量、TypedArray 等标准化能力。
要注意版本边界:async/await 是 ES2017,BigInt、可选链、空值合并等也是后续版本特性,不能笼统归入 ES6。
237. jquery的链式调用是怎么实现的?
答案:
jQuery 方法大多返回当前 jQuery 包装对象,也就是 return this,所以下一个方法可以继续在同一组元素上执行:
js
function Wrapper(elements) {
this.elements = elements;
}
Wrapper.prototype.addClass = function (name) {
this.elements.forEach((el) => el.classList.add(name));
return this;
};
Wrapper.prototype.hide = function () {
this.elements.forEach((el) => { el.hidden = true; });
return this;
};
new Wrapper([...document.querySelectorAll('div')])
.addClass('active')
.hide();jQuery 的遍历型、设置型方法通常返回包装对象;查询型 getter 如无参数的 .text()、.attr('name') 会返回具体值,因此会终止链。筛选等产生新集合的方法会通过类似 pushStack 的机制保存前一个集合,.end() 再回到上一级。
238. 写一个 repeat 方法,实现字符串的复制拼接
答案:
可用倍增法减少循环次数,实现接近 String.prototype.repeat 的计数语义:
js
/**
* @param {*} value
* @param {*} count
* @return {string}
*/
function repeat(value, count) {
const text = String(value);
let times = Number(count);
if (Number.isNaN(times)) times = 0;
if (!Number.isFinite(times) || times < 0) {
throw new RangeError('count 必须是有限的非负数');
}
times = Math.trunc(times);
let result = '';
let chunk = text;
while (times > 0) {
if (times % 2 === 1) result += chunk;
times = Math.floor(times / 2);
if (times > 0) chunk += chunk;
}
return result;
}
repeat('ab', 3); // 'ababab'
repeat('x', 2.9); // 'xx'输出长度为 L = 字符串长度 * count,因此时间和返回值空间至少都是 O(L);倍增只把控制循环降为 O(log count)。负数和无穷大抛错,NaN 按 0 处理;超大结果仍可能因引擎字符串长度或内存限制而抛错。
239. 使用Promise实现:限制异步操作的并发个数,并尽可能快的完成全部
有8个图片资源的url,已经存储在数组urls中。
urls类似于['https://image1.png', 'https://image2.png', ....]
而且已经有一个函数function loadImg,输入一个url链接,返回一个Promise,该Promise在图片下载完成的时候resolve,下载失败则reject。
但有一个要求,任何时刻同时下载的链接数量不可以超过3个。
请写一段代码实现这个需求,要求尽可能快速地将所有图片下载完成。
js
var urls = [
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting1.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting2.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting3.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting4.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting5.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn6.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn7.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn8.png",
];
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
console.log("一张图片加载完成");
resolve(img);
};
img.onerror = function() {
reject(new Error('Could not load image at' + url));
};
img.src = url;
});
}答案:
建立固定数量的 worker。每个 worker 完成一个任务后立刻领取下一个任务,因此任意时刻不超过上限,同时不会等整批任务一起结束:
js
/**
* @template T, R
* @param {T[]} items
* @param {number} limit
* @param {(item: T, index: number) => Promise<R>} mapper
* @return {Promise<R[]>}
*/
async function mapLimit(items, limit, mapper) {
if (!Number.isInteger(limit) || limit < 1) {
throw new RangeError('limit 必须是正整数');
}
const results = new Array(items.length);
let nextIndex = 0;
async function worker() {
while (true) {
const index = nextIndex++;
if (index >= items.length) return;
results[index] = await mapper(items[index], index);
}
}
const workerCount = Math.min(limit, items.length);
await Promise.all(Array.from({ length: workerCount }, () => worker()));
return results;
}
mapLimit(urls, 3, loadImg)
.then((images) => console.log('全部完成', images))
.catch(console.error);调度开销为 O(n),结果数组和 worker 状态为 O(n + limit);总耗时取决于任务时长,worker 会持续补位。结果按原 URL 顺序保存。任一加载失败时返回的 Promise 会拒绝,但已启动的图片请求不会自动取消;若要取消或收集全部错误,需要让加载函数支持 AbortSignal,或在 worker 内捕获并保存失败结果。
240. 使用Promise封装一个异步加载图片的方法
答案:
在设置 src 前注册加载和失败回调,并用 Promise 表示最终状态:
js
/**
* @param {string} url
* @param {{ crossOrigin?: string, signal?: AbortSignal }} options
* @return {Promise<HTMLImageElement>}
*/
function loadImage(url, { crossOrigin, signal } = {}) {
return new Promise((resolve, reject) => {
const img = new Image();
const cleanup = () => {
img.onload = null;
img.onerror = null;
signal?.removeEventListener('abort', onAbort);
};
const onAbort = () => {
cleanup();
img.src = '';
reject(new DOMException('图片加载已取消', 'AbortError'));
};
img.onload = () => {
cleanup();
resolve(img);
};
img.onerror = () => {
cleanup();
reject(new Error(`图片加载失败: ${url}`));
};
if (signal?.aborted) return onAbort();
signal?.addEventListener('abort', onAbort, { once: true });
if (crossOrigin) img.crossOrigin = crossOrigin;
img.src = url;
});
}JS 调度开销是 O(1),实际时间和内存由网络及图片尺寸决定。需要把跨域图片绘制到 Canvas 并读取像素时,应传 crossOrigin: 'anonymous',且图片服务器必须返回正确的 CORS 响应头。
241. 实现mergePromise函数
实现mergePromise函数,把传进去的数组按顺序先后执行,并且把返回的数据先后放到数组data中。
js
const time = (timer) => {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, timer)
})
}
const ajax1 = () => time(2000).then(() => {
console.log(1);
return 1
})
const ajax2 = () => time(1000).then(() => {
console.log(2);
return 2
})
const ajax3 = () => time(1000).then(() => {
console.log(3);
return 3
})
function mergePromise () {
// 在这里写代码
}
mergePromise([ajax1, ajax2, ajax3]).then(data => {
console.log("done");
console.log(data); // data 为 [1, 2, 3]
});
// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]答案:
数组中的元素是返回 Promise 的函数,必须在前一个完成后才调用下一个:
js
/**
* @param {Array<() => Promise<any>>} tasks
* @return {Promise<any[]>}
*/
async function mergePromise(tasks) {
const data = [];
for (const task of tasks) {
data.push(await task());
}
return data;
}也可用 reduce 串接 Promise,但 for...of 更直观。对题目示例会依次输出 1、2、3、done 和 [1, 2, 3]。调度复杂度为 O(n),结果空间为 O(n),总耗时约为各任务耗时之和。任一任务拒绝时函数立即拒绝,后续任务不会启动;如需失败后继续,应在每轮 await 外单独捕获并记录错误。
242. 【Promise第40题】下面代码的输出是什么?
js
const p1 = new Promise((resolve) => {
setTimeout(() => {
resolve('resolve3');
console.log('timer1')
}, 0)
resolve('resovle1');
resolve('resolve2');
}).then(res => {
console.log(res)
setTimeout(() => {
console.log(p1)
}, 1000)
}).finally(res => {
console.log('finally', res)
})答案:
输出顺序为:
text
resovle1
finally undefined
timer1
Promise { <fulfilled>: undefined }最后一行的 Promise 展示格式因浏览器/Node.js 控制台而异,但状态为 fulfilled,结果是 undefined。代码中的首个值确实拼写为 resovle1。
Promise 状态只改变一次,所以构造器中的第一个 resolve('resovle1') 生效,后面的 resolve('resolve2') 和定时器里的 resolve('resolve3') 都被忽略。第一个 .then 作为微任务先输出该值并返回 undefined;finally 随后执行,而且回调不接收上一个结果,所以参数是 undefined。之后 0ms 定时器输出 timer1,约 1 秒后另一个定时器打印已兑现为 undefined 的 p1。
243. 【Promise第39题】下面代码的输出是什么?
js
const async1 = async () => {
console.log('async1');
setTimeout(() => {
console.log('timer1')
}, 2000)
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 end')
return 'async1 success'
}
console.log('script start');
async1().then(res => console.log(res));
console.log('script end');
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.catch(4)
.then(res => console.log(res))
setTimeout(() => {
console.log('timer2')
}, 1000)答案:
输出顺序为:
text
script start
async1
promise1
script end
1
timer2
timer1async1 立即执行到 await。被等待的 Promise 只输出 promise1,从未调用 resolve 或 reject,因此一直 pending,async1 end 和 async1 success 都不会出现。
.then(2)、.then(Promise.resolve(3)) 和 .catch(4) 的参数都不是函数,会被忽略并发生值透传,最终的函数型 .then 在微任务阶段输出 1。最后两个定时器按延迟分别在约 1 秒和 2 秒输出 timer2、timer1。
244. 【Promise第38题】下面代码的输出是什么?
js
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve(6);
console.log(p)
}, 0)
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);答案:
输出顺序为:
text
3
7
4
1
2
5
Promise { <fulfilled>: 1 }Promise 构造器同步执行,所以先输出 3、7;主脚本随后输出 4。内部 Promise p 已被 resolve(1) 兑现,外层 Promise 被 resolve(2) 兑现,它们的 .then 回调按注册产生的微任务顺序输出 1、2。
0ms 定时器随后输出 5。此时再次执行 resolve(6) 不能改变 p 已确定的状态,所以打印的仍是兑现值为 1 的 Promise;具体控制台外观依运行环境而异。
245. 【Promise第37题】下面代码的输出是什么?
js
async function async1 () {
try {
await Promise.reject('error!!!')
} catch(e) {
console.log(e)
}
console.log('async1');
return Promise.resolve('async1 success')
}
async1().then(res => console.log(res))
console.log('script start')答案:
输出顺序为:
text
script start
error!!!
async1
async1 success调用 async1() 时执行到 await Promise.reject(...) 后暂停,主脚本继续同步输出 script start。随后微任务恢复 async1,拒绝在函数内部被 try...catch 捕获,输出 error!!!,再输出 async1。函数返回的 Promise.resolve('async1 success') 被 async 返回值吸收,最终外部 .then 输出 async1 success。
246. 【Promise第36题】下面代码的输出是什么?
js
async function async1 () {
await async2();
console.log('async1');
return 'async1 success'
}
async function async2 () {
return new Promise((resolve, reject) => {
console.log('async2')
reject('error')
})
}
async1().then(res => console.log(res))答案:
可观察结果是:
text
async2
Uncaught (in promise) error第二行是运行环境报告的未处理 Promise 拒绝,具体文案可能是 UnhandledPromiseRejection 等。async2 的 Promise 构造器同步输出 async2,随后以字符串 error 拒绝。await async2() 会让 async1 也以同一原因拒绝,因此不会输出 async1,也不会返回 async1 success。调用处只注册了成功回调而没有拒绝处理,最终形成未处理拒绝;应追加 .catch(...) 或在 async1 中使用 try...catch。
247. 【Promise第35题】下面代码的输出是什么?
js
async function testSometing() {
console.log("执行testSometing");
return "testSometing";
}
async function testAsync() {
console.log("执行testAsync");
return Promise.resolve("hello async");
}
async function test() {
console.log("test start...");
const v1 = await testSometing();
console.log(v1);
const v2 = await testAsync();
console.log(v2);
console.log(v1, v2);
}
test();
var promise = new Promise(resolve => {
console.log("promise start...");
resolve("promise");
});
promise.then(val => console.log(val));
console.log("test end...");答案:
输出顺序为:
text
test start...
执行testSometing
promise start...
test end...
testSometing
执行testAsync
promise
hello async
testSometing hello asynctest() 先同步执行到第一个 await;testSometing() 的函数体同步输出,但其返回值由 async 包装成 Promise,因此后续代码进入微任务。主脚本继续创建 Promise,构造器同步输出 promise start...,最后输出 test end...。
微任务阶段先恢复 test,输出 testSometing 并调用 testAsync,其函数体同步输出。此前注册的 promise.then 已在队列中,因此先输出 promise;待 testAsync 返回的 Promise 被吸收后,再恢复 test,输出最后两行。
248. 【Promise第34题】下面代码的输出是什么?
js
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function() {
console.log("setTimeout");
}, 0);
async1();
new Promise(function(resolve) {
console.log("promise1");
resolve();
}).then(function() {
console.log("promise2");
});
console.log('script end')答案:
输出顺序为:
text
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout主脚本先同步输出到 script end。async2() 同步输出 async2 并返回已兑现的 Promise,await 后的 async1 end 因而作为微任务排队;后面显式 Promise 的 .then 再排入微任务,所以依次输出 async1 end、promise2。微任务清空后才执行 0ms 定时器,输出 setTimeout。
249. 【Promise第33题】下面代码的输出是什么?
js
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
resolve('promise resolve')
})
console.log('async1 success');
return 'async1 end'
}
console.log('srcipt start')
async1().then(res => {
console.log(res)
})
new Promise(resolve => {
console.log('promise2')
setTimeout(() => {
console.log('timer')
})
})答案:
输出顺序为:
text
srcipt start
async1 start
promise1
promise2
async1 success
async1 end
timer前四行是同步输出,原代码中的 srcipt 拼写会原样出现。await 等到内部 Promise 以 promise resolve 兑现后恢复,但这个值没有被赋给变量;恢复时输出 async1 success,并由函数自己的 return 'async1 end' 决定外部 .then 的值。外部回调随后输出 async1 end。最后才执行定时器并输出 timer。第二个 Promise 一直 pending 不影响其构造器中已经注册的定时器。
250. 【Promise第32题】下面代码的输出是什么?
js
async function async1 () {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1')
})
console.log('async1 success');
return 'async1 end'
}
console.log('srcipt start')
async1().then(res => console.log(res))
console.log('srcipt end')答案:
输出为:
text
srcipt start
async1 start
promise1
srcipt end原代码中的 srcipt 拼写会原样输出。传给 await 的 Promise 构造器同步执行并输出 promise1,但它没有调用 resolve 或 reject,所以永远保持 pending。async1 会一直暂停,后面的 async1 success、返回值以及外部 .then 都不会执行。单独存在 pending Promise 本身不会让浏览器事件循环一直运行。
251. 【Promise第31题】下面代码的输出是什么?
js
async function fn () {
// return await 1234
// 等同于
return 123
}
fn().then(res => console.log(res))答案:
输出:
text
123实际执行的语句是 return 123。async 函数会返回一个以 123 兑现的 Promise,因此 .then 回调在微任务阶段输出 123。如果改成注释中的 return await 1234,非 Promise 值也会先按已兑现 Promise 处理,最终值是 1234,但是否使用 await 可能影响恢复时机和错误栈。
252. 【Promise第30题】下面代码的输出是什么?
js
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
setTimeout(() => {
console.log('timer1')
}, 0)
}
async function async2() {
setTimeout(() => {
console.log('timer2')
}, 0)
console.log("async2");
}
async1();
setTimeout(() => {
console.log('timer3')
}, 0)
console.log("start")答案:
输出顺序为:
text
async1 start
async2
start
async1 end
timer2
timer3
timer1async1 和 async2 的同步部分先执行;async2 先注册 timer2。主脚本随后注册 timer3 并输出 start。async2() 返回的 Promise 已兑现,因此 await 的继续部分作为微任务输出 async1 end,并在此时才注册 timer1。微任务结束后,三个同为 0ms 的定时器按注册先后执行:timer2、timer3、timer1。
253. 【Promise第29题】下面代码的输出是什么?
js
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
setTimeout(() => {
console.log('timer')
}, 0)
console.log("async2");
}
async1();
console.log("start")答案:
输出顺序为:
text
async1 start
async2
start
async1 end
timer调用 async1 后先同步输出 async1 start,async2 注册定时器并同步输出 async2。await 暂停 async1,主脚本输出 start。随后微任务恢复 async1,输出 async1 end;定时器属于后续宏任务,最后输出 timer。
254. 【Promise第28题】下面代码的输出是什么?
js
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
console.log('start')答案:
输出顺序为:
text
async1 start
async2
start
async1 endasync1() 先同步输出 async1 start,然后调用 async2();后者同步输出 async2 并返回一个以 undefined 兑现的 Promise。await 让 async1 的后续部分暂停,主脚本继续输出 start。当前同步任务结束后,微任务恢复 async1,输出 async1 end。
255. 【Promise第27题】下面代码的输出是什么?
js
function runAsync(x) {
const p = new Promise(r =>
setTimeout(() => r(x, console.log(x)), 1000)
);
return p;
}
function runReject(x) {
const p = new Promise((res, rej) =>
setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)
);
return p;
}
Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)])
.then(res => console.log("result: ", res))
.catch(err => console.log(err));答案:
按时间顺序输出:
text
0
Error: 0
1
2
3runReject(0) 的定时器最先到期。调用 rej 前会先求值参数 console.log(0),所以输出 0,随后 race 得到拒绝结果,.catch 输出字符串 Error: 0。约 1 秒后三个 runAsync 的定时器仍会执行并依次输出 1、2、3。
Promise 的 resolve/reject 只采用第一个参数,表达式里的第二个参数只是为了触发 console.log。Promise.race 只决定自身采用最先确定的结果,不会取消其余异步操作。相同到期时间的定时器通常按注册顺序处理。
256. 【Promise第26题】下面代码的输出是什么?
js
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
function runReject (x) {
const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
return p
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
.then(res => console.log(res))
.catch(err => console.log(err))答案:
按时间顺序输出:
text
// 约 1 秒
1
3
// 约 2 秒
2
Error: 2
// 约 4 秒
4两个成功任务都在约 1 秒到期,注册顺序使其通常依次输出 1、3。runReject(2) 在约 2 秒时先输出 2,再以字符串 Error: 2 拒绝;Promise.all 立即采用这个拒绝,.catch 随后的微任务输出 Error: 2,所以不会输出结果数组。
约 4 秒时另一个拒绝任务仍会输出 4,但 Promise.all 的状态已经确定,不会再次触发外层 catch。Promise.all 的快速失败不会取消已启动的任务;resolve/reject 的额外参数也会被忽略。
257. 【Promise第25题】下面代码的输出是什么?
js
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.all([runAsync(1), runAsync(2), runAsync(3)])
.then(res => console.log(res))答案:
输出(约 1 秒后):
text
1
2
3
[1, 2, 3]三个定时器按注册顺序到期。调用 r(x, console.log(x)) 前会先求值实参,所以先打印 x,而 resolve 只使用第一个参数 x。三个 Promise 全部兑现后,Promise.all 的回调以微任务执行;结果数组始终按输入顺序排列,而不是按完成先后排列。
258. 【Promise第24题】下面代码的输出是什么?
js
function promise1 () {
let p = new Promise((resolve) => {
console.log('promise1');
resolve('1')
})
return p;
}
function promise2 () {
return new Promise((resolve, reject) => {
reject('error')
})
}
promise1()
.then(res => console.log(res))
.catch(err => console.log(err))
.finally(() => console.log('finally1'))
promise2()
.then(res => console.log(res))
.catch(err => console.log(err))
.finally(() => console.log('finally2'))答案:
输出:
text
promise1
1
error
finally1
finally2Promise 构造器立即同步执行,因此先打印 promise1。随后两条链的反应以微任务交错执行:第一条链的 then 打印 1,第二条链的拒绝先透过缺少拒绝处理器的 then,再由 catch 打印 error。两个处理器完成后才分别使后续 finally 可执行,所以最后依次打印 finally1、finally2。finally 无论前序成功还是失败都会执行。
259. 【Promise第23题】下面代码的输出是什么?
js
Promise.resolve('1')
.then(res => {
console.log(res)
})
.finally(() => {
console.log('finally')
})
Promise.resolve('2')
.finally(() => {
console.log('finally2')
return '我是finally2返回的值'
})
.then(res => {
console.log('finally2后面的then函数', res)
})答案:
输出:
text
1
finally2
finally
finally2后面的then函数 2两个已兑现 Promise 首先各排入一个微任务,所以第一条链先打印 1,第二条链再打印 finally2。第一条链的 then 完成后才排入它的 finally,因此它随后打印。finally 的普通返回值不会替换原 Promise 的结果,所以第二条链最后的 then 收到的仍是字符串 2,不是 finally2 返回的新字符串。只有 finally 抛错或返回拒绝的 Promise 时,链才会改为拒绝。
260. 【Promise第22题】下面代码的输出是什么?
js
Promise.resolve()
.then(function success (res) {
throw new Error('error!!!')
}, function fail1 (err) {
console.log('fail1', err)
}).catch(function fail2 (err) {
console.log('fail2', err)
})答案:
输出:
text
fail2 Error: error!!!初始 Promise 已兑现,因此执行 success,它抛出的异常会使该 then 返回的新 Promise 变为拒绝。fail1 与 success 属于同一次 then,只能处理进入该 then 之前已有的拒绝,不能捕获另一个参数执行时产生的错误;后续 catch 的 fail2 才会捕获它。错误堆栈的具体文本因运行环境而异。
261. 【Promise第21题】下面代码的输出是什么?
js
Promise.reject('err!!!')
.then((res) => {
console.log('success', res)
}, (err) => {
console.log('error', err)
}).catch(err => {
console.log('catch', err)
})答案:
输出:
text
error err!!!Promise.reject 使 then 调用第二个参数,即拒绝处理器。该处理器正常返回 undefined,所以 then 产生的新 Promise 转为兑现状态,后面的 catch 不会执行。只有拒绝处理器再次抛错或返回拒绝的 Promise,catch 才会运行。
262. 【Promise第20题】下面代码的输出是什么?
js
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)答案:
输出:
text
1then 的参数必须是函数。数字 2 和 Promise.resolve(3) 都不是函数,会被当作缺失的处理器;兑现值因此原样向后透传,最终由 console.log 打印 1。传入一个 Promise 对象本身不会让链等待它,必须传函数,例如 .then(() => Promise.resolve(3))。
263. 【Promise第19题】下面代码的输出是什么?
js
const promise = Promise.resolve().then(() => {
return promise;
})
promise.catch(console.err)答案:
promise 的处理器返回了 promise 自身,形成 Promise 链式循环,规范要求将它拒绝为 TypeError,常见信息是:
text
TypeError: Chaining cycle detected for promise #<Promise>代码还有一个拼写问题:标准控制台方法是 console.error,console.err 通常为 undefined。非函数的 catch 参数等价于没有拒绝处理器,所以不会主动打印该错误,宿主最终通常报告未处理的 Promise 拒绝。若改成 promise.catch(console.error),则会明确打印上述 TypeError。
264. 【Promise第18题】下面代码的输出是什么?
js
Promise.resolve().then(() => {
return new Error('error!!!')
}).then(res => {
console.log("then: ", res)
}).catch(err => {
console.log("catch: ", err)
})答案:
输出:
text
then: Error: error!!!return new Error('error!!!') 只是返回一个普通对象,不等于抛出异常。then 返回的新 Promise 会以这个 Error 对象正常兑现,因此下一个 then 执行,catch 跳过。要进入 catch,应使用 throw new Error(...) 或 return Promise.reject(new Error(...))。控制台对 Error 对象的具体展示格式可能不同。
265. 【Promise第17题】下面代码的输出是什么?
js
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('timer')
resolve('success')
}, 1000)
})
const start = Date.now();
promise.then(res => {
console.log(res, Date.now() - start)
})
promise.then(res => {
console.log(res, Date.now() - start)
})答案:
输出顺序:
text
timer
success <耗时1>
success <耗时2>两个耗时通常都略大于或等于 1000 毫秒,具体数字不能写死。Promise 构造器只执行一次,定时器到期后打印 timer 并兑现 Promise;注册在同一 Promise 上的两个 then 随后按注册顺序在同一次微任务检查点执行,都得到同一个值 success。定时器延迟只是最短等待时间,系统调度和主线程阻塞都会增大实际耗时。
266. 【Promise第16题】下面代码的输出是什么?
js
Promise.reject(1)
.then(res => {
console.log(res);
return 2;
})
.catch(err => {
console.log(err);
return 3
})
.then(res => {
console.log(res);
});答案:
输出:
text
1
3初始 Promise 被拒绝,首个 then 没有拒绝处理器,因此拒绝继续传给 catch。catch 打印原因 1 并返回 3;普通返回值会使它产生的新 Promise 以 3 兑现,所以最后一个 then 打印 3。
267. 【Promise第15题】下面代码的输出是什么?
js
Promise.resolve(1)
.then(res => {
console.log(res);
return 2;
})
.catch(err => {
return 3;
})
.then(res => {
console.log(res);
});答案:
输出:
text
1
2初始 Promise 以 1 兑现,第一个 then 打印 1 并返回 2。每次 then/catch 都返回一个新的 Promise,普通返回值等价于让新 Promise 以该值兑现;由于链上没有拒绝,catch 被跳过,最后一个 then 收到并打印 2。
268. 【Promise第14题】下面代码的输出是什么?
js
const promise = new Promise((resolve, reject) => {
reject("error");
resolve("success2");
});
promise
.then(res => {
console.log("then1: ", res);
}).then(res => {
console.log("then2: ", res);
}).catch(err => {
console.log("catch: ", err);
}).then(res => {
console.log("then3: ", res);
})答案:
输出:
text
catch: error
then3: undefinedPromise 状态只能确定一次,先执行的 reject('error') 生效,后续 resolve('success2') 被忽略。拒绝会穿过两个仅有成功处理器的 then,由 catch 处理。catch 只打印而没有返回值,所以它返回的新 Promise 以 undefined 兑现,最后的 then 因而执行并打印 undefined。
269. 【Promise第13题】下面代码的输出是什么?
js
const promise = new Promise((resolve, reject) => {
resolve("success1");
reject("error");
resolve("success2");
});
promise
.then(res => {
console.log("then: ", res);
}).catch(err => {
console.log("catch: ", err);
})答案:
输出:
text
then: success1Promise 从 pending 变为 fulfilled 或 rejected 后便不可逆。第一次 resolve('success1') 已确定状态和值,后续 reject('error') 和 resolve('success2') 都没有效果,所以执行成功处理器,catch 不执行。
270. 【Promise第12题】下面代码的输出是什么?
js
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success");
console.log("timer1");
}, 1000);
console.log("promise1里的内容");
});
const promise2 = promise1.then(() => {
throw new Error("error!!!");
});
console.log("promise1", promise1);
console.log("promise2", promise2);
setTimeout(() => {
console.log("timer2");
console.log("promise1", promise1);
console.log("promise2", promise2);
}, 2000);答案:
逻辑输出顺序:
text
promise1里的内容
promise1 Promise { <pending> }
promise2 Promise { <pending> }
timer1
[宿主报告未处理的 Error: error!!!]
timer2
promise1 Promise { <fulfilled>: 'success' }
promise2 Promise { <rejected>: Error: error!!! }构造器同步打印第一行,两个 Promise 此时都未完成。约 1 秒后定时器先调用 resolve,但 Promise 处理器是微任务,必须等当前定时器回调继续打印 timer1 并结束后才执行;该处理器抛错,使 promise2 拒绝。约 2 秒时,promise1 已兑现、promise2 已拒绝。未处理拒绝的报告时机、错误堆栈以及 DevTools 对 Promise 的显示格式由宿主决定;某些控制台还会延迟展开对象,因此不能依赖其视觉快照。
271. 【Promise第11题】下面代码的输出是什么?
js
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
console.log('promise1', promise1)
console.log('promise2', promise2)
}, 2000)答案:
逻辑输出顺序:
text
promise1 Promise { <pending> }
promise2 Promise { <pending> }
[宿主报告未处理的 Error: error!!!]
promise1 Promise { <fulfilled>: 'success' }
promise2 Promise { <rejected>: Error: error!!! }开始时两个 Promise 都是 pending。约 1 秒后 promise1 兑现,其 then 在随后的微任务中抛错,于是 promise2 被拒绝;约 2 秒后的定时器看到前者已兑现、后者已拒绝。未处理拒绝的提示通常出现在两次定时器之间,但其准确时机、堆栈文字及 Promise 的控制台展示属于宿主行为。
272. 【Promise第10题】下面代码的输出是什么?
js
Promise.resolve().then(() => {
console.log('promise1');
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
});
const timer1 = setTimeout(() => {
console.log('timer1')
Promise.resolve().then(() => {
console.log('promise2')
})
}, 0)
console.log('start');答案:
输出:
text
start
promise1
timer1
promise2
timer2主脚本先打印 start。脚本任务结束后清空微任务队列,打印 promise1,并在此时才注册 timer2;先前已注册的 timer1 因而先执行。timer1 中创建的 Promise 微任务会在该定时器任务结束后、下一个定时器任务开始前执行,所以先打印 promise2,最后才是 timer2。
273. 【Promise第九题】下面两段代码分别输出什么?
代码一:
js
setTimeout(() => {
console.log('timer1');
setTimeout(() => {
console.log('timer3')
}, 0)
}, 0)
setTimeout(() => {
console.log('timer2')
}, 0)
console.log('start')代码二:
js
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => {
console.log('promise')
})
}, 0)
setTimeout(() => {
console.log('timer2')
}, 0)
console.log('start')答案:
代码一输出:
text
start
timer1
timer2
timer3timer1、timer2 在主脚本中依次注册;timer3 只有执行 timer1 时才注册,因此排在 timer2 后。
代码二输出:
text
start
timer1
promise
timer2timer1 执行时产生的是 Promise 微任务。每个任务结束后,事件循环都会先清空微任务队列,再取下一个定时器任务,所以 promise 位于 timer1 与 timer2 之间。
274. 【Promise第八题】下面代码的输出是什么?
js
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);答案:
输出:
text
1
2
4
timerStart
timerEnd
successPromise 构造器同步执行,所以先打印 1、2,主脚本再打印 4。定时器任务开始后打印 timerStart;resolve('success') 只把对应反应排入微任务队列,不会打断当前回调,因此继续打印 timerEnd。定时器回调结束后才执行 then 微任务并打印 success。
275. 【Promise第七题】下面代码的输出是什么?
js
console.log('start')
setTimeout(() => {
console.log('time')
})
Promise.resolve().then(() => {
console.log('resolve')
})
console.log('end')答案:
输出:
text
start
end
resolve
time主脚本中的同步日志先执行。Promise.then 回调属于微任务,setTimeout 回调属于后续任务;当前脚本任务结束后先清空微任务队列,所以 resolve 早于 time。即使定时器省略延迟或写成 0,也不会在当前调用栈内同步执行。
276. 【Promise第六题】下面代码的输出是什么?
js
const fn = () =>
new Promise((resolve, reject) => {
console.log(1);
resolve("success");
});
console.log("start");
fn().then(res => {
console.log(res);
});答案:
输出:
text
start
1
success定义 fn 时不会创建 Promise。先同步打印 start,随后调用 fn() 才执行 Promise 构造器并同步打印 1、完成兑现;then 回调仍要作为微任务在当前脚本结束后执行,因此最后打印 success。
277. 【Promise第四题】下面代码的输出是什么?
javascript
const promise1 = new Promise((resolve, reject) => {
console.log('promise1')
resolve('resolve1')
})
const promise2 = promise1.then(res => {
console.log(res)
})
console.log('1', promise1);
console.log('2', promise2);答案:
逻辑输出:
text
promise1
1 Promise { <fulfilled>: 'resolve1' }
2 Promise { <pending> }
resolve1Promise 构造器同步打印并立即兑现 promise1。调用 promise1.then(...) 会立刻返回新的 promise2,但处理器尚未运行,所以它仍是 pending。主脚本的两次日志结束后,微任务打印 resolve1,随后 promise2 以处理器的返回值 undefined 兑现。Promise 在不同控制台中的格式、延迟展开效果可能不同。
278. 【Promise第二题】下面代码的输出是什么?
js
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve('success')
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);答案:
输出:
text
1
2
4
3Promise 构造器是同步调用的,resolve 只确定 Promise 状态,不会立即执行 then,因此构造器内仍继续打印 2,主脚本打印 4。调用栈清空后才执行 then 微任务并打印 3。
279. 【Promise第一题】下面代码的输出是什么?
js
const promise1 = new Promise((resolve, reject) => {
console.log('promise1')
})
console.log('1', promise1);答案:
逻辑输出:
text
promise1
1 Promise { <pending> }Promise 构造器在创建时同步执行,所以先打印 promise1。执行器既没有调用 resolve 也没有调用 reject,该 Promise 会一直保持 pending;控制台对 Promise 对象的具体显示格式因环境而异。
280. 怎么实现一个扫描二维码登录PC网站的需求?
答案:
二维码登录的核心是用一个短时、一次性的登录会话,把已登录手机上的用户身份,经用户确认后授权给 PC 会话;二维码本身不应包含账号、长期 Token 或可直接登录的凭据。
- PC 向服务端申请二维码会话。服务端生成高熵随机
qrId,记录WAITING、过期时间和 PC 会话绑定信息,二维码只编码 HTTPS 地址及qrId。 - PC 通过 WebSocket/SSE 等待状态变化;不方便保持长连接时可短轮询。状态至少包括
WAITING、SCANNED、CONFIRMED、CANCELED、EXPIRED。 - 已登录手机扫描后,将自身认证信息和
qrId发给服务端。服务端校验二维码未过期、未使用,将其标记为SCANNED,手机展示 PC 设备、地点等信息供用户确认。扫码本身不能直接完成登录。 - 用户在手机确认后,服务端原子地把会话改为
CONFIRMED并绑定用户。PC 收到通知后,用原 PC 会话领取一次性授权结果,服务端再通过HttpOnly、Secure、合适SameSite的 Cookie 建立 PC 登录态。 qrId和领取凭据必须短时有效、不可预测、只能消费一次,并绑定发起它的 PC 会话;所有接口使用 TLS,确认接口要防 CSRF、重放和并发重复确认,并做限流、审计以及取消/过期处理。
WebSocket 只优化通知,不承担认证;认证结论必须以服务端的原子状态和会话校验为准。
281. js中的undefined和 ReferenceError: xxx is not defined 有什么区别?
答案:
undefined 是 JavaScript 的一个原始值,表示“没有值”,例如变量已声明但未赋值、读取对象不存在的属性、函数没有返回值:
js
let x; // x 的值是 undefined
({}).missing; // undefined
function f() {}
f(); // undefinedReferenceError: xxx is not defined 是异常,表示标识符解析失败,当前作用域链中根本找不到该绑定:
js
console.log(notDeclared); // ReferenceError边界上,typeof notDeclared 是特例,会得到字符串 'undefined';但访问 let/const 的暂时性死区仍会抛 ReferenceError。另外,“变量存在且值为 undefined”与“对象没有该属性”都可能读出 undefined,要区分属性是否存在应使用 Object.hasOwn(obj, key) 或 key in obj。
282. Math.ceil()、Math.round()、Math.floor()三者的区别是什么?
答案:
Math.ceil(x):向正无穷方向取整,返回大于等于x的最小整数。Math.floor(x):向负无穷方向取整,返回小于等于x的最大整数。Math.round(x):取最近整数;恰好相差0.5时向正无穷方向取。
js
Math.ceil(1.2) // 2
Math.floor(1.8) // 1
Math.round(1.5) // 2
Math.round(-1.5) // -1
Math.ceil(-1.8) // -1
Math.floor(-1.2) // -2三者都返回 Number,不会修复浮点数表示误差;传入不可转换为数字的值会得到 NaN。Math.round(-0.5) 的结果还是 JavaScript 中可区分的 -0。
283. 使用js生成1-10000的数组
答案:
可直接用 Array.from:
js
const numbers = Array.from({ length: 10_000 }, (_, index) => index + 1);
console.log(numbers[0]); // 1
console.log(numbers.at(-1)); // 10000
console.log(numbers.length); // 10000时间复杂度为 O(n),额外空间复杂度为 O(n),这里 n = 10000。若只需要逐个消费而不需要真正的数组,可用生成器按需产生值,将额外空间降为 O(1);数组长度必须是非负且不能超过运行时允许的最大数组长度。
284. 解释下如下代码的意图:Array.prototype.slice.apply(arguments)
答案:
它的意图是借用 Array.prototype.slice,把类数组 arguments 浅拷贝成真正的数组:
js
function example() {
const args = Array.prototype.slice.apply(arguments);
return Array.isArray(args);
}
example(1, 2); // trueapply 把 arguments 设为 slice 的 this;没有传第二个参数,相当于执行 slice()。更常见的旧式写法是 Array.prototype.slice.call(arguments)。现代代码优先用剩余参数 function f(...args) {} 或 Array.from(arguments)。转换是浅拷贝,嵌套对象仍共享引用;箭头函数没有自己的 arguments。
285. js中数组是如何在内存中存储的?
答案:
ECMAScript 规范只把数组定义为具有特殊 length 行为的对象,并不规定它在物理内存中必须连续、必须使用哈希表,或具体放在“栈/堆”的哪一块;这些都是引擎实现细节。
以 V8 一类现代引擎为例,密集数组通常使用专门的 elements backing store,并按元素类型区分小整数、双精度数和普通对象引用;这种“快速元素”适合按索引连续访问。数组出现空洞、极稀疏的大索引或频繁改变结构时,可能转换元素种类,甚至进入字典模式,用更适合稀疏键值的结构存储。数组上的非索引属性通常走对象属性存储,与索引元素分开。
因此实践边界是:
- 密集、同类型、从
0连续增长的数组通常最利于优化。 delete arr[i]会制造空洞,删除元素通常应使用splice;稀疏键值更适合Map或普通对象。- 不要依赖某个 V8 版本的内部布局;其他引擎和同一引擎的新版本都可能不同。
- “原始值一定在栈、对象一定在堆”只是教学简化,优化编译器可能内联、逃逸分析或用立即数表示,不能当成语言保证。
286. 直接在script标签中写 export 为什么会报错?
答案:
普通 <script> 会按 classic script 语法解析,而 import/export 只允许出现在 ECMAScript 模块中,因此直接写 export 会产生语法错误。应声明模块:
html
<script type="module">
export const value = 42;
</script>或加载外部模块:
html
<script type="module" src="./main.js"></script>模块默认使用严格模式、拥有独立作用域,并默认延后执行;外部模块还受 CORS 规则约束。旧浏览器不识别模块,需要构建工具输出兼容脚本。内联模块中的导出也不会自动成为全局变量,其他模块必须通过模块 URL 导入它。
287. 【Promise第三题】下面代码的输出是什么?
js
const promise = new Promise((resolve, reject) => {
console.log(1);
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);答案:
输出:
text
1
2
4Promise 构造器同步执行,所以打印 1、2,随后主脚本打印 4。执行器从未调用 resolve 或 reject,Promise 会一直保持 pending,注册的 then 因此永远不会被调度,3 不会输出。
288. 【Promise第五题】下面代码的输出是什么?
js
const fn = () => (new Promise((resolve, reject) => {
console.log(1);
resolve('success')
}))
fn().then(res => {
console.log(res)
})
console.log('start')答案:
输出:
text
1
start
success调用 fn() 时立即创建 Promise,构造器同步打印 1 并兑现。then 的回调不会同步执行,而是排入微任务队列,因此当前脚本继续打印 start;调用栈清空后才打印 success。
289. js 中的倒计时,怎么实现纠正偏差?
答案:
不要每次回调都简单执行“剩余秒数减一”,而应保存绝对截止时间,每次触发时用当前时间重新计算剩余量。这样单次延迟不会累计:
js
function startCountdown(durationMs, render, done = () => {}) {
const endAt = Date.now() + Math.max(0, durationMs);
let timerId;
let stopped = false;
function tick() {
if (stopped) return;
const remaining = Math.max(0, endAt - Date.now());
render(remaining);
if (remaining === 0) {
done();
return;
}
// 对齐下一秒边界;即使本次晚执行,下次也按真实时间重算。
timerId = setTimeout(tick, Math.min(1000, remaining % 1000 || 1000));
}
tick();
return () => {
stopped = true;
clearTimeout(timerId);
};
}
const cancel = startCountdown(10_000, ms => {
console.log(Math.ceil(ms / 1000));
});每次 tick 的时间和额外空间都是 O(1),总时间与触发次数成正比。页面进入后台时浏览器会强力节流定时器,但重新激活后该实现会立即按真实时间校正。若倒计时涉及抢购、Token 过期等安全规则,应以服务端截止时间为准,并估算服务器与客户端的时钟差;客户端显示不能作为最终判定。
290. Math.ceil 和 Math.floor 有什么区别?
答案:
Math.ceil(x) 向正无穷取整,返回大于等于 x 的最小整数;Math.floor(x) 向负无穷取整,返回小于等于 x 的最大整数。
js
Math.ceil(2.1) // 3
Math.floor(2.9) // 2
Math.ceil(-2.9) // -2
Math.floor(-2.1) // -3负数最容易混淆:ceil 不是“绝对值变大”,floor 也不是简单截掉小数。若要朝 0 截断应使用 Math.trunc。
291. 怎么使用 setTimeout 实现 setInterval?
答案:
用递归 setTimeout 在一次回调结束后再安排下一次,并返回取消函数:
js
function createInterval(fn, delay) {
let active = true;
let timerId;
function tick() {
if (!active) return;
fn();
if (active) timerId = setTimeout(tick, delay);
}
timerId = setTimeout(tick, delay);
return function cancel() {
active = false;
clearTimeout(timerId);
};
}
const cancel = createInterval(() => console.log(Date.now()), 1000);
// cancel();每次调度的时间和空间复杂度均为 O(1),异步回调不会形成同步递归栈。它与原生 setInterval 的语义有意不同:间隔从上一次同步回调结束后开始,因此不会让多个 tick 因回调耗时而排队,但会产生漂移。若 fn 返回 Promise 且要求任务不重叠,应把 tick 声明为 async 并在再次调度前 await fn(),同时处理拒绝。
292. 怎么使用 js 实现拖拽功能?
答案:
现代实现优先使用 Pointer Events,它能统一鼠标、触控和触控笔,并用指针捕获避免指针移出元素后丢失移动事件:
html
<div id="drag">拖动我</div>
<style>
#drag {
width: 100px; height: 100px; background: #4b8;
touch-action: none; user-select: none; cursor: grab;
}
</style>
<script>
const el = document.querySelector('#drag');
let dragging = false;
let startX = 0, startY = 0;
let baseX = 0, baseY = 0;
let nextX = 0, nextY = 0;
el.addEventListener('pointerdown', event => {
if (!event.isPrimary) return;
dragging = true;
startX = event.clientX;
startY = event.clientY;
el.setPointerCapture(event.pointerId);
});
el.addEventListener('pointermove', event => {
if (!dragging || !event.isPrimary) return;
nextX = baseX + event.clientX - startX;
nextY = baseY + event.clientY - startY;
el.style.transform = `translate(${nextX}px, ${nextY}px)`;
});
function finish(event) {
if (!dragging || !event.isPrimary) return;
dragging = false;
baseX = nextX;
baseY = nextY;
}
el.addEventListener('pointerup', finish);
el.addEventListener('pointercancel', finish);
</script>每个事件的计算与额外空间都是 O(1)。transform 通常比反复修改 left/top 更适合高频移动。真实项目还要按容器的 getBoundingClientRect() 限制边界,并考虑滚动、缩放、键盘可访问性、吸附和碰撞;复杂排序或画布拖拽应使用成熟库。
293. Js 动画与 CSS 动画区别及相应实现
答案:
CSS 动画主要通过 transition 和 @keyframes 声明,适合状态明确、时间轴固定的过渡,例如淡入、位移和悬停反馈。代码简洁,浏览器能统一调度;只改变 transform、opacity 时常可在合成阶段完成。
JavaScript 动画通常使用 requestAnimationFrame,也可使用 Web Animations API。它适合拖拽跟手、物理模拟、路径计算、多个动画动态编排及需要随时暂停/反转的场景。requestAnimationFrame 与屏幕刷新同步,但回调及样式计算仍会占用主线程。不要用 setInterval 驱动逐帧动画。
性能并非简单的“CSS 一定比 JS 快”:关键在于每帧改了哪些属性、是否触发布局/绘制、主线程工作量以及动画能否交给合成线程。简单视觉过渡优先 CSS;复杂交互选 JS/WAAPI,并尽量只改 transform/opacity、避免布局抖动,尊重 prefers-reduced-motion。
294. 异步编程有哪些实现方式?
答案:
JavaScript 常见异步组织方式有:
- 回调函数:底层 API 和 Node.js 中常见,直接但容易出现嵌套、重复错误处理和控制反转。
- 事件/发布订阅:适合 DOM 交互、长连接和一个来源多次发值;需要显式解绑,避免泄漏。
- Promise:表示一次异步结果,支持链式组合、统一的拒绝传播,以及
all、race、allSettled、any。 - async/await:建立在 Promise 上,让顺序流程和
try/catch/finally更清晰;await会暂停当前异步函数,不会阻塞 JavaScript 线程。互不依赖的任务不要逐个await,应先并发启动再组合等待。 - Generator + 执行器:通过
yield暂停/恢复,曾用于组织异步流程,现代业务通常由async/await取代。 - 异步迭代器、Stream、Observable:适合多次、持续或带背压的数据流;Observable 通常来自 RxJS 等库。
Web Worker/Worker Threads 是把计算放到其他线程的并行机制,不只是异步语法;它们通过消息传递通信,适合 CPU 密集任务。无论选哪种方式,还要设计错误传播、超时、取消(如 AbortController)和资源清理。
295. offsetWidth/offsetHeight,clientWidth/clientHeight 与 scrollWidth/scrollHeight 的区别?
答案:
三组属性都是只读的布局尺寸,通常返回 CSS 像素整数,但统计范围不同:
| 属性 | 包含 | 不包含 | 主要用途 |
|---|---|---|---|
offsetWidth/Height | 内容、padding、border、占位的滚动条 | margin、溢出到盒外的内容 | 元素 border box 的布局尺寸 |
clientWidth/Height | 内容和 padding | border、margin、滚动条 | 内部可视区域尺寸 |
scrollWidth/Height | 内容和 padding,包括不可见的溢出内容 | border、margin | 完整可滚动内容尺寸 |
没有溢出时,scrollWidth/Height 通常等于 clientWidth/Height。可用 el.scrollHeight > el.clientHeight 判断是否存在纵向溢出,但还要考虑取整误差。
这些属性描述布局尺寸,不反映 CSS transform 后的视觉大小;需要小数、位置或变换后的矩形时用 getBoundingClientRect()。根元素、body、隐藏元素及滚动条样式存在特殊行为,例如 display: none 的元素通常得到 0。读取布局尺寸还可能迫使浏览器同步计算布局,高频循环中应避免交替读写样式。
296. toPrecision 和 toFixed 和 Math.round 有什么区别?
答案:
num.toPrecision(n):保留n位有效数字,返回字符串;可能使用指数形式。num.toFixed(n):保留小数点后n位,返回字符串,并补足末尾的零。Math.round(num):取最近的整数,返回数字;恰好在两个整数中间时向正无穷方向取。
js
(12.345).toPrecision(4) // '12.35'
(12.345).toFixed(2) // '12.35'
(12.3).toFixed(2) // '12.30'
Math.round(12.5) // 13
Math.round(-12.5) // -12toPrecision/toFixed 是格式化方法,不应直接当作精确十进制运算;二进制浮点表示可能产生看似意外的舍入结果。金额等严格十进制场景应使用整数最小单位或十进制库。
297. 什么是 Polyfill ?
答案:
Polyfill 是在旧运行环境缺少某个标准 API 时,提供兼容实现的代码。它通常先做能力检测,再补到全局对象或原型上,例如为缺少 Array.prototype.includes 的环境实现同名方法。
边界是:Polyfill 只能补可由现有能力模拟的 API,不能真正补齐新语法解析能力、底层浏览器特性或完全一致的性能/安全语义。新语法通常由 Babel 等转译器转换;不修改全局、单独导入使用的兼容实现常称为 ponyfill。应按目标浏览器和实际使用按需引入,避免重复代码、污染全局,并保证实现符合规范的异常和边界行为。
298. 怎么检测浏览器版本?
答案:
业务代码应优先做功能检测,而不是根据浏览器名称和版本分支:
js
if ('IntersectionObserver' in globalThis) {
// 使用该能力
} else {
// 降级或加载 polyfill
}如果统计、诊断或服务端内容协商确实需要浏览器版本,可在支持的浏览器中读取 User-Agent Client Hints(navigator.userAgentData),或使用维护良好的 UA 解析库处理 navigator.userAgent;服务端可请求相应 Client Hints。
版本检测没有绝对可靠方案:UA 可伪造、格式会变,Client Hints 可能不可用且高熵信息需要异步授权/服务端声明,WebView 也常有自定义标识。因此不要用版本号推断能力,也不要再依赖 ActiveXObject 等历史私有特征;以能力检测和渐进增强为主。
299. 什么是“前端路由”?什么时候适合使用“前端路由”?“前端路由”有哪些优点和缺点?
答案:
前端路由是由浏览器端 JavaScript 把 URL 映射为页面组件和状态,在不整页刷新的情况下更新视图。常见实现有两种:Hash 模式监听 hashchange;History 模式使用 history.pushState/replaceState 并监听 popstate。History 模式的直接访问和刷新需要服务器把未知前端路径回退到应用入口。
它适合后台系统、编辑器、邮箱等交互密集的 SPA,页面间共享大量状态且需要快速局部切换的场景。以独立文档、首屏速度和搜索收录为核心时,可优先 MPA,或结合 SSR/SSG 与客户端 hydration,而不是只做纯客户端路由。
**优点:**切换时无需重载整页,可保留应用状态和公共布局;URL、前进后退、书签仍可表达视图;配合懒加载能按路由拆包。
**缺点:**增加路由状态、鉴权、数据加载和滚动恢复的复杂度;初始 JS、失败恢复和可访问性需要额外处理;纯 CSR 的首屏与 SEO 较弱;History 回退配置错误会导致刷新 404。路由守卫只是前端体验控制,真正的权限必须由服务端校验。
300. 什么是点击穿透,怎么解决?
答案:
点击穿透通常指旧移动浏览器中:上层元素在 touchend 中立即消失,约 300ms 后浏览器又合成一个 click,此时点击命中同一坐标下方的新元素,造成误操作。它不是普通事件冒泡,因为前后是两个事件,目标元素也可能不同。
处理原则是统一输入模型:
- 优先使用 Pointer Events 或直接使用
click,不要同一交互同时用touchend和click完成业务。 - 设置正确的 viewport,并可对交互区域使用
touch-action: manipulation;现代浏览器通常因此没有旧式 300ms 延迟。 - 必须处理触摸事件时,可在可取消且非 passive 的触摸处理器中
preventDefault(),阻止兼容鼠标事件,但要验证滚动和缩放不受破坏。 - 蒙层关闭可等对应
click完成后再卸载,或在过渡期间保留一个能拦截指针的遮罩;业务提交还应做幂等或防重复触发。
FastClick 主要服务旧浏览器,现代项目通常不应再引入。不要为消除延迟粗暴禁用页面缩放,这会损害可访问性。
301. 移动端的点击事件的有延迟,时间是多久,为什么会有? 怎么解决这个延时?
答案:
历史上的移动浏览器 click 延迟大约是 300ms。浏览器在第一次轻触后等待一小段时间,用来判断用户是否会第二次轻触并触发双击缩放,因此不能马上派发 click。
现代移动浏览器在页面设置合适的 viewport 时通常已移除该延迟:
html
<meta name="viewport" content="width=device-width, initial-scale=1">还可在按钮等交互区域使用 touch-action: manipulation,并优先采用 Pointer Events/标准 click。不要混用 touchend 与 click 执行同一动作,也不建议为此禁用用户缩放;FastClick 是旧时代兼容方案。边界上,老旧 WebView、特殊嵌入浏览器仍可能保留延迟,需要在目标设备实测。
302. 如何判断当前脚本运行在浏览器还是 node 环境中?
答案:
应检测环境提供的能力,而不是判断顶层 this:
js
const isBrowser =
typeof window !== 'undefined' &&
typeof window.document !== 'undefined';
const isNode =
typeof process !== 'undefined' &&
Boolean(process.versions?.node);typeof 不会因标识符不存在而抛错,适合同时在两种环境求值。this === window 不可靠:ES 模块顶层 this 是 undefined,打包器会改写作用域,Node 也可能存在 DOM 模拟或 window polyfill。Electron、jsdom 等混合环境甚至可能让两个判断都为真,所以实际代码更应检测所需 API,例如 typeof document?.createElement 的安全等价写法,或通过条件导出分别提供浏览器/Node 实现。跨环境全局对象使用 globalThis。
303. setTimeout 为什么不能保证能够及时执行?
答案:
setTimeout(fn, delay) 的 delay 是回调进入可调度状态前的最短等待时间,不是精确执行时刻。时间到达后,宿主只是把回调排入任务队列;它还必须等待当前调用栈结束,并等待此前排队的任务及每次任务后的微任务检查点。
实际延迟还会受这些因素影响:长任务阻塞主线程、Promise 微任务持续产生、定时器嵌套的最小延迟钳制、后台标签页节流、省电策略、系统调度和时钟精度。因此 setTimeout(fn, 0) 也只能表示“尽早在后续任务执行”。
需要动画时用 requestAnimationFrame;需要倒计时时按绝对截止时间重算;需要严格调度或实时保证则不能依赖浏览器主线程定时器。
304. 介绍一下 setTimeout 的运行机制
答案:
调用 setTimeout(fn, delay) 时,浏览器/Node 的宿主定时器设施开始计时,并返回可用于 clearTimeout 的句柄。达到最短延迟后,宿主将回调排入相应的任务队列,而不是直接打断当前 JavaScript。
事件循环会等当前任务的调用栈清空,并完成微任务检查点后,再选择可运行的后续任务,所以:
js
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(4));
console.log(3);
// 1、3、4、2这里主脚本先结束,Promise 反应作为微任务先运行,定时器回调作为后续任务再运行。delay 会被转换和钳制,嵌套定时器、后台页面等还有额外最小延迟;队列繁忙时只会更晚,不会保证准点。浏览器与 Node 的事件循环阶段并不完全相同,但“到期后排队,等待线程可用”这一原则一致。
305. ['10', '10', '10', '10', '10'].map(parseInt)
答案:
结果:
js
[10, NaN, 2, 3, 4]map 给回调传入 (value, index, array),而 parseInt 的前两个参数是 (string, radix),因此实际计算为:
js
parseInt('10', 0) // 10,radix 0 按前缀推断,当前字符串为十进制
parseInt('10', 1) // NaN,基数必须是 2~36(或 0)
parseInt('10', 2) // 2
parseInt('10', 3) // 3
parseInt('10', 4) // 4需要统一按十进制转换时应显式包一层:
js
['10', '10', '10', '10', '10'].map(value => parseInt(value, 10));
// [10, 10, 10, 10, 10]若要求整个字符串都是合法数字而不是解析数字前缀,可根据业务改用 Number(value) 并检查 Number.isNaN。
306. JavaScript中的错误有哪几种类型?
答案:
JavaScript 内建错误以 Error 为基类,常见类型有:
SyntaxError:源码或动态解析内容不符合语法。ReferenceError:引用了无法解析的标识符,也包括暂时性死区访问。TypeError:值的类型不支持当前操作,例如调用非函数、访问null的属性。RangeError:值虽是正确类型但超出允许范围,例如非法数组长度或递归过深。URIError:decodeURI/decodeURIComponent等处理非法 URI 序列。AggregateError:把多个错误聚合在一起,例如Promise.any全部拒绝。EvalError:为历史兼容保留,现代引擎基本不会由eval主动抛出它。
业务也可 throw new Error(...),或通过 class AppError extends Error 定义错误并保留 cause。Web API 还常抛 DOMException,它不等同于上述 ECMAScript 错误分类。错误也可按发生阶段概括为语法/加载错误与运行时错误,但实际处理应依据实例、错误码和上下文,而不是只匹配 message 文本。
307. Promise.all 和 Promise.allSettled 有什么区别?
答案:
Promise.all(iterable) 要求全部成功:所有输入兑现后,它以按输入顺序排列的值数组兑现;任意一个输入拒绝时,返回的 Promise 会尽快以该原因拒绝。它不会自动取消其他仍在运行的任务。适合“缺一个结果整体就不能继续”的场景。
Promise.allSettled(iterable) 会等待所有输入完成,返回的 Promise 正常兑现为同顺序的状态对象数组:
js
[
{ status: 'fulfilled', value: 1 },
{ status: 'rejected', reason: error }
]它适合批量操作、需要同时展示成功与失败明细的场景。注意拒绝项字段是 reason,不是 value。两者都会把普通值视为已兑现 Promise;空输入会得到已兑现的空数组,但通过 .then 注册的处理器仍异步执行。若需要失败时取消剩余网络请求,必须另外结合 AbortController 等机制。
308. JS中怎么阻止事件冒泡和默认事件?
答案:
阻止传播和阻止默认行为是两件事:
js
element.addEventListener('click', event => {
event.stopPropagation(); // 不再传播到后续祖先节点
event.preventDefault(); // 取消链接跳转、表单提交等默认动作
});stopImmediatePropagation() 更强:除停止传播外,还阻止当前元素上后续同类型监听器。preventDefault() 只有在 event.cancelable === true 时有效;passive 监听器中不能调用它来取消默认行为。
return false 不是通用替代方案:在 addEventListener 回调中返回 false 没有效果;内联/DOM0 事件处理器返回 false 主要用于取消默认行为,jQuery 才把它封装为同时阻止默认行为和传播。工程代码应明确调用对应方法。
309. 什么是“事件代理”
答案:
事件代理是利用事件冒泡,把许多子元素的监听统一绑定到一个稳定的祖先元素,再根据事件目标决定处理哪个子项:
js
const list = document.querySelector('#list');
list.addEventListener('click', event => {
const item = event.target.closest('[data-item-id]');
if (!item || !list.contains(item)) return;
console.log(item.dataset.itemId);
});event.target 是最初触发事件的节点,可能是按钮内部的图标;event.currentTarget 是绑定监听器的 list,所以通常用 closest 向上匹配并用 contains 限定边界。
优点是减少监听器数量,并天然支持之后动态插入的子元素。边界是并非所有事件都冒泡,例如 focus/blur 可改用 focusin/focusout,mouseenter 可视情况改用 mouseover;Shadow DOM 还要考虑 composedPath()。代理节点应尽量靠近业务容器,避免在 document 上承担无关事件和复杂匹配。
310. 谈谈你对事件冒泡和捕获的理解
答案:
DOM 事件传播通常分三阶段:
- 捕获阶段:事件从
window沿 DOM 树向目标的父节点传播。 - 目标阶段:事件到达目标元素;目标上的捕获监听器通常先于非捕获监听器执行。
- 冒泡阶段:若事件可冒泡,再从目标向祖先、
document、window返回。
js
parent.addEventListener('click', onCapture, { capture: true });
parent.addEventListener('click', onBubble); // 默认 capture: false捕获适合在事件到达目标前统一观察或拦截;冒泡更常用,也是事件代理的基础。event.stopPropagation() 停止事件继续经过其他节点,stopImmediatePropagation() 还会阻止当前节点的后续监听器;preventDefault() 只取消默认动作,不影响传播。并非所有事件都冒泡,监听器能否触发还受 bubbles、composed、Shadow DOM 边界及监听选项影响。
311. js中如何判断一个值是否是数组类型?
答案:
首选标准方法 Array.isArray:
js
Array.isArray([]); // true
Array.isArray(new Array(3)); // true
Array.isArray({ length: 0 }); // false
Array.isArray(new Uint8Array()); // false它能正确处理来自 iframe 等其他 realm 的数组。value instanceof Array 依赖当前 realm 的 Array.prototype,跨 iframe 可能返回 false;Object.prototype.toString.call(value) 可作旧环境方案,但可能受 Symbol.toStringTag 影响。TypedArray 不是 Array,需要用 ArrayBuffer.isView(value) 另行判断。
312. 浏览器为什么要有跨域限制?
答案:
跨域限制的核心目的,是隔离不同来源的权限,防止一个恶意站点借用用户在另一个站点的登录态去读取敏感数据或操纵其页面。例如恶意页面不应读取银行接口响应、访问跨源 iframe 的 DOM,或直接读取另一个源的存储。否则用户只要同时登录敏感网站并打开恶意页面,数据机密性和页面完整性就会失守。
限制主要由同源策略实施,但它不是“禁止所有跨域通信”:图片、脚本、表单等跨源加载/发送在一定条件下允许,Fetch/XHR 的请求也可能已经发出,只是脚本不能读取响应。正因跨站请求仍可能发生,CSRF 还必须通过 SameSite Cookie、CSRF Token、Origin 校验等防护,不能只依赖同源策略。
服务端可用 CORS 精确授权哪些源读取响应,页面间可用 postMessage 显式通信,也可通过同源后端代理。CORS 是浏览器读取权限控制,不替代身份认证、授权和输入校验。
313. 浏览器的同源策略是什么?
答案:
同源策略是浏览器的安全隔离规则。两个 URL 只有 scheme(协议)、host(主机)、port(端口) 都相同才同源;路径和查询参数不参与判断。例如 https://a.com/x 与 https://a.com/y 同源,而 http://a.com、https://sub.a.com、https://a.com:8443 都与 https://a.com 不同源。
它主要限制脚本读取跨源 Fetch/XHR 响应、访问跨源窗口/iframe 的 DOM,以及共享 DOM 存储等。跨源资源嵌入和某些写操作并非全部禁止,所以“同源”不等于“同站点”,Cookie 的 same-site 规则也不是同一概念。
受信任服务可通过 CORS 放宽响应读取,通过 window.postMessage 在窗口间显式交换消息,或使用同源后端代理。postMessage 必须校验 origin;History 路由、JSONP 等也不能绕过所有同源安全边界。
314. 浏览器的垃圾回收机制有哪些?
答案:
现代浏览器的 JavaScript 垃圾回收主要是可达性追踪:从全局对象、当前调用栈、闭包等 GC Roots 出发标记所有可达对象,未被标记的对象才可回收。典型基础算法是标记-清除,实际引擎还会组合:
- 分代回收:依据“大多数对象寿命短”,频繁回收新生代,较少扫描老生代。
- 复制/整理压缩:移动存活对象,减少内存碎片并提高分配效率。
- 增量和并发标记:把工作拆分或放到辅助线程,降低主线程长停顿。
- 写屏障:对象关系变化时维护分代、增量标记所需的信息。
引用计数是另一种思路,但单纯引用计数无法处理循环引用,现代主流 JS 引擎不以它作为完整堆回收的核心。GC 只回收“不可达”对象;被全局变量、未清理监听器/定时器、缓存或闭包意外持有的对象仍然可达,因此会形成内存泄漏。回收时机不确定,业务逻辑不能依赖 GC 及时运行;WeakMap/WeakSet 可用于不阻止键被回收的关联数据,但也不能观察准确回收时间。
315. xml和json有什么区别?
答案:
JSON 和 XML 都是文本数据格式,但数据模型和适用场景不同:
- JSON 直接表达对象、数组、字符串、数字、布尔值和
null,语法紧凑,Web API 中解析方便。标准 JSON 不支持注释、命名空间、属性或混合文本;开放系统交换通常使用 UTF-8。 - XML 表达带名称、属性、文本的层级标记树,支持命名空间、注释、CDATA、混合内容和处理指令,并有 XSD、XPath、XSLT 等成熟生态;它更适合复杂文档、强模式和需要保留标记语义的协议,但通常更冗长。
JSON 中数组是原生结构,XML 可通过重复元素表达列表;这不等于 XML “不能表示数组”。安全性也不是格式天然决定的:JSON 要防原型污染、超大输入等问题,XML 要禁用不必要的外部实体以防 XXE,并都应限制大小和嵌套深度。普通 REST 数据交换多选 JSON;文档、命名空间或既有企业协议可能更适合 XML。
316. document.write和innerHTML有什么区别
答案:
document.write() 向当前 HTML 解析流写内容。页面解析期间调用时,写入内容会参与后续解析;文档加载完成后再调用通常会隐式执行 document.open(),清空现有页面后重写文档。它还可能阻塞解析和资源加载,现代业务代码基本不应使用。
element.innerHTML 只把指定元素的后代替换为重新解析的 HTML,不会重写整个文档:
js
panel.innerHTML = '<strong>hello</strong>';替换会销毁旧子节点及其直接绑定的监听器。两者都是 HTML 注入入口,把不可信字符串传入会导致 XSS;纯文本使用 textContent,结构化内容优先用 createElement、模板或经过可靠消毒的 HTML。通过 innerHTML 插入的 <script> 通常不会按普通解析流方式执行,但这并不使它免于其他 XSS 向量。
317. 使用原生js给一个按钮绑定两个onclick事件
答案:
使用 addEventListener 注册两个不同的监听器:
html
<button id="btn">点击</button>
<script>
const btn = document.querySelector('#btn');
function firstHandler() {
console.log('first');
}
function secondHandler() {
console.log('second');
}
btn.addEventListener('click', firstHandler);
btn.addEventListener('click', secondHandler);
// 需要解绑时必须保留同一个函数引用:
// btn.removeEventListener('click', firstHandler);
</script>点击时通常按注册顺序执行。连续给 btn.onclick 赋值会覆盖前一个处理器,因此不适合绑定多个事件;同一个函数以相同类型和相同捕获设置重复注册则只保留一次。每次处理的时间复杂度为 O(k),k 是该事件实际执行的监听器数量。
318. 123['toString'].length + 123 的输出值是多少?
答案:
输出是 124。
123['toString'] 会对数字临时装箱并沿原型链取得 Number.prototype.toString 函数。函数对象的 length 表示第一个默认参数之前的形参数量,不包含剩余参数;Number.prototype.toString 有一个可选的形式参数 radix,所以其 length 是 1。最终执行的是数字加法:
js
123['toString'].length + 123
// 1 + 123 === 124方括号访问也避开了 123.toString 可能被解析器视为小数点的语法歧义。
319. ajax、axios、fetch有什么区别?
答案:
三者不在同一层级:
- AJAX 是“不刷新整页而异步请求并更新页面”的技术理念,不是某个具体库。传统实现通常直接使用
XMLHttpRequest,它采用事件式 API,能处理上传/下载进度、超时和中止。 - Fetch 是浏览器原生的 Promise 化网络 API,围绕
Request、Response、Headers 和 Stream 设计,适合async/await。它只在网络失败、被中止等情况下拒绝,HTTP404/500仍会兑现,必须检查response.ok。可用AbortController取消,也可结合它实现超时;跨源 Cookie 需正确设置credentials且服务端 CORS 允许。浏览器中的上传进度支持仍不如 XHR 直接。 - Axios 是第三方 HTTP 客户端。在浏览器可使用 XHR 或适配器,在 Node 中使用相应传输实现;默认会按状态码策略拒绝非 2xx,提供拦截器、请求/响应转换、超时、取消和较统一的配置,但会增加依赖与包体。
简单、标准化且重视流式响应时通常选 Fetch;需要既有 Axios 拦截器生态、浏览器/Node 一致封装时选 Axios;要精细上传进度或维护旧代码时可能直接用 XHR。无论使用哪一种,认证、重试幂等、取消和错误分类都需要显式设计。
320. for...in和for...of有什么区别?
答案:
for...in 枚举对象的可枚举字符串属性名,包括原型链上的属性,不包含 Symbol 键:
js
for (const key in obj) {
if (Object.hasOwn(obj, key)) console.log(key, obj[key]);
}for...of 调用对象的 [Symbol.iterator](),取得迭代器产出的值。数组产出元素值,字符串产出字符,Set 产出值,Map 产出 [key, value],Generator 产出 yield 的值:
js
for (const value of ['a', 'b']) console.log(value);
for (const [key, value] of map) console.log(key, value);普通对象默认不可迭代,直接 for...of obj 会抛 TypeError,应使用 Object.keys/values/entries(obj)。数组也不宜用 for...in:它得到的是字符串索引,还可能包含自定义和继承的可枚举属性,并会跳过空洞;for...of 按数组迭代器顺序取值,空洞位置会读成 undefined。
321. 什么是类数组对象?
答案:
类数组对象是具有非负整数 length,并通常以 0、1 等索引键保存元素的对象,例如 arguments、NodeList。它不是数组,因此没有 Array.prototype 上的 map、push 等实例方法。
常见转换方式:
js
const a = Array.from(arrayLike);
const b = [...iterableArrayLike];
const c = Array.prototype.slice.call(arrayLike);Array.from 只要求对象是类数组或可迭代对象;展开语法要求对象实现 Symbol.iterator,所以不是所有类数组都能展开。函数虽有表示形参数量的 length,但通常不把函数本身当作待遍历的类数组集合。
322. JavaScript脚本延迟加载的方式有哪些?
答案:
常见方式如下:
<script defer src="...">:下载与 HTML 解析并行,等文档解析完成后、DOMContentLoaded前按文档顺序执行,适合有依赖关系的普通脚本。<script async src="...">:并行下载,下载完成立即执行,执行时仍会占用主线程;多个脚本的执行顺序不确定,适合相互独立的脚本。<script type="module">:模块脚本默认具有类似defer的行为;动态import()可在需要时加载模块。- 动态创建
script标签:可在用户操作、空闲阶段或特定条件满足后再插入,动态脚本默认偏向异步执行。 - 将无
defer/async的脚本放在body末尾:只能减少它阻塞尚未解析 HTML 的影响,不会减少下载和执行成本。
setTimeout 只能推迟创建或执行动作,并不保证资源加载完成时间。现代项目通常优先用 defer、ES Module 和按需 import()。
323. 箭头函数的 this 指向哪⾥?
答案:
箭头函数没有自己的 this,它在定义位置按词法作用域捕获外层 this。因此其 this 与调用方式无关,call、apply、bind 传入的 thisArg 也不能改变它。
js
const obj = {
value: 1,
makeGetter() {
return () => this.value;
}
};
obj.makeGetter()(); // 1这里捕获的是调用 makeGetter 时的 this。若箭头函数定义在 ES Module 顶层,顶层 this 是 undefined;在不同脚本环境中顶层 this 可能不同,所以不能笼统地说箭头函数总指向 window。
324. 如果new一个箭头函数会怎么样?
答案:
用 new 调用箭头函数会直接抛出 TypeError:
js
const Foo = () => {};
new Foo(); // TypeError: Foo is not a constructor原因是箭头函数只有函数调用能力,没有构造函数所需的 [[Construct]] 内部方法,也没有用于实例继承的 prototype 属性。它还没有自己的 this,无法让 new 把新实例绑定为函数内的 this。需要构造实例时应使用普通函数或 class。
325. object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别是什么?
答案:
Object.assign 和对象展开语法默认都是浅拷贝:只复制第一层属性值,嵌套对象仍共享同一引用。二者都处理源对象自身的可枚举字符串键和 Symbol 键,不复制继承属性。
js
const source = { nested: { n: 1 } };
const a = Object.assign({}, source);
const b = { ...source };
a.nested === source.nested; // true
b.nested === source.nested; // true主要区别:
Object.assign(target, ...sources)会修改并返回target;对象展开通常创建一个新对象。assign通过普通赋值写入目标,可能触发目标对象已有的 setter;对象展开在新对象上创建数据属性,不触发目标 setter。- 多个源发生同名键时都以后面的值为准;
null、undefined作为源会被跳过。
需要深拷贝时可在数据可结构化克隆的前提下使用 structuredClone;JSON 往返并不是通用深拷贝。
326. typeof NaN 的结果是什么?
答案:
结果是:
js
typeof NaN; // "number"NaN 表示某个数值运算无法得到有效数值,它仍属于 Number 类型。它不等于自身,因此不能用 x === NaN 判断;应使用 Number.isNaN(x)。
327. null 和 undefined 有什么区别?
答案:
undefined 和 null 都是只有一个值的原始类型,但语义不同:
undefined通常表示“尚未提供或尚未初始化”,如未赋值变量、缺失参数、读取不存在的属性。null通常由开发者显式写入,表示“有意为空”或“当前没有对象”。
关键行为:
js
typeof undefined; // "undefined"
typeof null; // "object",历史遗留结果
null == undefined; // true,仅二者宽松相等
null === undefined; // false
Number(null); // 0
Number(undefined); // NaN默认值和解构默认参数只在值为 undefined 时生效,对 null 不生效。工程代码通常使用严格相等;只有明确想同时匹配二者时,value == null 才是有意且简洁的写法。
328. 数据类型检测的方式有哪些?
答案:
常用检测方式各有边界:
typeof:适合原始类型和函数;但typeof null是"object",数组、日期和普通对象也都返回"object"。Array.isArray(value):判断数组的首选方式,跨 iframe/realm 也可靠。value instanceof Ctor:判断Ctor.prototype是否出现在对象原型链上,适合类实例;对原始值通常为false,跨 realm 或原型被修改时不可靠。Object.prototype.toString.call(value):可区分多数内置类型,如[object Date]、[object Null];但Symbol.toStringTag可改变结果,不能把它当作安全的品牌校验。Object.getPrototypeOf、in或特征检测:判断对象能力或协议时往往比猜具体类型更合适。
js
typeof 1n; // "bigint"
Array.isArray([]); // true
Object.prototype.toString.call(null); // "[object Null]"constructor 属性可能不存在、被覆盖或来自其他 realm,一般不作为可靠类型检测。Object.is 是相等性比较,也不是类型检测 API。
329. Object.is() 与比较操作符 “===”、“==” 的区别?
答案:
三者采用不同的相等算法:
==是宽松相等,类型不同时可能进行 ToPrimitive、字符串/数字、布尔值等隐式转换,规则复杂。===是严格相等,不做类型转换;但NaN === NaN为false,且+0 === -0为true。Object.is使用 SameValue 语义,大多数结果与===相同,但把NaN视为与自身相同,并区分正零和负零。
js
0 == "0"; // true
0 === "0"; // false
Object.is(NaN, NaN); // true
Object.is(+0, -0); // false日常判断优先 ===;需要精确处理 NaN 或有符号零时用 Object.is。
330. isNaN 和 Number.isNaN 函数有什么区别?
答案:
全局 isNaN 会先把参数转换为数字,再判断转换结果是否为 NaN;Number.isNaN 不做类型转换,只有参数本身是数值类型的 NaN 才返回 true。
js
isNaN("hello"); // true,因为 Number("hello") 是 NaN
Number.isNaN("hello"); // false
Number.isNaN(NaN); // true
isNaN(""); // false,因为空字符串转为 0因此,判断一个值是否确实为 NaN 应优先使用 Number.isNaN。若目标是校验“能否转成有限数字”,应显式转换后结合 Number.isFinite,不要依赖全局 isNaN 的隐式转换。
331. 说说你对以下几个页面生命周期事件的理解:DOMContentLoaded,load,beforeunload,unload
答案:
DOMContentLoaded:HTML 已解析、DOM 已构建完成后在document上触发;不会等待图片等资源。普通脚本和defer/模块脚本会影响其时机,async脚本通常不会被它等待。样式表本身不阻塞 DOM 解析,但可能阻塞其后的同步脚本,因而间接推迟该事件。load:页面及其依赖资源(样式、图片、子框架等)完成加载后在window上触发。初始化 DOM 通常无需等到这里。beforeunload:页面即将卸载时触发,可在存在未保存数据时请求浏览器显示离开确认。浏览器使用通用提示文案,而且该事件在移动端等场景并不保证触发;不应无条件注册,否则还可能影响页面缓存。unload:文档正在卸载时触发,时机不可靠,也不能安全执行异步工作,现代应用通常避免依赖它。
保存状态或统计数据更适合监听 visibilitychange/pagehide,发送少量离场数据可用 navigator.sendBeacon() 或带 keepalive 的 fetch。这些事件都不能保证在进程崩溃或被系统强杀时执行。
332. 使用Promise实现每隔1秒输出1,2,3
答案:
可以把每次延时串到上一轮 Promise 后面:
js
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
function printEverySecond(values) {
return values.reduce(
(chain, value) => chain.then(async () => {
await sleep(1000);
console.log(value);
}),
Promise.resolve()
);
}
printEverySecond([1, 2, 3]);输出为 1、2、3,相邻输出约间隔 1 秒,第一次输出也在约 1 秒后。reduce 构造的是串行 Promise 链,不能把三个定时器同时启动,否则它们会几乎同时到期。构造链的时间和额外空间均为 O(n);总等待时间约为 n * 1000ms,定时器只能保证“不早于”指定延迟。空数组会立即返回 fulfilled Promise。
333. Promise中的值穿透是什么?
答案:
Promise 的“值穿透”是指 .then/.catch 对应位置传入的不是函数时,该处理器会被忽略,上一阶段的成功值或失败原因继续传到后面。
js
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log); // 1这里 2 和 Promise.resolve(3) 都不是回调函数,等价于没有提供成功处理器,所以值 1 一直向后传递。失败分支也类似:缺少有效的拒绝处理器时,拒绝原因会继续传播。要转换值必须传函数,例如 .then(() => 2)。
334. 如何使用js计算一个html页面有多少种标签?
答案:
统计当前文档中实际出现过的元素标签种类,可以遍历所有元素并用 Set 去重:
js
function countTagKinds(root = document) {
const names = new Set();
root.querySelectorAll('*').forEach(el => names.add(el.localName));
return names.size;
}
console.log(countTagKinds());querySelectorAll('*') 返回静态 NodeList,localName 在 HTML 文档中通常为小写标签名。时间复杂度 O(n),空间复杂度 O(k),其中 n 是元素数,k 是标签种类数。默认只统计当前 document tree:不会自动进入 iframe 的文档,也不会穿透 Shadow DOM;若需求包含这些边界,需要在同源且可访问的前提下递归处理。
335. bind() 连续调用多次,this的绑定值是什么呢?
javascript
var bar = function(){
console.log(this.x);
}
var foo = {
x:3
}
var sed = {
x:4
}
var func = bar.bind(foo).bind(sed);
func(); //?
var fiv = {
x:5
}
var func = bar.bind(foo).bind(sed).bind(fiv);
func(); //?答案:
两次调用都输出 3:
text
3
3第一次 bar.bind(foo) 创建的绑定函数已经把 this 固定为 foo。再对这个绑定函数调用 bind(sed) 或 bind(fiv),不能覆盖内部已有的绑定 this,只会继续预置参数。
边界是使用 new 调用可构造的绑定函数时,绑定的 thisArg 会被忽略,this 指向新实例;但本题是普通调用,所以始终使用第一次绑定的 foo。
336. 浏览器和 Node 中的事件循环有什么区别?
答案:
两者都以单线程执行 JavaScript,并在任务之间清空微任务,但宿主调度模型不同。
浏览器以 HTML 事件循环为基础:执行一个 task(如定时器、事件或消息)后做微任务检查点,清空 Promise/queueMicrotask 等队列;随后浏览器可能进行渲染,再进入下一轮。渲染不是普通宏任务,也不保证每轮都发生。
Node.js由 libuv 驱动,事件循环分为 timers、pending callbacks、poll、check、close callbacks 等阶段;setImmediate 在 check 阶段,定时器在 timers 相关阶段。Node 还具有优先级更高的 process.nextTick 队列:当前 JS 回调结束后通常先清空 nextTick,再处理 Promise 微任务。
因此 setTimeout(fn, 0) 与 setImmediate(fn) 的先后受调用位置、I/O 阶段及 Node/libuv 版本影响,不能写成普遍固定顺序。现代 Node 通常在每个回调后处理微任务,而不是简单地等整个阶段结束才处理。
337. 谈谈对 window.requestAnimationFrame 的理解
答案:
requestAnimationFrame(callback) 请求浏览器在下一次绘制前调用回调,适合把动画状态更新与显示器刷新节奏对齐。它是一次性的,持续动画需要在回调中再次注册;返回的 ID 可交给 cancelAnimationFrame 取消。
js
const start = performance.now();
let id;
function frame(now) {
const progress = Math.min((now - start) / 1000, 1);
box.style.transform = `translateX(${progress * 200}px)`;
if (progress < 1) id = requestAnimationFrame(frame);
}
id = requestAnimationFrame(frame);回调参数是同一时间轴上的高精度时间戳,应按时间差计算位移,而不是假设固定 60Hz。浏览器通常会在后台标签页暂停或降低调用频率以节能。它不保证回调一定准时,也不会自动让重计算变快;回调耗时超过帧预算仍会掉帧。
338. 浏览器一帧都会干些什么?
答案:
浏览器一帧的典型工作包括:处理已到期的任务和用户输入、执行 JavaScript 回调并清空微任务、运行 requestAnimationFrame,然后按需进行样式计算、布局、绘制和合成,最后把结果提交到屏幕。若主线程还有空闲时间,可能执行 requestIdleCallback。
这些步骤不是每帧都完整发生:只改 transform/opacity 时可能主要由合成线程处理;没有视觉变化时无需布局或绘制;空闲回调也不保证执行。60Hz 屏幕每帧理论预算约 16.7ms,但浏览器自身也占用时间。长任务、频繁强制同步布局和大面积绘制会超过预算,造成丢帧。优化重点是缩短主线程任务、批量读写布局属性,并优先使用低成本的合成属性。
339. 谈谈 Object.defineProperty 与 Proxy 的区别
答案:
Object.defineProperty 定义或修改某一个属性的描述符,可通过 getter/setter 拦截该属性的读写;Proxy 为一个目标对象的操作创建代理,可拦截 get、set、deleteProperty、has、ownKeys、函数调用等多种内部操作。
主要区别:
defineProperty必须逐个属性处理;新增或删除属性需要额外处理。它能定义不可枚举、只读等精确描述符。Proxy能统一观察新增、删除、数组索引和length等操作,但嵌套对象仍需在读取时继续代理,并非自动深层代理。- Proxy 必须通过代理对象访问才会触发拦截;直接操作原目标不会触发。
- Proxy 有不可违反的对象不变量,通常配合
Reflect转发默认行为;它无法被完整 polyfill。
defineProperty 并非绝对“不能监听数组”,而是逐索引劫持和处理新增索引成本高、覆盖不完整。选择时应看是否需要属性描述符,还是需要对象级、更多种类的操作拦截。
340. base64编码图片,为什么会让数据量变大?
答案:
Base64 把每 3 个原始字节(24 位)拆成 4 组 6 位,每组用一个 ASCII 字符表示,因此主体大小为:
text
4 * ceil(n / 3) 字节当数据足够大时,体积约增加 1/3,即 33.3%;尾部不足 3 字节时还会用 = 填充。若放进 Data URL,还要加上 data:image/...;base64, 前缀。
Base64 只是编码而非压缩。经过 HTTP gzip/Brotli 后实际传输差距可能缩小,但浏览器解码后的内容和内存表示仍有成本;把大图片内联还会失去独立缓存和流式加载优势,所以通常只适合很小且希望减少请求的资源。
341. html文档渲染过程,css文件和js文件的下载,是否会阻塞渲染?
答案:
浏览器边下载边解析 HTML,构建 DOM;解析 CSS 构建 CSSOM,二者生成渲染树后再布局、绘制和合成。资源下载通常可由预加载扫描器并行发起,但“并行下载”不代表“不阻塞解析或渲染”。
- CSS:外部样式表通常不阻塞 HTML 继续解析和 DOM 构建,但会阻塞首次渲染,因为渲染需要 CSSOM。样式表还会阻塞其后的同步脚本执行,以避免脚本读取到未应用的样式。
- 普通同步 JS:解析器遇到
<script src>时通常暂停 HTML 解析,等待脚本下载并执行;JavaScript 执行也占用主线程,会推迟样式计算和绘制。 defer:并行下载,HTML 解析完成后按顺序执行。async:并行下载,完成后尽快执行,顺序不保证,执行时仍会占主线程。- 模块脚本:默认延后执行,可用
async改变行为。
因此优化通常包括关键 CSS、减少阻塞资源、脚本使用 defer/模块、按需加载和缩短 JS 长任务。具体下载优先级和首次绘制时机由浏览器实现及资源属性共同决定。
342. 谈谈你对浏览器中进程和线程的理解
答案:
现代浏览器通常采用多进程架构,常见角色包括浏览器主进程、渲染进程、网络服务、GPU 进程以及各种工具进程。页面与渲染进程的对应关系受站点隔离、同源关系和浏览器策略影响,并非永远“一标签一进程”。多进程可以提高安全隔离、稳定性和并行能力,代价是更多内存及进程通信。
渲染进程内部又有多类线程:
- 主线程负责 DOM、样式、布局及大部分 JavaScript 和事件回调。
- 合成线程和栅格线程负责图层合成、栅格化等工作,部分滚动和动画可脱离主线程。
- Worker 线程可执行独立 JavaScript,但不能直接操作页面 DOM。
所谓 JavaScript“单线程”通常指同一个 realm/agent 的代码在一条执行栈上串行运行,不代表浏览器只有一个线程。主线程上的长 JS 任务会阻塞输入处理和渲染,这也是事件循环、异步 API 与 Worker 存在的重要原因。
343. 为什么JavaScript是单线程?
答案:
浏览器页面中的 JavaScript 采用串行执行模型,核心原因是共享可变状态,尤其是 DOM,需要确定且一致的修改顺序。若多个线程能同时读写同一棵 DOM,就必须引入锁、竞争处理和复杂同步,既容易出错,也会让脚本语义难以预测。
“单线程”只表示同一个 JavaScript agent 的执行栈同一时刻执行一段代码。浏览器本身是多进程、多线程的,网络、定时、渲染合成等由宿主并发处理,完成后通过事件循环把回调排队。Web Worker 还能在其他线程运行 JavaScript并通过消息或受控的共享内存通信,但不能直接操作 DOM。因此并发能力存在,只是页面主线程的 JS 与 DOM 操作保持串行。
344. 说说你对 Object.defineProperty 的理解
答案:
Object.defineProperty(obj, key, descriptor) 用于新增或重新定义对象的自有属性,并返回 obj。属性描述符分两类,不能混用:
- 数据描述符:
value、writable。 - 访问器描述符:
get、set。 - 两类共有:
enumerable、configurable。
js
const user = {};
let age = 0;
Object.defineProperty(user, 'age', {
enumerable: true,
configurable: false,
get() { return age; },
set(value) {
if (!Number.isInteger(value) || value < 0) throw new TypeError('invalid age');
age = value;
}
});通过该 API 新建属性时,未写出的布尔项默认是 false,这与对象字面量创建的普通属性不同。configurable: false 后通常不能删除属性或改变描述符种类;数据属性从 writable: true 改为 false 是少数允许的单向修改。它适合定义只读属性、隐藏元数据、getter/setter 等,但要观察整个对象的多种操作应考虑 Proxy。
345. ES6中的 Reflect 对象有什么用?
答案:
Reflect 是提供对象底层操作的静态 API 集合,不是构造函数。它把 get、set、has、deleteProperty、defineProperty、construct 等操作统一为函数形式,并与 Proxy 的大多数 trap 一一对应。
js
const proxy = new Proxy(target, {
get(target, key, receiver) {
console.log('get', key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver);
}
});使用 Reflect.get/set 时保留 receiver,能让原型链上的 getter/setter 获得正确的 this。部分方法也比对应的命令式操作更便于组合,例如 Reflect.deleteProperty 返回布尔值,Reflect.defineProperty 失败时返回 false,而 Object.defineProperty 会抛异常。Reflect 不是响应式机制,也不会自动改变对象行为;它主要提供规范化的底层操作与默认转发。
346. 什么是尾调用优化和尾递归?
答案:
尾调用是函数的最后一个动作直接返回另一个函数的调用结果,例如 return g(x);若返回后还要做加法、赋值等工作,就不是尾调用。尾递归则是函数在尾位置调用自身。
js
function factorial(n, acc = 1n) {
if (n <= 1n) return acc;
return factorial(n - 1n, n * acc);
}尾调用优化允许引擎复用当前调用帧,不必为每层调用保留栈帧,从而把尾递归的调用栈空间从 O(n) 降到 O(1);计算时间仍是 O(n)。
重要边界是:不能假设所有 JavaScript 引擎都会实际执行 proper tail call 优化。规范对符合条件的严格模式尾调用有定义,但主流引擎支持并不一致,上面的函数在大 n 下仍可能栈溢出。生产代码需要处理深递归时,优先改写为循环、显式栈或 trampoline。
347. 简单介绍下 ES6 中的 Iterator 迭代器
答案:
Iterator 是按顺序取值的统一协议。迭代器对象具有 next() 方法,每次返回 { value, done };done: true 表示迭代结束。可迭代对象则实现 [Symbol.iterator](),该方法返回迭代器。for...of、展开语法、数组解构等都消费这个协议。
js
const range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let current = this.from;
const end = this.to;
return {
next() {
return current <= end
? { value: current++, done: false }
: { value: undefined, done: true };
}
};
}
};
console.log([...range]); // [1, 2, 3]数组、字符串、Map、Set 等内置对象可迭代,普通对象默认不可迭代。异步数据使用另一套 [Symbol.asyncIterator]/for await...of 协议。若迭代器持有外部资源,还可实现 return(),让提前退出迭代时有机会清理。
348. js对象中,可枚举性(enumerable)是什么?
答案:
可枚举性是对象属性描述符中的 enumerable 标志,用来决定该属性是否参与常见枚举/复制操作。对象字面量和普通赋值创建的属性默认可枚举;Object.defineProperty 新建属性时若省略该项,默认不可枚举。
js
const obj = { visible: 1 };
Object.defineProperty(obj, 'hidden', { value: 2, enumerable: false });
Object.keys(obj); // ['visible']
obj.hidden; // 2,仍可直接访问边界如下:
Object.keys/values/entries只看自身、可枚举的字符串键。for...in还会遍历原型链上的可枚举字符串键。- 对象展开和
Object.assign复制自身可枚举的字符串键与 Symbol 键。 Object.getOwnPropertyNames可取得不可枚举字符串键,Reflect.ownKeys还能包含 Symbol。
不可枚举不等于私有或安全,只是不会出现在部分枚举操作中。
349. forEach 中能否使用 await ?
答案:
forEach 的回调可以写成 async 并在里面使用 await,但 forEach 不认识回调返回的 Promise,也不会等待它们。因此外层流程会立即继续,异步任务的完成顺序也不由数组顺序保证,回调里的拒绝还可能成为未处理拒绝。
需要串行执行时:
js
for (const item of items) {
await handle(item);
}需要并行并等待全部完成时:
js
await Promise.all(items.map(item => handle(item)));串行方式时间约为各任务耗时之和,并行方式约由最慢任务决定,但会一次启动全部任务;数据量大或服务有限流要求时应使用并发池。不要写 await items.forEach(...),因为 forEach 返回 undefined。
350. 如何中断Promise?
答案:
Promise 一旦创建,其 executor 会立即执行;Promise 本身没有通用的 cancel(),状态一旦 settled 也不可改变。真正的“取消”必须由底层异步操作配合,Web API 常用 AbortController:
js
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(r => r.json())
.catch(err => {
if (err.name !== 'AbortError') throw err;
});
controller.abort();Promise.race([request, timeout]) 只能让包装后的 Promise 提前失败,原请求仍可能继续执行;应在超时时同时调用 abort()。在链中 throw/返回 rejected Promise 只是转入后续 catch,并非终止所有处理,finally 仍会运行。返回一个永远 pending 的 Promise 虽能让后续链不执行,却会永久占住等待者,通常不是可靠的中断方案。
351. Object.create 和 new 有什么区别?
答案:
Object.create(proto, descriptors?) 直接创建对象,并把其 [[Prototype]] 设为传入的 proto;它不调用构造函数。new C(...args) 则创建以 C.prototype 为原型的对象,以新对象为 this 调用 C,最后在构造函数未显式返回对象时返回该实例。
js
function User(name) { this.name = name; }
User.prototype.say = function () { return this.name; };
const a = new User('A'); // 有自有属性 name
const b = Object.create(User.prototype); // 没有执行 User
b.name = 'B';Object.create(null) 还能创建没有原型、没有 toString 等继承成员的纯字典对象。注意 Object.create(User) 会让对象继承函数对象 User 本身,并不等价于 new User();通常应传 User.prototype。
352. 堆与栈有什么区别?
答案:
需要区分“内存区域”和“数据结构”两个语境。
在运行时内存中,调用栈保存函数调用帧、返回位置和部分局部状态,按后进先出管理,分配回收快但容量有限,递归过深会栈溢出。堆用于存放生命周期和大小更动态的数据,由运行时和垃圾回收器管理,容量通常更大,但分配、追踪和回收成本更高。JavaScript 引擎会做逃逸分析、装箱等优化,因此“原始值一定在栈、对象一定在堆”只是教学简化,不能当作语言保证。
在数据结构中,栈是 LIFO 线性结构,典型操作是 push/pop;堆是满足堆序性质的完全二叉树,常用于优先队列,分为大顶堆和小顶堆。两组概念名称相同,但含义和用途不同。
353. “严格模式”是什么?
答案:
严格模式是 ES5 引入的一组更严格的 JavaScript 语义,可在脚本或函数开头写 'use strict'; 启用。ES Module 和 class 内部天然处于严格模式。
典型变化包括:给未声明变量赋值会抛错;普通函数直接调用时 this 为 undefined 而不是全局对象;禁止 with;删除不可删除属性、给只读属性赋值等静默失败会改为抛错;eval 不再向外层作用域泄漏变量。
严格模式的价值是尽早暴露错误、减少隐式全局和歧义,并为引擎优化提供更清晰的语义。它不是安全沙箱,也不会自动让任意代码变安全。
354. 为什么部分请求中,参数需要使用 encodeURIComponent 进行转码?
答案:
URL 中的 &、=、?、#、/ 等字符具有分隔语义,参数值若原样包含这些字符,服务器可能把一个值误解析为新的参数、片段或路径。非 ASCII 字符也需要按 UTF-8 转为百分号编码。encodeURIComponent 专门编码单个路径段或查询参数值,会转义大多数保留字符。
js
const keyword = '前端 & JavaScript';
const url = `/search?q=${encodeURIComponent(keyword)}`;
// /search?q=%E5%89%8D%E7%AB%AF%20%26%20JavaScriptencodeURI 用于接近完整的 URL,会保留 :/?&=# 等结构字符,所以不适合直接编码参数值。实际构造查询串更推荐 URL 和 URLSearchParams,它们能减少手工拼接错误。不要对已经编码的值重复编码,服务端也应按同一约定解码。
355. 什么是 let 的临时性死区?
答案:
临时性死区(TDZ)是从进入块级作用域开始,到执行该变量的声明并完成初始化之前的区间。let、const 和 class 绑定在作用域创建时已经存在,但处于未初始化状态,此时访问(包括 typeof)会抛出 ReferenceError。
js
{
// console.log(value); // ReferenceError
let value = 1;
console.log(value); // 1
}这与 var 不同:var 在执行声明前已初始化为 undefined。TDZ 也解释了为什么同名块级变量会遮蔽外层变量,即使声明写在块的后面。
356. JS代码中的use strict是什么意思?
答案:
'use strict'; 是指令序言,用来让当前脚本或函数按严格模式执行;它必须出现在有效语句之前。ES Module 和类体默认就是严格模式,无需再写。
严格模式会把不少隐式或静默行为变为明确错误,例如禁止创建隐式全局变量、禁止 with,普通函数调用的 this 不再自动替换为全局对象,对只读属性赋值会抛错,参数名不能重复。
它的作用是减少语言历史遗留的模糊行为并尽早暴露问题,但不是权限隔离机制。还要注意拼写必须完全正确;'use strict' 只是字符串指令,不是函数调用。
357. common.js和es6中模块引入的区别?
答案:
CommonJS(CJS)使用 require、module.exports/exports;ES Module(ESM)使用 import/export。核心区别如下:
- CJS 依赖在运行时解析,
require可写在条件或函数中,传统加载流程是同步的;ESM 的静态import在解析阶段建立依赖图,只能位于模块顶层,动态场景用import()。 - CJS 模块执行后把
module.exports的当前值交给调用方并缓存模块;ESM 导入的是只读的 live binding,导出方更新绑定后导入方可观察到。对象本身在两种系统中仍可能共享并被修改。 - ESM 天然严格模式,支持静态分析、循环依赖绑定和 tree shaking;浏览器模块脚本默认延后执行,ESM 还支持 top-level
await。 - CJS 的顶层
this、__filename、__dirname等语义与 ESM 不同。
Node.js 同时支持两者,但文件扩展名、package.json 的 type 字段和包导出配置会影响解释方式;CJS/ESM 互操作存在默认导出包装等边界,不能假设两套语法总能无损混用。
358. 什么是变量提升
答案:
变量提升是执行代码前创建当前作用域绑定所表现出的现象,并不是源码真的被移动。
var声明在进入函数或全局作用域时已创建并初始化为undefined,所以声明前读取会得到undefined;赋值仍留在原位置。- 函数声明会较早完成初始化,通常可在声明文本之前调用。
let、const、class也会创建块级绑定,但声明执行前处于临时性死区,访问会抛ReferenceError,不能说它们“完全不提升”。
js
console.log(a); // undefined
var a = 1;
// console.log(b); // ReferenceError
let b = 2;同名声明、模块和块级函数声明还有更细的规范规则,工程代码不应依赖复杂的提升行为,最好先声明后使用。
359. 箭头函数和普通函数有啥区别?箭头函数能当构造函数吗?
答案:
箭头函数与普通函数的主要区别是:
- 箭头函数没有自己的
this,而是词法捕获外层this;call/apply/bind不能改写它。普通函数的this由调用方式决定。 - 箭头函数没有自己的
arguments、super、new.target,这些名字从外层环境取得;需要参数列表时应使用剩余参数。 - 箭头函数没有
[[Construct]],没有用于实例构造的prototype,所以new (() => {})会抛TypeError。 - 箭头函数不能作为 generator,不能在其函数体中使用
yield。 - 语法更简洁,并可用表达式函数体隐式返回值。
因此箭头函数适合回调和需要保留外层 this 的场景;对象方法、原型方法、事件处理器或构造器若需要动态 this,通常应使用普通函数。
360. WebSocket 中的心跳是为了解决什么问题?
答案:
WebSocket 心跳主要解决两类问题:
- 检测“半开连接”。网络切换、NAT 映射失效、进程异常等情况可能不会立刻触发
close,定期 ping 并要求 pong,超时后主动关闭和重连,才能较快发现失联。 - 保活。部分代理、负载均衡器、防火墙会回收长期空闲连接,周期性流量可避免被当作空闲连接清理。
协议本身定义了 Ping/Pong 控制帧,服务端和原生客户端通常可以使用;浏览器 WebSocket API 不暴露发送协议 Ping 的接口,因此网页常在应用层约定心跳消息。心跳间隔应小于中间设备的空闲超时,并加入超时、退避重连和随机抖动,避免大量客户端同时重连。收到任何有效业务消息时,也可视协议设计刷新存活时间。
361. 说说对 WebSocket 的了解
答案:
WebSocket 是建立在可靠传输连接之上的全双工消息协议。浏览器通常先通过 HTTP/1.1 Upgrade 握手,服务端返回 101 Switching Protocols 后,双方在同一连接上独立发送文本或二进制消息;生产环境一般使用加密的 wss://。
相比 HTTP 轮询,它的优势是服务器可主动推送、单条消息帧头较小、实时性好,适合聊天、协作编辑、行情和游戏。代价是服务端要维护长连接状态,还要处理鉴权、心跳、断线重连、消息补偿、背压和横向扩容。WebSocket 只保证连接内字节可靠有序,不自动提供业务消息确认、重放或跨重连的 exactly-once 语义。
浏览器 API 只有 open/message/error/close 等事件;跨域限制也不同于普通 CORS,服务端应校验 Origin 和身份凭证,并限制消息大小与频率。
362. Service worker是什么?
答案:
Service Worker 是浏览器在页面之外运行的、事件驱动的 Worker,可控制其作用域内页面的网络请求。它能监听 install、activate、fetch、push、sync 等事件,配合 Cache Storage 实现离线缓存、请求降级、后台同步和推送。
它没有 DOM 访问能力,也不是常驻线程,浏览器会在空闲时终止并在事件到来时重新启动,因此状态应存入 Cache Storage 或 IndexedDB,不能依赖全局变量长期存在。注册通常要求 HTTPS(本地开发例外),且控制范围受脚本路径和 scope 限制。
Service Worker 位于请求链路中,错误缓存策略可能长期发布旧资源;实践中要设计版本化、更新激活、缓存淘汰和网络失败策略。
363. 什么是 PWA?
答案:
PWA(渐进式 Web 应用)是一种让 Web 应用在支持能力的浏览器上获得更接近原生应用体验的建设方式,不是单一框架。常见组成包括 Web App Manifest、Service Worker、HTTPS、响应式界面,以及按需使用通知、后台同步等能力。
它可以提供可安装入口、独立窗口、离线或弱网可用、资源缓存和渐进增强:不支持某项能力的浏览器仍应能使用基本 Web 功能。优点是同一 URL 和代码分发、无需传统应用商店更新;边界是平台能力与浏览器支持不一致,后台运行、硬件访问和商店集成通常弱于原生应用。
“可安装”不等于自动高质量 PWA,仍需正确的缓存更新、离线回退、性能、可访问性和安全设计。
364. 如何判断一个对象是不是空对象?
答案:
先明确“空”的定义。若指没有自身可枚举字符串属性,常用:
js
function isEmptyObject(value) {
return value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
Object.keys(value).length === 0;
}如果连不可枚举属性和 Symbol 属性也算内容,应改用 Reflect.ownKeys(value).length === 0。若还要求必须是普通对象,可再检查原型为 Object.prototype 或 null。
JSON.stringify(obj) === '{}' 不可靠:它会忽略 undefined、函数、Symbol 键和不可枚举属性,还可能调用 toJSON、因循环引用而抛错。for...in 还会看到继承属性,也不适合直接判断自身是否为空。
365. NaN 是什么,用 typeof 会输出什么?
答案:
NaN 是 Number 类型中的特殊值,表示某次数值计算得不到有效数字,例如 Number('x') 或 0 / 0。因此:
js
typeof NaN; // "number"它与任何值(包括自己)做 === 都是 false。判断应使用 Number.isNaN(value);Object.is(value, NaN) 也能判断,但语义不如前者直接。
366. symbol 有什么用处?
答案:
Symbol 是原始类型,主要用于创建不会与普通字符串键冲突的属性键,以及实现语言协议。每次 Symbol(description) 都产生不同值;描述文本只用于调试,不决定相等性。
js
const id = Symbol('id');
const user = { [id]: 123 };
Object.keys(user); // []
Reflect.ownKeys(user); // [Symbol(id)]常见用途包括:
- 给对象或第三方对象添加低冲突的扩展字段。
- 使用
Symbol.iterator、Symbol.toPrimitive、Symbol.toStringTag等 well-known symbols 定制语言行为。 Symbol.for(key)/Symbol.keyFor通过全局注册表共享 Symbol。
Symbol 键不是私有字段:Object.getOwnPropertySymbols 和 Reflect.ownKeys 能取得它们。真正的类私有成员应使用 #field。Symbol 也不能隐式转换为字符串。
367. async/await 和 Promise 有什么关系?
答案:
async/await 建立在 Promise 语义之上,是编写 Promise 异步流程的语法形式。async 函数调用后总是返回 Promise:普通 return 成为 fulfilled 值,throw 成为 rejected 原因。await x 会按 Promise.resolve(x) 处理 x,暂停当前 async 函数,并在 Promise settled 后通过微任务继续;拒绝会在 await 位置抛出,可用 try/catch 处理。
js
async function load() {
try {
const response = await fetch('/api');
return await response.json();
} catch (error) {
console.error(error);
throw error;
}
}await 让依赖步骤更易读,但连续 await 会串行执行。互不依赖的任务应先启动,再 await Promise.all([...]),以免无意增加总耗时。await 不阻塞整个 JS 线程,只暂停当前 async 函数。
368. Promise中,resolve后面的语句是否还会执行?
答案:
会继续执行。调用 executor 参数 resolve 只尝试确定 Promise 的状态,不等同于 return,不会终止当前函数。.then 回调会在当前同步代码结束后的微任务中运行。
js
new Promise(resolve => {
console.log('A');
resolve('C');
console.log('B');
}).then(console.log);明确输出:
text
A
B
C后续再次调用 resolve 或 reject 不会改变已经确定的最终状态,但这些调用周围的普通语句仍会执行。若业务逻辑需要在解决后结束 executor,应显式写 return resolve(value) 或在下一行 return;这里的 return 只用于控制函数流程,其返回值会被 Promise 构造器忽略。
369. 写一个 LRU 缓存函数
答案:
可以利用 Map 保持插入顺序:最前面的键是最久未使用,访问后通过“删除再插入”移到末尾。
js
class LRUCache {
constructor(capacity) {
if (!Number.isInteger(capacity) || capacity <= 0) {
throw new RangeError('capacity must be a positive integer');
}
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return undefined;
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key, value) {
if (this.cache.has(key)) this.cache.delete(key);
this.cache.set(key, value);
if (this.cache.size > this.capacity) {
const leastRecentlyUsedKey = this.cache.keys().next().value;
this.cache.delete(leastRecentlyUsedKey);
}
return this;
}
has(key) {
return this.cache.has(key);
}
}
const lru = new LRUCache(2);
lru.put('a', 1).put('b', 2);
lru.get('a'); // 1,a 变为最近使用
lru.put('c', 3); // 淘汰 b
console.log(lru.has('b')); // falseget、put 的平均时间复杂度为 O(1),空间复杂度为 O(capacity)。未命中返回 undefined,因此若缓存值本身允许为 undefined,应用应使用 has 区分。Map 方案适合单进程内存缓存,不包含过期时间、并发一致性或持久化。
370. JSBridge是什么?
答案:
JSBridge 是 Hybrid App 中 WebView JavaScript 与 iOS/Android 原生代码之间的双向调用协议。Web 侧通常按“方法名、参数、调用 ID”发送请求,Native 查找允许的方法并执行,再用该 ID 回传成功或失败;Native 也可向 Web 推送事件。
常见通道有 WebView 注入对象/消息处理器,以及较旧的自定义 URL Scheme、拦截导航或 prompt。一个完整实现还需处理异步回调、序列化、超时、页面生命周期和版本协商。
安全边界很重要:只暴露白名单能力,校验页面来源、方法名和参数,限制任意导航与调试入口,敏感操作再次鉴权,并避免把可执行代码拼成字符串。JSBridge 不是某个统一标准,各容器的 API 和安全模型不同;设计不当可能让被注入或加载的网页直接获得设备高权限。
371. Babel 是什么?
答案:
Babel 是 JavaScript 源码转换工具链。它把较新的语法或提案语法解析为 AST,经插件转换后生成目标环境可执行的代码,例如把箭头函数、可选链等转换为较旧语法;@babel/preset-env 可根据目标浏览器选择转换。
Babel 主要转换语法,不会凭空给旧环境增加所有运行时 API。Promise、Map、Array.from 等能力通常需要按目标环境引入 core-js 等 polyfill;生成器或辅助函数也可能需要 runtime 支持。类型移除(TypeScript)、JSX 转换等同样可由相应插件完成。Babel 也不是打包器,模块打包、代码分块通常由 Vite、Webpack、Rollup 等工具负责。
372. CSR和SSR分别是什么?
答案:
**CSR(客户端渲染)**通常先返回较少的 HTML 和 JavaScript,浏览器执行脚本、请求数据并生成页面。首次可见内容依赖 JS 下载与执行,但页面启动后路由和交互可在客户端快速完成,服务器也可主要提供静态资源和 API。
**SSR(服务端渲染)**在请求时由服务器生成包含内容的 HTML,浏览器能更早展示和被爬虫读取;随后前端框架通常进行 hydration,把静态 HTML 接管为可交互应用。SSR 会增加服务器计算、缓存与同构代码复杂度,hydration 本身仍可能带来 JS 成本和不一致问题。
两者不是非此即彼:现代框架常按路由或组件混合 SSR、CSR、静态生成和流式渲染。需要 SEO、分享预览和较快首屏的内容页通常偏向 SSR/SSG;强交互后台系统可偏向 CSR。最终应以首屏性能、交互、数据时效、服务器成本和缓存策略选择。
373. 微前端中的应用隔离是什么,一般是怎么实现的?
答案:
微前端的应用隔离,是让多个微应用同页运行时,尽量不互相污染全局变量、DOM、样式、路由、事件和资源生命周期,并在卸载后恢复现场。通常分层实现:
- JavaScript 隔离:最强的是 iframe;同一页面方案常用 Proxy 包装
window、记录并恢复全局变量,也可使用 ShadowRealm/Worker 等受限环境。Proxy 沙箱挡不住所有闭包、原型修改和直接持有的真实全局对象,因此还需接入约束。 - 样式/DOM 隔离:iframe 天然隔离;Shadow DOM 可隔离大部分样式;也可在构建期给 CSS 加应用前缀、使用 CSS Modules,并限制 DOM 查询在应用容器内。Shadow DOM 不能隔离 JS,全局弹层还需专门挂载策略。
- 生命周期隔离:应用提供 bootstrap/mount/unmount,卸载时清理事件监听、定时器、订阅、WebSocket 和动态节点。
- 通信隔离:通过受控的 props、事件总线或消息协议通信,避免直接共享可变全局状态。
隔离强度与集成体验有取舍:iframe 最彻底但路由、布局和通信成本高;运行时沙箱体验更统一,却必须配合规范和清理机制。
374. 实现微前端有哪些技术方案?
答案:
常见微前端方案包括:
- 服务端/网关按路由聚合:Nginx 或服务端把不同路径交给不同应用。部署简单、隔离清楚,但跨应用切换常是整页刷新。
- iframe:每个子应用运行在独立浏览上下文,JS/CSS 隔离最好;缺点是自适应高度、路由、弹层、通信、登录态和无障碍集成较麻烦。
- 运行时容器编排:主应用负责路由、加载和 mount/unmount,子应用独立构建部署;体验统一,但要解决 JS 沙箱、样式隔离、公共依赖、通信和资源清理。single-spa、qiankun 属于这一方向。
- 构建时组合/Module Federation:运行时加载远程模块并共享依赖,组件级复用能力强;各应用的发布兼容、共享依赖版本和故障隔离需要严格治理。
- Web Components:以自定义元素和可选 Shadow DOM 作为接入协议,技术栈相对中立;状态、路由、服务共享仍需额外设计。
- 服务端片段/边缘组合:服务端把多个团队产出的 HTML 片段拼装后下发,首屏友好,但 hydration 和跨片段交互复杂。
选择依据是隔离强度、是否允许整页刷新、团队发布独立性、技术栈差异和迁移成本,而不是固定采用某一个框架。
375. 微前端可以解决什么问题?
答案:
微前端主要解决大型前端系统的组织与交付边界问题:
- 把单体应用按业务域拆给不同团队,降低仓库、构建、测试和发布的耦合。
- 允许子应用独立开发、部署和回滚,减少全量发布风险。
- 渐进式迁移遗留系统,使新旧技术栈在一段时间内共存,而不必一次性重写。
- 为多产品入口提供统一壳层、导航、登录和基础能力,同时保留业务团队自治。
它不天然解决代码质量、性能或团队协作,反而会引入重复依赖、跨应用通信、样式/运行时隔离、版本治理、监控和一致体验等成本。小型应用或单团队项目通常不值得采用。适合的前提是业务边界相对清晰,独立交付带来的收益大于基础设施和治理成本。
376. 什么是微前端?
答案:
微前端是一种前端架构方法:把大型 Web 前端按业务边界拆成多个可由不同团队独立开发、测试和部署的应用,再由统一入口在构建时、服务端或浏览器运行时组合成完整产品。
它借鉴微服务的自治和独立交付思想,但不是某个具体框架。一个成熟方案还需要定义路由与加载、生命周期、样式和 JavaScript 隔离、通信、鉴权、公共依赖、监控及故障降级。核心目标是组织和发布解耦;页面被拆成组件并不自动等于微前端,是否具有明确业务边界和独立交付能力更关键。
377. npm 是什么?
答案:
npm 同时指 Node.js 生态中常用的包管理 CLI、默认公共软件包注册中心及其相关服务。CLI 会依据 package.json 和锁文件安装依赖、解析版本、运行 scripts、发布包,也可执行包提供的命令。
bash
npm install lodash
npm run build
npm publish依赖默认安装到 node_modules,package-lock.json 用于记录可复现的解析结果。npm 不只是“下载工具”:它还涉及语义化版本范围、依赖树、工作区、认证和供应链安全。npx/npm exec 可执行本地或临时包命令;安装第三方包时仍需审查来源、版本和安装脚本。
378. forEach中return有效果吗?如何中断forEach循环?
答案:
forEach 回调里的 return 只结束当前这次回调,效果类似循环中的 continue;它不会让外层函数返回,也不能终止整个 forEach。forEach 自身始终返回 undefined。
需要提前停止时应选择与意图匹配的结构:
js
for (const item of items) {
if (shouldStop(item)) break;
}
const found = items.some(item => shouldStop(item)); // true 时停止
const allValid = items.every(item => isValid(item)); // false 时停止
const match = items.find(item => isTarget(item));通过抛异常可以强行跳出,但会混淆异常与正常控制流,通常不推荐。若只是跳过当前项,在回调中 return 是有效的。
379. 下面执行后输出什么?
js
for(var i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}答案:
明确输出为 5 个 6:
text
6
6
6
6
6var i 只有一个函数/全局作用域绑定,五个定时器回调闭包共享该绑定。同步 for 循环先执行完,此时 i 已递增为 6;随后事件循环才执行 setTimeout 回调,所以每次读取的都是同一个当前值 6。
把 var 改为 let 后,循环每次迭代会创建新的词法绑定,输出才是 1 到 5。定时器延迟为 0 也只表示达到最小延迟后排队,不会在当前同步循环中插入执行。
380. [] == ![]结果是什么?
答案:
结果是 true。转换过程为:
[]是对象,转布尔值为true,所以![]得到false。- 比较变成
[] == false。宽松相等把布尔值转为数字0。 - 另一边的空数组做 ToPrimitive 得到空字符串
'',再转数字得到0。 - 最终比较
0 == 0,结果为true。
js
[] == ![]; // true这类结果来自 == 的隐式转换规则。业务代码通常应使用 ===,此处 [] === ![] 为 false,因为两侧类型分别是对象和布尔值。
381. Object.is和===有什么区别?
答案:
Object.is 与 === 都不进行类型强制转换,大多数比较结果一致,只有两个关键差异:
js
NaN === NaN; // false
Object.is(NaN, NaN); // true
+0 === -0; // true
Object.is(+0, -0); // false=== 使用严格相等语义,Object.is 使用 SameValue 语义。对象比较时两者都比较引用,不会进行深比较。日常判断通常使用 ===;需要把 NaN 视为相同或区分正负零时使用 Object.is。数组方法 includes 使用的又是 SameValueZero:它能匹配 NaN,但不区分正负零。
382. instanceof能否判断基本数据类型?
答案:
普通情况下不能用 instanceof 可靠判断原始数据类型,因为它检查的是右侧构造器的 prototype 是否位于左侧对象的原型链上:
js
1 instanceof Number; // false
new Number(1) instanceof Number; // trueES6 允许右侧对象通过 Symbol.hasInstance 自定义判断,因此可以人为让它识别原始值:
js
class PrimitiveNumber {
static [Symbol.hasInstance](value) {
return typeof value === 'number';
}
}
console.log(1 instanceof PrimitiveNumber); // true这属于自定义协议,不改变 instanceof 默认基于原型链的含义。判断原始类型应直接用 typeof;判断 null 需用 value === null。
383. typeof 是否能正确判断类型?
答案:
typeof 能正确区分大多数原始类型:undefined、boolean、number、bigint、string、symbol,也能把可调用对象识别为 function。但它不能完整判断所有类型:
js
typeof null; // "object",历史遗留
typeof []; // "object"
typeof new Date(); // "object"
typeof /x/; // "object"因此判断数组用 Array.isArray,判断 null 用严格相等,判断类实例可用 instanceof(注意跨 realm 和原型变更),区分内置对象可按需要用 Object.prototype.toString.call 或专用 API。typeof undeclaredName 安全地返回 "undefined",但对处于临时性死区的 let/const 变量仍会抛 ReferenceError。
384. 什么是BigInt?
答案:
BigInt 是表示任意精度整数的原始类型,适合超过 Number.MAX_SAFE_INTEGER 的精确整数。可用 n 后缀或 BigInt() 创建:
js
const a = 9007199254740993n;
const b = BigInt('9007199254740993');
typeof a; // "bigint"它只表示整数,除法会向零截断。BigInt 与 Number 不能直接混合算术,必须显式转换;转换为 Number 可能丢失精度。它不能用于 Math 大多数方法,JSON.stringify 默认也不能直接序列化 BigInt,需要自定义表示方式。1n == 1 为 true,但 1n === 1 为 false,工程代码应避免依赖宽松相等。
385. 0.1+0.2为什么不等于0.3?
答案:
JavaScript 的 Number 采用 IEEE 754 双精度浮点数。0.1 和 0.2 转成二进制都是无限循环小数,只能舍入到 53 位有效二进制精度;两个近似值相加后,得到的可表示值是 0.30000000000000004,而不是精确的 0.3。
这不是 JavaScript 独有的问题,而是二进制浮点数的表示边界。浮点数比较通常应使用容差,并让容差随数值量级变化:
js
function nearlyEqual(a, b, epsilon = Number.EPSILON) {
return Math.abs(a - b) <= epsilon * Math.max(1, Math.abs(a), Math.abs(b));
}
console.log(nearlyEqual(0.1 + 0.2, 0.3)); // true金额等要求十进制精确的场景,不应只靠 EPSILON。可以把金额转换成最小货币单位后用整数计算;若数值可能超过 Number.MAX_SAFE_INTEGER,则使用 BigInt;需要任意精度小数及明确舍入规则时,应使用 decimal.js、big.js 一类十进制库。
386. '1'.toString()为什么不会报错?
答案:
字符串是基本类型,但读取基本类型的属性或调用其方法时,JavaScript 会临时执行装箱转换:把 '1' 转成对应的 String 包装对象,查找并调用 String.prototype.toString,调用结束后这个临时对象即可被回收。因此 '1'.toString() 能正常返回 '1',并不表示原值变成了对象。
这个过程可近似理解为:
js
const temp = Object('1');
const result = temp.toString();
// result === '1'Boolean、Number、String、Symbol 和 BigInt 基本值都能在属性访问时进行类似转换,但 null 和 undefined 不能装箱,访问它们的属性会抛出 TypeError。还要区分语法问题:'1'.toString() 没有歧义;数字字面量写成 1.toString() 会因小数点解析产生语法错误,应写成 (1).toString()、1..toString() 或 1 .toString()。
387. null是对象吗?为什么?
答案:
null 不是对象,它是一个独立的基本类型值,语义是“明确地没有对象或值”。typeof null 返回 'object' 是 JavaScript 早期类型标记设计遗留的兼容性问题,不能据此把 null 当作对象。
判断 null 应使用严格相等:
js
value === null如果要同时判断 null 和 undefined,可以有意使用 value == null,因为只有这两个值与 null 宽松相等;团队代码中应注明这种写法的意图。Object.prototype.toString.call(null) 返回 '[object Null]',也可用于通用类型识别。
388. 什么是内存泄漏?什么原因会导致呢?
答案:
内存泄漏是程序已经不再需要某块内存,但这块内存仍然可从垃圾回收器的根对象访问,因此无法被回收,长期运行后内存占用持续增长。现代 JavaScript 垃圾回收主要依据“可达性”,不能简单理解成引用计数为零。
常见原因包括:
- 意外创建全局变量,或把大对象长期挂在 window、全局单例上。
- 闭包长期保存已无用的大对象或 DOM 节点。
- 组件销毁后没有移除事件监听器、Observer、订阅或 WebSocket 回调。
- setInterval、长定时器、未结束的异步任务持续捕获上下文。
- 已从文档移除的 DOM 节点仍被 JavaScript 引用。
- Map、数组、缓存只增不减,没有容量上限、过期策略或 WeakMap 语义。
处理原则是让对象在不用时变得不可达:在生命周期清理函数中取消监听、定时器和请求,关闭连接,删除缓存项,避免无界集合,并缩小闭包捕获范围。排查时可用 Chrome DevTools 连续获取 Heap Snapshot,对比 retained size 和对象增长路径;仅观察一次内存升高不能断定泄漏,因为垃圾回收可能尚未发生。
389. webSocket如何兼容低浏览器
答案:
应先做能力检测,而不是仅根据 User-Agent 判断:
js
if ('WebSocket' in window) {
// 使用 WebSocket
} else {
// 使用降级传输
}现代可维护的降级方案通常是:服务端推送使用 SSE,客户端上行使用普通 HTTP;需要双向语义时使用长轮询,并在客户端统一封装连接、重连、心跳、消息序号和鉴权。SockJS 等库可以在支持 WebSocket 时使用它,在不支持或代理阻断时自动退化到流式传输或长轮询,但服务端也必须支持相同协议。
早期的 Flash Socket、ActiveX/HTMLFile 等方案已经淘汰,不应再作为生产兼容策略。还要考虑“浏览器支持但网络不可用”的情况:企业代理可能阻断 Upgrade,因此应监听连接失败并切换传输;HTTPS 页面应使用 wss,服务端需要正确处理反向代理的 Upgrade 头,并通过心跳、指数退避和消息去重保证弱网下的可靠性。
390. 如何实现浏览器内多个标签页之间的通信?
答案:
同源标签页通信优先使用 BroadcastChannel,接口直接、消息采用结构化克隆,关闭页面时调用 close 即可:
js
const channel = new BroadcastChannel('app-events');
channel.onmessage = ({ data }) => console.log(data);
channel.postMessage({ type: 'logout' });其他方案各有边界:
- localStorage 配合 storage 事件:兼容性好,但事件只在同源的其他文档触发,不会在执行 setItem 的当前页触发;适合低频小消息,不适合作为可靠队列。
- SharedWorker:多个同源页面共享一个 Worker,通过 MessagePort 通信,适合共享连接或状态,但支持范围和生命周期要单独评估。
- Service Worker:可通过 clients.matchAll 和 postMessage 协调受其控制的页面,适合已有 PWA/离线架构的应用。
- window.postMessage:适用于能拿到对方 window 引用的 opener、iframe 等关系,也支持跨源,但必须指定精确 targetOrigin,并校验 event.origin 和消息结构。
- WebSocket:通过服务端转发,可跨设备、跨浏览器实例同步,代价是需要连接管理和服务端状态。
sessionStorage 默认按顶层浏览上下文隔离,不能当作通用的多标签共享存储。通信涉及登录态、支付等敏感操作时,还要增加消息版本、来源校验和幂等处理。
391. 什么是跨域?
答案:
源由协议、主机和端口三部分组成,任一不同就是不同源。例如 https://a.example.com 与 http://a.example.com 协议不同,与 https://b.example.com 主机不同,与 https://a.example.com:8443 端口不同。
跨域本身不是错误;浏览器的同源策略限制一个源的脚本读取或操作另一个源的敏感资源,例如读取跨源 AJAX 响应、访问跨源 iframe 的 DOM。图片、脚本等部分资源可以跨源加载,但仍受 CSP、CORS、CORP 等规则约束。某个跨源请求也可能已经发送到服务器,只是响应不允许被脚本读取,因此不能把 CORS 当作服务端 CSRF 防护。
常见解决方式:
- API 由服务端正确配置 CORS,精确返回允许的 Origin、方法和请求头;非简单请求会先发 OPTIONS 预检。携带凭证时不能把 Access-Control-Allow-Origin 写成 *,还需明确允许凭证。
- 开发或部署层使用同源反向代理,由代理转发到目标服务。
- 跨窗口通信使用 postMessage,并校验 origin。
- JSONP 只支持 GET,且会执行远端脚本,是遗留方案。
document.domain 放宽子域隔离的做法能力有限且已不推荐。CORS 响应头必须由目标服务器或可信代理设置,前端无法通过给请求随意加头来解除同源策略。
392. Axios的原理是什么?
答案:
Axios 本质上是基于 Promise 的 HTTP 客户端封装。浏览器中通常通过适配器调用 XMLHttpRequest 或可用的 fetch,Node.js 中可使用相应的 HTTP 适配器;上层 API 不依赖具体传输实现。
一次请求大致经过这些步骤:
- axios(config) 实际调用 Axios 实例绑定后的 request 方法,同时实例方法又被混入该函数,所以既能 axios(config),也能 axios.get(url)。
- 合并库默认配置、实例默认配置和本次请求配置,并规范化 method、headers、baseURL、params 等字段。
- 组装 Promise 链。请求拦截器在发送前处理配置,响应拦截器在收到结果后处理响应或错误;请求拦截器通常按后注册先执行,响应拦截器按先注册先执行。
- dispatchRequest 执行请求/响应数据转换,选择适配器发送请求,并把状态、响应头和响应体整理为统一响应对象。
- 超时、网络错误、非预期状态码和取消操作进入拒绝链。现代代码通常用 AbortController 的 signal 取消请求。
拦截器并不是网络层中间件,而是围绕 dispatch 的链式处理,因此每个拦截器都必须返回配置/响应或 Promise,否则后续链会拿到 undefined。Axios 也不会自动解决 CORS、CSRF、重试和业务幂等;这些需要服务端配置或应用层策略。
393. 说说你对SPA的理解
答案:
SPA(Single Page Application)通常只首次加载一个 HTML 外壳,后续导航由前端路由拦截,按需加载组件和数据并更新当前文档,而不是每次跳转都向服务器请求一份新页面。路由常用两种模式:hash 模式监听 hashchange;history 模式使用 pushState/replaceState 和 popstate,后者要求服务器把未知前端路由回退到入口 HTML。
优点是页面切换快、状态共享自然、前后端职责清晰,适合交互密集的后台系统。代价是首屏 JavaScript 可能较大,弱网白屏和运行时错误影响更明显;纯客户端渲染还会增加 SEO、社交分享、无 JavaScript 可用性和监控的难度。
常见优化包括路由级代码分割、预加载关键资源、缓存接口数据、骨架屏,以及 SSR、SSG 或预渲染。SSR 生成首屏 HTML 后再 hydration,可以改善首屏和可抓取性,但会引入服务端渲染成本和水合一致性问题。
SPA 描述的是应用的页面与导航模型,不等于某个框架;Vue、React 既能构建 SPA,也能构建多页或服务端渲染应用。内容站、强 SEO 页面或脚本关闭后仍需完整工作的业务,MPA 或混合渲染往往更合适。
394. web常见的攻击方式有哪些,以及如何进行防御?
答案:
Web 防御要按攻击面分层,不能依赖某一个前端措施:
- XSS:攻击者让页面执行恶意脚本。输出到 HTML、属性、URL、JavaScript 等不同上下文时分别做正确编码;富文本用白名单净化;避免 innerHTML/eval;配合严格 CSP、HttpOnly Cookie 和 Trusted Types。只过滤 script 标签不够。
- CSRF:浏览器自动携带用户凭证发起伪造请求。使用 SameSite Cookie、不可预测且与会话绑定的 CSRF Token,并校验 Origin/Referer;修改操作不用 GET。CORS 不是 CSRF 防线。
- SQL/命令/模板注入:服务端使用参数化查询和安全 API,禁止拼接命令,限制数据库权限,并对输入做语义校验。前端校验只改善体验,不能提供安全边界。
- 点击劫持:使用 CSP frame-ancestors 或 X-Frame-Options 限制页面被嵌入,敏感操作增加确认。
- 会话劫持与中间人攻击:全站 HTTPS、HSTS,Cookie 设置 Secure、HttpOnly、合适的 SameSite;会话轮换、短有效期和异常登录检测。
- 文件上传与路径遍历:服务端校验真实类型、大小和文件名,随机化存储名,放到不可执行且与主站隔离的位置,路径解析后验证仍在允许目录。
- 依赖和供应链攻击:锁定依赖、审计漏洞、最小化第三方脚本,对外部静态资源使用 SRI,并保护构建与发布凭证。
此外要配置速率限制、权限校验、日志告警和安全响应头。认证只证明“是谁”,每个接口仍必须检查该用户是否有权操作目标资源,避免越权和 IDOR。
395. 如何实现上拉加载,下拉刷新?
答案:
上拉加载的核心是检测列表尾部哨兵进入滚动容器,然后请求下一页。优先使用 IntersectionObserver,避免在每次 scroll 中同步读取布局:
js
const observer = new IntersectionObserver(async ([entry]) => {
if (!entry.isIntersecting || loading || !hasMore) return;
loading = true;
try {
const page = await loadNextPage(cursor);
appendItems(page.items);
cursor = page.nextCursor;
hasMore = page.nextCursor != null;
} finally {
loading = false;
}
}, { root: scrollContainer, rootMargin: '0px 0px 200px 0px' });
observer.observe(sentinel);下拉刷新通常监听触摸或 Pointer Events:仅在容器已滚到顶部且手势向下时进入拖拽状态,对位移增加阻尼;超过阈值后在释放时触发刷新,否则回弹。移动端还需处理 touch-action、overscroll-behavior,避免与浏览器原生回弹冲突;如果平台已有原生下拉刷新,应优先复用。
两种交互都应维护 idle/loading/success/error 状态,防止重复请求;使用游标分页可减少数据插入造成的重复或遗漏。组件卸载时断开 Observer、移除事件并 AbortController.abort。列表较长时还应配合虚拟列表。scroll 兜底方案需要节流,并比较 scrollTop + clientHeight 与 scrollHeight,但要注意图片加载会改变高度。
396. 大文件怎么实现断点续传?
答案:
断点续传通常把文件切成固定大小的分片,为文件建立稳定标识,查询服务端已接收分片,只补传缺失部分,最后由服务端校验并合并。
前端流程如下:
js
function splitFile(file, chunkSize = 5 * 1024 * 1024) {
const chunks = [];
for (let start = 0, index = 0; start < file.size; start += chunkSize, index++) {
chunks.push({ index, blob: file.slice(start, start + chunkSize) });
}
return chunks;
}完整协议应包含:
- 计算文件标识。可组合文件名、大小、最后修改时间做快速标识,但真正去重或完整性校验应使用文件内容哈希;大文件哈希要分块计算或放到 Worker,避免阻塞主线程。
- 调用初始化/状态接口,获得 uploadId 和已上传分片集合。
- 对缺失分片并发上传,携带 uploadId、index、分片哈希;限制并发数,对网络错误指数退避重试,并支持 AbortController 暂停。
- 请求合并。服务端按索引组装,在临时目录中原子完成,验证分片数、总大小和最终哈希后再发布文件。
服务端不能信任客户端给出的路径和哈希,必须鉴权、限制大小与分片数、防止越权合并,并定期清理过期上传。对象存储场景可直接采用 S3 Multipart Upload 一类原生协议和预签名 URL。前端切片的时间复杂度为 O(n) 个分片描述,真正读取/哈希/上传的数据量为 O(fileSize);内存应保持 O(concurrency × chunkSize),不要一次读入整个文件。
397. 如何判断一个元素是否在可视区域中?
答案:
最直接的方法是读取元素的视口坐标。只要元素与视口有交集,就算部分可见:
js
function isPartlyVisible(el) {
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0 &&
rect.bottom > 0 && rect.right > 0 &&
rect.top < window.innerHeight && rect.left < window.innerWidth;
}
function isFullyVisible(el) {
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0 &&
rect.top >= 0 && rect.left >= 0 &&
rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
}高频或大量元素检测应使用 IntersectionObserver。它能异步报告目标与 root 的交叉比例,并可通过 threshold 和 rootMargin 定义“可见”的标准:
js
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
console.log(entry.target, entry.isIntersecting, entry.intersectionRatio);
}
}, { threshold: [0, 0.5, 1] });“可视”要先定义边界:滚动容器不是窗口时应把容器传给 root;被 opacity: 0、visibility: hidden 或其他元素遮挡时,几何相交仍可能成立;display: none 通常没有有效矩形。广告可见性等严格场景不能仅靠几何位置判断。
398. 什么是防抖和节流,以及如何编码实现?
答案:
防抖是在事件停止触发一段时间后只执行一次,适合搜索联想、输入校验、窗口尺寸变化后的计算。节流是在持续触发期间按固定间隔最多执行一次,适合滚动、拖拽、指针移动等持续反馈。
可运行实现如下,二者都保留 this 和参数,并提供取消能力:
js
function debounce(fn, wait) {
let timer = null;
function debounced(...args) {
const context = this;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn.apply(context, args);
}, wait);
}
debounced.cancel = () => {
clearTimeout(timer);
timer = null;
};
return debounced;
}
function throttle(fn, wait) {
let lastTime = 0;
let timer = null;
function throttled(...args) {
const context = this;
const remaining = wait - (Date.now() - lastTime);
if (remaining <= 0) {
clearTimeout(timer);
timer = null;
lastTime = Date.now();
fn.apply(context, args);
} else if (timer === null) {
timer = setTimeout(() => {
timer = null;
lastTime = Date.now();
fn.apply(context, args);
}, remaining);
}
}
throttled.cancel = () => {
clearTimeout(timer);
timer = null;
lastTime = 0;
};
return throttled;
}每次调用的时间、额外空间复杂度都是 O(1)。组件卸载时要调用 cancel,避免回调在销毁后执行。实际项目还需明确 leading、trailing 和 maxWait 语义;动画更新通常用 requestAnimationFrame 节流更贴合浏览器刷新周期。
399. 说说 Javascript 为什么会存在数字精度丢失的问题,以及如何进行解决?
答案:
JavaScript 的 Number 是 IEEE 754 binary64:1 位符号、11 位指数、52 位显式尾数,加上隐含位后约有 53 位二进制有效精度。许多十进制小数无法用有限二进制表示,存储时必须舍入,所以计算可能得到 0.30000000000000004 之类的结果。精度问题还包括大整数:超过 Number.MAX_SAFE_INTEGER 后,相邻整数不一定能区分。
解决方式取决于业务:
- 近似计算与比较:使用与数值量级相关的误差范围,不直接用 === 比较运算结果。
- 金额或固定小数位:统一转换成最小单位的整数计算,并明确四舍五入规则;转换本身也要谨慎,例如先按业务规则解析十进制字符串。
- 超大整数:使用 BigInt,但它不能与 Number 直接混算,也不表示小数,JSON 默认也不能序列化 BigInt。
- 任意精度十进制:使用 decimal.js、big.js 等库,在输入阶段尽量传十进制字符串,避免先经过 Number 丢失精度。
js
console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2); // true
console.log(0.1 + 0.2); // 0.30000000000000004toFixed 主要负责按规则格式化并返回字符串,不能恢复此前已经丢失的精度;把所有结果统一乘 100 也不总可靠,因为乘法本身仍使用二进制浮点数。
400. Javascript中如何实现函数缓存?函数缓存有哪些应用场景?
答案:
函数缓存(memoization)把参数映射到计算结果;相同输入再次调用时直接返回缓存。它适用于计算昂贵、重复调用多且结果主要由参数决定的函数,例如递归动态规划、解析/编译、格式化和派生数据选择器。
一个支持同步函数和自定义键的实现:
js
function memoize(fn, getKey = (...args) => args[0]) {
const cache = new Map();
function memoized(...args) {
const key = getKey(...args);
if (cache.has(key)) return cache.get(key);
const value = fn.apply(this, args);
cache.set(key, value);
return value;
}
memoized.cache = cache;
memoized.clear = () => cache.clear();
return memoized;
}
const fibonacci = memoize(function fib(n) {
if (!Number.isInteger(n) || n < 0) throw new RangeError('n must be a non-negative integer');
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
});Map 查询平均为 O(1),命中时除构造键外为 O(1);空间复杂度为 O(k),k 是不同键数量。多参数不能草率使用 JSON.stringify:属性顺序、循环引用、函数和 undefined 都可能造成问题。异步函数缓存 Promise 能合并并发请求,但失败的 Promise 通常应从缓存删除以便重试。缓存还应考虑 this、数据过期、容量上限和对象键的生命周期;无界 Map 本身可能造成内存泄漏,对仅以对象为键的场景可考虑 WeakMap。
401. 说说你对函数式编程的理解,以及优缺点?
答案:
函数式编程把计算表达为函数的组合,强调声明“结果是什么”,并尽量使用纯函数、不可变数据、高阶函数和函数组合。纯函数在相同输入下返回相同输出,且不修改外部状态;副作用并非完全禁止,而是被集中在系统边界,例如网络、存储和 UI 更新处。
js
const activeTotal = orders => orders
.filter(order => order.active)
.map(order => order.amount)
.reduce((sum, amount) => sum + amount, 0);优点包括:函数容易独立测试和复用;数据流明确,减少共享可变状态导致的时序问题;组合、惰性求值、并行化和缓存更容易推理。React/Redux 中的 reducer、数组 map/filter/reduce 都体现了这种思想。
缺点是频繁复制不可变结构可能带来时间与内存开销;过度 point-free、层层组合或抽象代数概念会降低团队可读性;真实业务的 I/O 和状态变化仍需建模;递归还受 JavaScript 调用栈与尾调用支持限制。
实践中更重要的是采用可控原则,而不是追求“完全无副作用”:业务核心保持纯净,边界显式注入依赖,更新复杂大对象时使用结构共享库或有纪律的局部可变实现。
402. Javascript本地存储的方式有哪些,有什么区别,及有哪些应用场景?
答案:
浏览器常见本地存储方式有:
| 方式 | 生命周期与容量特点 | 是否随请求发送 | 典型场景 |
|---|---|---|---|
| Cookie | 可设过期时间,单项通常约 4 KB | 同域匹配时自动携带 | 服务端会话标识、少量偏好 |
| localStorage | 同源持久保存,通常为数 MB,具体配额由浏览器决定 | 否 | 少量非敏感配置、草稿 |
| sessionStorage | 按标签页会话隔离,页面会话结束后清除 | 否 | 单标签页临时流程状态 |
| IndexedDB | 异步事务型对象数据库,可存大量结构化数据和 Blob | 否 | 离线数据、复杂查询、大数据 |
| Cache Storage | 保存 Request/Response,通常由 Service Worker 使用 | 否 | PWA 静态资源和接口响应缓存 |
localStorage 和 sessionStorage API 是同步的,大量读写会阻塞主线程,且只存字符串;复杂数据需序列化,并处理版本迁移和解析失败。IndexedDB 更适合大量数据,但 API、事务和索引设计更复杂。配额和淘汰策略因浏览器、设备和隐私模式而异,不能把客户端存储当永久数据库。
安全上,能在页面执行的脚本通常能读取 Web Storage 和 IndexedDB,因此不要保存密码、长期访问令牌等高价值秘密。会话 Cookie 可设置 HttpOnly 以阻止 JavaScript 读取,并配合 Secure、SameSite 和 CSRF 防护。Cookie 体积会增加匹配请求的网络开销,也不应滥用。
403. 说说 JavaScript 中内存泄漏有哪几种情况?
答案:
JavaScript 中典型的内存泄漏场景包括:
- 未声明赋值形成全局变量,或全局数组/单例不断累积对象。
- 闭包保存了不再需要的大数组、响应数据或 DOM 引用,且闭包仍被外部持有。
- DOM 节点已移除,但仍存在于变量、事件处理器或第三方组件缓存中,形成 detached DOM tree。
- addEventListener、事件总线、Observable、MutationObserver 等订阅未在组件销毁时取消。
- setInterval、递归 setTimeout、requestAnimationFrame 持续运行,回调捕获组件状态。
- Map、memoize 缓存、日志队列没有 TTL、LRU 上限或删除机制;WeakMap 使用不当也可能因 value 反向持有 key 而达不到预期。
- 未关闭的 WebSocket、Worker、流或长期挂起的 Promise 回调间接保留大量对象。
垃圾回收判断的是从全局对象、当前执行栈等根出发是否仍可达,而不是对象在业务上是否“已经没用”。因此修复重点是解除引用和结束资源生命周期:保存可移除的回调引用,清除定时器,disconnect Observer,abort 请求,关闭连接,并限制缓存。
排查可在固定操作前后强制经过多轮 GC 条件,比较 Heap Snapshot 的对象数量和 retained size,再沿 retaining path 找到根引用。内存曲线锯齿上升后能回落通常是正常分配;多轮相同操作后基线持续上升才更像泄漏。
404. 举例说明你对尾递归的理解,以及有哪些应用场景
答案:
尾递归是函数最后一步直接返回递归调用的结果,递归返回后不再做乘法、加法等工作。这样实现的调用理论上可复用当前栈帧,即尾调用优化。
普通阶乘不是尾递归,因为递归返回后还要乘 n;使用累加器后才是:
js
function factorialTail(n, acc = 1n) {
if (!Number.isInteger(n) || n < 0) throw new RangeError('n must be a non-negative integer');
if (n === 0) return acc;
return factorialTail(n - 1, BigInt(n) * acc);
}
console.log(factorialTail(5)); // 120n时间复杂度为 O(n)。只有引擎实现了正确尾调用优化时,调用栈空间才可为 O(1);否则仍是 O(n),n 很大时会栈溢出。ES2015 规范描述了严格模式下的正确尾调用,但主流 JavaScript 引擎并未普遍提供可依赖的实现,因此生产代码不应假设尾递归一定节省栈。
需要处理深层遍历、状态机、动态规划或数学递归时,可改写成循环,或使用 trampoline:让每一步返回下一个无参函数,由循环逐步执行,从而把栈空间降为 O(1)。尾递归主要改善递归表达与可优化性,并不会降低 O(n) 的运算次数。
405. 说说你对BOM的理解,以及常见的BOM对象有哪些?
答案:
BOM(Browser Object Model)是浏览器向 JavaScript 暴露的窗口及浏览器环境 API 的统称。它不像 DOM 那样只描述文档树;核心入口是 window。现代 BOM 能力分别由 HTML 等 Web 标准定义,“BOM”更多是开发中的分类概念。
常见对象和用途:
- window:当前浏览上下文的全局对象,提供窗口尺寸、滚动、弹窗、定时器、requestAnimationFrame 等能力。
- location:读取和修改 URL,assign/replace/reload 控制导航。修改 location 会触发页面跳转。
- history:back、forward、go 以及 pushState/replaceState;SPA 需监听 popstate。它不能任意读取用户的完整历史记录。
- navigator:浏览器与设备能力入口,如 userAgent、language、onLine、clipboard、geolocation 等;应优先做能力检测,不要依赖 UA 猜功能。
- screen:屏幕尺寸、可用区域、色深等信息,不等同于页面视口。
- frames、parent、top、opener:窗口关系;跨源窗口受同源策略限制,通信应使用 postMessage。
此外 alert/confirm、localStorage、fetch 等也常被广义归入浏览器环境 API。很多 API 受权限、用户手势、安全上下文和隐私策略限制,例如剪贴板、定位与弹窗不能保证随时可用。
406. 谈谈你知道的DOM常见的操作
答案:
DOM 操作可按职责归纳:
- 查询:document.getElementById、querySelector/querySelectorAll、closest、matches。
- 创建与复制:createElement、createTextNode、cloneNode、DocumentFragment。
- 插入、替换和删除:append/prepend、before/after、replaceWith、remove;传统 API 有 appendChild、insertBefore、replaceChild、removeChild。
- 内容:textContent 处理纯文本;innerHTML 解析 HTML,使用不可信输入时会产生 XSS 风险,应做上下文安全的净化。
- 属性与状态:getAttribute/setAttribute/removeAttribute、dataset、元素属性、classList、style。attribute 与 property 不总等价,例如 input 的 value 属性是初始值,而 value 属性值可反映当前状态。
- 事件:addEventListener/removeEventListener,也可利用冒泡做事件代理。
- 尺寸与位置:getBoundingClientRect、clientWidth、scrollTop、getComputedStyle。
性能上,应减少在循环中交替读取布局信息和写样式,否则会触发反复同步布局。可以先批量读取,再在 requestAnimationFrame 中集中写入;大量节点先放入 DocumentFragment,或一次 append 多个节点。列表更新应尽量复用节点,超长列表使用虚拟滚动。
querySelectorAll 返回静态 NodeList;getElementsByClassName 等部分旧接口返回实时集合,边遍历边修改 DOM 时结果可能变化。移除节点后还要按组件生命周期清理监听、Observer 和外部引用,避免内存泄漏。
407. 说说你对事件循环的理解
答案:
JavaScript 在一个线程上通过调用栈执行同步代码;浏览器把定时器、网络、事件等交给宿主环境处理,完成后将对应任务放入队列。事件循环在调用栈清空后取出一个宏任务执行,随后清空微任务队列,再给浏览器渲染机会,然后进入下一轮。
常见宏任务包括初始脚本、setTimeout、用户事件和 MessageChannel;微任务包括 Promise.then/catch/finally、queueMicrotask 和 MutationObserver。执行微任务时新产生的微任务也会在本轮继续执行,因此无限追加微任务会阻塞渲染和后续任务。
js
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
queueMicrotask(() => console.log('D'));
console.log('E');明确输出为 A、E、C、D、B。同步代码先执行;Promise 回调和 queueMicrotask 按入队顺序在当前宏任务结束后执行;setTimeout 回调属于后续宏任务。0 毫秒只表示达到最短延迟后可排队,不保证立即运行。
Node.js 也有事件循环,但阶段划分和 process.nextTick、setImmediate 的优先级与浏览器不同,不能把浏览器顺序机械套用。async/await 中 await 之后的代码本质上也通过 Promise 反应进入微任务。
408. 正则表达式是什么,有哪些应用场景?
答案:
正则表达式是描述字符串模式的语法,可用于匹配、查找、提取、替换和分割文本。JavaScript 可用字面量 /pattern/flags 或 new RegExp(pattern, flags) 创建;动态模式必须对用户提供的文本做正则转义。
常见能力包括字符类、量词、分组与捕获、边界、或分支、前后向断言。常用标志有 g(全局)、i(忽略大小写)、m(多行)、s(点匹配换行)、u(Unicode)和 y(粘连)。RegExp.test/exec 用于测试和逐次提取,String 的 match/matchAll/replace/search/split 用于字符串处理。
应用场景包括:从日志或 URL 中提取字段;批量文本替换;词法分析中的简单 token;输入格式的初步校验;编辑器搜索。复杂 HTML、完整编程语言、带嵌套结构的数据不应只靠正则解析,应使用对应解析器。邮箱、URL 等真实标准也很复杂,通常先做合理格式检查,再通过发送验证邮件或实际请求确认。
边界与风险:带 g 或 y 的正则是有状态的,test/exec 会更新 lastIndex;不当的嵌套量词和回溯可造成灾难性性能,处理不可信长文本时要限制长度并避免高风险模式。正则通常按输入长度线性扫描,但回溯型模式最坏可能指数级,不能笼统认为永远是 O(n)。
409. bind、call、apply 有什么区别?如何实现一个bind?
答案:
call、apply 和 bind 都能控制普通函数调用时的 this:call(thisArg, a, b) 立即调用并逐个传参;apply(thisArg, [a, b]) 立即调用并以数组或类数组传参;bind(thisArg, a) 不立即调用,而是返回绑定 this 且可预置参数的新函数。
一个支持偏函数和 new 调用的实现如下:
js
Function.prototype.myBind = function (thisArg, ...boundArgs) {
const target = this;
if (typeof target !== 'function') throw new TypeError('Bind target must be callable');
function bound(...callArgs) {
const args = [...boundArgs, ...callArgs];
if (new.target) {
return Reflect.construct(target, args, new.target);
}
return Reflect.apply(target, thisArg, args);
}
if (target.prototype) {
bound.prototype = Object.create(target.prototype, {
constructor: { value: bound, writable: true, configurable: true }
});
}
return bound;
};普通调用的时间复杂度为 O(m+n),因为要组合预置参数和本次参数;额外空间也是 O(m+n)。new bound() 时,构造产生的新对象应成为 this,绑定的 thisArg 会被忽略;构造函数显式返回对象时仍遵循构造返回规则。
该面试实现不能完全复制原生绑定函数的内部槽,以及 name、length、prototype 等所有描述符语义。箭头函数的 this 来自外层词法环境,call/apply/bind 都不能改变它;严格模式普通函数传 null/undefined 时 this 保持原值,非严格模式则可能替换为全局对象。
410. 说说ajax的原理,以及如何实现?
答案:
AJAX 是浏览器在不整页刷新的情况下,通过 JavaScript 发起 HTTP 请求并异步更新页面的技术模式,不是某一种协议。传统实现使用 XMLHttpRequest,现代代码也常用 fetch。
一个可运行的 Promise 封装:
js
function ajax({ method = 'GET', url, data, headers = {}, timeout = 10000 }) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.timeout = timeout;
xhr.responseType = 'json';
for (const [name, value] of Object.entries(headers)) {
xhr.setRequestHeader(name, value);
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({ status: xhr.status, data: xhr.response, xhr });
} else {
reject(new Error('HTTP status ' + xhr.status));
}
};
xhr.onerror = () => reject(new TypeError('Network error'));
xhr.ontimeout = () => reject(new Error('Request timeout'));
xhr.onabort = () => reject(new DOMException('Aborted', 'AbortError'));
xhr.send(data == null ? null : JSON.stringify(data));
});
}
ajax({
method: 'POST',
url: '/api/items',
data: { name: 'demo' },
headers: { 'Content-Type': 'application/json' }
});创建 XHR、open、设置回调和 send 都在主线程完成;网络由浏览器处理,完成事件进入任务队列,调用栈空闲后执行回调。请求的数据处理时间和内存均至少与请求/响应体大小 O(n) 相关。
边界包括同源策略/CORS、超时和取消、重复提交、状态码与业务错误的区分。GET 参数应编码到 URL;上传文件应发送 FormData,不能固定 JSON 化;204 响应或非 JSON 响应也要按协议处理。AJAX 异步不代表服务端操作天然幂等。
411. 说说new操作符具体干了什么?
答案:
执行 new Ctor(...args) 时,语义上会:
- 创建一个新对象。
- 把新对象的 [[Prototype]] 设为 Ctor.prototype;如果该值不是对象,则使用默认对象原型。
- 以新对象作为 this 调用构造函数,并传入参数。
- 如果构造函数显式返回非 null 的对象或函数,就返回该值;否则返回步骤 1 的新对象。
面试版实现:
js
function myNew(Ctor, ...args) {
if (typeof Ctor !== 'function') throw new TypeError('Ctor must be a constructor');
const proto = typeof Ctor.prototype === 'object' && Ctor.prototype !== null
? Ctor.prototype
: Object.prototype;
const instance = Object.create(proto);
const result = Ctor.apply(instance, args);
const isObject = result !== null &&
(typeof result === 'object' || typeof result === 'function');
return isObject ? result : instance;
}参数转发的时间和额外空间复杂度为 O(n),n 为参数数量;创建对象本身可视为 O(1)。这个手写版本不支持 class 构造器,因为 class 不能用 apply 普通调用,也不能完整模拟内建构造器和 new.target。生产代码要实现原生构造语义,应直接使用 Reflect.construct(Ctor, args)。箭头函数和部分不可构造函数即使 typeof 为 function,也不能被 new。
412. 什么是事件代理,以及它的应用场景有哪些?
答案:
事件代理是把多个子元素的同类事件监听放到共同祖先上,利用事件冒泡,在祖先处理器中根据 event.target 找到实际目标。它减少监听器数量,并能自然处理后续动态插入的子元素。
js
const list = document.querySelector('#list');
list.addEventListener('click', event => {
const button = event.target.closest('[data-action=remove]');
if (!button || !list.contains(button)) return;
const item = button.closest('li');
if (item) item.remove();
});这里使用 closest,而不是只比较 event.target,因为用户可能点到按钮内部的图标;contains 检查可防止 closest 匹配到代理容器之外的祖先。event.target 是最初触发节点,event.currentTarget 是当前执行监听器的代理节点。
适用场景包括表格行操作、菜单、动态列表、虚拟列表和大量同类控件。边界是事件必须能传播到代理节点:focus/blur、mouseenter/mouseleave 等不按普通方式冒泡,可改用 focusin/focusout、mouseover/mouseout,或在捕获阶段监听。子节点调用 stopPropagation 会阻断代理;mousemove 等高频事件即使代理也需要控制处理成本。代理不能代替每个目标的权限和状态校验。
413. typeof 与 instanceof 有什么区别
答案:
typeof 用于判断值的底层类型类别,返回字符串;instanceof 用于判断构造函数的 prototype 是否出现在对象的原型链上,两者回答的问题不同。
js
typeof 42; // 'number'
typeof (() => {}); // 'function'
typeof null; // 'object',历史遗留
[] instanceof Array; // true
new Date() instanceof Date; // truetypeof 适合区分 undefined、boolean、number、bigint、string、symbol、function 和一般 object,但不能细分数组、日期等对象。instanceof 右侧必须是可参与 instanceof 检查的对象;默认行为依赖原型链,因此原型被修改会影响结果,类还可通过 Symbol.hasInstance 自定义它。
instanceof 不能判断原始值,例如 'x' instanceof String 为 false,而 new String('x') instanceof String 为 true。来自另一个 iframe/realm 的数组拥有另一套 Array.prototype,可能导致 value instanceof Array 为 false,此时应使用 Array.isArray。精确识别内建对象可使用 Object.prototype.toString.call(value),业务对象则更适合使用明确的类型字段或可靠的 schema 校验。
414. JavaScript中的事件模型有哪些
答案:
从绑定 API 看,JavaScript 浏览器事件常分为三类:
- DOM0:element.onclick = handler。写法简单、兼容早,但同一属性只能保留一个处理器,后赋值会覆盖前者;置 null 可移除。
- DOM2 标准模型:addEventListener(type, handler, options),支持多个处理器、捕获/冒泡、once、passive、signal 等选项;removeEventListener 需要相同的 type、函数引用和 capture 值。
- 旧 IE 模型:attachEvent/detachEvent,只用于历史兼容,现代项目不应再使用。
标准传播过程为捕获阶段、目标阶段、冒泡阶段。事件从 Window/Document 沿 DOM 树向目标传播,随后通常从目标向上冒泡。event.target 表示原始目标,event.currentTarget 表示当前执行监听器所在节点,event.eventPhase 可查看阶段。
stopPropagation 阻止事件继续传播,但不会阻止当前节点上其他监听器;stopImmediatePropagation 还会阻止当前节点后续监听器;preventDefault 只取消可取消的默认行为,不会停止传播。并非所有事件都冒泡,例如 blur、focus、mouseenter、mouseleave;可使用对应的 focusin、focusout 等事件或捕获监听。passive: true 表示处理器不会调用 preventDefault,适合触摸和滚动性能场景。
415. JavaScript中执行上下文和执行栈是什么?
答案:
执行上下文是 JavaScript 代码执行时所需环境的抽象。常见类型有全局执行上下文、每次函数调用创建的函数执行上下文,以及很少使用的 eval 上下文。它包含当前代码的词法环境、变量环境、this 绑定等信息;函数环境还包含参数及对外层词法环境的引用。
创建函数上下文时,引擎先建立声明绑定:函数声明可在执行前使用;var 绑定被初始化为 undefined;let、const 和 class 绑定已创建但在声明执行前未初始化,访问会进入暂时性死区并抛出 ReferenceError。随后代码按顺序执行赋值和语句。词法作用域由函数定义位置决定,不由调用位置决定。
执行栈(调用栈)用后进先出结构管理当前正在执行的上下文:脚本开始时全局上下文入栈;调用函数时新上下文入栈;函数返回或抛出并完成处理时出栈。递归太深会因栈帧过多产生 Maximum call stack size exceeded。
异步操作等待期间,其回调不会一直占着调用栈。宿主在条件满足后把任务或微任务排队,事件循环等当前栈清空后再调用回调,并为这次调用创建新的函数执行上下文。闭包能让外层词法环境在外层函数出栈后仍被保留,但这不等于整个旧调用栈仍存在。
416. 谈谈对 this 对象的理解
答案:
this 是函数执行时获得的调用上下文,不是词法作用域变量。对普通函数,它主要由调用方式决定,而不是函数写在哪里:
- obj.fn():this 是点号左侧的 obj;把 fn 单独赋给变量再调用会丢失这层隐式绑定。
- fn.call(obj)、fn.apply(obj) 或 fn.bind(obj):显式指定 this;bind 返回新函数。
- new Fn():this 是新创建的实例;构造调用优先于 bind 预绑定的 this。
- 独立调用 fn():严格模式下 this 是 undefined;非严格脚本中通常替换为 globalThis。ES 模块和 class 方法默认按严格模式执行。
- DOM addEventListener 使用普通函数时,this 通常等于 currentTarget,但箭头函数不遵循该规则。
箭头函数没有自己的 this,它从定义位置的外层环境捕获 this,call/apply/bind 不能改变,也不能作为构造函数:
js
const counter = {
value: 1,
later() {
setTimeout(() => {
console.log(this.value); // 1,箭头函数捕获 later 的 this
}, 0);
}
};普通函数绑定优先级可概括为 new > 显式绑定 > 隐式绑定 > 默认绑定,但还要考虑构造函数显式返回对象、代理和宿主 API 等边界。this 与作用域链无关,“this 总指向函数的调用者”也不够准确;应直接观察调用表达式和函数类型。
417. Javascript如何实现继承?
答案:
JavaScript 的继承本质是原型委托:对象访问自身不存在的属性时,会沿 [[Prototype]] 链继续查找。常见实现有三种。
class+extends:现代代码首选,语法清晰,但底层仍是原型链。
js
class Person {
constructor(name) {
this.name = name
}
say() {
return this.name
}
}
class Employee extends Person {
constructor(name, role) {
super(name)
this.role = role
}
}
const employee = new Employee('Ada', 'developer')extends 同时建立实例方法和静态成员的继承关系;派生类构造器必须先调用 super(),之后才能使用 this。
- 寄生组合式继承:适合旧式构造函数。实例属性通过
Parent.call(this)初始化,原型方法通过Object.create继承。
js
function Parent(name) {
this.name = name
this.tags = []
}
Parent.prototype.say = function () {
return this.name
}
function Child(name) {
Parent.call(this, name)
}
Child.prototype = Object.create(Parent.prototype)
Object.defineProperty(Child.prototype, 'constructor', {
value: Child,
writable: true,
configurable: true
})它避免了 Child.prototype = new Parent() 导致父构造器执行两次的问题,也不会让各实例共享父构造器创建的引用属性。
Object.create(proto):让普通对象直接继承另一个对象,适合原型委托而非类式建模。
边界是:放在原型上的数组、对象等引用值会被所有实例共享;继承层次过深会增加耦合,业务建模中通常应优先考虑组合,只在确有“是一个”的关系时使用继承。
418. JavaScript中的原型,原型链分别是什么?
答案:
原型是对象内部 [[Prototype]] 指向的另一个对象,可通过 Object.getPrototypeOf(obj) 读取。当访问 obj.x 时,引擎先查找对象自身属性;没有找到,再到它的原型上查找。
构造函数还有一个容易混淆的公开属性 prototype。用 new Fn() 创建对象时,新对象的 [[Prototype]] 会被设置为 Fn.prototype:
js
function Person() {}
const person = new Person()
Object.getPrototypeOf(person) === Person.prototype // true
Person.prototype.constructor === Person // true原型链就是一组连续的 [[Prototype]] 引用。例如:
js
person -> Person.prototype -> Object.prototype -> null属性查找会沿这条链向上进行,找到即停止;到达 null 仍未找到就返回 undefined。给 person 赋同名属性通常会形成自有属性并遮蔽原型属性,不会修改原型。可以用 Object.hasOwn(person, key) 区分自有属性和继承属性。
__proto__ 是访问 [[Prototype]] 的历史兼容访问器,不建议在业务代码中依赖;读取和设置原型应使用 Object.getPrototypeOf、Object.setPrototypeOf 或创建时使用 Object.create。频繁动态修改原型还可能破坏引擎优化。
419. 什么是作用域链?
答案:
作用域链是 JavaScript 解析标识符时使用的词法环境链。每个执行上下文都有当前词法环境,环境中保存当前作用域的绑定,并通过外部引用连接到定义位置的上层词法环境。
js
const x = 'global'
function outer() {
const x = 'outer'
return function inner() {
console.log(x)
}
}
const fn = outer()
fn() // outer执行 inner 时,引擎先查它自己的作用域,再查 outer 的作用域,最后才到全局作用域。由于在 outer 中已经找到 x,全局的 x 被遮蔽。若整条链都没有该标识符,读取会抛出 ReferenceError。
作用域链由函数定义位置决定,而不是调用位置决定,这就是词法作用域。函数即使在原作用域结束后才执行,仍能通过闭包保留所需的外层环境。var 形成函数或全局作用域,let、const 还会形成块级作用域;同名内层绑定会遮蔽外层绑定。
420. 说说你对闭包的理解,以及闭包使用场景
答案:
闭包是函数与其创建时词法环境的组合。内层函数会按定义位置引用外层变量;即使外层函数已经返回,只要内层函数仍可达,被引用的词法环境就会继续存在。
js
function createCounter() {
let count = 0
return () => ++count
}
const counter = createCounter()
counter() // 1
counter() // 2这里返回的函数形成闭包,count 不能从外部直接访问,却能在多次调用之间保存状态。常见场景包括:
- 封装私有状态,如计数器、缓存、模块工厂。
- 函数工厂、柯里化和参数预绑定。
- 事件回调、定时器和异步任务中保留上下文。
- 实现防抖、节流、记忆化等高阶函数。
闭包保存的是变量绑定,不是创建瞬间的值快照,所以变量后续变化会被闭包观察到。还要注意两个边界:一是循环回调应使用 let 创建每轮绑定,避免多个回调共享同一个 var;二是闭包若长期持有大型对象或 DOM 引用,会延长其生命周期,应在不再需要时移除监听、清理定时器和释放引用。
421. 深拷贝浅拷贝有什么区别?怎么实现深拷贝?
答案:
浅拷贝只复制对象第一层属性。嵌套对象仍与原对象共享引用;展开语法、Object.assign、数组的 slice 等通常都是浅拷贝。深拷贝会递归复制整个对象图,使嵌套可变对象也相互独立,并且需要处理循环引用和特殊类型。
js
const source = { profile: { name: 'Ada' } }
const shallow = { ...source }
shallow.profile.name = 'Grace'
console.log(source.profile.name) // Grace现代环境复制结构化数据可优先使用:
js
const copy = structuredClone(source)structuredClone 能处理循环引用以及 Date、Map、Set、ArrayBuffer 等多种内置类型,还可转移部分可转移对象;但函数、DOM 节点等不可结构化克隆。JSON.parse(JSON.stringify(value)) 只适合纯 JSON 数据,它会丢失 undefined、Symbol、函数和原型信息,改变 Date,且无法处理循环引用。
需要定制语义时可以递归实现:
js
function deepClone(value, seen = new WeakMap()) {
if (Object(value) !== value || typeof value === 'function') return value
if (seen.has(value)) return seen.get(value)
if (value instanceof Date) return new Date(value.getTime())
if (value instanceof RegExp) {
const copy = new RegExp(value.source, value.flags)
copy.lastIndex = value.lastIndex
return copy
}
if (value instanceof Map) {
const copy = new Map()
seen.set(value, copy)
for (const [k, v] of value) copy.set(deepClone(k, seen), deepClone(v, seen))
return copy
}
if (value instanceof Set) {
const copy = new Set()
seen.set(value, copy)
for (const item of value) copy.add(deepClone(item, seen))
return copy
}
const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value))
seen.set(value, copy)
for (const key of Reflect.ownKeys(value)) {
const descriptor = Object.getOwnPropertyDescriptor(value, key)
if ('value' in descriptor) descriptor.value = deepClone(descriptor.value, seen)
Object.defineProperty(copy, key, descriptor)
}
return copy
}设对象图中有 n 个属性和集合元素,时间复杂度为 O(n),用于去重和处理环的额外空间为 O(n)。函数、私有字段、宿主对象等没有统一的通用复制语义,实际项目应先明确允许的数据类型。
422. == 和 ===有什么区别,分别在什么情况使用?
答案:
=== 是严格相等:通常要求两侧类型相同再比较值;对象比较引用是否相同。它不会做隐式类型转换,但有两个数值特例:NaN === NaN 为 false,+0 === -0 为 true。
== 是抽象相等:类型不同时会按规范进行转换后再比较,规则较多,例如:
js
'1' == 1 // true,字符串转数字
false == 0 // true,布尔值转数字
null == undefined // true
[] == '' // true,数组先转为原始值对象与原始值比较时会先执行原始值转换,可能调用 Symbol.toPrimitive、valueOf 或 toString,因此结果可能不直观。
业务代码默认使用 === 和 !==,可读性更高,也能避免意外转换。一个常见且可明确约定的例外是 value == null,它可以同时判断 null 和 undefined,而不会匹配其他普通值。若需要区分 +0、-0 或让 NaN 等于自身,可使用 Object.is。
423. 谈谈 Javascript 中的类型转换机制
答案:
JavaScript 类型转换分为显式转换和隐式转换。显式转换使用 String()、Number()、Boolean()、BigInt() 等;隐式转换由运算符、条件判断或属性访问触发。核心抽象操作主要是 ToPrimitive、ToString、ToNumber 和 ToBoolean。
- 转布尔值时,只有
false、0、-0、0n、NaN、空字符串、null、undefined为假值;包括空数组和空对象在内的其他值都是真值。 - 转数字时,
Number(null)是0,Number(undefined)是NaN,Number('')是0;Number('12px')是NaN,而parseInt('12px', 10)是12。 - 转字符串时大多数原始值会得到其文本形式;对象通常先转成原始值。
对象转原始值时优先遵循 obj[Symbol.toPrimitive](hint);没有该方法时,再按场景尝试 valueOf() 和 toString()。
js
1 + '2' // '12',有字符串时执行拼接
'6' - 1 // 5,减法要求数值
Boolean([]) // true
[] + [] // '',数组转为字符串后拼接+ 最容易产生歧义,因为它既表示数值相加又表示字符串拼接;其他算术运算符通常把操作数转为数值。Symbol 不能隐式转成普通字符串或数字,BigInt 也不能与 Number 直接做算术运算。实践中应在输入边界显式转换,并用 Number.isNaN、Number.isFinite 等方法校验结果。
424. Javascript字符串的常用方法有哪些?
答案:
JavaScript 字符串是不可变的,所有转换方法都会返回新字符串,不会修改原值。常用方法可以按用途归类:
- 读取:
str.length、charAt(i)、at(i)、charCodeAt(i)、codePointAt(i);at支持负索引。 - 查找和判断:
indexOf、lastIndexOf、includes、startsWith、endsWith。 - 截取:
slice(start, end)最常用且支持负数;substring会把负数当作0并可能交换参数;substr已废弃。 - 转换:
toUpperCase、toLowerCase、trim、trimStart、trimEnd、normalize、repeat、padStart、padEnd。 - 替换和拆分:
replace、replaceAll、split、concat。replace传普通字符串时默认只替换第一个匹配项。 - 正则相关:
search、match、matchAll。matchAll要求全局正则,并返回迭代器。
js
const text = ' hello-world '
text.trim().replace('-', ' ').toUpperCase() // 'HELLO WORLD'length 和大多数索引 API 按 UTF-16 码元计数,不一定等于用户看到的字符数。例如某些 emoji 的 length 大于 1。按 Unicode 码点遍历可使用 Array.from(str) 或 for...of;若要正确处理组合字符或 emoji 序列,应使用 Intl.Segmenter。
425. Javscript数组的常用方法有哪些?
答案:
数组方法首先要区分是否修改原数组。
会修改原数组:
- 增删:
push、pop、unshift、shift、splice。 - 排序和翻转:
sort、reverse。 - 填充和复制:
fill、copyWithin。
返回新值或新数组:
- 遍历转换:
map、filter、reduce、reduceRight、flatMap。 - 条件判断:
some、every。 - 查找:
find、findIndex、findLast、findLastIndex、includes、indexOf。 - 拼接和截取:
concat、slice、flat、join。 - 非破坏性更新:
toSorted、toReversed、toSpliced、with。 - 遍历但不收集结果:
forEach。
静态方法包括 Array.isArray、Array.from 和 Array.of;keys、values、entries 以及 for...of 可用于迭代。
js
const result = [3, 1, 2]
.filter(n => n > 1)
.toSorted((a, b) => a - b)
// [2, 3],原数组不变常见边界是:sort() 默认按字符串排序,数值排序要传 (a, b) => a - b;splice 会改原数组而 slice 不会;delete arr[i] 会留下空槽,通常应使用 splice;forEach 不会等待异步回调,需要串行等待时使用 for...of,并发时可用 Promise.all(arr.map(...))。
426. JavaScript中的简单数据类型有哪些?
答案:
JavaScript 有 7 种原始数据类型,也常被称为简单数据类型:
undefined:已声明但未赋值,或缺少返回值等场景的默认值。null:开发者显式表示“空”或“无对象”。boolean:true、false。number:双精度浮点数,包括NaN、Infinity;安全整数范围由Number.MAX_SAFE_INTEGER限定。bigint:任意精度整数,如123n,不能与number直接进行算术运算。string:不可变的 UTF-16 文本序列。symbol:唯一且不可变的标识值,常用作对象属性键。
原始值不可变,赋值和传参复制的是值;对象赋值复制的是引用值,因此多个变量可能指向同一对象。原始值访问方法时会发生临时装箱,但它本身不会因此变成对象。
typeof 有两个经典边界:typeof null === 'object' 是历史遗留行为,判断空值应直接与 null 比较;typeof function () {} === 'function',但函数本质上仍是可调用对象。
427. 你是怎么理解ES6中 Decorator 的?使用场景有哪些?
答案:
Decorator(装饰器)用于在类定义阶段对类或类成员进行声明式增强,例如替换方法、包装访问器或注册初始化逻辑。它并不是 ES6 正式特性;工程中常见的是 TypeScript/Babel 支持的装饰器语法,并且标准装饰器与 TypeScript 的旧版 experimentalDecorators 语义不同。
标准装饰器接收被装饰的值和上下文对象,并可返回替代实现:
js
function logged(method, context) {
if (context.kind !== 'method') return
return function (...args) {
console.log(`call ${String(context.name)}`, args)
return method.apply(this, args)
}
}
class Service {
@logged
query(id) {
return { id }
}
}常见场景有日志和埋点、权限检查、参数校验、依赖注入注册、ORM 字段声明、组件或自定义元素注册,以及响应式状态标记。它适合表达横切关注点,减少重复包装代码。
边界是:装饰器在定义类时运行,不是每次创建实例时运行;多个装饰器的求值和应用顺序需要明确。旧式装饰器常使用 (target, key, descriptor) 签名,与标准装饰器不兼容,迁移时不能只改编译选项。装饰器也容易隐藏控制流,业务逻辑复杂时应优先使用显式组合或高阶函数,并确保编译器、运行时及类型声明采用同一套语义。
428. 你是怎么理解ES6中Module的?使用场景有哪些?
答案:
ES Module(ESM)是 JavaScript 的标准模块系统,通过 export 暴露绑定、通过 import 声明依赖。它的依赖结构通常可以在执行前静态分析,因此构建工具能够做依赖检查、Tree Shaking 和代码分割。
js
// math.js
export const sum = (a, b) => a + b
export default { sum }
// app.js
import math, { sum } from './math.js'
const lazyModule = await import('./feature.js')主要特性是:
- 模块默认运行在严格模式,顶层
this为undefined。 - 每个模块在同一模块图中通常只求值一次,导出的对象可形成共享状态。
- 命名导入是只读的实时绑定,导出方修改值后,导入方能观察到变化;它不是值的静态拷贝。
- 静态
import/export只能出现在模块顶层;按条件加载使用返回 Promise 的import()。 - 支持循环依赖,但在绑定初始化前访问可能触发暂时性死区错误,应避免模块间互相依赖初始化副作用。
常见场景是拆分业务模块、封装公共库、按路由懒加载和复用单例服务。ESM 与 CommonJS 的加载时机、导出语义和文件解析规则不同;在 Node.js 中还要结合文件扩展名及 package.json 的 type 配置,不能假定 require 与 import 可无差别互换。
429. 你是怎么理解ES6中Proxy的?使用场景有哪些?
答案:
Proxy 为目标对象创建一层代理,通过 handler 中的陷阱函数拦截对象的底层操作,例如读取、赋值、删除、函数调用、构造、枚举和原型查询。Reflect 提供与这些操作对应的默认行为,适合在陷阱中转发。
js
const state = { count: 0 }
const proxy = new Proxy(state, {
get(target, key, receiver) {
console.log('get', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
if (key === 'count' && !Number.isInteger(value)) {
throw new TypeError('count must be an integer')
}
return Reflect.set(target, key, value, receiver)
}
})常见场景包括响应式依赖追踪、数据校验、访问日志、默认值、权限控制、缓存和远程对象封装。与 Object.defineProperty 相比,Proxy 能拦截新增、删除属性以及更多对象级操作。
Proxy 必须遵守语言不变量,例如不可把目标上不可配置且不可写的数据属性伪装成另一个值,否则会抛出 TypeError。它也有明显边界:只有经过代理对象的操作才能被拦截,直接操作原目标不会触发;代理与目标引用不相等;部分依赖内部槽或私有字段的方法可能因 this 变成代理而报错,需要谨慎绑定接收者。Proxy 不能被透明地“取消”,需要撤销能力时使用 Proxy.revocable。高频路径中还应评估其性能和调试成本。
430. 怎么理解ES6中 Generator的?使用场景有哪些?
答案:
Generator 是可暂停、可恢复的函数,用 function* 声明。调用 Generator 函数不会立即执行函数体,而是返回一个同时符合迭代器和可迭代协议的对象;调用 next() 才执行到下一个 yield。
js
function* range(start, end) {
for (let value = start; value <= end; value++) {
yield value
}
}
const iterator = range(1, 3)
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
;[...range(1, 3)] // [1, 2, 3]next(value) 传入的值会成为上一次 yield 表达式的结果,但第一次 next 的参数会被忽略。return() 可提前结束迭代,throw() 可把异常抛回 Generator 内部,yield* 可委托给另一个可迭代对象。
常见场景包括惰性生成大序列、自定义迭代器、按需遍历树或图、实现状态机。Generator 也曾用于把异步流程写成顺序形式,但现代代码通常优先使用 async/await;需要“异步且可逐项消费”时使用 async function* 和 for await...of。Generator 是一次性、有状态的迭代器,消费后若要重新遍历,必须重新调用 Generator 函数创建实例。
431. 你是怎么理解ES6中 Promise的?使用场景有哪些?
答案:
Promise 表示一个异步操作的最终结果,有 pending、fulfilled、rejected 三种状态;一旦从待定变为兑现或拒绝,状态和结果就不能再改变。Promise 构造器的执行器会同步执行,但 then、catch、finally 注册的处理函数会进入微任务队列异步执行。
js
fetch('/api/user')
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json()
})
.then(user => console.log(user))
.catch(error => console.error(error))
.finally(() => console.log('finished'))then 每次都返回一个新 Promise:回调返回普通值时下一个 Promise 兑现为该值;返回 Promise 或 thenable 时会吸收其最终状态;抛出异常时则转为拒绝。这使异步操作可以链式组合并统一传播错误。async/await 是建立在 Promise 之上的语法。
组合方法包括:Promise.all 全部成功才成功、任一失败就快速失败;allSettled 等待全部结束;race 采用第一个落定结果;any 采用第一个成功结果,全部失败时抛出 AggregateError。
Promise 本身不提供取消,取消网络或定时任务要由底层 API 配合 AbortController 等机制。还应避免未处理的拒绝,并注意 Array.prototype.forEach 不会等待异步回调;需要并发等待时使用 Promise.all(array.map(...)),需要顺序执行时使用 for...of 配合 await。
432. ES6中新增的Set、Map两种数据结构怎么理解?
答案:
Set 是不重复值的有序集合,Map 是键值映射。二者都按插入顺序迭代,并使用 SameValueZero 判断键或值是否相同,因此 NaN 与自身视为相同,+0 和 -0 视为相同。
js
const unique = new Set([1, 1, NaN, NaN])
;[...unique] // [1, NaN]
const map = new Map()
const key = { id: 1 }
map.set(key, 'value')
map.get(key) // 'value'Set 常用 add、has、delete、clear 和 size,适合去重、成员检测与集合运算。Map 常用 set、get、has、delete、clear 和 size,键可以是任意值,包括对象、函数和 NaN。对象键按引用区分,两个结构相同但引用不同的对象是两个键。
与普通对象相比,Map 没有原型键冲突,键类型不局限于字符串和 Symbol,迭代顺序及数量 API 也更直接;固定字段的数据记录仍更适合普通对象。常见实现下增删查平均接近 O(1),但规范不承诺具体底层结构。
WeakMap 和 WeakSet 只弱引用可作为弱键的对象,不能枚举,也没有 size,适合保存对象关联的私有元数据或缓存;当键对象不再被其他地方引用时,记录可被垃圾回收。
433. ES6中函数新增了哪些扩展?
答案:
ES6 对函数的主要扩展包括:
- 参数默认值:
function f(x = 1) {}。只有实参为undefined或缺省时才使用默认值,传null不会触发。默认参数按从左到右求值,并拥有独立的参数作用域。 - rest 参数:
function sum(...nums) {},把剩余实参收集成真正的数组;必须放在形参列表最后。 - 参数解构:可直接写
function f({ id, name = 'unknown' } = {}) {},外层默认值能处理整个参数缺省的情况。 - 箭头函数:
const add = (a, b) => a + b,适合短回调并词法捕获外层this。 - 函数的
name、length行为更明确:length只统计第一个默认参数之前的普通形参数量,不包含 rest 参数。 - 调用时展开参数:
fn(...args)可替代常见的apply用法。
箭头函数没有自己的 this、arguments、super 和 new.target,不能用作构造函数,也没有可供实例使用的 prototype;对象方法、需要动态 this 的回调或构造器不应盲目改成箭头函数。函数默认值中的表达式在每次调用时计算,而不是在函数定义时计算。
js
function createUser({ name, roles = [] } = {}) {
return { name, roles }
}
createUser() // { name: undefined, roles: [] }434. ES6中对象新增了哪些扩展?
答案:
ES6 对对象的核心扩展包括:
- 属性和方法简写:
{ name, say() {} }。 - 计算属性名:
{ [prefix + 'Id']: 1 }。 - 对象字面量方法中可使用
super访问其原型。 Object.is(a, b):与严格相等接近,但认为NaN等于自身、区分+0和-0。Object.assign(target, ...sources):把源对象自身可枚举属性浅复制到目标对象,同名属性后者覆盖前者。Object.setPrototypeOf、Object.getPrototypeOf:设置或读取对象原型。Object.getOwnPropertySymbols、Reflect.ownKeys:补充 Symbol 属性的读取与完整自有键枚举。
js
const key = 'score'
const score = 100
const user = {
name: 'Ada',
[key]: score,
greet() {
return `Hi, ${this.name}`
}
}需要区分版本:对象剩余/展开属性 { ...obj } 是 ES2018;Object.values、Object.entries、Object.getOwnPropertyDescriptors 是 ES2017;Object.fromEntries 是 ES2019。它们经常被统称为 ES6+ 对象扩展,但不属于 ES2015 本身。
Object.assign 和对象展开都是浅复制,嵌套对象仍共享引用。二者只处理自身可枚举属性并会读取源对象的 getter;Object.assign 通过赋值写目标,可能触发目标 setter,而对象展开在新对象上创建数据属性。原型继承属性和不可枚举属性不会被普通展开复制。
435. ES6中数组新增了哪些扩展?
答案:
ES6 对数组的主要扩展包括:
- 展开与解构:
const [first, ...rest] = list、fn(...list)、const copy = [...list]。展开得到的是浅拷贝。 Array.from:把可迭代对象或类数组对象转为数组,并可在创建时映射:Array.from(nodes, node => node.id)。Array.of:按参数创建数组,避免new Array(3)表示长度而不是元素的歧义。find、findIndex:返回第一个满足条件的元素或索引,找不到分别返回undefined和-1。fill、copyWithin:在原数组内填充或复制元素,都会修改原数组。keys、values、entries和默认迭代器:支持for...of以及键、值、键值对遍历。
js
const arrayLike = { 0: 'a', 1: 'b', length: 2 }
const list = Array.from(arrayLike) // ['a', 'b']
const found = list.find(item => item === 'b') // 'b'需要注意版本边界:includes 是 ES2016,flat/flatMap 是 ES2019,at 是 ES2022,findLast、toSorted、toReversed、toSpliced、with 是 ES2023;它们属于后续数组增强,不是 ES6 本身。
数组空槽也要谨慎:展开和 Array.from 会把空槽具体化为 undefined,而部分传统迭代方法会跳过空槽。fill 若填入对象,会让多个位置引用同一个对象,不会为每项创建独立副本。
436. var、let、const之间有什么区别?
答案:
三者的核心区别如下:
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数或全局作用域 | 块级作用域 | 块级作用域 |
| 声明前访问 | undefined | 暂时性死区,抛错 | 暂时性死区,抛错 |
| 同一作用域重复声明 | 允许 | 不允许 | 不允许 |
| 重新赋值 | 允许 | 允许 | 不允许 |
| 声明时初始化 | 可省略 | 可省略 | 必须初始化 |
var 声明会被创建并初始化为 undefined,所以声明前读取不会报错;let、const 的绑定也会在进入作用域时创建,但在执行声明语句前未初始化,处于暂时性死区。它们都不是简单的“完全不提升”。
const 限制的是绑定不可重新指向别处,并不冻结对象:
js
const user = { name: 'Ada' }
user.name = 'Grace' // 合法
// user = {} // TypeError在浏览器传统脚本顶层,var 声明通常会成为全局对象属性,而顶层 let、const 不会;ES Module 中声明则属于模块作用域。let 在 for 循环中还会为每次迭代创建独立绑定,适合闭包回调。
实践中默认使用 const,仅在变量确实需要重新赋值时使用 let;新代码通常避免 var,以减少函数作用域、重复声明和声明前访问带来的歧义。