Главная » Статьи » Классы JS — это не «просто синтаксический сахар»

Классы JS — это не «просто синтаксический сахар»

От автора: прочитав очередное сообщение в блоге о том, что классы JS являются «просто сахаром для прототипного наследования», я решил написать этот пост, чтобы еще раз прояснить, почему это утверждение вводит в заблуждение; пост, который, надеюсь, объясняет, в чем разница и почему важно ее понимать.

Использование use strict

Использование команды use strict в ES5 не будет запрещать вызывать конструкторы без ключевого слова new.

// ES5
function Test() { "use strict"; }
Test.call({}); // it's OK // ES6+
class Test {}
Test.call({}); // it throws

Причина в том, что современные классы имеют концепцию new.target, которую иначе невозможно воспроизвести в ES5 без использования транспиляторов, имитирующих такое поведение. Транспилятор также должен использовать проверку instanceof, что приводит к более медленному и раздутому коду.

Расширение встроенных функций

Вопреки моим экспериментам с подклассами Arrays, не представляется возможным расширять встроенные функции в 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 невозможно, также, как и всех остальных встроенных функций, включая String или другие.

// 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?” И вы правы: возможно, вам это не нужно, но дело в том, что это невозможно сделать с ES5: прототипное наследование не может этого сделать, а классы 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 здесь просто ненадежный и не предназначен для работы с видами. Вот и все: классы JS намного лучше сохраняют ожидания, чем ES5.

Super()

Если вам интересно, почему конструктор Array.apply(this, arguments) не работает в ES5? Ответ таков:

Array создает новый массив, он вообще не заботится о контексте, как и другие встроенные функции

Классы 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»)?

Возвращается кнопка с текстом Value

Возвращается экземпляр MyButton с полем textContent

И да, правильный ответ — последний, так что мы можем написать все подклассы как таковые:

function MySubClass() { var self = Class.apply(this, arguments) || this; // do anything with the self return self;
}

Наши ожидания не оправдаются … и что плохого в таком подходе?

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

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

Итак, вот еще один вариант, который исправляет первую проблемму, но не вторую:

function MySubClass() { var self = Class.apply(this, arguments); if (self == null) self = this; else if (!(self instanceof MySubClass)) Object.setPrototypeOf(self, MySubClass.prototype); // do things with self return 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 намного лучше и мощнее ES5? Да!

Методы

Строго говоря, это не какая-то особенность классов 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, но шаблон огромен, медленен и неудобен.

Стрелочные функции (=>)

В классах 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 чтоб обернуть каждый экземпляр отдельными частями, которые никогда не должны быть доступны снаружи.

Заключение

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

Соответственно, давайте перестанем говорить, что классы JS — это просто сахар, потому что количество деталей, отсутствующих в таком заявлении, нельзя упускать из виду или игнорировать, если только мы не решим, что не хотим использовать функции современных классов, которые могут значительно улучшить ООП в JS, в сравнении с тем, что было за последние 20 лет.

Однако в таком случае правильное утверждение было бы больше похоже на: «Мне не нравятся классы JS, поэтому я думаю, что нужно использовать прототипное наследование».

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

Автор: Andrea Giammarchi

Источник: webreflection.medium.com

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен