При сжатии javascript-кода минификатор делает две основные вещи.
В статье рассматриваются минификаторы YUI Compressor и ShrinkSafe.
На момент написания это лучшие минификаторы javascript.
Есть несколько несложных приемов программирования, которые могут увеличить сжимаемость JS-кода.
Минификатор заменяет все локальные переменные на более короткие
(Также о сжатии в статье Сжатие Javascript и CSS).
Например, вот такой скрипт:
function flyToMoon(moon) {
var spaceShip = new SpaceShip()
spaceShip.fly(moon.getDistance())
}
</div>
После минификации станет:
function flyToMoon(A) {
var B = new SpaceShip()
B.fly(A.getDistance())
}
Заведомо локальные переменные moon, spaceShip могут быть безопасно заменены на более короткие A,B. Минификатор использует патченный открытый интерпретатор javascript, написанный на java: Rhino, он разбирает код, анализирует и собирает обратно с заменой, поэтому все делается корректно.
С другой стороны, название функции в этом скрипте - глобальная переменная, оно не сжимается, чтобы сохранить возможность обращения к функции.
По тем же причинам не сжимается вызов SpaceShip и методы fly, getDistance().
Итак, сделаем какой-нибудь более реальный скрипт и напустим на него YUI Compressor.
function SpaceShip(fuel) {
this.fuel = fuel
this.inflight = false
this.showWarning = function(message) {
var warningBox = document.createElement('div')
with (warningBox) {
innerHTML = message
className = 'warningBox'
}
document.body.appendChild(warningBox)
}
this.showInfo = function(message) {
var messageBox = document.createElement('div')
messageBox.innerHTML = message
document.body.appendChild(messageBox)
}
this.fly = function(distance) {
if (distance<this.fuel) {
this.showWarning("Мало топлива!")
} else {
this.inflight = true
this.showInfo("Взлетаем")
}
}
}
function flyToMoon() {
var spaceShip = new SpaceShip()
spaceShip.fly(1000)
}
flyToMoon()
К сожалению, в YUI Compressor нельзя отключить убивание переводов строки, поэтому получилось такое:
function SpaceShip(fuel){this.fuel=fuel;
this.inflight=false;this.showWarning=function(message){
var warningBox=document.createElement("div");
with(warningBox){innerHTML=message;
className="warningBox"
}document.body.appendChild(warningBox)
};this.showInfo=function(message){
var messageBox=document.createElement("div");
messageBox.innerHTML=message;
document.body.appendChild(messageBox)
};this.fly=function(distance){
if(distance<this.fuel){this.showWarning("Мало топлива!")
}else{this.inflight=true;
this.showInfo("Взлетаем")
}}}function flyToMoon(){var A=new SpaceShip();
A.fly(1000)};flyToMoon()
Если посмотреть внимательно - видно, что локальные переменные вообще не сжались внутри SpaceShip, а сжались только в функции flyToMoon.
ShrinkSafe замечательно сжимает все, что только может.
function SpaceShip(_1){
this.fuel=_1;
this.inflight=false;
this.showWarning=function(_3){
var _4=document.createElement("div");
with(_4){
innerHTML=_3
className="warningBox";
}
document.body.appendChild(_4);
};
this.showInfo=function(_5){
var _6=document.createElement("div");
_6.innerHTML=_5;
document.body.appendChild(_6);
};
this.fly=function(_7){
if(_7<this.fuel){
this.showWarning("Мало топлива!");
}else{
this.inflight=true;
this.showInfo("Взлетаем");
}
};
};
function flyToMoon(){
var _8=new SpaceShip();
_8.fly(1000);
};
flyToMoon();
Все локальные переменные заменены на более короткие _варианты.
Почему YUI не сжал, а ShrinkSafe справился?
Дело в конструкции with() { ... } в методе showWarning. Эти два компрессора по-разному к ней относятся.
ShrinkSafe игнорирует нелокальные названия переменных внутри with:
// Было
with(val) {
prop = val
}
// Стало
with(_1) {
prop = _1
}
К сожалению, в последней версии ShrinkSafe есть баг: если переменная prop объявлена локально, то она заменяется:
// Было
var prop
with(val) {
prop = val
}
// Стало
var _1
with(_2) {
_1 = _2
}
Например, если val = { prop : 5 }, то сжатая таким образом конструкция with сработает неверно из-за сжатия prop до _1.
Впрочем, никогда не слышал, чтобы кто-то на такой баг реально напоролся. Видимо, люди локальные переменные не объявляют одноименные со свойствами аргумента with, чтобы код понятнее был.
Внутри оператора with(obj) никогда нельзя точно сказать: будет ли переменная взята из obj или из внешней области видимости.
Поэтому никакие переменные внутри with сжимать нельзя.
А раз так - получается, что локальные переменные с именами, упомянутыми внутри with тоже сжимать нельзя.
YUI Compressor почему-то (почему? есть идеи?) пошел еще дальше: он не минифицирует вообще никаких локальных переменных даже в соседних функциях.
Может быть, это баг (версия 2.3.5), а может - фича, не знаю. Будут идеи - пишите в комментариях. Например, локальные переменные функции fly вполне можно было минифицировать.
Вывод:
YUI категорически не любит with.
ShrinkSafe любит, но с багофичей.
Если заменить функцию showWarning на вариант без with, то YUI сожмет код без проблем:
// вариант без with
this.showWarning = function(message) {
var warningBox = document.createElement('div')
warningBox.innerHTML = message
warningBox.className = 'warningBox'
document.body.appendChild(warningBox)
}
Результат сжатия YUI без with:
function SpaceShip(A){this.fuel=A;
this.inflight=false;this.showWarning=function(B){var C=document.createElement("div");
C.innerHTML=B;C.className="warningBox";
document.body.appendChild(C)
};this.showInfo=function(B){var C=document.createElement("div");
C.innerHTML=B;document.body.appendChild(C)
};this.fly=function(B){if(B<this.fuel){this.showWarning("Мало топлива!")
}else{this.inflight=true;
this.showInfo("Взлетаем")
}}}function flyToMoon(){var A=new SpaceShip();
A.fly(1000)}
В примере не сжались вызовы к объекту document.
Для того, чтобы сжатие сработало, надо заменить обращение к глобальной переменной document вызовом локальной функции.
Например, вот так:
function SpaceShip(fuel) {
/* сокращенные локальные вызовы */
var doc = document
var createElement = function(str) {
return doc.createElement(str)
}
var appendChild = function(elem) {
doc.body.appendChild(elem)
}
this.fuel = fuel
this.inflight = false
this.showWarning = function(message) {
var warningBox = createElement('div')
warningBox.innerHTML = message
warningBox.className = 'warningBox'
appendChild(warningBox)
}
this.showInfo = function(message) {
var messageBox = createElement('div')
messageBox.innerHTML = message
appendChild(messageBox)
}
this.fly = function(distance) {
if (distance<this.fuel) {
this.showWarning("Мало топлива!")
} else {
this.inflight = true
this.showInfo("Взлетаем")
}
}
}
Обращение к document осталось в одном месте, что тут же улучшает сжатие:
(Здесь и дальше для сжатия использован ShrinkSafe, т.к он оставляет переводы строки.
Результаты YUI - по сути, такие же)
function SpaceShip(_1){
var _2=document;
var _3=function(_4){
return _2.createElement(_4);
};
var _5=function(_6){
_2.body.appendChild(_6);
};
this.fuel=_1;
this.inflight=false;
this.showWarning=function(_7){
var _8=_3("div");
_8.innerHTML=_7;
_8.className="warningBox";
_5(_8);
};
this.showInfo=function(_9){
var _a=_3("div");
_a.innerHTML=_9;
_5(_a);
};
this.fly=function(_b){
if(_b<this.fuel){
this.showWarning("Мало топлива!");
}else{
this.inflight=true;
this.showInfo("Взлетаем");
}
};
Как правило, в интерфейсах достаточно много обращений к document, и все они длинные, поэтому этот подход может уменьшить сразу код эдак на 10-20%.
Функции объявлены через var, а не function:
var createElement = function(str) { // (1)
return doc.createElement(str)
}
// а не
function createElement(str) { // (2)
return doc.createElement(str)
}
Это нужно для ShrinkSafe, который сжимает только определения (1). Для YUI - без разницы, как объявлять функцию, сожмет и так и так.
Существуют различные способы объявления объектов.
Один из них - фабрика объектов, когда для создания не используется оператор new.
Общая схема фабрики объектов:
function object() {
var private = 1 // приватная переменная для будущего объекта
return { // создаем объект прямым объявлением в виде { ... }
increment: function(arg) { // открытое свойство объекта
arg += private // доступ к приватной переменной
return arg
}
}
}
// вызов не new object(), а просто
var obj = object()
Прелесть тут состоит в том, что приватные переменные являются локальными, и поэтому могут быть сжаты. Кроме того, убираются лишние this.
function SpaceShip(fuel) {
var doc = document
var createElement = function(str) {
return doc.createElement(str)
}
var appendChild = function(elem) {
doc.body.appendChild(elem)
}
var inflight = false
var showWarning = function(message) {
var warningBox = createElement('div')
warningBox.innerHTML = message
warningBox.className = 'warningBox'
appendChild(warningBox)
}
var showInfo = function(message) {
var messageBox = createElement('div')
messageBox.innerHTML = message
appendChild(messageBox)
}
return {
fly: function(distance) {
if (distance<this.fuel) {
showWarning("Мало топлива!")
} else {
inflight = true
showInfo("Взлетаем")
}
}
}
}
function flyToMoon() {
var spaceShip = SpaceShip()
spaceShip.fly(1000)
}
function SpaceShip(_1){
var _2=document;
var _3=function(_4){
return _2.createElement(_4);
};
var _5=function(_6){
_2.body.appendChild(_6);
};
var _7=false;
var _8=function(_9){
var _a=_3("div");
_a.innerHTML=_9;
_a.className="warningBox";
_5(_a);
};
var _b=function(_c){
var _d=_3("div");
_d.innerHTML=_c;
_5(_d);
};
return {fly:function(_e){
if(_e<this.fuel){
_8("Мало топлива!");
}else{
_7=true;
_b("Взлетаем");
}
}};
};
function flyToMoon(){
var _f=SpaceShip();
_f.fly(1000);
};
flyToMoon();
Максимально возможное использование локальных переменных, собственно, и улучшает минификацию. А некоторые подходы в этой статье - лишь иллюстрации.