class

beomjin_97·2022년 1월 31일
0

javascript

목록 보기
1/13

0. class

ES6부터 도입된 새로운 객체 생성 메커니즘.
함수와 마찬가지로 값으로 사용할 수 있는 일급 객체.

class는 생성자 함수와 유사하지만 차이점이 존재한다.

  • new 연산자없이 호출하면 에러가 발생한다. (생성자 함수는 일반함수로 동작)
  • 상속을 지원한다. (extends, super)
  • 호이스팅이 발생하지 않는 것처럼 동작한다.
  • class내의 코드는 암묵적으로 strict mode가 지정되며, 해제할 수 없다.
  • class의 consturctor, prototype method, static method는 프로퍼티 어트리뷰트[[Emumerable]] 값이 false이다.


1. class 정의

class Person{}  //class 선언문
const Person = class{}  // 익명 클래스 표현식
const Person = class myClass{} // 기명 클래스 표현식
class Person {
  constructor(name) {
    // 인스턴스 생성 및 초기화
    this.name
  }

  // 프로토타입 메서드
  sayHi() {    
    console.log(`Hi ${this.name}!`)
  }

  // 정적 메서드
  static sayHello() {
    console.log('Hello')
  }
}

const me = new Person('beomjin_97');

console.log(me.name) // beomjin_97
me.sayHi()  // Hi beomjin97!
Person.sayHello()  // Hello


2. class hoisting

class 선언문은 let, const 키워드 처럼 호이스팅이 발생하지 않는 것처럼 보인다.
하지만 모든 선언문은 런타임 이전에 소스코드 평가과정에서 미리 평가되기 때문에 class 선언문도 변수 선언, 함수 정의와 마찬가지로 호이스팅이 발생한다.

console.log(Person);  //ReferenceError

class Person {}


3. instance 생성

class Person {}

const me = new Person();  // new 키워드와 함께 호출 -> 인스턴스 생성

class 표현식으로 정의된 class의 경우, 식별자를 사용해 인스턴스를 생성해야 한다.
기명 함수 표현식과 마찬가지로 클래스 표현식에서 사용한 클래스 이름은 외부코드에서 접근 불가능하기 때문이다. (함수 내부에서만 유효하다.)

const Person = class MyClass {}

const me = new Person();


4. method

class 내부에서 정의할 수 있는 메서드는 consturctor, prototype method, static method가 있다.

4.1 constructor

인스턴스를 생성하고 초기화하기 위한 메서드

class Person {
  constructor(name) {
    this.name = name'
  }
}

class의 constructor는 메서드로 해석되는 것이 아니라 클래스가 평가되어 생성된 함수 객체 코드의 일부가 된다. 클래스 정의가 평가되면 constructor의 기술된 동작을 하는 함수 객체가 생성된다.

class Person {
  constructor{
    // 프로퍼티가 추가되어 초기화된 인스턴스 생성
  	this.name = 'Kim';   
  	this.add = 'Seoul';
  }
}

const me = new Person();
console.log(me.name);  // Kim 

인스턴시 생성시 외부에서 인스턴스 프로퍼티의 초기값을 전달하려면 consturctor에 매개변수를 선언하고 초기값을 전달한다.

class Person {
  constructor (name, address) {
    // 프로퍼티가 추가되어 초기화된 인스턴스 생성
  	this.name = name;   
  	this.add = address;
  }
}

const me = new Person('Kim', 'Seoul');
console.log(me.name);  // Kim 

constructor 내에서는 인스턴스 생성과 동시에 인스턴스 프로퍼티를 추가하여 인스턴스를 초기화한다.
consturctor 내부에서 명시적으로 this가 아닌 값을 반환하는 것은 기본 동작을 해하는 코드 이므로 반드시 return문을 생략해야한다.

4.2 prototype method

class Person {
  constructor(name) {
    this.name
  }

  // 프로토타입 메서드
  sayHi() {    
    console.log(`Hi ${this.name}!`)
  }
}

const me = new Person('beomjin_97');

me.sayHi()  // Hi beomjin97!

클래스가 생성한 인스턴스는 프로토타입 체인의 일원이 된다.
그리고 클래스에서 정의한 메서드는 인스턴스의 프로토타입에 존재하는 프로토타입 메서드가 된다.
따라서 인스턴스는 프로토타입에서 메서드를 상속받아 사용할 수 있다.

Object.getPrototypeOf(me) === Person.prototype; //true
me instanceof Person;  //true

Object.getPrototypeOf(Person.prototype) === Object.prototype; //true
me instanceof Object; // true

me.constructor === Person; // true

4.3 static method

클래스는 함수 객체로 평가되므로 자신의 프로퍼티와 메서드를 소유할 수 있다. 클래스에서는 static 키워드를 이용해 정적 메서드를 정의한다.

정적 메서드는 클래스 정의후 인스턴스를 생성하지 않아도 호출할 수 있고 인스턴스가 아닌 클래스로 호출한다. 또한 프로토타입 체인 상에 정적메서드가 존재하기 때문에 당연하게도 인스턴스는 정적메서드를 상속받을 수 없다.

class Person {
  constructor(name) {
    this.name
  }

  // 정적 메서드
  static sayHello() {
    console.log('Hello')
  }
}

Person.sayHello()  // Hello

const me = new Person('Lee')
me.sayHello()  // TypeError

정적 메서드 내부의 this는 인스턴스가 아닌 클래스를 가리킨다. 메서드 내부에서 인스턴스 프로퍼티를 참조할 필요가 있으면 this를 사용하고 프로토타입 메서드로 정의해야한다.

반대로 this를 사용하지 않는 메서드는 정적 메서드로 정의하는 것이 좋다.

정적 메서드를 활용하는 방법중에 한가지는 클래스를 하나의 namespace로 사용하는 것이다. 클래스 내부에 정적 메서드를 모아 놓으면 충돌 가능성이 줄어들고 관련함수들을 구조화 할 수 있다. 전역에서 사용해야 할 유틸리티 함수를 전역으로 정의하지 않을 수 있는 것이다.



5 property

5.1 instance property

constructor 내부 코드가 실행되기 이전 constructor 내부의 this에는 이미 클래스가 암묵적으로 생성한 인스턴스의 빈 객체체가 바인딩되어 있다. 그리고 this에 인스턴스 프로퍼티를 추가하여 인스턴스를 초기화한다.

class Person {
  constructor(name) {
    this.name = name;
  }
}

const me = new Person('Lee');
console.log(me.name);

5.2 accessor property

클래스에서도 접근자 프로퍼티를 사용할 수 있다.

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('beomjin', 'kim')

console.log(me.fullName);   // beomjin kim
me.fullName = 'Kaley Cuoco';
console.log(me.fullName);   // Kaley Cuoco

클래스의 메서드가 기본적으로 프로토타입 메서드가 되는 것처럼 클래스의 접근자 프로퍼티도 인스턴스 프로퍼티가 아닌 프로토타입의 프로퍼티가 된다.

5.3 class field 정의 제안

클래스 필드는 객체 지향 언어에서 클래스가 생성할 인스턴스의 프로퍼티를 가리키는 용어이다.
자바의 클래스에서는 필수를 필드를 변수처럼 클래스 몸체에 this없이 선언한다. 또한 인스턴스 프로퍼티를 참조 할 때도 this를 생략하고 참조할 수 있다.

version 12이상의 Node.js와 chrome 72 이상에서는 클래스필드 문법을 사용할 수 있다.

class Person {
  name: 'Lee'
}

const me = new Person();
console.log(me);  // Person {name: 'Lee'}

클래스 필드를 정의하는 경우 this에 클래스 필드를 바인딩해서는 안된다.

class Person {
  this.name: 'Lee'  //ReferenceError
}

그러나 참조할 때는 this를 반드시 사용해야 한다.

class Person {
  name = 'Lee'
  hello() {
    console.log(this.name)
  }
}

const me = new Person();
me.hello()  // Lee

외부의 초기값으로 클래스 필드를 초기화해야 한다면 constuctor에서 클래스 필드를 초기화해야한다. (클래스 필드 문법을 사용하는 의미가 없다.)

class Person {
  name;
  consturctor(name) {
    this.name = name;
  }
}

클래스 필드를 통해 메서드도 정의할 수 있다. (함수가 일급객체 이기 때문)
그러나 이 함수는 프로토타입 메서드가 아닌 인스턴스 메서드가 된다. 따라서 권장되지 않는다.

class Person {
  name = 'Lee'

  getName = function() {
    return this.Name
  }
  // 화살표 함수도 가능하다.
}

5.4 private field 정의 제안

version 12이상의 Node.js와 chrome 72 이상에서는 private field 문법을 사용할 수 있다.

private 필드의 앞에 #을 붙여서 선언하고 참조할 때도 #을 붙여서 참조한다.

class Person {
  #name = 'Lee'
  hello() {
    console.log(this.#name)
  }
}

const me = new Person()   
me.hello()  // Lee
console.log(me.#name);  //SyntaxError  

private 필드는 반드시 클래스 몸체에 정의해야 하고 constructor에 직접 정의하면 에러가 발생한다.

class Person {
  #name = ""
  constructor(name) {
    this.#name = name
  }
  hello() {
    console.log(this.#name)
  }
}

const me = new Person('Lee');
me.hello()
// console.log(me.#name)

5.5 static field 정의 제안

version 12이상의 Node.js와 chrome 72 이상에서는 static field 문법을 사용할 수 있다.

class MyMath {
  static PI = 22/7     // static field
  static #num = 10     // static private field
  
  static increment() {   // static method
    return ++MyMath.#num;
  }
}


6. 상속을 통한 class 확장

서브클래스(자식 클래스): 상속을 통해 확장된 클래스
수퍼클래스(부모 클래스): 서브클래스에 상속된 클래스

6.1 extends

인스턴스의 프로토타입 체인뿐만 아니라 클래스 간의 프로토타입도 생성한다. 이를 통해 프로토타입 메서드, 정적 메서드 모두 상속한다.

class Base {} 

class Derived extends Base {}

6.2 동적 상속

클래스는 클래스뿐만 아니라 생성자 함수도 상속 받을 수 있다.

function Base(a) {
  this.a = a;
}

class Derived extends Base {}

const derived = new Derived(1)
console.log(derived.a) // 1

더 정확하게는 [[consturct]] 내부 메서드를 가진 모든 함수 객체로 평가될 수 있는 모든 표현식을 상속 받을 수 있다.

function base1() {}
class Base2 {}

let condition = true;

class Derived extends (condition ? Base1 : Base2) {}

6.3 서브클래스의 constructor

클래스에서 constructor를 생략하면 constructor() {} 가 암묵적으로 생성된다.

서브클래스에서 constructor를 생략하면 constructor(...args) { super(...args)}가 암묵적으로 정의된다. args는 클래스를 호출할 때 전달한 인수 리스트이다.

6.4 super

super 키워드는 함수처럼 호출할 수도 있고 식별자처럼 참조할 수도 있다.

  • 호출하면 super-constructor를 호출한다.
  • 참조하면 슈퍼클래스의 메서드를 호출할 수 있다.

new 연산자와 함꼐 서브클래스를 호출하면서 전달한 인수는 서브클래스에 암묵적으로 정의된 constructor의 super 호출을 통해 수퍼클래스의 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 }

서브클래스에 명시적인 consturctor가 있는 경우, 반드시 super를 호출해야 하며 전달된 인수가 수퍼클래스와 서브클래스에 배분되고 두 클래스는 협력하여 인스턴스를 생성한다.

class Base {
  constructor(a, b) {
    this.a = a
    this.b = b
  }
}

class Derived extends Base {
  constructor (a, b, c) {   // 매개변수 이름이 같을 필요는 없다.
    super(a, b);  // this를 참조하기 전에 super 호출이 먼저다.
    this.c = c;
  }
}

const derived = new Derived (1, 2, 3);
console.log(derived)  // Derived { a: 1, b: 2, c: 3 } 

매서드 내에서 super을 참조하면 수퍼클래스의 메서드를 호출할 수 있다.

class Base {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    return `Hi ${this.name}`
  }
}

class Derived extends Base {
  sayHi() {
    return ${super.sayHi()}. how are you today?`
    }
}
const derived = new Derived('Lee');
console.log(derived.sayHi()) // Hi Lee. how are you today?

6.5 상속 클래스의 인스턴스 생성 과정

자바스크립트 엔진은 클래스를 평가할 때 [[Constructorkind]] 의 값으로 클래스를 구분한다.
다른 클래스를 상속받지 않는 클래스는 [[Constructorkind]]의 값이 'base'로 설정되고 다른 클래스를 상속받는 서브클래스는 'derived'로 설정된다.

  1. 서브클래스의 super 호출
    서브클래스는 직접 인스턴스를 생성하지 않고 수퍼클래스에게 인스턴스 생성을 위임한다. 수퍼클래스가 인스턴스 생성의 주체이다.

  2. 수퍼클래스의 인스턴스 생성과 this 바인딩
    수퍼클래스의 constructor 내부 코드가 실행되기 전 암묵적으로 빈 객체를 생성하고 이를 this에 바인딩한다. 수퍼클래스 내부의 this는 생성된 인스턴스를 가리킨다. 사실 수퍼클래스가 생성한 인스턴스이지만 new.target은 서브클래스를 가리킨다. 따라서 인스턴스는 new.target에 따라 서브클래스가 생성한 것으로 처리되고 인스턴스의 prototype 프로퍼티가 가리키는 객체는 서브클래스의 프로토타입 객체이다.

  3. 수퍼클래스의 인스턴스 초기화
    수퍼 클래스의 constructor가 실행되어 this에 바인딩되어 있는 인스턴스를 초기화한다.

  4. 서브클래스 consstructor로의 복귀와 this 바인딩
    super의 호출이 종료되고 제어흐름이 서브클래스 constructor로 돌아온다. super은 인스턴스를 반환하고 서브클래스는 따로 인스턴스를 생성하지 않고 이 인스턴스를 this에 바인딩하여 사용한다.

  5. 서브클래스의 인스턴스 초기화
    서브클래스의 constructor가 실행되어 this에 바인딩되어 있는 인스턴스를 초기화한다.

  6. 인스턴스 반환
    클래스의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this가 암묵적으로 반환된다.

6.6 표준 빌트인 생성자 함수 확장

String, Number, Array와 같은 표준 빌트인 객체도 [[construct]] 내부 메서드를 갖는 생성자 함수 이므로 extends 키워드를 사용하여 확장할 수 있다.

class MyArray extends Array {
  uniq() {
    return this.filter((v, i, self) => self.indexOf(v) === i);
  }
}

const myArray = new MyArray(1, 1, 2, 3)
console.log(myArray.uniq());  // MyArray(4) [1, 2, 3]

MyArray 클래스가 생성한 인스턴스는 Array.prototype과 MyArray.prototype의 모든 메서드를 사용할 수 있다. 또한 uniq 메서드가 반환한 새로운 인스턴스는 Array 타입이 아닌 MyArray타입이다.

profile
Rather be dead than cool.

0개의 댓글