CSS-live.ru

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. Это тоже может быть интересно:

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

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