JavaScript面试题
本文最后更新于 2025年10月31日 中午
0.1+0.2
原因
JavaScript 使用 IEEE 754 标准的 64 位(1 位符号位 + 11 位指数位 + 52 位尾数位)双精度浮点数来存储数字,这种格式无法精确表示某些十进制小数,特别是像 0.1 和 0.2 这样的分数。
0.1 和 0.2 转成二进制时是无限循环小数,存储时只能截断到 52 位尾数位所以有精度损失,导致两者相加时也有损失:
- 0.1 ≈ 0.000110011001100110011…(无限循环)
- 0.2 ≈ 0.001100110011001100110…(无限循环)
- 0.1 + 0.2 ≈ 0.30000000000000004
解决
- 简单运算:转换为整数再计算
这是最常用、性能最好的方法。利用整数是精确存储的特性。步骤:先将小数乘以 10 的 N 次方,放大为整数 -> 进行整数运算 -> 最后结果再除以 10 的 N 次方,缩小回小数。 - 比较操作:使用
Number.EPSILON进行容差比较
只是想比较两个数字是否相等,而不是进行精确计算时,这是最佳实践。Number.EPSILON表示 JavaScript 中可表示的最小精度值(约 2.220446049250313e-16)。 - 显示目的:使用
toFixed()四舍五入并转换为字符串toFixed(n)方法会将数字四舍五入到小数点后 n 位,并返回一个字符串。但toFixed()本身也可能存在浏览器实现差异和舍入错误。
Observer
Observer(观察者)在不同语境下(设计模式、JS API)不同,但核心思想一致:订阅-通知机制。
定义
观察者模式(设计模式里的 Observer):一种一对多依赖关系,当对象状态发生变化时它会自动通知所有依赖它的对象。组成部分:
- Subject(被观察者 / 发布者):维护观察者列表,状态变化时通知。
- Observer(观察者 / 订阅者):订阅 Subject 的变化,并做出反应。
1 | |
常用
(1)MutationObserver
- 作用:监听 DOM 结构变化。
1 | |
(2)IntersectionObserver
- 作用:监听元素是否进入/离开视口(懒加载常用)。
1 | |
(3)ResizeObserver
- 作用:监听元素尺寸变化。
1 | |
总结
| 语境 | 解释 |
|---|---|
| 设计模式 | 一种订阅-通知机制(Observer Pattern) |
| JS API | 浏览器提供的 Observer 类(监听 DOM/元素变化) |
Observer 是观察者模式,一种订阅-通知机制:对象维护依赖关系,当状态变化时通知所有观察者。前端中的事件系统、Vue 响应式、Redux 都基于该思想。JS 中有内置的 Observer API,比如 MutationObserver 用来监听 DOM 变化,IntersectionObserver 用来监听元素是否进入视口,ResizeObserver 用来监听元素尺寸变化。
WebSocket
WebSocket 是一种全双工、持久化的通信协议(像“对讲机”或“电话”):
- 建立在 TCP 之上
- 客户端和服务端之间一旦建立连接,就可以双向通信
- 适合即时性很强的场景(如聊天、游戏、实时协作)
原理:
- 浏览器先发起一个 HTTP 请求
- 服务器返回 101 Switching Protocols,协议升级为 WebSocket
- 之后客户端和服务端通过 TCP 长连接双向传输数据
对比:
| 特性 | WebSocket | SSE (Server-Sent Events) |
|---|---|---|
| 通信方式 | 双向通信(全双工) | 单向通信(服务端 → 客户端) |
| 协议 | 独立协议(ws, wss) |
基于 HTTP/1.1 长连接 |
| 浏览器支持 | 现代浏览器支持 | 大部分浏览器原生支持 |
| 复杂度 | 较复杂,需要服务端配合实现 | 较简单,开箱即用 |
| 使用场景 | 聊天室、游戏、协作编辑 | 实时通知、股票价格、新闻推送 |
SSE
SSE 是服务器单向推送消息给浏览器的机制(像“电台广播”):
- 建立在 HTTP 协议之上(长连接)
- 只能服务端 → 客户端,不能反向
- 浏览器原生支持 EventSource 对象来接收消息
原理:
- 浏览器发起一个 HTTP 请求
- 服务器保持连接不断开,持续用 text/event-stream 格式 推送消息
- 浏览器通过事件监听接收数据
Web Worker
Web Worker 是浏览器提供的一种在后台开辟独立线程的机制:
- 主线程(UI 线程)用来渲染页面和响应交互
- Worker 线程独立运行,可以执行耗时的计算任务,避免阻塞主线程
特点:
- 不能操作 DOM(因为 DOM 只能在主线程中操作)
- 通过消息传递(postMessage / onmessage)与主线程通信
- 适合 CPU 密集型任务,比如加密、数据处理、大量计算
1 | |
对比:
| 特性 | Web Worker | Shared Worker |
|---|---|---|
| 生命周期 | 依附单个页面,页面关闭即销毁 | 可被同源的多个页面共享,直到全部关闭 |
| 通信方式 | 主线程 ↔ Worker 单独通信 | 多个页面通过 port 共享通信 |
| 使用场景 | 单页面的大计算任务 | 多页面间共享状态/数据 |
| DOM 操作 | ❌ 不支持 | ❌ 不支持 |
Shared Worker
Shared Worker 是 Web Worker 的一种特殊形式,允许多个页面(同源)共享同一个 Worker 实例:
- 不同的浏览器窗口、标签页、iframe 只要在同一个源下,都可以连接到同一个 Shared Worker
- 适合需要跨页面共享数据/状态的场景,比如在线文档协作、多个标签页之间同步消息
特点:
- 通过
new SharedWorker('worker.js')创建 - 需要使用
port(MessagePort)对象来通信 - 生命周期比普通 Worker 更长(只要有页面在用就不会销毁)
1 | |
ES6 新语法
参考:
https://juejin.cn/post/6854818580660387853
https://juejin.cn/post/6844903581426925581
- let、const
- Set、Map、Symbol
- Promise
- 模块导入导出
- 箭头函数
- 类
- 对象的解构
- 字符串模版
- 展开运算符
for...of循环和for...in循环
async 和 await
async 声明一个异步函数
- 自动将常规函数转换成 Promise,返回值也是一个 Promise 对象
- 只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数
- 异步函数内部可以使用 await
await 暂停异步的功能执行
- 放置在 Promise 调用之前,await 强制其他代码等待,直到 Promise 完成并返回结果
- 只能与 Promise 一起使用,不适用与回调
- 只能在 async 函数内部使用
使用 Map 和 Set
✅ 什么时候用 Map
- 需要存储键值对,并且键不一定是字符串
- 比如:
- 缓存:
map.set(obj, computedValue) - 记录用户 ID → 用户信息:
map.set(userId, userInfo) - 做索引表、映射关系
- 缓存:
✅ 什么时候用 Set
- 只需要存储一组唯一值
- 比如:
- 数组去重
[...new Set(arr)] - 判断值是否存在:
set.has(x) - 集合运算:交集、并集、差集
- 数组去重
✅ 总结
- Map 用来存储键值对,键可以是任意类型,适合做缓存、索引表等需要高效映射的场景
- Set 用来存储一组唯一值,适合数组去重、存在性判断、集合运算等场景
继承调用 super
super()→ 只能在constructor里用,作用是调用父类构造函数,初始化this- 使用
extends继承时,如果子类没有写constructor,JS 会自动调用super(...args) - 如果子类定义了
constructor则必须在其里面先调用super(),否则无法初始化this1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
// ❌ 如果不写 super,会报错:
// ReferenceError: Must call super constructor
super(name); // ✅ 必须调用
this.age = age;
}
}
const c = new Child("Tom", 18);
console.log(c.name, c.age); // Tom 18
- 使用
super.xxx()→ 在 子类的普通方法 或 静态方法 中用,用来调用父类对应的方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Parent {
sayHello() {
console.log("Hello from Parent");
}
}
class Child extends Parent {
sayHello() {
console.log("Hello from Child");
super.sayHello(); // 调用父类的方法
}
}
const c = new Child();
c.sayHello();
// Hello from Child
// Hello from Parent
可迭代对象
可迭代对象是指实现了[Symbol.iterator]方法属性的对象,可以用 for...of 循环的都是可迭代对象。类数组对象(伪数组对象)指可以通过索引属性访问元素并且拥有一个 length 属性的对象。可迭代对象和类数组对象并非是相互排斥的,比如,字符串既是可迭代对象又是类数组对象。
迭代器模式描述了一个方案,即可以把有些结构称为“可迭代对象”(iterable),因为它们实现正式的 Iterable 接口,而且可以通过迭代器 Iterator 消费。
箭头函数特点
- 箭头函数没有没有原型对象
- 箭头函数 this 值为其所在上下文的 this 值
- 箭头函数箭头函数不能使用 arguments、super、new.target
- 箭头函数不能定义生成器函数
for-in 和 for-of
- for-in 循环:用于遍历对象的键名(数组就是遍历索引下标值)
- for-of 循环:用于遍历可迭代对象(Array、String、Map、Set 等),不能遍历类数组对象。使用
for-of循环和Object.keys()、Object.vales()、Object.entries()配合可遍历对象
1 | |
判断对象属性
字符串属性:
| 自有属性 | 继承属性 | 可枚举属性 | 不可枚举属性 | |
|---|---|---|---|---|
| in 操作符 | ✅ | ✅ | ✅ | ✅ |
| for/in 循环 | ✅ | ✅ | ✅ | ❌ |
| hasOwnProperty() | ✅ | ❌ | ✅ | ✅ |
| propertyIsEnumerable() | ✅ | ❌ | ✅ | ❌ |
| Object.keys() | ✅ | ❌ | ✅ | ❌ |
| Object.getOwnPropertyNames() | ✅ | ❌ | ✅ | ✅ |
| Reflect.ownKeys() | ✅ | ❌ | ✅ | ✅ |
符号属性:
Object.getOwnPropertySymbols()返回名字是符号的自有属性,无论是否可枚举Reflect.ownKeys()返回所有属性名,包括可枚举和不可枚举属性,以及字符串属性和符号属性
判断数据类型
- typeof 操作符
- instanceof 操作符
- constructor 属性
- Object.prototype.toString.call() 方法
此外,还有 Array.isArray()、isNaN() 等方法可以精准判断。
1 | |
说明:
- Object.prototype.toString.call() 方法是最准确的方法。
- typeof 操作符只用于原始值,对于 Null 类型其返回为 Object。
- instanceof 操作符只用于引用值,且是假定只有一种全局环境,如果网页中包含多个框架多个全局环境,如果从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。
- constructor 属性只用于引用值,而且其可以被重写,所以不一定准确。
垃圾回收
JavaScript 是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。在 C 和 C++ 等语言中,跟踪内存使用对开发者来说是个很大的负担,也是很多问题的来源。JavaScript 则为开发者卸下了这个负担,通过自动内存管理实现内存分配和闲置资源回收。
基本思路很简单:确定哪个变量不会再使用,然后释放它所占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。垃圾回收过程是个近似且不完美的方案,因为某块内存是否还有用,属于“不可判定的”问题,意味着靠算法是解决不了的。
以函数中局部变量的正常生命周期为例。函数中的局部变量会在函数执行时存在。此时,栈(或堆)内存会分配空间以保存相应的值。函数在内部使用了变量,然后退出。此时,就不再需要那个局部变量,它占用的内存可以释放,供后面使用。这种情况下显然不再需要局部变量了,但并不是所有时候都会这么明显。垃圾回收程序必须跟踪记录哪个变量还会使用以及哪个变量不会再使用,以便回收内存。如何标记未使用的变量也许有不同的实现方式。不过,在浏览器的发展史上,用到过两种主要的标记策略:标记清理和引用计数。
JavaScript 最常用的垃圾回收策略是标记清理(mark-and-sweep)。当变量进入上下文,如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时,也会被加上离开上下文的标记。给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者,可以维护“在上下文中”和“不在上下文中”两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程中的实现并不重要,关键是策略。
垃圾回收程序运行的时候,会标记内存中存储的所有变量(标记方法有很多)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。
- 通过 const 和 let 声明提升性能。
- 隐藏类和删除操作。
输出判断题
1 | |
1 | |
1 | |
其它问题
- JavaScript 标准内置对象
- 数据类型(基本/集合)
- 原型、原型链、继承
- this 指向
- 数组方法
- 数组扁平化
- 期约(Promise)
- 微任务和宏任务
- 事件循环