Данные в JS

Работаем с данными в JS

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

Небольшая предыстория

До ES6 я привык к псевдо-классам в стиле ES5. Это было очень просто. Нужно было только: написать функцию, создать прототип со всеми данными и функциями по умолчанию, которые я хотел, и переназначить конструктор прототипа обратно в функцию конструктора. Даже наследование было относительно простым (хотя сложности свои были и здесь).

Как и многие, я устал от всего этого и от специально разработанных функций для быстрой постановки. Однако через некоторое время я захотел большего. Я хотел создавать данные внутри класса. Недолгие поиски и у меня под рукой оказывается решение WeakMaр. Неплохо… но все же как-то не то и неуклюже.

Тогда я обнаружил, что хочу чего-то действительно сложного: защищенных объектов. К тому времени вокруг было несколько библиотек, которые, в чем-то превосходили мою. Я перепробовал многие из них, но все они не оправдали ожиданий.

Примерно в то же время до меня дошли слухи, что Javascript скоро появится поддержка классов. Это воскресило мои надежды. И наверное слишком рано. Потому что, когда появился ES6 и классы, мои надежды были снова разрушены. Там отсутствовала поддержка для объявленных типов данных вообще. Поэтому, при написании кода для моей тогдашней работы, я изучал Proxy и WeakMap. Остановился я тогда на своем первом проекте для полностью привилегированной библиотеки классов. Это был грандиозный эксперимент. Это сработало настолько хорошо, что с его помощью был выпущен производственный код.

С тех пор я оттачивал свои навыки и создал несколько более новых и (возможно) лучших алгоритмов. Пока я делал это, я узнал о TC39 и его предложении данных, которое теперь является печально известным proposal-class-fields. Мои первоначальные ожидания были завышены, поскольку в определении класса я рассчитывал увидеть объявленные свойства.

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

Но потом они сделали это. Они объединили два предложения и сделали выбор, который полностью сломал то, как я намеревался его использовать. Если вы посмотрите в предложении, это аргумент Set vs. Define. Я не буду сейчас вдаваться в подробности, но сложно описать культурным словами тот шквал эмоций, что я испытал. Итак, вернувшись к работе, я приступил к созданию еще одной библиотеки классов, хотя бы лишь для того, чтобы показать, что подход, использованный в этом предложении, был более разрушительным, чем необходимо.

А теперь ближе к делу

Если вы классы ES6 не видели, то они выглядят так:

class Ex {
somefunc(...args) {
//Some code here
}
get prop() { return something; }
set prop(v) { something = v; }

...

constructor(...args) {
//Initialize the class here
}
}

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

Сначала я попытался обернуть определение класса и внести изменения таким образом, но это очень быстро усложнилось из-за того, что я не мог контролировать, какой объект экземпляра был получен конструктором. Это еще усложняется существованием super.

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

Посмотрите на этот код:

class Point2D extends PublicData({
x: 0,
y: 0,
}) {
translate(dx, dy) {...}
rotate(xAngle, yAngle) {...}
}
class Point3D extends PublicData(Point2D, {
z: 0
}) {
translate(dx, dy, dz) {
super.translate(dx, dy);

...

}
rotate(xAngle, yAngle, zAngle) {
super.rotate(xAngle, yAngle);

...

}
}

Очень простым и понятным способом мы добавили возможность включать объявленные данные в наши классы. Итак, что здесь происходит? Все, что вам нужно знать, находится в функции PublicData. Вы знаете, что у вас есть класс, расширяющий функцию, которая возвращает предварительно манипулированный объект вместо обычного вновь созданного. В нашем случае все не так, но похоже. Вместо того, чтобы просто заставить PublicData создать функцию, выполняющую трюк супер возврата, он создает новый класс в цепочке прототипов и использует данные, которые вы указали в качестве прототипа. Когда вы даете ему 2 параметра, как в случае Point3D, в примере выше, первый параметр используется в качестве базового класса для сгенерированного класса данных.

Функцию на удивление легко написать:

function PublicData(base, proto) {
//Section 1
switch(arguments.length) {
case 0:
base = Object;
proto = {};
break;
case 1:
if (typeof(base) === "function") {
proto = {};
}
else {
proto = base || {};
base = Object;
}
break;
default:
if (typeof(base) !== "function") {
throw new TypeError("Invalid 'base' argument");
}
if (!proto || (typeof(proto) !== "object")) {
throw new TypeError("Invalid 'proto' argument");
}
break;
}
//Section 2
let retval = function PublicDataShim(...args) {
if (!new.target) {
throw new TypeError(`${retval.name} requires 'new'`);
}
return Reflect.construct(base, args, new.target);
};
//Section 3
Object.defineProperties(retval.prototype, Object.getOwnPropertyDescriptors(proto));
Object.setPrototypeOf(retval.prototype, base.protoype);
Object.setPrototypeOf(retval, base);
return retval;
}

Раздел 1- это просто динамическая проверка аргументов. Вы можете передать либо просто прототип, либо базовый класс и прототип. Этот код просто гарантирует, что то, что вы передали, имеет смысл.

Раздел 2 довольно близок к определению конструктора по умолчанию для класса, который расширяет другой класс. Поскольку это функция, а не класс, необходимо использовать Reflect.construct, чтобы получить тот же эффект, что и при вызове super. Оператор throw добавлен для согласованности и подтверждения того, что new.target действительно существует. Если это не так, Reflect.construct не вернет объект экземпляра с правильным прототипом.

Раздел 3 - проверенный метод ES5 создания псевдоклассового класса. Единственное незначительное отличие состоит в том, что параметр proto копируется в прототип вновь созданной функции. Это удерживает нас от использования того, что было передано в качестве proto, в качестве средства для манипулирования вторым прототипом.

Если мы создадим экземпляр Point3D, экземпляр будет выглядеть так:

JavaScript

Данные не попадают в тот же объект, но сохраняют все, что вам нужно от свойств данных в прототипе.

Обратная сторона

Из-за неудачного решения, принятого при разработке JavaScript, мы застряли с противным маленьким “перочинным ножичком” в отношении размещения объектов в прототипе. Однако, после многих лет опыта разработки многими разработчиками, теперь есть хорошо известно решение этой проблемы.

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

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

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

Источник: https://medium.com/swlh/data-in-javascript-classes-7e15596168af