携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
本系列为我在整个JS学习完毕之后的一个回顾总结,每篇文章涉及知识可能贯穿整个JS知识,所有文章在发布之后也会不断更新编辑
JavaScript共有8种数据类型,分为两类,
第一类为基本数据类型,包括7种,分别为:
- 数字(Number)
- BigInt(任意精度的整数)-ES2019
- 字符串(String)
- 布尔值(Boolean)
- 空值(Null)
- 未定义(Undefined)
- 符号(Symbol)-ES6
第二类为引用数据类型,包括一种,为
- 对象(Object)
1.数据类型特性
本节对各个数据类型的特性做一个总结。
1.1数字
精度问题
数值范围
数字类型基于IEEE 754标准实现,使用64位二进制数,利用科学计数法来表示数值,其存储结构如下图所示:
- 符号位长度为1个二进制位,0表示整数,1表示负数
- 指数为长度为11个二进制位,指数部分也有正负区分,因此指数部分的范围为
。
- 有效数字位的长度为52个二进制位,但IEEE 754规定默认第一位为1,因此有效数字位的长度为53位
因此JS可以表示的安全整数的范围由有效数字位决定,即(
),可分别用
Number.MIN_SAFE_INTEGER和Number.MAX_SAFE_INTEGER获得。
JS可以表示的最大数值范围由指数位决定,由于采用科学计数法,基数为2:
- JavaScript能表示的最大值为
(也就是说可以表示的数的二进制位数是1024位)
- JavaScript能表示的最小值为
(有效数字的表示为
,当指数为负数时,表明将小数点往左移,因此最小值可以加上52个有效数字位)
这两个边界值可以通过Number.MAX_VALUE和Number.MIN_VALUE获取到
精度问题
- 0.1+0.2 != 0.3问题?
0.1和0.2转化为二进制表示的方式为:小数本身乘以2,取整数位作为二进制表示,然后取小数位重新参与计算,循环往复,直至小数部分为0。
- 则0.1表示为0.0 0011 0011 0011....
- 0.2表示为0.0011 0011 0011....
所以0.1和0.2的表示是无限循环的,在js中其表示被截断,0.1加0.2自然不准确等于0.3。
那么如何判断两个数的相等于否呢?解决该问题的方法通常为设置一个有效误差值,在有效误差内则可以认为相等。由于有效位数为52位,则精确的最小数为,因此我们可以令有效误差值为
,该值可以通过
Number.EPSILON(ES6) 获取。只要两个数的差值在小于有效误差值,则可以认为相等。
在条件允许的精度范围内,如何正确的计算如0.1+0.2这些“有问题”的表达式呢?使用num.toFixed()或num.toPrecision方法,将数字在指定位置处四舍五入截断。二者的区别为:
toFixed指定小数部分的位数toPrecision指定有效数字的位数
具体如下
let a = 0.1 + 0.2;
let b = a.toFixed(2);//0.30
//整数部分的0不是有效数字
let c = a.toPrecision(2);//0.30
Number(b)//0.3
Number(c)//0.3
即使是toFixed也会出现精度问题,如1.335.toFiexd(2)的返回值为1.33,而正确的值应该为1.34。因此如果有这样的运算需求可以通过big.js或decimal.js等库来解决
- 移位运算符问题?
js位运算符会将它的操作数视为 32 位元的二进制串(0 和 1 组成),因此当操作数大于次时,操作数会被截取成低位的32位,导致位运算结果不准确。
let a = 2**3
a.toString(2)//'100000000000000000000000000000000'
a<<1 // 0
数值字面量格式(表示方法)
Number类型的字面量表示有多种:
- 十进制整数。
let num = 5;
num = 5.0;
num // 5
小数部分如果为0也是整数
- 十进制浮点数。
let num = 0.1;
num = .1;
num = 1.1
- 八进制(以
0开头,表示为八进制数,且位数至少为2,如果为为1为,则是普通十进制数)。
let num = 010;
num //8
num = 01; // 1
八进制字面量在严格模式下是无效的,会导致 JavaScript 引擎抛出语法错误。
- 十六进制(以
0x前缀开头)
let hexNum1 = 0xA; // 10
hexNum2 = 0x1f; // 31
- 指数表示
2e2; // 200
2E3; // 2000
number. toExponential()可以获取数字的指数表示的字符串
- Infinity和-Infinity(表示无穷大和负无穷大)
1/0; //Infinity
-1/0; //-Infinity
- NaN(表示不是一个数字)
1.2BigInt
前面说过Number类型能精确表示的最大整数为,超过该值的表示可以用
BigInt来实现。BigInt的定义方式有两种:
- 在整数字面量尾部加
n - 使用
BigInt()函数
let n1 = 9007199254740991n
let n2 = BigInt(9007199254740992)
除了无符号右移以外,其余Number类型能用的运算符(+、-、*、/、**、%)都能用于BigInt类型。需要注意的是:BigInt类型只能与BigInt类型进行运算,无法与Number类型运算。且除法运算结果会被向下取整
序列化
使用JSON.strinfy()进行序列化时,BigInt类型不能正确被序列化,如果需要,我们需要在原型上重新定义toJSON方法
在对某个对象序列化时,如果有toJSON函数,会优先调用该函数获得一个可序列化的安全的“JSON值”,然后再对该值进行序列化,而不是直接对整个对象序列化。
BigInt.prototype.toJSON = function() { return this.toString(); }
let arr = [1n,2n,3n]
JSON.stringify(arr) // '["1","2","3"]'
1.3字符串
不可变特性
JS中的字符串是不可变的,对字符串的任何修改都会生成一个修改后的新字符串,原字符串不会改变。
let s = 'abc';
let a = s.concat('d'); //'abcd'
a===s; //false
s // 'abc'
类数组特性
由于字符串具有length属性和可索引的字符,因此字符串具有类数组的特性。但由于字符串的不可变特性,所以字符串不能通过索引直接修改字符串,除reverse()以外的其它数组方法都能通过call()被字符串调用。
s = 'abcd';
s[0] = 1;
s // 'abcd'
s[0] // a
//数组的方法都可以通过call函数来进行调用
Array.prototype.join.call(s, ',') //'a,b,c,d'
Array.prototype.map.call(s, (v)=>{return v+1}) //['a1', 'b1', 'c1', 'd1']
字符编码问题
通常情况下,JavaScript中的每个字符都由16位码元组成, 但16位码元只能表示65536个字符,仍有部分不常用的字符只能通过2个16位码元进行表示,而某些字符串操作也是按照16位码元为基本单位进行操作,因此当遇到这些2个16位码元进行表示的字符时就会出现问题。
如下所示,.length属性并不能正确的获取字符串的长度。
let s = String.fromCodePoint(97, 98, 0x1F60A, 100, 101)
console.log(s) //'ab😊de'
console.log(s.length) //6
模板字符串
字符串的字面量有三种定义方式,分别用单引号''、双引号""、反引号``包裹。使用反引号包裹的字符串称为模板字符串,模板字符串有许多特性。
- 多行字符串(字符串的换行不用转义字符
\n,可以直接敲回车换行,)
let s = `123
456`
s
//123
//456
- 字符串插值(将变量插入字符串时不再需要拼接字符串,易于阅读)
准确来说,模板,即${}内不仅能填充变量,而是任何表达式(即有返回值的语句)
let a = '+'
let c = '='
let fn = (n1, n2) => {return n1 * n2}
let s = `1${a}2${c}3` //1+2=3
s = `2*3=${fn(2, 3)}` //2*3=6
s = `2*3=${2*3}` //2*3=6
1.4布尔值
if、while等控制流语句会自动将任何数据类型转换为布尔值
1.5Null和Undefined
这两个类型都只有一个特殊值,null和undefined。
undefined表示一个变量定义了但未被赋值,也可以表示函数没有返回值null在语义上表示一个变量会保存一个对象,但暂时未存储
1.6符号
Symbol类型是一种基本数据类型,它的特性是每一个Symbol值都是独一无二的,因此其作用只有一个,一个 symbol 值作为一个对象属性的标识符,从而防止对象属性名冲突。
防止对象属性名冲突主要是因为对象属性名都是字符串,当我们使用其它人提供的对象并往对象中添加新的属性时。新添加的属性名可能会与旧属性名产生冲突。
Symbol作为对象属性
Symbol类型值的定义使用Symbol()函数,可传入一个字符串作为对该symbol类型值的描述。需要注意的是,当symbol类型值作为对象的属性名时,只能通过对象[symbol] = value的方式添加,无法通过点或者对象字面量的方式进行添加。
let mySymbol = Symbol('abc')
let obj = {
mySymbol: '123',
}
obj[mySymbol] = '456'
obj //{mySymbol: '123', Symbol(abc): '456'}
全局注册Symbol
我们可以把Symbol类型值注册在全局供整个应用使用。调用Symbol.for(key):
- 当key存在时,则key作为键值来获取对应symbol值
- 当key不存在时,则key既会作为键值也会作为描述符来在全局注册并生成一个Symbol类型值
let {b} = require('./b.js')
let a = Symbol.for('globlSymbol')
console.log(a === b) // true
let b = Symbol.for('globlSymbol')
exports.b = b
内建Symbol值
Symbol中也有一些常用的内置符号(不是全局注册的符号)来暴露语言内部的行为,我们可以直接访问、重写或模拟这些行为 。
比如Symbol.iterator是一个迭代器,当使用for..of语句时会调用遍历对象上的Symbol.iterator属性(是一个函数,返回一个迭代器)获取迭代器进行遍历。
内置符号也可简写为@@iterator
再比如Symbol.Primitive,在对象类型转化为原始类型时,会优先调用该属性对应的函数执行。该函数被调用时,会被传递一个字符串参数 hint,表示要转换到的原始值的预期类型,比如"number"、"string" 。
let myObj = {
[Symbol.toPrimitive] (hint) {
console.log('执行Symbol.toPrimitive')
return 1
}
}
console.log(1+myObj)
//执行Symbol.toPrimitive
//2
1.7对象
2类型检测
类型检测的方式有4种,分别是:
- typeof
- Object.prototype.toString()
- instanceof
- 其它API
2.1typeof
typeof的返回值永远为9个,如下所示
var a;
a = "1";
typeof a; // "string"
a=1;
typeof a; // "number"
a = true;
typeof a; // "boolean"
a = null;
typeof a; // "object"
a = undefined;
typeof a; // "undefined"
a = {b:"c"};
typeof a; // "object"
a = Symbol('1');
typeof a; // "symbol"
a = 1231515n;
typeof a; // 'bigint'
a = function () {};
typeof a; // 'function'
2.2toString()
所有 typeof 返回值为 "object" 的对象(如数组、null)都包含一个内部属性 [[Class]]。我们可以把它看作一个对象内部的分类,但这个分类只限于JavaScript的内置对象(如Math、Number、Boolean、String、Array、Date、Map、Set等等,不包括自定义对象。
这个属性无法直接访问, 一般通过 Object.prototype.toString() 来查看 ,当然,如果无法访问到其Object原型上的toString方法,可以通过call函数调用。
因此该方法是检测null的一个方法
let a;
a = Object.prototype.toString.call(null)
console.log(a) //[object Null]
a = Object.prototype.toString.call([1,2,3])
console.log(a) //[object Array]
a = Object.prototype.toString.call(1)
console.log(a) // [object Number]
a = Object.prototype.toString.call('1')
console.log(a) //[object String]
a = Object.prototype.toString.call(true)
console.log(a) //[object Boolean]
a = Object.prototype.toString.call(new Date())
console.log(a) // [object Date]
a = Object.prototype.toString.call(Math)
console.log(a)// [object Math]
a = Object.prototype.toString.call(1234n)
console.log(a)// [object BigInt]
2.3instanceof
instanceof用于检测某个对象其原型链上是否含有某个构造函数
2.4其它API
Array.isArray()
该API在任何情况下都能检测出数组,与instanceof的区别在于,当存在iframes时,instanceof Array可能无法检测出从iframes传递过来的数组对象
Number.isNaN()和isNaN()
isNaN()定义在window对象中,与之相比的Number.isNaN() 不会自动将传入的参数进行隐式的强制类型转换,转换成Number类型,只有在参数是值为 NaN 的数字时,才会返回 true。
3类型转换
3.1转换为数字
转换为数字的方式有两种:
- 通过函数转换,包括
Number()、parseInt()、parseFloat() - 隐式转换,通过
+、-、*、/运算符进行数值运算时转换
具体转换规则如下
- 布尔值转换为数字
true转换为1,false转换为0
null
-
Number(null)和隐式转换将其转换为0parseInt(null)、parseFloat(null)转换为NaN
undefined转换为数字是NaN- 字符串转换为数字
-
Number()只能转换纯数值模式的字符串。如Number('1')=1、;Number('1.1')=1.1、Number('011')=11(开头的0被忽略)、Number('0xf')=15(十六进制也能被识别)、Number('')=0(空字符串为0)、Number('12a')=NaN(除上述情况以外的其它情况返回NaN)- 隐式转换与
Number()转换规则相同,但+比较特殊,除非其位于开头,如+'0xf'=15,否则会被识别为字符串拼接 parseInt(string, radix)除了上述规则外能解析出开头为数值模式的字符串并忽略剩余的字符,且将小数点后的位数截断。其第二个参数表示字符串中数值的进制。如parseInt('15.2abc', 10)=15、parseInt('Ftt12', 16)=15parseFloat()与parseInt类似,但不截断小数点后的位数。如parseInt('15.2abc', 10)=15.2
- 对象转换为数字
-
- 首先调用对象的
valueOf()方法,将其返回值按照上述规则转换返回的值。如果返回值是一个对象,则调用toString()方法,将其返回值按照上述规则转换返回的值;如果返回值是一个其它基本类型值,再按上述规则转换。
- 首先调用对象的
const obj1 = {
valueOf() {
return 1
},
toString() {
return '2'
}
}
Number(obj1) //1
const obj2 = {
valueOf() {
return {}
},
toString() {
return '2'
}
}
Number(obj2) //2
3.2转换为字符串
转换为数字的方式有两种:
- 通过函数转换,包括
String() - 隐式转换,通过
+运算符
转换规则如下:
- 如果为对象,则调用
toString方法,转换值为方法其返回值 - 如果为
Number或Boolean类型,则直接转换为其字面量表示(事实上是先变为包装对象,再调用toString方法) - 如果为
null,则为'null' - 如果为
undefined,则为'undefined'
String(null)
'null'
String(undefined)
'undefined'
String(123)
'123'
String(true)
'true'
let obj = {
toString() {
return 'obj'
}
}
String(obj) // 'obj'
3.3转换为布尔值
- 字符串:空字符串
''转换后为false,其它字符串为true - 数字:
+0、-0和NaN转换后为false,其它为true(包括无穷) - 对象:转换后为
true null:转换后为falseundefined:转换后为false