본문 바로가기

모던 자바스크립트

8.1 프로토타입 상속

📢 기존에 있는 기능을 가져와 확장해야 하는 경우

 

사람에 관한 프로퍼티와 메서드를 가진 user라는 객체가 있는데, user와 상당히 유사하지만 약간의 차이가 있는 admin과 guest 객체를 만들어야 한다고 할 때, user의 메서드를 복사하거나 다시 구현하지 않고 프로토타입 상속(prototypal inheritance)을 이용하면 user에 약간의 기능을 얹어 admin과 guest객체를 만들 수 있다. 

 

[[Prototype]]

◾️ 자바스크립트 객체는 명세서에서 명명한 [[Prototype]]이라는 숨김 프로퍼티를 가진다. 

◾️ 숨김 프로퍼티 값은 null이거나 다른 객체에 대한 참조가 되는데, 다른 객체를 참조하는 경우 참조 대상을 프로토타입(prototype)이라고 한다. 

◾️ 프로토타입 상속 : object에서 프로퍼티를 읽으려고 하는데 해당 프로퍼티가 없으면 자바스크립트는 자동으로 프로토타입에서 프로퍼티를 찾는다. 

◾️ [[Prototye]] 프로퍼티는 내부 프로퍼티이면서 숨김 프로퍼티이지만 다양한 방법을 사용해 개발자가 값을 설정할 수 있다. 

◾️ __proto__ 사용하여 값을 설정

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

rabbit.__proto__ = animal;
ℹ️ __proto__는 [[Prototype]]용 getter, setter이다.

__proto__는 [[Prototype]]와 다르다. __proto__는 [[Prototype]]의 getter(획득자)이자 setter(설정자)이다. 
위 호환성 때문에 여전히 __proto__를 사용할 순 있지만 비교적 근래에 작성된 스크립트에선 __proto__대신 함수 Object.getPrototypeOfObject.setPrototypeOf을 써서 프로토타입을 획득(get)하거나 설정(set)한다.  

__proto__는 브라우저 환경에서만 지원하도록 자바스크립트 명세서에서 규정하였는데, 실상은 서버 사이드를 포함한 모든 호스트 환경에서 __proto__를 지원한다. 

 

 

◾️ 객체 rabbit에서 프로퍼티를 얻고싶은데 해당 프로퍼티가 없다면, 자동으로 animal이라는 객체에서 프로퍼티를 얻는다. 

 

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

rabbit.__proto__ = animal; // (*)

alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true

◾️ (*) : animal이 rabbit의 프로토타입이 되도록 설정 

◾️ (**) : alert함수가 rabbit.eats 프로퍼티를 읽으려 했는데, rabbit엔 eats라는 프로퍼티가 없기 때문에 [[Prototype]]이 참조하고 있는 객체인 animal에서 eats를 얻어낸다. 

➡️ rabbit의 프로토타입은 animal / rabbit은 animal을 상속받는다. 

◾️ 프로토타입을 설정해주었기 때문에 rabbit에서도 animal에 구현된 프로퍼티와 메서드를 사용할 수 있다. 

◾️ 상속 프로퍼티(inherited property) : 프로토타입에서 상속받은 프로퍼티

 

◾️ 프로토타입 animal에서 walk를 자동으로 상속 받았기 때문에 rabbit에서도 walk를 호출할 수 있게 된다. 

let animal = {
  eats: true,
  walk() {
    alert('동물이 걷는다');
  }
};
let rabbit = {
  jumps: true,
  __proto__: animal
};

alert( rabbit.walk ); // 동물이 걷는다

 

◾️ 프로토타입 체인

let animal = {
  eats: true,
  walk() {
    alert('동물이 걷는다');
  }
};
let rabbit = {
  jumps: true,
  __proto__: animal
};

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

longEar.walk(); // 동물이 걷는다 (animal -> rabbit에서 상속받음)
alert( longEar.jumps ); // true (rabbit에서 상속 받음)

◾️ 프로토타입 제약사항

  1. 순환 참조(circular reference)는 허용되지 않는다. __proto__를 이용해 닫힌 형태로 다른 객체를 참조하면 에러가 발생한다. 
  2. __proto__의 값은 객체나 null만 가능하다. 다른 자료형은 무시된다. 

 

 

 

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

◾️ 프로퍼타입은 프로퍼티를 읽을 때만 사용한다. 

◾️ 프로퍼티를 추가, 수정거나 지우는 연산은 객체에 직접 해야 한다. 

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: 'suzu',
  surname: 'lee',

  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); // suzu lee (*)

// setter 함수 실행
admin.fullName = 'matsumoto jun' // (**)
alert(admin.fullName); // matsumoto jun

◾️ (*) : admin.fullName은 프로토타입(user)에 있는 getter 함수(get fullName)를 호출하고

◾️ (**) : 프로토타입에 있는 setter 함수(set fullName)를 호출한다. 

 

 

'this'가 나타내는 것

◾️ this는 프로토타입에 영향을 받지 않는다. 

◾️ 메서드를 객체에서 호출했든 프로토타입에서 호출했든 상관없이 this는 언제나 .앞에 있는 객체가 된다. 

◾️ admin.fullName = 으로 setter함수를 호출 할 때, this는 user가 아닌 admin이 된다. 

◾️ 상속받은 메서드를 사용하더라도 객체는 프로토타입이 아닌 자신의 상태를 수정한다. 

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에 구현된 메서드를 사용할 수 있다. 이 때 상속받은 메서드의 this는 animal이 아닌 실제 메서드가 호출되는 시점의 점 앞에 있는 객체가 된다. 따라서 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); // jump, 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
  }
}

 

◾️ rabbit은 animal을, animal은 Object.prototype을, Object.prototype은 null을 상속받고 있다. animal이 Object.prototype를 상속받는 이유는 객체 리터럴 방식으로 선언하였기 때문이다. 

 

◾️ hasOwnProperty가 Object.prototype.hasOwnProperty에서 왔다. 

◾️ hasOwnProperty는 상속 프로퍼티이지만 열거 가능한(enumerable) 프로퍼티가 아니기 때문에 출력이 되지 않는다. Object.prototype에 있는 모든 메서드의 enumerable 플래그는 false인데, for...in은 오직 열거 가능한 프로퍼티만 순회 대상에 포함하기 때문에 출력되지 않는다. 

 

 

 

 

 

ℹ️ 키 - 값을 순회하는 메서드 대부분은 상속 프로퍼티를 제외하고 동작한다. 

Object.keys, Object.values 같이 객체의 키-값을 대상으로 무언가를 하는 메서드 대부분은 상속 프로퍼티를 제외하고 동작한다. 프로토타입에서 상속받은 프로퍼티는 제외하고, 해당 객체에서 정의한 프로퍼티만 연산 대상에 포함한다. 

 

📝 요약

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

 

 



📝 오늘 새롭게 배운 내용 📝

https://www.design1st.com/prototype-manufacturing-services/

✅ 프로토타입의 사전적 정의 : 원래의 형태 또는 전형적인 예, 기초 또는 표준

✅ 초기 모델을 정해놓고, 기본 특성은 그대로 이어 받으면서 조금씩 변형시켜서 완성해나갈 때 유용하다!

✅ 코드의 재사용성을 높일 수 있다. 

✅ this는 프로토타입에 영향을 받지 않는다. 점 앞의 객체를 가리킨다. 

✅ 참고하기 : [Javascript ] 프로토타입 이해하기

 

 

 

📌 [JAVASCRIPT_INFO 프로토타입 상속]

 

프로토타입 상속

 

ko.javascript.info

 

📌 [MDN Object.prototype]

 

Object.prototype

Object.prototype 속성은 Object 프로토타입(원형) 객체를 나타냅니다.

developer.mozilla.org