모던 자바스크립트 Deep Dive - 19. 프로토타입

둡둡·2024년 1월 23일

Modern Javascript Deep Dive

목록 보기
20/49

19. 프로토타입

  • 자바스크립트는 명령형, 함수형, 프로토타입 기반 객체 지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어
  • 자바스크립트는 객체 기반의 프로그래밍 언어
  • 원시 타입을 제외한 나머지 거의 모든 것이 '객체'

클래스(class)

  • ES6에서 도입된 클래스
  • 생성자 함수보다 엄격하며 클래스만의 함수도 제공함
  • 새로운 객체 생성 매커니즘으로 보여짐

19.1. 객체 지향 프로그래밍

  • 여러 개의 독립적 단위인 객체(object)의 집합으로 표현하는 프로그래밍 패러다임
    • 객체: 속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적인 자료 구조
  • 실세계의 실체(사물이나 개념)을 프로그래밍에 접목하려는 시도
    • 실체는 특징이나 성질을 나타내는 속성(attribute/property)을 가짐
    • 구체적인 속성을 통해 다른 실체와 구별하여 인식함
    • 다양한 속성 중에서 프로그램에 필요한 속성만 표현하는 것을 추상화(abstraction)
  • 객체 지향 프로그래밍은 객체의 상태(state) 데이터와 이를 조작할 수 있는 동작을 하나의 논리적인 단위로 묶은 복합적인 자료구조
    • 상태: 프로퍼티(property)
    • 동작: 메서드(method)
  • 관계성(relationship): 다른 객체와 메시지를 주고받거나 데이터를 처리하고, 다른 객체를 상속받아 사용하기도 함

19.2. 상속과 프로토타입

  • 상속(inheritance): 객체의 프로퍼티와 메서드를 다른 객체가 상속받아 사용할 수 있음
    • 객체 지향 프로그래밍의 핵심 개념
    • 코드의 재사용 -> 프로토 타입 기반의 상속으로 불필요한 중복 제거
  • 생성자 함수로 생성한 객체는 모두 각각 프로퍼티와 메서드를 가짐
    • 프로퍼티는 다를 수 있지만, 동일한 내용의 메서드는 하나만 생성하여 공유하는 것이 바람직함
    • 하지만 생성자 함수는 인스턴스를 생성할 때마다 모든 인스턴스가 메서드를 중복 소유함
    • 불필요한 메모리 낭비, 퍼포먼스 악영향 -> 상속을 통해 중복 제거 가능
  • 모든 인스턴스가 공통적으로 사용할 생성자 함수의 프로퍼티나 메서드를 프로토 타입으로 미리 구현하면 별도 구현 없이 공유하여 재사용 가능
// 생성자 함수
function Circle(radius) {
  this.radius = radius;
]

// Circle 생성자 함수의 getArea 메서드를 프로토타입에 추가
Circle.prototype.getArea = function() {
  return Math.PI * this.radius ** 2;
}

// 인스턴스 생성
const circle1 = new Circle(1);
const circle2 = new Circle(2);

// Circle 생성자 함수가 생성한 모든 인스턴스는 
// 부모 객체의 역할을 하는 프로토타입의 메서드를 상속 받음, 공유함
console.log(circle1.getArea === circle2.getArea); // true

console.log(circle1.getArea()); // 3.141592
console.log(circle2.getArea()); // 12.566370

19.3. 프로토타입 객체

  • 프로토타입 객체는 객체 간 상속을 구현하기 위해 사용됨
  • 모든 객체는 [[Prototype]] 내부 슬롯을 가짐
    • 객체 생성 방식에 따라 프로토타입이 결정되어 내부슬롯에 저장됨
    • 객체 리터럴 -> Object.prototype
    • 생성자 함수 -> prototype 프로퍼티에 바인딩되어 있는 객체
  • 모든 객체는 하나의 프로토타입을 가짐
    • 모든 프로토타입은 생성자 함수와 연결되어 있음
    • __proto__ 접근자 프로퍼티를 통해 자신의 프로토타입에 간접적으로 접근

19.3.1. __proto__ 접근자 프로퍼티

  • 모든 객체는 접근자 프로퍼티를 통해 자신의 프로퍼티 타입([[Prototype]] 내부 슬롯)에 간접적으로 접근 가능

__proto__는 접근자 프로퍼티

  • 내부 슬롯은 프로퍼티가 아님
  • [[Prototype]] 과 같은 일부 내부 슬롯은 접근자 프로퍼티를 통해 간접적으로 접근 가능
  • 접근자 프로퍼티는 자체적인 값이 아닌 접근자 함수 [[Get]]/[[Set]](getter/setter) 프로퍼티 어트리뷰트로 구성된 프로퍼티
const obj = {};
const parent = { x: 1 };

// getter 함수인 get __proto__ 호출하여 객체의 프로토타입 취득
obj.__proto__; 

// setter 함수인 set __proto__ 호출하여 객체 프로토타입 교체
obj.__proto__ = parent;

console.log(obj.x); // 1

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

  • 접근자 프로퍼티는 객체가 직접 소유하는 프로퍼티가 아닌, Object.prototype의 프로퍼티를 상속 받음
프로토타입 체인
  • 객체의 프로퍼티에 접근하려고 할 때, 해당 프로퍼티가 없다면 접근자 프로퍼티가 가리키는 참조를 따라 부모 역할을 하는 프로토타입의 프로퍼티를 순차적으로 검색함
  • 프로토타입 체인의 최상위 객체는 Object.prototype

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

  • 상호 참조로 인해 프로토타입 체인이 생성되는 것을 방지하기 위해 사용
  • 프로토타입 체인은 단방향 링크드 리스트로 구현되어야 함
  • 양방향으로 순환 참조하는 체인이 만들어진다면 체인 종점이 없어 무한 루프에 빠짐

__proto__ 접근자 프로퍼티를 코드 내에서 직접 사용하는 것은 지양

  • ES6에서 표준으로 채택한 접근자 프로퍼티
  • 모든 객체가 접근자 프로퍼티를 사용할 수 있는 것은 아니므로 코드에서 직접 사용하는 것은 권장하지 않음
    • 직접 상속을 통해 Object.prototype을 상속받지 않는 객체를 생성할 수도 있음
  • 프로토타입 참조: Object.getPrototypeOf 메서드 사용 권장
  • 프로토타입 교체: Object.setPrototypeOf 메서드 사용 권장

19.3.2. 함수 객체의 prototype 프로퍼티

  • 함수 객체만 소유함
  • 함수가 생성할 객체(인스턴스)의 프로토타입을 가리킴
  • 생성자 함수로서 호출할 수 없는 함수, non-constructor(화살표 함수, 메서드 축약 표현) 메서드는 소유하지 않음
  • 모든 객체가 가지고 있는 접근자 프로퍼티와 함수 객체만 가지고 있는 prototype 프로퍼티는 결국 동일한 프로토타입을 가리킴
    • 사용 주체만 다름(접근자 프로퍼티: 모든 객체, prototype 프로퍼티: 생성자 함수)

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

  • 모든 프로토타입은 constructor 프로퍼티를 가짐
  • constructor 프로퍼티는 자신을 참조하고 있는 생성자 함수를 가리킴
    • 함수 객체가 생성될 때 이뤄짐

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

  • 생성자 함수에 의해 생성된 객체(인스턴스)는 생성자 함수와 연결됨
  • constructor 프로퍼티가 가리키는 생성자 함수는 인스턴스를 생성한 생성자 함수
const me = new Person('Kim');
console.log(me.constructor === Person); // true
  • 리터럴 표기법에 의해 생성된 객체의 생성자 함수는 반드시 객체를 생성한 생성자 함수라고 단정할 수 없음
    • 값을 전달하지 않는 등의 여러 경우에 따라 생성 과정 등에 차이가 있음
  • 리터럴 표기법에 의해 생성된 객체도 상속을 위해 프로토타입이 필요하므로, 가상적인 생성자 함수를 가짐
  • 프로토타입은 생성자 함수와 함께 생성되어 prototype, constructor 프로퍼티에 의해 연결되어 있음
    • 프로토타입과 생성자 함수는 항상 쌍(pair)으로 존재함
    • 객체/함수로서 동일한 특성을 가짐
  • 프로토타입의 constructor 프로퍼티를 통해 연결된 생성자 함수 = 리터럴 표기법으로 생성한 객체의 생성자 함수

19.5. 프로토타입 생성 시점

  • 모든 객체는 생성자 함수와 연결됨
  • 프로토타입은 생성자 함수가 생성되는 시점에 함께 생성됨

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

  • 생성자 함수로서 호출할 수 있는 함수(constructor)는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 함께 생성함
    • 함수 호이스팅 시점에 생성된 프로토타입은 오직 constructor만을 가짐
    • 프로토타입도 객체 -> 모든 객체는 프로토타입을 가짐 -> 프로토타입도 자신의 프로토타입을 가짐
    • 생성된 프로토타입의 프로토타입은 Object.prototype
// 함수 호이스팅
console.log(Person.prototype); // { constructor: f }

// 생성자 함수
function Person(name) {
  this.name = name;
}
  • non-constructor는 프로토타입이 생성되지 않음
// 화살표 함수 (non-constructor)
const Person = name => {
  this.name = name;
}

console.log(Person.prototype); // undefined

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

  • 모든 빌트인 생성자 함수(Object, String, Number 등)는 전역 객체가 생성되는 시점에 프로토타입이 생성됨
  • 생성된 프로토타입은 빌트인 생성자 함수의 prototype 프로퍼티에 바인딩
  • 객체가 생성되기 전에 생성자 함수와 프로토타입은 이미 객체화되어 존재함
  • 이후 객체를 생성할 때 생성된 객체의 [[Prototype]] 내부 슬롯에 할당되고, 프로토타입을 상속 받음

19.6. 객체 생성 방식과 프로토타입 결정

  • 다양한 방식으로 생성된 모든 객체는 추상 연산(OrdinaryObjectCreate)에 의해 생성됨
  • OrdinaryObjectCreate
    • 생성할 객체의 프로토타입을 인수로 전달 받음
    • 생성할 객체에 추가할 프로퍼티 목록을 옵션으로 전달
    • 먼저 빈 객체를 생성한 후 프로퍼티를 객체에 추가함
    • 인수로 전달받은 프로토타입을 생성한 객체의 [[Prototype]] 내부 슬롯에 할당하여 반환

19.6.1. 객체 리터럴 생성

  • 객체 리터럴에 의해 생성되는 객체의 프로토타입은 Object.prototype

19.6.2. Object 생성자 함수 생성

  • Object 생성자 함수에 의해 생성되는 객체의 프로토타입도 Object.prototype
  • 객체 리터럴과 Object 생성자 함수에 의한 객체 생성 방식은 프로퍼티를 추가하는 방식에 차이가 있음
    • 객체 리터럴은 객체 리터럴 내부에 프로퍼티 추가
    • Object 생성자 함수는 빈 객체 생성 후 프로퍼티 추가

19.6.3. 생성자 함수

  • new 연산자로 생성자 함수를 호출하여 생성하는 경우의 프로토타입은 생성자 함수의 prototype 프로퍼티에 바인딩 된 객체

    • 표준 빌트인 객체 (Object 등) 생성자 함수의 프로토타입은 다양한 빌트인 메서드를 가짐(hasOwnProperty, propertyIsEnumerable 등)
    • 사용자 정의 생성자 함수의 프로토타입은 constructor만 가짐
    • 프로토타입에 프로퍼티를 추가하여 하위(자식) 객체가 상속 받으면 모든 객체가 사용 가능함
    function Person(name) {
      this.name = name;
    }
    
    // 프로토타입 메서드
    Person.prototype.sayHello = function() {
      console.log(`My name is ${this.name}`);
    }
    
    const me = new Person('Kim');
    const you = new Person('Park');
    
    me.sayHello; // My name is Kim
    you.sayHello; // My name is Park

19.7. 프로토타입 체인

  • 위 Person 생성자 함수로 생성된 me 객체는 Object.prototype의 메서드인 hasOwnProperty 호출 가능
    • Person.prototype 뿐만 아니라, Object.prototype 까지 상속 받음
  • 자바스크립트는 객체의 프로퍼티(메서드 포함)에 접근할 때, 접근하려는 해당 프로퍼티가 없다면 [[Prototype]] 내부 슬롯의 참조를 따라 부모 역할의 프로토타입의 프로퍼티까지 순차적으로 검색함 -> 프로토타입 체인
    • 자바스크립트가 객체지향 프로그래밍의 상속을 구현하는 메커니즘
  • 프로토타입 체인의 최상위 객체는 Object.prototype
    • 모든 객체는 Object.prototype을 상속 받음
    • 프로토타입 체인의 종점(end of prototype chain)
  • 프로토타입 체인은 상속과 프로퍼티 검색을 위한 메커니즘
    • 반대로 식별자는 스코프 체인을 통해 검색
    • 스코프 체인은 식별자 검색을 위한 메커니즘
  • 스코프 체인과 프로토타입 체인은 서로 협력하여 식별자와 프로퍼티를 검색하기 위해 사용됨

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

  • 프로토타입 프로퍼티: 프로토타입이 소유한 프로퍼티(와 메서드)
  • 인스턴스 프로퍼티: 인스턴스가 소유한 프로퍼티
  • 프로퍼티 섀도잉: 덮어쓰기가 아닌 상속 관계에 의해 프로퍼티가 가려지는 현상 (오버라이딩)
    • 프로토타입 프로퍼티와 같은 이름으로 인스턴스에 추가하면 인스턴스 프로퍼티로 추가함
    • 삭제하는 경우, 인스턴스 메서드만 삭제되고 프로토타입 메서드가 호출됨

19.9. 프로토타입의 교체

  • 프로토타입은 임의의 다른 객체로 변경 가능
    • 객체 간의 상속 관계를 동적으로 변경
    • 생성자 함수 또는 인스턴스에 의해 교체
  • 클래스를 통해 직관적으로 상속 관계를 구현할 수 있으므로 교체하는 것은 지양

19.9.1. 생성자 함수

  • 프로토타입으로 교체한 객체 리터럴에는 constructor 프로퍼티가 없음
  • 프로토타입을 교체하면 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴됨

19.9.2. 인스턴스

  • __proto__ 접근자 프로퍼티 또는 Object.setPrototypeOf 메서드를 통해 프로토타입 교체 가능
// 동일하게 me 객체의 프로토타입을 parent 객체로 교체
Object.setPrototypeOf(me, parent);
me.__proto__ = parent;
  • 동일하게 constructor 프로퍼티와 생성자 함수 간의 연결이 파괴됨

19.10. instanceof 연산자

  • instanceof 연산자
    • 좌변의 식별자가 우변의 생성자 함수 프로토타입 체인에 존재하는지 평가함
    • 생성자 함수의 prototype에 바인딩된 객체가 프로토타입 체인 상에 존재하는지 확인
// 생성자 함수
function Person(name) {
  this.name = name;
}

const me = new Person('Kim');

console.log(me instanceof Person); // true
console.log(me instanceof Object); // false

19.11. 직접 상속

19.11.1. Object.create에 의한 직접 상속

  • Object.create 메서드: 명시적으로 프로토타입을 지정하여 새로운 객체 생성
    • new 연산자 없이 객체 생성 가능
    • 프로토타입을 지정하면서 객체 생성 가능
    • 객체 리터럴에 의해 생성된 객체도 상속 가능
/**
* 지정된 프로토타입 및 프로퍼티를 갖는 새로운 객체를 생성하여 반환한다 
* @param {object} prototype 생성할 객체의 프로토타입으로 지정할 객체
* @param {object} [propertiesObject] 생성할 객체의 프로퍼티를 갖는 객체
* @returns {object} 지정된 프로토타입 및 프로퍼티를 갖는 새로운 객체
**/
Object.create(prototype[, propertiesObject])
  • 모든 객체가 Object.prototype의 빌트인 메서드를 상속받아 호출 가능하지만 지양
    • 간접 호출 권장

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

  • ES6에서는 객체 리터럴 내부에서 __proty__ 접근자 프로퍼티를 사용하여 직접 상속 구현
const myProto = { x: 10 };

const obj = {
  y: 20,
  __proto__: myProto
}

console.log(obj.x, obj.y); // 10 20
console.log(Object.getPrototypeOf(obj) === myProto); // true

19.12. 정적 프로퍼티/메서드

  • 정적 (static) 프로퍼티/메서드는 생성자 함수로 인스턴스를 생성하지 않아도 참조/호출 가능
  • 생성자 함수 객체가 소유한 프로퍼티/메서드 = 정적 프로퍼티/메서드
  • 인스턴스로 참조/호출할 수 없음
// 생성자 함수
function Person(name) {
  this.name = name;
}

// 정적 프로퍼티
Person.staticProp = 'static prop';

// 정적 메서드
Person.staticMethod = function() {
  console.log('staticMethod');
}

const me = new Person('Lee');

Person.staticMethod(); // statchMethod
me.staticMethod(); // TypeError
  • cf. 프로토타입 프로퍼티/메서드 표기 시, prototype -> # 으로 표기하기도 함
    (ex. Object.prototype.isPrototypeOf -> Object#isPrototypeOf)

19.13. 프로퍼티 존재 확인

19.13.1. in 연산자

  • 객체 내 특정 프로퍼티 존재 여부 확인
/**
* key : 프로퍼티 키를 나타내는 문자열
* object : 객체로 평가되는 표현식
**/
key in object

////////////////

const person = {
  name: 'Lee',
  address: 'Seoul'
};

console.log('name' in person); // true
console.log('address' in person); // true
console.log('age' in person); // false
  • 확인 대상 객체가 상속받은 모든 프로토타입의 프로퍼티를 확인하므로 주의
    • ex. 'toString' in person -> true
  • ES6에서 도입된 Reflect.has(object, key) : in 연산자와 동일하게 동작

19.13.2 Object.prototype.hasOwnProperty 메서드

  • 특정 프로퍼티 존재 여부 확인
  • Object.hasOwnProperty(key)
    • 전달받은 프로퍼티 키가 객체 고유의 프로퍼티 키인 경우에만 true 반환
    • ex. person.hawOwnProperty('toString) -> false

19.14. 프로퍼티 열거

19.14.1. for ... in 문

  • for (변수선언문 in 객체) {... } : 객체의 모든 프로퍼티를 순회하며 열거함
  • in 연산자와 동일하게 상속받은 프로토타입의 프로퍼티까지 열거함
    • toString은 열거할 수 없도록 정의된 프로퍼티이므로 열거되지 않음
    • prototype.toString의 [[Enumerable]] : false 이기 때문
  • 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중에서 [[Enumerable]]의 값이 true 프로퍼티를 순회하며 열거함
  • for ... in 문은 순서를 보장하지 않음
    • 하지만 대부분 모던 브라우저가 순서를 보장하여 정렬함
  • 일반적으로 for 문, for ... of, Array.prototype.forEach 사용 권장

19.14.2. Object.keys/values/entries 메서드

  • Object.keys : 자신의 열거 가능한 프로퍼티 키를 배열 반환
  • Object.values : ES8 도입, 자신의 열거 가능한 프로퍼티 값을 배열 반환
  • Object.entries : ES8 도입, 자신의 열거 가능한 프로퍼티 키와 값의 쌍을 배열로 반환
const person = {
  name: 'Lee',
  address: 'Seoul'
}
console.log(Object.entries(person)); // [['name', 'Lee'], ['address', 'Seoul']]

[출처] 모던 자바스크립트, Deep Dive

profile
괴발개발라이프

0개의 댓글