[흑묘테크]에서 this에 대한 발표를 했었는데, 발표 영상만으로는 설명하고자 하는 바를 잘 나타내지 못했던 것 같아서 보충자료로 this에 대한 내용을 정리하였습니다.
이 문서에서 나오는 예제는 비엄격 모드에서 실행되었습니다.
MDN에 this를 검색하면 다음과 같이 설명합니다.
this의 값은 함수를 호출한 방법에 의해 결정됩니다.
이 문장이 무슨 의미인지 파악해보기 위해, 개발자도구 콘솔창에 console.log를 통해 this를 출력해봅니다.
console.log(this)
// Window {...}
출력결과를 확인해보니 this는 브라우저의 전역 객체(Global Object)를 가리키고 있었습니다.
이번에는 다음과 같은 객체를 선언해보겠습니다.

이 코드를 통해 결과를 확인해보니 다음과 같은 결과가 나왔습니다.

메소드를 통해 출력한 this는 obj를 가리키고 있었습니다.
이처럼 달라지는 this의 출력결과를 통해서 "this의 값은 함수를 호출한 방법에 의해 결정됩니다."라는 문장의 의미를 이해할 수 있습니다.

this는 어떤 Object를 가리키는 예약어입니다.
(정확히는 실행 컨텍스트를 참조하고 있는데, 이에 대한 내용은 다음 발표자의 클로저를 참고해주세요.)
그리고 이 this가 어떤 Object를 참조할 지는 함수가 호출될 때 결정되는 데, 이를 동적 바인딩이라고 합니다.
this가 binding되는 데에는 4가지 규칙이 있습니다.
기본 바인딩, 암시적 바인딩, 명시적 바인딩, new 바인딩
함수가 직접 호출되었을 때 this가 전역 객체를 참조하게 되는데, 이를 기본 바인딩이라고 합니다.
아래와 같이 this를 반환하는 함수를 선언한 후, console.log를 통해 함수 실행 결과를 출력해보면 다음과 같이 나옵니다.
function getThis(){
return this
}
console.log(getThis());

브라우저 환경에서 출력 결과는 Window 객체이며, NodeJS 환경에서 출력 결과는 Object[global]로 나왔습니다.
두 결과가 다른 이유는 두 환경에서 전역 객체가 다르기 때문입니다.
- 브라우저 환경에서 전역객체는 Window 객체이며
- 노드환경에서의 전역객체는 global object입니다.
이처럼 함수가 직접 호출되었을 때, this는 각각의 환경에서의 전역객체를 참조하고 있었습니다.
다음 바인딩 규칙으로 넘어가기 전에 한 가지 언급하고 넘어가자면, NodeJS 환경에서 this는 두 가지 방식으로 동작할 수 있습니다.
다음 예제코드를 통해 확인해봅시다.

이해를 위해 exports에 a라는 변수를 추가해주었습니다.
전역에서 this를 출력한 경우
전역에서 바로 this를 출력했을 때, { a:10 }이라는 객체가 나왔습니다.
즉 this가 참조하고 있는 것은 module.exports 객체입니다.함수를 통해 this를 출력한 경우
getThis 함수 내에서 this가 참조하고 있는 것은 Object [global]입니다.
이처럼 노드환경에서는 "전역에서 사용된 this"와 "함수 내에서 사용된 this"가 참조하고 있는 것이 다르다는 점을 확인할 수 있습니다.
암시적 바인딩은 함수가 객체안의 메소드로서 호출되었을 때 발생합니다.
이 경우, this는 메소드를 호출한 객체를 참조하게 됩니다.

예시 코드를 보면 idol이라는 객체에 getName이라는 메소드가 존재합니다.
idol.getName()을 통해 메소드를 호출하는 경우, getName 함수 내에 this는 idol 객체에 바인딩됩니다.
따라서 this.name은 idol 객체의 name 프로퍼티에 접근할 수 있습니다.

이번에는 idol.getName()으로 메소드를 호출하는 대신, globalGetName이라는 변수를 선언했습니다.
그리고 이 변수에 idol.getName을 저장했습니다. 이제 전역환경에서 globalGetName()을 출력하면 어떤 결과가 나올까요?
이번에는 aespa 가 출력이 되지 않고, undefine가 출력이 되었습니다.
분명 아까와 같은 getName인데 왜 출력결과는 다르게 나왔을까요?
여러분, 맨 처음의 MDN에서 설명한 this의 정의, 기억하시나요?
this의 값은 함수를 호출한 방법에 의해 결정됩니다.
맞습니다. this는 어떻게 호출되었느냐에 따라서 binding되는 객체가 달라진다고 했습니다.
앞선 예제의 idol.getName()과 globalGetName()이 어떻게 호출되었는 지 비교해봅시다.

왼쪽 코드에서 getName 함수는 idol이라는 객체에 의해 실행되었습니다.
즉 암시적 바인딩이 일어났기 때문에, idol객체의 name에 접근할 수 있었습니다.
반면에 오른쪽 코드의 globalGetName()은 직접 호출되었습니다.
(쉽게 말하자면 ~~.globalGetName()으로 호출되지 않았습니다.)
그렇기 때문에 암시적 바인딩이 아닌 기본 바인딩이 발생한 것입니다.
따라서 함수가 호출되는 시점에서 this는 idol이 아닌 Window(혹은 전역객체)를 참조하게 되기 때문에, name이라는 프로퍼티에 접근할 수 없어 undefined를 출력한 것입니다.
슬슬 this가 함수가 호출한 방법에 의해 값이 달라진다는 말이 와닿는 것 같습니다.
그럼 암시적 바인딩 예제를 하나 더 보도록 합시다.

여기 idol2라는 객체를 하나 더 만들어주었습니다.
이 idol2의 name속성은 newJeans 입니다. 그리고 똑같이 getName이라는 메소드를 만들어주었는데, 여기에서는 idol1에 있는 getName을 그대로 가져와서 사용합니다.
이 경우 idol2.getName()은 어떤 것을 출력하게 될까요?
idol1의 name인 "aespa"를 가져올까요?
아니면 idol2의 name인 "newJeans"를 가져올까요?
정답은 다음과 같습니다.

idol2.getName()은 idol2로부터 호출되었습니다.
즉 idol1에서 getName을 가져왔더라도, 내부의 this는 idol2로 암시적 바인딩이 일어나게됩니다.
따라서 this.name은 idol2의 name에서 이름을 가져오게 됩니다.
그런데 만약 getName의 this를 항상 idol1 객체로 지정해서 바인딩하고 싶으면 어떻게 해야할까요?
이 경우에는 명시적 바인딩을 사용하면 됩니다.
시시각각 바뀌는 this를 고정시켜주고 싶다면, 명시적 바인딩을 사용합니다. 이를 이용하면 임의로 this를 원하는 객체에 바인딩을 할 수 있습니다.
명시적 바인딩을 구현하는 방법에는 다음과 같은 3가지 방법이 있습니다.
bind는 Function 프로토타입 메서드로, 바인딩된 함수의 복제본을 리턴합니다.
다음 예시코드를 봅시다.

idol2 객체에서 달라진 부분은 getName을 idol1.getName.bind(idol1)으로 가져오고 있다는 점입니다.
bind의 첫번째 인자로 객체를 주게되면, 해당 함수 내부의 this는 자동으로 그 객체를 참조하도록 고정할 수 있습니다.
즉, 이 코드에서는 idol2에서 getName을 호출했지만, 명시적 바인딩을 통해 this를 idol1으로 고정시켜주었으므로, idol1의 name을 출력합니다.
bind는 함수의 복제본을 리턴해주었다면, call과 apply는 this를 바인딩한 후에 직접 호출합니다.

마찬가지로 첫번째 인자로 오는 객체에 this를 바인딩합니다. 그리고 함수를 호출하여 코드를 실행합니다.
call과 apply의 차이점은 call()은 나열된 인수들을 받는 반면에 apply()는 인수 배열 하나를 받는다는 점입니다.
위 코드를 보면 getName이라는 함수는 count와 song을 인수로 받습니다.
바인딩 규칙 중 마지막인 new 바인딩에 대해 알아봅시다. new 연산자를 통해서 함수를 호출하게 되면, 해당 함수는 생성자 함수로 동작하게 됩니다.
new 연산자의 동작 과정을 간략히 살펴보겠습니다.
new Func()으로 함수가 호출되었을 때
1. 빈 객체가 생성되고 this가 바인딩됩니다.
2. 함수가 호출되고 함수의 코드가 실행됩니다.
3. 생성된 객체를 반환합니다.
이 과정에서 this가 새로 생성된 객체로 binding되는 것을 new 바인딩이라고 합니다.

Idol이라는 이름의 생성자 함수를 만들어보았습니다. 이를 통해 new 연산자가 어떻게 동작하는지 살펴보면
마지막으로 화살표 함수에서 this에 대해 알아봅니다.
먼저 앞서나온 바인딩 규칙에 대한 지식을 이용하여 다음 문제를 풀어봅시다.

이번에는 getName에서 단순히 this.name을 출력할 뿐만 아니라, 함수 내부에서 또다른 innerFunc 함수를 선언했습니다.
innerFunc함수에서도 this.name을 출력해줍니다.
그리고 innerFunc 함수를 getName 내부에서 호출합니다.
idol.getName()을 통해 호출하면, 처음 나오는 this.name과 innerFuc 내부의 this.name은 각각 어떤 것을 출력할까요?
정답은 다음과 같습니다.

두 번째 this.name의 경우, undefined가 출력되었습니다.
바로 this가 기본 바인딩이 일어났기 때문입니다. 즉 innerFuc의 this는 idol 객체가 아닌 전역 객체에 바인딩되어 name에 접근할 수 없었던 것입니다.
그런데 만약 innerFunc의 this 역시 getName의 this를 그대로 가져와서 idol 객체에 바인딩하고 싶으면 어떻게 해야할까요?
앞에서 배운 명시적 바인딩으로 this를 바꿔줄수도 있지만, 이렇게 상위 함수의 this를 그대로 가져오고 싶은 경우에는 화살표 함수를 사용할 수 있습니다.

이번에는 innerFunc를 화살표 함수로 바꾸어주었습니다. 그랬더니 this가 idol 객체에 바인딩되어 name 값을 출력해주었습니다.
MDN에 따르면 화살표 함수는 일반 함수와 달리, this에 대한 바인딩이 존재하지 않습니다.
대신 화살표 함수의 this는 화살표 함수를 둘러싸고 있는 환경의 this를 가져와서 사용하게 됩니다.
쉽게 생각하면, 화살표 함수에서 this는 상위의 this를 상속받게 되는 것입니다.
그리고 this에 대한 바인딩이 존재하지 않기 때문에, bind, call, apply와 같은 메소드 역시 사용할 수 없습니다.
흑묘테크의 더 많은 발표를 듣고 싶다면, 여기를 클릭하세요