지난 포스팅에 이어서 계속 정리해보도록 하겠습니다.
console.log.call.call.call.call.call.apply(a => a, [1, 2]);
위의 결과를 한 번 예상해보세요!
apply
, bind
, call
는 Function.prototype
에 있는 메서드로, 개인적으로 자바스크립트에서 가장 중요한 개념이라고 생각합니다. 이 메서드의 행동을 이해하고 사용할 수 있다는 뜻은
prototype
이 뭔지 알고 있다.this
가 어떻게 동작하는지 알고 있다.자바스크립트에서 핵심적인 요소들을 모두 이해하고 있다는 뜻이 됩니다.
apply
와 call
은 역할은 같고 매개변수만 차이가 있는 메서드들입니다. 자신을 호출한 메서드에 this
값을 특정 값으로 지정하고 매개변수와 함께 호출하는 메서드입니다. apply
는 매개변수들을 배열로 받고, call
은 매개변수를 동적으로 받는다는 차이 말고는 똑같습니다.
모든 자바스크립트의 함수(메서드)들은 Function
타입이므로, Function.prototype
의 메서드들을 사용할 수 있습니다. 즉, 함수 역시 객체이고 메서드를 가질 수 있습니다. 그리고 Function.prototype
의 메서드들은 자신을 호출한 함수를 특정 this
값으로 대신 호출해주는 역할을 합니다(bind
의 경우 메서드의 이름 그대로 this
값을 특정 값으로 바인딩된 새로운 함수를 리턴합니다).
중요한 것은, 대신 호출해준다는 것입니다. 이 점을 기억해주세요.
재밌는 점은, Function.prototype
의 메서드들 역시 Function
타입이므로, call.call.call
등과 같이 사용할 수 있다는 점입니다.
그럼 이제 해당 문제를 풀어보겠습니다.
우선, 맨 마지막 apply(a => a, [1, 2])
를 보겠습니다. 이 메서드만 해석해보자면
apply
를 호출한 함수의 this
를 a => a
로 지정한다.apply
를 호출한 함수 매개변수로 1, 2
로 넘긴다.이렇게 해석이 됩니다. 그렇다면 이제 apply
를 호출한 함수를 보면 됩니다. 이 함수는 call
입니다. 그리고 apply
는 이 call
을 대신 호출해줄 것입니다.
그러면 call
은 어떻게 호출이 될까요? 이는 명세를 봐야합니다.
- Let func be the this value.
- If IsCallable(func) is false, throw a TypeError exception.
- Let argList be a new empty List.
- 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.
- Perform PrepareForTailCall().
- Return ? Call(func, thisArg, argList).
1번 항목을 주목해야 합니다. Function.prototype
의 메서드들을 호출한 주체(this
)는 함수입니다. 따라서, 자바스크립트 엔진이 내부적으로 호출하는 함수인 func
를 this
로 지정하는 것입니다. 그리고 이 func
를 호출할 때 매개변수로 받은 thisArg
값을 이용하여 func
의 this
를 thisArg
로 지정하여 호출해주는 것입니다.
call.apply(a => a, [1, 2])
의 경우, apply
는 call
을 대신 호출해줄 때 this
를 a => a
함수로 지정해줍니다. 그리고 매개변수를 1, 2
로 적용하여 호출해줍니다.
call
은 첫번째 매개변수로 바인딩 될 this
값, 두번째 매개변수부터 호출될 매개변수들을 받는다고 했습니다. 따라서, call(1, 2)
와 같이 호출한 효과를 갖게 됩니다.
그리고 중요한 점은, apply
가 call
의 코드를 실행시킬 때 this
값을 a => a
함수로 바인딩했습니다. call
은 명세의 1번 항목에 따라 func
를 this
로 바인딩하는데, 결국 a => a
가 바인딩 됩니다. 그리고 이 func
의 this
를 1
로, 매개변수는 2
로 호출하게됩니다.
엄밀히 말하자면,
Arrow function
은 이미this
값이 바인딩 된 상태이므로, 자바스크립트에서 호출될 때thisArg
값을 무시해버립니다. 즉,call
이 대신 호출될 때func
의this
가 1로 바인딩되지 않습니다. 이 외에도,bind
로this
가 바인딩 된 함수 역시 더 이상 다른 값으로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])
로 해도 결과는 같습니다.
constructor
propertyconst 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 Object]': {} }
이 내용은 ES6
의 computed property 내용이므로 넘어가도록 하겠습니다.
__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]'
ES6
문법에 관한 예제입니다. 템플릿 문자열 안에 ${}
를 이용하여 표현식을 문자열로 쉽게 표현할 수 있습니다. 따라서 { Object }
로 해석되는데, 이는 Object shorthand 표기법으로 해석되며 { Object: Object }
와 같습니다.
아무튼, 결국 어떤 객체를 문자열로 출력하는 경우이므로 { Object: Object }.toString()
이 호출되어 '[object Object]'
가 됩니다.
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
[...[...'...']].length; // -> 3
ES6
의 spread operator에 관한 내용입니다. spread opreator인 ...
를 문자열과 배열 안에서 사용하는 경우 어떻게 되는지를 알면 쉽게 해석할 수 있는 결과입니다.
문자열에 사용하는 경우, 해당 문자열을 split('')
와 같이 빈 문자열로 쪼갠 결과와 같습니다. 그리고 이를 배열안에서 다른 배열에 사용하는 경우, 배열의 요소를 해당 위치 i
에 splice(i, 0, item1[, item 2,]...)
한 것과 비슷한 효과를 가집니다. (물론 splice
의 경우 해당 배열의 레퍼런스는 변하지 않지만, [...[]]
는 레퍼런스가 바뀝니다.)
// 1
...'...' // -> ['.', '.', '.']
// 2
[...'...'] // 1에 의해 아래와 같이 변환
[...['.', '.', '.']]
['.', '.', '.']
// 3
[...[...'...']] // 2에 의해 아래와 같이 변환
[...['.', '.', '.'] // 결국 2-2와 같음
['.', '.', '.'] // length는 3
spread operator는 내부적으로 iterator로 동작하므로, 관련 내용을 찾아보시면 되겠습니다.
오늘은 상당히 중요한 내용을 알아보았던 것 같습니다. 자바스크립트에 있어서 Function.prototype
은 자바스크립트 입문자 타이틀을 벗어나기 위한 핵심 개념들이 녹아있는 정수라고 생각합니다. 또한 모던 자바스크립트의 분기점 같은 ES6
역시 필수로 알아둬야 될 개념이라고 볼 수 있겠습니다.
그럼, 다음 시간에 또 뵙겠습니다.