8. 프로토타입과 프로토타입 상속(2)

protect-me·2021년 6월 17일
0
post-thumbnail

8.3 네이티브 프로토타입


요약

  • 모든 내장 객체는 같은 패턴을 따릅니다.
    - 메서드는 프로토타입에 저장됩니다(Array.prototype, Object.prototype, Date.prototype 등).
    - 객체 자체엔 데이터만 저장합니다(배열의 요소, 객체의 프로퍼티, 날짜 등).
  • 원시값 또한 래퍼 객체의 프로토타입에 Number.prototype, String.prototype, Boolean.prototype 같은 메서드를 저장합니다.
    undefinednull 값은 래퍼 객체를 가지지 않습니다.
  • 내장 프로토타입을 수정할 수 있습니다. 내장 프로토타입의 메서드를 빌려와 새로운 메서드를 만드는 것 역시 가능합니다. 그러나 내장 프로토타입 변경은 되도록 하지 않아야 합니다. 내장 프로토타입은 새로 명세서에 등록된 기능을 사용하고 싶은데 자바스크립트 엔진엔 이 기능이 구현되어있지 않을 때만 변경하는 게 좋습니다.(폴리필 적용 시에만)
  • obj = new Object()를 줄이면 obj = {}가 됩니다. 여기서 Object는 내장 객체 생성자 함수인데, 이 생성자 함수의 prototypetoString을 비롯한 다양한 메서드가 구현되어있는 거대한 객체를 참조합니다.

  • new Object()를 호출하거나 리터럴 문법 {...}을 사용해 객체를 만들 때, 새롭게 생성된 객체의 [[Prototype]]은 이전 챕터에서 언급한 규칙에 따라 Object.prototype을 참조합니다.

  • 따라서 obj.toString()을 호출하면 Object.prototype에서 해당 메서드를 가져오게 되죠.

let obj = {};

alert(obj.__proto__ === Object.prototype); // true

alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true
  • 그런데 이때 Object.prototype 위의 체인엔 [[Prototype]]이 없다는 점을 주의하셔야 합니다.

다른 내장 프로토타입

  • Array, Date, Function을 비롯한 내장 객체들 역시 프로토타입에 메서드를 저장해 놓습니다.
  • 배열 [1, 2, 3]을 만들면 기본 new Array() 생성자가 내부에서 사용되기 때문에 Array.prototype이 배열 [1, 2, 3]의 프로토타입이 되죠. Array.prototype은 배열 메서드도 제공합니다. 이런 내부 동작은 메모리 효율을 높여주는 장점을 가져다줍니다.
  • 명세서에선 모든 내장 프로토타입의 꼭대기엔 Object.prototype이 있어야 한다고 규정합니다. 이런 규정 때문에 몇몇 사람들은 "모든 것은 객체를 상속받는다."라는 말을 하죠.

let arr = [1, 2, 3];
// arr은 Array.prototype을 상속받았나요?
alert( arr.__proto__ === Array.prototype ); // true
// arr은 Object.prototype을 상속받았나요?
alert( arr.__proto__.__proto__ === Object.prototype ); // true
// 체인 맨 위엔 null이 있습니다.
alert( arr.__proto__.__proto__.__proto__ ); // null
  • 체인 상의 프로토타입엔 중복 메서드가 있을 수 있습니다. Array.prototype엔 요소 사이에 쉼표를 넣어 요소 전체를 합친 문자열을 반환하는 자체 메서드 toString가 있습니다.
let arr = [1, 2, 3]
alert(arr); // 1,2,3 <--Array.prototype.toString 의 결과
  • 그런데 Object.prototype에도 메서드 toString이 있습니다. 이렇게 중복 메서드가 있을 때는 체인 상에서 가까운 곳에 있는 메서드가 사용됩니다. Array.prototype이 체인 상에서 더 가깝기 때문에 Array.prototypetoString이 사용되죠.

원시값

  • 문자열과 숫자 불린값은 객체가 아닙니다. 그런데 이런 원시값들의 프로퍼티에 접근하려고 하면 내장 생성자 String, Number, Boolean을 사용하는 임시 래퍼(wrapper) 객체가 생성됩니다. 임시 래퍼 객체는 이런 메서드를 제공하고 난 후에 사라집니다.
  • 래퍼 객체는 보이지 않는 곳에서 만들어집니다. 최적화는 엔진이 담당하죠. 그런데 명세서에선 각 자료형에 해당하는 래퍼 객체의 메서드를 프로토타입 안에 구현해 놓고 String.prototype, Number.prototype, Boolean.prototype을 사용해 쓸 수 있도록 규정합니다.

    nullundefined에 대응하는 래퍼 객체는 없습니다.

네이티브 프로토타입 변경하기

  • 네이티브 프로토타입을 수정할 수 있습니다. String.prototype에 메서드를 하나 추가하면 모든 문자열에서 해당 메서드를 사용할 수 있죠.
String.prototype.show = function() {
  alert(this);
};
"BOOM!".show(); // BOOM!

🚨 프로토타입은 전역으로 영향을 미치기 때문에 프로토타입을 조작하면 충돌이 날 가능성이 높습니다. 두 라이브러리에서 동시에 String.prototype.show 메서드를 추가하면 한 라이브러리의 메서드가 다른 라이브러리의 메서드를 덮어쓰죠.
이런 이유로 네이티브 프로토타입을 수정하는 것을 추천하지 않습니다.

  • 모던 프로그래밍에서 네이티브 프로토타입 변경을 허용하는 경우는 딱 하나뿐입니다. 바로 폴리필을 만들 때입니다.
  • 폴리필은 자바스크립트 명세서에 있는 메서드와 동일한 기능을 하는 메서드 구현체를 의미합니다. 명세서에는 정의되어 있으나 특정 자바스크립트 엔진에서는 해당 기능이 구현되어있지 않을 때 폴리필을 사용합니다.
  • 폴리필을 직접 구현하고 난 후 폴리필을 내장 프로토타입에 추가할 때만 네이티브 프로토타입을 변경합시다.

프로토타입에서 빌려오기

let obj = {
  0: "Hello",
  1: "world!",
  length: 2,
};
obj.join = Array.prototype.join;
alert( obj.join(',') ); // Hello,world!
  • 예시를 실행하면 에러 없이 의도한 대로 동작합니다. 내장 메서드 join의 내부 알고리즘은 제대로 된 인덱스가 있는지와 length 프로퍼티가 있는지만 확인하기 때문입니다. 호출 대상이 진짜 배열인지는 확인하지 않죠. 다수의 내장 메서드가 이런 식으로 동작합니다.
  • 메서드 빌리기 말고도 obj.__proto__Array.prototype로 설정해 배열 메서드를 상속받는 방법이 있습니다. 이렇게 하면 obj에서 모든 Array메서드를 사용할 수 있습니다.
  • 그런데 이 방법은 obj가 다른 객체를 상속받고 있을 때는 사용할 수 없습니다. 자바스크립트는 단일 상속만을 허용한다는 점을 기억하시기 바랍니다.


8.4 프로토타입 메서드와 proto가 없는 객체


프로토타입에 직접 접근할 땐 다음과 같은 모던 메서드를 사용할 수 있습니다.

  • Object.create(proto, [descriptors])[[Prototype]]proto인 객체를 만듭니다. 참조 값은 null일 수 있고 프로퍼티 설명자를 넘기는 것도 가능합니다.
  • Object.getPrototypeOf(obj)obj[[Prototype]]을 반환합니다(__proto__ getter와 같습니다).
  • Object.setPrototypeOf(obj, proto)obj[[Prototype]]proto로 설정합니다(__proto__ setter와 같습니다).

사용자가 키를 직접 만들 수 있게 허용하면, 내장 __proto__ getter·setter는 안전하지 않습니다. 키가 "__proto__"일 때 에러가 발생할 수 있죠. 단순한 에러면 좋겠지만 보통 예측 불가능한 결과가 생깁니다.
이를 방지하려면 Object.create(null)을 사용해 __proto__가 없는 '아주 단순한 객체’를 만들거나, 맵을 일관되게 사용하는 것이 좋습니다.
한편, Object.create를 사용하면 객체의 얕은 복사본(shallow-copy)을 만들 수 있습니다.

let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

__proto__[[Prototype]]getter·setter라는 점과 다른 메서드처럼 Object.prototype에 정의되어 있다는 것도 확인해 보았습니다.

Object.create(null)을 사용하면 프로토타입이 없는 객체를 만들 수 있습니다. 이런 객체는 '순수 사전’처럼 사용됩니다. "__proto__"를 키로 사용해도 문제를 일으키지 않죠.

이런 내용과 더불어 아래 메서드들을 같이 살펴보면 좋습니다.

  • Object.keys(obj) / Object.values(obj) / Object.entries(obj)obj 내 열거 가능한 프로퍼티 키, 값, 키-값 쌍을 담은 배열을 반환합니다.
  • Object.getOwnPropertySymbols(obj)obj 내 심볼형 키를 담은 배열을 반환합니다.
  • Object.getOwnPropertyNames(obj)obj 내 문자형 키를 담은 배열을 반환합니다.
  • Reflect.ownKeys(obj)obj내 키 전체를 담은 배열을 반환합니다.
  • obj.hasOwnProperty(key) – 상속받지 않고 obj 자체에 구현된 키 중 이름이 key인 것이 있으면 true를 반환합니다.

Object.keys를 비롯하여 객체의 프로퍼티를 반환하는 메서드들은 객체가 ‘직접 소유한’ 프로퍼티만 반환합니다. 상속 프로퍼티는 for..in을 사용해 얻을 수 있습니다.

  • __proto__는 브라우저를 대상으로 개발하고 있다면 다소 구식이기 때문에 더는 사용하지 않는 것이 좋습니다. 표준에도 관련 내용이 명시되어있습니다.
  • Object.create(proto, [descriptors])
    [[Prototype]]proto를 참조하는 빈 객체를 만듭니다. 이때 프로퍼티 설명자를 추가로 넘길 수 있습니다.
  • Object.getPrototypeOf(obj)
    obj[[Prototype]]을 반환합니다.
  • Object.setPrototypeOf(obj, proto)
    obj[[Prototype]]proto가 되도록 설정합니다.
let animal = {
  eats: true
};

// 프로토타입이 animal인 새로운 객체를 생성합니다.
// Rabbit.prototype = animal;
// let rabbit = new Rabbit("White Rabbit");
let rabbit = Object.create(animal);

alert(rabbit.eats); // true

alert(Object.getPrototypeOf(rabbit) === animal); // true

Object.setPrototypeOf(rabbit, {}); // rabbit의 프로토타입을 {}으로 바꿉니다.
  • Object.create에는 프로퍼티 설명자를 선택적으로 전달할 수 있습니다.
let animal = {
  eats: true
};

let rabbit = Object.create(animal, {
  jumps: {
    value: true
  }
});

alert(rabbit.jumps); // true
  • Object.create를 사용하면 for..in을 사용해 프로퍼티를 복사하는 것보다 더 효과적으로 객체를 복제할 수 있습니다.
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

Object.create를 호출하면 obj의 모든 프로퍼티를 포함한 완벽한 사본이 만들어집니다, 사본엔 열거 가능한 프로퍼티와 불가능한 프로퍼티, 데이터 프로퍼티, getter, setter 등 모든 프로퍼티가 복제됩니다. [[Prototype]]도 복제되죠.

비하인드 스토리

[[Prototype]]을 다룰 수 있는 방법은 다양한 이유는 역사적으로 __proto__Legacy가 된 케이스이기 때문.

속도가 중요하다면 기존 객체의 [[Prototype]]을 변경하지 마세요. 객체 프로퍼티 접근 관련 최적화를 망칠 수 있기 때문

'아주 단순한' 객체

  • 그런데 커스텀 사전을 만드는 것과 같이 사용자가 직접 입력한 키를 가지고 객체를 만들다 보면 사소한 결함이 발견됩니다. 다른 문자열은 괜찮지만 __proto__는 키로 사용할 수 없다는 결함이죠.
let obj = Object.create(null);

let key = prompt("입력하고자 하는 key는 무엇인가요?", "__proto__");
obj[key] = "...값...";

alert(obj[key]); // "...값..."이 제대로 출력됩니다.

  • Object.create(null)로 객체를 만들면 __proto__ gettersetter를 상속받지 않습니다. 이제 __proto__는 평범한 데이터 프로퍼티처럼 처리되므로 버그 없이 예시가 잘 동작하게 됩니다.
  • 이런 객체는 ‘아주 단순한(very plain)’ 혹은 ‘순수 사전식(pure dictionary)’ 객체라고 부릅니다. 일반 객체 {...} 보다 훨씬 단순하기 때문이죠.



📚 참고 : javascript.info

profile
protect me from what i want

0개의 댓글