魔幻語言 JavaScript 系列之類型轉換、寬鬆相等以及原始值

編譯自:[1] + [2] – [3] === 9!? Looking into assembly code of coercion.html

全文從兩個題目來介紹類型轉換、寬鬆相等以及原始值的概念:web

[1] + [2] – [3] === 9app

若是讓 a == true && a == false 的值爲 true函數

第二道題目是譯者加的,由於這實際上是個很好的例子,體現出 JavaScript 的魔幻之處ui

變量值都具備類型,但仍然能夠將一種類型的值賦值給另外一種類型,若是是由開發者進行這些操做,就是類型轉換(顯式轉換)。若是是發生在後臺,好比在嘗試對不一致的類型執行操做時,就是隱式轉換(強制轉換)。this

類型轉換(Type casting)

基本包裝類型(Primitive types wrappers)

在 JavaScript 中除了 nullundefined 以外的全部基本類型都有一個對應的基本包裝類型。經過使用其構造函數,能夠將一個值的類型轉換爲另外一種類型。編碼

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 在這裏是一個新的對象,已經再也不是基本類型值,它的計算結果爲 trueprototype

上述例子,由於在 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"
複製代碼

發生這種狀況是由於 foobar 在轉換爲字符串的結果均爲 「[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 關鍵字,因此 foofoo2 都是字符串包裝類型,原始值都是 foo 。可是,它們如今引用了兩個不一樣的對象,因此 foo === foo2 將返回 false。這裏的關係運算符 >= 會在兩個操做數上調用 valueOf 函數,所以比較的是它們的原始值,'foo' > = 'foo' 的結果爲 true

[1] + [2] - [3] === 9

但願這些知識都能幫助揭開這個題目的神祕面紗

  1. [1] + [2] 將調用 Array.prototype.toString 轉換爲字符串,而後進行字符串拼接。結果將是 「12」
    • [1,2] + [3,4] 的值講師 「1,23,4」
  2. 12 - [3],減號運算符會將值轉換爲 Number 類型,因此等於 12-3,結果爲 9
    • 12 - [3,4] 的值是 NaN,由於"3,4" 不能被轉換爲 Number

總結

儘管不少人會建議儘可能避免強制隱式轉換,但瞭解它的工做原理很是重要,在調試代碼和避免錯誤方面大有幫助。

【譯文完】

再談點,關於寬鬆相等和原始值

這裏看另外一道題目,在 JavaScript 環境下,可否讓表達式 a == true && a == falsetrue

就像下面這樣,在控制檯打印出 ’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,具體過程以下:

  1. 調用對象的 valueOf() 方法,若是值是原值則返回
  2. 不然,調用對象的 toString() 方法,若是值是原值則返回
  3. 不然,拋出 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 == falsetrue 的答案

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');
}
複製代碼

閱讀更多

相關文章
相關標籤/搜索