본문 바로가기
  • soldonii's devlog
Javascript 공부/Advanced Javascript(-)

자바스크립트에서 this는 도대체 무엇일까?

by soldonii 2019. 8. 30.

*Udemy의 "Advanced Javascript Concepts"강의에서 학습한 내용을 정리한 포스팅입니다.

*자바스크립트를 배우는 단계라 오류가 있을 수 있습니다. 틀린 내용은 댓글로 말씀해주시면 수정하겠습니다. 감사합니다. :)


1. This?

아주 많이 보게 되지만, 볼 때마다 헷갈리는 this. 도대체 this가 뭘까?

THIS is the object that the function is a property of.

this는 위 설명대로, 현재 사용하려는 함수를 프로퍼티로 가지고 있는 객체를 가리킨다.

function a() {
  console.log(this)
}
a(); // Window {.........}

 

함수 a는 글로벌 실행 컨텍스트 내에서 선언된 후, 실행되고 있다. 함수 a는 글로벌 실행 컨텍스트 내에 존재하는 글로벌 객체인 Window의 프로퍼티 중 하나로 들어가 있는 것이다. 따라서 a 함수의 this는 a 함수를 프로퍼티로 가지고 있는 전역 객체인 Window를 가리키게 된다.

 

const obj = {
  name: 'Billy',
  sing: function() {
    return 'lalala ' + this.name;
  }
}

obj.sing(); // 'lalala Billy'

 

this를 근데 왜 쓰는걸까? 왜 그렇게 this를 많이 쓰는 것일까?

 

1) 함수를 프로퍼티로 가지고 있는 부모 객체의 값에 접근할 수 있다.

this를 사용하면 특정 함수를 프로퍼티로 가지고 있는 객체의 다른 값, 즉 호출하고자 하는 함수의 형제 프로퍼티에 접근해서 그 값을 이용할 수 있기 때문이다. 

예를 들어, 위 코드에서 sing 함수는 'lalala'와 더불어 노래를 부른 주체도 함께 호출하기를 원한다. 그냥 return 'lalala' + 'Billy'를 하면 되는 것이 아닌가 싶겠지만, 만약 name의 값이 달라지면? 그 때마다 일일이 모든 Billy를 수정할 것인가?

 

그 대신 this를 이용해보자. 위 코드에서 this는 무엇을 지칭할까? this의 정의를 다시 생각해보면, '함수를 프로퍼티로 가지고 있는 객체'라고 했다. sing 함수를 프로퍼티로 가지고 있는 객체는? obj이다. 따라서 여기서 this는 obj이다.

그러므로 this.name은 결국 obj.name과 같은 말이므로 'Billy'를 토해내게 된다. 만약 obj의 name의 값이 'Billy'에서 'Hyunsol'로 바뀔 경우, sing 함수는 알아서 'Hyunsol'을 받아내게 된다.

 

2) 같은 코드를 다른 여러 객체에서 사용할 수 있다.

function importantPerson() {
  console.log(this.name);
}

const name = 'Sunny';
const obj1 = {
  name: 'Cassy',
  importantPerson: importantPerson
}
const obj2 = {
  name: 'Jacob',
  importantPerson: importantPerson
}

 

importantPerson() 함수 내에서 this.name을 출력하게 하고, 이 함수를 다른 객체에서 가져다 쓸 수 있다. 이 경우 ojb1에서의 this.name은 'Cassy', obj2에서의 this.name은 'Jacob'이 자동으로 된다. 같은 코드를 여러번 반복해서 쓰지 않고 한 번만 쓸 수 있으므로 코드가 DRY(Do not Repeat Yourself)하게 된다.

2. 동적 스코프 vs. 정적 스코프

const a = function() {
  console.log('a', this); // 'a window'
  const b = function() {
    console.log('b', this); // 'b window'
    const c = {
      hi: function() {
        console.log('c', this); // 'c {hi: f}'
      }}
    c.hi()
  }
  b() // window.a(b())와 같다.
}
a() // window.a()와 같다.
const obj = {
  name: 'Billy',
  sing() {
    console.log('a', this); // 'a {name: "Billy", sing: f}'
    var anotherFunc = function() {
      console.log('b', this);
    }
    anotherFunc(); //'b window'
  }
}
obj.sing();

 

this가 아주 헷갈리는 이유는... this의 경우는 lexcial scope를 따르지 않고 dynamic scope를 따르기 때문이다!

여태까지 자바스크립트의 함수는 렉시컬 환경을 기억하기 때문에, 어디에서 호출되던지 관계 없이 해당 함수가 선언되었을 때의 환경을 기반으로 스코프가 결정된다고 배웠다. 그런데 this의 경우는 이와 별개로, this의 값은 함수를 어디에서 선언했는지가 아니라, 함수를 어떻게 호출했는지(how the function was called)에 따라서 결정된다.

 

위 사례를 보면, anotherFunc 변수에 저장된 함수는 분명 obj 객체 내에서 선언되어 있다. 정학히 말하면 obj 객체의 key 중 하나인 sing에 대응하는 value 값으로 sing()이라는 함수가 선언되어 있고, 이 sing 함수 내에서 anotherFunc 함수가 선언되어 있다. 그렇기 때문에 anotherFunc()를 실행했을 때 this값이 함수를 프로퍼티로 가지고 있는 obj가 되는 것이 아닌가 하고 생각하게 된다.

 

하지만 this만은 lexical scope(정적 스코프)를 따르지 않고 dynamic scope(동적 스코프)를 따르기 때문에, 함수가 어디에서 쓰여졌는지는 중요하지 않고 대신 어떻게 함수가 호출되었는지가 중요한 것이다. 위 경우 anotherFunc 함수는 괄호()를 사용하여 일반적인 방식으로 호출되고 있다. 이렇게 함수가 일반적인 방식으로 호출될 경우, 해당 함수 내부에 존재하는 this는 default 값으로 Window를 가리키게 된다. 따라서 위 사례에서 anotherFunc()를 실행했을 때 Window를 로그하게 되는 것이다.

 

도대체 왜 이렇게 만들어진 것인지는 잘 모르겠으나... 이러한 복잡함을 피하기 위한 방법으로는 3가지가 있다.

 

1. 화살표 함수 사용

화살표 함수는 자기 자신만의 고유한 this를 가지고 있지 않다. 일반적인 함수는 함수가 호출되는 방식에 따라서 this가 결정되었던 것과 달리, 화살표 함수에서는 둘러싸고 있는 렉시컬 스코프의 this값을 사용하게 된다. 즉 일반적인 변수들처럼 렉시컬 환경을 가지게 된다는 것이다.(선언된 장소에서의 값을 기억한다.)

const obj = {
  name: 'Billy',
  sing() {
    console.log('a', this); // 'a {name: "Billy", sing: f}'
    var anotherFunc = () => {
      console.log('b', this); // 'b {name: "Billy", sing: f}'
    }
    anotherFunc();
  }
}
obj.sing();

위 사례를 보면, anotherFunc 변수에 화살표 함수를 이용하여 함수를 선언했다. 따라서 this가 동적 스코프를 따르지 않고 정적 스코프를 따르게 되므로(일반적으로 자바스크립트 변수/함수가 렉시컬 환경을 기억하는 것과 동일하게) 이제 anotherFunc가 선언된 곳을 보면 객체 obj의 프로퍼티 중 하나로써 sing() 함수 내부에서 선언되어 있다. 따라서 sing() 함수를 프로퍼티로 가지고 있는 obj가 this가 된다. anotherFunc 함수 또한 sing 함수 내부에서 선언되어 있으므로, 마찬가지로 anotherFunc를 프로퍼티로 가지고 있는 객체는 여전히 obj이므로 obj가 this이다.

 

2. .bind(this)를 사용하기

.bind() 메소드는 첫번째 인자로 새롭게 반환될 함수가 가질 this를 전달하게 된다. 아래 코드에서 anotherFunc.bind(this)는 실행할 대상이 되는 anotherFunc 함수의 this 값을 obj 객체 내에서의 this로 전달한 것이다. 현재 obj 객체 내에서의 this는 당연히 obj 객체를 가리킨다. 따라서 원래 anotherFunc();를 실행할 경우, 호출되는 방식에 의거하여 여기에서 this는 Window를 가리키게 되지만, .bind(this)를 이용해서 this값을 강제적으로 obj 내에서의 this 값인 obj로 전달해준 것이다. 따라서 anotherFunc 내에서 this는 obj로 변경되었으므로 anotherFunc.bind(this)를 실행하면 'b {name: "Billy", sing: f}'가 출력된다.

const obj = {
  name: 'Billy',
  sing() {
    console.log('a', this); // 'a {name: "Billy", sing: f}'
    var anotherFunc = function() {
      console.log('b', this); // 'b {name: "Billy", sing: f}'
    }
    return anotherFunc.bind(this);
  }
}
obj.sing()(); // 

 

3. 원하는 this를 변수에 담기

anotherFunc()에서 this를 window를 가리킬 수 밖에 없기 때문에, 사용하기를 원하는 this를 특정 변수에 담아두고, anotherFunc 함수 내에서 this 대신 해당 변수를 사용하게 하면, 원하는 this를 이용할 수 있다.

const obj = {
  name: 'Billy',
  sing() {
    console.log('a', this);
    var self = this;
    var anotherFunc = function() {
      console.log('b', self);
    }
    anotherFunc();
  }
}
obj.sing();

댓글