Просматривал старые заметки в Evernote и набрел на список паттернов написания кода для JavaScript. Вот они.
Для повышения качества кода и улучшения понимания специфики JavaScript привожу тут весьма вольный перевод первой, не связанной с jQuery, части. Некоторые вещи могут показаться тривиальными, но большая часть программистов почему-то забывает о них, когда они действительно нужны.
Очень надеюсь, что этот пост кому-нибудь пригодится и вы найдёте для себя что-нибудь новое.
1. Объявление функций
Создание анонимных функций и присваивание их переменным.
Плохо
function getData() {
}
Хорошо
var getData = function () {
};
Преимущества:
- Улучшает понимание «функций как объектов».
- Навязывает хорошую привычку ставить точки с запятой.
- Нет навязанного предыдущим опытом представления о функциях и областях видимости.
Именованное функциональное выражение
var getData = function getData () {
};
Преимущества:
- Даёт отладчику определённое имя функции. Это упрощает изучение стека вызовов.
- Позволяет делать рекурсивные вызовы:
getData
может вызывать саму себя по имени.
Недостатки: не работает в IE, и CoffeeScript не понимает подобные выражения (https://github.com/jashkenas/coffee-script/issues/366).
Именованное функциональное выражение + «F»
var getData = function getDataF () {
};
Преимущества:
- Избавление от
(anonymous function)
в стеке вызовов. - Возможность рекурсивного вызова при использовании имя + «F».
- Работает в IE (по крайней мере до тех пор пока нет коллизии имён, как описано здесь: https://github.com/jashkenas/coffee-script/issues/366#issuecomment-242134).
Ссылки
2. Условия
Cпособы использования if-else.
Стандартный способ
if (type === 'foo' || type === 'bar') {
}
Альтернативный способ 1: регулярное выражение
if (/^(foo|bar)$/.test(type)) {
}
Альтернативный способ 2: поиск в объекте
Этот способ будет короче, когда в условии менее пяти элементов.
if (({foo:1, bar:1})[type]) {
}
Альтернативный способ 3: подход как в двоичном поиске
Этот подход лучше применять, когда нужно проверять диапазоны значений.
До:
if (value == 0) {
return result0;
} else if (value == 1) {
return result1;
} else if (value == 2) {
return result2;
} else if (value == 3) {
return result3;
} else if (value == 4) {
return result4;
} else if (value == 5) {
return result5;
} else if (value == 6) {
return result6;
} else if (value == 7) {
return result7;
} else if (value == 8) {
return result8;
} else if (value == 9) {
return result9;
} else {
return result10;
}
После:
if (value < 6) {
if (value < 3) {
if (value == 0) {
return result0;
} else if (value == 1) {
return result1;
} else {
return result2;
}
} else {
if (value == 3) {
return result3;
} else if (value == 4) {
return result4;
} else {
return result5;
}
}
} else {
if (value < 8) {
if (value == 6) {
return result6;
} else {
return result7;
}
} else {
if (value == 8) {
return result8;
} else if (value == 9) {
return result9;
} else {
return result10;
}
}
}
Альтернативный способ 4: Таблицы поиска
Таблицы поиска наиболее полезны, когда есть соответствие между ключом и значением.
До:
if (value == 0) {
return result0;
} else if (value == 1) {
return result1;
} else if (value == 2) {
return result2;
}
После:
// Определяем массив результатов.
var results = [result0, result1, result2];
// Возвращаем правильный результат.
return results[value];
Альтернативный способ 5: только логические операторы
Более короткий способ записи операторов.
var
type = 'foo',
type2 = 'bar',
result = 0;
type == 'foo' && result++;
console.log(result); // 1
!type == 'foo' || result++;
console.log(result); // 2
type == 'foo' && type2 == 'bar' && result++;
console.log(result); // 3
type == 'foo' && type2 == 'bar' && result == 3 && (result=0);
// Скобки нужны, чтобы избежать ошибки с неверной левой частью у присваивания
console.log(result); // 0
type == 'OOF' || result++; // Эквивалентно type != 'OOF' && result++;
console.log(result); // 1
Ссылки
http://paulirish.com/2009/perf/.
Paul Irish отмечает, что первый вариант (стандартный способ) не слишком походит, когда требуется уменьшить размер исходного кода, например, для кода-закладки (bookmarklet). Стандартный способ для небольшого количества условий обычно в цикле работает быстрее, чем регулярное выражение (альтернативный способ 1), и быстрее поиска по объекту (альтернативный способ 2). Скорость выравнивается примерно на десяти условиях. Смотрите http://jsperf.com/if-this-or-that.
3. Доступ к глобальному объекту
Доступ к глобальному объекту без указания идентификатора window
напрямую. Должно работать в ES3, ES5 и ES5-strict.
var global = (function () {
return this || (1, eval)('this');
}());
Тесты: http://jsperf.com/globalx.
4. Одно объявление var
Нужно использовать одно определение var
на функцию.
Преимущества:
- Единственное место в коде, где будут объявлены все локальные переменные, требуемые для функции.
- Защищает от логических ошибок, когда переменная используется до того, как она объявлена.
- Напоминает о том, что нужно объявлять переменные, и таким образом уменьшает количество глобальных переменных.
- Меньше букв (чтобы набирать и передавать).
function func() {
var a = 1
, b = 2
, sum = a + b
, myobject = {}
, i
, j;
// Тело функции...
}
function updateElement() {
var el = document.getElementById("result")
, style = el.style;
// Сделать что-нибудь с el и style...
}
От себя добавлю, что я не пользуюсь этим паттерном. Мне кажется, что современные IDE вроде WebStore могут позаботиться о локальных переменных вместо тебя, главное обращать внимание на их замечания.
5. Hoisting (подъём)
Объявление var
где-либо в функции действует так, как если бы переменные были объявлены в самом верху.
Неправильно:
myname = "global"; // глобальная переменная
function func() {
alert(myname); // "undefined"
var myname = "local";
alert(myname); // "local"
}
func();
Этот кусок кода будет себя вести будто написан так:
myname = "global"; // глобальная переменная
function func() {
var myname; // то же, что и var myname = undefined;
alert(myname); // "undefined"
myname = "local";
alert(myname); // "local"
}
func();
6. Оптимизация циклов for
Стандартный вариант
for (var i = 0; i < myarray.length; i++) {
// сделать что-нибудь с myarray[i]
}
Оптимизация 1
Кешировать размер массива, используя max
.
for (var i = 0, max = myarray.length; i < max; i++) {
// сделать что-нибудь с myarray[i]
}
Оптимизация 2
Использовать только одно объявление var
для соответствия предыдущим советам.
Замечание: недостаток в том, что станет немного сложнее копировать циклы целиком во время рефакторинга.
var i = 0,
max,
myarray = [];
for (i = 0, max = myarray.length; i < max; i++) {
// сделать что-нибудь с myarray[i]
}
Оптимизация 3
Заменить i++
на i = i + 1
или i += 1
, чтобы избежать излишней сложности.
var i = 0,
max,
myarray = [];
for (i = 0, max = myarray.length; i < max; i += 1) {
// сделать что-нибудь с myarray[i]
}
Предпочтительный вариант 1
var i, myarray = [];
for (i = myarray.length; i--;) {
// сделать что-нибудь с myarray[i]
}
Предпочтительный вариант 2
var myarray = [],
i = myarray.length;
while (i--) {
// сделать что-нибудь с myarray[i]
}
7. Оптимизация циклов for-in
Объект:
var man = {
hands:2,
legs:2,
heads:1
};
Где-то в коде был добавлен метод ко всем объектам:
if (typeof Object.prototype.clone === 'undefined') {
Object.prototype.clone = function () {
};
}
Неправильно
Цикл for-in без проверки hasOwnProperty
.
for (var i in man) {
console.log(i, ":", man[i]);
}
Результат в консоли:
hands : 2
legs : 2
heads : 1
clone: function()
Предпочтительный вариант 1
for (var i in man) {
if (man.hasOwnProperty(i)) { // фильтр
console.log(i, ":", man[i]);
}
}
Результат в консоли:
hands : 2
legs : 2
heads : 1
Предпочтительный вариант 2
Преимущество этого варианта в том, что можно избежать коллизий имён, если в объекте man
переопределено свойство hasOwnProperty
.
for (var i in man) {
if (Object.prototype.hasOwnProperty.call(man, i)) { // фильтр
console.log(i, ":", man[i]);
}
}
Предпочтительный вариант 3
Используем локальную переменную, чтобы закешировать Object.prototype.hasOwnProperty
.
var i,
hasOwn = Object.prototype.hasOwnProperty;
for (i in man) {
if (hasOwn.call(man, i)) { // фильтр
console.log(i, ":", man[i]);
}
}
8. (Не) изменение встроенных прототипов
Этот паттерн может сильно навредить поддерживаемости кода, потому что сделает его менее предсказуемым. Но можно сделать исключение, когда следующие условия выполняются:
- Ожидается, что будущие версии ECMAScript или JavaScript добавят эту функциональность. Например, можно добавить методы, описанные в ECMAScript 5, пока не все браузеры реализовали эту функциональность. В этом случае вы просто определяете необходимые методы заранее.
- Вы убеждаетесь, что ваше новое свойство ещё не существует. Ведь оно может являться частью JavaScript-движка в одном из браузеров или, возможно, было уже где-то добавлено в код.
- Вы задокументируете и обсудите с командой это изменение.
if (typeof Object.prototype.myMethod !== "function") {
Object.prototype.myMethod = function () {
// реализация...
};
}
9. switch
Улучшение читаемости и надёжности операторов switch
.
Правила:
- Выравнивайте каждый
case
сswitch
(исключение к правилам отступов после фигурной скобки) - Делайте отступы внутри каждого
case
. - Заканчивайте каждый
case
явнымbreak;
. - Избегайте fall-through (случай, когда вы специально убираете
break
). Если вы совершенно уверены, что fall-through — наилучшее решение, убедитесь, что задокументировали подобные случаи. Они могут выглядеть как ошибки для тех, кто будет читать код впоследствии. - Пишите
default
в концеswitch
, чтобы конструкция возвращала нормальный результат даже если ни один изcase
не сработал.
var inspect_me = 0,
result = '';
switch (inspect_me) {
case 0:
result = "zero";
break;
case 1:
result = "one";
break;
default:
result = "unknown";
}
10. Неявное приведение типов
Нужно избегать неявного приведения типов.
var zero = 0;
Плохо
JavaScript неявно приводит типы переменных, когда сравнивает их. Поэтому сравнения вроде false == 0
или "" == 0
возвращают true
.
if (zero == false) {
// Этот блок выполнится...
}
Хорошо
Чтобы избежать непонятных ситуаций, связанных с неявным приведением типов, всегда используйте операторы ===
и !==
. Эти операторы сравнивают не только значения, но и типы.
if (zero === false) {
// Не выполнится, потому что zero равно 0, а не false
}
Замечание: есть и другое мнение, заключающееся в том, что использование ===
является излишним, когда ==
достаточно. Например, когда вы используете typeof
, вы знаете, что оно возвращает строку, поэтому нет необходимости в стогом сравнении. Однако JSLint требует строгого равенства. Это делает код более последовательным и уменьшает умственное усилие, требуемое для чтения кода: это ==
специально поставили, или забыли поставить ===
.
11. Избегайте eval
Плохо 1
var property = "name";
alert(eval("obj." + property));
Хорошо 1
var property = "name";
alert(obj[property]);
Плохо 2
Важно понимать, что передача строк в setInterval()
, setTimeout()
и конструктор Function()
в большей части схожи с использованием eval
, и, следовательно, нужно этого избегать.
setTimeout("myFunc()", 1000);
setTimeout("myFunc(1, 2, 3)", 1000);
Хорошо 2
setTimeout(myFunc, 1000);
setTimeout(function () {
myFunc(1, 2, 3);
}, 1000);
setTimeout(myFunc, 1000, 1, 2, 3); // в некоторых браузерах (т.е. не в IE)
12. Конвертирование чисел с помощью parseInt
Нужно использовать второй параметр — основание.
Вариант 1
Если опустить в этом примере второй параметр (написать parseInt(year)
), то в результате получится 0. Это потому что «09» предполагает восьмеричное число, как если бы вы выполняли parseInt(year, 8)
, а «09» не является допустимым числом по основанию 8.
var month = "06",
year = "09";
month = parseInt(month, 10);
year = parseInt(year, 10);
Вариант 2
Если вы ожидаете данные вроде «08 hello», то parseInt()
вернёт число, в то время как остальные варианты вернут NaN
.
+"08" // Результат: 8
Number("08") // 8
13. Глобальные переменные
Глобальные переменные — это переменные, объявленные вне всех функций, или просто использованные без объявления.
myglobal = "hello"; // плохо
console.log(myglobal); // "hello"
console.log(window.myglobal); // "hello"
console.log(window["myglobal"]); // "hello"
console.log(this.myglobal); // "hello"
14. Проблемы с глобальными переменными
Плохо 1
function sum(x, y) {
// Неявная глобальная переменная
result = x + y;
return result;
}
Хорошо 1
function sum(x, y) {
// Переменная, объявленная внутри функции, недоступна вне ее.
var result = x + y;
return result;
}
Плохо 2
function foo() {
var a = b = 0;
// ...
}
Этот код будет вести себя так, как если бы вы написали:
var a = (b = 0);
Хорошо 2
function foo() {
var a, b;
// ...
a = b = 0; // обе переменные локальные
}
Иногда, когда пишешь приложение на Sencha Touch или Ext JS, очень нужно отследить порядок событий в различных компонентах и передаваемые в обработчики параметры. Зачастую это помогает лучше понять, что происходит и в самом фреймворке. Мне это тоже иногда необходимо, поэтому я написал небольшой миксин, который можно встраивать в свои компоненты.
Вариант для Sencha Touch:
Ext.define('Ext.debug.ShowEvents', {
requires: ['Ext.mixin.Observable'],
onClassMixedIn: function (cls) {
cls.prototype.fireEvent = function () {
console.log.apply(console, arguments);
Ext.mixin.Observable.prototype.fireEvent.apply(this, arguments);
};
}
});
Вариант для Ext JS:
Ext.define('Ext.debug.ShowEvents', {
requires: ['Ext.util.Observable'],
onClassMixedIn: function (cls) {
cls.prototype.fireEvent = function () {
console.log.apply(console, arguments);
Ext.util.Observable.prototype.fireEvent.apply(this, arguments);
};
}
});
Не забудьте указать фреймворку, где этот файл лежит. Использовать его нужно приблизительно так:
Ext.define('App.view.MyComponent', {
extend: 'Ext.Component',
mixins: ['Ext.debug.ShowEvents'],
...
});
Вместо Ext.Component
подойдет любой класс с Ext.mixin.Observable
(Ext.util.Observable
для Ext JS). Теперь в консоли вы увидите все события, которые генерирует этот класс.
Если что-то непонятно, можно спросить в комментариях.
Готово видео с моим выступлением на Frontend Dev Conf ’13. Я не тешу себя мыслью, что я идеальный рассказчик: слишком многое хочется в этом выступлении исправить. Но значит, мне есть к чему стремиться и над чем работать. В следующий раз я постараюсь, чтобы мой доклад звучал более гладко.
Я рекомендую всё-таки прочитать текстовую версию доклада (часть 1, часть 2), так как она более логичная и я потратил на нее значительно больше времени, чтобы донести и разъяснить сложные моменты.
private
Теперь, когда мы разобрались с наследованием, пришла пора посмотреть и на другие концепции из объектно ориентированного программирования. И начнём с ограничения доступа. Вероятно, вы догадались, что полноценных приватных методов и полей в JS нет и не планировалось. Но есть два различных подхода, чтобы ими обзавестись.
Замыкания
Я надеюсь, рассказывать, что такое замыкания, не требуется. В крайнем случае можете почитать какой-нибудь умный учебник.
Идея способа заключается в том, что мы замыкаем все необходимые приватные данные и функции внутри конструктора. Все публичные методы, которые работают с приватными данными, тоже должны быть объявлены в конструкторе.
var SomeClass = function (value) {
var someVar = value;
this.getSomeVar = function () {
return someVar;
}
};
Помните наш первый медленный вариант наследования? Это как раз он. Медленно, зато работает.
Аннотации
Второй подход активно используется, например, в Google Closure. Он не требует каких-то особых ухищрений, просто небольшая договорённость. Например, пусть все приватные поля и методы начинаются с подчёркивания (или заканчиваются подчёркиванием). И больше ничего, доступ к этим методам будет на нашей совести. Никаких предосторожностей, зато будет быстро работать.
Этот метод отлично работает со статическим анализатором кода, который, например, есть в Google Closure Compiler. Помечаем метод приватным — и компилятор будет выдавать предупреждения при попытке обратиться из-вне. Для этой цели используется специальные JSDoc-аннотации. Предыдущий пример с учётом всего сказанного будет выглядеть так:
/**
* @param {number} value
* @constructor
*/
var SomeClass = function (value) {
this._someVar = value;
};
/**
* @type {number}
* @private
*/
SomeClass.prototype._someVar = 0;
/**
* @returns {number}
*/
SomeClass.prototype.getSomeVar = function () {
return this._someVar;
};
static
Статические члены класса обычно создаются путём добавления их к объекту конструктора. Пожалуй, проще показать на примере.
var SomeClass = function () {};
SomeClass.staticVar = 0;
SomeClass.staticMethod = function () {};
Так как данные поля принадлежат объекту конструктора, их можно считать статическими. Таким образом, получается вот такая структура:
Обращаться к таким полям/методам придётся тоже через имя конструктора: SomeClass.staticMethod()
.
Однако это ещё не всё. JavaScript достаточно гибок, чтобы дать нам возможность самим организовать позднее статическое связывание (late static binding). Это, конечно, не так легко, но мы справимся.
Для начала определимся, что такое позднее статическое связывание. Поправьте меня, если я ошибаюсь, но с таким определением сталкиваются в основном программисты на PHP: в других языках подобная функциональность была заложена ещё в самом начале.
Итак, допустим, у нас есть два класса: ParentClass
и ChildClass
. В ParentClass
объявлено статическое свойство NAME
, содержащее имя класса, и статический метод getName()
, который это имя возвращает. В ChildClass
мы переопределяем свойство NAME
и присваиваем ему другое имя.
Так вот, в случае раннего связывания ChildClass.getName()
будет возвращать ParentClass.NAME
, потому что в этом методе стоит явная ссылка на класс, которому это свойство принадлежит. Т.е. getName()
определена как-то так:
ParentClass.getName = function () {
return ParentClass.NAME;
};
А в случае позднего статического связывания класс, который вызвал данный метод, определяется на этапе выполнения функции и поэтому ChildClass.getName()
вернет ChildClass.NAME
.
ParentClass.getName = function () {
var ctor = <каким-то образом определенный класс>;
return ctor.NAME;
};
А теперь попробуем позднее связывание реализовать.
Вариант 1
Первый вариант будет работать, если у нас доступно свойство __proto__
.
var ParentClass = function () {};
ParentClass.NAME = 'ParentClass';
ParentClass.getClassName = function () {
return this.NAME;
}
var ChildClass = function () {};
ChildClass.NAME = 'ChildClass';
ChildClass.__proto__ = ParentClass;
Когда мы вызываем ChildClass.getClassName()
, интерпретатор ищет в дереве наследования getClassName
и выполняет этот метод в контексте ChildClass
. Т.е. this.NAME == ChildClass.NAME
, что и требовалось получить. Если бы NAME
был не определён, то интерпретатор искал бы и его в дереве наследования.
Вариант 2
В случае, если __proto__
не доступно, придётся воспользоваться более сложными методами. В таком случае можно вынести все статические свойства в отдельный объект и наследовать их параллельно.
var ParentClass = function () {};
ParentClass.static = {};
ParentClass.static.NAME = 'ParentClass';
ParentClass.static.getClassName = function () {
return this.NAME;
};
ParentClass.prototype.static = ParentClass.static;
var ChildClass = function () {};
// Главная цепочка наследования
var tempCtor = function () {};
tempCtor.prototype = ParentClass.prototype;
ChildClass.prototype = new tempCtor();
// Наследование статических свойств
tempCtor.prototype = ParentClass.static;
ChildClass.static = new tempCtor();
ChildClass.prototype.static = ChildClass.static;
ChildClass.static.NAME = 'ChildClass';
Попробуем разобрать с той «простыней», которая у нас получилась.
Получаются 2 цепочки прототипов: одна для объектов, другая для статических полей и методов. Временная переменная tempCtop
используется для тех же целей, что и в методе наследования с временным конструктором.
Получить доступ к статическим свойствам можно, например, так: ChildClass.static.NAME
. Если нужно получить доступ из какого-либо метода класса, то это можно сделать через this.static
(именно для этого у нас есть конструкция ChildClass.prototype.static = ChildClass.static
). И, наконец, внутри статического метода все остальные статические методы и поля будут доступны просто через this
, например, this.NAME
. Весьма неплохо, как мне кажется.
private static
Лучший способ получить private static свойство — замкнуть переменную внутри анонимной функции. Хотя никто не мешает использовать способ с аннотациями и Google Closure Compiler. Покажу на примере синглтона:
var getClassInstance = function () {
var instance;
var Class = function () {
};
Class.prototype.method = function () {};
return function () {
if (!instance) {
instance = new Class();
}
return instance;
}
}();
superclass
Со ссылкой на родительский класс вообще всё просто — её нет. Поэтому, если вам нужна такая, добавьте её самостоятельно. Расширим пример с правильным вариантом наследования ещё одной строчкой:
var tempCtor = function () {};
tempCtor.prototype = ParentClass.prototype;
ChildClass.prototype = new tempCtor();
ChildClass.prototype.superclass = ParentClass;
Результат будет выглядеть так:
Вызвать метод предка можно так: this.superclass.prototype.method.call(this, arg0, arg1, ...)
.
Есть ещё один вариант, который больше подходит для использования в какой-нибудь библиотеке. Заключается он в том, чтобы добавить в прототип класса метод, скажем, inherited
, который будет смотреть, кто его вызвал, а затем искать в дереве наследования метод с таким же именем и вызывать его. Работать такой метод, несомненно, будет медленнее, но и избавит от необходимости писать много букв и помнить, к каком классу принадлежит перекрываемый метод. Достаточно будет просто написать this.inherited(arg0, arg1, ...)
. В простом варианте данный метод будет выглядеть, например, так:
Object.prototype.inherited = function () {
var proto = this.constructor.prototype;
var method;
var caller = arguments.callee.caller;
for (var i in proto) {
if (proto[i] === caller) {
method = i;
break;
}
}
if (method) {
if (proto.superclass &&
proto.superclass.prototype[method] !== undefined) {
return proto.superclass.prototype[method].apply(this, arguments);
}
}
};
Обратите внимание на переменную proto
. Чтобы данная функция заработала, нужно самим добавлять в прототип объекта свойство constructor
, указывающее на класс, к которому принадлежит данный объект. Так ведь свойство constructor
уже есть, можете сказать вы. Но нужно помнить, что не все браузеры его поддерживают, поэтому лучше перестраховаться.
А если у нас новый браузер или node.js, то можно воспользоваться свойством __proto__
.
Ключевое слово this
С ним всё на самом деле просто. В отличии от всех обычных объектно ориентированных языков, в которых this
является указателем на текущий экземпляр класса, в JavaScript this
указывает на контекст вызова функции. Т.е. если мы написали obj.method()
, то внутри функции method
значение this = obj
. А если мы скопировали функцию в какой-нибудь другой объект (obj2.method = obj.method
) и вызвали obj2.method()
, то внутри this == obj2
.
А что получится, если мы вызываем просто функцию, не принадлежащую никакому объекту? В этом случае this
будет равен корневому объекту. Это тот же объект, в который попадают все наши глобальные переменные, и он называется window
в том случае, когда речь заходит о браузерном JavaScript.
У объекта функции есть 2 стандартных метода, которые позволяют изменить контекст вызова: call
и apply
. Вы видели пример использования call
, когда речь шла о вызове метода предка. Эти два метода различаются только способом передачи параметров: call
ожидает первым параметром новый контекст, а потом все необходимые для передачи параметры. Для вызова же apply
нужно 2 параметра: новый контекст и массив с параметрами, передаваемыми в функцию.
Пространства имён
Как таковых пространств имён в JavaScript нет. Но многие библиотеки применяют некую эмуляцию. Для этого просто используются глобальные объекты, в которые уже помещают необходимые классы. При этом обращение выглядит приблизительно так: some.namespace.SomeClass
. Понятно, что объявить конструктор прямо так не получится:
some.namespace.SomeClass = function () {};
А не получится это потому, что some
может быть не определено или some.namespace
может не существовать. Значит, перед использованием пространства имен его нужно определить. Обычно это выглядит так:
some = some || {};
some.namespace = some.namespace || {};
Или же можно использовать какую-нибудь специальную функцию, которая сделает это за нас. Пример из Ext.js:
Ext.ns(‘some.namespace’);
Или вот пример из Google Closure Library:
goog.provide(‘some.namespace’);
Пример библиотеки
Чтобы всё обобщить попробуем создать свою небольшую библиотеку для работы с классами. Посмотреть конечный результат можно на GitHub. Сразу оговорюсь, что данная библиотека не предназначена для использования в реальных системах, а служит скорее примером, как можно собрать все те данные, что я привёл, в единое целое. Если увидите там неточность, скажите мне, я поправлю.
Расширение стандартных объектов
Расскажу про ещё одну интересную возможность JavaScript, которую трудно встретить в каком-нибудь другом нединамическом языке — расширение стандартных объектов. Связана она с тем, что прототип любого объекта — тоже объект, а значит его можно изменять. Т.е. когда нам нужен ещё один метод в классе Array
, то мы можем его легко добавить:
Array.prototype.doNothing = function () {};
Эта конструкция добавит doNothing
во всё массивы, включая уже созданные. А все потому, что прототип массива — это ссылка на Array.prototype
. Даже больше, если мы создадим наследника от Array, то его экземпляры тоже получат изменения.
На этом эффекте основаны различные так называемые shim-библиотеки. Они добавляют в прототипы классов методы, которых почему-то там не хватает.
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function () {...};
}
Google Closure
И напоследок хочу сказать пару слов о таком наборе инструментов от компании Google, как Google Closure, и в первую очередь о компиляторе. Дело в том, что он выполняет статическую проверку типов и позволяет на этапе компиляции увидеть проблемы кода. Так же работают компиляторы статических языков вроде Java. Например, он может следить, чтобы вы не пользовались приватными членами класса ниоткуда, кроме как из самого класса. Или же смотреть, чтобы вы не смогли передать параметры неверных типов в функцию. Конечно же это ограничивает вас в использовании языка, но вместе с тем уберегает от множества ошибок.
Ещё одна немаловажная часть Google Closure — это огромная библиотека. Если кого-то отпугивает её размер и сложность, учтите, что она разрабатывалась для написания очень больших JavaScript-приложений. На её основе сделаны большинство сервисов Google, в том числе Gmail, Google Reader и Google+1. Ещё кому-то библиотека может показаться слишком многословной, но не забываем, что написано так для того, чтобы Closure Compiler генерировал наиболее компактный и быстрый код.
Так вот, чтобы более эффективно использовать Google Closure Compiler, нам будет достаточно всего одного файла из библиотеки — base.js
. В нем описаны функции для наследования, вызова предков, управления зависимостями между js-файлами, а также некоторое количество дополнительных вспомогательных функций, большую часть из которых следует воспринимать скорее как инструкции для компилятора.
В любом случае попробуйте. Если вы любите красивый код и у вас есть страсть к статической типизации, то это для вас.
Вместо заключения
Каждый для себя решает: использовать ли ему объектно-ориентированное программирование или нет. Конечно, если вы создаёте небольшую страничку с парой несложных эффектов, то лучше будет воспользоваться jQuery и не заморачиваться. Но если вы под действием новостей пишете замену медленно уходящему на покой Google Reader, то без объектно-ориентированного подхода будет трудно. Я же в свою очередь всего лишь показал вам способ, как можно перенять лучшие практики вроде шаблонов проектирования и начать, наконец, писать понятный и структурированный код.
Информация с официального сайта библиотеки.
Введение
Как вы, должно быть, знаете, в JavaScript используется особенная модель
Итак, обо всем по порядку.
Объекты
Объекты в JavaScript представляют собой, в первую очередь, ассоциативный массив. В нем строковым или числовым (а при большом желании и типа boolean) ключам соответствует одно любое значение. Значением массива может быть и функция, в таком случае она является методом этого объекта.
Для наглядности я буду обозначать объекты таким вот образом:
Кстати, многие знают, что у обыкновенных массивов и объектов практически никаких различий, кроме набора доступных методов и небольшого количества синтаксического сахара. Например, нет никакой разницы между obj.property
и obj['property']
и следующий код:
var obj = {};
var arr = [];
генерирует идентичные объекты1. Единственной разница — в указателе на прототип. Кстати, о прототипах.
Прототипы
Прототип примерно соответствует классу в стандартной
Прототип — это то место, где будет искаться поле или метод в том случае, если его нет в самом объекте. Например, когда мы пишем a.method()
, то сначала наличие метода method
будет проверено в объекте a
, и, если там его нет, то в прототипе объекта a
. Каждый объект имеет свой прототип.
«[[прототип]]» на рисунке выше — это внутреннее свойство объекта, его нельзя прочитать напрямую. Но есть стандартная функция Object.getPrototypeOf(obj)
2, которая позволяет
Но по своей сути прототип тоже является объектом, arr = []
, то у нас будет сформирована вот такая структура:
И даже у Object
есть прототип, но он равен null
, а потому никуда не ссылается.
Конструктор
Идем дальше. Как вы, должно быть, знаете из ООП, конструктор — это специальная функция, которая вызывается при создании объекта. В JavaScript тоже есть такая функция. Вернее даже так: в JavaScript любая функция может исполнять роль конструктора. Я понимаю, это сложно сразу воспринять, особенно учитывая тот факт, что функции зачастую возвращают какие-то данные с помощью конструкции return
. Так вот, если вы воспользуетесь подобной функцией как конструктором, возвращаемое значение будет успешно отброшено и конструктор вернёт сконструированный объект3.
Для того чтобы вызвать new
:
var f = new F();
Необходимо упомянуть ещё об одной детали перед тем, как мы перейдём к связи конструкторов и прототипов. Любая функция может вести себя как объект. Вы же помните, что в JavaScript всё (почти) может вести себя как объект? Т. е. ничто не мешает нам написать4:
F.someValue = 1;
F.someMethod = function () {};
У объекта функции есть несколько стандартных свойств/методов. Одно из них называется prototype
. Это свойство и является будущим прототипом нашего объекта, сконструированного из этой функции.
prototype
у его конструктора. Важно понимать, что у объектов нет свойства prototype
, это свойство есть только у функций. И {}
, а это то же самое, что и new Object()
.
Пример создания объекта
var F = function () {
this.a = 1;
this.b = 2;
}
F.prototype.a = 3;
F.prototype.c = 4;
var f = new F();
f.a = 5;
f.c = 6;
Разберём происходящее в этом примере более подробно.
К моменту создания объекта в строке 7 мы приходим с такой структурой:
Затем происходит вызов конструктора и создаётся новый пустой объект с прототипом, взятым из функции F
:
Вслед за этим наш свежесозданный объект заполняется свойствами в конструкторе F
.
И уже затем конструктор возвращает объект. Дальше он присваивается переменной f
и дозаполняется.
Свойство __proto__
Помните, я сказал, что значение прототипа нельзя получить напрямую? Я несколько слукавил. Большая часть браузеров (кроме Internet Explorer) предоставляют подобную возможность. Для этого у каждого объекта есть свойство __proto__
.
Более того, его можно не только читать, но и писать. И эта его особенность пригодится нам в упрощённой схеме наследования. Как именно, я расскажу чуть позже.
Некоторые считают свойство __proto__
устаревшим (deprecated), но это не так. Оно скорее нестандартное, но и это будет в относительно скором времени исправлено. В разрабатываемой спецификации ECMAScript 6 оно уже описано. Если вдруг кто не знает, JavaScript — это один из диалектов ECMAScript (наряду с ActionScript), а потому должен подчиняться общей спецификации.
Наследование
Теперь, когда основные концепции разобраны, пора переходить к наследованию. Начнём с неправильных вариантов, которые зачастую выдаются за верные.
Вариант №1
Простое копирование прототипов.
var ParentClass = function () {};
ParentClass.prototype.method1 = function () {};
var ChildClass = function () {};
ChildClass.prototype = ParentClass.prototype;
ChildClass.prototype.method2 = function () {};
Вы, наверное, уже и сами видите, почему этот вариант неправильный. Но всё равно давайте разберём.
В строке 1 объявляем конструктор родителя:
Дальше добавляем в прототип ParentClass
метод method1
:
Строка 3. Объявляем дочерний конструктор:
Присваиваем дочернему прототипу родительский:
«Добавляем» ещё один метод к дочернему прототипу:
Как видите, получилось совсем не то, что ожидалось.
Вариант 2
Немного лучше первого хотя бы тем, что работает. Однажды меня пытались убедить, что именно так и должно выглядеть наследование.
var ParentClass = function () {}; // ParentClass contructor
ParentClass.prototype.method1 = function () {};
var ChildClass = function () {}; // ChildClass constructor
ChildClass.prototype = ParentClass.prototype;
var child = new ChildClass();
child.method2 = function () {};
Различие только в двух последних строчках. Мы создаём экземпляр класса и добавляем в уже созданный объект ещё один метод.
Но ведь в таком случае нужно каждый раз писать включение дополнительных методов в объект! Согласен, можно вынести инициализацию в конструктор, ведь именно для этого он и предназначен.
var ChildClass = function () { // ChildClass constructor
this.method2 = function () {};
};
Большая (и не всегда очевидная) проблема с этим методом — количество используемой памяти. Чуть позже я покажу результаты тестирования, они играют не в пользу данного метода. Ещё одна проблема в том, что наследование от ChildClass
неосуществимо прямыми методами, придётся самостоятельно вызывать конструктор родителя, чтобы он добавил недостающие методы.
Вариант 3
В этом варианте будем простым копированием добавлять необходимые поля и методы в прототип:
var ParentClass = function () {}; // ParentClass contructor
ParentClass.prototype.method1 = function () {};
var ChildClass = function () {}; // ChildClass constructor
for (var prop in ParentClass.prototype) {
if (ParentClass.prototype.hasOwnProperty(prop)) {
ChildClass.prototype[prop] = ParentClass.prototype[prop];
}
}
ChildClass.prototype.method2 = function () {};
Тут стоит обратить внимание на условие ParentClass.prototype.hasOwnProperty(prop)
. Эта конструкция for..in перебирает не только то, что находится в самом объекте, но ещё и то, что находится в его прототипе (строка [[прототип]] на диаграммах выше). Поэтому получается, что будут скопированы и те методы, которые достались по наследству от класса Object
. А зачем нам их копировать, если они и так будут у дочернего класса? Метод hasOwnProperty
как раз и позволяет избежать подобного поведения. Это стандартный метод класса Object
и он возвращает true
, если свойство находится в самом объекте, а не в его прототипе.
В результате выполнения кода выше мы получим 2 класса, как и задумывали:
Однако в таком способе, помимо необходимости вручную копировать поля и методы, есть и ещё один недостаток: невозможно использовать оператор instanceof
. childObject instanceof ParentClass
вернёт true
, то здесь это не так. Кстати, то же самое касается и предыдущего варианта.
Вариант 4. Правильный
Что же, самое время разобраться с правильным вариантом. Как вообще должна выглядеть правильная структура объекта? На мой взгляд, так:
Так как же получить подобную красоту? Начнём с того, что у нас есть 2 конструктора: ChildClass
и ParentClass
.
Требуется, чтобы ChildClass.prototype
был равен пустому объекту, у которого в прототипе будет стоять ParentClass.prototype
.
Чтобы получить такой результат, достаточно одной строки:
ChildClass.prototype = new ParentClass();
Хотя в этом случае будет выполнен и конструктор ParentClass
, что не всегда желательно. Поэтому лучшим решением будет создать временный класс с таким же прототипом, как в ParentClass
, но пустым конструктором. И уже экземпляр этого временного конструктора присваивать дочернему прототипу:
var tempCtor = function () {};
tempCtor.prototype = ParentClass.prototype;
ChildClass.prototype = new tempCtor();
Вот в принципе и всё. То, что многие разработчики считают архисложным, записывается в
Вариант 5. Используем Object.create
Идем дальше. Вы же помните, что в JavaScript есть несколько способов сделать одно и то же? Тоже и с наследованием. Всё шаманство с созданием временного класса и последующим созданием объекта можно выразить с помощью вызова всего лишь одного метода — Object.create
.
Посмотрим описание этого метода. Он принимает 2 параметра: прототип объекта и описание свойств. Свойства выходят за рамки этой статьи, можете посмотреть их сами, а вот первый параметр — это именно то что нужно. Получается, что если вызвать Object.create
только с одним параметром (прототипом создаваемого объекта), то результат будет аналогичным предыдущему способу.
ChildClass.prototype = Object.create(ParentClass.prototype);
К сожалению, Object.create
объявлен только в спецификации ECMAScript 5, поэтому данный способ наследования работает только в относительно новых браузерах (IE9+, FF4+, O12+).
Вариант 6. Магия с prototype.__proto__
Ну и напоследок, есть совсем простой способ получить то, что требуется. Я рассказывал про наличие в некоторых браузерах свойства __proto__
, которое представляет собой ссылку на прототип и которое в остальных браузерах недоступно. Получается, что мы можем просто указать прототипу класса, от кого он унаследован, безо всяких дополнительных действий.
ChildClass.prototype.__proto__ = ParentClass.prototype;
Приведу ещё раз для наглядности схему с тем, что делает эта инструкция:
Вот так, легко и просто. Но посмотрим на список браузеров, где подобная конструкция будет работать правильно: Chrome, Firefox, Safari >= 5, Opera >= 10.50. Ни одна из версий Internet Explorer сюда не входит.
Упомяну так же, что данный способ очень часто используется в node.js,
Сравнение различных вариантов
Итак, у нас получилось 3 рабочих варианта, 2
Поддержка браузерами
Google Chrome | Mozilla Firefox | Opera | IE6 | IE7 | IE8 | IE9 | IE10 | |
---|---|---|---|---|---|---|---|---|
Добавление методов в конструкторе | ||||||||
Копирование прототипа через for..in | ||||||||
Временный конструктор | ||||||||
Object.create | (4+) | (12+) | ||||||
__proto__ |
Производительность
Память (Мб) | Скорость определения (оп/с) | Скорость создания (оп/c) | |
---|---|---|---|
Добавление методов в конструкторе | 74.91 | 12 152 481 (34375,7%) | 31 716 (0,2%) |
Копирование прототипа через for..in | 5.61 | 7 946 (22,5%) | 13 880 306 (96,7%) |
Временный конструктор | 5.03 | 20 484 (57,9%) | 14 361 306 (100%) |
Object.create | 4.98 | 35 352 (100%) | 14 103 497 (98,2%) |
__proto__ | 4.98 | 22 474 (63,6%) | 13 893 309 (96,7%) |
Как видно, первый способ можно смело исключить
Второй способ уступает по скорости определения, но всё ещё вполне годен для использования, если не принимать в расчёт тот факт, что это не совсем наследование. На самом деле, он активно используется, когда вопрос заходит об аналоге mixin или traits.
Остальные способы примерно равны по производительности и потреблению памяти.
Использование памяти измерялось в Google Chrome. В нем есть инструмент Profiles, который позволяет создавать снимки памяти и изучать, на что она была потрачена. Вы можете и сами провести исследование и сравнить с моим. Вот для этого вам несколько ссылок: генератор страниц для проверки потребления памяти, тестирование скорости определения классов на jsperf и тестирование скорости создания объектов на jsperf.
Лично я продолжу применять способ с использованием временного конструктора
На самом деле, при создании массива будет еще создано поле
length
, в котором содержится длина.Поддерживается начиная с IE9.
Это не верно, если
функция-конструктор возвращает объект. В этом случае результатом будет возвращённый объект, а не полученный из этого конструктора.Т. е. в этом случае нет большой разницы между вызовом функции сnew
и вызовом безnew
.Кстати, именно так реализуются статические методы и свойства.