자바스크립트 클래스

jkpapa·2023년 3월 28일
0

JavaScript

목록 보기
3/10

클래스

  • 자바스크립트는 본래 객체를 다룰 때 생성자 함수와 프로토타입을 중심으로 관리해왔다.
  • 하지만 이는 다른 언어의 객체지향과는 다르고 생소하기 때문에 class라는 이름으로 포장하여 다른 언어의 클래스와 비슷한 문법의 형태로 만들었다.

자바스크립트 클래스는 프로토타입 기반의 자바스크립트 객체지향을 다른 언어의 객체지향과 비슷한 형태로 표현하는 최신문법이다.

⚠️ 그러나 클래스와 생성자 함수의 동작이 동일하지는 않다!

  1. 클래스는 함수와는 달리 호이스팅되지 않는다.
    • 클래스를 정의하기 이전에 생성하려고 시도하면 오류가 발생한다.
const inst = new Person('Tom', 25);
// ⛔Uncaught ReferenceError: Person is not defined

class Person{
  constructor(name, age){
    this.name = name;
    this.age = age;
  }
}
  1. new를 빼고 생성을 시도하면 오류가 발생한다.
    • new를 빼고 호출하면 오류없이 undefined를 반환하던 생성자 함수와 다르다.
class Person{
  constructor(name, age){
    this.name = name;
    this.age = age;
  }
}

const inst = Person('Tom', 25);
// ⛔Uncaught TypeError: 
// Class constructor Person cannot be invoked without 'new'

클래스 메서드

constructor 메서드

  • 클래스를 생성 할 때 각 인스턴스를 어떤 인자를 받아서 어떻게 만들지를 결정하는 메서드(인자를 받아 프로퍼티 초기화)
  • 클래스에 단 하나만 있을 수 있다.(초과시 오류 발생)
  • 인스턴스 초기화가 필요없는 클래스는 생략 할 수 있다.
class Person{
  constructor(name, age, married = false){
    this.name = name;
    this.age = age;
    this.married = married; // 기본값을 사용 할 수 있다.
    // 인스턴스 생성시 전달되지 않은 인자는 기본값으로 세팅
  }
}

const person1 = new Person('Tom', 25, true);
const person2 = new Person('Mike', 15);

메서드

  • 클래스는 객체에서와 같이 프로퍼티로써의 함수가 아니라 메서드를 사용할 수 있다.

  • 아래 코드를 보자

class Dog {
  bark () {
    return '멍멍';
  }
}
const badugi = new Dog();
console.log(badugi, badugi.bark());
/*
Dog {} '멍멍'

[[Prototype]]: Object
bark: ƒ bark()
constructor: class Dog
[[Prototype]]: Object
*/

function Dog2 () {
  this.bark = function () {
    return '멍멍';
  }
}
const badugi = new Dog2();
console.log(badugi, badugi.bark());
/*
Dog2 {bark: ƒ} '멍멍'
bark: ƒ ()
[[Prototype]]: Object
*/
  • 클래스 Dog로 생성한 인스턴스를 출력해보면 해당 인스턴스는 bark라는 메서드를 가지고 있지 않다. 메서드prototype에 들어가 있다.

객체의 메서드는 인스턴스 내에 들어가 있다. 객체는 메서드를 프로퍼티로 취급한다면, 클래스는 자신으로부터 파생된 모든 인스턴스들이 공통적으로 갖는 기능이라는 의미로서 프로토타입에 메서드가 저장되는 것 같다.

  • 반대로 생성자 함수로 생성한 인스턴스는 자신이 함수를 가지고 있다.

필드

  • 클래스를 생성할 때는 constructor메서드를 사용하여 프로퍼티를 정의했었다.
  • constructor밖에서 this를 사용할 필요 없이 인스턴스의 프로퍼티를 정의할 수 있는 문법을 필드라고 부른다.
// 필드값이 지정되어 있으므로 constructor 메서드 필요없음
// 필드값으로 객체의 프로퍼티 기본 값을 설정
class Person {
  name = 'Tom';
  age = 25;
  hello () {
    console.log(`Hello my name is ${this.name}, ${this.age} years old`);
  }
}

const person1 = new Person();

console.log(person1); // Person {name: 'Tom', age: 25}
person1.hello(); // Hello my name is Tom, 25 years old
  • 아래 코드를 보자
class Chicken {
  no = 0;
  menu = { '후라이드': 10000, '양념치킨': 12000 };

  constructor (name, no) {
    this.name = name;
    if (no) this.no = no;
  }
  introduce () {
    return `안녕하세요, ${this.no}${this.name}점입니다!`;
  }
  order (name) {
    return `${this.menu[name]}원입니다.`
  }
}

const chain0 = new Chicken('(미정)');
console.log(chain0, chain0.introduce());
/*
Chicken {no: 0, menu: {…}, name: '(미정)'} 
'안녕하세요, 0호 (미정)점입니다!'
*/

const chain1 = new Chicken('판교', 3);
console.log(chain1, chain1.introduce());
/*
Chicken {no: 0, menu: {…}, name: '(미정)'} 
'안녕하세요, 3호 판교점입니다!'
*/
  • no필드: 생성자 함수에서 사용했던 기본 값을 필드로 대체할 수 있다.
  • menu필드: 생성할 때 전달하는 인자의 영향을 받지 않기 때문에 필드로 지정했다.
  • 생성자 메서드: nameno를 인자로 받는데 만약 인스턴스를 생성 할 때 name만 인자로 전달 했다면 no는 기본 값으로 설정된다.
  • 프로퍼티로 넣어준 값들은 인스턴스에 각각 개별적으로 초기화된다. 같은 클래스에서 만들어진 인스턴스라도 모두 같은 데이터를 참조하는 것이아니다.
chain1.menu['양념치킨'] = 13000;

console.log(chain0.order('양념치킨'), chain1.order('양념치킨'));
// 12000원입니다. 13000원입니다.

정적 필드와 메서드

  • 자바스크립트에서는 다른 객체지향언어와 비슷하게 정적 필드, 메서드를 지원한다.
class Chicken {

  // 정적 변수와 메서드
  static brand = 'JS치킨';
  static contact () {
    return `${this.brand}입니다. 무엇을 도와드릴까요?`;
  }

  constructor (name, no) {
    this.name = name;
    this.no = no;
  }
  
  introduce () {
    return `안녕하세요, ${this.no}${this.name}점입니다!`;
  }
}

console.log(Chicken);
console.log(Chicken.contact());
  • 정적 필드와 일반 필드는 메모리 상에서 클래스나 생성자 함수가 차지 하는 자리가 있고, 생성된 인스턴스들이 차지하는 공간이 있다.
  • 인스턴스들은 각각 만들어진 대로 따로따로 메모리를 차지한다.
  • 반면 클래스나 정적으로 만들어진 필드, 메서드들은 하나의 공간만을 차지한다.
  • 단, 메서드는 프로토타입 한 공간에 들어있다.
  • 정적메서드는 정적인 필드만 사용 할 수 있다. 인스턴스가 사용하는 자원은 접근 할 수 없다.

💡 클래스도 함수

class Dog {
  bark () {
    return '멍멍';
  }
}

console.log(typeof Dog); //function

const dog = Dog; // 할당될 수 있는 일급 객체
const pome = new dog();

console.log(pome);

접근자 프로퍼티와 은닉

접근자 프로퍼티

  • getter, setter함수라고도 부른다.
  • 스스로 값을 가지지 않고 다른 프로퍼티의 값을 읽거나(get) 저장(set) 할 때 사용하는 특수한 프로퍼티이다.
  • get, set을 앞에 붙여서 사용한다.
const person1 = {
  age: 17,

  get koreanAge () {
    return this.age + 1;
  },

  set koreanAge (krAge) {
    this.age = krAge - 1;
  }
}
  • 접근자 프로퍼티가 생긴 모습은 함수와 같지만 프로퍼티처럼 사용해야 한다. 함수를 호출하는 방법 ()사용을 사용하면 오류가 발생한다.
console.log(person1, person1.koreanAge); // {age: 17} 18

/*
age: 19
koreanAge: (...)
get koreanAge: ƒ koreanAge()
set koreanAge: ƒ koreanAge(krAge)
[[Prototype]]: Object
*/

person1.koreanAge = 20;
console.log(person1, person1.koreanAge); // {age: 19} 20

person1.koreanAge();
// ⛔Uncaught TypeError: person1.koreanAge is not a function

⭐ 클래스에서도 사용할 수 있다.

class Person {
  constructor (name, age) {
    this.name = name;
    this.age = age;
  }
  get info() {
    return `${this.name}, ${this.age}`;
  }
  set personName(newName) {
    if (typeof newName !== 'string') return;
    
    this.name = newName;
  }
}

const person1 = new Person('Tom',25);
console.log(person1.info); //Tom, 25

person1.personName = 'Mike';
console.log(person1); // Person {name: 'Mike', age: 25}


person1.personName = 1;
console.log(person1); // Person {name: 'Mike', age: 25} 
// 이름을 바꾸는데 숫자가 인자로 들어와서 아무일도 일어나지 않았다.
  • getter
    • 반드시 값을 반환해야 한다.
    • 어떤 값을 가공해서 내보낼 때, 그것을 마치 일반 프로퍼티처럼 사용하고 싶을 때 사용한다.
  • setter
    • 외부에서 객체에 값을 넣어줌
    • 특정 프로퍼티에 특정 값이 저장되는 방식을 조작하거나 제약을 걸어둘 때 사용한다.

⚠️ 주의할 점! 필드 이름과 setter의 이름이 같지 않도록 해야한다.

  • 다음 코드를 보자
class Person {
  constructor (name, age) {
    this.name = name;
    this.age = age;
  }
  get info() {
    return `${this.name}, ${this.age}`;
  }
  set name(newName) {
    if (typeof newName !== 'string') return;
    
    this.name = newName;
  }
}

const person1 = new Person('Tom',25);
//⛔ Uncaught RangeError: Maximum call stack size exceeded
  • 최대 스택 크기를 초과했다고 오류메시지가 발생한다. 이유가 무엇일까?
  1. 인스턴스를 생성 할 때 생성자 함수가 실행된다.
  2. 생성자 함수에서 this.name = name부분은 name에 값을 할당하는 것이기 때문에 setter를 호출하게 된다.
  3. setter안에서 this.name = name을 실행하게 되는데 이 코드 역시 setter를 호출하게 된다!!!!!
  4. setter가 자기자신을 무한 호출하게 된다.
  • setter와는 다른 필드명을 사용하여 자기반복 호출을 방지해야한다.
class Person {
  constructor (name, age) {
    this.name = name;
    this.age = age;
  }
  get info() {
    return `${this.name}, ${this.age}`;
  }
  set name(newName) {
    if (typeof newName !== 'string') return;
    
    this._name = newName;
  }
}

const person1 = new Person('Tom',25);
  • setter에서 this._name으로 설정해주면 생성자에서 호출한 this.name은 접근자 프로퍼티를 가리키게 되고, setter는 _name데이터 프로퍼티에 값을 저장하게 된다.
  • 인스턴스를 출력해보자
console.log(person1); //Person {_name: 'Tom', age: 25}
  • 그런데 이런 형태도 불안하다. 데이터를 감추려고 했는데, 만약 클래스에서 내부적으로 데이터가 어떤 식으로 관리되는지 안다면 person1._name과 같은 형식으로 접근 할 수 있어 오류 발생의 가능성이 있다.

은닉

  • 객체지향의 주요 요소 중 하나인 객체 내부에 값을 감추는 은닉을 사용해보자
  • 인스턴스의 프로퍼티 값을 함부로 열람하거나 수정하지 못하도록 만들어준다.
  • 자바스크립트의 필드는 기본적으로 public하며 외부에서 읽히고 수정될 수 있다.
class Person {
  //은닉 필드
  #name = ''; 
  #age = 0;
  
  constructor (name, age) {
    this.#name = name;
    this.#age = age;
  }
}

const person1 = new Person('김복동', 32);

console.log(person1.#name); //오류발생
  • 필드명 앞에 #을 붙여 은닉할 수 있다.
  • 단, 클래스 바로 안쪽에 정의해야한다. constructor에만 정의하면 제대로 은닉되지 않을 수 있다. #필드를 하나의 프로퍼티로 인식함.
  • private 필드는 클래스 내부에서만 접근 할 수 있다.

상속

  • 객체 지향의 핵심적인 요소!
  • 서로 다른 클래스나 생성자 함수가 같은 속성들을 공유 할 때 이들의 관계를 정의함으로써 코드의 중복을 줄이고 효율을 높인다.

...뭔소리야?

예를 들어 계산기를 보도록 하자.

계산기에는 여러가지 버튼이 있다. 버튼에는 숫자를 입력하는 버튼, 연산자 버튼, 계산 버튼 등 여러가지 종류의 버튼이 존재한다. 하지만 버튼의 모양, 클릭시 반응, 차지하는 공간 등 다른 종류의 버튼이지만 같은 속성을 지니고 있다.

그래서 숫자 버튼과 계산 버튼은 다른 Class에서 파생되지만 그 두 Class가 Button이라는 클래스에서 NumberButton, FunctionButton과 같이 파생된다고 생각할 수 있다.

NumberButton, FunctionButton은 Button 클래스에서 상속된 것이다.

클래스의 상속 문법

  • 본격적으로 자바스크립트에서 상속하는 방법을 알아보자.
class Bird {
  wings = 2;
}

class Eagle extends Bird {
  claws = 2;
}

class Penguin extends Bird {
  swim () { console.log('수영중...'); }
}

class EmperorPenguin extends Penguin {
  size = 'XXXL';
}

Bird class는 기본 값으로 2인 wings필드만을 가지고 있다.
(모든 새는 날개가 2개다.)

Eagle은 Bird에서 확장extends한다. 객체지향의 언어로 표현한다면 Eagle class는 Bird class에서 파생된 것이다.

Bird class를 부모 클래스, Eagle을 자식 클래스라고 부른다. 그리고 Eagle이라는 class는 claws라는 자신만의 필드값을 가지고 있다.
(독수리는 발톱이 있다.)

  • 각각 클래스의 인스턴스를 만들고 출력해보자
const birdy = new Bird();
const eaglee = new Eagle();
const pengu = new Penguin();
const pengdol = new EmperorPenguin();

console.log(birdy, eaglee, pengu, pengdol);
/* 
Bird {wings: 2} 
Eagle {wings: 2, claws: 2} 
Penguin {wings: 2} 
EmperorPenguin {wings: 2, size: 'XXXL'}
*/
  • 자식 클래스에 상속받은 부모 클래스의 데이터가 포함되어 있는 것을 확인 할 수 있다.

    • 자식 클래스는 부모 클래스의 속성을 기본적으로 가져온다.
  • 그런데 EmperorPenguin의 부모인 Penguin에서 상속받은 메서드는 어디에 있는 걸까?

  • EmperorPenguin의 데이터를 자세히 살펴보자

EmperorPenguin {wings: 2, size: 'XXXL'}
size: "XXXL"
wings: 2
[[Prototype]]: Penguin
  constructor: class EmperorPenguin
  [[Prototype]]: Bird
    constructor: class Penguin
    swim: ƒ swim()
    [[Prototype]]: Object
  • 부모 클래스의 메서드는 인스턴스의 부모 class 프로토타입으로 저장되어 있는 것을 확인 할 수 있다.

  • 자바스크립트 클래스에서는 상속마저 프로토타입을 기반으로한다.

  • 따라서 Penguin class에서 파생된 class는 수영을 할 수 있지만 다른 class에서 파생된 class는 수영기능이 없다.

(독수리는 수영을 못하니까)

만약 같은 클래스에서 상속을 받아도 서로 다른 기능을 가질 수 있다는 것을 의미한다.
표현력이 말도 안되게 상승한다.

오버라이딩

  • 상속으로 클래스를 파생시키면 부모 클래스의 데이터로부터 다른 정보를 더할 수 있을 뿐 아니라 부모 클래스의 기능을 변경 할 수 있다.

... ❓

  • 다음 코드를 보자
class Bird {
  wings = 2;
  canFly = true;
  travel () { console.log('비행중...') }
}

class Eagle extends Bird {
  claws = 2;
}

class Penguin extends Bird {
  canFly = false;
  travel () { console.log('수영중...') }
}

Bird Class는 두 개의 wings, 날 수 있는 능력, 비행 메서드를 가지고 있다.

Eagle class는 Bird class를 단순히 상속하지만 Penguin class는 다르다.

부모 클래스가 가지고 있는 canFly의 값을 False로 변경했다.
(독수리는 날 수 있지만 펭귄은 날 수 없으니까)

  • 이런식으로 자식 클래스에서 부모로부터 물려받은 속성이나 기능을 필요에 의해서 덮어쓸 수 있다. 즉 현재 class에 맞춤 기능을 재설정 할 수 있다는 것이다.

이것이 오버라이딩이다!

super

  • 만약 자식 클래스에서 부모 클래스의 기능을 접근하고 싶을 때는 super라는 기능을 사용하면 된다.

그런데 굳이 데이터를 더 많이 담고 있는 자식 클래스가 부모 클래스에 접근 할 이유가 있을까?

class Person{
  name = ''
  age = 0

  constructor(name, age){
    this.name = name;
    this.age = age;
  }

  introduce(){
    return `안녕하세요 ${this.age}세, ${this.name}입니다.`
  }
}

class White extends Person{
  height = '170';

  constructor(name, age, height){
    this.name = name;
    this.age = age;
    this.height = height;
  }

  whiteIntroduce(){
    return `안녕하세요 ${this.age}세, ${this.name}, ${this.height}cm입니다.`
  }
}

Person 클래스는 필드로 name과 age를 가지고 있고, White 클래스는 Person을 상속받아 height이라는 필드를 추가적으로 가지고 있다.

그런데 생성자와 메서드 부분을 보자. 같은 작업이 반복해서 일어나고 있다!!!! 개발자가 피해야 할 것들 중 하나인 불필요한 반복이 생겼다.
이러면 우리가 시간을 내어 객체지향을 공부한 이유가 사라진다.

부모의 기능을 이용해서 자식의 기능을 만드려면 어떻게 해야 할까?

다음과 같이 바꿔 볼 수 있다.

class White extends Person{
  height = '170';

  constructor(name, age, height){
    super(name, age);
    this.height = height;
  }

  whiteIntroduce(){
    return super.introduce() + `${this.height}cm입니다.`
  }
}
  • 자식 클래스 생성자 함수에 사용된 super는 부모 클래스의 constructor를 가리킨다.
    • 부모 클래스의 생성자 함수가 실행되어 White 클래스의 필드 값으로 설정된다.
  • 반면 자식 클래스 메서드 내에서의 super는 부모 클래스를 가리킨다.

(이해를 돕기 위한 사진일 뿐 정확한 메모리 구조와는 다르다.)

  • super는 부모 클래스의 생성자 함수나 메서드에 추가적인 동작을 넣기 위해 사용한다.
    • 부모 클래스와 자식 클래스의 기능을 한 번에 사용해야 할 경우 잘 사용하면 매우 유용할 것이다!!

0개의 댓글