Study JavaScript 0629 - 내장 객체의 프로토타입

변승훈·2022년 6월 29일
0

Study JavaScript

목록 보기
37/43

내장 객체의 프로토타입

prototype 프로퍼티는 javascript 내부에서도 광범위하게 사용된다. 모든 내장 생성자 함수에서 prototype 프로퍼티를 사용한다.

1. Object.prototype

빈 객체가 있다고 가정해보자

let obj = {};
console.log( obj ); // "[object Object]" ?

"[object Object]" 문자열을 생성하는 코드는 어디에 있을까? toString 메소드에서 이 문자열을 생성한다는 것을 앞서 배워서 알고 있지만 도대체 코드는 어디에 있는 걸까?

obj = new Object()를 줄이면 obj = {}가 된다. 여기서 Object는 내장 객체 생성자 함수인데, 이 생성자 함수의 prototypetoString을 비롯한 다양한 메소드가 구현되어있는 거대한 객체를 참조한다.

그림을 이용해 살펴보자.

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

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

예시를 통해 확인해보자!

let obj = {};

console.log(obj.__proto__ === Object.prototype); // true

console.log(obj.toString === obj.__proto__.toString); //true
console.log(obj.toString === Object.prototype.toString); //true

그런데 이때 Object.prototype 위쪽엔 [[Prototype]] 체인이 없다는 점을 주의해야 한다!

console.log(Object.prototype.__proto__); // null

2. 다양한 내장 객체의 프로토타입

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을 상속받았을까?
console.log( arr.__proto__ === Array.prototype ); // true

// arr은 Object.prototype을 상속받았을까?
console.log( arr.__proto__.__proto__ === Object.prototype ); // true

// 체인 맨 위엔 null이 있다.
console.log( arr.__proto__.__proto__.__proto__ ); // null

체인 상의 프로토타입엔 중복 메소드가 있을 수 있다. Array.prototype엔 요소 사이에 쉼표를 넣어 요소 전체를 합친 문자열을 반환하는 자체 메소드 toString이 있다.

let arr = [1, 2, 3]
console.log(arr); // 1,2,3 <-- Array.prototype.toString의 결과

그런데 Object.prototype에도 메소드 toString이 있다. 이렇게 중복 메소드가 있을 때는 체인 상에서 가까운 곳에 있는 메소드가 사용된다.
Array.prototype이 체인 상에서 더 가깝기 때문에 예시에선 Array.prototypetoString이 사용되었다.

Chrome 개발자 console과 같은 도구를 사용하면 상속 관계를 확인할 수 있다. console.dir를 사용하면 내장 객체의 상속 관계를 확인하는 데 도움이 되니 참고하자!

배열이 아닌 다른 내장 객체들 또한 같은 방법으로 동작한다. 함수 또한 마찬가지다. 함수는 내장 객체 Function의 생성자를 사용해 만들어지는데 call, apply를 비롯한 함수에서 사용할 수 있는 메소드는 Fuction.prototype에서 받아온다. 참고로 함수에도 toString이 구현되어있다.

function f() {}

console.log(f.__proto__ == Function.prototype); // true
console.log(f.__proto__.__proto__ == Object.prototype); // true, 객체에서 상속받음

3. 원시값

문자열과 숫자, 불린값을 다루는 것은 까다롭다.

문자열과 숫자, 불린값은 객체가 아니다! 그런데 이런 원시 타입 값의 프로퍼티에 접근하려고 하면 내장 생성자 String, Number, Boolean을 사용하는 임시 래퍼(wrapper) 객체가 생성된다. 임시 래퍼 객체는 이런 메소드를 제공하고 난 후에 사라진다.

래퍼 객체는 보이지 않는 곳에서 만들어지는데 엔진에 의해 최적화가 이뤄진다. 그런데 명세서엔 각 자료형에 해당하는 래퍼 객체의 메소드를 프로토타입 안에 구현해 놓고 String.prototype, Number.prototype, Boolean.prototype을 사용해 쓰도록 규정한다.

nullundefined에 대응하는 래퍼 객체는 없다.
특수 값인 nullundefined는 문자열과 숫자, 불린값과는 거리가 있다. nullundefined에선 메소드와 프로퍼티 그리고 프로토타입도 이용할 수 없다!

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

네이티브 프로토타입은 수정할 수 있다. String.prototype에 메소드를 하나 추가하면 모든 문자열에서 해당 메소드를 사용할 수 있다.

String.prototype.show = function() {
  console.log(this);
};

"BOOM!".show(); // BOOM!

개발을 하다 보면 새로운 내장 메소드를 만들면 좋지 않을까 하는 생각이 들 때가 있다고 한다. 네이티브 프로토타입에 새 내장 메소드를 추가하고 싶은 유혹이 자꾸 생긴다는데, 이는 좋지 않은 방법이라고 한다.

★ 중요:
프로토타입은 전역으로 영향을 미치기 때문에 프로토타입을 조작하면 기존 코드와 충돌이 날 가능성이 크다! 두 라이브러리에서 동시에 String.prototype.show 메소드를 추가하면 한 라이브러리의 메소드가 다른 라이브러리의 메소드를 덮어쓰게 된다.
이런 이유로 네이티브 프로토타입을 수정하는 것은 추천하지 않는다!

모던 프로그래밍에서 네이티브 프로토타입 변경을 허용하는 경우는 폴리필을 만들 때"만" 허용한다!

폴리필은 javascript 명세서에 있는 메소드와 동일한 기능을 하는 메소드 구현체를 의미하며, 명세서에는 정의되어 있으나 특정 javscript 엔진에서는 해당 기능이 구현되어있지 않을 때 폴리필을 사용한다.

폴리필을 직접 구현하고 난 후, 폴리필을 내장 프로토타입에 추가할 때만 네이티브 프로토타입을 변경하자!

if (!String.prototype.repeat) { // repeat이라는 메소드가 없다고 가정합시다
  // 프로토타입에 repeat를 추가

  String.prototype.repeat = function(n) {
    // string을 n회 반복(repeat)합니다.

    // 실제 이 메소드를 구현하려면 더 복잡한 코드가 필요합니다.
    // 전체 알고리즘은 명세서에서 확인할 수 있는데,
    // 명세서를 완벽히 구현하지 않은 폴리필이라도 충분히 쓸만하니 예시는 이 정도로만 작성해보겠습니다.
    return new Array(n + 1).join(this);
  };
}

console.log( "라".repeat(3) ); // 라라라

5. 프로토타입에서 메소드 빌려오기

call/apply와 데코레이터, 포워딩에서 메소드 빌리기에 대한 내용이 있다. 이 부분은 7.1 금요일에 작성 할 예정이다(본인이 6.30일에 예비군이 있기 때문에...).

한 객체의 메소드를 다른 객체로 복사할 때 이 기법이 사용된다고 한다.

개발을 하다 보면 네이티브 프로토타입에 구현된 메소드를 빌려야 하는 경우가 종종 생긴다고 한다.

유사 배열 객체를 만들고 여기에 Array 메소드를 복사해보자!

let obj = {
  0: "Hello",
  1: "world!",
  length: 2,
};

obj.join = Array.prototype.join;

console.log( obj.join(',') ); // Hello,world!

내장 메소드 join의 내부 알고리즘은 제대로 된 인덱스가 있는지와 length 프로퍼티가 있는지만 확인하기 때문에 예시는 에러 없이 의도한 대로 동작한다. 호출 대상이 진짜 배열인지는 상관없으며, 다수의 내장 메소드가 이런 식으로 동작한다.

메소드 빌리기 말고도 obj.__proto__Array.prototype으로 설정해 배열 메소드를 상속받는 방법이 있다. 이렇게 하면 obj에서 모든 Array 메소드를 사용할 수 있습니다.

그런데 javascript는 단일 상속만 허용하기 때문에 이 방법은 obj가 다른 객체를 상속받고 있을 때는 사용할 수 없다.

메소드 빌리기는 여러 객체에서 필요한 기능을 가져와 섞는 것을 가능하게 해주기 때문에 유연한 개발을 가능하게 해준다!

profile
잘 할 수 있는 개발자가 되기 위하여

0개의 댓글