Как стать автором
Обновить

Самовызывающийся конструктор Джона Резига и размышление о том, почему это решение не прижилось

JavaScript *
Tutorial
Настала пора мысленно вернуться на четыре с небольшим года назад ко блогозаписи «Simple “Class” Instantiation» из блога Джона Резига, прославленного создателя необыкновенно удобной библиотеки jQuery. И вернёмся.

Однако же, так как я вовсе не вижу её в результатах поиска на Хабрахабре по слову «Resig», то поневоле приходится думать, что эту полезную блогозапись никто не удосужился перевести (или хотя бы пересказать) за четыре прошедших года — мне придётся, стало быть, самостоятельно пересказать блогозапись Резига прежде, чем я исполню моё главное намерение: поразмыслить вслух, почему же предложенный Резигом способ решения указанной им проблемы так и не сделался общераспространённым. И перескажу. (Сам этот пересказ ужé был бы полезен читателю, даже кабы я к нему ничего от себя не прибавил. А я прибавлю.)



Шестого декабря 2007 года Резиг рассмотрел, что получается, когда в джаваскрипте используется операция «new» для создания объекта (в языках с классами мы сказали бы «экземпляра класса»):

function User(first, last){
   this.name = first + " " + last;
}

var user = new User("John", "Resig");

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

Поэтому, рассудил Резиг, рано или поздно кто-нибудь попробует вызвать «User()» без «new» и тем получит на свою голову сразу две неприятные проблемы. Во-первых, переменная «user» останется неопределённою: функция «User()» задумана ведь как конструктор, а значения она никакого не возвращает. Во-вторых, что ещё хуже, попытки обращения к «this» изнутри такого (некорректно вызванного) конструктора неизбежно приведёт к засорению глобального пространства имён — а это чревато зловещими и трудноуловимыми последствиями. Обе проблемы Джон Резиг продемонстрировал на примере:

var name = "Resig";
var user = User("John", name);
// здесь переменная «user» не определена
// БОЛЕЕ ТОГО: значение «name» теперь ужé не «Resig»!
if ( name == "John Resig" ) {
   // фигассе!…
}

Но тем не менее, указал далее Резиг, вызов конструктора полезен. Он имеет то достоинство, что прототипное наследование (получение свойств объекта от прототипа), то есть вызов настоящего конструктора, работает существенно быстрее, чем получение тех же свойств в виде объекта, «сконструированного» вызовом простой функции:

// Вот это работает быстро:
function User(){}
User.prototype = { /* …куча свойств… */ };

// А вот это работает медленно:
function User(){
   return { /* …куча свойств… */ };
}

Резиг сделал отсюда естественный вывод, что неплохо бы всякий раз сочинять такую функцию, которая, с одной стороны, могла бы работать конструктором (обеспечивая быстрое прототипное наследование), а с другой стороны, могла бы быть вызвана без «new» и в таком случае прибегнуть к самой себе в качестве конструктора. На примере собственной функции $() из собственной библиотеки jQuery Резиг разумно показывает: ну разве было бы удобно, если бы пользователям библиотеки вместо «$("div")» приходилось бы записывать «new $("div")»? Конечно, нет.

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

function User(first, last){
   if ( this instanceof User ) {
      // мы находимся внутри конструктора:
      this.name = first + " " + last;
   } else {
      // мы находимся внутри обычной функции:
      return new User(first, last);
   }
}

Оператор «instanceof» здесь служит главнейшим средством, позволяющим обнаружить, был ли задействован оператор «new» при вызове функции — и это нетрудно показать на простом примере:

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()» не является конструктором, а создаёт конструктор — этим конструктором служит возвращаемая из «makeClass» анонимная функция «function(args)». Так как имя «класса» (имя, которое будет в итоге дано этой функции) ещё не известно заранее, то в момент выполнения она прибегает к служебному джаваскриптовому свойству «arguments.callee», именно оттуда берёт своё имя. Другой трюк состоит в том, что если эта анонимная функция вызвана без «new», то её аргументы arguments») заново передаются внутрь неё, когда она вызывает саму себя в роли конструктора return new arguments.callee(arguments)») — и тогда именно этот набор аргументов становится параметром args и передаётся методу init.



Пересказ продуманной блогозаписи Джона Резига на этом закончен; теперь я могу наконец рассказать о том, где он сам себя, по-видимому, перемудрил.

Неприятный элемент его задумки «makeClass» состоит в употреблении свойства «arguments.callee». Это свойство считается проблемным по отношению к ускорению производительности в браузерах (современные оптимизации интерпретаторов по какой-то причине не способны с ним справиться), так что в новой версии языка (ECMAScript 5) был даже введён так называемый «строгий режим», одним из нюансов которого является полный отказ от «arguments.callee». (В мае 2009 года Джон Резиг сам упомянул о том и был переведён на Хабрахабре.)

Как мне кажется, эта неприязнь к «arguments.callee» в сообществе авторов различных джаваскриптов и библиотек со временем отчасти перешла и на саму резиговскую идею самовызывающегося конструктора — хотя эта идея лично мне кажется здравою и полезною, а противопоставление «$("div")» и «new $("div")» представляется мне веским и убедительным аргументом в пользу этой идеи.

Другой причиною неприязни к самовызывающемуся конструктору является, по-видимому, представление об операторе «new» как о важной части языка JavaScript, незнание которой настолько постыдно, что вызванные отсутствием этого оператора ошибки как бы не надо и предотвращать. (В отношении компьютерщика к не особенно удобному инструменту всегда есть нечто мазохистское, и это чувство подчас порождает жгучую неприязнь к новичкам, нуждающимся в помощи: «нет уж, это было бы слишком, слишком просто; я потрахался — теперь и ты поди потрахайся».)

Я видел это не один раз.

Помню, что в мае нынешнего (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 нынче считается вредоносным, а безопаснее в явном виде записать имя класса.

(Конец цитаты.)

Автор запроса тогда пошёл, почитал про arguments.callee, да и снял свой запрос. Получается, что недостатки arguments.callee и уважение к new в очередной раз помешали внедрению самовызывающегося конструктора.

Кто из пользователей Leaflet станет читать хотя бы «Руководство для быстрого старта», тот наверняка заметит, что определяемый этой библиотекою глобальный объект называется (очевидно, для краткости) просто «L» — а не «Leaflet», например. Шесть букв экономятся. Но ведь можно, можно было бы сэкономить ещё четыре символа, кабы не записывать «new» и пробел перед каждым вызовом конструктора.

Иногда мне хочется думать, что Джон Резиг поступил бы дальновидно, кабы вовсе воздержался от arguments.callee, ограничившись только наглядным примером (шаблоном, паттерном) записи самовызывающегося конструктора:

function User(first, last){
   if ( this instanceof User ) {
      // мы находимся внутри конструктора:
      this.name = first + " " + last;
   } else {
      // мы находимся внутри обычной функции:
      return new User(first, last);
   }
}

Но только, конечно, чтобы не создавать лишнюю if-обёртку вокруг всей функции, этот пример следует упростить:

function User(first, last){
   if (!(this instanceof User)) return new User(first, last);

   // здесь и далее мы гарантированно находимся внутри конструктора
   this.name = first + " " + last;
   // …и далее следует всё остальное тело конструктора…
}

И надо отдать должное JavaScript FAQ, составленному azproduction: там это упрощение также приводится. Оно там просто не рекомендуется.

Такому наглядному примеру следовать легко и приятно. Он к тому же ещё и попонятнее, чем конструктор конструкторов — в том числе и для оптимизаторов джаваскрипта попонятнее.

Если хотите напоследок увидеть аналогичный положительный пример из жизни, то посмотрите на код zip.js из пакета, который обеспечивает раззиповывание ZIP-архивов — и который написан на чистом джаваскрипте под Node.js (без единой строчки Си++; я и не знал, что бывают эдакие кросс-платформенные шедевры!). Там вы увидите совершенно такой же самовызов конструктора:

var Reader = exports.Reader = function (data) {
   if (!(this instanceof Reader))
      return new Reader(data);
   this._data = data;
   this._offset = 0;
}

Вывод прост: изучайте труды Джона Резига, слушайтесь его советов, поступайте по его указаниям. Но только до определённого предела сложности.



Довесок.  К концу июля 2012 года Владимир Агафонкин (Mourner) всё же внедрил в свою библиотеку Leaflet возможность обойтись без оператора «new». Но надо сказать, что в итоге убедил его не я, а forgotten, сочинивший и выложивший на Хабрахабре собственную критическую рецензию о Leaflet.
Теги:
Хабы:
Всего голосов 78: ↑67 и ↓11 +56
Просмотры 12K
Комментарии Комментарии 50