Execution Context [6] - This와 This Binding

Marullo·2021년 4월 8일
1
post-thumbnail

1. this란?

this는 Javascript의 키워드다.
우리는 this를 이용해서 내/외부 Scope를 이외에 새로운 Scope를 가질 수 있다. (정확히는 this는 객체를 참조한다.)
즉, 함수에게 내/외부 Scope 이외에 "변수를 얻을 수 있는 새로운 원천"을 this를 통해 제공해 줄 수 있다.는 것이다.

예를 들면, Person.getAge()의 경우
getAge() 함수 내부에서 this를 사용하여 Person의 Scope(변수나 함수)를 사용할 수 있다.

이것이 가능한 이유는 실행 컨텍스트(Execution Context)의 this binding component 덕분이다.






2. This Binding Component

this 키워드를 사용하여 함수를 호출한 오브젝트에 접근할 수 있도록, 함수의 실행 컨텍스트안에서 this를 참조하고 있어야 한다. EC에서 오브젝트를 참조하기 위한 공간이 바로 TBC(This Binding Component)다.

우리는 외부 렉시컬 환경 참조를 통해 함수 바로 바깥에 있는 것들에 접근할 수 있으며, 선언적 환경 레코드를 통해 함수 내부에 있는 것들에 접근할 수 있다. 함수 내외부 뿐만아니라 TBC 덕분에, this를 통해 다른 객체에도 접근이 가능하다.

this는 동적으로 결정된다.

this로 참조할 객체는 호출 시점에 결정된다. 호출 시 함수 앞에 적힌 Object가 this로 참조할 객체가 된다.
뿐만 아니라, call(), apply(), bind() 등의 메소드를 통해, 호출 때 마다 this로 참조할 객체를 변경할 수도 있다.
즉 this는 동적으로 결정된다. 이것이 가능한 이유는 TBC만큼은 동적으로 바인딩되기 때문이다.






3. More about This

obj.func() 형태로 함수(메소드)를 호출하면, 함수 func에서 this로 오브젝트 obj를 참조할 수 있다.



3.1 this와 Global Object

  • 글로벌 오브젝트에서 this는 글로벌 오브젝트 스스로를 참조한다.
    • 글로벌 함수를 호출할 땐, 함수 앞에다가 글로벌 오브젝트를 써주지 않는데, 그건 묵시적으로 글로벌 오브젝트라고 간주하는 것이다.
  • window Object는 JS에서 만든 것이 아니라, 글로벌 오브젝트의 스코프도 아니다. 그러나 window Object와 글로벌 오브젝트를 같은 선상에 두고 사용하곤 한다.
    • 여기에는 Host Object개념이 적용된다.
function test(){ return this }
console.log(this === window) //=> true
// 전역 함수의 this(Global Object)와 window가 동일
let value = 100;        // 전역변수
console.log(this.value) //=> 100
// 글로벌 Scope에서 this로 자기 자신을 참조
// this가 글로벌 오브젝트를 참조하므로, this.value 형태로 글로벌 변수 사용이 가능하다.
var value = 100;
console.log(window.value) //=> 100
// window가 글로벌 오브젝트를 참조하므로, `window.value`형태로 글로벌 변수 사용이 가능하다.
this.value = 100;
console.log(window.value) //=> 100
// 현재 상태에서 this는 **Global Object**를 참조하므로 value는 Global Object에 설정된다.
// window가 **Global Object**를 참조하므로 value를 사용할 수 있다.

Host Object

window 오브젝트처럼, 다른 오브젝트를 마치 내것처럼 사용하는 개념을 Host Object라고 한다.
"DOM 오브젝트" 또한 Host Object다.
window와 전역 컨텍스트에서 참조하는 this가 똑같기 때문에, 종종 Global Object가 Window라고 하곤 한다. 그러나 실체는 다르다는 것을 알고 있어야 한다.



3.2 this와 window Object

this가 window를 참조하는 경우

window.onload = function(){
    console.log(this === window) //=> true
}
  • true가 출력된 것은 값과 타입이 같다는 것을 의미한다.
  • onload()가 비록 이벤트를 처리하는 핸들러 함수라도, function Object이다.
    따라서 onload() 호출문 앞에 적힌 window가 this로 참조된다.
  • onload 이벤트가 발생하면 실행 컨텍스트를 만들게 되고
    onload앞에 작성된 window를 this 바인딩 컴포넌트에 바인딩하게 된다.

this로 지역변수 사용하는 경우

window.onload = function(){
    var value = 100
    console.log(this.value) //=> undefined
}
// 변수 `value`는 핸들러 함수의 지역변수다.
// this는 window object를 참조하게 되므로, this.value로 지역변수에 접근할 수 없다.


3.3 this가 참조하는 오브젝트

var book = {
  point : 100,
  member : {
    point : 200,
    get(){
      console.log(this.point)             //200
      console.log(this === book.member)   
      //=> true 
      //this는 호출한 오브젝트 그 자체를 참조한다.
    }
  }
}
book.member.get()
  • get의 실행 컨텍스트 내부의 this바인딩 컴포넌트에는 member를 참조할 수 있도록 설정해놨다.
  • this는 호출한 오브젝트 그 자체를 참조한다.
  • book.member.get()에서 this로 참조하는 것은 member라는 오브젝트다. book은 그저 member를 찾아가는 경로에 불과하다.

this는 함수를 호출할 때 앞의 오브젝트가 뭔지가 중요하다.

var point = 999;

var obj = {
  point: 111,
  get() {
    var point = 555;
    console.log(this === window);
    console.log(this === obj);
    console.log(this.point);
  },
};

var func = obj.get;
func();       //=> true; false; 999;
obj.get();    //=> false; true; 111;
  • func()에 대한 결과
    • func()는 obj의 get과 같은 function Object를 받았다. 그러나 호출할 때 오브젝트를 명시하지 않았다.
    • 따라서 func의 실행 컨텍스트의 this 바인딩 컴포넌트는 Global Object를 참조하게 되며 이때, this는 window다.
    • point도 window에 있는 point를 가져온다.
  • obj.get() 에 대한 결과
    • obj.get() 호출할 때 오브젝트는 obj다.
    • 따라서 get()의 실행 컨텍스트의 this 바인딩 컴포넌트는 obj를 참조하게 되며 이때, this는 obj다.
    • point도 obj에 있는 point를 가져온다.

this와 strict 모드

우리는 오브젝트.함수이름()형태로 함수를 호출하지만 글로벌 오브젝트의 경우, 오브젝트의 이름이 없으므로 함수 이름만 작성하여 호출한다.

  • 하지만 strict 모드에서는 window.book()처럼 book()앞에 window를 명시해줘야한다.
  • strict 모드에서 전역함수 호출시에 window를 써주지 않는다면, 실행 컨텍스트의 this 바인딩 컴포넌트에 undefined가 설정되므로, this로 window를 참조할 수 없다.


3.4 this와 Instance(prototype)

  • 인스턴스의 목적은, 인스턴스마다 고유의 값을 유지하는데 있다.
  • 인스턴스에서 this의 목적은 this로 인스턴스를 참조하여, 인스턴스의 프로퍼티에 접근하기 위함이다.
    • this를 사용하지 않으면, 메소드에 필요한 값들을 인자로 넘겨주거나, 전역 변수를 사용해야된다. (이 방식은 정적환경의 이점을 버리는 것과 마찬가지다.)
  • __proto__ 접근의 목적
    • 생성자 함수를 통해 반환된 객체는 prototype Object의 메소드를 사용할 수 있다.
      인스턴스들 모두 메소드를 공유할 수 있다.
    • this를 사용하면, 객체마다 독립된 변수를 둘 수도 있다.

var book = {};
book.Point = function (point) { this.point = point; };
book.Point.prototype.getPoint = function () { console.log(this.point); };

var obj = new book.Point(100);
obj.getPoint(); //=> 100

var obj2 = new book.Point(200);
obj2.getPoint(); //=> 200

//메소드 변경
book.Point.prototype.getPoint = function () { console.log("changed"); };
obj.getPoint(); //=> changed
obj2.getPoint(); //=> changed
  • book.Point = function (point){}로 book이라는 객체에 Point라는 생성자 함수를 선언한다. (생성자 함수는 첫글자가 대문자라는 관례가 있다.)
    생성자 함수가 있으므로 new키워드로 book.Point라는 인스턴스를 생성할 수 있다.
    생성자 함수는 this.point로 반환할 객체에 point라는 변수에 인자로 들어온 point 값을 할당한 후, 객체를 반환한다. 이 point는 인스턴스마다 독립적인 변수가 된다.

  • book.Point.prototype.getPointPoint의 Prototype Object에 메소드를 추가한다. (클래스 원형에 메소드가 추가되었다고 생각하면 된다.)
    이를 통해 book.Point 인스턴스이 getPoint라는 함수를 호출하게 되면 IR(식별자 해결)시 Scope Chain을 타고 올라가 Prototyp Object의 메소드를 사용하게 된다.


즉 this로 인스턴스마다 독립적인 변수를 가질 수 있으며, prototype을 이용하여 메소드를 공유할 수 있다.






4. This 관련 메소드 (call, apply, bind)

4.1 this와 call()

this로 참조할 오브젝트를 변경할 수 있는 것이 call의 특징이다.

call 메소드 사용

  • call 메소드는 getTotal.call()와 같이 함수.call 형태로 사용한다.
  • getTotal.call(this, 10, 20)과 같이 파라미터를 넘길 수 있다.
    • 첫번째 인자는 파라미터로 넘어가지 않고, 두번째 인자 10부터 파라미터로 넘어가게 된다.
    • 첫번째 인자는 호출된 함수에서 this로 참조할 오브젝트를 명시한다. (모든 오브젝트가 가능하다.)
  • 호출하면서 첫번째 인자를, 실행 컨텍스트의 this바인딩 컴포넌트로 바인딩하겠다는 목적으로 사용한다.

example 1.

var value = 100;
function get(param) {
  console.log(this);
  return param + this.value;
}
var result = get.call(this, 20);
console.log(result);
  • get함수의 console.log(this)는 window를 반환하게 된다.
    • call 메소드의 인자로 this를 넘겼는데, 이때의 this는 Global Object인 window다.
    • 따라서 window의 value 값인 100과 param으로 넘긴 20이 더해진 120이 반환된다.

example 2.

Object를 바인딩하게 만들면 아래와 같다.

var obj = { value: 80 };

var result = get.call(obj, 20);
console.log(result); //=> 100
  • 위와 같이 call 메소드에 this로 참조할 오브젝트를 obj로 바꾸면, get함수는 obj를 this로 참조하게 된다. 따라서 100이 반환된다.

this로 참조할 오브젝트를 변경할 수 있는 것이 call의 특징이다.


example 3.

primitive 타입을 바인딩하는 call

function get() {
  return this.valueOf();
}
var result = get.call(123);
console.log(result);
  • this가 object를 참조해야하므로, 숫자를 작성하면 에러가 발생할 것 같지만, 그렇지 않다.
  1. 값(123)의 타입에 해당하는 Number 인스턴스를 생성하고, 123을 primitive 값으로 설정한 후에 넘기게 된다.
  2. 엔진은 this바인딩 컴포넌트에 Number 객체의 인스턴스를 바인딩한다.
  3. 함수에서는 this.valueOf로 primitive 값을 꺼낼 수 있다.


4.2 this와 apply()

apply 사용

  • getTotal.apply(this,[10,20])과 같이 사용법은 call메소드와 다르지 않다.
    다만 파라미터가 "배열"로 들어가야 된다.
  • apply는 파라미터 수가 유동적일 때 사용하면 된다. call메소드는 파리미터 수가 고정일 때 사용하면 된다.
    • 웹페이지에서 유저가 선택한 항목에 대한 처리를 하는 메소드가 있을 때, 어떤 유저는 세개를 선택할 수도 있고 어떤 유저는 다섯개를 선택할 수도 있다. 이런 처리를 하는 함수의 this바인딩을 변경하고 싶으면 apply를 쓰면 된다.

apply와 arguments 프로퍼티 사용

var obj = { 0: 10, 1: 20, 2: 30 };
var data = [4, 5, 6];

function get() {
  for (i = 0; i < arguments.length; i++) {
    console.log(arguments[i], this[i]);
  }
}
get.apply(obj, data);
  • get 메소드는 파라미터 수가 유동적이므로 따로 파라미터를 작성하지 않고, argument 프로퍼티를 이용해서 받는다.
    arguments 객체는 모든 함수 내에서 이용가능한 지역변수다.
    arguments는 array-like이지 array가 아니다. 따라서 Array의 prototype에 있는 메소드를 사용할 수 없다.


apply와 call을 쓰는 이유는, 데이터 중심적으로 처리하기 위함이다.

  • 데이터를 바꿔가면서 함수를 처리하겠다. 근데 베이스가 되는 데이터는 this로 제공하고, 가변적인 값은 인자로 넘겨줄 것인데, 그걸 call과 apply로 하면 편하다.
  • 여기에 인스턴스와 프로토타입의 개념까지 추가하면 더 넓은 범위, 더 다양한 데이터 처리가 용이해진다.


4.3 this와 callback

  • ES5의 map(), forEach() 메소드 처럼 콜백함수가 있는 메소드는 두 번째 파라미터에 this로 참조할 오브젝트를 작성할 수 있다.
    • ES5에는 콜백함수를 가지는 메소드가 7개 존재한다.
function get(data, obj) {
  return data.map(function (elem, idx, data) {
    return elem + this.base;
  }, obj); //this 바인딩
}

let result = get([5, 6, 7], {base:100} );
console.log(result); //[105,106,107]

result = get([5,6,7], {base : 10})
console.log(result); //[15,16,17]

this는 연산을 하는 메소드에서 베이스가 되는 값을 동적으로 바꿔주는데 용이하다.
1을 더하는 메소드였지만, 코드 실행 과정 중에, 100을 더하는 메소드로 바꾸고 싶다면, this 바인딩을 통해 바꿔줄 수도 있다.

  • Callback 함수는 독립적인 기능을 수행하고 있으며, 그것의 베이스가 되는 값을 this 바인딩을 통해 설정해준다.


4.4 this와 bind()

  • bind 메소드는 묶는 것이다.
    • this로 참조할 오브젝트와 파라미터 값을 묶는다. this와 파라미터, 그리고 함수를 묶어서 새로운 function Object를 반환해준다.
    • call, apply 뿐 아니라, 자동적인 this 바인딩과 차이는 무엇인가
      call과 apply는 매번 this를 binding해줘야 한다. 반면 bind는 한 번 바인딩한 것을 재활용할 수 있다.
      매개변수와 this가 확정된 Function Object를 반환하기 때문에, 매개변수를 안 넣어주고 호출만해줘도 된다.
  • bind 메소드는 두 번에 나눠 처리한다.
    1. function Object 생성과 초기화
    2. 생성한 function Object를 함수로 호출
      일반적으로 함수를 호출하면 바로 실행하지만(물론 실행 컨텍스트 만드는 건 당연), bind는 위와 같이 두 단계로 나눠서 처리한다. 각각의 단계에서 바인딩이 발생한다. 우선 묶기 위해서 단계를 나눴다고 생각하자

bind 메소드 사용

  • bind의 첫 번째 파라미터는, 함수에서 this로 참조할 오브젝트다.
  • bind의 두 번째 파라미터는, 호출된 함수에 전달할 값이다.

function 오브젝트 생성과 호출

var book = {
  point: 123,
  get() {
    return this.point;
  },
};

var obj = book.get.bind(book);
console.log(typeof obj); //새로운 function 반환

var result = obj();
console.log(result);
  • var obj = book.get.bind(book)

    • book.get 함수를 호출하는데, bind메소드를 사용했다.
    1. book.get함수를 호출하지 않고, 엔진은 새로운 function Object를 생성한다.
    2. bind를 호출한 (여기서는 get)function Object의 내부 프로퍼티 [[BountTargetFunction]]에 "새로 생성한 function Object"를 설정한다.
    3. 새로 생성한 function Object의 내부 프로퍼티 [[BoundThis]]에 bind 메소드의 첫번째 파라미터(book)를 설정한다.
      • get() 함수에서 this로 참조할 오브젝트를 설정하는 것이다.
      • get 앞에 작성된 오브젝트를 this로 참조하지 않는다. bind 의 첫 번째 파라미터가 없는 경우 this는 undefined이 된다.
      • 지금까지의 단계는 모두, 생성된 function Object가 나중에 호출되니까, 현재 상황을 미리 저장하는 과정이라 생각하면 된다.
    4. 새롭게 생성한 function Object를 변수 obj에 할당한다.
      • 즉, this를 커스텀 바인딩한 function Object를 새로 만들어 두는 것이고, 나중에 호출하는 것이다.
  • var result = obj()

    1. bind()가 생성한 function Object를 호출하게 된다.
    2. this가 bind 메소드로 바인딩된 book.get()함수가 호출된다.
  • return this.point

    1. this가 [[BountThis]]에 담긴 것을 참조한다.
    2. [[BoundThis]]book오브젝트를 참조하고 있었다.
    3. 따라서 book 오브젝트의 point 값인 123을 사용하여 반환한다.
var title = "글로벌";
function getTitle(param) {
  console.log(param, this.title);
}

var newObj = { title: "새 오브젝트" };

var bindedFunc = getTitle.bind(newObj, 99);
bindedFunc(); //=> 99 "새 오브젝트" //=> 매개변수까지 미리 바인딩 되어 있다.
getTitle(); //=> undefined "글로벌"





5. bind의 활용, 이벤트 처리

시나리오

  • 시나리오 "값 출력" 버튼을 클릭하면 값을 표시한다.
  • HTML의 형태
  <script src="point.js" defer></script>
  <button id="point">값 출력</button>

5.1 이벤트처리에서 bind의 활용

  • 이벤트 처리의 어려움은, 이벤트를 설정할 때(onClick 또는 addEventListener)의 오브젝트를 핸들러에서 this로 참조할 수 없다는 것입니다. 이 때 bind()를 이용해서 해결할 수 있다.
  • handler함수에서 this를 사용하고자 할 때 bind를 쓴다고 알고 있자. 핸들러 함수는 DOM 요소에 달려있지만, 그 바깥의 객체를 기억하고자 할 때 this가 필요할 수 있다. 이때 bind를 쓴다.
  • 핸들러 함수에서 bind를 쓰면, DOM 요소에 있는 값 말고, 전역변수 말고, 다른 객체의 값에 접근하기 위한 길을 하나 더 갖게 된다.
  • 여러가지 핸들러가 한 객체에 모여있을 때, 핸들러 함수끼리 프로퍼티를 공유하게 할 수도 있겠다.
var book = {
  myPoint: 100,
  setEvent(node) {
    node.onclick = this.show.bind(book, node); //핸들러 함수 설정
  },
  show(node, event) { //핸들러 함수
    console.log(node.textContent);
    console.log(this.myPoint);
  },
};
book.setEvent(document.getElementById("point"));
profile
한국외대 중국어&컴공 복수전공 - 세미 전공자의 기술 블로그

0개의 댓글