前言
ES6有6种基本数据类型,分别为:Undefined、Null、Boolean、Number、String、Symbol
1. typeof操作符
typeof操作符用来确定任意变量的数据类型(基本数据类型+引用数据类型),有7种返回值,均为String类型:undefined、boolean、string、number、object(表示值为对象或者null)、function、symbol。
typeof是一个操作符而不是函数,所以不需要参数(但可以使用参数)
console.log(typeof age); // undefined
console.log(typeof (1 == 1)); // boolean
console.log(typeof '123'); // string
console.log(typeof 123); // number
console.log(typeof null); // object
console.log(typeof (function () { })); // function
console.log(typeof Symbol()); // symbol
2. Undefined
Undefined类型只有一个值,就是特殊值undefined。当使用var或let声明了变量但没有初始化时,相当于给变量赋予了undefined值。
let message;
console.log(typeof message); // undefined
此外对于未声明的变量,只能执行一个有用的操作,就是调用typeof,返回的结果也是undefined。所以建议在声明变量的时候同时进行初始化,这样,当typeof返回undefined时,你就会知道那是因为给定的变量尚未声明,而不是声明了没初始化。
3. Null
Null类型同样只有一个值,就是特殊值null。null值表示一个空对象指针。
undefined值是由null值派生而来的,因此二者表面上相等,所以使用等于操作符(==)比较null和undefined始终返回true。但使用全等操作符(===)比较null和undefined始终返回false,因为二者不是一种数据类型。
console.log(undefined == null); // true
console.log(undefined === null); // false
即使二者有关系,但用途不同。任何时候,只要变量要保存对象,但当时又没有对象可保存,就用null来填充该变量。既保证null是空对象指针的语义,又可以和undefined区分开来。
4. Boolean
Boolean(布尔)类型有两个字面值:true和false。虽然布尔值只有两个,但其他所有数据类型的值都有相应布尔值的等价形式。可以调用Boolean()转型函数将某个类型的值转化为布尔值。下表总结了不同类型与布尔值之间的转换规则:
| 数据类型 | 转换为true的值 | 转换为false的值 |
|---|---|---|
| Boolean | true | false |
| String | 非空字符串 | 空字符串 |
| Number | 非零数值 | 0、NaN |
| Object | 任意对象 | null |
| Undefined | N/A(不存在) | undefined |
在流控制语句中,都会自动执行其他类型值到布尔值的转换。下面的例子中,执行到if语句时,message会自动转换为等价的布尔值true,从而进入if分支,输出"Value is true"。
let message = "Hello,world";
if (message) {
console.log("Value is true");
}
5. Number
Number类型使用IEEE 754格式表示整数和浮点数(双精度值)
5.1 整数
表示二进制数前缀为0b;八进制前缀为0;十进制直接写;十六进制前缀为0x
let binNum = 0b1001; // 二进制的9
let intNum = 55; // 十进制的55
let octalNum = 070; // 八进制的56
let hexNum = 0xA; // 十六机制的10
5.2 浮点值
因为存储浮点值使用的内存空间是存储整数值的两倍,所以ECMAScript总是想方设法把值转化为整数。在小数点后没有数字的情况下,数值会变成整数。类似地,如果数值本身就是整数,只是小数点后面均为0,那也会转化为整数。
let floatNum1 = 1.;
let floatNum2 = 10.0;
console.log(floatNum1); // 1
console.log(floatNum2); // 10
对于非常大或非常小的数值,可以用科学计数法表示,格式为一个数值(整数或浮点数)+e(E)+10的多少次幂
let floatNum = 3.14e7
浮点值的精确度最高可达17位小数,但在计算中容易存在微小的舍入误差,导致很难测试待定的浮点值,如下所示,0.1加0.2得到的并不是0.3,而是0.30000000000000004。因此永远不要测试某个特定的浮点值
let a = 0.1,
b = 0.2;
if (a + b == 0.3) {
console.log("You got 0.3")
} else {
console.log(a + b); // 0.30000000000000004
}
5.3 值的范围
ECMAScript将最小数值保存在Number.MIN_VALUE中,最大数值保存在Number.MAX_VALUE中。如果某个计算得到的数值结果超出了可以表示的范围,那么这个数值会自动转换为一个特殊的Infinity值
5.4 NaN
Number有一个特殊值叫NaN,意为”不是数值“(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。
console.log(0 / 0); // NaN
console.log(-0 / +0); // NaN
console.log(5 / 0); // Infinity
console.log(5 / -0); // -Infinity
console.log("hello" + 2); // hello2
console.log("hello" - 2); // NaN
NaN两大特性:任何涉及NaN的操作始终返回NaN;NaN不等于包括NaN在内的任何值
要判断一个值是否”不是数值“,ECMAScript提供了isNaN()函数,该函数接收一个参数,可以是任意数据类型。把一个值传给isNaN()后,该函数会尝试把它转为数值,任何不能转换为数值的值都会导致这个函数返回true。如下所示:
console.log(isNaN(NaN)) // true
console.log(isNaN(10)) // false
console.log(isNaN("10")) // false,可以转换为数值10
console.log(isNaN("blue")) // true
console.log(isNaN(true)) // false,可以转换为数值1
5.5 数值转换
有3个函数可以将非数值转换为数值:Number()、parseInt()、parseFloat()。Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。
Number转型函数
Number()转换规则如下:
| 数值 | 规则 |
|---|---|
| 布尔值 | true转换为1,false转换为0 |
| 数值 | 直接返回 |
| null | 返回0 |
| undefined | 返回NaN |
| 字符串 | 如果包含数值则返回对应的数值;如果是空字符串,返回0;其余返回NaN |
| 对象 | 根据valueOf()方法或toString()方法返回值进行转换,valueOf()方法优先 |
parseInt函数
考虑到用Number()函数转换字符串相对复杂且有点反常规,通常在需要得到整数时可以优先使用parseInt()函数。更专注于将字符串转换为整数形式,返回值要么为整数要么为NaN。转换规则如下:
- 字符串前面的空格(某些字符字面量也相当于一串空格,例如
\n、\t、\r、\f)会被忽略,从第一个非空字符开始转换; - 如果第一个字符是数值字符、加号或减号,则会继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。比如,"22.5"会被转换为22,因为小数点不是有效的数值字符,并不会四舍五入。
- 能够识别不同的整数格式。如果字符串以"0x"开头,就会被解释为十六进制整数,以此类推。不同的数值格式容易混淆,parseInt()也接收第二个参数,用于指定底数(进制数),如果提供底数,字符串前面的前缀可以去掉。为避免解析出错,建议始终传第二个参数。
console.log(parseInt('1234blue')); // 1234
console.log(parseInt('')) // NaN
console.log(parseInt('0xAF')); // 175
console.log(parseInt('AF', 16)); // 175
parseFloat函数
parseFloat()函数的工作方式与parseInt()类似,但也有不同之处,转换规则如下:
- 字符串前面的空格会被忽略,从第一个非空字符开始转换;
- 如果第一个字符是数值字符、加号或减号、小数点,则会继续检测每个字符,直到字符串末尾,或者碰到第二个小数点或非数值字符。比如,“22.34.5”将转化为"22.34";
- 能够识别所有浮点格式以及十进制格式,不能识别其他进制数,自然也不能指定底数,只有一个参数;
- 数字前的“0”会被忽略。比如,“00”将转化为“0”,“003.14”将转化为“3.14”;
- 如果字符串表示整数,则返回整数;
console.log(parseFloat('1234.0blue')) // 1234
console.log(parseFloat('..')) //NaN
console.log(parseFloat('1.23e4')) // 12300
console.log(parseFloat(' 000987.5')) // 987.5
console.log(parseFloat('00 987.5')) // 0
6. String
String表示零或多个16位Unicode字符序列。字符串可以使用双引号、单引号、反引号表示。
6.1 字符串的特点
ECMAScript中的字符串是不可变的(immutable),一旦创建,值不能更改。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量,如下所示:
let str = "Java";
str += "Script";
整个过程分为三步:创建、复制、销毁。首先会分配一个足够容纳10个字符的空间,然后填充上“Java”和“Script”,最后将原始字符串“Java”和字符串“Script”销毁,因为这两个字符串都没有用了。这也是早期一些浏览器在拼接字符串时非常慢的原因。
下面我们来做个实验验证一下,并给出一种数组拼接的优化方案进行对比:
首先我们在Js脚本中测试:
const count = 1e7;
console.time("str1");
let str1 = "Java";
for (let i = 0; i < count; i++) {
str1 += "Script";
}
console.timeEnd("str1");
console.time("str2");
let str2 = ["Java"];
for (let i = 0; i < count; i++) {
str2.push("Script");
}
str2 = str2.join("");
console.timeEnd("str2");
执行结果:
str1: 865.513ms
str2: 366.512ms
可以看到采用数组和join方法的效率相对使用"+"号进行字符串拼接快了不少。那么我们看看在chrome浏览器中表现如何:
str1: 527.275146484375 ms
str2: 498.763916015625 ms
可以看到现代浏览器在后台都有针对性地进行了优化,二者差别并不大,而且使用"+"号更加语义化。
6.2 转换为字符串的三种方法
toString方法
几乎所有值都有toString()方法(null和undefined值没有)。多数情况下,toString()不接收任何参数。不过在对数值调用这个方法时,toString()可以接收一个底数参数,默认情况下是十进制。
let age = 18;
let num = null;
console.log(age.toString()); // 18
console.log(age.toString(16)); // 12
console.log(num.toString()) // TypeError: Cannot read properties of null (reading 'toString')
String转型函数
如果你不确定一个值是不是null或undefined,可以使用String()转型函数。如果值为null,返回"null";如果值为undefined,返回“undefined”;其余的则调用toString()方法。
加号操作符
用加号操作符给一个值加上一个空字符串“”也可以将其转换为字符串。效果等同于String()转型函数
6.3 模板字面量
ES6新增了使用模板字面量定义字符串的能力。与使用单引号或双引号不同,模板字面量可以保留换行字符,可以跨行定义字符串。其主要应用场景有如下几个方面:
定义模板
let pageHTML = `
<div>
<a href="#">
<span>Jake</span>
</a>
</div>`;
定义带有插值的字符串
技术上讲,模板字面量并不是字符串,而是一种特殊的JavaScript句法表达式,只不过求值后得到的是字符串。模板字面量在定义时立即求值并转换为字符串实例,任何插入的变量也会从它们最近作用域中取值。字符串插值通过在${}中使用一个JavaScript表达式实现:
let value = 5;
let exponent = 'second';
// 5 to the second power is 25
console.log(`${value} to the ${exponent} power is ${value * value}`);
所有插入的值都会使用String()强制转型为字符串。还可以嵌套使用,因为模板字面量本身也是一种表达式。
console.log(`Hello, ${`world`}`); // Hello,world
模板字面量也支持定义标签函数,通过标签函数可以自定义插值行为。标签函数会接收被插值记号分隔后的模板数组和对每个表达式求值的结果。标签函数本身就是一个常规函数,通过前缀到模板字面量来应用自定义行为。
function zipTag(strings, ...expressions) {
return strings[0] +
expressions.map((e, i) => `${e}${strings[i + 1]}`).join("");
}
let a = 6,
b = 9;
let untaggedResult = `${a} + ${b} = ${a + b}`;
let taggedResult = zipTag`${a} + ${b} = ${a + b}`;
console.log(untaggedResult); // 6 + 9 = 15
console.log(taggedResult); // 6 + 9 = 15
使用模板字面量也可以直接获取原始的模板字面量内容,而不是被转换后的字符表示。可以使用String.raw标签函数
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9
6.4 常用字符串方法
可以参考这篇JavaScript 28个常用字符串方法及使用技巧 - 掘金 (juejin.cn)
7. Symbol
Symbol(符号)是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
7.1 基本用法
符号使用Symbol()函数初始化,也可以传入一个字符串参数用作对符号的描述,但是这个字符串参数与符号定义定义或标识完全无关:
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false
符号没有字面量语法。只要创建Symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性。此外,Symbol()函数不能与new关键字一起作为构造函数使用,这样做是为了避免创建符号包装对象:
let myString = new String('JavaScript');
console.log(typeof myString); // object
console.log(myString.valueOf()); // JavaScript
let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor
7.2 全局符号注册表
如果运行时的不同部分需要共享和重用符号实例,可以使用Symbol.for(key:String)方法:
let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // symbol
Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串键时,会检查全局运行时注册表,发现不存在对应的符号,于是会生成一个新符号实例并添加到注册表中。后续使用相同字符串键同样会检查注册表,发现已存在与该字符串对应的符号,返回该实例,进行重用。
let fooGlobalSymbol = Symbol.for('foo');
let otherFooGlobalSymbol = Symbol.for('foo');
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true
可以使用Symbol.keyFor(sym:Symbol)来查询全局注册表。如果查询的不是全局符号,则返回undefined:
let fooGlobalSymbol = Symbol.for("foo");
let fooSymbol = Symbol("foo");
console.log(Symbol.keyFor(fooGlobalSymbol)); // foo
console.log(Symbol.keyFor(fooSymbol)); // undefined
7.3 使用符号作为属性
对象字面量只能在计算属性语法中使用符号作为属性:
let s1 = Symbol("foo");
// 方法1
let o1 = {
[s1]: 'foo val'
};
// 方法2
let o2 = {};
o2[s1] = 'foo val';
console.log(o1); // { [Symbol(foo)]: 'foo val' }
console.log(o2); // { [Symbol(foo)]: 'foo val' }
还可以使用Object.defineProperty()和Object.defineProperties()来定义属性:
let s1 = Symbol("foo"),
s2 = Symbol("bar"),
s3 = Symbol("baz");
let o = {};
Object.defineProperty(o, s1, { value: 'foo val', enumerable: true });
console.log(o); // { [Symbol(foo)]: 'foo val' }
Object.defineProperties(o, {
[s2]: { value: 'bar val', enumerable: true },
[s3]: { value: 'baz val', enumerable: true }
})
console.log(o); // {[Symbol(foo)]: 'foo val', [Symbol(bar)]: 'bar val', [Symbol(baz)]: 'baz val'}
Object.getOwnPropertyNames()返回对象实例的常规属性数组;
Object.getOwnPropertySymbols()返回对象实例的符号属性数组,这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回两种类型的键:
let s1 = Symbol("foo"),
s2 = Symbol("bar");
let o = {
[s1]: 'foo val',
[s2]: 'bar val',
baz: 'baz val',
qux: 'qux val'
};
console.log(Object.getOwnPropertyNames(o));
// [ 'baz', 'qux' ]
console.log(Object.getOwnPropertySymbols(o));
// [ Symbol(foo), Symbol(bar) ]
console.log(Object.getOwnPropertyDescriptors(o));
// {baz:{...}, qux:{...}, [Symbol(foo)]:{...}, [Symbol(bar)]:{...}}
console.log(Reflect.ownKeys(o));
// [ 'baz', 'qux', Symbol(foo), Symbol(bar) ]
因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号并不会丢失。但是如果没有显示地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:
let o = {
[Symbol('foo')]: 'foo val',
[Symbol('bar')]: 'bar val'
};
console.log(o); // { [Symbol(foo)]: 'foo val', [Symbol(bar)]: 'bar val' }
let barSymbol = Object.getOwnPropertySymbols(o).find((sym) => sym.toString().match(/bar/));
console.log(barSymbol); // Symbol(bar)
当然可以使用全局符号,这实际上也是一种遍历:
let o = {
[Symbol.for('foo')]: 'foo val',
[Symbol.for('bar')]: 'bar val'
};
let barSymbol = Symbol.for('bar');
console.log(o); // { [Symbol(foo)]: 'foo val', [Symbol(bar)]: 'bar val' }
console.log(o[barSymbol]); // bar val
总结
- ES6有6种基本数据类型,分别为:
Undefined、Null、Boolean、Number、String、Symbol; - typeof操作符用来确定任意变量的数据类型,有7种返回值:
undefined、boolean、string、number、object(表示值为对象或者null)、function、symbol; - undefined与null表面上相等,但不全等;
- 在流控制语句中,都会自动执行其他类型值到布尔值的转换,通过调用
Boolean()转型函数; - Number类型包括:
整数、浮点数、NaN;存储浮点值使用的内存空间是存储整数值的两倍,ES6总是会想方设法把浮点数转化为整数; - NaN的两大特性:
任何涉及NaN的操作始终返回NaN、NaN不等于包括NaN在内的任何值;可以通过isNaN()函数判断一个值是否“不是数值”; - 将非数值转换为数值的方法:
Number()、parseInt()、parseFloat();后两个专注于将字符串值转换为整数或浮点数; - ECMAScript中的字符串是
不可变的(immutable); - 其他类型转换为字符串的方法:
toString()、String()、拼接一个空字符串; - ES6新增了模板字面量,可以跨行定义字符串,可以使用
${}传入一个JavaScript表达式进行插值; - 标签函数是一个常规函数,接收被插值符号分隔后的模板字串数组以及对每个表达式的求值结果,通过前缀到模板字面量来应用自定义行为。
- Symbol实例是唯一的、不可变的;
- 使用
Symbol(description?:string)创建实例,也可以使用Symbol.for(key:string)来创建注册全局符号; - 如果使用全局符号,可以使用
Symbol.keyFor(sym:Symbol)来查询对应的字符串键; - 对象字面量只能在计算属性语法中使用符号作为属性。
参考文献
[1] Matt Frisbie. JavaScript高级程序设计(第4版)[M]. 北京:人民邮电出版社,2020:30~47
[2] JavaScript 28个常用字符串方法及使用技巧 - 掘金 (juejin.cn)