TypeScript. Классы

Наконец-таки наследование здорового человека, через классы, а не через ломающие мозг прототипы! Теперь и в JavaScript! Скоро... Но пока в TypeScript. Реализация аналогична ООП в Java или C#.

Пример класса:

class Bird {
    name: string;
    constructor(name: string) {
        this.name = name;
    }

    fly(hight: number) { 
        console.log(`${this.name} взлетает на ${hight} м.`)
    }
}

let duck = new Bird('Дональд');
duck.fly(500);

Новый класс объявляется с помощью ключевого слова class. У класса есть свой конструктор, поля (name) и методы (fly). Конструктор - функция, которая будет вызвана при создании нового экземпляра класса. Для обращения к полям класса используем this. Для создания экземпляра класса используем оператор new. Все просто.

Наследование

Воспользуемся классом Bird из примера выше и унаследуемся от него в классах Duck и Chicken:

class Duck extends Bird {
    constructor(name: string) { super(name); }
    fly(hight = 10500) {
        console.log('Кря...');
        super.fly(hight);
    }
}

class Chicken extends Bird {
    constructor(name: string) { super(name); }
    fly(hight = 3) {
        console.log('Кудахтыж...');
        super.fly(hight/2);
    }
}

let donald = new Duck('Дональд');
let theSuperChicken: Bird = new Chicken('Cупер Кура');

donald.fly();
theSuperChicken.fly(50);

Наследование осуществляется с помощью ключевого слова extends. Классы "Утки" и "Курицы" переопределяют метод fly родительского класса "Птицы" в меру своих способностей. Для вызова конструктора или метода родительского класса используется super() или super.fly() соответственно.

Модификаторы доступа

public

Все члены класса в TypeScript по умолчанию являются публичными. Доступ к таким полям и методам класса возможен извне. Явно указать публичность члена класса можно с помощью public:

class Bird {
    public name: string;
    public constructor(name: string) {
        this.name = name;
    }
    public fly(hight: number) { 
        console.log(`${this.name} взлетает на ${hight} м.`)
    }
}

private

Если поле или метод класса помечен как private, то доступ к нему не возможен снаружи самого класса:

class Bird {
    private name: string;
    //...
}
let birdName = new Bird('Безымянный').name; // Ошибка: 'name' is private;

protected

Действие модификатора protected схоже с private, но члены класса с таким модификатором могут быть доступны для классов-наследников:

class Bird {
    protected name: string; // защищенное поле 'name'
    constructor(name: string) {
        this.name = name;
    }
    fly(hight: number) { 
        console.log(`${this.name} взлетает на ${hight} м.`)
    }
}

let birdName = new Bird('Безымянный').name; // Ошибка: 'name' is protected;

class Duck extends Bird {
    constructor(name: string) { super(name); }
    fly(hight = 10500) {
        console.log(`${this.name} к взлету готов!`); // получаем доступ к полю 'name'
        super.fly(hight);
    }
}

let donald = new Duck('Дональд');
donald.fly();

У класса может быть protected конструктор. Создать экземпляр этого класса не получится, но его можно унаследовать.

readonly

Свойства "только на чтение" должны быть инициализированы в конструкторе класса или при их объявлении:

class Bird {
    readonly name: string;
    readonly numOfWings = 2;        
    constructor(name: string) {
        this.name = name;
    }
}

let duck = new Bird('Скрудж Макдак'); // Надо вечерком перепройти Утиные истории...
duck.name = 'Дональд'; // Ошибка: 'name' is readonly

Свойства параметров конструктора

Последний пример можно немного сократить, объявив поле 'name' в параметрах конструктора:

class Bird {
    readonly numOfWings = 2;        
    constructor(readonly name: string) { } // поле 'name' будет создано автоматически
}

В транспилированном JavaScript это будет выглядеть так:

var Bird = (function () {
    function Bird(name) {
        this.name = name;
        this.numOfWings = 2;
    }
    return Bird;
}());

Можно не объявлять поле класса отдельной строкой, а можно делать это в параметрах конструктора, указав модификатор доступа (public, private, protected) или readonly. Соответствующее свойство класса будет создано автоматически.

static

Статические свойства класса (объявленные с использованием ключевого слова static) не привязываются к конкретному экземпляру класса, а являются общими для всех. Доступ к таким полям или методам можно получить, используя название класса, где они объявлены (через точку).

class Bird {
    constructor(private name: string) { }
    static numOfWings = 2; 
    static setNumOfWings(num: number) { 
        Bird.numOfWings = num;
    }
    say () {
        console.log(`${this.name}. Крыльев - ${Bird.numOfWings} шт.`);            
    }
}

let duck = new Bird('Утка');
duck.say(); // Утка. Крыльев - 2 шт.

Bird.setNumOfWings(4); // Меняем статическое свойство для всех птиц

let chicken = new Bird('Курица');
chicken.say(); // Курица. Крыльев - 4 шт.

В этом примере мы имеем статическое число крыльев static numOfWings и статический метод для их изменения static setNumOfWings(). Вызывая метод изменения числа крыльев для птиц, получаем изменение для всех экземпляров (уток и кур).

Геттеры и сеттеры

Геттеры (get) и сеттеры (set) в TypeScript - это специальные функции, которые позволяют организовать взаимодействие со свойствами объекта. В них можно реализовать некоторую логику получения и установки свойств.

let owner = true; 

class Duck {
    private _name: string;

    get name(): string {
        return this._name;
    }

    set name(name: string) {
        if (owner) {
            this._name = name;
        }
        else {
            console.log('Ошибка: Только хозяин может давать имя!');
        }
    }
}

let duck = new Duck();
duck.name = 'Черный плащ';
if (duck.name) {
    console.log(duck.name);
}

В примере выше мы организовали доступ к полю name таким образом, что получить имя может любой, а установить - только хозяин.

Абстрактные классы

Что-то среднее между интерфейсами и обычными классами. Создать экземпляр абстрактного класса нельзя. Часть реализации может присутствовать. С помощью ключевого слова abstract обозначаются как сами абстрактные классы, так и их методы, реализация которых в самом классе отсутствует, а должна быть написана в классах-наследниках.

abstract class Bird {
    abstract say(): string;
    fly(): void {
        console.log('Лечу...');
    }
}

Реализация метода say должна быть описана для каждого класса, который будет наследовать класс Bird.