[RBF] 생성자 함수와 프로토타입

Minha Ahn·2024년 11월 4일
2

데브코스

목록 보기
19/29
post-thumbnail

🪄 생성자 함수

1. 생성자 함수가 필요한 상황

사람의 정보가 담긴 객체를 만들 필요가 생겼습니다! 객체를 만들어볼게요.

const person1 = {
	name: "철수",
	age: 20,
	getInfo: function() {
	  return `${this.name} ${this.age}`;
	}
};
const person2 = {
	name: "영희",
	age: 30,
	getInfo: function() {
	  return `${this.name} ${this.age}`;
	}
};

지금은 두 사람의 정보가 각각 담긴 두 개의 객체를 만들었는데요.
만약 1000명의 사람의 정보가 담긴 객체를 만들어야 한다면 어떻게 해야할까요?

저라면 name과 age에 값을 넣지 않은 채로 복사해서 1000번 붙여넣기를 할 것 같아요.
그렇게 붙여넣은 1000개의 객체의 사람들의 정보를 넣으면 될테구요.

조금이라도 편하게 일하려고 꼼수를 부린 저는 템플릿을 만들게 되었습니다.
아까 언급한 name과 age에 값을 넣지 않은 채로 복사 한 것이 바로 템플릿이죠.

이런 아이디어를 반영한 것이 바로 생성자 함수입니다!



2. 객체 생성을 위한 생성자 함수

2-1. 생성자 함수란?

생성자 함수란, 객체를 생성할 때 사용하는 함수를 의미합니다.

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.getInfo = function() {
    return `${this.name} ${this.age}`;
  }
}
const person1 = new Person("철수", 20)
const person2 = new Person("영희", 30)

2-2. 일반 함수와 생성자 함수의 다른 점

  • 변수 생성 키워드(var, let, const)가 아닌 this 키워드 사용
    • this 키워드는 생성자 함수로 생성하게 될 객체를 의미합니다.
    • this.name : 생성하게 될 객체의 name 속성 의미
  • 전반적인 구성은 함수라기 보다는 객체와 유사
  • 함수를 호출할 때 new 키워드 사용

3. 생성자 함수 사용 예시

3-1. 사용자 정의 객체 생성

위에서 언급했던 예시가 바로 이 사례입니다! 고로 패스~


3-2. 표준 내장 객체 생성

자바스크립트가 이미 가지고 있는 생성자 함수도 있습니다. ( Number , String , Array , Object , Array 등)

const date = new Date();

참고로 new를 사용하지 않아도 객체가 생성되는 경우도 있는데요…
바로 참조 자료형(객체, 배열, 함수 등)이 그렇습니다. 정확히는 일급 객체가 그렇습니다.

const arr = [1, 2, 3];
function Hello() {
  console.log('hello');
}

GPT를 닥달해 얻어낸 대답은 이렇습니다.

  • 기본 자료형은 리터럴로 생성되면 원시값으로 생성될 뿐 객체가 아니다.
  • 일급 객체에 해당하는 참조 자료형은 리터럴로 생성해도 리터럴 자료형에 맞는 객체로 생성된다.
  • 이유는?
    • 메모리 효율성 : 메모리에서 불필요한 오버헤드를 피하기 위해 기본 자료형은 원시값으로 다룬다.
    • 객체의 필요성 : 참조 자료형은 복잡한 데이터를 구조화하고 조작할 필요가 있기 때문에 객체로 관리한다.




📰 프로토타입

1. 함수와 함께 생성되는 프로토타입

1-1. 프로토타입이란?

프로토타입이란, 새로운 객체가 생성되기 위한 원형이 되는 객체를 의미합니다.

저는 무슨 말인지 잘 이해가 안됩니다… 예시와 함께 차근차근 살펴보겠습니다.

일단 함수와 함께 생성되는 프로토타입이라고 하니, Hello라는 함수를 정의해보겠습니다.
이때!!! 함수를 정의하는 순간, 프로토타입이라 불리는 객체도 함께 생성된다고 해요. (화살표 함수는 제외)
Hello 함수도 마찬가지겠죠?

function Hello() {
  console.log("hello");
}
console.dir(Hello);

1-2. 프로토타입 구성

그렇다면 함수와 함께 생성되는 프로토타입 객체의 기본적인 구성은 어떻게 될까요?

바로 constructor [[Prototype]] 2가지 속성으로 구성됩니다.

constructor는 프로토타입 객체와 연결된 함수를 가르키고,
[[Prototype]]은 객체라면 다 갖고 있는 속성입니다.


1-3. 함수와 프로토타입의 연결

함수는 객체입니다. 그렇기 때문에 속성과 메서드를 가지고 있어요.
그렇다면 함수(객체)의 구성은 어떻게 되어있는지 확인해봅시다!!

function Hello() {
  console.log("hello");
}
console.dir(Hello);

여기서 프로토타입과 함수의 관계를 이해하기 위해서 주목해야할 속성은 prototype 입니다.
함수의 prototype 속성이 바로 함수의 프로토타입 객체를 가르키는 것이죠!
이제부터는 특정 함수 객체의 프로토타입 객체를 함수명.prototype이라고 지칭하겠습니다.

그리고 위에서 말씀드렸듯이 프로토타입 객체는 constructor 라는 속성으로 함수를 가르키고 있습니다.

그림으로 보면 이렇습니다

함수와 함수의 프로토타입 객체는 서로를 가르키고 있는 양방향 연결된 상태죠!


1-4. [[Prototype]] 속성

함수 객체와 프로토타입 객체, 그리고 자바스크립트의 모든 객체는 [[Prototype]]을 가지고 있는데요.
생긴 것도 이상하고 모든 객체가 가지고 있다고 하고…
뭔가 프로토타입 객체와 연관이 있을 것 같다는 느낌이 들기도 하고…
과연 이게 뭘까요..?!

일단 대괄호가 이중으로 붙여진 의미와 이유부터 살펴볼게요.
대괄호가 이중으로 붙여지면 내부 슬롯을 의미한다고 합니다.

내부 슬롯이란, ECMAScript 사양에서 정의된 특수한 속성으로, 객체의 상태와 행동을 정의하는 데 중요한 역할을 하며, 외부에서 접근할 수 없습니다. 대신 특정 메서드나 프로퍼티를 통해 간접적으로 사용할 수 있습니다.

그렇습니다. [[Prototype]] 속성은 너무 중요한 속성이라 내부 슬롯으로 관리되고 있었어요.
우리는 이 속성에 접근할 때는 __proto__ 로 접근할 수 있다는 점을 참고해두면 되겠습니다!
앞으로 [[Prototype]] 대신 __proto__(내부 슬롯 프로토타입)로 말씀드리겠습니다.

(그런데 그림에는 [[Prototype]]라 적혀있어요ㅠㅠ 동일한 의미라는 걸 기억해주세요!)

이번에는 __proto__은 무엇을 의미하는지 살펴보겠습니다.
바로 해당 객체의 프로토타입(부모 객체)을 참조하는 속성이라고 합니다.

저는 또 한 번에 이해하지 못했어요ㅠ
저희가 아까 만들어둔 Hello 함수로 확인해볼게요.

Hello 함수는 말 그대로 함수죠. 함수는 객체라고 했는데 자바스크립트의 내장 객체 중 어떤 객체에 해당할까요?
바로 Function 객체입니다. 즉, 저희가 만든 Hello 함수는 Function 객체입니다.

위에서 생성자 함수에 대해 설명드렸듯이
자바스크립트는 Function 객체 생성을 위한 Function 객체 생성자 함수도 가지고 있겠죠?
그리고 Function 객체 생성자 함수도 함수이기 때문에 프로토타입 객체를 가지고 있습니다.

아까 __proto__은 해당 객체의 프로토타입을 참조한다고 했습니다!
즉, Hello 함수의 __proto__ 속성은 바로 Function.prototype를 가르키고 있는겁니다.

Hello 함수는 함수함수는 Function 객체Hello의 [[Prototype]]은 Function 생성자 함수의 프로토타입 객체

Hello 함수의 프로토타입 객체! 즉, Hello.prototype에도 __proto__가 있었는데요!
여기서 __proto__은 무엇을 가르킬까요?

프로토타입 객체는 객체객체는 Object 객체프로토타입 객체는 Object 생성자 함수의 프로토타입 객체

따라서, Object.prototype을 가르키고 있습니다.
모든 함수의 프로토타입 객체에서 __proto__Object.prototype을 가르키고 있는 것이죠.

그림으로 보면 이렇습니다.



2. 생성자 함수와 프로토타입의 관계

2-1. (사용자 정의) 생성자 함수, 인스턴스, 프로토타입 관계

인스턴스란, 생성자 함수로 만들어진 객체입니다. 생성자 함수나 클래스로 만들어진 객체를 모두 인스턴스라고 합니다.

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.getInfo = function () {
    return `${this.name} ${this.age}`;
  };
}
const person1 = new Person("철수", 20);

console.dir(Person);
console.dir(person1);

이걸 그림으로 표현하면 이렇게 될 수 있을겁니다.
(모든 객체는 __proto__을 소유하고 있다는 걸 잊지 마세요!)

그렇다면 person1의 __proto__은 어디를 가르키고 있을까요?
바로바로 Person.prototype을 가르킵니다. 이렇게 말이죠!


2-2. 모든 인스턴스의 공통 항목은 프로토타입으로!

저희는 Person 생성자 함수를 통해서 Person 객체. 즉, 인스턴스를 쉽게 만들 수 있습니다.
그리고 각 인스턴스들에는 동일하게 name, age 속성과 getInfo라는 메서드가 각각 만들어져요.
이는 각 인스턴스들의 속성 및 메서드가 각각 다른 메모리에 할당된다는 걸 의미합니다.

그런데 getInfo 메서드의 경우 굳이 여러 번 생성할 필요가 없어요. 왜냐! 함수의 내용이 동일하거든요.
그런데 굳이 메서드를 각각 따로 만들어야 할까요?
1000개의 인스턴스가 있다면 동일한 내용의 getInfo 메서드도 1000개가 생기는건데.. 메모리가 너무 아깝습니다.

만약 하나로 퉁쳐서 같은 객체인 인스턴스들이 함께 사용한다면 메모리 절약 가능하겠죠?
어라? 근데 마침 모든 인스턴스들이 동일한 무언가를 참조하고 있어요.
바로 Person.prototype을 참조하고 있다는 겁니다. (Person의 프로토타입 객체는 딱 하나 뿐이죠!!)

그렇다면 getInfo 메서드를 Person 생성자 함수 내에서 정의해 모든 인스턴스가 각각 소유하는 방식이 아닌,
Person.prototype 내에서 정의해서 모든 인스턴스가 하나의 getInfo 메서드를 참조해 사용할 수 있는 방법을 사용할 수 있겠네요?

Person 생성자 함수의 prototype 접근은 Person.prototype 으로 할 수 있으니, 이렇게 바꾸면 됩니다!

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.getInfo = function () {
	return `${this.name} ${this.age}`;
};
const person1 = new Person("철수", 20);

각각 getInfo 메서드를 만들어 불필요한 메모리를 소비하는 대신, 하나의 getInfo 메서드를 참조해 사용할 수 있게 되었습니다.


2-3. 표준 내장 객체

표준 내장 객체도 한 번 쓱 보고 가볼게요.

첫번째로 Array를 보겠습니다. 아까 본 Person에서 Array로 바뀐 것 뿐입니다.

이번엔 Function을 볼게요.
여기서 유의해야 할 것은 Function 생성자 함수의 __proto__입니다.

Function 생성자 함수도 Function이니__proto__Function.prototype인데요!
이는 즉, 본인의 프로토타입 객체를 의미하는 것입니다.

Object까지만 보시죠. 여기도 유의해야 할 점이 있는데요.
모든 프로토타입 객체는 Object이기 때문에 __proto__Object.prototype입니다.
Object 생성자 함수의 프로토타입 객체에게는 본인을 의미하는 것이죠. 그래서 본인은 __proto__이 없습니다!



3. 프로토타입 체인

3-1. 프로토타입 체인 & 체이닝이란?

프로토타입 체인이란, 인스턴스에서 자신을 생성한 생성자 함수의 프로토타입에 접근할 수 있는 방법입니다. 그리고 그 과정을 프로토타입 체이닝이라고 합니다.

프로토타입 체인은 어떤식으로 동작할까요?

아까 작성했던 코드를 들고왔습니다.

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.getInfo = function () {
		return `${this.name} ${this.age}`;
	};
}
Person.prototype.getInfo = function () {
	return `prototype - ${this.name} ${this.age}`;
};

const person1 = new Person("철수", 20);

console.log(person1.getInfo());
  1. 인스턴스(여기선 person1 객체) 내에 원하는 속성 혹은 메서드가 있는지 확인 ⇒ 있으면 사용
  2. 1번에서 찾지 못했다면, 생성자 함수의 프로토타입 객체 내에 있는지 확인 ⇒ 있으면 사용
  3. 2번에서 찾지 못했다면, 생성자 함수의 프로토타입 객체에 연결된 다른 프로토타입 객체 내에 있는지 확인

이렇게 제일 작은 범위인 객체 내에서 찾다가 점점 프로토타입 → 상위 프로토타입 → … → 최상위 프로토타입으로
범위를 확장하며 검색하는 것이 프로토타입 체인입니다.

객체와 프로토타입에 동일한 이름의 속성이나 메서드가 있다면?? 먼저 찾아낸 걸 보여줍니다.


3-2. 프로토타입 객체에 직접 접근 금지

여기서 유의할 점이 있어요. 만약 내가 바로 프로토타입 객체로 접근하고 싶다고 해서,
person1.__proto__.getInfo() 를 호출하는 건 안된다는 점입니다.

오류가 발생하지는 않지만, 원하는 대로 동작하지 않게 됩니다. 바로 this 바인딩 때문입니다!
this 바인딩은 어떤 객체가 참조하도록 지정하는 과정을 의미하는데요.

함수에서 this는 본인을 호출한 객체를 의미하게 됩니다.
person1.getInfo() 라 한다면, getInfo 메서드를 호출한 것은 person1이기 때문에 this는 person1입니다.
그러니 this.namethis.age 는 person1에 있는 값을 불러오게 됩니다.

person1.__proto__.getInfo() 를 한다면, getInfo 메서드를 호출한 것은 proto가 되므로
this는 Person.proto가 되겠죠?

그러나 Person의 프로토타입 객체은 nameage 속성이 없어요. 그래서 undefined로 다뤄지게 됩니다.




🏓 생성자 함수의 활용

1. 생성자 함수를 프라이빗하게 사용

생성자 함수의 단점은 인스턴스 객체의 속성에 접근해서 조작하기 쉽다는 것입니다. 속성 이름만 알면 되니까요.
때문에 예기치 않은 동작을 구현할 수 있다는 위험이 있습니다.

이러한 단점을 클로저로 보완할 수 있습니다.

function Counter() {
  let count = 0; // 클로저
  
  this.increment = function () {
    count++;
  };

  this.decrement = function () {
    count--;
  };

  this.getCount = function () {
    return count;
  };
}

const counter = new Counter()
counter = {
  increment: function () {
    count++;
  },
  decrement: function () {
    count--;
  },
  getCount: function () {
    return count;
  }
}

counter 객체를 아무리 살펴봐도 count라는 속성이 없습니다. 이것이 바로 클로저를 이용한 사례죠.

counter 본인을 포함한 그 누구도 count 접근을 못합니다. 오로지 counter의 메서드만 count에 접근할 수 있어요.



2. 생성자 함수의 팩토리 패턴

팩토리는 공장이죠! 즉, 공장처럼 생성자 함수를 찍어내는 것을 의미합니다.

생성자 함수를 하나로 모아서 어떤 타입이냐에 따라 생성자 함수를 호출해 인스턴스를 만들어주는 것이죠.
(자판기 같네요! 원하는 음료를 누르면 그 음료를 딱 뽑아주니까요)

function createDrink(type, name) {
  function IonicDrink(name) {
    this.name = name;
    this.type = "ionic"; // 이온 음료
  }

  function SodaDrink(name) {
    this.name = name;
    this.type = "soda"; // 탄산 음료
  }

  switch (type) {
    case "ionic":
      return new IonicDrink(name);
    case "soda":
      return new SodaDrink(name);
  }
}

const drink = createDrink("soda", "코카 콜라");



3. 생성자 함수의 상속 구현

생성자 함수를 통해 상속을 구현할 수 있는데요!
ES5 이전까지는 이 방법을 통해 상속을 구현했습니다만…
매우 복잡하다는 단점과 클래스의 등장으로 굳이..? 사용하지 않아도 되는 방법인 것 같아요.

그래도 알아둬서 나쁠 것도 없고 언젠간 사용할 수도 있으니 보고가도록 하죠!

function Person(name) {
  this.name = name;
}

// Person 생성자 함수의 프로토타입 객체에 introduce라는 메서드 생성
Person.prototype.introduce = function () {
  return `I an ${this.name}`;
};

function Developer(name, position) {
  Person.call(this, name); // 부모 생성자 호출 => 여기서 Person 객체의 name을 상속받습니다.
  this.position = position;
}

// 프로토타입 조작
Developer.prototype = Object.create(Person.prototype);
Developer.prototype.constructor = Developer;

Peson.call 에 this와 name을 전달해 상속하는 형식이네요.
this는 새로 생성될 Developer 인스턴스를 가르키고 있으므로
Person.call로 새로 생성될 인스턴스에 name이라는 속성이 생겨나게 될겁니다.

Object.create 는 새로운 객체를 생성해 Person.prototype 내용을 새로운 객체에 입혀주는 역할입니다.
즉, Developer.prototypePerson.prototype을 상속받게 되는 것이죠.
이로 인해, Developer 인스턴스도 Person의 프로토타입에 있는 메서드와 속성에 접근할 수 있게 됩니다.

Developer.prototype은 아직 본인의 주인(함수)을 모르는 상태이기 때문에 constructor로 연결해줍니다.

그림으로 설명하자면 이렇습니다!


새로운 객체로 갈아끼우기라고 생각하면 될 것 같아요.

그리고 Developer.prototype에 속성이나 메서드를 넣고 싶다면
아예 새로운 객체로 갈아끼우는 과정을 진행한 후에 추가하셔야 합니다!





🤓 정리

생성자 함수

  • 생성자 함수 : 객체를 생성할 때 사용하는 함수
  • 일반 함수와 생성자 함수의 다른 점
    • this 키워드 사용
    • new 키워드 사용
  • 사용 예시
    • 사용자 정의 객체는 new 키워드와 함께 객체 생성
    • 표준 내장 객체에서 일급 객체인 경우 new 키워드 없이도 무조건 객체로 생성

프로토타입 객체

  • 프로토타입 객체 : 함수 생성 시 함께 생성되는 객체
  • 구성
    • constructor : 자신과 연결된 함수를 가르키는 속성
    • [[Prototype]] : 모든 객체가 가지고 있는 내부 슬롯으로, 해당 객체의 프로토타입 참조
  • 모든 인스턴스에서 반복적으로 사용되는 속성이나 메서드가 있다면 생성자 함수 내부가 아닌,
    프로토타입 객체에 만들어두면 메모리 절약 가능
    - 프로토타입 객체에 있는 속성이나 메서드에 직접 접근할 때는 객체명.__proto__.메서드 금지.
    에러가 발생하는 것은 아니나, this가 객체가 아닌 프로토타입 객체로 바인딩됨
  • 프로토타입 체인 : 인스턴스에서 자신을 생성한 생성자 함수의 프로토타입에 접근할 수 있는 방법
    • 필요한 속성이나 메서드를 찾지 못하면, 객체 내부 → 객체의 프로토타입 → 상위 프로토타입으로 점점 올라가며 검색

생성자 함수의 활용

  • 프라이빗하게 사용 : 속성 정의 대신, 생성자 함수에 변수를 이용해 클로저를 활용하는 방식
  • 생성자 함수의 팩토리 패턴 : 생성자 함수를 하나로 모아 타입에 맞게 적절한 객체를 찍어내는 방식
  • 생성자 함수의 상속 구현 : 부모 객체 생성자 함수.call(this) 와 프로토타입 갈아끼우기 방식
profile
프론트엔드를 공부하고 있는 학생입니다🐌

0개의 댓글