ARTICLE | 가깝지만 먼 당신... this

noopy·2021년 8월 17일
2

📃 ARTICLE

목록 보기
2/5
post-thumbnail

도대체 어딜 바라보는지 알 수 없는 this를 간파해보자.
참고) 코어 자바스크립트



this는 실행 컨텍스트가 생성될 때 결정된다.
실행 컨텍스트는 함수가 호출될 때 생성되므로,
함수 호출 시점에 따라 this가 바라보는 대상이 달라질 수 있음을 의미한다.

🧐 함수 호출과 this

전역 공간에서의 this

전역 공간에서 this는 전역 객체를 가리킨다.
브라우저 환경에서 전역객체는 window, Node.js 환경에서는 global이다.

console.log(this === window) // true

메서드로써 호출 시, 메서드 내부의 this

어떤 함수를 호출할 때, 함수 이름 앞에 객체가 있는 경우,
그 함수는 해당 객체의 메서드이다.

메서드로써 호출할 때,
즉 점표기법(.)이나 대괄호표기법[]으로 메서드를 호출한다면
호출 대상의 객체가 this가 된다.

const obj = {
  method: function (x) { console.log(this, x); }
};
obj.method(1) // { method: f }, 1
obj['method'](2) // { method: f }, 2

함수로써 호출 시, 함수 내부의 this

함수로써 호출할 때, this는 스코프 체인상의 최상위 객체인 window가 된다.
왜냐하면 함수는 호출 시 호출 주체(객체 지향에선 객체)를 명시하지 않기 때문에,
호출 시 실행 컨텍스트에 this가 지정되지 않고,
이럴 경우 this는 전역객체를 바라보게 된다.

😎 정리: 함수를 호출했을 때 앞에 점(.) | 대괄호[] 표기를 사용했다면
메서드로써 호출된 것이고,
이때의 this는 메서드 구문 앞에 객체가 된다.
함수로써 호출된 경우는 this가 전역 객체(window)가 된다.

this에 관해서 내가 오해하고 있던 사실은,
함수로써 호출될 때에도 해당 스코프에 this가 없다면
상위 객체에 닿을 때까지 올라가는 형식인 줄 알았다.
하지만 함수가 호출되고 실행 컨택스트가 만들어지면 this가 없기 때문에
this는 바로 전역 객체가 된다.

콜백함수 호출 시, 함수 내부의 this

A 함수의 인자로 B함수를 전달할 때, B함수를 콜백함수라 한다.
A 함수는 B 함수의 내부 로직에 따라 실행되고, this 또한 이 때 값이 결정된다.

document.body.querySelector('#a')
  .addEventListener('click', function (e) {
    console.log(this, e) // this = document.body.querySelector('#a')
})

콜백함수 또한 함수이기 때문에 this를 지정하지 않으면 this = window가 되지만,
addEventListener 내부의 콜백함수는 addEventListener 메서드의 this를 상속받는다.
즉, A 함수(메서드)가 콜백 함수의 this를 결정하고,
특별히 없을 경우 콜백 함수의 this는 전역 객체를 바라본다.

생성자 함수 내부의 this

객체지향 언어에서 생성자는 클래스(class)이고,
클래스를 통해 만든 객체를 인스턴스(instance)라 한다.
JS에선 생성자 함수를 통해 함수 또한 생성자의 역할을 할 수 있다.
어떤 함수가 new 키워드로 호출되면, 그때의 this는 곧 새로 만들 인스턴스 자신이 된다.

let Cat = function (name, age) {
  this.bark = '야옹';
  this.name = name;
  this.age = age;
};

let cheeze = new Cat('치즈', 7);
console.log(cheeze);
// Cat { bark: '야옹', name: '치즈', age: 7 } => 인스턴스 객체

🔥 과정 설명
Cat 변수에 name, age를 인자로 받는 익명 함수를 할당함.
👇🏻
함수 내부에선 this에 접근해 각 프로퍼티에 값을 대입.
👇🏻
new 키워드로 Cat 함수(생성자 함수)를 호출해 변수 cheeze에 할당.
👇🏻
console.log로 출력 시 Cat 클래스의 인스턴스 객체가 출력.

즉 cheeze에 할당된 생성자 함수 내부의 this는 cheeze 인스턴스를 바라본다.

🧐 this 바인딩

현재 컨텍스트에 this가 없다면, 바로 전역객체로 올라가지 않고
직전 컨텍스트의 this를 바라보게 할 순 없을까?

1. 변수 활용하기

이 방법은 꼼수라고 할 수 있다.
상위 스코프의 this를 변수에 저장한다음 내부 함수에서 변수를 사용하는 방법이다.

// obj라는 객체가 있고, outer 메서드가 있는 상황.
// outer 메서드 안에 inner라는 함수가 있음 (메서드 아님)
const obj = {
  outer: function () {
    let self = this;
    let inner = fuction () {
      console.log(self);
    };
    inner();
  }
};
obj.outer(); // this = obj

🔥 과정 설명
outer 스코프에서 self 변수에 this를 저장한다.
👇🏻
outer 스코프는 obj.outer() 형태로 실행되는데
이때 outer 메서드의 실행 컨텍스트 내부에 this = obj가 된다.
👇🏻
그다음 outer 메서드의 내부 위에서 아래로 파싱하며 self 변수에 this(=obj)가 할당된다.
👇🏻
inner() 가 실행되면 console.log(self) 는 obj가 출력된다.

2. 화살표 함수 사용하기

ES6에서, 함수 호출 시
this가 전역객체를 바라보는 문제를 보완하고자 화살표 함수를 도입했다.
화살표 함수는 함수 호출 시 실행 컨텍스트에서 this 바인딩 과정을 생략해 this가 아예 없고,
스코프 체인 상 가장 가까운 this에 접근한다.
그 전까지는 왜 이방법을 사용하지 않았냐면,
ES5까진 this의 이런 문제가 설계상의 오류였기 때문이라 한다 ㅋㅋㅋ

let obj = {
  outer: function () {
    console.log(this); // this = obj
    
    let inner = () => {
      console.log(this); // this = obj
    };
    inner();
  }
}

🧐 명시적으로 this 바인딩

call 메서드

call 메서드는 첫 번째 인자를 this로 바인딩하고,
나머지 인자들을 호출할 함수의 매개변수로 지정한다.

call 메서드를 활용하면 임의의 객체를 this로 지정할 수 있다.

const func = function (a, b, c) {
  console.log(this, a, b, c);
};

func(1, 2, 3); // this = window
func.call({ x: 1 }, 4, 5, 6); // this = {x : 1}
const obj = {
  a: 1,
  method: function (x, y) {
    console.log(this.a, x, y);
  }
};

obj.method(2, 3) // 1, 2, 3
obj.method.call({a : 4}, 5, 6) // 4, 5, 6

apply 메서드

첫번째 인자를 this로 바인딩하고,
두번째 인자를 배열로 받아 해당 배열의 요소들을 호출할 함수의 매개변수로 지정한다.

const func = function (a, b, c) {
  console.log(this, a, b, c);
};

func.apply({x : 1}, [4, 5, 6]); // {x : 1}, 4, 5, 6

const obj = {
  a: 1,
  method: function (x, y) {
    console.log(this.a, x, y);
  }
};
obj.method.apply({a: 4}, [5, 6]); // 4, 5, 6

bind 메서드

bind 메서드는 넘겨받은 this와 인수들을 바탕으로 새 함수를 반환한다.

const func = function (a, b, c, d) {
  console.log(this, a, b, c, d);
};

func(1, 2, 3, 4); // window{...}, 1, 2, 3, 4

const bindFunc = func.bind({x: 1})
bindFunc() // {x: 1} undefined undefined undefined undefined

const bindFunc2 = func.bind({x: 1}, 4, 5);
bindFunc2(); // {x: 1}, 4, 5, undefined, undefined
bindFunc2(6, 7); // {x: 1}, 4, 5, 6, 7
bindFunc2(8, 9); // {x: 1}, 4, 5, 8, 9

bindFunc 변수는 this를 {x: 1}로 지정한 새 함수가 담긴다.
bindFunc2 변수는 this를 {x: 1}로,
앞에서 두 개의 인수를 4, 5로 지정한 새 함수를 할당한다.

bind로 탄생한 새 함수는 흔적을 남긴다.

bind 메서드로 반환된 새 함수는 name 프로퍼티bound 접두사가 붙는다.

const func = function (a, b, c, d) {
  console.log(this, a, b, c, d);
};
console.log(func.name) // func

const bindFunc = func.bind({x: 1});
console.log(bindFunc.name)// bound func

원본 함수 func에 bind를 적용한 함수라는 걸 알 수 있는 덕분에 코드 추적에 용이하다.

내부함수에 this 전달하기 with call / bind

앞에 🧐 this 바인딩 1. 변수 활용하기에서 썼던 예시를
callbind로 깔끔하게 처리할 수 있다.

const obj = {
  outer: function () {
    console.log(this); // outer 메서드의 this = obj
    const inner = function () {
      console.log(this);
    };
    inner.call(this); // call 메서드로 즉시실행 + this 바인딩 to obj
  }
};
obj.outer();
const obj = {
  outer: function () {
    console.log(this); // this = obj
    
    const inner = function () {
      console.log(this);
    }.bind(this);
    inner(); // this = obj
  }
};
obj.outer();

💕 느낀점

자바스크립트의 유연함 덕분인지 this는 함수 호출에 따라 바라보는 객체가 매우 다르다.
그 때문에 여태까지 대충 넘어가 마음 한 켠에 부담으로 남아있었는데,
한번 쭉 훑고 나니 부담이 사라졌다.
아직 3주차이긴 하지만 여태까지 나는 어떠한 지식을 공부하기 위해
처음부터 끝까지 단계적으로 밟아나가려는 욕심이 있었다.
그렇게 하지 않으면 전에 배우지 못했던 지식들이 다음에 발목을 잡을 것 같기도 해서인데...
워낙에 강의에서 배울 게 많다보니 자연스럽게 이 공부법을 버리게 되었고,
당장에 필요하고 기초적인 지식들을 우선적으로 공부하고자 하는 욕심이 더 커졌다.
마치 가지치기를 하는 것처럼 그때 그때 필요한 지식들을 얻으니 오히려 더 안정적이고 부담감이 덜 하다.
또 단순히 읽는 것에 그치지 않고 TIL 이나 article로 남기니 한 문장을 적기 위해 여러번 곱씹는 과정에서 머릿속에 각인되는 것도 있다 ㅎㅎ
내 머릿속에 지식들이 쌓이는 게 기특하고 재밌다 힛😎

profile
💪🏻 아는 걸 설명할 줄 아는 개발자 되기

0개의 댓글