ES6 ввел классы в JavaScript, но они могут быть слишком простыми для сложных приложений. Поля класса (также называемые свойствами класса ) предназначены для предоставления более простых конструкторов с закрытыми и статическими членами. Предложение в настоящее время является этапом TC39 3: кандидат и, вероятно, будет добавлено к ES2019 (ES10). В настоящее время частные поля поддерживаются в Node.js 12, Chrome 74 и Babel.
Краткий обзор классов ES6 полезен, прежде чем мы посмотрим, как реализованы поля классов.
Основы класса ES6
Объектно-ориентированная модель наследования JavaScript может сбить с толку разработчиков из таких языков, как C ++, C #, Java и PHP. По этой причине ES6 ввел классы . Они в основном синтаксические сахара, но предлагают более знакомые концепции объектно-ориентированного программирования.
Класс — это шаблон объекта, который определяет, как ведут себя объекты этого типа. Следующий класс Animal
определяет общих животных (классы обычно обозначаются начальным капиталом, чтобы отличать их от объектов и других типов):
class Animal { constructor(name = 'anonymous', legs = 4, noise = 'nothing') { this.type = 'animal'; this.name = name; this.legs = legs; this.noise = noise; } speak() { console.log(`${this.name} says "${this.noise}"`); } walk() { console.log(`${this.name} walks on ${this.legs} legs`); } }
Объявления классов всегда выполняются в строгом режиме. Нет необходимости добавлять 'use strict'
.
Метод конструктора запускается при создании объекта типа Animal . Обычно он устанавливает начальные свойства и обрабатывает другие инициализации. speak()
и walk()
являются методами экземпляра, которые добавляют дополнительную функциональность.
Теперь из этого класса можно создать объект с new
ключевым словом:
let rex = new Animal('Rex', 4, 'woof'); rex.speak(); // Rex says "woof" rex.noise = 'growl'; rex.speak(); // Rex says "growl"
Добытчики и сеттеры
Сеттеры — это специальные методы, используемые только для определения значений. Точно так же, Getters — это специальные методы, используемые только для возврата значения. Например:
class Animal { constructor(name = 'anonymous', legs = 4, noise = 'nothing') { this.type = 'animal'; this.name = name; this.legs = legs; this.noise = noise; } speak() { console.log(`${this.name} says "${this.noise}"`); } walk() { console.log(`${this.name} walks on ${this.legs} legs`); } // setter set eats(food) { this.food = food; } // getter get dinner() { return `${this.name} eats ${this.food || 'nothing'} for dinner.`; } } let rex = new Animal('Rex', 4, 'woof'); rex.eats = 'anything'; console.log( rex.dinner ); // Rex eats anything for dinner.
Ребенок или Подклассы
Часто практично использовать один класс в качестве основы для другого. Класс Human
может наследовать все свойства и методы класса Animal
с помощью ключевого слова extends
. Свойства и методы могут быть добавлены, удалены или изменены по мере необходимости, чтобы создание человеческого объекта стало проще и более читабельным:
class Human extends Animal { constructor(name) { // call the Animal constructor super(name, 2, 'nothing of interest'); this.type = 'human'; } // override Animal.speak speak(to) { super.speak(); if (to) console.log(`to ${to}`); } }
super
ссылается на родительский класс, поэтому обычно это первый вызов, выполняемый в constructor
. В этом примере метод Human speak()
переопределяет тот, который определен в Animal
.
Объектные экземпляры Human
теперь могут быть созданы:
let don = new Human('Don'); don.speak('anyone'); // Don says "nothing of interest" to anyone don.eats = 'burgers'; console.log( don.dinner ); // Don eats burgers for dinner.
-let don = new Human('Don'); don.speak('anyone'); // Don says "nothing of interest" to anyone don.eats = 'burgers'; console.log( don.dinner ); // Don eats burgers for dinner.
Статические методы и свойства
Определение метода с ключевым словом static
позволяет вызывать его в классе без создания экземпляра объекта. Рассмотрим константу Math.PI
: нет необходимости создавать объект Math
перед доступом к свойству PI
.
ES6 не поддерживает статические свойства так же, как другие языки, но можно добавить свойства в само определение класса. Например, класс Human
можно адаптировать для сохранения количества созданных человеческих объектов:
class Human extends Animal { constructor(name) { // call the Animal constructor super(name, 2, 'nothing of interest'); this.type = 'human'; // update count of Human objects Human.count++; } // override Animal.speak speak(to) { super.speak(); if (to) console.log(`to ${to}`); } // return number of human objects static get COUNT() { return Human.count; } } // static property of the class itself - not its objects Human.count = 0;
Статический получатель COUNT
класса возвращает количество людей соответственно:
console.log(`Humans defined: ${Human.COUNT}`); // Humans defined: 0 let don = new Human('Don'); console.log(`Humans defined: ${Human.COUNT}`); // Humans defined: 1 let kim = new Human('Kim'); console.log(`Humans defined: ${Human.COUNT}`); // Humans defined: 2
Поля класса ES2019 (NEW)
Новая реализация полей класса позволяет инициализировать открытые свойства в верхней части класса вне любого конструктора:
class MyClass { a = 1; b = 2; c = 3; }
Это эквивалентно:
class MyClass { constructor() { this.a = 1; this.b = 2; this.c = 3; } }
Если вам все еще требуется конструктор, инициализаторы будут выполнены до его запуска.
Поля статического класса
В приведенном выше примере статические свойства были незаметно добавлены к объекту определения класса после его определения. Это не обязательно с полями класса:
class MyClass { x = 1; y = 2; static z = 3; } console.log( MyClass.z ); // 3
Это эквивалентно:
class MyClass { constructor() { this.x = 1; this.y = 2; } } MyClass.z = 3; console.log( MyClass.z ); // 3
Поля частного класса
Все свойства в классах ES6 являются открытыми по умолчанию и могут быть проверены или изменены вне класса. В приведенных выше примерах « Animal
нет ничего, что могло бы помешать изменению свойства food
без вызова eats
eats:
class Animal { constructor(name = 'anonymous', legs = 4, noise = 'nothing') { this.type = 'animal'; this.name = name; this.legs = legs; this.noise = noise; } set eats(food) { this.food = food; } get dinner() { return `${this.name} eats ${this.food || 'nothing'} for dinner.`; } } let rex = new Animal('Rex', 4, 'woof'); rex.eats = 'anything'; // standard setter rex.food = 'tofu'; // bypass the eats setter altogether console.log( rex.dinner ); // Rex eats tofu for dinner.
Другие языки часто позволяют объявлять private
свойства. Это невозможно в ES6, поэтому разработчики часто обходят его, используя соглашение о подчеркивании ( _propertyName
), замыкания, символы или WeakMaps . Подчеркивание дает подсказку разработчику, но ничто не мешает им получить доступ к этому свойству.
В ES2019 поля частного класса определяются с помощью префикса hash #
:
class MyClass { a = 1; // .a is public #b = 2; // .#b is private static #c = 3; // .#c is private and static incB() { this.#b++; } } let m = new MyClass(); m.incB(); // runs OK m.#b = 0; // error - private property cannot be modified outside class
Обратите внимание, что нет никакого способа определить частные методы, методы получения или установки. Этап 3 TC39: черновик предложения предлагает использовать префикс #
для имен, и он был реализован в Babel. Например:
class MyClass { // private property #x = 0; // private method (can only be called within the class) #incX() { this.#x++; } // private setter (can only be used within the class) set #setX(x) { this.#x = x; } // private getter (can only be used within the class) get #getX() { return this.$x; } }
Непосредственная выгода: более чистый код React!
У компонентов React часто есть методы, связанные с событиями DOM. Чтобы убедиться, что this
разрешает компонент, необходимо bind
каждый метод соответствующим образом. Например:
class App extends Component { constructor() { super(); this.state = { count: 0 }; // bind all methods this.incCount = this.incCount.bind(this); } incCount() { this.setState(ps => { count: ps.count + 1 }) } render() { return ( <div> <p>{ this.state.count }</p> <button onClick={this.incCount}>add one</button> </div> ); } }
Когда incCount
определен как поле класса ES2019, его можно назначить как функцию с помощью жирной стрелки ES6 =>
, которая автоматически привязывается к определяющему объекту. Больше нет необходимости добавлять объявления bind
:
class App extends Component { state = { count: 0 }; incCount = () => { this.setState(ps => { count: ps.count + 1 }) }; render() { return ( <div> <p>{ this.state.count }</p> <button onClick={this.incCount}>add one</button> </div> ); } }
Поля класса: улучшение?
Определения класса ES6 были упрощенными. Поля класса ES2019 требуют меньше кода, помогают удобочитаемости и предоставляют некоторые интересные возможности объектно-ориентированного программирования.
Использование #
для обозначения приватности получило некоторую критику, в первую очередь потому, что это уродливо и похоже на взлом. Большинство языков реализуют private
ключевое слово, поэтому попытка использовать этот член вне класса будет отклонена компилятором.
JavaScript интерпретируется. Рассмотрим следующий код:
class MyClass { private secret = 123; } const myObject = new MyClass(); myObject.secret = 'one-two-three';
Это вызвало бы ошибку времени выполнения в последней строке, но это серьезное последствие для простой попытки установить свойство. JavaScript преднамеренно прощает, а ES5 разрешает изменение свойств любого объекта.
Хотя это неуклюже, запись #
является недопустимой вне определения класса. Попытка доступа к myObject.#secret
может вызвать синтаксическую ошибку.
Дискуссия будет продолжена, но, как они или нет, поля классов были приняты в нескольких движках JavaScript. Они здесь, чтобы остаться.