[REAL Deep Dive into JS] 22. this

young_pallete·2022년 9월 22일
0

REAL JavaScript Deep Dive

목록 보기
22/46
post-thumbnail

🚦 본론

일반적인 변수는 렉시컬 스코프를 따른다

일반적으로 자바스크립트는 렉시컬 스코프를 따릅니다.
요새 스터디하면서 많은 스터디원분들께서 렉시컬 스코프라는 것을 자주 헷갈려하시는 것 같아요.

우리, 막 외우지 말고 하나하나 차근히 살펴봅시다.
렉시컬하다라는 것은 무엇일까요?

일반적으로 컴파일에서 쪼개진 토큰들의 의미를 분석하는 단계를 렉싱한다라고 표현해요.
뭔가 코난처럼 뇌리에 스치지 않나요?

몰랐었다면 뭔가 자신의 언어로는 완전히 형성이 안 되었지만, 뭔가 몽글몽글한 몇 개의 단어들이 떠오를 것 같아요.
좀 더 자신만의 언어로 해석하기 위해 설명을 보태볼게요!

의미 분석은 렉싱... 그렇다면 렉시컬 스코프는 뭘까요?
어떤 선언된 변수들의 의미를 분석하는 데 도움을 주는 스코프... 이런 느낌을 준다는 거겠죠?!

우리, 스코프가 뭐였죠?
제가 별도로 만들었던 8-1장에서 바로 변수가 사는 지역의 범위를 지켜주는 울타리 같은 거라고 했어요.

결국 렉시컬 스코프는 이해하면 별 게 없어요.

일반적으로 자바스크립트의 변수는 단지 선언된 곳을 기준으로 자신의 유효 범위를 결정짓는 데, 이것이 자신이 속한 스코프의 영역에 영향을 받는다~해서 자바스크립트의 대부분의 변수는 렉시컬 스코프를 따른다고 하는 거에요. 🥰

이 얘기를 왜 하냐구요?
this는 일반적인 변수가 아니기 때문입니다. 크흡.


this는 동적 스코프를 따른다

핵심은 this동적 스코프를 따른다는 겁니다.
이는 이전의 17장인 생성자 함수 글에서도 설명이 되었던 거군요!

const name = 'jaeyoung';

function getName() {
	return this.name;
}

const person = {
  	name: 'sunyoung',
	getName
}

console.log(person.getName()) // sunyoung

위의 내용이 이상하지 않나요? 그렇다면 정말 제대로 공부하신 게 맞아요! 😉
위의 결과가 어색한 이유는 다음과 같습니다.

렉시컬 스코프를 따르지 않는 this

const name = 'jaeyoung'; // 전역에서 선언했군!

function getName() { //오, 전역에서 함수를 선언했네!
	return this.name; // 그러면 name도 선언한 위치가 전역이니, jaeyoung이겠구만!
}

const person = {
  	name: 'sunyoung',
	getName
} // 객체를 선언했고, name은 sunyoung이고, 메서드는 getName의 참조 값을 할당했군!

console.log(person.getName()) 
// 응? 그러면 getName의 값은 jaeyoung인데, 왜 sunyoung이 나오지...?

이런 뇌의 흐름으로 인해, 어색한 게 정상이에요.
이러한 결과가 나온다는 것은 무엇일까요? 바로 this는 렉시컬 스코프를 따르지 않는다는 거에요. 바로 호출한 시점에 동적으로 결정짓는다 해서 동적 스코프를 따른다고 합니다.

😖 잠깐, 우리 머리 아픈데... 이거 외워야 할까요...?

저는 최대한 막 외우기보다는, 덜 헷갈리게 이해하려 노력하는 편이에요.
우리 이참에 한 번 this랑 친해져봐요 🙇🏻‍♂️

this = 자기 참조 변수

this라는 용어는 흔히들 자기 참조 변수로 말하고는 해요.
저는 항상 말의 뉘앙스를 정말 중요시 여겨요.
따라서 이 용어를 처음 들었을 때, 이런 의구심으로 엄청 고민했어요.

🤤 도대체, 자기를 참조한다는 건, 무슨 뜻일까...?
(a.k.a. 참조한다는 건 뭔지 몰라도... 좋은 거야... 너가 참조되었다면 나도 좋아... 🙃)

음, 저는 이 모든 원흉(?!)이 생성자 함수로부터 비롯되었다고 생각했어요.
생성자 함수에서는 암묵적으로 빈 객체를 생성하고, 나중에 이를 인스턴스의 값으로 반환합니다.

그런데, 이를 설명할 게 없는 거에요. 그래서 너 값이 뭔데?!라고 묻는데, 뭔가 모르는 객체라고 말하기가 어렵잖아요?

그래서 constructor한 생성자 함수로 인해 자기 자신의 값을 참조하고 나타내야 할 변수가 필요하게 된 거죠.

저는 그것을 this라고 정의하기로 했습니다!
적어도 자신이 어떤 객체인지를 표현해야 하기 위해서 나타난 친구라고 말이죠!!

알고 보면 좋은 this

뭔가 그렇게 좀 이해를 하다 보니까, 이제는 짜증나기보다는, 너무 재밌더라구요.
뭔가 저는 이런 스터디가 마치 계단을 오르는 것 같아요.

정말 죽을듯이 짜증나는데, 막힌 게 뚫리면 엄청난 쾌감이... 🥰

자, 그러면 우리 결국 생성자 함수에서 this라는 친구가 나왔다고 치자구요.
그러면 이로써 얻을 수 있는 게 무엇일까요?

생성자 함수는 반복해서 재사용할 수 있다.

생성자 함수의 가장 큰 장점은 바로 반복해서 재사용할 수 있다는 점이었죠?!
왜냐! this라는 요 객체는 생성자 함수 내에서 암묵적으로 "빈 객체로 생성된 것"을 값으로 가진다고 했죠? (이것은 리터럴로 생성할 때도 추상연산으로 인해 빈 객체로 생성됩니다!)

따라서 인스턴스들의 값은 독립적이며, 겹치지 않게 되는 거에요.
참조가 아니라, 아예 새롭게 생성된 객체 값이니까요 😉

💡 잠깐! 그런데 값이 하나하나씩 나온다는 건 엄청난 손해 아니에요?

🎉 정답입니다! 그래서 있는 친구가 바로 프로토타입이죠. 마치 다같이 유전자처럼 같은 값을 공유하는 거에요! 점점 자바스크립트가 재밌지 않나요 😉

이것이 왜 this가 좋은 친구라는 근거냐구요?
this는 반복해서 재사용할 수 있는 데 최적화 되어있기 때문입니다.

그냥 this가 우리에게 주는 뉘앙스는, 거, 잘 모르니까 고거 써!의 느낌이에요.

this가 없다면 어떻게 될까요?

가장 큰 문제. 생성자 함수의 인스턴스 값을 정확히 설명하지 못하겠죠. 😭
사실, 이것말고도 문제는 따지자면 많을 수 있어요.

저도 생각을 쭉 해봤는데요. 약간 억까(?)스러울 수 있지만 this가 없다면, 우선 하나하나 호출한 곳을 명시해야 하므로 쓸데 없이 코드가 늘어날 수 있겠네요. 또한, 오히려 헷갈리는 요소들이 등장할 수 있겠어요.

아까 썼던 위의 함수를 예시로 들어볼까요?

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

const person = { name: 'jaeyoung', getName }

이 친구는 this로 인해 편하게 정의하고, 호출이 가능했어요.
그런데 만약 this가 없다면 어떻게 될까요? 지금은 괜찮지만, 이를 참조할 객체가 많아진다면 번잡해집니다.

const person1 = { name: 'jaeyoung', getName: function () {
	console.log(person1.name)
}}
const person2 = { name: 'jaeyoung2', getName: function () {
	console.log(person2.name)
}}
const person2 = { name: 'jaeyoung3', getName: function () {
	console.log(person3.name)
}}

console.log(person1.getName()) // 

우와... 잠깐 예시로 쓰기만 해도 진짜 불편하네요... 😭

결과적으로 기존 메서드처럼 인수없이도 정상적으로 호출되지만, 호출한 객체를 function에 하나하나 써야 한다는 점이 굉장히 충격이군요!

😮 잠깐! 이를 파라미터로 반복해서 쓰면 어떨까요?!

한 번 이를 적용해볼까요?

function getName(target) {
	console.log(target.name)
};

const person1 = { name: 'jaeyoung', getName }
const person2 = { name: 'jaeyoung2', getName }
const person3 = { name: 'jaeyoung3', getName }

console.log(person1.getName(person1)) // jaeyoung - 자기가 자기를 호출한다니... 어색한 걸?
console.log(person2.getName(person3)) // jaeyoung3 - 잠깐... 너가 남의 이름을 호출하면 어떻게 해!

이런 웃픈(?) 상황이 벌어지게 됩니다.

결과적으로 그냥 this는 재사용성을 위해 엄청난 특화된 능력을 가진 친구에요.
우리가 굳이 설명하지 않아도, 아, 고거 있잖아, 고거 불러봐!하는 식으로 써주는 거죠 🙆🏻

그리고 가장 중요한 거!
그 '고거'는 생성자 함수로부터 나온, 암묵적으로 할당된 빈 객체를 기준으로 한다는 거죠~ 🎉

전역 스코프와, 선언된 함수에서 this는 전역 객체로 바인딩 된다.

우리, 전역 객체는 기본적으로 전역 프로퍼티와 전역 함수들을 담는 공간을 하는 객체에요.

즉, 브라우저 환경에서 우리가 전역 스코프에서 변수와 함수를 선언한다는 것은, Window라는 생성자 함수의 객체 속에 프로퍼티와 메서드를 추가한다는 것과 같아요.

function sayHello() {
	console.log('Hello!');
}

window.sayHello(); // Hello!

그러니까 결국 this를 일반 함수에서 호출한다는 것은

  1. 우리 갖고 있는 객체 불러봐~
  2. 전역에서 호출한 거니까 고거, Window 있잖아! 걔 불러~

하는 거죠. 굉장히 this, 알고 보면 구수한 친구죠? 🤣

전역 this 바인딩의 문제점

다만, 이러한 전역 객체로 바인딩된다는 것이 ES3에 나온 new 연산자를 까먹기만 해도 바인딩이 잘못되어 큰 오류가 발생할 수 있는 문제가 발생했어요.

당시에는 클래스와 같은 문법도 존재하지 않았기에, 이는 정말 골치 아픈 문제였겠죠?!
(우리가 아는 생성자 함수 방어코드의 대명사인 new.targetES6부터 지원되었답니다!)

따라서 이러한 전역 객체로 mapping되는 this가 골치 아파서, ECMAScript 5부터는 use strict mode로, 전역 객체의 this 바인딩이 곧 undefined로 나올 수 있도록 했어요. 🥰

"use strict"

console.log(this) // undefined

apply/call/bindthis 핸들링하기

그런데, 가끔은 이러한 this가 헷갈릴 때도 있기도 하고, 무엇보다 원하지 않는 객체에 바인딩되어 있을 수도 있겠죠!

그런 경우에 대안으로 사용하는 것이 위의 세 메서드입니다 :)

세 메서드는 Function의 프로토타입 객체에 있는 메서드라, 모든 함수가 다같이 쓸 수 있답니다 😉

Function.prototype.apply

apply는 함수 호출에 목적을 두고 있어요.
따라서 다음과 같이 인자들을 배열로 묶어서 전달하여, 바인딩을 동적으로 시도합니다!

function getThis(age, address) {
  this.age = age;
  this.address = address;
  
  return this;
}

const obj = { name: 'jaeyoung' };

console.log(getThis.apply(obj, [29, 'Seoul'])); 
// {name: 'jaeyoung', age: 29, address: 'Seoul'}

Function.prototype.call

이 친구도 함수 호출에 목적을 두고 있는데, 차이점이라면 배열이 아닌 인자들을 열거해서 전달합니다.

function getThis(age, address) {
  this.age = age;
  this.address = address;
  
  return this;
}

const obj = { name: 'jaeyoung' };

console.log(getThis.call(obj, 29, 'Seoul')); 
// {name: 'jaeyoung', age: 29, address: 'Seoul'}

Function.prototype.bind

이 친구는 좀 다른데요!
applycall함수 호출에 목적을 두었다면 이 친구는 순수 this 바인딩에 목적을 둡니다.

따라서 함수를 호출하지 않기에, 명시적으로 호출을 해주어야 합니다!

function getThis(age, address) {
  this.age = age;
  this.address = address;
  
  return this;
}

const obj = { name: 'jaeyoung' };

console.log(getThis.bind(obj)(29, 'Seoul')); 
// {name: 'jaeyoung', age: 29, address: 'Seoul'}

차이를 이해하셨나요?

  • 결국 인자를 어떻게 받느냐에 따라 applycall이 다르고,
  • this 바인딩에만 목적을 두었다면 bind를 사용하면 된답니다 🥰

🔥 마치며

후, 말은 하기 쉬운데... 글쓰기가 정말 힘들군요!
결국 정리하자면, 우리는 이 글을 통해 다음을 알 수 있겠군요 🙇🏻‍♂️

  1. this는 렉시컬 스코프가 아닌, 동적 스코프를 따른다. 풀어서 설명하면 동적으로 자신을 호출한 객체(자신을 불렀던 녀석)를 참조하는 자기 참조 변수이다!
    2.this는, 결국 재사용성을 위해 반드시 필요한 친구다. (a.k.a. 그거 있잖아 그거...!)
  2. 전역에서 호출된 this는 전역 객체에 바인딩되는데, strict mode 에서는 undefined로 나타난다.
  3. apply/call/bind를 통해 동적으로 바인딩을 우리가 해줄 수도 있다!

생각보다 많은 것들을 짚어나갔네요.

어떻게 보면 제 추측이 90% 들어간 글이긴 합니다! 만약 틀린 부분이 있으면 말씀해주세요 😭
그래도 이 글이, 누군가에게는 이해하는 데 꼭 도움이 되었으면 좋겠어요. 이상! 🌈

📁 참고자료

모던 자바스크립트 Deep Dive - 17장
모던 자바스크립트 Deep Dive - 22장
TCP SCHOOL - 전역 객체
스택 오버 플로우 - 왜 strict mode에서는 전역 this가 undefined인지

profile
People are scared of falling to the bottom but born from there. What they've lost is nth. 😉

0개의 댓글