Альтернативы переменным классов в ES6
Вопрос на русском для 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 ответ(ов)
В вашем примере:
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
позволяет изменить поведение статических свойств с типом ссылки и делает возможным их обновление.
Поскольку ваша проблема в основном касается стиля (вы не хотите перегружать конструктор множеством объявлений), ее также можно решить с точки зрения стиля.
На мой взгляд, во многих языках, основанных на классах, конструктор представляет собой функцию, название которой совпадает с именем класса. Мы можем использовать это для создания класса 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
, чтобы избежать этого. Мой первый метод не имеет этой проблемы, поскольку его "фейковая" часть конструктора имеет уникальное имя, соответствующее названию класса.
Вопрос о том, как реализовать старый способ создания классов с использованием прототипного наследования, вполне уместен. Давайте рассмотрим ваш пример кода, который показывает, как это может быть сделано.
В данном коде вы определяете класс 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
Таким образом, использование прототипов позволяет вам сохранять память, так как все экземпляры класса делят метод и свойства, определенные в прототипе, вместо того, чтобы дублировать их в каждый экземпляр.
Так что если вы хотите использовать "олдскульный" подход, который вы описали, это совершенно допустимо и может быть весьма эффективным, особенно если вам нужно создать множество экземпляров класса с небольшими изменениями в свойствах.
Простой альтернативный вариант для только констант — это определение константы вне класса. Она будет доступна только из самого модуля, если не будет дополнена геттером.
Таким образом, prototype
не будет загромождён, и вы получите константу.
// будет доступна только из самого модуля
const MY_CONST = 'string';
class MyClass {
// опционально, если требуется внешний доступ
static get MY_CONST() {
return MY_CONST;
}
// пример доступа
static someMethod() {
console.log(MY_CONST);
}
}
Это решение позволяет вам управлять областью видимости констант, сохраняя при этом возможность доступа к ним через статический метод или геттер, если необходимо.
Синтаксис членов класса в 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
мы можем без лишнего кода создавать классы и управлять их свойствами, что упрощает процесс написания кода.
Приватные свойства в классах JavaScript ES6
Может ли выражение (a == 1 && a == 2 && a == 3) когда-либо оцениться как истинное?
Как клонировать объект JavaScript, исключив один ключ?
Соответствуют ли 'Стрелочные функции' и 'Функции' или они взаимозаменяемы?
Добавление тега script в React/JSX