JavaScript:数据类型

131 阅读14分钟

数据类型

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
UndefinedN/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 的特性

  1. 任何涉及 NaN 的操作,始终返回 NaN。(如 NaN/10)
  2. 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 必须大写。它首先判断参数类型,针对不同类型基于如下规则进行转换:

  1. undefined:返回 NaN

  2. Null:返回 0

  3. 布尔值:true 为 1,false 为 0

  4. 数值:直接返回

  5. 字符串:

    空字符串(不包含字符)返回 0。

    有效的整数字符,包括带加、减号的情况,转换为十进制数值。

    有效的浮点值格式如"1.1",转换为相应的浮点值。

    有效的十六进制格式如"0xf",转换为对应的十进制整数值。

    如果字符串包含除上述情况之外的其他字符,则返回 NaN。

  6. 对象:调用 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()的规则比较简单:

  1. 忽略空格一直到第一个非空格字符。如果不是数值、+、-,则返回 NaN。

  2. 遇到特殊字符后停止转换并返回结果。

  3. 如果字符串以"0x"开头,解释为十六进制整数。

  4. 如果字符串以"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' }

关于符号,还有更多的内容,但是我在这里先不继续介绍。因为相关的内容需要后期的知识进行支持,不然很难理解。并且符号真的是一个很陌生,面试也几乎不会被问到的问题。等有空了,再来好好了解它吧!