● 클래스
클래스는 생성자 함수와 마찬가지로 인스턴스(기염둥이객체)를 생성할 수 있다. 생성자 함수가지고도 충분히 인스턴스를 만들어낼 수 있는데, 왜 클래스라는 것이 생기게 되었을까?
사실상 프로토타입기반 객체지향 언어인 자바스크립트는 클래스가 필요 없는 언어이다. 그렇지만 클래스가 생긴 이유는 아래와 같다.
자바, C#같은 클래스 기반 언어에 익숙한 언어들을 위해 편리함을 제공해줌
내 속마음 : 않의.. 그럼 프로토타입기반언어 그냥 무시하는거예욧? 왜 편리썽을 줍니까? 구냥 프로토타입기반 생성자 함수만씁시다! 자바스크립트만의 파이만 만들어요!! 절이 실으면 중이떠나라!!
하지만, 이런 편리성을 제공함으로서 협업도 넓어지고, 코드를 이해하는 폭도 넓어지고 사실상 이점이 많이 때문에 없는것 보단 있으면 좋은 거라고 생각하면 될 것 같다.
⚬ ES5에서의 생성자함수와 ES6에서의 클래스의 차이
var Person = (function(){
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function(){
console.log(`Hi! my name is ${this.name}`)
}
return Person
}())
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi(){
console.log(`Hi! my name is ${this.name}`)
}
}
클래스 | 생성자 함수 |
new 연산자 없으면 에러뜸 | new 연산자 없으면 일반함수로 작동됨 |
클래스는 상속 extends super 키워드 사용 | 그지같은(수퍼 익스땐두) 키워드들 지원안함 |
호이스팅 작동됨 | 호이스팅 작동됨 |
내부적으로 strict mode 작동됨 | strict mode가 모예연? 작동안함 |
프로퍼티 어트리뷰트 [[Enumerable]] 모두 false | 얜아닐걸요 |
● 클래스의 정의
클래스는 class 키워드를 사용해 정의한다. 클래스와 생성자 함수를 비교해 보았다. 둘이 비슷해보인다. 사실 클래스도 함수다. 즉, 클래스도 일급객체이다.
class Person1 {}
function Person(){}
const person2 = class {};
const person2 = function (){}
const Person3 = class MyClass {};
const person3 = function Myfunc(){}
여기서 짚고 가면 좋은 것
constructor : 함수 선언문, 함수 표현식, 클래스
non-constructor : 화살표 함수, 메서드(ES6 메서드 축약표현)
생김새가 좀 다르다. 디테일한 정의방식들은 아래에 더 깊게 파고들도록하갯다.
● 클래스의 호이스팅
클래스는 호이스팅된다.
console.log(Person); // ReferenceError: Cannot access 'Person' before initialization
const Person = class {};
클래스가 정의 이전에 참조를 해보았는데, 호이스팅이 작동되는 건가?
보니까 initialization이 나왔다. 엇 이건 어디서 많이 본듯한 ..?
let, const 가 바로 정의되는 부분 이전까지는 TDZ에 있어 initialization이 뜨게된다. 이건 실행컨텍스트의 선언전 렉시컬환경에서 이해할 수 있는 부분이다. 클래스도 이와 비슷하게 작동하는 것이다.
아래 예시를 보면 확실히 호이스팅이 된다는 걸 알 수 있다.
const Person = '';
{
console.log(Person) // ReferenceError: Cannot access 'Person' before initialization
const Person = class {};
}
● 인스턴스 생성
const Person3 = class MyClass {};
const person3 = function Myfunc(){}
let classCopy = new MyClass(); // MyClass is not defined
let classFunc = new Myfunc(); // MyFunc is not defined
- 클래스는 무조건 new 연산자와 호출해야한다.
- 식별자가 아니라 기명 클래스 표현식, 기명 함수 표현식으로 호출하면 작동되지 않는다.
● 메서드 생성
클래스몸체에서 정의할 수 있는 메서드는 constructor, 프로토타입메서드, 정적메서드 3가지가 있다.
클래스는 크게 두가지 영역으로 구분해서 보면 된다. 클래스 몸체 / constructor 영역
⚬ Constructor
인스턴스를 생성하고 초기화하기 위한 특수한 메서드이다. 그니까 인스턴스 값을 적는 종이임
class Person {
constructor(name) {
this.name = name;
}
}
클래스를 정의하면, 클래스는 함수 객체로 평가된다. 따라서 Function.prototype의 메서드들, Object.prototype의 메서드를 모두 상속받는다.
class Person {
constructor(name) {
this.name = name;
}
}
console.log(typeof Person) // function
Object.getPrototypeOf(Person) === Function.prototype // true
class에서 this는 생성자 함수와 같이 생성될 인스턴스를 가르킨다. constructor는 메서드로 해석되는 것이 아니라 클래스가 평가되어 생성한 함수 객체 코드의 일부가 된다.
class Person {
constructor(name) {
this.name = name;
}
}
function Person(name){
this.name = name;
}
내가 이해한바가 맞는지는 모르겠지만.. 위에 있는 class가 평가되면 아래와 같이 함수 객체로 평가되고, constructor도 함수객체의 코드로 바뀌게 된다는 것 같다.
class Person {
constructor(name) {
this.name = name;
}
constructor(name) {
this.name = name;
}
}
constructor 는 클래스 내에서 최대 한개만 존재할 수 있다.
class Person {}
let copyPerson = new Person();
console.log(copyPerson) // Person {}
constructor는 생략할 수 있다. 생략하면 빈 constructor가 정의되고 빈 객체로 생성된다.
class Person {
constructor(name) {
this.name = name;
// 명시적으로 객체를 반환하면 암묵적인 this 반환이 무시된다.
return {};
}
}
// constructor에서 명시적으로 반환한 빈 객체가 반환된다.
const me = new Person('Lee');
console.log(me); // {}
constructor는 암묵적으로 this, 즉 인스턴스를 반환한다. 하지만 return 에 참조 값을 반환하면 그 값으로 리턴된다. 하지만 원시 값을 넣으면 무시되고 암묵적으로 this가 반환된다.
class Person {
constructor(name) {
this.name = name;
// 명시적으로 원시값을 반환하면 원시값 반환은 무시되고 암묵적으로 this가 반환된다.
return 100;
}
}
const me = new Person('Lee');
console.log(me); // Person { name: "Lee" }
이는 생성자 함수에서도 똑같다.
⚬ 프로토타입 메서드
생성자 함수에서는 프로토타입 메서드를 추가하기 위해서는 함수명.prototype.메서드명 을 명시적으로 적어서 추가해줘야했다.
// 생성자 함수
function Person(name) {
this.name = name;
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
console.log(`Hi! My name is ${this.name}`);
};
const me = new Person('Lee');
me.sayHi(); // Hi! My name is Lee
하지만 클래스 에서는 아래와 같이 명시적으로 적을 필요가 없으며, 클래스 몸체 내부에서 정의된 함수라면 바로 프로퍼티 메서드가 된다.
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name;
}
// 프로토타입 메서드
sayHi() {
console.log(`Hi! My name is ${this.name}`);
}
}
const me = new Person('Lee');
me.sayHi(); // Hi! My name is Lee
그렇다면 클래스가 생성한 인스턴스의 프로토타입 체인을 확인해보자
class Person {
constructor(name) {
this.name = name;
}
}
let copyPerson = new Person("daeun");
- Person 클래스의 프로토타입은 Function.prototype 이기 때문에 Function.prototype의 메서드를 상속받는다.
- Function.prototype의 프로토타입은 Object.prototype이기 때문에 Person은 Object.prototype의 메서드를 상속받는다.
- 인스턴스 copyPerson의 프로토타입은 Person.prototype이기 때문에 Person.prototype의 메서드를 상속받는다.
- Person.prototype의 프로토타입은 Object.prototype이기 때문에 copyePerson은 Object.prototype 메서드를 상속받는다.
즉 위와 같이 보면 클래스는 생성자 함수와 마찬가지로 프로토타입 기반 객체 생성 매커니즘이다.
⚬ 정적 메서드
// 생성자 함수
function Person(name) {
this.name = name;
}
// 정적 메서드
Person.sayHi = function () {
console.log('Hi!');
};
// 정적 메서드 호출
Person.sayHi(); // Hi!
생성자 함수에서는 Person의 정적메서드를 추가하려면 명시적으로 적어야했다.
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name;
}
// 정적 메서드
static sayHi() {
console.log('Hi!');
}
}
하지만 클래스에서는 static이라는 키워드만 붙이면 정적 메서드가 된다.
위의 예제 클래스 Person은 아래의 프로토타입 체인을 생성한다.
인스턴스는 정적 메서드를 호출 할 수 없다. 정적 메서드가 Person.prototype에 있지 않기 때문이다.
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name;
}
// 정적 메서드
static sayHi() {
console.log("Hi!");
}
}
let copyPerson = new Person("daeun"); // 인스턴스 생성
Person.sayHi(); // Hi!
copyPerson.sayHi(); // 출력안됨 오류뜸
⚬ 정적 메서드와 프로토타입 메서드의 차이
- 두 개가 속해있는 프로토타입 체인이 각각 다르다.
- 정적 메서드는 클래스로 호출한다.
- 프로토타입 메서드는 인스턴스로 호출한다.
- 정적메서드는 인스턴스 프로퍼티를 참조할 수 없지만, 프로토타입 메서드는 인스턴스 프로퍼티를 참조할 수 있다.
class Person {
// 생성자
constructor(name) {
// 인스턴스 생성 및 초기화
this.name = name;
}
// 프로토타입 메서드
sayHello() {
console.log(this.name + ", " + "Hello~");
}
// 정적 메서드
static sayHi() {
console.log("Hi!");
}
}
let copyPerson = new Person('daeun')
Person.sayHi(); // Hi!
copyPerson.sayHello() // daeun, Hello~
- 두 개가 속해있는 프로토타입 체인이 각각 다르다.
sayHello 메서드는 Person.prototype의 프로퍼티가 된다. sayHi 메서드는 Person의 프로퍼티 즉, 메서드가 된다.
- 정적 메서드는 클래스로 호출한다.
위의 예시처럼 sayHi는 Person.sayHi로 호출한다.
- 프로토타입 메서드는 인스턴스로 호출한다.
sayHello는 copyPerson 이라는 인스턴스로 호출되고 있다.
- 정적메서드는 인스턴스 프로퍼티를 참조할 수 없지만, 프로토타입 메서드는 인스턴스 프로퍼티를 참조할 수 있다.
정적 메서드는 this를 클래스 내부몸체를 가르키고, 프로퍼티 메서드는 인스턴스 내부를 가르킨다.
따라서 this 를 사용하지 않는다면 정적메서드로, this를 사용한다면 프로퍼티 메서드로 활용하는 것이 맞다.
● 클래스의 인스턴스 생성 과정
⚬ 인스턴스 생성과 this 바인딩
new 연산자와 클래스가 호출되면 constructor의 내부 코드가 실행되기 앞서 암묵적으로 빈 객체가 생성된다. 이게 클래스가 만든 인스턴스다. 이 인스턴스는 자연적으로 [[prototype]] 내부 슬롯에 클래스.prototype이라는 값을 갖게 된다. 이 빈 객체는 this에 바인딩된다. 따라서 constructor 내부의 this는 클래스가 생성한 빈 인스턴스를 가르킨다.
⚬ 인스턴스 초기화
this에 바인딩 되어있는 인스턴스를 초기화한다. this에 바인딩 되어있는 인스턴스에 프로퍼티 먼저 추가하고 constructor의 인수로 받은 값들을 프로퍼티의 값으로 초기화한다. 근데 constructor가 생략됐으면 이 과정 생략됨염
⚬ 인스턴스 반환
모든 처리 끝나면 인스턴스가 바인딩된 this가 반환된다
class Person {
// 생성자
constructor(name) {
console.log(this); // Person {}
console.log(Object.getPrototypeOf(this) === Person.prototype); // true
this.name = name;
console.log(this); // Person { name : 'daeun'}
}
}
let copy = new Person("daeun");
● 프로퍼티
⚬ 인스턴스 프로퍼티
constructor 내부에서 this에 추가한 프로퍼티는 언제나 클래스가 생성한 인스턴스의 프로퍼티가 된다.
ES6의 클래스는 다른 객체지향 언어처럼 private, public, protected 키워드와 같은 접근 제한자를 지원하지 않는다.(2021년 1월 기준) 따라서 인스턴스 프로퍼티는 언제나 public하지만 다행히 private한 프로퍼티를 정의할 수 있는 방안이 현재 제공되고 있다.
⚬ 접근자 프로퍼티
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("Daeun", "Kim");
console.log(me); // Person{ firstName : 'Daeun', lastName : 'Kim'}
console.log(Object.getOwnPropertyNames(me)); // ['firstName', 'lastName']
console.log(Object.getPrototypeOf(me)); // 브라우저에서 상속된 프로퍼티까지 확인가능
console.log(me.fullName); // Daeun Kim
사실 클래스의 접근자 프로퍼티는 생성자 함수와 거의 똑같다.
- 생성자 함수와 똑같이 인스턴스가 getter, setter함수를 프로퍼티로 상속받는다.
- 하지만 [[Enumerable]]이 false라 프로퍼티가 열거되지 않으며 자신의 직접 프로퍼티로 만들어지진 않는다.
- 인스턴스가 상속받기 때문에 메서드로 쓸수 있다.
⚬ 클래스 필드 정의 제안
클래스 필드란 무엇인가..? 클래스 필드는 클래스 기반 객체지향 언어에서 비롯된 단어다. 클래스 필드란 생성할 인스턴스의 프로퍼티를 가르키는 용어다.
클래스 기반 객체지향 언어의 this는 언제나 클래스가 생성할 인스턴스를 가리키고 주로 클래스 필드가 생성자 또는 메서드의 매개변수 이름과 동일할 때 클래스 필드임을 명확히 하기 위해 사용된다.
자바스크립트의 클래스 몸체에는 메서드만 선언할 수 있다. 따라서 클래스 몸체에 자바와 유사하게 클래스 필드를 선언하면 문법 에러(SyntaxError)가 발생한다.
class Person {
// 클래스 필드 정의
name = 'Lee';
}
const me = new Person('Lee');
위 예제는 에러가 발생할 것 같지만 최신 브라우저(Chrome 72 이상) 또는 최신 Node.js(버전 12 이상)에서 실행하면 정상 동작한다.
그 이유는 자바스크립트에서도 인스턴스 프로퍼티를 마치 클래스 기반 객체지향 언어의 클래스 필드처럼 정의할 수 있는 새로운 표준 사양인 "Class field declarations"가 TC39 프로세스의 stage3(candidate)에 제안되었기 때문이다.
클래스 몸체에서 클래스 필드를 정의하는 경우 this에 클래스 필드를 바인딩해서는 안된다. this는 클래스의 constructor와 메서드 내에서만 유효하다. 클래스 몸체에서 클래스 필드를 정의하는 경우 this에 클래스 필드를 바인딩해서는 안된다. this는 클래스의 constructor와 메서드 내에서만 유효하다.
class Person {
// 클래스 필드 정의
this.name = 'Lee'; // 오류뜸 되지않음
}
클래스 필드에 초기값을 할당하지 않으면 undefined가 할당된다.
class Person {
name;
}
const copyPerson = new Person();
console.log(copyPerson.name) // undefined
class Person {
name = 'Lee';
// 클래스 필드에 함수를 할당
getName = function () {
return this.name;
}
// 화살표 함수로 정의할 수도 있다.
// getName = () => this.name;
}
const me = new Person();
console.log(me); // Person {name: "Lee", getName: f}
console.log(me.getName()); // Lee
함수는 일급 객체이므로 변수로 할당할 수 있다. 위의 예제를 보면 클래스필드에 함수를 할당하는 것과 같다. 클래스 필드에 있는 것들은 모두 인스턴스의 프로퍼티가 되므로, getName은 인스턴스의 프로퍼티가 된다.
하지만 이 방법은 권장되지 않는다.
⚬ private 필드 정의 제안
자바스크립트는 캡슐화를 완전하게 지원하지 않는다. 클래스도 private, public, protected 키워드와 같은 접근 제한자를 지원하지 않는다. 따라서 인스턴스 프로퍼티는 인스턴스를 통해 클래스 외부에서 언제나 참조할 수 있다. 즉, 언제나 public하다.
class Person {
constructor(name) {
this.name = name; // 인스턴스 프로퍼티는 기본적으로 public하다.
}
}
class Person {
name = 'Lee'; // 클래스 필드도 기본적으로 public하다.
}
다행히도 TC39 프로세스의 stage 3(candidate)에는 private 필드를 정의할 수 있는 새로운 표준 사양이 제안되었다.
다음 예제를 보자. 다음과 같이 private 필드의 선두에는 #을 붙여주며 참조할 때도 #을 붙여주어야 한다.
class Person {
#name = "";
constructor(name) {
this.#name = "name";
}
}
const me = new Person("daeun");
console.log(me.#name); // SyntaxError: Private field '#name' must be declared in an enclosing class
public 필드는 참조할 수 있지만 private 필드는 클래스 내부에서만 참조할 수 있다.
접근가능성 | public | private |
클래스 내부 | O | O |
자식 클래스 내부 | O | X |
클래스 인스턴스를 통한 접근 | O | X |
따라서 private 필드는 클래스 외부에서 접근할 수 없다. 그러나 접근자 프로퍼티를 통해 간접적으로 접근할 수 있다.
class Person {
#name = "";
constructor(name) {
this.#name = name;
}
get name() {
return this.#name;
}
}
const me = new Person("daeun");
console.log(me.name);
private 필드는 반드시 클래스 몸체에 정의해야한다. constructor에 정의를 하면 에러가 뜬다.
class Person {
constructor(name) {
this.#name = name; // constructor에 직접적으로 정의함 작동안됨
}
get name() {
return this.#name;
}
}
const me = new Person("daeun");
console.log(me.name);
⚬ static 필드 정의 제안
프로퍼티의 값이 원시값인 프로퍼티들은 private하게 필드를 설정해서 접근을 막을 수 있다. 근데 그럼 정적 메서드도 private하게 못만들어염?
static 키워드를 사용하여 정적 필드를 정의할 수는 없었지만 static public 필드, static private 필드, static private 메서드를 정의할 수 있는 새로운 표준 사양인 "Static class features"가 TC39 프로세스의 stage 3(candidate)에 제안되었다.
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
● 상속에 의한 클래스 확장
⚬ 클래스 상속과 생성자 함수 상속
프로토타입 기반 상속은 프로토타입 체인을 통해 다른 객체의 자산을 상속받는 개념이지만 상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것이다.
프로토타입 체인은 다른 객체의 자산을 물려받는 상속의 개념이지만, 상속에 의한 클래스의 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것이다. 아래 예시를봐보자.
Bird 클래스와 Lion클래스는 상속을 통해 Animal 클래스의 속성을 그대로받아 자신만의 고유한 속성을 추가해 확장했다.
class Animal {
constructor(age, weight) {
this.age = age;
this.weight = weight;
}
eat() { return 'eat'; }
move() { return 'move'; }
}
// 상속을 통해 Animal 클래스를 확장한 Bird 클래스
class Bird extends Animal {
fly() { return 'fly'; }
}
const bird = new Bird(1, 5);
console.log(bird); // Bird {age: 1, weight: 5}
console.log(bird instanceof Bird); // true
console.log(bird instanceof Animal); // true
console.log(bird.eat()); // eat
console.log(bird.move()); // move
console.log(bird.fly()); // fly
클래스 상속을 통해 다른 클래스를 확장하는 문법인 extends 키워드가 기본적으로 제공된다. extends 키워드를 사용하면 클래스 확장이 편하고 직관적이다. 하지만 생성자 함수는 생성자 함수를 상속받고 확장하는 건 없다.
자바스크립트는 클래스 기반 언어가 아니므로 생성자 함수를 사용하여 클래스를 흉내 내려는 시도를 권장하지는 않지만 의사 클래스 상속(pseudo classical inheritance)패턴을 사용하여 상속에 의한 클래스 확장을 흉내 내기도 했다. 클래스의 등장으로 다음 예제와 같은 의사 클래스 상속 패턴은 더는 필요하지 않다. 참고로만 알아두자.
// 의사 클래스 상속(pseudo classical inheritance) 패턴
var Animal = (function () {
function Animal(age, weight) {
this.age = age;
this.weight = weight;
}
Animal.prototype.eat = function () {
return 'eat';
};
Animal.prototype.move = function () {
return 'move';
};
return Animal;
}());
// Animal 생성자 함수를 상속하여 확장한 Bird 생성자 함수
var Bird = (function () {
function Bird() {
// Animal 생성자 함수에게 this와 인수를 전달하면서 호출
Animal.apply(this, arguments);
}
// Bird.prototype을 Animal.prototype을 프로토타입으로 갖는 객체로 교체
Bird.prototype = Object.create(Animal.prototype);
// Bird.prototype.constructor을 Animal에서 Bird로 교체
Bird.prototype.constructor = Bird;
Bird.prototype.fly = function () {
return 'fly';
};
return Bird;
}());
var bird = new Bird(1, 5);
console.log(bird); // Bird {age: 1, weight: 5}
console.log(bird.eat()); // eat
console.log(bird.move()); // move
console.log(bird.fly()); // fly
⚬ extends 키워드
// 수퍼(베이스/부모)클래스
class Base {}
// 서브(파생/자식)클래스
class Derived extends Base {}
extends 키워드의 역할은 수퍼클래스와 서브클래스 간의 상속 관계를 설정하는 것이다. 클래스도 프로토타입을 통해 상속 관계를 구현한다. 수퍼클래스와 서브클래스는 인스턴스의 프로토타입 체인뿐 아니라 클래스 간의 프로토타입 체인도 생성한다. 이를 통해 프로토타입 메서드, 정적 메서드 모두 상속이 가능하다.
⚬ 동적 상속
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
⚬ 서브클래스의 constructor
class Base {
constructor(name) {
this.name = name;
}
}
class Derived extends Base {
constructor(...args) {
super(...args);
}
}
const a = new Derived("daeun");
super의 constructor를 가져오려면 꼭 super(•••args)를 적어준다. super()는 수퍼클래스의 constructor를 호출해 인스턴스를 생성한다.
⚬ super 키워드
super 키워드는 함수처럼 호출할 수도 있고 this 같이 식별자처럼 참조할 수 있는 특수한 키워드이다.
- super를 호출하면 수퍼클래스의 constructor(super-constructor)를 호출한다.
- super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.
super 호출
class Base {
constructor(a, b) {
this.a = a;
this.b = b;
}
}
class Derived extends Base {
// 암묵적으로 constructor가 할당된다.
// constructor(...args){ super(...args); }
}
const stopStudy = new Derived(1, 3);
console.log(stopStudy);
그렇다면 서브클래스에도 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 stopStudy = new Derived(1, 2, 3);
console.log(stopStudy);
- 서브 클래스에서 constructor를 생략하지 않는 경우, 반드시 super를 호출해야한다.
- super을 호출하기 이전에 this를 참조할 수 없다.
class Base {
constructor(a, b) {
this.a = a;
this.b = b;
}
}
class Derived extends Base {
constructor(a, b, c) {
this.c = c; // 이딴일은 있을 수가 없읍니다.
super(a, b);
}
}
- super은 서브클래스의 constructor안에 있어야한다.
class Base {
constructor() {
super(); // SyntaxError: 'super' keyword unexpected here
}
}
function Foo() {
super(); // SyntaxError: 'super' keyword unexpected here
}
super 참조
- 서브클래스의 프로토타입 메서드 내에서 super.sayHi는 수퍼클래스의 메서드를 가르킨다.
class Base {
constructor(name) {
this.name = name;
}
sayHi() {
return `Hi ${this.name}`;
}
}
class Derived extends Base {
sayHi() {
return `${super.sayHi()}, how are you doing?`;
}
}
const stopStudy = new Derived("daeun");
console.log(stopStudy.sayHi());
class Base {
constructor(name) {
this.name = name;
}
sayHi() {
return `Hi ${this.name}`;
}
}
class Derived extends Base {
sayHi() {
const __super = Object.getPrototypeOf(Derived.prototype);
return `${__super.sayHi.call(this)}, how are you doing?`;
}
}
const stopStudy = new Derived("daeun");
console.log(stopStudy.sayHi());
그냥 __super.sayHi()를 하면 Base.prototype.sayHi() 이므로, sayHi라는 메서드가 바인딩되어있는 객체, 즉 Base.prototype에 있는 name을 가져오게된다. 그래서 undefined로 출력됨. 따라서 sayHi를 인스턴스 객체에 바인딩된 name을 가져오게 끔해야된다. 즉, Derived 의 constructor, 인스턴스의 프로퍼티를 가르켜야되는데, 항상 클래스의 프로토타입 메서드의 this는 인스턴스를 가르키기때문에 __super.sayHi.call(this)로 넣어주어야한다는 것이다.
이처럼 super 참조가 동작하기 위해서는 super를 참조하고 있는 메서드가 바인딩되어 있는 객체의 프로토타입을 찾을 수 있어야하며 이를 위해 메서드는 내부 슬롯 [[HomeObject]]를 가지며, 자신을 바인딩하고 있는 객체를 가리킨다.
주의할 것은 ES6의 메서드 축약 표현으로 정의된 함수만이 [[HomeObject]]를 갖는다는 것이다.
const obj = {
// foo는 ES6의 메서드 축약 표현으로 정의한 메서드다. 따라서 [[HomeObject]]를 갖는다.
foo() {},
// bar는 ES6의 메서드 축약 표현으로 정의한 메서드가 아니라 일반 함수다.
// 따라서 [[HomeObject]]를 갖지 않는다.
bar: function () {}
};
[[HomeObject]]를 가지는 ES6의 메서드 축약 표현으로 정의된 함수만이 super 참조를 할 수 있다. 단, super 참조는 수퍼클래스의 메서드를 참조하기 위해 사용하므로 서브클래스의 메서드에서 사용해야 한다.
super 참조는 클래스의 전유물이 아니다. 객체 리터럴에서도 super 참조를 사용할 수 있다. 단 ES6의 메서드 축약 표현으로 정의된 함수만 가능하다.
const base = {
name: 'Lee',
sayHi() {
return `Hi! ${this.name}`;
}
};
const derived = {
__proto__: base,
// ES6 메서드 축약 표현으로 정의한 메서드다. 따라서 [[HomeObject]]를 갖는다.
sayHi() {
return `${super.sayHi()}. how are you doing?`;
}
};
console.log(derived.sayHi()); // Hi Lee. how are you doing?
- 서브클래스의 정적 메서드 내에서 super.sayHi는 수퍼클래스의 정적 메서드 sayHi를 가리킨다.
class Base {
static sayHi() {
return 'Hi!';
}
}
class Derived extends Base {
static sayHi() {
// super.sayHi는 수퍼클래스의 정적 메서드를 가리킨다.
return `${super.sayHi()} how are you doing?`;
}
}
console.log(Derived.sayHi()); // Hi how are you doing?
⚬ 상속 클래스의 인스턴스 생성 과정
class Rectagle {
constructor(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
toString() {
return `width : ${this.width}, height : ${this.height}`;
}
}
class ColorRectagle extends Rectagle {
constructor(width, height, color) {
super(width, height);
this.color = color;
}
toString() {
return `${super.toString()}, color : ${this.color}`;
}
}
const makeRectagle = new ColorRectagle(30, 40, "yellow");
console.log(makeRectagle); // ColorRectagle { width: 30, height: 40, color: 'yellow' }
console.log(makeRectagle.getArea()); // 1200
console.log(makeRectagle.toString()); // width : 30, height : 40, color : yellow
1) 서브클래스의 super호출
자바스크립트 엔진은 클래스를 평가할 때 수퍼클래스와 서브클래스를 구분한다. [[ConstructorKind]]라는 내부슬롯에 수퍼클래스면 'base', 서브클래스면 'derived'로 값을 설정한다. 아무런 상속을 받지 않으면 base, 상속을 받으면 derived로 설정된다.
- new 연산자로 서브클래스가 호출되면, 서브클래스의 constructor에 있는 super의 키워드가 함수처럼 호출된다.
- super가 호출되면 수퍼클래스가 평가되어 함수 객체의 코드가 실행된다.
2) 수퍼클래스의 인스턴스 생성과 this 바인딩
- 수퍼클래스의 내부 코드가 실행되기 전에 빈 객체가 생성된다.
- 빈객체 = 인스턴스는 수퍼클래스가 생성했지만 생성된 인스턴의 프로토타입은 new.target에서 볼 수 있다 시피, 서브클래스가 생성한 것으로 처리된다.
class Rectangle {
constructor(width, height) {
// 암묵적으로 빈 객체, 즉 인스턴스가 생성되고 this에 바인딩된다.
console.log(this); // ColorRectangle {}
// new 연산자와 함께 호출된 함수, 즉 new.target은 ColorRectangle이다.
console.log(new.target); // ColorRectangle
...
class Rectangle {
constructor(width, height) {
// 암묵적으로 빈 객체, 즉 인스턴스가 생성되고 this에 바인딩된다.
console.log(this); // ColorRectangle {}
// new 연산자와 함께 호출된 함수, 즉 new.target은 ColorRectangle이다.
console.log(new.target); // ColorRectangle
console.log(Object.getPrototypeOf(this) === ColorRectangle.prototype); // true
console.log(this instanceof ColorRectangle); // true
console.log(this instanceof Rectangle); // true
...
3) 수퍼클래스의 인스턴스 초기화
- 수퍼 클래스는 this에 바인딩되어있는 인스턴스에 프로퍼티를 추가하고 constructor가 인수로 받은 초기값으로 인스턴스를 초기화한다.
class Rectangle {
constructor(width, height) {
// 암묵적으로 빈 객체, 즉 인스턴스가 생성되고 this에 바인딩된다.
console.log(this); // ColorRectangle {}
// new 연산자와 함께 호출된 함수, 즉 new.target은 ColorRectangle이다.
console.log(new.target); // ColorRectangle
console.log(Object.getPrototypeOf(this) === ColorRectangle.prototype); // true
console.log(this instanceof ColorRectangle); // true
console.log(this instanceof Rectangle); // true
// 인스턴스 초기화
this.width = width;
this.height = height;
console.log(this); // ColorRectangle {width: 2, height: 4}
...
4) 서브클래스의 constructor로 복귀와 this 바인딩
- super의 호출이 종료되고 서브클래스의 constructor로 돌아온다. 이때 super가 반환한 인스턴스가 this에 바인딩된다.
- 서브클래스는 인스턴스를 생성하지 않고, super가 반환한 인스턴스에 this를 바인딩해 그대로 사용한다.
class ColorRectangle extends Rectangle {
constructor(width, height, color) {
super(width, height);
// super가 반환한 인스턴스가 this에 바인딩된다.
console.log(this); // ColorRectangle {width: 2, height: 4}
...
따라서 super를 호출하기 이전에는 인스턴스가 생성되지도 않고, this 바인딩도 되지않는다. 그래서 super 이전에 this를 참조할 수 없는 것이다.
5) 서브클래스의 인스턴스 초기화
this에 바인딩되어있는 인스턴스에 프로퍼티를 추가하고 인수로 전달받은 초기값으로 인스턴스의 프로퍼티를 초기화한다.
6) 인스턴스의 반환
모든 처리가 끝나면 암묵적으로 인스턴스가 바인딩된 this가 반환된다.
class ColorRectangle extends Rectangle {
constructor(width, height, color) {
super(width, height);
// super가 반환한 인스턴스가 this에 바인딩된다.
console.log(this); // ColorRectangle {width: 2, height: 4}
// 인스턴스 초기화
this.color = color;
// 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.
console.log(this); // ColorRectangle {width: 2, height: 4, color: "red"}
}
...