this 깨 부수기

GI JUNG·2023년 11월 14일
0

javascript

목록 보기
12/12
post-thumbnail

🍀 THIS

this를 공부하긴 했는데 throttle예제를 보다가 this의 동작에 대해 헷갈려서 제대로 정리해보고자 한다. this는 화살표 함수가 나온 이유 중 하나인데, 화살표함수를 사용하면 this binding을 lexical scope를 이용하여 더 직관적으로 알 수 있다. this를 공부하면서 정말 머리 아픈 것은 callback으로 들어가 다른 함수에게 권한을 위임하는 순간 예측을 할 수 없다는 것이다.

다음 예시를 한 번 봐보자

// 예시 1
document.addEventListener("DOMContentLoaded", function () {
  console.log("함수가 콜백일 때 this는??", this) // 🤔
})
// 예시 2
function foo() {
  function bar() {
    console.log("함수가 콜백일 때 this는??", this) // 🤔
  }
  
  bar();
}
// 예시 3
document.addEventListener("DOMContentLoaded", () => {
  console.log("화살표 함수가 콜백일 때 this는??", this) // 🤔
})

에시 3은 화살표 함수는 document.addEventListener 내에 정의되어 있으며, 전역 스코프에서 선언돼있다. 화살표 함수는 lexical 환경에 binding된 this를 참조하기 때문에 strict mode에서는 undefined strict mode가 아닐 시 window를 가리키게 된다.

하지만, 화살표 함수가 아닌 함수가 callback으로 던져졌을 때는 테스트를 해보지 않으면 알 지 못할 것이다.

들어가기 앞서 화살표 함수가 아닌 함수가 this의 binding 동작은 함수가 호출되는 방식에 따라서 다르다는 것일반적인 함수에서의 this는 전역객체(window, global, globalThis)를 가리킴을 염두해 두어야 한다.

내가 공부하면서 this를 이해할 때 중요하게 작용한 3가지이다.

❗️❗️❗️ important
1. 함수가 호출되는 방식에 따라 this가 binding되는 값이 다름
2. 일반적으로 함수는 전역객체(window, global, globalThis)를 가리킨다.
3. 함수가 호출되는 시점 👉🏻 화살표 함수에 관함

여기서 3번의 이유는 특히 화살표 함수를 이해하는데 있어서 도움이 됐는데 함수가 호출되는 순간 실행컨텍스트가 실행되어 record안에 담을 this의 값을 결정하기 때문이다.

1️⃣ 전역공간에서의 THIS

전역 공간에서의 this는 매우 간단하다. 그저 최상위 객체와 binding된다. 이 때 runtime 환경에 따라서 this는 다르다.
EX) browser -> window, nodejs -> global

// browser에서 test시
console.log('전역공간에서의 this', this); // window
// nodejs에서 test시
console.log('전역공간에서의 this', this); // global

2️⃣ 함수 & 메서드

앞서 말했듯이 this는 호출되는 방식에 따라서 binding되는 값이 다르다고 했다. 함수가 호출되는 방식 중 함수로서 호출되는지 메서드로서 호출되는지에 따라서 this와 binding되는 값은 다르다.
이에 대해 알아보기 앞서 함수와 메서드의 차이를 짚고 넘어가보자.

🤔 함수 VS 메서드

함수와 메서드는 어떤 동작을 수행하는 점에서 같은데 무엇이 다를까?
본인은 메서드와 함수를 구분하는 방법은 객체 안에서 property로서 선언된 함수를 메서드라고 알고 있었다.
코어 자바스크립트에서 되게 명확하게 설명해주는데 이것을 짚어보자. 이 책에서는 객체를 통해 호출되는 함수를 메서드라고 정의한다.

function printInfo (arg) {
  console.log('this', this);
  console.log('arguement', arg);
  console.log(`name: ${this.name}\tage: ${this.age}`);
}

const person = {
  name: 'gi jung',
  age: 29,
  printInfo
}

printInfo.call({name: 'someone', age: 30}, 'something') // 1️⃣ 함수로서 호출됨
console.log("-".repeat(10))
person.printInfo(); // 2️⃣ 메서드로서 호출됨

1️⃣ 함수로서 호출: printInfo 함수가 단독으로 호출되었다. .(dot)이나 대괄호를 통해 호출된 적이 없다.
2️⃣ 메서드로서 호출: printInfo 함수는 .(dot)을 통해 호출되었다. 이는 객체를 통해서 함수를 호출함을 의미한다.

💡 따라서, 메서드와 함수의 구분은 호출되는 시점에 객체를 통한 호출인지 아닌지에 대한 유무이다.

예시

function printInfo (arg) {
  console.log('함수로서 호출된 this 👉🏻', this);
}

const person = {
  name: 'gi jung',
  age: 29,
  printPerson: function () {
    console.log('메서드로서 호출된 this 👉🏻', this);
    console.log(`name: ${this.name}\tage: ${this.age}`);
  }
}

printInfo() // 1️⃣ 함수로서 호출됨
console.log("=".repeat(10))
person.printPerson(); // 2️⃣ 메서드로서 호출됨

다시 한 번 복습해보자. 서론에서 일반적으로 this는 전역객체와 binding된다고 하였다. 따라서 함수가 함수로서 호출된 1️⃣ 은 this가 전역객체(window)를 가리킨다.
2️⃣에서 printPerson이란 함수는 person이란 객체를 통해서 호출되었다. 이는 일반적인 동작과 다르게 함수가 메서드로서 호출되는 경우는 자신을 호출한 객체를 가리킨다. 여기서 메서드로 호출되었다는 것은 .(dot) 또는 []대괄호를 통해 호출되었음을 의미한다.

🤔🤔🤔🤔 그럼 여기서 궁금증이 생긴다. 그럼 .을 여러번 이용해서 메서드가 호출되었다면 어느 객체를 가리켜야할까?

궁금증을 해결해보기 위해서 person에 family 객체를 property로서 정의해보자.

const person = {
  name: 'gi jung',
  age: 29,
  family: {
    mom: 'mom',
    dad: 'dad',
    printParent: function () {
      console.log('.(dot)을 두번 이용하여 호출된 this는?? 👉🏻', this)
    }
  },
  printPerson: function () {
    ...//
  }
}

person.family.printParent(); // 🤔 뭐가 나올까?
person["family"]["printParent"]();

본인은 테스트하기 전에 person이라고 예상했다. 하지만 person.family가 this와 binding된다.

.(dot) 또는 대괄호를 연달아서 호출하는 것과 상관없이 메서드를 호출할 때 .(dot) 또는 대괄호 바로 앞에 있는 객체와 binding된다.

그리고 아래는 함수를 참조값으로 전달하여 호출 시 어떻게 되는지 궁금해서 테스트 해 본 것이다.

function printInfo (arg) {
  console.log('함수로서 호출된 this 👉🏻', this);
}

const person = {
  name: 'gi jung',
  age: 29,
  family: {
    mom: 'mom',
    dad: 'dad',
    printParent: function () {
      console.log('.(dot)을 두번 이용하여 호출된 this는?? 👉🏻', this)
    }
  },
  printPerson: function () {
    console.log('메서드로서 호출된 this 👉🏻', this);
    console.log(`name: ${this.name}\tage: ${this.age}`);
  }
}

const printPerson = person.printPerson;
printPerson(); // window

위에서 공부한대로 값을 예상해보면 window가 나옴을 확인할 수 있다. 함수의 참조 값을 할당했다고 해도 호출되는 방식을 보면 .(dot)을 이용한 호출 즉, 객체를 통한 호출(메서드 호출)이 아니다. 이렇듯 함수가 호출될 때 this가 결정된다.

그럼 이제 함수와 메서드에서 this가 binding되는 값을 정리해보자.

📚📚 정리
1. 함수로서 호출 시: 일반적인 함수의 동작처럼 최상위객체(window, global)를 가리킨다.
2. 메서드로서 호출 시: 객체를 통한 호출시 객체와 binding되며 .(dot) 또는 []대괄호를 얼마나 많이 연달아 쓰던 상관없이 함수호출 . 또는 [] 바로 앞의 객체와 binding된다.

3️⃣ 생성자 함수에서 this

생성자 함수도 함수와 다를바가 없지만, new keyword를 이용하여 함수를 호출하면 class와 같이 동작한다. 물론, prototype을 가져오지 않고 static을 선언할 수 없는 등 다른 차이점이 존재한다. 다른 블로그 등 서적에서는 blueprint(청사진)으로 설명을 많이하며 공통적인 속성들을 인스턴스화할 수 있다. 먼저 생성자 함수를 어떻게 쓰는지 살펴보자

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.printInfo = function() {
    console.log(`name: ${this.name}\tage: ${this.age}`);
  }
}

const jjung = new Person("jjung", 29); // 👉🏻 생성자 함수 호출
console.log(jjung);

class를 이용하면 constructor를 이용하여 멤버들을 초기화할 수 있듯이 this를 이용하여 속성들을 선언하며 접근할 수 있다. 앞서 언급했듯이 생성자 함수 호출은 new를 이용하여 호출한다. 이 때, 함수의 앞 글자는 Component처럼 대문자여야만 할 줄 알았는데 관례적으로 대문자로 쓴다고 하며 소문자로 작성해도 상관없다.

printInfo라는 함수가 있는데 console안에 this를 이용하여 속성에 접근한 것을 볼 수 있다. 아래 console에 찍힌 것을 보면

name과 age를 속성으로 가지는 객체가 출력됨을 볼 수 있다. new keyword를 이용하여 함수를 호출하면 사진에서 볼 수 있듯이 객체가 만들어지며 이렇게 만들어진 객체를 인스턴스(instance)라고 한다. 이 때 this는 이 인스턴스를 가리킨다. 즉, this는 생성자 함수 호출을 통해 만들어진 객체를 가리키게 된다.

한 번 this를 찍어보자!!


function Person(name, age) {
  ...//
  console.log('생성자 함수에서의 this ->', this);
}

const jjung = new Person("jjung", 29); // 👉🏻 생성자 함수 호출

📚📚 정리
정리를 해 보자면 함수가 new keyword를 이용하여 생성자 함수로서 호출되면 this는 instance된 객체를 가리킨다.

그리고, 내가 이것저것 실험하면서 헷갈렸던 문제가 있다🥲🥺. 바로 화살표 함수를 이용했을 때 어떻게 동작하는 것이냐는 것이다.

// 생성자 함수로서 호출
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.printInfo = function() {
    console.log(`name: ${this.name}\tage: ${this.age}`);
  }

  this.printWithArrowFunction = () => {
    console.log("생성자 함수 내 화살표 함수는 어떤 this를 가리킬까?", this);
  };
}

const jjung = new Person("jjung", 29);
jjung.printWithArrowFunction();
// 메서드로서 호출
const person = {
  name: 'jjung',
  age: 29,
  printWithArrowFunction: () => {
    console.log("객체 내 화살표 함수는 어떤 this를 가리킬까?", this);
  }
}

person.printWithArrowFunction();

첫 번째 코드는 생성자로서 호출이고 두 번째 코드는 메서드로서 호출이다. 아직 화살표함수에 대해서 다루지 않았으니 this가 자기 자신을 가리킨다고 생각할 것이다. 하지만, 화살표 함수라면 일반 함수와 다르게 동작을 한다. 결과는 아래와 같다.

첫 번째 코드 결과: { name: 'jjung', age: 29 }
두 번재 코드 결과 : window

왜 그럴까???라고 의문이 드는 것이 당연하다. 스스로 만든 예제는 나의 무지를 일깨워주었고 실행 컨텍스트까지 공부를 하고 나서야 깨닫게 되었다. 일단 이유는 화살표 함수에서는 this를 binding의 부재가 존재하며 this와 binding을 위하여 상위 lexical scope(call stack기준 하위)를 타고 올라간다.
아래에서 화살표 함수를 다루겠지만 this를 완벽히 이해하기 위해서는 적어도 실행 컨텍스트 기본적인 부분을 알아야 이해할 수 있을 것 같다. 실행 컨텍스트에 대해서 간략이 요약하면 아래와 같다.

  1. 실행컨텍스트는 함수가 호출될 때 생성된다.
  2. 함수가 호출되어 동작을 수행할 때 필요한 정보들을 가진 코드다.
  3. 실행컨텍스에는 lexical environment = environment record(scope내 식별자를 기록) + outer environment reference(상위 lexical environment를 참조)로 이루어져 있다.

4️⃣ 콜백에서 this

이해를 해야 암기가 되는 나에게 this를 공부하면서 정말 어려웠던 부분이다. 일단, this를 공부하는 사람들에게 콜백에서의 this의 동작은 이해하려 하지지 말라고 말해주고 싶다. 🥺🥺🥺

그냥 자기 멋대로다. this에 관해서 계속 찾아보고 책도 읽어보고 하면서 가장 와닿은 문장이 있었다. javascript의 설계상의 오류라는 문장이다. javascript를 제외한 다른 언어에서는 일반적으로 this는 class에서 주로사용되고 만들어지는 인스턴스를 참조하게되어있다. 근데 javascript에서 this는 되도록 이해보다는 암기에 가까운 것 같다.

콜백이란 함수가 다른 함수의 인자로 전달되는 함수로서 콜백을 받는 함수에게 제어권을 위임하는 함수이다.

💡 콜백함수란?

  • 다른 함수의 인자로 전달되는 함수
  • 콜백을 받는 함수에게 제어권을 위임하는 함수

다시 말하지만, 콜백에서의 this는 예측할 수 없다. 그냥 테스트해보는 것이 빠르다고 생각된다.
그래도 공부하면서 얻은 중요한 포인트는 콜백이 다른 함수에게 제어권을 준다는 것이다. 이 말을 좀 더 쉽게 풀어서 말해보자면 제어권을 가진 함수가 콜백이 호출될 때 this를 결정한다는 것이다. 아직 무슨 말인지 이해가 안 갈 수도 있지만 예시를 통해서 체득을 해 보자.

⭐️⭐️⭐️ important
콜백에서의 this는 콜백이 인자로서 다른 함수에 넘겨질 때 다른 함수는 콜백의 제어권을 갖는다. 이 때 제어권을 갖는 함수가 콜백함수 호출 시 this를 결정짓는다.

예시

// 예시 1
setTimeout(function () {
  console.log('setTimeout에서의 callback의 this는??', this); // 1️⃣
}, 1000)

// 예시 2
const arr = [1, 2, 3];
arr.forEach(() => 
	console.log('forEach에서의 callback의 this는??', this) // 2️⃣
)

// 예시 3
document.addEventListener("DOMContentLoaded", function () {
  console.log('setTimeout에서의 callback의 this는??', this); // 3️⃣
})

1️⃣에서 console을 출력하는 콜백함수의 제어권을 가진 함수는 setTimeout함수이다. setTimeout은 콜백함수의 this를 지정해 주지 않으므로 콜백함수는 함수로서의 호출과 같이 동작하여 this는 최상위 객체를 가리킨다.

2️⃣에서 제어권을 가진 함수는 forEach함수이다. forEach함수도 1️⃣과 마찬가지로 콜백함수의 this를 결정지어주지 않아서 최상위 객체를 가리킨다.

3️⃣에서 제어권을 가진 함수는 addEventListener이다. 1️⃣과 2️⃣와 다르게 addEventListener함수는 콜백에게 this가 무엇인지 알려주기로 한다. 이 때 this는 document객체라고 결정지어주어서 console에는 document 객체가 출력된다.

이와 같이 콜백에서의 this의 동작은 제어권을 가진 함수가 this를 결정한다. 이 때 this를 결정하지 않으면 일반 함수처럼 this binding이 결정된다.

🤔 그럼 this를 내 마음대로 지정하지 못 하는 것인가???

  • ⑴: call, apply, bind를 이용하면 내가 원하는 값과 this를 binding할 수 있다. 이는 아래에서 다룬다.
  • 변수 또는 closure를 활용

⑴은 아래에 나오는 내용으로 ⑵에 관해서 다뤄보자.

변수를 이용한 this 지정

// custom foreach 정의
function customForEach(callback, iter) {
  const self = iter;
  
  for (const num of iter) {
    callback(num, self)
  }
};

const arr = [1, 2, 3];

customForEach((num, self) => {
  console.log(`num: ${num}\t`, "this:", self)
}, arr)

이렇게 변수에 원하는 값을 callback에 넘겨주어서 내가 원하는 값에 this와 binding할 수 있다.

<결과>

closure를 이용한 this 지정

const person = {
  name: 'jjung',
  age: 29,
  outer: function () {
    const self = this;

    function inner() {
      console.log('this', self); // self를 this로 바꾸면 window가 나온다. ❌
    }

    inner();
  }
}

person.outer();

outer는 메서드로서 호출이 되었다. 따라서 outer 함수는 호출이 되어 실행컨텍스트를 만들 때 this = person으로 binding한다. 그리고 self에 this를 할당하면 self = this = person이 되므로 self는 결국 person 리터럴 객체값을 가리킨다. inner는 함수로서의 호출이지만 closure를 통해 self에 접근할 수 있으므로 this를 내가 원하는 person값과 binding하는 것이 가능해진다.
만약 console에 있는 self -> this로 바꾸게 되면 일반함수 호출로서 전역객체를 가리키게 된다.

<결과>

이 방법을 구현하면서 제어권을 가지는 함수가 저런 느낌으로 this를 결정짓게 만들겠구나라고 생각이 든다.(개인의견!!🤣)

정리해보자면 콜백에서의 this의 바인딩은 간단하다

📚📚 정리
그저 테스트를 통해 확인하는 방법말고 없는 것 같다. 🥲

5️⃣ call, apply, bind

4️⃣에서 콜백을 다루면서 원하는 this로 binding하기 위해 트릭을 사용했다. 하지만, call, apply, bind메서드를 이용하여 내가 원하는 값과 this를 binding할 수 있다. 이를 명시적 바인딩이라고 한다.

call & apply

call과 apply는 this를 원하는 값으로 binding시킨다는 점에서는 같지만 call은 인자를 풀어서 받고 apply는 배열로 받는다.

사용
call: 함수.call(this와 binding되길 원하는 값, ...인자들)
apply: 함수.call(this와 binding되길 원하는 값, [인자들])

예시는 간단하게 이름을 print하는 함수로 만들어보자.
먼저 this를 binding하지 못하는 것을 보자

// this = window
function printName() {
  console.log('name:', this.name);
}

printName(); // 👉🏻 name: undefined

this.name은 undefined로 나온다. 전역에서 호출된 것이므로 this=window가 될 것이고 name이라는 변수는 선언한 적이 없으니 window에는 없는 속성이다. 따라서, undefined가 나온다.

그럼 내가 원하는 이름이 잘 출력되게끔 {name: 'jjung'}이란 객체와 binding을 어떻게 할까???
코드를 보자

// call을 이용한 this binding
function printName() {
  console.log('name:', this.name);
  console.log('this', this);
}

const person = { name: 'jjung' };
printName.call(person); // 👉🏻 name: jjung
// call을 이용한 this binding
function printName() {
  console.log('name:', this.name);
  console.log('this', this);
}

const person = { name: 'jjung' };
printName.apply(person); // 👉🏻 name: jjung

내가 원하는 값인 { name: 'jjung' }객체와 this가 잘 binding되는 것을 볼 수 있다.

이제 두 번째 인자가 어떻게 쓰이는지 살펴보자. call과 apply의 두 번째 인자는 함수를 실행시킬 때 전달할 인자값을 call은 풀어서 apply는 배열 내에 담아서 사용하면된다. 적절한 예시로 값을 더하는 예시를 보자

// call
function add (...rearNums) { // ⭐️
  return [...this, ...rearNums].reduce((a, b) => a + b);
}

const frontNums = [1, 2, 3];
const sum = add.call(frontNums, 4, 5, 6)
console.log('sum =', sum);
// apply
function add (...rearNums) { // ⭐️
  return [...this, ...rearNums].reduce((a, b) => a + b);
}

const frontNums = [1, 2, 3];
const sum = add.apply(frontNums, [4, 5, 6])
console.log('sum =', sum);

call과 apply가 두 번째 인자로 전달하는 방식이 다르며 apply는 배열로 넘겨주었어도 자체적으로 인자를 풀어서 받아온다. 이렇게 call과 apply를 이용하여 this = frontNums = [1, 2, 3]과 잘 binding한 것을 확인할 수 있다.

💡💡💡
이 때, 주의사항이 있는데 화살표 함수를 사용한다면 this를 lexical scope에서 찾기 때문에 다른 방법을 사용해야 한다. 예를 들면 closure를 이용한 currying이용하여 해결할 수 있다.

const flat = (arr) =>
  arr.reduce((acc, cur) => {
    Array.isArray(cur) ? acc.push(...flat(cur)) : acc.push(cur);
    return acc;
  }, []);

const flattenAndConcat = (...arrs) => {
  return arrs.reduce((acc, cur) => {
    if (cur.some((element) => Array.isArray(element))) {
      cur = flat(cur);
    }
    return [...acc, ...cur];
  }, []);
};

const add =
  (...frontNums) =>
  (...rearNums) => 
	flattenAndConcat(frontNums, rearNums).reduce((a, b) => a + b);

const sum1 = add([1, 2, 3])(4, 5, 6);
const sum2 = add([1, 2, 3])([4, 5, 6]);
const sum3 = add(1, 2, 3)(4, 5, 6);
const sum4 = add(1, 2, 3)([4, 5, 6]);

console.log("sum1", sum1);
console.log("sum2", sum2);
console.log("sum3", sum3);
console.log("sum4", sum4);

어쩌다가 코드가 엄청 길어졌는데... 막상 짜다보니까 인자가 풀어서 들어오거나 배열로 들어올 수도 있고 고차원 배열로 들어오는 것까지 고려하면서 혼자 이러쿵 저러쿵 하다가 길어졌다.중요한 점은 add함수만 보면 된다. add함수를 보면 closure를 이용하여 원하는 값을 보존시킬 수 있다. 이제와서 생각해보지만 맥락과는 무관한 내용인가....? 🤔🤔🤔

bind

bind는 es5(2009)에 나온 기능이며 call, apply와 같은 기능을 하지만, 고차함수 개념처럼 함수를 실행하여 값을 즉시 평가하는 것이 아닌 새로운 함수를 반환한다는 점에서 다르다.

function printName() {
  console.log('name:', this.name);
  console.log('this', this);
}

const person = { name: 'jjung' };
const newFunction = printName.bind(person); // 👉🏻 새로운 함수 반환
newFunction(); // 함수를 호출해야 원하는 결과를 얻을 수 있다.

그리고 bind를 이용한 함수는 함수이름에 bound라는 접두어가 붙는다.

console.log('함수 이름:', newFunction.name); // 

그리고 bind메서드를 이용하면 원하는 값을 closure처럼 보존해서 사용할 수 있다. 위에 값을 더하는 예시를 보자.

function add (...rearNums) { 
  return [...this, ...rearNums].reduce((a, b) => a + b);
}

const frontNums = [1, 2, 3];
const sum1To6 = add.bind(frontNums, 4, 5, 6)
const sum1To9 = sum1To6(7, 8, 9)

console.log('sum1To9 =', sum1To9);

1 ~ 6까지 더한 값을 보존한 함수에 7, 8, 9를 더한 값을 평가할 수 있게 된다. 어떻게 보면 위에서 설명한 closure개념이 들어가 있지 않나 싶다.

6️⃣ 화살표 함수에서의 this

위의 생성자 함수 호출 때 잠깐 살펴봤던 화살표 함수는 일반 함수가 this와 binding하는 방법이 다르다. 어떤 사람이 this binding은 js의 설계상의 오류라고 말하며 개발자들의 혼동을 줄여주기 위해 일관된 this binding을 제공한다. 화살표 함수는 상위 lexical scope에서 this를 찾아서 binding한다.

일반 함수와 화살표 함수와의 차이점을 비교해보자.
혼동을 피하기 위해 일반 함수를 함수 선언문으로 선언한 함수라고 특정하자.

❗️❗️❗️
화살표 함수

  • 함수 표현식으로 변수 선언부만 hoisting되며 할당 부분은 호이스팅 되지 않는다.
  • 화살표 함수가 실행되고 실행컨텍스트가 생성되는 과정에서 this를 binding하는 과정이 빠진다.
  • 화살표 함수는 this를 scope chaining을 통해 상위 lexical environment에서 this를 찾아 binding한다.

일반 함수

  • 변수 선언부와 할당부가 모두 hoisiting된다.
  • 함수가 호출되는 방식에 따라 실행컨텍스트가 실행되는 과정에서 this를binding한다. 즉, 함수의 실행컨텍스트가 생성될 때 this가 결정된다.

화살표 함수가 this와 binding되는 과정을 이해하고 싶으면 scope와 실행 컨텍스트를 먼저 공부하고 오는 것을 추천한다.(본인은 this부터 공부하다가 포기했었던 이력이 있다 ㅜㅜ....)

위의 생성자를 다룰 때 잠깐 소개 했던 예제를 다시 가져오자.

// 생성자 함수로서 호출
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.printWithArrowFunction = () => {
    console.log("생성자 함수 내 화살표 함수는 어떤 this를 가리킬까?", this);
  };
}

const jjung = new Person("jjung", 29);
jjung.printWithArrowFunction();
// 메서드로서 호출
const person = {
  name: 'jjung',
  age: 29,
  printWithArrowFunction: () => {
    console.log("객체 내 화살표 함수는 어떤 this를 가리킬까?", this);
  }
}

person.printWithArrowFunction();

먼저 첫 번째 케이스부터 분석해보면, 생성자 함수도 함수다!!! 이 부분이 중요하다. 생성자 함수가 호출이 되면
실행컨텍스트가 생성되면서 this가 결정된다.

new Person('jjung', 29)호출

printWithArrowFunction호출

화살표 함수가 실행되면 this binding의 부재로 인해 이를 가까운 lexical scope에서 찾게된다.(그림의 사다리 타고!) 그러면, 상위 스코프는 this를 name: 'jjun', age: 29인 객체를 가리키고 있으므로 이 객체를 출력하게 된다.

그렇다면 두 번째 케이스는 왜 window가 나올까? 두 번째 케이스에서는 화살표함수는 렉시컬 스코프를 통해 this를 참조하는데 스코프라는 사다리를 타고 가다보니 this를 가지고 있는 가장 가까운 친구는 전역객체인 window이다. person 객체는 함수가 아니므로 실행 컨텍스트를 생성하지 않기 때문에 화살표함수가 this를 찾아나가는 여정을 떠나 전역객체에 도달하게 되는 것이다.

🔥 마치며

예전에 this를 공부하다가 실행 컨텍스트와 여러가지 기본 개념이 제대로 다져지지 않아서 this를 1주일 내내 했는데도 이해를 제대로 하지 못 한 경험이 있다.... 메타인지를 빠르게 하고 다른 개념을 정복하고 왔어야 했는데 ㅜㅜ bounce와 throttling을 공부하다가 this를 다시 보고 한 번 쭉 정리할 겸 해서 블로깅을 했는데 블로깅하면서 더 배운 것들이 많다. this를 공부하려는 사람들한테 this를 공부하기 이전에 실행컨텍스트와 클로저, 스코프를 선행하라고 알려주고 싶다. 그리고 화살표함수가 나오게 된 이유 중 하나라고 생각하며 화살표 함수를 잘못 쓰면 예상치 못 한 오류에 빠질 것 같다.

📚 참고

코어 자바스크립트
모던 deep dive
화살표 함수 남용 X
Gentle Explanation of "this" in JavaScript
this 설명

profile
step by step

1개의 댓글

comment-user-thumbnail
2023년 11월 14일

좋은 정보 감사합니다

답글 달기