[JavaScript] this 이해하기

서동경·2023년 2월 3일
0
post-thumbnail

🍀 this란?

자바스크립트의 this는 바인딩을 통해 특정 객체에 접근할 수 있는 "예약어"이다.

바인딩이란 식별자와 값을 연결하는 과정을 말한다. 즉 this의 바인딩이란, "식별자의 역할을 하는 this"와 "this가 가르킬 객체"를 연결하는 과정이다.

this가 기본적으로 바인딩되는 객체는 전역 객체이지만 상황에 따라 바인딩되는 객체가 달라지기 때문에 바인딩 규칙을 잘 숙지할 필요가 있다.

🌱 바인딩 규칙

🧩 암시적 바인딩

this는 함수의 호출 방식에 따라 다르게 바인딩된다. 이렇게 바인딩될 객체가 동적으로 결정되는 바인딩을 "암시적 바인딩"이라고 한다. 하지만 여러가지 환경적 요소가 암시적 바인딩에 관여하므로 예측하기가 매우 어렵다.

자주 쓰이는 함수 호출 방식으로는 일반적인 함수 호출, 메서드 호출, 생성자 함수에서 호출 등이 있다.

💬 일반 함수 호출

일반 함수가 this를 호출한다면, this는 전역 객체에 바인딩된다. 즉 브라우저에서는 window, node.js 환경에서는 global을 가르킨다.

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

func(); // window, global

💬 메서드 호출

메서드가 this를 호출한다면, this는 호출한 메서드를 가지고 있는 Object를 가리킨다.

const obj = {
  func: function () {
    console.log(this);
  },
};

obj.func(); // Object

💬 생성자 함수 호출

생성자 함수의 this는 생성자 함수가 생성하는 객체, 즉 Instance를 가리킨다.

function Workout(name) {
  this.name = name;
}

const squat = new Workout("Squat");
console.log(squat.name); // Squat // this가 instance를 가리키기 때문에, squat.name에 "Squat"가 할당되어있음.

만약 new 키워드를 사용하지 않는다면, 생성자 함수도 일반 함수처럼 전역 객체를 가르킨다.

function Workout(name) {
  this.name = name;
}

const squat = Workout("Squat");
console.log(window.name); // Squat // this는 instance가 아닌 전역 객체를 가리키기 때문에, window.name에 "Squat"가 할당되어있음.

💬 이벤트 핸들러 호출

이벤트 핸들러에서 this를 호출한다면, this는 이벤트가 발생한 HTML 요소를 가리킨다.

const btn = document.querySelector(".btn");
btn.addEventListener("click", function () {
  console.log(this); // <button class="btn"></button>
});

🧩 명시적 바인딩

암시적 바인딩은 예측이 어렵게 동작할 수 있으므로, this의 객체를 명시적으로 지정하는 방식도 필요하다. 이런 방식의 바인딩을 "명시적 바인딩"이라고 한다.

call, apply, bind 메서드를 통해 명시적 바인딩이 가능하다.

💬 call

this에 바인딩될 객체를 명시적으로 지정하고, 함수의 매개변수를 입력받는 메서드이다.

첫 번째 인자로 this가 가리킬 객체, 두 번째 인자부터는 함수의 매개변수를 입력한다.

const workout = {
  name: "Squat",
  todayWorkout: function () {
    return `오늘의 운동: ${this.name} / `;
  },
};

function getInfo(kg, sets, reps) {
  return this.todayWorkout() + `${kg}KG ${sets}SETS ${reps}REPS`;
}

const strengthTraining = getInfo.call(workout, 140, 3, 5);
// 첫 번째 매개변수에는 "this가 가리킬 객체"
// 두 번째 매개변수부터 "함수의 매개변수"를 입력 !
console.log(strengthTraining);
// 오늘의 운동: Squat / 140KG 3SETS 5REPS

💬 apply

this에 바인딩될 객체를 명시적으로 지정하고, 함수의 매개변수를 배열 형태로 입력받는 메서드이다.

첫 번째 인자로 this가 가리킬 객체, 두 번째 인자에는 배열인 함수의 매개변수를 입력한다.

const workout = {
  name: "Squat",
  todayWorkout: function () {
    return `오늘의 운동: ${this.name} / `;
  },
};

function getInfo(kg, sets, reps) {
  return this.todayWorkout() + `${kg}KG ${sets}SETS ${reps}REPS`;
}

const hypertrophyTraining = getInfo.apply(workout, [100, 3, 12]);
console.log(hypertrophyTraining);
// 첫 번째 매개변수에는 "this가 가리킬 객체"
// 두 번째 매개변수에는 "함수의 매개변수를 배열로" 입력 !
// 오늘의 운동: Squat / 100KG 3SETS 12REPS

💬 bind

'지정한 객체로 바인딩된 함수'를 리턴하는 메서드이다. 함수를 실행하지는 않기 때문에 명시적으로 함수를 호출해야 한다.

첫 번째 인자로 this가 가리킬 객체를 입력한다. 함수의 매개변수는 리턴받은 함수를 호출할 때 입력한다.

const workout = {
  name: "Squat",
  todayWorkout: function () {
    return `오늘의 운동: ${this.name} / `;
  },
};

function getInfo(kg, sets, reps) {
  return this.todayWorkout() + `${kg}KG ${sets}SETS ${reps}REPS`;
}

const endurance = getInfo.bind(workout);
// 매개변수로 "this가 가리킬 객체"를 입력하고
const enduranceTraining = endurance(80, 3, 20);
// 함수의 매개변수는 리턴받은 함수를 호출할 때 입력 !
console.log(enduranceTraining);
// 오늘의 운동: Squat / 80KG 3SETS 20REPS

🧩 화살표 함수의 this 바인딩

일반 함수와 화살표 함수의 가장 큰 차이점은 this의 동작 방식이다. 결론부터 말하자면 일반 함수에서의 this가 무조건 전역 객체로 바인딩되는 것이 불편하다면 화살표 함수를 쓰면 된다.

화살표 함수에서의 this는 언제나 상위 스코프의 this이다. 이를 Lexical this라고 부른다. 일반 함수는 함수를 "호출"할 때 바인딩할 객체가 동적으로 정해지지만, 화살표 함수는 함수를 "선언"할 때 바인딩할 객체가 정적으로 정해진다.

추가적으로 화살표 함수의 this는call, apply, bind 메서드를 통해 변경할 수 없다.

🧩 엄격 모드(Strict mode)에서의 this 바인딩

기본 값인 느슨한 모드(Sloppy mode)의 유연성으로 인한 에러들을 방지하기 위해 스크립트 최상단에 use strict 구문을 추가해 엄격 모드를 사용할 수 있다.

엄격 모드에서의 this 역시 다르게 동작하는데, 기본적으로 전역 객체를 가리키는 것이 아니라 undefined이다. 이는 this가 무분별하게 window 혹은 global 가리켜서 발생하는 에러를 방지한다.

💥 일반 함수의 this 바인딩 규칙으로 인한 문제점

일반 함수의 this 바인딩 규칙으로 인해 내부 함수의 this는 모두 전역 객체를 가리키게 된다. 여기서 내부 함수란 함수 내부 함수, 메서드 내부 함수, 콜백 함수가 대표적이다.

call, apply, bind 메서드를 통해 명시적으로 지정해 주는 방법 외에 어떤 해결 방법이 있는지 확인해보자.

🔒 첫 번째 문제 상황: 함수 내부 함수

  const Workout = function (name, sets, reps) {
    this.name = name;
    this.sets = sets;
    this.reps = reps;
    this.getInfo = function () {
      console.log(this); // Workout
      console.log(
        `오늘의 운동: ${this.name} / ${this.sets}SETS ${this.reps}REPS`
      ); // 오늘의 운동: Squat / 3SETS 10REPS

      const innerFunc = function () {
        console.log(this); // Window
        console.log(
          `오늘의 운동: ${this.name} / ${this.sets}SETS ${this.reps}REPS`
        ); // 오늘의 운동: undefined / undefinedSETS undefinedREPS
      };
      innerFunc(); 
    };
  };

const squat = new Workout("Squat", 3, 10);
squat.getInfo();

다음 코드를 실행할 경우, getInfo 함수의 this는 생성자 함수의 인스턴스인 squat로 바인딩되어 의도하던 문구가 출력된다. 그러나 getInfo 함수의 내부 함수인 innerFunc의 this는 일반 함수의 바인딩 규칙에 따라 전역 객체인 window에 바인딩되어 의도하던 문구가 출력되지 않는다.

🔑 화살표 함수를 사용하여 해결 !

그러나, 아래와 같이 화살표 함수를 이용한다면 이를 해결할 수 있다.

const Workout = function (name, sets, reps) {
  this.name = name;
  this.sets = sets;
  this.reps = reps;
  this.getInfo = function () {
    console.log(this); // Workout
    console.log(
      `오늘의 운동: ${this.name} / ${this.sets}SETS ${this.reps}REPS`
    ); // 오늘의 운동: Squat / 3SETS 10REPS

    const innerFunc = () => {
      console.log(this); // Workout
      console.log(
        `오늘의 운동: ${this.name} / ${this.sets}SETS ${this.reps}REPS`
      ); // 오늘의 운동: Squat / 3SETS 10REPS
    };
    innerFunc();
  };
};

const squat = new Workout("Squat", 3, 10);
squat.getInfo();

함수의 정의 방식만 함수 표현식에서 화살표 함수로 바꾼 코드이다. 화살표 함수는 선언할 때 정적으로 바인딩된다. 즉 innerFunc의 this는 무조건 Lexical this인squat 인스턴스를 가르키게 된다!

🔒 두 번째 문제 상황: 메서드 내부 함수

메서드의 내부 함수의 this 역시 콜백 함수의 this처럼 전역 객체를 가르킨다. 결국 내부 함수를 호출한 주체는 메서드가 아닌 일반 함수이기 때문이다.

const obj = {
  func: function () {
    function innerFunc(){
    	console.log(this);
    }
    innerFunc(); //사실 이놈이 호출의 주체임!
  },
};

obj.func(); // Window

다음 코드에서 this 호출의 주체는 innerFunc 함수이기 때문에 this는 전역 객체를 가리킨다.

🔑 this 값을 변수에 저장하여 변수를 사용하여 해결!

const obj = {
  func: function () {
    const that = this; // Object를 가리키는 this를 변수에 저장하고,
    console.log(this);
    function innerFunc() {
      console.log(that); // 그 변수(that)를 this 대신 가져다 쓴다 !
    }
    innerFunc();
  },
};

obj.func();

첫 번째 문제 상황(함수 내부 함수)처럼 화살표 함수를 통해서 해결할 수도 있지만, 이번에는 this 값을 변수에 넣어 그 변수를 사용하는 방법을 사용해보겠다. that 변수에 할당한 this는 메서드가 호출한 this로서 Object를 가르킨다. 그러므로 that 변수 역시 Object를 가리키기 때문에 이걸로 사용하면 된다.

근데 가독성 측면에서 화살표 함수가 나아보인다.

🔒 세 번째 문제 상황: 콜백 함수

함수의 파라미터로 들어가 있는 함수를 콜백 함수라고 부르는데, 컴퓨터 입장에서 콜백함수도 그냥 일반 함수로 처리한다. 따라서 콜백 함수에서의 this도 전역 객체를 가리킨다.

const obj = {
  bodyParts: ["Chest", "Back", "Shoulder", "Leg"],
  getInfo: function () {
    console.log(this); // Object
    this.bodyParts.forEach(function (bodyPart) {
      console.log(bodyPart);
      console.log(this); // Window
    });
  },
};

obj.getInfo();

forEach 메서드의 콜백함수는 아니나 다를까 Window를 가리킨다.

🔑 고차 함수(콜백 함수를 인자로 갖는 그 함수)의 파라미터인 thisArg로 해결 !

const obj = {
  bodyParts: ["Chest", "Back", "Shoulder", "Leg"],
  getInfo: function () {
    console.log(this); // Object
    this.bodyParts.forEach(function (bodyPart) {
      console.log(bodyPart);
      console.log(this); // Object
    }, this);
  },
};

obj.getInfo();

MDN 문서를 보면, [,thisArg]라는 파라미터가 종종 보인다. 선택적으로 사용 가능한 이 파라미터를 가진 고차 함수들은 이 파라미터에 할당된 값을 콜백함수의 this값으로 지정해줄 수 있다.

그러므로, 위와 같이 forEach 메서드의 인자인 [,thisArg]this를 넣어주면, 콜백함수의 this가 해당 고차 함수가 정의된 스코프의 this를 가리키도록 할 수 있다.

profile
개발 공부💪🏼

0개의 댓글