TIL 27 JavaScript - (2) this

Leo·2021년 5월 12일
0

Javascript

목록 보기
11/17
post-thumbnail

다른 대부분의 객체지향 언어에서 this는 클래스에서만 사용할 수 있기 때문에 혼란의 여지가 없거나 많지 않다. 그러나 자바스크립트의 this는 어디서든 사용할 수 있다. 상황에 따라 this가 바라보는 대상이 달라지는데 정확한 작동 방식을 이해하지 못하면 문제가 생겨도 원인을 파악하기 힘들다.

함수와 객체(메서드)의 구분이 느슨한 자바스크립트에서 this는 실질적으로 이 둘을 구분하는 거의 유일한 기능이다.

상황에 따라 달라지는 this


자바스크립트에서 this는 기본적으로 실행 컨텍스트가 생성될 때 함께 결정된다. 즉, this는 함수를 호출할 때 결정된다. 함수를 어떤 방식으로 호출하느냐에 따라 this가 가리키는 값이 달라진다는 것이다.

전역 공간에서의 this


전역 공간에서 this는 전역 객체를 가리킨다. 브라우저 환경에서 전역객체는 window이고 Node.js 환경에서는 global이다.

아래의 예시를 봐보자. 전역공간에서 선언한 변수 a에 1을 할당했을 뿐인데 window.a와 this.a모두 1이 출력된다. 전역공간에서의 this는 전역객체를 의미하므로 두 값이 같은 값을 출력한다.

자바스크립트의 모든 변수는 실은 특정 객체의 프로퍼티로서 작동한다. 사용자가 var 연산자를 이용해 변수를 선언하더라도 실제 자바스크립트 엔진은 어떤 특정 객체의 프로퍼티로 인식하는 것이다.

여기서 특정 객체란 실행 컨텍스트의 LexicalEnvironment이다. 실행 컨텍스트는 변수를 수집해서 L.E의 프로퍼티로 저장한다. 이후 어떤 변수를 호출하면 L.E를 조회해서 일치하는 프로퍼티가 있을 경우 그 값을 반환한다. 전역 컨텍스트의 경우 L.E는 전역객체를 그대로 참조한다.

정리하면,

  1. 실행 컨텍스트는 변수를 수집해서 L.E의 프로퍼티로 저장한다.
  2. 어떤 변수를 호출하면 L.E를 조회해서 일치하는 프로퍼티가 있을 경우 그 값을 반환한다.
  3. 전역 컨텍스트의 경우 L.E는 전역객체를 그대로 참조한다.
  4. 자바스크립트 엔진이 전역 컨텍스트의 L.E(전역 객체를 참조하는)의 프로퍼티로 인식되어 1이 출력된다.

위의 과정을 한 문장으로 요약하면, ‘전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다’라는 뜻이다.

그리고 위의 예시에서 직접 a를 호출할 때에도 1이 나오는 이유는 변수 a에 접근할 때 스코프체인에서 a를 검색하다 가장 마지막에 도달하는 전역 스코프의 L.E, 전역객체에서 해당 a를 발견해서 그 값을 반환하기 때문이다. 간단하게 (window.)이 생략된 것이다.

그런데 전역변수 선언과 전역객체의 프로퍼티 할당 사이에 전혀 다른 경우가 있다. ‘삭제’ 명령에 대해 그렇다

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

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


함수와 메서드

프로그래밍 언어에서 함수와 메서드는 미리 정의한 동작을 수행하는 코드 뭉치들로, 이 둘을 구분하는 유일한 차이는 독립성이다. 어떤 함수를 객체의 프로퍼티에 할당한다고 해서 그 자체로 무조건 메서드가 되는 것이 아니라 객체의 메서드로서 호출할 경우에만 메서드로 동작하고 그렇지 않으면 함수로 작동한다.

익명함수를 변수에 담아 호출했을 때는 함수ex. func()이고, obj 객체의 프로퍼티에 할당해서 호출한 경우ex. obj.method()에는 메서드이다. 함수로서 호출과 메서드로서의 호출을 구분하는 방법은 함수 앞에 (.)점이 있는지 여부로 판단할 수 있다. 점 표기법이 아닌 대괄호 표기법일 때도 어떤 함수를 호출할 때 그 함수 이름 앞에 객체가 명시돼 있으면 메서드로 호출, 아무것도 없으면 함수이다.

메서드 내부에서의 this

어떤 함수를 메서드로서 호출하는 경우 호출 주체는 함수명(프로퍼티 명) 앞의 객체이다. 점 표기법의 경우 마지막 점 앞에 명시된 객체가 곧 this이다.

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

어떤 함수를 함수로서 호출할 때 this가 지정되지 않는다. this에는 호출한 주체에 대한 정보가 담겨있는데 함수로서 호출은 호출 주체가 없고 개발자가 직접 실행한 것이기 때문에 호출 주체의 정보를 알 수 없다. 실행 컨텍스트를 활성화 할 때 this가 지정되지 않은 경우 전역 객체를 바라보는데, 따라서 함수의 this는 전역 객체를 가리킨다.

메서드의 내부함수에서의 this

메서드 내부에서 정의하고 실행한 함수에서의 this는 헷갈리기 쉬우니 코드를 통해 알아보자. 아래의 console.log(this)에서의 this가 무엇을 가리키는지 알아보자.

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

결과는 아래와 같다. 앞에서 .이 있고 없고로 함수와 메서드를 구분할 수 있다고 했다.

(1) obj1
(2) window
(3) obj2

결과를 보기전에 호출되는 모습을 봐보자.

(1) obj1.outer();
(2) innerFunc();
(3) obj2.innerMethod();

1번 에서는 .앞에 obj1 2번에서는 함수 자체를 호출하였고, 3번에서는 .앞에 obj2가 있다. 그래서 this가 가리키는 것이 1번, 3번은 .앞에 있는 객체를 가리키고 2번은 함수 자체를 호출했기 때문에 전역 객체인 window를 가리킨다.

결국은 this 바인딩에서 주변 환경은 중요하지 않고, 해당 함수를 호출하는 구문 앞에 점 또는 대괄호 표기가 있는지 없는지가 중요하다.

메서드의 내부 함수에서의 this를 우회하는 방법

호출 주체가 없을 때는 자동적으로 전역객체를 바인딩하지 않고, 호출 당시 주변 환경의 this를 그대로 상속받아 사용하려한다. 예를 들면 this 가 현재 컨텍스트에 바인딩된 대상이 없으면 직전 컨텍스트의 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();

innerFunc1 내부에서는 window객체를 가리키고 innerFunc2 내부에서는 obj를 가리킨다. outer 스코프에서 self라는 변수에 this를 저장한 상태에서 innerFunc2에 넘겨주면 window가 아닌 outer스코프의 this처럼 위장한 것 처럼 보인다.

this를 바인딩하지 않는 함수

ES6에서는 함수 내부에서 this가 전역객체를 바라보는 문제를 해결하기 위해 화살표 함수를 도입했다. 화살표 함수는 this를 바인딩하지 않기 때문에 상위 스코프의 this를 그대로 활용할 수 있다. 이렇게 되면 방금 앞에서 살펴본 우회법이 필요가 없게 된다.

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

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


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

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

(1) forEach 메서드를 돌며 배열의 요소와 전역 객체(window) 출력
(2) 3초 후에 전역객체 출력

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

(3) 클릭 할 때마다 앞서 지정한 엘리먼트와 클릭이벤트에 관한 정보가 들어있는 객체 출력

(1), (2)의 setTimeout 함수forEach 메서드는 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않아서 전역객체를 참조한다.

(3)의 addEventListener 메서드는 콜백함수를 호출할 때 자신의 this를 상속하도록 정의되어있다. 그래서 .앞의 id=“a”인 버튼을 가리키게 된다.

콜백 함수에서의 this는 정확히 무엇이다라고 정의할 수 없다. 콜백 함수의 제어권을 가지는 함수가 콜백 함수에서의 this를 정하고 따로 정하지 않았다면 전역객체를 바라본다.

생성자 함수 내부에서의 this


어떤 함수가 생성자 함수로서 호출된 경우 내부에서의 this는 곧 새로 만들 구체적인 인스턴스 자신이 된다.

var Cat = function (name, age) {
  this.bark = '야옹';
  this.name = name;
  this.age = age;
}

var choco = new Cat('초코', 7);
var nabi = new Cat('나비', 10);

console.log(choco, nabi);

6번째 줄에서 실행한 생성자 함수 내부에서의 this는 choco 인스턴스 가리킨다.
7번째 줄에서 실행한 생성자 함수 내부에서의 this는 nabi 인스턴스를 가리킨다.

명시적으로 this를 바인딩하는 방법


call 메서드

call 메서드는 메서드의 호출 주체인 함수를 직시 실행하도록 하는 명령이다. call 메서드의 첫 번째 인자를 this로 바인딩하고 그 이후에 인자들을 호출할 함수의 매개변수로 사용한다.

var obj = {
  a:1,
  method: function (x, y) {
    console.log(this.a, x, y);
  }
};

obj.method(2, 3); // 1 2 3
obj.method.call({a:4}, 5, 6); // 4 5 6

apply 메서드

apply 메서드는 call 메서드와 기능적으로 완전히 동일하다. 다만 apply메서드는 두 번째 인자를 배열로 받아 그 배열의 요소들을 호출할 함수의 매개변수로 지정한다는 점에서만 차이가 있다.

var func = function (a, b, c) {
  console.log(this, a, b, c); // {x:1} 2 3 4 
};
func.apply({x:1}, [2, 3, 4]);
// 원래는 전역 객체를 가리키지만 apply메서드를 통해 {x:1}을 가리킨다.

var obj = {
  a: 1,
  method: function(x, y) {
    console.log(this.a, x, y);
  }
};
obj.method.apply({a:4}, [5, 6]); // 4 5 6
// 원래는 obj를 가리켜서 1이 출력되어야 하지만 apply 메서드를 통해 {a:4}를 가르키고 this.a가 1이 아닌 4가 출력된다.

Reference

  • 코어자바스크립트(정재남 지음)
profile
느리지만 확실하게

0개의 댓글