八种类型
JavaScript语言规定了8种类型,每一个值都属于某一种数据类型。
- Undefined
- Null
- Boolean
- String
- Number
- Symbol (ES2015/ES6新增)
- BigInt (ES2020/ES11新增)
- Object
其中Symbol是ES2015中新添加的,我们就先从这这个最不熟悉的类型开始讲解。而ES2020中新添加的BigInt,我们放在Number类型之后讲。
Symbol
symbol代表唯一的标识。
Symbol()函数会返回一个symbol类型的值。创建时,我们可以给symbol一个描述,就像这样Symbol("description")
,这主要的目的是用来debugging。你无需知道这个值具体是什么,你只用知道它与任何值都不相等就行,哪怕我们给他相同的描述,它也是不同的值。1
2
3
4let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
如果我们直接去console一个Symbol,它会报错TypeError: Cannot convert a Symbol value to a string
,因为Symbol不能隐式转换成string,我们可以用toString()
进行显示转换,也可以通过Symbol的description属性来去到他的描述1
2console.log(id1.toString()); // Symbol(id)
console.log(id1.description); // id
Symbol的作用
Symbol的作用只有一个,作为对象属性的唯一标识符。我们知道对象的key,只有可能是string和symbol,所以我们也可以说它是非字符串对象key的集合。
其最主要的目的就是防止对象属性发生冲突。
我们通过几个例子来理解一下:1
2
3
4
5
6
7
8const obj1 = {
a: 'aaa',
b: 'bbb',
}
const obj2 = {
a: 'a'
}
我们对上面两个对象进行合并的话,属性a就会被覆盖。就会变成这样。1
2
3
4{
a: 'a',
b: 'bbb'
}
如果我们不想在整合的时候因为冲突而产生覆盖,用Symbol就能解决这个问题。1
2
3
4
5
6
7
8const obj1 = {
[Symbol('a')]: 'aaa',
b: 'bbb'
}
const obj2 = {
[Symbol('a')]: 'a'
}
这样合并后的结果就是1
2
3
4
5{
Symbol(a): 'aaa',
Symbol(a): 'a',
b: 'bbb'
}
因为Symbol()
返回值是唯一的,也就是: Symbol('a') === Symbol('a')
的返回值为false。
在举一个例子,在开发中我们经常会遇到这样一个需求。根据传入不同的参数去做不同的事情:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj = {
a: 'a',
b: 'b',
c: 'c'
}
function handler(type) {
if (type === obj.a) {
...
} else if (type === obj.b) {
...
} else if (type === obj.c {
...
}
}
我们在调用的时候传入obj.a
或者obj.b
或obj.c
,这样的话其实我们并不用关心obj对象中a、b、c具体的值,他们各自的值是多少无所谓,只要他们直接互不相同就行。这是Symbol就很好地满足了这个需求。我们就可以把obj对象写成这样1
2
3
4
5const obj = {
a: Symbol(),
b: Symbol(),
c: Symbol()
}
获取Symbol()对应属性的值
在symbol和对象使用的时候我们就需要[]了。1
2
3
4
5
6
7
8
9
10let s = symbol();
const obj = {
s = 1,
[s] = 2
}
console.log(obj.s) // 1
console.log(obj['s']) // 1
console.log(obj[s]) // 2
Symbol 与 for…in
Symbols在for…in中不可枚举,Object.keys也是枚举不到key的。如果想要枚举的话,可以借助Object.getOwnPropertySymbols(obj)
这个方法。Reflect.ownKeys(obj)
这个方法会返回对象所有key的数组,使用这个相当于使用Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
Symbol 与 JSON.stringify()
当使用JSON.stringify()的时候,symbol作为key也会被忽略。
全局Symbol
我们知道,通常symbol是不同的。但是有时候我们希望相同名称的symbol的值是相同的。symbol提供了一个全局注册表。使用Symbol.for(key)
,它会从注册表里去读取,如果这个symbol不存在,它便会创建一个。它是全局可用的,甚至跨service worker、iframe。1
2
3
4
5
6
7
8// read from the global registry
let id = Symbol.for("id"); // if the symbol did not exist, it is created
// read it again (maybe from another part of the code)
let idAgain = Symbol.for("id");
// the same symbol
alert( id === idAgain ); // true
还有一个是与Symbol.for(key)
相反的操作Symbol.keyFor(sym)
,它是传入全局symbol变量,返回它的name,注意的是它只适用于全局的Symbol,非全局的Symbol会返回undefined。1
2
3
4
5
6
7// get symbol by name
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// get name by symbol
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
Undefined
表示未定义,任何变量在未赋值之前都是Undefined类型、值为undefined。它与null还是有一些区别,null表示定义了但是值为空,undefined表示根本就没有定义。这里有一个要注意的问题,undefined在JavaScript中不是一个关键词,它可能会被改写的。一般建议用void 0
来获取undefined的值。因为 void 运算会把任意一个表达式变成 undefined,也就是说不管void后面跟什么返回的都是undefined。不少 JavaScript 压缩工具在压缩过程中,也是将 undefined 用 void 0 代替掉了。
Null
Null表示空值。前面也说过它代表着定义了但是值为空,它与undefined不同,null是JavaScript的关键词,可以放心的使用。
Boolean
这个很简单就两个值,true和false。
String
在JavaScript中最重要的一点就是,String是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征。
ES7确定了字符串的最大长度为2^53-1
。但是在老版本V8中字符串最大长度限制在256MB(2^28 - 16)
,而在新版本中32位平台还与之前一样,64位平台的最大长度将限制在2^(32-1)-1-24
大约1GB。
但是我们要知道JavaScript中String的最大长度,并不是字符数。
要讲清楚这个概念,我们就不得不引入码点和代码单元的概念。
码点是指与一个编码表中的某个字符对应的代码值。在Unicode标准中,码点采用十六进制书写,并加上前缀U+,例如U+0041 就是拉丁字母A 的码点。Unicode的码点可以分成17个级别。
第1个代码级别称为基本字符区域(BMP),(U+0000 - U+FFFF),在基本字符区域中,每个字符用16位表示,通常被称为代码单元,换句话说也就是在基本字符区域内一个码点需要一个代码单元,一个码点对应一个字符。
而其余的16个级别,码点从U+10000 到U+10FFFF , 其中包括一些辅助字符(supplementary character)。而辅助字符采用一对连续的代码单元进行编码。也就是需要两个代码单元。
总结一下:一个字符对应一个码点(且每个字符的码点都是唯一的),但一个码点可能需要一个代码单元或者两个代码单元才能表示。
而JavaScript字符串把每个UTF16单元当做一个字符来处理,所以处理非BMP的时候就需要注意。比如 “𝌷”的length
就为2。
一个空的字符串的length为0.
如果你要是直接访问String.length返回的是1
1 | console.log(String.length) // 1 |
这是因为String中的这个length其实继承于Function.prototype的。函数实例的length为声明的参数长度,String接受一个value参数,所以length为1。
Number
在JavaScript中Number这种类型使用 IEEE754 格式来表示
整数和浮点数值(浮点数值在某些语言中也被称为双精度数值)。接下来我们讲讲什么是IEEE754
IEEE754
我们先记住几个基础知识,十进制小数点向左移动1位相当于将该数除以10,向右移动1位相当于将该数乘以10。同理,二进制小数点向左移动1位相当于将该数除以2,向右移动1位相当于将该数乘以2。
我们可以根据上面的概念,引入科学计数法。一个很大或者很小的数可以表示为:1
2
3a * 10^n
|a|>=1 且 |a|<10
n为整数
如果用科学记数法来表示二进制数:1
2
3
4a * 2^n
指数基数为2
|a|>=1且|a|<2,也就是说a的范围包含两个区间(-2,-1]、[1,2)
n为整数
浮点数
在计算器科学中,浮点是一种对于实数的近似值数值表示法,由一个有效数字(即尾数)加上幂数来表示,通常是乘以某个基数的整数次指数得到。以这种表示法表示的数值,称为浮点数。
IEEE 754规定了四种表示浮点数值的方式
- 单精度(32位)
- 双精度(64位)
- 延伸单精度(43比特以上,很少使用)
- 延伸双精度(79比特以上,通常以80位实现)
JavaScript使用的是双精度浮点型。
二进制浮点数由三部分组成
- 符号位s(sign bit),0表示正数,1表示负数。
- 阶码e(exponent bias),规定为实际指数值加上一个偏移值。偏移值为2^(n-1)−1,其中的n为存储指数的比特位长度。
由于科学记数法的指数可为正,可为负。因此,一个数转换为浮点数之后,符号位、阶码这两部分都是带符号的。
如果我们要比较两个浮点数的大小,那么除了要判断比较数值本身的符号位,还需要再判断比较阶码的符号位,最后才是非符号部分的比较。显然,这会复杂化比较逻辑。
在使用了偏移值之后,无论指数部分是正是负,都可以转换为非负数。将真值映射到正数域的数值(真值在数轴上正向平移一个偏移量),称为移码。使用移码来比较两个真值的大小比较简单,只要高位对齐后逐位比较即可,不用考虑符号位问题。 - 尾数f(fraction),用于存储“有效数字”的小数部分,使用原码表示。
在64位双精度浮点型:
- 符号位,占1bit
- 指数为,占11bit
- 尾数,占52bit
在32位单精度浮点型:
- 符号位,占1bit
- 指数为,占8bit
- 尾数,占23bit
IEEE 754计算
- 如果指数位全为1,且尾数不为0,就超过了最大值,这样的数就无法表示,表示为NaN,此时无视符号位。这就是为什么
NaN != NaN
。因为NaN有不同的可能。 - 如果指数位全为1,尾数为0的话,根据符号位得到两个特殊值,符号位为0就是正无穷Infinity,符号位为1的话就得负无穷-Infinity。
- 如果指数位和尾数位都为0的话,就会根据符号位表示正负0。
- 在正常的浮点数区间,那么对应的数字的计算公式为:
(-1)^s x(2^(e-1023))x(1.f)
对于e-1023和1.f可能会有些疑惑。其中 1023 代表指数的偏移量,那么为什么会有这个偏移量呢,首先,作为11位的阶码,如果是无符号,那么表示的范围为[0-2047]
, 当处于我们第三种情况时,e的范围要去掉0和2047,变成[1,2046]
,如果要表示正负,则需要使用一个bit去作为符号位,范围为[-1022, 1023]
,如果没有偏移量,那么需要引入补码,计算更加复杂,为了简化运算,则使用无符号的阶码,引入了偏移量的概念,通过偏移将其转换成[1,2046]
,所以偏移量为 1023。 那么对于1.f来说,因为对于浮点数,在规范中采用科学计数法的方法,对于十进制12.34来说,用科学技术法表示为 1.234×10^2,不会表示为 0.1234×10^3,其首位肯定是一个不为0的数字,那么在二进制中,只有0和1,那么他的首位就只能是1(用科学计数法表示,首位不能为0)。因为对于所有的浮点数他的首位都是1,因此可以省略一位进行,不需要额外占用1位,所以f实际上的有效位数有53位(其中首位是1)。在计算时需要使用上这个隐含的1,这也是1.f的由来. - 当指数为0且尾数不为0时,是非规范化的数字,用于表示那些非常接近0的数字。
从上面的分类我们得知,前两种是非正常的值,第一种含有 2^52 种情况(尾数有52位),第二种含有两种情况(正无穷和负无穷),那么根据排列组合,共有 2*1*2^52 = 2^53
种情况。这些非正常的值在JS中以 NaN、+Infinity、-Infinity表示,因此在Number中多了三种情况。那么总数就是 2^64-2^53+3
。
Number数值范围
Number的数值范围为-2^53 到 2^53。由上可知在双精度浮点型,尾数占52bit又由于整数位的第一位肯定为1,所以第一位可以省略掉,从而多了一位。
浮点数精度问题
因为十进制小数转成二进制会变成无限循环小数,它就变得不准确了。
从十进制数转为二进制数,小数的转换采用的乘2取整的过程。这必然就会有无限不循环的情况产生。这样用计算机存储产生截取是必然的,必定会有一定的精度损失!
遇到浮点数的比较,正确的方法应该是使用JavaScript提供的最小精度值:1
Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON
检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。
BigInt
这个是ES2020新增的,我们从上得知Number的最大整数是2^53-1。如果超过了这个数就会有问题。而BigInt的出现就再也不用担心这个问题。
创建BigInt的方式也很简单
- 直接在整数后面加n
let a = 123n
; - 调用BigInt函数
let a = BigInt(123);
BigInt运算
就像Number运算一样,只不过在做除法运算的时候会舍去小数部分。1
21n + 2n // 3n
1n / 2n // 0n
我们要注意BigInt类型和Number类型不能混用。
在BigInt和Number直接可以用BigInt()和Number()进行显示转换。但是如果BigInt超过了Number,会进行截断。
BigInt也不支持一元加法,我们知道可以用+”value”,将String类型的转为Number类型。这在BigInt中并不支持。
比较运算符,例如 < 和 >,使用它们来对 bigint 和 number 类型的数字进行比较没有问题。
由于 number 和 bigint 属于不同类型,它们可能在进行 == 比较时相等,但在进行 ===(严格相等)比较时不相等。
在进行布尔运算时与Number一样,BigInt 0n
为 false
,其他值为 true
。
Object
Object就是一组数据和功能的集合。对象通过new操作符后面跟着要创建的对象类型的名称来创建。
let o = new Object(); //如果不传参数的话,可以省略圆括号但是不推荐。
Object是所有类的基类。Object类所具有的任何属性和方法也同样存在于更具体的对象中。Object的每个实例都具有下列的属性和方法。
- constructor
用于创建当前对象的函数,也就是构造函数。 - hasOwnProperty
检测给定的属性是否在当前对象实例中,可以是String也可以是Symbol。 - isPrototypeOf
检测一个对象是否是另一个对象的原型。或者说一个对象是否被包含在另一个对象的原型链中。就是说如果B继承与A,那么A就是B的原型。A.isPrototypeOf(B) === true
- propertyIsEnumerable
此方法可以确定对象中指定的属性是否可以被 for…in 循环枚举,但是通过原型链继承的属性除外。如果对象没有指定的属性,则此方法返回 false。 - toLocaleString
返回对象的字符串表示形式,针对当前语言环境进行了本地化。但只有Array、Date和Number重写了此方法。其他的只是默认调用toString方法。 - toString
每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 “[object type]”,其中 type 是对象的类型。 - valueOf
valueOf():返回对象的字符串、数值或布尔值表示。通常与 toString()方法的返回值
相同。
Number、String和Boolean,这三个构造方法,在new的时候产生与其基本类型对应的对象类型。而直接调用的时候表示强制类型转换。
Symbol、和BigInt比较特殊,直接用new的时候回报错。
我们发现有时候对象可以直接用在基本类型上,比如字符串字面量可以直接调用String类型的方法,这是因为
JavaScript运算符提供了装箱操作,他会根据基础类型构造一个临时对象,使我们能在基础类型上调用对于对象的方法。