본문으로 건너뛰기

클래스_TypeScript 노트 4

무료2019-01-27#TypeScript#TypeScript Class#TS类继承#TS访问控制#TypeScript成员修饰符

멤버 수식자, 추상 클래스 등의 특성이 모두 활성화되며, 런타임 오버헤드도 증가하지 않음

一.클래스 멤버

TypeScript 의 클래스 정의는 [ES6 Class 사양](/articles/class-es6 笔记 10/) 과 일치하며, 정적 프로퍼티, 인스턴스 프로퍼티, 액세스 등을 모두 지원합니다:

class Grid {
    static origin = {x: 0, y: 0};
}

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        this._fullName = newName;
    }
}

하지만 2 가지 점에 주의가 필요합니다:

  • ES3 는 getter/setter 를 서포트하지 않으므로, ES5+ 로 컴파일 설정해야 합니다

  • getter 만으로 setter 가 없는 프로퍼티는 자동으로 readonly 로 추론됩니다 (멤버 수식자 중 하나)

二.멤버 수식자

액세스 제어 수식자

3 개의 액세스 제어 수식자를 서포트합니다:

  • public: 클래스의 멤버 속성/메소드는 기본적으로 모두 public 이며, 액세스 제한이 없습니다

  • private: 해당 클래스 선언의 외부에서 그 멤버에 액세스할 수 없습니다 (this.xxx 로 프라이빗 멤버에 액세스할 수 없는 등)

  • protected: private 과 유사하지만, 파생 클래스에서도 보호된 멤버에 액세스할 수 있습니다

예를 들어:

class Animal {
    // 프라이빗 멤버 속성
    private name: string;
    // 보호된 멤버 속성
    protected ancestor: string;
    // 쓰지 않으면, 기본적으로 public
    constructor(theName: string) { this.name = theName; }
    // public 멤버 메소드
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

주의, 이러한 액세스 제어는 모두 컴파일 시의 제한이며, 런타임에서는 강제적인 체크를 수행하지 않습니다. TypeScript 의 설계 원칙에 부합합니다:

컴파일 산출물에 런타임 오버헤드를 추가하지 않음

또한, 클래스 멤버의 액세스 가능성도 타입 체크의 일부이며, private/protected 수식자는덕 타입을 깨뜨립니다. 예를 들어:

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
}
class Person {
    name: string;
    constructor(theName: string) { this.name = theName; }
}
// 정확함: 덕 타입 (닮았으면)
let person: Animal = new Person('Sam');

class Plant {
    // 프라이빗 멤버 name
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
// 오류: name 속성의 액세스 가능성이 일치하지 않음 (닮아도 안 됨, 액세스 가능성이 다름)
// Property 'name' is private in type 'Plant' but not in type 'Person'.
let invalidPlant: Plant = new Person('Stone');

P.S.특별히, protected constructor 는 해당 클래스가 직접 인스턴스화할 수 없음을 나타내지만, 상속은 서포트합니다

readonly 수식자

readonly 수식자를 통해 속성을 읽기 전용으로 선언할 수 있으며, 선언 시 또는 생성자 내에서만赋值할 수 있습니다. 예를 들어:

class Octopus {
    readonly name: string;
    public readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error! name is readonly.

P.S.물론, readonly 와 액세스 제어 수식자는 모순되지 않으며, 같은 속성에 작용할 수 있습니다

파라미터 속성

생성자 내에서 초기화되는 속성에 대해:

class Octopus {
    readonly name: string;
    constructor (theName: string) {
        this.name = theName;
    }
}

간략화 방식 이 있습니다:

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}

그 중에서, name파라미터 속성이라고 불리며, 생성자의 형식 파라미터 이름 앞에 private/protected/public/readonly 수식자를 붙여 선언합니다

三.상속

class A extends B {
    //...
}

Babel 변환과 유사합니다 (명확히 하기 위해, 중요한 부분만 발췌):

function _inherits(subClass, superClass) {
  // 부모 클래스의 프로토타입 속성을 상속
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
  // 부모 클래스의 정적 속성을 상속
  if (superClass)
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass);
}

// 서브 클래스 생성자 내에서 부모 클래스의 인스턴스 속성을 상속
function A() {
    // 부모 클래스 생성자를 통해 서브 클래스 인스턴스 this 에 부모 클래스 인스턴스 속성을 추가
    return A.__proto__ || Object.getPrototypeOf(A)).apply(this, arguments)
}

TypeScript 의 Class 상속도 프로토타입 기반의 상속으로 컴파일 치환됩니다. 다음과 같습니다:

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };

    return function (d, b) {
        // 부모 클래스의 정적 속성을 상속
        extendStatics(d, b);
        function __() { this.constructor = d; }
        // 부모 클래스의 프로토타입 속성, 및 인스턴스 속성을 상속
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();

// __extends(A, B);

양자는 대동소이하며, 구현으로 보면, TypeScript 컴파일 산출물은 더욱 견고합니다. 그 목표는:

ES3+ 를 서포트하는 모든 호스트 환경에서 실행

P.S.비교재미있는 것은 정적 속성의 상속으로, 상세는 [一。어떻게 정적 속성을 상속하는가?](/articles/class 继承-es6 笔记 12/#articleHeader2) 참조

四.추상 클래스

TypeScript 에도 추상 클래스의 개념이 있습니다:

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

추상 클래스에는 구현이 있는 구체적 메소드 (move 등) 도 있고, 선언만으로 구현하지 않는 추상 메소드 (makeSound 등) 도 있지만, 서브 클래스는 이러한 메소드를 구현해야 합니다:

class Cat extends Animal {
    makeSound() {
        console.log('meow meow meow');
    }
}

또 하나의 유사한 개념은 인터페이스로, 양자의 차이는 인터페이스에서는「추상 메소드」만 정의할 수 있다는 것입니다 (abstract 키워드는 없으며, 정확히는 메소드 시그니처). 예를 들어:

interface Animal {
  // abstract makeSound(): void; 와 비교
  makeSound(): void;
}

// class Cat extends Animal 과 비교
class Cat implements Animal {
    makeSound() {
        console.log('meow meow meow');
    }
}

五.클래스와 타입

클래스를 선언함과 동시에, 해당 클래스 인스턴스의 타입도 선언합니다. 예를 들어:

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter: Greeter;
greeter = new Greeter("world");

그 중에서, 인스턴스 greeterGreeter 타입입니다. 즉, Class 선언은 타입 의미를 가집니다:

  • 해당 클래스 인스턴스의 타입: Greeter

  • 클래스 자신의 타입: typeof Greeter

실제로, 클래스 자신의 타입은 정적 속성, 인스턴스 속성, 생성자, 프로토타입 메소드 등의 특징을 제약합니다. 예를 들어:

class GreeterDuck {
    // 클래스 자신의 타입 제약
    static standardGreeting = 'Hi';
    greeting: string;
    constructor(message: string) { }
    greet() { return 'there'; }
    // 많아도 되고 적어도 안 됨 (덕 타입)
    sayHello() { /*...*/ }
}

let greeterType: typeof Greeter = GreeterDuck;

더 나아가:

// Class 타입에서 Interface 를 확장
interface HelloGreeter extends Greeter {
    sayHello(): void;
}

let hGreeter: HelloGreeter = new GreeterDuck('world');

그렇습니다. 클래스는 타입 의미를 가지므로, 인터페이스는 클래스로부터 상속할 수 있습니다

참고 자료

댓글

아직 댓글이 없습니다

댓글 작성