0%

数据结构与内置对象

目标:掌握 JavaScript 中的各种数据结构(数组、Map、Set、TypedArray)和 JSON 处理,理解它们的特性、使用场景和最佳实践。

目录

  1. 集合类型总览
  2. JSON:数据交换格式
  3. 其他内置对象
  4. 实战应用场景
  5. 代码片段速查
  6. 常见问题与解决方案
  7. 速记与总结

1. 集合类型总览

1.1 数组(Array)与类数组

数组是 JavaScript 中最常用的数据结构之一,用于存储有序的元素集合。

1.1.1 数组的核心特性

1. 有序集合,索引从 0 开始

1
2
3
4
const arr = ['a', 'b', 'c'];
console.log(arr[0]); // 'a'
console.log(arr[1]); // 'b'
console.log(arr[2]); // 'c'

2. length 属性可读写

1
2
3
4
5
6
7
8
9
10
11
12
const arr = [1, 2, 3];
console.log(arr.length); // 3

// 增大 length 不会立刻分配元素
arr.length = 5;
console.log(arr); // [1, 2, 3, empty × 2]
console.log(arr[3]); // undefined

// 减小 length 会截断数组
arr.length = 2;
console.log(arr); // [1, 2]
console.log(arr[2]); // undefined(元素被删除)

3. 稀疏数组(Sparse Array)

稀疏数组是指中间有空缺元素的数组。

1
2
3
4
5
6
7
8
9
10
11
// 创建稀疏数组
const sparse = new Array(5);
console.log(sparse); // [empty × 5]

// 或者
const sparse2 = [1, , , 4];
console.log(sparse2); // [1, empty × 2, 4]

// 检查元素是否存在
console.log(1 in sparse2); // false(索引1不存在)
console.log(0 in sparse2); // true(索引0存在)

稀疏数组的特殊行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const sparse = [1, , , 4];

// forEach 跳过空洞
sparse.forEach((item, index) => {
console.log(index, item);
});
// 输出:
// 0 1
// 3 4
// (跳过了索引1和2)

// map 也会跳过空洞,但保留位置
const mapped = sparse.map(x => x * 2);
console.log(mapped); // [2, empty × 2, 8]

// filter 会移除空洞
const filtered = sparse.filter(x => true);
console.log(filtered); // [1, 4]

1.1.2 数组的创建方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 方式1:数组字面量(最常用)
const arr1 = [1, 2, 3];

// 方式2:Array 构造函数
const arr2 = new Array(3); // [empty × 3](稀疏数组)
const arr3 = new Array(1, 2, 3); // [1, 2, 3]

// 方式3:Array.of(推荐,避免构造函数歧义)
const arr4 = Array.of(3); // [3](不是 [empty × 3])
const arr5 = Array.of(1, 2, 3); // [1, 2, 3]

// 方式4:Array.from(从类数组或可迭代对象创建)
const arr6 = Array.from('abc'); // ['a', 'b', 'c']
const arr7 = Array.from([1, 2, 3], x => x * 2); // [2, 4, 6]
const arr8 = Array.from({ length: 3 }, (_, i) => i); // [0, 1, 2]

// 方式5:扩展运算符
const arr9 = [...'abc']; // ['a', 'b', 'c']

Array.of vs new Array

1
2
3
4
5
6
7
// new Array 的歧义
new Array(3); // [empty × 3](创建长度为3的空数组)
new Array(1, 2, 3); // [1, 2, 3](创建包含这些元素的数组)

// Array.of 总是创建包含参数的数组
Array.of(3); // [3](不是空数组)
Array.of(1, 2, 3); // [1, 2, 3]

1.1.3 常用数组方法详解

不改变原数组的方法(返回新数组)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const arr = [1, 2, 3, 4, 5];

// map:映射,返回新数组
const doubled = arr.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(arr); // [1, 2, 3, 4, 5](原数组不变)

// filter:过滤,返回满足条件的元素
const evens = arr.filter(x => x % 2 === 0);
console.log(evens); // [2, 4]

// slice:切片,返回子数组
const slice1 = arr.slice(1, 3); // [2, 3](索引1到2)
const slice2 = arr.slice(2); // [3, 4, 5](从索引2到末尾)
const slice3 = arr.slice(-2); // [4, 5](最后2个)

// concat:连接,返回新数组
const concat = arr.concat([6, 7]);
console.log(concat); // [1, 2, 3, 4, 5, 6, 7]

// ES2023 新增:不改变原数组的排序/反转/拼接
const sorted = arr.toSorted((a, b) => b - a); // 降序
const reversed = arr.toReversed(); // 反转
const spliced = arr.toSpliced(1, 2, 99, 100); // 在索引1删除2个,插入99和100

改变原数组的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const arr = [1, 2, 3];

// push:尾部添加
arr.push(4);
console.log(arr); // [1, 2, 3, 4]

// pop:尾部删除
const last = arr.pop();
console.log(last); // 4
console.log(arr); // [1, 2, 3]

// shift:头部删除
const first = arr.shift();
console.log(first); // 1
console.log(arr); // [2, 3]

// unshift:头部添加
arr.unshift(0);
console.log(arr); // [0, 2, 3]

// splice:删除/插入/替换
arr.splice(1, 1, 99); // 从索引1删除1个元素,插入99
console.log(arr); // [0, 99, 3]

// reverse:反转
arr.reverse();
console.log(arr); // [3, 99, 0]

// sort:排序
arr.sort((a, b) => a - b);
console.log(arr); // [0, 3, 99]

查找与判断方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const arr = [1, 2, 3, 2, 4];

// indexOf:查找第一个匹配的索引
console.log(arr.indexOf(2)); // 1

// lastIndexOf:查找最后一个匹配的索引
console.log(arr.lastIndexOf(2)); // 3

// includes:判断是否包含
console.log(arr.includes(3)); // true
console.log(arr.includes(5)); // false

// find:查找第一个满足条件的元素
const found = arr.find(x => x > 2);
console.log(found); // 3

// findIndex:查找第一个满足条件的索引
const index = arr.findIndex(x => x > 2);
console.log(index); // 2

// findLast / findLastIndex(ES2023)
const lastFound = arr.findLast(x => x > 2);
console.log(lastFound); // 4

其他常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const arr = [1, 2, 3, 4, 5];

// forEach:遍历
arr.forEach((item, index) => {
console.log(index, item);
});

// reduce:归约
const sum = arr.reduce((acc, val) => acc + val, 0);
console.log(sum); // 15

// some:是否有元素满足条件
const hasEven = arr.some(x => x % 2 === 0);
console.log(hasEven); // true

// every:是否所有元素都满足条件
const allPositive = arr.every(x => x > 0);
console.log(allPositive); // true

// flat:扁平化
const nested = [1, [2, 3], [4, [5]]];
console.log(nested.flat()); // [1, 2, 3, 4, [5]]
console.log(nested.flat(2)); // [1, 2, 3, 4, 5]
console.log(nested.flat(Infinity)); // [1, 2, 3, 4, 5]

// flatMap:先 map 再 flat(1)
const doubled = arr.flatMap(x => [x, x * 2]);
console.log(doubled); // [1, 2, 2, 4, 3, 6, 4, 8, 5, 10]

1.1.4 类数组(Array-like)

类数组对象具有数组的特征(数值索引、length 属性),但不是真正的数组。

常见的类数组对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. arguments(函数参数对象)
function test() {
console.log(arguments); // Arguments(3) [1, 2, 3]
console.log(Array.isArray(arguments)); // false
console.log(arguments.length); // 3
console.log(arguments[0]); // 1
}

test(1, 2, 3);

// 2. DOM NodeList
const nodes = document.querySelectorAll('div');
console.log(nodes); // NodeList
console.log(Array.isArray(nodes)); // false

// 3. HTMLCollection
const elements = document.getElementsByTagName('div');
console.log(elements); // HTMLCollection
console.log(Array.isArray(elements)); // false

// 4. 自定义类数组对象
const arrayLike = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};

类数组转换为数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 方式1:Array.from(推荐)
function test() {
const arr1 = Array.from(arguments);
console.log(Array.isArray(arr1)); // true
}

// 方式2:扩展运算符
function test() {
const arr2 = [...arguments];
console.log(Array.isArray(arr2)); // true
}

// 方式3:Array.prototype.slice.call
function test() {
const arr3 = Array.prototype.slice.call(arguments);
console.log(Array.isArray(arr3)); // true
}

// 方式4:Array.prototype.map.call(如果需要转换)
function test() {
const doubled = Array.prototype.map.call(arguments, x => x * 2);
console.log(doubled); // [2, 4, 6]
}

常见坑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ❌ 错误:直接对类数组使用数组方法
function test() {
arguments.map(x => x * 2); // TypeError: arguments.map is not a function
}

// ✅ 正确:先转换为数组
function test() {
const arr = Array.from(arguments);
arr.map(x => x * 2); // 正常
}

// ✅ 或者:使用 call
function test() {
Array.prototype.map.call(arguments, x => x * 2);
}

1.2 Map 与 WeakMap

1.2.1 Map:键值对集合

Map 是 ES6 引入的键值对数据结构,与 Object 相比有以下优势:

1. 键可以是任意类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const map = new Map();

// 字符串键
map.set('name', 'Alice');

// 数字键
map.set(123, '数字键');

// 对象键
const objKey = { id: 1 };
map.set(objKey, '对象键的值');

// 函数键
const fnKey = () => {};
map.set(fnKey, '函数键的值');

// 获取值
console.log(map.get('name')); // 'Alice'
console.log(map.get(123)); // '数字键'
console.log(map.get(objKey)); // '对象键的值'
console.log(map.get(fnKey)); // '函数键的值'

Object vs Map 的键

1
2
3
4
5
6
7
8
9
10
11
12
// Object 的键会被转换为字符串
const obj = {};
obj[1] = 'one';
obj['1'] = 'one string';
console.log(obj); // { '1': 'one string' }(数字1被转换为字符串)

// Map 的键保持原类型
const map = new Map();
map.set(1, 'one');
map.set('1', 'one string');
console.log(map.get(1)); // 'one'
console.log(map.get('1')); // 'one string'(不同的键)

2. 保持插入顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
const map = new Map();
map.set('first', 1);
map.set('second', 2);
map.set('third', 3);

// 遍历顺序就是插入顺序
for (const [key, value] of map) {
console.log(key, value);
}
// 输出:
// first 1
// second 2
// third 3

3. 常用 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const map = new Map();

// 设置键值对
map.set('name', 'Alice');
map.set('age', 25);

// 获取值
console.log(map.get('name')); // 'Alice'
console.log(map.get('unknown')); // undefined

// 检查是否存在
console.log(map.has('name')); // true
console.log(map.has('unknown')); // false

// 删除键值对
map.delete('age');
console.log(map.has('age')); // false

// 获取大小
console.log(map.size); // 1

// 清空
map.clear();
console.log(map.size); // 0

// 遍历
map.set('a', 1);
map.set('b', 2);

// for...of
for (const [key, value] of map) {
console.log(key, value);
}

// forEach
map.forEach((value, key) => {
console.log(key, value);
});

// keys / values / entries
console.log([...map.keys()]); // ['a', 'b']
console.log([...map.values()]); // [1, 2]
console.log([...map.entries()]); // [['a', 1], ['b', 2]]

4. Map 的创建

1
2
3
4
5
6
7
8
9
10
11
12
// 方式1:空 Map
const map1 = new Map();

// 方式2:从数组创建
const map2 = new Map([
['name', 'Alice'],
['age', 25]
]);

// 方式3:从 Object 创建
const obj = { name: 'Alice', age: 25 };
const map3 = new Map(Object.entries(obj));

5. 适用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 场景1:需要对象作为键
const userMetadata = new Map();
const user1 = { id: 1 };
const user2 = { id: 2 };

userMetadata.set(user1, { lastLogin: new Date() });
userMetadata.set(user2, { lastLogin: new Date() });

// 场景2:需要保持插入顺序
const orderedMap = new Map();
orderedMap.set('first', 1);
orderedMap.set('second', 2);
// 遍历时顺序是确定的

// 场景3:频繁添加/删除键值对
// Map 的性能通常比 Object 更好

1.2.2 WeakMap:弱引用的 Map

WeakMap 与 Map 的区别:

  1. 键必须是对象(不能是原始值)
  2. 弱引用:如果键对象没有其他引用,会被垃圾回收
  3. 不可遍历:没有 keysvaluesentriesforEachsize
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ✅ 正确:对象作为键
const wm = new WeakMap();
const obj = {};
wm.set(obj, 'value');

// ❌ 错误:原始值不能作为键
wm.set('string', 'value'); // TypeError

// 获取值
console.log(wm.get(obj)); // 'value'

// 检查是否存在
console.log(wm.has(obj)); // true

// 删除
wm.delete(obj);

// ❌ 没有这些方法
// wm.keys() // TypeError
// wm.forEach() // TypeError
// wm.size // undefined

弱引用的意义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 示例:为 DOM 元素存储元数据
const elementData = new WeakMap();

function attachData(element, data) {
elementData.set(element, data);
}

function getData(element) {
return elementData.get(element);
}

// 使用
const div = document.createElement('div');
attachData(div, { clicked: 0, hovered: false });

// 当 div 被移除且没有其他引用时
// elementData 中的条目会被自动回收
// 不会造成内存泄漏

WeakMap vs Map

1
2
3
4
5
6
7
8
9
10
11
// Map:强引用,会阻止垃圾回收
const map = new Map();
let obj = { data: 'large' };
map.set(obj, 'metadata');
obj = null; // obj 仍然被 map 引用,不会被回收

// WeakMap:弱引用,不会阻止垃圾回收
const wm = new WeakMap();
let obj2 = { data: 'large' };
wm.set(obj2, 'metadata');
obj2 = null; // obj2 可以被回收,wm 中的条目也会消失

典型用途

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 为对象存储私有数据
const privateData = new WeakMap();

class User {
constructor(name) {
privateData.set(this, { name });
}

getName() {
return privateData.get(this).name;
}
}

// 2. 缓存计算结果
const cache = new WeakMap();

function expensiveOperation(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}
const result = /* 复杂计算 */;
cache.set(obj, result);
return result;
}

1.3 Set 与 WeakSet

1.3.1 Set:无重复值集合

Set 是 ES6 引入的集合数据结构,用于存储唯一值。

1. 自动去重

1
2
3
4
5
6
const set = new Set([1, 2, 2, 3, 3, 3]);
console.log(set); // Set(3) {1, 2, 3}

// 添加重复值会被忽略
set.add(2);
console.log(set); // Set(3) {1, 2, 3}(没有变化)

2. SameValueZero 比较

Set 使用 SameValueZero 算法比较值,类似于 ===,但对 NaN 友好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const set = new Set();

// NaN 的比较
set.add(NaN);
set.add(NaN);
console.log(set.size); // 1(NaN 只算一个)

// 对象比较(引用比较)
const obj1 = {};
const obj2 = {};
set.add(obj1);
set.add(obj2);
console.log(set.size); // 2(不同对象)

// 原始值比较
set.add(1);
set.add('1');
console.log(set.size); // 4(1 和 '1' 不同)

3. 常用 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const set = new Set([1, 2, 3]);

// 添加元素
set.add(4);
console.log(set); // Set(4) {1, 2, 3, 4}

// 检查是否存在
console.log(set.has(2)); // true
console.log(set.has(5)); // false

// 删除元素
set.delete(2);
console.log(set.has(2)); // false

// 获取大小
console.log(set.size); // 3

// 清空
set.clear();
console.log(set.size); // 0

// 遍历
set.add(1);
set.add(2);
set.add(3);

// for...of
for (const value of set) {
console.log(value);
}

// forEach
set.forEach(value => {
console.log(value);
});

// 转换为数组
const arr = [...set]; // [1, 2, 3]

4. Set 的创建

1
2
3
4
5
6
7
8
9
10
11
// 方式1:空 Set
const set1 = new Set();

// 方式2:从数组创建
const set2 = new Set([1, 2, 3]);

// 方式3:从字符串创建
const set3 = new Set('abc'); // Set(3) {'a', 'b', 'c'}

// 方式4:从可迭代对象创建
const set4 = new Set([1, 2, 3].map(x => x * 2)); // Set(3) {2, 4, 6}

5. 典型用途

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 用途1:数组去重
const arr = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3]

// 用途2:记录访问状态
const visited = new Set();
function visit(id) {
if (visited.has(id)) {
console.log('已访问');
return;
}
visited.add(id);
console.log('首次访问');
}

// 用途3:集合运算
const set1 = new Set([1, 2, 3]);
const set2 = new Set([2, 3, 4]);

// 交集
const intersection = new Set([...set1].filter(x => set2.has(x)));
console.log([...intersection]); // [2, 3]

// 并集
const union = new Set([...set1, ...set2]);
console.log([...union]); // [1, 2, 3, 4]

// 差集
const difference = new Set([...set1].filter(x => !set2.has(x)));
console.log([...difference]); // [1]

1.3.2 WeakSet:弱引用的 Set

WeakSet 与 Set 的区别:

  1. 元素必须是对象
  2. 弱引用:如果对象没有其他引用,会被垃圾回收
  3. 不可遍历:没有 keysvaluesforEachsize
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ✅ 正确:对象作为元素
const ws = new WeakSet();
const obj = {};
ws.add(obj);

// ❌ 错误:原始值不能作为元素
ws.add('string'); // TypeError

// 检查是否存在
console.log(ws.has(obj)); // true

// 删除
ws.delete(obj);

// ❌ 没有这些方法
// ws.forEach() // TypeError
// ws.size // undefined

典型用途

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 标记对象是否已处理
const processed = new WeakSet();

function processObject(obj) {
if (processed.has(obj)) {
console.log('已处理,跳过');
return;
}

// 处理对象
console.log('处理对象');
processed.add(obj);
}

// 使用
const obj1 = { id: 1 };
const obj2 = { id: 2 };

processObject(obj1); // '处理对象'
processObject(obj1); // '已处理,跳过'
processObject(obj2); // '处理对象'

1.4 Typed Arrays(类型数组)

Typed Arrays 是用于处理二进制数据的数组类型,提供高性能的数值运算能力。

1.4.1 什么是 Typed Arrays?

Typed Arrays 是基于 ArrayBuffer 的视图,用于:

  • 高性能数值运算
  • WebGL 图形处理
  • 音视频处理
  • 与 WebAssembly 交互
  • 网络协议解析

1.4.2 ArrayBuffer

ArrayBuffer 是一块原始二进制数据缓冲区。

1
2
3
4
5
6
// 创建 8 字节的缓冲区
const buffer = new ArrayBuffer(8);
console.log(buffer.byteLength); // 8

// ArrayBuffer 本身不能直接访问
// 需要通过视图(TypedArray)访问

1.4.3 TypedArray 视图

TypedArray 是 ArrayBuffer 的视图,提供了不同的数据类型和字节序。

常见的 TypedArray 类型

类型 字节数 值范围
Int8Array 1 -128 到 127
Uint8Array 1 0 到 255
Int16Array 2 -32768 到 32767
Uint16Array 2 0 到 65535
Int32Array 4 -2^31 到 2^31-1
Uint32Array 4 0 到 2^32-1
Float32Array 4 32位浮点数
Float64Array 8 64位浮点数

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建 ArrayBuffer
const buffer = new ArrayBuffer(16); // 16 字节

// 创建视图
const view8 = new Uint8Array(buffer); // 8位无符号整数视图
const view32 = new Uint32Array(buffer); // 32位无符号整数视图

// 写入数据
view8[0] = 255;
view8[1] = 16;

view32[0] = 123456;

// 读取数据
console.log(view8[0]); // 255
console.log(view32[0]); // 123456

TypedArray 的特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 长度固定
const arr = new Int8Array(5);
console.log(arr.length); // 5

// 2. 只能存储数值
arr[0] = 100;
arr[1] = -50;
// arr[2] = 'string'; // 会被转换为数字或 NaN

// 3. 没有空洞(稀疏数组)
const arr2 = new Int8Array(3);
console.log(arr2); // Int8Array(3) [0, 0, 0](初始化为0)

// 4. 从数组创建
const arr3 = new Int8Array([1, 2, 3]);
console.log(arr3); // Int8Array(3) [1, 2, 3]

// 5. 从普通数组转换
const normalArr = [1, 2, 3];
const typedArr = new Int8Array(normalArr);
console.log(typedArr); // Int8Array(3) [1, 2, 3]

实际应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 示例1:图像数据处理
function processImageData(imageData) {
const data = new Uint8ClampedArray(imageData.data);

// 处理每个像素(RGBA,每个像素4个字节)
for (let i = 0; i < data.length; i += 4) {
// 灰度化
const gray = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = gray; // R
data[i + 1] = gray; // G
data[i + 2] = gray; // B
// data[i + 3] 是 Alpha,保持不变
}

return data;
}

// 示例2:高性能数值计算
function calculateSum(numbers) {
const typedArray = new Float64Array(numbers);
let sum = 0;
for (let i = 0; i < typedArray.length; i++) {
sum += typedArray[i];
}
return sum;
}

// 示例3:数据序列化
function serializeData(data) {
const buffer = new ArrayBuffer(data.length * 4);
const view = new Float32Array(buffer);
for (let i = 0; i < data.length; i++) {
view[i] = data[i];
}
return buffer;
}

2. JSON:数据交换格式

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,广泛用于前后端数据传输。

2.1 JSON 格式与语法

2.1.1 基本规则

JSON 基于 JavaScript 对象字面量,但更严格:

  1. 字符串必须用双引号(不能用单引号)
1
2
3
{
"name": "Alice" // ✅ 正确
}
1
2
3
{
'name': 'Alice' // ❌ JSON 不支持单引号
}
  1. 键必须是双引号包裹的字符串
1
2
3
4
{
"name": "Alice", // ✅ 正确
"age": 25
}
1
2
3
4
{
name: "Alice", // ❌ JSON 不支持无引号的键
age: 25
}
  1. 不允许尾随逗号
1
2
3
4
{
"name": "Alice",
"age": 25 // ✅ 正确(最后一项没有逗号)
}
1
2
3
4
{
"name": "Alice",
"age": 25, // ❌ JSON 不允许尾随逗号
}
  1. 不支持的类型
    • undefined
    • NaN
    • Infinity
    • 函数
    • Symbol
    • Date(会被转换为字符串)

合法的 JSON 值类型

  • 字符串(string)
  • 数字(number)
  • 布尔值(boolean)
  • null
  • 对象(object)
  • 数组(array)

示例合法 JSON

1
2
3
4
5
6
7
8
9
10
{
"name": "Alice",
"age": 25,
"isActive": true,
"hobbies": ["reading", "game"],
"profile": {
"online": true,
"lastLogin": null
}
}

2.1.2 JSON vs JavaScript 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// JavaScript 对象(更灵活)
const jsObj = {
name: 'Alice', // 键可以不用引号
age: 25,
undefined: undefined, // 可以有 undefined
fn: function() {}, // 可以有函数
date: new Date(), // 可以有 Date 对象
symbol: Symbol('test') // 可以有 Symbol
};

// JSON(更严格)
const jsonStr = JSON.stringify(jsObj);
console.log(jsonStr);
// {"name":"Alice","age":25}
// undefined、函数、Date、Symbol 都被过滤了

2.2 JSON.parse()JSON.stringify()

2.2.1 JSON.parse(text, reviver?)

JSON.parse 将 JSON 字符串解析为 JavaScript 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 基本用法
const json = '{"name":"Alice","age":25}';
const obj = JSON.parse(json);
console.log(obj); // { name: 'Alice', age: 25 }

// 解析数组
const jsonArray = '[1,2,3]';
const arr = JSON.parse(jsonArray);
console.log(arr); // [1, 2, 3]

// 解析嵌套对象
const nestedJson = '{"user":{"name":"Alice"}}';
const nested = JSON.parse(nestedJson);
console.log(nested.user.name); // 'Alice'

reviver 参数

reviver 是一个函数,用于在解析过程中转换值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const json = '{"age":"25","date":"2026-01-05"}';

// 使用 reviver 转换值
const obj = JSON.parse(json, (key, value) => {
if (key === 'age') {
return Number(value); // 将字符串转换为数字
}
if (key === 'date') {
return new Date(value); // 将字符串转换为 Date
}
return value;
});

console.log(obj.age); // 25(数字)
console.log(obj.date); // Date 对象

reviver 的执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
const json = '{"a":1,"b":{"c":2}}';

JSON.parse(json, (key, value) => {
console.log(key, value);
return value;
});

// 输出顺序(从内到外):
// c 2
// b {c: 2}
// a 1
// '' {a: 1, b: {c: 2}}(根对象,key 为空字符串)

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ❌ 无效的 JSON 会抛出 SyntaxError
try {
JSON.parse('{name:"Alice"}'); // 键没有引号
} catch (error) {
console.error('解析失败:', error.message);
// SyntaxError: Expected property name or '}' in JSON
}

// ✅ 安全解析
function safeParse(json, defaultValue = null) {
try {
return JSON.parse(json);
} catch (error) {
console.error('JSON 解析失败:', error);
return defaultValue;
}
}

2.2.2 JSON.stringify(value, replacer?, space?)

JSON.stringify 将 JavaScript 值序列化为 JSON 字符串。

1
2
3
4
// 基本用法
const obj = { name: 'Alice', age: 25 };
const json = JSON.stringify(obj);
console.log(json); // '{"name":"Alice","age":25}'

特殊值的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
name: 'Alice',
age: 25,
undefined: undefined, // 被忽略
fn: function() {}, // 被忽略
symbol: Symbol('test'), // 被忽略
date: new Date(), // 转换为 ISO 字符串
arr: [1, undefined, 3] // undefined 转换为 null
};

const json = JSON.stringify(obj);
console.log(json);
// {"name":"Alice","age":25,"date":"2026-01-05T00:00:00.000Z","arr":[1,null,3]}

replacer 参数

replacer 可以是函数或数组。

1. 函数形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = {
name: 'Alice',
age: 25,
password: 'secret'
};

// 过滤敏感信息
const json = JSON.stringify(obj, (key, value) => {
if (key === 'password') {
return undefined; // 返回 undefined 会被忽略
}
return value;
});

console.log(json); // '{"name":"Alice","age":25}'

2. 数组形式

1
2
3
4
5
6
7
8
9
10
const obj = {
name: 'Alice',
age: 25,
password: 'secret',
email: 'alice@example.com'
};

// 只序列化指定的键
const json = JSON.stringify(obj, ['name', 'age']);
console.log(json); // '{"name":"Alice","age":25}'

space 参数

space 用于控制缩进,便于阅读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const obj = { name: 'Alice', age: 25, hobbies: ['reading', 'game'] };

// 不缩进
console.log(JSON.stringify(obj));
// '{"name":"Alice","age":25,"hobbies":["reading","game"]}'

// 缩进 2 个空格
console.log(JSON.stringify(obj, null, 2));
// {
// "name": "Alice",
// "age": 25,
// "hobbies": [
// "reading",
// "game"
// ]
// }

// 缩进使用制表符
console.log(JSON.stringify(obj, null, '\t'));
// {
// "name": "Alice",
// "age": 25,
// ...
// }

toJSON 方法

对象可以实现 toJSON 方法来自定义序列化行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
name: 'Alice',
date: new Date(),
toJSON() {
return {
name: this.name,
date: this.date.toISOString()
};
}
};

console.log(JSON.stringify(obj));
// '{"name":"Alice","date":"2026-01-05T00:00:00.000Z"}'

2.2.3 常见坑与解决方案

1. 循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ❌ 循环引用会导致错误
const obj = {};
obj.self = obj;
JSON.stringify(obj); // TypeError: Converting circular structure to JSON

// ✅ 解决方案:使用 replacer 检测循环引用
function stringifyCircular(obj) {
const seen = new WeakSet();

return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]'; // 循环引用标记
}
seen.add(value);
}
return value;
});
}

2. Date 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
// Date 会被转换为 ISO 字符串
const obj = { date: new Date() };
const json = JSON.stringify(obj);
console.log(json); // '{"date":"2026-01-05T00:00:00.000Z"}'

// 恢复 Date 对象
const parsed = JSON.parse(json, (key, value) => {
if (key === 'date') {
return new Date(value);
}
return value;
});
console.log(parsed.date instanceof Date); // true

3. 大整数精度问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// JavaScript 中的数字是 64 位浮点数
// 超出 2^53-1 的整数会丢失精度
const bigInt = 9007199254740992; // 2^53
console.log(JSON.stringify({ id: bigInt }));
// '{"id":9007199254740992}'

const bigger = 9007199254740993; // 2^53 + 1
console.log(JSON.stringify({ id: bigger }));
// '{"id":9007199254740992}'(精度丢失)

// ✅ 解决方案:使用字符串
const obj = { id: '9007199254740993' };
console.log(JSON.stringify(obj));
// '{"id":"9007199254740993"}'

4. undefined、函数、Symbol

1
2
3
4
5
6
7
8
9
const obj = {
name: 'Alice',
undefined: undefined, // 被忽略
fn: function() {}, // 被忽略
symbol: Symbol('test') // 被忽略
};

console.log(JSON.stringify(obj));
// '{"name":"Alice"}'

3. 其他内置对象

3.1 Date 对象

1
2
3
4
5
6
7
8
9
// 创建日期
const now = new Date();
const specific = new Date('2026-01-05');
const timestamp = new Date(1704412800000);

// 格式化
console.log(now.toISOString()); // ISO 字符串
console.log(now.toLocaleString()); // 本地字符串
console.log(now.getTime()); // 时间戳

3.2 RegExp 对象

1
2
3
4
5
6
7
// 创建正则
const regex = /pattern/flags;
const regex2 = new RegExp('pattern', 'flags');

// 使用
const match = 'hello'.match(/l/g);
console.log(match); // ['l', 'l']

3.3 Math 对象

1
2
3
4
5
6
7
8
9
10
11
// 数学常量
console.log(Math.PI); // 3.14159...
console.log(Math.E); // 2.71828...

// 常用方法
console.log(Math.max(1, 2, 3)); // 3
console.log(Math.min(1, 2, 3)); // 1
console.log(Math.round(3.7)); // 4
console.log(Math.floor(3.7)); // 3
console.log(Math.ceil(3.2)); // 4
console.log(Math.random()); // 0-1 之间的随机数

4. 实战应用场景

4.1 数组去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 方式1:Set
const arr = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(arr)];

// 方式2:filter + indexOf
const unique2 = arr.filter((item, index) => arr.indexOf(item) === index);

// 方式3:reduce
const unique3 = arr.reduce((acc, item) => {
if (!acc.includes(item)) {
acc.push(item);
}
return acc;
}, []);

4.2 深拷贝

1
2
3
4
5
6
// 使用 JSON(有限制)
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}

// 限制:不能处理函数、undefined、循环引用等

4.3 数据转换

1
2
3
4
5
6
7
8
9
// 数组转对象
const arr = [['a', 1], ['b', 2]];
const obj = Object.fromEntries(arr);
console.log(obj); // { a: 1, b: 2 }

// 对象转数组
const obj2 = { a: 1, b: 2 };
const arr2 = Object.entries(obj2);
console.log(arr2); // [['a', 1], ['b', 2]]

5. 代码片段速查

5.1 数组操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 数组去重
const unique = [...new Set(arr)];

// 数组扁平化
const flat = arr.flat(Infinity);

// 数组分组
const groupBy = (arr, key) =>
arr.reduce((acc, item) => {
const group = item[key];
if (!acc[group]) acc[group] = [];
acc[group].push(item);
return acc;
}, {});

// 数组分块
const chunk = (arr, size) =>
Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
);

5.2 Map/Set 操作

1
2
3
4
5
6
7
8
9
10
11
12
// Map 转对象
const mapToObj = map => Object.fromEntries(map);

// 对象转 Map
const objToMap = obj => new Map(Object.entries(obj));

// Set 交集
const intersection = (set1, set2) =>
new Set([...set1].filter(x => set2.has(x)));

// Set 并集
const union = (set1, set2) => new Set([...set1, ...set2]);

5.3 JSON 操作

1
2
3
4
5
6
7
8
9
10
11
// 安全解析
const safeParse = (json, defaultValue = null) => {
try {
return JSON.parse(json);
} catch {
return defaultValue;
}
};

// 格式化输出
const prettyJson = obj => JSON.stringify(obj, null, 2);

6. 常见问题与解决方案

6.1 数组方法的选择

1
2
3
4
// 需要新数组 → map、filter、slice
// 需要修改原数组 → push、pop、splice
// 需要查找 → find、includes、indexOf
// 需要遍历 → forEach、for...of

6.2 Map vs Object

1
2
3
4
5
6
7
8
9
// 使用 Map:
// - 键是对象
// - 需要保持插入顺序
// - 频繁添加/删除

// 使用 Object:
// - 键是字符串/symbol
// - 需要原型链
// - 需要 JSON 序列化

6.3 Set vs Array

1
2
3
4
5
6
7
8
9
// 使用 Set:
// - 需要去重
// - 只需要检查存在性
// - 不需要顺序(虽然 Set 保持插入顺序)

// 使用 Array:
// - 需要索引访问
// - 需要重复元素
// - 需要数组方法

7. 速记与总结

7.1 核心概念速记

  • “数组有序可变长,类数组长得像但没方法。”

    • 数组是有序集合,length 可读写;类数组有数值索引和 length,但没有数组方法。
  • “Map 键任意值,WeakMap 键必须对象且弱引用(不可遍历)。”

    • Map 的键可以是任意类型;WeakMap 的键必须是对象,弱引用,不可遍历。
  • “Set 去重看 SameValueZero,WeakSet 只装对象也弱引用。”

    • Set 自动去重,使用 SameValueZero 比较;WeakSet 只能存储对象,弱引用。
  • “TypedArray 固定长度、数值专用,适合高性能场景。”

    • TypedArray 长度固定,只存储数值,适合高性能计算。
  • “JSON 只认双引号、无函数无 undefined,parse/stringify 搭配 reviver/replacer 更灵活。”

    • JSON 格式严格;使用 reviver/replacer 可以自定义解析和序列化。

7.2 选择指南

需求 推荐数据结构 原因
有序元素集合 Array 最常用,功能丰富
键值对(对象键) Map 支持对象作为键
键值对(弱引用) WeakMap 不阻止垃圾回收
唯一值集合 Set 自动去重
对象标记(弱引用) WeakSet 不阻止垃圾回收
高性能数值计算 TypedArray 固定长度,类型固定
数据交换 JSON 标准格式,广泛支持

7.3 最佳实践

  1. 数组操作优先使用不改变原数组的方法(map、filter、slice)
  2. 需要对象作为键时使用 Map
  3. 需要去重时使用 Set
  4. 为对象存储元数据时使用 WeakMap
  5. JSON 序列化时注意特殊值的处理
  6. 大整数使用字符串避免精度丢失

结语:JavaScript 提供了丰富的数据结构,每种都有其适用场景。理解它们的特性和区别,能够帮助你选择最合适的数据结构,编写更高效、更易维护的代码。