프로토타입을 쉽게 사용하기 위한 sugar syntax로, 생성자 함수와 내부 구조는 같지만 선언하는 문법이 다르다. 어떻게 보면 생성자 함수를 좀 더 유연하게 작성할 수 있는 문법이 아닐까?
class Person {
getInfo() {}
}
const person = new Person();
person.getInfo();
function Person() {
this.getInfo = function() {}
}
const person = new Person();
person.getInfo();
Class와 생성자 함수의 구조를 비교해 보면, 인스턴스를 생성하는 방식은 같지만 문법적으로 차이가 있다. 언뜻 보기에도 Class가 더 간결하고 직관적인 문법을 제공한다.class Person {
// Person 객체의 인스턴스가 생성되었을 때 한 번 실행됨
constructor(name, age) { // 매개변수를 받아주기 위해 선언하는 함수
}
getInfo() { // 멤버 속성에 들어가 있지 않고 처음부터 프로토타입에 들어 있음
console.log();
}
}
const personC = new Person("영희", 30);
➡️ class에서 메서드를 만들면 자동으로 프로토타입에 추가되어 메모리를 효율적으로 사용하게 됩니다.
Class를 통한 상속이란 기존 클래스의 속성과 메서드를 새로운 클래스가 물려받아 사용하는 것을 뜻한다.
class Person {
constructor(name, age) {}
greet() {}
}
class Employee extends Person {
constructor(name, age, position) {
super(); // 부모의 name과 age를 사용하겠다.
this.name = name;
this.age = age;
this.position = position;
}
}
1️⃣
상속받을 class
와 extends
를 사용한다.
2️⃣
super
를 사용하여 부모의 constructor
를 참조한다.
3️⃣
부모의 greet()
메서드에 접근할 수 있게 된다.
➡️ Rabbit이 Animal class를 상속받았을때 위와 같은 흐름이 실행된다.
1️⃣
extends
를 사용하면 Rabbit(자식) 의 프로토타입이 Animal(부모) 의 프로토 타입을 접근할 수 있다.
2️⃣
객체 Rabbit에 원하는 메서드가 없습니다. 그럼 Rabbit의 프로토타입을 확인합니다.
3️⃣
Rabbit의 프로토타입에도 원하는 메서드가 없다면 extends 로 관계가 맺어진 Animal의 프로토타입에 있는지 확인합니다
정적 메서드를 사용하면 인스턴스를 생성하지 않고 메서드를 사용할 수 있다.
class Math {
static Pi = 3;
static add(n1, n2) { // static
return n1 + n2;
}
}
// const math = new Math(); 생성하면 오류 남
const sum = Math.add(10, 20);
console.log(Math.Pi);
1️⃣
정적으로 지정하고 싶은 요소에 static
을 작성한다.
2️⃣
인스턴스를 생성하지 않고 바로 호출해 변수에 할당한다.
➡️ Animal class의 정적 메소드를 Rabbit이 상속받았을때 위와 같은 흐름이 실행된다.
➡️ 정적메서드 에서의 상속
일반 메서드
는 부모 클래스의 프로토타입 -> 자식 클래스의 프로토타입 순으로 연결됩니다.
정적 메서드
는 부모 클래스 -> 자식 클래스 순으로 상속됩니다.
class Animal {}
class Rabbit extends Animal {}
// 정적 메서드
alert(Rabbit.__proto__ === Animal); // true
// 일반 메서드
alert(Rabbit.prototype.__proto__ === Animal.prototype); // true
__proto__
를 부모 클래스에 연결 한다.➡️ 데이터 형식 변환, 계산 등 특정 객체와 상관없는 메서드를 추가할 때, 특정 인스턴스와 무관한 공통 기능을 구현할 때 사용하기 좋다.
class MathUtil {
static add(a, b) {
return a + b;
}
}
console.log(MathUtil.add(5, 10)); // 15
➡️ 공유 값이나 설정이 필요한 경우, 모든 인스턴스가 공통으로 사용할 수 있는 기본 설정 값이나 규칙을 정의할 때 유용하다.
class AppConfig {
static defaultColor = "blue";
}
console.log(AppConfig.defaultColor); // "blue"
static 변수를 클래스에 두고 재활용할 때는 여러 인스턴스가 공유하는 특성 때문에 관리와 충돌 위험이 발생할 수 있다.
➡️ 공유 상태로 인해 발생하는 데이터 충돌
만약 인스턴스마다 다른 값을 가질 필요가 있는데 static 변수를 사용할 경우, 의도치 않게 모든 인스턴스가 같은 값을 가지게 되어 상태 관리가 어렵거나 버그가 발생할 수 있다.
class Counter {
// 모든 인스턴스와 공유 -> 변경 시 모든 인스턴스에 영향
static count = 0;
increment() {
Counter.count++;
}
}
const counter1 = new Counter();
const counter2 = new Counter();
counter1.increment();
console.log(Counter.count); // 1
counter2.increment();
console.log(Counter.count); // 2 (counter1도 영향을 받음)
➡️ 인스턴스로 독립적으로 구현하여 해결하기
class Counter {
constructor() {
this.count = 0; // 각 인스턴스마다 독립적인 count 속성
}
increment() {
this.count++;
}
getCount() {
return this.count;
}
}
const counter1 = new Counter();
counter1.increment();
console.log(counter1.getCount()); // 1
const counter2 = new Counter();
counter2.increment();
console.log(counter1.getCount()); // 1, 다른 인스턴스에 영향 없음
console.log(counter2.getCount()); // 1, 각 인스턴스가 독립적임
접근자 프로퍼티의 본질은 함수로, get은 값을 획득하고, set은 설정하는 역할을 담당한다.
class Car {
constructor(speed) {
this._speed = speed;
}
/*
아래 방법의 문제점:
오류 - Maximum call stack size exceeded -> 콜스택 사이즈 초과
이미 값이 -여서 set 값이 계속 바뀌려 해 오류 발생
해결법:
1. 초기값을 다른 이름으로 지정 (speed -> _speed)
*/
set speed(value) {
this.speed = value < 0 ? 0 : value;
}
get speed() {
return this._speed;
}
}
const car = new Car(200);
car.speed = -100;
console.log(car._speed);
/*
speed를 호출하기 위해 get 함수를 생성한다.
*/
console.log(car.speed);
프라이빗으로 값을 조작하면 외부에서 변경할 수 없다.
// 인스턴스로 speed를 변경할 수 없음
#speed = 0;
메서드를 재정의하는 현상이다.
class Animal {
sound() {
console.log("sound!");
}
}
class Dog extends Animal {
run() {
console.log("run");
}
sound() {
console.log("dog sound");
}
}
const dog = new Dog();
dog.run();
dog.sound(); // 오버라이딩에 의해 sound가 재정의되어 실행됨
Class는 중요할수록 마지막에 등장하는 개념인데, 그만큼 또 어렵다. 생성자 함수와 비교하며 얼마나 유연하게 동작하는지 아는 게 중요하며, 사용법을 익히려면 많이 문제를 찾아서 풀어봐야겠다.