재미있고 교묘한 JavaScript 예제
JavaScript는 훌륭한 언어입니다. JavaScript는 구문이 단순하며 큰 생태계를 가지고 있습니다. 가장 중요한 점은 훌륭한 공동체를 가지고 있다는 것입니다.
동시에, 우리 모두는 JavaScript가 까다로운 부분을 가진 꽤 재미있는 언어라는 것을 알고 있습니다. 몇몇 특징은 우리의 일상적인 일을 순식간에 지옥으로 바꾸기도 하고, 우리를 크게 웃게 만들기도 합니다.
WTFJS의 아이디어는 Brian Leroux에 속해있습니다. 이 목록들은 그의 이야기에서 꽤 영감을 받았습니다. “WTFJS” at dotJS 2012:
이 핸드북은 npm
를 이용하여 설치할 수 있습니다. 그냥 실행합시다:
$ npm install -g wtfjs
이제 당신은 커맨드 창에서 'wtfjs'를 실행할 수 있게 되었습니다. 당신이 선택한 '$PAGER'에서 'wtfjs'가 열릴 것 입니다. 아니면 계속 여기서 읽어도 됩니다.
출처는 https://github.com/denysdovhan/wtfjs 여기에서 확인 할 수 있습니다.
현재, wtfjs는 아래와 같은 언어로 번역되었습니다:
- 💪🏻 시작하기에 앞서
- ✍🏻 표기법
- 👀 예제
[]
와![]은 같다
true
는![]
와 같지 않지만,[]
와도 같지 않다- true는 false
- baNaNa
NaN
은NaN
이 아니다- 이것은 실패다
[]
은 truthy 이지만true
는 아니다null
은 falsy 이지만false
은 아니다document.all
은 객체이지만undefined
이다- 최소 값은 0 보다 크다
- 함수는 함수가 아니다
- 배열 추가
- 배열의 후행 쉼표
- 배열 평등은 몬스터
undefined
과Number
parseInt
은 나쁜 놈이다true
와false
를 이용한 수학- HTML 주석은 JavaScript에서도 유효하다
NaN
은 숫자가 아니다[]
과null
은 객체이다- 마법처럼 증가하는 숫자
- 정확도
0.1 + 0.2
- 패치 번호
- 세 숫자의 비교
- 재미있는 수학
- RegExps 추가
- 문자열은
String
의 인스턴스가 아니다 - backticks으로 함수 호출
- Call call call
constructor
속성- 객체 속성의 키로서의 객체
__proto__
을 사용한 프로토 타입 접근`${{Object}}`
- 디폴트 값으로 구조 해제
- Dots와 spreading
- 라벨
- 중첩된 라벨들
- 교활한
try..catch
- 이것은 다중 상속인가?
- 스스로 생성되는 Generator
- 클래스의 클래스
- 강제할 수 없는 객체
- 까다로운 화살표 함수
- 화살표 함수는 생성자가 될 수 없다
arguments
와 화살표 함수- 까다로운 return
- 객체에 할당 연결
- 배열을 사용한 객체 속성 접근 s
- Null 및 관계 연산자
Number.toFixed()
다른 숫자 표시Math.max()
이하Math.min()
null
과0
비교- 동일한 변수 재선언
- 디폴트 동작 Array.prototype.sort()
- resolve()은 Promise instance를 반환하지 않는다
- 📚 기타 resources
- 🎓 License
— “Just for Fun: 우연한 혁명가의 이야기”, Linus Torvalds
이 목록의 주요 목표는 가능한 JavaScript의 몇 가지의 엄청난 예제들을 모으고, 작동 방식을 설명하는 것 입니다. 이전에 우리가 몰랐던 것들을 배우는 것이 재미있기 때문입니다.
당신이 초보자라면, 이 노트를 사용하여 JavaScript에 대해 자세히 알아볼 수 있을 것입니다. 이 노트의 설명을 읽는 것에 더 많은 시간을 할애할 수 있기를 바랍니다.
당신이 전문 개발자라면, 우리가 사랑하는 JavaScript의 모든 기이한 점과 예상치 못한 것들에 대한 예시에 훌륭한 참조로 간주할 수 있습니다.
어쨌든, 이것을 읽읍시다. 당신은 아마 새로운 것들을 찾을 수 있을 것입니다.
// ->
식의 결과를 표시하는 데 사용됩니다. 예를 들면:
1 + 1; // -> 2
// >
console.log
또는 다른 출력의 결과를 의미합니다. 예를 들면:
console.log("hello, world!"); // > hello, world!
//
설명에 사용되는 주석입니다. 예를 들면:
// Assigning a function to foo constant
const foo = function() {};
배열은 배열이 아닙니다:
[] == ![]; // -> true
추상 항등 연산자는 양쪽을 숫자로 변환하여 비교하고, 서로 다른 이유로 양 쪽의 숫자는 0
이 됩니다. 배열은 truthy 하므로, 오른쪽의 값은 0
을 강요하는 truthy value의 반대 값 즉, false
입니다. 그러나 왼쪽은 빈 배열은 먼저 boolean이 되지 않고 숫자로 강제 변환되고 빈 배열은 truthy 임에도 불구하고 0
으로 강요됩니다.
이 표현식이 어떻게 단순화 되는지는 아래와 같습니다:
+[] == +![];
0 == +false;
0 == 0;
true;
배열은 true
와 같지 않지만 배열이 아닌것도 true
와 같지 않습니다;
배열은 false
와 같지만 배열이 아닌것도 false
와 같습니다:
true == []; // -> false
true == ![]; // -> false
false == []; // -> true
false == ![]; // -> true
true == []; // -> false
true == ![]; // -> false
// According to the specification
true == []; // -> false
toNumber(true); // -> 1
toNumber([]); // -> 0
1 == 0; // -> false
true == ![]; // -> false
![]; // -> false
true == false; // -> false
false == []; // -> true
false == ![]; // -> true
// According to the specification
false == []; // -> true
toNumber(false); // -> 0
toNumber([]); // -> 0
0 == 0; // -> true
false == ![]; // -> true
![]; // -> false
false == false; // -> true
!!"false" == !!"true"; // -> true
!!"false" === !!"true"; // -> true
다음 단계를 고려합시다:
// true is 'truthy' and represented by value 1 (number), 'true' in string form is NaN.
true == "true"; // -> false
false == "false"; // -> false
// 'false' is not the empty string, so it's a truthy value
!!"false"; // -> true
!!"true"; // -> true
"b" + "a" + +"a" + "a"; // -> 'baNaNa'
이것은 JavaScript에서 구식 농담이지만 재해석 되었습니다. 원본은 다음과 같습니다:
"foo" + +"bar"; // -> 'fooNaN'
식은 'foo' + (+'bar')
으로 평가되고 숫자가 아닌 'bar'
형태로 변환됩니다.
NaN === NaN; // -> false
아래의 사항들로 동작의 논리를 엄격하게 정의합니다:
- 만약
Type(x)
와Type(y)
가 다르면 false를 반환합니다.- 만약
Type(x)
이 숫자이고
x
가 NaN이면 false를 반환합니다.y
가 NaN이면 false를 반환합니다.- … … …
IEEE에서 정의한 NaN
:
4 개의 상호 배타적인 관계 : 보다 작음, 같음, 보다 큼, 순서 없음. 마지막의 경우 하나 이상의 피연산자가 NaN일 때 발생합니다. 모든 NaN은 자신을 포함한 모든 것과 순서 없이 비교해야 합니다.
— “IEEE754 NaN 값에 false를 반환하는 것의 근거는 무엇입니까?” StackOverflow에서
당신은 믿지 않을지도 모르지만 …
(![] + [])[+[]] +
(![] + [])[+!+[]] +
([![]] + [][[]])[+!+[] + [+[]]] +
(![] + [])[!+[] + !+[]];
// -> 'fail'
기호를 하나하나 나누면 아래와 같은 패턴이 자주 발생하는 것을 알 수 있습니다:
![] + []; // -> 'false'
![]; // -> false
그래서 []
를 false
으로 바꾸는 시도를 해봅니다. 하지만 많은 내부 함수 호출(binary + Operator
-> ToPrimitive
-> [[DefaultValue]]
)때문에 오른쪽 피 연산 문자열로 변환하게 됩니다:
![] + [].toString(); // 'false'
문자열을 배열로 생각하면 [0]
을 통해 첫 번째 문자에 접근할 수 있습니다:
"false"[0]; // -> 'f'
나머지는 분명하지만 i
는 꽤 까다롭습니다. fail
속 i
는 'falseundefined'라는 문자열을 생성하고 ['10']
인덱스를 사용하여 요소를 잡습니다.
배열은 truthy 한 값이지만 true
와 같지는 않다.
!![] // -> true
[] == true // -> false
다음은 ECMA-262 명세된 것의 세션에 대한 링크입니다:
null
은 falsy 값이라는 사실에도 불구하고 false
는 아닙니다.
!!null; // -> false
null == false; // -> false
동시에 0
또는 ''
와 같은 falsy 값은 false
와 동일합니다.
0 == false; // -> true
"" == false; // -> true
설명은 이전 예제와 동일합니다. 다음은 해당 링크입니다:
⚠️ 이 파트는 브라우저 API 의 일부이며 Node.js 환경에서는 작동하지 않습니다.⚠️
document.all
은 배열과 같은 클래스이고 페이지의 DOM 노드에 대한 엑세스를 제공한다는 사실에도 불구하고 typeof
함수의 undefined
으로 반응합니다.
document.all instanceof Object; // -> true
typeof document.all; // -> 'undefined'
동시에 document.all
은 undefined
와 동일하지 않습니다.
document.all === undefined; // -> false
document.all === null; // -> false
하지만 동시에:
document.all == null; // -> true
특히 이전 버전의 IE에서
document.all
은 DOM 요소에 접근하는 방법을 사용했습니다. 이것은 표준이 된 적은 없지만 이전 JavaScript 코드에서 사용되었습니다. 새로운 APIs(document.getElementById
와 같은)에서 표준이 진행되었을 때 이 API 호출은 쓸모 없게 되었고 표준 위원회는 이를 어떻게 처리할지 결정해야 했습니다. 광범위하게 사용되기 때문에 그들은 API를 유지하기로 결정했지만 JavaScript 명세된 것을 고의로 위반했습니다. 이것이undefined
의 상황에서 엄격한 평등 비교을 사용했을 때false
를 응답하고 추상 평등 비교을 사용할 때true
로 응답하는 이유는 명시적으로 허용하는 명세된 것의 의도적인 위반 때문입니다.— “오래된 특징 - document.all” WhatWG의 HTML 명세된 것 — “Chapter 4 - ToBoolean - Falsy values” YDKJS의 Types & Grammar
Number.MIN_VALUE
은 0 보다 큰 가장 작은 숫자입니다:
Number.MIN_VALUE > 0; // -> true
Number.MIN_VALUE
은5e-324
입니다. 즉, 부동 소수점 정밀도 내에서 표현할 수 있는 가장 작은 양수입니다. 이 말은 0 에 도달할 수 있는 가장 가까운 값이라는 의미 입니다. 이것은 소수가 제공할 수 있는 최상의 값이라고 정의할 수 있습니다.비록 엄격하게 실제로 숫자는 아니지만 전체적으로 가장 작은 값은
Number.NEGATIVE_INFINITY
이라고 할 수 있습니다.— “자바 스크립트에서 왜
0
은Number.MIN_VALUE
보다 작습니까?” StackOverflow에서
⚠️ V8 v5.5 또는 그 이하의 버전에서는 버그가 있을 수 있습니다.(Node.js <=7)⚠️
이것을 undefined is not a function 모두가 알고 있지만 이건 어떨까요?
// Declare a class which extends null
class Foo extends null {}
// -> [Function: Foo]
new Foo() instanceof null;
// > TypeError: function is not a function
// > at … … …
이것은 명세된 것의 일부가 아닙니다. 현재 수정된 버그 일 뿐이므로 향후 아무 문제 없을 것입니다.
두 개의 배열을 추가하려면 어떻게 해야 할까요?
[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'
연결이 발생합니다.차근차근 다음을 봅시다:
[1, 2, 3] +
[4, 5, 6][
// call toString()
(1, 2, 3)
].toString() +
[4, 5, 6].toString();
// concatenation
"1,2,3" + "4,5,6";
// ->
("1,2,34,5,6");
4 개의 빈 배열을 만듭니다. 그럼에도 불구하고 후행 쉼표로 인해 세가지 , 요소가 있는 배열을 얻게 됩니다:
let a = [, , ,];
a.length; // -> 3
a.toString(); // -> ',,'
후행 쉼표 ("마지막 쉼표"라고도 함)는 JavaScript 에 새로운 요소, 매개 변수 또는 속성을 추가할 때 유용하게 사용할 수 있습니다. 만약 새 속성을 추가하려는 상황에서 이미 후행 쉼표를 사용하고 있는 경우 이전 마지막 줄을 수정하지 않고 새 줄을 추가할 수 있습니다. 이렇게 하면 버전 관리가 더 깔끔 해지고 코드 편집이 덜 번거로울 수 있습니다.
— 후행 쉼표 MDN에서
배열 평등은 아래에서 볼 수 있듯 JavaScript에서는 몬스터입니다:
[] == '' // -> true
[] == 0 // -> true
[''] == '' // -> true
[0] == 0 // -> true
[0] == '' // -> false
[''] == 0 // -> true
[null] == '' // true
[null] == 0 // true
[undefined] == '' // true
[undefined] == 0 // true
[[]] == 0 // true
[[]] == '' // true
[[[[[[]]]]]] == '' // true
[[[[[[]]]]]] == 0 // true
[[[[[[ null ]]]]]] == 0 // true
[[[[[[ null ]]]]]] == '' // true
[[[[[[ undefined ]]]]]] == 0 // true
[[[[[[ undefined ]]]]]] == '' // true
아래의 예제를 주의 깊게 살펴 보아야 합니다! 이 동작은 7.2.13 추상 동등 비교에 설명되어 있습니다.
Number
생성자에 인수를 전달하지 않으면 0
값을 얻게 됩니다. 실제 인수가 없는 경우 undefined
값이 형식 인수에 할당되기 때문에 인수가 없는 Number
는 매개 변수 값으로 undefined
를 사용합니다. 그러나 undefined
를 통과하면 NaN
을 얻을 수 있습니다.
Number(); // -> 0
Number(undefined); // -> NaN
명세된 것에 따르면:
- 함수의 호출로 인수가 전달되지 않은 경우
n
은+0
이 됩니다. - 또는 let
n
be ?ToNumber(value)
. undefined
의 경우ToNumber(undefined)
는NaN
으로 반환해야 합니다.
다음은 해당 부분입니다:
parseInt
은 특이한 점으로 유명합니다:
parseInt("f*ck"); // -> NaN
parseInt("f*ck", 16); // -> 15
💡 설명: 이는 parseInt
알 수 없는 문자에 도달할 때까지 문자별로 계속 구문 분석을 하기 때문에 발생합니다. 'f*ck'
에서 f
는 16 진수로 15
입니다.
Infinity
정수로 파싱하는 것은…
//
parseInt("Infinity", 10); // -> NaN
// ...
parseInt("Infinity", 18); // -> NaN...
parseInt("Infinity", 19); // -> 18
// ...
parseInt("Infinity", 23); // -> 18...
parseInt("Infinity", 24); // -> 151176378
// ...
parseInt("Infinity", 29); // -> 385849803
parseInt("Infinity", 30); // -> 13693557269
// ...
parseInt("Infinity", 34); // -> 28872273981
parseInt("Infinity", 35); // -> 1201203301724
parseInt("Infinity", 36); // -> 1461559270678...
parseInt("Infinity", 37); // -> NaN
null
을 파싱하는 것에도 주의합시다:
parseInt(null, 24); // -> 23
💡 설명:
null
을 문자열"null"
로 변환하려고 합니다. 0 부터 23 까지의 기수에 대해서 변환할 수 있는 숫자가 없으므로 NaN을 반환합니다. 24 에,"n"
, 14 번째 문자가 숫자 체계에 추가됩니다. 31에,"u"
, 21 번째 문자가 추가되고 전체 문자열을 디코딩 할 수 있게 되었습니다. 37에서 더 이상 생성할 수 있는 유효 숫자 집합이 없으며NaN
이 반환됩니다.— “parseInt(null, 24) === 23… wait, what?” StackOverflow에서
8 진수에 대해서 잊지맙시다:
parseInt("06"); // 6
parseInt("08"); // 8 if support ECMAScript 5
parseInt("08"); // 0 if not support ECMAScript 5
💡 설명: 입력 문자열이 "0"으로 시작하는 경우, 기수는 8 (octal) 또는 10 (decimal)입니다. 정확히는 어떤 기수가 선택되는가는 구현에 따라 다릅니다. ECMAScript 5는 10 (decimal)진수를 사용하도록 지정하지만 모든 브라우저가 이것을 지원하지는 않습니다. 그러므로 parseInt
을 사용할 때는 항상 기수를 지정합시다.
parseInt
항상 입력을 문자열로 변환:
parseInt({ toString: () => 2, valueOf: () => 1 }); // -> 2
Number({ toString: () => 2, valueOf: () => 1 }); // -> 1
부동 소수점값을 파싱하는 동안 주의하세요.
parseInt(0.000001); // -> 0
parseInt(0.0000001); // -> 1
parseInt(1 / 1999999); // -> 5
💡 설명: ParseInt
은 문자열 인수를 취하고 지정된 기수의 정수를 반환합니다. 또한 ParseInt
은 문자열 매개 변수에서 첫 번째가 아닌 숫자를 포함하여 모든 것을 제거합니다. 0.000001
은 문자열 "0.000001"로 바뀌고
parseInt은
0으로 반환됩니다.
0.0000001이 문자열로 변환되면
"1e-7"로 되므로
parseInt은
1을 반환합니다.
1/1999999은
5.00000250000125e-7로 해석되고
parseInt은
5`을 리턴합니다.
몇 가지 수학을 해봅시다:
true -
true +
// -> 2
(true + true) * (true + true) -
true; // -> 3
흠… 🤔
Number
생성자를 사용하여 값을 숫자로 강제 변환할 수 있습니다. true
가 1
로 강제되는 것은 분명합니다:
Number(true); // -> 1
단항 더하기 연산자는 값을 숫자로 변환하려고 합니다. 이것은 정수와 소수의 문자열 표현일 뿐아니라 비문자열인 true
, false
와 null
값도 변환할 수 있습니다. 특정 값을 파싱할 수 없는 경우 NaN
으로 평가됩니다. 그것은 더 쉽게 true
를 1
로 강제할 수 있음을 의미합니다:
+true; // -> 1
덧셈 또는 곱셈을 수행할 때 ToNumber
메서드가 호출됩니다. 명세된 것에 따르면 아래의 메서드를 반환합니다:
만약
argument
이 true이면 1이 반환됩니다. 만약argument
이 false이면 +0이 반환됩니다.
이 때문에 boolean 값을 일반 숫자로 추가하고 올바른 결과를 얻을 수 있습니다.
해당 부분:
이것이 <!--
(HTML 주석으로 알려진) JavaScript에서도 주석으로 사용될 수 있다는 것이 깊은 인상을 남깁니다.
// valid comment
<!-- valid comment too
인상 깊었나요? 이는 HTML 과 유사한 주석 <script>
태그를 이해하지 못하는 브라우저가 정상적으로 저하되도록 하기 위한 것 입니다. Netscape 1.x과 같은 브라우저는 더 이상 인기가 없습니다. 따라서 더 이상 스크립트 태그에 HTML 주석을 넣을 필요가 없습니다.
Node.js는 V8 엔진을 기반으로 하기때문에 Node.js 런타임에서도 HTML 과 유사한 주석을 지원합니다. 또한 그것은 명시된 것의 일부입니다:
NaN
의 타입은 'number'
이다:
typeof NaN; // -> 'number'
typeof
와 instanceof
운영의 작동 방식에 대한 설명:
typeof []; // -> 'object'
typeof null; // -> 'object'
// however
null instanceof Object; // false
typeof
연산자의 동작은 명시된 섹션에서 정의됩니다:
명시된 것에 의하면 typeof
연산자는 Table 35: typeof
연산자 결과에 따라 문자열을 반환합니다. [[Call]]
을 구현하지 않는 null
, 일반, 표준 이국 및 비표준 이국 객체의 경우 문자열 "object"
을 반환합니다.
그러나 toString
메서드를 사용하여 개체의 유형을 확인할 수 있습니다.
Object.prototype.toString.call([]);
// -> '[object Array]'
Object.prototype.toString.call(new Date());
// -> '[object Date]'
Object.prototype.toString.call(null);
// -> '[object Null]'
999999999999999; // -> 999999999999999
9999999999999999; // -> 10000000000000000
10000000000000000; // -> 10000000000000000
10000000000000000 + 1; // -> 10000000000000000
10000000000000000 + 1.1; // -> 10000000000000002
이는 이진 부동 소수점 산술에 대한 IEEE 754-2008 표준으로 인해 발생합니다. 이 척도에서는 가장 가까운 짝수로 반올림됩니다. 더 읽어보기:
- 6.1.6 숫자 유형
- IEEE 754 on Wikipedia
잘 알려진 농담. 0.1
과 0.2
의 추가는 is 매우 정확합니다:
0.1 +
0.2(
// -> 0.30000000000000004
0.1 + 0.2
) ===
0.3; // -> false
”부동 소수점 수학이 깨졌습니까?”에 대한 대답 StackOverflow에서:
프로그램에서 상수
0.2
와0.3
은 실제 값에 대한 근사치가 됩니다.0.2
에 가장 가까운double
이 유리수0.2
보다 크지만0.3
에 가장 가까운double
이 유리수0.3
보다 작습니다.0.1
과0.2
의 합은 유리수0.3
보다 커지기 때문에 코드의 상수와 일치하지 않습니다.
이 문제는 0.30000000000000004.com이라고 불리는 웹사이트에도 있을 정도로 잘 알려져 있습니다. JavaScript 뿐만 아니라 부동 소수점 수학을 사용하는 모든 언어에서 발생합니다.
Number
또는 String
과 같은 객체에 자신의 방법을 추가할 수 있습니다.
Number.prototype.isOne = function() {
return Number(this) === 1;
};
(1.0).isOne(); // -> true
(1).isOne(); // -> true
(2.0)
.isOne()(
// -> false
7
)
.isOne(); // -> false
분명히, Number
객체를 JavaScript에서 다른 객체처럼 확장할 수 있습니다. 그러나, 정의된 메서드의 동작이 명시된 것의 일부가 아닌 경우 권장되지 않습니다. Number
의 속성 목록은 다음과 같습니다:
1 < 2 < 3; // -> true
3 > 2 > 1; // -> false
왜 이렇게 작동할까요? 음, 문제는 표현의 첫 부분에 있습니다. 어떻게 작동하는지 봅시다:
1 < 2 < 3; // 1 < 2 -> true
true < 3; // true -> 1
1 < 3; // -> true
3 > 2 > 1; // 3 > 2 -> true
true > 1; // true -> 1
1 > 1; // -> false
우리는 이것을 크거나 같음 연산자(>=
)로 이 문제를 해결할 수 있습니다:
3 > 2 >= 1; // true
명시된 것을 읽으면서 관계 연산자에 대해 자세히 알아봅시다:
종종 JavaScript에서 산술 연산 결과는 예상치 못한 결과일 수 있습니다. 아래의 예들을 고려합시다:
3 - 1 // -> 2
3 + 1 // -> 4
'3' - 1 // -> 2
'3' + 1 // -> '31'
'' + '' // -> ''
[] + [] // -> ''
{} + [] // -> 0
[] + {} // -> '[object Object]'
{} + {} // -> '[object Object][object Object]'
'222' - -'111' // -> 333
[4] * [4] // -> 16
[] * [] // -> 0
[4, 4] * [4, 4] // NaN
처음 4 가지 예시에서 무슨 일이 일어나고 있나요? JavaScript에서 덧셈을 이해하기 위한 작은 표 입니다:
Number + Number -> addition
Boolean + Number -> addition
Boolean + Boolean -> addition
Number + String -> concatenation
String + Boolean -> concatenation
String + String -> concatenation
다른 예들을 추가하면 어떨까요? ToPrimitive
과 ToString
메서드는 덧셈을 하기 전 []
과 {}
을 암시적으로 요구합니다. 아래의 명시를 통해 평가 프로세스에 대해 자세히 알아봅시다:
특히, {} + []
여기에 예외가 있습니다. [] + {}
는 괄호가 없으면 코드 블록으로 해석한 다음 단항 +로 해석되어 []
숫자로 변환하기 때문입니다. 다음을 따릅니다:
{
// a code block here
}
+[]; // -> 0
[] + {}
와 동일한 출력을 얻으려면 괄호로 묶으면 가능합니다.
({} + []); // -> [object Object]
아래와 같은 숫자를 추가할 수 있다는 것을 알고 있었나요?
// Patch a toString method
RegExp.prototype.toString =
function() {
return this.source;
} /
7 /
-/5/; // -> 2
"str"; // -> 'str'
typeof "str"; // -> 'string'
"str" instanceof String; // -> false
String
생성자는 문자열을 반환합니다:
typeof String("str"); // -> 'string'
String("str"); // -> 'str'
String("str") == "str"; // -> true
new
로 다음을 시도해 봅시다:
new String("str") == "str"; // -> true
typeof new String("str"); // -> 'object'
객체? 그게 뭔가요?
new String("str"); // -> [String: 'str']
문자열 생성자에 대한 추가 정보가 명시된 것:
모든 매개 변수를 콘솔에 기록하는 함수를 선언해 보겠습니다:
function f(...args) {
return args;
}
의심할 여지없이 함수를 다음과 같이 호출할 수 있습니다:
f(1, 2, 3); // -> [ 1, 2, 3 ]
그러나 backticks를 사용하여 모든 함수를 호출할 수 있다는 것을 알고있나요?
f`true is ${true}, false is ${false}, array is ${[1, 2, 3]}`;
// -> [ [ 'true is ', ', false is ', ', array is ', '' ],
// -> true,
// -> false,
// -> [ 1, 2, 3 ] ]
음, 당신이 Tagged template literals 에 친숙하다면 이것이 놀랍지는 않을 겁니다. 위의 예에서 f
함수는 템플릿 리터럴에 대한 태그입니다. 템플릿 리터럴앞의 태그를 사용하면 함수로 템플릿 리터럴을 파싱할 수 있습니다. 태그 함수의 첫 번째 인수는 문자열 값의 배열을 포함합니다. 나머지 인수는 표현식과 관련이 있습니다. 예:
function template(strings, ...keys) {
// do something with strings and keys…
}
이 magic behind는 💅 styled-components라 불리는 React community에서 인기있는 유명한 도서관에 있습니다.
명세서를 링크합니다:
@cramforce에 의해 발견됨.
console.log.call.call.call.call.call.apply(a => a, [1, 2]);
당신의 마음을 아프게 할 수 있으니 주의하세요! 이 코드를 머릿속에 재현해봅시다. apply
메소드를 사용하여 call
을 적용하고 있습니다. 더 읽어보기:
- 19.2.3.3 Function.prototype.call(
thisArg
, ...args
) - **19.2.3.1 ** Function.prototype.apply(
thisArg
,argArray
)
const c = "constructor";
c[c][c]('console.log("WTF?")')(); // > WTF?
이 예제를 차근차근 살펴봅시다:
// Declare a new constant which is a string 'constructor'
const c = "constructor";
// c is a string
c; // -> 'constructor'
// Getting a constructor of string
c[c]; // -> [Function: String]
// Getting a constructor of constructor
c[c][c]; // -> [Function: Function]
// Call the Function constructor and pass
// the body of new function as an argument
c[c][c]('console.log("WTF?")'); // -> [Function: anonymous]
// And then call this anonymous function
// The result is console-logging a string 'WTF?'
c[c][c]('console.log("WTF?")')(); // > WTF?
Object.prototype.constructor
는 인스턴스 객체를 생성한 Object
생성자 함수에 대한 참조를 반환합니다. 문자열의 경우 String
, 숫자의 경우 Number
를 의미합니다.
{ [{}]: {} } // -> { '[object Object]': {} }
왜 그렇게 작동할까요? 여기에서 Computed property name 을 사용합니다. 이러한 대괄호 사이에 객체를 전달하면 객체를 문자열로 강제 변환하기 때문에 속성 키 '[object Object]'
와 {}
값을 얻습니다.
다음과 같이 "대괄호 지옥"을 만들 수 있습니다:
({ [{}]: { [{}]: {} } }[{}][{}]); // -> {}
// structure:
// {
// '[object Object]': {
// '[object Object]': {}
// }
// }
여기에서 객체 리터럴에 대해 자세히 알아보세요:
아시다시피 primitives 에는 prototypes 이 없습니다. 그러나, __proto__
primitives 에 대한 값을 얻으려고 한다면 다음과 같이 할 수 있습니다:
(1).__proto__.__proto__.__proto__; // -> null
이것은 프로토타입이 없는 무언가가 ToObject
메소드를 사용하여 래퍼 객체로 래핑되기 때문에 발생합니다. 차근차근 살펴봅시다:
(1)
.__proto__(
// -> [Number: 0]
1
)
.__proto__.__proto__(
// -> {}
1
).__proto__.__proto__.__proto__; // -> null
__proto__
에 대한 자세한 정보는 아래와 같습니다:
아래 식의 결과는 무엇일까요?
`${{ Object }}`;
답은:
// -> '[object Object]'
Shorthand property notation 을 Object
사용하여 속성이 있는 객체를 정의했습니다:
{
Object: Object;
}
그 다음 객체를 템플릿 리터럴에 전달 했으므로 toString
메서드가 해당 객체를 호출합니다. 이것이 문자열 '[object Object]'
을 얻는 이유입니다.
이 예시를 고려하세요:
let x,
{ x: y = 1 } = { x };
y;
위의 예시는 다음과 같은 질문을 위한 훌륭한 일입니다. y
의 값은 무엇인가요? 그 답은:
// -> 1
let x,
{ x: y = 1 } = { x };
y;
// ↑ ↑ ↑ ↑
// 1 3 2 4
위의 예에서:
- 값을 지정하지 않고
x
를 선언하므로 이는undefined
입니다. - 그 다음
x
값을 객체 속성x
로 압축합니다. - 그 다음 구조화를 사용하여
x
값을 추출하고y
에 할당합니다. 값이 정의되어 있지않으면1
을 기본값으로 사용합니다. y
의 값을 반환합니다.
- Object initializer at MDN
배열의 확산으로 흥미로운 예를 구성할 수 있습니다. 이를 고려하세요:
[...[..."..."]].length; // -> 3
왜 3
일까요? spread operator을 사용할 때 @@iterator
메소드가 호출되고 반환된 Iterator는 반복할 값을 얻는데 사용됩니다. 문자열의 기본 Iterator는 문자열을 문자로 확산합니다. 확산 후 이러한 문자를 배열로 압축합니다. 그런 다음 이 배열을 다시 확산하고 배열로 다시 압축합니다.
문자열 '...'
은 세 개의.
로 구성되며 문자열의 길이는 3
입니다.
이제 차근차근 살펴봅시다:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...'...']].length // -> 3
분명하게 우리는 원하는 양의 배열 요소를 펼치고 래핑할 수 있습니다:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...[...'...']]] // -> [ '.', '.', '.' ]
[...[...[...[...'...']]]] // -> [ '.', '.', '.' ]
// and so on …
JavaScript에서 라벨에 대해 아는 프로그래머는 많지 않습니다. 라벨들은 꽤 재미있습니다:
foo: {
console.log("first");
break foo;
console.log("second");
}
// > first
// -> undefined
라벨 되어있는 문장들은 break
또는 continue
문과 함께 사용됩니다. 라벨을 사용하여 루프를 식별할 수 있고 break
또는 continue
문을 사용해 프로그램이 루프를 중단해야 하는지 또는 실행을 계속해야 하는지에 대한 여부를 알 수 있습니다.
위의 예를 보면 foo
라는 라벨을 볼 수 있습니다. 그 뒤로 console.log('first');
을 실행한 후 실행을 중단합니다.
JavaScript의 라벨에 대해 더 읽을거리:
a: b: c: d: e: f: g: 1, 2, 3, 4, 5; // -> 5
이전의 예와 유사합니다. 다음 링크를 따르세요:
이 표현은 무엇을 반환할까요? 2
? 아니면 3
?
(() => {
try {
return 2;
} finally {
return 3;
}
})();
정답은 3
입니다. 놀랐나요?
아래의 예를 살펴보세요:
new class F extends (String, Array) {}(); // -> F []
다중 상속인 것 같습니까? 아닙니다.
흥미로운 부분은 extends
절 ((String, Array)
)의 값입니다. 그룹화 연산자는 항상 마지막 인수를 반환하기 때문에 (String, Array)
은 사실 Array
입니다. 그 말은 이제 막 Array
를 확장하는 클래스를 만들었다는 이야기입니다.
스스로 생성되는 Generator의 예를 살펴봅시다:
(function* f() {
yield f;
})().next();
// -> { value: [GeneratorFunction: f], done: false }
보이는 것처럼 리턴된 값은 이것의 value
와 f
가 같은 객체입니다. 이러한 경우 아래와 같은 일을 해볼 수 있습니다:
(function* f() {
yield f;
})()
.next()
.value()
.next()(
// -> { value: [GeneratorFunction: f], done: false }
// and again
function* f() {
yield f;
}
)()
.next()
.value()
.next()
.value()
.next()(
// -> { value: [GeneratorFunction: f], done: false }
// and again
function* f() {
yield f;
}
)()
.next()
.value()
.next()
.value()
.next()
.value()
.next();
// -> { value: [GeneratorFunction: f], done: false }
// and so on
// …
이러한 일들이 작동하는 이유를 이해하려면 다음 명세서를 읽으십시오:
아래의 읽기 애매한 구문을 생각해봅시다:
typeof new class {
class() {}
}(); // -> 'object'
마치 클래스 내부에 클래스를 선언하는 것 같습니다. 오류여야 하지만 문자열 'object'
을 얻었습니다.
ECMAScript 5 시대부터 keywords 는 property names 으로 허용됩니다. 따라서 아래와 같은 간단한 객체 예제로 생각합시다:
const foo = {
class: function() {}
};
그리고 ES6에서 축약 메소드 정의를 표준화하였습니다. 또한 클래스는 익명이 될 수 있습니다. 그래서 우리가 : function
부분을 지우면 아래와 같은 결과 값을 얻을 것 입니다:
class {
class() {}
}
디폴트 클래스의 결과 값은 항상 단순한 객체입니다. 그리고 이것의 타입은 'object'
이어야 합니다.
더 읽을거리:
잘 알려진 기호를 사용하면 유형 강제를 제거할 수 있습니다. 아래를 보세요:
function nonCoercible(val) {
if (val == null) {
throw TypeError("nonCoercible should not be called with null or undefined");
}
const res = Object(val);
res[Symbol.toPrimitive] = () => {
throw TypeError("Trying to coerce non-coercible object");
};
return res;
}
이제 우리는 아래와 같이 사용할 수 있습니다:
// objects
const foo = nonCoercible({ foo: "foo" });
foo * 10; // -> TypeError: Trying to coerce non-coercible object
foo + "evil"; // -> TypeError: Trying to coerce non-coercible object
// strings
const bar = nonCoercible("bar");
bar + "1"; // -> TypeError: Trying to coerce non-coercible object
bar.toString() + 1; // -> bar1
bar === "bar"; // -> false
bar.toString() === "bar"; // -> true
bar == "bar"; // -> TypeError: Trying to coerce non-coercible object
// numbers
const baz = nonCoercible(1);
baz == 1; // -> TypeError: Trying to coerce non-coercible object
baz === 1; // -> false
baz.valueOf() === 1; // -> true
아래의 예를 고려하세요 w:
let f = () => 10;
f(); // -> 10
좋아요, 하지만 이건 어떨까요?:
let f = () => {};
f(); // -> undefined
undefined
대신 {}
을 기대할 수도 있습니다. 이것({}) 또한 화살표 함수의 구문 중 하나이기 때문에 f
는 undefined 으로 리턴될 것입니다. 하지만 리턴 값을 괄호로 묶어서 화살표 함수에 직접 {}
객체를 리턴할 수는 있습니다.
let f = () => ({});
f(); // -> {}
아래의 예를 생각해봅시다:
let f = function() {
this.a = 1;
};
new f(); // -> f { 'a': 1 }
이제, 화살표 함수를 이용하여 동일하게 시도해봅시다:
let f = () => {
this.a = 1;
};
new f(); // -> TypeError: f is not a constructor
화살표 함수는 생성자로 사용할 수 없으며 new 와 함께 사용하면 오류가 발생합니다. 왜냐하면 렉시컬 범위의 this
가 있고 prototype
이 없기 때문에 그래서 말이 안될 것 입니다.
아래의 예를 생각해봅시다 w:
let f = function() {
return arguments;
};
f("a"); // -> { '0': 'a' }
이제, 화살표 함수를 이용하여 동일하게 시도해봅시다 n:
let f = () => arguments;
f("a"); // -> Uncaught ReferenceError: arguments is not defined
화살표 함수는 짧고 렉시컬 범위의 this
에 초점을 둔 기존 함수의 경량화된 버전입니다. 동시에 화살표 함수는 arguments
객체에 대한 바인딩을 제공하지 않습니다. 유효한 대안으로 rest parameters
을 사용하여 같은 결과를 얻을 수 있습니다:
let f = (...args) => args;
f("a");
- Arrow functions at MDN.
return
구문 또한 까다롭습니다. 이것을 생각해봅시다:
(function() {
return
{
b: 10;
}
})(); // -> undefined
return
과 반환된 표현식은 같은 줄에 있어야 합니다:
(function() {
return {
b: 10
};
})(); // -> { b: 10 }
이는 대부분 줄 바꿈 뒤에 세미콜론을 자동으로 삽입하는 자동 세미콜론 삽입이라는 개념 때문입니다. 첫번째 예시에서 return
문과 객체 리터럴 사이에 세미콜론이 삽입되어 있으므로 함수는 undefined
를 반환하고 객체 리터럴은 평가되지 않습니다.
var foo = { n: 1 };
var bar = foo;
foo.x = foo = { n: 2 };
foo.x; // -> undefined
foo; // -> {n: 2}
bar; // -> {n: 1, x: {n: 2}}
오른쪽에서 왼쪽으로, {n: 2}
이 foo에 할당되고, 이 할당의 결과{n: 2}
는 foo.x 에 할당되어 있고, bar는 foo를 할당하고 있기 때문에 bar는 {n: 1, x: {n: 2}}
입니다. 그런데 bar.x가 아닌 반면에 foo.x는 왜 정의되지 않은 것일까요?
Foo와 bar는 같은 객체 {n: 1}
를 참조하고 있고 lvalues는 할당되기 전에 결정됩니다. foo = {n: 2}
은 새로운 객체를 생성하고 있으므로 foo는 새로운 객체를 참조하도록 업데이트됩니다. 트릭은 foo.x = ...
의 foo 에 있습니다. lvalue 값은 사전에 확인되었고 여전히 이전 foo = {n:1}
객체를 참조하고 x 값을 추가하여 업데이트합니다. 체인 할당 후에도 bar는 여전히 이전의 foo 객체를 참조하지만 foo는 x가 존재하지 않는 새로운 {n: 2}
객체를 참조합니다.
다음과 동일합니다:
var foo = { n: 1 };
var bar = foo;
foo = { n: 2 }; // -> {n: 2}
bar.x = foo; // -> {n: 1, x: {n: 2}}
// bar.x point to the address of the new foo object
// it's not equivalent to: bar.x = {n: 2}
var obj = { property: 1 };
var array = ["property"];
obj[array]; // -> 1
다차원 배열의 수도코드는 무엇입니까?
var map = {};
var x = 1;
var y = 2;
var z = 3;
map[[x, y, z]] = true;
map[[x + 10, y, z]] = true;
map["1,2,3"]; // -> true
map["11,2,3"]; // -> true
대괄호 연산자 []
는 toString
을 사용하여 전달된 식을 변환합니다. 단일 요소 배열을 문자열으로 변환하는 것은 포함된 요소를 문자열로 변환하는 것과 유사합니다:
["property"].toString(); // -> 'property'
null > 0; // false
null == 0; // false
null >= 0; // true
긴 얘기를 짧게 하자면, 만약 null
이 0
보다 작으면 false
이고 null >= 0
은 true
입니다. 여기에서 이에 대한 자세한 설명을 읽으십시오 여기.
Number.toFixed()
는 다른 브라우저에서 약간 이상하게 작동할 수 있습니다. 아래 예를 확인하세요:
(0.7875).toFixed(3);
// Firefox: -> 0.787
// Chrome: -> 0.787
// IE11: -> 0.788
(0.7876).toFixed(3);
// Firefox: -> 0.788
// Chrome: -> 0.788
// IE11: -> 0.788
본능적으로 IE11은 올바르고 Firefox/Chrome이 잘못되었다고 생각할 수 있지만 사실은 Firefox/Chrome이 더 직접적으로 숫자의 표준(IEEE-754 Floating Point)을 준수하고 있는 반면 IE11는 더 명확한 결과를 제공하기 위한 노력으로 그것들을 미세하게 거역하고 있습니다.
몇 가지 간단한 테스트를 통해 이 문제가 발생하는 이유를 확인할 수 있습니다:
// Confirm the odd result of rounding a 5 down
(0.7875).toFixed(3); // -> 0.787
// It looks like it's just a 5 when you expand to the
// limits of 64-bit (double-precision) float accuracy
(0.7875).toFixed(14); // -> 0.78750000000000
// But what if you go beyond the limit?
(0.7875).toFixed(20); // -> 0.78749999999999997780
부동 소수점 번호는 내부적으로 10진수 리스트로 저장되는 것이 아니라 대게 toString과 유사한 호출에 의해 반올림되지만 실제로 내부적으로는 매우 복잡한 방법론을 통해 저장됩니다.
이 경우 끝에 있는 "5"는 실제로 진짜 5 보다 매우 작은 부분입니다.합리적인 길이로 반올림하면 5...으로 렌더링되지만 실제로는 내부적으로 5는 아닙니다.
그러나 IE11은 하드웨어 한계에서 문제를 줄이기 위해 값을 강제로 반올림하는 것처럼 보이기 때문에 toFixed(20)의 사례에서도 끝에 0만 추가한 값을 입력 보고 할 것입니다.
toFixed
에 대한 ECMA-262 정의의 NOTE 2
를 참고하세요.
Math.min(1, 4, 7, 2); // -> 1
Math.max(1, 4, 7, 2); // -> 7
Math.min(); // -> Infinity
Math.max(); // -> -Infinity
Math.min() > Math.max(); // -> true
- Why is Math.max() less than Math.min()? by Charlie Harvey
다음 표현들은 모순을 의미한것 같습니다:
null == 0; // -> false
null > 0; // -> false
null >= 0; // -> true
만약 null >= 0
이 실제로 true
이면 어떻게 null
이 0
과 같지도 않고 크지도 않을까요? (이는 보다 적은 경우에도 동일하게 작동합니다.)
이 세가지 식이 평가되는 방식은 모두 다르며 예기치 않은 동작들을 생성합니다.
첫째, 추상 평등 비교 null == 0
입니다. 일반적으로 이 연산자가 양쪽 값을 제대로 비교할 수없으면 둘 다 숫자로 변환한 후 숫자를 비교합니다. 그러면 다음 동작을 예상할 수 있습니다:
// This is not what happens
(null == 0 + null) == +0;
0 == 0;
true;
그러나 spec을 자세히 읽어보면 숫자 변환은 null
이나 undefined
의 한 면에서는 일어나지 않습니다. 그러므로 등호 한쪽에 null
이 있으면 다른 한쪽에 null
또는 undefined
가 있어야 true
를 리턴합니다. 이 경우 그렇지 않기 때문에false
을 리턴합니다.
다음은 관계 비교 null > 0
입나다. 여기서 알고리즘은 추상 평등 연산자와 달리 null
을 숫자로 변환합니다. 따라서 다음과 같은 동작이 발생합니다:
null > 0 + null = +0;
0 > 0;
false;
마지막으로 관계 비교 null >= 0
입니다. 이 표현이 null > 0 || null == 0
의 결과라고 주장할 수 있는데, 만약 그렇다면, 위의 결과는 이 역시 false
라는 것을 의미할 것입니다. 그러나 사실 >=
연산자는 매우 다른 방식으로 작동하는데, 이는 기본적으로 <
연산자와 반대되는 방식입니다. 위보다 큰 연산자를 사용한 예도 연산자보다 작기 때문에 이 식은 실제로 다음과 같이 평가됩니다.
null >= 0;
!(null < 0);
!(+null < +0);
!(0 < 0);
!false;
true;
JavaScript에서는 변수를 다시 선언할 수 있습니다:
a;
a;
// This is also valid
a, a;
strict 모드에서도 작동합니다:
var a, a, a;
var a;
var a;
모든 정의가 하나의 정의로 병합됩니다.
숫자 배열을 정렬해야 한다고 상상해보세요.
[ 10, 1, 3 ].sort() // -> [ 1, 10, 3 ]
기본 정렬 순서는 요소들을 문자열로 변환한 후 UTF-16 코드 단위 값의 시퀀스를 비교할 때 작성됩니다.
문자열 이외의 정렬을 시도하면 comparefn
을 통과시키세요.
[ 10, 1, 3 ].sort((a, b) => a - b) // -> [ 1, 3, 10 ]
const theObject = {
a: 7
};
const thePromise = new Promise((resolve, reject) => {
resolve(theObject);
}); // -> Promise instance object
thePromise.then(value => {
console.log(value === theObject); // -> true
console.log(value); // -> { a: 7 }
});
value
는 thePromise
정확하게 말하면 theObject
에서 해결되는 것 입니다.
resolve
함수에 또 다른 Promise
를 넣는 것은 어떨까요?
const theObject = new Promise((resolve, reject) => {
resolve(7);
}); // -> Promise instance object
const thePromise = new Promise((resolve, reject) => {
resolve(theObject);
}); // -> Promise instance object
thePromise.then(value => {
console.log(value === theObject); // -> false
console.log(value); // -> 7
});
이 함수는 promise 같은 객체의 중첩된 레이어(예시: 무언가로 해결되는 promise으로 해결되는 promise)를 단일 레이어로 평탄화합니다.
명세서는 ECMAScript 25.6.1.3.2 Promise Resolve Functions입니다. 하지만 그것은 인간 친화적이지 않습니다.
- wtfjs.com — a collection of those very special irregularities, inconsistencies and just plain painfully unintuitive moments for the language of the web.
- Wat — A lightning talk by Gary Bernhardt from CodeMash 2012
- What the... JavaScript? — Kyle Simpsons talk for Forward 2 attempts to “pull out the crazy” from JavaScript. He wants to help you produce cleaner, more elegant, more readable code, then inspire people to contribute to the open source community.