数据类型
基本类型(基本数值、基本数据类型)是一种既非对象也无方法的数据。在 JavaScript 中,共有7种基本类型:string,number,bigint,boolean,null,undefined,symbol (ECMAScript 2016新增)。
引用自MDN-原始数据
多数情况下,基本类型直接代表了最底层的语言实现。所有基本类型的值都是不可改变的。它们被称为原始值
原始值
除 Object 以外的所有类型都是不可变的(值本身无法被改变)。例如,与 C 语言不同,JavaScript 中字符串是不可变的(译注:如,JavaScript 中对字符串的操作一定返回了一个新字符串,原始字符串并没有被改变)。我们称这些类型的值为“原始值”。
多数情况下,基本类型直接代表了最底层的语言实现。
所有基本类型的值都是不可改变的。但需要注意的是,基本类型本身和一个赋值为基本类型的变量的区别。变量会被赋予一个新值,而原值不能像数组、对象以及函数那样被改变。
可以看到,原数据并没有被改变,可以被替换,但是替换后,原数据可能在一定时间被回收机制销毁,但你还是没有改变它。
Undefined
Undefined
只有一个值,就是特殊值undefined
,当开发者在使用let
或者const
定义一个变量但没有初始化时,就相当于给变量赋予了undefined
值。
let message;
console.log(message === undefined); // true
// 等价于
let message = undefined;
console.log(message === undefined); // true
除此之外,还有一个方法可以获得undefined
。那就是未定义的变量,比如:
console.log(typeof message); // undefinex; 未定义变量
未定义的变量只有这一个有用的操作,另外一个
delete
也可以操作未定义变量,但在严格模式下会报错。
Null
Null
类型同样只有一个特殊值null
。
其实,js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信:
- 000:对象
- 010:浮点数
- 100:字符串
- 110:布尔
- 1:整数
但是,对于 undefined
和 null
来说,这两个值的信息存储是有点特殊的。
-
null
:所有机器码均为0 -
undefined
:用 −2^30 整数来表示
那么,当使用typeof
来判断null
的时候,就直接被当做一个对象了。也可以说null
值表示一个空对象指针。
当你定义一个将来要保存对象值的变量时,最好使用
null
来初始化。
由于undefined
是null
派生而来的。因此 ECMA-262 将它们定义为表面相等:
console.log(null == undefined); // true
注意!
JavaScript
中的==
会将操作数转换。
虽然很多时候,undefined
和null
傻傻分不清,但是用途不一样。永远不必显式地将变量值转换为undefined
,null
不同,任何时候,只要变量需要保存对象,而当时又没有那个变量保存,就要用null
来填充该变量,这样可以保存null
是空对象指针的语义,并进一步将其与undefined
区分开来。
Boolean
boolean
(布尔值)类型是 ECMAScript 中使用最频繁的类型之一,有两个字面值:true
和false
。
这两个布尔值不同于数值,因此
true
不等于1,false
不等于0;注意,布尔值字面量区分大小写,因此
True
和False
(其它大小混写形式)是有效的标识符,但不是布尔值。
虽然布尔值只有两个,但所有其它 ECMAScript 类型的值都有相应布尔值的等价形式。利用Boolean()
即可转换。如:
let message = 'message';
let isBoolean = Boolean(message); // true;
Boolean()
转型函数可以在任意类型的数据上调用,而且始终返回一个布尔值。什么值能转换为true
或false
取决于数据类型和实际的值;例如:
数据类型 | 转换为true的值 | 转换为false的值 |
---|---|---|
Boolean | true | false |
String | 非空字符串 | ”“ (空字符串) |
Numer | 非零数值 | 0、NaN(后面发文详谈) |
Object | 任意对象 | null |
Undefined | N/A(不存在) | undefined |
可能大部分开发者对于该转换非常陌生,但是换一个角度看下:
let message = 'Hello world';
if (message) {
console.log('Value is true');
}
在例子当中,会得到一个打印结果。这里很明显,message
会被自动转换为等价的布尔值true
。由于存在自动转换,理解流控制语句使用的什么变量非常重要。尤其是对象需要注意,对象的值太灵活多变。
Number
ECMAScript
中最有意思或者说最令开发者头疼的就是Number
了,Number
类型使用 IEEE 754 格式表示整数和浮点值(在某些语言当中也叫双精度值)。不同的数值类型相应地也有不同的数值字面量格式。
最基本的是十进制整数;如:
let intNum = 55;
还有八进制(0-8)的或者16(以16为基数)进制,对于八进制,严格模式是无效的,会导致JavaScript
引擎抛出语法错误。如:
let octalNum1 = 070; // 八进制的 56
let octalNum2 = 079; // 无效的八进制值,当成 79 处理
let octalNum3 = 08; // 无效的八进制值,当成 8 处理
ECMAScript 2015 或 ES6 中的八进制值通过前缀 0o 来表示;严格模式下,前缀 0 会被视为语法错误,如果要表示 八进制值,应该使用前缀 0o。 ------李松峰
要创建十六进制字面量,必须让真正的数值前缀 0x(区分大小写),然后是十六进制数字(0-9以及A-F)。
十六进制当中数字中的大小写均可。如:
let hexnum1 = 0xA; // 十六进制 10;
let hexnum1 = 0x1f; // 十六进制 31;
由于
JavaScript
保存数值的方式,实际中可能存在正零(+0)和负零(-0);它们在所有情况当中都认为是等同的。
浮点数
数值中必须包含小数点,而且小数点后面必须要有一个数字,比如:
let folat1 = 1.1;let folat2 = 0.3;let folat4 = .4; // 有效,但不推荐
因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为整数。
在小数点后面没有数字的情况下,数值就会变成整数。类似地,如果数值本身就是整数,只是小 数点后面跟着 0(如 1.0),那它也会被转换为整数,如:
let floatNum1 = 1.; // 小数点后面没有数字,当成整数 1 处理let floatNum2 = 10.0; // 小数点后面是零,当成整数 10 处理
对于非常大或非常小的数值,浮点值可以用科学记数法来表示。
科学记数法用于表示一个应该乘以 10 的给定次幂的数值。
ECMAScript 中科学记数法的格式要求是一个数值(整数或浮点数)后跟一个大写或小写的字母 e,再加上一个要乘的 10 的多少次幂。比如:
let floatNum = 3.125e7; // 等于 31250000
科学记数法也可以用于表示非常小的数值,例如 0.000 000 000 000 000 03。这个数值用科学记数法 可以表示为 3e-17。
默认情况下,ECMAScript 会将小数点后至少包含 6 个零的浮点值转换为科学记数(例如,0.000 000 3 会被转换为 3e7)。
浮点值的精确度最高可达 17 位小数,但在算术计算中远不如整数精确。
例如,0.1 加 0.2 得到的不 是 0.3,而是 0.300 000 000 000 000 04。
由于这种微小的舍入错误,导致很难测试特定的浮点值。比如:
if (a + b == 0.3) { // 别这么干! console.log("You got 0.3.");}
永远不要测试某个特定的浮点值!
之所以存在这种舍入错误,是因为使用了 IEEE 754 数值,这种错误并非 ECMAScript 所独有。其他使用相同格式的语言也有这个问题。 ------Matt Frisbie
值的范围
由于内存的限制,ECMAScript 并不支持表示这个世界上的所有数值。
ECMAScript 可以表示的最小 数值保存在 Number.MIN_VALUE 中,这个值在多数浏览器中是 5e-324;可以表示的最大数值保存在 Number.MAX_VALUE 中,这个值在多数浏览器中是 1.797 693 134 862 315 7e+308。
如果某个计算得到的 数值结果超出了 JavaScript
可以表示的范围,那么这个数值会被自动转换为一个特殊的 Infinity
(无 穷)值。
任何无法表示的负数以-Infinity
(负无穷大)表示,任何无法表示的正数以 Infinity
(正无穷大)表示
如果计算返回正 Infinity
或负 Infinity
,则该值将不能再进一步用于任何计算。
这是因为 Infinity
没有可用于计算的数值表示形式。
要确定一个值是不是有限大(即介于 JavaScript
能表示的 最小值和最大值之间),可以使用 isFinite()
函数,如下所示:
let result = Number.MAX_VALUE + Number.MAX_VALUE;console.log(isFinite(result)); // false
使用
Number.NEGATIVE_INFINITY 和 Number.POSITIVE_INFINITY
也可以获 取正、负Infinity
。没错,这两个属性包含的值分别就是-Infinity
和Infinity
。
NaN
有一个特殊的数值叫 NaN
,意思是“不是数值”(Not a Number
),用于表示本来要返回数值的操作失败了(而不是抛出错误)。
比如,用 0 除任意数值在其他语言中通常都会导致错误,从而中止代码执 行。但在 ECMAScript 中,0、+0 或-0 相除会返回 NaN
:
console.log(0/0); // NaNconsole.log(-0/+0); // NaN
如果分子是非 0 值,分母是有符号 0 或无符号 0,则会返回 Infinity
或-Infinity
:
console.log(5/0); // Infinityconsole.log(5/-0); // -Infinity
NaN
有几个独特的属性。首先,任何涉及 NaN
的操作始终返回NaN(如 NaN/10)
,同时,NaN
不等于包括 NaN
在内的任何值。如:
console.log(NaN == NaN); // false
为此,ECMAScript 提供了isNaN()
函数,该函数接收一个参数,可以是任意数据类型,然后判断这个参数是否“不是数值”。
把一个值传给 isNaN()
后,该函数会尝试把它转换为数值。
某些非数值的 值可以直接转换成数值,如字符串"10"或布尔值。任何不能转换为数值的值都会导致这个函数返回 true
。举例如下:
console.log(isNaN(NaN)); // trueconsole.log(isNaN(10)); // false,10 是数值console.log(isNaN("10")); // false,可以转换为数值 10console.log(isNaN("blue")); // true,不可以转换为数值console.log(isNaN(true)); // false,可以转换为数值 1
该函数返回true
,则说明和NaN
等价(不是相等),且并不能成功转化为数值。
数值转换
有 3 个函数可以将非数值转换为数值:Number()
、parseInt()
和 parseFloat()
。
Number()
是 转型函数,可用于任何数据类型。
后两个函数主要用于将字符串转换为数值。对于同样的参数,这 3 个 函数执行的操作也不同。
Number()
函数基于如下规则执行转换:
- 布尔值,
true
转换为 1,false 转换为 0。 - 数值,直接返回。
null
,返回 0。undefined
,返回NaN
。- 字符串,应用以下规则:
- 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。 因此,
Number("1")
返回 1,Number("123")
返回 123,Number("011")
返回 11(忽略前面 的零)。 - 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
- 如果字符串包含有效的十六进制格式如"
0xf
",则会转换为与该十六进制值对应的十进制整数值。 - 如果是空字符串(不包含字符),则返回 0。
- 如果字符串包含除上述情况之外的其他字符,则返回
NaN
。
- 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。 因此,
- 对象,调用
valueOf()
方法,并按照上述规则转换返回的值。如果转换结果是NaN
,则调用toString()
方法,再按照转换字符串的规则转换。
考虑到用 Number()
函数转换字符串时相对复杂且有点反常规,通常在需要得到整数时可以优先使 用 parseInt()
函数。parseInt()
函数更专注于字符串是否包含数值模式。
字符串最前面的空格会被忽略,从第一个非空格字符开始转换。
如果第一个字符不是数值字符、加号或减号,parseInt()
立即返回 NaN
。
这意味着空字符串也会返回 NaN
(这一点跟 Number()
不一样,它返回 0)。
如果第一个字符 是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。
比如, "1234blue"
会被转换为1234
,因为"blue"
会被完全忽略。类似地,"22.5"
会被转换为22
,因为小数 点不是有效的整数字符。
下面几个转换示例有助于理解上述规则:
let num1 = parseInt("1234blue"); // 1234 let num2 = parseInt(""); // NaN let num3 = parseInt("0xA"); // 10,解释为十六进制整数 let num4 = parseInt(22.5); // 22 let num5 = parseInt("70"); // 70,解释为十进制值 let num6 = parseInt("0xf"); // 15,解释为十六进制整数
事实上,如果提供了十六进制参数,那么字符串前面的"0x
"可以省掉:
let num1 = parseInt("AF", 16); // 175 let num2 = parseInt("AF"); // NaN
因为不传底数参数相当于让 parseInt()
自己决定如何解析,所以为避免解析出错,建议始终传给 它第二个参数。
parseFloat()
函数的工作方式跟 parseInt()
函数类似。但是第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。因此,"22.34.5"将转换 成 22.34。
parseFloat()
函数的另一个不同之处在于,它始终忽略字符串开头的零。
这个函数能识别前面讨 论的所有浮点格式,以及十进制格式(开头的零始终被忽略)。
十六进制数值始终会返回 0。因为 parseFloat()
只解析十进制值,因此不能指定底数。
最后,如果字符串表示整数(没有小数点或者小 数点后面只有一个零),则 parseFloat()
返回整数。如:
let num1 = parseFloat("1234blue"); // 1234,按整数解析 let num2 = parseFloat("0xA"); // 0 let num3 = parseFloat("22.5"); // 22.5 let num4 = parseFloat("22.34.5"); // 22.34 let num5 = parseFloat("0908.5"); // 908.5 let num6 = parseFloat("3.125e7"); // 31250000
String
String
(字符串)数据类型表示零或多个 16 位 Unicode
字符序列。字符串可以使用双引号(")、 单引号(')或反引号(`)标示,因此下面的代码都是合法的:
let firstName = "John";let lastName = 'Jacob';let lastName = `Jingleheimerschmidt`
不过要注意的是,以某种引号作为字符串开头,必须仍然以该种引号作为字符串结尾。 比如,下面的写法会导致语法错误:
let firstName = 'Nicholas"; // 语法错误:开头和结尾的引号必须是同一种
1. 字符字面量
字符串数据类型包含一些字符字面量,用于表示非打印字符或有其他用途的字符,如下表所示:
字面量 | 含义 |
---|---|
\n | 换行 |
\t | 制表 |
\b | 退格 |
\r | 回车 |
\f | 换页 |
\ | 反斜杠(\) |
\' | 单引号('),在字符串以单引号标示时使用,例如'He said, 'hey.'' |
" | 双引号("),在字符串以双引号标示时使用,例如"He said, "hey."" |
` | 反引号(),在字符串以反引号标示时使用,例如 He said, `hey.`` |
\xnn | 以十六进制编码 nn 表示的字符(其中 n 是十六进制数字 0~F),例如\x41 等于"A" |
\unnnn | 以十六进制编码 nnnn 表示的 Unicode 字符(其中 n 是十六进制数字 0~F),例如\u03a3 等于希腊字 符"Σ" |
这些字符字面量可以出现在字符串中的任意位置,且可以作为单个字符被解释:
let text = "This is the letter sigma: \u03a3.";
在这个例子中,即使包含 6 个字符长的转义序列,变量 text 仍然是 28 个字符长。因为转义序列表 示一个字符,所以只算一个字符。
字符串的长度可以通过其 length 属性获取:
console.log(text.length); // 28
这个属性返回字符串中 16 位字符的个数。
字符串的特点
字符串属于原始值,是不可改变的。变量的值只能赋新值改变,原值则被销毁。
ECMAScript 中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。要修改
某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量,如:
let lang = "Java";lang = lang + "Script";
这里是赋新的值,而Java
这个字符串则被销毁掉。
转化为字符串
有两种方式把一个值转换为字符串。
首先是使用几乎所有值都有的 toString()
方法。这个方法唯 一的用途就是返回当前值的字符串等价物。如:
let age = 11;let ageAsString = age.toString(); // 字符串"11"let found = true;let foundAsString = found.toString(); // 字符串"true"
toString()
方法可见于数值、布尔值、对象和字符串值。(没错,字符串值也有toString()
方法, 该方法只是简单地返回自身的一个副本。)null
和 undefined
值没有 toString()
方法。
多数情况下,toString()
不接收任何参数。不过,在对数值调用这个方法时,toString()
可以接收一个底数参数,即以什么底数来输出数值的字符串表示。
默认情况下,toString()
返回数值的十进制字符串表示。
而通过传入参数,可以得到数值的二进制、八进制、十六进制,或者其他任何有效基 数的字符串表示,如:
let num = 10;console.log(num.toString()); // "10"console.log(num.toString(2)); // "1010"console.log(num.toString(8)); // "12"console.log(num.toString(10)); // "10"console.log(num.toString(16)); // "a"
如果你不确定一个值是不是 null
或 undefined
,可以使用 String()
转型函数,它始终会返回表 示相应类型值的字符串。
String()
函数遵循如下规则:
- 如果值有
toString()
方法,则调用该方法(不传参数)并返回结果。 - 如果值是
null
,返回"null
"。 - 如果值是
undefined
,返回"undefined
"。
let value1 = 10;let value2 = true;let value3 = null;let value4;console.log(String(value1)); // "10"console.log(String(value2)); // "true"console.log(String(value3)); // "null"console.log(String(value4)); // "undefined"
这里展示了将 4 个值转换为字符串的情况:一个数值、一个布尔值、一个 null
和一个 undefined
。
数值和布尔值的转换结果与调用 toString()
相同。因为 null
和 undefined
没有 toString()
方法, 所以 String()
方法就直接返回了这两个值的字面量文本。
用加号操作符给一个值加上一个空字符串""也可以将其转换为字符串。
模板字面量
ECMAScript 6 新增了使用模板字面量定义字符串的能力。
与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串:
let myMultiLineString = 'first line\nsecond line';let myMultiLineTemplateLiteral = `first linesecond line`;console.log(myMultiLineString);// first line// second line"console.log(myMultiLineTemplateLiteral);// first line
由于模板字面量会保持反引号内部的空格,因此在使用时要格外注意。格式正确的模板字符串看起 来可能会缩进不当: // 这个模板字面量在换行符之后有 25 个空格符:
let myTemplateLiteral = `first line second line`; console.log(myTemplateLiteral.length); // 47
字符串插值
模板字面量最常用的一个特性是支持字符串插值,也就是可以在一个连续定义中插入一个或多个 值。
技术上讲,模板字面量不是字符串,而是一种特殊的 JavaScript
句法表达式,只不过求值后得到的是字符串。
模板字面量在定义时立即求值并转换为字符串实例,任何插入的变量也会从它们最接近的作用域中取值。
let value = 5;let exponent = 'second';// 以前,字符串插值是这样实现的:let interpolatedString = value + ' to the ' + exponent + ' power is ' + (value * value);// 现在,可以用模板字面量这样实现:let interpolatedTemplateLiteral = `${ value } to the ${ exponent } power is ${ value * value }`;console.log(interpolatedString); // 5 to the second power is 25console.log(interpolatedTemplateLiteral); // 5 to the second power is 25
所有插入的值都会使用 toString()
强制转型为字符串,而且任何 JavaScript
表达式都可以用于插值。嵌套的模板字符串无须转义:
console.log(`Hello, ${ `World` }!`); // Hello, World!将表达式转换为字符串时会调用 toString():let foo = { toString: () => 'World' };console.log(`Hello, ${ foo }!`); // Hello, World!在插值表达式中可以调用函数和方法:function capitalize(word) { return `${ word[0].toUpperCase() }${ word.slice(1) }`;}console.log(`${ capitalize('hello') }, ${ capitalize('world') }!`); // Hello, World!此外,模板也可以插入自己之前的值:let value = '';function append() { value = `${value}abc` console.log(value);}append(); // abcappend(); // abcabcappend(); // abcabcabc
模板字面量标签函数
模板字面量也支持定义标签函数(tag function
),而通过标签函数可以自定义插值行为。
标签函数会接收被插值记号分隔后的模板和对每个表达式求值的结果。
标签函数本身是一个常规函数,通过前缀到模板字面量来应用自定义行为。
标签函数接收到的参数依次是原始字符串数组和对每个表达式求值的结果。
这个函数的返回值是对模板字面量求值得到的字符串:
let a = 6;let b = 9;function simpleTag(strings, aValExpression, bValExpression, sumExpression) { console.log(strings); console.log(aValExpression); console.log(bValExpression); console.log(sumExpression); return 'foobar';}let untaggedResult = `${ a } + ${ b } = ${ a + b }`;let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;// ["", " + ", " = ", ""]// 6// 9// 15console.log(untaggedResult); // "6 + 9 = 15"console.log(taggedResult); // "foobar"
原始字符串
使用模板字面量也可以直接获取原始的模板字面量内容(如换行符或 Unicode
字符),而不是被转 换后的字符表示。
为此,可以使用默认的 String.raw
标签函数:
// Unicode 示例// \u00A9 是版权符号console.log(`\u00A9`); // ©console.log(String.raw`\u00A9`); // \u00A9// 换行符示例console.log(`first line\nsecond line`);// first line// second lineconsole.log(String.raw`first line\nsecond line`); // "first line\nsecond line"// 对实际的换行符来说是不行的// 它们不会被转换成转义序列的形式console.log(`first linesecond line`);// first line// second lineconsole.log(String.raw`first linesecond line`);// first line// second line
另外,也可以通过标签函数的第一个参数,即字符串数组的.raw
属性取得每个字符串的原始内容:
function printRaw(strings) { console.log('Actual characters:'); for (const string of strings) { console.log(string); } console.log('Escaped characters;'); for (const rawString of strings.raw) { console.log(rawString); }}printRaw`\u00A9${ 'and' }\n`;// Actual characters:// ©//(换行符)// Escaped characters:// \u00A9// \n
Symbol
Symbol
(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。
符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险,这也成为它目前最主要的功能之一。
尽管听起来跟私有属性有点类似,但符号并不是为了提供私有属性的行为才增加的(尤其是因为 Object API 提供了方法,可以更方便地发现符号属性)。
相反,符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。
Symbol(1) === Symbol(1); // false
作为属性使用,可以保证不会覆盖原有属性、被属性覆盖。
Q.E.D.