类型与类型转换

400 阅读14分钟

类型

JS 语言规定了 8 种语言类型,分别是:UndefinedNullNumberStringBooleanObjectSymbolBigInt(从 V8 的 6.7 版本开始支持,对应的 Chrome 版本为 67,Node 10 内置的版本是 V8 6.6,所以在 BigInt 不能在 Node 10 中使用)。

Undefined 和 Null

Undefined 类型表示未定义,它的类型只有一个值,就是 undefined。任何变量在未赋值之前都是 Undefined 类型,值为 undefined,一般我们可以用全局变量 undefined 来表达这个值,或者用 void 运算来把任意一个表达式变成 undefined 值。

因为 JS 代码中,undefined 是一个变量,并非是一个关键字,所以为了避免无意中被篡改,会使用 void 0 来表示 undefined 值。void 0 是为了让 undefined 在这里以值得方式出现,而不是以变量的方式出现。

那我们可以直接使用 undefined = 1 来改变 undefined 的值吗?

// 情况 1
var undefined = 1
console.log(undefined) // undefined
// 情况 2
function fn() {
    var undefined = 100;
    console.log(undefined)
}
fn() // 100
// 情况 3
var obj = {};
obj.undefined = 12
console.log(obj.undefined) // 12

可以发现在函数内部 undefined 局部变量被重新赋值,所以平时建议使用 void 0 来表示 undefined 值。

有时候我们需要判断一个变量的值是不是 undefined,我们常常会用 str === undefined 来判断,但是假如这个变量还未声明就会出现错误。这时候就可以使用 typeof 来判断,typeof 不会报错,因为 typeof 有一个安全的防范机制。

if (typeof str === 'undefined') {
  console.log('is undefined')
}

当然除了使用 undefined 方法之外,还可以检查全局变量是否是全局对象的属性,这样也不会报错:

if (window.DEBUG) { ... }

null 跟 undefined 有一定的表意区别,null 表示定义了但是为空,并且 null 是关键字,不必担心被重新赋值。

Boolean

有两个值,true 和 false,表示逻辑意义上的真和假。

String

String 字符串有最大长度 2^53 - 1,但是这个长度不是字符数,而是字符串的 UCS-2 编码 之后的字节长度。我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UCS-2 编码。所以字符串的最大长度,实际上是受字符串的编码长度影响的。字符集和编码相关可以参考文章:Unicode 和 编码方式的理解

最初 JavaScript 语言采用 Unicode 字符集,但是只支持一种编码方法 UCS-2

UCS-2 在 1990 年公布,UTF-16 编码在 1996 年公布,明确宣布是 UCS-2 的超集。两者关系简单说,就是 UTF-16 取代了 UCS-2,或者说 UCS-2 整合进了 UTF-16。所以现在只有 UTF-16,没有 UCS-2。

JS 只能处理 UCS-2 编码,所以造成所有字符在这门语言中都是 2 个字节,如果是 4 个字节的字符,会当作两个双字节的字符处理。所以 JS 中的字符函数都受到这个性质的影响,无法返回正确结果。所以在处理双字节字符时,应该格外小心。

console.log('𝌆'.length) // 2
console.log('𝌆' === '\uD834\uDF06') // true
'𝌆'.charCodeAt(0) // 55348
parseInt('D834', 16) // 55348

Number

JS 中的数字类型是基于 IEEE 754 标准规定的双精度浮点数规则来实现的(即 64 位二进制)。对于 64 位的浮点数,最高的1位是符号位 S,接着的 11 位是指数 E,剩下的 52 位为有效数字 M。

img

JS 中能够被安全呈现的整数为 2^53 - 1,ES6 中提供 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 来表示最大整数和最小整数。后来又提出了一种 BigInt 的类型提供一种方法来表示大于 2^53 - 1 的整数。

有几个特殊的情况:

  • NaN
  • Infinity
  • -Infinity

JS 中也有 +0 和 -0 的区别。在加法中没有区别,但是在除法中会区分。区分 +0 和 -0 的方式,正是检测 1/x 是 Infinity 还是 -Infinity。

另外在二进制浮点数的运算中,存在精度问题,在处理带有小数的数字时需要特别注意。比如:

console.log(0.1 + 0.2 === 0.3) // false

那 JS 在计算 0.1 + 0.2 时到底发生了什么呢?首先,十进制的 0.1 和 0.2 会被转换成二进制的,但是由于浮点数用二进制表示时是无穷的:

0.1 -> 0.0001 1001 1001 1001...(1100循环)
0.2 -> 0.0011 0011 0011 0011...(0011循环)

IEEE 754 标准的 64 位双精度浮点数的小数部分最多支持53位二进制位,所以两者相加之后得到二进制为:

0.0100110011001100110011001100110011001100110011001100 

因浮点数小数位的限制而截断的二进制数字,再转换为十进制,就成了0.30000000000000004。所以在进行算术计算时会产生误差。

那应该怎么判断 0.1 + 0.2 等于 0.3 呢?最常见的方法是设置一个误差范围值,即“机器精度”,这个值通常是:2^-52。ES6 中提供 Number.EPSILON 来表示这个值。

Symbol

Symbol 是 ES6 中引入的新类型,在 ES6 中整个对象系统被 Symbol 重构。

Symbol 可以具有字符串类型的描述,但是即使描述相同,Symbol 也不相等。

创建 Symbol 的方式是使用全局的 Symbol 函数。

const mySymbol = Symbol('my symbol');

一些标准中提到的 Symbol,可以在全局的 Symbol 函数的属性中找到。例如,我们可以使用 Symbol.iterator 来自定义 for…of 在对象上的行为。

Object

对象,是 JS 的核心机制之一。在 JS 中,对象的定义是“属性的集合”。属性会分为数据属性访问器属性,两者都是 key-value 结构,key 可以是字符串或者 Symbol 类型。

JS 中有几个基本类型,在对象中有构造器:

1.Number

2.String

3.Boolean

4.Symbol

所以,3 和 new Number(3) 是完全不同的值,一个是 Number 类型,一个是对象类型。

Number、String、Boolean 可以两用,当跟 new 搭配时,他们产生对象。当直接调用时,表示强制类型转换。Symbol 类型使用 new 调用时会抛出错误,但它仍然是 Symbol 对象的构造器。

为什么给对象添加的方法能用在基本类型上?因为 . 操作符提供了装箱操作,根据基础类型构造一个临时对象,使得我们在基础类型上调用对应对象的方法。

console.log('abc'.charAt(0)) // a

BigInt

可以用在一个整数字面量后面加 n 的方式定义一个 BigInt。它与 Number 有些不同点:不能用于 Math 对象中的方法;不能和任何 Number 实例混合运算,两者必须转换成同一种类型。在两种类型来回转换时要小心,因为 BigInt 变量在转换成 Number 变量时可能会丢失精度。

const theBiggestInt = 9007199254740991n;
const previousMaxSafe = BigInt(Number.MAX_SAFE_INTEGER);

对任何 BigInt 值使用 JSON.stringify() 都会引发 TypeError,因为默认情况下 BigInt 值不会在 JSON 中序列化。

类型转换

因为 JS 是弱类型语言,所以类型转换发生非常频繁,大部分我们熟悉的运算的都会进行类型转换。其中,最常提起的就是 == 运算。其他运算,比如加减乘除都会涉及到类型转换,转换规则如下:

在这里比较复杂的部分就是 Number 和 String 之间的相互转换,以及对象和基本类型之间的相互转换。

StringToNumber

字符串转为数值类型,存在一个语法结构,类型转换支持十进制、二进制、八进制和十六进制。还包括正负号科学记数法,使用大写或者小写的 e 来表示。例如:

30 // 十进制
0b111 // 二进制表示7
0o13 // 八进制表示11
0xFF // 十六进制表示255
1e3 // 科学计数法表示1000
-1e-2 // 科学计数法表示-0.01

使用 Number 来进行字符串到数值类型的转换

再不传入第二个参数的情况下,parseInt 只支持十六进制的前缀 “0x”,而且会忽略非数字字符,不支持科学计数法。所以在任何环境下,都建议传入 parseInt 的第二个参数。

parseFloat 则是直接把字符串当作十进制来解析,不会引入任何其他进制。

当不符合上面的规则时,字符串转为数值返回 NaN。

NumberToString

在较小的范围内,数字到字符串的转换是可以看成是转换成十进制的字符串表示的。当 Number 绝对值较大或者较小时,字符串表示则是使用科学计数法来表示。

String(30) // "30"
String(0o13) // "11"
String(0xFF) // "255"
String(90071992547409911203123) // "9.00719925474099e+22"

除了使用 String 显示转换之外,还可以使用其他方法,比如:

const a = 42
const b = a.toString() // "42"

a.toString 涉及隐式类型转换,因为 toString() 对 42 这样的基本类型值不适用,所以 JavaScript 引擎会自动为 42 创建一个封装对象,也就是下面提到的装箱操作,然后再对该对象调用 toString 方法。

装箱转换

使用装箱转换的基本类型在对象中都有对应的类 Number,String,Boolean,Symbol,所谓装箱转换,就是把基本类型转为对应的对象

全局的 Symbol 函数无法使用 new 来调用,但是我们可以利用装箱机制来得到一个 Symbol 对象,我们可以利用一个函数的 call 方法来强迫产生装箱。我们定义一个函数,函数里面只有 return this,然后我们调用函数的 call 方法到一个 Symbol 类型的值上,这样就会产生一个 symbolObject。

const symbolObject = (function(){ return this; }).call(Symbol("a"));

console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true

使用内置的 Object 函数,也可以调用装箱能力。

const symbolObject = Object(Symbol("a"));

console.log(typeof symbolObject); //object
console.log(symbolObject instanceof Symbol); //true
console.log(symbolObject.constructor == Symbol); //true

每一类装箱对象皆有私有的 Class 属性,这个属性可以用 Object.prototype.toString 获取:

const symbolObject = Object(Symbol("a"));
console.log(Object.prototype.toString.call(symbolObject)); // [object Symbol]

在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。但是 call 本身会产生装箱操作,所以还需要借助 typeof 来区分基本类型还是对象类型。

拆箱转换

在 JavaScript 标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。

对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。

拆箱转换会尝试调用 valueOftoString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。

Number 的拆箱转换会优先调用 valueOf。

const o = {
  valueOf: () => {console.log("valueOf"); return {}},
  toString: () => {console.log("toString"); return {}}
}

o * 2
// valueOf
// toString
// TypeError

到 String 的拆箱转换会优先调用 toString。

const o = {
  valueOf: () => {console.log("valueOf"); return {}},
  toString: () => {console.log("toString"); return {}}
}

String(o)
// toString
// valueOf
// TypeError

在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。

const o = {
  valueOf: () => {console.log("valueOf"); return {}},
  toString: () => {console.log("toString"); return {}}
}

o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}

console.log(o + "")
// toPrimitive
// hello

运算符与类型转换

四则运算符

加法运算符有以下几个特点:

  • 运算中其中一方为字符串,那么就会把另一方也转换为字符串
  • 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"

那对于第二条规则来看,4 + [1,2,3] 为什么是将数组转为字符串而不是数字呢,可以理解为数组是对象类型使用valueOf 转换后还是数组,使用 toString 转换为字符串是原始类型,所以停止转换。

另外对于加法还需要注意这个表达式 'a' + + 'b'

'a' + + 'b' // "aNaN"

因为 + 'b' 等于 NaN,所以结果为 "aNaN",你可能也会在一些代码中看到过 + '1' 的形式来快速获取 number 类型。

那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字

4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN

比较运算符

  • 如果是对象,就通过 toPrimitive 转换对象
  • 如果是字符串,就通过 unicode 字符索引来比较
let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  }
}
a > -1 // true

在以上代码中,因为 a 是对象,所以会通过 valueOf 转换为原始类型再比较值。

== 和 ===

对于 == 来说,如果对比双方类型不一样的话,就会进行类型转换。

假如我们需要对比 x 和 y 是否相同 ==,就会进行如下判断流程:

  1. 首先判断两者类型是否相同,类型相同就直接比较大小了

  2. 类型不同的话,就开始进行类型转换

  3. 首先判断是否在对比 null 和 undefined,如果是的话,直接返回 true

  4. 判断两者是否为 string 和 number,如果是的话,将 string 转为 number

    1 == '1'
    转为
    1 == 1
    
  5. 判断其中一方为 boolean,是的话就将 boolean 转为 number 再进行判断

    '1' == true
    转为
    '1' == 1
    转为
    1 == 1
    
  6. 判断其中一方是 object 且另一方为 string 、number 或 symbol,是的话就把 object 转为原始类型进行判断。

    '1' == { a: 1 }
    转为
    '1' == '[object Object]'
    

为了更详细的理解 == 的判断流程,我们可以看一下规范里是怎么讲的 The Abstract Equality Comparison Algorithm

11.9.3 The Abstract Equality Comparison Algorithm

The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:

  1. If Type(x) is the same as Type(y), then // 如果 x 和 y 的类型相同 a. If Type(x) is Undefined, return true. // 如果 x 是 undefined,返回 true b. If Type(x) is Null, return true. // 如果 x 是 null,返回 true c. If Type(x) is Number, then // 如果 x 是数值 I. If x is NaN, return false. // 如果 x 是 NaN,返回 false II. If y is NaN, return false. // 如果 y 是 NaN,返回 false III. If x is the same Number value as y, return true. // 如果 x 是跟 y 相同的数值,返回 true IV. If x is +0 and y is −0, return true. V. If x is −0 and y is +0, return true. VI. Return false. d. If Type(x) is String, then return true if x and y are exactly the same sequence of characters (same length and same characters in corresponding positions). Otherwise, return false. // 如果 x 和 y 是完全相同的字符序列(相同长度,相同位置相同字符)是返回 true,否则返回 false e. If Type(x) is Boolean, return true if x and y are both true or both false. Otherwise, return false. // 如果是布尔类型的,如果 x 和 y 同时都为 true 或 false 时返回 true,否则返回 false f. Return true if x and y refer to the same object. Otherwise, return false. // 如果 x 和 y 指向同一个对象返回 true,否则返回 false
  2. If x is null and y is undefined, return true.
  3. If x is undefined and y is null, return true.
  4. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ToNumber(y). // 如果 x 是数值,y 是字符串的话,将 y 转为 number 再进行比较
  5. If Type(x) is String and Type(y) is Number, return the result of the comparison ToNumber(x) == y. // 如果 x 是 字符串,y 是数值的话,将 x 转为 number 再进行比较
  6. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y. // 如果 x 是布尔,将 x 转为数值再进行比较
  7. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).
  8. If Type(x) is either String or Number and Type(y) is Object, return the result of the comparison x == ToPrimitive(y). // 如果 x 是 string 或者 number 类型,y 是 object 类型,将 y 转为基本类型值之后比较
  9. If Type(x) is Object and Type(y) is either String or Number, return the result of the comparison ToPrimitive(x) == y.
  10. Return false.

看完了上面的步骤,让我们来分析一下 [] == ![]

  1. [] == false ![] 优先级较高,所以先将 [] 转为 boolean 值再取反为 false
  2. [] == 0 如果一方为布尔值,将布尔值转为数值类型 为 0
  3. '' == 0 如果一方是 object,另一方是基本类型值,则将 object 转为基本类型值,[] 转为基本类型为 ''
  4. 0 == 0 如果一方是数值,另一方是 string,则将 string 转为 number,'' 转为 number 为 0
  5. 返回 true