类型系统
JavaScript 存在两套类型系统,其一是基础类型系统,可以通过 typeof 来检测,该类型系统包括8种类型:
- undefined 值
- Number 类型
- BigInt 大数类型
- String 类型
- Boolean 类型
- Symbol 符号类型
- Object 类型
- function 类型
注:null 值是特殊存在,因为 typeof null === ‘object’。
其二是对象类型系统,可以通过 instanceof 来检测,对象类型系统是基础类型系统“object”的一个分支,例如:Object、Function、Array、RexExp 、Date 和 Promise 等。
类型检测
检测数据类型可以有以下几种方法:
- typeof适合检测基础类型,对于基础类型来说,除了 null 都可以调用 typeof 显示正确的类型。但对于引用数据类型,除了函数之外,都会显示"object"。* instanceof用来检测引用数据类型,本质上是基于原型链判断。* constructor返回构造函数。除了 undefined 和 null 之外,其他类型都可以通过 constructor 属性来判断类型。* ** Object.prototype.toString.call()**返回格式为 [object xxx],是判断一个变量的类型最准确的方法。注:
一道面试题:为什么是Object.prototype.toString.call() ?
这是因为 toString 为 Object 的原型方法,而 Array ,function 等类型作为 Object 的实例,都重写了 toString 方法。不同的对象类型调用 toString 方法时,根据原型链的知识,调用的是对应的重写之后的 toString 方法( function 类型返回内容为函数体的字符串,Array 类型返回元素组成的字符串),而不会去调用 Object 上原型toString 方法(返回对象的具体类型),所以采用 obj.toString() 不能得到其对象类型,只能将 obj 转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用 Object 上原型 toString 方法。
包装类:面向对象的妥协
面向对象的语言通常认为“一切都是对象”。于是在“对象类型系统”中就出现了一个问题:如果是这样,那么 number 基础类型与 Number 对象类型,以及其他基础类型与相应的对象类型如何被统一呢?
为了实现“一切都是对象”的目标,JavaScript在类型系统上做出了一些妥协,其结果是:为部分基础类型系统中的“值类型”设定对应的包装类;然后通过包装类,将“值类型数据”作为对象来处理。
基础类型 | 包装类 |
---|---|
boolean | Boolean |
number | Number |
string | String |
symbol | Symbol |
这样一来,基础类型数据通过包装类转换而来的结果,和对象类型系统中的每一个实例一样,都成了理论上的“对象”。
但需要注意的是,值类型数据经过"包装类"包装后得到的对象与原数据将不再是同一数据,只是两者具有等同的值而已。
显式包装
通过 Boolean()、Number()、String()、Symbol()、Object() 等显式的将四种值类型数据包装成对应的对象。
隐式包装
对于值类型数据来说,如果它用作普通求值运算或赋值运算,那么是以“非对象”的形式存在的。
'hello' + 'world!'
但是当对值类型数据进行对象类型系统的相关运算时,比如成员属性的存取、方法的调用等,就需要包装成对象:
var x = 100;
// 成员属性的取
x.constructor
x['constructor']
// 方法调用
x.toString
// delete运算
delete x.toString
但也有例外,比如:instanceof 运算不会对原数据进行包装。
对象 — 值类型的隐式转换
在进行数学运算的情况下,对象会被自动转换为原始值,然后对这些原始值进行运算,并得到运算结果。
这是一个重要的限制:因为 obj1 + obj2
(或者其他数学运算)的结果不能是另一个对象!
例如,我们无法使用对象来表示向量或矩阵(或成就或其他),把它们相加并期望得到一个“总和”向量作为结果。这样的想法是行不通的。
因此,由于我们从技术上无法实现此类运算,所以在实际项目中不存在对对象的数学运算。如果你发现有,除了极少数例外,例如日期相减或比较(Date
对象),其他情况通常是写错了。
转换规则
1.转换为布尔值——所有的对象在布尔上下文(context)中均为 true
,因此,只需关注字符串和数字转换。
2.转换为数值——发生在对象相减或应用数学函数时。
3.转换为字符串——发生在像 alert(obj)
这样输出一个对象和类似的上下文中。
如何决定应用哪种转换?
类型转换在各种情况下有三种变体。它们被称为 “hint”,在规范所述:
"string"
对象到字符串的转换,发生在当我们对期望一个字符串的对象执行操作时,如 “alert”,将对象作为属性键之类的情况下。
转换方法:obj[Symbol.toPrimitive](hint)
--> obj.toString()
--> obj.valueOf()
// 输出
alert(obj);
// 将对象作为属性键
anotherObj[obj] = 123;
"number"
对象到数字的转换,例如当我们进行数学运算时(除了二元加法),以及大多数内建的数学函数。
转换方法:obj[Symbol.toPrimitive](hint)
-->obj.valueOf()
--> obj.toString()
// 显式转换
let num = Number(obj);
// 数学运算(除了二元加法)
let n = +obj; // 一元加法
let delta = date1 - date2;
// 小于/大于的比较
let greater = user1 > user2;
"default"
在少数情况下发生,当运算符“不确定”期望值的类型时。
例如,二元加法 +
可用于字符串连接(当运算元包含字符串时),也可以用于数字(相加)。因此,当二元加法得到对象类型的参数时,它将依据 "default"
hint 来对其进行转换。
此外,如果对象被用于与字符串、数字或 symbol 进行 ==
比较,这时到底应该进行哪种转换也不是很明确,因此使用 "default"
hint。
“default” 的转换方法与“number”一致,obj[Symbol.toPrimitive](hint)
-->obj.valueOf()
--> obj.toString()
// 二元加法使用默认 hint
let total = obj1 + obj2;
// obj == number 使用默认 hint
if (user == 1) { ... };
总结
为了进行转换,JavaScript 尝试查找并调用三个对象方法:
1.调用 obj[Symbol.toPrimitive](hint)
,如果这个方法存在的话,就不再需要 obj.valueOf()
或 obj.toString()
了。
2.否则,如果 hint 是 "string"
—— 先尝试调用 obj.toString()
,不行的话调用 obj.valueOf()
。
3.否则,如果 hint 是 "number"
或 "default"
—— 先尝试调用 obj.valueOf()
,不行的话调用 obj.toString()
。
Symbol.toPrimitive
任何对象都可以通过 Symbol.toPrimitive 这个符号属性来改变它作为值得效果。一旦对象具有 Symbol.toPrimitive 属性,那么 value() 和 toString() 在值运算的隐式转换中就无效了。
let x = new Number(100);
x[Symbol.toPrimitive] = () => 0;
console.log(x); // 0
通常,它被实现转换方法,像这样:
let user = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint) {
alert(`hint: ${hint}`);
return hint == "string" ? `{name: "${this.name}"}` : this.money;}
};
// 转换演示:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500
从代码中我们可以看到,user[Symbol.toPrimitive]
方法处理了所有的转换情况。
toString/valueOf
如果没有 Symbol.toPrimitive
,那么 JavaScript 将尝试寻找 toString
和 valueOf
方法。
这些方法必须返回一个原始值。如果 toString
或 valueOf
返回了一个对象,那么返回值会被忽略(和这里没有方法的时候相同)。
默认情况下,普通对象的 toString
和 valueOf
方法:
toString
方法返回一个字符串"[object Object]"
。函数、数组等对象重写了 toString 方法,表现方式有所不同。valueOf
方法返回对象自身。
let a = () => {};
a.toString(); // '() => {}'
a.valueOf(); // () => {}
let b = [1, 2];
b.toString(); // '1, 2'
b.valueOf(); // [1, 2]
let c = {'c': 1};
c.toString(); // '[Object Object]'
c.valueOf(); // {'c': 1}
注:
由于历史原因,发生隐式转换时,如果 toString
或 valueOf
返回一个对象,则不会出现 error,但是这种值会被忽略(就像这种方法根本不存在)。这是因为在 JavaScript 语言发展初期,没有很好的 “error” 的概念。
相反,Symbol.toPrimitive
更严格,它 必须 返回一个原始值,否则就会出现 error。
动态类型 — 值类型的隐式转换
JavaScript 中数据的类型并不是由变量声明来决定的,它的类型将会延迟到它绑定一个数据才能确定。而“变量声明时没有类型含义”也带来了一个问题:既然解释器并不知道源代码中的变量类型,那么也就无法检错。例如:
value_3 = value_1 + value_2;
在引擎对这行源代码做语法解释期间,并不能确知二者能否进行“+”运算,更无法确定“+”运算所表达的是算术上的“求和”还是字符串的“连接”。
由于类型只能在代码执行过程中才能获知,所以 JavaScript 也就只能采用“运算过程中执行某种类型转换规则”来解决不同类型间的运算问题。
问题是:无论是 toString()、valueOf() 还是 Symbol.toPrimitive,这些转换规则背后的逻辑都只确保了“返回结果是 [ 值 ]”,并没有确保这些 “[ 值 ]” 是表达式运算所预期的那种类型。
所以在 JavaScript 的表达式运算中发生的,最终必然是以值类型为基础的类型转换。
换句话,“引用->值” 和 “值 -> 值“的类型转换是 JavaScript 中类型转换的终极目标。
总结
从上面,我们知道了类型转换经过两个运算阶段:
1.对象类型——值类型。
2.值类型——值类型。
例如:
let obj = {
// toString 在没有其他方法的情况下处理所有转换
toString() {
return "2";}
};
alert(obj * 2); // 4,对象被转换为原始值字符串 "2",之后它被乘法转换为数字 2。
1.乘法 obj * 2
首先将对象转换为原始值(字符串 “2”)。
2.之后 "2" * 2
变为 2 * 2
(字符串被转换为数字)。
二元加法在同样的情况下会将其连接成字符串,因为它更愿意接受字符串:
let obj = {
toString() {
return "2";}
};
alert(obj + 2); // 22("2" + 2)被转换为原始值字符串 => 级联
值类型 — 值类型的隐式转换
参考表格所列。
ToNumber
值类型 | 转换结果 |
---|---|
undefined | NaN |
Null | 0 |
true | 1 |
false | 0 |
string | 根据内部规则来进行转换 |
Object | 先尝试调用 toPrimitive,否则 valueOf() |
symbol | 报错 |
注:number 到 string 的转换内部规则比较复杂,如有必要,建议 Number() 显式转换。
ToString
值类型 | 转换结果 |
---|---|
undefined | ’Undefined‘ |
boolean | ‘true’ or ‘false’ |
number | 对应的字符串类型 |
Object | 先尝试调用 toPrimitive,否则 toString() |
symbol | 报错 |
ToBoolean
值类型 | 转换结果 |
---|---|
undefined | false |
number | 0 和 NaN 返回 false,其他返回 true |
Object | true |
symbol | true |
对象 — 值类型的显式转换
对象与值类型之间存在一种简单、显式的转换方法。
x\转换到 | Object | number | boolean | string | symbol |
---|---|---|---|---|---|
值或对象 | Object(x) | Number(x) | Boolean() | String() | Symbol() |
值类型 — 值类型的显式转换
ToNumber
包括 parseInt() 、parseFloat(),但不建议使用。一般来说,使用 Number() 来进行转换,将是更加安全的策略。
ToString
包括 String()、toString() 方法,甚至是类似 toLocaleSting() 等的方法。一般来说,建议使用 String() 方法。
ToBoolean
使用”!!“确实可以用来显式转换为布尔值。但推荐使用 Boolean() 方法。