3.this

  • 대부분 객체지향 언어에서 this는 클래스로 생성한 인스턴스 객체를 의마한다.
  • 그러나 자바스크립트에서의 this는 어디에서든 사용 가능 하다.
  • 함수와 객체(메서드)의 구분이 느슨한 자바스크립트 에서는 this는 실질적으로 이둘을 구분하는 유일한 기능이다.

 

3.1 상황에 따라 달라지는 this

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

- 전역공간에서 this

  • 전역공간에서 this는 전역 객체를 가르킨다.
//browser 환경에서 전역객체
console.log(this)    // {alert: f() ........}
console.log(window)  // {alert: f() ........}
console.log(this === window) //true
//nodejs 환경에서 전역객체
console.log(this)    // {process: {title: 'node' ........}
console.log(global)  // {process: {title: 'node' ........}
console.log(this === global) //true

 

- 다음 소스코드를 살펴보자.

var a = 1;
console.log(a);         // 1
console.log(window.a);  // 1
console.log(this.a);    // 1
  • 위에서 우리가 확인할 수 있는 사실은 자바스크립트의 모든 변수는 실은 특정객체의 프로퍼티 라는점이다.
  • 특정 객체란 실행 컨텍스트의 LexicalEnvironment 이다.
  • 전역컨텍스트의 L.E는 전역객체를 그대로 참조한다.
  • '전역변수를 선언하면 자동으로 전역객체의 프로퍼티로도 할당한다' 라는 말은 틀린말이다.
  • '전역변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다'가 올바른 표현이다.

 

- 그럼 전역공간에서는 var로 indentifer를 선언하는 대신, window의 프로퍼티에 직접 할당해도 상관없을까?

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

window.a = 3;
b = 4;
console.log(a, window.a, this.a); // 3 3 3
console.log(b, window.b, this.b); // 4 4 4
  • 대부분의 경우에는 이말이 맞다
  • 그러나 전역변수의 선언과 전역객체의 프로퍼티 할당사이에 전혀다른 경우가 있다. '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); // error !
  • 전역객체의 프로퍼티로 할당한 경우는 삭제가 되는 반면
  • 전역변수로 선언한 경우에는 삭제가 되지않는다.
  • var로 선언한 전역변수와 전역객체의 프로퍼티는 호이스팅 여부 및 configurable 여부에서 차이를 보인다.

 

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

 - method vs function

  • 함수와 메서드는 근본적으로 코드뭉치이다.
  • 유일한 차이는 독립성에 있다.
  • 함수는 그 자체로 독립적인 기능을 수행하는 반면, 메서드는 자신을 호출한 대상객체에 관한 동작을 수행한다.
  • JS에서는 상황별로 this 키워드에 다른값을 부여하게 함으로써 이를 구현했다.

 

  • JavaScript를 처음 접하는 사람들은 흔히 메서드를 '객체의 프로퍼티로 할당된 함수'로 이해하지만 이는 반은맞고 반은 틀리다.
  • 객체의 메서드로서 호출할 경우에만 메서드로 동작하고, 그렇지않으면 함수로 동작한다.
var func = function (x) {
  console.log(this, x);
}

func(1);  // Window {.....} 1

var obj = {
  method: func
}

obj.method(2); // {methog: f} 2
  • 위의 예제에서 볼 수 있듯이, 원래의 익명함수는 그대로인데 이를 변수에담아 호출한 경우와 obj객체의 프로퍼티에 할당해서 호출한 경우에 this가 달라지는 것이다.

 

- 그렇다면 '함수로서 호철'과 '메서드로서의 호출'을 어떻게 구분할까??

  • 앞에 점(.)이 있는지 여부만으로 간단하게 구분할 수 있다. (대괄호 표기법 포함)

 

var obj = {
  method: function (x) { console.log(this, x); }
}
obj.method(1);    // { method: f } 1
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}
obj['methodA']();            // { methodA: f, inner { .... } } { === obj}

obj.inner.methodB();         // { methodB: f } { === obj.inner}
obj.inner['methodB']();      // { methodB: f } { === obj.inner}
obj['inner'].methodB();      // { methodB: f } { === obj.inner}
obj['inner']['methodB']();   // { methodB: f } { === obj.inner}

 

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

  • fucntion을 함수로서 호출할 경우에는 this가 지정되지 않는다.
  • this란 호출한 주체의 정보이기 때문이다. 그러므로 함수로서의 호출의 this는 window 객체이다
  • 그러나 더글라스 크락포드는 이를 명백한 설계상의 오류라고 지적했다.

 

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

  • 앞서 말한 '설계상의 오류' 때문에 실제 동작과 우리의 예측과는 다를 때가있다.
var obj1 = {
  outer: function () {
    console.log(this)              // (1)
    var innerFunc = function() {
      console.log(this)           // (2) (3)
    }
    innerFunc();
    
    var obj2 = {
      innerMethod: innerFunc
    };
  
    obj2.innerMethod();
  }
};

obj1.outer();
  • 정답은 (1): obj1, (2): window, (3): obj2이다.
  • 같은함수임에도 7번째줄에 binding 되는 this와 12번째줄에 의해서 바인딩되는 this가 달라진 것이다.
  • 즉 this란 함수를 실행하는 당시 scope와는 관계없이, function 앞의 object가 가장중요한 것이다.

 

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

  • 위와같은 상황에서 우리는 this를 확실하게 구분할 수 있지만, 본래 this의 취지와는 달라졌다.
  • 호출주체가 없을때도 주변 환경의 this를 그대로 상속받는게 더 일관적이다.
  • this 역시 현재 컨텍스트에 바인딩 된 대상이 없으면 직전 컨텍스트의 this를 바라보도록 말이다.
  • 가장 대표적인 방법은 변수를 활용하는 방법이다.
var obj1 = {
  outer: function () {
    console.log(this)             // (1) { outer : f}
    var innerFunc1 = function() {
      console.log(this)           // (2) { window }
    }
    innerFunc1();
    
    
    var self = this;
    var innerFunc2 = function() {
      console.log(self);
    }
  
    innerFunc2();               // (3) { outer: f }
  }
};

obj1.outer();
  • 위처럼 변수에 할당해서 출력한다면 의도한 대로 동작한다.
  • 사람마다 self, _this, that 혹은 다른변수명을 많이쓰지만 self가 가장많이쓰인다.

 

- ES6 에서 this를 바인딩하지 않는 함수

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

obj.outer();
  • arrow function에서 실행컨텍스트를 생성할때 this binding 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있다. 
  • react에서 arrow function 사용시에 this를 bind 해주지 않아도 되는 이유도 위와같은 이유때문이다.
    (arrwo function을 쓰지않는다면 this.methodName.bind(this); 이런식으로 해주어야함. 그렇지않으면 null)

 

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

  • 함수 A의 제어권을 다른 함수(또는 메서드) B에게 넘겨주는 경우 함수 A를 콜백 함수라 한다.
  • A는 함수 B의 내부로직에 따라 실행되며, this 역시 함수 B내부로직에서 정한 규칙에 따라 값이 결정됨.
  • 기본적으로 콜백함수도 함수라 window를 참조하지만, 제어권을 받은 함수에서 콜백함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 됩니다.

 

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

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

documnet.body.innHTML += '<button id="a"> 클릭 </button>';
documnet.body.querySelector('#a').addEventListener('click', function (e) { console.log(this,(e); });
  • 위는 콜백함수 이다.
  • 1,2의 경우는 따로 this를 지정하지 않아 this는 window 객체가 된다.
  • 그러나 마지막 3번째 콜백함수에서는 제어권을 앞에 지정한엘리먼트에게 수여하기때문에 this 는 a tag가 될 것이다.

 

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

  • 객체지향 언어에서는 생성자를 class, 클래스를 통해 만든 객체를 instance라고 한다.
  • 프로그래밍 적으로 본다면 생성자는 인스턴스를 만들기위한 틀이다.
  • 어떤 함수가 생성자 함수로서 호출된 경우 내부에서 this는 만들어질 구체적인 인스턴스이다.
  • 생성자 함수 (new 명령어)를 호출하면 생성자의 prototype 프로퍼티를 참조하는 __proto__ 라는 프로퍼티가 있는 개체를 만들고, 공통 프로퍼티등을 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: '초코' ..... }
Cat{ bark: '야옹' name: '나비' ..... }

*/

 

 

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

3.2.1 Call method

  • call method는 호출 주체인 함수를 즉시 실행하도록 하는 명령이다. 
// Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

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

func(1,2,3);     // Window {...} 1 2 3
func({ x: 1 }, 3,4,5) // { x: 1} 4 5 6
  • 위와같이 call method를 사용하면 임의 객체로 this를 지정 할 수있다.
// Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

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

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

 

3.2.2 apply Method

  • call method와 거의 비슷하지만, 두번째 이후 인자의 매개변수는 array로 한번에 받는다.
// Function.prototype.call(thisArg[, argsArray])

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

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

 

3.2.3 실 사용 예시

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

  • 생성자 내부에 다른 생성자와 공통된 내용이 있을 경우 call 또는 apply를 이용해 다른 생성자를 호출하면, 간단하게 반복을 줄일 수 있다.
  • (마치 상속과 비슷한 느낌이다.)
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.call(this, name, gender);
  this.company = school;
}

var by = new Student('보영', 'female', '한국대');
var jn = new Employee('재난', 'male', '구글');

 

- 여러 인수를 묶어 하나의 배열로 전다할고 싶을때 - apply 활용

var number = [10, 20, 3, 16, 45];
var max = min = numbers[0];
numbers.forEach(function(number) {
  if(number > max) {
    max = number;
  }
  if(number < min) {
    min = number;
  }
});

console.log(max, min);          // 45 3
  • 위와 같이 배열의 가장 큰값 작은값을 구하려면 구현해야된다.
  • 그러나 Math.min, Math.min 과 apply를 사용한다면 아주 간결하게 만들수있다. 
var number = [10, 20, 3, 16, 45];
var max = Math.max.apply(null, numbers);
var min = Math.min.apply(null, numbers);
console.log(max, min);          // 45 3
  • es6에서는 sperad 연산자의 등장과 함께, 더욱 쉽게 구현 가능해졌다.
  • 그러나 실무에선 es5환경이 훨씬많으므로, 위와같은 방법을 더 많이쓴다.
var number = [10, 20, 3, 16, 45];
var max = Math.max(...numbers);
var min = Math.min(...numbers);
console.log(max, min);          // 45 3

 

3.2.4 bind 메서드

  • bind는 ES5에서 추가된 기능으로, call과 비슷하지만 즉시 호출하지않고, 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하는 메서드이다.
// Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])

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

func(1,2,3,4);     // Window {...} 1 2 3 4


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

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

 

- name property

  • bind 메서드를 적용해서 새로만든 함수는 name property에 bound 접두어가 붇는다.
  • 'bound xxx' 라면 xxx원본 함수에 bind 메소드를 적용한 것이다.
  • call 이나 apply 보다 코드를 추적하기에 더 수월하다.
var func = function(a,b,c,d) {
  console.log(this,a,b,c,d);
};

var bindFunc = func.bind({x:1}, 4, 5);
console.log(func.name);       // func
console.log(bindFunc.name);   // bound func

 

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

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

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

obj1.outer();

 

- *** callback 함수에 this context 전달

var obj1 = {
  printThis: function() {
    console.log(this);
  },
  printThisLater1: function() {
    setTimeout(this.logThis, 500);
  },
  printThisLater2: function() {
    setTimeout(this.logThis.bind(this), 500);
  },
};

obj1.printThisLater1();  // window { ... }
obj1.printThisLater2();  // obj { logThis: f, ... }

// polling 기능을 만들던중, polling이 좀 비효율적이라 
// long polling으로 기능을 만들고 싶었다.
// react component 내부에서 setTimeOut 함수를 호출 할경우, 
// state를 읽지못하는 이슈가 존재했다. function 실행하는 주체가 global 객체로 변하면서
// state를 읽을수 없는 문제였다.
// 그러나 this context에 대한 개념을 이해한뒤에 bind로 this 값을 전달하면서 
// long polling을 위해 state값을 읽고 해결할수있었다.

 

3.2.5 Arrow Function의 예외 사항

  • ES6에 새롭게 도입된 Arrow Function은 실행 컨텍스트 생성 시 this를 바인딩 하는 과정이 제외되었다.
  • 이 내부에는 this가 아예 없으며, this에 접근하면 스코프체인상 가장 가까운 this에 접근하게 된다.
var obj = {
  outer: function () {
    console.log(this);
    var innerFunc = () => {
      console.log(this);
    }
    innerFunc();
  }
}

obj.outer();
  • 이렇게 하면 구지 변수로 this를 우회하거나 call/apply/bind를 사용할 필요가 없다. (ES6 한정)

 

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

  • 콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this를 인자로 지정할 수 있는 경우가 있다.
  • 배열메서드에 많이 포진되어 있으며 ES6의 map, set에서도 일부 존재한다.
var report = {
  sum: 0,
  count: 0,
  add: function () {
    var args = Array.prototype.slice.call(arguments);
    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()); // 240 3 80
  • 9번째 줄에서 전달해준 this가 바인딩 되면서, 콜백함수에서도 sum,count를 참조할 수 있게 된다.

- (참고) 콜백함수와 함께 thisArg를 인자로 받는 메서드들

Array.prototype.forEach(callback[, thisArg])
Array.prototype.map(callback[, thisArg])
Array.prototype.filter(callback[, thisArg])
Array.prototype.some(callback[, thisArg])
Array.prototype.every(callback[, thisArg])
Array.prototype.find(callback[, thisArg])
Array.prototype.findIndex(callback[, thisArg])
Array.prototype.flatMap(callback[, thisArg])
Array.prototype.from(arryLike[, callback[, thisArg])
Map.prototype.forEach(callback[, thisArg])
Set.prototype.forEach(callback[, thisArg])

 

3.3 정리

  • 다음 규칙은 명시적 this 바인딩이 없는 한 늘 성립합니다. this를 예측하는 연습을 해봅시다.
    1. 전역공간에서 this는 전역객체 (window or global)을 참조한다
    2. 어떤 함수에서 메소드로 this를 호출한경우 호출 주체 (메서드앞 객체) 를 참조한다.
    3. 어떤 함수를 함수로서 호출할경우 this는 전역객체를 참조한다.
    4. 콜백함수 내부에서의 this는 해당 콜백함수의 제어권을 넘겨받은 함수가 정의 한 바에 따르며, 정의하지 않으면 전역객체를 참조
    5. 생성자 함수에서 this는 생성 될 인스턴스를 참조
  • 다음은 명시적 this 바인딩이다. 위 규칙의 부합하지 않을 경우 다음 내용을 바탕으로 this를 예측가능하다.
    1. call, apply 메서드는 this를 명시적으로 지정하면서 함수 또는 메서드를 호출.
    2. bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해서 새로운 함수를 만듬
    3. 요소를 순회하면서 콜백 함수를 반복호출하는 일부 메서드는 this를 인자로 넘겨 줄 수 있다.

+ Recent posts