前言
在数年前更新的ES6中,新增了两个数据类型:BigInt和Symbol。乍一看它们的名字,你可能会有些许迷惑:BigInt?什么东西,超大的Int?Symbol又是什么鬼?当然,没有人会做无用功,它们被设计出来也是有原因的,且听我细细道来,带你通透♂通透~
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类型的加减乘除等运算需要要求同类型,但是数字大小的比较可以不是同类型,Number和BigInt可以混合比较数字大小,还可以在同一个数组中进行混合排序。
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]
尽管如此,但是Number和BigInt并不是严格相等的,它们是宽松相等的。
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的定义是这样的:
我们可以将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了。
当然这也适用于我们为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)
这就从一定的程度上避免了重要属性被误修改导致数据错误。
但它真的不能被访问吗?NoNoNo,它当然可以被访问了。
Object.getOwnPropertySymbols(user)
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!");
}
}
在这个例子中,我本应判断RED、BLUE等对应的后面的字符串
red,blue来判断危险等级,但它们不唯一,任何叫red和blue的都能触发警报,一只叫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]);
}
当我们遍历的时候,结果会是这个样子:
如果我们不想让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被私有化了起来,或者说相对藏起来了,但还是能找到.......
只是现在用循环遍历我们看不到这个属性了
最贴近实际的应用
在这里我们要实现一个组件出错报错的机制,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,即使list和list2都有着同样的组件名,也不会报错不准确了。
程序结果:
报错已经被正确找出并消除。
总结
BigInt
ES6新增的BigInt解决了超过安全数范围整数不能被表示的问题,
BigInt和Number两种数据类型必须转化为相同的数据类型才能进行算术运算,
但它们可以不转化直接进行大小的比较。
Bigint不能直接进行JSON序列化,我们可以在其prototype中写toJSON()方法实现序列化。
Symbol
Symbol的唯一作用就是给Object中当作标识符,
Symbol具有唯一值,它的输出值就是它本身,可以将其作为一串很长很长且唯一的数字来理解,
它具有唯一性,私密性,可以避免变量命名的冲突,避免错误修改Object中的数据,也可以用来快速添加一个元素而不用考虑其是否在原Object中存在,
它也可以利用Symbol.for(key)创造同样的Symbol。
OK,这一期就到这里吧!制作不易,如果你觉得好,请给我点个赞吧!知识来源主要为MDN web Docs和Youtube,如果写的不对敬请指正!