從 ++[[]][+[]]+[+[]]==10? 深入淺出弱類型 JS 的隱式轉換

oke66 8年前發布 | 17K 次閱讀 JavaScript開發 JavaScript ECMAScript

起因

凡是都有一個來源和起因,這個題不是我哪篇文章看到的,也不是我瞎幾把亂造出來的,我也沒這個天賦和能力,是我同事之前丟到群里,叫我們在瀏覽器輸出一下,對結果出乎意料,本著實事求是的精神,探尋事物的本質,不斷努力追根溯源,總算弄明白了最后的結果,最后的收獲總算把js的隱式類型轉換刨根問底的搞清楚了,也更加深入的明白了為什么JS是弱類型語言了。

題外話

一看就看出答案的大神可以跳過,鄙文會浪費你寶貴的時間,因為此文會很長,涉及到知識點很多很雜很細,以及對js源碼的解讀,而且很抽象,如果沒有耐心,也可以直接跳過,本文記錄本人探索這道問題所有的過程,會很長。

可能寫的不太清楚,邏輯不太嚴密,存在些許錯誤,還望批評指正,我會及時更正。去年畢業入坑前端一年,并不是什么老鳥,所以我也是以一個學習者的身份來寫這篇文章,逆向的記錄自己學習探索的過程,并不是指點江山,揮斥方遒,目空一切的大神,如果寫的不好,還望見諒。

首先對于這種問題,有人說是閑的蛋疼,整天研究這些無聊的,有啥用,開發誰會這么寫,你鉆牛角尖搞這些有意思嗎?

對于這種質疑,我只能說:愛看不看,反正不是寫給你看。

當然,這話也沒錯,開發過程中確實不會這么寫,但是我們要把開發和學習區分開來,很多人開發只為完成事情,不求最好,但求最快,能用就行 :blush: 。學習也是這樣,停留在表面,會用API就行,不會去深層次思考原理,因此很難進一步提升,就是因為這樣的態度才誕生了一大批一年經驗重復三五年的API大神 :smile: 。

但是學習就不同,學習本生就是一個慢慢深入,尋根問底,追根溯源的過程,如果對于探尋問題的本質追求都沒有,我只能說做做外包就好,探究這種問題對于開發確實沒什么卵用,但是對我們了解JavaScript這門語言卻有極大的幫助,可以讓我們對這門語言的了解更上一個臺階。 JavaScript為什么是弱類型語言,主要體現在哪里,弱類型轉換的機制又是什么?

有人還是覺得其實這對學習JS也沒什么多大卵用,我只能說: 我就喜歡折騰,你管得著?反正我收獲巨多就夠了。

++[[]][+[]]+[+[]]===10?這個對不對,我們先不管,先來看幾個稍微簡單的例子,當做練習入手。

一、作業例子:

這幾個是留給大家的作業,涉及到的知識點下面我會先一一寫出來,為什么涉及這些知識點,因為我自己一步步踩坑踩過來的,所以知道涉及哪些坑,大家最后按照知識點一步一步分析,一定可以得出 答案來,列出知識點之后,我們再來一起分析++[[]][+[]]+[+[]]===10?的正確性。

  1. {}+{}//chrome:"[object Object][object Object]",Firfox:NaN
  1. {}+[]//0
  1. []+{}//"[object Object]"

首先,關于1、2和3這三個的答案我是有一些疑惑,先給出答案,希望大家看完這篇文章能和我討論一下自己的想法,求同存異。

4.{}+1

5.({}+1)

6.1+{}

7.[]+1

8.1+[]

9.1-[]

10.1-{}

11.1-!{}

12.1+!{}

13.1+"2"+"2"

14.1+ +"2"+"2"

15.1++"2"+"2"

16.[]==![]

17.[]===![]</code></pre>

這幾個例子是我隨便寫的,幾乎包含了所有弱類型轉換所遇到的坑,為什么會出現這種情況,就不得不從JS這門語言的特性講起,大家都知道JS是一門動態的弱類型語言,那么你有沒有想過什么叫做弱類型?什么叫做動態?大家都知道這個概念,但有沒有進一步思考呢?

今天通過這幾個例子就來了解一下JS的弱類型,什么是動態暫時不做探討。

二、強弱類型的判別

按照計算機語言的類型系統的設計方式,可以分為強類型和弱類型兩種。二者之間的區別,就在于計算時是否可以不同類型之間對使用者透明地隱式轉換。從使用者的角度來看,如果一個語言可以隱式轉換它的所有類型,那么它的變量、表達式等在參與運算時,即使類型不正確,也能通過隱式轉換來得到正確地類型,這對使用者而言,就好像所有類型都能進行所有運算一樣,所以這樣的語言被稱作弱類型。與此相對,強類型語言的類型之間不一定有隱式轉換。

三、JS為什么是弱類型?

弱類型相對于強類型來說類型檢查更不嚴格,比如說允許變量類型的隱式轉換,允許強制類型轉換等等。強類型語言一般不允許這么做。具體說明請看 維基百科的說明

根據強弱類型的判別定義,和上面的十幾個例子已經充分說明JavaScript 是一門弱類型語言了。

先講一講一些概念,要想弄懂上面題目答案的原理,首先你要徹底弄懂以下的概念,有些時候對一些東西似懂非懂,其實就是對概念和規則沒有弄透,弄透之后等會回過頭對照就不難理解,不先了解透這些后面的真的不好理解,花點耐心看看,消化一下,最后串通梳理一下,一層一層的往下剝,答案迎刃而解。

為了能夠弄明白這種隱式轉換是如何進行的,我們首先需要搞懂如下一些基礎知識。如果沒有耐心,直接跳到后面第四章 4.8 小結 我總結的幾條結論,這里僅給想要一步步通過過程探尋結果的人看。

四、ECMAScript的運算符、{}解析、自動分號插入

4.1 ECMAScript 運算符優先級

運算符 描述
. [] () 字段訪問、數組下標、函數調用以及表達式分組
++ — - + ~ ! delete new typeof void 一元運算符、返回數據類型、對象創建、未定義值
* / % 乘法、除法、取模
+ - + 加法、減法、字符串連接
<< >> >>> 移位
< <= > >= instanceof 小于、小于等于、大于、大于等于、instanceof
== != === !== 等于、不等于、嚴格相等、非嚴格相等
& 按位與
^ 按位異或
   
&& 邏輯與
   
?: 條件
= oP= 賦值、運算賦值
, 多重求值

4.2 ECMAScript 一元運算符(+、-)

一元運算符只有一個參數,即要操作的對象或值。它們是 ECMAScript 中最簡單的運算符。

delete , void , -- , ++ 這里我們先不扯,免得越扯越多,防止之前博文的啰嗦,這里咋們只講重點,有興趣的可以看看 w3school(點我查看) 對這幾個的詳細講解。

上面的例子我們一個一個看,看一個總結一個規則,基本規則上面例子幾乎都包含了,如有遺漏,還望反饋補上。

這里我們只講 一元加法一元減法

我們先看看 ECMAScript5規范 (熟讀規范,你會學到很多很多)對一元加法和一元減法的解讀,我們翻到11.4.6和11.4.7。

其中涉及到幾個ECMAScript定義的抽象操作,ToNumber(x),ToPrimitive(x)等等 下一章詳細解答,下面出現的抽象定義也同理,先不管這個,有基礎想深入了解可以提前熟讀 ECMAScript5規范(點擊查看)

規范本來就是抽象的東西,不太好懂不要緊,我們看看例子,這里的規范我們只當做一種依據來證明這些現象。

大多數人都熟悉一元加法和一元減法,它們在 ECMAScript 中的用法與您高中數學中學到的用法相同。

一元加法本質上對數字無任何影響:

var iNum = 20;
iNum = +iNum;//注意不要和iNum += iNum搞混淆了;
alert(iNum);    //輸出 "20"

盡管一元加法對數字無作用,但對字符串卻有有趣的效果,會把字符串轉換成數字。

var sNum = "20";
alert(typeof sNum); //輸出 "string"
var iNum = +sNum;
alert(typeof iNum); //輸出 "number"

這段代碼把字符串 "20" 轉換成真正的數字。當一元加法運算符對字符串進行操作時,它計算字符串的方式與 parseInt() 相似,主要的不同是只有對以 "0x" 開頭的字符串(表示十六進制數字),一元運算符才能把它轉換成十進制的值。因此,用一元加法轉換 "010",得到的總是 10,而 "0xB" 將被轉換成 11。

另一方面,一元減法就是對數值求負(例如把 20 轉換成 -20):

var iNum = 20;
iNum = -iNum;
alert(iNum);    //輸出 "-20"

與一元加法運算符相似,一元減法運算符也會把字符串轉換成近似的數字,此外還會對該值求負。例如:

var sNum = "20";
alert(typeof sNum); //輸出 "string"
var iNum = -sNum;
alert(iNum);        //輸出 "-20"
alert(typeof iNum); //輸出 "number"

在上面的代碼中,一元減法運算符將把字符串 "-20" 轉換成 -20(一元減法運算符對十六進制和十進制的處理方式與一元加法運算符相似,只是它還會對該值求負)。

4.3 ECMAScript 加法運算符(+)

在多數程序設計語言中,加性運算符(即加號或減號)通常是最簡單的數學運算符。

在 ECMAScript 中,加性運算符有大量的特殊行為。

我們還是先看看 ECMAScript5規范 (熟讀規范,你會學到很多很多)對加號運算符 ( + )解讀,我們翻到11.6.1。

前面讀不懂不要緊,下一章節會為大家解讀這些抽象詞匯,大家不要慌,但是第七條看的懂吧, 這就是為什么1+"1"="11"而不等于2的原因 ,因為規范就是這樣的,瀏覽器沒有思維只會按部就班的執行規則,所以規則是這樣定義的,所以最后的結果就是規則規定的結果,知道規則之后,對瀏覽器一切運行的結果都會豁然開朗,哦,原來是這樣的啊。

在處理特殊值時,ECMAScript 中的加法也有一些特殊行為:

  • 某個運算數是 NaN,那么結果為 NaN。
  • -Infinity 加 -Infinity,結果為 -Infinity。
  • Infinity 加 -Infinity,結果為 NaN。
  • +0 加 +0,結果為 +0。
  • -0 加 +0,結果為 +0。
  • -0 加 -0,結果為 -0。

不過,如果某個運算數是字符串,那么采用下列規則:

  • 如果兩個運算數都是字符串,把第二個字符串連接到第一個上。
  • 如果只有一個運算數是字符串,把另一個運算數轉換成字符串,結果是兩個字符串連接成的字符串。

例如:

var result = 5 + 5; //兩個數字
alert(result);      //輸出 "10"
var result2 = 5 + "5";  //一個數字和一個字符串
alert(result);      //輸出 "55"

這段代碼說明了加法運算符的兩種模式之間的差別。正常情況下,5+5 等于 10(原始數值),如上述代碼中前兩行所示。不過,如果把一個運算數改為字符串 "5",那么結果將變為 "55"(原始的字符串值),因為另一個運算數也會被轉換為字符串。

注意:為了避免 JavaScript 中的一種常見錯誤,在使用加法運算符時,一定要仔細檢查運算數的數據類型

4.4 ECMAScript 減法運算符(-)

減法運算符(-),也是一個常用的運算符:

var iResult = 2 - 1;

減、乘和除沒有加法特殊,都是一個性質,這里我們就單獨解讀減法運算符(-)

我們還是先看看 ECMAScript5規范 (熟讀規范,你會學到很多很多)對減號運算符 ( - )解讀,我們翻到11.6.2。

與加法運算符一樣,在處理特殊值時,減法運算符也有一些特殊行為:

  • 某個運算數是 NaN,那么結果為 NaN。
  • Infinity 減 Infinity,結果為 NaN。
  • -Infinity 減 -Infinity,結果為 NaN。
  • Infinity 減 -Infinity,結果為 Infinity。
  • -Infinity 減 Infinity,結果為 -Infinity。
  • +0 減 +0,結果為 +0。
  • -0 減 -0,結果為 -0。
  • +0 減 -0,結果為 +0。
  • 某個運算符不是數字,那么結果為 NaN。

注釋:如果運算數都是數字,那么執行常規的減法運算,并返回結果。

4.5 ECMAScript 前自增運算符(++)

直接從 C(和 Java)借用的兩個運算符是前增量運算符和前減量運算符。

所謂前增量運算符,就是數值上加 1,形式是在變量前放兩個加號(++):

var iNum = 10;
++iNum;

第二行代碼把 iNum 增加到了 11,它實質上等價于:

var iNum = 10;
iNum = iNum + 1;

我們還是先看看 ECMAScript5規范 (熟讀規范,你會學到很多很多)對前自增運算符 ( ++ )解讀,我們翻到11.4.4。

此圖有坑,后面會說到,坑了我很久。。。

看不懂這些抽象函數和詞匯也不要緊,想要深入了解可以通讀 ECMAScript5規范中文版 ,看幾遍就熟悉了,第一次看見這些肯定一臉懵逼,這是什么玩意,我們只要明白++是干什么就行,這里不必去深究v8引擎怎么實現這個規范的。

至于

var a=1;
console.log(a++);//1
var b=1;
cosole.log(++b);//2

還弄不明白的該好好補習了,這里不在本文的知識點,也不去花篇幅講解這些,這里我們只要明白一點: 所謂前增量運算符,就是數值上加 1

4.6 ECMAScript 自動分號(;)插入

盡管 JavaScript 有 C 的代碼風格,但是它不強制要求在代碼中使用分號,實際上可以省略它們。

JavaScript 不是一個沒有分號的語言,恰恰相反上它需要分號來就解析源代碼。 因此 JavaScript 解析器在遇到由于缺少分號導致的解析錯誤時,會自動在源代碼中插入分號。

4.6.1例子

var foo = function() {
} // 解析錯誤,分號丟失
test()

自動插入分號,解析器重新解析。

var foo = function() {
}; // 沒有錯誤,解析繼續
test()

4.6.2工作原理

下面的代碼沒有分號,因此解析器需要自己判斷需要在哪些地方插入分號。

(function(window, undefined) {
    function test(options) {
        log('testing!')

    (options.list || []).forEach(function(i) {

    })

    options.value.test(
        'long string to pass here',
        'and another long string to pass'
    )

    return
    {
        foo: function() {}
    }
}
window.test = test

})(window)

(function(window) { window.someLibrary = {} })(window)</code></pre>

下面是解析器"猜測"的結果。

(function(window, undefined) {
    function test(options) {

    // 沒有插入分號,兩行被合并為一行
    log('testing!')(options.list || []).forEach(function(i) {

    }); // <- 插入分號

    options.value.test(
        'long string to pass here',
        'and another long string to pass'
    ); // <- 插入分號

    return; // <- 插入分號, 改變了 return 表達式的行為
    { // 作為一個代碼段處理
        foo: function() {}
    }; // <- 插入分號
}
window.test = test; // <- 插入分號

// 兩行又被合并了 })(window)(function(window) { window.someLibrary = {}; // <- 插入分號 })(window); //<- 插入分號</code></pre>

解析器顯著改變了上面代碼的行為,在另外一些情況下也會做出錯誤的處理。

4.6.3 ECMAScript對自動分號插入的規則

我們翻到7.9章節,看看其中插入分號的機制和原理,清楚只寫以后就可以盡量以后少踩坑

**必須用分號終止某些 ECMAScript 語句 ( 空語句 , 變量聲明語句 , 表達式語句 , do-while 語句 , continue 語句 , break 語句 , return 語句 ,throw 語句 )。這些分號總是明確的顯示在源文本里。然而,為了方便起見,某些情況下這些分號可以在源文本里省略。描述這種情況會說:這種情況下給源代碼的 token 流自動插入分號。**

還是比較抽象,看不太懂是不是,不要緊,我們看看實際例子,總結出幾個規律就行,我們先不看抽象的,看著頭暈,看看具體的總結說明, 化抽象為具體

首先這些規則是基于兩點:

  1. 以換行為基礎;
  2. 解析器會盡量將新行并入當前行,當且僅當符合ASI規則時才會將新行視為獨立的語句。

4.6.3.1 ASI的規則

1. 新行并入當前行將構成非法語句,自動插入分號。

if(1 < 10) a = 1
console.log(a)
// 等價于
if(1 < 10) a = 1;
console.log(a);

2. 在continue,return,break,throw后自動插入分號

return
{a: 1}
// 等價于
return;
{a: 1};

3. ++、--后綴表達式作為新行的開始,在行首自動插入分號

a
++
c
// 等價于
a;
++c;

4. 代碼塊的最后一個語句會自動插入分號

function(){ a = 1 }
// 等價于
function(){ a = 1; }

4.6.3.2 No ASI的規則

1. 新行以 ( 開始

var a = 1
var b = a
(a+b).toString()
// 會被解析為以a+b為入參調用函數a,然后調用函數返回值的toString函數
var a = 1
var b =a(a+b).toString()

2. 新行以 [ 開始

var a = ['a1', 'a2']
var b = a
[0,1].slice(1)
// 會被解析先獲取a[1],然后調用a[1].slice(1)。
// 由于逗號位于[]內,且不被解析為數組字面量,而被解析為運算符,而逗號運算符會先執
行左側表達式,然后執行右側表達式并且以右側表達式的計算結果作為返回值
var a = ['a1', 'a2']
var b = a[0,1].slice(1)

3. 新行以 / 開始

var a = 1
var b = a
/test/.test(b)
// /會被解析為整除運算符,而不是正則表達式字面量的起始符號。瀏覽器中會報test前多了個.號
var a = 1
var b = a / test / .test(b)

4. 新行以 + 、 - 、 % 和 * 開始

var a = 2
var b = a
+a
// 會解析如下格式
var a = 2
var b = a + a

5. 新行以 , 或 . 開始

var a = 2
var b = a
.toString()
console.log(typeof b)
// 會解析為
var a = 2
var b = a.toString()
console.log(typeof b)

到這里我們已經對ASI的規則有一定的了解了,另外還有一樣有趣的事情,就是“空語句”。

// 三個空語句
;;;

// 只有if條件語句,語句塊為空語句。 // 可實現unless條件語句的效果 if(1>2);else console.log('2 is greater than 1 always!');

// 只有while條件語句,循環體為空語句。 var a = 1 while(++a < 100);</code></pre>

4.6.4 結論

建議絕對不要省略分號,同時也提倡將花括號和相應的表達式放在一行, 對于只有一行代碼的 if 或者 else 表達式,也不應該省略花括號。 這些良好的編程習慣不僅可以提到代碼的一致性,而且可以防止解析器改變代碼行為的錯誤處理。

關于JavaScript 語句后應該加分號么?(點我查看) 我們可以看看知乎上大牛們對著個問題的看法。

4.7 ECMAScript 對{}的解讀,確切說應該是瀏覽器對{}的解析

js引擎是如何判斷{}是代碼塊還是對象的?

這個問題不知道大家有沒有想過,先看看幾個例子吧?

首先要深入明白的概念:

4.7.1 JavaScript的語句與原始表達式

原始表達式是表達式的最小單位——它不再包含其他表達式。javascript中的原始表達式包括this關鍵字、標識符引用、字面量引用、數組初始化、對象初始化和分組表達式,復雜表達式暫不做討論。

語句沒有返回值,而表達式都有返回值的,表達式沒有設置返回值的話默認返回都是undefined。

在 javascript 里面滿足這個條件的就函數聲明、變量聲明(var a=10是聲明和賦值)、for語句、if語句、while語句、switch語句、return、try catch。

但是 javascript 還有一種函數表達式,它的形式跟函數聲明一模一樣。如果寫 function fn() { return 0;} 是函數聲明而寫var a = function fn(){ return 0;} 等號后面的就是函數表達式。

4.7.2 再來看看幾個例子吧

1.{a:1}
2.{a:1};
3.{a:1}+1

我們直接在chrome看看結果:

很奇怪是吧:

再來看看在Firefox下面的情況:

其他IE沒測,我Mac沒法裝IE,大家自行測試。

第二個很好理解,在有;的情況下,chrome和Firefox一致的把{a:1}解析為代碼塊,那么{a:1}怎么理解這個代碼塊,為什么不報錯,還記得goto語句嗎,JavaScript保留了goto的語法,我最先也半天沒緩過神來,還好記得c語言里面的這個語法,沒白學,其實可以這么理解:

{
a:
 1;
};

關于第二個{a:1}+1的答案,chrome和Firefox接過也一致,兩個瀏覽器都會把這段代碼解析成:

{
a:
 1;
};
+1;

其中關于{a:1}兩個瀏覽器就達成了不一樣的意見,只要{}前面沒有任何運算符號,Firefox始終如一的把{}解析成{};也就是我們熟知的代碼塊,而不是對象字面量。

而chrome就不同了,如果{a:1}后面和前面啥也沒有,{a:1}在chrome瀏覽器會首先檢查這個是不是標準對象格式,如果是返回這個對象,如果不是,則當做代碼塊執行代碼。當然這種情況基本可以不考慮,你寫代碼就寫個{a:1}然后就完了?

共同的特點:

  1. 當{}的前面有運算符號的時候,+,-,*,/,()等等,{}都會被解析成對象字面量,這無可爭議。
  2. 當{}前面沒有運算符時候但有;結尾的時候,或者瀏覽器的自動分號插入機制給{}后面插入分號(;)時候,此時{}都會被解析成代碼塊。

如果{}前面什么運算符都沒有,{}后面也沒有分號(;)結尾,Firefox會始終如一的解析為代碼塊,而chrome有細微的差別,chrome會解析為對象字面量。

這里也是我通過瀏覽器輸出結果進行的一種歸納,當然可能還有沒有總結到位的地方,也可能還有錯誤,發現 ECMAScript規范 對于{}何時解析為對象何時解析為代碼塊也沒有找到比較詳細的解答,有可能也是我看的不仔細,遺漏了這塊,還望大家能解答一下這塊。

4.8 小結

如果你覺得以上的很繁瑣,我是新手,也看不太懂,不要緊,不要慌,循序漸進,以后會懂的,這里我就直接總結出幾個結論,總結的不對的地方,還望反饋指出,對照著結論來驗證上面的十幾個例子:

  1. 數組下標([])優先級最高, 一元運算符(--,++,+,-)的優先級高于加法或減法運算符(+,-);
  2. ++前增量運算符,就是數值上加 1;
  3. 一元運算符(+,-)的后面如果不是數字,會調用 ToNumber 方法按照規則轉化成數字類型。
  4. 對于加號運算符(+)
    首先執行代碼,調用 ToPrimitive 方法得到原始值
    ①如果原始值是兩個數字,則直接相加得出結果。
    ②如果兩個原始值都是字符串,把第二個字符串連接到第一個上,也就是相當于調用 concat 方法。
    ③如果只有一個原始值是字符串,調用 ToString 方法把另一個運算數轉換成字符串,結果是兩個字符串連接成的字符串。
  5. 對于減號運算符(-)
    不知道大家有沒有看到 ECMAScript 規范,這里比+少了一步 ToPrimitive ,所以 - 相對容易理解。
    ①如果是兩個數字,則直接相減得出結果。
    ②如果有一個不是數字,會調用 ToNumber 方法按照規則轉化成數字類型,然后進行相減。
  6. 分號的插入
    ①新行并入當前行將構成非法語句,自動插入分號。
    ②在continue,return,break,throw后自動插入分號
    ③++、--后綴表達式作為新行的開始,在行首自動插入分號
    ④代碼塊的最后一個語句會自動插入分號
    ⑤新行以 ( 、[、\、+ 、 - 、,、. % 和 *開始都不會插入分號
  7. {}的兩種解讀
    ①當{}的前面有運算符號的時候,+,-,*,/,()等等,{}都會被解析成對象字面量,這無可爭議。
    ②當{}前面沒有運算符時候但有;結尾的時候,或者瀏覽器的自動分號插入機制給{}后面插入分號(;)時候,此時{}都會被解析成代碼塊。
    ③如果{}前面什么運算符都沒有,{}后面也沒有分號(;)結尾,Firefox會始終如一的解析為代碼塊,而chrome有細微的差別,chrome會解析為對象字面量。

五、ECMAScript的規范定義的抽象操作

前面關于 ECMAScript規范 的解讀,涉及到幾個重要的抽象操作:

  • GetValue(v) : 引用規范類型
  • Type(x) : 獲取x的類型
  • ToNumber(x) : 將x轉換為Number類型
  • ToString(x) : 將x轉換為String類型
  • SameValue(x,y) : 計算非數字類型x,y是否相同
  • ToPrimitive(x) : 將x轉換為原始值

5.1 原始值

首先,讓我們快速的復習一下。 在 JavaScript 中,一共有兩種類型的值(ES6的 symbol 暫不做討論):

原始值(primitives)

  1. undefined
  2. null
  3. boolean
  4. number
  5. string

對象值(objects)。 除了原始值外,其他的所有值都是對象類型的值,包括數組(array)和函數(function)等。</code></pre>

5.2 GetValue(v)

這里的每個操作都有其嚴格并復雜的定義,可以直接查閱ECMA規范文檔對其的詳細說明。

附上在線中文文檔地址: ECMAScript

我們先看看GetValue(v) : 引用規范類型,下面是 ECMAScript規范 的解讀:

這什么鬼,我也不太懂,反正就是關于引用規范的一些抽象描述,鄙人才疏學淺,也不能化抽象為具體的解釋一番,太抽象了,好難啊,功力還不夠,不懂但對我們解決上面的問題也沒有什么影響,我們只看關鍵的幾個:

這里我們先看下SameValue()和ToPrimitive()兩個操作。

5.3 SameValue(x,y)

我們還是先看看 ECMAScript5規范 (熟讀規范,你會學到很多很多)對 SameValue 方法解讀,我們翻到9.12。

這個SameValue操作說的就是,如果x,y兩個值類型相同,但又不同時是Number類型時的比較是否相等的操作。

5.4 ToPrimitive(input [ , PreferredType])

ToPrimitive() 方法

轉換成原始類型方法。

還是來看看 ECMAScript 標準怎么定義 ToPrimitice 方法的:

是不是看了這個定義,還是一臉懵逼,ToPrimitive這尼瑪什么玩意啊?這不是等于沒說嗎?

再來看看火狐MDN上面文檔的介紹:

JS::ToPrimitive

查了一下資料,上面要說的可以概括成:

ToPrimitive(obj,preferredType)

JS引擎內部轉換為原始值ToPrimitive(obj,preferredType)函數接受兩個參數,第一個obj為被轉換的對象,第二個 preferredType為希望轉換成的類型(默認為空,接受的值為Number或String)

在執行ToPrimitive(obj,preferredType)時如果第二個參數為空并且obj為Date的事例時,此時preferredType會 被設置為String,其他情況下preferredType都會被設置為Number如果preferredType為Number,ToPrimitive執 行過程如 下:

  1. 如果obj為原始值,直接返回;
  2. 否則調用 obj.valueOf(),如果執行結果是原始值,返回之;
  3. 否則調用 obj.toString(),如果執行結果是原始值,返回之;
  4. 否則拋異常。

如果preferredType為String,將上面的第2步和第3步調換,即:

  1. 如果obj為原始值,直接返回;
  2. 否則調用 obj.toString(),如果執行結果是原始值,返回之;
  3. 否則調用 obj.valueOf(),如果執行結果是原始值,返回之;
  4. 否則拋異常。</code></pre>

    首先我們要明白 obj.valueOf()obj.toString() 還有原始值分別是什么意思,這是弄懂上面描述的前提之一:

    toString用來返回對象的字符串表示。

    var obj = {};
    console.log(obj.toString());//[object Object]

var arr2 = []; console.log(arr2.toString());//""空字符串

var date = new Date(); console.log(date.toString());//Sun Feb 28 2016 13:40:36 GMT+0800 (中國標準時間)</code></pre>

valueOf方法返回對象的原始值,可能是字符串、數值或bool值等,看具體的對象。

var obj = {
  name: "obj"
};
console.log(obj.valueOf());//Object {name: "obj"}

var arr1 = [1]; console.log(arr1.valueOf());//[1]

var date = new Date(); console.log(date.valueOf());//1456638436303 如代碼所示,三個不同的對象實例調用valueOf返回不同的數據</code></pre>

原始值指的是['Null','Undefined','String','Boolean','Number']五種基本數據類型之一,一開始就提到過。

弄清楚這些以后,舉個簡單的例子:

var a={};
ToPrimitive(a)

分析:a是對象類型但不是Date實例對象,所以preferredType默認是Number,先調用a.valueOf()不是原始值,繼續來調 用a.toString()得到string字符串,此時為原始值,返回之.所以最后ToPrimitive(a)得到就是"[object Object]".</code></pre>

如果覺得描述還不好明白,一大堆描述晦澀又難懂,我們用代碼說話:

const toPrimitive = (obj, preferredType='Number') => {
    let Utils = {
        typeOf: function(obj) {
            return Object.prototype.toString.call(obj).slice(8, -1);
        },
        isPrimitive: function(obj) {
            let types = ['Null', 'String', 'Boolean', 'Undefined', 'Number'];
            return types.indexOf(this.typeOf(obj)) !== -1;
        }
    };

if (Utils.isPrimitive(obj)) {
    return obj;
}

preferredType = (preferredType === 'String' || Utils.typeOf(obj) === 'Date') ?
 'String' : 'Number';

if (preferredType === 'Number') {
    if (Utils.isPrimitive(obj.valueOf())) {
        return obj.valueOf()
    };
    if (Utils.isPrimitive(obj.toString())) {
        return obj.toString()
    };
} else {
    if (Utils.isPrimitive(obj.toString())) {
        return obj.toString()
    };
    if (Utils.isPrimitive(obj.valueOf())) {
        return obj.valueOf()
    };
}

}

var a={}; ToPrimitive(a);//"[object Object]",與上面文字分析的一致</code></pre>

5.5 ToNumber(x)

這個就比ToPrimitive() 方法好理解多了,就是把其他類型按照一定的規則轉化成數字類型,也就是類似Number()和parseInt()的方法。

還是繼續看看ECMAScipt規范中對于Number的轉換

是不是又看到 ToPrimitive() 方法了,是不是看了上面的就好理解多了,如果ToNumber(x)這個x是對象就要調用ToPrimitive方法返回x的原始值,是不是一下子就串起來了。

5.6 ToString(x)

這個理解起來跟 ToNumber 方法大同小異,還是繼續看看ECMAScipt規范中對于String的轉換.

對數值類型應用 ToString

ToString 運算符將數字 m 轉換為字符串格式的給出如下所示:

  1. 如果 m 是 NaN,返回字符串 "NaN"。
  2. 如果 m 是 +0 或 -0,返回字符串 "0"。
  3. 如果 m 小于零,返回連接 "-" 和 ToString (-m) 的字符串。
  4. 如果 m 無限大,返回字符串 "Infinity"。
  5. 否則,令 n, k, 和 s 是整數,使得 k ≥ 1, 10k-1 ≤ s < 10k,s × 10n-k 的數字值是 m,且 k 足夠小。要注意的是,k 是 s 在十進制表示中的數字的個數。s 不被 10 整除,且s 的至少要求的有效數字位數不一定要被這些標準唯一確定。
  6. 如果 k ≤ n ≤ 21,返回由 k 個 s 在十進制表示中的數字組成的字符串(有序的,開頭沒有零),后面跟隨字符 '0' 的 n-k 次出現。
  7. 如果 0 < n ≤ 21,返回由 s 在十進制表示中的、最多 n 個有效數字組成的字符串,后面跟隨一個小數點 '. ',再后面是余下的 k-n 個 s 在十進制表示中的數字。
  8. 如果 -6 < n ≤ 0,返回由字符 '0' 組成的字符串,后面跟隨一個小數點 '. ',再后面是字符 '0' 的 -n 次出現,再往后是 k 個 s 在十進制表示中的數字。
  9. 否則,如果 k = 1,返回由單個數字 s 組成的字符串,后面跟隨小寫字母 'e',根據 n-1 是正或負,再后面是一個加號 '+' 或減號 '-' ,再往后是整數 abs(n-1) 的十進制表示(沒有前置的零)。
  10. 返回由 s 在十進制表示中的、最多的有效數字組成的字符串,后面跟隨一個小數點 '. ',再后面是余下的是 k-1 個 s 在十進制表示中的數字,再往后是小寫字母 'e',根據n-1 是正或負,再后面是一個加號 '+ ' 或減號 '-' ,再往后是整數 abs(n-1) 的十進制表示(沒有前置的零)。

六、驗證分析++[[]][+[]]+[+[]]==10?

養兵千日,用兵一時。

了解了這么多深入的基礎知識,該發揮用武之地了,我已經用完洪荒之力了,是時候表演真正的技術了。

好像前面忘記講 == 符號了,不要緊,之前這個我的上一篇博文已經非常詳細的分析過了,可以看看我的這篇 博文(點擊查看) 。這里就不花篇幅介紹了,感覺越來越坑,越寫越多。

這里就簡單說一下,總結一下==轉換規則:

==運算規則的圖形化表示

1. undefined == null,結果是true。且它倆與所有其他值比較的結果都是false。

  1. String == Boolean,需要兩個操作數同時轉為Number。

  2. String/Boolean == Number,需要String/Boolean轉為Number。

  3. Object == Primitive,需要Object轉為Primitive(具體通過valueOf和toString方法)。

瞧見沒有,一共只有4條規則!是不是很清晰、很簡單。</code></pre>

1.首先++[[]][+[]]+[+[]]首先拆分一下:

根據 4.1 ECMAScript 運算符優先級 可以這樣拆分:

相當于這樣:

(++[[]][+[]])
+
([+[]])

2.先來分析右邊的[+[]]

①先看里面的+[]

  • 根據 4.2 ECMAScript 一元運算符(+、-) 可以知道,一元運算符會調用 ToNumber 方法把 ToNumber([]) 轉化成數字。

  • 根據 5.5 ToNumber(x) 的轉換規則,x為[]是數組對象,因此會調用 ToPrimitive 方法。

  • 根據 5.4 ToPrimitive(input [ , PreferredType]) 的轉換規則,空數組先調用 valueOf() 方法,得到[]不是原始值,繼續調用 toString() 方法,得到 ""空字符串

  • 遞歸的調用之后成了 ToNumber("") ,答案顯而易見,根據 5.5 ToNumber(x) 的轉換規則對照圖片可以看出ToNumber("")===0。 那么[+[]]就變相的成了[0]

此時成了(++[[]][+[]])+[0]

3.再來分析左邊邊的++[[]][+[]]

  • +[]上面已經分析出來了,結果為0,那么此時就成了++[[]][0]

  • 根據 4.2 ECMAScript 一元運算符(+、-) 可以知道,數組下標的優先級高于一元運算符++,那么理所當然成了這樣 ++([[]][0]) ,而[[]][0]可以看出數組下標為0也就是第一個元素,此時為[],那么最后成了++[].

  • ++[]這是什么鬼 :ghost: ,根據 4.5 ECMAScript 前自增運算符(++) 沒有發現任何有調用 ToNumber 的方法,瀏覽器試了一下,果然有問題,報錯啦,到底哪里出問題了呢,為什么走著走著就走偏了。問題出在哪一步呢?

4.分析問題錯誤的原因

為什么++([[]][0])在瀏覽器不報錯,而++[]報錯,我知道問題就出在這一步,但是一直相不出原因,光瞎想是沒用的,沒事繼續讀讀ECMAScript規范,然后中文版的并沒有看出什么玩意,最后在github英文版找到原因了。

首先我們在瀏覽器輸出一下++[]

無意之中照著錯誤搜,搜到了這個后綴自增++:

順便看看大同小異的前綴自增++

Increment Operator_操作的第5步PutValue(expr, newValue)要求expr是引用。這就是問題的關鍵,為什么之前我沒發現,因為之前我一直看的是中文版,來看看中文版的截圖對比一下

發現后面的3,4,5都沒有,我一度以為自己理解錯了,為什么這個規則沒有調ToNumber()卻也能得到數字,原來是翻譯中這塊內容遺漏了,我該好好補習英語了,盡量多看英文文檔。

看到第五條大大的 Call PutValue(expr, newValue).

閱讀 es5英文文檔 ,可以看到_Prefix Increment Operator_操作的第5步PutValue(expr, newValue)要求expr是引用。

我們還是來看看 PutValue 到底是什么定義,這里我們只需要知道++a,這個a是引用類型才不會報Uncaught ReferenceError: Invalid left-hand side expression in postfix operation這個錯誤。

而我們知道[[]][0]是對象的屬性訪問,而我們知道對象的屬性訪問返回的是引用,所以可以正確執行。

5.進一步拆分

++[[]][0]可以這么拆分,只要保持引用關系就行:

var refence=[[]][0];
++refence;

再來進一步拆分

var refence=[];
refence=refence+1;

最后就成了

refence=[]+1;

根據 4.3 ECMAScript 加法運算符(+) ,[]+1可以看成是ToPrimitive([])+ToPrimitive(1),根據 5.4 ToPrimitive(input [ , PreferredType]) 的轉換規則,空數組先調用 valueOf() 方法,得到[]不是原始值,繼續調用 toString() 方法,得到 "" 空字符串。

于是就成了 ""+1 ,根據 4.3 ECMAScript 加法運算符(+) ,有一個字符串,另外一個也會變成字符串,所以""+1==="1"。所以 ++[[]][0] === "1" ;

好像分析的是這么回事,其實錯了,大家不要跟著我錯誤的步驟走,我其實忽略了很重要的一點。

看看規范有一點遺漏了,就是 Let oldValue be ToNumber(GetValue(expr)).

就是++時候舊的值要進行 ToNumber() 運算,最后最后一步應該是這樣子的:

refence=ToNumber([])+1;

ToNumber([])===0,別問我為什么,照著我上面的分析自己分析一遍,不難,我因為分析多了,所以一眼就看出來了,所以最后成了0+1=1的問題,所以 ++[[]][0] === 1

6. 謎底揭開?

左邊++[[]][0] === 1;

右邊[+[]]的值為[0];

所以最后成了1+[0]的問題了。

根據 5.4 ToPrimitive(input [ , PreferredType]) 的轉換規則,[0]數組先調用 valueOf() 方法,得到[0]不是原始值,繼續調用 toString() 方法,得到 “0” 的字符串。

所以最后就成了 1+"0"==="10"

7.最后的最后

于是最后就成了 "10" == 10 的問題,根據ECMAScript規范==的對應法則:

對比第5條,可以發現最后成了ToNumber("10")與10的比較,而ToNumber("10") === 10,

左邊最后 === 10,

右邊最后 === 10。

10 === 10為true.

所以 ++[[]][+[]]+[+[]]==10 為true,大功告成,可喜可賀,實力分析了一波,有錯誤還望批評指正。

七、我的疑惑

1. {}+{}//chrome:"[object Object][object Object]",Firfox:NaN

  1. {}+[]//0

  2. []+{}//"[object Object]"</code></pre>

    通過這個例子,我發現剛才4.7 ECMAScript 對{}的解讀還不夠徹底,首先我們按照前面常規的思維來解答:

    1.第一種思維:第一個{}解析為對面字面量

    第一例子: {}+{} 首先左邊{}和右邊{}會調用 ToPrimitive 兩邊都會得到:"[object Object]",所以最后就是這兩個相同的字符串相加,得到: "[object Object][object Object]" ,chrome符合,Firefox不符合。

    第二個例子: {}+[] 按照這種思維首先左邊{}和右邊[]會調用 ToPrimitive ,分別得到"[object Object]"和""空字符串,那么相加結果應該是 "[object Object]" ,為什么結果成了 0 ,而且在 chromeFirfox 都是0?

    第三個例子: []+{} 按照這種思維首先左邊[]和右邊{}會調用 ToPrimitive ,分別得到""空字符串和"[object Object]",最后相加結果 "[object Object]" ,這個沒有任何疑惑,chrome和Firefox都符合。

    2.第一種思維:第一個{}解析為代碼塊

    第一例子: {}+{} 瀏覽器這么解析,把{}不解析為對象字面量而是代碼塊,也就是let a={}這種塊,代碼可以看成是這樣 {};+{} ,那么{};執行啥也沒有,接下來就是 +{} ,+是一元運算符,上面講到了,這里+{}執行時候首先會調用ToNumber(),參數{}是object會首先調用 ToPrimitive 得到原始值: "[object Object]" ,這時候就可以發現ToNumber("[object Object]")轉化的就是 NaN 了,chrome不符合,Firefox符合。

    第二個例子: {}+[] 按照這種思維,最后解析成 {};+[] ,+是一元運算符,上面講到了,這里+[]執行時候首先會調用ToNumber(),參數[]是object會首先調用 ToPrimitive 得到原始值: ""空字符串 ,最后根據規則ToNumber("")得到數字0.這種思維下沒有任何疑惑,chrome和Firefox都符合。

    第三個例子: []+{} 首先左邊[]和右邊{}會調用 ToPrimitive ,分別得到""空字符串和"[object Object]",最后相加結果 "[object Object]" ,這個沒有任何疑惑,chrome和Firefox都符合。

    那么問題來了?問題的矛盾就在于第一條和第二條,chrome和Firefox對于{}+{}解析是不一樣的,對于第一個{}chrome解析為對象字面量,而Firefox解析為代碼塊,這無可厚非, 關鍵是第二個例子{}+[] ,既然第一個例子 {}+{} 的第一個{}chrome解析為對象字面量而第二個例子 {}+[] 中,chrome卻解析為代碼塊,匪夷所思,有誰能扒一扒源碼分析一下,chrome對{}的詳細解析,到底什么時候解析為代碼塊,什么時候解析為對象字面量?有點想不明白為什么這么不一致,而 Firefox始終如一,第一個{}一直解析為代碼塊,運算符號后面{}解析為對象字面量 。

    3.捉摸不透

    Firefox的我能理解,開頭{}一律解析為代碼塊(block),而chrome卻讓人捉摸不透。。。

    八、結束

    前面的十幾個例子,大家有興趣對照著規則自己一個一個做做,看看自己是否真的理解了,理解了也再熟悉一遍,學習本來就是一個重復的過程。

    突然靈機一動:

    var obj = {

     valueOf: function() {
         return 18;
     }
    

    };

    console.log(

         1 <= "2",
         "1" <= "a",
         obj >= "17"
    

    );</code></pre>

    這個答案又是多少呢?

    var obj = {

     valueOf: function() {
         return {a:1};
     },
     toString:function(){
         return 0;
     }
    

    };

console.log(obj==[]);</code></pre>

最后這個呢?

var obj = {
        valueOf: function() {
            return {a:1};
        },
        toString:function(){
            return "0";
        }
    };

console.log(obj==[]);</code></pre>

九、相關資料

從 []==![] 為 true 來剖析 JavaScript 各種蛋疼的類型轉換 (厚顏無恥的也參考一下自己文章)

ECMAScript5.1中文版

Annotated ECMAScript 5.1

JavaScript 秘密花園

w3school中文文檔

JS魔法堂:ASI(自動分號插入機制)和前置分號

JavaScript中,{}+{}等于多少?

 

來自:https://github.com/jawil/blog/issues/5

 

 

 本文由用戶 oke66 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!