Я написал некоторый код:

class Base {
    // Default value
    myColor = 'blue';

    constructor() {
        console.log(this.myColor);
    }
}

class Derived extends Base {
     myColor = 'red'; 
}

// Prints "blue", expected "red"
const x = new Derived();

Я ожидал, что мой инициализатор поля производного класса запустится до конструктора базового класса. Вместо этого производный класс не изменяет свойство myColor до тех пор, пока не будет запущен конструктор базового класса, поэтому я наблюдаю неправильные значения в конструкторе.

Это ошибка? В чем дело? Почему это происходит? Что я должен сделать вместо этого?

23
Ryan Cavanaugh 24 Апр 2017 в 22:08

2 ответа

Лучший ответ

Не ошибка

Во-первых, это не ошибка в TypeScript, Babel или вашей среде выполнения JS.

Почему так должно быть

Первое, что вы можете сделать, это «Почему бы не сделать это правильно !?!?». Давайте рассмотрим конкретный случай eScript TypeScript. Фактический ответ зависит от того, для какой версии ECMAScript мы генерируем код класса.

Эмиссия нижнего уровня: ES3 / ES5

Давайте рассмотрим код, испускаемый TypeScript для ES3 или ES5. Я упростил + аннотировал это немного для удобства чтения:

var Base = (function () {
    function Base() {
        // BASE CLASS PROPERTY INITIALIZERS
        this.myColor = 'blue';
        console.log(this.myColor);
    }
    return Base;
}());

var Derived = (function (_super) {
    __extends(Derived, _super);
    function Derived() {
        // RUN THE BASE CLASS CTOR
        _super();

        // DERIVED CLASS PROPERTY INITIALIZERS
        this.myColor = 'red';

        // Code in the derived class ctor body would appear here
    }
    return Derived;
}(Base));

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

Правильный ли производный класс испускает?

Нет, вы должны поменять заказ

Многие утверждают, что производный класс emit должен выглядеть так:

    // DERIVED CLASS PROPERTY INITIALIZERS
    this.myColor = 'red';

    // RUN THE BASE CLASS CTOR
    _super();

Это супер неправильно по ряду причин:

  • Это не имеет соответствующего поведения в ES6 (см. Следующий раздел)
  • Значение 'red' для myColor будет немедленно перезаписано значением базового класса 'blue'
  • Инициализатор поля производного класса может вызывать методы базового класса, которые зависят от инициализации базового класса.

В этом последнем пункте рассмотрим этот код:

class Base {
    thing = 'ok';
    getThing() { return this.thing; }
}
class Derived extends Base {
    something = this.getThing();
}

Если инициализаторы производного класса выполняются перед инициализаторами базового класса, Derived#something всегда будет undefined, когда ясно, что это должно быть 'ok'.

Нет, вы должны использовать машину времени

Многие другие утверждают, что нужно сделать туманное что-то еще , чтобы Base знал, что Derived имеет инициализатор поля.

Вы можете написать примеры решений, которые зависят от знания всей совокупности кода, который будет выполняться. Но TypeScript / Babel / etc не может гарантировать, что это существует. Например, Base может находиться в отдельном файле, где мы не видим его реализацию.

Эмиссия нижнего уровня: ES6

Если вы еще этого не знали, пришло время узнать: классы не являются функцией TypeScript . Они являются частью ES6 и имеют определенную семантику. Но классы ES6 не поддерживают инициализаторы полей, поэтому они преобразуются в ES6-совместимый код. Это выглядит так:

class Base {
    constructor() {
        // Default value
        this.myColor = 'blue';
        console.log(this.myColor);
    }
}
class Derived extends Base {
    constructor() {
        super(...arguments);
        this.myColor = 'red';
    }
}

Вместо

    super(...arguments);
    this.myColor = 'red';

Должны ли мы иметь это?

    this.myColor = 'red';
    super(...arguments);

Нет, , потому что это не работает . Недопустимо ссылаться на this перед вызовом super в производном классе. Это просто не может работать таким образом.

ES7 +: открытые поля

Комитет TC39, который контролирует JavaScript, изучает возможность добавления полевых инициализаторов в будущую версию языка.

Вы можете прочитать об этом на GitHub или прочитайте конкретный вопрос о порядке инициализации.

ООП переподготовка: виртуальное поведение от конструкторов

Все языки ООП имеют общее руководство, некоторые применяются явно, некоторые неявно по соглашению:

Не вызывайте виртуальные методы из конструктора

Примеры:

В JavaScript мы должны немного расширить это правило

Не наблюдайте виртуальное поведение от конструктора

И

Инициализация свойства класса считается виртуальной

Решения

Стандартным решением является преобразование инициализации поля в параметр конструктора:

class Base {
    myColor: string;
    constructor(color: string = "blue") {
        this.myColor = color;
        console.log(this.myColor);
    }
}

class Derived extends Base {
    constructor() {
        super("red");
     }
}

// Prints "red" as expected
const x = new Derived();

Вы также можете использовать шаблон init, хотя вы должны быть осторожны, чтобы не наблюдать за его виртуальным поведением и , чтобы не делать что-то в производном {{X1 }} метод, который требует полной инициализации базового класса:

class Base {
    myColor: string;
    constructor() {
        this.init();
        console.log(this.myColor);
    }
    init() {
        this.myColor = "blue";
    }
}

class Derived extends Base {
    init() {
        super.init();
        this.myColor = "red";
    }
}

// Prints "red" as expected
const x = new Derived();
34
Ryan Cavanaugh 8 Дек 2017 в 21:25

Я бы с уважением утверждал, что это, на самом деле, ошибка

Совершая неожиданные действия, это нежелательное поведение, которое нарушает общие сценарии использования расширений классов. Вот порядок инициализации, который бы поддерживал ваш вариант использования, и я бы поспорил лучше:

Base property initializers
Derived property initializers
Base constructor
Derived constructor

Проблемы / Решения

- компилятор машинописного текста в настоящее время генерирует инициализации свойств в конструкторе

Решение здесь состоит в том, чтобы отделить инициализации свойств от вызова функций конструктора. C # делает это, хотя он вводит базовые свойства после производных свойств, что также нелогично. Это может быть достигнуто путем создания вспомогательных классов, чтобы производный класс мог инициализировать базовый класс в произвольном порядке.

class _Base {
    ctor() {
        console.log('base ctor color: ', this.myColor);
    }

    initProps() {
        this.myColor = 'blue';
    }
}
class _Derived extends _Base {
    constructor() {
        super();
    }

    ctor() {
        super.ctor();
        console.log('derived ctor color: ', this.myColor);
    }

    initProps() {
        super.initProps();
        this.myColor = 'red';
    }
}

class Base {
    constructor() {
        const _class = new _Base();
        _class.initProps();
        _class.ctor();
        return _class;
    }
}
class Derived {
    constructor() {
        const _class = new _Derived();
        _class.initProps();
        _class.ctor();
        return _class;
    }
}

// Prints:
// "base ctor color: red"
// "derived ctor color: red"
const d = new Derived();

- не сломается ли базовый конструктор, потому что мы используем свойства производного класса?

Любая логика, которая ломается в базовом конструкторе, может быть перемещена в метод, который будет переопределен в производном классе. Поскольку производные методы инициализируются до вызова базового конструктора, это будет работать правильно. Пример:

class Base {
    protected numThings = 5;

    constructor() {
        console.log('math result: ', this.doMath())
    }

    protected doMath() {
        return 10/this.numThings;
    }
}

class Derived extends Base {
    // Overrides. Would cause divide by 0 in base if we weren't overriding doMath
    protected numThings = 0;

    protected doMath() {
        return 100 + this.numThings;
    }
}

// Should print "math result: 100"
const x = new Derived();
1
frodo2975 2 Окт 2018 в 06:29
43595943