4.1Data Structures Intro
Class 문법 기초
추억의 쪽지 시험
데이터 구조에는 다양한 것들이 있고, 각자의 필요가 있어 쓰인다. 이번 섹션에선 각각의 데이터 구조를 직접 자바스크립트 ES6 class
문법을 통해 구현한다.
class란?
A blueprint for creating objects with pre-defined properties and methods
class
는 객체를 생성할 수 있는 일종의 청사진, 붕어빵 틀이다. 공통 속성과 메소드를 정의한 틀을 만들고, 그 틀을 바탕으로 여러 객체 인스턴스를 만들 수 있다.
사실 자바스크립트는 Java, Python과 같은 클래스 기반 언어(class-based OOP) 가 아니라 프로토타입 기반 언어(prototype-based) 이다. class
문법은 ES6 이후 도입된 syntactic sugar, 즉 문법적으로 더 읽기 쉽도록 만든 표현일 뿐이며, 내부적으로는 여전히 프로토타입 체인을 기반으로 동작한다.
The Class Keyword
javascript// class명은 관례상 대문자로 시작 class Student { constructor(firstName, lastName){ this.firstName = firstName; this.lastName = lastName; } } const firstStudent = new Student("Sungyup", "Ju");
constructor
는 클래스 인스턴스를 생성할 때 자동으로 호출된다.this
는 인스턴스 자신을 가리킨다.
인스턴스 메소드
인스턴스마다 사용할 수 있는 메소드는 클래스 내부에 메소드로 정의한다.
javascriptclass Student { constructor(firstName, lastName, year){ this.firstName = firstName; this.lastName = lastName; this.tardies = 0; // 지각 횟수 } fullName(){ return `Your full name is ${this.firstName} ${this.lastName}.` } markLate(){ this.tardies += 1; if(this.tardies >= 3){ return "YOU ARE EXPELLED!" } return `${this.firstName} ${this.lastName} has been late ${this.tardies} time(s).` } } const firstStudent = new Student("Sungyup", "Ju"); firstStudent.fullName(); // Your full name is Sungyup Ju. firstName.markLate(); // Sungyup Ju has been late 1 time(s).
- 모든 인스턴스는 같은
fullName()
과markLate()
메소드를 공유한다. - 실제, 내부적으로는
Student.prototype.fullName
과 같은 형태로 정의된다.
클래스 메소드
때로는 특정 기능이 개별 인스턴스에 속할 필요 없이, 클래스 그 자체에 속하는 것이 더 적절한 경우가 있다. static
키워드로 각 인스턴스가 아닌, 클래스 자체에 메소드를 추가할 수 있다.
예를 들어, 아래의 Point 클래스에서 두 점 사이의 거리를 구하는 메소드는 각 인스턴스의 메소드로 등록할 수는 없다. 이런 유틸리티 함수나 공통 계산 함수 등을 구현할 때 클래스 자체에 static 메소드를 추가한다.
javascriptclass Point { constructor(x, y){ this.x = x; this.y = y; } static distance(a, b){ const dx = a.x - b.x; const dy = a.y - b.y; return Math.hypot(dx, dy); // 빗변의 길이(hypotenuse)를 구함 } } const p1 = new Point(5, 4); const p2 = new Point(1, 3); Point.distance(p1, p2);
🤠 개인 탐구
클래스 관련 기본 사항들을 짚고 넘어가는 김에 다른 문법들도 복습하고 가자.
상속과 super: extends
와 super
클래스는 다른 클래스를 상속할 수 있다. 즉, 기존 클래스(부모 클래스)의 기능을 재사용하고 확장할 수 있다.
상속을 위해선 extends
키워드를 사용하고, 생성자(constructor) 안에서 부모 클래스의 생성자를 호출하기 위해 super()
를 사용한다.
javascriptclass Person { constructor(name){ this.name = name; } greet(){ return `Hello, I'm ${this.name}`; } } class Students extends Person { constructor (name, grade){ super(name); // 부모(Person) constructor 호출 this.grade = grade; // 확장 } }
extends
는 부모 클래스의 모든 속성과 메소드를 자식 클래스에 물려준다.super()
는 부모의 constructor를 호출해 초기화 작업을 수행한다.- 부모 클래스의 메소드도 모두 그대로 사용 가능한데, 덮어쓸 수도 있다.
접근 제어: getter
와 setter
속성처럼 보이지만 계산을 필요로 하는 경우, getter
를 통해 계산하는 함수를 작성하고 속성에 접근할 수 있도록 한다. 또, 객체의 값을 바꿀 때는 직접 덮어쓰는 것보다 안전하게 setter
를 활용해 유효성 검사를 실행할 수 있다.
javascriptclass Rectangle { constructor(width, height){ // 내부에서만 사용하는 변수에는 관례상 _를 붙임 this._width = width; this._height = height; } // 게터 : 속성처럼 보이지만 사실 계산을 통해 나오는 값 get area(){ return this._width * this._height; } // 세터: 값을 덮어 쓸 때 set width(value){ if(value <= 0) throw new Error('Width must be positive!'); this._width = value; } set height(value){ if(value <= 0) throw new Error('Height must be positive!'); this._height_ = value; } } const rect = new Rectangle(10, 5); rect.area; // 50. 속성에 접근하는것 같지만 계산된 값 rect.width = 20; // 덮어씌우는 것 같지만 세터로 변경됨 rect.area; //100
Private: 외부에서 접근 못하게 막기
#
를 통해 private 필드를 선언할 수 있다.(ES2022)
API를 설계할 때 사용자가 건드리면 안 되는 속성이나 로직을 감출 때(비밀번호, 캐시, 내부 계산 결과 등) 사용한다.
javascriptclass Counter{ #count = 0; increment(){ this.#count++; return this.#count; } get value(){ return this.#count; } } const c = new Counter(); c.increment(); //1 c.value; // 1 c.#count; // SyntaxError: Private field must be declared in an enclosing class
참고로, TypeScript의 private
는 컴파일 단계에서 접근 제한을 적용해주지만, 실제 런타임에서 보호해주지는 않는다. JS의 #
으로 보호해야 한다.
그래서, 왜 class를 사용할까?
모든 인스턴스들이 메소드를 공유한다는 것은 메소드들이 프로토타입에 올라가고, 모든 인스턴스마다 메소드를 복사하지 않는다는 것을 의미한다.
즉, __proto__
나 Object.create()
를 통해 프로토타입 체인을 구성하기 때문에 메모리 구조가 효율적이다.
javascriptclass A { greet() { return "hello"; } } const a = new A(); const b = new A(); a.greet(); // 실제로는 A.prototype.greet() b.greet(); // 실제로는 A.prototype.greet()