객체 지향 언어를 다뤄보았다면 한 번 즈음은 들어봤을 만한 단어 this
, 주로 클래스 내에서 자기 자신을 가리키기 위해 사용하는 방식으로 알고 있었다. 그런데 JS에서는 종종 다른 느낌을 받아서, 이번에 공부를 통해 알게된 this
연산자의 사용에 대한 여러가지 케이스들을 작성해보려고 한다.
class Person {
height;
weight;
constructor(h, w) {
this.height = h; // 객체 자기 자신을 가리킬 때 쓰이는 this
this.weight = w;
};
}
const p1 = new Person();
기존에 알고 있던 가장 보편적인 this
의 쓰임이다. class
의 목적 중에 캡슐화, 은닉화 정도를 위해 다른 언어들에서는 내부 변수를 private
선언하며 get(), set()
함수를 정의해 외부 스코프에서도 클래스의 내부 변수에 접근하는 방법이 있다. 여기서 메서드 하나를 만들어보자.
class Person {
... // height, weight, constructor
heightAverageDifference(average) {
return this.height - average
}
}
클래스 속성의 height
값에 평균치를 뺀 편차를 반환하는 함수이다. this
는 Person
으로 만들어진 하나의 객체를 가리키기 때문에 내부 선언된 height, weight
값에 접근할 수 있다는 특성이 있다. 여기까지가 기존에 알던것이고, 이 다음부터 나오는 것은 모르고 있었던 JS
내의 this
사용 예시이다.
function returnBMI (h, w) {
let height;
this.height = h;
return w / height ** 2;
}
returnBMI(1.7, 70)// Error
육안으로 봤을 때는 정상적으로 작동할 것 같지만 이 코드는 그럴수가 없다. 여기서 사용되는 this
는 당연히 함수 본인 returnBMI()
를 가리킬 것 같지만, 실제로는 다른 곳을 가리키고 있다. 함수 내 사용되는 this
는 함수를 호출한 곳을 가리키는데, 인터넷을 서칭하다보니 함수에서 실행된 컨텍스트 내(object, class 어디에도 바인딩 되지 않은 케이스
의 this
는 거의 무조건적으로 global scope
을 가리킨다고 한다.
function thisIsGlobal () { console.log(this); }
thisIsGlobal();
임의의 함수를 만든 후, 실행을 해보면 다음과 같은 결과가 나온다.
<ref *1> Object [global] {
global: [Circular *1],
queueMicrotask: [Function: queueMicrotask],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
},
structuredClone: [Getter/Setter],
clearInterval: [Function: clearInterval],
clearTimeout: [Function: clearTimeout],
setInterval: [Function: setInterval],
setTimeout: [Function: setTimeout] {
[Symbol(nodejs.util.promisify.custom)]: [Getter]
},
atob: [Getter/Setter],
btoa: [Getter/Setter],
performance: [Getter/Setter],
fetch: [AsyncFunction: fetch],
crypto: [Getter]
} // nodeJS 환경에서 실행 시
Window {0: Window, window: Window, self: Window, document: document, name: '', location: Location, …} // 브라우저 개발자 도구의 console에서 실행시
둘 다 global
을 가리킬텐데 왜 결과가 다르게 나오는지에 대한 의문이 들 수 있다. 바로 설명하자면 nodeJS
에서는 다음과 같이 전역 스코프가 구성되어 있고, 브라우저 같은 경우 Window
가 전역 객체를 의미하기 때문에 Window
를 출력한다. 실행 컨텍스트의 call stack
구조를 보면 초기 상태에서 this
가 Window
를 가리키고 있는 이유이기도 하다. 브라우저 콘솔에 다음과 같은 코드를 쳐보면 알 수 있다.
$ var a = 10;
$ let b = 20;
$ console.log(window.a); // 10
$ console.log(window.b); // Error
Window
가 전역 객체이기 때문에 console.log(window.b)
도 정상 작동해야 할 것 같지만, 관련해서 정보를 찾아보니까 Window
객체 내부에 보이지 않는 개념적인 블록 스코프가 존재한다고 한다. let
과 const
는 블록 스코프 내에서만 유효하므로 window
에서 접근을 할수가 없는 것이다. 사실 실사용이 자주 될 것 같지 않은 개념이기는 한데, this
가 전역 스코프를 가리킨다는 내용은 분명 중요하다.
const personObject = {
function thisFunction () {
console.log(this); // personObject 객체
},
thisArrowFunction = () => {
console.log(this); // globalScope
}
}
class personClass {
function thisFunction () {
console.log(this);
}; // personClass 객체
thisArrowFunction = () => {
console.log(this);
}; // personClass 객체
}
헷갈릴수도 있는 코드이기도 한데, 출력 값은 주석을 참고하면 될 것 같다. 오브젝트 내에서의 this
같은 경우 일반 함수에서 쓰였을 때는 스코프체인으로 묶여있는 Object
를 가리킨다. 화살표 함수 같은 경우 그렇지 않고, 전역 스코프를 가리키는데 자신이 생성된 환경을 가리키기 때문이다. 이것에 대해서 좀 더 잘 이해하기 위해서는 Lexical Enviroment / Scope
에 관련해서 찾아보면 될 것이다. 반면 class
는 모든 this
가 객체 자신을 가리키는데, 정확히 어떤 스코프 차이가 있는지는 잘 모르겠지만 뭐 그렇다. 여러 번 코드를 작성해보면 this
가 어디를 가리키고 있는지에 대한 감이 올 것이다.
class personClass {
function f1() {
function f2() { console.log(this); }
}
}
다음과 같이 되어 있는 코드에서 "this
는 당연히 personClass를 가리키겠지" 라고 생각했다면 오답이다. 함수를 실행시켜보면 this
는 전역을 가리키는 것을 알 수 있는데, 함수 컨텍스트에서 쓰인 this
는 무조건 전역을 가리킨다. this
의 시점이 함수 실행이면 사실 상 전역을 가리킨다고 보면 된다. 그렇다면 메서드 내의 함수에서 this
가 전역을 가리키게 만드려고 하면 어떻게 해야 할까? 바로 화살표 함수를 쓰면 된다.
class personClass {
function f1() {
function f2() { console.log(this); } // 전역
}
function f3() {
f4 = () => { console.log(this); } // personClass 객체
}
}
기본적으로 f1()
이나 f3()
에서 this
를 사용한다면 모두 동일하게 personClass
를 가리키지만 그 안의 f2()
와 f4()
에서 쓰이는 this
는 각기 다른 곳을 가리킨다. arrow function
을 사용하면 함수에서의 실행 컨텍스트 개념이 아니라 personClass
와 바인딩 되어 있는 느낌이라 그대로 this
를 사용하여 내부 값에도 접근 할 수 있다.