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

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

8.1 프로토타입 상속


요약

  • 자바스크립트의 모든 객체엔 숨김 프로퍼티 [[Prototype]]이 있는데, 이 프로퍼티는 객체나 null을 가리킵니다.
  • obj.__proto__를 사용하면 프로토타입에 접근할 수 있습니다. __proto__[[Prototype]]getter·setter로 쓰이는데, 요즘엔 잘 쓰지 않습니다. 자세한 사항은 뒤쪽 챕터에서 다룰 예정입니다.
  • [[Prototype]]이 참조하는 객체를 '프로토타입’이라고 합니다.
  • obj에서 프로퍼티를 읽거나 메서드를 호출하려는데 해당하는 프로퍼티나 메서드가 없으면 자바스크립트는 프로토타입에서 프로퍼티나 메서드를 찾습니다.
  • 접근자 프로퍼티가 아닌 데이터 프로퍼티를 다루고 있다면, 쓰기나 지우기와 관련 연산은 프로토타입을 통하지 않고 객체에 직접 적용됩니다.
  • 프로토타입에서 상속받은 method라도 obj.method()를 호출하면 method 안의 this는 호출 대상 객체인 obj를 가리킵니다.
  • for..in 반복문은 객체 자체에서 정의한 프로퍼티뿐만 아니라 상속 프로퍼티도 순회 대상에 포함합니다. 반면, 키-값과 관련된 내장 메서드 대부분은 상속 프로퍼티는 제외하고 객체 자체 프로퍼티만을 대상으로 동작합니다.

[[Prototype]]

  • 프로토타입의 동작 방식은 '신비스러운’면이 있습니다. object에서 프로퍼티를 읽으려고 하는데 해당 프로퍼티가 없으면 자바스크립트는 자동으로 프로토타입에서 프로퍼티를 찾기 때문이죠. 프로그래밍에선 이런 동작 방식을 '프로토타입 상속’이라 부릅니다. 언어 차원에서 지원하는 편리한 기능이나 개발 테크닉 중 프로토타입 상속에 기반해 만들어진 것들이 많습니다.

  • 자바스크립트의 객체는 명세서에서 명명한 [[Prototype]]이라는 숨김 프로퍼티를 갖습니다. 이 숨김 프로퍼티 값은 null이거나 다른 객체에 대한 참조가되는데, 다른 객체를 참조하는 경우 참조 대상을 '프로토타입(prototype)'이라 부릅니다.

  • 프로토타입의 동작 방식은 '신비스러운’면이 있습니다. object에서 프로퍼티를 읽으려고 하는데 해당 프로퍼티가 없으면 자바스크립트는 자동으로 프로토타입에서 프로퍼티를 찾기 때문이죠. 프로그래밍에선 이런 동작 방식을 '프로토타입 상속’이라 부릅니다. 언어 차원에서 지원하는 편리한 기능이나 개발 테크닉 중 프로토타입 상속에 기반해 만들어진 것들이 많습니다.
  • [[Prototype]] 프로퍼티는 내부 프로퍼티이면서 숨김 프로퍼티이지만 다양한 방법을 사용해 개발자가 값을 설정할 수 있습니다.
let animal = {
  eats: true
};
let rabbit = {
  jumps: true
};

rabbit.__proto__ = animal; // (*)

// 프로퍼티 eats과 jumps를 rabbit에서도 사용할 수 있게 되었습니다.
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
  • (**)로 표시한 줄에서 alert 함수가 rabbit.eats 프로퍼티를 읽으려 했는데, rabbiteats라는 프로퍼티가 없습니다. 이때 자바스크립트는 [[Prototype]]이 참조하고 있는 객체인 animal에서 eats를 얻어냅니다. 아래 그림을 밑에서부터 위로 살펴보세요.
  • 이제 “rabbit의 프로토타입은 animal입니다.” 혹은 "rabbitanimal을 상속받는다."라고 말 할 수 있게 되었습니다.
  • 프로토타입을 설정해 준 덕분에 rabbit에서도 animal에 구현된 유용한 프로퍼티와 메서드를 사용할 수 있게 되었네요. 이렇게 프로토타입에서 상속받은 프로퍼티를 '상속 프로퍼티(inherited property)'라고 합니다.
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에서 상속받음)

  • 프로토타입 체이닝엔 두 가지 제약사항이 있습니다.
  1. 순환 참조(circular reference)는 허용되지 않습니다. - __proto__를 이용해 닫힌 형태로 다른 객체를 참조하면 에러가 발생합니다.
  2. __proto__의 값은 객체나 null만 가능합니다. 다른 자료형은 무시됩니다.
    여기에 더하여 객체엔 오직 하나의 [[Portotype]]만 있을 수 있다는 당연한 제약도 있습니다. 객체는 두 개의 객체를 상속받지 못합니다.

쓸 때는 프로토타입을 사용하지 않습니다.

  • 프로토타입은 프로퍼티를 읽을 때만 사용합니다.
  • 프로퍼티를 추가, 수정하거나 지우는 연산은 객체에 직접 해야 합니다.
  • 객체 rabbit에 메서드 walk를 직접 할당해 보겠습니다.
let animal = {
  eats: true,
  walk() {
    /* rabbit은 이제 이 메서드를 사용하지 않습니다. */
  }
};

let rabbit = {
  __proto__: animal
};

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

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

rabbit.walk()를 호출하면 프로토타입에 있는 메서드가 실행되지 않고, 객체 rabbit에 추가한 메서드가 실행됩니다.

  • 그런데 접근자 프로퍼티(accessor property)는 setter 함수를 통해서 프로퍼티에 값을 할당하므로 이 규칙이 적용되지 않습니다. 접근자 프로퍼티에 값을 할당하는 것은 함수를 호출하는 것과 같기 때문입니다.
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 , state of admin modified
alert(user.fullName); // John Smith , state of user protected

'this’가 나타내는 것

  • 메서드를 객체에서 호출했든 프로토타입에서 호출했든 상관없이 this는 언제나 . 앞에 있는 객체가 됩니다.
  • admin.fullName=으로 setter 함수를 호출할 때, thisuser가 아닌 admin이 되죠.
  • 상속받은 메서드를 사용하더라도 객체는 프로토타입이 아닌 자신의 상태를 수정합니다.
// 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이라는 프로퍼티가 없습니다.)

  • rabbit 말고도 bird, snake 등이 animal을 상속받는다고 해봅시다. 이 객체들도 rabbit처럼 animal에 구현된 메서드를 사용할 수 있겠죠. 이때 상속받은 메서드의 thisanimal이 아닌 실제 메서드가 호출되는 시점의 점(.) 앞에 있는 객체가 됩니다. 따라서 this에 데이터를 쓰면 animal이 아닌 해당 객체의 상태가 변화합니다.
  • 메서드는 공유되지만, 객체의 상태는 공유되지 않는다고 결론 내릴 수 있습니다.

for…in 반복문

  • 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
  • obj.hasOwnProperty(key)를 이용하면 상속 프로퍼티를 순회 대상에서 제외할 수 있습니다. 이 내장 메서드는 key에 대응하는 프로퍼티가 상속 프로퍼티가 아니고 obj에 직접 구현되어있는 프로퍼티일 때 true를 반환합니다.
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
  }
}



8.2 함수의 prototype 프로퍼티


요약

이번 챕터에선 생성자 함수를 이용해 만든 객체에 [[Prototype]]을 설정해 주는 방법에 대해 간략히 알아보았습니다. 이 방법을 기반으로 하는 고급 프로그래밍 패턴에 대해선 추후 학습할 예정입니다.

몇 가지 사항만 명확하게 이해하고 있으면 지금까지 배운 것들은 복잡하지 않습니다.

  • F.prototype 프로퍼티는 [[Prototype]]과는 다릅니다. F.prototypenew F()를 호출할 때 만들어지는 새로운 객체의 [[Prototype]]을 설정합니다.
  • F.prototype의 값은 객체나 null만 가능합니다. 다른 값은 무시됩니다.
  • 지금까지 배운 내용은 생성자 함수에 "prototype"를 설정하고, 이 생성자 함수를 new를 사용해 호출할 때만 적용됩니다.

일반 객체에 "prototype" 프로퍼티를 사용하면 아무런 일이 일어나지 않습니다.

let user = {
  name: "John",
  prototype: "Bla-bla" // 마술은 일어나지 않습니다.
};

모든 함수는 기본적으로 F.prototype = { constructor : F }를 가지고 있으므로 함수의 "constructor" 프로퍼티를 사용하면 객체의 생성자를 얻을 수 있습니다.

  • new F()와 같은 생성자 함수를 이용하면 새로운 객체를 만들 수 있다는 걸 앞서 배운 바 있습니다.
  • 그런데 F.prototype이 객체면 new 연산자는 F.prototype을 사용해 새롭게 생성된 객체의 [[Prototype]]을 설정합니다.
  • F.prototype에서 "prototype"은 F에 정의된 일반 프로퍼티라는 점에 주의해 주시기 바랍니다. 앞서 배웠던 ‘프로토타입’ 객체와 같아 보이지만 F.prototype에서 "prototype"은 이름만 같은 일반 프로퍼티입니다.
let animal = {
  eats: true
};

function Rabbit(name) {
  this.name = name;
}

Rabbit.prototype = animal;

let rabbit = new Rabbit("White Rabbit"); //  rabbit.__proto__ == animal

alert( rabbit.eats ); // true
  • Rabbit.prototype = animal은 "new Rabbit을 호출해 만든 새로운 객체의 [[Prototype]]animal로 설정하라."라는 것을 의미합니다.
  • 그림에서 가로 화살표는 일반 프로퍼티인 "prototype"을, 세로 화살표는 [[Prototype]]을 나타냅니다. 세로 화살표는 rabbitanimal을 상속받았다는 것을 의미합니다.

F.prototypenew F를 호출할 때만 사용됩니다.

F.prototype 프로퍼티는 new F가 호출될 때만 사용됩니다. new F를 호출해 새롭게 만든 객체의 [[Prototype]]을 할당해 주죠.

새로운 객체가 만들어진 후에 F.prototype 프로퍼티가 바뀌면(F.prototype = <another object>) new F로 만들어지는 새로운 객체는 또 다른 객체를 [[Prototype]]으로 갖게 됩니다. 다만, 기존에 있던 객체의 [[Prototype]]은 그대로 유지됩니다.

함수의 prototype 프로퍼티와 constructor 프로퍼티

  • 개발자가 특별히 할당하지 않더라도 모든 함수는 "prototype" 프로퍼티를 갖습니다.
  • 기본 프로퍼티인 "prototype"은 constructor 프로퍼티 하나만 있는 객체를 가리키는데, 이 constructor 프로퍼티는 함수 자신을 가리킵니다.
function Rabbit() {}
// 기본 prototype:
// Rabbit.prototype = { constructor: Rabbit }

alert( Rabbit.prototype.constructor == Rabbit ); // true
  • 생성자 함수의 경우
  • 특별한 조작을 가하지 않았다면 Rabbit을 구현한 객체 모두에서 [[Prototype]]을 거쳐 constructor 프로퍼티를 사용할 수 있습니다.
function Rabbit() {}
// 기본 prototype:
// Rabbit.prototype = { constructor: Rabbit }

let rabbit = new Rabbit(); // {constructor: Rabbit}을 상속받음

alert(rabbit.constructor == Rabbit); // true (프로토타입을 거쳐 접근함)

  • constructor 프로퍼티를 사용하면 기존에 있던 객체의 constructor를 사용해 새로운 객체를 만들 수 있습니다.
function Rabbit(name) {
  this.name = name;
  alert(name);
}

let rabbit = new Rabbit("White Rabbit");
let rabbit2 = new rabbit.constructor("Black Rabbit");
  • 자바스크립트는 알맞은 "constructor" 값을 보장하지 않습니다.
  • 함수에 기본으로 "prototype" 값이 설정되긴 하지만 그게 전부입니다. "constructor"에 벌어지는 모든 일은 전적으로 개발자에게 달려있습니다.
  • 함수의 기본 "prototype" 값을 다른 객체로 바꾸면 이 객체엔 "constructor"가 없을 겁니다.
  • 이런 상황을 방지하고 알맞은 constructor를 유지하려면 "prototype" 전체를 덮어쓰지 말고 기본 "prototype"에 원하는 프로퍼티를 추가/제거해야 합니다.
function Rabbit() {}
// Rabbit.prototype 전체를 덮어쓰지 말고
// 원하는 프로퍼티는 그냥 추가하세요.
Rabbit.prototype.jumps = true
// 이렇게 하면 기본 Rabbit.prototype.constructor가 유지됩니다.
  • constructor 프로퍼티를 수동으로 다시 만들어주는 것도 대안이 될 수 있습니다.
Rabbit.prototype = {
  jumps: true,
  constructor: Rabbit
};
// 수동으로 추가해 주었기 때문에 알맞은 constructor가 유지됩니다.



📚 참고 : javascript.info

profile
protect me from what i want

0개의 댓글