Очень быстрые классы на JavaScript с красивым синтаксисом

При написании серьезных проектов перед JavaScript программистами встает выбор: пожертвовать качеством кода и писать классы руками, или же пожертвовать скоростью и использовать систему классов. А если использовать систему, то какую выбрать?

В статье рассмотрена система автора, которая не уступает по скорости классам, написанным «от руки» (другими словами — одна из самых быстрых в мире). Но при этом классы имеют приятную структуру в стиле Си.

Системы классов


Есть шутка, что каждый программист должен написать свою систему классов. Кто не знаком с проблемой — смотрите этот комментарий, там их собрано минимум 50 штук.

Каждый из этих велосипедов отличается своим набором возможностей, своим стилем программирования и своим падением скорости. Так, например, создание класса MooTools примерно в 90 раз медленнее, чем создание класса, написанного от руки. Зачем тогда нужны все эти системы?

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

Замечание: некоторые серьезные проекты все же не используют систему классов, и похоже не сильно от этого страдают. Как пример — смотрите исходник Derby.js. Но я воспринимаю Derby как черный ящик, который делает что-то за вас, так что разработчики не сильно поощряют копание в его внутренностях (поправьте, если не прав); а в Ext наследование наоборот очень важно.

Преимущества систем

Чего мы хотим от системы? Прежде всего — это вызов родительских методов. Вот пример из MooTools:

var Cat = new Class({
    Extends: Animal,
    initialize: function(name, age){
        this.parent(age); // вызов родительского метода
    }
});

Поначалу это очень красиво выглядит: внутри любой функции у вас есть метод parent. И рефакторить удобно — если переименовать метод, то вызов родителя не сломается. Но за красоту и удобство приходится платить большую цену — каждый метод в классе будет обернут в такую вот страшную упаковку:

var wrapper = function(){
    if (method.$protected && this.$caller == null) throw new Error('The method "' + key + '" cannot be called.');
    var caller = this.caller, current = this.$caller;
    this.caller = current; this.$caller = wrapper;
    var result = method.apply(this, arguments);
    this.$caller = current; this.caller = caller;
    return result;
}.extend({$owner: self, $origin: method, $name: key});

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

Что еще критически важно? У каждого экземпляра класса должны быть свои свойства:

var Cat = new Class({
    food: [],
    initialize: function(name){
        this.name = name;
    }
});

var cat1 = new Cat('Мурка');
var cat2 = new Cat('Мурзик');

// массивы разные
cat1.food.push('Мышь');
cat2.food.length == 0; // пустой массив

Как видите, MooTools создал для каждого класса свой собственный массив food. Как бы это все делалось при традиционном подходе? Свойства мы присваивали бы в конструкторе:

function Cat() {
    this.food = [];
    Cat.superclass.constructor.call(this)
}
Cat.prototype.meow = function() {/*...*/}

Насчет методов есть несколько вариантов, в примере выше показан вариант с функцией extend Дугласа Крокфорда. При традиционной системе в коде много мусора типа «Cat.prototype...» и «superclass.constructor.call(this...)», такой код тяжело воспринимать и рефакторить.

Пару слов о приватных членах класса

То, что абсолютно нормально в С++, бывает очень вредным в JavaScript. Я говорю это из своего опыта: если в классах есть приватные методы и переменные, то такие классы часто становятся неподдерживаемыми. Если вы хотите что-то изменить в таком куске кода — то иногда вам не остается ничего кроме как выбросить старый код и переписать всё с нуля.

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

А теперь давайте создадим систему классов, которая была бы такой же удобной, как C++, но при этом такой же быстрой, как классы, написанные от руки. И чтобы работало без препроцессоров.

Пишем быстрые классы


Итак, самый быстрый способ создать класс на JS — это написать его руками, используя прототипы:

function Animal() {}
Animal.prototype.init = function() {}

Под этот способ оптимизированы все движки браузеров. Шаг в сторону — и получим падение производительности, например:

Animal.prototype = {
    init: function() {}
}

В этом примере прототип был присвоен как объект. Хром это кушает нормально, а вот в Firefox скорость создания классов падает существенно.

Быстрое наследование

Теперь нам нужно вызывать родительские методы. Существует ли что-нибудь быстрее, чем цепочка прототипов? А давайте просто переименуем родительский метод в классе-наследнике!

function Cat() {} // наследник Animal
Cat.prototype.Animal$init = Animal.prototype.init;
Cat.prototype.init = function() {
    this.Animal$init(); // вызов родительского метода
}

Мы скопировали метод из прототипа родителя, и при этом его переименовали. Быстрее уже просто нельзя. Само собой, мы не будем делать это руками — за нас все сделает система классов.

В этом примере не будет работать оператор instanceof, но на практике без него можно прекрасно обходиться. Я говорю про реальные приложения и задачи: если вам нужно отличать тип Animal от Cat — то это реальная задача, и она прекрасно решается. Но если вы хотите делать это оператором instanceof — то извините, вам к другому доктору.

Еще при таком наследовании нет цепочки прототипов (так как прототипы копируются) — это дает небольшое ускорение по сравнению с традиционными решениями.

Удобные свойства

Присваивать руками в конструкторе свойства по умолчанию — это тоже не слишком приятно. Так что, пускай за нас это делает скрипт, как в MooTools. Как это будет работать: система классов сама сгенерирует функцию-конструктор, которая присвоит свойства по умолчанию. Выглядеть это будет так:

ClassManager.define(
'Cat',
{
    Extends: 'Animal',

    food: [],
    init: function() {
        this.Animal$init();
    }
});

В результате получим:

// сгенерированный конструктор
function Cat() {
    this.food = [];
    this.init.apply(this, arguments);
}
// у которого будет такой прототип
Cat.prototype.Animal$init = Animal.prototype.init;
Cat.prototype.init = init: function() {
    this.Animal$init();
}

Переопределенные родительские методы переименовываются по такому правилу:

<имя_класса_родителя> + "$" + <имя_метода>

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

ClassManager


Теперь немного пиара моего решения. Тест скорости, ClassManager vs Native (ссылка на jsperf):



Разницу в скорости создания классов можно списать на погрешность jsperf (на старых графиках она одинакова для всех вариантов теста). К сведению: на практике у меня бывало что один и тот же код, запущенный как 2 разных теста — выполнялся с 20% разницей в скорости.

Почему вызов Native метода такой медленный — там написано вот такое:

NativeChildClass.prototype.method = function() {
    NativeParentClass.prototype.method.apply(this);
}

Сразу заметно разницу в скорости между вызовом из своего собственного прототипа и через apply. Если вам кажется, что тут я считерил — то напишите свои тесты, быстрее все равно не будет.

Отдельно стоит сказать про Firefox: создание класса, который сгенерирован в браузере — сейчас существенно медленнее (на моем старом ноуте — всего 400 000 операций в секунду). Но мой ClassManager позволяет собирать классы на сервере — и в FF они работают даже быстрее, чем Native. К тому же это ускорит загрузку страницы.

ClassManager vs другие системы

За основу я взял тест автора DotNetWise, но… его тест подло врет: у него тестируется генерация класса плюс 500 итераций по методам. Как вы понимаете, качество и скорость сгенерированного кода не зависят от времени его генерации, и у каждого протестированного фреймворка это время вносит свою погрешность. Более того, у меня классы можно собрать на сервере.

Так что намного более справедливо будет сперва создать классы, а потом уже их тестировать. И если нужно сравнивать время генерации классов — то правильно будет создать для этого отдельный тест, а не домешивать его к скорости вызова методов.

В оригинальном тесте — система автора DNW, конечно же, лидирует. Но если исправить тест, то в хроме на первом месте будет мой ClassManager, за ним идет Fiber, а потом уже DNW. В FF на первом месте TypeScript, потом Native, потом ClassManager. Даже так, это очень специфический тест — тут создание класса меряется вместе с вызовом методов (в неправильных пропорциях), так что я считаю, что реальной картины он не отражает. Тем не менее, вот ссылка и результаты:



Возможности ClassManager


Начну с очень важной детали: для моих классов работают подсказки IDE! По крайней мере, в большинстве случаев (пользуюсь PhpStorm). Вот пример того, как могут выглядеть классы:

Lava.ClassManager.define(
// все классы лежат в пространствах имен, даже глобальные
'Lava.Animal',
{
	// добавляет методы on(), _fire() и другие
	Extends: 'Lava.mixin.Observable',
	// можно и так:
	// Implements: 'Lava.mixin.Observable',

	name: null,
	toys: [], // для каждого экземпляра - свой массив

	init: function(name) {
		this.name = name;
	},

	takeToy: function(toy) {
		this.toys.push(toy)
	}

});

Lava.ClassManager.define(
'Lava.Cat',
{
	Extends: 'Lava.Animal',

	// перечисляем имена объектов, которые будут вынесены в прототип
	Shared: ['_shared'],

	// этот объект будет вынесен в прототип, он станет общим для всех классов
	_shared: {
		likes_food: ['мышь', 'вискас']
	},

	breed: null,

	init: function(name, breed) {
		this.Animal$init(name);
		this.breed = breed;
	},

	eat: function(food) {
		if (this._shared.likes_food.indexOf(food) != -1) {
			// отправляем событие, метод из Lava.mixin.Observable
			this._fire('eaten', food);
		}
	}

});

var cat = new Lava.Cat('Гарфилд', 'Персидская');

// добавляем слушатель - такую возможность предоставил нам Lava.mixin.Observable
cat.on('eaten', function(garfield, food) {
	console.log('Гарфилд сьел ' + food);
}, {});

cat.eat('мышь'); // выведет в консоль "Гарфилд сьел мышь"


Стандартные директивы:
  1. Extends — прямое наследование. Потомок может быть наследован только от одного родителя.
  2. Implements — для миксинов и множественного наследования. Домержит в потомка свойства и методы из миксина, но все что переопределено в классе — имеет приоритет.
  3. Shared — выносит объект в прототип. По умолчанию все объекты в теле класса — копируются для каждого экземпляра, но их можно сделать общими.

Бонусы:
  1. Есть возможность патчинга методов класса «на лету» и статические конструкторы. Например, вы хотите применить багфикс внутри IE, и отключить его в других браузерах. В конструкторе класса вы можете выбрать нужный вам метод и заменить его в прототипе — даже если ваш класс находится в середине цепочки наследования.
  2. Экспорт сгенерированных классов. Вы можете сгенерировать конструкторы на сервере — это сэкономит время загрузки страницы и ускорит создание объектов в Firefox.
  3. Пространства имен (namespaces) и пакеты. Подробности читайте в документации.

В планах есть добавление таких модификаторов как abstract и final.

Недостатки:
  1. Сейчас директива Shared умеет переносить в прототип только объекты (не массивы). Как временное решение — можно создать объект со свойством-массивом, так что это всего лишь небольшое неудобство. Есть задача на доработку, но она пока не в приоритете.
  2. И более заметный недостаток: сейчас нет инструмента, который мог бы сжимать имена членов класса (если их просто переименовать — то сломается вызов родительских методов). Есть планы по его созданию, он обязательно появится, но не завтра. Интересно, если бы я не сказал об этом сам, то вы бы обратили на это внимание?

Где взять?

Standalone-версия лежит в этом репозитории. В нем же есть ссылка на сайт основного фреймворка — там вы найдете отличную документацию (на английском), еще там можно посмотреть примеры в коде, и взять несколько универсальных классов типа Observable (события), Properties (свойства с событиями) и Enumerable («живой» массив).

P.S.
Да, кстати: основной фреймворк называется LiquidLava, и создавался он как лучшая альтернатива Angular и Ember. Интересно?

UPD
В комментариях меня исправили: скорость вызова Native метода можно увеличить, если заменить apply на call. Первый тест ClassManager vs Native был обновлен: в FF скорость вызова Native метода сравнялась со скоростью ClassManager, а в Хроме все еще немного уступает.
Share post

Comments 110

    +16
    ES6 наступает вам на пятки.
      +1
      Судя по таблицам его поддержки мы не будем им пользоваться в ближайший год. Судя по спецификации — классы ES6 — это синтаксический сахар над прототипами, и мне будет интересно сравнить их производительность со своим решением. Вообщем, пока ES6 наступит мне на пятку — я еще успею завоевать немного популярности :) И продукт развивается, так что время у меня еще есть.
        0
        6to5/Traceur все-таки наступают вам на пятки.
      +1
      Вроде же либы уже есть позволяющие использовать синтаксис ес6?
        +2
        Да, есть 6to5

        Спасибо за статью, сам использую что-то похожее на решение от мозиллы, что скажете об их реализации?
          –3
          Когда начинал работу над ClassManager, то от Object.create сразу отказался — хотел поддерживать IE8. Спасибо что напомнили, померяю скорость, но результат мы получим тот же, так что для меня разницы нет.

          Что насчет поддержки старых браузеров — то на начальной стадии она точно была, и сейчас скорее всего есть, но чтоб быть уверенным — стоит проверить заново.
            +3
            Незабудьте про loose-mode. Ну и пользоваться es6 гораздо приятнее, чем любым костыльным синтаксисом.
              –9
              «костыльным»? извините, но мой синтаксис не более «костыльный» чем синтаксис ES6. Когда вы видите мой класс — вы точно знаете, что будет сгенерировано.

              Мой инструмент решает реальные задачи реальных людей. Если вам так важно видеть синие слова «public» «get» и «constructor» в IDE, то вам мой инструмент не нужен, пользуйтесь ES6.
                +7
                В какой альтернативной вселенной ваш синтаксис удобнее es6 синтаксиса?
                Lava.ClassManager.define('Lava.Animal', {
                    Extends: 'Lava.mixin.Observable',
                    name: null,
                    toys: [],
                    init: function(name) {
                        this.name = name;
                    },
                    takeToy: function(toy) {
                        this.toys.push(toy)
                    }
                });
                

                class Animal extends Observable {
                    constructor(name) {
                        this.name = name;
                        this.toys = toys;
                    }
                
                    takeToy(toy) {
                        this.toys.push(toy);
                    }
                }
                

                Ну и помимо удобства, конечно поддержка ide и стандартизированность.
                  +1
                  С таким дерзким настроем мало кто будет пользоваться вашим поделием.
          +1
          В FF на первом месте TypeScript, потом Native, потом ClassManager.

          Не совсем понятно зачем тестируется TypeScript, ведь он генерирует более-менее удобоваримый нативный javascript.

          Как ваше решение работает с require.js?
            0
            Да прекрасно работает, только что даже проверил чтоб быть уверенным.
            Там простой объект в стандартной обертке…
            if (typeof module != 'undefined' && module.exports) {… } else { _global.Lava = Lava; }
            +5
            Вот что мне однажды ответил mraleph по поводу Vanilla.prototype.toString.call(this), когда я делал что-то подобное ;]
            Я думаю большая разница все-таки из-за того, что это prototype. Прочитать простое свойство это 1 mov, а прочитать prototype это куча кода
                              ;;; @60: load-function-prototype.
            0x45729a09   201  8b73ff         mov esi,[ebx+0xff]
            0x45729a0c   204  807e07b5       cmpb [esi+0x7],0xb5
            0x45729a10   208  0f853006be11   jnz 0x5730a046              ;; deoptimization bailout 7
            0x45729a16   214  f6460902       test_b [esi+0x9],0x2
            0x45729a1a   218  751d           jnz 249  (0x45729a39)
            0x45729a1c   220  8b730f         mov esi,[ebx+0xf]
            0x45729a1f   223  81fea180002e   cmp esi,0x2e0080a1          ;; object: 0x2e0080a1 <the hole>
            0x45729a25   229  0f841b06be11   jz 0x5730a046               ;; deoptimization bailout 7
            0x45729a2b   235  8b56ff         mov edx,[esi+0xff]
            0x45729a2e   238  807a0780       cmpb [edx+0x7],0x80
            0x45729a32   242  7508           jnz 252  (0x45729a3c)
            0x45729a34   244  8b760b         mov esi,[esi+0xb]
            0x45729a37   247  eb03           jmp 252  (0x45729a3c)
            0x45729a39   249  8b760f         mov esi,[esi+0xf]
            

              +6
              В вашем бенчмарке была своя специфика (я правда уже не помню какая :)). В данном конкретном случае специфика немножко другая.

              Если вам кажется, что тут я считерил — то напишите свои тесты, быстрее все равно не будет.


              Легким движением руки

              - NativeParentClass.prototype.method.apply(this);
              + NativeParentClass.prototype.method.call(this);
              


              NativeInstance.method разгоняется на Хроме в 20 раз см jsperf.com/liquidlava-class-system-performance/9

              А все почему? Потому что f.apply(this) Crankshaft не распознает. Он всегда умел f.apply(this, arguments) распознавать и недавно его научили f.call(...). Если вызов метода родителя распознается — то все проинлайнивается и различные инварианты типа LoadFunctionPrototype выносятся LICMом за пределы самого горячего цикла и, как следствие, на результат бенчмарка влияют мало (см IR цикла, красная полоса — индикатор вложенности циклов).

              Кстати, LoadFunctionPrototype похудел, он все еще больше чем одинокий mov, но меньше, чем то чудовище, которое раньше генерировалось.

              Хочется еще отметить, что замеряемый код содержит в себе один печальный антипаттерн — начальное значение this.counter экземплярам потомков приходит из прототипа, из-за этого если создать два экземпляра и дергать у них по-очереди method, то в этом самом методе возникает нездоровый полиморфизм, который может скушать на этом микробенчмарке до 50% производительности.

              В целом, наследование в JS, особенно когда у базового «класса» много детей и часть методов ими всеми разделяется, в настоящее время это performance antipattern, потому что все разделяемые методы становятся мучительно полиморфными внутри. Поэтому в коде для которго действительно важна каждая инструкция наследования стоит избегать или насильно клонировать все методы с прототипа базового класса в прототип дочернего — причем не копировать по ссылке, а именно клонировать, например, с помощью компиляции клонов через Function-конструктор.
                0
                Собственно получается, что если уж делать убер-обёртку, то с полным клонированием методов и выносом свойств в конструктор. Вот тогда заживем ;]
                  0
                  Благодарю за консультацию, обновил тест и ссылку на главной.

                  Как я понимаю, тот факт что this.counter берется из прототипа на производительность не повлиял — по крайней мере, в этом тесте.
                    +1
                    Он влияет только если создать больше одного объекта и у обоих дернуть метод пару раз — иначе незаметно (связанно это с тем, что inline caches проходят через PREMONOMOPRHIC state и это скрывает полиморфизм, второй объект выявляет это — потому что PREMONOMOPRHIC уже пройден и мы находимся в MONOMORPHIC). Посмотрите на Lava method call (2) в моей версии.
                      0
                      В том и дело, что ваш Lava method call (2) исполняется ровно с такой же скоростью как и остальные.
                      А в Firefox он вообще в 2 раза быстрее всех остальных.
                        +1
                        Если взять открыть в чистой вкладке в Хроме и выполнить сначала «Lava method call», а потом «Lava method call (2)», то должна нарисоваться такая картина. Если их выполнить в каком-то другом порядке или допусти выполнить «Lava method call» еще раз после того как «Lava method call (2)» открутится, то «Lava method call» будет такой же медленный как и «Lava method call (2)», потому что type feedback стал полиморфный.

                        С FF картина интересная, все что я говорю — оно только к V8 относится. Скорее всего в этом микробенчмарке, он (совершенно правильно) понимает что полиморфизма тут быть не может. Ох, если бы только было легко посмотреть в их сгенерированный код, без пересборки jsshell из исходников.
                          0
                          Теперь вижу, еще раз благодарю за консультацию, буду улучшать производительность.
                          Только у меня падение скорости не в 2 раза, а так:
                          Lava method call in child class (client generated class) = 13,498
                          Lava method call in child class (server generated class) = 11,233
                          Lava method call (2) = 9,283
                            +1
                            Конкретное значение уже зависит от CPU и т.д.

                            Вот после некоторых мучений нарисовал тест демонстрирующий проблему с полиморфизмом на FF

                            jsperf.com/liquidlava-class-system-performance-ff

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

                            0
                            Разрешите уточнить:
                            Правильно будет именно копировать функции, или же достаточно все начальные значения присваивать в конструкторе, а из прототипа убрать? (все-равно конструктор генерируется)
                              +1
                              Это решает проблему с «отложенной» инициализацией полей и, как следствие, убирает проблему нестабильной формы объекта, копирование функций решает другую проблему — если у вас есть разные классы наследники, то функция сидящая на прототипе базового класса будет видеть их всех — и как следствие будет полиморфной, даже если каждый отдельный класс наследник будет производить объекты стабильной формы. Если каждый класс наследник пользуется своей копией «базовой» функции — то каждая такая копия мономорфна.
                    +1
                    Посмотрел сообщения от RubaXa, вспомнил специфику. Там сравнивалось Base.prototype.method.call(this) и __.parent.call(this), где __ указывал на method в потомке, а __.parent на метод в базовом классе, т.е. Base.prototype.method. Причем все это было год назад, до того как V8 научился распознавать f.call. Из-за этого .call не проинлайнивался, и как следствие LICM не выносил LoadFunctionPrototype из цикла. Как следствие каждая лишняя инструкция была на счету и отражалась на результате микробенчмарка. (Плюс еще во втором случае на одно разыменование меньше, все складывается)
                      0
                      Все верно, собственно я тогда писал заметку о разных способах реализации super и придумался такой вариант, ну и ради красивой картинки сравнил их между собой (just for fun), что и вызвало мои вопросы.
                    0
                    В этом примере не будет работать оператор typeof, но на практике без него можно прекрасно обходиться. Я говорю про реальные приложения и задачи: если вам нужно отличать тип Animal от Cat — то это реальная задача, и она прекрасно решается. Но если вы хотите делать это оператором typeof — то извините, вам к другому доктору.


                    Эммм… Вы видимо опечатались. Возможно имелось ввиду instanceof, а typeof как бы возвращает у всех объектов «object».
                      +1
                      ой! спасибо! :)
                      0
                      1. Какие изменения в архитектуре приложения у вас произошли, и с какими трудностями столкнулись при переходе к ООП-коду? (особенности верстки и т.п.)
                      2. Можно ли при создании класса задать набор свойств? Есть ли их значение по умолчанию? Как вы присваиваете большое количество свойств?

                      Пример:
                      function Dog(name) {
                          this.name = name || "Тузик"; // Когда таких свойств у объекта много, вы так же пишите или по другому?
                      }
                      
                        –2
                        1) В JavaScript я начинал именно с MooTools и ООП. У меня были большие проекты, где нужна надежность и качество, так что ООП — это красота и спасение, а не зло. Трудности возникают если есть лапша неструктурированного кода, которую приходится переписывать.

                        2) Конечно, можно. Разрешите пригласить вас в исходник основного фреймворка
                        — там больше 100 классов, которые решают задачи из реальной жизни.
                        Пример свойств по умолчанию:
                        Lava.define('Lava.Something', {
                        // все это будет скопировано для каждого экземпляра класса
                        name: "Вася",
                        names: ["Коля", "Петя"], 
                        number: 123
                        });
                        
                          0
                          причем value-типы «name» и «number» будут вынесены в прототип, а names будет присвоен в сгенерированном конструкторе
                        0
                        То, что абсолютно нормально в С++, бывает очень вредным в JavaScript. Я говорю это из своего опыта: если в классах есть приватные методы и переменные, то такие классы часто становятся неподдерживаемыми. Если вы хотите что-то изменить в таком куске кода — то иногда вам не остается ничего кроме как выбросить старый код и переписать всё с нуля.

                        В C++ есть бесценный модификатор доступа protected. Без него код с классами и наследованием скатывается в одну из двух крайностей — либо все кишки торчат наружу, либо класс будет сложно расширить. Даже в ES6 такое не планируется, однако есть статья, где предлагается неплохой способ это реализовать. Могу перевести ее для хабра, если нужно.
                          0
                          Класс, спасибо за ссылку, благодаря вам открыл для себя Mozart.js, до этого пользовался jsface, но как-то был не в восторге.
                          +1
                          Приватные члены — это плохая практика.

                          А как же инкапсуляция?
                            –2
                            Инкапсуляция не нарушается. Приватный член класса — он так же является и защищенным, но с более сильными ограничениями.
                            Когда пишешь большое приложение на том же C++ — то почти все члены класса у вас будут именно защищенными, а не приватными, согласны?

                            Другие дело, что в JavaScript за настоящую приватность вы платите слишком большую цену, разговор был именно об этом, так что другими словами обмен получается невыгодным.
                              +3
                              Когда пишешь большое приложение на том же C++ — то почти все члены класса у вас будут именно защищенными, а не приватными, согласны?

                              Нет, не согласен. Я, правда, пишу не на C++, а на C#, но у меня нет смещения баланса в сторону protected-членов (кроме абстрактных классов).
                                +1
                                Вообще не согласен. Члены классы должны быть приватными, а всё что нужно наружу должно реализовываться через интерфейсы. Protected оправдан только для очень тесно взаимодейтсвующих классов, как правило в пределах одного юнита.
                                –2
                                Приватные члены — это, строго говоря, т.н. «сокрытие данных», а не инкапсуляция.

                                Плохой практикой ее можно назвать по двум причинам:
                                1) Если переборщить с приватными членами, мы бессмысленно затрудним наследование пользователю.
                                2) Приватные члены в JS в лучшем случае эмулируются, и без препроцессоров эта эмуляция не очень удобна. Так зачем мучаться? ЕМНИП, в Python тоже их нет, но, вроде бы, никто такими вопросами не задается.
                                  –1
                                  Приватные члены — это, строго говоря, т.н. «сокрытие данных», а не инкапсуляция.

                                  Инкапсуляция, в том числе, включает в себя сокрытие данных.

                                  Плохой практикой ее можно назвать по двум причинам:
                                  1) Если переборщить с приватными членами, мы бессмысленно затрудним наследование пользователю.

                                  Вы говорите только про JS, или про любой ОО-язык?
                                    –1
                                    Нет, инкапсуляция — это объединение данных и поведение в одной сущности. Сокрытие данных, как видите, штука ортогональная.

                                    > Вы говорите только про JS, или про любой ОО-язык?
                                    что именно?
                                      0
                                      Нет, инкапсуляция — это объединение данных и поведение в одной сущности. Сокрытие данных, как видите, штука ортогональная.

                                      О, wiki-war.

                                      In programming languages, encapsulation is used to refer to one of two related but distinct notions, and sometimes to the combination thereof:

                                      A language mechanism for restricting access to some of the object's components.
                                      A language construct that facilitates the bundling of data with the methods (or other functions) operating on that data.

                                      Some programming language researchers and academics use the first meaning alone or in combination with the second as a distinguishing feature of object-oriented programming, while other programming languages which provide lexical closures view encapsulation as a feature of the language orthogonal to object orientation.

                                      Я отношусь к этим «some programming language researchers and academics», уж простите.

                                      Вы говорите только про JS, или про любой ОО-язык?

                                      что именно?

                                      Вот это:
                                      Если переборщить с приватными членами, мы бессмысленно затрудним наследование пользователю.
                                        –5
                                        А я к другим. До свиданья.
                                  0
                                  Ну так инкапсулировать можно и без приватных членов.
                                    0
                                    В ООП? Интересно, каким образом.
                                      0
                                      Разве объединение методов и свойств в объект (класс) не является их инкапсуляцией?
                                        0
                                        Зависит от терминологической базы. Я отношусь к той части сообщества, которая считает, что сокрытие информации является неотъемлимой частью инкапсуляции.
                                          0
                                          Согласен, но в чем цель сокрытия информации?
                                            0
                                            В сокрытии сложности.
                                              0
                                              Что если скрыть сложность в псевдо-приватных методах/свойствах (они будут публичные, но о них не будет знать пользователь)?
                                                +1
                                                Пользователь всегда знает о публичных методах и свойствах. Смысл сокрытия информации именно в том, чтобы пользователь не тратил силы на разбирательство, что он должен знать, а что — нет.
                                                  +1
                                                  Для ЯП, в которых отсутствует механизм сокрытия данных, используется префикс "_" для сообщения пользователю о том, что на эти данные не нужно тратить силы для их разбирательства.
                                                    +1
                                                    Префикс — это соглашение. Соглашение требует обработки. Обработка требует сил.

                                                    Именно поэтому для сокрытия данных лучше использовать механизмы языка, а не просто соглашения.
                                                      0
                                                      Все требует обработки. Как минимум в классе необходимо найти область public прежде чем изучать его интерфейс.
                                                        +3
                                                        … однако при попытке обратиться к любому не-публичному свойству ошибка будет от компилятора, а не от собственного мозга. Вот и бонус.
                                                          0
                                                          Значит дело все таки не в сокрытии сложности, а в защите членов класса и переносе ответственности на компилятор (как в случае со строгой типизацией)?
                                                            0
                                                            Во-первых, это сокрытие сложности, которое усилено компилятором.
                                                            А во-вторых, в некоторых языках/платформах private-члены вам вообще никак не видны (ну кроме прямого влезания в код).
                                                              0
                                                              Во-первых, это сокрытие сложности, которое усилено компилятором.

                                                              Вот автор и считает, что это усиление компилятором излишне.

                                                              А во-вторых, в некоторых языках/платформах private-члены вам вообще никак не видны (ну кроме прямого влезания в код).

                                                              Без прямого влезания в код (чтение документации) можно скрыть и без private. Все зависит от используемого генератора документации.
                                                                0
                                                                Вот автор и считает, что это усиление компилятором излишне.

                                                                Ну и зря. Количество информации неизбежно увеличивается, ее сокрытие уменьшается. Другое дело, что в конкретно взятом js может быть невозможно реализовать истинное сокрытие информации — так это не означает, что оно не нужно, это означает, что его нельзя сделать, и надо смириться.

                                                                Без прямого влезания в код (чтение документации) можно скрыть и без private. Все зависит от используемого генератора документации.

                                                                Документацией дело не ограничивается. Еще есть API browsers и так далее.
                                                                  0
                                                                  в конкретно взятом js может быть невозможно реализовать истинное сокрытие информации

                                                                  Возможно.

                                                                  Еще есть API browsers

                                                                  Если не лезть в исходники, то всегда все зависит от используемой системы.
                                                                    0
                                                                    Если не лезть в исходники, то всегда все зависит от используемой системы.

                                                                    Еще зависит от того, что сразу заложено в платформу. Например, в .net изначально есть понятие публичного интерфейса сборки.
                                                                      0
                                                                      Я не знаком с .net, но думаю любой ее механизм можно перенести на ЯП, в котором не используется private.
                                                                        0
                                                                        Если вы это сделаете силами компилятора/среды выполнения, то это будет эквивалентно private. А если иначе — то это будет не тот же механизм.
                                                                          0
                                                                          Повторюсь, я не знаком с .net, потому могу ошибаться.
                                                          0
                                                          Ну, если язык позволяет создавать Интерфейсы, можно публичку описать в них и имплементировать в класс.
                                                            0
                                                            Совершенно верно. Так же можно поступить в любом ЯП без private.
                                                              0
                                                              Ну как бы Интерфейсы есть не в любом языке. Опять же предложенный способ не каноничен, но если уж сложно найти методы с модификатором public, то можно и в отдельном Интерфейсе описать их.
                                                                0
                                                                Ну как бы Интерфейсы есть не в любом языке

                                                                Что мешает реализовать, в случае отсутствия?
                                                                  0
                                                                  Отсутствие множественного наследования, например?
                                                                    0
                                                                    Стало любопытно.
                                                                    А есть языки, в которых нет интерфейсов, и нет множественного наследования?
                                                                      0
                                                                      Конечно.

                                                                      (или вы имели в виду ОО-языки?)
                                                                        0
                                                                        0
                                                                        Множественное наследование это другое. Для реализации интерфейсов достаточно хранить список реализуемых классом интерфейсов в виде системного свойства класса и проверить, реализованы ли все приведенные в интерфейсе методы в классе при его объявлении, иначе выдать ошибку.
                                                                          0
                                                                          Кому хранить, кому проверять?
                                                                            0
                                                                            Ну это уже вопрос реализации. Можно хранить прямо в классе, это позволит реализовать метод instanceof с учетом интерфейса, можно в неком менеджере, дабы не заполнять класс системными свойствами.
                                                                              0
                                                                              Вы говорите про проверки времени выполнения, реализованные прямо в коде силами программиста?
                                                                                0
                                                                                По другому никак. Не совсем программистом, можно использовать менеджер для этих целей.
                                                                                  0
                                                                                  … который написан программистом. Соответственно, каждый следующий программист, подключающийся к проекту, должен знать, что надо использовать менеджер, а не обычные объекты. И это уменьшение сложности?
                                                                                    0
                                                                                    каждый следующий программист, подключающийся к проекту, должен знать, что надо использовать менеджер

                                                                                    Как и каждый программист, должен знать синтаксис ЯП.

                                                                                    это уменьшение сложности?

                                                                                    Так дело ведь не в сложности. Если что то есть из коробки, это намного проще, чем сделанное руками, но речь идет о таких ЯП, в которых нет из коробки.
                                                                                      0
                                                                                      Как и каждый программист, должен знать синтаксис ЯП.

                                                                                      Меньше необходимых знаний — меньше ошибок, как это ни удивительно.

                                                                                      но речь идет о таких ЯП, в которых нет из коробки.

                                                                                      … а то, что вы получаете — не то же самое, что из коробки, а подобие.
                                                                                        0
                                                                                        Меньше необходимых знаний — меньше ошибок, как это ни удивительно.

                                                                                        Ничуть. Такой же объем знаний, такой же объем ошибок (если все написано качественно).

                                                                                        а то, что вы получаете — не то же самое, что из коробки, а подобие

                                                                                        Совсем не согласен. То что есть из коробки, тоже подобие чего то другого и так далее. Размытый аргумент, чтобы им оперировать.

                                                                                        На деле разница будет только в двух аспектах:
                                                                                        • На каком этапе обнаруживается ошибка
                                                                                        • Защита от дурака, который зачем то решит полезть в закрытую область класса
                                                                                          0
                                                                                          Ничуть. Такой же объем знаний, такой же объем ошибок (если все написано качественно).

                                                                                          Не такой же. Нужно знать ЯП + ваш фреймворк. Вместо того, чтобы знать только ЯП.

                                                                                          На каком этапе обнаруживается ошибка

                                                                                          Этого аргумента достаточно.
                                                                                            0
                                                                                            Не такой же.

                                                                                            Но в ЯП с этой функциональностью ее все равно нужно знать. Так какая разница?

                                                                                            Этого аргумента достаточно.

                                                                                            Согласен, что это хорошее подспорье, но в JS нет промежуточной компиляции.
                                                                                              0
                                                                                              Но в ЯП с этой функциональностью ее все равно нужно знать. Так какая разница?

                                                                                              Это входит в знание ЯП.

                                                                                              Согласен, что это хорошее подспорье, но в JS нет промежуточной компиляции.

                                                                                              Alas.
                                                                                                0
                                                                                                Это входит в знание ЯП.

                                                                                                Но объем знаний то будет одинаковый.

                                                                        0
                                                                        Как бы много чего может помешать))) Максимум что у вас получится, если делать руками — это костыльная подделка, но зачем их вообще реализовывать руками, я не знаю. Мой кейс про интерфейсы был также эфемерен, как и ваше сетование на сложность изучения публичного интерфейса класса. На мой взгляд, ниче сложного в этом нет, особенно если присутствует группировка методов по модификаторам (public наверху) в коде. Интерфейсы все же для другого создавались)
                                                                          0
                                                                          костыльная подделка, но зачем их вообще реализовывать руками

                                                                          Я тоже не знаю зачем их реализовывать, когда можно прекрасно жить без интерфейсов.

                                                                          ваше сетование на сложность изучения публичного интерфейса класса

                                                                          Вы меня с кем то путаете. Я подобного не говорил.

                                                                          если присутствует группировка методов по модификаторам

                                                                          У меня группировка по сверткам (folding) используется, это еще более простой вариант.
                                                        0
                                                        Private поля (и методы) решают вовсе не задачу сокрытия сложности. Не надо считать пользователей ваших классов идмотами, которых пугает код. Тем более, что информация как правило и не скрывается, можно посмотреть h файл.
                                                        Поля закрывают для гарантии того, что объекты класса будут находиться в инварианте на границах вызова своих методов.
                                                        И если класс предполагает уточнение своей реализации, т.е. не объявлен как final, то логично полагать, что потомки изменят и инварианты объектов. А, значит, private будет только мешать.
                                                        Конечно, если класс описывает сложный объект, часть состояния которого управляется только им и не должна быть доступна потомкам (подсчет ссылок, к примеру), private данные в нём уместны. Но вместе с тем в таких ситуациях может бфть правильным решением будет перенос части функционала в другой класс, который будет включён, или от которого будет наследование.

                                                        Так что получается, что private наиболее целесообразна для объявления методов, которые нигде не определяются :)
                                                          0
                                                          И если класс предполагает уточнение своей реализации, т.е. не объявлен как final, то логично полагать, что потомки изменят и инварианты объектов.

                                                          А как же LSP?
                                                            0
                                                            Не понял вопроса. Как вы связываете изменение инвариантов потомками и lsp?
                                                              0
                                                              Изменение инвариантов приводит к нарушению LSP (потому что объект унаследованного класса ведет себя не так, как объект базового).
                                                                0
                                                                Почему изменение инвариантов должно привести к изменению поведения? Инвариант это состояние объекта. При изменении инвариантов контракты менять вовсе не обязательно :)
                                                                  0
                                                                  Мы вот про этот инвариант говорим?

                                                                  Потому как в моем понимании инвариант — это набор условий, который всегда верен для объекта. Если он меняется между объектами — то (при некоторых изменениях, не всех) один объект нельзя использовать вместо другого, потому что наши ожидания о его инварианте не будут совпадать с реализацией.
                                                                    0
                                                                    Lsp говорит о том, что наследник не должен усиливать предусловия и ослаблять постусловия.
                                                                    Ок, я понял, что ожидая худшего вы исходите из того, что наследник сделает именно это: усилит предусловие и начнёт кидать исключения там, где базовый класс работает корректно.

                                                                    Имхо пред и постусловия относятся к тем ограничениям, которые обязательно должны быть покрыты тестами. И этой защиты достаточно. Можно и нужно исходить из того, что программист, наследующий от класса, должен понимать что и как реализовано в нём.
                                                                    А объявление закрытыми полей, значение которых определяется алгоритмами, переопределяемыми наследниками, зачастую ведёт к ненужному увеличению сложности.
                                                                    Как вы предлагаете устанавливать состояние объекта в производных классах?
                                                                      0
                                                                      Имхо пред и постусловия относятся к тем ограничениям, которые обязательно должны быть покрыты тестами. И этой защиты достаточно.

                                                                      … и тест, который будет автоматически проверять на них все классы, унаследованные от x?

                                                                      Можно и нужно исходить из того, что программист, наследующий от класса, должен понимать что и как реализовано в нём.

                                                                      В этом случае мы снова теряем сокрытие сложности, и это плохо.

                                                                      Как вы предлагаете устанавливать состояние объекта в производных классах?

                                                                      У меня нет единого ответа. В каждом случае по-своему.
                                            0
                                            Зачем имена в строках и какие-то пространства имён?
                                            Почему не просто:

                                            var MyClass = Lava.ClassManager.define({
                                            Extends: ParentClass
                                            });
                                              +1
                                              Есть весомая причина: в вашем примере ParentClass уже должен существовать на момент вызова define.
                                              А с моим подходом я складываю тела классов в массив Lava.classes, и сами классы создаю уже в Lava.init().
                                              Отложенное создание позволяет сделать monkey-патчинг для классов ядра (кто знает, что может понадобиться программистам, которые будут мой продукт использовать).

                                              В вашем подходе этого делать нельзя, в лучшем случае заменяется целый файл в сборке, а в моем — можно заменить даже метод в классе, который находится в начале цепочки наследования, причем не изменяя файлы проекта.
                                              0
                                              Интересный проект. Система классов сильно напомнила классы из Sencha/ExtJS.
                                                0
                                                К чему то похожему пришел в VimScript, но в связи с особенностями (читать — ограничениями) языка, пришлось пойти по пути Perl.
                                                Интересное решение, обязательно попробую его в каком нибудь проекте.
                                                  +1
                                                  Почему не использовать TypeScript?
                                                    0
                                                    Это такой же вопрос как «Ява против C#».

                                                    ClassManager и TypeScript — это два разных инструмента, которые решают одну и ту же задачу разными способами.
                                                    TypeScript крут! Поддержка IDE, рефакторинг… но вот те, кто сидят на линуксе — не пользуются IDE от Microsoft. Лично мне без разницы, у меня стоит Visual Studio. Давайте поищем более существенные отличия.

                                                    В комментарии выше я уже упоминал, что классы из моего фреймворка можно monkey-патчить, а в случае TypeScript, если не ошибаюсь, то вам нужно заменить весь файл с классом. То есть в моем случае у вас больше контроля.
                                                    ClassManager позволяет патчить классы во время выполнения, а TypeScript нет.
                                                    Ну и, наконец, разница в скорости.

                                                    Какой инструмент выбрать — чаще всего зависит от решаемой задачи.
                                                      0
                                                      > но вот те, кто сидят на линуксе — не пользуются IDE от Microsoft
                                                      У них компилятор для node и source-map генерируются (хотя даже сгенерированный код легко и просто читать в большинстве случаев). Отлаживайте на здоровье :)
                                                    0
                                                    Очень странный тест, либо я ничего не понял. Зачем тестировать Backbone в такого рода тесте? В реализации наследования никакой разницы с нативным наследованием(через прототипы) не будет, а для ситуаций когда нужен «общий массив» то существуют другие структуры.
                                                      0
                                                      Для полной картины все таки желательно собрать побольше фреймворков. В статье было упомянуто, что если прототип присвоить как объект, то в Firefox падает скорость создания классов. Возможно, какие-то фреймворки используют Object.create — это обязательно будет влиять на скорость, или может знают некоторые хитрости, чтоб ускорить свои классы в определенных браузерах…
                                                        0
                                                        Всё что делает extend это
                                                        extend, опуская шаманства с конструктором и цепочкой прототипа
                                                        _.extend = function(obj) {
                                                            each(slice.call(arguments, 1), function(source) {
                                                              if (source) {
                                                                for (var prop in source) {
                                                                  obj[prop] = source[prop];
                                                                }
                                                              }
                                                            });
                                                            return obj;
                                                          };
                                                        


                                                        т.е по факту просто копирует каждое из свойств(в том числе и prototype-chain) в новый объект, наследование там нужно только тогда когда некоторые функции родителя будут изменены со временем(например благодаря прототипам я не парюсь на счёт зависимостей и порядка выполнения и загрузки кода).

                                                        Я к тому что в этом тесте никакого смысла тестировать библиотеку нет, она ничего нового в классическую реализацию наследования через прототипы не вносит, да ещё и работает в разы дольше просто потому что взаимодействие между частями организовано по-другому(нет нужды в приватных переменных, а если ОЧЕНЬ надо — то они обычно организованы через замыкание), да ещё и всякая фигня при инициализации происходит(дефолты, сет, определение айдишника...). Так что слегка некоретно сравнивать один механизм наследования с цепочкой инициализации экземпляра класса.
                                                      +1
                                                      В угоду «экономии на спичках» жертвовать нормальным синтаксисом, поддержкой IDE, все возможных линтеров, а в дальнейшем нативной реализацией, и получить уродский синтаксис, непонятную структуру но зато выигрыш в мифических попугаях.
                                                      А по опыту я могу сказать, что тормозить в вашем приложении будут не создание классов, а манипуляции с DOM и тупые алгоритмы работы с данными.
                                                        0
                                                        Он вроде пишет, что есть поддержка IDE
                                                        –2
                                                        Похоже на то, что вы пытаетесь сделать JS более похожим на Python ( underscore-методы и переименования в цепочке наследования). Так что мне уже нравится. :) Ещё бы научить JS строгой типизации.
                                                          0
                                                          А в чем проблема? Используйте flowtype.org/
                                                          0
                                                          А вы сравнивали ваш генератор с тем же coffee.script?
                                                            0
                                                            С CoffeScript не сравнивал, но в нем используется наследование по методу Дугласа Крокфорда (функция extend). Могу предположить, что будет на уровне с остальными фреймворками, но если хотите быть уверенным — то лучше проверить самому.
                                                            0
                                                            Есть более красивый вариант, нежели
                                                            Cat.prototype.Animal$init = Animal.prototype.init
                                                            , и, кажется, не сильно дороже. Вот такой вот:
                                                            Cat.prototype.super = function(name, ...rest){
                                                                return Animal.prototype[name].apply(this, rest);
                                                            };
                                                            

                                                            (можно rest заменить на arguments, конечно)
                                                            Вызов this.super('init'); всё же смотрится лучше, нежели this.Animal$init();.

                                                            Only users with full accounts can post comments. Log in, please.