Javascript.RU

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


Автор: Alex S (не зарегистрирован), дата: 23 апреля, 2010 - 15:28
#permalink

На самом деле можно еще описать класс в таком стиле:

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 в описанном примере).


Автор: Зарегистрирован (не зарегистрирован), дата: 16 ноября, 2011 - 15:57
#permalink

К слову

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 не просто переименовывает свойства, а дублирует их, что теоретически может выйти боком, когда свойства по ходу дела меняются, или когда делается перебор свойств (например, для сериализации).


Автор: tyrus.mlearner (не зарегистрирован), дата: 2 февраля, 2012 - 13:21
#permalink

А что же делать, если unused - мое API в конструкторе для объекта, и самим объектом не используется, но используется внешним (либо другим js-кодом, либо даже flash'ом)? Получается он его похерит Sad


Отправить комментарий

Приветствуются комментарии:
  • Полезные.
  • Дополняющие прочитанное.
  • Вопросы по прочитанному. Именно по прочитанному, чтобы ответ на него помог другим разобраться в предмете статьи. Другие вопросы могут быть удалены.
    Для остальных вопросов и обсуждений есть форум.
P.S. Лучшее "спасибо" - не комментарий, как все здорово, а рекомендация или ссылка на статью.
Содержание этого поля является приватным и не предназначено к показу.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Разрешены HTML-таги: <strike> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <u> <i> <b> <pre> <img> <abbr> <blockquote> <h1> <h2> <h3> <h4> <h5> <p> <div> <span> <sub> <sup>
  • Строки и параграфы переносятся автоматически.
  • Текстовые смайлы будут заменены на графические.

Подробнее о форматировании

CAPTCHA
Антиспам
5 + 6 =
Введите результат. Например, для 1+3, введите 4.
 
Текущий раздел
Поиск по сайту
Реклама
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Последние комментарии
Последние темы на форуме
Forum