[REAL Deep Dive into JS] 17. 생성자 함수에 의한 객체 생성

young_pallete·2022년 9월 15일
0

REAL JavaScript Deep Dive

목록 보기
17/46

🚦본론

우리, 이전에 10장에서 객체 리터럴을 공부해왔어요.
그때, 객체를 생성하는 5가지가 있다고 했죠? 그 중 하나인 생성자 함수를 오늘 다루려 합니다.

쉽게 말하자면, new 연산자와 함께 호출해서, 객체를 생성하는 함수를 생성자 함수라 해요.

const person = new Object();

person.name = 'Lee'

사실, 객체 리터럴이 값을 이해하는 데 더욱 직관적이에요. 따라서 다른 문맥 없이 위처럼 쓰는 경우는 흔치 않아요!

그렇다면 왜 쓸까요? 우리는 객체 리터럴과 비교하여 그 필요성에 주목할 필요가 있겠어요 😮

생성자 함수의 필요성

기존 객체 리터럴 방식은 간단히 값을 주는데 편했어요.
그런데, 똑같은 객체를 여러 개를 생성해야 하는 경우에는 많이 불편했습니다.

예컨대 다음을 볼까요?

const obj = {
	a: 1,
    b: 2,
};

위의 코드는 평범한 객체 리터럴을 할당받는 문입니다.
그런데 말이죠, 만약 저 obj라는 친구가 obj1, obj2, ... ,obj100까지 있어야 한다면 어떻게 될까요?

그렇다면 2가지의 문제점이 생깁니다.

  • 코드 왜이렇게 더러워!: 약 400줄의 코드가 저렇게 똑같은 문을 복사했으니, 매우 보기 좋지 않아요.
  • 수정 너무 힘든데?: 만약 리터럴 값 중 하나가 수정된다면? 100개의 값들을 다 일일이 수정해야 해요. 매우 불편하겠죠?

따라서 우리는 똑같은 프로퍼티 구조를 가진 객체를 쉽게 관리하기 위해 생성자 함수라는 선택지를 갖고 있는 거에요! 😉

그리고, 이제 생성자 함수와 함께 우리의 애증의 절친, this를 곧 만나게 돼요!

생성자 함수와 this

사실 new와 함께하는 생성자 함수는 얼핏 보면 일반 함수와 별반 달라보이지 않아요.
그렇지만 어떤 값을 반환하는지에 대해서는 매우 다른 차이가 발생해요!

생성자 함수의 내부에는 암묵적으로 this라는 객체가 반환되게 되는데요. 이 this라는 것은 자기 참조 변수로써, 렉시컬 스코프가 아닌 동적 스코프를 따라 바인딩될 값을 동적으로 결정해요!

이 말이 와닿지 않죠? 예시를 통해 설명을 드릴게요.

var a = 1;
var b = 2;

function reassignValues() {
	this.a = 100;
    this.b = 200;
}

const testObj = {
	a: 3,
  	b: 4,
    reassignValues
}

testObj.reassignValues();
console.log(a, b, testObj); // 1 2 {a: 100, b: 200, reassignValues: ƒ}

🙇🏻‍♂️ 뭔가 우리가 함수와는 달리, 결과가 낯설지 않나요?

분명 우리가 아는 '함수'는 렉시컬 스코프에요.
따라서 this binding이 렉시컬 스코프를 따랐다면, 선언된 시점을 기준으로 스코프를 탐색하기 때문에 var avar b의 값을 바꿔줬어야 해요.

하지만 this의 경우는 어디서 호출했는지를 따집니다.
우리는 reassignValues라는 메서드를 호출했어요. 이때 thistestObj를 가리키고 있습니다! 우리는 testObj의 안에서 호출을 하고 있기 때문이에요.(이름이 괜히 자기 참조 변수가 아니겠죠? 🙇🏻‍♂️)

따라서 thistestObjthis를 바인딩하게 되고, 결과적으로는 testObjab를 바인딩하게 되는 거에요!

이 자세한 내용은 22장에서 다루어진다고 하니, 생성자 함수는 이런 this라는 친구가 좀 다르게 동작한다!라고 이해하면 편할 거 같아요 😉


생성자 함수의 생성

저도 이 책에서 좀 더 생각을 해봄직했던 게, 결국 생성자 함수란 어떤 인스턴스를 초기 정의할 때 사용하는 게 아닐까? 라는 생각이 들었어요.

  • 인스턴스의 반복적인 생성을 효율적으로 다룰 수 있게 하고
  • 인스턴스 초기화를 안정적으로 할 수 있게 하는 것

이것이 생성자 함수를 올바르게 사용하는 게 아닐까 싶더라구요.

이때 올바르게 쓴다는 것은, 우리가 올바르게 코드를 작성할 수 있어야 한다는 것이 전제가 됩니다. 따라서 이 생성자 함수의 원리를 살펴봐요!

this가 주는 함정

초기에 생성자 함수를 생성할 때 가장 헷갈리는 것은, 반환문이 없다는 것입니다.

function Person(name) {
	this.name = name;
}
const person = new Person('jaeyoung');

console.log(person) // { name: 'jaeyoung' }

보통, 우리가 return을 해야 비로소 undefined가 아닌 값을 갖게 되는데요. 어떻게 생성자 함수는 값으로 할당이 되는 걸까요?

이유는 바로, 생성자 함수에서는 암묵적으로 빈 객체를 생성하게 되고, 이를 this에 바인딩하기 때문입니다!

일반적으로 리턴값이 없다면 this를 반환하기 때문에, 실제로는 return this라는 문이 생략되어 있는 거에요!

이러한 처리는 함수 내부가 한 줄씩 실행되는 런타임 이전에 실행된다고 합니다!
즉, new라는 키워드를 인식한 순간, 일반 함수와 달리 this라는 것을 일단 함수 내부에 생성하고 시작한다는 거죠.

이후에는, 우리가 초기화하는 의도를 담아낸 함수 내부 코드를 실행하게 되어, 결과적으로 새로운 인스턴스를 생성하게 되는 거에요.

🚨 만약 반환문이 this가 아니면 원하는 인스턴스가 생성되지 않을 수 있어, 반환문은 반드시 생략하는 것을 권장해요.
그렇지 않다면, 반환한 값이 객체라면 this가 아닌 해당 반환 객체를 반환하므로 원하지 않는 결과가 발생할 수 있어요! 🙇🏻‍♂️

함수의 내부 슬롯

이 파트는 저도 이번에 새롭게 깨닫게 된 파트네요!

이 책을 좀 더 서술해보자면, 자바스크립트에서 객체는 값을 가지는 일급 객체인데요!
저자는 어떻게 호출을 하는지에 대해 의문을 던지네요!

그리고 이 해답은, [[Environment]], [[FormalParameters]]와 같은 내부 슬롯과

  • [[Call]]: 호출이 가능하게 함. 일반 함수 호출 시 사용.
  • [[Construct]]: 내부 메서드를 갖도록 함. 생성자 함수로써 호출할 때 사용.

라는 내부 메서드 때문입니다. 따라서 우리가 생성자 함수를 호출하는 것은 [[Construct]]이라는 호출 메서드를 갖고 있기 때문이겠네요! 그렇게 되면, [[Construct]] 메서드로 인해 우리는 생성자 함수로 객체를 생성할 수 있게 되는 거죠!

✨ 이때, 결국 힌트가 되는 것은 new 연산자겠죠?!

그러나, 다 같은 함수가 아니다!

constructornon-constructor라는 주제로 이 함수를 또 분류했는데요!
이는 결국 또 this와 맞물리게 됩니다.

non-constructor은 쉽게 말하자면 얘 자체가 생성자가 될 수 없는 함수인 거에요!
음... 예컨대 대표적으로 화살표 함수가 있어요!

화살표 함수는 this가 없어요. 그저 상위의 스코프를 탐색하는 친구죠! 따라서 암묵적 this 생성이 불가능하니, 당연히 non-constructor입니다.
또한 ES6 축약 표현으로 만들어진 메서드 역시 non-constructor입니다. 이 친구 역시 문법적으로 non-constructor로 지정되어 있어요! 🙇🏻‍♂️

이러한 non-constructor의 경우에는 생성자 함수로 호출하면 오류를 내뱉게 되니, 주의하길 바랍니다 😉

new.target

이거는 은근 많이 쓰는 편이에요.
대체로 생성자 함수를 실수로 new 연산자로 호출하지 않을 때를 대비한 방어 코드라고 보시면 될 거 같군요!

new.target은 생성자 함수로 호출될 시 함수 자신을 가리키는데요. 만약 일반 함수로 호출되면 undefined인 친구에요.

따라서 다음과 같이 생성자 함수를 다시 만들어줄 수 있답니다!

function Test() {
  if (!new.target) return new Test();
  ...
}

이렇게 생성자 함수가 아니라면 다시 생성자 함수로 재귀호출을 해줌으로써, 비로소 원하는 값을 가질 수 있게 되는 거죠!

다만 new.targetES6부터 도입됐기에, 그 이전 버전은 다음과 같이 사용할 수 있어요.

function Test() {
  if (!(this instanceof Test)) return new Test();
  ...
}

결국 this가 곧 반환될 인스턴스인데, 만약 아니라면 생성자 함수가 아니겠죠?!

이러한 방어 코드 덕분에, 우리는 Object, String 등의 생성자 함수를 호출할 때, new를 사용하지 않아도 동작하는 거에요!

const a = Array(5).fill(1)
console.log(a) // [1,1,1,1,1]

🌈 마치며

아무래도 저 역시 바닐라 자바스크립트로 개발을 한지 너무 오래되어서인지, 간만에 마주한 생성자 함수가 낯설었어요.

그러나, 결국 다음만 정리한다면 이 내용은 그렇게 어렵지 않은 거 같아요!

  1. 생성자 함수는 반복되는 인스턴스의 생성을 도움으로써 재사용성 및 유지보수성이 높아진다.
  2. 일반 함수는 constructor함수와 non-constructor함수로 나뉜다.
  3. constructor함수가 new 연산자로 호출되면 생성자 함수가 된다.
  4. 생성자 함수는 암묵적으로 this라는 빈 객체가 생성되고, 이 this가 곧 반환될 인스턴스의 값이다.

다들 즐거운 코딩하시길 바라요. 이상! 🌈

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

0개의 댓글