본문 바로가기

개념/javaScript

[javaScript] 프로토타입 체인

시작하며

프로토타입 체인을 이해하기 위해선 '클래스와 인스턴스', '객체지향 프로그래밍'의 기초와 '프로토타입'에 대한 이해가 필요하다.


extends와 super이용해서 상속 클래스 구현하기

class Animal{
	constructor(type,family){
        this.type = type;
        this.family = family
    }
    
    eat(){
    	console.log('냠냠')
    }
    
    sleep(){
    	console.log('쿨쿨')
    }
}

class Dog extends Animal{
	constructor(type,family,name,species,gender){
    	//상위 클래스로부터 상속받은 프로퍼티 정의하기 위해선 super필요함
        super(type,family);
        
        //Dog 클래스에서만 추가되는 속성들
        this.name = name;
        this.species = species;
        this.gender = gender;
    }
    
    //Dog 클래스에서만 추가되는 메서드
    bark(){
    	console.log('왈왈!')
    }
    
}

const mongja = new Dog('포유류','개과','몽자','푸들','암컷');
mongja.type; //'포유류'
mongja.family; //'개과'
mongja.name; //'몽자'
mongja.species; //'푸들'
mongja.gender; //'암컷'
mongja.bark() //'왈왈!'
mongja.eat() //'냠냠'
mongja.sleep() //'쿨쿨'

위처럼 클래스 상속을 잘 이용해면 하나의 클래스를 이용해서 다른 클래스로 확장해 요긴하게 쓸 수 있다.

기존에 기능 + 새로운 기능 에다가, 포함 관계까지 명확해 보여서 이해하기도 쉬운 것 같다.

 

아래의 더보기를 누르면, 몇몇 속성들을 상위 클래스에서 지정한 내용으로 그대로 가져오고,

추가로 몇몇 속성들을 하위클래스에서 지정하는 코드 예시를 볼 수 있다.

👇🏻👇🏻👇🏻👇🏻

더보기

-상위 클래스 속성 그대로 가져오기-

이번엔 개과 Canine라는 상위클래스의 속성을 따로 Dog이라는 하위클래스에서 지정해주지 않고

그대로 물려받아 갖고 오고, 추가로 몇가지 속성들을 더하는 버전으로 작성해 보았다.

class Canine{
	constructor(){
        this.type = '포유류';
        this.family = '개과';
    }
    
    eat(){
    	console.log('냠냠')
    }
    
    sleep(){
    	console.log('쿨쿨')
    }
}

class Dog extends Canine{
	constructor(name,species,gender,type,family){
    	//상위 클래스로부터 상속받은 프로퍼티 정의하기 위해선 super필요함
        //🛑super로 물려받으려면 constructor에 인자로 꼭 들어가야 한다.
        //그런데 여기서 꼭 마지막에 넣어야한다!!
        //자동으로 순서대로 name,species,gender세가지 인자를 속성으로 설정하고
        //type,family은 Canine의 값을 그대로 가져온다.
        super(type,family)
        
        //Dog 클래스에서만 추가되는 속성들
        this.name = name;
        this.species = species;
        this.gender = gender;
    }
    
    //Dog 클래스에서만 추가되는 메서드
    bark(){
    	console.log('왈왈!')
    }
    
}

const mongja = new Dog('몽자','푸들','암컷');
mongja.type; //'포유류'
mongja.family; //'개과'
mongja.name; //'몽자'

 

-상위 클래스 속성 그대로 가져오기 : construtor인자 순서에 주의하기-

만약 위의 코드에서 constructor(type,family,name,species,gender)와 같이 물려받아 가져올

type,family 인자를 먼저 적게 되면 아래의 코드와 같이 꼬여버린다.

class Canine{
	constructor(){
        this.type = '포유류';
        this.family = '개과';
    }
    
    eat(){
    	console.log('냠냠')
    }
    
    sleep(){
    	console.log('쿨쿨')
    }
}

class Dog extends Canine{
	constructor(type,family,name,species,gender){
    	//인자로 들어온것의 첫번째는 type이, 두번째는 family가 차지하고
        //그 값들은 따로 한번더 정의되지 않고, 상위 클래스 Canine의 값으로 덮어씌어진다.
        super(type,family)
        
        //그리고 세번째 인자가 name이 되고,
        //네번째 인자가 species, 다섯번째 인자가 'gender'가 된다.
        this.name = name;
        this.species = species;
        this.gender = gender;
    }
    
    //Dog 클래스에서만 추가되는 메서드
    bark(){
    	console.log('왈왈!')
    }
    
}

const mongja = new Dog('몽자','푸들','암컷','??','???');
mongja.type; //'포유류'
mongja.family; //'개과'
mongja.name; //'암컷'
mongja.species; //'??'
mongja.gender; //'???'
//🛑세번째 인자인 '암컷'이 name으로 들어간다.

 

때문에 constructor에 넣어주는 인자의 순서와 길이, 내용은 아주 중요하다.

 

상위 클래스로부터 super로 가져오는 속성들은 constructor()인자에서 외부에서 받아오는 것들 이후에 마지막 순서로 적어준다!!

👆🏻👆🏻👆🏻👆🏻

 

 

더 자세한 내용은 링크를 참고

 


프로토타입 체인이란?

위의 extends와 super을 사용해 클래스의 상속을 보여준 예시 코드에서

하위 클래스인 Dog은 상위클래스인 Animal의 속성과 메서드를 참조할 수 있다. 이런 것을 "상속"이라고 한다.

 

위의 코드에서 아래 부분만 떼어내서 보면, 

하위 클래스 Dog의 인스턴스로 만들어진 mogja는 따로 정의한 적 없는

type과 family라는 속성과,

eat(), sleep()이라는 메서드를 가지고 있다.

const mongja = new Dog('포유류','개과','몽자','푸들','암컷');
mongja.type; //'포유류' //Dog 클래스엔 this.type=type을 설정한 적 없다. (super로 Animal속성 가져옴)
mongja.family; //'개과' //Dog 클래스엔 this.family=family을 설정한 적 없다. (super로 Animal속성 가져옴)
mongja.eat() //'냠냠' //Dog 클래스엔 eat이라는 메서드를 정의한 적 없다. (Animal에 정의)
mongja.sleep() //'쿨쿨' //Dog 클래스엔 sleep이라는 메서드를 정의한 적 없다. (Animal에 정의)

속성의 경우는 Dog클래스를 정의할 때 super()메서드로 상위 Animal의 속성 설정을 가져오도록 했으며,

eat()과 sleep()의 경우, Dog클래스에 정의되어 있지 않아서, 자바스크립트가 알아서 상위 Animal에 가서

찾아온 것이다.

 

이 Animal클래스 파생 > Dog클래스 인스턴스 > mogja객체의 역사를 보기위해서 

__proto__로 타고 올라가면 다음과 같다.

mongja.__proto__ //Animal {}
mongja.__proto__.constructor //ƒ Dog()
mongja.__proto__.__proto__ //{}
mongja.__proto__.__proto__.__proto__ //[Object: null prototype] {}

이런식으로 모든 객체들은  '상속'을 통해 서로서로 링크처럼 연결되어 있다.

이런 개념이 바로  "프로토타입 체인"이다.

 

프로토 타입 체인은 아래의 코드에서 한눈에 볼 수 있다.

let div = document.createElement('div');

div.__proto__ //HTMLDivElement
div.__proto__.__proto__ //HTMLElement
div.__proto__.__proto__.__proto__ //Element
div.__proto__.__proto__.__proto__.__proto__ //Node
div.__proto__.__proto__.__proto__.__proto__.__proto__ //EventTarget 
div.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__ //Object
div.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__ //null

추가로 위 두 코드에서 확인할 수 있듯이

모든 프로토타입 체인의 끝은 Object.prototype 이다.

프로토 타입 체인 만들어보기

구조

├── Grub
│   └── Bee
│       ├── HoneyMakerBee
│       └── ForagerBee

 

1. 최상위 Grub 클래스

 

👎🏻 아쉬운 코드

class Grub {
 constructor(){
  //🛑외부나 상위에서 받아오는 인자가 없을 경우 constructor에 인자로 넣을 필요가 없다.
  //🚫그러나 이경우 인자에 뭘 넣어줘도 반영이 안되기 때문에 좀 아쉽다.
  this.age = 0;
  this.color = 'pink';
  this.food = 'jelly';
 }

 eat(){
  return 'Mmmmmmmmm jelly'
 }
}

module.exports = Grub;

 

👍🏻 좋은 코드

class Grub {
  constructor(age = 0, color = 'pink', food = 'jelly'){
    //🛑이렇게 인자들의 기본값을 작성하면 아무것도 작성되어 있지 않을땐 기본값을 세팅한다
    //인자가 들어와도 제대로 작동해서 더 좋다.
    this.age = age;
    this.color = color;
    this.food = food;
  }
  eat(){
    return 'Mmmmmmmmm ' + this.food;
  }
}

module.exports = Grub;
  • 인자가 들어올 경우를 대비해서 인자에 기본값을 설정하는 형태로 지정하면 더 활용도가 좋다

 

 

2. 차상위 Bee 클래스

 

👎🏻 아쉬운 코드

const Grub = require('./Grub');

class Bee extends Grub{
  constructor(food){
    super(food);
    //🚫super에 인자를 적어주는 것은 나중에 값이 많아지면 사용성이 좋지 않다.
    //++사실 적으나 안적으나 값을 가져오는 것은 매한 가지인데,
    //괜히 값을 적으면 인자변수 순서도 그대로 적어줘야되고 괜히 복잡해진다.
    //오히려 적어서 값이 꼬여버린다.
    //그리고 super안에 인자를 넣으면 꼭 같은 변수가 constructor에도 있어야한다. (안그럼 오류남)
    this.age = 5;
    this.color = 'yellow';
    this.job = 'Keep on growing'
  }
}

module.exports = Bee;
  • super안에 인자 이름을 넣게되면 괜히 순서도 신경써주지 않으면 속성값들이 꼬여버린다.
  • 별도의 super 명시 없이도 Grub의 메서드 eat()사용 가능하다.(속성은 super필요)

👍🏻 좋은 코드

const Grub = require('./Grub');

class Bee extends Grub {
  constructor(age = 5, color = 'yellow', job = 'Keep on growing'){
    super();
     //🛑super()안에 인자들을 넣지 않고 그대로 다 가져온다.
     //🛑super()로 가져온 다음, 새로 받는 것들을 밑에 다시 정의하는게 깔끔하다.
     //🛑super()안에 인자가 없다면 constructor에 상속받을 인자가 없어도 된다.
     //🛑+ super를 안쓰면 오류난다.
    this.age = age;
    this.color = color;
    this.job = job;
  }
}

module.exports = Bee;
  • super안에 인자들을 적지 않고 전체를 그 순서 그대로 불러온다.
  • 인자를 적지 않으면 위에 꼭 constructor로 해당 인자를 받지 않고 위에서 그대로 긁어온다.
  • 긁어왔고 겹치는 것(age,color)들은 다시 정의, 안 긁어온 추가되는 속성(job)은 새로 정의한다. (긁어오고 새로 정의안되는 것(food)은 그냥 그대로 남는다.)

 

3. 하위 HoneyMakerBee 클래스(ForagerBee 클래스와 동급)

 

👎🏻 아쉬운 코드

const Bee = require('./Bee');

class HoneyMakerBee extends Bee {
  //🛑 Grub은 체인으로 타고 올라갈 수 있으므로 Bee에만 extends를 걸어준다.
  constructor(color,food){
    super(color,food);
    //🚫super에 인자를 적어주는 것은 나중에 값이 많아지면 사용성이 좋지 않다.
    //++사실 적으나 안적으나 값을 가져오는 것은 매한 가지인데,
    //괜히 값을 적으면 인자변수 순서도 그대로 적어줘야되고 괜히 복잡해진다.
    //오히려 적어서 값이 꼬여버린다.
    //그리고 super안에 인자를 넣으면 꼭 같은 변수가 constructor에도 있어야한다. (안그럼 오류남)
    this.age = 10;
    this.job = `make honey`;
    this.honeyPot = 0;
  }

  makeHoney(){
    this.honeyPot++
  }

  giveHoney(){
    this.honeyPot--
  }
}

module.exports = HoneyMakerBee;
  • extends에는 바로 상위의 클래스만 걸어줘도 된다.(체인으로 알아서 타고 올라감)
  • Bee만 extends로 걸어줘도 Grub의 속성과 메서드 사용 가능하다.

👍🏻 좋은 코드

const Bee = require('./Bee');

class HoneyMakerBee extends Bee{
  constructor(age = 10, job = 'make honey', honeyPot = 0) {
    super();
    //🛑super()안에 인자들을 넣지 않고 그대로 다 가져온다.
    //🛑super()로 가져온 다음, 새로 받는 것들을 밑에 다시 정의하는게 깔끔하다.
    //🛑super()안에 인자가 없다면 constructor에 상속받을 인자가 꼭 없어도 된다.
    //🛑+ super를 안쓰면 오류난다.
    this.age = age;
    this.job = job;
    this.honeyPot = honeyPot;
  }

  makeHoney() {
    this.honeyPot++;
  }

  giveHoney() {
    this.honeyPot--;
  }
}

module.exports = HoneyMakerBee;

 

4. 하위 ForagerBee 클래스 (HoneyMakerBee 클래스와 동급)

 

👎🏻 아쉬운 코드

const Bee = require('./Bee');

class ForagerBee extends Bee{
  //🛑 Grub은 체인으로 타고 올라갈 수 있으므로 Bee에만 extends를 걸어준다.
  constructor(color,food){
    super(color,food);
    //🚫super에 인자를 적어주는 것은 나중에 값이 많아지면 사용성이 좋지 않다.
    //++사실 적으나 안적으나 값을 가져오는 것은 매한 가지인데,
    //괜히 값을 적으면 인자변수 순서도 그대로 적어줘야되고 괜히 복잡해진다.
    //오히려 적어서 값이 꼬여버린다.
    //그리고 super안에 인자를 넣으면 꼭 같은 변수가 constructor에도 있어야한다. (안그럼 오류남)
    this.age = 10;
    this.job = `find pollen`;
    this.canFly = true;
    this.treasureChest = []
  }

  forage(treasure){
    this.treasureChest.push(treasure)
  }
}

module.exports = ForagerBee;

👍🏻 좋은 코드

const Bee = require('./Bee');

class ForagerBee extends Bee{
  constructor(age = 10, canFlay = true, job = 'find pollen', treasureChest = []) {
    super();
    //🛑super()안에 인자들을 넣지 않고 그대로 다 가져온다.
    //🛑super()로 가져온 다음, 새로 받는 것들을 밑에 다시 정의하는게 깔끔하다.
    //🛑super()안에 인자가 없다면 constructor에 상속받을 인자가 없어도 된다.
    //🛑+ super를 안쓰면 오류난다.
    this.age = age;
    this.canFly = canFlay;
    this.job = job;
    this.treasureChest = treasureChest;
  }

  forage(treasure) {
    this.treasureChest.push(treasure);
  }
}

module.exports = ForagerBee;

 

-중요요약-

  1. super()로 인자없이 긁어온다음 this.prop = prop으로 재정의 해주기
  2. 기본 값이 정해져있다면 (constructor인자에 prop = 'value'로 기본값을 정해주면) 값이 인자로 들어와도 쓸 수 있음

끝으로

자바스크립트는 다른 프로그래밍 언어들과는 다르게 내부적으로 class 개념과 상속이 존재하지 않는다고 한다. 

그리고 이 커다란 구멍을 매꾸는 것이 prototype이다.

class와 extend라고 쓰지만, 사실 프로토타입을 통해 동일하게 작동하도록 구현해 놓은 것이라고 한다.

그래서 프로토타입이라는 개념이 있어, 자바스크립트에서도 객체지향프로그래밍이 가능해진 것이다.

 

프로토타입을 이해하려고 몇일을 짱돌을 굴렸지만, 아직 프로토타입을 100%이해한것 같지는 않다.

그래도 프로토타입이 어떤 것이고, 자바스크립트가 어떤 원리로 클래스와 상속을 구현하는지는 이해하게 되었다.

이후 다른 프로그래밍 언어도 배우고, 조금 더 써보면 더 확실해지지 않을까 싶다.

 

 

휴, 프로토타입 쉽지 않다.


관련글

[javaScript] 프로토타입

[javaScript] 객체지향프로그래밍

[javaScript] 클래스와 인스턴스