0%

JavaScript类型

八种类型

JavaScript语言规定了8种类型,每一个值都属于某一种数据类型。

  1. Undefined
  2. Null
  3. Boolean
  4. String
  5. Number
  6. Symbol (ES2015/ES6新增)
  7. BigInt (ES2020/ES11新增)
  8. Object

其中Symbol是ES2015中新添加的,我们就先从这这个最不熟悉的类型开始讲解。而ES2020中新添加的BigInt,我们放在Number类型之后讲。

Symbol

symbol代表唯一的标识。
Symbol()函数会返回一个symbol类型的值。创建时,我们可以给symbol一个描述,就像这样Symbol("description"),这主要的目的是用来debugging。你无需知道这个值具体是什么,你只用知道它与任何值都不相等就行,哪怕我们给他相同的描述,它也是不同的值。

1
2
3
4
let 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
2
console.log(id1.toString());  // Symbol(id)
console.log(id1.description); // id

Symbol的作用

Symbol的作用只有一个,作为对象属性的唯一标识符。我们知道对象的key,只有可能是string和symbol,所以我们也可以说它是非字符串对象key的集合。
其最主要的目的就是防止对象属性发生冲突。

我们通过几个例子来理解一下:

1
2
3
4
5
6
7
8
const 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
8
const 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.bobj.c,这样的话其实我们并不用关心obj对象中a、b、c具体的值,他们各自的值是多少无所谓,只要他们直接互不相同就行。这是Symbol就很好地满足了这个需求。我们就可以把obj对象写成这样

1
2
3
4
5
const obj = {
a: Symbol(),
b: Symbol(),
c: Symbol()
}

获取Symbol()对应属性的值

在symbol和对象使用的时候我们就需要[]了。

1
2
3
4
5
6
7
8
9
10
let 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
3
a * 10^n
|a|>=1 且 |a|<10
n为整数

如果用科学记数法来表示二进制数:

1
2
3
4
a * 2^n
指数基数为2
|a|>=1且|a|<2,也就是说a的范围包含两个区间(-2,-1]、[1,2)
n为整数

浮点数

在计算器科学中,浮点是一种对于实数的近似值数值表示法,由一个有效数字(即尾数)加上幂数来表示,通常是乘以某个基数的整数次指数得到。以这种表示法表示的数值,称为浮点数。

IEEE 754规定了四种表示浮点数值的方式

  • 单精度(32位)
  • 双精度(64位)
  • 延伸单精度(43比特以上,很少使用)
  • 延伸双精度(79比特以上,通常以80位实现)

JavaScript使用的是双精度浮点型。

二进制浮点数由三部分组成

  1. 符号位s(sign bit),0表示正数,1表示负数。
  2. 阶码e(exponent bias),规定为实际指数值加上一个偏移值。偏移值为2^(n-1)−1,其中的n为存储指数的比特位长度。
    由于科学记数法的指数可为正,可为负。因此,一个数转换为浮点数之后,符号位、阶码这两部分都是带符号的。
    如果我们要比较两个浮点数的大小,那么除了要判断比较数值本身的符号位,还需要再判断比较阶码的符号位,最后才是非符号部分的比较。显然,这会复杂化比较逻辑。
    在使用了偏移值之后,无论指数部分是正是负,都可以转换为非负数。将真值映射到正数域的数值(真值在数轴上正向平移一个偏移量),称为移码。使用移码来比较两个真值的大小比较简单,只要高位对齐后逐位比较即可,不用考虑符号位问题。
  3. 尾数f(fraction),用于存储“有效数字”的小数部分,使用原码表示。

在64位双精度浮点型:

  1. 符号位,占1bit
  2. 指数为,占11bit
  3. 尾数,占52bit

在32位单精度浮点型:

  1. 符号位,占1bit
  2. 指数为,占8bit
  3. 尾数,占23bit

IEEE 754计算

  1. 如果指数位全为1,且尾数不为0,就超过了最大值,这样的数就无法表示,表示为NaN,此时无视符号位。这就是为什么NaN != NaN。因为NaN有不同的可能。
  2. 如果指数位全为1,尾数为0的话,根据符号位得到两个特殊值,符号位为0就是正无穷Infinity,符号位为1的话就得负无穷-Infinity。
  3. 如果指数位和尾数位都为0的话,就会根据符号位表示正负0。
  4. 在正常的浮点数区间,那么对应的数字的计算公式为:(-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的由来.
  5. 当指数为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的方式也很简单

  1. 直接在整数后面加n
    let a = 123n;
  2. 调用BigInt函数
    let a = BigInt(123);

BigInt运算

就像Number运算一样,只不过在做除法运算的时候会舍去小数部分。

1
2
1n + 2n  // 3n
1n / 2n // 0n

我们要注意BigInt类型和Number类型不能混用。
在BigInt和Number直接可以用BigInt()和Number()进行显示转换。但是如果BigInt超过了Number,会进行截断。
BigInt也不支持一元加法,我们知道可以用+”value”,将String类型的转为Number类型。这在BigInt中并不支持。

比较运算符,例如 < 和 >,使用它们来对 bigint 和 number 类型的数字进行比较没有问题。
由于 number 和 bigint 属于不同类型,它们可能在进行 == 比较时相等,但在进行 ===(严格相等)比较时不相等。

在进行布尔运算时与Number一样,BigInt 0nfalse,其他值为 true

Object

Object就是一组数据和功能的集合。对象通过new操作符后面跟着要创建的对象类型的名称来创建。

let o = new Object();  //如果不传参数的话,可以省略圆括号但是不推荐。

Object是所有类的基类。Object类所具有的任何属性和方法也同样存在于更具体的对象中。Object的每个实例都具有下列的属性和方法。

  1. constructor
    用于创建当前对象的函数,也就是构造函数。
  2. hasOwnProperty
    检测给定的属性是否在当前对象实例中,可以是String也可以是Symbol。
  3. isPrototypeOf
    检测一个对象是否是另一个对象的原型。或者说一个对象是否被包含在另一个对象的原型链中。就是说如果B继承与A,那么A就是B的原型。A.isPrototypeOf(B) === true
  4. propertyIsEnumerable
    此方法可以确定对象中指定的属性是否可以被 for…in 循环枚举,但是通过原型链继承的属性除外。如果对象没有指定的属性,则此方法返回 false。
  5. toLocaleString
    返回对象的字符串表示形式,针对当前语言环境进行了本地化。但只有Array、Date和Number重写了此方法。其他的只是默认调用toString方法。
  6. toString
    每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 “[object type]”,其中 type 是对象的类型。
  7. valueOf
    valueOf():返回对象的字符串、数值或布尔值表示。通常与 toString()方法的返回值
    相同。

Number、String和Boolean,这三个构造方法,在new的时候产生与其基本类型对应的对象类型。而直接调用的时候表示强制类型转换。

Symbol、和BigInt比较特殊,直接用new的时候回报错。

我们发现有时候对象可以直接用在基本类型上,比如字符串字面量可以直接调用String类型的方法,这是因为
JavaScript运算符提供了装箱操作,他会根据基础类型构造一个临时对象,使我们能在基础类型上调用对于对象的方法。