JavaScript의
this
는 상황에 따라 다양한 의미를 가져 JavaScript 개발자들을 종종 곤란하게 만들곤 한다. 그래서, 이 문서에서 this
에 대해 각 상황별로 의미하는 것을 정리하였다.문서는
if (...) ... else if (...) ... else if (...) ...
형태로 작성되었으며, 첫 번째 상황(Situation)과 일치하지 않으면 그 다음 상황으로 넘어가는 방식으로 읽으면 되겠다.소개할 상황들은 다음과 같다.
하나씩 보도록 하자
1. Arrow Function을 이용해 정의된 경우
const arrowFunction = () => { console.log(this); };
위 코드에서,
this
는 항상 Parent Scope의 this
와 동일한 값을 갖게 된다.const outherThis = this; const arrowFunction = () => { // 항상 `true` console.log(this === outherThis); };
다른 예제들
Arrow Function으로 정의된 경우,
this
값은 bind
를 이용해 바꿀 수 없다.// bind 된 값은 무시되고, `true`가 콘솔에 출력 arrowFunction.bind({ foo: 'bar' })();
물론,
call
과 apply
로도 this
값은 바꿀 수 없다.// 마찬가지로 `true`가 콘솔에 출력된다. arrowFunction.call({ foo: 'bar' }); arrowFunction.apply({ foo: 'bar' });
Arrow Function의
this
는 다른 객체의 멤버로 호출되어도 바뀌지 않는다.const obj = { arrowFunction }; // `ture`가 콘솔에 출력된다. obj.arrowFunction();
당연히 생성자로도 사용할 수 없고,
this
값이 바뀌지도 않는다.// TypeError: arrowFunction is not a constructor new arrowFunction();
인스턴스 메서드에서의 바인딩
만약 메서드가 항상 클래스를 참조하도록 구현하고자 한다면, 가장 좋은 방법은 Class Fields와 함께 Arrow Function을 사용하는 것이다.
class Whatever { someMethod = () => { // 항상 Whatever 클래스의 인스턴스를 참조 console.log(this); }; }
이 패턴은 특히 컴포넌트(React 또는 Web Components)에서 인스턴스 메서드를 Event Listener로 이용하고자 할 때 매우 유용하다.
참고로 Class Fields는 그저
constructor
에서 멤버를 정의했던 것에 대한 Syntax Sugar이기 때문에, 위 로직은 아래와 같이 정의될 수도 있다.class Whatevery { constructor() { const outherThis = this; this.someMethod = () => { // 항상 Whatever 클래스의 인스턴스를 참조 console.log(this); // 항상 `ture` 값을 갖는다. console.log(this === outherThis); }; } }
2. new
키워드를 통해 Function 또는 Class가 호출되는 경우
new Whatever();
위 코드는
Whatever
Function(클래스인 경우에는 Constructor Function)을 호출하는데, 이 때의 this
는 Object.create(Whatever.prototype)
의 반환 값으로 설정된다.class MyClass { constructor() { console.log( this.constructor === Object.create(MyClass.prototype).constructor, ); } } // `true`가 콘솔에 출력 new MyClass();
이전 버전 형태로 클래스를 정의하는 경우도 동일하다.
function MyClass() { console.log( this.constructor === Object.create(MyClass.prototype).constructor, ); } // `true`가 콘솔에 출력 new MyClass();
여기서
constructor
프로퍼티를 이용해 비교했는데, 이는 당연히 this === Object.create()
형태로 비교해버리면 false
가 되어버리기 때문.서로 동일한
constructor
를 참조하고 있다는 의미로 이렇게 비교한 것이다.다른 예제들
new
키워드를 이용해 함수를 호출할 때, this
의 값은 bind
로 바뀌지 않는다.const BoundMyClass = MyClass.bind({ foo: 'bar' }); // `bind` 된 객체는 무시되고, `ture`가 콘솔에 출력된다. new BoundMyClass();
call
과 apply
역시 마찬가지로 this
값에 영향을 끼치지 못한다.또한 객체의 멤버로 호출된다 해도
this
값은 바뀌지 않는다.const obj = { MyClass }; // 콘솔에 `true`가 출력된다. new obj.MyClass();
3. this
값을 Bind 하는 경우
function someFunction() { return this; } const boundObject = { hello: 'world' }; const boundFunction = someFunction.bind(boundObject);
위 코드에서
boundFunction
을 어떤 상황에서든지 호출하게 되면, this
의 값은 Bind 된 객체인 boundObject
가 된다.// `false` console.log(someFunction() === boundObject); // `true` console.log(boundFunction() === boundObject);
⚠️ 주의 사항 외부의this
를 참조하기 위해bind
메서드를 사용하지 말고, 이 대신 Arrow Function을 이용하도록 한다.this
에 대한 의미를 명확히 함으로써, 향후 코드를 디버깅할 때 이해하기 쉽도록 구성하기 위함. 또한bind
를 이용해this
값을 설정할 때, Parent Object(기존this
에 바인딩된)와 관련(Relate)이 없는 값으로 객체를 바인딩하지 않는다. 이는this
와 관련된 예상치 못한 에러를 발생시킬 수 있기 때문. 이 대신 사용할 값을 인수(Argument)로 전달하도록 하자. 더 명확하며, Arrow Function과 함께 사용할 수도 있다.
다른 예제들
bind
로 바인딩 된 함수에 대해, 이 함수의 this
는 call
이나 apply
로 변경할 수 없다.// `call`은 무시되고, `true`가 콘솔에 출력된다. console.log(boundFunction.call({ foo: 'bar' }) === boundObject); // `apply`는 무시되고, `true`가 콘솔에 출력된다. console.log(boundFunction.apply({ foo: 'bar' }) === boundObject);
마찬가지로, Bound 된 함수가 다른 객체의 멤버로 호출된다 해도
this
의 값은 바뀌지 않는다.const obj = { boundFunction }; // 상위 객체는 무시되고, `true`가 콘솔에 출력된다. console.log(obj.boundFunction() === boundObject);
4. this
를 호출 시(Call-time) 설정하는 경우
function someFunction() { return this; } const someObject = { hello: 'world' }; // `true` console.log(someFunction.call(someObject) === someObject); // `true` console.log(someFunction.apply(someObject) === someObject);
이 때의
this
의 값은 call
과 apply
로 전달된 객체가 된다.⚠️ 주의 사항 마찬가지로,this
의 값을 지정해주기 위해call
과apply
로 전달되는 객체는 Parent Object(기존this
에 바인딩된)와 관련(Relate)이 없는 객체여서는 안된다. 언급했듯이 이는this
로 인해 예상치 못한 에러를 야기시키며, 이 대신 사용할 값을 인수(Argument)로 직접 전달하도록 하는 방식을 이용한다. (당연히 Arrow Function과 함께 사용할 수 있다.)
안타깝게도
this
는 특정 상황에서 다르게 값이 바인딩될 수 있기 때문에, 명확히 this
의 값이 지정되지 않은 경우에는 이해하기 어려운 코드가 되어버릴 수 있다.Don't
element.addEventListener('click', function (event) { // DOM 스펙에 명시되었듯이, 여기서의 `this`는 `element`를 가리킨다. // 따라서, `true`가 콘솔 창에 출력된다. console.log(this === element); });
Do
element.addEventListener('click', (event) => { // 이상적으로는, 이렇게 Parent Scope에서 가져오는 것이 적절하다. console.log(element); // 만약 불가능하다면, 이렇게 대체제를 이용해도 되겠다. console.log(event.currentTarget); });
5. 함수가 Parent Object를 통해 호출되는 경우 ( parent.func()
)
const obj = { someMethod() { return this; }, }; // `true` console.log(obj.someMethod() === obj);
여기서는 함수가
obj
객체의 멤버로 호출되었기 때문에, this
의 값은 obj
가 된다.이는 호출 시(Call-time) 결정되는 것이기에,
- 함수가 Parent Object 없이 호출되거나
- 다른 Parent Object에 의해 호출되는 경우
이러한 연결 관계가 끊어지게 된다.
const { someMethod } = obj; // `false` - Parent Object 없이 호출됨 console.log(someMethod() === obj); const anotherObj = { someMethod }; // `false` - 다른 Parent Object에 의해 호출됨 console.log(anotherObj.someMethod() === obj); // `true` console.log(anotherObj.someMethod() === anotherObj);
당연히, 첫 번째
someMethod() === obj
는 false
인데, 이는 obj
객체의 멤버로써 호출되지 않았기 때문이다.아래의 코드가 동작하지 않는 이유와 동일하다.
const $ = document.querySelector; // TypeError: Illegal invocation const el = $('.some-element');
querySelector
는 자신의 this
를 이용해 DOM 노드를 탐색하는데, 이 때 this
가 연결이 끊어진 상태이기 때문에 에러가 발생되는 것.위 코드는 다음과 같이 작성하여 정상적으로 동작하게끔 만들어 줄 수 있다.
const $ = document.querySelector.bind(document); // 또는 const $ = (...args) => document.querySelector(...args);
한 가지 재미있는 점은, 모든 API가
this
를 사용하지는 않는다는 것.가령
console.log
같은 메서드는 자신의 this
를 참조하지 않기 때문에, this
에 어떠한 값이 들어가 있어도 정상적으로 사용할 수 있다.⚠️ 주의 사항 지금까지의 주의사항과 동일하게, Parent Object(기존this
에 바인딩된)와 관련(Relate) 없는 객체에 해당 함수를 이식(Transplant)시키지 않는다. 이는this
로 인해 예상치 못한 에러를 야기시키며, 이 대신 사용할 값을 인수(Argument)로 직접 전달하도록 하는 방식을 이용한다. (Arrow Function과 함께 사용할 수 있다.)
6. Function 또는 Parent Scope가 Strict Mode에서 동작하는 경우
function someFunction() { 'use strict'; return this; } // `true` console.log(someFunction() === undefined);
여기서의
this
값은 undefined
가 된다. (참고로, Parent Scope가 Strict Mode일 때는 굳이 'use strict'
를 명시하지 않아도 되며, 모든 모듈은 Strict Mode로 동작한다.)7. 모두 아니라면...
function someFunction() { return this; } // `true` console.log(someFunction() === globalThis);
⚠️ 주의 사항this
를 이용해 전역 객체를 참조하도록 하지 않는다. 이 대신, 의미가 명확한globalThis
를 이용한다.