JavaScript에서의 this 란
📄

JavaScript에서의 this 란

Created
Jun 20, 2021 02:40 AM
Tags
js
 
JavaScript의 this 는 상황에 따라 다양한 의미를 가져 JavaScript 개발자들을 종종 곤란하게 만들곤 한다. 그래서, 이 문서에서 this 에 대해 각 상황별로 의미하는 것을 정리하였다.
 
문서는 if (...) ... else if (...) ... else if (...) ... 형태로 작성되었으며, 첫 번째 상황(Situation)과 일치하지 않으면 그 다음 상황으로 넘어가는 방식으로 읽으면 되겠다.
 
소개할 상황들은 다음과 같다.
 
  1. Arrow Function을 이용해 정의된 경우
  1. 아니면, new 키워드를 통해 Function 또는 Class가 호출되는 경우
  1. 아니면, this 값을 Bind 하는 경우
  1. 아니면, this 를 호출 시(Call-time) 설정하는 경우
  1. 아니면, 함수가 Parent(상위) Object를 통해 호출되는 경우 ( parent.func() )
  1. 아니면, Function 또는 Parent Scope가 Strict Mode에서 돟작하는 경우
  1. 아니면, ...
 
하나씩 보도록 하자

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' })();
 
물론, callapply 로도 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)을 호출하는데, 이 때의 thisObject.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();
 
callapply 역시 마찬가지로 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 로 바인딩 된 함수에 대해, 이 함수의 thiscall 이나 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 의 값은 callapply 로 전달된 객체가 된다.
 
⚠️ 주의 사항 마찬가지로, this 의 값을 지정해주기 위해 callapply 로 전달되는 객체는 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() === objfalse 인데, 이는 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 는 Global this 객체인 globalThis 와 동일하다.
 
⚠️ 주의 사항 this 를 이용해 전역 객체를 참조하도록 하지 않는다. 이 대신, 의미가 명확한 globalThis 를 이용한다.
 
 

Loading Comments...