类型转换

构造器

基本类型拥有构造器

  1. String
  2. Number
  3. Boolean
  4. Symbol

构造器是两用的,当使用new调用时产生对象,直接使用时表示强制类型转换,这很容易识别。

typeof String(12); // 'string'
typeof new String(); // 'object'

Symbol 比较特殊,使用new调用会抛出 TypeError[0],ES6 不再支持创建原始值包装对象。所以,BigInt 也不支持new。如果不考虑兼容性,我想就不会有构造器两用这一说法了。

📌 关于为什么 ES6 开始不支持显示new,从开发者的角度,这种创建包装对象的方式很鸡肋;换句话说,语言设计上模糊了对象和基本类型间的关系,对象的方法可以直接在基本类型上使用。这也是我们优先选择使用的方式,让 JavaScript 引擎决定什么时候应该使用封装对象。

123 .toString(); // '123'

.运算符提供了装箱(boxed)[1]操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。123 .toString()是显式强制类型转换,不过其中涉及隐式转换,JavaScript 引擎会为 123 自动创建一个包装对象。

📌 关于如何检测new调用,实现机制是通过 instanceof[2] 操作符判断 this 的指向是否与指定构造器相关联。

ToString

  • 长 number 转换成 string 采用指数形式表示,感性的理解为防止字符串过长吧
1.04 * (1000000000000000000000000).toString(); // 1.04e+24
  • 对象强制类型转换为 string 是通过 ToPrimitive[3] 操作
  • array 转 string 会特殊处理
[1, 2, 3].toString(); // '1,2,3'
[null].toString(); // ''
  • JSON-safe 的值都可以使用 JSON.stringify() 字符串化
JSON.stringify("42"); // '"42"'

ToNumber

  • undefined 转换为 NaN
  • 空字符串形式被转换为 0
  • 对象(包括数组)会先进行拆箱操作,再强制类型转换为数字
  • + 运算符的一元形式
Number(undefined); // NaN
Number("\n"); // 0
Number([]); // 0
Number(["abc"]); // NaN
+"13"; // 13

ToBoolean

假值(falsy value)

  • undefined
  • null
  • false
  • +0、-0 和 NaN
  • ""

字符串不都是真值

假值列表之外的值都是真值,""是假值列表中唯一的字符串,除此之外,字符串都是真值。

假值对象(false object)

假值对象是个很模糊的概念,并不属于 JavaScript 语言的范畴。

Boolean(document.all); // false

形式

  1. !!和 Boolean() 显式类型转换
  2. if(..) 条件判断
  3. 循环语句中的条件判断
  4. ? : 三元表达式中的条件判断
  5. || 和 && 左边的操作数

区别在于三元表达式中的 a 可能被执行两次

  • a || b 可以看作 a ? a : b
  • a && b 可以看作 a ? b : a

+运算

前面介绍过+运算符的一元形式,更为隐蔽的是它会为对象进行 ToPrimitive[4] 操作。

+运算符的重载使它能够进行字符串拼接,如果其中一个操作数是字符串(或者调用 ToPrimitive(拆箱)能得到字符串),则执行字符串拼接,否则执行数字加法。

[1, 2] + [3, 4]; // '1,23,4'

上述表达式首先会进行 valueOf() 操作无法得到基本类型值,然后再调用 toString() 得到"1,2"和"3,4"。+将它们拼接后返回结果。

还有一个值得注意的地方,在处理 Symbol 时候 🤨

let s1 = Symbol("cool");
String(s1); // 'Symbol(cool)'

let s2 = Symbol("not cool");
s2 + ""; // TypeError

Symbol 不能被隐式强制类型转换成字符串类型。另外,Symbol 不能被强制类型转换为数字,但可以被强制类型转换为布尔值。好在我们不会经常用到它的强制类型转换。

==相等

📌==在比较时会进行强制类型转换,而===不会

NaN == NaN; // false
+0 == -0; // true
  1. null 和 undefined 只和对方和自身相等
  2. 一边是数字值,另一边进行 ToNumber 操作
  3. 一边是布尔值,两边都进行 ToNumber 操作
  4. 一边是对象,对象进行 ToPrimitive拆箱 操作
  5. 两边都是字符串,则比较两个字符串对应的字符编码值(ASCII)
  6. 两边都是对象,这时比较的是对象地址是否是同一个,和===原理是一样的

<比较

  1. 双方都进行 ToPrimitive 操作
  2. 两边都是字符串,根据字母顺序来比较
  3. 出现非字符串,两边都进行 ToNumber 操作
let o1 = { a: 12 };
let o2 = { a: 13 };

o1 < o2; // false
o1 == o2; // false
o1 > o2; // false

o1 <= o2; // true
o1 >= o2; // true

关系比较的隐式强制类型转换比较晦涩,为了安全起见,应该对其进行显式强制类型转换为同一类型值再进行比较。

Number([12]) < Number("013"); // true

[[Class]]

如果你了解 JavaScript,就会知道使用 typeof 判断数据类型有时并不那么可靠。对于返回 'object' 的对象有其内部的[[Class]]私有属性,可以通过 Object#原型toString(..) 查看 🔍

Object.prototype.toString.call([1, 2]); // [object Array]

实际上它输出了对应的内置对象[5]

nullundefined呢?

Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call(undefined); // [object Undefined]

即使并不存在相应的内置对象,[[Class]] 值仍然打印出结果,这符合规范定义。

ECMA2020

Historically, this function was occasionally used to access the String value of the [[Class]] internal slot that was used in previous editions of this specification as a nominal type tag for various built-in objects. The above definition of toString preserves compatibility for legacy code that uses toString as a test for those specific kinds of built-in objects. It does not provide a reliable type testing mechanism for other kinds of built-in or program defined objects. In addition, programs can use @@toStringTag in ways that will invalidate the reliability of such legacy type tests.

也就是说,不是所有内置对象都有 [[Class]],如果你想自定义类型,可以使用 👉Symbol.toStringTagopen in new window

多说一句,ES6 后可以使用 Class 关键字定义类,我想这是为什么后续数据结构不实现 [[Class]] 的原因。

装箱

Object.prototype.toString.call("str"); // [object String]
Object.prototype.toString.call(123); // [object Number]
Object.prototype.toString.call(true); // [object Boolean]

前面说过.运算符提拱了装箱操作,所谓装箱,就是把基本数据类型转换成对应的对象。调用call方法也会产生装箱,因为不能使用new,可以利用 call 机制得到 Symbol 对象。

let symbol = (function () {return this;}).call(Symbol("a"));
typeof symbol; // 'object'

调用 Function#call 会将基本类型[6]装箱。就像前面说的,Object#toString 可以获取封装对象的 [[Class]],它比 typeof 和 instanceof 更准确。之所以这么说,是因为 JavaScript 没有提供访问并修改对象私有 Class 属性的方法。

除此上述三种方式之外,你也可以使用Object()创建包装对象。

🚫

  • 装箱会创建临时对象,频繁做装箱转换会造成性能问题
  • call 本身会进行装箱操作,所以需要配合typeof来区分基本类型还是对象类型

拆箱

ToPrimitive 操作会将对象包装类型转换成基本类型,也被称为拆箱(unboxed)。

拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型,到 String 的拆箱转换会优先调用 toString。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。此时就说明这个拆箱转换失败了。

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

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

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

// toPrimitive
// hello
console.log(o + ""); // +运算符的隐式强制类型转换

扩展

Error

Error 内置对象,常见的包括 SyntaxError、 TypeError 和 ReferenceError。TypeError 表示作用域查找成功但是语法判断错误,ReferenceError 属于变量作用域查找不成功。

内置对象(build-in object)

内置对象

数据类型

和大多数编程语言类似,JavaScript 数据类型分为基本类型和引用类型。区别在于存储在不同的内存空间,一般来说,引用数据类型占用空间大,存储在堆空间,栈中仅保存对其的引用地址指针。对计算机而言,对栈空间的处理速度往往比堆空间要快。

规范类型描述
List、Record描述函数传参过程
Set解释字符集等
Completion Record描述异常、跳出等语句执行过程
Reference描述对象属性访问、delete 等
Property Descriptor描述对象属性
Lexical Environment、Environment Record描述变量和作用域
Data Block描述二进制数据

instanceof

instanceof操作符原理实现 📋

function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left);

  while (1) {
    if (!proto) return false;
    if (proto === right.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}

小结

好了,JavaScript 的类型转换就讲到这吧。细心的你可能会发现我并没有讲述parseInt()API,我认为大多数情况,使用Number()是更好的选择,如果非要使用 parseInt,请一定记得传入第二个参数 radix 指明解析进制;是的,我也没有讲~运算符,我认为它并不能给你的代码带来清晰易读的效果。如果你不幸遇到了这奇怪的代码,记住 🙄

  • ~x 大致等同 ~(x+1)
  • ~~常被用来截除数字值的小数部分

提问

  • 为什么形如 'str'.slice() 的调用会成功
  • 如何判断数组
  • [] + {}{} + []
  • [] == ![]{} == []