编译自:[1] + [2] – [3] === 9!? Looking into assembly code of coercion.html
全文从两个题目来介绍类型转换、宽松相等以及原始值的概念:web
[1] + [2] – [3] === 9app
若是让 a == true && a == false 的值为 true函数
第二道题目是译者加的,由于这实际上是个很好的例子,体现出 JavaScript 的魔幻之处ui
变量值都具备类型,但仍然能够将一种类型的值赋值给另外一种类型,若是是由开发者进行这些操做,就是类型转换(显式转换)。若是是发生在后台,好比在尝试对不一致的类型执行操做时,就是隐式转换(强制转换)。this
在 JavaScript 中除了 null
和 undefined
以外的全部基本类型都有一个对应的基本包装类型。经过使用其构造函数,能够将一个值的类型转换为另外一种类型。编码
String(123); // '123'
Boolean(123); // true
Number('123'); // 123
Number(true); // 1
复制代码
基本类型的包装器不会保存很长时间,一旦完成相应工做,就会消失lua
须要注意的是,若是在构造函数前使用 new
关键字,结果就彻底不一样,好比下面的例子:spa
const bool = new Boolean(false);
bool.propertyName = 'propertyValue';
bool.valueOf(); // false
if (bool) {
console.log(bool.propertyName); // 'propertyValue'
}
复制代码
因为 bool
在这里是一个新的对象,已经再也不是基本类型值,它的计算结果为 true
。prototype
上述例子,由于在 if 语句中,括号间的表达式将会装换成布尔值,好比
if (1) {
console.log(true);
}
复制代码
其实,上面这段代码跟下面同样:
if ( Boolean(1) ) {
console.log(true);
}
复制代码
parseFloat
函数的功能跟 Number
构造函数相似,但对于传参并无那么严格。当它遇到不能转换成数字的字符,将返回一个到该点的值并忽略其他字符。
Number('123a45'); // NaN
parseFloat('123a45'); // 123
复制代码
parseInt
函数在解析时将会对数字进行向下取整,而且可使用不一样的进制。
parseInt('1111', 2); // 15
parseInt('0xF'); // 15
parseFloat('0xF'); // 0
复制代码
parseInt
函数能够猜想进制,或着你能够显式地经过第二个参数传入进制,参考 MDN web docs。
并且不能正常处理大数,因此不该该成为 Math.floor 的替代品,是的,Math.floor
也会进行类型转换:
parseInt('1.261e7'); // 1
Number('1.261e7'); // 12610000
Math.floor('1.261e7') // 12610000
Math.floor(true) // 1
复制代码
可使用 toString 函数将值转换为字符串,可是在不一样原型之间的实现有所不一样。
String.prototype.toString
返回字符串的值
const dogName = 'Fluffy';
dogName.toString() // 'Fluffy'
String.prototype.toString.call('Fluffy') // 'Fluffy'
String.prototype.toString.call({}) // Uncaught TypeError: String.prototype.toString requires that 'this' be a String
复制代码
Number.prototype.toString
返回将数字的字符串表示形式,能够指定进制做为第一个参数传入
(15).toString(); // "15"
(15).toString(2); // "1111"
(-15).toString(2); // "-1111"
复制代码
Symbol .prototype.toString
返回 Symbol(${description})
Boolean.prototype.toString
返回 “true”
或 “false”
Object.prototype.toString
返回一个字符串 [ object $ { tag } ]
,其中 tag 能够是内置类型好比 “Array”,“String”,“Object”,“Date”,也能够是自定义 tag。
const dogName = 'Fluffy';
dogName.toString(); // 'Fluffy' (String.prototype.toString called here)
Object.prototype.toString.call(dogName); // '[object String]'
复制代码
随着 ES6 的推出,还可使用 Symbol 进行自定义 tag。
const dog = { name: 'Fluffy' }
console.log( dog.toString() ) // '[object Object]'
dog[Symbol.toStringTag] = 'Dog';
console.log( dog.toString() ) // '[object Dog]'
复制代码
或者
const Dog = function(name) {
this.name = name;
}
Dog.prototype[Symbol.toStringTag] = 'Dog';
const dog = new Dog('Fluffy');
dog.toString(); // '[object Dog]'
复制代码
还能够结合使用 ES6 class 和 getter:
class Dog {
constructor(name) {
this.name = name;
}
get [Symbol.toStringTag]() {
return 'Dog';
}
}
const dog = new Dog('Fluffy');
dog.toString(); // '[object Dog]'
复制代码
Array.prototype.toString
在每一个元素上调用 toString
,并返回一个字符串,而且以逗号分隔。
const arr = [
{},
2,
3
]
arr.toString() // "[object Object],2,3"
复制代码
若是了解类型转换的工做原理,那么理解强制转换就会容易不少。
加号运算符
在做为二元运算符的 +
若是两边的表达式存在字符串,最后将会返回一个字符串。
'2' + 2 // '22'
15 + '' // '15'
复制代码
可使用一元运算符将其转换为数字:
+'12' // 12
复制代码
其余数学运算符
其余数学运算符(如 -
或 /
)将始终转换为数字。
new Date('04-02-2018') - '1' // 1522619999999
'12' / '6' // 2
-'1' // -1
复制代码
上述例子中,Date 类型将转换为数字,即 Unix 时间戳。
若是原始值是 假,则使用逻辑非将输出 真,若是 真,则输出为 假。 若是使用两次,可用于将该值转换为相应的布尔值。
!1 // false
!!({}) // true
复制代码
值得一提的是,即便 ToInt32 其实是一个抽象操做(仅限内部,不可调用),将一个值转换为一个有符号的 32 位整数。
0 | true // 1
0 | '123' // 123
0 | '2147483647' // 2147483647
0 | '2147483648' // -2147483648 (too big)
0 | '-2147483648' // -2147483648
0 | '-2147483649' // 2147483647 (too small)
0 | Infinity // 0
复制代码
当其中一个操做数为 0 时执行按位或操做将不改变另外一个操做数的值。
在编码时,可能会遇到更多强制转换的状况,好比这个例子:
const foo = {};
const bar = {};
const x = {};
x[foo] = 'foo';
x[bar] = 'bar';
console.log(x[foo]); // "bar"
复制代码
发生这种状况是由于 foo
和 bar
在转换为字符串的结果均为 “[object Object]”
。就像这样:
x[bar.toString()] = 'bar';
x["[object Object]"]; // "bar"
复制代码
使用模板字符串的时候也会发生强制转换,在下面例子中重写 toString
函数:
const Dog = function(name) {
this.name = name;
}
Dog.prototype.toString = function() {
return this.name;
}
const dog = new Dog('Fluffy');
console.log(`${dog} is a good dog!`); // "Fluffy is a good dog!"
复制代码
正由于如此,宽松相等(==)被认为是一种很差的作法,若是两边类型不一致,就会试图进行强制隐式转换。
看下面这个有趣的例子:
const foo = new String('foo');
const foo2 = new String('foo');
foo === foo2 // false
foo >= foo2 // true
复制代码
在这里咱们使用了 new
关键字,因此 foo
和 foo2
都是字符串包装类型,原始值都是 foo
。可是,它们如今引用了两个不一样的对象,因此 foo === foo2
将返回 false
。这里的关系运算符 >=
会在两个操做数上调用 valueOf
函数,所以比较的是它们的原始值,'foo' > = 'foo'
的结果为 true
。
但愿这些知识都能帮助揭开这个题目的神秘面纱
[1] + [2]
将调用 Array.prototype.toString
转换为字符串,而后进行字符串拼接。结果将是 “12”
[1,2] + [3,4]
的值讲师 “1,23,4”
12 - [3]
,减号运算符会将值转换为 Number 类型,因此等于 12-3
,结果为 9
NaN
,由于"3,4"
不能被转换为 Number尽管不少人会建议尽可能避免强制隐式转换,但了解它的工做原理很是重要,在调试代码和避免错误方面大有帮助。
【译文完】
这里看另外一道题目,在 JavaScript 环境下,可否让表达式 a == true && a == false
为 true
。
就像下面这样,在控制台打印出 ’yeah'
:
// code here
if (a == true && a == false) {
console.log('yeah');
}
复制代码
关于宽松相等(==),先看看 ECMA 5.1 的规范,包含 toPrimitive
:
规范很长很详细,简单总结就是,对于下述表达式:
x == y
复制代码
至于 ToPrimitive
,即求原始值,能够简单理解为进行 valueOf()
和 toString()
操做。
稍后咱们再详细剖析,接下来先看一个问题。
就像这样:
// code here
if (x == !x) {
console.log('yeah');
}
复制代码
可能不少人会想到下面这个,毕竟咱们也曾热衷于各类奇技淫巧:
[] == ![] // true
复制代码
但答案毫不仅仅局限于此,好比:
var x = new Boolean(false);
if (x == !x) {
console.log('yeah');
}
// x.valueOf() -> false
// x is a object, so: !x -> false
var y = new Number(0);
y == !y // true
// y.valueOf() -> 0
// !y -> false
// 0 === Number(false) // true
// 0 == false // true
复制代码
理解这个问题,那下面的这些例子都不是问题了:
[] == ![]
[] == {}
[] == !{}
{} == ![]
{} == !{}
复制代码
在来看看什么是 ToPrimitive
看规范:8.12.8 [[DefaultValue]] (hint)
若是是 Date
求原始值,则 hint 是 String
,其余均为 Number
,即先调用 valueOf()
再调用 toString()
。
若是 hint 为 Number
,具体过程以下:
valueOf()
方法,若是值是原值则返回toString()
方法,若是值是原值则返回// valueOf 和 toString 的调用顺序
var a = {
valueOf() {
console.log('valueof')
return []
},
toString() {
console.log('toString')
return {}
}
}
a == 0
// valueof
// toString
// Uncaught TypeError: Cannot convert object to primitive value
// Date 类型先 toString,后 valueOf
var t = new Date('2018/04/01');
t.valueOf = function() {
console.log('valueof')
return []
}
t.toString = function() {
console.log('toString')
return {}
}
t == 0
// toString
// valueof
// Uncaught TypeError: Cannot convert object to primitive value
复制代码
到目前为止,上面的都是 ES5 的规范,那么在 ES6 中,有什么变化呢
7.1.1ToPrimitive ( input [, PreferredType] )
在 ES6 中吗,是能够自定义 @@toPrimitive
方法的,这是 Well-Known Symbols(§6.1.5.1)中的一个。JavaScript 内建了一些在 ECMAScript 5 以前没有暴露给开发者的 symbol,它们表明了内部语言行为。
来自 MDN 的例子:
// 没有 Symbol.toPrimitive 属性的对象
var obj1 = {};
console.log(+obj1); // NaN
console.log(`${obj1}`); // '[object Object]'
console.log(obj1 + ''); // '[object Object]'
// 拥有 Symbol.toPrimitive 属性的对象
var obj2 = {
[Symbol.toPrimitive](hint) {
if (hint == 'number') {
return 10;
}
if (hint == 'string') {
return 'hello';
}
return true;
}
};
console.log(+obj2); // 10 -- hint is 'number'
console.log(`${obj2}`); // 'hello' -- hint is 'string'
console.log(obj2 + ''); // 'true' -- hint is 'default'
复制代码
有了上述铺垫,答案就呼之欲出了
a == true && a == false
为 true
的答案var a = {
flag: false,
toString() {
return this.flag = !this.flag;
}
}
复制代码
或者使用 valueOf()
:
var a = {
flag: false,
valueOf() {
return this.flag = !this.flag;
}
}
复制代码
或者是直接改变 ToPrimitive 行为:
// 其实只需设置 default 便可
var a = {
flag: false,
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
return 10
}
if (hint === 'string') {
return 'hello'
}
return this.flag = !this.flag
}
}
复制代码
这个问题在严格相等的状况下,也是可以成立的,这又是另外的知识点了,使用 defineProperty
就能实现:
let flag = false
Object.defineProperty(window, 'a', {
get() {
return (flag = !flag)
}
})
if (a === true && a === false) {
console.log('yeah');
}
复制代码