본문 바로가기

JavaScript

[ JavaScript ] this

반응형

자바스크립트의 this는 굉장히 골칫덩어리다. 이 글을 통해 this를 정복해보자. 정복당하는건 아닐지..

 

자바스크립트의 함수는 호출될 때 매개변수로 전달되는 인자값 이외에 arguments 객체와 this를 암묵적으로 전달 받는다. 따라서 this는 함수 호출시 동적으로 결정된다. 그런데 호출 시점에 결정된다는게 어떤 의미인지 잘 와닿지 않기 때문에 함수를 종류별 나누고 각 케이스마다 어떻게 되는지 살펴보자. 그래서 우선 'this는 함수의 종류에 따라 바인딩 대상이 달라진다' 로 이해하자. 함수의 종류는 여러가지가 있는데 나는 여섯 가지로 분류해봤다. 아래가 이 글의 목차라고 생각하면 된다. 

 

함수 종류

1. 일반 함수
2. 객체의 메소드
3. 화살표 함수 
4. 생성자 함수 
5. apply/call/bind
6. addEventListener의 콜벡함수

 

1. 일반 함수

  • 일반 함수의 this는 전역객체에 바인딩된다. 클라이언트측에서는 window 객체가 된다. 
function foo() {
  console.log("foo's this: ",  this);  // window
  function bar() {
    console.log("bar's this: ", this); // window
  }
  bar();
}
foo();

 

 

 

  • 메소드의 내부함수일 경우에도 this는 전역객체에 바인딩된다. 메소드의 this는 객체 자신을 가리킴에 유의하자. 
var value = 1;

var obj = {
  value: 100,
  foo: function() {
    console.log("foo's this: ",  this);  // obj
    console.log("foo's this.value: ",  this.value); // 100
    function bar() {
      console.log("bar's this: ",  this); // window
      console.log("bar's this.value: ", this.value); // 1
    }
    bar();
  }
};

obj.foo();

여기서 bar 함수와 foo 함수의 호출 시점을 비교해볼 필요가 있다. 함수가 호출되는 코드를 보면 foo는 객체의 메소드로 호출되고 있고, bar는 foo 내부에서 bar 자체로 호출되고 있다. 일반함수의 경우 bar처럼 그 자체로 호출되면 this는 전역객체가 된다. 하지만 foo처럼 메소드로서 호출되면 this는 메소드를 포함하는 객체가 된다. 일반함수라고 명시한 이유는 화살표 함수는 약간 다르게 작동하기 때문이다. 이에 대한 설명은 화살표 함수 부분에서 나오니 우선 넘어가자.

 

 

 

  • 콜벡함수의 this도 전역객체에 바인딩된다. 하지만 addEventListener의 콜벡함수는 예외다. 이는 마지막 부분에 나온다. 
const ary = [1,2,3,4];

ary.map(function(e) {
    console.log(this); // window
})

 

2. 객체의 메소드 

  • 메소드 호출시 this는 해당 메소드를 호출한 객체에 바인딩 된다. 
var obj1 = {
    name: 'Lee',
    sayName: function() {
      console.log(this.name);
    }
  }
  
  var obj2 = {
    name: 'Kim'
  }
  
  obj2.sayName = obj1.sayName;
  
  obj1.sayName(); // Lee
  obj2.sayName(); // Kim

 

아래의 경우를 주의해야 한다. 메소드 안에 있는 함수를 밖으로 꺼내면 this는 더이상 메소드 내부의 this가 아니다. 

 

  var sayName2 = obj1.sayName;
  
  /* 이렇게 된다
  var sayName2 = function() {
  	console.log(this.name);
  }
  */
  
  sayName2(); // this는 window, 따라서 this.name = undefined

이를 함수 호출 시점과 연관시켜 생각해보자. sayName2는 왜 this가 window로 바뀌었을까? 답은 간단하다. 호출 시점에 함수 그 자체로 호출되었기 때문이다. 

 

3. 화살표 함수

  • 화살표 함수에서 this는 상위 (함수 레벨)스코프의 this에 바인딩 된다.
  • 화살표 함수는 this가 없다고 생각하면 편하다. 따라서 위로 올라가다가 상위 스코프의 this를 찾는다. 이를 lexical this라고 부른다.
var obj1 = {
    name: 'Lee',
    sayName: () => {
      console.log(this.name); // this는 window 객체
    }
  }
    
obj1.sayName(); // undefined

 

2번의 예제 코드에서 메소드를 화살표 함수로만 바꿔줬다. 상위 스코프는 전역이므로 sayName 메소드의 this는 window 객체가 된다. 쉽게 말해서 화살표 함수의 this는 상위 함수의 this를 가져온다고 이해하자. obj1은 함수가 아니므로 화살표 함수의 상위 함수는 전역이 된다. 참고로 자바스크립트에서 전역은 익명함수로서 콜스텍에 푸쉬된다. 

 

화살표 함수는 일반함수와 다르게 this가 함수 선언 시점에 결정된다. 따라서 화살표 함수의 this는 정적으로 결정된다고 볼 수 있다. 즉, 화살표 함수의 this는 함수 호출 시점과는 상관 없다

 

4. 생성자 함수

  • 일반 함수와 생성자 함수의 문법적 차이는 new밖에 없다. 따라서 생성자 함수를 구분해주기 위해 일반적으로 첫글자를 대문자로 선언한다.
  • 생성자 함수의 this는 빈 객체 {} 에 바인딩 된다. 여기에 this를 사용하여 동적으로 프로퍼티나 메소드를 생성할 수 있다.
  • this는 새로 생성된 객체를 가리키므로 this를 통해 생성한 프로퍼티와 메소드는 새로 생성된 객체에 추가된다.

5. apply , call, bind 함수

세 가지 메소드는 전부 함수에 this를 바인딩해주는 메소드다. 원하는 값 또는 객체를 함수의 this로 쓰고 싶을때 사용하면 편하다. 각각 사용방식에 약간 차이가 있다. 참고로, 화살표 함수에 적용하면 this는 바인딩 되지 않는다. apply, call, bind를 적용할 때는 반드시 일반함수를 쓰자.  

1) bind

  • thisArg : func의 this를 의미한다. 따라서 func에서 this를 출력하면 thisArg가 나온다. 참고로 원래 함수의 this는 window다.
  • arg1, arg2 ... : func의 첫번째 인자, 두번째 인자... 가 된다. arg1, arg2만 넣어줬다고 가정했을때 함수에 인자를 넣어주지 않아도 자동으로 arg1, arg2 ... 가 각각 첫번째, 두번째 인자가 된다. 함수에 인자를 한개 넣어주면 func(arg1, arg2, 넣어준 인자) 꼴로 호출된다.
  • bind는 즉시 호출되지 않으므로 다른 변수에 저장했다가 호출하는 것이 일반적이다.
// 구문
func.bind(thisArg[, arg1[, arg2[, ...]]])

// 예제
const obj = {
    x: 42,
    getX: function() {
      return this.x;
    }
};
  
const noBind = obj.getX;
console.log(noBind()); 
// expected output: undefined

const yesBind = obj.getX.bind(obj);
console.log(yesBind());
// expected output: 42

이해를 돕기 위해, unboundGetX = obj.getX는 아래와 똑같다.
const unboundGetX = function() { return this.X }

noBind를 호출하면 this.x가 반환되는데, 여기서 this는 윈도우 객체다. 윈도우 객체에 x 변수가 없으므로 undefined가 출력된다.yesBind를 호출하면 this.x가 반환되는데, 여기서 this는 bind에 의해 obj 객체가 된다. 따라서 42(obj.x)가 출력된다.

2) call

  • bind와 거의 유사하지만 즉시호출된다는 차이가 있다.
// 구문
func.call(thisArg[, arg1[, arg2[, ...]]])

// 예제
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
}

console.log(new Food('cheese', 5).name);
// expected output: "cheese"

Product를 호출할 때 thisArg로 Food의 this를 넘겼기 때문에 Food의 name, price에는 인자값들이 들어간다.

3) apply

  • call과 거의 유사하지만 인자값들이 유사배열 형태로 들어간다. call과 같이 즉시호출된다.
// 구문
func.apply(thisArg, [argsArray])

 

6. addEventListener의 콜백함수

  • addEventListener 함수의 콜백 함수를 화살표 함수로 정의하면 this가 상위 컨택스트인 전역 객체 window에 바인딩 된다.
  • addEventListener 함수의 콜백 함수를 일반 함수로 정의하면 this는 이벤트 리스너에 바인딩된 요소(currentTarget)에 바인딩 된다.
  • addEventListener의 콜백함수는 마치 addEventListener의 메소드로 동작하는 것으로 이해하면 된다. 

 

응용사례

화살표 함수, 일반함수, 이벤트리스너에서의 this를 이해해야 풀 수 있는 예제를 소개한다. 아래 결과가 왜 이렇게 되는지 예상해보자.

 

var button = document.getElementById('myButton');

button.addEventListener('click', function() {
  console.log(this); // => button

  function inner() {
    console.log(this); // => Window
  }
  inner();
});

 

addEventListener 함수의 콜벡 함수 내부의 this는 currentTarget을 가리키므로 this를 출력하면 button이 출력될 것이다. inner 함수는 그 자체로 호출되었기 때문에 this는 전역 객체에 바인딩된다. 내부함수의 this가 button을 가리키도록 하는 방법에는 두 가지가 있다.

 

 

1) 함수 외부에서 this를 다른 변수에 저장하는 방법

 

var button = document.getElementById('myButton');

button.addEventListener('click', function() {
  console.log(this); // => button

  const that = this; // 콜벡함수의 this를 저장

  function inner() {
    console.log(that); // => button
  }
  inner();
});

 

 

2) 화살표 함수를 사용하는 방법

 

var button = document.getElementById('myButton');

button.addEventListener('click', function() {
  console.log(this); // => button

  const inner = () => {
    console.log(this); // => button (화살표 함수는 상위 (함수 레벨)스코프의 this를 따라간다)
  }
  inner();
});

화살표 함수는 함수 선언시에 this가 결정된다. inner 함수의 상위 함수레벨 스코프는 이벤트리스너의 콜백함수이므로 inner 함수는 호출에 상관없이 this → button이 된다. 

 

 

 

마지막은 퀴즈다! 각각 무엇을 출력할지 맞춰보자.

let obj1 = {
    inner() {
        return function() {
            return this;
        }
    }
}

let obj2 = {
    inner() {
        return () => {
            return this;
        }
    }
}

console.log(obj1.inner()()); 
console.log(obj2.inner()());
더보기

첫번째는 window를 출력하고, 두번째는 obj2 객체를 출력한다.

 

두 케이스 모두 obj.inner() 의 결과는 함수인데 일반함수냐, 화살표함수냐 차이다. 

 

일반함수인 경우 '함수()' 꼴로 그 자체로 호출되었으므로 this는 전역객체가 된다.

 

화살표 함수인 경우 this는 선언시점의 상위 함수레벨 스코프의 this를 따른다. obj2.inner()로 반환되는 함수의 상위 스코프는 inner 함수이므로 inner의 this인 obj2 객체가 출력된 것이다. 

 

 

 

 

<참조>

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/bind  

https://poiemaweb.com/js-this  

https://www.zerocho.com/category/JavaScript/post/5b0645cc7e3e36001bf676eb   

 

반응형