자바스크립트 딥다이브 - 클래스

ChoiYongHyeun·2023년 12월 17일
0

고난과 어려움의 연속 ..,.

클래스

자바스크립트는 프로토타입 기반 객체 지향 언어이다. 따라서 클래스가 필요 없는 객체 지향 프로그래밍 언어다.

const Person = (function () {
  function Person(name) {
    this.name = name;
  }

  Person.prototype.introduce = function introduce() {
    console.log(`hi i am ${this.name}`);
  };

  return Person;
})();

let tom = new Person('tom');
let jerry = new Person('jerry');
tom.introduce(); // hi i am tom
jerry.introduce(); // hi i am jerry
console.log(tom.hasOwnProperty('name')); // true

클래스 없이도 객체를 만들고 prototype 과 체이닝을 통해 상속이 가능한 객체를 생성 할 수 있었다.

하지만 다른 클래스 기반 객체지향 프로그래밍에 익숙한 프로그래머가 더 빠르게 학습 할 수 있도록 클래스 기반 객체지향 프로그래밍 언어와 유사한 새로운 객체 생성 메커니즘을 제시하였는데, 그것이 바로 클래스 이다.

야호

클래스 정의

예시를 먼저 보자

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

  introduce() {
    console.log(`hi i am ${this.name}`);
  }

  static sayHi() {
    console.log('hi~!');
  }
}

let tom = new Person('tom');
Person.sayHi(); // hi~!
tom.introduce(); // hi i am tom
tom.sayHi(); // TypeError: tom.sayHi is not a function

내부 구조와 사용된 코드들을 보면

class 클래스명 {construdctor 객체 , 메소드 , static 메소드} 가 사용된 것을 볼 수 있다.

이 때 static 메소드 로 선언된 것들은 Person 함수에서만 사용 가능하고 인스턴스에서는 접근 불가능한 정적 메소드임을 알 수 있다.

클래스는 전처럼 함수 선언문 형식으로 사용하여도 되고

const MakePerson = class Person {
  ...
}

함수 표현식 형식으로 사용해도 된다.

함수 표현식 형식으로 사용해도 된다는 것은 클래스 가 값으로 평가되는 표현식이라는 것이며

이는 클래스 또한 함수 (이면서 객체) 라는 것을 의미한다.

그럼 클래스의 몸체({}) 내부에 있는 것들을 하나씩 살펴보자

constructor(){} 객체

// 클래스 몸체
constructor(name) {
    this.name = name;
  }

// 생성자 함수
  function Person(name) {
    this.name = name;
  }

constructor(){} 쓰인 것은 생성할 인스턴스의 값을 생성하고 설정하는 것으로 생성자 라고도 불린다.

function Person(name){this.name = name} 이런 식으로 생성자 함수를 이용하여 인스턴스를 생성하는 과정이 new Person() 으로 호출 되었을 때 비어있는 인스턴스를 생성하고 , 해당 인스턴스를 Person 생성자 함수의 this 와 바인딩 하여 프로퍼티들을 생성해준 것처럼

class Person {constructor(name){...}} 으로 인스턴스를 생성하는 것도 똑같다.

new class(name) 으로 호출되었을 때 빈 인스턴스를 만든 후 만든 빈 인스턴스를 클래스 내에 존재하는 this 와 바인딩한다.

이후 바인딩 된 thisconstructor 내부에서 this.name = name 으로 설정해줘 인스턴스의 프로퍼티를 설정하는 것이다.

이처럼 constructor(){...} 은 인스턴스의 프로퍼티 값을 초기화하고 할당해주기 때문에 생성자 라고 불린다.

[[constructor]] 내부 슬롯과 상관이 있는걸까 ?

이름은 동일하지만 다르다. 생성자 로 쓰이는 것은 인스턴스의 프로퍼티를 설정해주는 것이고
객체의 내부 메소드인 [[constructor]] 는 객체가 생성된 생성자 함수를 가리키는 것이다.

constructor(){} 는 생성자 함수가 생성할 인스턴스를 초기화 해주는 생성자 라고 하였다.

그렇기 때문에 생성자 는 생략하거나 2개이상 존재 할 수 없다.

2개 이상 작성 할 경우엔 오류가 나고, 생략할 경우엔 암묵적으로 constructor(){} 처럼 빈 생성자가 정의된다.

프로토타입 메소드

생성자 함수에서 프로토타입 메소드를 추가해주기 위해서는 다음처럼 명시적으로 프로토타입에 메소드를 추가해야 했다.

function Person(name) {
  this.name = name;
}

Person.prototype.introduce = function introduce() {
  console.log(`hi i am ${this.name}`);
};

하지만 클래스를 이용하면 명시적으로 추가해줄 필요 없이 클래스 몸체 내부에서 선언 할 수 있다.

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

  introduce() {
    console.log(`hi i am ${(this, name)}`);
  }
}

더 많은 프로토타입 메소드를 추가하는 것도 가능하다.

이 때 프로토타입 메소드를 추가 할 때 , 를 작성하지 않는다.

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

  introduce() {
    console.log(`hi i am ${(this, name)}`);
  }

  sayHi() {
    console.log('hi~!!');
  }
}

다음처럼 함수 몸체에 작성된 메소드는 자동으로 prototype 의 메소드로 들어간다.

정적 메소드

정의

정적 메소드란 클래스의 인스턴스가 아닌 클래스 자체에 속하는 메소드로 인스턴스에서는 호출 할 수 없지만 클래스에서 호출이 가능하다.

정적 메소드를 사용하기 위해선 정적 메소드임을 표현하기 위해 static 을 통해 표현하는 것이 가능하다.

위에서 sayHi 메소드는 인스턴스의 프로퍼티를 사용하지 않기 때문에 정적 메소드로 변경해보자

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

  introduce() {
    console.log(`hi i am ${this.name}`);
  }

  static sayHi() {
    console.log('hi~!!');
  }
}

let tom = new Person('tom');
Person.sayHi(); // hi~!!
tom.sayHi(); // TypeError

정적 메소드와 프로토타입의 메소드를 테이블 형태로 표현하면 다음과 같다.

정적 메소드프로토타입 메소드
호출 방법클래스 이름을 통한 호출 (예: ClassName.method() )인스턴스를 통한 호출 (예: instance.method() )
사용 예시javascript MathUtility.add(5, 3); javascript const obj = new MyClass(); obj.method();
this 키워드 사용클래스 자체를 가리킴 (예: this.constructor)인스턴스를 가리킴 (예: this.property)
인스턴스에 의존성의존하지 않음인스턴스에 의존함
호출 시 생성 여부인스턴스 생성 없이 직접 호출 가능인스턴스를 생성한 후 호출 가능
예시 코드javascript class MathUtility { static add(x, y) { return x + y; } } MathUtility.add(5, 3); javascript class MyClass { method() { /* some logic */ } } const obj = new MyClass(); obj.method();

정적 메소드와 프로토타입 프로토타입 체인이 다르기 때문에 서로 가리키는 this 도 다르며 인스턴스 의존성도 다르다.

정적 메소드가 가리키는 this 는 클래스이며 , 프로토타입 메소드가 가리키는 this 는 생성한 인스턴스이다.

클래스 필드 정의 제안

클래스 필드란 클래스 기반 객체지향 언어에서 클래스가 생성할 인스턴스의 프로퍼티를 가리키는 언어이다.

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

  introduce() {
    console.log(`hi i am ${this.name} and i lived in ${this.address}`);
  }
}

let tom = new Person('tom');
tom.introduce(); // hi i am tom and i lived in korea
console.log(tom.address);

만약 생성 할 때 매개변수를 받지 않고 기본적인 프로퍼티들을 정의하려면 constructor(){} 내부에서 this 로 인스턴스를 지정하고 값을 정의했어야 했다. (this.address = 'korea' 가 그 예시)

하지만 최신 버전의 브라우저에서는 그럴 필요 없이 기본적인 인스턴스의 프로퍼티를 설정할 영역인 클래스 필드 를 정의하고, 그 안에서는 = 연산자를 통해 지정해줄 수 있다.

class Person {
  // 클래스 필드
  address = 'korea';

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

클래스 필드에서 지정 할 때에는 this 를 사용하지 않는다.

그 이유는 클래스 필드에서 호출되는 this 는 인스턴스가 아닌 클래스 자체를 가리키기 때문이다.

thisconstructor(){} 내부에서만 유효하다.

private 필드 정의 제안

캡슐화와 정보 은닉에서 살펴 보았듯이 자바스크립트에서는 완벽한 정보 은닉을 제공하지 않는다.

정보 은닉을 위해서 이전에서 클로저 를 이용하여 정보를 은닉하는 불편함을 겪었다.

const Person = (function () {
  let _age; // 숨기고 싶은 프로퍼티를 생명주기가 짧은 렉시컬 환경에 생성
  
  function Person(name, age) {
    this.name = name;
    _age = age; // 생명주기가 짧은 외부 함수의 프로퍼티를 참조하도록 설정하여 은닉
  }

  Person.prototype.introduce = function () {
    console.log(`hi i am ${this.name} and ${_age} years old`);
  };

  return Person;
})();

let tom = new Person('tom', 20);
tom.introduce(); // hi i am tom and 20 years old
console.log(tom.age); // undefined
console.log(tom._age); // undefined
console.log(Person._age); // undefined

하지만 최신 브라우저와 최신 Node.js 에서는 private 기능이 구현 되어 있다.

private 필드 를 정의해주고 그 안에서 # 을 사용해주면 된다.

와우

class Person {
  // private 필드 정의
  #age;

  constructor(name, age) {
    this.name = name;
    this.#age = age;
  }

  introduce() {
    console.log(`hi i am ${this.name} and ${this.#age} years old`);
  }
}

let tom = new Person('tom', 20);
tom.introduce(); // hi i am tom and 20 years old
console.log(tom.#age); // SyntaxError: Private field '#age' must be declared in an enclosing class

이처럼 private 로 정의해준 프로퍼티는 클래스 함수 내에서만 접근 가능하며 인스턴스에서의 접근이 불가능하다.

하지만 메소드로의 접근은 가능하기에 필요한 기능을 제공하면서, 직접적인 접근을 제한 할 수 있다.

class Person {
  // private 필드 정의
  #age;

  constructor(name, age) {
    this.name = name;
    this.#age = age;
  }

  introduce() {
    console.log(`hi i am ${this.name} and ${this.#age} years old`);
  }

  getAge() {
    return this.#age;
  }
}

let tom = new Person('tom', 20);
console.log(tom.getAge()); // 20
클래스 내부 자식 클래스 내부 클래스 인스턴스를 통한 접근
퍼블릭 필드 ✔️ 직접 접근 가능 ✔️ 직접 접근 가능 ✔️ 직접 접근 가능
프라이빗 필드 ✔️ 직접 접근 가능 ❌ 직접 접근 불가능 ❌ 직접 접근 불가능

다음과 같은 특징을 가지고 있기 때문에 프라이빗 필드로 선언된 프로퍼티는 클래스 내부에서 선언된 메소드로 접근 하는 것은 가능하지만 그 외로는 불가능하다.

인스턴스 프로퍼티에만 국한되는 것이 아니라 정적 프로퍼티에도 pirvate 필드를 선언하는게 가능하다.

  //프라이빗 정적 프로퍼티 설정 
  static #Hi = 'hellalilu';
  // 정적 메소드 선언
  static sayHi() {
    console.log(this.#Hi);
  }
}

Person.sayHi(); // hellalilu
console.log(Person.#Hi); // SyntaxError: Private field '#Hi' must be 
           				 //declared in an enclosing class

상속에 의한 클래스 확장

클래스 상속과 생성자 함수 상속

상속에 의한 클래스 상속은 기존 클래스를 상속받아 새로운 클래스를 확장 하는 것이다.

생긴것만 먼저 보자

사람과 한국인을 추상화한 클래스 Person , Korean 클래스를 만들어보자

class Person {
  // 프라이빗 필드 정의
  #name;
  #age;

  // 생성자 정의
  constructor(name, age) {
    this.#name = name;
    this.#age = age;
  }

  // 프로토타입 메소드 정의
  getName() {
    return this.#name;
  }

  getAge() {
    return this.#age;
  }

  sayHi() {
    console.log(`hi i am ${this.#name} and ${this.#age} years old`);
  }
}

class Korean extends Person {
  constructor(name, age, address) {
    super(name, age);
    this.address = address;
  }

  sayHi() {
    console.log(
      `hi i am ${super.getName()} and ${super.getAge()} years old` +
        `and i lived in ${this.address}`,
    ); // 프로토타입 오버라이딩
  }
  
    playGangnamStype() {
    console.log('dance dance');
  } // 새로운 프로토타입 메소드 추가
}

let leedongdong = new Korean('leedongdong', 20, 'korea');

leedongdong.sayHi(); // hi i am leedongdong and 20 years old and i lived in korea

코드를 살펴보면 Person이라는 클래스가 있고 Korean 이라는 클래스가 있는 것으로 보인다.

이 때 Korean 클래스를 생성 할 때 extends Person 이라는 문구가 추가 되었고

익숙치 않은 super 이라는 단어가 보인다.

벌써 어질어질하다.

이처럼 클래스들은 클래스끼리 프로퍼티와 메소드를 상속 할 수 있고 상속 받은 클래스에서는 상속 받은 프로토타입을 오버라이딩 하거나 새로운 값을 추가할 수 있다.

extends

상속 받을 클래스 extends 상속 할 클래스 를 통해 클래스 간 상속이 가능하다.

이처럼 상속을 통해 확장된 클래스를 서브 클래스, 상속한 클래스를 슈퍼 클래스라 부른다.

또는 서브 클래스를 파생 클래스 또는 자식 클래스

슈퍼 클래스를 베이스 클래스 또는 부모 클래스라고 한다.

extends 는 부모 클래스와 자식 클래스 간 상속 관계를 설정하는 걸로, 프로토타입을 통해 상속 관계를 구현한다.

자식 클래스는 부모 클래스의 프로토타입 메소드 , 정적 메소드 모두 상속 받는데, 이런 상속은 프로토타입 체인을 통해 일어난다.

동적 상속

extends 는 클래스 뿐이 아니라 [[constructor]] 를 내부 슬롯으로 갖는 생성자 함수 객체면 모두 사용 가능하다 .

그렇기 때문에 삼항 연산자를 이용하여 조건에 따라 클래스를 상속 시키는 것도 가능하다.

클래스도 일급 객체이다.

자식 클래스의 constructorsuper

코드에서 보면

class Korean extends Person {
  constructor(name, age, address) {
    super(name, age);
    this.address = address;
  }

가 보이는데 익숙치 않다.

super는 함수처럼 호출 될 수도 있고 this와 같이 식별자처럼 참조 할 수 있는 특수한 키워드이다.

  • super 를 참조하면 부모 클래스의 메소드를 호출 할 수 있다.

super 호출

super를 호출하면 부모 클래스의 constructor 를 호출한다.

let leedongdong = new Korean('leedongdong', 20, 'korea');

이렇게 객체를 생성하면 자바스크립트 엔진은 Korean 의 몸체에 들어가 Constructor(){} 부분으로 인스턴스를 생성하고 초기화 할 것이다.

class Korean extends Person {
  constructor(name, age, address) {
    super(name, age); // Person 의 constructor(name ,age){...} 시행
    this.address = address; // 위에서 생성된 인스턴스에 address 라는 프로퍼티 동적 추가
  }

이 부분을 보면 자식 클래스의 constructor 를 만들 때 우선적으로 super(name , age) 를 한 모습을 볼 수 있다.

super(name, age) 가 보이면 자바스크립트 엔진은 부모 클래스로 넘어가 부모 클래스의 생성자를 호출한다.

class Person {
  // 프라이빗 필드 정의
  #name;
  #age;

  // 생성자 정의
  constructor(name, age) {
    this.#name = name;
    this.#age = age;
  }

부모 클래스인 constructor(name ,age){} 로 넘어가 인스턴스를 생성하고 프로퍼티 값을 설정한다.

이 때 생성된 인스턴스는 부모 클래스로부터 생성된 인스턴스이다.

이후 부모 클래스로부터의 생성이 끝나면 this.address = address; 이 부분이 실행되어 방금 생성된 인스턴스에 새로운 프로퍼티인 address 를 동적으로 추가하는 것이다.

이렇게 super() 로 호출하는 것은 부모 클래스의 생성자를 호출한다.

super 참조

오케이 위에서 그러면 인스턴스 생성은 끝났다.

그 다음 프로토타입 메소드를 생성하는 과정을 살펴보자

class Korean extends Person {
  
  ...
  
  sayHi() {
    console.log(
      `hi i am ${super.getName()} and ${super.getAge()} years old` +
        `and i lived in ${this.address}`,
    ); // 프로토타입 오버라이딩
  }
  
    playGangnamStype() {
    console.log('dance dance');
  } // 새로운 프로토타입 메소드 추가
}

sayHi 는 부모 클래스에도 있는 프로토타입 메소드이지만 Korean 클래스에서 오버라이딩 한 모습을 볼 수 있다.

sayHi 를 살펴보면 ${super.getName()} 이 존재하는 걸 볼 수 있다.

아까는 super() 를 통해 부모 클래스의 생성자를 호출했다면

super.메소드이름 을 통해 부모클래스 의 프로토타입 메소드에 직접적으로 접근 할 수 있다.

이를 super 참조 라고 한다.

위 예시에서는 super 참조를 통해 이름과 나이를 가져왔다.

super 참조 딥다이브

super 참조가 동작하기 위해서는 super 가 참조하여 불러올 메소드가 super참조 해온 클래스와 바인딩이 되어 있어야 한다.

위에서는 sayHi 메소드가 Person 의 프로토타입으로 바인딩 되어 있어서 Person.sayHi 로 불러오는 것이 가능했다. (엄밀히 말하면 Person.prototype.sayHi)

이 때 바인딩 여부는 선언된 메소드가 [[HomeObject]] 가 해당 클래스를 가리키고 있느냐가 중요한데

메소드 축약 표현으로 정의된 함수만이 [[HomeObject]] 를 갖는다.

그러니 웬만하면 메소드 축약 표현을 사용하자 :)

profile
빨리 가는 유일한 방법은 제대로 가는 것이다

0개의 댓글