数据类型
JavaScript 数据类型有 7 种。
6 种简单类型:Undefined、Null、Boolean、Number、String、Symbol
1 种复杂类型:Object
BigInt 和 function 比较有争议,有些地方会把它们当作新的数据类型,也有些地方认为它们只是特殊的对象。
typeof 操作符
typeof 操作符可以返回变量的数据类型。它能够返回 7 种:
- "undefined"表示 undefined;
- "boolean"表示 boolean;
- "string"表示 string;
- "number"表示 number;
- "symbol"表示 symbol;
- "object"表示值 object 或 null;
- "function"表示值为函数;
null 被识别为 object 是因为它被认为是一个空对象。
Undefined
Undefined 类型只有一个值,就是特殊值 undefined。增加它的目的是正式明确 Null 和未初始化变量的区别。
当 var 或 let 声明变量却没有给初始值,它的值就是 undefined。
let message;
console.log(message); // undefined
没初始化的变量也会返回 undefined。
// age 没有被声明
console.log(age); // 也是undefined,只是会报错
但两个 undefined 是不一样的。
用它作为判断条件时的情况:
let message;
if (message) {
// 这个快不会执行
}
if (!message) {
// 这个快会执行
}
// 没有声明age
if (age) {
// 直接报错
}
Null
这个词是发音是[nʌl]。
null 类型只有一个值 null,表示空对象指针。
声明一个变量的时候最好进行初始化。如果实在不知道一开始应该赋什么值,null 会是很好的选择。
ECMA-262 认为 null 和 defined 表面上相等
因为 undefined 由 null 派生而来。所以用==会返回 true。
console.log(null == undefined); // true
Null 是假值
所以作为判断条件和 undefined 一样。
let message = null;
if (message) {
// 不会执行
}
if (!message) {
// 会执行
}
Boolean
Boolean(布尔值)有两个字面值:true 和 false。这两个布尔值不同于数字,true 不等于 1,false 不等于 0.
true 和 false 必须是小写。True 和 False 是另外两个标识符。
所有 ECMAScript 的值都有布尔值的等价形式。使用 Boolean()可以转变。
let messageBoolean = Boolean("How are you!");
不同类型值转变成布尔值的规则:
数据类型 | 转换为 true | 转换为 false |
---|---|---|
String | 非空字符串 | 空字符串 |
Number | 非零数值(包括无穷值) | 0、NaN |
Object | 任意对象 | null |
Undefined | N/A(不存在) | undefined |
当我们使用 if (message) 判断时,判断条件 message 会被转换成对应的布尔值。所以记住这张表很重要。
Number
Number 类型使用 IEEE 754 格式表示整数和浮点数。
十进制整数
最基本的是十进制整数。
八进制字面量
八进制的第一个数字必须是 0,然后是相应的八进制数字(数值 0-7)。如果数字超出范围,就会将其识别成十进制。
let num1 = 070; // 八进制的56
let num2 = 079; // 当成十进制的79
八进制字面量在严格模式下无效,会导致 JavaScript 引擎报错。
十六进制字面量
十六进制的前两位必须是 0x,然后是十六进制数字(数值 0-9 和 A-F)。字母大小写都可。
let num = 0xa; // 十六进制的10
浮点值
定义浮点值必须包含小数点,且小数点后必须有至少一个数字。
let num = 1.1;
let num2 = 0.1; // 有效,但不推荐
存储浮点值使用的内存空间是整数值的两倍,所以 ECMAScript 总是想把值转换为整数。
在小数点后没有数字或只有 0 的情况下,数值就会变成整数。
浮点值科学计数法
对于非常大或非常小的值,浮点值可以用科学计数法表示。
let num = 3.125e7; // 31250000
用大写或小写的 e,加上一个数值(比如 7),表示 10 的 7 次方。
10 的负多少次方就加上“-”号:
let num = 3125e-3; // 3.125
ECMAScript 会默认将小数点后至少 6 个 0 的浮点数转换为科学计数法。(例如,0.000 000 3) 会被转换为 3e-7。
- 浮点精确度为 17 位小数
浮点计算中可能出错
比如 0.1 加 0.2 等于 0.300 000 000 000 000 04 而不是 0.3。
要特别小心这种特殊情况。
会出这种问题,是因为 IEEE 754。使用这个格式的语言都会有这个问题。
Number 值的范围
由于内存的限制,ECMAScript 的数值存在上限和下限。
最小数值保存在 Number.MIN_VALUE,这个值在大多数浏览器中是 5e-324。
最大数值保存在 Number.MAX_VALUE,这个值在大多数浏览器中是 1.797 693 134 862 315 7e+30。
当数值超过范围时,太大的会被转换成一个特殊的 Infinity(无穷值),太小的就是-Infinity(负无穷值)。Infinity 和-Infinity 不能被计算,他们没有可用于计算的数值表示形式。
使用 isFinite(),可以判断一个数是不是无限。
let num = Number.MAX_VALUE;
console.log(isFinite(num)); //true
用 Number.NEGATIVE_INFINITY 和 Number.POSITIVE_INFINITY 也可以获 取正、负 Infinity。这两个属性包含的值分别就是-Infinity 和 Infinity。
除法中,如果分子是非 0 值,分母是 0 或-0,会返回 Infinity 或-Infinity:
console.log(5 / 0); // Infinity
console.log(5 / -0); // -Infinity
NaN
它是一个特殊的 Number,typeof 返回的也是 number。但他表达的意思是“不是数值”。
它出现在本来要返回数值的操作失败了,不抛出错误而返回的值。
比如 0 除任何数都会出错,这样就会返回 NaN。
console.log(0 / 0); // NaN
在 ECMAScript 中,除了 0 以外,+0 和-0 除任意数也是 NaN。
NaN 的特性
- 任何涉及 NaN 的操作,始终返回 NaN。(如 NaN/10)
- NaN 不等于任何值,包括它自己。
console.log(NaN == NaN); // false
isNaN()
isNaN()可以判断参数是否“不是数值”。但存在一个问题,它会把任何类型的数据都先转换成数值(遵照 Number()函数的规则),所以不能转换的也会识别成 NaN,从而返回 true。
console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false,10 是数值
console.log(isNaN("10")); // false,可以转换为数值10
console.log(isNaN("blue")); // true,不可以转换为数值
console.log(isNaN(true)); // false,可以转换为数值1
数值转换
Number()
Number()函数首字母 N 必须大写。它首先判断参数类型,针对不同类型基于如下规则进行转换:
-
undefined:返回 NaN
-
Null:返回 0
-
布尔值:true 为 1,false 为 0
-
数值:直接返回
-
字符串:
空字符串(不包含字符)返回 0。
有效的整数字符,包括带加、减号的情况,转换为十进制数值。
有效的浮点值格式如"1.1",转换为相应的浮点值。
有效的十六进制格式如"0xf",转换为对应的十进制整数值。
如果字符串包含除上述情况之外的其他字符,则返回 NaN。
-
对象:调用 valueOf()方法,并按照上述规则转换返回的值。如果转换结果是 NaN,则调用 toString()方法,再按照转换字符串的规则转换。
let num1 = Number("Hello world!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
parseInt()
parseInt()的规则比较简单:
-
忽略空格一直到第一个非空格字符。如果不是数值、+、-,则返回 NaN。
-
遇到特殊字符后停止转换并返回结果。
-
如果字符串以"0x"开头,解释为十六进制整数。
-
如果字符串以"0"开头,且紧跟着数值字符,在非严格模式下会被某些实现解释为八进制整数。
let num1 = parseInt(""); // NaN
let num2 = parseInt("1234blue"); // 1234
let num3 = parseInt("A"); // NaN
let num4 = parseInt("1.1"); // 1
let num5 = parseInt("0xA"); // 10,解释为十六进制整数
let num6 = parseInt(22.5); // 22
let num7 = parseInt("70"); // 70,解释为十进制值
let num8 = parseInt("0xf"); // 15,解释为十六进制整数
let num9 = parseInt(true); //NaN,不能识别布尔值
所以它只能得到整数,可以用它取整。
为了防止混淆,parseInt()还可以传入第二个参数,用于指定底数。最后得到的都是十进制数。
let num1 = parseInt("0xAF", 16); // 175
let num2 = parseInt("AF", 16); // 175 ,传入底数就可以省略0x
let num3 = parseInt("AF"); // NaN,不传那肯定要报错
let num4 = parseInt("10", 2); // 2,按二进制解析
let num5 = parseInt("10", 8); // 8,按八进制解析
let num6 = parseInt("10", 10); // 10,按十进制解析
let num7 = parseInt("10", 16); // 16,按十六进制解析
推荐始终传入第二个参数
parseFloat()
parseFloat()也从头开始检测,并在检测到无效字符后停止。
但是它允许小数点出现一次。也就是它可以返回浮点数。并且它只解析十进制数。所以十六进制因为 0 开头,始终得到 0。它也没有第二个参数。
let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("a"); //NaN
let num4 = parseFloat("22.5"); // 22.5
let num5 = parseFloat("22.34.5"); // 22.34
let num6 = parseFloat("0908.5"); // 908.5
let num7 = parseFloat("3.125e7"); // 31250000
let num8 = parseInt(true); //NaN,不能识别布尔值
String
字符串表示零或多个 16 位 Unicode 字符序列。
可以用双引号、单引号、反引号标识,在某些语言中,使用不同的引号有不同的效果。但是对 ECMAScript 来说,都是一样的:
let name1 = "Mike";
let name2 = "Mike";
let name3 = `Mike`;
字符串一旦创建就不可修改,这里的不可修改并不是说不能拼接:
let lang = "Java";
lang = lang + "Script";
而是这个过程在内存中,首先会创建一个新的 10 字符空间容纳“JavaScript”,然后销毁目前的“Java”和“Script”,然后将值“JavaScript”给 lang。所以早期拼接字符串非常慢。现在的浏览器都有针对性地解决了这个问题。
字符字面量——可以被识别的特殊字符
字面量 | 含义 |
---|---|
\n | 换行 |
\t | 制表 |
\b | 退格 |
\r | 回车 |
\f | 换页 |
\\ | 反斜杠| |
\' | 单引号标识的字符串内部使用,如'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."; 的长度是 28,即使包含 6 长度的转义序列。
模板字面量
模板字面量要使用反引号“`”。
按格式读取字符串
模板字面量保留换行字符,会识别跨行的赋值。
let myMultiLineString = "first line\nsecond line";
console.log(myMultiLineString);
// first line
// second line"
let myMultiLineTemplateLiteral = `first line
second line`;
console.log(myMultiLineTemplateLiteral);
// first line
// second line
// 两种写法等价
console.log(myMultiLineString === myMultiLinetemplateLiteral); // true
使用模板字面量要注意缩进的问题:
let myTemplateLiteral = `first line
second line`;
console.log(myTemplateLiteral.length);
// output:47。因为第一行换行符之后有25 个空格符
let secondTemplateLiteral = `
first line
second line`;
console.log(secondTemplateLiteral[0] === "\n");
// output:true。因为这个模板字面量以一个换行符开头
字符串插值
模板字面量支持字符串插值。即可以在一个字符串中再插入其他字符串,也被称为字符串格式化。
使用它需要按照${}的格式,所有插入值都会被 toString()强制转换成字符串。:
let value = 5;
let exponent = "second";
let interpolatedTemplateLiteral = `${value} to the ${exponent} power is ${
value * value
}`;
可以插入函数
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(); // abc
append(); // abcabc
append(); // 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
// 15
console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "foobar"
因为表达式参数的数量是可变的,所以通常应该使用剩余操作符(rest operator)将它们收集到一个 数组中:
let a = 6;
let b = 9;
function simpleTag(strings, ...expressions) {
console.log(strings);
for (const expression of expressions) {
console.log(expression);
}
return "foobar";
}
let taggedResult = simpleTag`${a} + ${b} = ${a + b}`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(taggedResult); // "foobar"
对于有 n 个插值的模板字面量,传给标签函数的表达式参数的个数始终是 n,而传给标签函数的第 一个参数所包含的字符串个数则始终是 n+1。因此,如果你想把这些字符串和对表达式求值的结果拼接 起来作为默认返回的字符串,可以这样做:
let a = 6;
let b = 9;
function zipTag(strings, ...expressions) {
return (
strings[0] + expressions.map((e, i) => `${e}${strings[i + 1]}`).join("")
);
}
let taggedResult = zipTag`${a} + ${b} = ${a + b}`;
console.log(taggedResult); // "6 + 9 = 15"
原始字符串
对于一些特殊值,如果我们不希望他们被识别,可以使用 String.raw。
// Unicode 示例
// \u00A9 是版权符号
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9
// 换行符示例
console.log(`first line\nsecond line`);
// first line
// second line
console.log(String.raw`first line\nsecond line`); // "first line\nsecond line"
// 对实际的换行符来说是不行的
// 它们不会被转换成转义序列的形式
console.log(String.raw`first line
second line`);
// first line
// second line
也可以通过标签函数的第一个参数,即字符串数组的.raw 属性取得每个字符串的原始内容:
function printRaw(strings) {
for (const string of strings) {
console.log(string);
}
for (const rawString of strings.raw) {
console.log(rawString);
}
}
printRaw`\u00A9${"and"}\n`;
// ©
//(换行符)
// \u00A9
// \n
转换为字符串
.toString()
null 和 undefined 不能使用这个方法。
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"
对于数值可以指定转化为几进制:
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"
String()
String()是.toString()的加强版。对于 null 和 undefined,返回“null”和“undefined”。
console.log(String(null)); // "null"
let mess;
console.log(String(mess)); // "undefined"
console.log(String(mess1)); //未声明的undefined, 得到ReferenceError
字符串长度 String.length
截取字符串
String.prototype.slice()
接收两个 index 参数:开始,结束。
如果未传入结束,默认截到末尾。
const str = "The quick brown fox jumps over the lazy dog.";
console.log(str.slice(31));
// expected output: "the lazy dog."
console.log(str.slice(4, 19));
// expected output: "quick brown fox"
console.log(str.slice(-4));
// expected output: "dog."
console.log(str.slice(-9, -5));
// expected output: "lazy"
String.prototype.substring()
和 slice()类似。但是会将负数参数直接识别成 0
String.prototype.substr()
接收两个参数:开始,返回的字符串个数
第二个参数未传入时,默认截到末尾。第二个参数超过字符串长度时,默认截到末尾。
const str = 'The quick brown fox jumps over the lazy dog.';
console.log(str.substr(0,8);
// output: "The quic"
console.log(str.substr(-5, 8);
// output: " dog."
console.log(str.substr(0,50);
// output: "The quick brown fox jumps over the lazy dog."
console.log(str.substr(4);
// output: "quick brown fox jumps over the lazy dog."
Symbol
Symbol(符号)是 ECMAScript6 新增的数据类型。typeof 返回 symbol。
符号实例是唯一、不可变的。它的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
符号用来创建唯一记号,进而用作非字符串形式的对象属性。
符号没有字面量语法,这也是它发挥作用的关键。只要创建 Symbol()实例,并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号还是字符串属性。
创建普通符号
创建符号的方法需要用到 Symbol()函数。 创建普通符号:
let sym = Symbol();
符号实例是唯一的,独立创建出来的他们是不一样的:
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
console.log(genericSymbol); // Symbol()
console.log(otherGenericSymbol); // Symbol()
console.log(genericSymbol == otherGenericSymbol); // false
还可以传入一个字符串参数作为对符号的描述(description)。将来可以通过这个字符串来调试代码。
let fooSymbol = Symbol("foo");
但是,这个字符串参数与符号定义或标识完全无关:
let fooSymbol = Symbol("foo");
let otherFooSymbol = Symbol("foo");
console.log(fooSymbol); // Symbol(foo);
console.log(otherFooSymbol); // Symbol(foo);
console.log(fooSymbol == otherFooSymbol); // false
创建全局符号
全局函数需要用 Symbol.for()。
let sym = Symbol.for("apple");
想要共享和重用符号实例,就可以向上面这样用一个字符串作为键,在全局符号注册表中创建并重用符号。
这样得到的两个符号是等价的:
let fooGlobalSymbol = Symbol.for("foo"); // 创建新符号
let otherFooGlobalSymbol = Symbol.for("foo"); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true
Symbol.for()对每个字符串键都执行幂等操作。在调用时,它会检查全局注册表,若不存在,就会生成一个新符号实例并添加到注册表中。如果存在,就会返回该符号实例。
必须使用 Symbol.for()去创建和获取,不然向下面这样就会报错:
let localSymbol = Symbol("foo");
let globalSymbol = Symbol.for("foo");
console.log(localSymbol); // Symbol(foo)
console.log(globalSymbol); // Symbol(foo)
console.log(localSymbol === globalSymbol); // false
Symbol()函数不能和 new 关键字一起作为构造函数使用。
这样做是为了避免创建符号包装对象。Boolean、String 和 Number,都支持构造函数且可用于初始化包含原始值的包装对象。
let myBoolean = new Boolean();
console.log(typeof myBoolean); // "obecjt"
let myString = new String();
console.log(typeof myString); // "object"
let myNumber = new Number();
console.log(typeof myNumber); // "object"
let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor
如果确实想使用符号包装对象,可以借用 Object()函数:
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof myWrappedSymbol); // object
符号的用处
作为对象的属性使用
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。比如,对象字面量属性和 Object.defineProperty()/Object.defineProperties()定义的属性。
对象字面量只能在计算属性语法中使用符号作为属性。
let s1 = Symbol("foo");
let o = {
[s1]: "foo val",
};
console.log(o); // { [Symbol(foo)]: 'foo val' }
关于符号,还有更多的内容,但是我在这里先不继续介绍。因为相关的内容需要后期的知识进行支持,不然很难理解。并且符号真的是一个很陌生,面试也几乎不会被问到的问题。等有空了,再来好好了解它吧!