함수 호출 방식에 따른 this 바인딩 방식

MochaChoco·2023년 9월 30일
0
post-thumbnail

그동안 최신 React나 Vue 문법으로 페이지를 구축할 때는 this 키워드를 쓸 일이 잘 없었는데, 오래 전에 구축한 웹 사이트를 유지보수하면서 this 키워드를 자주 사용하게 되었다. 다만 특정 상황에서 this 키워드가 생각한 것과 다르게 동작해서 애를 먹었는데, 이 참에 this 키워드에 대해 제대로 공부해야겠다는 생각이 들었다.
따라서 이번 포스트에서는 this 키워드에 대해 공부한 내용을 정리하고자 한다.

javascript에서 this 키워드란?

this는 생성자 혹은 메소드(멤버 함수)에서 객체를 가리킬 때 사용하는 키워드이다. 새로 생성된 객체에 생성자의 속성을 넣어줄 때나 객체의 속성에 접근할 때 주로 사용한다. javascript의 this는 Java와 달리 함수 호출 방식에 따라 바인딩되는 객체가 달라진다.

함수 호출 방식

javascript의 함수 호출 방식은 함수 호출, 메소드 호출, 생성자 호출, apply/call/bind를 통한 호출 4가지로 분류된다.

const showThis = function () {
  console.log(this);
};

// 1. 함수 호출
showThis(); // window

// 2. 메소드 호출
const obj = {
  name: "test-obj",
  func: showThis,
};
obj.func(); // { "name": "test-obj", fuuc: f }

// 3. 생성자 함수 호출
const instance = new showThis(); // instance

// 4. apply, call, bind 호출
const bar = { name: "bar" };
showThis.apply(bar); // { "name": "bar" }
showThis.call(bar); // { "name": "bar" }
showThis.bind(bar)(); // { "name": "bar" }

1. 함수 호출

기본적인 함수 호출 방식을 말하며, 이 때 this는 전역 객체(Global Object)에 바인딩된다. 전역 객체란, 최상위 객체를 의미하며 일반적으로 Browser-side에서는 window, Server-side(Node.js)에서는 global 객체를 말한다.

1) 전역 함수의 this는 전역 객체를 가리킨다.

// 전역 함수를 선언
function showThis() {
  console.log(this);
}

this.showThis();	// window

2) 전역 함수 뿐만 아니라 내부 함수의 this는 전역 객체를 가리킨다.

// 전역 함수 뿐만 아니라 내부 함수도 this는 window 객체를 가리킨다.
function showOuter() {
  console.log("outer's this: ", this); // window
  function showInner() {
    console.log("inner's this: ", this); // window
  }
  showInner();
}
showOuter();

3) 메소드의 내부 함수의 this는 전역 객체를 가리킨다.

// 바깥에서 var로 선언한 변수는 window 객체에 바인딩됨
var value = 1;

const obj = {
  value: 50,
  showOuter: function () {
    console.log("show's this: ", this); // obj
    console.log("show's this.value: ", this.value); // 50

    function showInner() {
      console.log("showInner's this: ", this); // window
      console.log("showInner's this.value: ", this.value); // 1
    }
    showInner();
  },
};

obj.showOuter();

4) 일반적인 콜백함수의 경우에 this는 전역 객체를 가리킨다.

var value = 1;

const obj = {
  value: 50,
  show: function () {
    setTimeout(function () {
      console.log("callback's this: ", this); // window
      console.log("callback's this.value: ", this.value); // 1
    }, 500);
  },
};

obj.show();

5) 이벤트 핸들러의 this는 이벤트를 받는 HTML 요소를 가리킨다.

<button id="btn">click</button>

const btn = document.getElementById("btn");

btn.addEventListener("click", function () {
   console.log(this);  // btn
});

※ this가 전역객체를 참조하는 것을 회피하는 방법

ES6에 추가된 화살표 함수를 사용하거나, 메소드의 this가 객체를 가리키는 것을 이용하여 this를 다른 변수에 할당하여 그것을 참조하면 된다.

// 바깥에서 var로 선언한 변수는 window 객체에 바인딩됨
var value = 1;

const obj = {
  value: 50,
  showOuter: function () {
    const innerThis = this; // 멤버 함수가 가리키는 this(obj)를 innerThis변수에 할당

    function showInner1() {
      console.log("showInner1's this: ", this); // window
      console.log("showInner1's this.value: ", this.value); // 1
    }

    // innerThis가 가리키는 변수(obj)를 참조한다.
    function showInner2() {
      console.log("showInner2's this: ", innerThis); // obj
      console.log("showInner2's this.value: ", innerThis.value); // 50
    }

    // 화살표 함수의 경우 상위 스코프의 this(이 경우에는 obj)를 가리킨다.
    const showInner3 = () => {
      console.log("showInner3's this: ", this); // obj
      console.log("showInner3's this.value: ", this.value); // 50
    };

    showInner1();
    showInner2();
    showInner3();
  },
};

obj.showOuter();

이외에도 후술할 apply, call, bind 메소드를 사용하면 this를 명시적으로 바인딩할 수 있다.

2. 메소드 호출

메소드(method)란 객체의 멤버 함수를 말한다. 메소드가 호출될 때 메소드 내부의 this는 해당 메소드를 호출한 객체에 바인딩된다.

1) 메소드 내부의 this는 메소드를 호출한 객체를 가리킨다.

const obj1 = {
  name: "Kim",
  showName: function () {
    console.log(this.name);
  },
};

const obj2 = {
  name: "Park",
};

obj2.showName = obj1.showName;

obj1.showName(); // Kim
obj2.showName(); // Park

2) 프로토타입 객체 메소드 내부의 this도 해당 메소드를 호출한 객체를 가리킨다.

function Person(name) {
  this.name = name;
  console.log(this);
}

Person.prototype.getName = function () {
  return this.name;
};

const me = new Person("Kim");
console.log(me.getName()); // Kim

Person.prototype.name = "Park";
console.log(Person.prototype.getName()); // Park

3. 생성자 함수 호출

생성자 함수의 경우 this 또한 호출한 객체를 가리킨다. 다만, 기존 함수와는 다르게 바인딩 방식이 복잡하다. 아래의 코드는 Person 함수를 생성자 함수로 호출하는 예제인데, 아래의 코드를 보면서 생성자 함수에 대해 설명하도록 한다.

※ 생성자 함수란?

생성자 함수는 객체를 생성하는 역할을 하는 함수를 말한다.
다른 객체 지향 언어와 달리 javascript의 생성자 함수는 따로 형식이 정해져 있지 않으며, 단순히 기존 함수에 new 연산자를 붙여서 호출하면 해당 함수는 생성자 함수로 동작하게 된다.

따라서 생성자 함수는 일반적으로 첫 문자를 대문자로 서술하여 일반적인 다른 함수와 구분 짓도록 한다.

function Person(nm) {
  this.name = nm;
  console.log(this);
  // 암묵적으로 this에 바인딩 된 객체를 반환한다.
}

// 생성자 함수이므로 {name : 'Kim'} 객체가 반환됨
const person1 = new Person("Kim"); // { name: 'Kim' }
console.log(person1); // { name: 'Kim' }

// 생성자 함수가 아니므로 아무 값도 반환되지 않음.
const person2 = Person("Park");	// window
console.log(person2); // undefined

Person 생성자 함수의 동작은 다음과 같다.

1) Person 함수가 new 키워드로 호출되어 생성자 함수로써 동작을 시작하면, 제일 먼저 빈 객체가 생성되는데 이 빈 객체가 this에 바인딩 된다.
2) 파라미터로 받은 nm을 빈 객체의 name 변수로 할당해준다.
3) 생성자 종료 시점에서 이 객체를 암묵적으로 반환하는데, 이후에 메소드에서 호출하는 this 객체는 이 객체를 가리킨다.

이러한 과정 때문에 생성자 함수에서는 명시적으로 반환문을 사용하지 않아야 하며, this 이외의 다른 값을 반환하게 될 경우 생성자 함수의 기능을 잃어버리게 된다.

4. apply, call, bind 호출

맨 처음 함수 호출 방식에 따라 this에 바인딩되는 객체가 달라진다고 언급 했었는데, 이러한 암묵적인 방법 이외에 this를 특정 객체에 명시적으로 바인딩하는 방법도 있다.
모든 함수 객체의 프로토타입 객체인 Function.prototype 객체의 메소드인 apply, call, bind 함수를 사용하는 방법이다.

// func : 호출될 함수를 나타내는 함수 객체
// thisArg : this 값으로 설정할 객체

// Function.prototype.apply
// [...args] : : 함수에 전달할 파라미터들로 구성된 배열 또는 유사 배열 객체
func.apply(thisArg, [...args]);

// Function.prototype.call
// args1, args2, arg3, ... : 함수에 전달할 파라미터들
func.call(thisArg, args1, args2, arg3, ...);
          
// Function.prototype.bind
// args1, args2, arg3, ... : 함수에 전달할 파라미터들
func.bind(thisArg, args1, args2, arg3, ...); // 즉시 호출되지 않음

apply 함수와 call 함수는 첫번째 파라미터를 제외한 나머지를 배열로 받느냐 아니냐의 차이점 정도만 가지고 있을 뿐 유사하게 동작한다. bind 함수는 call 함수와 파라미터가 동일하며, 코드가 선언되는 시점에서 즉시 호출되지 않는다는 차이점이 있다.

1) apply, call 예제

아래는 생성자 함수로 Person 객체를 각각 생성한 후에, person1.showName 함수 내부의 this를 apply, call를 이용하여 각각 person2, person3 객체에 바인딩하는 예제이다.

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.showName = function () {
    console.log(`My name is ${this.name}, and I am ${this.age} years old.`);
  };
}

const person1 = new Person("Kim", 20);
const person2 = new Person("Park", 30);
const person3 = new Person("Choi", 25);

person1.showName(); // My name is Kim, and I am 20 years old.
person1.showName.apply(person2); // My name is Park, and I am 30 years old.
person1.showName.call(person3); // My name is Choi, and I am 25 years old.
person1.showName(); // My name is Kim, and I am 20 years old.

콘솔을 확인해보면 person1.showName 함수 내부의 this가 영구적으로 바인딩 된 것이 아니라 apply, call 함수를 사용한 시점에만 person2, person3로 바인딩된 것을 알 수 있다.

2) bind 예제

만약 this를 바인딩을 하고 즉시 사용하는게 아닌, 추후에 사용해야 한다면 bind 함수를 사용하면 된다. 아래는 bind함수를 이용하여 person1.showName 내부의 this를 person2 객체로 바인딩하는 예제이다.

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.showName = function () {
    console.log(`My name is ${this.name}, and I am ${this.age} years old.`);
  };
}

const person1 = new Person("Kim", 20);
const person2 = new Person("Park", 30);
const bindFunc = person1.showName.bind(person2);

person1.showName(); // My name is Kim, and I am 20 years old.
bindFunc(); // My name is Park, and I am 30 years old.

이렇게 bind 함수는 함수 호출 시점을 결정할 수 있다.

참고자료

함수 호출 방식에 의해 결정되는 this
[JS] 자바스크립트에서의 this
[TIL] JS_함수 메소드(apply, call, bind) 정리

profile
길고 가늘게

0개의 댓글