티스토리 뷰
[JavaScript | ES6] Class (this, public & private 필드, 상속)
blueprint-12 2023. 7. 18. 22:28Class란?
class는 객체를 생성하기 위한 템플릿
클래스는 데이터와 이를 조작하는 코드를 하나로 추상화한다.
자바스크립트에서 클래스는 프로토타입을 이용해서 만들어졌지만 ES5의 클래스 의미와는 다른문법과 의미를 가진다.
1. Class 정의
Class는 "특별한 함수"이다. 함수를 표현식과 선언문으로 정의할 수 있듯 클래스 문법도 class 표현식과 class 선언 두 가지 방법을 제공한다.
- 클래스는 재정의될 수 없다. 재정의를 시도하면 SyntaxError가 발생한다.
1-1. Class 선언
Class 를 정의하는 한 가지 방법은 class 선언을 이용하는 것
class를 선언하기 위해서는 클리스의 이름과 함께 class 키워드를 사용한다.
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
- class 클래스명(위의 예시에서 Rectangle)
🤔함수 선언과 클래스 선언의 차이점 ( Hoisting )
- 함수의 경우 정의하기 전에 호출이 가능하지만 클래스는 반드시 정의한 뒤에 사용할 수 있다는 점이다. => new 키워드를 통해 class를 클래스 선언 이전에 사용하려하면 ReferenceError가 발생한다.
2-1. Class 표현식
Class 표현식은 class를 정의하는 또 다른 방법
- Class 표현식은 이름을 가질 수도 있고, 갖지 않을 수도 있다.
- 이름을 가진 class 표현식의 이름은 클래스 body의 local scope에 한해 유효하다. (하지만, 클래스의(인스턴스 이름이 아닌) `name` 속성을 통해 찾을 수 있다.
- 클래스 선언과 동일하게 호이스팅 제한이 있다. (선언부보다 먼저 사용하면 ReferenceError 발생)
// unnamed
let Rectangle = class { // class의 우측에 클래스명이 없다. let옆의 변수명도 대문자로 시작한다.
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name);
// 출력: "Rectangle"
// named
let Rectangle = class Rectangle2 {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name);
// 출력: "Rectangle2"
3. Class body( 클래스 본문 )
- class body는 중괄호 {} 로 묶여 있는 안쪽 부분이다.
- 우리가 메서드나 constructor와 같은 class 멤버를 정의할 곳이다.
- 클래스의 body는 strict mode에서 실행된다. => 여기에 적힌 코드는 성능 향상을 위해 더 엄격한 문법이 적용된다.
3-1. Constructor ( 생성자 )
- constructor 메서드는 class 로 생성된 객체를 생성하고 초기화하기 위한 특수한 메서드
- 이 메서드는 클래스 안에 한 개만 존재할 수 있다. => 여러 개의 constructor 메서드가 존재하면 SyntaxError 발생
- constructor는 부모 클래스의 constructor를 호출하기 위해 `super 키워드`를 사용할 수 있다.
3-2. 프로토타입 메서드
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
// Getter
get area() {
return this.calcArea();
}
// 메서드
calcArea() {
return this.height * this.width;
}
}
const square = new Rectangle(10, 10);
console.log(square.area); // 100
3-3. 정적 메서드와 속성
- static 키워드는 클래스를 위한 정적(static) 메서드를 정의한다. 정적 메서드는 클래스의 인스턴스화(instantiating) 없이 호출되며, 클래스의 인스턴스에서는 호출할 수 없다.
- 정적 메서드는 어플리케이션을 위한 유틸리티(utility)함수를 생성하는데 주로 사용된다.
- 반면, 정적 속성은 캐시, 고정 환경설정 또는 인스턴스 간에 복제할 필요가 없는 기타 데이터에 유용
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static displayName = "Point";
static distance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.hypot(dx, dy);
}
}
const p1 = new Point(5, 5);
const p2 = new Point(10, 10);
p1.displayName; // undefined
p1.distance; // undefined
p2.displayName; // undefined
p2.distance; // undefined
console.log(Point.displayName); // "Point"
console.log(Point.distance(p1, p2)); // 7.0710678118654755
- 예시를 보면 알겠지만, 클래스 인스턴스인 p1을 통해서는 static 속성 및 메서드가 호출되지 않고 undefined를 반환한다. 클래스명.정적메서드를 통해서 호출된다.
3-4. 프로토타입 및 정적 메서드를 사용한 this 바인딩
메서드를 변수에 할당한 다음 호출하는 것과 같아, 정적 메서드나 프로토타입 메서드가 this 값 없이 호출될 때, this 값은 메서드 안에서 undefined가 된다.
이 동작은 "use strict" 명령어 없이도 같은 방식으로 동작하는데, 위에서 말했듯 class의 body가 strict mode로 실행되기 때문이다.
class Animal {
speak() {
return this;
}
static eat() {
return this;
}
}
let obj = new Animal();
obj.speak(); // the Animal object
let speak = obj.speak;
speak(); // undefined
Animal.eat() // class Animal
let eat = Animal.eat;
eat(); // undefined
// ======== 아래는 전통 함수 방식 non-strict-mode
function Animal() { }
Animal.prototype.speak = function() {
return this;
}
Animal.eat = function() {
return this;
}
let obj = new Animal();
let speak = obj.speak;
speak(); // global object (in non–strict mode)
let eat = Animal.eat;
eat(); // global object (in non-strict mode)
- 클래스 코드를 전통적인 방식의 함수 기반 non-strict-mode 구문으로 재작성하면, this 메서드 호출은 기본적으로 전역 객체인 초기 this값에 자동으로 바인딩된다.
- 하지만, class 문법 내부이므로 strict mode에서는 자동 바인딩이 발생하지 않는다. this 값은 전달된 대로 유지된다.
🤸♀️ 클래스 내부 this 값 추가 정리
클래스 필드로 바인딩된 메서드 만들기
JS에서 `this`는 동적으로 결정된다.
따라서 객체 메서드를 여기저기 전달해 전혀 다른 컨텍스트에서 호출하게 되면 this는 메서드가 정의된 객체를 참조하지 않는다.
class Button {
constructor(val) {
this.val = val;
}
click() {
alert(this.val);
}
}
let button = new Button('방가미');
setTimeout(button.click, 1000); // ReferenceError(alert is not defined)
- 위처럼 this의 컨텍스트를 알 수 없게 되는 문제를 losing this(잃어버린 this)라고 한다.
문제는 두 방법을 사용해 해결이 가능하다.
- setTimeout(()=> button.click(), 1000) 와 같이 래퍼 함수 전달
- 생성자 안 등에서 메서드를 객체에 바인딩하기
위의 방법 외에도 클래스 필드를 사용해서 문제를 해결할 수 있다.
class Button {
constructor(val) {
this.val = val;
}
click = () => {
alert(this.val);
};
}
let button = new Button('방가미');
setTimeout(button.click, 1000);
- 클래스 필드 click = () => {...} 는 각 Button이 만든 인스턴스 객체마다 독립적인 함수를 만들어주고 이 함수의 this를 해당 객체에 바인딩시켜준다. 따라서 button.click을 아무 곳에나 전달할 수 있다.
- 하지만 클래스를 활용하는 것보단 화살표함수를 사용해서 생성된 컨택스트레 this 를 바인딩하는 것 같은데 단점도 분명히 존재한다. 화살표 함수는 기본적으로 prototype을 생성하지 않기 때문에 class명.prototype으로 접근이 불가능하다. + 프로토타입이 없으니 상속도 안된다. 그렇기 때문에 명확한 의도가 없다면 가급적 화살표 함수를 class 내에서 사용하는 것을 지양하는 것이 좋다는 의견도 있다. (참고 자료: 코어자바스크립트 댓글)
3-5. 인스턴스 속성
인스턴스 속성은 반드시 클래스 메서드 내에 정의되어야 한다.
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
정적(클래스 사이드) 속성과 프로토타입 데이터 속성은 반드시 클래스 선언부 바깥 쪽에 정의되어야 한다.
Rectangle.staticWidth = 20;
Rectangle.prototype.prototypeWidth = 25;
3-6. Field 선언
🔺 public 과 private 필드 선언은 JS 표준화 위원회에 실험적 기능(stage 3) TC39로 제안되어 있다. 현재 이를 지원하는 브라우저가 제한적인 상태이지만 Babel과 같은 build 시스템을 사용하면 사용할 수 있다.
p.s. TS사용하면 무조건 사용가능한 거 아닌가?!
3-6-1. Public 필드 선언
class Rectangle {
height = 0;
width;
constructor(height, width) {
this.height = height;
this.width = width;
}
}
- 필드를 먼저 선언함으로서 클래스 정의는 self-documenting에 가까워졌고 필드는 언제나 존재하는 상태가 된다.
- 필드 선언은 기본 값과 같이 선언될 수도 있다.
3-6-2. Private 필드 선언
class Rectangle {
#height = 0; //# 이름(해쉬 이름) 즉, prefix를 가진 식별자로 선언된다.
#width;
constructor(height, width) {
this.#height = height; // 해쉬는 이름 자체의 일부이며 선언과 접근 시에 모두 사용된다.
this.#width = width;
}
}
- 클래스의 바깥에서 private 필드를 접근하려고 하면 에러 발생 => scope 밖에서 #이름에 접근하는 경우 syntax error
- private 필드는 클래스 내부에서만 읽고 쓰기가 가능
- 클래스 외부에서 보이지 않도록 정의하였으므로 클래스가 버전업되면서 내부 구현이 바뀌더라도 클래스 사용자 입장에서는 이에 아무런 영향을 받지 않음
- private 필드는 사용 전에 선언되어야 한다.
- 일반적인 프로퍼티와 다르게 private 필드는 값을 할당하면서 만들어질 수 없다.
참고로 private 인스턴스 메서드는 private 인스턴스 필드와는 다르게 class 인스턴스로부터 접근이 가능하다.
class ClassWithPrivateMethod {
#privateMethod() {
return 'hello world'
}
getPrivateMessage() {
return this.#privateMethod()
}
}
const instance = new ClassWithPrivateMethod()
console.log(instance.getPrivateMessage())
// expected output: "hello world"
- private 인스턴스 메서드는 generator, async 그리고 async generator 함수가 될 수 있다. private getter와 setter 또한 가능하다. 아래는 그 예시 코드
class ClassWithPrivateAccessor {
#message
get #decoratedMessage() {
return `✨${this.#message}✨`
}
set #decoratedMessage(msg) {
this.#message = msg
}
constructor() {
this.#decoratedMessage = 'hello world'
console.log(this.#decoratedMessage)
}
}
new ClassWithPrivateAccessor();
// expected output: "✨hello world✨"
4. Public class fileds
퍼블릭 필드는 쓰기 가능(writable), 열거 가능(enumerable), 구성 가능(configurable)한 속성이다. 따라서 비공개 필드와 달리 프로토 타입 상속에도 참여한다.
4-1. 퍼블랙 클래스 필드 문법
class ClassWithField {
instanceField;
instanceFieldWithInitializer = "instance field";
static staticField;
static staticFieldWithInitializer = "static field";
}
문법 제한 사항
- 정적 property(필드 혹은 메서드)의 이름이 "prototype" 이 될 수 없다.
- 클래스 필드( static 혹은 인스턴스)의 이름이 constructor 가 될 수 없다. (클래스에는 단 1개의 생성자 함수만 존재해야 하므로)
const PREFIX = "prefix";
class ClassWithField {
field;
fieldWithInitializer = "instance field";
[`${PREFIX}Field`] = "prefixed field";
}
const instance = new ClassWithField();
console.log(Object.hasOwn(instance, "field")); // true
console.log(instance.field); // undefined
console.log(instance.fieldWithInitializer); // "instance field"
console.log(instance.prefixField); // "prefixed field"
- 퍼블랙 인스턴스 필드는 클래스로 생성된 모든 인스턴스에 존재한다. 퍼블릭 필드를 선언함으로써 어디든 존재한다는 것을 보증할 수 있다.
- 퍼블릭 인스턴스 필드는 베이스 클래스의 생성 시(생성자 본문이 실행되기 전) 또는 서브 클래스에서 super()가 반환된 직후에 인스턴스에 추가된다. 이니셜라이저가 없는 필드는 정의되지 않은 상태로 초기화된다(undefined 반환). 프로퍼티와 마찬가지로 필드 이름은 계산될 수 있다. => 계산된 프로퍼티 문법 사용가능
계산된 필드 이름은 class 정의 시간에만 한번 평가 된다. 이 뜻은 각각의 클래스가 항상 고정된 필드 명들을 갖는다는 의미이다. 또한, 두 인스턴스들이 계산된 이름으로 다른 필드 명을 가질 수 없다.
class C {
[Math.random()] = 1;
}
console.log(new C());
console.log(new C());
// Both instances have the same field name
필드 이니셜라이저(initializer)에서 `this` 키워드는 생성중인 클래스 인스턴스를 참조하고, `super`는 base가 되는 클래스의 인스턴스 메서드는 포함하지만 인스턴스 필드는 포함하지 않는 base 클래스의 `prototype` 프로퍼티를 참조한다.
lass Base {
baseField = "base field";
anotherBaseField = this.baseField;
baseMethod() {
return "base method output";
}
}
class Derived extends Base {
subField = super.baseMethod();
}
const base = new Base();
const sub = new Derived();
console.log(base.anotherBaseField); // "base field"
console.log(sub.subField); // "base method output"
new 키워드로 초기화되어 생성된 인스턴스들의 this 키워드는 각각의 인스턴스를 가리키게 된다.
class C {
obj = {};
}
const instance1 = new C();
const instance2 = new C();
console.log(instance1.obj === instance2.obj); // false
표현식은 동기적으로 평가된다. => `await` 또는 `yield`를 초기화 표현식에 사용할 수 없다.
클래스의 인스턴스 필드는 각 생성자가 실행되기 전에 추가되므로 생성자 내에서 필드 값에 엑세스할 수 있다. 그러나 파생 클래스의 인스턴스 필드는 super() 메서드 반환 후에 정의되므로 base 클래스의 생성자는 파생 클래스의 필드에 엑세스 할 수 없다.
class Base {
constructor() {
console.log("Base constructor:", this.field);
}
}
class Derived extends Base {
field = 1;
constructor() {
super();
console.log("Derived constructor:", this.field);
this.field = 2;
}
}
const instance = new Derived();
// Base constructor: undefined
// Derived constructor: 1
console.log(instance.field); // 2
😈 필드는 하나씩 추가되기 때문에 이 점을 고려해서 필드를 작성해야 한다. (모든 인스턴와 정적 메서드는 상관x)
class C {
a = 1;
b = this.c;
c = this.a + 1;
d = this.c + 1;
}
const instance = new C();
console.log(instance.d); // 3
console.log(instance.b); // undefined
- 필드가 하나씩 추가된다는 소리는 순서에 영향을 받는다는 말 즉, 위의 값이 할당되기 전에 할당되지않은 값의 변수에 접근하여 연산하게 된다면 instance.b 처럼 undefine 라는 결과가 나올 수 있다.
- b는 this.c의 값을 사용해야하는데 c는 b의 위쪽이 아니라 아래에 선언된 변수이기 때문임
- 참고: 위와 같은 케이스는 private 필드에서 더 중요하다 => 초기화되지않은 private 필드에 접근할 경우 TypeError를 던지기 때문이다(private 필드가 아래에 선언되었을 경우에도 마찬가지, 만일 선언조차되어있지 않다면 SyntaxError를 던진다).
클래스 필드 사용하기
Note: 클래스 필드 사양이 [[DefineOwnProperty]] 시멘틱으로 확장되기 전에는 Babel과 tsc를 포함한 대부분의 트랜스파일러가 클래스 필드를 DerivedWithContsructor 형식으로 변환하였는데, 클래스 필드가 표준화된 후 미묘한 버그가 발생하였다.
클래스 필드는 생성자의 인수에 의존할 수 없으므로 필드 이니셜라이저는 일반적으로 각 인스턴스에 대해 동일한 값으로 평가된다. ( Date.now()나 객체 이니셜라이저처럼 동일한 표현식이 매번 다른 값으로 평가될 수 있는 경우는 제외 )
class Person {
name = nameArg; // nameArg는 생성자함수 스코프 밖에 있기 때문에 x
constructor(nameArg) {}
} // BAD
class Person {
// 모든 클래스 Person의 인스턴스는 같은 name 값을 갖는다.
name: "DRAGON"; //GOOD
}
- 하지만 빈(empty) 클래스 필드를 작성하는 것은 코드를 읽는 사람으로 하여금 이 클래스 필드가 있다는 것을 알려주기 때문에 의미가 있다. => 타입스크립트 같네용
- 아래와 같이 작성하여 name, age 라는 클래스 필드가 있음을 명시적으로 알려준다.
class Person {
name;
age;
constructor(name, age) {
this.name = name;
this.age = age;
}
}
반복되는 코드로 보일 수 있으나 `this`의 값이 동적으로 변경될 때를 고려해야 한다. 명시적인 클래스 필드 선언을 통해 인스턴스에 어떤 필드가 확실히 존재할지 명확하게 알 수 있다.
class Person {
name;
age;
constructor(properties) {
Object.assign(this, properties);
}
}
Base 클래스가 실행된 후에 평가되므로 Base 클래스 생성자가 생성한 프로퍼티에 엑세스할 수 있다.
class Person {
name;
age;
constructor(name, age) {
this.name = name;
this.age = age;
}
}
class Professor extends Person {
name = `Professor ${this.name}`;
}
console.log(new Professor("Radev", 54).name); // "Professor Radev"
- 여기서 Base 클래스는 Person 클래스이다. 생성자에 name과 age 클래스 필드가 존재하고 Professor가 Person에 명시된 클래스 인스턴스 변수 name에 재할당을 하고 있다.
extends 키워드를 통한 클래스 상속(sub classing)
extends 키워드는 클래스 선언이나 클래스 표현식에서 다른 클래스의 자식 클래스를 생성하기 위해 사용
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name) {
super(name); // super class 생성자를 호출하여 name 매개변수 전달
}
speak() {
console.log(`${this.name} barks.`);
}
}
let d = new Dog('Mitzie');
d.speak(); // Mitzie barks.
- subclass 에서 constructor가 있다면, this 를 사용하기 전에 가장 먼저 super()를 호출해야 한다.
🔺 서브 클래스에서 항상 super를 호출하라는 것은 아니다. 클래스 상속에서 super()를 호출해야하는 경우는 크게 2가지로 1. 부모 클래스의 생성자를 호출하여 초기화해야하는 경우와 2. 자식 클래스에서 부모 클래스의 메서드를 오버라이딩하는 경우가 있다.
또한, es5에서 사용되던 전통적인 함수 기반의 클래스를 통하여 확장할 수도 있다. (class 키워드말고 function 키워드)
function Animal (name) {
this.name = name;
}
Animal.prototype.speak = function () {
console.log(`${this.name} makes a noise.`);
}
class Dog extends Animal {
speak() {
console.log(`${this.name} barks.`);
}
}
let d = new Dog('Mitzie');
d.speak(); // Mitzie barks.
// 유사한 메서드의 경우, 자식의 메서드가 부모의 메서드보다 우선합니다
클래스는 생성자가 없는 객체(non-constructible)를 확장할 수 없다. 만약 기존의 생성자가 없는 객체를 확장하고 싶다면, Object.setPrototypeOf() 메서드를 사용하자.
const Animal = { //생성자가 없는 객체
speak() {
console.log(`${this.name} makes a noise.`);
}
};
class Dog {
constructor(name) {
this.name = name;
}
}
// 이 작업을 수행하지 않으면 speak를 호출할 때 TypeError가 발생합니다
Object.setPrototypeOf(Dog.prototype, Animal);
// Object.setPrototypeOf 메서드를 통해서 Animal을 Dog 클래스에 확장
let d = new Dog('Mitzie');
d.speak(); // Mitzie makes a noise.
super 를 통한 상위 클래스 호출
super 키워드는 객체의 부모가 가지고 있는 메서드를 호출하기 위해 사용된다. => 이는 프로토타입 기반 상속보다 좋은 점 중 하나이다.
class Cat {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Lion extends Cat {
speak() {
super.speak(); //부모 클래스인 Cat의 speak 메서드 사용
console.log(`${this.name} roars.`);
}
}
let l = new Lion('Fuzzy');
l.speak();
// Fuzzy makes a noise.
// Fuzzy roars.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields
'Frontend > JavaScript' 카테고리의 다른 글
[JavaScript] 클로저와 클로저의 활용(with IIFE) (0) | 2023.08.03 |
---|---|
[JavaScript | ES6 ] get(접근자), set(설정자), 프로퍼티 플래그와 설명자 (0) | 2023.07.24 |
[JavaScript] Array.from + selectedOptions 으로 선택된 요소의 값만 담은 배열반환하기 (0) | 2023.05.11 |
[JavaScript] 이중부정 연산자(!!) (0) | 2023.04.17 |
[node.js] process.env 환경변수 설정과 dotenv 라이브러리 (0) | 2023.04.13 |
- Total
- Today
- Yesterday
- 타입스크립트 DT
- nvm 설치순서
- 타입스크립트 장점
- Prittier
- grid flex
- 원티드 FE 프리온보딩 챌린지
- nvm경로 오류
- 부트캠프항해
- getServerSideProps
- reactAPI
- aspect-ratio
- && 셸 명령어
- text input pattern
- 프리렌더링확인법
- 형제 요소 선택자
- 틸드와 캐럿
- 원티드 프리온보딩 FE 챌린지
- float 레이아웃
- getStaticPaths
- 프리온보딩 프론트엔드 챌린지 3월
- 항해99프론트후기
- D 플래그
- is()
- tilde caret
- 원티드 프리온보딩 프론트엔드 챌린지 3일차
- ~ ^
- 원티드 3월 프론트엔드 챌린지
- fs모듈 넥스트
- 항해99프론트
- 항해99추천비추천
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |