ES6: классы изнутри
Перевод статьи ES6 Classes in Depth с сайта ponyfoo.com, опубликовано на css-live.ru с разрешения автора — Николаса Беваквы.
Встречайте «ES6 изнутри». Впервые здесь? Тогда, возможно, вам стоит изучить такие фичи ES6, как деструктирование, литералы шаблона, стрелочные функции, оператор расширения и оставшиеся параметры или литерал объекта. Сегодня поговорим о «классах» в ES6.
Как и в прошлых статьях, рекомендую вам установить Babel и повторять за мной, копируя примеры с помощью REPL, либо командной строки
babel-node
и файла. Это поможет гораздо лучше усвоить идеи, обсуждаемые в серии. Если вы не из тех, кто любит устанавливать что-либо на свой компьютер, то вам есть смысл залезть на CodePen и кликнуть иконку с шестерёнкой для JavaScript — у него есть препроцессор Babel, который с лёгкостью позволяет опробовать ES6.
Начнём!
Классы в JavaScript? Что ты имеешь в виду?
JavaScript — прототипно-ориентированный язык, что это еще за классы в ES6? Классы — синтаксический «сахар» поверх прототипного наследования — уловка, делающая язык притягательнее для программистов, пришедших из других парадигм, и, возможно, не совсем знакомых с цепочками прототипов. Многие фичи в ES6 (такие как деструктирование), по сути, синтаксический «сахар» — и классы не исключение. Я остановился на этом подробно, поскольку так нам будет легче понять базовую технологию, стоящую за классами в ES6. Саму структуру языка не переделывали, а просто упростили работу с прототипным наследованием для тех, кто привык к классам.
Хотя мне и не нравится термин «классы» для этой конкретной фичи, я вынужден признаться, что в действительности работать с этим синтаксисом гораздо легче, чем с синтаксисом обычного прототипного наследования в ES5, и это выигрыш для каждого — как их ни называй.
Теперь, когда с этим мы разобрались, я предположу, что вы понимаете прототипное наследование — иначе вы бы вряд ли читали блог о JavaScript. Вот как вы описали бы автомобиль Car, для которого можно создать экземпляр, заправить и запустить.
function Car () { this.fuel = 0; this.distance = 0; } Car.prototype.move = function () { if (this.fuel < 1) { throw new RangeError('Бензин закончился') } this.fuel-- this.distance += 2 } Car.prototype.addFuel = function () { if (this.fuel >= 60) { throw new RangeError('Бензобак заполнен') } this.fuel++ }
Чтобы запустить автомобиль, вы могли бы использовать следующий кусок кода.
var car = new Car() car.addFuel() car.move() car.move() // <- RangeError: 'Бензин закончился'
Отлично. А что на счёт классов в ES6? Синтаксис очень похож на объявление объекта, только здесь впереди подставляется class Name
, где Name
— название класса. Здесь мы используем нотацию сигнатуры метода, которую мы обсуждали вчера при объявлении методов с помощью сокращённого синтаксиса. constructor
— такой же метод-конструктор, как в ES5, так что это можно использовать для инициализации любых переменных, которые могут быть у экземпляров.
class Car { constructor () { this.fuel = 0 this.distance = 0 } move () { if (this.fuel < 1) { throw new RangeError('Бензин закончился') } this.fuel-- this.distance += 2 } addFuel () { if (this.fuel >= 60) { throw new RangeError('Бензобак заполнен') } this.fuel++ } }
Если вы вдруг не заметили, запятые между свойствами и методами в классе недопустимы (почему так, для меня самого пока загадка), в отличие от литералов объекта, где запятые (по-прежнему) обязательны. Из-за этой разницы выбор между обычным литералом объекта и классом обернется для многих немалой головной болью, но надо признать, без запятых код и вправду смотрится почище.
У классов зачастую есть статические методы. Возьмите, к примеру, нашего старого приятеля Array
. У каждого экземпляра массива есть его «персональные» методы .filter
, .reduce
и .map
. Но и у «класса» Array
есть свои статические методы, например, Array.isArray
. Добавить подобные методы к «классу» Car
и в ES5 довольно просто:
function Car () { this.topSpeed = Math.random() } Car.isFaster = function (left, right) { return left.topSpeed > right.topSpeed }
В нотации ES6, с class
, мы можем вставить перед методом static
, по той же логике синтаксиса, что у get
и set
. Опять же, лишь «сахар» для ES5, поскольку перевести это в синтаксис старой версии не составляет труда.
class Car { constructor () { this.topSpeed = Math.random() } static isFaster (left, right) { return left.topSpeed > right.topSpeed } }
Дополнительную сладость «сахарку» class
в ES6 придает то, что в придачу к нему идет ключевое слово extends, дающее возможность легко «наследоваться» от других «классов». Мы знаем, что Тесла проезжает больше на том же количестве топлива, и в коде ниже видно, как класс Tesla
расширяет класс Car
(Tesla extends Car
) и «переопределяет» (принцип, который может быть знаком вам по C#) метод move
, позволяя покрыть большее расстояние.
class Tesla extends Car { move () { super.move() this.distance += 4 } }
Специальное ключевое слово super
указывает на класс Car
, от которого мы унаследовали — и раз уж мы упомянули C#, это сродни base
. Смысл его существования в том, что чаще всего, когда мы переопределяем метод, заново реализуя его в наследуемом классе — класс Тесла
в нашем примере — нам бывает нужно вызвать и метод базового класса. Таким образом нам не приходится заново копировать логику в наследуемый класс при каждом переопределении метода. Это было бы особенно паршиво, поскольку всякий раз при изменении базового класса нам бы пришлось переносить его логику в каждый наследуемый класс, превращая поддержку кода в сущий кошмар.
Теперь, если вы проделаете следующее, то заметите, что автомобиль Tesla проезжает две дистанции в силу base.move()
, как обычная машина, и еще четыре таких же дистанции сверх, потому что это вам не что-нибудь, а Tesla
.
var car = new Tesla() car.addFuel() car.move() console.log(car.distance) // <- 6
Чаще всего приходится переопределять метод constructor
. Здесь можно просто вызвать super()
, передавая любые аргументы, нужные базовому классу. Автомобили Тесла в два раза быстрее, так что мы просто вызываем конструктор базового класса Car
с удвоенной заявленной скоростью speed
.
class Car { constructor (speed) { this.speed = speed } } class Tesla extends Car { constructor (speed) { super(speed * 2) } }
Завтра мы перейдём к синтаксису let
, const и for ... of
. Увидимся!
P.S. Это тоже может быть интересно: