클래스

</>·2022년 3월 26일
4
post-thumbnail

목표

  • 25장의 내용을 최대한 이해하고 정리하기

25. 클래스(Class)

25-1. 자바스크립트와 클래스

  • 자바스크립트는 프로토타입 기반 객체지향 언어 로 다른 객체지향 언어와 다르지만 객체지향 프로그래밍 능력을 가지고 있다.
  • 프로토타입 기반 객체지향 언어는 클래스가 필요 없는 언어이다. ES5에서는 클래스 없이도 생성자 함수와 프로토타입을 통해 객체지향 언어의 상속을 구현할 수 있었다.
// 생성자 함수
var Person = (function () {
  function Persion(name) {
    this.name = name;
  }
  
  // 프로토타입 메서드
  Person.prototype.sayHi = functon () {
    console.log("Hi! My name is "+ this.name);
  };
  
  return Person;
}());

var me = new Person("Lee");
me.sayHi();
  • 하지만, ES6에서 도입된 클래스 는 자바나 C와 같은 클래스 객체지향 프로그래밍 언어와 비슷한 새로운 객체 생성 매커니즘을 제시했다.
  • 그렇다고 ES6 클래스가 기존의 프로토타입 기반 객체지향 모델을 폐지하는 것이 아니고 기존 프로토타입 기반 패턴을 클래스 기반 패턴처럼 사용하도록 만든 것이다.

25-1-1. 클래스와 생성자 함수

  • 클래스와 생성자 함수는 모두 프로토타입 기반의 인스턴스를 생성하지만 정확히 동일하게 동작하지는 않는다.
  • 클래스는 생성자 함수보다 엄격하며 다음과 같은 몇 가지 차이가 있다.
    • 생성자 함수는 new 연산자 없이 호출하면 일반 함수로 호출되지만 클래스를 new 없이 호출하면 에러가 발생한다.
    • 클래스는 생성자 함수와 달리 extendssuper 키워드를 지원한다.
    • 클래스는 생성자 함수와 달리 클래스 내의 모든 코드는 암묵적으로 strict mode가 지정되어 실행되며 해제할 수 없다.

25-2. 클래스 정의

  • 클래스는 class 키워드로 정의한다.
  • 클래스 이름은 파스칼 케이스(앞의 글자가 대문자)를 사용하는 것이 일반적이다. 물론, 파스칼 케이스를 사용하지 않아도 에러는 발생하지 않는다.
class Person {}
  • 클래스 내부에 constructor(생성자), 프로토타입 메서드, 정적 메서드 등과 같은 메서드들을 추가할 수 있다.
class Person {
  // 생성자
  constructor(name) {
    this.name = name
  }
  
  // 프로토타입 메서드
  sayHi() {
    // 인스턴스로 호출
    console.log(`Hi ${this.name}`);
  }
  
  // 정적 메서드
  static sayHello() {
    // 클래스로 호출
  	console.log(`Hello ${this.name}`);
  }
}

const me = new Person("Lee");

console.log(me.name);
me.sayHi();
Person.sayHello();

// 결과
"Lee"
"Hi Lee"
"Hello Person"

25-2-1. 생성자 함수와 클래스 정의 비교

  • 왼쪽은 생성자 함수고, 오른쪽은 클래스이다.

25-3. 클래스 호이스팅 생략

25-4. 인스턴스 생성

  • 클래스는 생성자 함수의 일종이며 new 연산자와 함께 호출되어 인스턴스를 생성한다.
class Person {}

const me = new Person();
console.log(me);
  • 클래스는 반드시 new 연산자와 함께 호출해야 한다. 그렇지 않으면 다음과 같은 에러가 발생한다.
class Person {}

const me = Person();

// 결과
"Uncaught TypeError: Class constructor Person cannot be invoked without 'new'"

25-5. 메서드

  • 클래스 몸체에서 정의할 수 있는 메서드는 constructor(생성자), 프로토타입 메서드, 정적 메서드 세 가지가 있다.

25-5-1. constructor

  • 생성자(constructor) 는 인스턴스를 생성하고 초기화하기 위한 특수한 메서드이다.
class Person {
  constructor(name) {
    // 인스턴스 생성 및 초기화
    this.name = name;
  }
}
  • 위 코드를 브라우저 개발자도구 콘솔에서 찍어보면 다음과 같다.
console.log(typeof Person);		// function
console.dir(Person);

  • 클래스는 평가되어 함수 객체가 되고 함수 객체는 고유의 프로퍼티를 가지고 있다.
  • constructor의 특징은 다음과 같다.
  1. constructor는 클래스 내의 최대 한 개만 존재할 수 있다. 2개 이상일 경우 문법 에러가 발생한다.
class Person {
  constructor() {}
  constructor() {}
}

// 결과
"Uncaught SyntaxError: A class may only have one constructor"
  1. constructor는 생략할 수 있다. 생략하면 빈 constructor가 암묵적으로 정의된다.
class Person {}
  1. 인스턴스를 생성할 때 클래스 외부에서 인스턴스 프로퍼티의 초기값을 전달하려면 constructor에 매개변수를 선언하고 인스턴스를 생성할 때 초기값을 전달하면 된다.
class Person {
  constructor(name, address) {
    this.name = name;
    this.address = address;
  }
}

const me = new Person("Lee", "Seoul");
console.log(me);
  1. constructor는 별도의 반환문을 갖지 않아야 한다. new 연산자와 함께 클래스가 호출되면 암묵적으로 this(인스턴스)를 반환하기 때문이다.
class Person {
  constructor(name) {
    this.name = name;
    
    return {};
  }
}

const me = new Person("Lee");
console.log(me);
  • 위 코드에서 constructor 내부에서 명시적으로 다른 값을 반환하는 것은 클래스의 기본 동작을 훼손하는 행위이므로 return문을 반드시 생략해야 한다.

25-5-2. 프로토타입 메서드

  • 클래스 몸체에서 정의한 메서드는 생성자 함수와 달리 prototype 프로퍼티에 메서드를 추가하지 않아도 기본적으로 프로토타입 메서드가 된다.
class Person {
  constructor(name) {
    this.name = name;
  }
  
  sayHi() {
    console.log(`my name is ${this.name}`);
  }
}

const me = new Person("Lee");
me.sayHi();

// 결과
"my name is Lee"

25-5-3. 정적 메서드

  • 정적 메서드는 인스턴스를 생성하지 않아도 호출할 수 있는 메서드이다.
class Person {
  constructor(name) {
    this.name = name;
  }
  
  static sayHi() {
    console.log(`my name is ${this.name}`);
  }
}

Person.sayHi();

// 결과
"my name is Person"

25-5-4. 클래스에서 정의한 메서드의 특징

  1. function 키워드를 생략한 메서드 축약 표현을 사용한다.
  2. 객체 리터럴과는 다르게 클래스에 메서드를 정의할 때는 콤마가 필요 없다.
  3. 암묵적으로 strict mode로 실행된다.
  4. for ... in 문이나 Object.keys 메서드 등으로 열거할 수 없다.
  5. 내부 메서드 [[Construct]] 를 갖지 않는 non-constructor이다. 따라서, new 연산자와 함께 호출할 수 없다.

25-6. 클래스의 인스턴스 생성 과정

1. 인스턴스 생성과 this 바인딩

  • new 연산자 함께 클래스를 호출하면 암묵적으로 빈 객체가 생성된다. 이 객체는 클래스가 생성한 인스턴스가 될 객체이다.
  • 클래스가 생성한 인스턴스의 프로토타입으로 클래스의 prototype 프로퍼티가 가리키는 객체가 설정된다.
  • 생성된 객체 즉, 인스턴스는 this에 바인딩된다. 따라서, constructor 내부의 this는 인스턴스를 가리킨다.

2. 인스턴스 초기화

  • constructor의 내부 코드가 실행되어 this에 바인딩되어 있는 인스턴스를 초기화한다.
  • this에 바인딩되어 있는 인스턴스에 프로퍼티를 추가하고 constructor가 인수로 전달받은 값으로 인스턴스의 프로퍼티 값을 초기화한다.

3. 인스턴스 반환

  • 클래스의 모든 처리가 끝나면 완성된 인스턴스가 바인딩된 this를 암묵적으로 반환한다.
class Person {
  constructor(name) {
    // 1. 암묵적으로 인스턴스가 생성되고 this에 바인딩된다.
    console.log(this);
    console.log(Object.getPrototypeOf(this) === Person.prototype);
    
    // 2. this에 바인딩되어 있는 인스턴스를 초기화한다.
    this.name = name;
    console.log(this);
    
    // 3. this가 암묵적으로 반환된다.
  }
}

const me = new Person("james");

// 결과
Person {}
true
Person {name: 'james'}

25-7. 프로퍼티

25-7-1. 인스턴스 프로퍼티

  • 인스턴스 프로퍼티는 constructor 내부에서 정의해야 한다.
class Person {
  constructor(name) {
    // 인스턴스 프로퍼티
    this.name = name;
  }
}

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

25-7-2. 접근자 프로퍼티

  • 접근자 프로퍼티는 자체적으로 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티이다.
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('heedo', 'na');

console.log(`${me.firstName} ${me.lastName}`);

me.fullName = "yijon back";
console.log(me.fullName);

// 결과
"heedo na"
"yijon back"
  • 접근자 프로퍼티는 getter 함수와 setter 함수로 구성되어 있다.
  • getter는 인스턴스 프로퍼티에 접근할 때마다 프로퍼티 값을 조작하거나 별도의 행위가 필요할 때 사용한다.
  • setter는 인스턴스 프로퍼티에 값을 할당할 때마다 프로퍼티 값을 조작하거나 별도의 행위가 필요할 때 사용한다.

25-7-3. 클래스 필드 정의 제안

  • 클래스 필드(class field)는 클래스 기반 객체지향 언어에서 클래스가 생성할 인스턴스의 프로퍼티를 말하며 클래스 멤버라고도 부른다.
  • 자바스크립트의 클래스에서 인스턴스 프로퍼티를 선언하고 초기화하려면 반드시 constructor 내부에서 this에 프로퍼티를 추가해야 한다.
class Person {
  name = "Lee";
}

const me = new Person("Lee");
  • 그렇다면 this에 프로퍼티를 추가하지 않은 위의 코드는 에러가 발생할까?
  • 발생할 수도 있고 아닐 수도 있는데 최신 브라우저 또는 최신 Node.js(12 버전 이상)에서 실행하면 정상적으로 잘 동작한다.
  • 클래스 몸체에서 클래스 필드를 정의할 수 있는 클래스 필드 정의 제안은 ECMAScript의 정식 표준 사양으로 승급되지 않았지만 최신 브라우저 및 Node.js는 표준 사용으로 승급이 확실시되는 이 제안을 선제적으로 미리 구현해 놓았다.
  • 그렇다면 클래스 필드는 언제 사용할까?
class Person {
  // 클래스 필드 정의
  name;
  
  constructor(name) {
    this.name = name;
  }
  
  getName = function () {
  	return this.name;
  }
}

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

console.log(me.getName());

// 결과
Person {name: "Lee"}
"Lee"
  • 위 코드와 같이 인스턴스를 생성할 때 외부의 초기값으로 클래스 필드를 초기화해야 한다면 굳이 constructor 밖에서 클래스 필드를 정의할 필요가 없다.
  • 어차피 클래스가 생성한 인스턴스에 클래스 필드에 해당하는 프로퍼티가 없다면 자동으로 추가되기 때문이다.

인스턴스 프로퍼티를 정의하는 방식

  • 인스턴스를 생성할 때
    • 외부 초기값으로 클래스 필드를 초기화할 필요가 있다면 constructor에서 인스턴스 프로퍼티를 정의하는 기존 방식을 사용한다.
    • 외부 초기값으로 클래스 필드를 초기화할 필요가 없다면 기존 constructor에서 인스턴스 프로퍼티를 정의하는 방식과 클래스 필드 정의 제안 모두 사용할 수 있다.

25-7-4. private 필드 정의 제안

  • 자바스크립트는 다른 객체지향 언어와 달리 private, protected 키워드와 같은 접근 제한자를 제공하지 않는 등 캡슐화를 완벽하게 지원하지 않는다.
  • 인스턴스 프로퍼티는 인스턴스를 통해 클래스 외부에서 언제나 참조할 수 있으므로 public하며 외부에 그대로 노출될 수 있다.
  • 다행히도 최신 브라우저와 Node.js에 private 필드를 정의할 수 있는 새로운 표준 사양이 제안되어 있다.
class Person {
  #name = "";
  
  constructor(name) {
    this.#name = name;
  }
}

const me = new Person("Lee");
console.log(me.#name);
            
// 결과
"Uncaught SyntaxError: Private field '#name' must be declared in an enclosing class"
  • private 필드의 선두에는 #을 붙여준다. 또한, 참조할 때에도 #을 붙어주어야 한다.
  • private 필드는클래스 내부에서만 참조할 수 있다. 다만, 접근자 프로퍼티를 통해 간접적으로 접근하는 방법이 있다.
class Person {
  #name = '';
  
  constructor(name) {
    this.#name = name;
  }

  get getName() {
    return this.#name.trim();
  }
}

const me = new Person("Lee");
console.log(me.getName);

// 결과
"Lee"

25-7-5. static 필드 정의 제안

  • 클래스에는 static 키워드를 사용하여 정적 메서드를 정의할 수 있지만 static 키워드를 사용하여 정적 필드를 정의할 수는 없다.
  • 하지만, 최신 브라우저 및 Node.js에는 static public 필드, static private 필드, static private 메서드를 정의할 수 있게 되었다.
class MyMath {
  // static public 필드 정의
  static PI = 21 / 7;

  // static private 필드 정의
  static #num = 10;
  
  // static 메서드
  static increment() {
    return ++Mymath.#num;
  }
  
  console.log(MyMath.PI);
  console.log(MyMath.increment());
}
profile
개발자가 되고 싶은 개발자

0개의 댓글