Javascript

코어 자바스크립트 - this

이 챕터를 읽고 난 후 다음의 질문에 답할 수 있어야 한다.

  • this란 무엇인가?
  • this가 참조하는 대상은 어떻게 결정되는가?
  • 컨텍스트에 따라 this가 참조하는 객체는 어떻게 달라지는가?
  • this를 바인딩하려면 어떤 메서드를 써야 하는가?
  • call/apply 메서드를 어떻게 활용할 수 있는가?
  • 화살표 함수에서 this는 무엇을 참조하는가?

this

입문자의 입장에서 this를 활용하게 되는 경우는 많지 않았다.

낯설기 때문에 this에 대한 개념을 공부할 때면 항상 느껴지는 불편함이 있다.

하지만 여기저기서 this가 자주 언급되는 것으로 봐서 확실히 짚고 넘어갈 필요가 있어 보인다.

이 책에서도 this에 대해 상세히 다루고 있다.

 

this는 특정 객체를 가리키는 일종의 속성이라고 할 수 있다.

그리고 this가 참조하는 대상은 문맥에 따라 달라진다.

 

this의 참조 대상

전역 공간에서 this는 전역 객체를 참조한다. 함수 내부에서 호출되는 this는 왠지 모르게 함수를 참조할 것 같지만,

전역 공간에서와 마찬가지로 전역 객체를 참조한다.

 

하지만 같은 함수라도 특정 객체의 메서드로서 호출하게 되면 this는 호출의 주체가 되는 객체를 참조한다.

this가 참조하는 객체는 함수가 호출될 때 결정되며, 어떻게 호출하느냐가 그 핵심이다.

 

무엇에 따라 참조 대상이 달라지는가?

전역 공간에서 this는 전역 객체를 참조한다.

브라우저라면 window객체, node에서라면 global 객체가 될 것이다.

함수 실행을 통해 this를 호출해도 여전히 전역 객체를 참조한다.

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

func(1); // <ref *1> Object [global] {...} 1

 

메서드로 호출하면 호출의 주체가 되는 객체를 참조한다.

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

var obj = {
  a: func
};

obj.a(2); // { a: [Function: func] } 2
obj['a'](2); // { a: [Function: func] } 2

 

메서드 내부에 있는 함수라면?

var obj1 = {
  outer: function() {
    console.log(this); 
    // (1) { outer: [Function: outer] }
    var innerFunc =function() {
      console.log(this); 
      // (2) <ref *1> Object [global] {...}
      // (3) { innerMethod: [Function: innerFunc] }
    }
    innerFunc();

    var obj2 = {
      innerMethod: innerFunc,
    };
    obj2.innerMethod();
  } 
};

obj1.outer();

순서에 따라 설명하자면,

  1. obj1.outer()에 의해 메서드로서 호출되었으니 호출의 주체(obj1)를 참조
  2. innerFunc()에 의해 함수로서 호출되었으니 전역 객체를 참조
  3. obj2.innerMethod()에 의해 메서드로서 호출되었으니 호출의 주체(obj2)를 참조

 

콜백 함수에서는 콜백 함수의 제어권을 갖고 있는 함수의 로직에 따라 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);
})
// 엘리먼트

이벤트리스너의 경우 점 표기법 앞에 지정된 엘리먼트를 참조하게 된다.

만약 제어권을 갖고 있는 함수가 특정 객체를 바인딩한 상태라면,

콜백 함수에서의 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);
})
// 엘리먼트

this는 곧 생성될 인스턴스 자체를 참조하게 된다. lee와 kim이 각각 참조된다.

 

this를 바인딩하기 위해서는

call()

var func = function(a, b, c) {
  console.log(this, a, b, c);
};

func(1, 2, 3); // <ref *1> Object [global] {...} 1 2 3
func.call({x:1}, 4, 5, 6); // { x: 1 } 4 5 6

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()

var func = function(a, b, c) {
  console.log(this, a, b, c);
};

func(1, 2, 3); // <ref *1> Object [global] {...} 1 2 3
func.apply({x:1}, [4, 5, 6]); // { x: 1 } 4 5 6

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

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

call과 apply는 매개변수를 받는 방식이 다르다는 점을 제외하고 완전히 동일한 역할을 수행한다.

this를 명시적으로 지정하면서 함수나 메서드를 호출한다.

 

bind()

var func = function (a, b, c, d) {
  console.log(this, a, b, c, d); // <ref *1> Object [global] {...} 1 2 3 4
};

func(1, 2, 3, 4);

var bindFunc1 = func.bind({x:1});
bindFunc1(5, 6, 7, 8); // { x: 1 } 5 6 7 8

var bindFunc2 = func.bind({x:1}, 4, 5);
bindFunc2(6, 7); // { x: 1 } 4 5 6 7
bindFunc2(1, 2); // { x: 1 } 4 5 1 2

bind 메서드는 this와 함수에 넘길 인자를 일부 지정하면서 새로운 함수를 만든다.

bind를 통해 만든 함수의 name 속성에는 접두어가 붙는 특징이 있다.

var a = function(){}

console.log(a.name); // a

var b = a.bind();
var c = a.bind();

console.log(b.name); // bound a
console.log(c.name); // bound a

 

call, apply 메서드의 활용법

유사배열 객체를 배열로 변환시켜 배열 메서드를 적용할 수 있다.

var obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};

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

var arr = Array.prototype.slice.call(obj);
console.log(arr); // [ 'a', 'b', 'c', 'd' ]

function a() {
  var argv = Array.prototype.slice.call(arguments);
  argv.forEach(function(arg){
    console.log(arg);
  });
}

a(1, 2, 3);

// 1
// 2
// 3

 

 

ES5에서 도입된 Array.from()를 사용하면 더 간단하다.

var obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};

var arr = Array.from(obj);

console.log(arr); // [ 'a', 'b', 'c' ]

 

배열 내 최대 최소 구하기

var numbers = [10, 20, 40, 30, 50];

var max = min = numbers[0];

numbers.forEach((number)=> {
  if (number > max){
    max = number;
  } 
  
  if (number < min ) {
    min = number;
  }
});

 

apply를 쓴다면 더 간단하다.

var numbers = [10, 20, 40, 30, 50];

var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);

console.log(max, min);

 

스프레드 연산자를 쓴다면 좀 더 편리하다.

var numbers = [10, 20, 40, 30, 50];

var max = Math.max(...numbers);
var min = Math.min(...numbers);

console.log(max, min);

 

화살표 함수

상위 컨텍스트에서 this를 가져다 쓰기 위해

call 메서드를 쓴다면,

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

 

bind를 메서드를 쓴다면,

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

 

화살표 함수로도 가능하다.

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

화살표 함수는 this를 바인딩하는 과정을 생략하기 때문에 함수 내부에 this가 존재하지 않는다.

따라서 스코프체인상 가장 가까운 this에 접근하게 된다.