BigInt 与 Symbol:两个特殊类型的设计哲学
BigInt 在 2020 年才正式进入 ECMAScript 标准(ES2020),这意味着 JavaScript 在过去 25 年里一直缺少处理超过 2^53 的整数的能力。Symbol 则从 ES2015 起存在,却至今仍是最被低估的语言特性之一。
🔹 Level 1 · 你需要知道的
BigInt:超大整数的解决方案
// BigInt 的创建方式
const a = 42n; // 字面量(后缀 n)
const b = BigInt(42); // 函数调用
const c = BigInt("9007199254740993"); // 从字符串转换(安全!)
const d = BigInt("0xff"); // 十六进制字符串
// 与普通数字的比较
typeof 42n // "bigint"
typeof 42 // "number"
// BigInt 可以表示任意精度的整数
const huge = 9007199254740993n; // Number 无法精确表示
console.log(huge); // 9007199254740993n(正确!)
// 基本算术(只能与 BigInt 运算)
10n + 20n // 30n
10n * 20n // 200n
10n ** 3n // 1000n
10n / 3n // 3n(整数除法,截断而不是四舍五入)
10n % 3n // 1n
-10n / 3n // -3n(向零取整)
BigInt 不支持的操作:
// ❌ 不能和 Number 混用
1n + 1 // TypeError: Cannot mix BigInt and other types
1n + "1" // TypeError
// ✅ 需要显式转换
Number(1n) + 1 // 2(转为 Number,但可能丢精度!)
1n + BigInt(1) // 2n(转为 BigInt)
// ❌ BigInt 不支持小数
1.5n // SyntaxError
BigInt(1.5) // RangeError: The number 1.5 is not safe to convert to a BigInt
// ❌ BigInt 不能用于 Math 函数
Math.max(1n, 2n) // TypeError
// ❌ JSON.stringify 无法序列化 BigInt
JSON.stringify(42n) // TypeError: Do not know how to serialize a BigInt
// ✅ 自定义序列化
BigInt.prototype.toJSON = function() { return this.toString(); };
JSON.stringify({ id: 42n }) // '{"id":"42"}'
BigInt 的适用场景:
// 1. 区块链/加密货币 — 64位/256位大整数
const blockchainId = 115792089237316195423570985008687907853269984665640564039457584007913129639935n;
// 2. 精确时间戳(微秒/纳秒)
const nanoTimestamp = BigInt(Date.now()) * 1000000n; // 毫秒转纳秒
// 3. 64位整数 ID(来自后端的 Snowflake ID 等)
const snowflakeId = BigInt("1234567890123456789");
// 4. 大数运算(密码学、数论)
function factorial(n) {
if (n <= 1n) return 1n;
return n * factorial(n - 1n);
}
factorial(50n) // 30414093201713378043612608166979581188299763898377856000000000000n
Symbol:唯一键的保证
// Symbol 的创建——每次调用创建一个新的唯一值
const s1 = Symbol();
const s2 = Symbol();
s1 === s2 // false!每个 Symbol 都是唯一的
// 可以加描述(仅用于调试,不影响唯一性)
const s3 = Symbol("myKey");
const s4 = Symbol("myKey");
s3 === s4 // false!描述相同但仍然是不同的 Symbol
s3.toString() // "Symbol(myKey)"
s3.description // "myKey"
Symbol 作为对象键(避免属性名冲突):
// 场景:第三方库添加属性到用户对象,避免覆盖用户现有属性
const USER_META = Symbol("userMeta");
function addMetaToUser(user) {
user[USER_META] = {
lastModified: Date.now(),
modifiedBy: "system"
};
}
const user = { name: "Alice", id: 1 };
addMetaToUser(user);
// Symbol 键不出现在普通枚举中
Object.keys(user) // ["name", "id"](看不到 Symbol 键)
Object.values(user) // ["Alice", 1]
JSON.stringify(user) // '{"name":"Alice","id":1}'(Symbol 键被忽略)
for (const key in user) {} // 遍历不到 Symbol 键
// 需要专门的方法才能访问
Object.getOwnPropertySymbols(user) // [Symbol(userMeta)]
user[USER_META] // { lastModified: ..., modifiedBy: "system" }
Reflect.ownKeys(user) // ["name", "id", Symbol(userMeta)]
全局 Symbol 注册表:
// Symbol.for 在全局注册表中查找/创建
const s5 = Symbol.for("app.config");
const s6 = Symbol.for("app.config");
s5 === s6 // true!同一个 key 返回同一个 Symbol
Symbol.keyFor(s5) // "app.config"(从全局表中查找 key)
Symbol.keyFor(Symbol("test")) // undefined(局部 Symbol,不在全局表)
🔸 Level 2 · 它是怎么运行的
BigInt 的内部实现:多精度整数
BigInt 在 V8 中使用**多位数组(multi-digit array)**实现,每个"位"是64位无符号整数:
BigInt 内存布局(以 2^64 + 5 为例):
┌───────────────────────────────────────────────────────┐
│ BigInt 对象(堆上分配) │
│ │
│ Map 指针 ─→ BigInt Hidden Class │
│ length: 2 ─→ 使用了 2 个 64 位数字 │
│ sign: 0 ─→ 正数 │
│ │
│ 数字数组(小端序): │
│ [0]: 5 ─→ 低64位 = 5 │
│ [1]: 1 ─→ 高64位 = 1(即 2^64 的系数) │
│ │
│ 实际值 = 1 × 2^64 + 5 = 18446744073709551621 │
└───────────────────────────────────────────────────────┘
比较:Number 的 42 在 V8 中:
┌───────────────────────────────────────────────────────┐
│ SMI(小整数,嵌入指针) │
│ 直接存储在指针的高63位,零堆分配 │
└───────────────────────────────────────────────────────┘
BigInt 的性能成本:
加法操作性能对比(近似值,基于 V8 实测):
Number 加法(Smi范围内):
~0.3 纳秒/次(直接CPU指令,无内存分配)
Number 加法(HeapNumber):
~1-2 纳秒/次(需要堆分配)
BigInt 加法(小BigInt,1-2个digit):
~10-30 纳秒/次(需要堆分配+多精度算法)
约比 Number 慢 10-50 倍
BigInt 乘法(大BigInt,n个digit):
O(n^1.585) 时间复杂度(Karatsuba 算法)
对于256位整数(4个digit):约100-500纳秒/次
注意:随着 V8 对 BigInt 的优化改进,上述数字会持续变化
实际性能取决于 BigInt 的大小和具体运算
Symbol 的内部实现:唯一 ID 系统
Symbol 内部结构(概念性):
每个 Symbol 在引擎内部有一个唯一的数字 ID(递增计数器)
Symbol() 调用时:
1. 读取全局计数器 nextSymbolId(从0开始递增)
2. 创建一个 Symbol 对象,内含 [[Description]] 和 [[SymbolId]]
3. nextSymbolId++
Symbol 比较(=== 操作):
比较两个 Symbol 的 [[SymbolId]] 是否相同
比较是 O(1) 的整数比较
Symbol.for() 的全局注册表:
维护一个 Map<string, Symbol>(全局唯一)
Symbol.for("key") → 查找 Map,存在则返回,否则创建并存入
11 个 Well-Known Symbols(规范内置):
Symbol.iterator ── 使对象可迭代(for...of)
Symbol.asyncIterator ── 异步迭代(for await...of)
Symbol.toPrimitive ── 对象转基础类型(见第6章)
Symbol.toStringTag ── Object.prototype.toString 的标签
Symbol.hasInstance ── instanceof 的自定义行为
Symbol.isConcatSpreadable── Array.prototype.concat 的展开行为
Symbol.species ── 派生对象的构造函数
Symbol.match ── String.prototype.match 的行为
Symbol.matchAll ── String.prototype.matchAll 的行为
Symbol.replace ── String.prototype.replace 的行为
Symbol.search ── String.prototype.search 的行为
Symbol.split ── String.prototype.split 的行为
Symbol.unscopables ── with 语句中排除的属性
Well-Known Symbols 实战
// Symbol.iterator:赋予对象可迭代能力
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
for (const n of range) {
console.log(n); // 1, 2, 3, 4, 5
}
[...range] // [1, 2, 3, 4, 5]
Array.from(range) // [1, 2, 3, 4, 5]
// Symbol.toStringTag:自定义 Object.prototype.toString 结果
class MyCollection {
get [Symbol.toStringTag]() {
return "MyCollection";
}
}
Object.prototype.toString.call(new MyCollection())
// "[object MyCollection]"(通常是 "[object Object]")
// Symbol.hasInstance:自定义 instanceof 行为
class EvenNumber {
static [Symbol.hasInstance](num) {
return Number.isInteger(num) && num % 2 === 0;
}
}
2 instanceof EvenNumber // true
3 instanceof EvenNumber // false
4 instanceof EvenNumber // true
🔺 Level 3 · 规范怎么定义的
6.1.5 The Symbol Type
规范原文(ECMA-262 第 6.1.5 节):
6.1.5 The Symbol Type
The Symbol type is the set of all non-String values that may be used as the key of an Object property (6.1.7).
Each possible Symbol value is unique and immutable.
Each Symbol value immutably holds an associated value called [[Description]] that is either undefined or a String value.
6.1.9 The BigInt Type
规范原文(ECMA-262 第 6.1.9 节):
6.1.9 The BigInt Type
The BigInt type represents an integer value. The value may be any size and is not limited to a particular bit-width. Generally, where not otherwise noted, operations are designed to return exact mathematically-based answers. For binary operations, BigInts act as two's complement binary strings, with negative numbers treated as having infinite precision leading 1 bits.
关键表述:
- "any size and is not limited to a particular bit-width" — BigInt 是真正的任意精度整数,没有位宽限制。
- "exact mathematically-based answers" — BigInt 运算结果是精确的数学整数,不像 Number 会有浮点误差。
- "two's complement binary strings" — 按位运算时,负数被视为无限精度的补码。
Well-Known Symbols 规范表
规范第 6.1.5.1 节列出了所有 Well-Known Symbols:
| Symbol 名 | 规范描述 | 用途 |
|---|---|---|
@@asyncIterator |
A method that returns the default AsyncIterator for an object | for await...of |
@@hasInstance |
A method that determines if a constructor object recognizes an object as one of the constructor's instances | instanceof |
@@isConcatSpreadable |
A Boolean valued property that if true indicates that an object should be flattened to its array elements by Array.prototype.concat | Array.concat |
@@iterator |
A method that returns the default Iterator for an object | for...of, 展开运算 |
@@match |
A regular expression method that matches the regular expression against a string | String.match |
@@matchAll |
A regular expression method that returns an iterator that yields matches of the regular expression against a string | String.matchAll |
@@replace |
A regular expression method that replaces matched substrings of a string | String.replace |
@@search |
A regular expression method that returns the index within a string that matches the regular expression | String.search |
@@species |
A function valued property that is the constructor function that is used to create derived objects | Array.map 等 |
@@split |
A regular expression method that splits a string at the indices that match the regular expression | String.split |
@@toPrimitive |
A method that converts an object to a corresponding primitive value | 类型转换(见第6章) |
@@toStringTag |
A String valued property that is used in the creation of the default string description of an object | Object.toString |
@@unscopables |
An object valued property whose own and inherited property names are property names that are excluded from the with environment bindings of the associated object | with 语句 |
💎 Level 4 · 边界与陷阱
陷阱1:new BigInt() 会 TypeError
// ❌ BigInt 不是构造函数,不能 new
new BigInt(42) // TypeError: BigInt is not a constructor
// ✅ 作为函数调用
BigInt(42) // 42n
// 为什么设计成不能 new?
// Symbol 也是如此:
new Symbol() // TypeError: Symbol is not a constructor
Symbol() // 正确,返回一个 Symbol 值
// 设计原因:
// BigInt 和 Symbol 是原始类型(primitive),
// 使用 new 会产生包装对象,导致行为不一致:
// new Boolean(false) 是 truthy 对象,if (new Boolean(false)) {} 进入分支
// 规范故意不提供 BigInt 和 Symbol 的包装对象构造
陷阱2:1n + 1 为什么报 TypeError
// ❌ BigInt 和 Number 不能混用
1n + 1 // TypeError: Cannot mix BigInt and other types, use explicit conversions
// 这是有意为之的设计决策,原因:
// 1. 精度安全:如果允许混用,Number 的精度限制会悄悄影响结果
// BigInt(Number.MAX_SAFE_INTEGER) + 1n → 如果隐式转换,会有精度丢失
// 2. 类型明确:强制显式转换让代码意图更清晰
// ✅ 正确做法:显式转换
Number(1n) + 1 // 2(BigInt → Number,可能丢精度!)
1n + BigInt(1) // 2n(Number → BigInt,安全,但 Number 必须是整数)
// ❌ BigInt(1.5) 会报错
BigInt(1.5) // RangeError: The number 1.5 cannot be converted to a BigInt
// 比较运算是允许混用的(不涉及算术运算)
1n < 2 // true
1n == 1 // true(宽松相等,规范特殊处理)
1n === 1 // false(严格相等,类型不同)
陷阱3:Symbol 键在 JSON 中的消失
const KEY = Symbol("key");
const obj = {
normal: "visible",
[KEY]: "invisible",
123: "visible too"
};
// JSON.stringify 只序列化:字符串键 + 数字键(转字符串)
JSON.stringify(obj) // '{"123":"visible too","normal":"visible"}'
// Symbol 键完全消失!没有警告,没有错误。
// 传播操作也保留 Symbol
const clone = { ...obj };
clone[KEY] // "invisible"(展开运算保留 Symbol 键)
// Object.assign 也保留 Symbol
const target = {};
Object.assign(target, obj);
target[KEY] // "invisible"
// 只有 JSON.stringify 和 for...in 会忽略 Symbol 键
自定义 JSON 序列化(包含 Symbol 值):
// 如果需要序列化包含 Symbol 键的对象,需要手动处理
function serializeWithSymbols(obj) {
const symbols = Object.getOwnPropertySymbols(obj);
const result = { ...obj }; // 拷贝普通属性
for (const sym of symbols) {
// 用 Symbol 的 description 作为字符串键(需确保唯一性!)
result[`__symbol__${sym.description}`] = obj[sym];
}
return JSON.stringify(result);
}
陷阱4:Symbol.iterator 实现自定义迭代
// 赋予普通对象可迭代能力
class Fibonacci {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {
let prev = 0, curr = 1;
const limit = this.limit;
return {
next() {
if (prev > limit) {
return { value: undefined, done: true };
}
const value = prev;
[prev, curr] = [curr, prev + curr];
return { value, done: false };
},
[Symbol.iterator]() { return this; } // 使迭代器本身也可迭代
};
}
}
const fib = new Fibonacci(100);
[...fib] // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
// for...of 使用
for (const n of new Fibonacci(20)) {
console.log(n); // 0, 1, 1, 2, 3, 5, 8, 13
}
// 解构也使用 Symbol.iterator
const [a, b, c] = new Fibonacci(100); // a=0, b=1, c=1
陷阱5:Symbol.for 与 Symbol() 的全局性差异
// Symbol.for 创建全局共享 Symbol
const s1 = Symbol.for("shared");
// 在模块B中:
const s2 = Symbol.for("shared"); // 得到与 s1 完全相同的 Symbol
s1 === s2 // true
// Symbol() 创建本地唯一 Symbol
const s3 = Symbol("local");
const s4 = Symbol("local");
s3 === s4 // false(不同的 Symbol!)
// Symbol.for 的全局性跨越了:
// - 不同的 JavaScript 文件/模块
// - 不同的 <script> 标签
// 但不跨越:
// - 不同的 Realm(不同的 iframe 或 Worker)
// - Node.js 中不同的 vm 沙箱
// 实际应用:跨库共享协议
// 库A:
const PLUGIN_KEY = Symbol.for("myapp.plugin");
// 库B(只要用同一个字符串):
const PLUGIN_KEY_B = Symbol.for("myapp.plugin");
// PLUGIN_KEY === PLUGIN_KEY_B → true,可以正确识别插件
本章小结
-
BigInt 是 ES2020 引入的任意精度整数类型:适用于超过 2^53-1 的整数(区块链ID、64位时间戳、大数运算)。BigInt 不能与 Number 混用(会 TypeError),不能用于 Math 函数,不能被 JSON.stringify 序列化。性能约比 Number 慢 10-50 倍,应在确实需要精确大整数时才使用。
-
BigInt 内部用多个64位整数组成的数组实现:加法和乘法是多精度运算,时间复杂度随位数增长(乘法为 O(n^1.585))。小 BigInt(如区块链常见的256位)每次运算约需 100-500 纳秒。
-
Symbol 保证唯一性:两次
Symbol()调用即使描述相同也会返回不同的 Symbol。Symbol 作为对象键时,JSON.stringify、for...in、Object.keys等均会忽略它,需要Object.getOwnPropertySymbols()或Reflect.ownKeys()才能获取。 -
ECMAScript 定义了 13 个 Well-Known Symbols:这些内置 Symbol 是元编程的核心接口,通过
Symbol.iterator可以让任意对象支持for...of和展开运算,通过Symbol.toPrimitive可以精确控制类型转换行为,通过Symbol.hasInstance可以自定义instanceof的判断逻辑。 -
Symbol.for创建全局共享 Symbol,Symbol()创建本地唯一 Symbol:跨模块/跨库的协议用Symbol.for(需要约定好字符串 key),库内部的"私有"属性键用Symbol()(避免外部干扰)。两者不能互相替代,Symbol.keyFor可以区分这两种 Symbol。