본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로프로그래밍 › JavaScript › 언어 기능

프로토타입과 상속

목차

프로토타입과 상속

자바스크립트에서 상속은 함수의 프로토 타입을 활용하고, 다른 객체에 공유 프로퍼티(shared properties)를 제공하는 객체이다.

ES6에서 화살표 함수는 프로토타입을 가지지 않는다.

객체에서 프로퍼티를 읽으면 먼저 객체 자신을 찾고, 없으면 프로토타입 연결을 따라 올라간다. 아래 두 객체는 같은 greet 함수를 공유한다.

const personMethods = {
  greet() { return `안녕하세요, ${this.name}`; }
};

const kim = Object.create(personMethods);
kim.name = "Kim";
const lee = Object.create(personMethods);
lee.name = "Lee";

console.log(kim.greet()); // 안녕하세요, Kim
console.log(Object.getPrototypeOf(kim) === personMethods); // true
flowchart LR
  kim["kim: name"] --> methods["personMethods: greet()"]
  lee["lee: name"] --> methods
  methods --> proto[Object.prototype]

class 문법도 이 연결을 사용하는 표현이다. 메서드는 보통 클래스의 프로토타입에 놓이고 인스턴스가 공유한다. 화살표 함수는 일반 함수와 달리 생성자로 사용할 수 없고 자체 prototype 프로퍼티도 없다. 하지만 화살표 함수를 객체의 필드에 할당할 수는 있다. 이 경우 각 인스턴스에 함수가 생기므로 메서드 공유 방식과 다르다.

같은 이름의 프로퍼티가 있으면

console.log(kim.greet()); // personMethods의 메서드를 찾는다.
kim.greet = () => '별도 인사';
console.log(kim.greet()); // kim 자신의 메서드를 먼저 찾는다.
console.log(lee.greet()); // lee는 공유 메서드를 계속 사용한다.

객체 자신에 같은 이름의 프로퍼티를 만들면 프로토타입의 값을 가린다. 프로토타입의 메서드를 수정하면 이를 가리지 않은 인스턴스의 동작이 함께 바뀔 수 있다. 그래서 공통 동작과 인스턴스별 상태를 어디에 둘지 구분해야 한다.

console.log(Object.hasOwn(kim, 'name'));  // true
console.log(Object.hasOwn(lee, 'greet')); // false

프로퍼티가 객체 자신의 것인지 확인할 때는 Object.hasOwn을 쓴다. API 응답처럼 출처를 신뢰할 수 없는 객체를 순회할 때는 상속된 프로퍼티가 섞일 수 있다는 점도 고려한다. 단순한 데이터 묶음에는 상속 계층을 깊게 만들기보다 필요한 필드와 함수만 두는 편이 읽기 쉽다.

[[Prototype]]과 생성자의 .prototype

이름이 비슷한 두 대상을 구별해야 한다. 모든 일반 객체는 다른 객체를 가리키는 내부 [[Prototype]] 연결을 가질 수 있다. 반면 생성자 함수의 .prototype 프로퍼티는 new가 새 인스턴스의 [[Prototype]]으로 사용할 객체를 가리킨다. Object.getPrototypeOf(instance)로 앞의 연결을 확인할 수 있다.

function Person(name) {
  this.name = name;
}
Person.prototype.greet = function () {
  return `안녕하세요, ${this.name}`;
};

const park = new Person('Park');
console.log(Object.getPrototypeOf(park) === Person.prototype); // true
console.log(park.greet()); // 안녕하세요, Park

new Person은 새 객체를 만들고 Person.prototype에 연결한 뒤 Person을 새 객체의 this로 호출한다. greet 함수는 인스턴스마다 복사되지 않는다. 앞의 Object.create(personMethods) 예제는 생성자 호출 없이 같은 종류의 연결을 직접 만든다. 두 방식을 한 객체에 섞을 이유는 없지만 라이브러리의 객체 구조를 읽을 때 차이를 알면 유용하다.

위에서 kim.greet를 자신의 프로퍼티로 만들었으므로 이후 personMethods.greet를 바꿔도 kim.greet()는 별도 인사를 반환한다. lee.greet()는 바뀐 공유 메서드를 따라간다. 값을 읽을 때마다 프로토타입 체인을 탐색하기 때문이다. 객체 생성 시점에 부모의 메서드를 복사해 둔 결과가 아니다.

순회할 때의 함정

const record = Object.create({ inherited: '공유값' });
record.own = '내 값';

console.log('inherited' in record);             // true
console.log(Object.hasOwn(record, 'inherited')); // false
console.log(Object.keys(record));                // ['own']

in은 프로토타입에서 찾은 속성까지 포함하지만 Object.hasOwn은 객체 자신의 속성만 검사한다. Object.keys도 열거 가능한 자기 속성만 반환한다. for...in은 열거 가능한 상속 속성까지 순회하므로, 외부 JSON이나 설정 객체에서 자기 속성만 처리하려면 구분이 필요하다. Object.create(null)로 만든 객체는 Object.prototype 메서드도 상속하지 않으므로 record.hasOwnProperty(...) 같은 직접 호출보다 Object.hasOwn(record, ...)이 안전하다.

class extends는 이 구조를 가리는 편의 문법이다. 공유 메서드가 많은 객체라면 class나 프로토타입 메서드를 고려하고, 독립적인 데이터 레코드라면 상속 관계를 만들 필요가 없다. 실행 중 프로토타입 자체를 갈아 끼우면 기존 객체의 속성 탐색과 엔진 최적화에 영향을 줄 수 있으므로, 객체를 만들 때 연결을 정하는 쪽이 예측하기 쉽다.

참고: MDN 프로토타입과 상속, MDN 속성 소유권과 열거.

같은 카테고리의 글