5

Альтернативы переменным классов в ES6

13

Вопрос на русском для StackOverflow.com:


Проблема с классами и переменными класса в ES6

В настоящее время в ES5 многие из нас используют следующий паттерн в фреймворках для создания классов и переменных класса, который достаточно удобен:

// ES 5
FrameWork.Class({

    variable: 'string',
    variable2: true,

    init: function(){

    },

    addItem: function(){

    }

});

В ES6 мы можем создавать классы нативно, но у нас нет возможности определять переменные класса:

// ES6
class MyClass {
    const MY_CONST = 'string'; // <--- это невозможно в ES6
    constructor(){
        this.MY_CONST;
    }
}

К сожалению, приведённый выше код не сработает, так как классы могут содержать только методы.

Я понимаю, что могу использовать this.myVar = true в constructor, но не хочу "загромождать" конструктор, особенно когда у меня 20-30 параметров для более крупного класса.

Я обдумывал различные способы решения этой проблемы, но пока не нашел подходящих решений. (Например, создать обработчик ClassConfig и передать объект parameter, который будет объявлен отдельно от класса. Затем обработчик присоединял бы его к классу. Я также думал об использовании WeakMaps для интеграции каким-либо образом.)

Какие у вас есть идеи для решения данной ситуации?

5 ответ(ов)

0

В вашем примере:

class MyClass {
    const MY_CONST = 'string';
    constructor(){
        this.MY_CONST;
    }
}

Поскольку MY_CONST является примитивом (https://developer.mozilla.org/en-US/docs/Glossary/Primitive), мы можем просто сделать следующее:

class MyClass {
    static get MY_CONST() {
        return 'string';
    }
    get MY_CONST() {
        return this.constructor.MY_CONST;
    }
    constructor() {
        alert(this.MY_CONST === this.constructor.MY_CONST);
    }
}
alert(MyClass.MY_CONST);
new MyClass

// alert: string ; true

Однако, если MY_CONST имеет тип ссылки, например, static get MY_CONST() {return ['string'];}, вывод alert будет string, false. В таком случае оператор delete может помочь:

class MyClass {
    static get MY_CONST() {
        delete MyClass.MY_CONST;
        return MyClass.MY_CONST = 'string';
    }
    get MY_CONST() {
        return this.constructor.MY_CONST;
    }
    constructor() {
        alert(this.MY_CONST === this.constructor.MY_CONST);
    }
}
alert(MyClass.MY_CONST);
new MyClass

// alert: string ; true

Наконец, для переменной класса, а не const:

class MyClass {
    static get MY_CONST() {
        delete MyClass.MY_CONST;
        return MyClass.MY_CONST = 'string';
    }
    static set U_YIN_YANG(value) {
      delete MyClass.MY_CONST;
      MyClass.MY_CONST = value;
    }
    get MY_CONST() {
        return this.constructor.MY_CONST;
    }
    set MY_CONST(value) {
        this.constructor.MY_CONST = value;
    }
    constructor() {
        alert(this.MY_CONST === this.constructor.MY_CONST);
    }
}
alert(MyClass.MY_CONST);
new MyClass
// alert: string, true
MyClass.MY_CONST = ['string', 42];
alert(MyClass.MY_CONST);
new MyClass
// alert: string, 42 ; true

Таким образом, мы видим, что использование оператора delete позволяет изменить поведение статических свойств с типом ссылки и делает возможным их обновление.

0

Поскольку ваша проблема в основном касается стиля (вы не хотите перегружать конструктор множеством объявлений), ее также можно решить с точки зрения стиля.

На мой взгляд, во многих языках, основанных на классах, конструктор представляет собой функцию, название которой совпадает с именем класса. Мы можем использовать это для создания класса ES6, который будет стилистически корректным, но не будет объединять типичные действия, выполняющиеся в конструкторе, с объявлениями свойств. Мы просто используем фактический конструктор JS в качестве "области объявления", а затем создаем функцию с именем класса, которую мы рассматриваем как "область других действий конструктора", вызывая ее в конце истинного конструктора.

"use strict";

class MyClass {
    // только объявите ваши свойства, а затем вызовите this.MyClass(); отсюда
    constructor() {
        this.prop1 = 'blah 1';
        this.prop2 = 'blah 2';
        this.prop3 = 'blah 3';
        this.MyClass();
    }

    // все остальные "конструкторские" действия, больше не переплетенные с объявлениями
    MyClass() {
        doWhatever();
    }
}

Обе части будут вызваны при создании нового экземпляра.

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

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


ПРИМЕЧАНИЕ: Я намеренно не использую типичные идиоматические идеи "инициализации" (такие как метод init() или initialize()), потому что их часто используют по-разному. Существует некое предположительное различие между идеей конструктора и инициализации. Работая с конструкторами, люди знают, что они вызываются автоматически в процессе инстанцирования. Увидев метод init, многие люди без второго взгляда предположат, что нужно что-то делать в формате var mc = MyClass(); mc.init();, потому что именно так обычно инициализируют. Я не пытаюсь добавить процесс инициализации для пользователя класса, я пытаюсь добавить к процессу конструирования самого класса.

Хотя некоторые люди могут сначала удивиться, это как раз и является частью идеи: это дает понять, что намерение относится к конструированию, даже если это заставляет их немного удивиться и подумать "так не работают конструкторы ES6", и взглянуть на сам конструктор, чтобы понять: "О, они вызывают это внизу, я вижу", что намного лучше, чем не передать это намерение (или неправильно его передать) и, вероятно, получить много людей, использующих его неправильно, пытаясь инициализировать его извне и т.д. Это очень намеренно для предложенной мной схемы.


Для тех, кто не хочет следовать этой схеме, точно противоположное тоже сработает. Вынесите объявления в другую функцию в начале. Возможно, назовите ее "properties" или "publicProperties" или чем-то подобным. Затем поместите остальное в обычный конструктор.

"use strict";

class MyClass {
    properties() {
        this.prop1 = 'blah 1';
        this.prop2 = 'blah 2';
        this.prop3 = 'blah 3';
    }

    constructor() {
        this.properties();
        doWhatever();
    }
}

Обратите внимание, что этот второй метод может выглядеть более аккуратно, но у него также есть врожденная проблема: метод properties будет перекрыт, если один класс, использующий этот метод, наследует от другого. Вам нужно будет дать более уникальные имена для метода properties, чтобы избежать этого. Мой первый метод не имеет этой проблемы, поскольку его "фейковая" часть конструктора имеет уникальное имя, соответствующее названию класса.

0

Вопрос о том, как реализовать старый способ создания классов с использованием прототипного наследования, вполне уместен. Давайте рассмотрим ваш пример кода, который показывает, как это может быть сделано.

В данном коде вы определяете класс MyClass с помощью функции конструктора. Внутри конструктора вы объявляете переменную countVar, которая инициализируется на основе параметра count. Однако переменная foo добавляется в MyClass.prototype, что обеспечивает всем экземплярам класса доступ к одной и той же переменной foo, что экономит память.

В вашем случае, когда вы создаете два экземпляра класса MyClass, o1 и o2, и присваиваете o1.foo новое значение "newFoo", экземпляры используют разные ссылки на foo. Это означает, что o2.foo по-прежнему будет иметь значение по умолчанию, так как он не был изменен.

Вот что происходит в вашем коде:

var o1 = new MyClass(2); // Создает экземпляр с countVar равным 3
var o2 = new MyClass(3); // Создает экземпляр с countVar равным 4
o1.foo = "newFoo"; // Для o1 создается собственное поле foo

console.log(o1.foo, o2.foo); // Вывод: newFoo foo
console.log(o1.countVar, o2.countVar); // Вывод: 3 4

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

Так что если вы хотите использовать "олдскульный" подход, который вы описали, это совершенно допустимо и может быть весьма эффективным, особенно если вам нужно создать множество экземпляров класса с небольшими изменениями в свойствах.

0

Простой альтернативный вариант для только констант — это определение константы вне класса. Она будет доступна только из самого модуля, если не будет дополнена геттером.

Таким образом, prototype не будет загромождён, и вы получите константу.

// будет доступна только из самого модуля
const MY_CONST = 'string'; 

class MyClass {
    
    // опционально, если требуется внешний доступ
    static get MY_CONST() {
        return MY_CONST;
    }

    // пример доступа
    static someMethod() {
        console.log(MY_CONST);
    }
}

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

0

Синтаксис членов класса в ES7:

ES7 предлагает решение для упрощения вашего конструктора. Вот пример:

class Car {
  
  wheels = 4;
  weight = 100;

}

const car = new Car();
console.log(car.wheels, car.weight);

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

class Car {

  constructor() {
    this.wheels = 4;
    this.weight = 100;
  }

}

const car = new Car();
console.log(car.wheels, car.weight);

Обратите внимание, что этот синтаксис может не поддерживаться всеми браузерами и может потребовать транспиляции в более раннюю версию JS.

Бонус: фабрика объектов:

function generateCar(wheels, weight) {

  class Car {

    constructor() {}

    wheels = wheels;
    weight = weight;

  }

  return new Car();

}

const car1 = generateCar(4, 50);
const car2 = generateCar(6, 100);

console.log(car1.wheels, car1.weight);
console.log(car2.wheels, car2.weight);

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

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь