안녕하세요. 3주차 발표를 맡은 이하령입니다. 이번에는 챕터 3의 this에 대해 보겠습니다.
우선 들어가기 전에 this를 왜 알아야 하는지에 대해 간단히 살펴봅시다.
흔히들 알고 있는 객체지향에서의 this는 클래스로 생성한 인스턴스의 객체를 의미할겁니다. 하지만 자바스크립트에서의 this는 정말 다양한 곳에서 다양하게 쓰일 수 있습니다.
그렇기 때문에 아마 굉장히 헷갈릴 수 있습니다. this가 뭔지 모른다면, 이게 왜 이렇게 되는걸까 참 답답할 수 있습니다. 다른 사람이 짠 코드를 이해하는건 기본일텐데 말이에요.
면접 질문으로도 꽤 자주 나온다고 합니다. 그렇기 때문에 저희는 this에 대해 꼭 알아야 할 필요가 있습니다. 어떻게 보면 자바스크립트의 중요한 특징 중 하나이기도 하니까요.
this는 상황에 따라 달라집니다.
우선 this가 언제 결정되는지 봅시다. this는 실행 컨텍스트가 만들어질 때 결정됩니다. 실행 컨텍스트의 수집 정보를 보면 사실 ThisBinding 이라는 것도 포함되어 있어요.
그리고 실행 컨텍스트는 대부분 함수가 호출되면 만들어지죠.
결국 this는 함수가 호출될 때 만들어진다! 라고 이해할 수 있습니다.
우선 전역 공간에서의 this를 보겠습니다.
전역 공간에서의 this는 전역 객체를 가리킵니다. 전역 객체가 전역 컨텍스트를 생성하니까요.
JS의 런타임 환경이 어디인지에 따라 window 객체와 global 객체로 나뉩니다.
그런데 제가 실제로 확인해보니.. node 환경에서는 빈 객체가 나왔습니다. 구글링을 좀 더 해봤더니 node에서는 전역 환경의 this만 global이 아니라 module.exports를 가리킨다는 말이 있었어요. 실제로도 저렇게 출력되고요...
MDN 공식 문서에서는 global이라고 나오긴 하는데 저도 정확히 왜 그런지는 모르겠습니다. 혹시 아시는분 있으면 알려주세요..ㅜㅜ
그리고 함수를 어떻게 호출하느냐에 따라 this가 달라집니다. 우선 함수를 호출하는 방법에는 두가지가 있습니다.
먼저 함수로서 호출하는 방법입니다. 함수로서 호출하는건 그 자체로 독립적인 기능을 수행합니다. 반면 메서드로서 호출한다는건 자신을 호출한 대상 객체에 관한 동작을 수행하는거죠.
메서드로서 호출하는지 판단하는 방법은 굉장히 간단합니다. 여기서 메소드로 호출하는건 어떤걸까요?
네. 맞습니다. 바로 obj.method(3)이랑 obj‘method’ 입니다. 이처럼 함수를 호출할 때 함수 앞에 객체 이름이 있는가를 보면 됩니다.
그럼 먼저 함수를 메서드로 호출했을 때의 this에 대해 설명드리겠습니다.
이때의 this는 함수명 앞의 객체입니다. 아래 예시에서는 obj와 inner이 되겠죠.
그럼 이번엔 함수 내부에서의 this에 대해 설명드리겠습니다.
우선 결론부터 말하자면 전역 객체입니다.
함수로서 호출한 경우는 누가 그 함수를 호출했는지 해당 객체를 명시하지 않고 개발자가 코드에 직접 관여해 실행한 것입니다. 호출 주체의 정보를 알 수 없게되죠. 이처럼 this가 지정되지 않았으니 이럴 경우 this는 전역 객체를 바라보게 됩니다.
한번 예시를 볼게요.
우선 이 코드가 실행될 때 콜스택에 전역 컨텍스트가 만들어 질겁니다.
environmentRecord에는 obj1 정보가 수집될겁니다. outer에는 아무것도 없겠죠.
그리고 ThisBinding에는 전역객체가 바인딩 될겁니다.
obj1.outer() 코드가 실행되면서 콜스택에서는 outer가 쌓일겁니다. 그럼 outer의 실행컨텍스트 정보도 수집해야겠죠.
outer함수를 봅시다. outer에는 innerFunc 변수와 obj2 변수가 있네요. 이게 environmentRecord로 수집되겠죠? 그리고 outerEnvironmentReference에는 outer 함수가 선언될 당시의 LexicalEnvironment가 들어가게 되므로 아래와 같이 될겁니다.
그리고 obj1이 this 바인딩됩니다. 메소드로서 호출했으니까요.
그러면 outer함수가 실행되므로 console.log(this)에는 obj1이 찍히겠네요.
그리고 innerFunc 함수가 실행됩니다. 콜스택에 올려지고 실행 컨텍스트가 만들어집니다.
innerFunc에는 콘솔만 있으므로 EnvironmentRecord는 아무것도 없을거고 innerFunc함수가 선언될 당시의 LexicalEnvironment인 [outer, {innerFunc, obj2}]가 outerEnvironmentReference에 들어가겠죠.
이건 사실 innerFunc가 콜스택에 올려지기 직전에 있던 실행 컨택스트를 참조한다고 해도 무방할것 같습니다.
그리고 여기서는 함수로 호출되었으니 콘솔에는 전역객체가 찍히겠죠.
innerFunc 실행이 끝났으니 콜스택에서 pop될겁니다.
그리고 obj2.innerMethod() 에서 innerMethod가 실행되어 콜스택에 올라가고 실행 컨텍스트가 만들어집니다.
여기도 innerFunc가 콜스택에 올려졌을 때랑 마찬자기로 LexicalEnvironment가 생성될 겁니다.
아마 LexicalEnvironment는 실행되면서 계속 변하기 때문에 할당된 값에 따라 구체적인 것은 다를 수 있습니다.
어쨌든, 여기서는 메소드로서 함수가 호출되었으므로 obj2가 촐력될겁니다.
함수 실행이 끝났으므로 pop되고
outer도 pop되고
전역 컨텍스트도 pop되어 콜스택이 비워지겠죠.
다음은 메서드 내부 함수에서의 this 우회 방법에 대해 말씀드리겠습니다.
사실 우리가 생각하는 this와는 좀 많이 다르게 흘러갔죠. 그래서 이걸 우회하고 싶다! 할 때 아래와 같은 방법을 사용하면 됩니다.
ES5 문법까지는 단순 변수를 활용합니다. 코드에서 var self = this;
부분이 바로 이겁니다. 뒤에서 좀 더 자세히 설명드리겠지만 여러 메소드를 활용한 명시적 바인딩으로도 가능합니다.
ES6(ES2015) 부터는 화살표 함수를 사용할 수 있습니다. 아마 많이들 익숙할 겁니다.
화살표 함수는 this를 바인딩하지 않습니다. 그래서 선언된 시점에서의 상위 스코프의 this가 바인딩됩니다.
한번 코드를 봅시다.
현재 innerFunc는 화살표 함수로 되어있네요. innerFunc(); 즉, 함수로서 호출했지만 콘솔에는 전역객체가 아닌 obj1이 찍힐겁니다. 선언된 시점의 상위 스코프가 obj1이니까요!
다음은 콜백 함수 호출 시 해당 함수 내부에서의 this에 대해 보겠습니다.
콜백함수는 다음 시간에 다룰 내용이지만 간단하게 말하자면 함수 A의 제어권을 다른 함수나 메서드 B에게 넘겨주는 경우, A를 콜백함수라고 칭합니다.
기본적으로 여기에서의 this는 전역객체입니다. 하지만 제어권을 받은 함수에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 해당 대상을 참조합니다. 조금있다 뒤에서 좀 더 자세히 설명드리겠습니다.
코드를 봅시다. setTimeout이라는 콜백함수를 실행했습니다. 0.3초 후 전역객체가 출력될겁니다. 다음은 forEach문입니다. 여기서도 전역객체와 각 원소가 순서대로 출력될겁니다.
그런데 마지막은 조금 다릅니다. addEventListener 메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의돼 있습니다. 여기서의 this는 document.body.querySelector('#a') 가 됩니다. 즉, 출력된 내용 처럼 버튼 element인거죠.
다음은 생성자 함수 내부에서의 this입니다. 여기서의 this는 새로 만들어질 인스턴스 객체 자체입니다. 우리가 흔히 알고 있던 this와 비슷하죠.