John Rezig's self-invoking constructor and reflection on why this decision did not take root

  • Tutorial
It is time to mentally go back four and a half years ago to the blog post “ Simple“ Class ”Instantiation ” from the blog of John Rezig, the illustrious creator of the unusually convenient jQuery library. And come back.

However, since I don’t see it at all in the search resultson Habrahabr by the word "Resig", one involuntarily has to think that no one bothered to translate (or at least retell) this useful blog post over the past four years - so I have to retransmit the blog post by Rezig on my own before I fulfill my main intention : Reflect out loud why the method proposed by Rezig to solve the problem he indicated has not yet become widespread. And I will retell. (The retelling itself would already be useful to the reader, even if I didn’t add anything to him. And I’ll add.)



On December 6, 2007, Rezig examined what happens when the “new” operation is used in the javascript to create an object (in languages ​​with classes, we would say “class instance”):

function User(first, last){
   this.name = first + " " + last;
}
var user = new User("John", "Resig");

Rezig rightly noted that for beginners in javascript it is not quite obvious that the appearance of “this” in the function code indicates that we have an object constructor. (I’ll add in parentheses myself: if the function is located in the bowels of a library, this circumstance also needs to be documented - otherwise the library user will not differ much from the beginner: not everyone reads the source code with the body of the function, especially since it is often used in a minified, unreadable way.)

Therefore, Rezig reasoned, sooner or later someone will try to call “ User ()without “ new and thereby get two unpleasant problems on their heads. First, the variable "user "will remain undefined: the function" User () "is intended as a constructor, but it does not return any value. Secondly, what is worse, attempts to appeal to « the this » from the inside of the (incorrectly called) designer will inevitably lead to clogging the global namespace - but it is fraught with sinister and subtle effects. John Resig demonstrated both problems using an example:

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

Nevertheless, Resig pointed out further, calling the constructor is useful. It has the advantage that prototype inheritance (getting the properties of an object from a prototype), that is, calling a real constructor, works much faster than getting the same properties as an object “constructed” by calling a simple function:

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

Rezig made a natural conclusion from here that it would be nice to compose a function every time that, on the one hand, could work as a constructor (providing quick prototype inheritance), and on the other hand, could be called without " new " and in that case resort to myself as a constructor. On the example of their own function $ () from the library jQuery Resig own reasonably shows that well, perhaps it would be convenient if the user instead of the library « $ (" div ") » had to record the « new $ (" div ") »? Of course not.

Fortunately, Resig continued, everyone these problems lend themselves to a simple solution using conditional notation of the function body:

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

Operator « the instanceof » here serve as the primary means by which to detect whether the involved operator « new » in a function call - and it is easy to show by a simple example:

function test(){
   alert( this instanceof test );
}
test();     // сработает как alert( false );
new test(); // сработает как alert( true ); 

Having found this solution and making sure it is working, Rezig said: now let's wrap this solution in a generalized means of creating constructors of "classes", which could be used whenever there is a need for such functions. For this purpose, John Rezig posted this piece of free code:

// 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 );
   };
}

The previous example, to use this code, must be rewritten so that the body of the previous constructor becomes the body of the “ init ” method in the prototype:

var User = makeClass();
User.prototype.init = function(first, last){
   this.name = first + " " + last;
};
var user = User("John", "Resig");
user.name // выдаёт «John Resig»

The logic of the work of " makeClass " John Rezig also explained in sufficient detail. Function « makeClass () » not a designer, and the designer creates - this constructor is returned from « makeClass » anonymous function « function (the args) ». Since the name of the “class” (the name that will be given to this function as a result) is not yet known in advance, at the time of execution it resorts to the service javascript property “ arguments.callee ”, it takes its name from there. Another trick is that if this anonymous function is called without " new ", then its arguments (" arguments ")re-passed inside it when it calls itself as a constructor (" return new arguments.callee (arguments) ") - and then this particular set of arguments becomes the args parameter and is passed to the init method .



The retelling of John Rezig's thoughtful blog post is now over; now I can finally talk about where he himself seems to have been too clever.

An unpleasant element of his makeClass idea is the use of the arguments.callee property . This property is considered to be problematic in relation to accelerating performance in browsers (modern interpreter optimizations are not able to cope with it for some reason), so the so-called “strict mode” was even introduced in the new version of the language (ECMAScript 5), one of the nuances which is a complete rejection of " arguments.callee ". (In May 2009, John Rezig himselfhe mentioned it and was transferred to Habrahabr .)

It seems to me that this dislike of arguments.callee in the community of authors of various javascripts and libraries over time has partly shifted to the very Rezigov idea of ​​a self-invoking constructor - although this idea personally seems to me sound and useful , and the opposition of " $ (" div ") " and " new $ (" div ") " seems to me a strong and convincing argument in favor of this idea.

Another reason for the hostility to the self-invoking constructor is, apparently, the idea of the “ new ” operatoras an important part of the JavaScript language, ignorance of which is so shameful that errors caused by the absence of this operator do not need to be prevented. (There is always something masochistic about the computer scientist’s not particularly convenient tool, and this feeling sometimes gives rise to a burning dislike for newcomers who need help: "No, that would be too much, too simple; I had sex - now you go and have some fun." )

I have seen this more than once.

I remember that in May of this (2011) year in the JavaScript FAQ compiled by azproduction , it was said:

- It is better, more familiar and ideological to create objects through new. Designers should be capitalized.

- I prefer to be based on conventions and do not check this inside the constructor - I called the constructor without new and therefore has flown to the globals - which means "the fool himself." And in no case do I encourage an error with new - some people check if this is a global means the user called the constructor without new and create an object inside the constructor and return it - this is an encouragement to the error and an ideologically incorrect approach.

(End of quote.)

I also remember the case of Vladimir Agafonkin, the creator of the beautiful Leaflet library for displaying maps, which was mentioned more than once on Habrahabr. In August of this (2011) year, he received a request for a merger at Github , the author of which at the beginning of each designer suggested putting something like this code:

if ( !(this instanceof arguments.callee) ){
   return new arguments.callee(arguments);
}

Agafonkin answered him:

“Keeping novice JS authors from making mistakes is very useful, but I don’t like the idea of ​​such a dull language that allows incorrect syntax instead of telling the user that he was wrong.

- Instead of creating an instance of the object even without the “new”, it seems to me better to do something like throw new Error ("You forgot to put the new keyword before the class constructor.") .

- And one more thing: I read somewhere that  arguments.callee is now considered malicious, and it’s safer to explicitly write the class name.

(End of quote.)

The author of the request then went, read about arguments.callee ,Yes, and withdrew his request. It turns out that the shortcomings of arguments.callee and respect for new once again prevented the implementation of a self-invoking constructor.

Which of the Leaflet users will read at least the “ Quick Start Guide ” will probably notice that the global object defined by this library is called (obviously, for brevity) simply “ L ” - and not “ Leaflet ”, for example. Six letters are saved. But you can, it would be possible to save four more characters, so as not to write new and a space before each call to the constructor.

Sometimes I want to think that John Rezig would have been forward-thinking if he had refrained from arguments.callee altogether , confining himself only to a graphic example (template, pattern) of a self-invoking constructor record:

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

But only, of course, in order not to create an extra if- wrapper around the entire function, this example should be simplified:

function User(first, last){
   if (!(this instanceof User)) return new User(first, last);
   // здесь и далее мы гарантированно находимся внутри конструктора
   this.name = first + " " + last;
   // …и далее следует всё остальное тело конструктора…
}

And we must pay tribute to the JavaScript FAQ compiled by azproduction : this simplification is also given there. It is simply not recommended there.

It is easy and pleasant to follow such a good example. It is also more understandable than the constructor constructor - including for javascript optimizers more clearly.

If you want to finally see a similar positive example from life, then look at the zip.js code from the package, which provides zipping of ZIP archives - and which is written in pure javascript under Node.js (without a single line of C ++;I didn’t know that there are such cross-platform masterpieces!). There you will see exactly the same constructor self-call:

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

The conclusion is simple: study the works of John Rezig, obey his advice, act on his instructions. But only to a certain limit of complexity.



Appendage. By the end of July 2012, Vladimir Agonfonkin ( Mourner ) nevertheless implemented the ability to do without the “new” operator in his Leaflet library . But I must say that in the end it was not me who convinced him, but forgotten , who composed and posted his own critical review of Leaflet on Habrahabr .

Also popular now: