CodeMash 2012の「ワット」講演で言及されたこれらの奇妙なJavaScript動作の説明は何ですか?


753

CodeMash 2012「ワット」の話は、基本的にRubyとJavaScriptのいくつかの奇妙な癖を指摘しています。

結果のJSFiddleをhttp://jsfiddle.net/fe479/9/で作成しました

(Rubyを知らないので)JavaScript固有の動作を以下に示します。

JSFiddleで、私の結果の一部がビデオの結果と一致しないことがわかりましたが、その理由はわかりません。ただし、JavaScriptがどのようにバックグラウンドで動作するかを知りたいと思っています。

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

+JavaScriptで配列を使用する場合の演算子にかなり興味があります。これはビデオの結果と一致します。

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

これはビデオの結果と一致します。何が起きてる?なぜこれがオブジェクトなのか。+オペレーターは何をしますか?

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

これはビデオと一致しません。ビデオは結果が0であることを示唆していますが、[オブジェクト]を取得しています。

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

これもビデオと一致しません。変数を出力すると2つのオブジェクトがどのように生成されますか?多分私のJSFiddleは間違っています。

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

ワット+ 1を実行するとwat1wat1wat1wat1...

これは文字列から数値を減算しようとするとNaNが発生する単純な動作だと思います。


4
{} + []は、ステートメントまたは式として解析されることに依存しているため、ここで説明するように、基本的にはトリッキーで実装に依存する唯一のものです。どの環境でテストしていますか(FirefowとChromeでは予想どおり0でしたが、NodeJで「[オブジェクトオブジェクト]」が取得されました)?
hugomg 2012年

1
私はWindows 7でFirefox 9.0.1を実行していますが、JSFiddleはそれを[Object]に評価します
NibblyPig

@missingno NodeJS REPLで0を取得
OrangeDog 2012年

41
Array(16).join("wat" - 1) + " Batman!"
Nick Johnson

1
@missingno質問を投稿ここでは、しかし、のために{} + {}
IonicăBizău

回答:


1479

これは、表示されている(および表示されるはずの)結果の説明のリストです。私が使用しているリファレンスはECMA-262標準のものです。

  1. [] + []

    加算演算子を使用する場合、左と右の両方のオペランドがプリミティブに最初に変換されます(§11.6.1)。どおり§9.1、プリミティブ戻りに有効とオブジェクトについて、そのデフォルト値、(ここでは配列)オブジェクトを変換するtoString()メソッド呼び出しの結果であるobject.toString()§8.12.8を)。配列の場合、これは呼び出しarray.join()§15.4.4.2)と同じです。空の配列を結合すると空の文字列が生成されるため、加算演算子のステップ7で2つの空の文字列を連結したものが返されます。これは空の文字列です。

  2. [] + {}

    と同様に[] + []、両方のオペランドが最初にプリミティブに変換されます。"オブジェクトオブジェクト"(§15.2)の場合も、これはを呼び出した結果です。これは、object.toString()nullでない、未定義でないオブジェクトの場合"[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]"(参照[] + {})。したがって、の結果を取得+{}するToNumberには、文字列に適用する必要があります"[object Object]"。以下からのステップ以下の場合は§9.3.1を、我々が得るNaN結果として:

    文法はの拡張として文字列を解釈できない場合はStringNumericLiteral、その後の結果はToNumberはあるのNaN

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

    §15.4.1.1§15.4.2.2Array(16)参加する引数の値を取得するには長さ16を使用して新しい配列を作成し、§11.6.2我々はAに両方のオペランドを変換する必要があること#5、#6に示す手順番号を使用してToNumberToNumber(1)単に1(§9.3)、一方、ToNumber("wat")再びNaN通り§9.3.1§11.6.2のステップ7に続いて§11.6.3では

    いずれかのオペランドがNaNの場合、結果はNaNになります。

    したがって、の引数Array(16).joinNaNです。§15.4.4.5(Array.prototype.join)に従ってToString"NaN"§9.8.1)である引数を呼び出す必要があります。

    mNaNの場合、文字列を返し"NaN"ます。

    §15.4.4.5のステップ10に続いて、連結"NaN"と空の文字列を15回繰り返します。これは、表示されている結果と同じです。as引数の"wat" + 1代わりに使用する場合"wat" - 1、加算演算子は数値1に変換"wat"する代わりに文字列に変換するため、効果的にを呼び出しますArray(16).join("wat1")

{} + []ケースの結果が異なる理由について:関数の引数として使用すると、ステートメントがExpressionStatementになる{}ため、空のブロックとして解析できなくなり、代わりに空のオブジェクトとして解析されます。リテラル。


2
では、なぜ[] +1 => "1"および[] -1 => -1なのでしょうか。
Rob Elsner、2014年

4
@RobElsner はrhsオペランドを使用するだけで[]+1、とほぼ同じロジックに従います。説明を参照のことを覚えておいてください。5.時点では0(ポイント3)です。[]+[]1.toString()[]-1"wat"-1ToNumber(ToPrimitive([]))
Ventero 2014年

4
この説明は欠けている/多くの詳細を省略しています。たとえば、「オブジェクト(この場合は配列)をプリミティブに変換すると、デフォルト値が返されます。これは、有効なtoString()メソッドを持つオブジェクトの場合、object.toString()を呼び出した結果ですが、[]のvalueOfが完全に欠落しています。最初に呼び出されますが、戻り値がプリミティブ(配列)でないため、代わりに[]のtoStringが使用されます。実際の詳細な説明2ality.com/2012/01/object-plus-object.html
jahav

30

これは回答というよりコメントですが、何らかの理由であなたの質問にコメントすることはできません。JSFiddleコードを修正したかったのですが。しかし、私はこれをハッカーニュースに投稿し、誰かがここに再投稿することを提案しました。

JSFiddleコードの問題は、({})(括弧内の左中括弧)が{}(コード行の開始としての中括弧)と同じではないことです。したがって、タイプout({} + [])するとき、タイプするとき{}ではない何かを強制します{} + []。これは、JavaScriptの全体的な「ワット」の一部です。

基本的な考え方は、これらの両方のフォームを許可する単純なJavaScriptでした。

if (u)
    v;

if (x) {
    y;
    z;
}

これを行うために、左中かっこの2つの解釈が行われました。1。必須ではありません。2 。どこにでも表示できます。

これは間違った動きでした。実際のコードでは、途中に開始ブレースが表示されません。また、2番目ではなく最初の形式を使用すると、実際のコードはより壊れやすくなる傾向があります。(私の最後の仕事で約1か月に1回、私のコードへの変更が機能していないときに同僚のデスクに呼び出されました。問題は、カーリーを追加せずに「if」に行を追加したことでした。中かっこ。結局、1行だけを書いているときでも、中かっこが常に必要になるという習慣を採用しました。)

幸い、多くの場合、eval()はJavaScriptの完全な機能を複製します。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()の両方に関係するものを書くのは少し汚い気がします。]


15
This was a wrong move. Real code doesn't have an opening brace appearing in the middle of nowhere-同意しない(一種の):過去に、Cの変数のスコープにこのようなブロックを使用することがよくあります。この習慣は、スタック上の変数がスペースを占有する組み込みCを実行するときにしばらくの間取り上げられたため、それらが不要になった場合は、ブロックの最後でスペースを解放する必要があります。ただし、ECMAScriptのスコープはfunction(){}ブロック内のみです。したがって、私はこの概念が間違っていることに同意しませんが、JSの実装が(おそらく)間違っていることに同意します。
Jess Telford 2013年

4
@JessTelford ES6では、letブロックスコープの変数を宣言するために使用できます。
Oriol

19

@Venteroのソリューションの2番目です。必要に応じて、+オペランドの変換方法について詳しく説明します。

最初のステップ(§9.1): (プリミティブ値は、プリミティブに両方のオペランドを変換undefinednull、ブール値、数値、文字列、他のすべての値は、配列および機能を含むオブジェクトです)。オペランドがすでにプリミティブである場合は、完了です。そうでない場合はオブジェクトで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

2番目の手順(§11.6.1):オペランドの1つが文字列の場合、もう1つのオペランドも文字列に変換され、2つの文字列を連結して結果が生成されます。それ以外の場合は、両方のオペランドが数値に変換され、それらを加算して結果が生成されます。

変換プロセスの詳細な説明:「JavaScriptの{} + {}とは何ですか?


13

私たちは仕様を参照するかもしれませんが、それは素晴らしくて最も正確ですが、ほとんどの場合は、次のステートメントでよりわかりやすい方法で説明することもできます。

  • +そして、-演算子は、プリミティブ値でのみ動作します。より具体的には、+(加算)は文字列または数値のいずれかで動作し、+(単項)および-(減算および単項)は数値でのみ動作します。
  • 引数としてプリミティブ値を期待するすべてのネイティブ関数または演算子は、最初にその引数を目的のプリミティブ型に変換します。これは、任意のオブジェクトで使用できる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)

0

以前に共有されたものを支持する。

この動作の根本的な原因の一部は、JavaScriptの型付けが弱いためです。たとえば、オペランドタイプ(int、string)および(int int)に基づいて2つの可能な解釈があるため、式1 +“ 2”はあいまいです。

  • ユーザーは2つの文字列を連結しようとしています。結果は「12」です。
  • ユーザーは2つの数値を追加しようとしています。結果:3

したがって、さまざまな入力タイプで、出力の可能性が増加します。

加算アルゴリズム

  1. オペランドをプリミティブ値に強制変換

JavaScriptプリミティブは、文字列、数値、null、未定義、およびブールです(シンボルはES6で近日提供予定)。その他の値はすべてオブジェクトです(配列、関数、オブジェクトなど)。オブジェクトをプリミティブ値に変換する強制プロセスは次のように説明されます。

  • object.valueOf()が呼び出されたときにプリミティブ値が返された場合は、この値を返します。それ以外の場合は続行します

  • object.toString()が呼び出されたときにプリミティブ値が返された場合は、この値を返します。それ以外の場合は続行します。

  • TypeErrorをスローする

注:日付値の場合、順序はvalueOfの前にtoStringを呼び出すことです。

  1. オペランドの値が文字列の場合、文字列を連結します

  2. それ以外の場合は、両方のオペランドを数値に変換してから、これらの値を追加します

JavaScriptの型のさまざまな強制値を知ることは、混乱する出力をより明確にするのに役立ちます。以下の強制表を参照してください

+-----------------+-------------------+---------------+
| Primitive Value |   String value    | Numeric value |
+-----------------+-------------------+---------------+
| null            | null            | 0             |
| undefined       | undefined       | NaN           |
| true            | true            | 1             |
| false           | false           | 0             |
| 123             | 123             | 123           |
| []              | “”                | 0             |
| {}              | “[object Object]” | NaN           |
+-----------------+-------------------+---------------+

また、JavaScriptの+演算子は左結合であることを知っておくとよいでしょう。

文字列を含む追加は常にデフォルトで文字列連結になるため、「1 + 2」を利用すると「12」が得られます。

あなたはこのブログ投稿でもっと多くの例を読むことができます(私が書いた免責事項)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.