В 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).
В результате размер существенно уменьшился.
this.Это - концептуально другой подход: методы записываются не в прототип, а в сам объект во время создания. При этом приватные свойства и методы являются локальными переменными функции-конструктора.
В следующем коде находится два неиспользуемых метода 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, используется именно такой стиль объявления свойств.