你不知道的 JavaScript - 数据类型

我想表明的是,类型转换是非常有用且被低估了的工具,你应该在自己的代码中使用它。在我看来,如果能够正确使用的话,类型转换不仅能够工作,而且也会让你的代码质量更高。所有的反对者和怀疑者肯定会嘲笑这样的立场,但我坚信这是提高你 JavaScript 水平的关键一点。

本文是一篇读书笔记,主要总结概括 JS 中的数据类型,以及它们之间存在的一些强制类型转换、隐式转换。本文对应《你所不知道的JavaScript(中卷)》—— “类型和语法“ 中的类型部分。

类型

对语言引擎和开发人员来说,类型是值的内部特征,它定义了值的行为,以使其区别于其它值。

JS 中有七种内置类型:

  • Null
  • Undefined
  • Boolean
  • Number
  • String
  • Object
  • Symbol (ES6 新增)

我们一般将前五种称之为简单基本类型;Object 称之为复杂类型(引用类型)。

检测类型

通常我们使用 typeof 用于判断值的类型

1
2
3
4
5
6
7
8
9
10
typeof undefined;     // 'undefined'
typeof true; // 'boolean'
typeof 123; // 'number'
typeof 'hello'; // 'string'
typeof { a: 1 }; // 'object'
typeof Symbol(); // 'symbol'

// 特殊
typeof null; // 'object'
typeof function() {}; // 'function'

这里有两个需要注意的地方:

  1. 对于 Null 类型的判断,typeof 返回的结果是 object。这是 JS 之前遗留的 bug,因此,我们对于 Null 类型的判断需要进行一下兼容:

    1
    (!a && typeof a === 'object');

    对于所有的 Object 类型来说,都属于真值(truthy value),而 Null 类型属于假值(falsy),借用 ! 操作符可以将其强制转换为 Boolean 类型,即可检验是真值还是假值,从而区分开 Null 类型和 Object 类型。

  2. 可以看到通过 typeof 运算符返回的结果都是 String 类型。那么对于下面这个经常可以看到的面试题,你应该能一眼就想出答案了。

    1
    typeof typeof typeof false;   // 'stirng'

我们知道 JS 里对 Object 还定义了几个原生的高阶对象:Array、Date、RegExp 等。而在实际项目中,我们对一个对象类型的检测,还需要追踪到是否属于原生的几种高阶对象,而 typeof 运算符显然无法实现这一需求。

目前,比较通用的方法是通过 Object.prototype.toString.call(obj) 进行判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function typeof(obj) {
const toString = Object.prototype.toString;
const map = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object Undefined]': 'undefined',
'[object Null]': 'null',
'[object String]': 'string',
'[object Object]': 'object',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regExp',
'[object Symbol]': 'symbol'
};
return map[toString.call(obj)];
}

可以看到该方法几乎可以判断我们日常使用到的所有值的类型。

变量没有类型,但它们持有的值有类型。类型定义了值的行为特征。

从上一节我们已经了解到,JS 中有七种内置类型。而对于这七种类型中,有的类型内部有一些特殊的值,下面我们将简要介绍一下一些特殊的值类型。

Number

和其他语言不同,JS 中的数字类型直接包含了整数类型和浮点数类型。

安全范围

JS 中的数字类型采用的是双精度 64 位格式存储,但是能够被“安全”呈现的最大整数是:2^53 - 1

为什么是这个值?

我们分析下双精度浮点数的结构:

  • 1 位符号位
  • 11 位指数位
  • 52 位尾数位

使用 52 位表示一个数的整数部分,那么最大可以精确表示的数应该是 2^52 - 1 才对, 就像 64 位表示整数时那样: 2^63 - 1 (去掉 1 位符号位)。 但其实浮点数在保存数字的时候做了规格化处理,以 10 进制为例:
20*10^2 => 2*10^3 //小数点前只需要保留 1 位数
对于二进制来说, 小数点前保留一位, 规格化后始终是 1.***, 节省了 1 bit,这个 1 并不需要保存。

在 ES6 中,最大整数(2^53 - 1)被定义为:Number.MAX_SAFE_INTEGER;最小整数(-2^53 + 1)被定义为:Number.MIN_SAFE_INTEGER

而对于 JS 中的位操作,只能适用 32 位有符号整数,也就是最大值为 2^31 - 1

浮点数

对于浮点数中的 . 有一个需要注意的地方。. 运算符是一个有效的数字字符,会被优先识别为数字常量的一部分,然后才是对象属性访问运算符。看下面几个例子:

1
2
3
4
5
6
7
// 非法,`.` 被视为数字的一部分
42.toFixed(3) // SynaxError

// 有效,toFixed 前的 `.` 被视为对象属性访问运算符
(42).toFixed(3); // '42.000'
0.42.toFixed(3); // '0.420'
42..toFixed(3); // '42.000'

NaN

如果数学运算的操作数不是数字类型,就无法返回一个有效的数字,这种情况下返回 NaN。一般用于指出数字类型中的错误情况。

NaN 有一个特性,它是 JS 中唯一一个和自身不想等的值。

1
2
NaN == NaN;    // false
NaN === NaN; // false

JS 原生自带一个函数 isNaN(),初学者们经常认为可以用来判定一个变量是否为 NaN,但实际上它的检测过程过于死板,它的检测原理为:“检查参数是否不是 NaN,也不是数字”。看下面一个例子:

1
2
3
4
5
var a = 2 / 'foo';
var b = 'foo';

isNaN(a); // true
isNaN(b); // true

可以看到对于非数字类型的值,通过 NaN() 函数都会返回 true

改进方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 方法1
if (!Number.isNaN) {
Number.isNaN = function(n) {
return (
typeof n === 'number' && window.isNaN(n);
)
}
}

// 方法2
if (!Number.isNaN) {
Number.isNaN = function(n) {
return n !== n;
}
}

Infinity, -Infinity

JS 数字运算溢出或者用数字除以 0 时结果会返回 Infinity-Infinity

计算结果一旦获得无穷数,那么无法再得到有穷数。

几个需要注意的计算式:

1
2
3
4
(-)Infinity / (-)Infinity;   // NaN
(有穷正数) / (-)Infinity; // (-)0
(有穷负数) / (-)Infinity; // (-)-0
Infinity == -Infinity; // false

0, -0

这里的特殊点主要就是 -0-0 除了可以用来作常量外,还可以是某些数学运算的返回值。注意,加法减法运算不会得到 -0

注意几个计算式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var a = -0;

// -0 由数字转变为字符串会得到 '0'
a.toString(); // '0'
a + ''; // '0'

// '-0' 字符串转数字会得到 -0
Number('-0'); // -0
+'-0'; // -0

// -0 和 0 相等
0 == -0; // true
0 === -0; // true

// 怎么判断 -0
function isNegZero(n) {
n = Number(n);

return (n === 0) && (1 / n === -Infinity);
}

Undefined、Null

Undefined 类型只有一个值,即 undefined

Null 类型也只有一个值,即 null

undefinednull 常用来表示 “空的” 值或 “不是值” 的值。但两者有以下差别:

  • null 指空值(empty value)
  • undefined 指没有值(missing value)

或者:

  • undefined 指从未赋值
  • null 指曾赋过值,但是目前没有值

还有一个需要注意的地方:null 是一个关键字,不是标志符;而 undefined 是一个标志符,也就是可以将其当作变量进行赋值。

1
2
var undefined = 1;    // 合法
var null = 2; // SyntaxError

因此,像 underscore 这些库,为了防止这个隐患,不直接使用 undefined 直接表示 Undefined 类型的值,而是通过 void 0 来表示 Undefined 类型值。

原生函数

JS 为基本类型值提供了封装对象,称为原生函数。JS 中常用的原生函数如下:

  • String()
  • Number()
  • Boolean()
  • Array()
  • Object()
  • Function()
  • RegExp()
  • Date()
  • Error()
  • Symbol()

我们知道,对于一些基本类型值的创建不仅可以使用其字面量常量的方法进行创建,也可以使用其原生函数作为构造函数进行创建。

对于这一部分,主要想强调的是对于简单标量基本类型值(比如 Number、String 等)。我们知道,基本类型值不是对象,因而从逻辑上讲它们不应该有方法。但是,我们却又经常使用一些例如字符串的 lengthtoUpperCase() 等属性和方法。实际上,JS 引擎会自动对这些基本类型值进行封装(用相应类型的封装对象来包装它)来实现对这些属性和方法的访问。引擎实际进行的操作如下:

  • 调用相应原生函数,创建一个实例
  • 在实例上调用制定的方法
  • 销毁这个实例

经过此番处理,使得基本类型值也能过使用其原生函数中的方法和属性了。

注意,Null、Undefined 类型没有原生函数,因此无法对它们进行封装后调用方法和属性值,否则会报错。

类型转换

将值从一种类型转换为另一种类型通常称为类型转换,这是显式的情况;隐式的情况称为强制类型转换。而隐式、显示在 JS 中并没有主观的定义,对于某些转换情况,有的人一眼能够辨别出来,在他们心中可能就是显式转换;而对于分辨不出来的,他们可能判定为是隐式转换。

但是,对于调用各类型的原生函数进行的类型转换我们都判定为显式转换

ToPrimitive

在介绍具体的类型转换规则之前,我们需要先了解下 JS 的 ToPrimitive 操作。这个操作主要涉及到 Object 类型的一些转换规则,它有两种转换规则:

  • String 规则:
    1. 检测对象是否有 toString() 方法,如果有且调用后的值为基本类型值,则进行返回,否则执行下一步
    2. 检测对象是否有 valueOf() 方法,如果有且调用后的值为基本类型值,则进行返回,否则执行下一步
    3. 抛出 TypeError 错误
  • Number 规则:
    1. 检测对象是否有 valueOf() 方法,如果有且调用后的值为基本类型值,则进行返回,否则执行下一步
    2. 检测对象是否有 toString() 方法,如果有且调用后的值为基本类型值,则进行返回,否则执行下一步
    3. 抛出 TypeError 错误

而一般来说,只有 Date 对象的 ToPrimitive 操作默认采用 String 规则;其它对象的 ToPrimitive 操作都默认采用 Number 规则。

下面是几个基本类型的转换规则:

ToString

1
2
3
4
5
6
null         => 'null'
undefined => 'undefined'
true / false => 'true' / 'false'
数字 => '数字'
对象 => 调用 `toString()` 方法。在该方法未被修改的情况下,返回为:`[object Object]`
数组 => 将各项字符串化后再用 `,` 号连接起来
1
2
var a = [1, 2, 3];
a.toString(); // '1,2,3'

ToBoolean

  • undefined、null、false、+0、-0、NaN、''、假值对象(指被规范废弃的方法,常存在于老版本 IE 浏览器中,比如:document.all) => false
  • 除去上一项中列出的值,其余值均转换为 true

即使上述两条已经概括了所有值类型的布尔转换规则,但是还要强调一点:空数组、空对象、空函数,它们都不在第一条的范围之类,因此它们转换为布尔类型是,值均为 true

ToNumber

1
2
3
4
5
6
null        => 0
undefined => NaN
true => 1
false => 0
字符串 => 尝试进行转换(字符串里的内容是否为数字),若转换失败返回为 NaN
对象(含数组)=> 采用 ToPrimitive 操作,若得到基本类型值,再将该值通过上述的类型规则进行转换。

Symbol 的转换

Symbol 类型的转换比较特殊,总结有以下几条注意规则:

  • 允许 Symbol 显式转换(调用 String() 原生函数)为字符串;不允许其他方法进行字符串转换,否则报错。
  • 不允许将 Symbol 类型转换为数字类型
  • 允许 Symbol 类型转换为布尔类型,值为 true

部分隐式转换规则

  1. + 操作符:
  • 可以将字符串与数字进行互相转换(字符串能够被转换为数字的前提下)
  • 可以将日期对象转换为时间戳(单位:毫秒)
  • 有两个操作数时,可进行字符串拼接或数学运算操作。如果其中有操作数是字符串或者通过 ToPrimitive 能转换为字符串的对象,则进行字符串拼接;否则执行数字加法。
    1
    2
    3
    4
    5
    6
    7
    8
    // 字符串 => 数字
    +'123'; // 123

    // 数字 => 字符串
    123 + ''; // '123'

    // 时间戳转换
    var timestamp = +new Date();
  1. 位操作符:先执行 ToNumber 强制类型转换,再将其转换为 32 位整数。下面介绍几个特殊点
    • | 操作符:0 | x 表示将 x 转变为 32 位数字。
      1
      2
      3
      4
      0 | -0;        // 0
      0 | NaN; // 0
      0 | Infinity; // 0
      0 | -Infinity; // 0
      注意,以上操作均返回 0,因为以上特殊数字无法以 32 位格式呈现。
    • ~ 操作符:将数组转换为 32 位后,再执行 “非” 操作。~x 大致等于 -(x + 1)。因此,当 x = -1 时,~x 值为0。代码中常利用这一特性进行代码的简化:
      1
      2
      3
      4
      if (str.indexOf('..') > -1) { ... }

      // 简化为
      if (~str.indexOf('..')) { ... }
    • ~~ 操作符:将值截除为一个 32 位整数(不管正数还是负数,都直接截除掉小数部分)。该操作 x | 0 也能够实现,但是 ~~ 运算优先级高一些。
  2. parseInt()parseFloat()解析数字字符串。解析允许字符串中含有非数字字符,解析按从左到右的顺序,遇到非数字字符就停止。而 Number() 等属于将字符串转换为数字,不允许出现非数字字符,否则会失败并返回 NaN
  3. ! 运算符:将值强制转变为布尔值,并将值进行真假反转。!! 则实现将值直接转变为布尔值。
  4. a || b 等同于 a ? a : ba && b 等同于 a ? b : a。这里让大家注意的是他们的返回结果,可以看到返回的结果并非是布尔值,而是左边的值或右边的值中的一种。将其转换为三元表达式的写法可以更直接看出返回的值是什么。

宽松相等和严格相等

== 允许在相等比较中进行强制类型转换,而 === 不允许。

===== 在比较 Object 类型时,采取相同的比较方法:两个对象指向同一个值时视为相等,不发生强制类型转换。

== 采取的比较算法叫做:抽象相等比较算法。其规则如下:

  1. 两个值类型相同,则仅比较它们是否相等
  2. 两个值类型不同时,将两者转换为相同的类型后再进行比较:
  • 字符串和数字的比较:将字符串转换为数字进行比较
  • 其他类型和布尔值比较:将布尔值转换为数字进行比较
  • nullundefined 之间的比较:在 == 中,返回相等。同时,它们也都与自身相等
  • 对象和非对象进行比较:将对象进行 ToPrimitive 转换,得到的基本类型值再进行比较

== 比较中,有几个需要注意的地方:

  • 如果两边的值中有 true 或者 false,千万不要用 ==
  • 如果两边的值中有 []''0,尽量不要使用 ==

抽象关系比较

比较操作符 <><=>= 也会触发隐式强制类型转换,其比较规则如下:

  1. 比较双方首先调用 ToPrimitive 操作,如果结果出现非字符串,就根据 ToNumber 规则将双方强制类型转换为数字来进行比较(Number 类型的 valueOf() 返回的依旧是 Number 类型值)
  2. 如果比较双方都是字符串,则按照字母顺序来进行比较

需要注意的是,<=>= 在语义的理解中应该是 “小于或等于”、“大于或等于” 的意思。但在 JS 中,是 “不大于”、“不小于” 的意思,判断的机制为 !(a > b)!(a < b)。因此,返回的结果有时会和实际的理解不完全相等。

参考