Статьи

Новый взгляд на JavaScript Mixins

В этой статье я подробно расскажу о JavaScript-миксинах и представлю менее привычную, но, на мой взгляд, более естественную технику миксинов, которая, как я надеюсь, окажется для вас полезной. [Большое спасибо блестящему @kitgoncharov за просмотр и улучшение кода, на котором основан этот блог!]

Повторное использование функций

В JavaScript каждый объект ссылается на объект-прототип, от которого он может наследовать свойства. Прототипы являются отличными инструментами для повторного использования: один экземпляр прототипа может определять свойства для бесконечного числа зависимых экземпляров. Прототипы также могут наследоваться от других прототипов, образуя цепочки прототипов, которые более или менее имитируют иерархии наследования классических языков, таких как Java и C ++. Многоуровневые иерархии наследования иногда полезны для описания естественного порядка объектов, но если основной мотивацией является повторное использование функций, они могут быстро превратиться в грубые лабиринты бессмысленных подтипов, разочаровывающих избыточностей и неуправляемой логики («является кнопкой или прямоугольником») элемент управления? расскажите, что, давайте заставим Button наследовать от Rectangle, а Rectangle может наследовать от Control … подождите минуту … »).

К счастью, когда дело доходит до повторного использования функций, JavaScript предлагает жизнеспособные альтернативы. В отличие от более жестко структурированных языков, объекты JavaScript могут вызывать любые открытые функции независимо от происхождения. Самый простой подход — делегирование — любая публичная функция может быть вызвана напрямую через вызов или применение. Это мощная функция, и я широко ее использую. Однако делегирование настолько удобно, что иногда оно фактически работает против структурной дисциплины в вашем коде; Более того, синтаксис может стать немного многословным. Миксины — отличный компромисс, позволяющий заимствовать и обращаться к целым функциональным единицам с минимальным синтаксисом, и они очень хорошо работают с прототипами. Они предлагают описательное мастерство иерархического наследования без проблем со взломом мозга, связанных с многоуровневым, однокоренным происхождением.

Основы

В общей информатике, mixin — это класс, который определяет набор функций, относящихся к типу (например, Person, Circle, Observer). Классы миксинов обычно считаются абстрактными в том смысле, что сами по себе они не будут созданы — вместо этого их функции копируются (или «заимствуются») конкретными классами как средство «наследования» поведения без установления формальных отношений с поставщиком поведения.

Хорошо, но это JavaScript, и у нас нет классов. Это на самом деле хорошо, потому что это означает, что мы можем вместо этого использовать объекты (экземпляры), которые предлагают ясность и гибкость: наш миксин может быть обычным объектом, прототипом, функцией — чем угодно, и процесс микширования становится прозрачным и очевидным.

Вариант использования

Я собираюсь обсудить несколько методов смешивания, но все примеры кодирования направлены на один вариант использования: создание круглых, овальных или прямоугольных кнопок. Вот схематическое представление (с использованием новейших высокотехнологичных устройств). Квадратные прямоугольники представляют объекты смешивания, округлые прямоугольники представляют фактические кнопки…


 

1. Классические миксины

Сканируя первые две страницы, возвращенные поиском Google по запросу «javascript mixin», я заметил, что большинство авторов определяют объект mixin как полноценный тип конструктора с его набором функций, определенным в прототипе. Это можно рассматривать как естественный прогресс — ранние миксины были классами, и это самое близкое, что есть в JavaScript к классу. Вот миксин круга, смоделированный после этого стиля:

var Circle = function() {};
Circle.prototype = {
area: function() {
return Math.PI * this.radius * this.radius;
},
grow: function() {
this.radius++;
},
shrink: function() {
this.radius--;
}
};

На практике, однако, такой тяжелый миксин не нужен. Достаточно простого литерала объекта:

var circleFns = {
area: function() {
return Math.PI * this.radius * this.radius;
},
grow: function() {
this.radius++;
},
shrink: function() {
this.radius--;
}
};

функция расширения

И как такой объект mixin смешивается с вашим объектом? С помощью функции расширения (иногда называемой дополнением). Обычно просто копируют (не клонируют) функции миксина в принимающий объект. Краткий обзор показывает некоторые незначительные изменения в этой реализации. Например, в Prototype.js пропущена проверка hasOwnProperty (предполагается, что миксин не будет иметь перечисляемых свойств в своей цепочке прототипов), в то время как другие версии предполагают, что вы хотите скопировать только объект-прототип миксина. Вот версия, которая является одновременно безопасной и гибкой …

function extend(destination, source) {
for (var k in source) {
if (source.hasOwnProperty(k)) {
destination[k] = source[k];
}
}
return destination;
}

… который мы можем назвать, чтобы расширить наш прототип …

var RoundButton = function(radius, label) {
this.radius = radius;
this.label = label;
};

extend(RoundButton.prototype, circleFns);
extend(RoundButton.prototype, buttonFns);
//etc. ...

2. Функциональные миксины

Если функции, определенные в миксинах, предназначены исключительно для использования другими объектами, зачем вообще создавать миксины как обычные объекты? Другими словами, миксин должен быть процессом, а не объектом. Логический вывод состоит в том, чтобы превратить наши миксины в функции, в которые потребительские объекты внедряются путем делегирования, тем самым полностью исключая посредника (функцию расширения).

var asCircle = function() {
this.area = function() {
return Math.PI * this.radius * this.radius;
};
this.grow = function() {
this.radius++;
};
this.shrink = function() {
this.radius--;
};
return this;
};

var Circle = function(radius) {
this.radius = radius;
};
asCircle.call(Circle.prototype);
var circle1 = new Circle(5);
circle1.area(); //78.54

Такой подход кажется правильным. Mixins как глаголы вместо существительных; легкие универсальные магазины. Здесь также есть и другие вещи — стиль программирования является естественным и лаконичным: это всегда относится к получателю набора функций, а не к абстрактному объекту, который нам не нужен и никогда не будет использовать; более того, в отличие от традиционного подхода, нам не нужно защищать от случайного копирования унаследованных свойств, и (для чего это нужно) функции теперь клонируются, а не копируются.

Теперь вот миксин для функций кнопок …

var asButton = function() {
this.hover = function(bool) {
bool ? mylib.appendClass('hover') : mylib.removeClass('hover');
};
this.press = function(bool) {
bool ? mylib.appendClass('pressed') : mylib.removeClass('pressed');
};
this.fire = function() {
return this.action();
};
return this;
};

Соедините два миксина, и у нас будут круглые кнопки: 

var RoundButton = function(radius, label, action) {
this.radius = radius;
this.label = label;
this.action = action;
};

asButton.call(RoundButton.prototype);
asCircle.call(RoundButton.prototype);

var button1 = new RoundButton(4, 'yes!', function() {return 'you said yes!'});
button1.fire(); //'you said yes!'

3. Добавление параметров

Эта функциональная стратегия также позволяет параметризовать заимствованное поведение с помощью аргумента опций. Давайте посмотрим на это в действии, создав миксин asOval с пользовательским коэффициентом роста и сжатия:

var asOval = function(options) {
this.area = function() {
return Math.PI * this.longRadius * this.shortRadius;
};
this.ratio = function() {
return this.longRadius/this.shortRadius;
};
this.grow = function() {
this.shortRadius += (options.growBy/this.ratio());
this.longRadius += options.growBy;
};
this.shrink = function() {
this.shortRadius -= (options.shrinkBy/this.ratio());
this.longRadius -= options.shrinkBy;
};
return this;
}

var OvalButton = function(longRadius, shortRadius, label, action) {
this.longRadius = longRadius;
this.shortRadius = shortRadius;
this.label = label;
this.action = action;
};

asButton.call(OvalButton.prototype);
asOval.call(OvalButton.prototype, {growBy: 2, shrinkBy: 2});

var button2 = new OvalButton(3, 2, 'send', function() {return 'message sent'});
button2.area(); //18.84955592153876
button2.grow();
button2.area(); //52.35987755982988
button2.fire(); //'message sent'

4. Добавление кеширования

Поэтому, возможно, вы обеспокоены тем, что этот подход создает дополнительные накладные расходы на производительность, поскольку мы переопределяем одни и те же функции при каждом вызове. С помощью превосходного jsperf.com я запустил метрики для каждой миксиновой стратегии через 4 браузера (результаты вы можете увидеть в конце этой статьи). Удивительно, но Chrome 12 работает значительно лучше, используя функциональный подход, для других браузеров функциональный миксин работает примерно вдвое быстрее, чем классический миксин. Учитывая, что эти миксины, вероятно, будут вызываться только один раз для определения типа (в отличие от однократного создания экземпляра), разница во времени не должна вызывать особого беспокойства, тем более что мы все еще говорим о 26000 миксинах в секунду даже в IE8!

Однако на случай, если подобные ставки не дают вашему менеджеру спать по ночам, есть решение. Формируя замыкание вокруг миксинов, мы можем кэшировать результаты первоначального запуска определения, и влияние на производительность остается выдающимся. Функциональные миксины теперь легко превосходят классические миксины в каждом браузере (в моих тестах в 20 раз в Chrome и в 13 раз в Firefox 4). Опять же, это не имеет большого значения в любом случае, но оставляет приятное чувство;-)

Вот версия asRectangle с добавленным кешированием…

var asRectangle = (function() {
function area() {
return this.length * this.width;
}
function grow() {
this.length++, this.width++;
}
function shrink() {
this.length--, this.width--;
}
return function() {
this.area = area;
this.grow = grow;
this.shrink = shrink;
return this;
};
})();

var RectangularButton = function(length, width, label, action) {
this.length = length;
this.width = width;
this.label = label;
this.action = action;
}

asButton.call(RectangularButton.prototype);
asRectangle.call(RectangularButton.prototype);

var button3 =
new RectangularButton(4, 2, 'delete', function() {return 'deleted'});
button3.area(); //8
button3.grow();
button3.area(); //15
button3.fire(); //'deleted'

5. Добавление карри

Все в жизни — это компромисс, и упомянутое улучшение кэширования не является исключением. Теперь мы утратили возможность создавать истинные клоны для каждого миксина, более того, мы больше не можем настраивать наши заимствованные функции, передавая аргументы опций в миксин. Последняя проблема может быть решена путем запуска функции карри над каждой кэшированной функцией, тем самым предварительно назначая пользовательские параметры для последующих вызовов функций.

Вот миксин asRectangle с функциями, соответствующим образом каррированными, чтобы позволить параметризацию приращений роста и сжатия.

Function.prototype.curry = function() {
var fn = this;
var args = [].slice.call(arguments, 0);
return function() {
return fn.apply(this, args.concat([].slice.call(arguments, 0)));
};
}

var asRectangle = (function() {
function area() {
return this.length * this.width;
}
function grow(growBy) {
this.length += growBy, this.width +=growBy;
}
function shrink(shrinkBy) {
this.length -= shrinkBy, this.width -= shrinkBy;
}
return function(options) {
this.area = area;
this.grow = grow.curry(options['growBy']);
this.shrink = shrink.curry(options['shrinkBy']);
return this;
};
})();

asButton.call(RectangularButton.prototype);
asRectangle.call(RectangularButton.prototype, {growBy: 2, shrinkBy: 2});

var button4 = new RectangularButton(2, 1, 'add', function() {return 'added'});
button4.area(); //2
button4.grow();
button4.area(); //12
button4.fire(); //'added'

Показатели эффективности

Как и было обещано, вот краткое изложение моих тестов jsperf , сгруппированных по технике и браузеру.
Помните, что результаты выполняются в тысячах операций в секунду, поэтому большие цифры — это хорошо.


 

Заворачивать

JavaScript — это сочетание функций и состояний. Состояние обычно характерно для экземпляров, в то время как функции почти наверняка будут разделены между экземплярами. Может быть, в наших интересах разделить эти две основные проблемы, и, возможно, миксины могут помочь нам в этом.

В частности, функциональный паттерн миксин предлагает четкое разграничение. Объекты находятся в состоянии, в то время как функции организованы в группы, как фрукты на дереве, созревшие для сбора. Фактически стратегия может быть расширена за пределы чистых миксинов — функциональные наборы могут выступать в качестве хранилищ для любого объекта …

var myCircle = asCircle.call({radius:25});
myCircle.area(); //1963.50

Получайте удовольствие от изучения миксинов, и, как всегда, дайте мне знать об исправлениях и других отзывах!

От http://javascriptweblog.wordpress.com/2011/05/31/a-fresh-look-at-javascript-mixins