JavaScript 中的数据类型分为两类:原始类型和引用类型。
原始类型
在 JavaScript 中原始类型是除引用类型外的其他任何类型,表示在语言底层不可变的数据,是不可再细分的基本数据类型。
原始类型的数据不可变指的是,当原始值存入某块内存空间后,该空间就固定了。后面任何会改变该值的操作,都只是将新值存入新的内存空间,而最初那块内存空间和值都没有任何改变。
最新的 ECMAScript 标准共定义了 7 种原始类型:Number、BigInt、String、Boolean、Null、Undefined、Symbol。
Number
Number 类型是 JavaScript 中的主要数值类型,用于表示整数和浮点数。
数字类型字面量有多种写法,具体如下:
// 任何一个数字字面量前面都可以加上一个减号(-)变成负值
// 最常见的形式
const num1 = 123; // 123 --> 整数
const num2 = 12.34; // 12.34 --> 浮点数
const num3 = .23; // .23 --> 当整数部分为 0 时,可省略,
// 数字类型前面可加上前缀,来表示不同的进制
// 二进制(0b or 0B)、八进制(0 or 0o pr 0O)、十六进制(0x or 0X)
const num4 = 0b10101; // 21
const num5 = 010101; // 4161
const num6 = 0x3f7; // 1015
// 数字类型中间可加上 e 或 E,再跟一个整数指数,来表示实数乘以 10 的指数次幂
const num7 = 123e3; // 123000 --> 相当于 123 * 10^3
const num8 = 864E5; // 86400000 --> 相当于 864 * 10^5,该值可用于将毫秒转换为天
const num9 = 345.2e-1; // 34.52 --> 相当于 345.2 * 10^(-1)
// 为容易看清数字段,可用下划线(_)将数字字面量分隔
const num10 = 1_000_000; // 1000000
const num11 = 34_23_0; // 34230
数字类型有几个特殊的值:
Infinity、-Infinity- 在算术中除数为 0 时,就会出现这两个值,它们代表了数字类型的正负无穷
- 可以通过
Number.POSITIVE_INFINITY和window.Infinity直接访问Infinity - 可以通过
Number.NEGATIVE_INFINITY直接访问-Infinity
NaNNaN表示 Not a Number(非数字),虽然它仍然属于Number数据类型- 任何要求返回数字类型,但又无法正确转化为数字或无穷大的操作,最终都会返回
NaN- 如
0 / 0、Infinity / Infinity、Number('12a 51')等
- 如
- 可以通过
Number.NaN和window.NaN直接访问NaN - 可以通过
window.isNaN(x)判断某个值转换为数字类型是不是NaN
需要特别说明的是 JavaScript 数字类型采用的是 IEEE-754 标准定义的双精度 64 位二进制格式来存储数值的。
这首先意味着 JavaScript 能够显示的数值是有范围的,JavaScript 能表示的最大和最小数值分别存在了 Number.MAX_VALUE 和 Number.MIN_VALUE 里。在 JavaScript 里,大于最大的那个,小于最小的那个,就代表正负无穷大。
其次意味着你能够准确表示的整数也是有限的,JavaScript 可以准确表示 Number.MIN_SAFE_INTEGER ~ Number.MAX_SAFE_INTEGER 间的所有整数。超出这个范围的值可能会在末尾的数字上损失精度,因此是不安全的,我们可以通过 Number.isSafeInteger() 方法来判断某个值是否是一个“安全整数”。
还意味着 JavaScript 中的数字运算是不准确的,IEEE-754 双精度浮点表示法可以精确地表示如:1/2、1/8、1/1024 这种分数,而不能精确表示 1/10、1/100 这种十进制分数。所以,尽管 JavaScript 有很大的数值精度,能够非常近似地表示 0.1,但仍然无法精确表示,这就导致了一些计算上的问题。
至于为什么 IEEE-754 双精度浮点表示法会导致这些问题?
可以参考文章:前端应该知道的JavaScript浮点数和大数的原理
BigInt
BigInt 是 ES2020 新增的数值类型,它可以表示任意精度的整数。开发者可以使用它安全地存储和操作大整数,突破 Number 类型的安全整数限制。
BigInt 可以通过在整数末尾加小写字母 n 或调用构造函数来创建,和 Number 类型一样,它的字面量也可以在前面加前缀来改变进制:
// 任何一个 BigInt 字面量前面都可以加上一个减号(-)变成负值
// 普通形式
const bigNum1 = 1n; // 1n
const bigNum2 = BigInt(Number.MAX_SAFE_INTEGER); // 9007199254740991n
// BigInt 类型前面可加上前缀,来表示不同的进制
// 二进制(0b or 0B)、八进制(0 or 0o pr 0O)、十六进制(0x or 0X)
const bigNum3 = 0b111n; // 7n
const bigNum4 = 0o111n; // 73n
const bigNum5 = 0x111n; // 273n
值得注意的是 BigInt 不能和 Number 进行混合运算,不能试图去表示小数,也不能用于 Math 对象方法传参,否则会抛错。
String
字符串类型是 JavaScript 中表示文本数据的类型,它由一对匹配的单引号或双引号包裹:
// 字符串的值不包括包裹符号
// 由 '' 包裹
const str1 = 'abc'; // abc
// 由 "" 包裹
const str2 = "abc"; // abc
ES6 引入了模板字符串,使字符串字面量可以由反引号(`)包裹。单双引号字符串中如果要加入变量,只能使用 + 进行拼接,而在模板字符串中可以直接通过 ${} 包裹引入 JavaScript 表达式。
let status = "good";
// 单双引号字符串引入变量
const str3 = "Today is a" + status + "day."; // Today is a good day
const str4 = `Today is a ${status} day`
另外模板字符串还有个高级但不太常用的特性,带标签的模板字符串。我们可以在模板字符串开头的反引号前添加一个函数名,那么模板字符串中的文本和 JavaScript 表达式就会作为参数传递给这个函数。而该模板字符串最终的值就是函数的返回值。
function transName(strings, name) {
let s0 = strings[0]; // My name is
let s1 = strings[1]; // , thank you!
let outName;
if(name === 'dusk.star') {
outName = 'duskStar';
}
return `${s0}${outName}${s1}`
}
let name = 'dusk.star';
let str5 = transName`My name is ${name}, thank you!`;
console.log(str5); // My name is duskStar, thank you!
关于字符串的书写规范,双引号和反引号可以出现在单引号定义的字符串中,同理,双引号和反引号定义的字符串里也可以出现其他两种引号。不过引号之间不能在闭合前交叉使用,且单双引号定义的字符串不能跨行写,否则抛错。
const str6 = "we are 'Chinese'"; // we are 'Chinese'
const str7 = `we
are "Chinese"`; // we\n are "Chinese"
const str8 = 'we
are Chinese'; // error
const str9 = "123"456"; // error
如果我们要在单引号包裹的字符串中加入一个单引号怎么办呢,按照上面对书写规范的描述,直接写成 ''' 是要报错的。当然,我们可以改用 "'" 来进行规避,不过这并没有从根本上解决问题。
于是 JavaScript 引入了转移符 \,它可以跟其后面的字符组合在一起形成转义序列,用以在字符串中表示一个无法直接表示的字符。
例如,我们可以用 'I\'m happy' 来表示 I'm happy,而不会报错。用 \\ 表示 \、\n 表示换行符等,具体所有的转义字符可点击此处查看。
JavaScript 字符串使用的是 UTF-16 编码(Unicode 字符集的一种),因此它的本质是数个 16 位值组成的有序序列,其中的每一个值都代表一个 Unicode 字符。
例如字符串 'A',大写字母 A 的 Unicode 字符编码是 65,它在底层的存储就是 00000000 01000001。
我们可以通过 str[x] 的方式去访问每一个值,也可以通过 length 属性去查看该字符串包含多少个 16 位值。不过,目前 Unicode 已经包含了数以百万计的字符,要表示某些靠后的字符 16 位是不够的,有些字符要两个甚至三个 16位 Unicode 码点才能表示,这时 length 属性值可能就不像你预期的那样了。
Boolean
布尔类型是用于在逻辑上表达真或假的数据类型,它很简单,就只有两个值:true、false。
它可以通过字面量声明,但更多情况下是通过比较操作产生,进而影响 JavaScript 控制结构的行为。
// 字面量声明
const value = true; // true
// 流程控制
if(1 === 2) { // 1 === 2 --> false
xxx; // 不执行
}else {
xxx; // 执行
}
在类型转换中,JavaScript 的任何值都可以转换为布尔值。
会转换为 false 的值有:undefined、null、0、-0、NaN、"" 空字符串。
需要注意的是," " 不是空字符串,它是有一个空格值的字符串,它转换为布尔值是 ture。
除上述 6 个值以外,其他所有值,包括所有对象类型的值,都转换为 true。
Null && Undefined
Null 类型和 Undefined 类型都表示“无值”或“值不存在”的意思,它两都有且仅有一个具体值,分别是 null 和 undefined。
undefined 是全局对象的一个常量属性,而 null 不是,访问未赋值的变量、不存在的对象属性或获取没有明确返回值函数的返回值时,都得到的是 undefined。从本意来讲,null 更倾向于代表“无”的对象,而 undefined 则表示“无”的原始值。
但其实你非要用 undefined 作为对象变量的初始值或把 null 赋值给准备存储原始值的变量也毫无影响。它两的含义和用法都差不多,甚至进行比较操作 null == undefined 的结果都是 true。
那为什么 JavaScript 要规定两个类型来起同一个作用呢?其实严格上讲这是 JavaScript 的语言伴生缺陷,更多的细节可以参考阮一峰的这篇文章:undefined与null的区别。
Symbol
符号类型是 ES6 新增的原始类型,用来表示唯一且不可修改的原始值。
它没有字面量语法,要获得一个 Symbol 类型的值,就必须通过 Symbol(符号描述) 函数创建。而 Symbol 值的唯一且不可修改性就体现在,Symbol() 函数永远不会返回相同的值,就算传入的符号描述一样。
要注意的是,从 SE6 开始不再支持利用包装器从原始值显示地创建一个原始值对象,这意味着你不能用 new 运算符去调用 Symbol() 函数,虽然由于一些历史遗留原因,仍可以使用 new Boolean()、new String()、new Number() 等。
let sym1 = Symbol('name'); // Symbol(name)
let sym2 = Symbol('name'); // Symbol(name)
console.log(sym1 == sym2); // false
let sym3 = new Symbol('name'); // error
符号的设计的初衷,是为了给对象设置不能被看到和操纵的私有属性,让其只能在对象内部使用。
举个例子,我们要在游戏中定义一个冒险者,他具有攻击力、声明值、防御力等属性,而他对怪物每次造成的伤害是攻击力乘上一个随机数,于是我们需要一个生成随机数的方法。
对于对象来说,随机数产生方法,只会在内部充当一个工具使用,完全没有必要暴露在对象外部,使对象外部也能访问该方法。
SE6 以前对象的属性名必须是字符串,而有符号类型后,符号可以作为对象的属性名而存在,这种属性称之为符号属性。符号属性无法被枚举,即使是能够得到所有无法枚举属性的 Object.getOwnPropertyNames(),除非使用 ES6 新增的 Object.getOwnPropertySymbols() 方法。
不过,有时我们也想要在各种地方使用同一个符号值,而不是每次都得到一个新的 Symbol,当然普通符号做不到这一点,于是 JavaScript 还允许我们通过 Symbol.for(key) 定义共享符号。
JavaScript 会维护一个全局的运行时符号注册表,用 Symbol.for(key) 创建的符号就会放入其中。key 是一个字符串,它既是该符号的符号描述,也是符号注册表中与某个符号关联的键。每当调用 Symbol.for(key) 时,会首先检查给定的 key 是否已经在注册表中了,如果已经在了,就直接返回之前创建的那个,而不再新建一个 Symbol。
let sym5 = Symbol.for('abc');
let sym6 = Symbol.for('abc');
console.log(sym5 == sym6); // true
此外,JavaScript 还内置一些具有特殊作用的共享符号,称为知名符号。它们产生的原因是,ECMAScript 作为一门成熟的编程语言,应尽量减少黑盒部分,不能说很多底层 API 你只知道它可以实现某个功能,具体怎么实现的不知道,也没法人为干预。所以 ES6 后,用知名符号的方式,暴露了某些内部实现。
例如,String.prototype.replace() 替换字符串子串的功能是通过 Symbol.repalce 实现的。更多知名符号,大家可以查询 MDN。
引用类型
引用类型也称对象类型,是 JavaScript 中最重要且最复杂的数据类型。
在 JavaScript 中对象类型有很多种,除了 Object、Function、Array 等常见的结构,还有 Math、Date、Regex 等内置功能模块,它们都属于对象类型。
对象是一种复合值,它可以由原始值和其他对象组合而成,这也是它区别于原始类型不可细分性的地方。从本质上讲,对象是一个属性和方法的有序或无序集合,有序如 Array,无序如普通对象,而属性、方法则是对象存储数据、操作的键值对。
// 普通对象字面量
let obj = {
position: 'China',
number: 123,
sym: Symbol('符号')
}
包装对象
按理说,一个值如果具备属性和方法,那它就该是对象。可是根据我们的使用经验,字符串、数字这些原始类型的值也有它们的属性和方法。这是为什么?难道 JavaScript 底层真是 “一切皆对象”?
事实并非如此,原始类型是真正的原始类型,没有属性和方法,只不过在使用诸如 'abcd'.slice(1) 等方法的时候,JavaScript 后台会自动先将原始值包装成一个对象,由这个包装对象去执行该方法,再将方法处理得到的原始值传回。
举个例子,把 字符串 'ashgkaacbvh' 里的 'a' 全部剔除。
// 明面上的过程
let str = 'ashgkaacbvh';
console.log(str.replaceAll('a', '')); // 'shgkcbvh'
// 执行到 str.replaceAll('a', '') 时的后台处理过程
let _str = new String(str);
console.log(_str.replaceAll('a', '')); // 'shgkcbvh'
_str = null; // 销毁包装对象,等待垃圾回收
字符串如此,数字和布尔类型亦是如此,Null 和 Undefined 则不然,它们没有对应的包装器,所以它们的值没有可调用的属性和方法。
最后引用一段 MDN 对于原始类型包装对象的描述:
字符串字面量 (通过单引号或双引号定义) 和 直接调用 String 方法 (没有通过 new 生成字符串对象实例) 的字符串都是基本字符串。JavaScript 会自动将基本字符串转换为字符串对象,只有将基本字符串转化为字符串对象之后才可以使用字符串对象的方法。当基本字符串需要调用一个字符串对象才有的方法或者查询值的时候 (基本字符串是没有这些方法的),JavaScript 会自动将基本字符串转化为字符串对象并且调用相应的方法或者执行查询。
后话
作为 JavaScript 最复杂的数据类型,对象类型还有诸多值得深究的知识,普通对象、Function、Array、... 每一种结构都有各自的作用、特点和细节,就这一篇文章完全无法详细说明。
况且要彻底掌握对象类型,还涉及到原型、继承等概念,这些都会在日后的文章中详细说明,敬请期待。
写在最后
One day you'll leave this world behind.So live a life you will remember! --- Avicii
我是暮星,一枚有志于在前端领域证道的攻城狮。
优质前端内容持续输出中......,欢迎点赞 + 关注。