CodeMash 2012의 'Wat'대화에서 언급 된이 이상한 JavaScript 동작에 대한 설명은 무엇입니까?


753

CodeMash 2012 '와트'이야기는 기본적으로 루비와 자바 스크립트와 약간 기괴한 단점을 지적한다.

http://jsfiddle.net/fe479/9/ 에서 결과의 JSFiddle을 만들었습니다 .

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 + Object
{} + {}
result:
[Object][Object]

이것은 비디오와도 일치하지 않으며, 변수를 출력하면 두 객체가 어떻게됩니까? 어쩌면 내 JSFiddle이 잘못되었을 수 있습니다.

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

왓 + 1을하면 wat1wat1wat1wat1...

나는 이것이 문자열에서 숫자를 빼려고하면 NaN이되는 간단한 행동이라고 생각합니다.


4
내가 설명대로 {} +는 [], basicaly 유일한 까다로운 및 구현에 의존 하나 여기 그것이 문장이나 표현으로 구문 분석에 의존하기 때문에. 어떤 환경에서 테스트하고 있습니까 (Firefow 및 Chrome에서는 0을 예상했지만 NodeJs에는 "[object Object]"가 있음)?
hugomg 2018 년

1
Windows 7에서 Firefox 9.0.1을 실행하고 있으며 JSFiddle은 [Object]로 평가합니다.
NibblyPig

@missingno 나는 NodeJS REPL에서 0을 얻었습니다
OrangeDog

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. [] + {}

    와 마찬가지로 [] + []두 피연산자가 모두 기본 요소로 먼저 변환됩니다. "객체 객체"(§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.2 , Array(16)가입 인수의 값을 얻으려면 길이 (16)로 새로운 배열을 생성, §11.6.2 우리가에 두 피연산자를 변환해야한다는 # 5, # 6은 단계 를 사용하여 번호를 지정하십시오 ToNumber. ToNumber(1)단순히 1 ( §9.3 ) 반면, ToNumber("wat")다시 인 NaN§9.3.1 . 의 7 단계에 따라 §11.6.2 , §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
왜 [] +1 => "1"과 [] -1 => -1입니까?
Rob Elsner 5

4
@RobElsner 는 rhs 피연산자 와 마찬가지로 []+1거의 동일한 논리를 따릅니다 . 대한 의 설명 참조 그 기억 5. 지점은 0 (점 3)입니다. []+[]1.toString()[]-1"wat"-1ToNumber(ToPrimitive([]))
Ventero

4
이 설명이 누락되었거나 자세한 내용이 생략되었습니다. 예를 들어, "객체 (이 경우에는 배열)를 프리미티브로 변환하면 기본값이 반환됩니다. 유효한 toString () 메서드가있는 객체의 경우 object.toString ()을 호출 한 결과입니다"는 valueOf의 []가 먼저 호출되지만 반환 값은 기본이 아니며 (배열 임) []의 toString이 대신 사용됩니다. 내가 대신 실제 심층적 인 설명에이를보고 추천 할 것입니다 2ality.com/2012/01/object-plus-object.html
jahav의

30

이것은 답변보다 더 많은 의견이지만, 어떤 이유로 든 귀하의 질문에 대해서는 언급 할 수 없습니다. JSFiddle 코드를 수정하고 싶었습니다. 그러나 나는 이것을 Hacker News에 게시했으며 누군가 내가 그것을 다시 게시 할 것을 제안했습니다.

JSFiddle 코드의 문제점은 ({})(괄호 안의 중괄호 열기)가 {}(코드 줄의 시작과 같이 중괄호 열기) 와 같지 않다는 것 입니다. 따라서 입력 out({} + [])할 때 입력 할 {}때가 아닌 다른 것을 강제합니다 {} + []. 이것은 자바 스크립트의 전반적인 '왓'의 일부입니다.

기본 아이디어는 간단한 JavaScript가 다음 두 형식을 모두 허용하고자한다는 것입니다.

if (u)
    v;

if (x) {
    y;
    z;
}

이 1. : 이렇게하려면, 두 가지 해석이 여는 중괄호로 만들어진 필요하지 2.이 나타날 수 있습니다 어디서나 .

이것은 잘못된 움직임이었습니다. 실제 코드에는 중간에 여는 괄호가 없으며 실제 코드는 두 번째가 아닌 첫 번째 형식을 사용할 때 더 취약합니다. (마지막 직장에서 2 개월마다 한 번씩, 내 코드 수정이 작동하지 않을 때 동료의 책상에 전화를 받았는데 문제는 곱슬 머리를 추가하지 않고 "if"에 줄을 추가했다는 것이 었습니다 나는 단지 한 줄만 쓸 때에도 항상 중괄호가 필요한 습관을 채택했습니다.)

다행스럽게도 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

4
@JessTelford ES6에서는 let블록 범위 변수를 선언 하는 데 사용할 수 있습니다 .
Oriol

19

@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에서 {} + {} 란 무엇입니까? "


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에 대한 산술 연산을 받으 므로 다음과 같이 됩니다.NaNNaNArray(16).join(NaN)

0

이전에 공유 한 내용을 뒷받침합니다.

이 동작의 근본 원인은 부분적으로 JavaScript의 형식이 약하기 때문입니다. 예를 들어 피연산자 유형 (int, string) 및 (int int)에 따라 두 가지 가능한 해석이 있으므로 1 + "2"라는 표현은 모호합니다.

  • 사용자는 두 문자열을 연결하려고합니다. 결과 : "12"
  • 사용자는 두 개의 숫자를 추가하려고합니다. 결과 : 3

따라서 다양한 입력 유형으로 출력 가능성이 증가합니다.

추가 알고리즘

  1. 피연산자를 기본 값으로 강제 변환

JavaScript 프리미티브는 string, number, null, undefined 및 boolean입니다 (심볼은 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.