\(@^0^@)/

[BOOK] 코어 자바스크립트, 프로토타입 2 본문

BOOKS/코어 자바스크립트

[BOOK] 코어 자바스크립트, 프로토타입 2

minjuuu 2022. 4. 14. 12:39
728x90

오늘 읽은 범위 : (프로토타입) p.160 ~ p.174


< 책에서 기억하고 싶은 내용 >

프로토타입 체인

메서드 오버라이드

인스턴스가 동일한 이름의 프로퍼티 또는 메서드를 가지고 있는 상황이라면?

var Person = function (name) {
    this.name = name;
};
Person.prototype.getName = function () {
    return this.name;
};

var iu = new Person('지금');
iu.getName = function () {
    return '바로 ' + this.name;
};
console.log(iu.getName());    // 바로 지금

당연히 iu.__proto__.getName이 아닌 iu 객체에 있는 getName 메서드를 호출한다.
여기서 일어난 현상을 오버라이드라고 한다. 메서드 위에 메서드를 덮어 씌웠다는 표현.
원본을 제거하고 다른 대상으로 교체하는 것이 아니라 원본이 그대로 있는 상태에서 다른 대상을 그 위에 얹는다.

JS 엔진이 getName이라는 메서드를 찾는 방식은 가장 가까운 대상인 자신의 프로퍼티를 검색하고, 없으면 그다음으로 가까운 대상인 __proto__를 검색하는 순서로 진행된다.


그렇다면 메서드 오버 라이딩이 이루어져 있는 상황에서 prototype에 있는 메서드에 접근하려면 어떻게 할까?

console.log(iu.__proto__.getName());    // undefined

this가 prototype 객체 (iu.__proto__)를 가리키는데 prototype 상에는 name 프로퍼티가 없기에 undefined가 출력됨.

만약 prototype에 name 프로퍼티가 있다면 그 값을 출력한다.

Person.prototype.name = '이지금';
console.log(iu.__proto__.getName());    // 이지금

이렇게 하면 원하는 메서드 (prototype에 있는 getName)가 호출되는 것을 볼 수 있다.
다만 this가 prototype을 바라보고 있는데, 이 것을 인스턴스를 바라보도록 call이나 apply로 바꿔주면 해결된다.

console.log(iu.__proto__.getName.call(iu));    // 지금

즉, 일반적으로 메서드가 오버라이드 된 경우에는 자신으로부터 가장 가까운 메서드에만 접근할 수 있지만,
그다음으로 가까운 __proto__의 메서드도 우회적인 방법을 통해서 접근이 가능하다.


 

객체와 배열의 내부구조

  • 객체
    • 첫 줄을 통해 Object의 인스턴스임을 알 수 있다.
    • 프로퍼티 a의 값은 1
    • prototype 내부에는 hasOwnproperty, isPrototypeOf, toLocaleString, toString, valueOf 등의 메서드가 있다.
    • constructor는 생성자 함수인 Object를 가리키고 있다.
  • 배열
    • 배열 리터럴의 prototype에는 pop, push 등의 배열 메서드 및 constructor가 있다.
    • prototype안에는 또다시 prototype가 등장한다.
    • 열어보니 객체의 prototype과 동일한 내용으로 이루어져 있음!
      그 이유는 바로 prototype 객체가 '객체' 이기 때문이다.
    • 기본적으로 모든 객체의 __proto__에는 Object.prototype이 연결된다.

배열의 내부 도식

__proto__는 생략이 가능하기 때문에 배열이 Array.prototype 내부의 메서드를 마치 자신의 것처럼 실행할 수 있음.
Object.prototype도 마찬가지. 생략 가능한 __proto__를 한 번 더 따라가면 Object.prototype을 참조할 수 있기 때문이다.

var arr = [1, 2];
arr(.__proto__).push(3);
arr(.__proto__)(.__proto__).hasOwnProperty(2);    // true

 

  • 프로토타입 체인 (prototype chain) :
    어떤 데이터의 __proto__프로퍼티 내부에 다시 __proto__ 프로퍼티가 연쇄적으로 이어진 것

  • 프로토타입 체이닝(prototype chaining) : 이 체인을 따라가며 검색하는 것
    8
  • 프로토타입 체이닝 진행 흐름
    어떤 메서드를 호출 -> JS 엔진은 데이터 자신의 프로퍼티들을 검색해서 원하는 메서드가 있으면 그 메서드를 실행
    -> 없으면 __proto__를 검색해서 있으면 그 메서드를 실행 -> 없으면 다시 __proto__를 검색해서 실행

var arr = [1, 2];
Array.prototype.toString.call(arr);     // 1,2
Object.prototype.toString.call(arr);    // [object Array]
arr.toString();                         // 1,2

arr.toString = function () {
    return this.join('_');
};
arr.toString();                         // 1_2

arr 변수는 배열이므로 arr.__proto__는 Array.prototype을 참조하고
Array.prototype은 객체이므로 Array.prototype.__proto__는 Object.prototype을 참조할 것이다.

(toString 메서드는 Array.prototype 뿐만 아니라, Object.prototype에도 있는 메서드)
Array, Object의 각 프로토타입에 있는 toString메서드를 arr에 적용했을 때 출력 값은 차이가 있으며
arr.toString을 실행했을 때의 결과는 Array.prototype.toString을 적용한 것과 동일하다.


객체 전용 메서드의 예외사항

어떤 생성자 함수든 prototype은 반드시 객체이기 때문에 Object.prototype이 언제나 프로토타입 체인의 최상단에 존재.
따라서 객체에서만 사용할 메서드는 다른 여느 데이터 타입처럼 프로토타입 객체 안에 정의할 수 없다.
객체에서만 사용할 메서드를 Object.prototype 내부에 정의한다면 다른 데이터 타입도 해당 메서드를 사용할 수 있게 되기 때문. (어느 데이터 타입이건 거의 무조건 프로토타입 체이닝을 통해 최상위 객체에 존재할 수 있다)

이 같은 이유로 객체만을 대상으로 동작하는 객체 전용 메서드들은 부득이 Object.prototype이 아닌 Object에 스태틱 메서드 (static method)로 부여할 수밖에 없었다.
ex) Object.getPrototypeOf(instance) -> instance.getPrototype()
Object.freeze(instance) -> instance.freeze()

또한, 생성자 함수인 Object와 인스턴스인 객체 리터럴 사이에는 this를 통한 연결이 불가능하기 때문에 여느 전용 메서드처럼 '메서드명 앞의 대상이 곧 this'가 되는 방식 대신 this의 사용을 포기하고 대상 인스턴스를 인자로 직접 주입해야 하는 방식으로 구현되어 있다.

반대로 같은 이유에서 Object.prototype에는 어떤 데이터에서도 활용할 수 있는 범용적인 메서드들이 있다.
toString, hasOwnProperty, valueOf, isPrototypeOf 등은 모두 변수가 마치 자신의 메서드인 것처럼 호출할 수 있다.

예외적으로 Object.create를 이용하면 Object.prototype의 메서드에 접근할 수 없는 경우가 있다.
Object.create(null)은 __proto__가 없는 객체를 생성한다.


다중 프로토타입 체인

JS의 기본 내장 데이터 타입들은 모두 프로토타입 체인이 1단계(객체)이거나 2단계(나머지)로 끝나는 경우만 있었지만
사용자가 새롭게 만드는 경우에는 그 이상도 얼마든지 가능하다.
위에서 도식화했던 이미지같이 대각선의 __proto__를 연결 해나 가기만 하면 무한대로 체인 관계를 이어나갈 수 있다.

대각선의 __proto__를 연결하는 방법은 __proto__가 가리키는 대상, 즉 생성자 함수의 prototype이 연결하고자 하는 상위 생성자 함수의 인스턴스를 바라보게끔 해주면 된다.

var Grade = function () {
    var args = Array.prototype.slice.call(arguments);
    for (var i = 0; i < args.length; i++) {
        this[i] = args[i];
    }
    this.length = args.length;
};
var g = new Grade(100, 80);
  • 변수 g는 Grade의 인스턴스를 바라본다.
  • Grade의 인스턴스는 여러 개의 인자를 받아 각각 순서대로 인덱싱 해서 저장하고
    length 프로퍼티가 존재하는 등으로 배열의 형태를 지니지만, 배열의 메서드를 사용할 수는 없는 유사 배열 객체.
  • 인스턴스에서 배열 메서드를 직접 사용하기 위해서는 g.__proto__, 즉 Grade.prototype이 배열의 인스턴스를 바라보게 하면 된다.
Grade.prototype = [];
console.log(g);    // Grade(2) [100, 80]
g.pop();
console.log(g);    // Grade(1) [100]
g.push(90);
console.log(g);    // Grade(2) [100, 90]


< 프로토타입 요약 >

  • 직각삼각형의 대각선 방향, 즉 __proto__ 방향을 계속 찾아가면 최종적으로는 Object.prototype에 당도하게 된다.
  • 이런 식으로 __protop__안에 다시 __proto__를 찾아가는 과정 프로토타입 체이닝이라 한다.
  •  프로토타입 체이닝을 통해 각 프로토타입 메서드를 자신의 것처럼 호출할 수 있다.
  • 이때 접근 방식은 자신으로부터 가장 가까운 대상을 중점으로 점차 먼 대상으로 나아가며, 원하는 값을 찾으면 검색을 중단한다.
  • Object.prototype에는 모든 데이터 타입에서 사용할 수 있는 범용적인 메서드만이 존재하며,
    객체 전용 메서드는 여느 데이터 타입과 달리 Object 생성자 하수에 스태틱 하게 담겨 있다.

[ 출처 : 코어 자바스크립트 ]

728x90