魔幻語言 JavaScript 系列之型別轉換、寬鬆相等以及原始值
編譯自:
[1] + [2] – [3] === 9!? Looking into assembly code of coercion。
全文從兩個題目來介紹型別轉換、寬鬆相等以及原始值的概念:
[1] + [2] – [3] === 9
如果讓 a == true && a == false 的值為 true
第二道題目是譯者加的,因為這其實是個很好的例子,體現出 JavaScript 的魔幻之處
變數值都具有型別,但仍然可以將一種型別的值賦值給另一種型別,如果是由開發者進行這些操作,就是
型別轉換
(顯式轉換)。如果是發生在後臺,比如在嘗試對不一致的型別執行操作時,就是
隱式轉換
(強制轉換)。
型別轉換(Type casting)
基本包裝型別(Primitive types wrappers)
在 JavaScript 中除了
null
和
undefined
之外的所有基本型別都有一個對應的基本包裝型別。透過使用其建構函式,可以將一個值的型別轉換為另一種型別。
String
(
123
);
// ‘123’
Boolean
(
123
);
// true
Number
(
‘123’
);
// 123
Number
(
true
);
// 1
基本型別的包裝器不會儲存很長時間,一旦完成相應工作,就會消失
需要注意的是,如果在建構函式前使用
new
關鍵字,結果就完全不同,比如下面的例子:
const
bool
=
new
Boolean
(
false
);
bool
。
propertyName
=
‘propertyValue’
;
bool
。
valueOf
();
// false
if
(
bool
)
{
console
。
log
(
bool
。
propertyName
);
// ‘propertyValue’
}
由於
bool
在這裡是一個新的物件,已經不再是基本型別值,它的計算結果為
true
。
上述例子,因為在 if 語句中,括號間的表示式將會裝換成布林值,比如
if
(
1
)
{
console
。
log
(
true
);
}
其實,上面這段程式碼跟下面一樣:
if
(
Boolean
(
1
)
)
{
console
。
log
(
true
);
}
parseFloat
parseFloat
函式的功能跟
Number
建構函式類似,但對於傳參並沒有那麼嚴格。當它遇到不能轉換成數字的字元,將返回一個到該點的值並忽略其餘字元。
Number
(
‘123a45’
);
// NaN
parseFloat
(
‘123a45’
);
// 123
parseInt
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
可以使用
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] - [3] === 9
希望這些知識都能幫助揭開這個題目的神秘面紗
[1] + [2]
將呼叫
Array。prototype。toString
轉換為字串,然後進行字串拼接。結果將是
“12”
[1,2] + [3,4]
的值是
“1,23,4”
12 - [3]
,減號運算子會將值轉換為 Number 型別,所以等於
12-3
,結果為
9
12 - [3,4] 的值是
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
:
11。9。3 The Abstract Equality Comparison Algorithm
9。1 ToPrimitive
稍作總結
規範很長很詳細,簡單總結就是,對於下述表示式:
x
==
y
型別相同,判斷的就是 x === y
型別不同
如果 x,y 其中一個是布林值,將這個布林值進行 ToNumber 操作
如果 x,y 其中一個是字串,將這個字串進行 ToNumber 操作
如果 x,y 一方為物件,將這個物件進行 ToPrimitive 操作
至於
ToPrimitive
,即求原始值,可以簡單理解為進行
valueOf()
和
toString()
操作。
稍後我們再詳細剖析,接下來先看一個問題。
Question:是否存在這樣一個變數,滿足 x == !x
就像這樣:
// 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
ToPrimitive
看規範:8。12。8
[[DefaultValue]] (hint)
如果是
Date
求原始值,則 hint 是
String
,其他均為
Number
,即先呼叫
valueOf()
再呼叫
toString()
。
比如當 hint 為
Number
,具體過程如下:
呼叫物件的
valueOf()
方法,如果值是原值則返回
否則,呼叫物件的
toString()
方法,如果值是原值則返回
否則,丟擲 TypeError 錯誤
// 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 中,有什麼變化呢
ES6 中 ToPrimitive
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‘
);
}
閱讀更多
Can (a== 1 && a ==2 && a==3) ever evaluate to true?