자바스크립트 프로토타입

[[Prototype]]

자바스크립트의 모든 객체는 [[Prototype]] 내부 슬롯을 가집니다. 이 내부 슬롯이 가리키는 값은 null이나 다른 객체를 참조하게 되는데 참조되는 객체를 프로토타입이라 부릅니다. 일반 객체를 만들었을 때 객체 메서드를 쓸 수 있는 이유가 바로 이 프로토타입을 통해 상속 받기 때문입니다. 내부 슬롯 [[Prototype]]에 직접 접근할 수는 없지만(객체 간에 상속 관계를 무너뜨리지 않기 위해 직접 접근을 허용 X) 접근자 프로퍼티 __proto__를 통해 접근이 가능합니다.

내부 슬롯이란?
이중 대괄호([[...]])로 감싸져 있으며 주로 객체의 내부 상태나 동작을 관리하기 위해 사용됩니다. 실제로 접근할 수 없고 일부에 한해서 접근자 프로퍼티(__proto__)를 제공합니다.

const iphone = {
  siri: true,
};
 
const galaxy = {
  samsungPay: true,
};
 
galaxy.__proto__ = iphone;
console.log(galaxy.siri); // true

갤럭시는 아이폰의 시리를 사용할 수 없습니다. 하지만 접근자 프로퍼티 __proto__를 통해 아이폰은 갤럭시의 프로토타입이 되었고 갤럭시는 아이폰으로부터 상속받게 됩니다. 이제 갤럭시는 아이폰의 시리 기능을 사용할 수 있습니다.

프로토타입 체인

const apple = {
  airDrop: true,
};
 
const iphone = {
  siri: true,
  __proto__: apple,
};
 
const galaxy = {
  samsungPay: true,
};
 
galaxy.__proto__ = iphone;
console.log(galaxy.airDrop); //true

아이폰의 프로토타입은 애플인데요. 아이폰은 애플에게 에어드랍을 상속받아 사용할 수 있습니다. 갤럭시 또한 아이폰이 프로토타입이므로 에어드랍을 사용할 수 있습니다. 이렇게 프로토타입은 서로 연결되어 있으며 부모의 부모 프로퍼티까지 참조할 수 있습니다. 이것이 프로토타입 체이닝입니다. 프로토타입은 오직 객체나 null만 가능한데요. 오직 하나의 객체만 존재할 수 있으며 단방향 링크드 리스트로 구현되어야 합니다.

for..in

for..in 문은 상속 프로퍼티도 순회 대상에 포함됩니다.

let animal = {
  eats: true,
};
 
let rabbit = {
  jumps: true,
  __proto__: animal,
};
 
for (const prop in rabbit) {
  console.log(prop); // jumps eats
}
 
console.log(Object.keys(rabbit)); // ['jumps']

Object.keys는 자신의 키만 반환합니다. obj.hasOwnProperty(key) 메서드를 이용하면 상속 프로퍼티를 순회 대상에서 제외할 수 있습니다.

let animal = {
  eats: true,
};
 
let rabbit = {
  jumps: true,
  __proto__: animal,
};
 
for (const prop in rabbit) {
  const isOwn = rabbit.hasOwnProperty(prop);
 
  if (isOwn) {
    console.log(prop); // jumps
  }
}
 
console.log(Object.keys(rabbit)); // ['jumps']

아래 그림과 같이 프로토타입이 형성되고 있습니다. rabbit의 프로토타입은 animal이고 animal의 프로토타입은 Object.prototype입니다. Object.prototype의 프로토타입은 null이 됩니다. Object.prototype의 프로퍼티는 분명 hasOwnProperty를 소유하는데 for..in 문에서 순회되지 않은 이유는 hasOwnProperty 프로퍼티는 내부 슬롯 enumerable값이 false이기 때문입니다.

https://ko.javascript.info/prototype-inheritance
https://ko.javascript.info/prototype-inheritance

함수의 prototype 프로퍼티

인스턴스를 생성할 수 있는 함수는 prototype 프로퍼티를 가집니다. 이 prototype 프로퍼티는 내부 슬롯 [[Prototype]]과 다릅니다.

let animal = {
  eats: true,
};
 
function Rabbit(name) {
  this.name = name;
}
 
Rabbit.prototype = animal;
 
let rabbit = new Rabbit('흰 토끼'); //  rabbit.__proto__ === animal
console.log(rabbit.eats);

위 코드에서 Rabbit 생성자 함수를 통해 rabbit 인스턴스를 만들었고 rabbit의 프로토타입을 animal로 지정했습니다. 함수의 프로퍼티 prototype은 위 코드와 같이 인스턴스의 프로토타입을 설정할 수 있습니다.

https://ko.javascript.info/function-prototype
https://ko.javascript.info/function-prototype

즉 생성자 함수의 prototype 프로퍼티는 인스턴스의 프로토타입을 변경하기 위해 존재하며 __proto__는 객체 자신의 프로토타입에 접근, 변경하기 위해 존재합니다.

function Person(name) {
  this.name = name;
}
 
Person.prototype.describe = function () {
  console.log(this.name);
};
 
const jane = new Person('jane');
jane.describe();
https://exploringjs.com/es5/ch17.html
https://exploringjs.com/es5/ch17.html

생성자 함수 Person은 jane 객체를 생성했습니다. jane의 프로토타입은 Person.prototype이 되며 프로토타입 상속을 통해 describe 함수를 실행할 수 있게 됩니다. 모든 프로토타입은 constructor 프로퍼티를 가지고 있습니다. 이때 Person.prototype의 constructor 참조는 생성자 함수 Person이 된다. 또한 Person의 prototype 프로퍼티를 통해 프로토타입에 접근할 수 있습니다.

모든 함수가 prototype 프로퍼티를 소유하는 것은 아닌데요. 생성자 함수로 호출할 수 없는 함수는 prototype 프로퍼티를 소유하지 않으며 프로토타입도 생성하지 않습니다. 이러한 함수를 non-constructor라고 하며 화살표 함수와 es6의 메서드가 이에 해당됩니다.

내장 객체의 프로토타입

const arr = [1, 2, 3];
console.log(arr.filter((n) => !(n % 2))); // 2

arr가 filter 메서드를 사용할 수 있는 이유는 Array.prototype의 메서드를 상속받기 때문이다. 위 코드는 리터럴을 사용해 생성했지만 사실 아래 코드와 같습니다.

const arr = new Array([1, 2, 3]);
const string = 'string';
console.log(string.__proto__ === String.prototype); // true
const arr = [1, 2, 3, 4];
console.log(arr.__proto__ === Array.prototype); // true
const fn = () => {};
console.log(fn.__proto__ === Function.prototype); // true
const number = 1;
console.log(number.__proto__ === Number.prototype); // true
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true

모든 내장 프로토타입의 꼭대기엔 Object.prototype이 존재하고 Object.prototype의 프로토타입은 null입니다.

https://ko.javascript.info/prototype-methods
https://ko.javascript.info/prototype-methods

아래 코드와 같이 내장 객체 prototype 프로퍼티를 이용하여 메서드를 추가할 수 있습니다. 하지만 이 방법은 좋지 않은데요. 프로토타입은 전역으로 영향을 미치기 때문에 기존 코드와 충돌 날 가능성이 크기 때문에 사용해서는 안 됩니다.

// ❌
String.prototype.hi = function () {
  console.log('hi');
};
 
const name = 'string';
a.hi(); // hi

참조