
Однако же, так как я вовсе не вижу её в результатах поиска на Хабрахабре по слову «Resig», то поневоле приходится думать, что эту полезную блогозапись никто не удосужился перевести (или хотя бы пересказать) за четыре прошедших года — мне придётся, стало быть, самостоятельно пересказать блогозапись Резига прежде, чем я исполню моё главное намерение: поразмыслить вслух, почему же предложенный Резигом способ решения указанной им проблемы так и не сделался общераспространённым. И перескажу. (Сам этот пересказ ужé был бы полезен читателю, даже кабы я к нему ничего от себя не прибавил. А я прибавлю.)
Шестого декабря 2007 года Резиг рассмотрел, что получается, когда в джаваскрипте используется операция «new» для создания объекта (в языках с классами мы сказали бы «экземпляра класса»):
function User(first, last){
this.name = first + " " + last;
}
var user = new User("John", "Resig");
Резиг справедливо подметил, что для начинающих программистов на джаваскрипте не вполне очевидно, что появление «this» в коде функции указывает на то, что перед нами конструктор объекта. (Я от себя в скобках прибавлю: если функция находится в недрах некоторой библиотеки, то это обстоятельство нуждается также и в документировании — а не то пользователь библиотеки не многим будет отличаться от новичка: исходный код с телом функции читают не все, тем более что он нередко применяется в минифицированном, не читаемом виде.)
Поэтому, рассудил Резиг, рано или поздно
var name = "Resig";
var user = User("John", name);
// здесь переменная «user» не определена
// БОЛЕЕ ТОГО: значение «name» теперь ужé не «Resig»!
if ( name == "John Resig" ) {
// фигассе!…
}
Но тем не менее, указал далее Резиг, вызов конструктора полезен. Он имеет то достоинство, что прототипное наследование (получение свойств объекта от прототипа), то есть вызов настоящего конструктора, работает существенно быстрее, чем получение тех же свойств в виде объекта, «сконструированного» вызовом простой функции:
// Вот это работает быстро:
function User(){}
User.prototype = { /* …куча свойств… */ };
// А вот это работает медленно:
function User(){
return { /* …куча свойств… */ };
}
Резиг сделал отсюда естественный вывод, что неплохо бы всякий раз сочинять такую функцию, которая, с одной стороны, могла бы работать конструктором (обеспечивая быстрое прототипное наследование), а с другой стороны, могла бы быть вызвана
К счастью, продолжил Резиг, все эти проблемы поддаются простому решению при помощи условной записи тела функции:
function User(first, last){
if ( this instanceof User ) {
// мы находимся внутри конструктора:
this.name = first + " " + last;
} else {
// мы находимся внутри обычной функции:
return new User(first, last);
}
}
Оператор «instanceof» здесь служит главнейшим средством, позволяющим обнаружить, был ли задействован
function test(){
alert( this instanceof test );
}
test(); // сработает как alert( false );
new test(); // сработает как alert( true );
Найдя это решение и убедившись в его работоспособности, Резиг сказал: а теперь давайте обернём это решение в обобщённое средство создания конструкторов «классов», который можно было бы использовать всякий раз, когда явится надобность в такого рода функциях. Для этой цели Джон Резиг выложил вот какой кусок свободного кода:
// makeClass - By John Resig (MIT Licensed)
function makeClass(){
return function(args){
if ( this instanceof arguments.callee ) {
if ( typeof this.init == "function" )
this.init.apply( this, args.callee ? args : arguments );
} else
return new arguments.callee( arguments );
};
}
Предыдущий пример, чтобы использовать этот код, должен быть переписан таким образом, чтобы тело прежнего конструктора сделалось телом метода «init» в прототипе:
var User = makeClass();
User.prototype.init = function(first, last){
this.name = first + " " + last;
};
var user = User("John", "Resig");
user.name // выдаёт «John Resig»
Логику работы «makeClass» Джон Резиг также пояснил достаточно подробно. Функция «makeClass()» не является конструктором, а создаёт конструктор — этим конструктором служит возвращаемая
Пересказ продуманной блогозаписи Джона Резига на этом закончен; теперь я могу наконец рассказать о том, где он сам себя,
Неприятный элемент его задумки
Как мне кажется, эта неприязнь
Другой причиною неприязни к самовызывающемуся конструктору является,
Я видел это не один раз.
Помню, что в мае нынешнего (2011) года в JavaScript FAQ, составленном azproduction, говорилося:
— Лучше, привычнее и идеологические создавать объекты через new. Конструкторы стоит называть с заглавной буквы.
— Я предпочитаю основываться на соглашениях и не проверяю this внутри конструктора — вызвал конструктор без new и поэтому утекло в глобалы — значит «сам дурак». И ни в коем случае не поощряю ошибку с new — некоторые проверяют если this это глобал значит пользователь вызвал конструктор без new и создают внутри конструктора объект и возвращают его — это поощрение ошибки и идеологически не верный подход.
(Конец цитаты.)
Помню также случай с Владимиром Агафонкиным, создателем прекрасной библиотеки Leaflet для отображения карт, не раз упомянутой на Хабрахабре. В августе нынешнего (2011) года к нему на Гитхабе поступил запрос на слияние, автор которого в начало каждого конструктора предлагал засунуть примерно вот такой код:
if ( !(this instanceof arguments.callee) ){
return new arguments.callee(arguments);
}
Агафонкин на это ответил ему:
— Удержание начинающих JS-авторов от совершения ошибок весьма полезно, но мне не нравится мысль о таком отуплении языка, которое допускает некорректный синтаксис вместо того, чтобы сообщить пользователю, что он ошибся.
— Вместо того, чтобы создать экземляр объекта даже без «new», мне кажется, лучше было бы сделать нечто вроде throw new Error("You forgot to put the new keyword before the class constructor.").
— И вот ещё что: я где-то читал, что arguments.callee нынче считается вредоносным, а безопаснее в явном виде записать имя класса.
(Конец цитаты.)
Автор запроса тогда пошёл, почитал
Кто из пользователей Leaflet станет читать хотя бы «Руководство для быстрого старта», тот наверняка заметит, что определяемый этой библиотекою глобальный объект называется (очевидно, для краткости)
Иногда мне хочется думать, что Джон Резиг поступил бы дальновидно, кабы вовсе воздержался
function User(first, last){
if ( this instanceof User ) {
// мы находимся внутри конструктора:
this.name = first + " " + last;
} else {
// мы находимся внутри обычной функции:
return new User(first, last);
}
}
Но только, конечно, чтобы не создавать лишнюю
function User(first, last){
if (!(this instanceof User)) return new User(first, last);
// здесь и далее мы гарантированно находимся внутри конструктора
this.name = first + " " + last;
// …и далее следует всё остальное тело конструктора…
}
И надо отдать должное JavaScript FAQ, составленному
Такому наглядному примеру следовать легко и приятно. Он к тому же ещё и попонятнее, чем конструктор конструкторов — в том числе и для оптимизаторов джаваскрипта попонятнее.
Если хотите напоследок увидеть аналогичный положительный пример из жизни, то посмотрите
var Reader = exports.Reader = function (data) {
if (!(this instanceof Reader))
return new Reader(data);
this._data = data;
this._offset = 0;
}
Вывод прост: изучайте труды Джона Резига, слушайтесь его советов, поступайте по его указаниям. Но только до определённого предела сложности.
Довесок. К концу июля 2012 года Владимир Агафонкин