編譯自:[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');
}
複製代碼