클래스는 보통 연관이 있는 데이터들은 한군데에 모아놓는 역할을 한다.
자바스크립트는 프로토타입 기반의 객체 지향 언어이다.
프로토타입 기반 객체지향 언어는 클래스가 필요 없다. 그래서 앞에서 우리는 클래스가 필요없는 생성자 함수와 프로토타입을 통해 객체 지향 언어의 상속을 배웠다.
var Person = (function (){
function Person(name){
this.name = name;
}
Person.prototype.sayHi = function (){
console.log('Hi! My name is ' + this.name);
};
return Person;
}());
var me = new Person("Lee");
me.sayHi(); // Hi my name is Lee
자바스크립트에 여러가지 이유로 클래스 개념이 도입되었지만, 클래스도 함수이며 기존 프로토타입 기반의 패턴을 클래스 기반처럼 사용할 수 있도록 만들어놓은 것 뿐이다.
단, 클래스와 생성자 함수는 모두 프로토타입 기반의 인스턴스를 생성하지만 정확히 동일하게 동작하지는 않는다.
클래스는 생성자함수보다 엄격하다.
클래스를 new 없이 호출하면 에러 발생. 생성자 함수는 new 없으면 일반함수로 호출 가능
클래스는 상속 지원하는 extends, super 제공. 생성자는 그런거 없음
클래스는 호이스팅이 발생하지 않는 것처럼 동작. 생성자 함수는 함수선언문으로 정의되면 함수 호이스팅, 함수 표현식으로 정의하면 변수 호이스팅 발생
클래스 내 모든 코드에는 strict mode 지정. strictmode 해제 불가. 그러나 생성자 모드는 strict mode X
클래스는 constructor, 프로토타입 메서드, 정적 메서드는 모두 프로퍼티 어트리뷰트 [[Enumerable]] 값이 false임. 열거되지 않는다.
class Person {}
클래스 몸체에는 0개 이상의 메서드만 정의할 수 있다. 정의할 수 있는 메서드는 constructor(생성자), 프로토타입 메서드, 정적 메서드 이렇게 3가지이다.
class Person {
constructor(name){
this.name = name;
}
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();
Person.sayHello();
p.420 생성자 함수와 클래스 비교 코드
클래스도 함수이므로 호이스팅이 발생한다.
클래스 선언문으로 정의한 클래스는 함수 선언문과 같이 소스코드 평가 과정, 즉 런타임 이전에 먼저 평가되어 함수 객체를 생성한다. 생성된 함수 객체는 생성자 함수로서 호출할 수 있는 constructor이다.
프로토타입도 함께 생성된다. 프로토타입과 constructor는 쌍으로 존재한다.
console.log(Person); // Reference Error
class Person {}
클래스도 호이스팅이 일어날까 ? 위 코드에서 Reference 에러가 발생하는것으로 보아 호이스팅이 일어나지 않는 것처럼 보인다.
const Person = '';
{
console.log(Person); // 만약 호이스팅이 안일어난다면 ''이 출력되어야 한다. 그러나 ReferenceError
class Person {}
}
즉, 클래스도 호이스팅이 발생한다. 클래스는 let, const 로 선언한 변수처럼 호이스팅이 된다.
따라서, 클래스 선언문 이전에 TDZ가 존재하므로 호이스팅이 일어나지 않는 것처럼 보인다.
모든 선언문은 런타임 이전에 먼저 실행된다고 못박자
클래스는 인스턴스 생성 외에는 존재 이유가 없다. 따라서 꼭 new 연산자 붙여 호출한다.
class Person{}
const me = new Person();
console.log(me);
constructor 는 이름 못 바꾼다. 특수 메서드이다. 생성자 함수이다.
class Person {
constructor(name){
this.name = name; // constructor 내부의 this는 생성자 함수와 마찬가지로 클래스가 생성한 인스턴스를 가리킨다.
}
}
클래스도 함수이기 때문에 함수 객체 고유 프로퍼티 다 가지고 있다. 함수랑 동일하게 프로토타입과 연결되어 있으며 자신의 스코프 체인을 구성한다.
모든 함수가 가지고 있는 prototype 프로퍼티가 가리키는 프로토타입 객체의 constructor 프로퍼티는 클래스 자신을 가리키고 있다. 즉, 클래스는 인스턴스를 생성하는 생성자 함수이다.
constructor는 클래스 몸체에서 정의한 메서드이다. 그런데 클래스가 생성한 인스턴스에는 constructor 메서드가 안보인다.
constructor는 단순한 메서드가 아니라, 클래스가 평가되어 생성한 함수 객체 코드의 일부라고 생각하면 된다.
즉, 클래스 정의가 평가되면 constructor 에 적힌 동작을 하는 함수 객체가 생성되는 것이다.
constructor는 인스턴스 생성 및 인스턴스 프로퍼티 추가를 통한 인스턴스 초기화를 한다.
constructor 는 return문 무조건 생략해야 한다. 원시값 return시에는 무시되지만,
다른 객체 return하면 this, 즉 생성한 인스턴스를 반환 하지 못하고
다른 객체를 return에 혼돈을 주기 때문이다.
클래스는 그냥 메서드 추가하면 기본적으로 프로토타입 메서드가 됨
class Person {
constructor(name){
this.name = name;
}
// 프로토타입 메서드
sayHi() {
console.log(`Hi my name is ${this.name});
}
}
const me = new Person('Lee');
me.sayHi();
클래스가 생성한 인스턴스는 프로토타입 체인의 일원이 된다.
클래스에서의 정적 메서드는 static 키워드 붙이면 된다.
class Person {
constructor(name){
this.name = name;
}
// 프로토타입 메서드
sayHi() {
console.log(`Hi my name is ${this.name});
}
static sayHello() {
console.log('hello');
}
}
const me = new Person('Lee');
me.sayHi();
정적 메서드는 Person 클래스에 바인딩된다.
클래스도 함수객체이므로 자신의 프로퍼티/메서드를 소유할 수 있다.
따라서 정적 메서드는 인스턴스 생성 안해도 클래스 정의 이후 호출 가능하다.
반대로 인스턴스는 정적 메서드 사용 못한다.
인스턴스의 프로토타입 체인 상에 정적메서드가 없기 때문이다.
프로토타입 메서드의 this는 생성한 인스턴스를 가리키고, 정적 메서드의 this는 클래스를 가리킨다.
즉, 인스턴스의 프로퍼티를 참조해야 한다면 프로토타입 메서드를 사용해야 한다.
class Person {
constructor(firstName, lastName) {
(this.firstName = firstName), (this.lastName = lastName);
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
set fullName(name) {
[this.firstName, this.lastName] = name.split(" ");
}
}
const me = new Person("Seonyeong", "Kim");
console.log(`${me.firstName} ${me.lastName}`); // Seonyeong Kim
me.fullName = "Seonyeong Yun";
console.log(me); // Person { firstName: 'Seonyeong', lastName: 'Yun' }
console.log(me.fullName); // Seonyeong Yun
Java랑 다르게 Javascript에서는 클래스 몸체에서는 메서드만 정의하고, 인스턴스 프로퍼티는 constructor 내부에서 this를 이용해 추가할 수 있었다.
최신 브라우저, Node.js 버전에서는 클래스 몸체에서 this없이 필드 정의하는 것이 가능하다.
class Person {
name = "lee"
}
const me = new Person();
console.log(me); // Person {name: "lee"}
그러나 클래스 필드를 참조하려면 this를 사용해야된다.
class Person {
name = "Lee"
constructor(){
console.log(name); // ReferenceError
}
}
new Person();
클래스 필드 초기화 안하면 undefined
class Person {
name;
constructor(name){
this.name = name;
}
}
const me = new Person('Lee');
constructor에서 초기화할 수 있긴 함. 근데 걍 굳이 constructor 가 정의,
초기화 다 해주기 때문에 밖에서 정의 해줄 필욘 없다.
private 필드는 최신 브라우저, 최신 Node.js에서만 지원한다.
‘#’ 을 붙여주면 된다.
class Person {
#name = '';
constructor(name) {
this.#name = name;
}
}
const me = new Person('Lee');
console.log(me.#name); // SyntaxError: Private field name must be declared in ~
private 필드는 클래스 내부에서만 참조할 수 있다.
private필드는 constructor에서 정의하면 안된다. 몸체에서 해줘야 함.
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'
}
}
const bird = new Bird(1,5); // 메서드들은 프로토타입 메서드임. 인스턴스 메서드 x
console.log(bird); // Bird {age: 1, weight: 5}
class subClass extends superClass
확장된 클래스를 subclass 자식클래스
상속된 클래스를 superclass 부모클래스
인스턴스 사이 프로토타입 체인과 클래스 간 프로토타입 체인 생성한다.
즉, 프로토타입 메서드, 정적 메서드 모두 상속된다
서브 클래스의 constructor 생략 시 암묵적으로 아래와 같은 constructor 가 정의됨
super()는 수퍼 클래스의 constructor 호출하여 인스턴스 생성함
class Base{
constructor(){}
}
class Derived extends Base {
// 암묵적으로 정의
// constructor(...args) {super(...args);}
}
const derived = new Derived();
console.log(derived); // Derived {}
만약 수퍼 클래스의 프로퍼티를 그대로 갖는 인스턴스를 만든다면, 서브클래스에서 constructor를 생략할 수 있다.
그러나, 서브클래스에서 자기만의 프로퍼티를 갖는 인스턴스를 생성한다면? constructor 를 생략하면 안된다.
왜냐하면 수퍼클래스의 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}
Derived 클래스를 호출 할 때 전달된 1,2,3 은 Derived 클래스의 constructor에 전달되고,
super 호출을 통해 Base 클래스의 constructor에도 일부 (1,2)가 전달된다.
인스턴스 초기화를 위해 전달한 인수는 수퍼클래스와 서브클래스에 배분된다.
주의 사항, super 사용 예제코드 중 몇 개 가져옴
super 사용해 수퍼클래스의 메서드 참조 가능
class Base{
constructor(name){
this.name = name;
}
sayHi(){
return `Hi my name is ${this.name}`;
}
}
class Derived extends Base {
sayHi(){
return `${super.sayHi()}. How are u doing?`;
}
}
const derived = new Derived('yun');
console.log(derived.sayHi()); // hi my name is yun. How are u doing?
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(width, height);
this.color = color;
}
toString() {
return super.toString() + `, color = ${this.color}`;
}
}
const colorRectangle = new ColorRectangle(2, 4, "red");
console.log(colorRectangle); // ColorRectangle { width: 2, height: 4, color: 'red' }
console.log(colorRectangle.getArea()); // 8
console.log(colorRectangle.toString()); // width = 2, height = 4, color = red
그림을 보면 Rectangle 클래스의 프로토타입은 getArea, toString 을 프로퍼티로 가지고 있고,
ColorRectangle 클래스의 프로토타입은 오버라이딩한 toString 만 프로퍼티로 가지고 있다.
그리고 ColorRectangle의 인스턴스인 colorRectangle은 수퍼 클래스와 서브클래스의 constructor 모두를 거치는데, 수퍼 클래스의 this와 서브 클래스의 this 모두 colorRectangle이라는 인스턴스를 바인딩하므로
width, height, color 을 갖는다.
자바스크립트 엔진은 서브클래스와 수퍼클래스를 구분하기 위해 base또는 derived 를 값으로 갖는 내부슬롯 [[ConstructorKind]] 를 갖는다. 다른 클래스를 상속받는 서브클래스는 derived, 아닌 클래스들은 base라는 값을 가지고 있다.
서브클래스가 super를 호출할 때 서브클래스는 직접 인스턴스를 생성하는게 아니라 super를 통해 수퍼클래스에게 이 역할을 넘긴다.
수퍼 클래스의 constructor가 호출되면 빈 객체를 생성한다. 이는 수퍼클래스가 생성한 인스턴스이다.
그리고 이 인스턴스가 this에 바인딩된다.
new 연산자로 서브 클래스를 호출하였고, super를 통해 인스턴스는 수퍼클래스가 생성했다. 라고 생각하면 된다.
new 연산자로 호출한 것은 서브 클래스이기 때문에 생성된 인스턴스의 프로토타입은 수퍼클래스의 프로토타입 객체가 아니라, 서브클래스의 프로토타입 객체이다.
수퍼클래스의 constructor 내부 코드가 실행되면, 바인딩된 인스턴스를 초기화한다.
super가 종료되면, 다시 서브클래스의 constructor 코드로 복귀한다.
그 때, super가 반환한 인스턴스가 서브 클래스 constructor 내부의 this에 바인딩 된다.
그래서 서브 클래스는 별도의 인스턴스 생성없이, 반환받은 인스턴스를 this에 바인딩해 사용하는 것이다.
따라서, super 키워드를 호출하지 않으면 인스턴스가 생성되지 않아 this바인딩도 안된다.
즉 서브 클래스 내 constructor 내부의 인스턴스 초기화는 반드시 super 호출 이후에 이뤄져야 한다.
extends를 통해 확장할 때에는, 클래스 뿐 아니라 String, Number, Array같은 표준 빌트인 객체들도 [[Construct]] 내부 메서드를 갖는 생성자 함수이므로 확장이 가능하다.
class MyArray extends Array {
uniq() {
return this.filter((v, i, self) => self.indexOf(v) == i);
}
average() {
return this.reduce((pre, cur) => pre + cur, 0) / this.length;
}
}
const myArray = new MyArray(1, 1, 2, 3);
console.log(myArray); // MyArray(4)[(1, 1, 2, 3)]
console.log(myArray.uniq()); // MyArray(3)[(1, 2, 3)]
console.log(myArray.average()); // 1.75
// 메서드 체이닝
console.log(myArray.filter(v => v % 2).uniq().average()); // 2
인스턴스 myArray는 Array.prototype 과 MyArray.prototype 의 모든 메서드를 사용할 수 있다.
이 때, Array.prototype 의 메서드 중, map, filter 와 같이 새로운 배열을 반환하는 메서드는 MyArray 클래스의 인스턴스를 반환한다. 그렇기 때문에 uniq, average 와 같은 MyArray의 메서드를 연이어 호출할 수 있다.