javascript

[CoreJavascript] 3과 this

프로일기꾼 2025. 1. 30. 13:28

this 바인딩

- this binding은 실행 컨텍스트가 활성화 될 때 한다.(= 실행 컨텍스트가 생성되는 순간에 this를 바인딩한다.)
- 실행 컨텍스트는 컨텍스트에 해당하는 함수가 호출될 때 생성된다. 곧 this는 함수가 호출될 때에 비로소 결정되는 것이다. 그렇기에 정적으로 코드만 봤을 때 어떤 것의 this가 무엇이다 바로 예측할 수 있는 것이 아니라 이 함수를 어떤 식으로 호출했느냐에 따라서 this가 얼마든지 달라질 수 있다.(= this는 동적으로 바인딩 된다.)

 

호출하는 방식에 따른 this

* 아래의 다섯가지에 대한 차이점을 이해하면 this에 대해 완벽하게 정리할 수 있다.

  1. 전역공간에서 호출할 경우
  2. 함수를 함수로서 호출할 경우
  3. 함수를 메서드로서 호출할 경우
  4. 함수를 callback 함수로 넘겼을 때
  5. 함수를 생성자 함수로 호출할 경우

 

1. 전역공간에서 호출할 경우

  • 전역공간은 무조건 처음부터 호출되기에, 여기서 this는 정해져 있다. 그런데 나머지 것들(2번 ~ 5번)에 대해서는 this가 다 다르다.
  • 전역 공간에서의 this는 전역 객체(window - 브라우저 환경, global - nodejs 환경)를 가리킨다.
  • 전역 컨텍스트를 실행하는 주체가 전역 객체(= 호스트 객체)이다. 런타임에 따라 전역 객체의 정보가 달라진다.(window 혹은 global)
  • window, global 모두 ECMAScript에서 정의한 '전역 객체'가 아닌, ECMAScript에서 정의된 전역 객체의 내용(특성)을 바탕으로 런타임(브라우저, 노드)에서 제공하는 구현체이다.

 

2. 함수를 함수로서 호출할 경우

  • 함수 내부에서의 this또한 전역 객체(window/global)를 가리킨다.
function a() {
  console.log(this);
}
a();​


  

  • this가 출력될 때, 전역객체를 출력하는 것이 이상하게 여겨질 수 있는데 위와 같은 코드에서 a라는 함수를 호출하는 순간에 이것을 실행해주는 주체가 누구인지 생각해봤을 때 '전역 객체'라고 생각하는 것이 좋다.
function b() {
  console.log(this); // (1)
  function c() {
    console.log(this); // (2)
  }
}
b();

 

 

  • 위와 같은 코드에서 (1) 결과가 전역객체가 나오는 것은 어떻게든 이해해줄 수 있는데, (2)에서도 전역객체가 나오는 것은 납득이 안될 수 있다. 이것을 JS의 실수라고 보는 견해도 있다. 이러한 견해가 수렴되어 ES6에서는 this 바인딩을 하지 않는 arrow function이 나왔다. arrow function은 바로 위 컨텍스트에 있는 this를 그대로 가져다 사용한다. 따라서 ES6에서는 이 함수를 통해 이 문제점은 해소가 되는데, ES5 환경에서는 함수로써 호출했을 때 this는 무조건, 언제나 전역 객체를 가리킨다.(*암기*)

 

3. 함수를 메서드로서 호출할 경우

  • 메서드를 호출한 주체(메서드명의 '점' 바로 앞이 this)
# 1번
var a = {
  b: function() {
    console.log(this);
  }
}

// b라는 메서드를 a.으로 호출했다. 곧 점(.) 바로 앞이 this가 된다.(a가 this)
// b함수를 a 객체의 "메서드"로서 호출
a.b(); 

# 2번
var d = {
  e: function () {
    function f() {
      console.log(this);
    }
    f();
  }
}
d.e(); // 전역객체가 출력. f라는 함수가 함수로써 호출되었기 때문.

# 3번
var a = {
  b: {
    c: function() {
      console.log(this);
    }
  }
}

a.b.c(); // this는 a.b

 

  • 메서드는 아래와 같은 방법으로도 호출이 가능하다.
// this는 obj
obj.func();
obj['func']();

// this는 person.info
person.info.getName();
person.info['getName']();
person['info'].getName();
person['info']['getName']();

 

메서드 내부 함수에서 전역객체를 출력하지 않게끔 하는법

var a = 10;
var obj = {
  a: 20,
  b: function() {
    console.log(this.a); // (1)

    function c() {
      console.log(this.a); // (2)
    }
    c();
  }
}
obj.b();

 

  • (1)에서는 b가 메서드로서 호출되었기에 obj 내에 있는 a를 찾아 20을 출력한다.
  • (2)에서는 c가 함수로서 호출되었기에 전역객체에 있는 a를 찾아 10을 출력한다.
  • call, apply와 같은 명시적인 this 바인딩 명령을 쓰지 않고는 this 자체를 직접 다른 값으로 덮어씌울 수 없다.
  • 스코프 체인을 활용하여 위와 같은 함수를 사용하지 않고 this를 다른 값으로 우회할 수 있다. 스코프 내 외부 환경 참조를 통하여 this를 가져오는 것이다. 아래의 코드 내용과 같다.
var a = 10;
var obj = {
  a: 20,
  b: function() {
    var self = this; // obj.b()와 같이 호출된다고 했을 때, 이때의 this는 obj인데, 이 값을 self 담는다. 개발자들의 취향에 따라 self라는 변수명 대신 _this, that과 같은 것들이 종종 쓰인다.
    console.log(this.a); // (1)

    function c() { // 내부 함수는 자신의 LexicalEnvironment에서 self를 찾고, 해당 내용이 없기에 outerEnvironmentReference를 타고 외부(b)에 있는 함수의 LexicalEnvironment에서 self를 찾는다. 
      console.log(self.a); // (2)
    }
    c();
  }
}
obj.b();

// 출력결과: (1) - 20 / (2) - 20



  • 아래와 같은 방법으로 a를 표현하면 a가 10이 아닌 20을 출력할 수 있게 된다.
// ES6 arrow function
var a = 10;
var obj = {
  a: 20,
  b: function() {
    console.log(this.a);

    const c = () => { 
      console.log(this.a);
    }
    c();
  }
}
obj.b();

// ES5 call & apply
var a = 10;
var obj = {
  a: 20,
  b: function() {
    console.log(this.a);

    function c() { 
      console.log(this.a);
    }
    c.call(this);
  }
}
obj.b();

 

4. 함수를 callback 함수로 넘겼을 때

  • 기본적으로는 함수 내부에서와 동일.
function a(x, y, z) {
  console.log(this, x, y, z);
}
var b = {
bb: 'bbb'
}

a.call(b, 1, 2, 3); // (1)
a.apply(b, [1, 2, 3]); // (2)

var c = a.bind(b);
c(1, 2, 3) // (3)

var d = a.bind(b, 1, 2);
d(3); // (4)

 

  • 위 1,2,3,4 모두 {bb: 'bbb'} 1 2 3 이 출력된다.
  • 3가지 함수(call, apply, bind)의 API 스펙을 보면 아래와 같다.
  명시적인 this binding
  [ ] 안에 있는 것은 생략 가능한 것.
  func.call(thisArg[, arg1[, arg2[, ... ]]])
  func.apply(thisArg, [argsArray])
  func.bind(thisArg[, arg1[, arg2[, ... ]]])
  • func를 호출할 때, this를 첫번째 인자로 인식하게 해달라고 개발자가 직접 명시한다.
var callback = function() {
  console.dir(this); // callback 함수 내부에서의 this는 콜백 함수 자체가 결정할 수 있는 것이 아니라, callback 함수를 전달받는 곳 즉, b 속성 내 cb를 전달받는 곳에서 어떻게 처리하냐에 따라 this가 달라진다.
};

var obj = {
  a: 1,
  b: function(cb) {
    cb(); // window 출력
    cb.call(this); // obj 출력
  }
};
obj.b(callback);

 

  • 위 주석에 대한 내용을 더 구체적으로 이해하기 위해 아래의 코드를 살펴보자.
# 1번
var callback = function() {
  console.dir(this);
}
var obj = {
  a: 1
}
// setTimeout(callback, 100); -> window 출력
// setTimeout(callback.bind(obj), 100); -> obj 출력

# 2번
document.body.innerHTML += '<div id="a">클릭</div>';

document.getElementById('a').addEventListener(
  'click',
  function() {
    console.dir(this); // 이때는 window가 아닌 element 즉, div#a 가 나온다. 왜냐하면 addEventListener 내부에서 콜백함수를 처리할 때 전역객체가 아닌 이벤트 타겟이 된 엘리먼트를 출력하게끔 되어 있기 때문.
  }
)

 

정리

  • 기본적으로는 함수의 this와 같다.
  • 제어권을 가진 함수가 콜백의 this를 지정해둔 경우도 있다.
  • 이 경우에도 개발자가 this를 binding해서 콜백을 넘기면 그에 따른다.

 

5. 함수를 생성자 함수 호출 시

  • new 연산자를 사용한 경우 this의 출력.
  • new 연산자를 사용했다는 말은 생성자 함수의 내용을 바탕으로 인스턴스 객체를 만드는 명령. 이때는 새로 만들 인스턴스 객체 그 자체가 this가 된다.

function Person(n, a) {
  this.name = n;
  this.age = a;
}
var roy = Person('hnoo', 30); 
console.log(window.name, window.age); // hnoo 30 출력(함수로서 Person을 출력했기에) 

function Person(n, a) {
  this.name = n;
  this.age = a; 
} 

var roy = new Person('hnoo', 30); 
console.log(window.name, window.age); // '' '' 출력 
console.log(roy.name, roy.age); // Person{name: 'hnoo', age: 30} 출력