// 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 );
};
}
especially this line this.init.apply( this, args.callee ? args : arguments );
What's the difference between args
and arguments
? Can args.callee
ever be false
?
Arguments is an array-like structure that javascript creates that contains all passed in paremeters.
Args is a parameter of the function itself.
Absolutely,
So in the example above:
returns this following function saved into variable
class
so when I call
class({args: false});
so
args
gives you the ability to override the defaultarguments
created by javascriptI believe at this point this function can be rewritten to appeal to strict mode in ES5 and beyond.
arguments.callee
will give you issues if you have any sort of linter looking at your code. I believe the code can be rewritten as follows(http://jsfiddle.net/skipallmighty/bza8qwmw/):You could create inheritance as follows:
If I am wrong about the reimplementation of this class then I would be very pleased to know how I can improve on it.
Following on your title of the question rather than the specific question about your example:
I never really get why they need to complicate it like this. Why not just do this? It's a better example (according to me) of "simple" class instantiation in js:
You write that the existing answers don't have enough detail, but even after reading your specific questions, I'm not completely sure exactly which aspects of the code are throwing you for a loop — it has a number of tricky parts — so I apologize in advance if this answer goes overboard with details about things you've already understood!
Since
makeClass
is always meant to be called the same way, it's a bit easier to reason about it if we remove one level of indirection. This:is equivalent to this:
Since we're no longer dealing with an anonymous function, we no longer need the
arguments.callee
notation: it necessarily refers toMyClass
, so we can replace all instances of it withMyClass
, giving this:where
args
is an identifier forMyClass
's first argument, andarguments
, as always, is an array-like object containing all ofMyClass
's arguments.The line you're asking about is only reached if the "class" has a function named
init
in its prototype (which will be the "constructor"), so let's give it one:Once we've done that, consider this:
Inside the call to
MyClass
,this
will refer to the object being constructed, sothis instanceof MyClass
will be true. Andtypeof this.init == "function"
will be true, because we madeMyClass.prototype.init
be a function. So we reach this line:Here
args
is equal to'value'
(the first argument), so it's a string, so it doesn't have thecallee
property; soargs.callee
is undefined, which in a Boolean context means it's false, soargs.callee ? args : arguments
is equivalent toarguments
. Therefore, the above line is equivalent to this:which is equivalent to this:
(if you don't already know how
apply
works, and how it differs fromcall
, see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/apply).Does that make sense so far?
The other case to consider is this:
Inside the call to
MyClass
,this
will refer to the global object (typicallywindow
), sothis instanceof MyClass
will be false, so we reach this line:where
arguments
is an array-like object containing a single element:'value'
. Note that this is not the same asnew MyClass('value')
.Terminological note: So the call to
MyClass('value')
results in a second call toMyClass
, this time withnew
. I'm going to call the first call (withoutnew
) the "outer call", and the second call (withnew
) the "inner call". Hopefully that's intuitive.Inside the inner call to
MyClass
,args
now refers to the outer call'sarguments
object: instead ofargs
being'value'
, it's now an array-like object containing'value'
. And instead ofargs.callee
being undefined, it now refers toMyClass
, soargs.callee ? args : arguments
is equivalent toargs
. So the inner call toMyClass
is callingthis.init.apply(this, args)
, which is equivalent tothis.init('value')
.So the test on
args.callee
is intended to distinguish an inner call (MyClass('value')
→new MyClass(arguments)
) from a normal direct call (new MyClass('value')
). Ideally we could eliminate that test by replacing this line:with something hypothetical that looked like this:
but JavaScript doesn't allow that notation (nor any equivalent notation).
You can see, by the way, that there are a few small problems with Resig's code:
MyClass.prototype.init
, and then we instantiate the "class" by writingvar myInstance3 = new MyClass();
, thenargs
will be undefined inside the call toMyClass
, so the test onargs.callee
will raise an error. I think this is simply a bug on Resig's part; at any rate, it's easily fixed by testing onargs && args.callee
instead.callee
, then the test onargs.callee
will produce a false positive, and the wrong arguments will be passed into the constructor. This means that, for example, we cannot design the constructor to take anarguments
object as its first argument. But this issue seems difficult to work around, and it's probably not worth worrying about.@ruakh: Great analysis. Almost two years after the original question and your answer, I am still attracted to the matter. I hope my observations are not entirely superfluous. They are quite lengthy, though. Would justify a nice stand-alone blog article :-).
Both issues with John Resig's original code, which you mention at the end, can be resolved using a private flag to distinguish what you call an inner from an outer call.
We can even get rid of using
arguments.callee
altogether by assigning the anonymous function to a local variable before returning it.It is even possible to avoid making the inner call at all, like this, which is also very good for performance. When we have a modern JavaScript which has
Object.create
, we can simplify as follows:This is not the fastest possible solution, though. We can avoid the prototype chain lookup starting at the instance object because we know that
init
must be in the prototype.So we may use a
var init=constructor.prototype.init
statement to obtain it, then check it forfunction
type, and then apply it.When we want to be backwards compatible, we can either load one of the existing polyfills, e. g. from Mozilla Developer Network, or use the following approach:
When you decide against using a 'public static final'
Function.VOID
like that, you can use a declaration likevar VOID=function(){}
at the top ofmakeClass
. But that will cause a private function to be created inside every class constructor you are going to produce. We can also define a 'static' method on our utility itself usingmakeClass.VOID=function(){}
. Another popular pattern is to pass a single instance of this little function intomakeClass
using an immediately called wrapper function.Looking at this code we may become quite confused. Every instance of every class constructor that we will be creating in the future using the direct constructor invokation without
new
will technically be an instance of one and the same void constructor, namely thefunction(){}
we passed in as an argument to our wrapper function.How can this possibly work?
Forgive me when I'll be explaining something which you already know. The secret lies in the fact that we change the prototype of
Void
toconstructor.prototype
before we usenew
to instantiate it. At this point, each new object gets an internal property informally denoted by[[Prototype]]
whose value is the current value of the constructor's prototype property. When the value of the constructor's prototype property is replaced later, it has no further effect on our object just created. See: Section 13.2.2 [[Construct]] of the ECMA Standard-262 5th Edition.Therefore the following works out for all "classes" that we make with this tool: