类型转换
构造器
基本类型拥有构造器
- String
- Number
- Boolean
- 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
形式
- !!和 Boolean() 显式类型转换
- if(..) 条件判断
- 循环语句中的条件判断
- ? : 三元表达式中的条件判断
- || 和 && 左边的操作数
区别在于三元表达式中的 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
- null 和 undefined 只和对方和自身相等
- 一边是数字值,另一边进行 ToNumber 操作
- 一边是布尔值,两边都进行 ToNumber 操作
- 一边是对象,对象进行 ToPrimitive拆箱 操作
- 两边都是字符串,则比较两个字符串对应的字符编码值(ASCII)
- 两边都是对象,这时比较的是对象地址是否是同一个,和===原理是一样的
<比较
- 双方都进行 ToPrimitive 操作
- 两边都是字符串,根据字母顺序来比较
- 出现非字符串,两边都进行 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]
那null
和undefined
呢?
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.toStringTag
多说一句,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()
的调用会成功 - 如何判断数组
[] + {}
,{} + []
呢[] == ![]
,{} == []