모던 자바스크립트 Deep Dive 정리
1. 클래스는 프로토타입의 syntactic sugar 인가?
▶ 클래스 = 함수
▷ JS의 프로토타입 기반 패턴을 다른 프로그래밍 언어의 클래스 기반 패턴처럼 사용할 수 있도록 하는 syntactic sugar
클래스 만의 독특한 특징
▶ 클래스는 new 없이 호출 불가, 생성자 함수는 new 없으면 일반함수로 호출
▶ 상속을 지원하는 extends, super 키워드 존재
▶ 클래스는 호이스팅 발생하지 않는 것처럼 동작, 생성자 함수는 함수 선언문일 경우 함수 호이스팅, 함수 표현식일 경우 변수 호이스팅 발생
▶ 클래스의 모든 코드에는 암묵적으로 strict mode 적용
▶ 따라서 클래스는 프로토타입 기반의 객체지향을 구현한다는 점에서 생성자 함수와 유사하지만, 단순히 프토토타입 패턴의 syntactic sugar라기 보다는 새로운 객체생성 패러다임이라고 볼 수 있다
2. 클래스 정의
▶ 클래스는 함수이며 역시 일급객체에 해당
▷ 기명 클래스, 무기명 클래스, 클래스를 변수에 할당 가능 (일급 객체)
// 클래스 선언문
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name; // name 프로퍼티는 public하다.
}
// 프로토타입 메서드
sayHi() {
console.log(`Hi! My name is ${this.name}`);
}
// 정적 메서드
static sayHello() {
console.log('Hello!');
}
}
// 인스턴스 생성
const me = new Person('Lee');
// 인스턴스의 프로퍼티 참조
console.log(me.name); // Lee
// 프로토타입 메서드 호출
me.sayHi(); // Hi! My name is Lee
// 정적 메서드 호출
Person.sayHello(); // Hello!
3. 클래스 호이스팅
▶ 클래스는 클래스 정의 이전에 참조 불가
▶ let, const로 정의한 변수처럼, 클래스 선언문 이전에 TDZ에 빠짐
▷ 따라서 호이스팅이 발생하지 않는 것처럼 동작
※ 자바스크립트에서 var, let, const, function, function*, class 키워드를 사용하여 선언된 모든 식별자는 호이스팅됨
※ 모든 선언문은 런타임 이전에 먼저 실행되기 때문이다
4. 인스턴스 생성
▶ 클래스는 인스턴스를 생성하기 위해 만들어진다
▶ 오직 new 연산자를 통해서만 인스턴스를 만들 수 있다
▶ 기명 함수 표현식과 마찬가지로, 클래스 이름은 외부 코드에서 접근 불가능하다
5. 메서드
5-1. constructor (생성자)
▶ 인스턴스를 생성하고 초기화하기 위한 특수 메서드
▷ 이름 변경 불가
▷ 클래스 내에 최대 1개만 존재할 수 있다
▷ 생략할 수 있고, 생략되면 빈 constructor가 암묵적으로 정의된다
▷ 생성자 함수과 마찬가지로, 내부에 return문이 있으면 안됨 (인스턴스 반환되지 않을 수 있음)
5-2. 프로토타입 메서드
▶ 클래스 몸체에서 정의한 메서드는 클래스의 prototype 객체에 추가하지 않아도 기본적으로 프로토타입 메서드가 된다
▶ 생성자 함수와 마찬가지로, 클래스가 생성한 인스턴스는 프로토타입 체인의 일원이 된다
5-3. 정적 (static) 메서드
▶ 인스턴스를 생성하지 않고도 호출할 수 있는 메서드
▶ 메서드 앞에 static 키워드를 붙이면 정적 메서드가 된다
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name;
}
// 정적 메서드
static sayHi() {
console.log('Hi!');
}
}
▷ 프로토타입 메서드가 클래스의 prototype 프로퍼티에 존재하는 것과 다르게, 정적 메서드는 클래스에 바로 바인딩된다
▷ 따라서 인스턴스 생성없이 바로 사용한다
5-4. 정적 메서드와 프로토타입 메서드의 차이
▶ 속해있는 프로토타입 체인이 다르다
▷ 프로토타입 메서드 : 클래스의 prototype 객체
▷ 정적 메서드 : 클래스에 직접 바인딩
▶ 호출 방식
▷ 프로토타입 메서드 : 인스턴스로 호출
▷ 정적 메서드 : 클래스로 호출
▶ 인스턴스 프로퍼티 참조 여부
▷ 프로토타입 메서드 : 가능
▷ 정적 메서드 : 불가
▶ 정적 메서드의 예
▷ Math.max() 또는 JSON.parse() 같은 표준 빌트인 객체의 메서드
▷ 전역에서 사용 가능한 유틸리티 함수로 주로 사용된다
6. 클래스의 인스턴스 생성 과정
▶ 인스턴스 생성과 this 바인딩
▶ 인스턴스 초기화
▶ 인스턴스 반환
7. 프로퍼티
7-1. 인스턴스 프로퍼티
▶ 인스턴스 프로퍼티는 constructor 내부에서 정의해야 한다
▶ 인스턴스의 프로퍼티는 언제나 public
7-2. 접근자 프로퍼티
▶ 자체적으로는 값을 갖지 않고, 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수
▷ getter 함수와 setter 함수
▷ 클래스에서도 접근자 프로퍼티를 사용할 수 있다
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// fullName은 접근자 함수로 구성된 접근자 프로퍼티다.
// getter 함수
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
// setter 함수
set fullName(name) {
[this.firstName, this.lastName] = name.split(' ');
}
}
const me = new Person('Ungmo', 'Lee');
// 데이터 프로퍼티를 통한 프로퍼티 값의 참조.
console.log(`${me.firstName} ${me.lastName}`); // Ungmo Lee
// 접근자 프로퍼티를 통한 프로퍼티 값의 저장
// 접근자 프로퍼티 fullName에 값을 저장하면 setter 함수가 호출된다.
me.fullName = 'Heegun Lee';
console.log(me); // {firstName: "Heegun", lastName: "Lee"}
// 접근자 프로퍼티를 통한 프로퍼티 값의 참조
// 접근자 프로퍼티 fullName에 접근하면 getter 함수가 호출된다.
console.log(me.fullName); // Heegun Lee
// fullName은 접근자 프로퍼티다.
// 접근자 프로퍼티는 get, set, enumerable, configurable 프로퍼티 어트리뷰트를 갖는다.
console.log(Object.getOwnPropertyDescriptor(Person.prototype, 'fullName'));
// {get: ƒ, set: ƒ, enumerable: false, configurable: true}
7-3. 클래스 필드 정의 제안
▶ 클래스 필드
▷ 클래스가 생성할 인스턴스의 프로퍼티를 가리키는 용어
▷ 생성자 내부가 아닌 클래스 필드에 프로퍼티나 메서드를 정의하는 것은 현재 ECMAScript 표준으로 확정 예정
class Person {
name = 'Lee'; // 클래스 필드도 기본적으로 public하다.
}
// 인스턴스 생성
const me = new Person();
console.log(me.name); // Lee
▶ this.name = name 처럼 외부값으로 인스턴스 프로퍼티의 초기화가 필요한 경우에는 굳이 클래스 필드에서 정의할 필요 없음
▷ constructor에서 인스턴스 프로퍼티를 정의하는 기존 방식 사용하면 됨
▶ 외부 값으로 정의할 필요 없이 인스턴스 프로퍼티를 생성하고자 한다면 클래스 필드 정의 방식을 사용할 수 있다
▷ 함수도 할당 가능하지만, 권장되지 않는다 (프로토타입 메서드가 아닌 인스턴스 메서드가 된다)
▷ 이는 인스턴스가 여러개 생성될 경우, 인스턴스 메서드를 담을 메모리가 필요해진다는 것을 의미한다 (메모리 손해)
7-4. private 필드 정의 제안
▶ 자바스크립트에서는 완전한 캡슐화를 지원하지 않는다
▷ public, private, protected 같은 접근 제한자가 없음
▷ 모든 인스턴스 프로퍼티는 public이므로, 인스턴스를 통해 클래스 외부에서 접근 가능
class Person {
constructor(name) {
this.name = name; // 인스턴스 프로퍼티는 기본적으로 public하다.
}
}
// 인스턴스 생성
const me = new Person('Lee');
console.log(me.name); // Lee -> 클래스 외부인 전역에서 접근 가능
▶ 클래스 필드 정의와 마찬가지로, private 필드 역시 ECMAScript 표준 사양 등록이 확실시된다
▶ private 필드의 선두에는 #을 붙여준다
class Person {
// private 필드 정의
#name = '';
constructor(name) {
// private 필드 참조
this.#name = name;
}
}
const me = new Person('Lee');
// private 필드 #name은 클래스 외부에서 참조할 수 없다.
console.log(me.#name);
// SyntaxError: Private field '#name' must be declared in an enclosing class
7-5. static 필드 정의 제안
▶ 클래스, private 필드 정의와 마찬가지로, static필드 역시 ECMAScript 표준 사양 등록이 확실시된다
class MyMath {
// static public 필드 정의
static PI = 22 / 7;
// static private 필드 정의
static #num = 10;
// static 메서드
static increment() {
return ++MyMath.#num;
}
}
console.log(MyMath.PI); // 3.142857142857143
console.log(MyMath.increment()); // 11
8. 상속에 따른 클래스 확장
8-1. 클래스 상속과 생성자 함수 상속
▶ 상속에 의한 클래스 확장은 지금까지의 프로토타입 기반 상속과는 다른 개념
▶ 프로토타입 기반 상속
▷ 프로토타입 체인을 통해 다른 객체의 자산을 상속받는 개념
▶ 상속에 의한 클래스 확장
▷ 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의
8-2. extends 키워드
▶ 상속을 통해 클래스를 확장하려면 extends 키워드를 사용하여 상속받을 클래스를 정의
// 수퍼(베이스/부모)클래스
class Base {}
// 서브(파생/자식)클래스
class Derived extends Base {}
▶ 서브 클래스 : 상속을 통해 확장된 클래스 (aka 파생 클래스, 자식 클래스)
▶ 수퍼 클래스 : 서브 클래스에게 상속된 클래스 (aka 베이스 클래스, 부모 클래스)
▶ 수퍼 클래스와 서브 클래스는 인스턴스의 프로토타입 체인 뿐만 아니라 클래스 간의 프로토타입 체인을 생성
▷ 이를 통해 프로토타입 메서드, 정적 메서드 모두 상속 가능
8-3. 동적 상속
▶ extends 키워드는 클래스 뿐만 아니라 생성자 함수를 상속받아 클래스를 확장할 수도 있다
▷ extends 앞에는 반드시 클래스가 와야 한다
// 생성자 함수
function Base(a) {
this.a = a;
}
// 생성자 함수를 상속받는 서브클래스
class Derived extends Base {}
const derived = new Derived(1);
console.log(derived); // Derived {a: 1}
▶ extends 키워드 뒤에는 클래스 뿐만 아니라 [[Construct]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식을 사용할 수 있다
▷ 이를 통해 동적으로 상속받을 대상을 결정할 수 있다
function Base1() {}
class Base2 {}
let condition = true;
// 조건에 따라 동적으로 상속 대상을 결정하는 서브클래스
class Derived extends (condition ? Base1 : Base2) {}
const derived = new Derived();
console.log(derived); // Derived {}
console.log(derived instanceof Base1); // true
console.log(derived instanceof Base2); // false
8-4. 서브 클래스의 constructor
▶ 클래스에서 constructor를 생략하면, 비어있는 constructor가 클래스에 암묵적으로 정의된다
▶ 서브 클래스에서 constructor를 생략하면 아래와 같은 constructor가 암묵적으로 정의된다
// args는 new 연산자와 함계 클래스를 호출할 때 전달한 인수의 리스트이다
constructor(...args) { super(...args); }
▶ super() : 수퍼 클래스의 constructor를 호출하여 인스턴스를 생성
8-5. super 키워드
▶ super 키워드는 함수처럼 호출할 수도 있고 this처럼 식별자로 참조할 수도 있다
▷ super 호출 : 수퍼 클래스의 constructor를 호출한다
▷ super 참조 : 수퍼 클래스의 메서드 호출 가능
super 호출
▶ 수퍼 클래스의 constructor 내부에서 추가한 프로퍼티를 그대로 갖는 인스턴스를 생성한다면, 서브 클래스의 constructor를 생략해도 된다
class Base {
constructor(a, b) {
this.a = a;
this.b = b;
}
}
// 서브클래스
class Derived extends Base {
// 다음과 같이 암묵적으로 constructor가 정의된다.
// constructor(...args) { super(...args); }
}
const derived = new Derived(1, 2);
console.log(derived); // Derived {a: 1, b: 2}
▶ 수퍼 클래스의 constructor로 추가한 프로퍼티를 서브 클래스에서 상속받아 활용할 수 있다
class Base {
constructor(a, b) { // ④
this.a = a;
this.b = b;
}
}
// 서브클래스
class Derived extends Base {
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}
▶ 1 에서 전달된 인수들은 서브 클래스의 constructor 2로 먼저 전달되고, 3에서 super 호출을 통해 일부 인수가 수퍼 클래스의 constructor 4로 전달된다
▶ super 호출 시 주의사항 (에러 발생)
▶ 서브 클래스에서 constructor를 생략하지 않는 경우, 서브 클래스의 constructor에서는 반드시 super를 호출해야 한다
▷ 즉, 서브 클래스에서 constructor를 사용하면, 반드시 super() 호출 필요
▷ 수퍼 클래스의 consturctor가 빈 함수일 경우에도 적용
▶ 서브 클래스의 constructor에서 super를 호출하기 전에는 this를 참조할 수 없다
class Base {}
class Derived extends Base {
constructor() {
// ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
this.a = 1;
super(); // constructor() 코드 블록 선두에 있어야 함
}
}
const derived = new Derived(1);
▶ super는 반드시 서브 클래스의 constructor 에서만 호출한다
▷ 다른 클래스나 함수에서 호출하면 에러 발생
super 참조
▶ 메서드 내에서 super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다
▶ 아래 코드에서 서브 클래스의 프로토타입 메서드의 super.sayHi는 수퍼 클래스의 프로토타입 메서드 sayHi를 가리킴
// 수퍼클래스
class Base {
constructor(name) {
this.name = name;
}
sayHi() {
return `Hi! ${this.name}`;
}
}
// 서브클래스
class Derived extends Base {
sayHi() {
// super.sayHi는 수퍼클래스의 프로토타입 메서드를 가리킨다.
return `${super.sayHi()}. how are you doing?`;
}
}
const derived = new Derived('Lee');
console.log(derived.sayHi()); // Hi! Lee. how are you doing?
▶ super 참조는 super를 참조하는 메서드의 [[HomeObject]] 를 활용해 수퍼 클래스의 메서드까지 거슬러 올라간다
▷ ES6의 메서드 축약표현으로 정의된 함수만이 [[HomeObject]]를 갖기 때문에, 클래스에서는 반드시 메서드 축약표현을 사용해야 한다
▷ 즉, super 참조는 ES6 메서드 축약표현으로 정의된 메서드만 가능하다
▶ 이 밖에도 super를 통해 수퍼 클래스의 정적 메서드도 참조할 수 있다
8-6. 상속 클래스의 인스턴스 생성 과정
1. 서브 클래스의 super 호출
▶ 서브 클래스는 자신이 직접 인스턴스를 생성하지 않고 수퍼 클래스에 인스턴스 생성을 위임한다
▷ 이것이 서브 클래스의 constructor에서 반드시 super를 호출해야 하는 이유이다 (없으면 에러 발생)
2. 수퍼 클래스의 인스턴스 생성과 this 바인딩
▶ 수퍼 클래스의 constructor는 인스턴스가 될 객체를 만들고, 이것이 this에 바인딩된다
▷ 하지만 인스턴스는 서브 클래스가 생성한 것으로 처리된다
▷ 인스턴스 = new 연산자와 함께 호출 = new.target이 가리키는 서브 클래스
3. 수퍼 클래스의 인스턴스 초기화
▶ 수퍼 클래스의 constructor가 실행되어 this에 바인딩되어 있는 인스턴스를 초기화
▷ this에 바인딩된 인스턴스에 프로퍼티를 추가하고 constructor가 전달받은 인수로 인스턴스 프로퍼티 값을 초기화
4. 서브 클래스 constructor로의 복귀와 this 바인딩
▶ 서브 클래스는 별도의 인스턴스를 생성하지 않고 super가 반환한 인스턴스를 this에 바인딩
▷ 서브 클래스의 constructor에서 super를 호출하고 나서야 this를 참조할 수 있는 것은 이 때문이다
5. 서브 클래스의 인스턴스 초기화
▶ super 호출 이후 서브 클래스의 constructor에 기술되어 있는 인스턴스 초기화 실행
6. 인스턴스 반환
▶ 클래스의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적 반환
8-7. 표준 빌트인 생성자 함수 확장
▶ extends 키워드 뒤에는 [[Construct]] 내부 메서드를 갖는 함수 객체로 평가될 수 있는 모든 표현식 사용 가능
▷ 따라서 String이나 Number 같은 표준 빌트인 생성자 함수도 사용 가능
▶ 예를 들어, Array 생성자 함수를 상속받아 확장한 커스텀 클래스가 생성한 인스턴스는, Array.prototype과 커스텀 클래스의 prototype 의 모든 메서드를 사용할 수 있다