[Core JavaScript] 3. This

Fe·2022년 9월 24일
0

Core Javascript

목록 보기
3/3
post-thumbnail

본문에 앞서서

자바스크립트를 공부하면서 본 코드 중에 this가 들어가지 않은 코드를 찾을 수가 없을 정도로 this는 필수적이고 아주 중요한 개념이다. 하지만 그만큼 혼란스러운 개념이기도 한데, this는 아무데서나 쓰일 수 있기 때문이다. 어느 상황에서 this를 사용했는지에 따라 this가 가리키는 대상이 달라지기 때문에 잘 공부해서 기억해야겠다.

this

자바스크립트에서 this는 실행 컨텍스트가 생성될 때, 즉 함수를 호출할 때 결정된다. 실행 컨텍스트는 이전 글에서 살펴봤듯이 함수를 호출할 때 생성되기 때문이다.

전역 공간에서 this

전역 공간에서 this는 전역 객체를 가리킨다. 전역 컨텍스트를 생성하는 것이 전역 객체이기 때문이다. 전역 객체는 런타임 환경에 따라 다른 이름을 가지고 있는데, 브라우저 환경에서 window이고, Node.js 환경에서는 global이다.

각 환경에서 this를 입력해보면 전역 객체에 대한 정보를 출력한다.

전역 공간 여담

여담으로 전역 공간에서만 발생하는 특이한 성질을 보고 가자. 자바스크립트에서 전역 변수란, 변수이면서 동시에 전역객체의 프로퍼티이기도 하다.

var a = 1;
console.log(a); // 1
console.log(window.a); // 1
console.log(this.a); // 1

Node.js 환경의 경우 window.a 대신 global.a를 사용해도 같은 결과를 얻는다.

우리는 a에 1을 할당했을 뿐인데 어떻게 window.a와 this.a가 모두 1을 출력하는 것일까?

자바스크립트의 모든 변수는 특정 객체의 프로퍼티로서 동작한다. 이 때문에 위의 세 문장이 모두 1을 출력하는 것이다. var 연산자를 이용해 변수를 선언하더라도 자바스크립트 엔진은 어떤 특정 객체의 프로퍼티로 인식한다. 여기서 특정 객체실행 컨텍스트의 LexicalEnvironment이다. 실행 컨텍스트는 변수를 수집해서 LexicalEnvironment의 프로퍼티로 저장한다. 이후 변수를 호출하면 LexicalEnvironment를 조회해서 일치하는 프로퍼티가 있는 경우 그 값을 반환하게 된다. 전역 컨텍스트의 경우에는 LexicalEnvironment가 전역 객체를 그대로 참조한다.

특정 객체의 프로퍼티로 동작한다 했으니 window.athis.a의 결과가 1이 나오는 것은 당연해졌다.

그렇다면 a만 출력했을 때는 어떻게 1이 나올까?

어차피 a에 접근하려 하면 스코프 체인에서 a를 검색하다가 마지막에 도달하는 전역 스코프의 LexicalEnvironment(전역 객체가 된다)에서 a를 발견하고 그 값을 반환하기 때문이다. 간단하게 window.를 생략한 채 변수 이름만 써도 접근이 가능하다는 것을 알아두면 된다. 사실 원리를 알기 전에도 당연하게 받아들였을 것이다.

여기서 한 가지 의문이 생긴다.

var b = 2window.b = 2는 같을까?

일반적으로는 그렇지만 삭제 명령(delete)에 대해서는 전혀 다르다.

var a = 1;
delete window.a; // false
console.log(a, window.a, this.a); // 1 1 1
var b = 2;
delete b; // false
console.log(b, window.b, this.b); // 2 2 2
window.c = 3;
delete window.c; // true
console.log(c, window.c, this.c); // Uncaught ReferenceError : c is not defined
window.d = 4;
delete d; // true
console.log(d, window.d, this.d); // Uncaught ReferenceError : d is not defined

전역객체의 프로퍼티로 할당한 경우에는 삭제가 되었지만 전역변수로 할당한 경우에는 삭제가 되지 않았다. 전역변수로 선언하면 자바스크립트 엔진이 자동으로 전역객체의 프로퍼티로 할당하면서 해당 프로퍼티의 configurable 속성(변경 및 삭제 가능성)false로 정의한다.

결론: var로 선언한 전역변수와 전역객체의 프로퍼티는 호이스팅 여부 및 configurable 여부에서 차이를 보인다.

메서드로서 호출할 때 그 메서드 내부에서의 this

함수 vs 메서드

먼저 함수와 메서드의 차이를 짚고 넘어가자. 둘을 구분하는 차이는 독립성이다.

함수: 그 자체로 독립적인 기능 수행
메서드: 자신을 호출한 대상 객체에 관한 기능 수행

하지만 어떤 함수를 객체의 프로퍼티에 저장했다고 해서 항상 메서드가 되는 것은 아니다. 객체의 메서드로서 호출한 경우에만 메서드로 동작하고, 그렇지 않으면 함수로 동작한다.

var func = function (x) {
  console.log(this, x);
};
func(1); // Window {...} 1

var obj = {
  method: func
};
obj.method(2); // { method: f } 2

func(1)을 호출했을 때는 this가 전역객체를 가리킨다. 반면 obj.method(2)를 호출했을 때는 this가 obj 객체를 가리킨다. func의 값과 obj의 프로퍼티 method의 값은 모두 첫 번째 줄의 함수를 참조한다.

결론: 함수를 변수에 담아 호출하는 경우와 객체의 프로퍼티에 할당해서 호출하는 경우에 this는 달라진다.

함수로서 호출 vs 메서드로서 호출 구분하기

구분 방법은 간단하다. 메서드로 호출하는 방법은 두 가지가 있다.

  1. 함수 앞에 .이 있으면 메서드이다. (점 표기법)
  2. 대괄호 표기법으로 호출했으면 메서드이다.
var obj = {
  method: function (x) {console.log(this, x);}
};
obj.method(1); // { method: f } 1
obj['method'](2); // { method: f } 2

모두 메서드로서 호출이다.

메서드 내부에서의 this

메서드로서 호출하는 방법까지 알아보았으니 이제 이 상황에서 this가 무엇을 가리키는지만 알면 된다.

var obj = {
  methodA: function () {console.log(this);}
  inner: {
  	methodB: function () {console.log(this);}
  }
};
// 이 둘은 this가 obj를 가리킨다.
obj.methodA();               // { methodA: f, inner: {...} }
obj['methodA']();            // { methodA: f, inner: {...} }

// 이 넷은 this가 obj.inner를 가리킨다.
obj.inner.methodB();         // { methodB: f }
obj.inner['methodB']();      // { methodB: f }
obj['inner'].methodB();      // { methodB: f } 
obj['inner']['methodB']();   // { methodB: f }

함수를 메서드로서 호출하는 경우 호출 주체는 함수명(프로퍼티명) 앞의 객체이다.
점 표기법을 사용하는 경우, 점이 아무리 많아도 마지막 점 앞까지의 객체가 this가 된다.
대괄호 표기법도 마찬가지로 마지막 함수명(프로퍼티명) 앞까지의 객체가 this가 된다.

함수로서 호출할 때 그 함수 내부에서의 this

this에는 호출한 주체가 담긴다. 하지만 함수로 호출한다는 것은 호출 주체를 명시하지 않고 개발자가 코드에 직접 관여해서 실행한 것이다. 따라서 this는 지정되지 않는다.

2장에서 거의 그냥 넘어가다시피 했던 내용 중 실행 컨텍스트의 ThisBinding이 있었다. this가 바라봐야 할 대상을 저장하는데, 지정되지 않은 경우 전역 객체가 지정된다. 따라서 함수로서 호출할 때 그 내부에서의 this는 전역 객체를 가리킨다.

var obj1 = {
  outer: function() {
    console.log(this);
    var innerFunc = function() {
      console.log(this);
    }
    innerFunc(); // (2)
    
    var obj2 = {
      innerMethod: innerFunc
    };
    obj2.innerMethod(); // (3)
  }
};
obj1.outer(); // (1)

위 코드에서 obj1.outer(), innerFunc(), obj2.innterMethod()를 호출했을 때의 결과를 맞혀보자.

  • obj1.outer() - 3번째 줄에서 this가 출력된다. 이 문장은 점 표기법을 이용한 메서드로서의 호출이다. 따라서 출력 결과는 obj1이다.
  • innerFunc() - 바로 위의 innerFunc 내부에서 this가 출력된다. 점이나 대괄호가 없는 함수로서의 호출이다. 따라서 출력 결과는 전역 객체이다.
  • obj2.innerMethod() - 바로 위의 obj2 내부에서 innerFunc이 실행된다. 얼핏 보면 innerFunc()과 같지만 점 표기법을 이용한 메서드로서의 호출이다. 따라서 출력 결과는 obj2이다.

따라서 (1): obj1, (2): 전역 객체, (3): obj2이다.

같은 함수여도 함수로서 호출하느냐, 메서드로서 호출하느냐에 따라 바인딩되는 this의 대상이 서로 달라진다. 이것을 결정하는 요소로 오직 함수 호출 구문에 점이나 대괄호 표기가 있는지만 보면 된다.

메서드의 내부 함수에서 this 우회하기

분명 다른 함수 내부에서 함수를 실행하는데, this는 전역 객체를 가리키고 있다면 참 아이러니한 상황이다. 호출 주체가 없을 때(함수로서 실행할 때) 자동으로 전역 객체를 가리키지 않고 주변 환경에 따라 this를 사용할 수 있으면 좋겠다. ES5까지는 자체적으로 내부함수에 this를 상속할 방법이 없었다. 따라서 이 경우 변수를 활용하는 방법을 사용했다.

var obj = {
  outer: function() {
    console.log(this); // { outer: f }
    var innerFunc1 = function() {
      console.log(this); // Window { ... }
    };
    innerFunc1();
    
    var self = this;
    var innerFunc2 = function() {
      console.log(self); // { outer: f }
    };
    innerFunc2();
  }
};
obj.outer();

innerFunc2()는 함수로서의 호출이라 전역 객체를 가리켜야 하지만 obj를 가리키고 있다. 변수를 하나 만들어서 this를 할당하고, 그 변수를 출력했기 때문에 this가 우회되었다. 상위 스코프의 this를 저장해서 내부함수에서 활용했을 뿐이다.

this를 바인딩하지 않는 함수

ES5까지는 변수를 이용해서 this를 우회했다면 ES6에는 화살표 함수가 도입되어 우회할 수 있게 되었다. 화살표 함수는 this를 바인딩하는 과정이 없어서 상위 스코프의 this를 그대로 활용할 수 있다.

var obj = {
  outer: function() {
    console.log(this); // { outer: f }
    var innerFunc = () => {
      console.log(this); // { outer: f }
    };
    innerFunc();
  }
};
obj.outer();

콜백 함수 호출 시 그 함수 내부에서의 this

함수 A의 제어권을 다른 함수(메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라고 한다. 콜백 함수도 함수이기 때문에 기본적으로 this가 전역 객체를 참조하지만 제어권을 받은 함수에서 별도로 this의 대상을 지정할 수 있다.

setTimeout(function () {console.log(this); }, 300);

[1, 2, 3, 4, 5].forEach(function (x) {
  console.log(this, x);
});

document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a')
	.addEventListener('click', function (e) {
  		console.log(this, e);
    });

setTimeout 함수와 forEach 메서드는 그 내부에서 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않는다. 따라서 전역 객체를 참조한다.
한편 addEventListener 메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의돼 있다. 여기서는 메서드명의 . 앞부분이 this가 된다.

결론: 콜백 함수에서는 this를 하나로 정의할 수 없다. 제어권을 가지는 함수가 콜백 함수에서 this를 무엇으로 할지 결정하고, 특별히 정의하지 않았다면 전역 객체를 가리킨다.

생성자 함수 내부에서의 this

생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수이다. 객체지향에서 생성자를 클래스, 클래스를 통해 만든 객체를 인스턴스라 한다. 생성자는 구체적인 인스턴스를 만드는 일종의 이다.

자바스크립트에서 new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작한다. 그리고 어떤 함수가 생성자 함수로서 호출된 경우 this는 새로 만들 인스턴스 자신이 된다.

var Cat = function (name, age) {
  this.bark = '야옹';
  this.name = name;
  this.age = age;
};
var choco = new Cat('초코', 7);
var nabi = new Cat('나비', 5);
console.log(choco, nabi);

// 결과
// Cat { bark: '야옹', name: '초코', age: 7 }
// Cat { bark: '야옹', name: '나비', age: 5 }

new 키워드와 함께 만든 두 개의 인스턴스에서 this는 각각 choconabi를 가리키고 있다.


앞에서 다룬 화살표 함수 이외에도 명시적으로 this를 바인딩하는 방법으로 세 가지 메서드가 있다. 앞에서 배운 규칙에 부합하지 않는다면 명시적으로 바인딩했다고 추측할 수 있다. 이 부분은 따로 포스팅하려 한다.

정리

  1. 전역 공간에서 this전역 객체(window, global)를 가리킨다.
  2. 어떤 함수를 함수로서 호출했을 때 this전역 객체를 가리킨다.
  3. 어떤 함수를 메서드로서 호출(점 표기법, 대괄호 표기법)했을 때 this함수명(프로퍼티명) 앞의 객체를 가리킨다.
  4. 콜백 함수 내부에서 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역 객체를 가리킨다.
  5. 생성자 함수에서 this생성될 인스턴스를 가리킨다.
profile
하고 싶은 게 많은 사람

0개의 댓글