WTFJS 해설 - 4

undefcat·2021년 4월 17일
0

WTFJS

목록 보기
4/8
post-thumbnail

WTFJS - 4

지난 포스팅에 이어서 계속 정리해보도록 하겠습니다.

Examples

Call call call

console.log.call.call.call.call.call.apply(a => a, [1, 2]);

💡 해설

위의 결과를 한 번 예상해보세요!

apply, bind, callFunction.prototype에 있는 메서드로, 개인적으로 자바스크립트에서 가장 중요한 개념이라고 생각합니다. 이 메서드의 행동을 이해하고 사용할 수 있다는 뜻은

  1. 함수도 객체임을 알고 있다.
  2. prototype이 뭔지 알고 있다.
  3. this가 어떻게 동작하는지 알고 있다.

자바스크립트에서 핵심적인 요소들을 모두 이해하고 있다는 뜻이 됩니다.

applycall은 역할은 같고 매개변수만 차이가 있는 메서드들입니다. 자신을 호출한 메서드에 this값을 특정 값으로 지정하고 매개변수와 함께 호출하는 메서드입니다. apply는 매개변수들을 배열로 받고, call은 매개변수를 동적으로 받는다는 차이 말고는 똑같습니다.

모든 자바스크립트의 함수(메서드)들은 Function타입이므로, Function.prototype의 메서드들을 사용할 수 있습니다. 즉, 함수 역시 객체이고 메서드를 가질 수 있습니다. 그리고 Function.prototype의 메서드들은 자신을 호출한 함수를 특정 this값으로 대신 호출해주는 역할을 합니다(bind의 경우 메서드의 이름 그대로 this값을 특정 값으로 바인딩된 새로운 함수를 리턴합니다).

중요한 것은, 대신 호출해준다는 것입니다. 이 점을 기억해주세요.

재밌는 점은, Function.prototype의 메서드들 역시 Function타입이므로, call.call.call 등과 같이 사용할 수 있다는 점입니다.

그럼 이제 해당 문제를 풀어보겠습니다.

우선, 맨 마지막 apply(a => a, [1, 2])를 보겠습니다. 이 메서드만 해석해보자면

  1. apply를 호출한 함수의 thisa => a로 지정한다.
  2. apply를 호출한 함수 매개변수로 1, 2로 넘긴다.

이렇게 해석이 됩니다. 그렇다면 이제 apply를 호출한 함수를 보면 됩니다. 이 함수는 call입니다. 그리고 apply는 이 call대신 호출해줄 것입니다.

그러면 call은 어떻게 호출이 될까요? 이는 명세를 봐야합니다.

  1. Let func be the this value.
  2. If IsCallable(func) is false, throw a TypeError exception.
  3. Let argList be a new empty List.
  4. If this method was called with more than one argument, then in left to right order, starting with the second argument, append each argument as the last element of argList.
  5. Perform PrepareForTailCall().
  6. Return ? Call(func, thisArg, argList).

1번 항목을 주목해야 합니다. Function.prototype의 메서드들을 호출한 주체(this)는 함수입니다. 따라서, 자바스크립트 엔진이 내부적으로 호출하는 함수인 functhis로 지정하는 것입니다. 그리고 이 func를 호출할 때 매개변수로 받은 thisArg값을 이용하여 functhisthisArg로 지정하여 호출해주는 것입니다.

call.apply(a => a, [1, 2])의 경우, applycall을 대신 호출해줄 때 thisa => a 함수로 지정해줍니다. 그리고 매개변수를 1, 2로 적용하여 호출해줍니다.

call은 첫번째 매개변수로 바인딩 될 this값, 두번째 매개변수부터 호출될 매개변수들을 받는다고 했습니다. 따라서, call(1, 2)와 같이 호출한 효과를 갖게 됩니다.

그리고 중요한 점은, applycall의 코드를 실행시킬 때 this값을 a => a 함수로 바인딩했습니다. call은 명세의 1번 항목에 따라 functhis로 바인딩하는데, 결국 a => a가 바인딩 됩니다. 그리고 이 functhis1로, 매개변수는 2로 호출하게됩니다.

엄밀히 말하자면, Arrow function은 이미 this값이 바인딩 된 상태이므로, 자바스크립트에서 호출될 때 thisArg 값을 무시해버립니다. 즉, call이 대신 호출될 때 functhis가 1로 바인딩되지 않습니다. 이 외에도, bindthis가 바인딩 된 함수 역시 더 이상 다른 값으로 bind 되지 않습니다.

따라서 이 예제의 결과는 2입니다.

그러면, 그 뒤의 call.call.call...은 무슨 의미가 있을까요? 정답은 아무런 의미도 없습니다.

실제로 실행된 함수는 apply 뿐입니다. 다만, apply가 실행하는 코드는 call의 코드였을 뿐입니다.

console.log.call.call.call...에서 console.log는 아무런 일도 하지 않습니다. 그저 Function.prototype의 메서드들을 호출하기 위해 썼을 뿐입니다.

즉, 맨 앞에는 어떤 함수가 와도 상관 없습니다. Function.prototype.call.apply(a => a, [1, 2])로 해도 결과는 같습니다.

A constructor property

const c = 'constructor';
c[c][c]('console.log("WTF?")')(); // > WTF?

💡 해설

이 문제는 constructor 프로퍼티와 Function 객체를 호출했을 때 어떤 일이 발생하는지만 안다면 쉽게 해석할 수 있는 문제입니다.

간략히 알아보자면, constructor 프로퍼티는 생성자 함수를 가리키는 프로퍼티입니다. 모든 정의된 함수는 new 키워드와 함께 생성자 함수로 호출될 수 있는데, 생성자 함수로 만들어진 객체는 .constructor 프로퍼티를 갖고 있으며, 이 constructor 프로퍼티는 자신을 만든 생성자 함수를 가리킵니다.

Function객체는 함수로 호출된다면 new Function처럼 생성자 함수로 호출한 효과와 같습니다. 그리고 Function 생성자 함수는 매개변수로 받은 문자열을 함수의 코드로 갖는 새로운 함수를 생성합니다.

이제, 동작을 알아보도록 하겠습니다.

c[c]의 경우, 'constructor'['constructor']입니다. 문자열 타입에 []를 이용하여 프로퍼티에 접근하므로, 자바스크립트에서는 'constructor'String Wrapper Class를 이용하여 String 타입의 객체로 만듭니다. 그리고 해당 객체의 .constructor 프로퍼티에 접근합니다. 즉, String 객체가 됩니다.

String === c[c] // -> true

c[c][c]의 경우, 앞에서 생성자 함수 객체의 .constructor를 가리킵니다. String객체는 그 자체가 함수입니다. 그리고 함수객체를 생성하는 객체는 Function입니다. 따라서

Function === c[c][c] // -> true

결국 Function('console.log("WTF")')()를 호출하는 셈이 되며, 이는 console.log("WTF") 코드를 담은 새로운 함수를 생성하고 즉시 실행()한 코드가 되므로, 콘솔창에 'WTF'가 찍히게 됩니다.

Object as a key of object's property

{ [{}]: {} } // -> { '[object Object]': {} }

💡 해설

이 내용은 ES6computed property 내용이므로 넘어가도록 하겠습니다.

Accessing prototypes with __proto__

(1).__proto__.__proto__.__proto__; // -> null

💡 해설

자바스크립트에서 객체가 아닌 값들은 특정 상황에 Wrapper Class로 감싸져 호출됩니다. 이런 예로, 우리는 이전 포스팅에서 String에 대해 알아보았습니다. 숫자 타입도 역시 마찬가지입니다.

__proto__는 일반적인 경우 코드에서 직접적으로 사용할 일이 없기 때문에 간단히 설명하자면 생성자 함수의 prototype 프로퍼티와 연결되어 있는 프로퍼티입니다. 이에 대한 내용은 명세를 확인하시기 바랍니다.

위의 코드를 하나씩 해석해보겠습니다.

(1).__proto__ === Number.prototype // -> true

(1).__proto__.__proto__ === Object.prototype // -> true

(1).__proto__.__proto__.__proto__ // -> null

Object.prototype.prototype // -> undefined

Object.prototype.__proto__ // -> null

__proto__의 경우 그 값이 없는 경우 null이 리턴되도록 명시돼있습니다.

`${{Object}}`

`${{ Object }}`; // -> '[object Object]'

💡 해설

ES6 문법에 관한 예제입니다. 템플릿 문자열 안에 ${}를 이용하여 표현식을 문자열로 쉽게 표현할 수 있습니다. 따라서 { Object }로 해석되는데, 이는 Object shorthand 표기법으로 해석되며 { Object: Object }와 같습니다.

아무튼, 결국 어떤 객체를 문자열로 출력하는 경우이므로 { Object: Object }.toString()이 호출되어 '[object Object]'가 됩니다.

Destructuring with default values


let x,
    { x: y = 1 } = { x };

y; // -> 1

💡 해설

마찬가지로 ES6 문법에 관한 내용이며, Destructuring Assignment(+with default value)에 관한 내용입니다.

let x, // x를 선언하지만 값은 할당되지 않았습니다.
    { x: y = 1 } = { x }; // 우선 우변은 { x: x }가 됩니다. 아직 할당되지 않았습니다.
						  // 좌변의 변수 y에 우변의 key가 x인 값을 할당합니다.
						  // 만약 없으면 기본값 1을 할당합니다.

// 따라서 1입니다.
y; // -> 1

Dots and spreading

[...[...'...']].length; // -> 3

💡 해설

ES6spread operator에 관한 내용입니다. spread opreator...를 문자열과 배열 안에서 사용하는 경우 어떻게 되는지를 알면 쉽게 해석할 수 있는 결과입니다.

문자열에 사용하는 경우, 해당 문자열을 split('')와 같이 빈 문자열로 쪼갠 결과와 같습니다. 그리고 이를 배열안에서 다른 배열에 사용하는 경우, 배열의 요소를 해당 위치 isplice(i, 0, item1[, item 2,]...) 한 것과 비슷한 효과를 가집니다. (물론 splice의 경우 해당 배열의 레퍼런스는 변하지 않지만, [...[]]는 레퍼런스가 바뀝니다.)

// 1
...'...' // -> ['.', '.', '.']

// 2
[...'...'] // 1에 의해 아래와 같이 변환
[...['.', '.', '.']]
['.', '.', '.']

// 3
[...[...'...']] // 2에 의해 아래와 같이 변환
[...['.', '.', '.'] // 결국 2-2와 같음
['.', '.', '.'] // length는 3

spread operator는 내부적으로 iterator로 동작하므로, 관련 내용을 찾아보시면 되겠습니다.

4편을 마치며

오늘은 상당히 중요한 내용을 알아보았던 것 같습니다. 자바스크립트에 있어서 Function.prototype은 자바스크립트 입문자 타이틀을 벗어나기 위한 핵심 개념들이 녹아있는 정수라고 생각합니다. 또한 모던 자바스크립트의 분기점 같은 ES6 역시 필수로 알아둬야 될 개념이라고 볼 수 있겠습니다.

그럼, 다음 시간에 또 뵙겠습니다.

profile
undefined cat

0개의 댓글