21 декабря 2011 в 14:09

Самовызывающийся конструктор Джона Резига и размышление о том, почему это решение не прижилось 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.
+56
5196
211
Mithgol 24,4

Комментарии (50)

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

Зачем, зачем подменять собою функции минификатора?
–1
Mithgol #
В данном случае я вёл речь не об экономии того объёма джаваскрипта, который отдаётся во браузер, а об экономии усилий самогó того программиста, которому набирать этот код. Разве оператор «new» в свете вышеизложенных соображений не начинает выглядеть как своего рода boilerplate code?
+2
k12th #
У создания «инстанса класса» и у вызова функции — разная семантика. Различая эти конструкции на письме, мы сделаем наш код более читабельным. Как по мне, так 4 символа — небольшая плата за ясность.
0
Mithgol #
Если автор джаваскриптовой библиотеки делает свои конструкторы самовызывающимися, то он не принуждает пользователя библиотеки (другого программиста на джаваскрипте) к отказу от «new»: если тот пожелает делать вызов конструктора более заметным при помощи «new», то имеет полное право.

Внедрение самовызывающихся конструкторов, однако же, даёт выбор, то есть обеспечивает возможность отказа от «new», если будет на то воля и желание пользователя библиотеки.

Программист, использующий библиотеку, может даже использовать «new» только перед частью вызовов конструкторов, а перед другою частью конструкторов от «new» воздерживаться — и это поведение также всецело будет поддерживаться библиотекою…
+4
k12th #
А потом придет другой программист, и будет недоумевать — почему тут new L(…), а там — просто L(…). Мне не нравится эта идея.
+1
B_Vladi #
Поддерживаю.
Делать нужно либо так либо так. Если речь о библиотечной функции (jQuery, Leaflet), зачем вообще делать из неё конструктор — сделайте фабрику и вызывайте без new.

Более того, утверждение: «здесь и далее мы гарантированно находимся внутри конструктора» ошибочно:
var instance = new Constructor;
Consctructor.call(instance);

–1
Mithgol #
Constructor.call(instance) это в общем-то такое извращение, на которое можно пойти только сознательно и с пониманием того, что происходит.
0
Maccimo #
Вы не правы.
Новичок способен пойти на любое извращение, подсмотренное им в чужом коде и показавшееся ему интересным.
Логика у него при этом примерно следующая: «Другие так делают, значит так делать можно».
+1
corker #
На этот случай должен быть coding guideline.
0
k12th #
Да, и еще надо чтобы они соблюдались. А для этого нужно code review как минимум.
+1
jMas #
На данный момент поддерживаю k12th.
Необходимо находить баланс в удобности использования библиотеки.
Как по мне, важней правильно именовать методы и в целом заботиться о хорошей структуре библиотеки, чем о «дурачках», которые не понимают что такое ООП.
+2
jMas #
Да простите меня за столь вольное название новичков. :) Я любя.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
–2
Mithgol #
Минификатором же, как раз напротив, будут обработаны именно условия «if (!(this instanceof имяКонструктора)) return new имяКонструктора(аргумент1, аргумент2, );» внутри библиотеки.
–2
Mithgol #
Попутно прошу также прощения за некорректность, я несколько оговорился: «L» не конструктор, а корневой объект, чьими методами являются конструкторы. Оговорку исправил во блогозаписи.
+1
Mourner #
Это просто для удобства, чтобы не загромождать код одним и тем же постоянно встречающимся словом. Я позаимствовал идею из jQuery с его $: вместо того, чтобы рекомендовать конструкции вроде
(function () { var L = Leaflet; ... }());
в каждом месте, где используется библиотека, я решил сделать наоборот — создавать переменную L безопасно, сохраняя старое значение (если есть), и дать возможность восстановить его, объявив для Leaflet любой другой неймспейс:
var Leaflet = L.noConflict(); // после этого L указывает на старое значение

Или писать безопасно в стиле jQuery Plugins:
(function (L) {
    ...
}(L.noConflict()));
0
k12th #
Я понимаю, чем вы руководствовались. Но меня просто отпугивают однобуквенные переменные еще со времен копания в кишках tinyMCE. До определенной степени это вкусовщина, и мне лень расписывать, в чем опасности такого подхода.
+1
Mourner #
Ну если кто-то чувствует опасность, можно воспользоваться описанным в комментарии методом и заменить на что душе угодно. :) Главное, что есть выбор.
+10
TheShock #
В atom.Class есть статический метод invoke. Как-то так:
var MyClass = atom.Class({
	Static: function () {
		invoke: function () {
			// source here
		}
	}
});


Эта функция отвечает за то, что произойдёт при вызове класса как функции без «new». По-умолчанию это создание класса, такое поведение, как описанное в топике, но иногда поведение альтернативное.
Но иногда поведение меняется. Вспомним оригинальный JavaScript и разницу между следующими строчками:
var number =     Number(arg);
var number = new Number(arg);

var string =     String(arg);
var string = new String(arg);


Интересно, что вызов метода без new ознатает приведение, а не создание. Я часто пользуюсь этой же техникой. Например, когда метод может получить объект, похожий на точку. Это может быть массив [x, y], может быть объект с полями {x, y}, а может быть и собственно Point. Если пришёл объект или массив, то из него создаётся точка, а если точка, то она не создаётся, а возвращается существующая. Таким образом имеем код:

var source = new Point(3, 4);
 
var clone  = new Point(source);
var same   =     Point(source);

console.log(
	clone == source, // false
	 same == source  // true
);


Идея, в общем, в том, что вызов с new и вызов без new могут оба использоваться для создания изящного api библиотеки.
0
Terion #
Долго думал над заметкой и каментами — и, кажется, этот вариант логичнее.
Все-таки создание объект и выхов функции — разные вещи…
В ключе архитектуры jquery — там самовызов очень кстати и он во многом определяет и её парадигму и её популярность. Но это скорее частный случай…
Хотя, конечно, как-то гложет неуверенность все-равно
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
0
Nutochka #
приводить собственные типы отдельными функциями, названными через lower-case (point(...) или toPoint, неважно)

Я изначально делал так: Point.from. Но в моей реализации были недостатки с контекстом, необходимо было биндить
[ [1,1], [2,2], [3,3] ].map( Point.from );
[ [1,1], [2,2], [3,3] ].map( Point.from.bind(Point) );
[ [1,1], [2,2], [3,3] ].map( Point );


Последнее мне нравится особо =)
+4
Nutochka #
пс. это TheShock, с компа Нюточки зашёл, забыл перелогиниться =)
0
vanfukov #
Да какая разница :)
НЛО прилетело и опубликовало эту надпись здесь
0
TheShock #
Или не получится сделать Extends или надо мутить с контекстами. В invoke ссылка на this всегда одна — текущий вызываемый контекст. Как-то так:
LibCanvas.Geometry = atom.Class({
	Static: {
		from : function (obj) {
			return obj instanceof this ? obj : new this(obj);
		}
	}
});

LibCanvas.Point = atom.Class({
	Extends: LibCanvas.Geometry,
});

[ [0,1], [1,0], [1,1] ].map( LibCanvas.Point.from ); // fail


LibCanvas.Geometry = atom.Class({
	Static: {
		invoke : function (obj) {
			return obj instanceof this ? obj : new this(obj);
		}
	}
});

LibCanvas.Point = atom.Class({
	Extends: LibCanvas.Geometry,
});

[ [0,1], [1,0], [1,1] ].map( LibCanvas.Point ); // success
НЛО прилетело и опубликовало эту надпись здесь
0
TheShock #
Ну, на самом деле в рамках прототипной модели как раз this должен ссылать на сам класс. Другое дело, что ребёнку оно не унаследуется, если не привязать метод вручную

var Parent = function() {};
Parent.method = function () {
  // this is link to Parent
};

var Child = function() {};
Child.method = Parent.method;


Я понимаю ваше убеждение на счёт toPoint. но учтите, что:
1. он не так прост — там должно быть не просто создание нового инстанса, так что минимум:
function toPoint(obj) {
    return obj instanceof Point ? obj : new Point(obj);
}

2. такой метод должна иметь не только точка, но и прямоугольник, круг, полигон и все остальные фигуры:
function toRectangle(obj) {
    return obj instanceof Rectangle ? obj : new Rectangle(obj);
}
function toCircle(obj) {
    return obj instanceof Circle ? obj : new Circle(obj);
}


Единственный вариант соблюсти столь любимый мной принцип DRY — это создать фабрику функций для приведения к классу.
function factory (Class) {
  return function (obj) {
    return obj instanceof Class ? obj : new Class(obj);
  }
};
var toRectangle = factory(Rectangle);
var toCircle = factory(Circle);


Но вариант с наследованием мне кажется более изящным…
НЛО прилетело и опубликовало эту надпись здесь
0
TheShock #
пользователь не дурак, он вряд ли будет приводить уже обозначенный тип к самому себе или сам проверит instanceof

мне больше эта функциональность нужна внутри библиотеки, чтобы пользователь мог передавать любые аргументы и они прозрачно подставлялись.
var rect = new Rectangle(0,5,10,15);
var rect = new Rectangle({ from:[0,5], to:[10,15] });
var rect = new Rectangle({ from:{x:0,y:5}, to:{x:10,y:15} });
var rect = new Rectangle({ from:new Point(0, 5), to:new Point(10, 15) });

Хотя я согласен, что реально такая функциональность нужна действительно редко.
+3
alist #
Отсутствие arguments.callee — совершенно не проблема. Просто нужно использовать named function expression:

function makeClass(){
return function Constructor(args){
if ( this instanceof Constructor ) {
if ( typeof this.init == "function" )
this.init.apply( this, arguments );
} else
return new Constructor( arguments );
};
}
+1
alist #
Эх, не дружу я с формой комментариев. Вот отформатированный код: pastie.org/3051845
0
Mithgol #
Рассказываю: используйте пару псевдоэлементов <source> и </source>, окаймляя ими код на Хабрахабре — и тем невозбранно достигнете желаемого.
0
alist #
Спасибо, а я -то понадеялся на <code></code>, для которого есть кнопка.
–1
EgorK #
Все равно без new есть проблема, если мы создаем новый объект внутри объекта с тем же прототипом. Это, конечно, не проблема, если мы используем чью-то библиотеку, но может привести к проблемам при написании новых библиотек если мы привыкли писать без new везде. Хотя это тоже можно обойти, добавив флажок initializeStarted или что-нибудь в таком духе
–2
bolk #
Мицгол, а что удивительного в реализации unzip в js?
+1
slik #
Пара простых правил, которые искореняют всякие непонятки:
1. JavaScript надо не просто знать, его надо понимать.
2. Долой привычки, надо читать докуметацию/код, раз уж решили использовать чужую работу.
3. Надо думать головой а не одним местом.
+2
slik #
Кстати, интересно было почитать про опыт разных разработчиков.
+8
spmbt #
В защиту new надо сказать такие слова.
В программистской среде существуют «мемы» — уловимые глазом сокращения понятий. Мемов для понятия «создать экземпляр класса» существуют 2 основных: оператор new и функция или метод create(). Причём, программисты-идеологи типа Крокфорда более склонны поддерживать второе. Это упрощает работу: если в тексте увидел new или create, то здесь встретится конструктор. Если мы введём допустимость конструктора без этих слов, мы усложним работу для других по чтению своего кода в дальнейшем. Правда же, код будет иезуитским, если j7() — конструктор, а j91() — функция?

По arguments.callee — совсем другая история. Её неперевариваемость лучше всего, хотя и не досконально, объясняют эти комментарии: habrahabr.ru/blogs/javascript/130713/#comment_4334433, stackoverflow.com/questions/103598/why-was-the-arguments-callee-caller-property-deprecated-in-javascript — в 2 словах — нет явного вызова функции как самой себя. Это также усложняет авто-оптимизацию, хотя это другая сторона проблем.

Сокращение корневых объектов — третья история. Здесь совсем не о чем спорить — корневые объекты стараются сделать покороче, потому что их чаще всего в коде приходится писать. $, ext, qx, GA, GP_… (последнее — от Google+). Но это не повод сокращать мемы и вообще сокращать (не в целях обфускации) нечто узнаваемое (document, window, addListener, .next()...).
+2
dbelka #
А потом куча народу пишет на JQuery и не знает, что
$(this) !== $(this);

это два разных обьекта.
+3
powerman #
На самом деле эта проблема не специфична для JavaScript, она универсальна: стоит ли пытаться предотвращать некоторые типы ошибок и какой ценой это допустимо делать? Философский вопрос без правильного ответа — нужно искать идеальный баланс, а он в разных случаях будет разным.

Мой ответ на этот вопрос звучит так: я разделяю интерфейсы на внешние (публичные) и внутренние.

На внешних должна быть реализована максимально возможная защита «от дурака» (a.k.a. «от хакера» a.k.a. полная валидация входных данных), при этом предпочтительнее не исправлять молча ошибки (создавая объект на лету при вызове без new) а отказываться работать и выдавать ошибку.

На внутренних интерфейсах реализуется только та защита, которую можно сделать не написав ни одной лишней (нужной только для защиты, а не для основной функциональности) строки кода — т.е. то, чего можно достичь архитектурой, стилем написания кода, соглашениями, ….
+1
MrSLonoed #
Мне эта проблема кажется проблемой, стоящей рядом с проблемой нулевого указателя в Си, т.е. делают кучу обёрток, механизмов, усложнений. А ведь это по сути дело стиля написания и аккуратности.
Простое отличие: конструкторы называть с большой буквы, методы с маленькой. Ошибся — сам себе бяка.
Единственный механизм защиты, который должен быть — выброс исключения при вызове конструктора без new.
Конечно есть исключение — внешний интерфейс сложных библиотек. Там может быть оправданным показанный в статье механизм, но я предпочту этому хорошую документацию.
0
catsmeetman #
В книге «JavaScript шаблоны» все эти мелочи разжованы.
+2
diseaz #
Иногда мне хочется думать, что Джон Резиг поступил бы дальновидно, кабы вовсе воздержался от arguments.callee, ограничившись только наглядным примером

Аналог можно реализовать и без arguments.callee:

'use strict';
function makeClass() {
  var newClass;
  var factoryIndicator = {};
  return newClass = function(isFactory, args) {
    if (this instanceof newClass) {
      if (typeof this.init == 'function') {
        this.init.apply(
          this, (isFactory == factoryIndicator) ? args : arguments);
      }
    } else {
      return new newClass(factoryIndicator, arguments);
    }
  };
}
+2
kirilloid #
Кстати тот самый 'use strict' как раз препятствует засорению глобального пространства имён. Потому, что this внутри «свободных» функций == undefined.
И в строгом режиме ошибка быстро всплывёт.
+2
Mourner #
Спасибо за упоминание Leaflet. :)

Насчёт L вместо Leaflet — как уже прокомментировали выше, тут различие только в названии переменной, в случае же с new меняется семантика кода. Я предпочитаю, чтобы различие между созданием объекта с помощью определённой функции в качестве конструктора и просто вызовом функции было явным, а не скрытым какими-то магическими конструкциями — как по мне, это делает код более читабельным и понятным, особенно людям, привычным к активному применению ООП.
+1
diseaz #
Промазал с веткой. habrahabr.ru/blogs/javascript/135027/#comment_4489753 должно быть тут.
0
diseaz #
Я уверен, что настоящие ООПшники никогда не используют методы Array.concat и Array.slice, ведь они неявно создают новый объект и не обозначены как конструкторы. И в своих программах никогда не возвращают из функции объекты, созданные в этой функции, а всегда принимают в параметрах объекты, которые нужно модифицировать, чтобы пользователь в явном виде вызывал new и никогда не забывал, где чего и сколько он создал.

Наверное, мне должно быть жаль, что я не настоящий ООПшник и никогда не видел ни одного вживую.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.