編譯自:

[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?