網站搜尋

對於 CodeMash 2012 的「Wat」演講中提到的這些奇怪的 JavaScript 行為,有什麼解釋?


《CodeMash 2012 的 'Wat' 講座》基本上指出了 Ruby 和 JavaScript 的一些奇怪怪癖。

我已在 http://jsfiddle.net/fe479/9/ 做了一個 JSFiddle 顯示結果。

下列列出的是 JavaScript 特有的行為(因為我不懂 Ruby)。

我在 JSFiddle 中發現我的一些結果與視頻中的不一致,我不確定原因。然而,我對 JavaScript 在每種情況下如何在背後運作感到好奇。

Empty Array + Empty Array
[] + []
result:
<Empty String>

我對在 JavaScript 中與陣列一起使用 + 運算符非常好奇。這與影片的結果相符。

Empty Array + Object
[] + {}
result:
[Object]

這與影片的結果相符。這裡發生了什麼事?為什麼這是一個物件。+ 運算子是做什麼的?

Object + Empty Array
{} + []
result:
[Object]

這與影片不符。影片顯示結果是 0,而我得到的是 [Object]。

Object + Object
{} + {}
result:
[Object][Object]

這也與影片不符,而且輸出一個變數怎麼會產生兩個物件呢?也許我的 JSFiddle 有錯。

Array(16).join("wat" - 1)
result:
NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN

執行 wat + 1 會得到 wat1wat1wat1wat1...

我懷疑這只是直接的行為,試圖從字串中減去一個數字會導致 NaN。

答案#1

這裡是一份針對你所看到(以及應該看到)的結果的解釋清單。我使用的參考資料來自 ECMA-262 標準。

  1. [] + []

    在使用加法運算子時,左右操作數首先會被轉換成原始值 (§11.6.1)。根據 §9.1,將一個物件(在本例中為陣列)轉換為原始值會返回其預設值,而對於具有有效 toString() 方法的物件,預設值是呼叫 object.toString() 的結果 (§8.12.8)。對於陣列而言,這與呼叫 array.join() 相同 (§15.4.4.2)。對空陣列進行 join 的結果是空字串,因此加法運算子的第 #7 步會返回兩個空字串的串接結果,即空字串。

  2. [] + {}

    類似於 [] + [],兩個運算元首先都會被轉換為原始值。對於「Object 物件」 (§15.2) 來說,這又是呼叫 object.toString() 的結果,而對於非 null、非 undefined 的物件,這會是 "[object Object]" (§15.2.4.2)。

  3. {} + []

    這裡的 {} 並不是被解析為物件,而是作為一個空區塊解析(§12.1,至少當你沒有強制將該語句視為一個表達式時,不過這一點稍後會詳細說明)。空區塊的返回值是空的,所以該語句的結果與 +[] 相同。一元 + 運算子 (§11.4.6) 返回 ToNumber(ToPrimitive(operand))。我們已經知道,ToPrimitive([]) 是空字串,並且根據 §9.3.1,ToNumber("") 是 0。

  4. {} + {}

    類似於先前的例子,第一個 {} 被解析為一個返回值為空的區塊。同樣地,+{} 與 ToNumber(ToPrimitive({})) 相同,而 ToPrimitive({}) 是 "[object Object]"(參見 [] + {})。因此,要得到 +{} 的結果,我們必須對字串 "[object Object]" 應用 ToNumber。按照 §9.3.1 的步驟,我們得到的結果是 NaN:

    如果語法無法將字串解釋為 StringNumericLiteral 的擴展,則 ToNumber 的結果是 NaN。

  5. Array(16).join("wat" - 1)

    根據 §15.4.1.1 和 §15.4.2.2,Array(16) 會建立一個長度為 16 的新陣列。要取得 join 的參數值,§11.6.2 的步驟 #5 和 #6 顯示我們必須使用 ToNumber 將兩個運算元都轉換為數字。ToNumber(1) 只是 1 (§9.3),而 ToNumber("wat") 根據 §9.3.1 仍然是 NaN。依照 §11.6.2 的步驟 7,§11.6.3 規定

    如果任一運算元是 NaN,結果就是 NaN。

    所以傳給 Array(16).join 的參數是 NaN。根據 §15.4.4.5(Array.prototype.join),我們必須對參數呼叫 ToString,結果是 "NaN"(§9.8.1):

    如果 m 是 NaN,返回字串 "NaN"。

    根據 §15.4.4.5 的步驟 10,我們得到 "NaN" 與空字串連接的結果重複 15 次,這就是你看到的結果。當使用 "wat" + 1 而不是作為參數的 "wat" - 1 時,加法運算子會將 1 轉換為字串,而不是將 "wat" 轉換為數字,因此實際上會呼叫 Array(16).join("wat1")。

至於為什麼你在 {} + [] 的情況下看到不同的結果:當將它用作函數參數時,你正在強制該語句成為一個 ExpressionStatement,這使得無法將 {} 解析為空區塊,因此它反而被解析為一個空物件字面值。

答案#2

這更多的是一個評論而不是答案,但因為某些原因我無法對你的問題進行評論。我想要糾正你的 JSFiddle 代碼。不過,我在 Hacker News 上發佈了這個,有人建議我把它重新發佈到這裡。

在 JSFiddle 代碼中的問題是 ({})(括號內的開大括號)並不等同於 {}(作為程式碼行開頭的開大括號)。所以當你輸入 out({} + []) 時,你正強迫 {} 成為某個它實際上不是的東西,當你輸入 {} + [] 時也是如此。這是 Javascript 整體「wat」現象的一部分。

基本的想法很簡單,JavaScript 想要允許這兩種形式:

if (u)
    v;

if (x) {
    y;
    z;
}

為此,對開括號做了兩種解釋:1. 它是非必需的,2. 它可以出現在任意位置。

這是一個錯誤的舉動。真正的程式碼不會出現半天無緣無故的開括號,而且使用第一種形式而不是第二種形式的程式碼通常也比較脆弱。(在我上一份工作的時候,大約每隔一兩個月,我就會被叫到同事的桌前,因為他們對我的程式碼的修改無法正常運作,問題在於他們在“if”語句中加了一行卻沒有加上大括號。我最終養成了習慣,認為大括號總是必須的,即使你只寫一行程式碼。)

幸運的是,在許多情況下 eval() 將會複製 JavaScript 的完整 wat 特性。JSFiddle 的程式碼應該如下所示:

function out(code) {
    function format(x) {
        return typeof x === "string" ?
            JSON.stringify(x) : x;
    }   
    document.writeln('&gt;&gt;&gt; ' + code);
    document.writeln(format(eval(code)));
}
document.writeln("<pre>");
out('[] + []');
out('[] + {}');
out('{} + []');
out('{} + {}');
out('Array(16).join("wat" + 1)');
out('Array(16).join("wat - 1")');
out('Array(16).join("wat" - 1) + " Batman!"');
document.writeln("</pre>");

[而且這是我許多很多年以來第一次寫 document.writeln,我覺得寫任何涉及 document.writeln() 和 eval() 的東西都有點不乾淨的感覺。]

答案#3

我支持 @Ventero 的解決方案。如果你願意,你可以更詳細地說明 + 如何轉換其運算元。

第一步 (§9.1): 將兩個運算元都轉換為原始值(原始值包括 undefined、null、布林值、數字、字串;其他所有值都是物件,包括陣列和函數)。如果運算元已經是原始值,則完成。如果不是,則它是一個物件 obj,並執行以下步驟:

  1. 呼叫 obj.valueOf()。如果它返回一個原始值,那麼你就完成了。Object 和陣列的直接實例會返回它們自己,所以你還沒完成。
  2. 呼叫 obj.toString()。如果它返回一個原始值,那你就完成了。{} 和 [] 都會返回一個字串,所以你已經完成了。
  3. 否則,拋出一個 TypeError。

對於日期,第 1 步和第 2 步互換。你可以如下觀察轉換行為:

var obj = {
    valueOf: function () {
        console.log("valueOf");
        return {}; // not a primitive
    },
    toString: function () {
        console.log("toString");
        return {}; // not a primitive
    }
}

互動(Number() 首先轉換為原始值,然後轉換為數字):

> Number(obj)
valueOf
toString
TypeError: Cannot convert object to primitive value

第二步 (§11.6.1): 如果其中一個運算元是字串,另一個運算元也會被轉換為字串,結果由兩個字串串接而成。否則,兩個運算元都會被轉換為數字,結果由它們相加而得。

轉換過程的更詳細說明:「在 JavaScript 中,{} + {} 是什麼?」

答案#4

我們可以參考規格說明,這很好且最準確,但大多數情況也可以用以下說明更易理解地解釋:

  • + 和 - 運算子僅適用於原始值。更具體地說,+(加法)可以與字串或數字一起使用,而 +(一元運算)和 -(減法和一元運算)僅能與數字一起使用。
  • 所有期望原始值作為參數的原生函數或運算子,會先將該參數轉換為所需的原始類型。這是透過 valueOf 或 toString 完成的,這些方法在任何物件上都是可用的。這就是為什麼當這些函數或運算子在物件上調用時不會拋出錯誤的原因。

所以我們可以說:

  • [] + [] 和 String([]) + String([]) 一樣,也和 '' + '' 一樣。我上面提到過 +(加法)對數字也是有效的,但在 JavaScript 中陣列沒有有效的數字表示方式,所以改用字串相加。
  • [] + {} 等同於 String([]) + String({}),也等同於 '' + '[object Object]'
  • {} + []。這個需要更多的解釋(參見 Ventero 的回答)。在這種情況下,大括號並不是被當作物件,而是當作一個空的區塊,因此結果與 +[] 相同。單目運算子 + 僅能對數字使用,所以實作會試圖從 [] 取得數字。首先它會嘗試 valueOf,而對於陣列來說會回傳同一個物件,因此接下來它會使用最後的手段:將 toString 的結果轉換為數字。我們可以寫作 +Number(String([])),這與 +Number('') 相同,也就是 +0。
  • Array(16).join("wat" - 1) 減法 - 僅適用於數字,所以它與以下程式碼相同:Array(16).join(Number("wat") - 1),因為 "wat" 無法轉換為有效的數字。我們會得到 NaN,而對 NaN 的任何算術運算結果仍是 NaN,所以我們得到:Array(16).join(NaN)。