前言
ECMAScript是可以动态转化类型的动态弱类型语言,和强类型语言如 Java 比的话,看起来更“灵活”,但是会让初学者 捉摸不透 琢磨不透,举个例子:
// java
int java1 = 2 + "3"; // 报错
boolean java2 = "1" == 2; // 报错
// javascript
const js1 = 2 + "3"; // 23
const js2 = 2 == "3"; // false2
3
4
5
6
7
java很好理解,申明变量的时候规定了变量的数据类型,等号右侧值和等号左侧数据类型不相等必然会报错。
然而JavaScript就很“灵活”了,申明变量的时候不用指定数据类型,用var、let、const都行,只要表达式没问题,我都可以给你返回数据,留下一脸懵逼的我和你,看不懂为什么返回这玩意。
有的人选择逃避不去深入了解,平时就根据自己猜想和几次失败经验写代码,程序能跑通我就万事大吉的心态。其实原理没有那么可怕,静下心学习一下,结合实例去实验、对比和参考,多用几次就能记住啦。
开始正文之前,先出一道题:以下两个运算返回值是什么?
1、[ ] == ! [ ] (这不是颜文字,这不是颜文字,这不是颜文字) 2、2 + [3,4,5]
类型转换
在了解类型之间转换关系之前,需要了解以下几个知识点,valueOf()、toString()和ToPrimitive():
valueOf()
JavaScript调用valueOf方法将对象转换为原始值。你很少需要自己调用valueOf方法;当遇到要预期的原始值的对象时,JavaScript会自动调用它。
JavaScript的许多内置对象都重写了该函数,以实现更适合自身的功能需要。因此,不同类型对象的valueOf()方法的返回值和返回值类型均可能不同。
| 对象 | 返回结果 |
|---|---|
| Array | 数组对象本身(对象类型). |
| Boolean | 布尔值本身(原始类型). |
| Date | 从 1970 年 1 月 1 日午夜开始计的毫秒数 UTC(原始类型). |
| Function | 函数本身(对象类型). |
| Number | 数字值本身(原始类型). |
| Object | 对象本身(对象类型). 前提是该对象自己没有重写 ValueOf() 方法 |
| String | 字符串值(原始类型). |
| Math/Error | 没有 valueOf 方法. |
| 我们在创建自己的对象时,应该适当的重写valueOf()方法,举个例子: |
var a = {
value:3
}
var sum = 1 + a
console.log(sum) // 1[object Object]
var a = {
value:3,
valueOf(){
return this.value
}
}
var sum= 1 + a
console.log(sum) // 42
3
4
5
6
7
8
9
10
11
12
13
14
toString()
简单来说就是,返回这个对象的字符串表示。用一个字符串来描述这个对象的内容。
每个对象都有一个toString()方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString()方法被每个Object对象继承。默认情况下,如果此方法在自定义对象中没有被覆盖(重写),toString()就会返回 "[object type]",其中type是对象的类型。
具体转换规则可查看:http://www.ecma-international.org/ecma-262/#sec-object.prototype.tostring
和valueOf()对比,虽然valueOf()期望返回原始类型的值,但是实际上有一些对象在逻辑上无法找到与之对应的原始值,因此只能返回对象本身。 toString()则不一样,因为不管什么对象,我们总有办法“描述”它,因此js内置对象的toString()总能返回一个原始string类型的值。
let v={
name:'Vinsea'
}
v.toString() // [object Object]2
3
4
我们在创建自己的对象时,应该适当的重写toString()方法,举个例子:
let v={
name:'Vinsea',
toString(){
return 'My name is ' + this.name
}
}
v.toString() // My name is Vinsea
vv == "My name is Vinsea" // true2
3
4
5
6
7
8
对象转原始类型
调用 ToPrimitive(input [ , PreferredType ])
它会将input转化成一个原始类型的值。PreferredType参数要么不传入,要么是Number 或 String。
- 如果PreferredType参数是Number,ToPrimitive这样执行:
- 如果input本身就是原始类型,直接返回input。
- 调用input.valueOf(),如果结果是原始类型,则返回这个结果。
- 调用input.toString(),如果结果是原始类型,则返回这个结果。
- 抛出TypeError异常。
- 如果PreferredType参数不为Number时的执行顺序:
- 如果input本身就是原始类型,直接返回input。
- 调用input.toString(),如果结果是原始类型,则返回这个结果。
- 调用input.valueOf(),如果结果是原始类型,则返回这个结果。
- 抛出TypeError异常。
- 如果PreferredType参数没有传入:
- 如果input是内置的Date类型,PreferredType 视为String
- 否则PreferredType 视为 Number
来源:http://www.ecma-international.org/ecma-262/#sec-toprimitivehttp://www.ecma-international.org/ecma-262/#sec-ordinarytoprimitive
var a = {}
a.toString = function () {
return 1
}
a.valueOf = function () {
return 2
}
String(a) // "1"
Number(a) // 2
a + '' // "2"
+ a // 2
a.toString = null
String(a) // "2"
a.valueOf = null
String(a) // 异常2
3
4
5
6
7
8
9
10
11
12
13
14
15
数据类型转换
在JS中常见的类型转换可以分为三种情况:转为 Boolean、转为 String、转为 Number,这三种情况对应的抽象操作为: ToBoolean(argument) 、 ToString(argument) 、 ToNumber(argument),我们不能直接在JS中使用,它们是(ECMAScript内置函数)在实现js的过程中用来做类型转换的。
转为Boolean
转Boolean调用的是 ToBoolean(argument) 方法,规则如下:
| 参数类型 | 返回结果 |
|---|---|
| Undefined | false. |
| Null | false. |
| Boolean | 本身. |
| Number | 如果参数是 +0, -0, 或者 NaN, 返回 false; 否则返回 true. |
| String | 如果参数是空字符串 (长度是0), 返回 false; 否则返回 true. |
| Symbol | true. |
| Object | true. |
来源:http://www.ecma-international.org/ecma-262/#sec-toboolean
所以,转为Boolean总结一下就是: 除了本身是false、Undefined、Null、+0,-0,NaN、空字符串,其他都返回true。
if(!undefined){
console.log(!undefined); // true
}
if(!null){
console.log(!null); // true
}
if(!NaN){
console.log(!NaN); // true
}
if(!0){
console.log(!0); // true
}
if(!''){
console.log(!''); // true
}
if('Vinsea'){
console.log('Vinsea'); // Vinsea
}
if([]){
console.log([]); // []
}
if({}){
console.log({}); // {}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
转为String
转String调用的是 ToString(argument) 方法,规则如下:
| 参数类型 | 返回结果 |
|---|---|
| Undefined | "undefined". |
| Null | "null". |
| Boolean | 如果参数是true,返回"true"; 否则返回 "false". |
| Number | 返回 NumberToString(argument) 方法的结果. |
| String | 本身. |
| Symbol | 抛出 TypeError exception 异常. |
| Object | 1.先转换为原始类型:primValue = ToPrimitive(argument, hint String). 2.对primValue调用ToString(primValue),返回该结果. |
转为Number
转Number调用的是 ToNumber(argument) 方法,规则如下:
| 参数类型 | 返回结果 |
|---|---|
| Undefined | NaN. |
| Null | +0. |
| Boolean | 如果参数是 true, 返回 1; 如果是 false,返回 +0. |
| Number | 本身. |
| String | 将字符串中的内容转化为数字(比如"12"->12),如果转化失败则返回NaN(比如"12vinsea"->NaN). |
| Symbol | 抛出 TypeError exception 异常. |
| Object | 1.先转换为原始类型:primValue = ToPrimitive(argument, hint String). 2.对primValue调用ToNumber(primValue),返回该结果. |
四则运算
减法、乘法、除法
转换规则:只要其中一方是数字,那么另一方就会被转为数字(ToNumber)
console.log(2 * '4') // 8
console.log(2 * 'vinsea') // NaN
console.log(8 / '4') // 2
console.log('6' - 2) // 4
console.log(3 * { valueOf: function () { return 5 } }); // 152
3
4
5
加法
转换规则:
- 加号两端先调用 ToPrimitive 方法
- 只要加号两端的任意一个操作数是字符串,那么返回结果就是字符串拼接,否则表示算数的加法
- 如果加号两端不是字符串或者数字,那么就转换为数字或者字符串
所以开头的第二题,分析过程是:
console.log(2 + [3,4,5])
// 首先,加号左边是原始值number,右边是一个数组对象
// 两边调用 ToPrimitive ,结合刚才的 ToPrimitive 规则:
// PreferredType参数是空,所以PreferredType默认参数变成Number,继续调用 ToPrimitive
// ToPrimitive([3,4,5],Number),参数是Number
// [3,4,5]不是原始类型,所以继续下一步
// [3,4,5]调用valueOf返回的不是原始类型,继续下一步
// [3,4,5]调用toString,返回 3,4,5 是原始类型,所以
// 2 + [3,4,5] 变成了:2 + '3,4,5'
// 加号其中一方是字符串,根据加法规则2:
// 2 + '3,4,5' 变成了:'2' + '3,4,5'
// 所以 2 + [3,4,5] = 23,4,52
3
4
5
6
7
8
9
10
11
12
13
来源: http://www.ecma-international.org/ecma-262/#sec-addition-operator-plushttp://www.ecma-international.org/ecma-262/#sec-getvalue
文档中可能需要用到的术语解释(暂时这么翻译)
lref:符号左边表达式的计算结果 lval:lref 调用 Getvalue 之后的结果 lprim:lval 调用 ToPrimitive 之后的结果 lstr:lprim 调用 ToString 之后的结果 lnum:lprim 调用 ToNumber 之后的结果
rref:符号右边边表达式的计算结果 rval:rref 调用 Getvalue 之后的结果 rprim:rval 调用 ToPrimitive 之后的结果 rstr:rprim 调用 ToString 之后的结果 rnum:rprim 调用 ToNumber 之后的结果
比较运算
双等号 ==
在ES规范中称为 Abstract Equality Comparison(抽象相等比较),规则如下:
| x | y | 返回结果 |
|---|---|---|
| null | undefined | true |
| undefined | null | true |
| Number | String | x == ToNumber(y) |
| String | Number | ToNumber(x) == y |
| Boolean | Number | ToNumber(x) == y |
| Number | Boolean | x == ToNumber(y) |
| String/Number | Object | x == ToPrimitive(y) |
| Object | String/Number | ToPrimitive(x) == y |
如果都不是表格中的情况,则返回 false
注:如果x和y类型一致的话,就用三等号( === )比较判断
http://www.ecma-international.org/ecma-262/#sec-abstract-equality-comparison
三等号 ===
在ES规范中称为 Strict Equality Comparison(严格相等比较),规则如下: // TODO
如果 x 和 y 类型不一致,返回 false 如果 x/y 是 Number 类型:
| x | y | 返回结果 |
|---|---|---|
| NaN | Number | false |
| Number | NaN | false |
| +0 | -0 | true |
| -0 | +0 | true |
| 都不满足返回false |
如果 x/y 不是Number类型,调用 SameValueNonNumber(x,y) 方法:
如果都不是表格中的情况,则返回 false
http://www.ecma-international.org/ecma-262/#sec-abstract-equality-comparisonhttp://www.ecma-international.org/ecma-262/#sec-samevaluenonnumber
[] == [] // false
// 1. 两遍类型都为 Object,比较引用地址,不同返回false
[] == ![] // true
// 1. ![]强制类型转换 变为 [] == false
// 2. 返回 [] == ToNumber(false), 即 [] == 0
// 3. 返回ToPromitive([]) == 0,数组的valueOf为本身,不是原始值,则返回toString()即 "" == 0
// 4. 返回ToNumber("") == 0, 即 0 == 0
// 5. 返回 true
// 类似上面
{} == !{} // false
{} == {} // false2
3
4
5
6
7
8
9
10
11
12
13
大于/小于号
// TODO
var a = { b: 42 }
var b = { b: 43 }
a < b // false
a == b // false
a > b // false
a <= b // true
a >= b // true2
3
4
5
6
7
8
http://www.ecma-international.org/ecma-262/#sec-abstract-relational-comparison
强制类型转换
直接调用Boolean(value)、Number(value)、String(value)完成的类型转换,属于强制类型转换。
new Boolean(value)、new Number(value)、new String(value)传入各自对应的原始类型的值,可以实现“装箱”——将原始类型封装成一个对象。
其实这三个函数不仅仅可以当作构造函数,它们可以直接当作普通的函数来使用,将任何类型的参数转化成原始类型的值:
Boolean('Vinsea'); // true
Number('Vinsea'); // Vinsea
String({name:'Vinsea'}); // "[object Object]"2
3
底层就是调用刚才说的三种转换:ToBoolean ( argument )、ToNumber ( argument )、ToString ( argument )
思考题
[ ] + [ ] = ? [ ] + { } = ? { } + [ ] = ?
参考 http://www.ecma-international.org/ecma-262/#sec-type-conversionhttp://0313.name/archives/480https://blog.csdn.net/just_do_it2009/article/details/71079784https://www.jianshu.com/p/b161aeecb6d6http://2ality.com/2012/01/object-plus-object.html