Оптимальное ООП с 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).

В результате размер существенно уменьшился.

Запись в 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, используется именно такой стиль объявления свойств.