【學習筆記】JS 的 {} + {} 與 {} + [] 的結果是什麼?

前言

在 JS 中,+ 符號是很常見的一種,它有如下的使用狀況算法

  • 數字的加法運算,二元運算
  • 字符串的鏈接運算,二元運算,優先級最高
  • 正號,一元運算,可延伸爲強制轉換其餘類型的運算元爲數字類型

另外一個常見的是花括號 {},它有兩個用途也很常見編程

  • 對象的字面定義
  • 區塊語句

加號運算符 +

除了上面說明的常見狀況外,在標準中轉換的規則還有如下幾個,要注意它的順序:operand + operand = result數組

  • 使用 ToPrimitive 運算轉換左與右運算元爲原始數據類型值(primitive)
  • 在第 1 步轉換後,如有運算元出現原始數據類型是"字符串"類型值時,則另外一運算元做強制轉換爲字符串,而後做字符串的鏈接運算(concatenation)
  • 其餘狀況時,全部運算元都會轉換爲原始數據類型的"數字"類型,而後做數學的相加運算(addition)

ToPrimitive 內部運算

所以,加號運算符只能使用於原始數據類型,那麼對於對象類型的值要如何轉換爲原始數據類型?瀏覽器

JavaScript 對象轉換到基本類型值時,會使用 ToPrimitive 算法,這是一個內部算法,是編程語言在內部執行時遵循的一套規則markdown

在 ECMAScript 6th Edition #7.1.1,有一個抽象的 ToPrimitive 運算,它會用於對象轉換爲原始數據類型,這個運算不僅會用在加號運算符也會用在關係比較或值相等比較的運算中,下面是有關 ToPrimitive 的說明語法編程語言

ToPrimitive(input, PreferredType?)
input 表明代入的值,而 PreferredType 能夠是數字(Number)或字符串(String)其中一種,這會表明"優先的"、"首選的"的要進行轉換到哪種原始類型,轉換的步驟會依這裏的值而有所不一樣函數

但若沒有提供這個值也就是預設狀況,則會設置轉換的 hint 值爲 default,這個首選的轉換原始類型的指示(hint 值),是在做內部轉換時由 JS 視狀況自動加上的,通常狀況就是預設值oop

轉換算法

當對象發生到基本類型值的轉換時,會按照下面的邏輯調用對象上的方法:ui

  • 若是存在 obj[Symbol.toPrimitive],則先調用 objSymbol.toPrimitivethis

  • 不然按下面規則來:
    PreferredType 爲數字 Number

    • 當 PreferredType 爲數字 Number 時,input 爲要被轉換的值,如下是轉換這個 input 值的步驟
    • 若 input 是原始數據類型,則直接返回 input
    • 不然,若 input 是個對象時則調用對象的 valueOf() 方法,若能獲得原始數據類型的值,則返回這個值
    • 不然,若 input 是個對象時則調用對象的 toString() 方法,若能獲得原始數據類型的值,則返回這個值
    • 不然,拋出 TypeError 錯誤

    PreferredType 爲字符串 String

    • 當 PreferredType 爲字符串 String 時,input 爲要被轉換的值,如下是轉換這個 input 值的步驟:
    • input 是原始數據類型,則直接返回 input
    • 不然,若 input 是個對象時則調用對象的 toString() 方法,若能獲得原始數據類型的值,則返回這個值
    • 不然,若 input 是個對象時則調用對象的 valueOf() 方法,若能獲得原始數據類型的值,則返回這個值
    • 不然,拋出 TypeError 錯誤

    PreferredType 沒提供時即 hint 爲 default 時

    • 此時與 PreferredType 爲數字 Number 時的步驟相同

Symbol.toPrimitive

  • Symbol.toPrimitive 是一個內置的 Symbol 值,它是做爲對象的函數值屬性存在的,當一個對象轉換爲對應的原始值時,會調用此函數
  • 該函數被調用時會被傳遞一個字符串參數 hint ,表示要轉換到的原始值的預期類型。hint 參數的取值是 "number""string""default" 中的任意一個
  • Symbol.toPrimitive 在類型轉換方面優,先級是最高的
let ab = {
    valueOf() {
        return 0;
    },
    toString() {
        return '1';
    },
    [Symbol.toPrimitive]() {
        return 2;
    }
}
console.log(1+ab); // 3
console.log('1'+ab); // 12
複製代碼

例子

// 擁有 Symbol.toPrimitive 屬性的對象
var obj2 = {
  [Symbol.toPrimitive](hint) {
    if(hint == "number"){
        return 10;
    }
    if(hint == "string"){
        return "hello";
    }
    return true;
  }
}
 
console.log(+obj2);     //10 --hint in "number"
console.log(`${obj2}`); //hello --hint is "string"
console.log(obj2 + ""); //"true"

let obj = {
  [Symbol.toPrimitive](hint) {
    if(hint === 'number'){
      console.log('Number場景');
      return 123;
    }
    if(hint === 'string'){
      console.log('String場景');
      return 'str';
    }
    if(hint === 'default'){
      console.log('Default 場景');
      return 'default';
    }
  }
}
console.log(2*obj); // Number場景 246
console.log(3 + obj); // Default 場景 3default
console.log(obj + "");  // Default場景 default
console.log(String(obj)); //String場景 str
複製代碼

valueOf 與 toString 方法

而在 JS 的 Object 原型的設計中,都必定會有兩個 valueOf 與 toString 方法,因此這兩個方法在全部對象裏面都會有,不過它們在轉換過程當中有可能會交換被調用的順序

對於原始類型數據,toString 及 valueOf 方法的使用

const str = "hello", n = 123, bool = true;
console.log(typeof(str.toString()) + "_" + str.toString()) // string_hello
console.log(typeof(n.toString()) + "_" + n.toString())  // string_123
console.log(typeof(bool.toString()) + "_" + bool.toString()) //string_true


console.log(typeof(str.valueOf()) + "_" + str.valueOf()) //string_hello
console.log(typeof(n.valueOf()) + "_" + n.valueOf()) //number_123
console.log(typeof(bool.valueOf()) + "_" + bool.valueOf()) //boolean_true

// console.log(str.valueOf) => ƒ valueOf() { [native code] }
console.log(str.valueOf === str) // false
// console.log(n.valueOf) => ƒ valueOf() { [native code] }
console.log(n.valueOf === n) // false
// bool.valueOf() => true
console.log(bool.valueOf() === bool) // true
複製代碼

toString 方法對於原始類型數據而言,其效果至關於類型轉換,將原類型轉爲字符串;valueOf 方法對於原始類型數據而言,其效果將至關於返回原數據

複合對象類型數據使用 toString 及 valueOf 方法

var obj = {};
console.log(obj.toString());    // [object Object] 返回對象類型
console.log(obj.valueOf());     // {} 返回對象自己
複製代碼

綜合案例

const test = { 
 i: 10, 
 toString: function() { 
    console.log('toString'); 
    return this.i; 
 }, 
 valueOf: function() { 
    console.log('valueOf'); 
    return this.i; 
 } 
} 
alert(test); // 10 toString 
alert(+test); // 10 valueOf 
alert(''+test); // 10 valueOf 
alert(String(test)); // 10 toString 
alert(Number(test)); // 10 valueOf 
alert(test == '10'); // true valueOf 
alert(test === '10'); // false
複製代碼

補充 toString() 和 String() 的區別

  • toString() 和 String() 方法均可以轉換爲字符串類型
  • toString()
    • toString() 能夠將全部的數據都轉換爲字符串,但要排除 null 和 undefined,null 和 undefined 調用 toString() 方法會報錯
    • 若當前數據爲數字類型,則 toString() 括號中能夠寫一個數字表明進制,能夠將數字轉化爲對應進制字符串
    var num = 123;
    console.log(num.toString() + '_' + typeof(num.toString()));   // 123_string 
    console.log(num.toString(2) + '_' + typeof(num.toString()));    // 1111011_string
    console.log(num.toString(8) + '_' + typeof(num.toString()));    // 173_string
    console.log(num.toString(16) + '_' + typeof(num.toString()));   //7b_string
    複製代碼
  • String()
    String() 能夠將 null 和 undefined 轉換爲字符串,可是無法轉進制字符串

注意下面兩點: Symbol.toPrimitive 和 toString 方法的返回值必須是基本類型值
valueOf 方法除了能夠返回基本類型值,也能夠返回其餘類型值

數字實際上是預設的首選類型,即在通常狀況下加號運算中的對象要做轉型時,都是先調用 valueOf 再調用 toString

注意:Date 對象的預設首選類型是字符串 String

JS 對於 Object 與 Array 的設計

在 JS 中所設計的 Object 純對象類型的 valueOf 與 toString 方法,它們的返回以下:

  • valueOf 方法返回值: 對象自己
  • toString 方法返回值: "[object Object]" 字符串值,不一樣的內建對象的返回值是 "[object type]" 字符串
    • type 指的是對象自己的類型識別,例如 Math 對象是返回 "[object Math]" 字符串
    • 但有些內置對象由於覆蓋了這個方法,因此直接調用時不是這種值(注意: 這個返回字符串的前面的 "object" 開頭英文是小寫,後面開頭英文是大寫)

所以能夠利用 Object 中的 toString 來進行各類不一樣對象進行判斷,這在之前 JS 能用的函數庫或方法很少的年代常常看到,不過它須要配合使用函數中的 call 方法,才能輸出正確的對象類型值,例如:

Object.prototype.toString.call([])  // "[object Array]"
Object.prototype.toString.call(new Date) // "[object Date]"
複製代碼

對象的這兩個方法均可被覆蓋,可用下面的代碼來觀察這兩個方法的運行順序,下面這個都是先調用 valueOf 的狀況:

let obj = {
  valueOf: function () {
      console.log('valueOf');
      return {}; // object
  },
  toString: function () {
      console.log('toString');
      return 'obj'; // string
  }
}
console.log(1 + obj);  //valueOf -> toString -> '1obj'
console.log(+obj); // // valueOf -> toString -> NaN
console.log('' + obj); // valueOf -> toString -> 'obj'
複製代碼

先調用 toString 的狀況比較少見,大概只有 Date 對象或強制要轉換爲字符串時纔會看到

let obj = {
  valueOf: function () {
      console.log('valueOf');
      return 1; // number
  },
  toString: function () {
      console.log('toString');
      return {}; // object
  }
}
alert(obj); // toString -> valueOf -> alert("1");
String(obj); // toString -> valueOf -> "1";
複製代碼

而下面這個例子會形成錯誤,由於不論順序是如何都得不到原始數據類型的值,錯誤消息是"TypeError: Cannot convert object to primitive value",從這個消息中能夠得知它這裏面會須要轉換對象到原始數據類型:

let obj = {
  valueOf: function () {
      console.log('valueOf');
      return {}; // object
  },
  toString: function () {
      console.log('toString');
      return {}; // object
  }
}
console.log(obj + obj); // valueOf -> toString -> error!
複製代碼

數組 Array 很經常使用,雖然它是個對象類型,但它與 Object 的設計不一樣,它的 toString 有覆蓋,說明一下數組的 valueOf 與 toString 的兩個方法的返回值:

  • valueOf 方法返回值:對象自己(與Object同樣)
  • toString 方法返回值:至關於用數組值調用 join(',') 所返回的字符串,即 [1,2,3].toString() 會是 "1,2,3",這點要特別注意

Function 對象不多會用到,它的 toString 也有被覆蓋,因此並非 Object 中的那個 toString,Function 對象的 valueOf 與 toString 的兩個方法的返回值:

  • valueOf 方法返回值:對象自己(與Object同樣)
  • toString 方法返回值:函數中包含的代碼轉爲字符串值

Number、String、Boolean 三個包裝對象

包裝對象是 JS 爲原始數據類型數字、字符串、布爾專門設計的對象,全部的這三種原始數據類型所使用到的屬性與方法,都是在這上面提供的

包裝對象的 valueOf 與 toString 的兩個方法在原型上有通過覆蓋,因此它們的返回值與通常的 Object 的設計不一樣:

  • valueOf 方法返回值:對應的原始數據類型值
  • toString 方法返回值:對應的原始數據類型值,轉換爲字符串類型時的字符串值

toString 方法會比較特別,這三個包裝對象裏的 toString 的細部說明以下:

  • Number 包裝對象的 toString 方法:能夠有一個傳參,能夠決定轉換爲字符串時的進位(二、八、16)
  • String 包裝對象的 toString 方法:與 String 包裝對象中的 valueOf 相同返回結果
  • Boolean 包裝對象的 toString 方法:返回 "true""false" 字符串

注意,常被搞混的是直接使用 Number()、String() 與 Boolean() 三個強制轉換函數的用法,這與包裝對象的用法不一樣,包裝對象是必須使用 new 關鍵字進行對象實例化的,如 new Number(123),而 Number('123') 則是強制轉換其餘類型爲數字類型的函數

Number()、String() 與 Boolean() 三個強制轉換函數所對應的就是在 ECMAScript 標準中的 ToNumber、ToString、ToBoolean 三個內部運算轉換的對照表,而當它們要轉換對象類型前,會先用上面說的 ToPrimitive 先將對象爲原始數據類型再進行轉換到所要的類型值

實例

字符串 + 其餘原始類型

字符串在加號運算中有最高的優先運算,與字符串相加一定是字符串鏈接運算(concatenation)。全部的其餘原始數據類型轉爲字符串,能夠參考 ECMAScript 標準中的 ToString 對照表,如下爲一些簡單的例子

'1' + 123 // "1123"
'1' + false // "1false"
'1' + null // "1null"
1' + undefined // "1undefined" 複製代碼

數字 + 其餘的非字符串的原始數據類型

數字與其餘類型做相加時,除了字符串會優先使用字符串鏈接運算(concatenation)之外,其餘都要依照數字爲優先,因此除了字符串以外的其餘原始數據類型都要轉換爲數字來進行數學的相加運算

1 + true // true 轉爲1, false 轉爲 0 -> 2
1 + null // null 轉化爲 0 -> 1
1 + undefined // undefined 轉爲NaN -> NaN
複製代碼

數字/字符串之外的原始數據類型做加法運算

當數字與字符串之外的其餘原始數據類型直接使用加號運算時,就是轉爲數字再運算,這與字符串徹底無關

true + true // 2
true + null // 1
undefined + null // NaN
複製代碼

[] + []

[] + [] // ""
複製代碼
  • 兩個數組相加,依然按照 valueOf -> toString 的順序
  • valueOf 返回數組自己,因此會以 toString 的返回值纔是原始數據類型,而 [] 轉化爲字符串爲 ""
  • 因此最後這個運算至關於兩個空字符串在相加,依照加法運算規則第2步驟是字符串鏈接運算(concatenation),兩個空字符串鏈接最後得出一個空字符串

{} + {}

{} + {} // "[object Object][object Object]"
複製代碼
  • 兩個空對象相加,依然按照 valueOf -> toString 的順序
  • valueOf 返回對象自己,因此會以 toString 的返回值纔是原始數據類型,即 {} 轉化爲字符串爲 "[object Object]"
  • 因此最後這個運算至關於兩個 "[object Object]" 字符串相加,依照加法運算規則第2步驟,是字符串鏈接運算(concatenation)

特別注意: {} + {} 在不一樣的瀏覽器有不一樣結果

  • 有些瀏覽器如 Firefox、Edge 瀏覽器會把 {} + {} 直譯爲至關於 +{} 語句,由於它們會認爲以花括號開頭 ({) 的是一個區塊語句的開頭而不是一個對象字面量,因此會略過第一個{},把整個語句認爲是個 +{} 的語句
  • 至關於強制求出數字值的 Number({}) 函數調用運算,即 Number("[object Object]"),最後得出的是 NaN

若在第一個空對象加上圓括號 (()),這樣 JS 就會認爲前面是個對象,就能夠得出一樣的結果:

({}) + {} // "[object Object][object Object]"
複製代碼

或是分開來先聲明對象的變量值也能夠得出一樣的結果,像下面這樣:

let foo = {}, bar = {};
foo + bar;
複製代碼

注: 上面說的行爲與加號運算的對象字面值是否是個空對象無關,就算是裏面有值的對象,如 {a:1, b:2} 也是一樣的結果

{} + []

上面所述的把 {} 看成區塊語句的狀況在此也會發生,不過此次全部的瀏覽器都會有一致結果,若 {} 在前面,而 [] 在後面,則前面(左邊)那個運算元會被認爲是區塊語句而不是對象字面量

因此 {} + [] 至關於 +[] 語句,即至關於強制求出數字值的 Number([]) 運算,即 Number("") 運算,最後得出的是 0數字

{} + [] // 0
{a: 1} + [1,2] // +"1,2" -> NaN
複製代碼

特別注意:若第一個是 {} 時,後面加上其餘的像 數組、數字或字符串這時加號運算會直接變爲一元正號運算,也就是強制轉爲數字的運算,這是個陷阱要當心

[] + {}

[] + {} // "" + "[object Object]" -> "[object Object]"
[1, 2] + {a:1}  // "1,2[object Object]"
複製代碼

Date 對象

Date 對象的 valueOf 與 toString 的兩個方法的返回值:

  • valueOf 方法返回值:給定的時間轉爲 UNIX 時間(自 1 January 1970 00:00:00 UTC 起算),可是以微秒計算的數字值
  • toString 方法返回值:本地化時間的字符串

Date 對象上面有說起是首選類型爲"字符串"的一種異常的對象,這與其餘的對象的行爲不一樣(通常對象會先調用 valueOf 再調用 toString ),在進行加號運算時它會優先使用 toString 來進行轉換,最後一定是字符串鏈接運算(concatenation),如如下的結果:

1 + (new Date()) // "1Mon May 17 2021 02:06:39 GMT+0800 (中國標準時間)"
複製代碼

要得出 Date 對象中的 valueOf 返回值,須要使用一元加號 + 來強制轉換它爲數字類型,如如下的代碼:

+new Date() // 1621188445596
複製代碼

Symbol 類型

ES6 中新加入的 Symbol 數據類型,它不算是通常的值也不是對象,它並無內部自動轉型的設計,因此徹底不能直接用於加法運算,使用時會報錯

相關文章
相關標籤/搜索