본문 바로가기

데일리

[23.10.20] 프로토타입

글의 목적

  • 본격적으로 객체를 정리하기에 앞서 프로토타입을 정리하려고 한다.
  • ES6에서 클래스가 추가가 되고 js가 프로토타입 기반 객체지향언어에서 클래스 기반 객체지향 언어로 바뀐것이 아닌가 라고 생각하시는 분들도 있으신 것 같은데 사실은 그냥 음.... 좀 다르다. 이 부분도 정리해보려고 한다. 아니 이 부분은 클래스를 정리하면서 다루겠다.

본론

1. 프로토타입

  • js는 원시값을 제외한 모든것이 객체이다 라는 말을 다시금 기억하자

1.1 객체지향 프로그래밍

  • 간단하게 속성들에 집중하여 추상화를 통해서 객체를 정의하고 객체와 객체간의 관계로 메시지를 주고받거나 데이터를 처리하는 등 객체간의 집합으로서 프로그램을 표현하는 것이다.
    • 모든 실체(사물이나 개념)는 속성을 가지고 있고 그 속성에 집중
    • 프로그램에 필요한 속성만 간추려서 표현 -> 추상화
    • 객체의 상태데이터인 프로퍼티와 동작 데이터인 메서드로 객체의 상태와 동작을 나타냄
    • 객체가 고유의 기능을 수행하면서 동시에 객체와 객체간 메시지를 주고 받거나 데이터를 처리
    • 상속을 통해서 다른 객체의 상태와 동작을 활용

1.2 상속과 프로토타입

  • 상속은 어떤 객체의 프로퍼티, 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것을 말함.
  • js는 프로토타입을 기반으로 상속을 구현함.

2023.10.19 - [데일리] - [23.10.19] 생성자 함수에 의한 객체 생성

 

[23.10.19] 생성자 함수에 의한 객체 생성

글의 목적 객체를 만드는 방식으로 객체 리터럴만 정리했는데 생성자 함수에 의한 객체 생성도 확실하게 개념을 잡아두고 싶어서 작성한다. 원래는 js엔진의 내부슬롯과 내부메서드를 다룰까

paikpaik.tistory.com

1.2.1 생성자 함수의 단점

  • 생성자 함수를 사용해서 인스턴스를 생성하면 동일한 메서드가 생성된다.
function Monster(hp) {
  this.hp = hp;
  this.attack = function () {
    return 0.1 * this.hp;
  };
}
const monster1 = new Monster(800);
const monster2 = new Monster(1600);
console.log(monster1.attack === monster2.attack); // false
// false이기 때문에 동일한 메서드가 서로 다른 참조 주소를 가지고 있다는 의미
  • 동일한 메서드의 중복은 메모리를 불필요하게 낭비하는 것과 같다. ()
  • 상속을 통해서 불필요한 중복을 제거할 수 있다.
function Monster(hp) {
  this.hp = hp;
}
Monster.prototype.attack = function () {
  return 0.1 * this.hp;
};

const monster1 = new Monster(800);
const monster2 = new Monster(1600);
console.log(monster1.attack === monster2.attack); // true
  • 원리를 간단하게 설명하자면 생성자함수와 객체의 프로토타입 객체를 따로 두고 동일한 프로토타입 객체에서 상속을 해주는것이기 때문에 true가 나온다.

1.3 프로토타입 객체

  • 프로토타입 객체는 객체 간 상속을 구현하기 위해 사용됨.
  • 프로토타입은 어떤 객체의 부모객체의 역할을 하는 객체로서 다른 객체의 공유 프로퍼티(메서드도 포함)를 제공함.
  • 프로토타입을 상속받은 자식 객체는 부모객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용할 수 있음.
  • 쉽게 생각해서 프로토타입은 능력(메서드)을 지닌 갑옷이라고 생각하면 됨. aws에 익숙하신 분들은 역할을 나눠주는 IAM이라고 생각해도 될듯. 생각만! 완전히 다름! :)
  • 모든 객체는 하나의 프로토타입을 가진다. (단 [[Prototype]]내부 슬롯의 값이 null이면 프로토타입은 없다.)
  • 모든 프로토타입은 생성자 함수와 연결되어 있다. (생성자함수 <-> 프로토타입 <-> 객체)

1.3.1 proto 접근자 프로퍼티

  • 모든 객체는 proto 접근자 프로퍼티를 통해 자신의 프로토타입, 즉 [[Prototype]] 내부 슬롯에 간접적으로 접근할 수 있다.

1.3.1.1 proto 는 접근자 프로퍼티다.

  • 내부슬롯과 내부메서드에 대해서 정리는 안했지만 한가지 중요한 점은 내부 슬롯은 프로퍼티가 아니라는 점이다.
  • js는 내부슬롯과 내부메서드에 접근과 호출을 제공하지 않고 간접적으로 proto 접근자 프로퍼티를 통해서 [[Prototype]]내부슬롯의 값이 프로토타입에 접근할 수 있다.
  • 사실 접근자 프로퍼티는 값을 가지지 않는다. 단지 [[get]]과 [[set]]으로 구성된 프로퍼티일 뿐이다.
  • 위에 말이 조금 어려울 수 있다. 내부슬롯과 내부메서드를 정리하지 않았기 때문. 하지만 중요한것은 객체에 proto 접근자 프로퍼티로 접근하면 자체적으로 값을 얻거나 가지는것이 아닌 [[get]]과 [[set]]을 통해서 값을 얻거나 할당한다 (교체가 적절)
const son = {};
const parent = { name: "mother" };

// getter 함수인 get __proto__가 호출되어 son 객체의 프로토타입을 얻어냄.(갑옷을 get함)
son.__proto__;

// setter 함수인 set __proto__가 호출되어 son 객체의 프로토타입을 교체함.(훔친 갑옷을 set함)
son.__proto__ = parent;

console.log(son); // {}
console.log(son.name); // mother

1.3.1.2 proto 접근자 프로퍼티는 상속을 통해 사용된다.

  • proto 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아니라 Object.prototype의 프로퍼티다
  • 그러므로 소유하는것이 아니라 모든 객체는 상속을 통해 Object.prototype.proto 접근자 프로퍼티를 사용하는 것이다.(교체가 적절하다고 쓴 이유)
const son = {};
const parent = { name: "mother" };

// 모든 객체는 __proto__ 프로퍼티를 소유하지 않는다.
console.log(son.hasOwnProperty("__proto__")); // false
console.log(parent.hasOwnProperty("__proto__")); // false

// __proto__ 접근자 프로퍼티는 모든 객체의 프로토타입 객체인 Object.prototype의 접근자 프로퍼티다.
console.log(Object.getOwnPropertyDescriptor(Object.prototype, "__proto__"));
/*{
  get: [Function: get __proto__],
  set: [Function: set __proto__],
  enumerable: false,
  configurable: true
}*/

// 모든 객체는 Object.prototype의 접근자 프로퍼티인 __proto__를 상속받아 사용할 수 있다.
console.log({}.__proto__ === Object.prototype); // true

1.3.1.3 proto 접근자 프로퍼티를 통해 프로토타입에 접근하는 이유

  • [[Prototype]] 내부 슬롯의 값인 프로토타입에 접근하기 위해 접근자 프로퍼티를 사용하는 이유는 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위함이다.
const child = { name: "son" };
const parent = { nick: "mother" };

child.__proto__ = parent;
parent.__proto__ = child;

console.log(child.nick); // TypeError: Cyclic __proto__ value
console.log(parent.name); // TypeError: Cyclic __proto__ value
  • 이렇게 각각의 객체가 프로토타입으로 설정했을때 에러가 나지 않으면 순환오류가 되기 때문에 에러를 발생시킨다.
  • 따라서 이렇게 proto 접근자 프로퍼티를 통해서 단방향 링크드 리스트로 구현을 해놓은 것이다.

1.3.1.4 proto 접근자 프로퍼티를 코드 내에서 직접 사용하는것은 권장하지 않음

  • proto 접근자 프로퍼티는 모든 객체가 사용할수 있는것은 아니다.(브라우저 지원문제, 직접 상속을 통한 객체생성)
const commonObj = {};
// nonPrototypeObj는 프로토타입 체인의 종점이다. 따라서 Object.__proto__를 상속받을 수 없다.
const nonPrototypeObj = Object.create(null);

console.log(commonObj); // {}
console.log(nonPrototypeObj); // [Object: null prototype] {}

// nonPrototypeObj는 Object.__proto__를 상속받을 수 없다.
console.log(commonObj.__proto__); // [Object: null prototype] {}
console.log(nonPrototypeObj.__proto__); // undefined

// 그래서 Object.getPrototypeOf 메서드를 사용하는것이 좋다.
console.log(Object.getPrototypeOf(commonObj)); // [Object: null prototype] {}
console.log(Object.getPrototypeOf(nonPrototypeObj)); // null
  • 따라서 Object.getPrototypeOf와 Object.setPrototypeOf 메서드로 proto 처리를 대신하는것을 권장함.
// 아까의 예문을 메서드를 사용해서 수정하면 이렇게 된다.
const son = {};
const parent = { name: "mother" };

// getter 함수인 get __proto__가 Object.getPrototypeOf메서드로 호출되어 
// son 객체의 프로토타입을 얻어냄.(갑옷을 get함)
Object.getPrototypeOf(son); // son.__proto__;

// setter 함수인 set __proto__가 Object.setPrototypeOf메서드로 호출되어 
// son 객체의 프로토타입을 교체함.(훔친 갑옷을 set함)
Object.setPrototypeOf(son, parent); // son.__proto__ == parent;

// 결과적으로 아까와 똑같다
console.log(son); // {}
console.log(son.name); // mother

1.4 객체 생성 방식과 프로토타입의 결정

  • 사실 내부적으로 더 복잡하고 각각의 프로토타입의 생성시점을 통해서 추상연산되는 과정을 정리해야하지만 그러면 객체를 정리하려는 나의 목적과는 너무 세세하게 들어가는것 같아서 앞에서 정리한 객체 생성 방식에 따른 프로토타입만 정리하려고 한다.
  • 아래 글을 보면 다양한 객체 생성 방식을 확인할 수 있다.

2023.10.19 - [데일리] - [23.10.17] 객체 리터럴

 

[23.10.17] 객체 리터럴

글의 목적 layered architecture로 express 서버를 만들던 중 그냥 만들면 MVC에서 뎁스하나만 추가하여 계층만 나눈것이기 때문에, 했던거 또 하느니 class로 dependency injection을 하면서 만들었는데 객체에

paikpaik.tistory.com

1.4.1 객체 리터럴에 의해 생성된 객체의 프로토타입

  • js엔진은 객체 리터럴을 평가하여 객체를 생성할때 추상연산을 호출하고 이때 전달되는 프로토타입은 Object.prototype이다.
  • 즉, 객체 리터럴에 의해 생성되는 객체의 프로토타입은 Object.prototype이다.
const obj = { name: "test" };

// 객체 리터럴에 의해 생성된 obj 객체는 Object.prototype을 상속받음.
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty("name")); // true

1.4.2 Objcet 생성자 함수에 의해 생성된 객체의 프로토타입

  • Object 생성자 함수에 의해 생성되는 객체의 프로토타입은 Object.prototype이다. (객체 리터럴과 동일)
const obj = new Object();
obj.name = "test";

// Object 생성자 함수에 의해 생성된 obj 객체는 Object.prototype을 상속받음.
console.log(obj.constructor === Object); // true
console.log(obj.hasOwnProperty("name")); // true

1.4.3 생성자 함수에 의해 생성된 객체의 프로토타입

  • 생성자 함수에 의해 생성되는 객체의 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩되어 있는 객체다.
  • 생성자 함수를 통해서 생성된 메서드는 즉각적으로 프로토타입 체인에 반영되고 상속을 통해서 자신의 메서드처럼 사용할 수 있다.
function Person(name) {
  this.name = name;
}

Person.prototype.sayHi = function () {
  console.log(`Hi I'm ${this.name}`);
};

const john = new Person("john");
const doe = new Person("doe");

john.sayHi(); // Hi I'm john
doe.sayHi(); // Hi I'm doe

결론

  • 여기까지가 프로토타입의 맛만 본 수준이다.
  • 이정도도 누군가에겐 깊은 내용일수 있지만 더 깊이 있게 다루지 않은 이유는 클래스 객체를 정리하는것이 최종 목표이고 클래스 객체를 이해하는데에는 이정도도 충분하다고 생각했기 때문이다.
  • 하지만 클래스를 정리하다가 꼭 필요한 부분이 있다면 다시 정리하고 수정하려고 한다.
  • 현재는 객체를 이해하기 위한 프로토타입만 다뤘다. 함수(함수도 객체지만)의 프로토타입은 전혀 건들지 않았다.