共计 5582 个字符,预计需要花费 14 分钟才能阅读完成。
记不清在某处看见了这一比较,当时对强制转换这块理解的还没有特别清晰,故有此一文。以为我会以标题的表达式来展开?那你就错了,下面直接上 [] == ![] 是如何转换的:
- 因为! 运算符的优先级比较高,所以表达式右侧先运行![],得出 false,表达式变为[] == false
- 强制将 false 转换为 0,表达式变为[] == 0
- 将 [] 强制转换为原始类型后为 ””,表达式变为 ”” == 0
- 将 ”” 转换为 Number 类型,表达式变为 0 == 0
- 两侧类型相同,直接返回 0 === 0 的结果 true
前言
本文旨在总结 js 中强制转换的规则及触发强制转换的几种场景。ES6 标准中定义了六种原始类型,分别是 Undefined,Null,String,Number,Boolean,Symbol。本文中的强制转换指的是在代码运行时,触发了数值的隐式转换,而不是代码显示的指定转换操作。
原始类型间强制转换
发生在原始类型之间的转换,以个人的理解是其他类型转换为 String,Number 或者 Boolean 类型。
转换为 String 类型
其他原始类型转换为 String 类型通常发生在 + 两边存在字符串时,会将 + 另一边的值转换为 String 类型。
考虑如下代码:
var strAddNum = "test" + 1;
var numAddStr = 1 + "test";
var boolAddStr = true + "test";
var undAddStr = undefined + "";
var nullAddStr = null + "";
console.log(strAddNum);
console.log(numAddStr);
console.log(boolAddStr);
console.log(undAddStr);
console.log(nullAddStr);
代码传送门,以上代码的运行结果均为字符串。其他原始类型转换为 String 类型基本是其值的字符串形式,具体如下:
- Undefined,”undefined”
- Null,”null”
- Boolean,”true” 或 ”false”
- Number,值为 NaN,”NaN”
- Number,值为 + 0 或 -0,”0″
- Number,值为 +Infinity,”Infinity”
- Number,值为 -Infinity,”-Infinity”
Number 转为字符串具体可参考 ES2018 7.1.12.1 章节
注意:Symbol 类型无法转换为 String 类型。
转换为 Number 类型
转换为 Number 类型的情况,+-*/% 等运算中,除了 + 之外其他运算均会转换成 Number 类型,+ 运算时需要满足两侧未出现 String 类型,该值才会被转换为 Number 类型。+ 运算时情况较为复杂,后面会专门描述其相关转换规则。考虑如下代码:
var trueAddTrue = true + true;
var trueAddFalse = true + false;
var trueAdda0 = true + 0;
var nullAddTrue = null + true;
var undefinedAdd0 = undefined + 0;
var strAdd0 = "" + 0;
console.log(trueAddTrue);
console.log(trueAddFalse);
console.log(trueAdda0);
console.log(nullAddTrue);
console.log(undefinedAdd0);
console.log(strAdd0);
代码传送门,在运行代码之前可以先考虑下以上代码答打印的结果分别是什么?然后再运行,看是否符合你的预期。其他原始类型转换为 Number 类型的具体如下:
- Undefined,NaN
- Null,+0
- Boolaen,值为 true,1
- Boolean,值为 false,+0
- String,不可转为 Number 的,NaN
- String,可转为 Number 的就是其对应的 Number 值(具体可参考 ES2018 7.1.3.1)
注意:Symbol 类型同样无法转换为 Number 类型。
转换为 Boolean 类型
转换为 Boolean 类型的情况较为简单,除了以下情况转换为 Boolean 类型会是 false,其他情况均是 true
- Undefined
- Null
- Number,+0,-0,NaN
- String,长度为 0 的字符串
这几种 false 的情况在 ES 标准中有明确规定 7.1.2
对象强制转换为原始类型
ES 中将对象转换为原始类型的算法,大致可描述为三种情形:
- 如果该对象设置了[Symbol.toPrimitive],调用该函数,如果其返回值为非 Object 类型则返回结果,否则抛出 TypeError 异常
- 若未指定转换提示则转换提示为 ”default”
- 若转换提示为 ”default”,则将其置为 ”number”
- 当指定转换提示为 ”number” 时先调用该对象的 valueOf 函数并判断其结果,如果是原始类型则返回结果,否则调用该对象的 toString 函数并判断其返回结果,如果结果为原始类型则返回,否则抛出异常 TypeError
- 当指定转换提示为 ”string” 时先调用 toString 函数并判断其返回结果,如果是原始类型则返回结果,否则调用该对象的 valueOf 函数并判断其返回结果,如果结果为原始类型则返回,否则抛出异常 TypeError
上述三种情形中第一种情形优先级最高,第二三种情形优先级并列,具体需要根据使用场景判断是哪一种。其中的指定转换提示是 ES 标准内部调用该算法时指定的。
第一种情形只有 Symbol 对象和 Date 对象内置了 [Symbol.toPrimitive],且该属性的 writeable 为 false,enumerable 为 false,configurable 为 true
对象转换为原始类型时发生的强制转换非特殊情况均为第二种,第三种情况较为少见。在正常编码工作中应该使用第二种情形就够用了,第三种情形几乎不会出现,要了解更多细节可查阅 ES 标准。
var test = {[Symbol.toPrimitive]: function(hint) {console.log(hint)
},
valueOf: function() {console.log("valueOf")
},
toString: function() {console.log("toString")
}
}
test + ""; //"default"test * 0; //"number"String(test); //"string"
代码传送门上述代码指定了分别指定了 test 对象的 [Symbol.toPrimitive],valueOf 和 toString 函数,可以观察到并 valueoOf 和 toString 函数均未被调用,指定的[Symbol.toPrimitive] 函数可以接受一个提示参数,这个参数就是强制转换时的强制转换提示。这样我们在函数中就可以根据转换场景的不同分别返回不同的值。
原始类型强制转换为对象(装箱)
在开始描述这个问题之前,可以先思考一下,都有哪些场景会是强制的将原始类型转换为对象,其实这种场景几乎在 js 代码中随处可见,考虑如下代码:
var str = "testString";
str.replace("test", "");
如上代码中定义的 str 的值并不是一个对象而是一个原始类型 String,原始类型显然是没有方法可以调用的。
实际上这里的 str 在执行 str.replace 时 str 其值会被强制转换为对象,得到一个 String 类型的实例对象,而该实例的原型上定义了一系列方法,且该实例是无法被获取的,在执行完这行代码后,该实例就会被回收,所以这里的 str 依然是一个字符串。
考虑如下代码:
var a = 3;
a.fn = function(){};
a.fn();
强制转换的几种场景
在 js 代码中会出现强制转换的场景通常有三种:
- + 运算
- -,*,/,% 运算
- == 比较
- 作为判断条件
+ 运算
一元 + 运算
做一元 + 运算时,均会被强制转为 Number 类型,例如
var a = {[Symbol.toPrimitive]: function(hint) {console.log(hint); // number
if(hint === "number") {return 2;} else {return 9;}
}
};
console.log(+a); // 2
var b = "3";
console.log(+b); // 3
代码传送门
二元 + 运算
二元 + 运算为几种强制转换中复杂度仅次于 == 比较的一种情形,个人总结其转换步骤如下:
- 先将两侧数值强制转换为原始类型(未指定转换提示,即转换提示为 hint default);
- 若两侧存在 String 类型,均转换为 String 类型,则返回两侧拼接的字符串;
- 若第 2 未返回,则两侧数值强制转换为 Number 类型,返回计算结果;
var a = "";
var b = {[Symbol.toPrimitive]: function(hint) {console.log(hint); // "default"
if(hint === "default") {return 2;} else {return 9;}
}
};
var c = a + b; // 这里 b 转换为原始类型返回的是 Number 类型 2,由于 a 是 "",所以 b 被转换为"2",后与"" 拼接返回 "2"
console.log(c); // "2"
var d = 3;
var e = {[Symbol.toPrimitive]: function(hint) {console.log(hint); // "default"
if(hint === "default") {return 2;} else {return 9;}
}
};
var f = d + e; // 这里 e 转换为原始类型返回的是 Number 类型 2,由于两侧均没有 String 类型,则至第 3 步,强制转换为 Number 后返回两侧相加的结果 5
console.log(f); // 5
代码传送门
-,*,/,% 运算
这几个运算符这涉及的强制转换都是转换为 Number 类型的,所以这里只要搞清楚转换为 Number 是怎样的过程就可以了。上文中已经对原始类型转换为 Number 类型做了描述,这里补充一下 Object 转换为 Number 的过程:
- 将对象转换为原始类型,且转换时会指定转换提示为 ”number”;
- 转换为原始类型后再根据原始类型转换为 Number 类型进行转换;
var a = 8;
var b = {[Symbol.toPrimitive]: function(hint) {console.log(hint); // "number"
if(hint === "number") {return 2;} else {return 9;}
}
};
console.log(a-b); // 6
console.log(a/b); // 4
console.log(a*b); // 16
console.log(a%b); // 0
console.log(undefined * 0); //NaN
console.log(null * -1); // 0
console.log(false * -1); //0
console.log(true * -1); // -1
console.log("1" * -1); // -1
代码传送门
== 比较
== 比较的基础 === 比较
x === y,其具体比较步骤如下:
- 若 x 和 y 的类型不一致,返回 false;
- 若 x 和 y 为 Number 类型,则若 x 和 y 中有一个为 NaN 返回 false;若 x 和 y 的值相等则返回 true;若 x 是 +0,y 是 - 0 或者 x 是 -0,y 是 + 0 则返回 true;其他情况返回 false;
- 若 x 和 y 为 Undefined 类型,返回 true
- 若 x 和 y 为 Null 类型,返回 true
- 若 x 和 y 为 String 类型,其值相同则返回 true,否则返回 false
- 若 x 和 y 为 Boolean 类型,其值均为 true 或均为 false 返回 true,否则返回 false
- 若 x 和 y 为 Symbol 类型,其值为同一个 Symbol 值则返回 true,否则返回 false
- 若 x 和 y 为 Object 类型,其值为同一个对象(其引用地址相同)则返回 true,否则返回 false
x == y 规则
== 比较的转换规则虽然稍微多一点,实际上也就几条规则,两侧的数值类型符合哪种就按哪种去转换,只不过有的可能需要转两次,具体如下:
- 如果两侧类型相等则直接返回 === 的结果;
- 若 x 为 undefined 和 y 为 null 或 x 为 null 和 y 为 undefined,返回 true
- 若两侧为 String 类型和 Number 类型,将 String 类型转换为 Number 类型,继续用 == 比较
- 若有一侧存在 Boolean 类型,将 Boolean 类型转换为 Number 类型,继续用 == 比较
- 若两侧为 String,Number 或 Symbol 类型和 Object 类型,将 Object 类型转换原始类型,继续用 == 比较
- 其他返回 false
下面列举一些可能有点违反直觉的比较
"0" == false; // true
false == 0; // true
false == ""; // true
false == []; // true
""== 0; // true"" == []; // true
0 == []; // true
[] == ![]; //true
作为条件判断
这种情形到没有太多可说的,基本上就是,除了 undefined,null,+0,-0,NaN,”” 这六个值会被转为 false,其他情况均为 true;
出现将不是 Boolean 类型的值强制转换的情况为
- if(…)
- for(…;…;…)第二个条件表达式
- while(…)和 do…while(…)中的条件表达式
- …?…:… 三元表达式中的第一个条件表达式
- || 和 &&
结论
其实上面描述了这么多,日常开发环境中用到比较多的应该是作为判断条件,+,== 这三种情况了,这三种中最常见的应该是判断条件的情况了,这种情况反而是最简单的一种了。