Как разработчик React, у меня есть подозрение на использование классов JS. В пузыре React мне кажется, что это плохая практика, когда я где-то их вижу.

Однако недавно у меня появилась задача переписать наш @tolgee/core, то есть VanillaJS, и до сих пор везде были классы. Мне почему-то казалось, что это неправильно. Но потом я понял, что на самом деле не знаю, как заменить классы вне React. Хм …


Подожди, а что не так с уроками?

Классы — это, по сути, добавленный синтаксический сахар, который создает впечатление, что вы работаете с языком программирования, основанным на классах, хотя на самом деле это не так. Классы в JavaScript реализованы иначе, чем другие. Мои основные претензии таковы:

  • Как this работы является причиной многих вопросов, и правильно понять это непросто
  • Концепция наследования становится устаревшей, и классы поощряют ее использование.
  • Приватные поля в классах пока не везде поддерживаются, да и синтаксис у них странный (и Typescript приватные поля на самом деле не приватные)
  • Экземпляры класса плохо сочетаются с деструктурирующим присваиванием.

Классы по JavaScript пытались приблизить его к традиционным объектно-ориентированным языкам, но не до конца. Поэтому, если вы привыкли к ООП, вы не получите очень хорошего опыта, и вам будет казаться, что JS — дрянной язык. Но это не должно быть так.


Как рыба из воды

В React вам не нужны классы, и даже когда вы используете внешние библиотеки, они обычно не требуют от вас написания классов. Так что легко сказать, что «классы не нужны, просто напишите функции», однако, как только вы выходите за пределы экосистемы React, все меняется.

Когда я это понял, я задумался: почему так? Как получается, что с React классы не нужны?


Состояние и жизненный цикл

Если вы пишете сложную систему, вам нужен способ, как где-то хранить свое состояние. В React есть способ обработки состояния в ваших компонентах, и он предоставляет вам жизненный цикл, который определяет, когда внутреннее состояние создается, изменяется и уничтожается. Обратите внимание, что это очень похоже на экземпляры класса. В некотором смысле компонент React концептуально очень похож на класс, поскольку он дает вам фрейм, в котором живет ваш код, и вы можете «подключиться» к его жизненному циклу.

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


Классы без классов

Так что у меня есть оправдание для классов, но это означало бы, что я должен жить с их недостатками. Мне нужно использовать class, this, extendsа также new как Java-программист? Как-то плохо себя чувствует. Мне нравится мир простых объектов и простых функций. Мне нравится простой мир.

Давайте попробуем «эмулировать» классы, как это делает React.

Один из способов — отделить методы от данных. Это часто используется в языках, которые не поддерживают классы (например, C).

// equivalent of constructor
function createCar(color) {
  return {
    color,
  };
}

// equivalent of methods
function setCarColor(car, color) {
  car.color = color;
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

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


Объекты закрытия

я наткнулся на лекция Дэвида Крокфордагде он рекомендует использовать замыкание вместо класса.

Безусловно, худшая новая функция в es6 — это class. Его настоятельно рекомендовали все ребята из Java, которым сейчас приходится писать на JavaScript и которые не хотят изучать новый язык. Это дает им иллюзию, что они пишут на языке, который, по их мнению, они уже знают.

Дэвид Крокфорд

Если вы посмотрите на это, это чем-то напоминает определение компонента React.

function Car(color) {
  const state = {
    color,
  };

  function setColor(color) {
    state.color = color;
  }

  return Object.freeze({
    setColor,
  });
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

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

Несмотря на то, что этот объект представляет собой «экземпляр», данные не хранятся непосредственно в нем, поэтому мы можем заморозить его с помощью Object.freeze. Данные на самом деле хранятся в замыкании конструктора, а в экземпляре мы «сливаем» только те функции, которые хотим сделать общедоступными.


Как насчет наследства?

Вместо традиционного наследования здесь можно использовать композицию, так как вы можете вызывать любые другие конструкторы и комбинировать их методы в своем новом объекте.

function Car(color) {

  ...

  const { setSpeed } = Vehicle();

  return Object.freeze({
    setColor,
    setSpeed,
  });
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

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

Несмотря на то, что я не видел, чтобы этот метод использовался во многих местах, он кажется мне вполне естественным. Мне нравится, как я точно вижу, что и где передается. Это также дает преимущество в оптимизации кода, поскольку приватные методы действительно приватные (в отличие от Typescript частные методы), поэтому оптимизатор может оптимизировать их соответствующим образом.

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

// this is perfectly ok
const { setColor } = Car(...)
Войти в полноэкранный режим

Выйти из полноэкранного режима


Выглядит неплохо. В чем подвох?

Никакого улова, кроме…

Использование машинописного текста не так гладко, как с классами. С классами Typescript умеет с ними работать и
поэтому, если вы создаете класс Car затем вы можете использовать тип Car в качестве аннотации типа для его экземпляра. Это невозможно
с функциями. Можно вывести тип, но нам нужно быть более явным.

let car: ReturnType<typeof Car>;
Войти в полноэкранный режим

Выйти из полноэкранного режима

Со временем я начал использовать соглашение, согласно которому с каждым конструктором я также экспортирую тип экземпляра следующим образом:

export function Car(...) {
  ...
}
export type CarInstance = ReturnType<typeof Car>
Войти в полноэкранный режим

Выйти из полноэкранного режима

По какой-то причине Typescript имеет серьезную проблему с рекурсией в объектах. Это ломает эту систему, если вы хотите вернуть сам экземпляр в любом методе (что является довольно распространенной практикой для цепочечных объектов).

export function Car(...) {
  const car = Object.freeze({
    ...
    addWheel: (...) => {
      ...
      return car
    }
  })
  return car
}

export type CarInstance = ReturnType<typeof Car>
Войти в полноэкранный режим

Выйти из полноэкранного режима

Если вы встроите этот файл в Car.d.ts с tsc компилятор (v4.8.4), он выдаст что-то вроде этого:

export declare function Car(...): Readonly<{
  addWheel: (...) => Readonly<any>;
}>;

export declare type Instance = ReturnType<typeof Tolgee>;
Войти в полноэкранный режим

Выйти из полноэкранного режима

Обратите внимание, что тип рекурсивной функции сводится к any, что весьма печально. Я не мог найти никакого способа обойти это, только явно ввести функцию, что не идеально, но в моем случае все еще приемлемо. Будем надеяться, что в будущем станет лучше Typescript версии. Если влиятельные люди будут дергать за ниточки, возможно, мы добьемся некоторого улучшения.

Недостатки Typescript весьма существенны, однако для моего варианта использования они все же перевешивают недостатки классов и их «неуместность» в JavaScript.


Соглашения

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

Также нет четкого названия для всего этого соглашения, я видел, что оно называется объектами Крокфорда или классами Крокфорда, но я бы предпочел «объекты замыкания», поэтому имя имеет некоторую ссылку на его значение.


Вывод

Энтузиасты Python используют слово «pythonic» для конструкций, которые им кажутся естественными. Я бы назвал это «javascriptic», если бы это был термин, но даже если это не так, я думаю, вы понимаете, что я имею в виду.

ПС: проверьте Tolgee.io и дай нам гитхаб звезды

Оригинал статьи был опубликован на наш блог.