Оптимальное ООП с Google Closure Compiler
В javascript есть несколько основных стилей объявления объектов и свойств.
Они основаны на разной структуре кода, и поэтому - по-разному сжимаются компилятором.
Цель этой статьи - выбрать наилучший стиль ООП с точки зрения минификации кода и понять, почему ООП в таком стиле сжимается лучше всего.
Сжатие будет осуществляться продвинутым способом: --compilation_level ADVANCED_OPTIMIZATIONS .
Мы рассмотрим различные варианты ООП для виджета SayWidget с двумя приватными методами init, setElemById и публичным методом setSayHandler .
Кроме того, для проверки, как работает удаление недостижимого кода, в виджете будет присутствовать неиспользуемый метод unused
Первый стиль ООП основывается на классическом механизме наследования javascript, при котором свойства и методы берутся из прототипа.
function SayWidget(id) {
this.setElemById(id)
this.init()
}
SayWidget.prototype = {
init: function() {
this.elem.style.display = 'none'
},
setElemById: function(id) {
this.elem = document.getElementById(elem)
},
setSayHandler: function() {
this.elem.onclick = function() {
alert('hi')
}
},
unused: function() { alert("unused") }
}
window['SayWidget'] = SayWidget
SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler
Результат сжатия:
function a(b) {
this.c(b);
this.b()
}
a.prototype = {b:function() {
this.a.style.display = "none"
}, c:function() {
this.a = document.getElementById(elem)
}, d:function() {
this.a.onclick = function() {
alert("hi")
}
}};
window.SayWidget = a;
a.prototype.setSayHandler = a.prototype.d;
Как видно, все свойства. кроме экстернов, были заменены на короткие.
Что очень кстати, был удален неиспользуемый метод unused .
Этот метод, в принципе, аналогичен предыдущему, но методы добавляются в прототип по одному.
function SayWidget(id) {
this.setElemById(id)
this.init()
}
SayWidget.prototype.init = function() {
this.elem.style.display = 'none'
}
SayWidget.prototype.setElemById = function(id) {
this.elem = document.getElementById(id)
}
SayWidget.prototype.setSayHandler = function() {
this.elem.onclick = function() {
alert('hi')
}
}
SayWidget.prototype.unused = function() { alert("unused") }
window['SayWidget'] = SayWidget
SayWidget.prototype['setSayHandler'] = SayWidget.prototype.setSayHandler
После сжатия:
function a(b) {
this.a = document.getElementById(b);
this.a.style.display = "none"
}
a.prototype.b = function() {
this.a.onclick = function() {
alert("hi")
}
};
window.SayWidget = a;
a.prototype.setSayHandler = a.prototype.b;
А здесь - уже интереснее. Благодаря тому, что каждая функция описана отдельно, Google Closure Compiler может построить граф взаимодействий и заинлайнить функции. Что и произошло: в прототипе осталась только одна функция a.prototype.b (бывшая setSayHandler ).
В результате размер существенно уменьшился.
Это - концептуально другой подход: методы записываются не в прототип, а в сам объект во время создания. При этом приватные свойства и методы являются локальными переменными функции-конструктора.
В следующем коде находится два неиспользуемых метода unused : один публичный и один - приватный.
function SayWidget(id) {
var elem
setElemById(id)
init()
function init() {
elem.style.display = 'none'
}
function setElemById(id) {
elem = document.getElementById(id)
}
function unused() {
alert("unused")
}
this.setSayHandler = function() {
elem.onclick = function() {
alert('hi')
}
}
this.unused = function() {
alert("unused")
}
}
window['SayWidget'] = SayWidget
После сжатия:
function b(c) {
function d() {
a.style.display = "none"
}
function e(f) {
a = document.getElementById(f)
}
var a;
e(c);
d();
this.a = function() {
a.onclick = function() {
alert("hi")
}
};
this.b = function() {
alert("unused")
}
}
window.SayWidget = b;
Этот стиль ООП обычно сжимается лучше прототипного, т.к. обычный компрессор (или Google Closure Compiler в безопасном режиме) сжимает только локальные переменные.
Но продвинутый режим Google Closure Compiler уравнивает локальные переменные с обычными (кроме экстернов) - он сжимает и то и другое.
Поэтому никакого преимущества этот стиль ООП не показал. Более того, в данном компилятору не удалось ни заинлайнить вызовы ни удалить неиспользуемый публичный метод.
Последний из рассматриваемых стилей работает без вызова оператора new. Он просто возвращает объект с нужными методами.
// object = sayWidget(id)
function sayWidget(id) {
var elem
setElemById(id)
init()
function init() {
elem.style.display = 'none'
}
function setElemById(id) {
elem = document.getElementById(id)
}
function unused() {
alert("unused")
}
var me = { // новый объект
setSayHandler: function() {
elem.onclick = function() {
alert('hi')
}
},
unused: function() {
alert("unused")
}
}
me['setSayHandler'] = me.setSayHandler
return me
}
window['sayWidget'] = sayWidget
После сжатия:
function c(a) {
function d() {
b.style.display = "none"
}
function e(f) {
b = document.getElementById(f)
}
var b;
e(a);
d();
a = {a:function() {
b.onclick = function() {
alert("hi")
}
}, b:function() {
alert("unused")
}};
a.setSayHandler = a.a;
return a
}
window.sayWidget = c;
Результат аналогичен предыдущему.
Победил в итоге, вот сюрприз, самый длинный способ - добавление каждого свойства через прототип кодом вида: SayWidget.prototype.methodXXX = ... .
Более того, эта победа не случайна. Такой способ позволяет компилятору построить самый полный граф взаимодействий и произвести инлайны.
А определение функций в прототипе вместо замыкания - дает возможность успешно вырезать неиспользуемые символы. В результате - уменьшение размера кода.
В библиотеке Google Closure Library, под которую заточен Closure Compiler, используется именно такой стиль объявления свойств.
|
На самом деле можно еще описать класс в таком стиле:
var oop = {};
(function (ns){
function KeyType() {}
/**
* @constructor
*/
function TestObject() {
this._markedObj = new KeyType()
}
TestObject.prototype = {
_markedObj : null,
doIt: function() {
opns.addEvent("keydown", function(obj) {
return obj instanceof KeyType
})
},
getMarked: function() {
return this._markedObj
},
dummy: function(event) {
opns.isEvent(event)
}
}
/* export TestObject */
ns.TestObject = TestObject
})(oop)
В данной записи пространство имен oop будет содержать "публичный" класс TestObject, который в свою очередь использует "приватный" класс KeyType. В данном случае closure сможет убрать, к примеру, метод dummy, если он не используется. И вообще, похоже, closure сможет выполнить все оптимизации за исключением одной - вызов функции вводящей определение TestObject не будет заинлайнен.
У приведенного кода есть только одно преимущество перед описанным добавлением через прототип - этот способ позволяет вводить приватные сущности, используемые публично видимыми классами (типа KeyType в описанном примере).
К слову
a.prototype.b = ...;
a.prototype.setSayHandler = a.prototype.b;
- не полный аналог
a.prototype.setSayHandler = ...;
Полный аналог выглядел бы так:
a.prototype.b = ...;
a.prototype.setSayHandler = a.prototype.b;
delete a.prototype.b; // <<<<<
Короче, GCC не просто переименовывает свойства, а дублирует их, что теоретически может выйти боком, когда свойства по ходу дела меняются, или когда делается перебор свойств (например, для сериализации).
А что же делать, если unused - мое API в конструкторе для объекта, и самим объектом не используется, но используется внешним (либо другим js-кодом, либо даже flash'ом)? Получается он его похерит