Books
코어 자바스크립트
3. this

03 this

대부분의 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의미한다. 그러나 자바스크립트에서 this는 어디서든 사용할 수 있고 상황에 따라 달라진다.

3.1 상황에 따라 달라지는 this

this는 실행 컨텍스트가 생설될 때 결정되는데 실행 컨텍스트는 함수를 호출할 때 생성되므로, this는 함수를 호출할 때 결정된다고 할 수 있다.

3.1.1 전역 공간에서의 this

전역 공간에서 this는 전역 객체를 가리킨다. 이 전역 객체는 런타임 환경에 따라 다른데 브라우저는 window, nodejs는 global

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

자바스크립트의 모든 변수는 실은 특정 객체의 프로퍼티로 동작한다. 특정 객체란 실행 컨텍스트의 LE이다. 그럼 var로 변수를 선언하는 대신 window의 프로퍼티로 직접 할당해도 똑같이 동작할까? 대부분은 그렇다

그런데 삭제 명령은 다르다.

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

처음부터 전역객체의 프로퍼티로 할당한 경우는 삭제가 되는데 전역변수로 선언한 경우에는 삭제가 되지 않는다. 이는 의도치 않게 삭제되는 것을 막고자 엔진에서 전역 변수를 선언하면 전역 객체의 프로퍼티로 할당하면서 해당 프로퍼티의 속성(변경 및 삭제)을 false로 정의한다.

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

함수 vs 메서드

이 둘을 구분하는 유일한 차이는 독립성이다. 함수는 그 자체로 독립적인 기능을 하고, 메서드는 자신을 호출한 대상 객체에 관한 동작을 수행한다.

var func = function (x) {
  console.log(this, x);
}
func(1);    // Window , 1
 
var obj = {
  method: func
};
obj.method(2);    // { method: f } 2

함수를 변수에 담아 호출한 경우와 객체의 프로퍼티에 할당해서 호출한 경우 this가 달라진다. 그럼 어떻게 구분할까? 함수 앞에 점(.)으로 구분한다. (진짜로) 그 함수 이름 앞에 객체가 명시돼 있는 경우 메서드이고 그렇지 않으면 모두 함수로 호출한거다.

메서드 내부에서의 this

this는 호출 주체에 대한 정보가 담긴다. 어떤 함수를 메서드로서 호출하면 호출 주체는 함수명 앞의 객체이다.

var obj = {
  methodA: function () { console.log(this); },
  inner: {
    methodB: function () { console.log(this); },
  }
};
obj.methodA();  // { methodA: f, inner: { ... } }
obj.inner.methodB();  // { methodB: f }

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

함수 내부에서의 this

함수로서 호출할 경우 this가 지정되지 않는다. 지정되지 않은 경우 this는 전역 객체를 바라본다고 했다. 그래서 함수에서의 this는 전역 객체를 가리킨다. 더글라스 크락포드는 이는 설계상 오류라고 지적한다.

메서드 내부함수에서의 this

어떤 함수를 메서드로서 호출했는지 함수로서 호출했는지 파악하면 this의 값을 맞출 수 있다.

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

같은 함수임에도 innerFunc()obj2.innerMethod()로 호출할 때 바인딩 되는 this가 다르다. this 바인딩에는 함수를 어떻게 호출했는지 (함수인지 메서드인지)만 중요하다.

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

호출 주체가 없을 때 자동으로 전역 객체를 바인딩하지 않고 호출 당시 주변 환경의 this를 그대로 상속받을 수는 없나? ES5까지는 내부 함수에 this를 상속할 방법이 없고 변수를 활용해 우회할 수 있다.

var obj = {
  outer: function() {
    console.log(this);  // obj1
    var innerFunc1 = function() {
      console.log(this);
    }
    innerFunc();  // 전역 객체
 
    var self = this;
    var innerFunc2 = function() {
      console.log(self);
    }
    innerFunc2(); // obj1
  }
}
obj.outer();

this를 바인딩하지 않는 함수

ES6에서 this를 바인딩하지 않는 화살표 함수를 도입했다. 화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지고 상위 스코프의 this를 그대로 활용한다.

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

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

함수 A의 제어권을 다른 함수 B에게 넘겨주는 경우 함수 A를 콜백 함수라고 한다. 함수 A는 함수 B의 내부 로직에 따라 실행되고 this 역시 함수 B 내부로직에서 정한 규칙에 따라 결정된다.

setTimeout(function () { console.log(this); }, 300);  // (1)
 
[1,2,3,4,5].forEach(function (x) {
  console.log(this, x); // (2)
})
 
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a')
  .addEventListener('click', function (e) {
    console.log(this, e);   // (3)
  });
  • (1), (2) - 내부에서 this를 지정하지 않는다. 전역객체
  • (3) addEventListener 메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의되어 있다. 그래서 점 앞 부분(document.body.querySelector('#a'))이 this가 된다.

3.1.5 생성자 함수 내부에서의 this

생성자 함수는 어떤 공통된 성질을 지니는 객체들을 생성하는 데 사용하는 함수이다. 자바스크립트는 함수에 생성자로서의 역할을 함께 부여했다. new와 함께 함수를 호출하면 해당 함수가 생성자로서 동작한다. 그리고 생성자 함수로 호출된 경우 내부에서 this는 인스턴스 자신이 된다.

생성자 함수를 호출하면 생성자의 prototype 프로퍼티를 참조하는 __proto__라는 프로퍼티가 있는 객체를 만들고 공통 속성과 개성을 해당 객체(this)에 부여한다.

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

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

3.2.1 call 메서드

Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

call 메서드는 호출 주체인 함수를 즉시 실행하도록 하는 명령이다. 이때 call 메서드의 첫 번째 인자를 this로 바인딩한다.

var func = function (a, b, c) {
  console.log(this, a, b, c);
}
func(1,2,3); // window, 1 2 3
func.call({ x: 1}, 4, 5, 6);  // { x: 1} 4 5 6

3.2.2 apply 메서드

Function.prototype.apply(thisArg[, argsArray])

applycall과 동일한데 두번째 인자를 배열로 호출할 함수의 매개변수로 지정한다.

3.2.3 call/apply 메서드의 활용

유사배열객체에 배열 메서드를 적용

var obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};
 
Array.prototype.push.call(obj, 'd');
console.log(obj);
 
var arr = Array.prototype.slice.call(obj);

객체에는 배열 메서드를 직접 적용할 수 없지만, 키가 0또는 양의 정수인 프로퍼티가 존재하고 length 프로퍼티의 값이 0 또는 양의정수인 객체를 call또는 apply 메서드를 이용해 배열 메서드를 차용할 수 있다.

ES6에서는 유사배열객체 또는 순회 가능한 모든 종류의 데이터 타입을 배열로 전환하는 Array.from 메서드를 새로 도입했다.

생성자 내부에서 다른 생성자를 호출

function Person(name, gender) {
  this.name = name;
  this.gender = gender;
}
function Student(name, gender, school) {
  Person.call(this, name, gender);
  this.school = school;
}
function Employee(name, gender, company) {
  Person.apply(this, [name, gender]);
  this.company = company;
}
var by = new Student('보영', 'female', '단국대');
var jn = new Employee('재난', 'male', '구골');

3.2.4 bind 메서드

Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])

bind 메서드는 ES5에서 추가된 기능, call과 비슷하지만 즉시 호출하지는 않고 this와 인수들을 바탕으로 새로운 함수를 반환한다.

name 프로퍼티

bind 메서드로 새로 만든 함수는 name 프로퍼티에 bound 접두어가 붙는다.

상위 컨텍스트의 this를 내부함수나 콜백 함수에 전달하기

var obj = {
  outer: function () {
    console.log(this);
    var innerFunc = function () {
      console.oog(this);
    };
    innerFunc.call(this);
  }
};
obj.outer();
 
var obj1 = {
  outer: function() {
    console.log(this);
    var innerFunc = function() {
      console.log(this);
    }.bind(this);
    innerFunc();
  }
}
obj1.outer();

3.2.5 화살표 함수의 예외사항

ES6에 도입된 화살표 함수는 실행 컨텍스트 생성 시 this를 바인딩하는 과정이 제외되었다. 이 함수 내부에는 this가 없고, 접근 시 스코프 체인 상 가장 가까운 this에 접근한다.

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

3.2.6 별도의 인자로 this를 받는 경우 (콜백 함수 내에서의 this)

var report = {
  sum: 0,
  count: 0,
  add: function() {
    var args = Array.prototype.slice.call(arguments); // args = [60, 85, 95]
    args.forEach(function (entry) {
      this.sum += entry;
      ++this.count;
    }, this);
  },
  average: function() {
    return this.sum / this.count;
  }
};
report.add(60, 85, 95);
console.log(report.sum, report.count, report.average());

3.3 정리

  • 전역공간에서의 this는 전역객체를 참조한다.
  • 어떤 함수를 메서드로서 호출한 경우 this는 메서드 호출 주체를 참조한다.
  • 어떤 함수를 함수로서 호출한 경우 this는 전역객체를 참조한다. 메서드의 내부함수도 동일하다.
  • 콜백 함수 내부에서의 this는 해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우 전역객체
  • 생성자 함수에서의 this는 생설될 인스턴스를 참조한다.

명시적인 this 바인딩

  • call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출한다.
  • bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만든다.
  • 요소를 순회하면서 콜백 함수를 반복 호출하는 일부 메서드는 별도 인자로 this를 받기도 한다.