Статьи

Делегирование против наследования в JavaScript

Когда его спросили, что он мог бы сделать по-другому, если бы ему пришлось переписывать Java с нуля, Джеймс Гослинг предложил, что он может покончить с наследованием классов и написать язык только для делегирования .

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

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

В качестве примера использования я собираюсь продемонстрировать, как вызов и применение могут продвигать чистый, функциональный код подхода для повторного использования. Затем я расскажу, как спецификация ES 5 позволяет повторно использовать встроенные функции, формализуя концепцию универсальных функций.

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

Предположим, нам нужен объект Rectangle для приложения для рисования. Давайте создадим его старомодным способом, используя new и конструктор.

var Rectangle = function(left, top, length, width, options) {
02	    this.left = left;
03	    this.top = top;
04	    this.length = length;
05	    this.width = width;
06	    if (options) {
07	        this.color = options.color;
08	        this.border = options.border;
09	        this.opacity = options.opacity;
10	        //... etc.
11	    }
12	}
13	 
14	var myRectangle = new Rectangle(10, 10, 30, 20, {color:'#FAFAFA', opacity:0.7});

Нам также нужно знать, перекрывает ли прямоугольник другой. Мы добавим эту функцию в прототип:

Rectangle.prototype.overlaps = function(another) {
02	    var r1x1 = this.left,
03	        r1x2 = this.left + this.width,
04	        r1y1 = this.top,
05	        r1y2 = this.top + this.height,
06	        r2x1 = another.left,
07	        r2x2 = another.left + another.width,
08	        r2y1 = another.top,
09	        r2y2 = another.top + another.height;       
10	 
11	    return (r1x2 >= r2x1) && (r1y2 >= r2y1) && (r1x1 <= r2x2) && (r1y1 <= r2y2);
12	}
13	 
14	myRectangle.overlaps(myOtherRectangle);

Теперь предположим, что в другом месте нашего приложения есть панель инструментов, которая отображает несколько панелей. Мы хотели бы знать, перекрывают ли эти дашлеты друг друга. Мы могли бы использовать наследование — иметь прототип Dashlet наследовать от Rectangle. Но экземпляры дашлетов теперь обременены набором не относящихся к делу атрибутов: непрозрачность, цвет (и другие типичные функции рисования, такие как поворот, масштабирование и наклон). Подумайте, запутывание. Подумайте, след памяти. Более того, если наследование — это наша вещь, могут быть более подходящие кандидаты, например ContentFrame или Portlet.

Подумайте об этом … все, что мы действительно хотим сделать, это увидеть, перекрываются ли два дашлета. Предполагая, что у дашлета есть атрибуты left, top, width и height (или даже если мы должны их получить), делегирование выполняет ту же цель с гораздо меньшим размером:

Rectangle.prototype.overlaps.call(dashlet1, dashlet2);

Таким образом, мы можем даже сравнить два объектных литерала. Вот весь сценарий, чтобы вы могли проверить его:

var Rectangle = function(left, top, length, width, options) {
02	    //whatever...
03	}
04	 
05	Rectangle.prototype.overlaps = function(another) {
06	    var r1x1 = this.left,
07	        r1x2 = this.left + this.width,
08	        r1y1 = this.top,
09	        r1y2 = this.top + this.height,
10	        r2x1 = another.left,
11	        r2x2 = another.left + another.width,
12	        r2y1 = another.top,
13	        r2y2 = another.top + another.height;       
14	 
15	    return (r1x2 >= r2x1) && (r1y2 >= r2y1) && (r1x1 <= r2x2) && (r1y1 <= r2y2));
16	}
17	 
18	Rectangle.prototype.overlaps.call(
19	    {left: 10, top: 10, width 12, height: 6},
20	    {left: 8, top: 15, width 9, height: 16});
21	//true
22	Rectangle.prototype.overlaps.call(
23	    {left: 10, top: 10, width 12, height: 6},
24	    {left: 8, top: 25, width 9, height: 16});
25	//false;

Общие функции

Это все замечательно, но разве было бы неплохо вставлять экземпляры во встроенные функции? К сожалению, многие встроенные функции предназначены для выдачи ошибки TypeError, если значение this не имеет указанного типа:

Date.prototype.getMilliseconds.apply({year:2010});
2	//TypeError: Date.prototype.getMilliseconds called on incompatible Object

К счастью, спецификация EcmaScript 5 формализует концепцию универсальных функций. Это функции, которые по своей природе позволяют использовать значение this любого типа. Например, мы можем вызвать метод поиска String в контексте массива.

var hasNumbers = "".search.call(['a','b','c'],/[0-9]/) > -1;

Я каталогизировал весь список встроенных универсальных функций в конце статьи. Сначала давайте рассмотрим несколько примеров по типу:

Универсальные методы Array.prototype
toString, toLocaleString, concat, join, pop, push, reverse, shift, slice, sort, splice, unshift, indexOf, lastIndexOf, каждые, некоторые, forEach, сопоставление, фильтр, уменьшение, reduRight

Большинство из этих функций преобразуют это в Object перед вызовом, поэтому, если мы используем String в качестве контекста, те функции, которые непосредственно манипулируют аргументом (например, push и shift), удивят пользователя возвращением Object. Однако некоторые другие универсальные функции Array хорошо работают со строками:

[].forEach.apply("javascript",[function(char) {console.log("give me a " + char.toUpperCase())}]);
02	//give me a J
03	//give me a A
04	//etc...
05	 
06	var increment = function(char) {return String.fromCharCode(char.charCodeAt(0)+1)};
07	var hiddenMammal = [].map.call('rhinocerous',increment).join(''); // "sijopdfspvt"
08	 
09	var myObj = {'0':'nil', '1':'one', length:2};
10	[].push.call(myObj,'two');
11	myObj; //{'0':'nil', '1':'one', '2':'two' length:3}

Универсальные методы String.prototype
charAt, charCodeAt, concat, indexOf, lastIndexOf, localeCompare, сопоставление, замена, поиск, сращивание, разделение, подстрока, toLowerCase, toLocaleLowerCase, toUpperCase, to LocaleLowerCase, trim, substr

Большинство из этих функций преобразуют объект this в строку перед вызовом. Таким образом, если мы внедряем массив в качестве контекста, нам нужно будет преобразовать результат обратно в массив в конце, используя split.

"".trim.apply([" a","b "]).split(",");
02	//["a","b"]
03	 
04	"".toLowerCase.apply(["DIV","H1","SPAN"]).split(",");
05	//["div","h1","span"]
06	 
07	"".match.call(["a16","b44","b bar"],/[a-z][0-9]+/g);
08	//["a16", "b44"]
09	 
10	"".replace.call(
11	    ['argentina','brazil','chile'],
12	    /\b./g, function(a){ return a.toUpperCase(); }
13	).split(',');
14	//['Argentina',"Brazil","Chile"]

Общие методы Date.prototype
toJSON

Этот метод требует, чтобы значение this имело метод toISOString.

Object.prototype.toString
OK не является строго универсальной функцией (поскольку каждый первоклассный объект является объектом — ошибка типа никогда не может быть вызвана при вызове или применении — если только не используется строгий режим ES 5), тем не менее, это отличный кандидат для демонстрации полномочия делегирования.

С самого раннего появления JavaScript разработчики пытались найти лучший способ определить, является ли объект массивом. Водонепроницаемое решение только недавно видел принятие господствующего и использует способность массива , чтобы попасть внутрь метода ToString Объекта:

function isArray(obj) {
2	    return Object.prototype.toString.call(obj) == "[object Array]";
3	}

Мета-делегирование (в
некотором роде) Начиная с ES 5, сама функция применения была «восстановлена». Второй аргумент больше не должен быть массивом. Любой объект, который имеет свойства длины и индекса, может быть использован (например, аргументы или предположительно строка).

ES 5, 15.3.4.3: В редакции 3 выдается ошибка TypeError, если второй аргумент, переданный в Function.prototype.apply, не является ни объектом массива, ни объектом аргументов. В Выпуске 5 вторым аргументом может быть любой тип общего объекта типа массива, который имеет допустимое свойство длины.

 
К сожалению, браузеры не смогли быстро принять этот.

Делегирование с помощью «статических» функций (только Mozilla)
Дмитрий Сошников отмечает, что движок SpiderMonkey поддерживает очень простую форму делегирования, просто передавая аргументы в определение автономной функции. Приятно!

Array.map('abc', String.toUpperCase); //["A", "B", "C"]
2	String.toUpperCase(['a']); //"A"

 

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

Наследование реализации — это хорошая концепция — я жил и дышал ею в течение 12 лет, которые я программировал на Smalltalk и Java, — но мы должны быть открыты для более гибких и универсальных альтернатив там, где они существуют. Делегирование функций с помощью call и apply позволяет утилитам JavaScript выбирать необходимую функциональность без лишней интуитивной, раздутой и чрезмерно сложной иерархии.

Приложение: Справочник по общим функциям

(См. 5-е издание ECMA-262)
15.4.4.2 Array.prototype.toString ()
15.4.4.3 Array.prototype.toLocaleString ()
15.4.4.4 Array.prototype.concat ([item1 [, item2 [,…]]])
15.4 .4.5 Array.prototype.join (разделитель)
15.4.4.6 Array.prototype.pop ()
15.4.4.7 Array.prototype.push ([item1 [, item2 [,…]]])
15.4.4.8 Array.prototype.reverse ( )
15.4.4.9 Array.prototype.shift ()
15.4.4.10 Array.prototype.slice (начало, конец)
15.4.4.11 Array.prototype.sort (
comparefn ) 15.4.4.12 Array.prototype.splice (начало, deleteCount [, item1 [, item2 [,…]]])
15.4.4.13 Array.prototype.unshift ([item1 [, item2 [,…]]])
15.4.4.14 Array.prototype.indexOf (searchElement [, fromIndex])
15.4.4.15 Array.prototype.lastIndexOf (searchElement [, fromIndex])
15.4.4.16 Array.prototype.every (callbackfn [, thisArg])
15.4.4.17 Массив. prototype.some (callbackfn [, thisArg])
15.4.4.18 Array.prototype.forEach (callbackfn [, thisArg])
15.4.4.19 Array.prototype.map (callbackfn [, thisArg])
15.4.4.20 Array.prototype.filter (callbackfn [, thisArg])
15.4.4.21 Array.prototype.reduce (callbackfn [, initialValue])
15.4.4.22 Array.prototype.reduceRight (callbackfn [, initialValue])
15.5.4.4 String.prototype.charAt (pos)
15.5.4.5 String .prototype.charCodeAt (pos)
15.5.4.6 String.prototype.concat ([string1 [, string2 [,…]]])
15.5.4.7 String.prototype.indexOf (searchString, position)
15.5.4.8 String.prototype.lastIndexOf (searchString, position)
15.5.4.9 String.prototype.localeCompare (that)
15.5.4.10 String.prototype.match (regexp)
15.5.4.11 String.prototype.replace (searchValue,
replaceValue ) 15.5.4.12 String.prototype.search (regexp)
15.5.4.13 String.prototype. slice (начало, конец)
15.5.4.14 String.prototype.split (разделитель, лимит)
15.5.4.15 String.prototype.substring (start, end)
15.5.4.16 String.prototype.toLowerCase ()
15.5.4.17 String.prototype.toLocaleLowerCase ()
15.5.4.18 String.prototype.toUpperCase ()
15.5.4.19 String.prototype.toLocaleUpperCase ()
15.5.4.20 String.prototype.trim ()
15.9.5.44 Date.prototype.toJSON (ключ)
B.2.3 String.prototype.substr (начало, длина)

Дальнейшее чтение

Аллен Холуб в JavaWorld Почему расширяется — это Злой
Билл Веннерс: разговор с создателем Java, Джеймсом Гослингом,
Ник Фитцджеральд: ООП : Хорошие части: передача сообщений, утка, компоновка объектов, а не наследование — Отличный пост, в котором Ник сбрасывает некоторые наследства подробнее и намечены три дополнительные альтернативы.