본문 바로가기

JavaScript

this와 Dynamic binding

객체 지향 프로그래밍에 대해 공부하다 this를 명확하게는 모른다는 것이 느껴졌다 🥲

this가 자바스크립트에서 어떤 역할을 하는지, 동적 바인딩이 어떻게 이루어지는지 알아보고 더불어 화살표 함수에 대해서도 공부해보자.

 

Object-Oriented Programming과 JavaScript

객체 지향 프로그래밍을 정리해보고, 처음 접했을 때도 혼돈의 카오스였던 자바스크립트의 Prototype Chaining에 대해서 알아보자. 객체 지향 프로그래밍이란? OOP (Object-Oriented Programming)은 프로그램

devlog-of-yein.tistory.com

 


 

👉🏼  this 란,

자바스크립트 엔진에 의해 암묵적으로 생성되는 객체(object)로, 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수이다. 이 this를 통해 속성이나 메소드를 참조할 수 있다. 

 

여기서 this가 가리키는 값이 동적으로 결정된다는 점에 유의해야 한다!

이것이 this의 동적 바인딩 특성인데, 변수인 this가 가리키는 값(value, 원시 값 또는 객체)이 함수 호출 방식에 따라 결정된다는 것이다.

경우에 따른 예시를 참고해보자.

 

 

1. this 바인딩이 전역 객체(window / global)를 가리키는 경우

(1) 일반 함수 또는 내부 함수 호출

function playVideo() {
  console.log(this); // Window {...}
  
  function playAudio() {
    console.log(this); // Window {...}
  }
  
  playAudio();
}

playVideo();

 

🙌🏽  strict mode에서는?

'use strict'를 이용한 strict mode에서는 일반 함수 내 this가 undefined로 바인딩된다.

this는 객체 내부에서 프로퍼티나 메소드를 참조하기 위한 변수이기 때문에, 그 의미를 잃는 일반 함수 내의 this는 할당하지 않는 것이다.

 

 

(2) 콜백 함수 호출

아래 예시와 같이 객체 내부에 있어도, 콜백 함수는 객체에 속하지 않기 때문에 일반 함수처럼 Window 객체를 참조한다.

const video = {
  title: 'a',
  tags: ['a', 'b', 'c'],
  showTags: function(){
    this.tags.forEach(function(tag) {
      console.log(tag); // a b c
      console.log(this.title); // undefined undefined undefined
    });
  }
};

video.showTags();

 


 

2. this 바인딩이 특정 객체를 가리키는 경우

(1) 메소드 함수로 호출

선언식, 표현식에 상관없이 객체 내부 메소드로 호출된 this는 자신을 호출한 객체를 참조한다.

따라서 위 showTags 예시에서 this.tag가 video.tag를 가리켜 forEach를 수행한 것이다.

const video = {
  title: 'a',
  play() {
    console.log(this); // {title: 'a', play: f, stop: f}
  }
};

video.stop = function () {
  console.log(this); // {title: 'a', play: f, stop: f}
}

video.play();
video.stop();

 

(2) 생성자 함수 호출

생성자 함수는 반복적으로 사용되는 객체 생성을 효율적으로 해준다. 꼴은 함수이지만 new 연산자와 함께 호출하여 사용하면 해당 함수는 객체(인스턴스)를 생성하는 생성자 함수로 동작한다. 

이러한 생성자 함수에서 this는, 생성자로부터 만들어지는 인스턴스를 가리킨다. 

function Film(genre) {
  this.genre = genre;
}

Film.prototype.getGenre = function() {
  return `The genre of this movie is ${this.genre}`;
};

const dune = new Film("SF");
console.log(dune.getGenre()); // The genre of this movie is SF

 

🙌🏽  생성자 함수에 대해

만약 new 키워드 없이 생성자 함수를 사용할 경우 일반 함수로 동작한다.

Film 생성자는 반환 값이 없기 때문에 tenet은 undefined가 되어 메소드를 불러오는데 에러가 발생한다. 

const tenet = Film("action");
console.log(tenet.getGenre()); // Uncaught TypeError: Cannot read properties of undefined (reading 'getGenre')

Array 생성자를 쓸 때 new 연산자 없이 그냥 Array(10) 이런 식으로 썼던 경우가 종종 있었는데, 똑같이 배열로 잘 쓰였던 것으로 보아 반환 값도 동일하게 설정되어있나 보다. 문제는 없었지만 앞으로는 문법 잘 지켜서 써야지..!

 

 

(3) Function.prototype 메소드 간접 호출 (call, apply, bind)

call, apply, bind를 통해 호출된 this는 메소드 첫 번째 인수로 전해진 값을 가리킨다.

function getThisBinding() {
  console.log(arguments);
  return this;
}

const thisArg = {a: 1};

console.log(getThisBinding.call(thisArg, 1, 2,3)); 
console.log(getTHisBinding.apply(thisArg, [1, 2, 3]));
console.log(getThisBinding.bind(thisArg)(1, 2, 3));  // 명시적으로 인수와 함께 함수 호출
console.log(getThisBinding.bind(thisArg); // getTHisBinding
// 세가지 결과 모두 아래와 같다.
// Argument(3) [1, 2, 3, callee: f, Symbol(Symbol.iterator): f]
// {a: 1}

 

🙌🏽   Funtion.prototype 메소드에 대해

1️⃣  callapply는 인수를 전달하는 방식에서 차이가 있을 뿐, 모두 함수 호출과 함께 호출한 함수에 this를 바인딩하는 역할을 한다.

arguments 객체와 같은 원래는 배열 메소드를 사용할 수 없는 유사 배열 객체에 메소드를 사용할 수 있도록 해주는 것이 대표적인 용도다.

function convertArgsToArray() {
  console.log(arguments); // Arguments(3) [1, 2, 3, callee: f, Symbol(Symbol.iterator): f]
  const arr = Array.prototype.slice.call(arguments); // 배열 복사본 생성
  console.log(arr); // [2, 3]
}

convertArgsToArray(1, 2, 3);

 

2️⃣  bind함수를 호출하지 않으므로 인수와 함께 함수를 호출시켜주어야 한다. 호출 없이 사용한다면 this 바인딩이 교체된 함수를 새롭게 생성해 반환시킨다.

2-(1)에서 살펴본 것처럼, 메소드의 this와 내부 콜백 함수의 this가 일치하지 않는 문제를 해결할 때 유용하게 쓰인다. 실제로는 두 this가 일치해야 문맥적으로 자연스러운 경우가 많기 때문이다.

const person = {
  name: "Lee",
  foo: function(callback) { setTimeout(callback, 100); }
  bar: function(callback) { setTimeout(callback.bind(this), 100); }
};

person.foo(function() {
  console.log(`my name is ${this.name}.`) // my name is .
};

person.bar(function() {
  console.log(`my name is ${this.name}.`) // my name is Lee.
};

브라우저 환경에서는 window.name은 없는 값이기 때문에 빈 문자열로, Node.js 환경에서는 undefined로 나온다.

 

 


 

thisArg와 화살표 함수

다시 콜백 함수 예시로 돌아가서 어떻게 하면 전역 객체가 아닌 상위 객체를 참조할 수 있는지 확인해보자.

const video = {
  title: 'a',
  tags: ['a', 'b', 'c'],
  showTags: function(){
    this.tags.forEach(function(tag) {
      console.log(tag); // a b c
      console.log(this.title); // undefined undefined undefined
    });
  }
};

video.showTags();

 

1. thisArg

forEach에서 메소드를 요소로 가지는 객체(video)를 this로 참조하기 위해서는 forEach의 thisArg 인자를 활용해야 한다.

추가해준 this는 콜백 함수 바깥이므로 자신을 호출한 객체를 참조하게 되어 (아래 2-(1)을 통해 이해할 수 있다) forEach 내부 콜백 함수의 this가 video 객체를 가리키게 된다.

ES5에서 도입되었는데 그보다 전에는 콜백 환경 바깥에서 this를 변수에 저장하고 가져다 쓰는 등 지저분하게 처리했다고 한다.

const video = {
  title: 'a',
  tags: ['a', 'b', 'c'],
  showTags: function(){
    this.tags.forEach(function(tag) {
	  console.log(tag); // a b c
	  console.log(this.title); // a a a
    }, this); // this는 video 객체를 참조
  }
};

video.showTags();

 

2. 화살표 함수 (Arrow function)

사실 나는 this 식별자를 활용해본 적이 드물고, ES6 문법으로 공부했기 때문에 forEach 등 고차 함수를 사용할 때 자연스레 화살표 함수를 사용했었다. 화살표 함수 도입의 핵심이 this임을 알고도 this에 대해서 잘 이해하지 못했기 때문에 와닿지 않았던 것 같다.

 

화살표 함수는 function 키워드를 생략하고 화살표를 사용해 기존의 함수보다 간략하게 정의할 수 있다는 장점이 있다. 표현 상의 간결성뿐 아니라 콜백 함수에서 this가 전역 객체를 가리키는 바인딩 문제를 해결해준다.

이 외에도 인스턴스를 생성할 수 없는 non-constructor인 점, 중복된 매개변수 이름을 선언할 수 없다는 점 등 일반 함수와 다른 점이 여럿 있지만 지금은 this에 초점을 맞춰보자!

 

또한 함수 자체의 this 바인딩을 갖지 않기 때문에, 화살표 함수 내부에서 this는 상위 스코프의 this를 그대로 따라간다. 이렇게 함수의 위치에 따라서 this가 정해지는 현상을 lexical this라고 한다. (arguments도 마찬가지로 상위 스코프의 arguments를 참조한다.) this 바인딩을 갖지 않으므로  위에서 살펴본 call, apply, bind 메소드를 화살표 함수에 사용하는 것은 무용지물인 셈이다.

ES6 이전 문법에서는 상위 this를 참조하기 위해 bind를 활용해서 나타냈어야 했지만, 화살표 함수의 lexical this 덕분에 스코프 체인을 탐색하며 찾게 된다.

(function () { return this.x; }).bind(this); // 일반 함수로 상위 this 참조
() => this.x; // 화살표 함수로 상위 this 참조

 

'JavaScript' 카테고리의 다른 글

실행 컨텍스트 작동 원리와 클로저  (0) 2022.02.06
Object-Oriented Programming과 JavaScript  (0) 2022.01.20