반응형
Jake Seo
제이크서 위키 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (715)
    • 일상, 일기 (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 자료구조
  • item9
  • 자바스크립트 인터뷰
  • 작업기억공간
  • try-with-resources
  • 이펙티브자바
  • bean Validation
  • 싱글톤 패턴
  • serverless computing
  • 서버리스 컴퓨팅
  • 자바 검증
  • NEXT JS
  • 자바스크립트
  • 도커공식문서
  • 참조 해제
  • 외래키 제약조건
  • 느린 쿼리
  • 프로그래머의 뇌
  • 객체복사
  • rust
  • 자바스크립트 면접
  • item8
  • 이펙티브 자바
  • 알고리즘
  • 싱글턴
  • prerendering
  • 디자인패턴
  • Javadoc 자바독 자바주석 주석 Comment
  • 슬로우 쿼리
  • 자바
  • Next.js
  • 플라이웨이트패턴
  • 팩터리 메서드 패턴
  • 추상 팩터리 패턴
  • 러스트
  • 빈 검증
  • 메이븐 라이프사이클
  • pnpm
  • Pre-rendering
  • 토비의 스프링
  • 이펙티브 자바 item9
  • Java
  • MSSQL
  • 스프링 검증
  • 자바 디자인패턴
  • item7
  • next js app
  • 메이븐 페이즈
  • 싱글톤
  • 메이븐 골

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 위키 블로그

웹 개발자를 위한 자바스크립트의 모든 것 4장 클래스
자바스크립트/웹개발자를 위한 자바스크립트의 모든 것

웹 개발자를 위한 자바스크립트의 모든 것 4장 클래스

2022. 12. 29. 00:20

자바스크립트 클래스

  • ES6 가 나오기 이전에는 프로토타입을 이용해 클래스처럼 구현하는 것이 가능했다.
  • ES2015(ES6) 가 나온 이후에는 클래스처럼이 아닌 클래스를 구현하는 것이 가능해졌다.

클래스 기본 문법 살펴보기

class Color {
  constructor(r = 0, g = 0, b = 0) {
    this.r = r;
    this.g = g;
    this.b = b;
  }

  get rgb() {
    return `rgb(${this.r}, ${this.g}, ${this.b})`;
  }

  set rgb(value) {
    // TODO ...
  }

  toString() {
    return this.rgb;
  }

  static fromCSS(css) {
    // TODO ...
  }
}

let c = new Color(30, 144, 255); // new 키워드를 통한 생성
c = Color.fromCSS("00A"); // 정적 팩토리 메서드를 통한 생성

클래스는 아래의 요소들을 가진다.

  • 생성자 (constructor)
    • 보통 여기서 유효성 검사까지 많이 한다.
  • 데이터 필드 (r, g, b)
  • 접근자 속성 (get(), set())
  • 프로토타입 메서드 (toString())
    • 인스턴스 메서드라고 하지 않는 이유는 프로토타입에서 상속되기 때문이다.
  • 정적 메서드 (fromCSS)

클래스 정의 방법

// 일반 정의
class Color {}

// 익명 클래스 정의
let Color = class {};

// 클래스 정의를 변수로 참조하기
let C = class Color {};
  • 클래스 선언은 함수 선언처럼 호이스팅 되지 않는다.
  • 임시 데드존과 같이 호이스팅 된다.
  • let, const 와 마찬가지로 전역에서 선언해도 전역 객체의 속성이 아니다.

클래스 생성자의 특징

  • new 키워드를 사용해야만 생성자 호출이 가능하다.
    • 생성자 함수를 직접 호출하려하면, 오류가 발생한다.
    • 사실 Reflect 를 이용하면 호출이 가능하긴 하다.

ES5 에서 함수를 클래스처럼 이용하는 방식은 사실상 new 키워드 없이도 함수호출이 되어 혼란스러운 버그를 초래하기도 했다. this instanceof Color 와 같은 구문을 통해 예외처리를 해주어야만 했다.

Color(); // TypeError: Class constructor Color cannot be invoked without 'new'

클래스 내부 코드의 특징

  • 항상 엄격 모드("strict mode")이다.
  • 클래스 내부 속성을 정의할 때 세미콜론이나 컴마를 쓰지 않는다.
  • 내부 메서드를 선언적이고 간결하게 정의할 수 있다.
    • ES5는 Color.prototype.toString = function toString() {...} 와 같은 방식으로 정의해왔다.

이전보다 메모리 효율이 좋다.

// ES2015 `class` 키워드를 이용한 클래스와 메서드 정의
class Color {
  print() {
    console.log("color");
  }
}

var c = new Color();

console.log(typeof c.print.prototype); // undefined

// ES5 `function` 키워드를 이용한 클래스와 메서드 정의
function ColorFunction() {}
const Color2 = ColorFunction;
Color2.prototype.print = function () {
  console.log("color");
};
var c2 = new Color2();

console.log(typeof c2.print.prototype); // object
  • class 키워드를 이용한 클래스 메서드에는 prototype 이 안붙어있다.
  • function 키워드를 이용한 클래스 메서드에는 prototype 이 붙어있다.

정적 메서드 코드 살펴보기

class Color {
  constructor(r = 0, g = 0, b = 0) {
    this.r = r;
    this.g = g;
    this.b = b;
  }

  get rgb() {
    return `rgb(${this.r}, ${this.g}, ${this.b})`;
  }

  set rgb(value) {
    // TODO ...
  }

  toString() {
    return this.rgb;
  }

  static fromCSS(css) {
    const match = /^#?([0-9a-f]{3}|[0-9a-f]{6});?$/i.exec(css);

    if (!match) {
      throw new Error("invalid CSS code: " + css);
    }

    let vals = match[1];
    if (vals.length === 3) {
      vals = vals[0] + vals[0] + vals[1] + vals[1] + vals[2] + vals[2];
    }

    return new this(
      parseInt(vals.substring(0, 2), 16),
      parseInt(vals.substring(2, 4), 16),
      parseInt(vals.substring(4, 6), 16)
    );
  }
}
  • static 키워드가 붙은 메서드는 개별 인스턴스 메서드가 아닌 Color 클래스 자체의 메서드가 된다.
Color.fromCSS = function fromCSS(css) {};
  • ES5 식으로는 위와 같이 표현된다. 프로토타입 속성이 아님에 유의하자.

접근자 속성 코드 살펴보기

class Color {
  constructor(r = 0, g = 0, b = 0) {
    this.r = r;
    this.g = g;
    this.b = b;
  }

  get rgb() {
    return `rgb(${this.r}, ${this.g}, ${this.b})`;
  }

  set rgb(value) {
    let s = String(value);
    let match = /^rgb\((\d{1, 3}),(\d{1, 3}), (\d{1,3})\)$/i.exec(
      s.replace(/\s/g, "")
    );

    if (!match) {
      throw new Error(`Invalid rgb color string '${s}'`);
    }
  }

  toString() {
    return this.rgb;
  }

  static fromCSS(css) {
    const match = /^#?([0-9a-f]{3}|[0-9a-f]{6});?$/i.exec(css);

    if (!match) {
      throw new Error("invalid CSS code: " + css);
    }

    let vals = match[1];
    if (vals.length === 3) {
      vals = vals[0] + vals[0] + vals[1] + vals[1] + vals[2] + vals[2];
    }

    return new this(
      parseInt(vals.substring(0, 2), 16),
      parseInt(vals.substring(2, 4), 16),
      parseInt(vals.substring(4, 6), 16)
    );
  }
}

let c = new Color();
r.rgb = "rgb(30, 144, 255)";

ES5

ES5 에서는 아래와 같이 작성해야 했다.

Object.defineProperty(Color.prototype, "rgb", {
  get: function () {
    return `${this.r}, ${this.g}, ${this.b}`;
  },
  set: function (value) {
    // ...
  },
  configurable: true,
});

static 접근자

원한다면 정적인 접근자 속성 정의도 가능하다.

class StaticAccessorExample {
  static get cappedClassName() {
    return this.name.toUpperCase();
  }
}

계산된 메서드 이름

런타임에 결정되는 이름으로 메서드를 만들고 싶을 때 유용하다.

let name = "foo" + Math.floor(Math.random() * 1000);
class SomeClass {
  [name]() {
    // ...
  }
}
  • 대괄호 안에 어떤 표현이든 넣을 수 있다.
  • 클래스 정의가 평가되는 시점에 식이 평가된다.
  • 결과가 문자열이나 심볼이 아닌 경우, 문자열로 반환된다.
  • 결과가 메서드 이름으로 사용된다.

정적 메서드

class Guide {
  static [6 * 7]() {
    // Guide["42"]() -> logging "GOOD"
    console.log("GOOD!");
  }
}

ES5

var name = "foo" + Math.floor(Math.random() * 1000);
SomeClass.prototype[name] = function () {
  // ...
};

서브클래스

  • ES5 에서 상속을 구현하는 방식은 꽤 복잡했다.
  • class 문법이 생기면서 이러한 부분이 많이 개선되었다.
class ClassWithAlpha extends Color {}

extends 를 이용하면, 두 개의 상속 체인이 생성된다. 해당 생성자로 생성된 객체에 대한 병렬상속관계라고 한다. 하나는 생성자 자체에 있으며, 하나는 생성자의 프로토타입 객체에 있다.

생성자 상속 체인

  • ColorWithAlpha 서브클래스 생성자를 만든다.
    • Color (슈퍼클래스 생성자 함수) Color 의 프로토타입을 만들어 모든 정적 속성/메서드를 ColorWithAlpha 에서 접근 가능하게 한다.
    • Function.prototype 이외의 프로토타입을 갖는 함수의 개념이 등장한다.

프로토타입 체인

  • 서브클래스 프로토타입 객체인 ColorWithAlpha.prototype 을 만든다.
    • Color.prototype 을 객체의 프로토타입으로 만들어 ColorWithAlpha 로 만든 객체가 슈퍼클래스 속성과 메서드를 상속하도록 한다.
  • 생성자 상속 체인은 ES2015 의 새로운 기능이다.

두 개의 체인 살펴보기

  • ColorWithAlpha -> Color -> Function.prototype -> Object.prototype
  • ColorWithAlpha.prototype -> Color.prototype -> Object.prototype
const color = new Color(10, 10, 10);
Object.getPrototypeOf(color); // {constructor: ƒ, toString: ƒ}constructor: class Colorrgb: (...)toString: ƒ toString()get rgb: ƒ rgb()set rgb: ƒ rgb(value)[[Prototype]]: Object
Object.getPrototypeOf(color.__proto__); // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}

const colorWithAlpha = new ColorWithAlpha(10, 10, 10, 1);
Object.getPrototypeOf(colorWithAlpha); // Color {constructor: ƒ}
Object.getPrototypeOf(colorWithAlpha.__proto__); // {constructor: ƒ, toString: ƒ}
  • Object.getPrototypeOf() 메서드로 살펴볼 수 있다.

super 키워드

super: 서브클래스 생성자에서 상위 클래스의 생성자를 호출하거나 상위 클래스의 속성 및 메서드를 참조하는데 사용할 수 있다.

서브 클래스의 기본(자동 생성) 생성자

명시적 생성자가 없더라도, 슈퍼 클래스의 생성자를 기본적으로 호출한다. 많은 경우에 서브 클래스에 직접 생성자를 작성할 필요가 없다.

// 서브 클래스에 자동 생성된 생성자
constructor(/* 인수들 */) {
  super(/* 전달 */)
}

ES5 에서는 아래와 같은 과정을 거쳐야 했다.

var ColorWithAlpha = function ColorWithAlpha() {
  Color.apply(this, arguments);
};

ColorWithAlpha.prototype = Object.create(Color.prototype);
ColorWithAlpha.prototype.constructor = ColorWithAlpha;
  • ColorWithAlpha 에서 Color 의 정적 속성과 메서드를 사용할 수 없었다.
  • 실수하기 쉬운 코드이다.

서브 클래스의 커스텀 생성자

class ColorWithAlpha extends Color {
  constructor(r = 0, g = 0, b = 0, a = 1) {
    super(r, g, b);
    this.a = a; // super() 뒤에 와야 에러가 나지 않는다.
  }
}
  • super() 를 통해 상위클래스의 생성자를 먼저 호출해야 this 를 사용할 수 있다.
    • 엔진마다 다른 에러가 난다.
    • ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
    • ReferenceError: Must call super constructor before using |this| in ColorWithAlpha class constructor
    • ReferenceError: this is not defined
  • 당연히 super() 를 두번 호출해도 에러가 난다.

ES5 에서는 아래와 같이 작성할 수 있었다.

var ColorWithAlpha = function ColorWithAlpha(r, g, b, a) {
  this.a = a; // 여기서는 클래스를 먼저 생성하지 않아도 에러가 나지 않는다.
  Color.call(this, r, g, b);
};

서브 클래스의 슈퍼 클래스 메서드 접근

ES 2015

class Color {
  // ...

  brightness() {
    return Math.sqrt(
      (this.r * this.r * 0.299) +
      (this.g * this.g * 0.587) +
      (this.b * this.b * 0.114) +
    )
  }
}

먼저, 슈퍼 클래스의 내용은 위와 같다.

사람의 눈이 빨, 녹, 파의 밝기를 다르게 인식하여 상수가 각각 다르다.

class ColorWithAlpha extends Color {
  // ...

  brightness(bgColor) {
    let result = super.brightness() * this.a;

    if (bgColor && this.a !== 1) {
      result = (result + bgColor.brightness() * (1 - this.a)) / 2;
    }

    return result;
  }
}
  • 밝기 정의는 불투명도를 고려해야 하는 ColorWithAlpha 에서는 조금 다르게 동작해야 한다.
  • Color 클래스의 brightness() 를 이용하여, ColorWithAlpha 의 brightness() 메서드를 구현했다.

ES5

ColorWithAlpha.prototype.brightness = function brightness(bgColor) {
  var result = Color.prototype.brightness.call(this) * this.a;
  if (bgColor && this.a !== 1) {
    result = (result + bgColor.brightness() * (1 - this.a)) / 2;
  }

  return result;
};
  • super 로 간단하게 표현할 수 있는 부분을 길게 적었어야 했다.
var superproto = Object.getPrototypeOf(ColorWithAlpha.prototype);
var result = superproto.brightness.call(this) * this.a;
  • 위와 같이 Object.getPrototypeOf() 를 이용하여 참조해도 된다.

정적 메서드 상속

class Color {
  // 이전 내용과 동일...
}

class ColorWithAlpha extends Color {
  // 이전 내용과 동일...
}

const ca = ColorWithAlpha.fromCSS("#1E90FF");
console.log(String(ca)); // "rgba(30, 144, 255, 1)"
console.log(ca.constructor.name); // "ColorWithAlpha"
console.log(ca instanceof ColorWithAlpha); // true
  • 여기서 사용된 fromCSS() 는 Color 클래스의 정적메서드인데 어떻게 ColorWithAlpha 클래스를 생성했을까?
    • 이전에 배웠던, 생성자 상속 체인 덕에 가능하다.
  • ColorWithAlpha -> Color -> Function.prototype -> Object.prototype
  • ColorWithAlpha.prototype -> Color.prototype -> Object.prototype
class Color {
  // ...

  static fromCSS(css) {
    const match = /^#?([0-9a-f]{3}|[0-9a-f]{6});?$/i.exec(css);

    if (!match) {
      throw new Error("invalid CSS code: " + css);
    }

    let vals = match[1];
    if (vals.length === 3) {
      vals = vals[0] + vals[0] + vals[1] + vals[1] + vals[2] + vals[2];
    }

    return new this(
      parseInt(vals.substring(0, 2), 16),
      parseInt(vals.substring(2, 4), 16),
      parseInt(vals.substring(4, 6), 16)
    );
  }
}
  • new Color 가 아닌 new this() 를 이용했기 때문에 ColorWithAlpha 에서 호출한 경우, 새 ColorWithAlpha 를 반환한다.
  • ColorWithAlpha 에서의 this 는 ColorWithAlpha 이다.

새 인스턴스를 반환하는 메서드

  • Array.prototype 의 slice, map 과 같은 메서드는 새 인스턴스를 생성한다.
  • 이전의 fromCSS 도 new this() 를 통해 새로운 인스턴스를 생성했다.
class Color {
  constructor(r = 0, g = 0, b = 0) {
    this.r = r;
    this.g = g;
    this.b = b;
  }

  static fromCSS(css) {
    const match = /^#?([0-9a-f]{3}|[0-9a-f]{6});?$/i.exec(css);

    if (!match) {
      throw new Error("invalid CSS code: " + css);
    }

    let vals = match[1];
    if (vals.length === 3) {
      vals = vals[0] + vals[0] + vals[1] + vals[1] + vals[2] + vals[2];
    }

    return new this(
      parseInt(vals.substring(0, 2), 16),
      parseInt(vals.substring(2, 4), 16),
      parseInt(vals.substring(4, 6), 16)
    );
  }

  halfBright() {
    const ctor = this.constructor || Color;
    return new ctor(
      Math.round(this.r / 2),
      Math.round(this.g / 2),
      Math.round(this.b / 2)
    );
  }
}

class ColorSubclass extends Color {}
  • 새 인스턴스가 반환하는 타입에 집중해보자.
  • ColorSubclass.fromCSS() 와 colorSubclass.halfBright() 은 각각 ColorSubclass 타입을 반환하게 될 것이다.

[Symbol.species] 를 이용하여 일괄적으로 타입 변경해보기

class Color {
  constructor(r = 0, g = 0, b = 0) {
    this.r = r;
    this.g = g;
    this.b = b;
  }

  static fromCSS(css) {
    const match = /^#?([0-9a-f]{3}|[0-9a-f]{6});?$/i.exec(css);

    if (!match) {
      throw new Error("invalid CSS code: " + css);
    }

    let vals = match[1];
    if (vals.length === 3) {
      vals = vals[0] + vals[0] + vals[1] + vals[1] + vals[2] + vals[2];
    }

    const ctor = (this && this[Symbol.species]) || Color;
    return new ctor(
      parseInt(vals.substring(0, 2), 16),
      parseInt(vals.substring(2, 4), 16),
      parseInt(vals.substring(4, 6), 16)
    );
  }

  halfBright() {
    const ctor =
      (this && this.constructor && this.constructor[Symbol.species]) || Color;
    return new ctor(
      Math.round(this.r / 2),
      Math.round(this.g / 2),
      Math.round(this.b / 2)
    );
  }
}

class ColorSubclass extends Color {
  static get [Symbol.species]() {
    return Color;
  }
}
  • [Symbol.species] 를 통해, 한번에 반환 타입을 변경할 수 있는 패턴을 적용했다.
  • 이 패턴은 표준 라이브러리에서 종종 사용되는데, 보통 정적 메서드에는 사용되지 않는다.
    • Color 를 반환하는 ColorWithAlpha.fromCSS() 같은 오해의 소지가 있는 코드를 작성할 수 있기 때문이다.
  • 이 패턴이 적용된 클래스를 상속하여, 모든 내장 메서드가 어떤 타입을 따르게 할지 오버라이드할 수 있다.

fromCSS() 에서는 this && this[Symbol.species] 를 사용하고, halfBright() 에서는 this && this.constructor && this.constructor[Symbol.species] 을 사용한 이유는 그냥 다양한 케이스를 보여주기 위해서이다. 서로 바꿔도 에러가 나지 않는다.

[Symbol.species] 테스트

class Base {
  constructor(data) {
    this.data = data;
  }

  static get [Symbol.species]() {
    return this;
  }

  // species 패턴을 따르지 않음
  static create(data) {
    const ctor = this || Base;
    return new ctor(data);
  }

  // species 패턴을 따름
  clone() {
    const ctor =
      (this && this.constructor && this.constructor[Symbol.species]) || Base;
    return new ctor(this.data);
  }
}

class Sub1 extends Base {}
class Sub2 extends Base {
  static get [Symbol.species]() {
    return Base;
  }
}

const b = Sub1.create(2); // Sub1
console.log(b);
const bclone = b.clone(); // Sub1
console.log(bclone);

const c = Sub2.create(2); // Sub2
console.log(c);
const cclone = c.clone(); // Base
console.log(cclone);

species 패턴을 따르는 경우, 오버라이드 된 타입 (Base) 을 사용한다.

[Symbol.species] 테스트2 : null 값 반환해보기

class Base {
  constructor(data) {
    this.data = data;
  }

  static get [Symbol.species]() {
    return this;
  }

  // species 패턴을 따르지 않음
  static create(data) {
    const ctor = this || Base;
    return new ctor(data);
  }

  // species 패턴을 따름
  clone() {
    const ctor =
      (this && this.constructor && this.constructor[Symbol.species]) || Base;
    return new ctor(this.data);
  }
}

class Sub1 extends Base {}
class Sub2 extends Base {
  static get [Symbol.species]() {
    return null; // Base 클래스에서 작성해둔 기본 값을 사용한다. 일반적인 내장 클래스는 이렇게 동작한다.
  }
}

const b = Sub1.create(2); // Sub1
console.log(b);
const bclone = b.clone(); // Sub1
console.log(bclone);

const c = Sub2.create(2); // Sub2
console.log(c);
const cclone = c.clone(); // Base
console.log(cclone);
  • ... || Base 와 같이 코딩한 이유는 Array 같은 내장 클래스가 그렇게 하기 때문이다.

내장 객체 상속

  • ES2015 에서 내장 객체를 class 로 서브클래싱하는 것이 편해졌다.
class Elements extends Array {
  select(source) {
    if (source) {
      if (typeof source === "string") {
        const list = document.querySelectorAll(source);
        list.forEach((element) => this.push(element));
      } else {
        this.push(source);
      }
    }

    return this;
  }

  style(props) {
    this.forEach((element) => {
      for (const name in props) {
        element.style[name] = props[name];
      }
    });

    return this;
  }
}

new Elements()
  .select("div")
  .style({ color: "green" })
  .slice(1)
  .style({ border: "1px solid red" });
  • Array 는 이전에 배웠던 species 패턴을 사용한다.
    • [Symbol.species] 를 null 로 재정의하면, 체인에서 .select() 의 반환 타입이 Array 로 변경되어 style() 메서드를 찾지 못할 것이다.

super 를 이용할 수 있는 곳

  • 메서드에서는 super 키워드를 마음껏 이용할 수 있다.
  • 프로퍼티에 할당된 함수에서는 super 키워드를 이용할 수 없다.
class SuperClass {
  test() {
    return "SuperClass's test";
  }
}

class SubClass extends SuperClass {
  test1() {
    return "SubClass's test1: " + super.test();
  }
}

SubClass.prototype.test2 = function () {
  return "SubClass's test2: " + super.test(); // 에러 발생, `super` 키워드를 사용할 수 없다.
};

const obj = new SubClass();
obj.test1();
obj.test2();
  • super 클래스를 사용할 수 없는 이유는 속성에 할당된 함수에는 링크가 없기 때문이다.
  • super 는 [[HomeObject]] 라는 포함 함수의 내부 필드에 의존한다.
  • 자바스크립트 엔진은 [[HomeObject]] 필드에서 객체를 가져오고 프로토타입을 가져온 후 아래와 같이 해당 객체에서 method 속성을 찾는다.
let method = (the running method);
let homeObject = method.[[HomeObject]];
let proto = Object.getPrototypeOf(homeObject);
let value = proto.foo

3계층 구조 살펴보기

class Base {
  test() {
    return "Base test";
  }
}

class Sub extends Base {
  test() {
    return "Sub test> " + super.test();
  }
}

class SubSub extends Sub {
  test() {
    return "SubSub test> " + super.test();
  }
}

const obj = new SubSub();
console.log(obj.test()); // SubSub test> Sub test> Base test
  • 프로토타입 체인은 obj -> SubSub.prototype -> Sub.prototype -> Base.prototype -> Object.prototype 순이다.

잘못된 3계층 구조

function getFakeSuper(o) {
  return Object.getPrototypeOf(Object.getPrototypeOf(o));
}

class Base {
  test() {
    console.log("Base's test");
    return "Base test";
  }
}

class Sub extends Base {
  test() {
    console.log("Sub's test");
    return "Sub test> " + getFakeSuper(this).test.call(this);
  }
}

class SubSub extends Sub {
  test() {
    console.log("SubSub's test");
    return "Subsub test> " + getFakeSuper(this).test.call(this);
  }
}

const obj = new SubSub();
console.log(obj.test());
  • this 는 계속 obj (SubSub) 이다.
  • obj (SubSub) 이기 때문에, getFakeSuper(this).test.call(this) 는 Sub.test() 를 계속하여 호출하게 된다.
  • Stack overflow 가 일어나게 된다.

그림으로 살펴보기

  • 위의 그림처럼 각 메서드의 [[HomeObject]] 를 통해 다음 프로토타입을 가져오는 방식이 되어야 한다.

mixin 메서드 주의하기

// 믹스인
let sayHiMixin = {
  sayHi() {
    alert(`Hello ${this.name}`);
  },
  sayBye() {
    alert(`Bye ${this.name}`);
  },
};

// 사용법:
class User {
  constructor(name) {
    this.name = name;
  }
}

// 메서드 복사
Object.assign(User.prototype, sayHiMixin);

// 이제 User가 인사를 할 수 있습니다.
new User("Dude").sayHi(); // Hello Dude!
  • 위는 믹스인 코드의 예제이다.
  • mixin 메서드에서는 super 를 사용하면, 복사된 객체의 프로토타입이 아닌 원래 HomeObject 의 프로토타입을 계속 사용하므로 주의해야 한다.
  • 사용한다면, 새로운 계층이 아닌 원래 계층 내에서 계속 작동할 계획일 때 사용하자.

Object.prototype 제거하기

class A {
  constructor() {}
}

class B extends Object {
  constructor() {
    super();
  }
}
  • A.prototype 의 프로토타입 과 B.prototype 의 프로토타입 모두 Object.prototype 이 된다.
  • 두 클래스는 사실상 동일하다.
class X extends null {}

const o = new X();
console.log(o.toString); // undefined
console.log(Object.getPrototypeOf(X.prototype) === null); // true
  • toString(), hasOwnProperty() 등 오브젝트의 기본 메서드를 원하지 않는다면 null 을 상속하면 된다.
  • 몇몇 환경에서는 오류로 판명되는 경우도 있다.

new.target

함수와 생성자를 호출하는 방법은 두가지가 있다.

  • 직접 호출
  • 객체 생성의 일부로서 호출 (super 혹은 Reflect.construct 를 통해)

추상 클래스(서브 클래스의 일부로 인스턴스 생성만 허용) 혹은 최종 클래스(서브 클래스를 허용하지 않음)를 만들고 싶을 수 있다.

new.target 은 함수가 어떻게 호출되었는지 알 수 있게 해준다.

기본 사용법

class Base {
  constructor() {
    console.log(new.target.name);
  }
}

new Base(); // "Base"
class Sub extends Base {
  constructor() {
    super();
  }
}

new Sub(); // "Sub"

Sub 에서 super() 로 호출하여 결과가 "Sub" 가 된다.

추상 클래스 만들기

class Shape {
  constructor(color) {
    if (new.target === Shape) {
      throw new Error("Shape can't be directly instantiated");
    }

    this.color = color;
  }

  toString() {
    return (
      "[" +
      this.constructor.name +
      ", slides = " +
      this.sides +
      ", color = " +
      this.color +
      "]"
    );
  }
}

class Triangle extends Shape {
  get sides() {
    return 3;
  }
}

class Rectangle extends Shape {
  get sides() {
    return 4;
  }
}

const t = new Triangle("orange");
console.log(String(t)); // [Triangle, slides = 3, color = orange]

const r = new Rectangle("blue");
console.log(String(r)); // [Rectangle, slides = 4, color = blue]

const s = new Shape("red"); // ERROR
  • 다만 생성자 호출 없이 Object.create() 를 이용한다면, 프로토타입으로 직접 사용하는 객체를 만들 수 있다.

파이널 클래스 만들기

class Thingy {
  constructor() {
    if (new.target !== Thingy) {
      throw new Error("Thingy subclasses aren't supported.");
    }
  }
}

class InvalidThingy extends Thingy {}

console.log("Creating Thingy...");
const t = new Thingy(); // 작동
console.log("Creating InvalidThingy...");
const i = new InvalidThingy(); // ERROR: "Thingy subclasses aren't supported"
  • new Thingy() 생성자로 생성되지 않으면, Error 를 던진다.
    • new Thingy() 생성자로 객체 생성 시에는 new.target 이 Thingy 인 것을 알 수 있다.

호출된 방식에 따라 바뀌는 함수

const TwoWays = function TwoWays() {
  if (!new.target) {
    console.log("Called directly; using 'new' instead");
    return new TwoWays();
  }

  console.log("Called via 'new'");
};

console.log("With new:");
let t1 = new TwoWays();
// "Called via 'new'"

console.log("Without new:");
let t2 = TwoWays();
// "Called directly; using 'new' instead"
// "Called via 'new'"
  • new 키워드를 사용하지 않는 경우 new.target 이 비어있다는 특성을 이용한 코드이다.
    • new.target 의 결과가 undefined 가 된다.

클래스 선언 vs 클래스 표현식

// 클래스 선언
class Class1 {}
// 익명 클래스 표현식
let color = class {};
// 명명된 클래스 표현식
let C = class Color {};
  • 3가지 방식으로 클래스를 사용할 수 있다.
  • 표현식이 유효한 곳에 클래스 정의가 나올 수 있다.

클래스 선언

  • function 선언과 유사한 점
    • 스코프에 클래스 이름을 추가한다.
    • 끝에 세미콜론을 붙이지 않아도 된다.
  • function 선언과 다른 점
    • let 과 const 처럼 절반만 호이스트되며, 임시 데드존에 참여한다.
    • let 과 const 처럼 전역객체의 속성이 아닌 전역을 만든다.
Bar = 100;

class Bar {}
// Uncaught ReferenceError: Cannot access 'Bar' before initialization
  • 반만 호이스팅된다.
class Bar {}

console.log(typeof this.Bar); // undefined
  • 전역 객체 프로퍼티를 건드리지 않으면서, 전역을 만든다.
    • let, const 와 같다.

클래스 표현식

  • function 표현식과 유사한 점
    • 명명된 방식과 익명 방식이 모두 있다.
    • 생성자가 자동으로 생성된다.
  • 메서드에서 name 을 사용하여 메서드 이름을 사용하거나, Class.name 을 이용하여 클래스 이름을 사용할 수 있다.
  • 끝에 세미콜론을 붙여야 한다.
    • 대부분의 경우에는 자동 세미콜론 삽입이 처리해주긴 한다.
let name = "foo" + Math.floor(Math.random() * 1000);

const C = class TheClass {
  [name]() {
    console.log(
      "This is the method " + name + " in the class " + TheClass.name
    );
  }
};

console.log(typeof TheClass); // undefined
console.log(typeof C); // function

const theClass = new C();
theClass.foo655(); // This is the method foo655 in the class TheClass

액션 플랜

생성자 함수를 사용할 바에는 클래스를 이용하자.

  • prototype 을 이용한 상속은 문법도 복잡하고 실수하기 쉽다.
  • class 를 이용하자.
반응형
저작자표시 비영리 (새창열림)

'자바스크립트 > 웹개발자를 위한 자바스크립트의 모든 것' 카테고리의 다른 글

웹 개발자를 위한 자바스크립트의 모든 것 6장 이터러블, 이터레이터, 제너레이터  (0) 2023.01.16
웹 개발자를 위한 자바스크립트의 모든 것 5장 새로운 객체 기능  (0) 2023.01.02
웹 개발자를 위한 자바스크립트의 모든 것 3장 새로운 함수 기능  (0) 2022.12.22
웹 개발자를 위한 자바스크립트의 모든 것 2장 let, const 정리  (0) 2022.12.18
웹 개발자를 위한 자바스크립트의 모든 것 1장 ES2015 - ES2020 정리  (0) 2022.12.17
    '자바스크립트/웹개발자를 위한 자바스크립트의 모든 것' 카테고리의 다른 글
    • 웹 개발자를 위한 자바스크립트의 모든 것 6장 이터러블, 이터레이터, 제너레이터
    • 웹 개발자를 위한 자바스크립트의 모든 것 5장 새로운 객체 기능
    • 웹 개발자를 위한 자바스크립트의 모든 것 3장 새로운 함수 기능
    • 웹 개발자를 위한 자바스크립트의 모든 것 2장 let, const 정리
    Jake Seo
    Jake Seo
    ✔ 잘 보셨다면 광고 한번 클릭해주시면 큰 힘이 됩니다. ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바