[모딥다] 19장.프로토타입

vanLan·2026년 2월 2일

모딥다

목록 보기
12/25
post-thumbnail

19장. 프로토타입

📁 객체지향 프로그래밍

  • 실체: 특징이나 성질을 나타내는 속성을 가지고 있고, 이를 통해 실체를 인식하거나 구별 가능.
  • 추상화: 다양한 속성 중에서 프로그램에 필요한 속성만 간추려 내어 표현하는 것.
  • 객체: 속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적인 자료구조.
  • 객체지향 프로그래밍: 이러한 독립적인 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임.
    • 객체의 상태를 나타내는 데이터와 상태 데이터를 조작할 수 있는 동작을 하나의 논리적 단위로 묶어서 생각함.
    • 객체는 상태 데이터와 동작을 하나의 논리적인 단위로 묶은 복합적인 자료구조라 할 수 있음.
    • 프로퍼티: 객체의 상태, 메서드: 객체의 동작

📁 상속과 프로토타입

  • 객체지향 프로그래밍의 핵심 개념으로, 어떤 객체의 프로퍼티 또는 메서드를 다른 객체가 상속받아 그대로 사용할 수 있는 것.

    // 생성자 함수
    function Circle(radius) {
      this.radius = radius;
      this.getArea = function () {
        return Math.PI * this.radius ** 2;
      }
    }
    
    const circle1 = new Cirecle(1);
    const circle2 = new Cirecle(2);
    
    // Circle 생성자 함수는 인스턴스를 생성할 때마다 동일한 동작을 하는
    // getArea 메서드를 중복 생성하고 모든 인스턴스가 중복 소유함.
    // getArea 메서드는 하나만 생성하여 모든 인스턴스가 공유해서 사용하는 것이 바람직함.
    console.log(circle1.getArea === circle2.getArea);  // false
  • JS는 프로토타입을 기반으로 상속을 구현하여 불필요한 중복을 제거.

    // 생성자 함수
    function Circle(radius) {
      this.radius = radius;
    }
    
    // Circle 생성자 함수가 생성한 모든 인스턴스가 getArea 메서드를
    // 공유해서 사용할 수 있도록 프로토타입에 추가.
    // 프로토타입은 Circle 생성자 함수의 prototype 프로퍼티에 바인딩 되어있음.
    Circle.prototype.getArea = function () {
      return Math.PI * this.radius ** 2;
    }
    
    const circle1 = new Cirecle(1);
    const circle2 = new Cirecle(2);
    
    console.log(circle1.getArea === circle2.getArea);  // true

📁 프로토타입 객체

  • 객체 간 상속을 구현하기 위해 사용됨.
  • 어떠한 객체의 부모 객체의 역할을 하는 객체로서 다른 객체에 공유 프로퍼티(메서드 포함)를 제공함.
  • 프로토타입 객체를 상속받은 자식 객체는 부모 객체의 프로퍼티를 자신의 프로퍼티처럼 자유롭게 사용 가능.
  • 모든 객체는 [[Prototype]]이라는 내부 슬롯을 가지며, 이 내부 슬롯의 값은 프로토타입의 참조임. [[Prototype]]에 저장되는 프로토타입은 객체의 생성 방식에 따라 결정됨.
  • 모든 객체는 하나의 프로토타입을 갖고, 그리고 모든 프로토타입은 생성자 함수와 연결되어 있음.
  • [[Prototype]] 내부 슬롯에 직접 접근할 수 없지만, __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입에 간접적으로 접근 가능.
  • 프로토타입은 자신의 constructor 프로퍼티를 통해 생성자 함수에 접근할 수 있고, 생성자 함수는 자신의 prototype 프로퍼티를 통해 프로토타입에 접근할 수 있음.
    [1. 생성자 함수 (Person)]
          |
          | (prototype 프로퍼티: "내 파트너는 쟤야")
          | ➡️ 참조값 저장
          |
          v
    [2. 프로토타입 객체 (Person.prototype)]
          |
          | (constructor 프로퍼티: "나를 만든 건 쟤야")
          | ➡️ 참조값 저장 (역참조)
          |
          ^
          |
          | (__proto__ 접근자: "내 부모님은 쟤야")
          | ➡️ 참조값 매핑 (상속)
          |
    [3. 인스턴스 (me)]
  • 헷갈릴수 있는 포인트
    • 생성자 함수를 사용하게 되면, 생성된 인스턴스의 [[prototype]] 내부슬롯은 부모 객체.prototype을 참조하게 됨.
    • 즉, 인스턴스의 진짜 상속의 대상은 생성자 함수가 아닌, 그 생성자 함수의 prototype 객체임.

__proto__ 접근자 프로퍼티

  • 내부 슬롯은 프로퍼티가 아니기 때문에, JS는 원칙적으로 내부 슬롯과 내부 메서드에 직접적으로 접근하거나 호출 할 수 없음.
  • 모든 객체는 __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입 객체, 즉 [[Prototype]] 내부 슬롯에 간접적으로 접근할 수 있음.

__proto__ 접근자 프로퍼티다.

  • 접근자 프로퍼티는 자체적으로 값([[Value]] 프로퍼티 어트리뷰트)를 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수, [[Get]], [[Set]] 프로퍼티 어트리뷰트로 구성된 프로퍼티임.

  • __proto__ 프로퍼티를 통해 프로토타입에 접근하면 내부적으로 [[Get]]이 호출, 새로운 프로토타입을 할당하면, [[Set]]이 호출됨.

    const obj = {};
    const parent = { x: 1 };
    
    obj.__proto__;
    obj.__proto__ = parent;
    
    console.log(obj.x);  // 1;

__proto__ 접근자 프로퍼티는 상속을 통해 사용됨.

  • __proto__ 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아닌 Object.prototype의 프로퍼티임.

  • 모든 객체는 상속을 통해 Object.prototype.__proto__ 프로퍼티를 사용할 수 있음.

    const person = { name: 'Lee' };
    
    // person 객체는 직접적으로 __proto__ 프로퍼티를 소유하지 않음.
    console.log(person.hasOwnProperty('__proto__'));  // false
    
    // 모든 객체는 Object.prototype의 접근자 프로퍼티 __proto__를 상속 받아 사용.
    console.log({}.__proto__ === Object.prototype);  // true
    console.log({}.__proto__ === person.__proto__);  // true

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

  • 상호 참조에 의해 프로토타입 체인이 생성되는 것을 방지하기 위해서임.

    const parent = {};
    const child = {};
    
    child.__proto__ = parent;
    parent.__proto__ = child;  // TypeError: Cyclic __proto__ value
  • 위 코드 처럼 서로가 자신의 프로토타입이 되는 비정상적인 프로토타입 체인(순환 참조)이 만들어지기 전 __proto__ 접근자 프로퍼티는 에러를 발생시킴.

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

  • Object.getPrototypeOf 메서드와 Object.setPrototypeOf 메서드를 사용하는 것을 권장.

    // 상속 받는 곳이 없는 순수 객체 생성
    const obj = Object.create(null);
    
    console.log(obj.__proto__);  // undefined
    const obj = {};
    const parent = { x: 1 };
    
    Object.getPrototypeOf(obj);  // obj.__proto
    Object.setPrototypeOf(obj, parent)  // obj.__proto__ = parent
    
    console.log(obj);  // 1

함수 객체의 prototype 프로퍼티

  • 함수 객체만이 소유하는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킴.
  • 책이 너무 어렵게 설명하고 있는 거 같음.
    어렵게 생각하지말고, 상속될 주체가 prototype이니까 함수에 의해 생성될 인스턴스가 가져갈 프로토타입을 갖는다 생각하자.
  • proto 접근자 프로퍼티는 모두가 갖고 있음. 왜? 부모 프로토타입에 접근하기 위해.
  • prototype 프로퍼티는 생성자 함수만 갖음.(화살표 함수를 제외한 함수가 다 갖고 있지만 생성자 함수가 아닌 함수들은 쓸모 없음) 왜? 자식 인스턴스에게 상속해야 되니까.

프로토타입의 constructor 프로퍼티와 생성자 함수

  • 모든 프로토타입 객체는 constructor 프로퍼티를 갖음.

  • 자신을 참조하고 있는 생성자 함수를 가리키려고.

    // 생성자 함수
    function Person(name) {
      this.name = name;
    }
    
    const me = new Person('Kim');
    
    // me 객체의 생성자 함수는 Person
    console.log(me.constructor === Person);  // true
    
    const member1 = new Person('철수');
    
    // member1이 뭘로 만들어졌는지 모르겠지만, 
    // 걔를 만든 기계(constructor)를 가져와서 영희를 또 찍어내자!
    const member2 = new member1.constructor('영희');
    
    console.log(member2.name); // '영희'
    console.log(member2 instanceof Person); // true

📁 리터럴 표기법에 의해 생성된 객체의 생성자 함수와 프로토타입

  • 책에서 겁나 어렵게 써있는데... JS엔진은 추상 연산을 통해 그에 맞는 생성자 함수와 프로토타입을 지정해 준다로 알고 넘어가면 될듯함.

    리터럴 표기법내부적으로 연결되는 생성자 함수상속받는 프로토타입
    객체 {}ObjectObject.prototype
    함수 function() {}FunctionFunction.prototype
    배열 []ArrayArray.prototype
    정규표현식 /abc/RegExpRegExp.prototype
  • 리터럴로 생성해도 생성자 함수와 같이 constructor__proto__가 연결됨.

    // 1. 객체 리터럴
    const obj = {};
    console.log(obj.constructor === Object); // true
    console.log(obj.__proto__ === Object.prototype); // true
    
    // 2. 배열 리터럴
    const arr = [1, 2, 3];
    console.log(arr.constructor === Array); // true
    console.log(arr.__proto__ === Array.prototype); // true
    
    // 3. 함수 리터럴
    const func = function() {};
    console.log(func.constructor === Function); // true
    console.log(func.__proto__ === Function.prototype); // true
  • 프로토타입과 생성자 함수는 단독으로 존재할 수 없고 언제나 쌍으로 존재.


📁 프로토타입의 생성 시점

  • 프로토타입 객체는 생성자 함수가 생성되는 시점에 더불어 생성됨.
  • 생성자 함수는 '빌트인 생성자 함수'와 '사용자 정의 생성자 함수'로 구분.

사용자 정의 생성자 함수와 프로토타입 생성 시점

  • 화살표 함수나, 메서드 축약 표현으로 정의 하지 않은 일반 함수로 정의한 함수 객체는 new 연산자와 함께 생성자 함수로 호출 가능.
  • 생성자 함수로 호출할 수 있는 함수는 함수 정의가 평가되어 함수 객체를 생성하는 시점에, constructor를 프로퍼티로 갖는 프로토타입 객체도 더불어 생성됨.
  • 이때 생성된 프로토타입 객체constructor 프로퍼티만을 갖는 객체이며, 생성된 프로토타입의 프로토타입은 Object.prototype임.

빌트인 생성자 함수와 프로토타입 생성 시점

  • 모든 빌트인 생성자 함수는 전역 객체가 생성되는 시점에 생성되며, 각 생성된 프로토타입 객체는 빌트인 생성자 함수의 prototype 프로퍼티에 바인딩되 있음.
  • 이후 생성자 함수 또는 리터럴 표기법으로 객체를 생성하면, 프로토타입은 생성된 객체의 [[Prototype]] 내부 슬롯에 할당됨.

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

  • 모든 객체는 추상 연산 OrdinaryObjectCreate에 의해 생성된다는 공통점이 있음.
  • 이는 자신이 생성할 객체의 프로토타입을 인수로 전달 받고,
    • 빈 객체를 먼저 생성.
    • 인수로 전달된 프로퍼티 목록을 객체에 추가.
    • 인수로 전달된 프로토타입을 생성한 객체의 [[Prototype]] 내부 슬롯에 할당.
    • 생성된 객체를 반환.

📁 프로토타입 체인

  • JS는 객체의 프로퍼티에 접근하려고 할때 해당 객체에 접근하려는 프로퍼티가 없다면 [[Prototype]] 내부 슬롯의 참조를 따라 자신의 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색.
  • 프로토타입 체인의 최상위에 위치하는 객체는 언제나 Object.prototype 임.

📁 오버라이딩과 프로퍼티 섀도잉

const Person = (function() {
  // 생성자 함수
  function Person(name) {
    this.name = name;
  }
  
  // 프로토타입 메서드
  Person.prototype.sayHello = function () {
    console.log(`Hi! My name is ${this.name}`)
  };
  
  // 생성자 함수를 반환
  return Person;
}());

const me = new Person('Kim');

// 인스턴스 메서드
me.sayHello = function () {
  console.log(`Hey! My name is ${this.name}`);
};

// 인스턴스 메서드가 호출됨. 프로토타입 메서드는 인스턴스 메서드에 의해 가려짐.
me.sayHello();  // Hey! My name is Kim
  • 프로토타입 프로퍼티와 같은 이름의 프로퍼티를 인스턴스에 추가하면 프로토타입 체인을 따라 프로토타입 프로퍼티를 검색하여 프로토타입 프로퍼티를 덮어쓰는 것이 아닌 인스턴스 프로퍼티로 추가함.
  • 이때 인스턴스 메서드 sayHello는 프로토타입 메서드 sayHello를 오버라이딩 했고, 프로토타입 메서드 sayHello는 가려지게 됨.
  • 이를 프로퍼티 섀도잉이라고 함.
// 인스턴스 메서드 삭제
delete me.sayHello;

me.sayHello();  // Hi! My name is Kim
  • 당연하게 프로토타입 메서드가 아닌 인스턴스 메서드 sayHello가 삭제됨.
// 프로토타입 체인을 통해 프로토타입 메서드는 삭제되지 않음
delete me.sayHello;

me.sayHello();  // Hi! My name is Kim
  • 하위 객체를 통해 프로토타입의 프로퍼티를 변경/삭제는 불가능.
// 프로토타입 메서드 변경
Person.prototype.sayHello = function () {
  console.log(`Hey! My name is ${this.name}`);
};
me.sayHello();  // Hey! My name is Lee

// 프로토타입 메서드 삭제
delete Person.prototype.sayHello;
me.sayHello();  // TypeError: me.sayHello is not a function
  • 프로토타입 프로퍼티를 변경/삭제 하려면 하위 객체를 통해 프로토타입 체인 접근이 아닌 프로토타입에서 직접 접근해야 함.

📁 프로토타입의 교체


📁 instanceof 연산자

객체 instanceof 생성자 함수
  • 어렵게 생각하지말고, 우변인 생성자 함수의 prototype 프로퍼티가 가리키는 객체가 좌변인 객체의 프로토타입 체인 상에 존재하는지(__proto__ 접근자로 타고 올라가 접근이 가능한지)를 판별하는 거라고 보면 됨.

📁 직접 상속

Object.create에 의한 직접 상속

  • 첫 번째 인자: 부모가 될 객체 (필수)

  • 두 번째 인자: 태어날 객체의 속성 정의 (선택, 좀 복잡함)

    // 부모가 될 객체
    const myProto = { x: 10 };
    
    // 1. "myProto를 부모로 삼는 객체 obj를 만들어라!"
    // (두 번째 인자는 생략 가능하지만, 쓴다면 이렇게 디스크립터로 써야 함)
    const obj = Object.create(myProto, {
      y: { value: 20, writable: true, enumerable: true, configurable: true }
    });
    
    console.log(obj.x); // 10
    console.log(obj.y); // 20
    console.log(Object.getPrototypeOf(obj) === myProto); // true

객체 리터럴 내부에서 proto에 의한 직접 상속

  • 가장 쉽고 직관적인 방법

    const myProto = { x: 10 };
    
    // "객체를 만드는데, 내 부모(__proto__)는 myProto로 설정할게."
    const obj = {
      y: 20,
      __proto__: myProto // 👈 여기서 바로 상속 지정!
    };
    
    console.log(obj.x); // 10 (상속됨)
    console.log(Object.getPrototypeOf(obj) === myProto); // true

📁 정적 프로퍼티/메서드

  • 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출할 수 있는 프로퍼티/메서드를 말함.

    // 생성자 함수
    function Person(name) {
      this.name = name;
    }
    
    // 프로토타입 메서드
    Person.prototype.sayHello = function () {
      console.log(`Hi! My name is ${this.name}`);
    };
    
    // 정적 프로퍼티
    Person.staticProps = 'static prop';
    
    // 정적 메서드
    Person.staticMethod = function () {
      console.log('staticMethod');
    }
    
    const me = new Person('Kim');
    
    // 생성자 함수에 추가한 정적 프로퍼티/메서드는 생성자 함수로 참조/호출
    Person.staticMethod();
    
    // 정적 프로퍼티/메서드는 인스턴스로는 참조/호출 불가
    me.staticMethod();  // TypeError: me.staticMethod is not a function
  • 생성자 함수가 생성한 인스턴스는 자신의 프로토타입 체인에 속한 객체의 프로퍼티/메서드에는 접근할 수 있지만, 정적 프로퍼티/메서드는 인스턴스의 프로토타입 체인에 속한 객체의 프로퍼티/메서드가 아니기 때문에 접근이 불가함.


📁 프로퍼티 존재 확인

in 연산자

key in object

// in 연산자 대신 Reflect.has 메서드를 사용할 수도 있음
Reflect.has(object, key);

Object.prototype.hasOwnPropery 메서드

  • 인수로 전달받은 프로퍼티 키가 객체의 고유의 프로퍼티 키인 경우에만 true를 반환
  • 상속 받은 프로토타입의 프로퍼티 키인 경우 false를 반환 함.

📁 프로퍼티 열거

for ... in 문

  • 객체의 모든 프로퍼티를 순회하며 열거.
const person = {
  name: 'Kim',
  address: 'Seoul'
};

for (const key in person) {
  console.log(key + ': ' + person[key]);
}
  • 프로퍼티의 프로퍼티 어트리뷰트 [[Enumerable]]의 값이 true인 것들만 순회하며 열거.
  • 프로퍼티 키가 심벌인 프로퍼티는 열거하지 않음.
  • 상속받은 프로퍼티는 제외하고 객체 자신의 프로퍼티만 열거하려면 Object.prototype.hasOwnPropery 메서드를 사용하여 조건부 확인 해야함.
  • 프로퍼티를 열거할 때 순서를 보장하지 않음. (모던 브라우저는 순서를 보장)

Object.keys/values/entries 메서드

const person = {
  name: 'Kim',
  address: 'Seoul',
  __proto__: { age: 20 }
};

console.log(Object.keys(person));  // ["name", "address"]
console.log(Object.values(person));  // ["Kim", "Seoul"]
console.log(Object.entries(person));  // [["name", "Kim"], ["address", "Seoul"]]
profile
프론트엔드 개발자를 꿈꾸는 이

0개의 댓글