티스토리 뷰

8.1 Prototypal inheritance

javascript에는 프로토타입 상속이라는 기능이 있다. Java에서의 상속과 비슷해 보이는 개념인데, 조금 다른 듯하다.

[[Prototype]]

사실 javascript에는 [[Prototype]]이라는 속성이 숨겨져 있다. 해당 속성은 null이거나 다른 객체를 참조하는데, 다른 객체를 참조할 경우 해당 객체를 프로토타입이라 부른다. 

 

object라는 객체가 있다고 가정했을 때, 해당 객체의 어느 속성을 읽으려고 하는데 해당 속성이 없다면 자동으로 프로토타입해서 해당 속성을 찾는다. 이러한 동작 방식을 '프로토타입 상속'이라 한다. 

 

기본적으로 숨겨져있는 속성이지만, 개발자가 설정할 수 있는 방법들이 있다. '__proto__'를 통해 접근한다.

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal;

'__proto__'는 [[Prototype]]그 자체가 아니라 해당 속성의 getter,setter 역할을 한다. 사실 요즘에는 '__proto__'는 잘 쓰이지 않는다고 한다. 

let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal; // (*)

// 프로퍼티 eats과 jumps를 rabbit에서도 사용할 수 있게 되었습니다.
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true

위 예시처럼 상속받은 속성을 상속 프로퍼티라고 한다. 

let animal = {
  eats: true,
  walk() {
    alert("동물이 걷습니다.");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// 메서드 walk는 rabbit의 프로토타입인 animal에서 상속받았습니다.
rabbit.walk(); // 동물이 걷습니다.

당연히 체인처럼 연속해서 프로토타입을 상속 받을 수도 있다. 

let animal = {
  eats: true,
  walk() {
    alert("동물이 걷습니다.");
  }
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

let longEar = {
  earLength: 10,
  __proto__: rabbit
};

// 메서드 walk는 프로토타입 체인을 통해 상속받았습니다.
longEar.walk(); // 동물이 걷습니다.
alert(longEar.jumps); // true (rabbit에서 상속받음)

다만, 아래 제약사항을 지켜야 한다.

  • 순환 참조 불가, 에러가 발생
  • 값으로 null이나 객체만 가능

Writing doesn't use prototype

프로토타입은 프로퍼티를 읽어낼 때 사용되고, 새로운 프로퍼티를 생성하거나 제거하기 위해서는 해당 객체에 직접 접근해야 한다.

let animal = {
  eats: true,
  walk() {
    /* rabbit은 이제 이 메서드를 사용하지 않습니다. */
  }
};

let rabbit = {
  __proto__: animal
};

rabbit.walk = function() {
  alert("토끼가 깡충깡충 뜁니다.");
};

rabbit.walk(); // 토끼가 깡충깡충 뜁니다.

접근자 프로퍼티가 존재할 경우 동작이 조금 다르다.

let user = {
  name: "John",
  surname: "Smith",

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  },

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

let admin = {
  __proto__: user,
  isAdmin: true
};

alert(admin.fullName); // John Smith (*)

// setter 함수가 실행됩니다!
admin.fullName = "Alice Cooper"; // (**)

alert(admin.fullName); // Alice Cooper, setter에 의해 추가된 admin의 프로퍼티(name, surname)에서 값을 가져옴
alert(user.fullName); // John Smith, 본래 user에 있었던 프로퍼티 값

(**)줄의 코드는 fullName이라는 속성을 생성하면서 새롭게 값을 할당하는 것처럼 보이지만, 실은 admin객체의 fullName의 세터함수를 호출한다.

the value of "this"

앞전의 예시를 보았을 때, this는 어느 객체를 가리킬까? 결론은, '.'앞에 있는 객체를 가리킨다.

// animal엔 다양한 메서드가 있습니다.
let animal = {
  walk() {
    if (!this.isSleeping) {
      alert(`동물이 걸어갑니다.`);
    }
  },
  sleep() {
    this.isSleeping = true;
  }
};

let rabbit = {
  name: "하얀 토끼",
  __proto__: animal
};

// rabbit에 새로운 프로퍼티 isSleeping을 추가하고 그 값을 true로 변경합니다.
rabbit.sleep();

alert(rabbit.isSleeping); // true
alert(animal.isSleeping); // undefined (프로토타입에는 isSleeping이라는 프로퍼티가 없습니다.)

for...in loop

for...in 은 프로토타입에서 상속받은 함수도 모두 순회한다. 

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

// Object.keys는 객체 자신의 키만 반환합니다.
alert(Object.keys(rabbit)); // jumps

// for..in은 객체 자신의 키와 상속 프로퍼티의 키 모두를 순회합니다.
for(let prop in rabbit) alert(prop); // jumps, eats

아래 예시처럼 해당 객체에서 직접 구현되어 있는 프로퍼티만 걸러낼 수도 있다.

let animal = {
  eats: true
};

let rabbit = {
  jumps: true,
  __proto__: animal
};

for(let prop in rabbit) {
  let isOwn = rabbit.hasOwnProperty(prop);

  if (isOwn) {
    alert(`객체 자신의 프로퍼티: ${prop}`); // 객체 자신의 프로퍼티: jumps
  } else {
    alert(`상속 프로퍼티: ${prop}`); // 상속 프로퍼티: eats
  }
}

참고로, Object.prototype에 있는 메서드는 열거되지 않는다.(enumerable 플래그가 false이기 때문).

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday