[JavaScript] this의 정의와 용법 알고 쓰기 (feat. 실무에서 헷갈리는 사례)

박수현·2023년 5월 10일
0
post-thumbnail

🧑‍💻 JavaScript의 this

이번 글에서는 this의 정의와 용법에 대해서 알아보겠습니다.
자바스크립트의 경우 함수를 호출할 때, 함수가 어떻게 호출되었는지에 따라 this에 바인딩할 객체가 동적으로 결정됩니다.

함수를 호출하는 방식은 아래와 같이 다양합니다.

  1. 함수 호출
  2. 메서드 호출
  3. 생성자 함수 호출
  4. apply/call/bind 호출

🏃 1. 함수 호출

  • 전역 객체는 모든 객체의 유일한 최상위 객체를 의미하며 일반적으로 Browser-side에서는 window, Server-side(Node.js)에서는 global 객체를 의미합니다.
// in browser console
this === window; // true

// in Terminal
node;
this === global; // true
  • 글로벌 영역에 선언한 함수에서 this는 전역 객체에 바인딩됩니다.
  • 즉, this는 선언된 메서드를 가지고 있는 그 상위 오브젝트를 의미하죠.
  • 단, strict 모드에서는 this가 undefined가 됩니다.
function foo() {
  console.log("foo's this: ", this); // window
  function bar() {
    console.log("bar's this: ", this); // window
  }
  bar();
}
foo();
  • 또한, foo 안의 bar라는 내부함수도 this는 전역 객체에 바인딩됩니다.
  • 내부 함수에서 this가 전역 객체를 참조하는 것을 회피 하는 방법은 글의 마지막에서 다룰 apply, call, bind 등의 방법이 있습니다.

🏃 2. 메서드 호출

함수의 객체가 프로퍼티 값이면 메서드로서 호출됩니다. 이때 메서드 내부의 this는 해당 메서드를 호출한 객체에 바인딩됩니다.

var obj1 = {
  name: "Lee",
  sayName: function () {
    console.log(this.name);
  },
};

var obj2 = {
  name: "Kim",
};

obj2.sayName = obj1.sayName;

obj1.sayName(); // obj1
obj2.sayName(); // obj2

🏃 3. 생성자 함수 호출

자바스크립트에서는 비슷한 객체를 여러개 사용하길 원할 경우 생성자를 만들어서 사용합니다. 생성자객체를 복사해주는 기계라고 생각하면 쉽습니다!

function Constructor() {
  this.name = "pySoo";
}

var obj = new Constructor();

console.log(obj);

여기서의 this새롭게 복사된 객체를 의미합니다. this.name은 새로 생긴 객체의 name 키값에 "pySoo"라는 값으로 할당해달라는 의미가 됩니다.


🏃 4. apply/call/bind 호출

this에 바인딩될 객체는 함수 호출 패턴에 의해 결정됩니다. 이러한 자바스크립트 엔진의 암묵적 this 바인딩 이외에 this를 특정 객체에 명시적으로 바인딩 하는 방법도 있습니다.

그것은 바로 Function.prototype의 apply, call, bind 메서드를 이용하는 방법입니다.

1. apply

func.apply(thisArg, [argsArray]);

// thisArg: 함수 내부의 this에 바인딩할 객체
// argsArray: 함수에 전달할 argument의 배열

기억해야 할 점은 apply 메서드를 호출하는 주체함수이며 apply 메서드는 this를 특정 객체에 바인딩할 뿐 본질적인 기능함수 호출입니다.

글로만 읽으면 이해하기 쉽지 않으니 코드를 통해 살펴보겠습니다!

const Person = function (name) {
  this.name = name;
};

const obj = {};

// apply() 메서드는 생성자 함수 Person을 호출합니다. 이 때 this에 객체 obj를 바인딩합니다.
Person.apply(obj, ["pySoo"]);

console.log(obj); // { name: 'pySoo' }

apply 메서드를 호출한 후 Person 함수의 thisobj 객체가 됩니다.

Person 함수는 this의 name 프로퍼티에 매개변수 name의 값을 할당하는데, this에 바인딩된 obj 객체에는 name 프로퍼티가 없으므로 name 프로퍼티가 동적으로 추가되고 값이 할당됩니다.


2. call

Person.apply(foo, [1, 2, 3]);
Person.call(foo, 1, 2, 3);

call 메서드의 경우 apply와 기능은 같지만 apply의 두 번째 인자에서 배열 형태로 넘긴 것을 각각 하나의 인자로 넘기는 점이 다릅니다.


3. bind

bind는 apply, call 메서드와 달리 함수를 실행하지 않는다. 함수에 인자로 전달한 this가 바인딩된 새로운 함수를 리턴합니다.

여기서부터는 코드가 조금 길어지니 천천히 읽어보도록 하겠습니다.

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

Person.prototype.doSomething = function (callback) {
  if (typeof callback == "function") {
    // --------- 1) this = Person 객체
    callback();
  }
};

function foo() {
  console.log(this.name); // --------- 2) this = 전역 객체 window
}

var p = new Person("Lee");
p.doSomething(foo); // undefined

이 코드에서의 문제점을 발견하였나요? 1의 시점에서 this는 Person 객체이지만 2의 시점에서 this는 전역 객체 window를 가리킵니다 🤯 1번 항목에서 다뤘던 함수에서는 this가 전역 객체에 바인딩 되는 경우인 것이죠.

따라서, 콜백 함수 내부의 this(1)콜백함수를 호출하는 함수 내부의 this(2)일치시켜 주어야 합니다.


bind 메서드를 이용하면 해결이 가능합니다!

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

Person.prototype.doSomething = function (callback) {
  if (typeof callback == "function") {
    // callback.call(this);와 동일한 코드입니다.
    callback.bind(this)();
  }
};

function foo() {
  console.log(this.name);
}

var p = new Person("Lee");
p.doSomething(foo); // 'Lee'

bind 메서드에서 함수에 인자로 전달한 this가 바인딩된 새로운 함수를 리턴합니다. 쉽게 얘기하자면 bindthis를 수정할 수 있게 해주는 메서드이며, this가 Person으로 고정된 function이 할당되는 것입니다.


🏃 5. 추가 자료: 이벤트 리스너

실무에서는 이벤트 리스너 또는 제이쿼리를 썼을 때의 this 용법이 헷갈리기 때문에 문제가 되는 경우가 있습니다.

this의 개념에 대해서 알아보았으니 이벤트 리스너와 제이쿼리의 사례까지 꼼꼼하게 잡아보고 가도록 하겠습니다!

1. 이벤트 리스너

document.body.onclick = function () {
  console.log(this); // <body>
};

앞서 1번 사례에서 함수의 경우 전역 객체인 window에 바인딩 된다고 배웠는데, 이 경우 body에 바인딩이 되었습니다.

이벤트가 발생하는 경우, 내부적으로 this가 이벤트의 currentTarget과 일치하게 됩니다. (주의❗)

2. 제이쿼리

$("div").on("click", function () {
  console.log(this); // <div>
  function inner() {
    console.log("inner", this); // inner window
  }
});

사람들을 미치게 하는 응용사례입니다. 앞서 클릭 이벤트에서 내부적으로 this를 currentTarget으로 바꾼다고 배웠습니다. 하지만 inner 함수 호출 시에는 this가 window가 됩니다.

사실 inner는 죄가 없습니다... 함수의 this는 기본적으로 전역 객체 window라는 원칙을 충실히 따랐을 뿐이죠. 잘못이 있다면 이벤트 리스너내부적으로 this를 바꿨음에도 명시적으로 알리지 않은 것이 잘못이라고 할 수 있겠습니다 😅


해결1. this를 변수에 저장

$("div").on("click", function () {
  console.log(this); // <div>
  const that = this;
  function inner() {
    console.log("inner", that); // inner <div>
  }
});

위의 문제를 해결하기 위해서는 this를 변수에 저장해서 사용하는 방법 또는,

해결2. 화살표 함수

$("div").on("click", function () {
  console.log(this); // <div>
  const inner = () => {
    console.log("inner", this); // inner <div>
  };
});

ES6 화살표 함수를 쓰는 방법이 있습니다. 화살표 함수는 this에 window 대신 상위 함수의 this를 가져옵니다.


글을 마치며

  • this는 기본적으로 window이지만 객체 메서드 호출, 생성자 함수 호출, 또는 apply, call, bind를 사용할 때 this가 변경됩니다.

  • 이벤트 리스너의 경우 this를 내부적으로 바꿀 수도 있으니 항상 this를 확인해보는 것이 중요합니다.

profile
반갑습니다. 꾸준함과 글쓰기를 좋아하는 프론트엔드 개발자입니다. 블로그를 https://enjoydev.life로 이전했습니다 😀

0개의 댓글