🔥BigInt 和 Symbol:它们为什么被设计出来?解决了哪些痛点?🔥

164 阅读11分钟

前言

在数年前更新的ES6中,新增了两个数据类型:BigIntSymbol。乍一看它们的名字,你可能会有些许迷惑:BigInt?什么东西,超大的Int?Symbol又是什么鬼?当然,没有人会做无用功,它们被设计出来也是有原因的,且听我细细道来,带你通透♂通透~

0B58D23FE80F9FC16EADAFDF0DC25218.jpg

BigInt?超级大的Int?

无敌的Number倒下了

我们都知道在JavaScript中Number可以表示各种数字,浮点数也好,整数也好,不像其他语言要精确定义,Number直接全部就能表示,比较全能,但是呢,它其实是有大小范围的:

Number 类型使用 IEEE 754 64位双精度浮点数 存储所有数值,其能精确表示的整数范围是 -2^53 + 1 到 2^53 - 1±9007199254740991,而这个范围就叫安全整数

  • Number.MAX_SAFE_INTEGER = 9007199254740991(2^53 - 1)
  • Number.MIN_SAFE_INTEGER = -9007199254740991(-2^53 + 1)
  • (这两个数可以直接通过Number进行调用)
  • 超出这一部分的数字会丢失精度,不能正确地表示和进行算术运算。

So,为了解决很大很大的数不能表示的问题,ES6就推出了BigInt数据类型。

BigInt是个啥?怎么用?

BigInt 是一种内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数。这原本是 Javascript 中可以用 Number 表示的最大数字。BigInt 可以表示任意大的整数。

BigInt的用法:

const num1 = 1234567899876543221n; // 直接在数字后加n,就能被解析为BigInt类型
const num2 = BigInt(12345678); // 调用函数,将Number转为BigInt
const num3 = BigInt("1234567899876543221"); // 调用函数将字符串转为BigInt
const num4 = BigInt("0x1fffffffffffff"); // 16进制转换
const num5 = BigInt("0b11111111111111111111111111111111111111111111111111111") // 2进制转换
console.log(num1); // 1234567899876543221n
console.log(num2); // 12345678n
console.log(num3); // 1234567899876543221n
console.log(num4); // 9007199254740991n
console.log(num5); // 9007199254740991n

转换为BigInt的数字和Number有一些相似的地方,但也有不同,比如:其不能运用Math对象的方法,不能和Number类型进行混合运算,要将两者转换为同一种类型进行运算(否则会报错)。

BigInt的运算与比较

BigInt类型的数字可以进行+,-,*,/,%等运算的,和Number类型唯一区别就是其能表示超过安全数范围的整数

const num1 = BigInt(Number.MAX_SAFE_INTEGER) + 1n;
console.log(num1); // 9007199254740992n
const num2 = 4n / 2n; 
const num3 = 5n/2n; // 注意!!!BigInt除法会向下取整,忽略小数部分
console.log(num2+'   '+num3); // 2   2  BigInt类型在进行字符串拼接时会自动抛弃“n”后缀
const num4 = 2n**4n  // JS中的幂运算
console.log(num4); // 16n
const num5 = 5n % 2n;
console.log(num5); // 1n

虽然BigInt类型的加减乘除等运算需要要求同类型,但是数字大小的比较可以不是同类型,NumberBigInt可以混合比较数字大小,还可以在同一个数组中进行混合排序。

1n < 2;
// ↪ true

2n > 1;
// ↪ true

2 > 2;
// ↪ false

2n > 2;
// ↪ false

2n >= 2;
// ↪ true

const mixed = [4n, 6, -12n, 10, 4, 0, 0n];
// ↪  [4n, 6, -12n, 10, 4, 0, 0n]

mixed.sort();
// ↪ [-12n, 0, 0n, 10, 4n, 4, 6]

尽管如此,但是NumberBigInt不是严格相等的,它们是宽松相等的。

2n == 2;
// true
2n === 2 ;
// false

BigInt的JSON序列化

有趣的是,BigInt类型不能被 JSON.stringify() 序列化,如果直接利用这个方法尝试序列化某个BigInt类型的数字,则会报错:TypeError: Do not know how to serialize a BigInt

const num = BigInt(1233211234567);
console.log(JSON.stringify(num));
// TypeError: Do not know how to serialize a BigInt

当然我们也有办法将其实现JSON序列化,转换为JSON格式:我们可以在其原型prototype中定义toJSON函数:

BigInt.prototype.toJSON = function(){
    return this.toString();
}
const num = BigInt(1233211234567);
console.log(JSON.stringify(num));
// “1233211234567”

为啥要定义这个函数呢?因为JSON.stringify()在运行时会优先检查对象是否有 toJSON() 方法,如果有,就用它的返回值,如果没有 toJSON(),则按照默认的 JSON 序列化规则处理(如 Object → {}Array → []BigInt → TypeError

利用这一点,我们可以定义toJSON()函数,来达到序列化的目的。

Symbol?象征着啥玩意?

Symbol为什么存在?

对于它,MDN Web Docs有着一个清晰的定义:

每个从 Symbol() 返回的 symbol 值都是唯一的。一个 symbol 值能作为对象属性的标识符;这是该数据类型仅有的目的。

Symbol的存在就是为了当对象属性的标识符,因为它具有唯一的特性嘛。

Symbol是什么东西?

Symbol的定义是这样的:

image.png 我们可以将Symbol理解为一个特殊值,一个唯一值,可以想象成是一串很长的数字,如998352716533354,每一次在调用Symbol时都会生成不同的值,而description只是帮你理解这个Symbol是干嘛用的东西,实际上里面的内容对于Symbol没有任何影响,仅仅是一个描述,相当于注释

每两个Symbol都不会相等。

const sym1 = Symbol('cat');
const sym2 = Symbol('cat');
sym1 == sym2 // false
sym1 === sym2 // false

Symbol能用来做些什么?

我们可以利用Symbol来更好的进行Debug,避免冲突,看到下面一个例子:

let user = {
    ......
}

假设这是一个很复杂的JSON,我们想为里面的属性添加独有的id来进行追踪,于是我们令user.id = 1984422,但我们事先并不知道里面存在了一个id,于是我们的id就把user的id覆盖了,不小心修改了数据内容造成了错误,所以我们就可以添加Symbol,来实现这个目的:

const symId = Symbol('id');
user[symId] = 1984422;

我们就为这个数组添加了一个唯一的id了。

image.png

当然这也适用于我们为Object添加属性的时候,由于JSON结构过于复杂,我们没有时间一个个看是不是存在某个属性,我们就可以利用Symbol来创建唯一的一个属性名,从而进行我们需求的实现。

let user = {
    name:'Skye',
    age:20,
    city:'New York',
    id:1001
}
const symId = Symbol('id');
user[symId] = 1984422;

如果输出symId,则结果为Symbol('id'),输出user[symId],则结果为1984422

Symbol的私密性

Symbol具有一定的私密性,当我们为一个Object添加一个Symbol属性时,它不可以被直接遍历得到:

Object.getOwnPropertyNames(user)

image.png

这就从一定的程度上避免了重要属性被误修改导致数据错误。

但它真的不能被访问吗?NoNoNo,它当然可以被访问了。

举个栗子.jpg 我们只需要使用:
Object.getOwnPropertySymbols(user)

屏幕截图 2025-07-13 182326.png

Symbol也可以不是唯一的?

Symbol能够被访问,它其实也可以不是唯一的,我们只需要用Symbol.for('')来创建Symbol即可:

const sym1 = Symbol.for('cat')

const sym2 = Symbol.for('cat')

const sym3 = Symbol.for('cat')

const sym4 = Symbol.for('cat')

这些Symbol全部相同,sym1===sym2......

Symbol.for(key) 是 JavaScript 提供的全局 Symbol 注册机制,它允许你在 全局 Symbol 注册表 中查找或创建 Symbol

利用Symbol.for(key)时,会通过key先查找全局的Symbol,如果找到了就返回同样的Symbol,找不到就会创建新的Symbol。

OK,现在我食言了,我之前说description(也就是这里的key)对于Symbol没啥用,那是建立在Symbol唯一的基础上,每新建一个Symbol,即使description相同,也会是两个不同的东西。现在你对于description的了解是全面的了。

Symbol的一些实际应用

唯一值属性

比如我需要在全局定义一些唯一的值:

// 枚举类型
// 枚举类型是一种数据类型,允许定义一组命名常量,
// 便于表示离散的值,提高代码可读性和可维护性。
const STATUS = {
    READY : Symbol('ready'),
    RUNNING: Symbol('running'),
    DONE: Symbol('done')
}
let state = STATUS.READY;

if(state === STATUS.READY){
    console.log('ready');
}

或者我要实现这个需求:

我定义了几种颜色,不同颜色对应不同危险等级,比如blue对应low,但是有意思的来了,我家的猫咪也叫blue,那么我该怎么做,才能使得代码不会误判呢?

const RED = 'red'
const BLUE = 'blue'
const ORANGE = 'orange'
const YELLOW = 'yellow'
const cat = 'blue'

function getThreatLevel(color){
    switch (color){
        case RED:
            return 'severe';
        case ORANGE:
            return 'high';
        case YELLOW:
            return 'elevated';
        case BLUE:
            return 'low';
        default:
            console.log("I DON'T KNOW THAT COLOR!");
    }
}

那就用Symbol就可以解决了,因为Symbol具有唯一值嘛!

const RED = Symbol('red');
const BLUE = Symbol('blue');
const ORANGE = Symbol('orange');
const YELLOW = Symbol('yellow');
const cat = 'blue';
function getThreatLevel(color){
    switch (color){
        case RED:
            return 'severe';
        case ORANGE:
            return 'high';
        case YELLOW:
            return 'elevated';
        case BLUE:
            return 'low';
        default:
            console.log("I DON'T KNOW THAT COLOR!");
    }
}

在这个例子中,我本应判断REDBLUE等对应的后面的字符串 redblue来判断危险等级,但它们不唯一,任何叫redblue的都能触发警报,一只叫blue的猫甚至也可以触发警报,这是不对的,所以我们利用Symbol的唯一值属性,就可以解决了。

现在switch-case要判断的是RED,BLUE所对应的唯一值,只有这些值才能触发警报,所以我们只能通过访问对应的标识符才能触发警报了。

私有性

class Train {
    constructor(){
        this.length = 0;
    }
    add(car,contents){
        this[car] = contents;
        this.length++;
    }
}

let freightTrain = new Train();
freightTrain.add('refrigerator car','cattle');
freightTrain.add('flat car','aircraft parts');
freightTrain.add('rank car','milk');
freightTrain.add('hopper car','coal');

for(car in freightTrain){
    console.log(car, freightTrain[car]);
    
}

当我们遍历的时候,结果会是这个样子:

image.png

如果我们不想让length显示该怎么办呢?相信经过Symbol狂轰乱炸的你,已经知道了,我们要用Symbol将它私密化起来,令其不能通过一般方式被访问。

所以我们可以修改为这样:

const length = Symbol('length');

class Train {
    constructor(){
        this[length] = 0;
    }
    add(car,contents){
        this[car] = contents;
        this[length]++;
    }
}

let freightTrain = new Train();
freightTrain.add('refrigerator car','cattle');
freightTrain.add('flat car','aircraft parts');
freightTrain.add('rank car','milk');
freightTrain.add('hopper car','coal');

for(car in freightTrain){
    console.log(car, freightTrain[car]);
    
}

现在的length被私有化了起来,或者说相对藏起来了,但还是能找到.......

只是现在用循环遍历我们看不到这个属性了

image.png

最贴近实际的应用

在这里我们要实现一个组件出错报错的机制,AlertService是处理错误用的,MyComponent是各种各样的页面组件

class AlertService {
    constructor(){
        this.alerts = {};
    }
    addAlert(symbol,alertText){
        this.alerts[symbol] = alertText;
        this.renderAlerts();
    }
    removeAlert(symbol){
        delete this.alerts[symbol];
    }

    renderAlerts(){}; // 用于处理完错误信息刷新页面展现出来,这个对象方法不用管
}

const alertService = new AlertService();

class MyComponent{
    constructor(thing){
        this.componentId = Symbol(thing);
    }

    errorHandler(msg){
        alertService.addAlert(this.componentId,msg);
        setTimeout(()=>{
            alertService.removeAlert(this.componentId);
            console.log('Removed alert'+this.componentId);
            
        },5000);
    }
}

let list = new MyComponent('listComponent');
let list2 = new MyComponent('listComponent');
let form = new MyComponent('inputComponent');


list.errorHandler('Problem1');
list2.errorHandler('Uh oh!')

我们利用MyComponent中的this.componentId = Symbol(thing);给每一个错组件都加上唯一的ID,之后实例化Component并手动让组件出错,测试程序。

Symbol在这里的作用就是防止组件冲突,通过唯一的ID,即使listlist2都有着同样的组件名,也不会报错不准确了。

程序结果:

image.png 报错已经被正确找出并消除。

总结

BigInt

ES6新增的BigInt解决了超过安全数范围整数不能被表示的问题,

BigInt和Number两种数据类型必须转化为相同的数据类型才能进行算术运算,

但它们可以不转化直接进行大小的比较。

Bigint不能直接进行JSON序列化,我们可以在其prototype中写toJSON()方法实现序列化。

Symbol

Symbol的唯一作用就是给Object中当作标识符,

Symbol具有唯一值,它的输出值就是它本身,可以将其作为一串很长很长且唯一的数字来理解,

它具有唯一性,私密性,可以避免变量命名的冲突,避免错误修改Object中的数据,也可以用来快速添加一个元素而不用考虑其是否在原Object中存在,

它也可以利用Symbol.for(key)创造同样的Symbol。

OK,这一期就到这里吧!制作不易,如果你觉得好,请给我点个赞吧!知识来源主要为MDN web Docs和Youtube,如果写的不对敬请指正!

4a679daabe894daf8a7b5786375ac806.gif