Frontend/Today I Learned

[JS] 프로토타입 Prototype

joycie416 2024. 12. 17. 19:40

※ 본 내용은 코어 자바스크립트 (정재남)의 내용을 정리한 글입니다.

 

앞서 정리한 프로토타입 기반 객체 지향 언어인 JS에 대해 이어서 이야기해보자.

 

01 프로토타입 개념

프로토타입(prototype)은 단어 그 자체처럼 원형, 원조로 생각하면 된다. 상속된다는 느낌보다 대를 잇는 느낌으로 보면 된다.

 

01-1 Constructor, Instance, Prototype

프로토타입을 이해할 때 중요한 개념은 생성자(constructor) 함수, 프로토타입(prototype), 인스턴스(instance)와 `__proto__`이다. '코어 자바스크립트'에서는 아래 삼각형으로 그 관계를 설명하고 있다.

 

출처 하단 참고 문서에 기입

 

JS의 함수는 본래 prototype 속성을 가지고 있고, new 키워드를 통해 생성된 인스턴스는 `__proto__`라는 속성을 가지고 있다. 앞서 말했듯이 프로토타입은 대를 잇는 것과 유사하고, 그 대는 `__proto__`와 `prototype`으로 이어진다. 생성자(constructor)의 원형(`prototype`, 원조)이 있으면 그 생성자(constructor)에서 파생된 인스턴스(instance)는 `__proto__`라는 속성을 가지고, `__proto__` 속성은 한 단계 위, 즉 생성자(constructor)의 `prototype`을 참조한다. 

 

위 삼각형에서 `Instance.__proto__`는 회색으로 표현된 이유는 `__proto__`라는 속성을 직접 사용하지 않고도 prototype에 추가된 속성이나 메서드를 사용할 수 있기 때문이다. 아래 예시를 살펴보자.

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

Person.prototype.getYear = function () {
  return this.year;
}
Person.prototype.species = '인간';

const kim = new Person ('김철수', 2000);

console.log(kim.__proto__.getYear()) // undefined
// console.log(Object.getPrototypeOf(kim).getYear()) // undefined (윗 줄과 동일)
console.log(kim.getYear()) // 2000

 

첫 번째 console.log에서 undefined가 출력된 이유는 getYear의 호출 주체가 `kim.__proto__`이기 때문인데, 해당 객체에는 year이라는 속성이 없으므로 undefined가 출력된 것이다. `__proto__`를 생략하면 getYear의 호출 주체가 kim이 되어 원하는대로 2000이 잘 출력되었다. 이처럼 JS에서는 `__proto__`를 생략하고 `prototype`의 메서드를 사용할 수 있는 것이다.

 

다시 한번 정리하면, `__proto__` 속성은 생략 가능하도록 구현되어 있기 때문에 생성자 함수의 prototype에 어떤 메서드나 속성이 있다면 instance에서도 마치 자신의 것처럼 해당 메서드나 속성에 접근할 수 있게 된다.

 

하지만 현재 크롬 브라우저 콘솔창에 `console.dir(kim)`을 입력하면 `__proto__` 속성이 보이지 않을 것이다. 대신 `[[prototype]]`을 보여주고 있다. 이는 ES5부터 `__proto__`대신 `[[prototype]]`이라는 명칭으로 정의되어 있을 뿐만 아니라 `kim.__proto__`와 같이 직접 접근하는 것을 허용하지 않기 때문이다. `kim.__proto__`에 접근하려면 `Object.getPrototypeOf(kim)`을 사용하면 된다. 이 글에서는 편의상 `__proto__`를 계속 사용하도록 하겠다.

console.dir(kim)

 

 

01-2 `constructor` 속성

생성자 함수의 prototype 속성에는 `contructor`라는 속성이 있다. 마찬가지로 인스턴스의 `__proto__` 속성 내부에도 `constructor` 속성이 있다. 이 속성은 원래의 생성자 함수(자기 자신)을 참조한다. 인스턴스의 원형을 알려주는 수단으로 사용된다.

console.log(Person.prototype.constructor === Person) // true
console.log(kim.__proto__.constructor === Person) // true
// console.log(Object.getPrototypeOf(kim).constructor === Person) // true

 

하지만 읽기 전용 속성이 부여된 경우가 아니라면 `constructor` 속성을 직접 변경할 수 있다.

function Animal () {};
Animal.prototype.species = '동물';

const animal = new Animal();
animal.constructor = Person;

console.log(animal.constructor); // [Function: Person]
console.log(animal instanceof Person) // false

const what = new animal.constructor('hi', 2024);
console.log(what) // Person {name: 'hi', year: 2024 }

 

 

02 프로토타입 체인

02-1 메서드 override

메서드는 크게 정적 메서드와 비정적 메서드 두 종류가 있다. 정적 메서드는 생성자 함수를 통해 인스턴스화 되면 사용될 수 없으며, 비정적 메서드는 인스턴스에서 사용할 수 있다. 즉 Array.prototype이 아닌 Array에만 사용할 메서드는 정적메서드이고, Array의 인스턴스에도 사용할 수 있는 메서드는 비정적 메서드인 것이다. (`Array.from`는 정적 메서드이고 `map`, `forEach` 등은 비정적 메서드이다.)

 

그렇다면 인스턴스의 메서드가 정적 메서드와 이름이 같다면 어떻게 될까?

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

Person.prototype.getYear = function () {
  return this.year;
}
Person.prototype.species = '인간';
Person.prototype.getName = function () {
  return this.name;
}

const kim = new Person ('김철수', 2000);

kim.getName = function() {
  return '내 이름은 '+ this.name;
}

console.log(kim.getName()) // '내 이름은 김철수'

 

`kim`의 `getName`이 호출된 것을 확인할 수 있다. 그러면 `Person`의 `getName`을 호출하려면 어떻게 하면 될까? `kim.__proto__.getName()`을 떠올릴 가능성이 크다. 하지만 실행해보면 undefined가 출력될 것이다. 이 이유는 앞서 언급했듯이 `getName`의 호출주체가 `kim`이 아니라 `kim.__proto__`이기 때문이다. 따라서 `Person`, 즉 원형(prototype)의 `getName`을 제대로 사용하려면 this 바인딩이 필요하다.

console.log(kim.getName()); // '내 이름은 김철수'
console.log(kim.__proto__.getName.call(kim)); // '김철수'
// console.log(Object.getPrototypeOf(kim).getName.call(kim)) // '김철수'

 

02-2 프로토타입 체인

그러면 클래스처럼 프로토타입을 상속하려면 어떻게 하면 될까? 정확히는 프로토타입을 상속한다는 말보다 제목처럼 프로토타입을 연결한다는 말이 더 잘 어울린다. 위에 잠간 힌트 아닌 힌트가 있었는데, constructor를 수정하듯이 prototype도 직접 변경하면 된다.

 

프로토타입 체인을 직접 만들기 전에 한 가지 예를 보고 가자.

const arr = [1,2];
console.dir(arr);

 

`arr`의 `__proto__`는 `Array.prototype`을 참조할 것이고, `Array.prototype.__proto__`는 `Object.prototype`을 참조할 것이다. 참고로 JS의 모든 객체의 `__proto__`는 기본적으로 `Object.prototype`으로 귀결된다. 이렇게 `__proto__` 속성이 연쇄적으로 이어진 것을 프로토타입 체인이라고 하며, 이 체인을 따라 검색하는 것을 프로토타입 체이닝이라고 한다. 또한 같은 이름의 메서드가 프로토타입 체인 상에 존재하면 가장 가까운 메서드를 사용하게 된다.

 

이제 프로토타입 체인을 직접 만들어보자.

function Numbers () {
  const args = Array.prototype.slice.call(arguments);
  for (let i = 0; i < args.length; i++) {
    this[i] = args[i]
  }
  this.length = args.length;
}

 

위와 같이 Numbers를 정의하면 유사 배열 객체가 된다. this 바인딩을 하지 않고 이 객체에 Array의 메서드를 적용하고 싶다면, 아래와 같이 `Numbers.prototype`을 배열로 바꿔주면 된다.

Numbers.prototype = [];

 

그러면 `pop` 처럼 다른 배열의 다른 메서드들도 사용할 수 있게 된다.

const nums = new Numbers(9,7,5,3,1);
console.log(nums); // Array { '0': 9, '1': 7, '2': 5, '3': 3, '4': 1, length: 5 }
console.log(nums.pop()) // 1
console.log(nums.slice(1,5)) // [7,5,3]

 


참고 문서

  • 이미지 출처
 

[JS] 프로토타입(prototype) 파헤치기 🕵️

자바스크립트의 prototype과 __proto__ | 프로토타입 상속 | 프로토타입 체이닝

velog.io

    • MDN
 

Object prototypes - Web 개발 학습하기 | MDN

Javascript에서는 객체를 상속하기 위하여 프로토타입이라는 방식을 사용합니다. 본 문서에서는 프로토타입 체인이 동작하는 방식을 설명하고 이미 존재하는 생성자에 메소드를 추가하기 위해 프

developer.mozilla.org

 

상속과 프로토타입 - JavaScript | MDN

JavaScript는 동적 타입이고 정적 타입이 없기 때문에, (Java 또는 C++와 같은) 클래스 기반 언어에 경험이 있는 개발자에게는 약간 혼란스럽습니다.

developer.mozilla.org

 

'Frontend > Today I Learned' 카테고리의 다른 글

[JS] 객체 지향 언어 : 프로토타입 vs. 클래스  (0) 2024.12.16
[JS] 클로저(closure)  (1) 2024.12.10
[JS] this  (0) 2024.12.02
[Next.js] Zustand 사용하기  (2) 2024.11.28
[React] TanStack Query로 custom hook 만들기  (0) 2024.10.31