Привет, Хабр! Начну с того, что мне надоела убогость классов и наследования в JavaScript! Просидев тысячи часов над крупным JS-проектом, это стало для меня просто очевидным. Особенно когда переключаешься с бэкенда с использованием Yii2, на фронтенд. Ведь в Yii2 и php есть настоящие классы, настоящие protected/private поля, есть trait, всякие dependency injection и behavior. И вот сразу после всех этих штук, создаёшь такой файл NewClass.js для того чтобы написать какой-нибудь класс, и понимаешь, что в JavaScript ничего этого нет. И даже более того, классы можно писать сотнями разных способов — прототипное/функциональное наследование, ES6 классы, и разные сахара с использованием внешних библиотек. Тут я сказал себе — "хватит это терпеть!".
Что нам предлагают в современных стандартах?
В ES6 появилась возможность описания классов более привычным для всех языков способом, с помощью синтаксиса class {}. Однако это скорее более привычная запись классов с использованием старого прототипного наследования, и в нём так и не появилось ни protected, ни privatе модификаторов доступа к свойствам класса. В новейшем ES2017 стандарте этого до сих пор и нет.
Велосипедим
Конечно, не хотелось быть собирателем велосипедов, и первое, что я сделал, прежде чем сесть за свой вариант библиотеки, я стал искать уже существующие решения. И, всё что будет описываться ниже, не моё открытие — раму для велосипеда уже нашёл в идеях других источников, и библиотеке mozart. Последнюю хотелось бы особо отметить, т. к. она послужила хорошей основой для дальнейшего развития идеи реализации почти настоящих классов.
Краткий обзор возможностей
Чтобы не превращать статью в пересказ README проекта, опишу лишь кратко список возможностей, и приведу пример использования, а ниже расскажу, как работает вся эта магия.
var Figure = Class.create(function ($public, $protected, _) { $public.x = 0; $public.y = 0; $protected.name = 'figure'; $protected.init = function (x, y) { _(this).id = 123; // private this.x = x; this.y = y; }; $protected.protectedMethod = function () { console.log('protectedMethod: ', this.id, this.name, this.self.x, this.self.y); }; this.square = function (circle) { return 2 * Math.PI * circle.radius; } }); var Circle = Class.create(Figure, function ($public, $protected, _) { $public.radius = 10; $public.publicMethod = function () { console.log('publicMethod: ', _(this).id, _(this).name, this.radius); _(this).protectedMethod(); }; }); var circle = new Circle(2, 7); circle.radius = 5; circle.publicMethod(); // publicMethod: undefined figure 5 / protectedMethod: 123 figure 2 7 console.log(Circle.square(circle)); // 31.415926536
var Layer = Class.create(function ($public, $protected, _) { $protected.uid = null; $protected.init = function () { _(this).uid = Date.now(); } }); var Movable = Class.create(function ($public, $protected, _) { $public.x = 0; $public.y = 0; $protected.init = function (x, y) { this.x = x; this.y = y; } $public.move = function () { this.x++; this.y++; } }); var MovableLayer = Class.create([Layer, Movable], function ($public, $protected, _, $super) { $protected.init = function (x, y) { $super.get(Layer).init.apply(this, arguments); $super.get(Movable).init.apply(this, arguments); } }); var layer = new MovableLayer(); // смотрите предыдущий пример console.log(layer instanceof Layer, layer instanceof Movable); // true false console.log(Class.is(layer, Layer), Class.is(layer, Movable)); // true true
var Human = Class.create(function ($public, $protected, _) { $protected.birthday = null; $public.getBirthday = function () { return _(this).birthday; }; $public.setBirthday = function (day) { _(this).birthday = day; }; $public.getAge = function () { var date = new Date(_(this).birthday); return Math.floor((Date.now() - date.getTime()) / (1000 * 3600 * 24 * 365)); }; }); var human = new Human(); human.birthday = '1975-05-01'; console.log(human.age);
var SortableMixin = function ($public, $protected, _) { $public.sort = function () { _(this).data.sort(); }; }; var Archive = Class.create(null, SortableMixin, function ($public, $protected, _) { $protected.init = function () { _(this).data = [3, 9, 7, 2]; }; $public.outData = function () { console.log(_(this).data); }; }); var archive = new Archive(); archive.sort(); archive.outData(); // [2, 3, 7, 9]
Разоблачаем фокус
Так как объекты в JavaScript не имеют никаких настроек доступов для его свойств, мы сможем сымитировать похожее на protected/private поведение, путём скрытия защищённых данных. При обычном функциональном наследовании это делается путём замыкания на самом конструкторе, а все методы создаются для каждого экземпляра класса:
var SomeClass = function () { var privateProperty = 'data'; this.someMethod = function () { return privateProperty; }; }; var data = []; for (var i = 0; i < 10000; i++) { data.push(new SomeClass()); }
При выполнении данного кода в памяти создадутся помимо самих объектов, ещё 10000 функций someMethod, что сильно откушает память. При этом нельзя так просто вынести объявление функции за пределы конструктора, так как в этом случае функция потеряет доступ к privateProperty.
Для решения данной проблемы, нам нужно объявлять функцию метода лишь один раз, а получать защищённые данные только за счёт указателя на объект this:
var SomeClass; (function () { var privateData = []; var counter = -1; SomeClass = function () { this.uid = ++counter; }; SomeClass.prototype.someMethod = function () { var private = privateData[this.uid]; }; })();
Так уже лучше, но всё-таки плохо. Во-первых, извне становится доступен некий идентификатор uid. А во-вторых, сборщик мусора никогда не очистит то, что попадёт в массив privateData и будет медленно но верно отжирать память. Для решения сразу двух проблем в ES6 появились замечательные классы Map и WeakMap.
Map — это почти те же массивы, но в отличие от них, в качестве ключа можно передать любой объект JavaScript. На для нас будут более интересны WeakMap — это тоже что и Map, но в отличие от него, WeakMap не мешает сборщику мусора очищать объекты, которые попадают в него.
Перепишем:
var SomeClass; (function () { var privateData = new WeakMap(); SomeClass = function () {}; SomeClass.prototype.someMethod = function () { var private = privateData.get(this); }; })();
Так мы получили private. С реализацией protected всё гораздо сложнее — для хранения защищённых данных их нужно разместить в неком общем хранилище для всех производных классов, но при этом давать доступы для конкретного класса не для всех свойств, а только те, что объявлены в нём самом. В качестве такого хранилища мы опять используем WeakMap, а в качестве ключа — прототип объекта:
SomeClass.prototype.someMethod = function () { var protected = protectedData.get(Object.getPrototypeOf(this)); };
Для ограничения доступа только к тем protected-свойствам, которые есть в самом классе, мы будем выдавать классу не сам объект с защищёнными данными, а связанный объект, нужные свойства которого будут получаться из основного объекта, путём объявления геттера и сеттера:
var allProtectedData = { notAllowed: 'secret', allowed: 'not_secret' }; var currentProtectedData = {}; Object.defineProperties(currentProtectedData, { allowed: { get: function () { return allProtectedData.allowed; }, set: function (v) { allProtectedData.allowed = v; }, } }); currentProtectedData.allowed = 'readed'; console.log(allProtectedData.allowed, currentProtectedData.allowed, currentProtectedData.notAllowed); // readed readed undefined
Вот примерно как-то так это работает.

Ну а дальше осталось лишь обвесить всё это красотой и возможностями, и вуаля!
Заключение
Подробное описание возможностей вы найдёте в README проекта. Всем спасибо за внимание!
