Рубріки: Теория

Почему классы JS это не просто «синтаксический сахар»

Богдан Мирченко

JS-инженер Андреа Джаммарки написал на medium.com пост, в котором назвал заблуждением утверждение, что классы JS — это просто синтаксический сахар для прототипного наследования. По его мнению, есть множество вещей, которые можно смоделировать с помощью ES5 и прототипного наследования, но ни один из этих методов не настолько быстрый и безопасный, как использование соответствующего синтаксиса для классов JS.

Вот как он обосновал позицию в пользу использования классов JS:

  • Использование директивы use strict в ES5 не запрещает вызов конструкторов без служебного слова new.
// ES5
function Test() { "use strict"; }
Test.call({}); // it's OK

// ES6+
class Test {}
Test.call({}); // it throws

Современные классы имеют свойство new.target, которое невозможно сымитировать в ES5 без использования транспайлера, копирующего такое поведение.

  • Андреа Джаммарки отмечает, что, несмотря на его попытки создать подклассы для массивов, невозможно расширить внутренние функции в ES5 полезным или значимым образом.
// ES5 epic fail
function List() { "use strict"; }
List.prototype = Object.create(Array.prototype);

var list = new List;
list.push(1, 2, 3);

JSON.stringify(list);
// {"0":1,"1":2,"2":3,"length":3}

list.slice(0) instanceof List; // false

По словам автора, он не использует в конструкторе свойство Array.apply(this, arguments), так как расширение массива ES5 неудобно.

// ES5 epic fail v2
function Text(value) {
  "use strict";
  String.call(this, value);
}

new Text("does this work?");
// nope, it doesn't ... no way it can.

С помощью прототипного наследования не получится расширить String, а с помощью классов JS можно

  • list.slice (0) не является экземпляром List из-за свойства Symbol.species
// ES6+
class List extends Array {}

(new List).slice(0) instanceof List; // true
[].slice.call(new List) instanceof List; // true
  • В конструкторе ES5 не работает свойство Array.apply(this, arguments), потому что:

       — встроенный массив создает новый массив, которому не важен контекст и другие встроенные функции;

       — классы JS обновляют экземпляры, что невозможно сделать в ES5 без транспайлера.

// ES5
function Button() {
  return document.createElement('button');
}

function MyButton(value) {
  Button.call(this);
  this.textContent = value;
}

Object.setPrototypeOf(MyButton, Button);
Object.setPrototypeOf(MyButton.prototype, Button.prototype);
  • При вызове свойства new MyButton(“content”) возвращается экземпляр MyButton со свойством textContent.
function MySubClass() {
  var self = Class.apply(this, arguments) || this;
  // do anything with the self
  return self;
}

Этот подход плох тем, что:

       — если суперкласс возвращает что-то еще, теряется наследование;

       — если суперкласс является встроенным, вместо этого может быть команда self, указывающая на примитив.

Как в этом случае работают классы JS:

// ES6+
class Button {
  constructor() {
    return document.createElement('button');
  }
}
class MyButton extends Button {
  constructor(value) {
    super();
    this.textContent = value;
  }
}
document.body.appendChild(new MyButton("hello"));
  • В классах JS методы не могут быть построены, как и сокращенные буквальные методы.
// ES6+
class Test {
  method() {}
}
new Test.prototype.method;
// TypeError: Test.prototype.method is not a constructor

В ES5 все функции могут использоваться как конструкторы.

// ES5
function Test() {}
Test.prototype.method = function () {
  if (!(this instanceof Test))
    throw new TypeError("not a constructor");
};
  • В классах JS нельзя перечислить статические и нестатические методы. ES5 позволяет это сделать, но boilerplate-код будет огромный, медленный и неудобный.
  • В классах JS стрелочные функции можно указывать в определении класса. То же самое действие в ES5 будет чересчур громоздким:
// ES5
function WithArrows() {
  Object.defineProperties(this, {
    method1: {
      configurable: true,
      writable: true,
      value: () => "arrow 1"
    }
  });
}

// ES6+
class WithArrows {
  method1 = () => "arrow 1";
}

// (new WithArrows).method1();
  • В классах JS есть частные свойства и частные методы.
// ES6+
class WithPrivates {
  #value;
  #method(value) {
    this.#value = value;
  }
  constructor(value) {
    this.#method(value);
  }
}

В ES5 можно смоделировать частные свойства, только используя транспайлер и коллекции WeakMap.

Автор призывает перестать называть классы JS просто «синтаксическим сахаром». По мнению Андреа Джаммарки, множество вещей можно симулировать с помощью прототипного наследования, но некоторые просто невозможно. Использование современных классов из ES6 поможет создать лучшее ООП в JS, чем оно было в течение последних 20 лет.

Останні статті

Что такое прокси-сервер: пояснение простыми словами, зачем нужны прокси

Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…

21.11.2024

Что такое PWA приложение? Зачем необходимо прогрессивное веб-приложение

Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…

19.11.2024

Как создать игру на телефоне: программирование с помощью конструктора

Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…

17.11.2024

Google Bard: эффективный аналог ChatGPT

В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…

14.11.2024

Скрипт и программирование: что это такое простыми словами

Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…

12.11.2024

Дедлайн в разработке: что это такое простыми словами

Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…

11.11.2024