
인트로
안녕하세요 오늘은 어제 DOM 때문에 못했던 클래스 공부를 해보려고 합니다.
또 속을 수 없어서 페이지 수부터 확인했습니다. 짧진 않지만 DOM만큼의 빌런은 아니었습니다.
처음부터 프로토타입이랑 비교하면서 얘기하는데 프로토타입 공부는 이미 했기 때문에 넘어가려고 합니다.
프로토타입 내가 해냄
인트로프로토타입을 읽을 때는 노션에 살짝 쿵 정리하고 넘어갔는데 마침 부트캠프에서 프로토타입 내용을 다뤄 블로그에 써보려고 합니다. 프로토타입 교체 부분에서 머리를 한번 깬 경험이
i-did-it.tistory.com
클래스와 생성자 함수의 차이
클래스는 new 연산자 없이 호출하면 에러가 발생합니다.
생성자 함수의 경우 new 없이 호출하면 일반함수처럼 호출됩니다.
따라서 이를 막기 위해 new.target으로 제어를 해줬습니다.
생성자 함수에서 지원하지 않는 extends와 super 키워드를 제공합니다.
클래스는 let과 const처럼 호이스팅이 발생하지 않는 "척"합니다.
클래스의 모든 코드에는 암묵적으로 strict mode가 지정되어 실행되며 해제할 수 없습니다.
클래스의 constructor, 프로토타입 메서드, 정적메서드는 모두 enumerable 값이 false인데요
이것은 열거가 안되기 때문에 for ...in 반복문 사용이 불가능하다는 걸 의미합니다.
클래스 정의
클래스는 다음과 같이 정의할 수 있습니다.
이때 클래스의 이름은 파스칼 케이스를 사용하는 것이 일반적입니다.
//클래스 선언문
class Person {};
//익명 클래스 표현식
const Person = class {};
//기명 클래스 표현식
const Person = class MyClass {};
클래스는 일급 객체이기 때문에 일급 객체의 특징을 가지고 있습니다.
- 변수나 자료구조에 저장
- 런타임에 생성 가능
- 함수의 매개변수에게 전달
- 함수의 반환값으로 사용
- 동적으로 프로퍼티 추가 제거 가능
클래스의 메서드
클레스에서 정의할 수 있는 메서드는 다음과 같습니다
- constructor (생성자)
- 프로토타입 메서드
- 정적 메서드
class Person {
constructor(name) {
//인스턴스 생성 및 초기화
this.name = name; //public
}
//프로토타입 메서드
sayHi() {
console.log(`Hi I am ${this.name}`);
}
//정적메서드
static sayHello() {
console.log("Hello!");
}
};
const me = new Person('yangpa');
console.log(me.name);// yangpa
me.sayHi(); // Hi I am yangpa
Person.sayHello(); // Hello!
me.sayHello(); // error
호이스팅 되는 척하는 클래스
클래스는 함수로 평가됩니다.
따라서 클래스 선언문으로 정의한 클래스는 런타임 이전에 먼저 평가되어 함수 객체를 생성합니다.
이때 클래스가 평가되어 생성된 함수 객체는 constructor입니다. 생성하는 시점에 프로토타입도 함께 생성됩니다.
클래스는 호이스팅하는 "척"하기 때문에 클래스 정의 이전에 참조가 불가능합니다.
let, const처럼 선언문 이전에 일시적 사각지대(TDZ)에 빠지기 때문입니다.
(TDZ란?)
저는 TDZ를 선언 단계와 초기화 단계 사이에 머물러있는 시점으로 보고 있습니다.
let과 const로 선언한 변수를 아래 예제처럼 실행하려고 하면 에러가 뜨는데요.
변수 초기화되기 전에는 참조할 수 없기 때문입니다.
console.log(a); //Cannot access 'a' before initialization
let a;
애초에 선언을 안하면 다음과 같은 에러가 떠요.
console.log(a); //a is not defined
어쨋든 TDZ에 빠지기 때문에 호이스팅이 발생하지 않는 척하는 겁니다.
인스턴스 생성해보기
클래스는 new 연산자를 사용해 인스턴스를 생성합니다
class Person {}
const me = Person();
//Class constructor Person cannot be invoked without 'new'
오히려 반가운 에러
class Person {}
const me = new Person();
이렇게 인스턴스를 생성할 수 있었습니다.
참고로 기명 클래스 표현식을 썼다고 클래스 이름(MyClass)으로 인스턴스 생성하면 에러가 뜹니다.
식별자(Person)를 사용해야해요.
const Person = class MyClass {}
const me = new MyClass(); //MyClass is not defined
메서드
constructor
인스턴스를 생성하고 초기화하기 위한 특수 메서드입니다. constructor는 이름을 변경할 수 없습니다.
constructor 내부에서 this에 추가한 프로퍼티는 인스턴스 프로퍼티가 됩니다.
즉 constructor 내부의 this는 클래스가 생성한 인스턴스를 가리킵니다.
constructor는 생략할 수 있으며 클래스 내에서 최대 한 개만 존재할 수 있다는 특징을 가집니다.
클래스가 호출되면 암묵적으로 this, 즉 인스턴스를 반환하기 때문에 별도의 반환문을 가지면 안됩니다.
class Person {
constructor(name) {
this.name = name;
return {};
}
}
const me = new Person('yangpa');
console.log(me); // {}
class Person2 {
constructor(name) {
this.name = name;
}
}
const me2 = new Person2('yangpa');
console.log(me2); // Person2 { name: 'yangpa' }
생략할 경우 클래스에는 빈 constructor가 암묵적으로 정의되고 빈 constructor에 의해 빈 객체를 생성합니다.
초기화된 인스턴스를 생성하려면 constructor내부에서 this에 인스턴스 프로퍼티를 추가하면 됩니다.
인스턴스를 생성할 때 외부에서 인스턴스 초기값을 전달하려면 constructor에 매개변수를 선언하고 초기값을 전달하면 되죠.
인스턴스를 초기화하려면 constructor를 생략해서는 안됩니다.
프로토타입 메서드
클래스에 정의한 메서드는 클래스의 prototype 프로퍼티에 메서드를 추가하지 않아도 기본적으로 프로토타입 메서드가 됩니다. 예제를 확인해봅시다.
//생성자함수
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function () {
console.log(`Hi ${this.name}`);
}
const me = new Person('yangpa');
me.sayHi(); //Hi yangpa
//클래스
class Person2 {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hi ${this.name}`);
}
}
const me2 = new Person2('yangpa2');
me2.sayHi(); //Hi yangpa2
클래스에서 정의한 메서드는 인스턴스의 프로토타입에 존재하는 프로토타입 메서드가 됩니다.
인스턴스는 프로토타입 메서드를 상속받아 사용할 수 있습니다.
정적 메서드
정적 메서드는 인스턴스를 생성하지 않아도 호출할 수 있는 메서드입니다.
클래스에서는 메서드에 static 키워드를 붙이면 정적 메서드(클래스 메서드)가 됩니다.
정적 메서드는 클래스에 바인딩된 메서드가 됩니다.
정적 메서드는 인스턴스의 프로토타입 체인상에 존재하지 않기 때문에 인스턴스가 상속받지 못합니다.
호출할 수 없습니다.
프로토타입 메서드와 정적 메서드 차이
속해 있는 프로토타입 체인이 다릅니다.
정적 메서드는 클래스로 호출하고 프로토타입 메서드는 인스턴스로 호출합니다.
정적 메서드는 인스턴스 프로퍼티를 참조할 수 없지만 프로토타입 메서드는 인스턴스 프로퍼티를 참조할 수 있습니다.
class Person {
constructor(name) {
this.name = name;
}
static sayMyName() {
return this.name
}
sayName() {
return this.name
}
}
console.log(Person.sayMyName()); //Person
const me = new Person('pa');
console.log(me.sayName()); //pa
프로토타입 메서드와 정적 메서드 내부의 this 바인딩은 다르다는 걸 기억하세요.
메서드 내부에서 this를 사용하지 않더라도 프로토타입 메서드로 정의할 수 있지만 인스턴스로 호출해야하므로
this를 사용하지 않는 메서드는 정적 메서드로 정의하는 게 좋습니다.
클래스가 인스턴스를 생성하는 과정
인스턴스를 생성하는 과정을 보겠습니다.
1. 인스턴스 생성과 this 바인딩
new 연산자와 함께 클래스를 호출하면 constructor 내부 코드가 실행되기 전에 암묵적으로 빈 객체가 생성됩니다.
이 빈 객체가 클래스가 생성한 인스턴스입니다. 생성한 인스턴스의 프로토타입은 클래스의 prototype 프로퍼티가 가리키는 객체가 됩니다. 그리고 인스턴스는 this에 바인딩되는데 따라서 constructor 내부의 this는 클래스가 생성한 인스턴스를 가리킵니다.
2. 인스턴스 초기화
constructor 내부 코드에 의해 인스턴스를 초기화합니다.
인스턴스에 프로퍼티를 추가하고 constructor가 인수로 받은 초기값을 이용해 초기화를 해줍니다,
constructor가 생략되었을 경우 이 과정은 생략됩니다.
3. 인스턴스 반환
모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환됩니다.
class Person {
constructor(name) {
//인스턴스 생성 및 this 바인딩
console.log(this); //Person {}
console.log(Object.getPrototypeOf(this)=== Person.prototype); //true
// 인스턴스 초기화
this.name = name;
// this 암묵적 반환
}
}
주석으로 언급한 적있지만 인스턴스 프로퍼티는 언제나 public입니다.
그리고 접근자 프로퍼티를 클래스에서도 사용할 수 있습니다.
접근자 함수를 통해 프로퍼티 값을 조작하거나 사용이 가능합니다. (getter, setter)
(setter는 하나의 매개변수만 받을 수 있습니다)
클래스 필드란?
클래스 필드란 클래스가 생성할 인스턴스의 프로퍼티를 의미합니다.
자바스크립트의 클래스에서 인스턴스 프로퍼티를 선언하고 초기화하려면 반드시 constructor 내부에서 this에 프로퍼티를 추가해야합니다.
참조도 마찬가지입니다. 인스턴스 프로퍼티를 참조하려면 반드시 this를 사용해 참조해야합니다.
최신 Node.js(버전 12이상)에서는 아래 코드가 정상 작동합니다.
class Person {
name = 'Lee'
}
const me = new Person();
console.log(me); //Person {name: 'Lee'}
함수는 일급 객체이므로 함수를 클래스 필드에 할당할 수 있습니다.
따라서 클래스 필드를 통해 메서드 정의가 가능합니다.
클래스 필드에 함수를 할당하는 경우, 이 함수는 프로토타입 메서드가 아니라 인스턴스 메서드가 됩니다.
따라서 클래스 필드에 함수를 할당하는 것은 자제하는 게 좋습니다.
private 필드
최신 브라우저(Chrome 74이상)와 최신 Node.js(버전 12이상)은 private 필드를 정의할 수 있습니다.
private 필드 앞에는 #을 붙여줍니다. 참조할 때도 #을 붙여야합니다.
class Person {
#name = '';
constructor (name){
this.#name = name;
}
}
const me = new Person('yangpa');
console.log(me.#name); //클래스 외부에서 참조 불가능
//Private field '#name' must be declared in an enclosing class
private 필드는 클래스 내부에서만 참조 가능합니다.
물론 접근자 프로퍼티를 통해 간접적으로 접근하는 것은 가능합니다.
class Person {
#name = '';
constructor (name){
this.#name = name;
}
get sayName() {
return this.#name;
}
}
const me = new Person('yangpa');
console.log(me.sayName); //yangpa
static 필드
원래 static 키워드를 사용한 정적 필드 정의가 불가능했는데요 최신 브라우저와 최신 Node.js에서는
static 필드 정의가 가능해졌습니다. static 키워드를 앞에 붙이면 됩니다.
상속으로 클래스 확장하기
이제 마지막으로 상속을 살펴보려고 합니다.
상속으로 클래스를 확장할 수 있습니다. 그러나 이것은 프로토타입 기반 상속과는 다릅니다.
프로토타입 기반 상속은 프로토타입 체인을 통해 다른 객체의 자산을 상속받는 개념입니다.
그러나 상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는겁니다.
아래 예제를 통해 살펴봅시다.
class Animal {
constructor(age,weight) {
this.age= age;
this.weight = weight;
}
eat() { return 'eat';}
move() { return 'move';}
}
class Bird extends Animal {
fly() {return 'fly';}
}
class Lion extends Animal {
attack() {return 'attack';}
}
const bird = new Bird(1,3);
const lion = new Lion(3,70);
console.log(bird.eat()); //eat
console.log(bird.move()); //move
console.log(bird.fly()); //fly
console.log(lion.attack()); //attack
console.log(bird.attack()); //error
Bird 클래스와 Lion 클래스는 상속을 통해 Animal 클래스의 속성을 그대로 사용합니다.
거기에 각 클래스만의 고유한 속성을 추가 확장하였습니다.
extends
extends 키워드를 통해 상속받을 클래스를 정의할 수 있습니다.
상속을 통해 확장된 클래스를 서브클래스(파생 클래스, 자식 클래스)라고 합니다.
서브클래스에게 상속된 클래스를 수퍼클래스(베이스 클래스 ,부모 클래스)라고 합니다.
수퍼클래스와 서브클래스는 클래스 간의 프로토타입 체인도 생성하기 때문에 프로토타입 메서드, 정적 메서드를 모두 상속할 수 있습니다.
extends 키워드는 생성자 함수를 상속받아 클래스를 확장할 수도 있는데요.
extends 키워드 앞에는 반드시 클래스가 와야합니다.
function Base1(a) {
this.a = a
}
class Derived extends Base1 {}
const derived = new Derived(2);
console.log(derived); // Derived {a : 2}
동적으로 상속받을 대상을 결정할 수도 있습니다.
class base1 {
constructor() {
this.a = 1;
}
}
class base2 {
constructor() {
this.a = 2;
}
}
class Derived extends (true ? base1 : base2) {}
const derived = new Derived();
console.log(derived); //Derived {a :1}
super
서브클래스에서 constructor를 생략하면 다음과 같은 constructor 암묵적으로 정의됩니다.
constructor(...args) { super(...args);}
super는 호출하면 수퍼클래스의 constructor를 호출합니다.
super를 참조하면 수퍼클래스의 메서드를 호출할 수 있습니다.
1. 호출
class base1 {
constructor(a, b) {
this.a = a;
this.b = b;
}
}
class Derived extends base1 {
constructor(a, b, c) {
super(a, b);
this.c = c;
}
}
const derived = new Derived(1, 2, 3);
console.log(derived); // Derived { a: 1, b: 2, c: 3 }
주의할 점
서브클래스에서 constructor를 생략하지 않은 경우 super를 반드시 호출해줘야합니다.
super를 호출하기 전에는 this를 참조할 수 없습니다.
super는 서브클래스의 constructor에서만 호출해야합니다.
2. 참조
메서드 내에서 super를 참조하면 수퍼클래스의 메서드를 호출할 수 있습니다.
class base1 {
constructor(name) {
this.name = name;
}
sayHi() {
return `Hi ${this.name}`;
}
}
class Derived extends base1 {
sayHi() {
return `${super.sayHi()}. Good?`;
}
}
const derived = new Derived("yangpa");
console.log(derived.sayHi()); //Hi yangpa. Good?
상속 클래스가 인스턴스를 생성하는 과정
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
toString() {
return `width = ${this.width}, height = ${this.height}`;
}
}
class ColorRectangle extends Rectangle {
constructor(width, height, color) {
//super 호출
super(width, height);
this.color = color;
}
toString() {
return super.toString() + `, color = ${this.color}`;
}
}
const colorRectangle = new ColorRectangle(2, 4, "red");
console.log(colorRectangle.getArea()); // 8
console.log(colorRectangle.toString()); // width = 2, height = 4, color = red
자바스크립트 엔진이 클래스를 평가할 때 수퍼클래스와 서브클래스를 구분하기 위한 값을 내부 슬롯에 가집니다.
위 예제의 경우 "base" 또는 "derived"를 값으로 갖는 내부 슬롯 [[ConstructorKind]]를 갖습니다.
다른 클래스를 상속받지 않는 클래스는 내부 슬롯 값이 "base"로 설정됩니다.
서브클래스는 내부 슬롯 [[ConstructorKind]] 값이 "derived"로 설정됩니다.
이 값을 통해 수퍼 클래스와 서브클래스는 new 연산자와 함께 호출되었을 때의 동작이 구분됩니다.
다른 클래스를 상속받지 않는 클래스는 new 연산자와 함께 호출되었을 때 빈 객체(인스턴스)를 생성합니다. 그리고 이를 this에 바인딩합니다. <- 이미 위에서 설명한 내용입니다.
그러나 서브클래스는 자신이 직접 인스턴스를 생성하지 않습니다. 수퍼클래스에게 인스턴스 생성을 해달라고 합니다.
그렇기때문에 우리는 서브클래스의 constructor에서 반드시 super를 호출해야합니다.
서브클래스가 new 연산자와 함께 호출되면 서브클래스 constructor 내부의 super 키워드가 함수처럼 호출됩니다.
super가 호출되면 수퍼클래스의 constructor가 호출됩니다. 서브클래스 내부에 super 호출이 없으면 에러가 발생합니다.
결국 실제 인스턴스를 생성하는 건 수퍼클래스이기 때문입니다.
생성된 인스턴스는 this에 바인딩됩니다. this는 생성된 인스턴스를 가리킵니다.
위 예제에서 수퍼클래스 this를 출력하면 ColorRectangle {} 이 나옵니다.
수퍼클래스의 constructor가 실행되어 인수로 받은 값을 이용해 인스턴스를 초기화합니다.
super호출이 종료되고 이제 서브클래스 constructor가 일을 합니다.
super가 반환한 인스턴스가 this에 바인딩되고 서브클래스는 반환된 인스턴스를 그대로 사용합니다.
super 호출 이후 서브클래스의 constructor에 인수로 전달받은 초기값으로 인스턴스의 프로퍼티를 초기화합니다.
모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환됩니다.
상당히 길지만 천천히 읽어보면 내용이 어렵지는 않습니다.
표준 빌트인 객체도 생성자 함수이므로 extends 키워드를 통해 확장할 수 있습니다.
Array를 상속받아 확장한 클래스로 생성한 인스턴스는 Array.prototype와 클래스.prototype의 모든 메서드를 사용할 수 있습니다. 이때 map, filter와 같이 새로운 배열을 반환하는 메서드는 클래스의 인스턴스를 반환합니다.
드디어 클래스가 끝났습니다.
코드재사용에 아주 유용할 것 같습니다.
MDN & 모던 자바스크립트 Deep Dive 내용을 참고하였습니다.
'내가 해냄 > JS' 카테고리의 다른 글
프로토타입 내가 해냄 (0) | 2023.03.15 |
---|---|
배열 고차 함수 내가 해냄 (0) | 2023.03.14 |
ES6 함수 내가 해냄 (0) | 2023.03.14 |
DOM 내가 해냄 (1) | 2023.03.07 |
this 내가 해냄 (0) | 2023.03.06 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!