코딩/Javascript

[JS-중급편-OOP] 10.클래스

드리프트 2020. 12. 30. 17:42
728x170

 

 

객체에 관해서는 지금까지 많은 부분을 다루었습니다.

 

객체를 만드는 방법을 알아 보았고, 프로토타입 상속에 대해 배웠고, 객체를 확장하는 마법도 보았습니다.

 

이 모든 과정에서 우리는 매우 낮은 수준에서 수박 겉핥기만 했습니다.

 

좀더 깊게 이해하는게 필요할 때입니다.

 

그래서 이 모든 것을 단순화하고 쉽게 이해할 수 있게 ES6 버전의 JavaScript에서는 클래스라는 것을 지원합니다.

 

다른 객체 지향 프로그래밍 언어에 대한 배경 지식이 있는 분들은 아마도 그 용어에 익숙 할 겁니다.

 

자바스크립트 세계에서 클래스는 특별한 것이 아닙니다.

 

객체로 작업 할 때 입력해야 하는 내용을 단순화하는 몇 가지 새로운 키워드와 규칙에 지나지 않습니다.

 

다음 섹션에서는 이것이 의미하는 바를 좀더 살펴 볼 겁니다.

 

 

클래스 문법과 객체 생성

우리는 코드를 작성하면서 클래스에 대해 배울 예정입니다.

 

클래스에 대해 알아 볼게 많기 때문에 모든 것을 한꺼번에 다 하려고 하진 않겠습니다.

 

일단 객체를 생성 할 때 클래스 문법을 사용하는 방법을 알아 봅시다.

 

클래스 만들기

클래스는 템플릿으로 생각할 수 있습니다. 템플릿 객체는 생성 될 때 참조합니다.

 

 Planet이라는 새 클래스를 만들고 싶다고 가정 해 보겠습니다. 

 

해당 클래스의 가장 기본적인 버전은 다음과 같습니다.

 

class Planet {

}

 

class라는 키워드와 클래스에 부여 할 이름을 사용합니다.

 

클래스의 본문은 중괄호 { 및 } 안에 있습니다.

 

보시다시피 우리 클래스는 현재 비어 있습니다.

 

이 클래스를 기반으로 객체를 생성하려면 다음처럼 하면 됩니다.

 

let myPlanet = new Planet();

 

우리는 객체의 이름을 선언하고 new 키워드를 사용하여 Planet 클래스를 기반으로 객체를 생성 (일명 인스턴스화)합니다.

내부에서 일어나는 일을 시각화해야 한다면 다음과 같은 결과를 볼 수 있습니다.

 

 

예전에 Object.create() 를 사용하여 객체를 만들었을 때랑 약간은 다릅니다.

 

차이점은 여기서 우리는 new 키워드를 사용해서 myPlanet객체를 만들었기 때문입니다.

 

new 키워드로 객체를 만들면 다음과 같은 일이 자바스크립트 내부적으로 일어납니다.

 

  1. 우리의 새로운 객체는 단순히 그냥 Planet 타입입니다.

  2. 우리의 새로운 객체의 [[prototype]]은 우리의 새로운 함수 또는 클래스의 프로토타입 속성이 됩니다.

  3. 새로 생성된 객체를 초기화하는 생성자 함수가 실행됩니다.

추가 세부 사항으로 더 어려워지기전에 여기서 중요하게 살펴볼게 있습니다.

 

그것은 위의 세 번째 항목에서 언급 한 소위 생성자 함수입니다.

 

 

생성자 함수

 

생성자는 클래스 내부에 있는 함수입니다.

 

새로 생성 된 객체를 초기화하는 역할을 하며 객체 생성 중에 생성자 함수 내부에 있는 코드를 실행하여 객체를 초기화합니다.

 

선택적 사항이 아닙니다.

 

모든 클래스에는 생성자 함수가 있습니다.

 

클래스에 생성자 함수가 포함되어 있지 않은 경우 (현재 우리 Planet 클래스의 경우) JavaScript는 자동으로 빈 생성자를 생성합니다.

 

계속해서 Planet 클래스의 생성자를 만들어 보겠습니다.

 

class Planet {
  constructor(name, radius) {
    this.name = name;
    this.radius = radius;
  }
}

 

생성자를 정의하기 위해 constructor 이라는 특수 키워드를 사용합니다.

 

함수와 마찬가지로 사용하려는 인수를 지정할 수도 있습니다.

 

우리의 경우 name 과 radius 값을 인자로 지정하고 이를 사용하여 객체의 name 과 radius 속성을 설정합니다.

 

...
...
    this.name = name;
    this.radius = radius;
...
...

 

생성자 내부에서는 훨씬 더 많은 일을 할 수 있지만, 기억해야 할 중요한 점은 Planet 클래스를 사용하여 새 객체를 만들 때마다 이 코드가 실행된다는 것입니다.

 

말하자면 다음은 Planet 클래스를 호출하여 객체를 만드는 방법입니다.

 

let myPlanet = new Planet("Earth", 6378);
console.log(myPlanet.name); // Earth

 

생성자에 설정해야하는 두 개의 인수는 실제로 Planet 클래스 자체에 직접 설정됩니다.

 

myPlanet 객체가 생성되면 생성자가 실행되고 전달 된 name 과 radius 값이 이 인스턴스를 참조하는 this 키워드를 통해 객체에 설정됩니다.

 

 

우리가 클래스 문법과 그에 대한 세부 사항에 대해 배우는 동안 이 모든 것이 단지 겉보기일 뿐이라는 사실을 잊지 마십시오.

 

클래스 문법을 사용하지 않았다면 다음과 같이 할 수도 있습니다.

 

function Planet(name, radius) {
  this.name = name;
  this.radius = radius;
};

let myPlanet = new Planet("Earth", 6378);
alert(myPlanet.name); // Earth

 

최종 결과는 클래스 문법으로 얻은 결과와 거의 동일합니다.

 

우리가 어떻게 거기에 도달했는지가 다를 뿐입니다.

 

하지만 이 비교가 잘못된 인상을 주지 않도록 하십시오.

 

클래스의 다른 유용한 문법은 여기에서 본 것처럼 일반적인 함수 접근 방식을 사용하여 변환하기가 쉽지 않습니다.

 

 

클래스 더 알아보기

우리의 클래스 객체는 함수와 비슷해 보이지만 몇 가지 단점이 있습니다.

 

클래스 문법에 들어가는 특별한 것 중 하나가 생성자 함수라는 것을 보았습니다.

 

클래스 내부에 들어갈 수 있는 유일한 것은 함수입니다.

 

이 모든 작업을 확인하기 위해 지구 표면적을 반환하는 getSurfaceArea 함수를 추가해 보겠습니다. 

 

계속해서 다음과 같이 변경하십시오.

 

class Planet {
  constructor(name, radius) {
    this.name = name;
    this.radius = radius;
  }

  getSurfaceArea() {
    let surfaceArea = 4 * Math.PI * Math.pow(this.radius, 2);
    return surfaceArea;
  }
}		

 

생성 된 객체에서 getSurfaceArea를 호출하여 작동하는지 확인해 봅시다.

 

let earth = new Planet("Earth", 6378);
alert(earth.getSurfaceArea());

 

이 코드가 실행되면 5 억 1 천 1 백만 평방 킬로미터가 표시됩니다.

 

좋습니다.

 

클래스 본문에 들어갈 수있는 다른 것들은 getters와 setters라고 언급 했으므로 우리는 그것들도 넣어 보겠습니다.

 

우리는 그것들을 우리 행성의 중력을 나타내는 데 사용할 것입니다.

 

class Planet {
  constructor(name, radius) {
    this.name = name;
    this.radius = radius;
  }

  getSurfaceArea() {
    let surfaceArea = 4 * Math.PI * Math.pow(this.radius, 2);
    return surfaceArea;
  }

  set gravity(value) {
    this._gravity = value;
  }

  get gravity() {
    return this._gravity;
  }
}

let earth = new Planet("Earth", 6378);
earth.gravity = 9.81;
earth.getSurfaceArea();

alert(earth.gravity) // 9.81

 

그게 전부입니다.

 

클래스 본문에 추가하는 모든 거는 우리가 생성한 earth 객체에 있지 않는다는 것입니다.

 

대신 Planet 프로토 타입 (Planet.prototype)에 있습니다.

 

 

공유 인스턴스가 잘 작동 할 때는 모든 객체가 불필요하게 클래스 내부 사본을 가지고 다니는 것을 원하지 않기 때문에 이는 좋은 일입니다!

 

이를 감안할 때 위 다이어그램에 표시된 것을 볼 수 있습니다.

 

우리의 중력 getters 및 setters와 getSurfaceArea 함수는 전적으로 프로토 타입에 있습니다!

 

더보기

클래스 내부의 함수가 이상하게 보이는 이유는 무엇입니까?

한 가지 눈치 챘을 수도 있는 것은 클래스 본문 내부의 함수 모양이 약간 이상해 보인다는 것입니다.

 

예를 들어 함수 키워드가 누락되었습니다.

 

그 이상함은 클래스와 직접적인 관련이 없습니다.

 

객체 내부에 함수를 정의 할 때 사용할 수 있는 약식 문법이 있습니다.

 

다음과 같이 작성하는 대신 :

let blah = {
  zorb: function() {
    // something interesting
  }
};	

 

다음과 같이 zorb 함수 정의를 축약 할 수 있습니다.

let blah = {
  zorb() {
    // something interesting
  }
};

 

클래스 본문 내에서 함수를 지정할 때 사용된 것은 이 축약된 형식입니다.

 

 

객체 확장하기

 

마지막으로 살펴볼 것은 이 클래스 기반 세계에서 객체를 확장하는 것과 관련이 있습니다.

 

이를 돕기 위해 우리는 감자 행성으로 알려진 완전히 새로운 유형의 행성과 함께 작업 할 것입니다.

 

 

감자 행성에는 일반 행성의 모든 특성이 포함 되어 있지만 감자 행성은 완전히 감자로 구성되어 있습니다.

 

우리가 할 일은 감자 행성을 클래스로 정의하는 것입니다.

 

그 기능은 대부분 Planet 클래스의 기능을 반영하지만 생성자에있는 potatoType 인수와 potatoType의 값을 콘솔에 출력하는 getPotatoType 메서드와 같은 몇 가지 추가 기능이 있습니다.

 

별로 좋지 않은 접근 방식은 감자 행성 클래스를 다음과 같이 정의하는 것입니다.

 

class PotatoPlanet {
  constructor(name, radius, potatoType) {
    this.name = name;
    this.radius = radius;
    this.potatoType = potatoType;
  }

  getSurfaceArea() {
    let surfaceArea = 4 * Math.PI * Math.pow(this.radius, 2);
    console.log(surfaceArea + " square km!");
    return surfaceArea;
  }

  getPotatoType() {
    let thePotato = this.potatoType.toUpperCase() + "!!1!!!";
    console.log(thePotato);
    return thePotato;
  }

  set gravity(value) {
    this._gravity = value;
  }

  get gravity() {
    return this._gravity;
  }
}

 

우리는 PotatoPlanet 클래스를 가지고 있으며, 여기에는 새로운 감자 관련 항목뿐만 아니라 Planet 클래스의 모든 기능도 포함되어 있습니다.

 

이 접근법은 코드를 복제하고 있기 때문에 좋지 않습니다.

 

이제 코드를 복제하는 대신에, 우리 Planet 클래스가 제공하는 기능을 PotatoPlanet에 필요한 몇 가지 추가 기능으로 확장하는 방법이 있다면 어떨까요?

 

더 나은 접근법이 아닐까요? extends 키워드를 이용하는 방법이 있습니다.

 

PotatoPlanet 클래스를 통해 Planet 클래스를 확장하면 다음과 같은 작업을 수행 할 수 있습니다.

 

class Planet {
  constructor(name, radius) {
    this.name = name;
    this.radius = radius;
  }

  getSurfaceArea() {
    let surfaceArea = 4 * Math.PI * Math.pow(this.radius, 2);
    return surfaceArea;
  }

  set gravity(value) {
    this._gravity = value;
  }

  get gravity() {
    return this._gravity;
  }
}

class PotatoPlanet extends Planet {
  constructor(name, width, potatoType) {
    super(name, width);

    this.potatoType = potatoType;
  }

  getPotatoType() {
    let thePotato = this.potatoType.toUpperCase() + "!!1!!!";
    return thePotato;
  }
}

 

PotatoPlanet 클래스를 어떻게 선언하는지 보십시오.

 

extends 키워드를 사용하고 확장 할 클래스인 Planet을 지정합니다.

 

class PotatoPlanet extends Planet {
  .
  .
  .
  .
}

 

거기에서 명심해야 할 다른 것은 생성자와 관련이 있습니다. 

 

생성자를 수정할 필요없이 클래스를 확장하려면 클래스 내부에 생성자를 지정하는 것을 완전히 건너 뛸 수 있습니다.

 

class PotatoPlanet extends Planet {
  sayHello() {
    console.log("Hello!");
  }
}

 

우리의 경우에는 potatoType 에 대한 속성을 추가하였기 때문에 PotatoPlanet 클래스에 생성자를 별도로 추가했습니다.

 

class PotatoPlanet extends Planet {
  constructor(name, width, potatoType) {
    super(name, width);

    this.potatoType = potatoType;
  }
...
...
}

 

super 키워드를 사용하고 필요한 관련 인수를 전달하여 부모 (Planet) 생성자를 명시적으로 호출할 수 있습니다.

 

이 super  호출은 객체의 Planet 부분이 초기화의 일부로 필요한 것이 무엇이든 트리거 되도록 합니다.

 

PotatoPlanet을 사용하기 위해 우리는 객체를 생성하고 객체의 메서드를 호출할 수 있습니다.

 

다음은 spudnik이라고하는 PotatoPlanet 유형의 객체를 만드는 예입니다.

 

let spudnik = new PotatoPlanet("Spudnik", 12411, "Russet");
spudnik.gravity = 42.1;
spudnik.getPotatoType();

 

멋진 점은 spudnik이 PotatoPlanet 클래스의 일부로 정의한 기능에 액세스 할 수 있을 뿐만 아니라 확장한 Planet 클래스에서 제공하는 모든 기능도 사용할 수 있다는 것입니다.

 

더 복잡한 버전의 프로토 타입을 다시 살펴보면 그 이유를 알 수 있습니다.

 

프로토 타입 체인을 따라 가면 spudnik 객체에서 PotatoPlanet.prototype, Planet.prototype, 마지막으로 Object.prototype으로 이동합니다.

 

우리의 spudnik 객체는 이러한 프로토 타입 정거장에 정의 된 모든 속성 또는 메서드에 액세스 할 수 있습니다.

 

이것이 바로 PotatoPlanet이 자체적으로 많은 것을 정의하지 않더라도 Object 또는 Planet에서 사물을 호출 할 수 있는 이유입니다.

 

이것은 객체를 확장하는 강력한 기능입니다.

 

 

결론

클래스 문법은 객체 작업을 정말 쉽게 만듭니다.

 

클래스 문법은 우리가 하고 싶은 일에 더 집중할 수 있게 해준다는 것입니다.

 

Object.create() 및 객체 프로토타입으로 작업하면 많은 제어기능을 제공하겠지만 대부분의 경우 해당 제어가 필요하지 않은 경우가 많습니다.

 

클래스로 작업함으로써 우리는 복잡성을 버리고 단순성에 집중할 수 있습니다.

 

 

그리드형