[JavaScript] this

김진용·2023년 4월 13일

자바스크립트

목록 보기
6/6
post-thumbnail

1. 시작하기에 앞서

아래의 내용은 제가 책을 읽으면서 공부한 내용입니다!

잘못 이해하고 작성한 내용이 있을 수 있고, 꼭 알아야 하는데 넘어간 부분이 있을 수 있습니다.

만약, 잘못되거나 부족한 부분이 있다면 댓글로 알려주시면 감사하게 받아들이겠습니다!

2. this를 왜 알아야 할까?

단순히 알고리즘 문제만 풀 때에는 this에 대해 궁금한 적이 없었다.

누군가가 이런게 있다고 알려주지 않았다면, 지금도 모른채로 있었을 것 같다.

하지만 분명히 필요하고 중요한 도구일 테니까 만들었을거다 나보다 똑똑한 사람들 만든 언어니까

그래서 this 가 대체 무엇인지, 왜 써야 하는지, 어떻게 사용하는지에 대해 공부해보려고 한다!

3. this는 뭘까?

개발할 때 변수, 함수 등을 명시하는 식별자의 이름은 그 쓰임새를 파악하기 쉽도록 지으라고 한다!

같은 논리라면 this는 "이것"이다.
즉, 뭔가를 지칭하는 대명사 같은 용도라는게 분명할거다

근데, 일상생활에서 "이것"이 될 수 있는건 정말정말 많다
내가 지금 먹고 있는 사과도, 쓰고 있는 노트북도, 덮고 있는 이불도 다 이거! 라고 할 수 있는데?
결국, this상황에 따라 가르키는데 달라진다는 생각이 들었다!

자세히 알기 전에 잘못된 부분부터 짚고 넘어가보자

3-1. this는 자기 자신이 아니다

function plusCount(number) {
  console.log("plusCount 호출 횟수 : ", number);
  this.count++;
};

plusCount.count = 0;
for (let i = 0; i < 5; i++) {
  if (i > 2) {
    plusCount(i);
  };
};

console.log(plusCount.count);

// 결과
// plusCount 호출 횟수 :  3
// plusCount 호출 횟수 :  4
// 0

this.count++ 의 this는 plusCount 자기 자신을 기대하고 작성된 것이다.
하지만 호출은 2번 됐지만, plusCount.count 의 값은 0이다

즉, this 는 자기 자신을 가르키는 단순한 작업을 하는게 아닌거다

3-2. this는 함수의 렉시컬 스코프를 참조하지 않는다.

function one() {
  var result = 2;
  this.two();
};

function two() {
  console.log(this.result);
};

one();

// 결과
// TypeError: this.two is not a function

먼저 this를 이용해서 two() 함수를 참조하는 것부터 잘못된 것이 된다.

이런 식으로 렉시컬 스코프 안에 존재하는 변수를 this를 이용해서 접근하려는 것은 잘못된 방법이 된다.

결국, this 는 렉시컬 스코프와 관련이 없다.

4. this에 대해서

결론부터 말하자면, this는 함수 선언 위치가 아닌 함수의 호출 시점에 따라 달라지게 된다

호출 시점을 이해하기 위해 호출부호출 스택에 대해 간단히 짚고 넘어가자

// 4. 호출부와 호출 스택
function one() {
  // 호출 스택 : one
  // 호출부는 one 바로 직전인 "전역 스코프"의 내부
  console.log("one");
  two();
};

function two() {
  // 호출 스택 : one => two
  // 호출부는 two 바로 직전인 "one" 내부
  console.log("two");
};

one(); // one의 호출부

위 코드처럼 호출 스택은 함수가 실행되기까지 거쳐간 함수들이라고 생각하면 된다.

그리고 호출부는 현재 실행 중인 함수 직전 함수 내부에 있게 된다.

위 내용처럼 간단하게만 코드가 작성되지 않으니까, 복잡한 상황에서의 호출 시점을 파악할 수 있는 능력을 길러야겠다. 디버깅을 이용하거나, 개발자 도구를 이용하자

이제부터는 호출부가 어떻게 this 의 참조를 결정하는지 알아보려고 한다!

⭐ 총 4가지 규칙이 있는데, 규칙이 아닌 상황에 맞게 설명해보려고 한다. ⭐

4-1. 일반 함수에서의 this

일반적인 함수로 호출될 때, this전역 객체를 참조한다.

이 과정은 첫 번째 규칙단독 함수 실행에 대한 규칙에 의해서 적용된 결과다.

function one() {
  console.log(this.a);
};

var a = 2;
one();

// 결과
// 2

전역 스코프에 변수를 선언하면 변수명과 같은 이름의 전역 객체 프로퍼티가 생성된다.
그래서 one() 함수를 호출했을 때, this.a 는 전역 객체 a 를 참조하게 된다.
이렇게 기본적인 함수 형태에서는 전역 객체를 참조한다.
만약 console.log(this) 였다면? 브라우저에서는 window 를, vscode에서는 global 을 출력한다.

그런데, vscode 에서는 undefined 가 출력된다. 그 이유는 엄격 모드가 기본 상태이기 때문!

엄격 모드에서는 전역 객체는 this 의 기본 바인딩 대상에서 제외되기 때문이다.

4-2. 객체의 메서드에서의 this

함수가 객체 내부의 메서드로 호출될 때, this객체를 참조한다.

이 과정은 두 번째 규칙호출부의 객체 존재 유무에 의해서 적용된 결과다.

function result1() {
  console.log(this);
}

const two = {
  a: 2,
  result1: result1,
  result2() {
    console.log(this.a);
  },
};

two.result1();
two.result2();

// 결과
// { a: 2, result1: [Function: result1], result2: [Function: result2] }
// 2

이 코드에서는 result1()result2() 함수의 호출부는 two 객체의 메서드가 된다.

더 복잡하게 말하자면, 함수 호출 시점에 콘텍스트 객체가 존재한다면 암시적 바인딩에 의해서 콘텍스트 객체가 this 에 바인딩 된다.

콘텍스트 객체 : 현재 실행되는 코드에 대한 정보를 담고 있는 객체

조금 더 복잡한 예시를 한 번 보자

const outer = {
  outerMethod() {
    const that = this;
    function inner() {
      console.log(this);
      console.log(that);
    };

    inner();
  }
};

outer.outerMethod();

// 결과
// window(브라우저)
// outer 객체

같은 this 인 것 같지만, 이렇게 다르게 나오는 이유는 호출부를 잘 보면 된다.

this 는 호출부가 inner() 함수 내부다. 즉, 일반 함수 내부니까 전역 객체가 되는 것이다.
그리고 thatthis 를 할당한 시점은 outerMethod() 내부다. 결국, this 의 호출부가 객체 내부의 메서드니까 객체를 참조하게 되서 that 도 객체를 참조하게 되는 것!

4-3. 클래스와 인스턴스에서의 this

클래스에서 this생성된 인스턴스를 참조한다.

class Three {
  constructor() {
    console.log(this);
  };
};

const threeInstance = new Three();

// 결과
// Three {}

이 코드에서는 Three 라는 클래스에 의해 생성된 인스턴스 객체 threeInstance 가 된다.

그래서 일반적으로 클래스를 만들 때, this.name = name 식으로 할 수 있는 것이었다!
this 가 인스턴스를 참조하기 때문에 this.name인스턴스 객체.name 과 같은 의미가 되기 때문이다!

4-4. 이벤트 함수에서의 this

<button id="myButton">클릭</button>

클릭 버튼을 만들고, 이 버튼에 clickButton 이라는 이벤트 핸들러를 추가해보자

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

function clickButton() {
  console.log(this); // HTML 요소
  
  const buttonObj = {
    buttonMethod() {
      console.log(this); // buttonObj 객체
    }
  };
  
  buttonObj.buttonMethod();
};
myButton.addEventListener("click", clickButton);

여기서 clickButton 은 일반 함수처럼 생겨서 this 가 전역 객체를 참조할 것만 같다.

하지만, 이 함수는 이벤트를 처리하기 위한 이벤트 핸들러로의 역할을 하게 되면서 this 는 이벤트를 받는 HTML 요소를 참조하게 된다.

그렇다면 버블링이 발생했을 때에는 this 가 어떤 요소를 참조할지 살펴보자

<div id="outer">
  버블링 클릭
  <button id="inner1">자식 버튼1</button>
</div>

div 태그 내부에 button 태그 두 개를 자식 요소로 넣은 뒤, 클릭 이벤트를 각각 걸어주었다

const bubbleEvent = document.getElementById("outer");
const inner1Event = document.getElementById("inner1");

function bubbleFunction() {
  console.log(this);
};

bubbleEvent.addEventListener("click", bubbleFunction);
inner1Event.addEventListener("click", bubbleFunction);

이렇게 작성한 뒤에 자식 버튼1 버튼을 클릭하면, 이벤트 버블링에 의해서 div 태그의 이벤트 핸들러도 동작한다.

콘솔에 찍히는 것은 inner1 HTML 요소 다음에 outer HTML 요소가 됐다!

결국, 이벤트 핸들러가 동작하는 현재 요소를 참조하는 것 같다.

4-5. 화살표 함수와 this

자바스크립트에서 화살표 함수는 정말 많이 사용한다.

화살표 함수 내부의 this 는 조금 다르게 동작하는데, 이유는 어휘적 바인딩을 따르기 때문이다.

const obj = {
  a: 1,
  method1() {
    setTimeout(function () {
      console.log(this) // window(브라우저)
    }, 1000);
  },
  method2() {
    setTimeout(() => {
      console.log(this) // obj 객체
    }, 1000)
  }
};

obj.method1();
obj.method2();

여기서 setTimeout을 사용한 이유는 함수가 실행될 때가 아닌, 함수가 호출된 스코프를 이용하는 동작을 보여주기 위해서다

두 메서드는 같은 setTimeout 을 이용하는데, this 의 결과가 다르다.

화살표 함수에서의 this 는 호출 시점이 아니라, 선언 되었을 때의 this 를 참조하기 때문이다. 즉, 렉시컬 스코프를 이용하는 것이라고 할 수 있다.

만약 일반 함수에서도 this 가 렉시컬 스코프를 이용하게 하고 싶으면 어떻게 하면 될까??

const obj = {
  a: 1,
  method1() {
    const that = this
    setTimeout(function () {
      console.log(that) // obj 객체
    }, 1000);
  },
};

위 코드처럼 other = this 가 참조하는 것을 할당해주면 된다.

ES6 이후에 나온 화살표 함수는 이러한 동작을 대신하기 위해 나온 것이라고 한다.

👉 주의해야할 점은 화살표 함수나 other = this 를 이용하는 방법은, 온전히 this 를 이용하는게 아니라 렉시컬 스코프를 이용하는 것이라는 거다! 👈

그러니까 두 가지 방법을 이용하지 않고 그냥 렉시컬 스코프를 쓰거나, 둘 중에 한 가지 방법만 선택해서 쓰는게 좋다고 한다.

4-6. 객체 체인과 this

여러 객체가 서로 연결되어 있을 때 this 의 결과가 어떻게 나오는지 확인해보자.

function methodFunc() {
  console.log(this.a);
};

const obj2 = {
  a: 100,
  obj2Method: methodFunc
};

const obj1 = {
  a: 10,
  obj2: obj2,
};

obj1.obj2.obj2Method(); // 100

중첩된 객체가 있을 때에도 결국 호출 시점이 this 에 영향을 준다는 것을 확인할 수 있다.

obj1 객체의 a 값은 영향을 주지 않고, 호출부인 obj2this 가 참조하게 된다.

4-7. call, apply와 this

Function.prototype.call() 은 첫 번째 인자로 this 가 참조할 값을 넣고, 뒤에는 그 함수에서 요구하는 인자 목록을 각각 넣어준다.

Function.prototype.apply() 는 첫 번째 인자로 this 가 참조할 값을 넣고, 뒤에는 그 함수에서 요구하는 인자 목록을 배열로 넣어준다.

const callObj1 = {
  name: "최",
  greeting(age, height) {
    console.log(`이름: ${this.name}--, 나이: ${age}, 키: ${height}`);
  }
}

const callObj2 = {
  name: "박"
}
const data = [25, 165];
callObj1.greeting(...data);
callObj1.greeting.call(callObj2, ...data);
callObj1.greeting.apply(callObj2, data);
// 이름: 최--, 나이: 25, 키: 165
// 이름: 박--, 나이: 25, 키: 165
// 이름: 박--, 나이: 25, 키: 165

코드의 출력 결과를 통해서 알 수 있듯이, call() 의 사용 여부에 따라 this 가 참조하는 객체가 달라졌음을 알 수 있다.

call(callObj2)apply(callObj2) 를 통해서 greeting() 함수가 실행될 때 this 가 참조하는 객체를 callObj2 로 지정해준 것이 된다.

만약 this 가 참조할 객체를 null 로 지정해서 넘겨준다면 전역 객체를 참조하게 된다.

추가적으로 apply()call() 은 다른 방법으로도 활용할 수 있다. 바로 유사 배열 객체를 배열로 만드는 것!

function sum() {
  const args = Array.prototype.slice.apply(arguments);
  console.log(arguments.reduce((acc, cur) => acc + cur, 0)); // TypeError: arguments.reduce is not a function
  console.log(Array.from(arguments).reduce((acc, cur) => acc + cur, 0)); // 정상적으로 나온다.
  return args.reduce((acc, cur) => acc + cur, 0);
}

console.log(sum(1, 2, 3)); // 6
console.log(sum(4, 5, 6, 7)); // 22

arguments 는 함수에 전달된 인자를 배열 형태로 보여준다. 하지만, 그렇다고 바로 배열 메서드를 활용할 수 없다.

여기서 applu(arguments) 를 해주면, thisarguments 로 하기 때문에 Array.prototype.slice 에서 이용할 수 있는 것이라고 한다.

그리고 ES6 이후부터는 Array.from() 메서드를 이용해서 유사 배열 객체를 배열로 이용할 수 있게 된다.

4-8. bind와 this

Function.prototype.bind() 역시 call()apply() 와 비슷한 동작을 하지만, 약간의 차이점이 있어서 분리해서 정리했다.

bind() 는 함수를 바로 호출하는게 아니라 새로운 함수를 반환하는 것이 차이라고 할 수 있는데, 예시를 보면서 이해해보자

function addNumber(a, b) {
  console.log(this);
  return a + b;
};

addNumber.call({name: "박"}, 10, 20); // 30

const newAddNumber = addNumber.bind({name: "박"}, 10, 20);
newAddNumber(); // 30

위 코드를 보면 똑같이 this 가 참조하는 값을 첫 번째 인자로 전달하는 것을 알 수 있다.

하지만, bind() 를 사용한 경우 함수를 반환해서 새로운 함수를 만들기 때문에 이 새로운 함수를 실행했을 때 출력되는 것을 확인할 수 있다.

bind() 를 사용하면 사용할 this 를 고정할 수 있다!

5. 결론

위에서 4가지 규칙이 있다고 했지만 제대로 언급하지 못했다.

this 바인딩의 순서를 결정짓는 4가지 규칙과 순서를 정리하면서 마무리해보자

  1. new 생성자 함수를 이용해 함수를 호출한 경우

    이 경우에는 새로 생성한 객체를 this 가 참조한다.

  2. call, apply 그리고 bind를 이용해 함수를 호출한 경우

    이 경우에는 명시적으로 지정한 객체를 this 가 참조한다.

  3. 객체와 연결된 경우

    이 경우에는 그 객체를 this 가 참조한다.

  4. 위 3가지에 속하지 않는 경우

    이 경우에는 기본값인 전역 객체를 this 가 참조한다.

이 4가지 규칙과 순서에 의해서 this 바인딩이 결정되고, 화살표 함수는 이 규칙에 포함되지 않는 예외이다. 그래서 그냥 렉시컬 스코프를 쓰라고 한 것

this 는 쉽게 파악하기 힘든 것 같지만, 그만큼 유용하게 사용할 수 있을 것 같다.

6. 참고한 자료

You Dont Know JS : this와 객체 프로토타입, 비동기와 성능

profile
개발자가 되기 위해 꿈틀대고 있습니다.

0개의 댓글