Simple “Class” Instantiation

2019-03-15 14:02发布

问题:

From John Resig blog:

// 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?

回答1:

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:

var MyClass = makeClass();

is equivalent to this:

function MyClass(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 );
}

Since we're no longer dealing with an anonymous function, we no longer need the arguments.callee notation: it necessarily refers to MyClass, so we can replace all instances of it with MyClass, giving this:

function MyClass(args)
{
  if ( this instanceof MyClass )
  {
    if ( typeof this.init == "function" )
      this.init.apply( this, args.callee ? args : arguments );
  }
  else
    return new MyClass( arguments );
}

where args is an identifier for MyClass's first argument, and arguments, as always, is an array-like object containing all of MyClass'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:

MyClass.prototype.init =
  function (prop)
  {
    this.prop = prop;
  };

Once we've done that, consider this:

var myInstance1 = new MyClass('value');

Inside the call to MyClass, this will refer to the object being constructed, so this instanceof MyClass will be true. And typeof this.init == "function" will be true, because we made MyClass.prototype.init be a function. So we reach this line:

this.init.apply( this, args.callee ? args : arguments );

Here args is equal to 'value' (the first argument), so it's a string, so it doesn't have the callee property; so args.callee is undefined, which in a Boolean context means it's false, so args.callee ? args : arguments is equivalent to arguments. Therefore, the above line is equivalent to this:

this.init.apply(this, arguments);

which is equivalent to this:

this.init('value');

(if you don't already know how apply works, and how it differs from call, 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:

var myInstance2 = MyClass('value');

Inside the call to MyClass, this will refer to the global object (typically window), so this instanceof MyClass will be false, so we reach this line:

return new MyClass( arguments );

where arguments is an array-like object containing a single element: 'value'. Note that this is not the same as new MyClass('value').

Terminological note: So the call to MyClass('value') results in a second call to MyClass, this time with new. I'm going to call the first call (without new) the "outer call", and the second call (with new) the "inner call". Hopefully that's intuitive.

Inside the inner call to MyClass, args now refers to the outer call's arguments object: instead of args being 'value', it's now an array-like object containing 'value'. And instead of args.callee being undefined, it now refers to MyClass, so args.callee ? args : arguments is equivalent to args. So the inner call to MyClass is calling this.init.apply(this, args), which is equivalent to this.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:

return new MyClass( arguments );

with something hypothetical that looked like this:

return new MyClass.apply( itself, arguments );

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:

  • If we define a constructor MyClass.prototype.init, and then we instantiate the "class" by writing var myInstance3 = new MyClass();, then args will be undefined inside the call to MyClass, so the test on args.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 on args && args.callee instead.
  • If our constructor's first argument happens to actually have a property named callee, then the test on args.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 an arguments object as its first argument. But this issue seems difficult to work around, and it's probably not worth worrying about.


回答2:

@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.

// makeClass - By Hubert Kauker (MIT Licensed)
// original by John Resig (MIT Licensed).
function makeClass(){
    var isInternal;
    return function(args){
        if ( this instanceof arguments.callee ) {
            if ( typeof this.init == "function" ) {
                this.init.apply( this, isInternal ? args : arguments );
            }
        } else {
            isInternal = true;
            var instance = new arguments.callee( arguments );
            isInternal = false;
            return instance;
        }
    };
}

We can even get rid of using arguments.callee altogether by assigning the anonymous function to a local variable before returning it.

// makeClass - By Hubert Kauker (MIT Licensed)
// original by John Resig (MIT Licensed).
function makeClass(){
    var isInternal;
    var constructor = function(args){
        if ( this instanceof constructor ) {
            if ( typeof this.init == "function" ) {
                this.init.apply( this, isInternal ? args : arguments );
            }
        } else {
            isInternal = true;
            var instance = new constructor( arguments );
            isInternal = false;
            return instance;
        }
    };
    return constructor;
}

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:

// makeClass - By Hubert Kauker (MIT Licensed)
// original by John Resig (MIT Licensed).
function makeClass(){
    var constructor = function(){
        if ( this instanceof constructor ) {
            if ( typeof this.init == "function" ) {
                this.init.apply( this, arguments );
            }
        } else {
            var instance = Object.create(constructor.prototype);
            if ( typeof instance.init == "function" ) {
                instance.init.apply( instance, arguments );
            }
            return instance;
        }
    };
    return constructor;
}

This is not the fastest possible solution, though. We can avoid the prototype chain lookup starting at the instance object because we know thatinitmust be in the prototype.
So we may use a var init=constructor.prototype.init statement to obtain it, then check it forfunctiontype, 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:

// Be careful and check whether you really want to do this!
Function.VOID = function(){};

function makeClass(){
    // same as above...

            Function.VOID.prototype = constructor.prototype;
            var instance = new Function.VOID();

    // same as above...
}

When you decide against using a 'public static final' Function.VOID like that, you can use a declaration like var VOID=function(){} at the top of makeClass. 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 using makeClass.VOID=function(){}. Another popular pattern is to pass a single instance of this little function into makeClass using an immediately called wrapper function.

// makeClass - By Hubert Kauker (MIT Licensed)
// original by John Resig (MIT Licensed).
var makeClass = (function(Void) {
    return function(){
        var constructor = function(){
            var init=constructor.prototype.init, 
                hasInitMethod=(typeof init == "function"), 
                instance;
            if ( this instanceof constructor ) {
                if(hasInitMethod) init.apply( this, arguments );
            } else {
                Void.prototype = constructor.prototype;
                instance = new Void();
                if(hasInitMethod) init.apply( instance, arguments );
                return instance;
            }
        };
        return constructor;
    };
})(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 the function(){} 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 to constructor.prototype before we use new 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:

var MyClass = makeClass();
var obj1 = new MyClass();
var obj2 = MyClass();

alert( obj1 instanceof MyClass );    // true
alert( obj2 instanceof MyClass );    // true

alert( obj1.constructor == MyClass );    // true
alert( obj2.constructor == MyClass );    // true


回答3:

What's the difference between args and arguments?

Arguments is an array-like structure that javascript creates that contains all passed in paremeters.

Args is a parameter of the function itself.

Can args.callee ever be false?

Absolutely,

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 );
  };
}
var class = makeClass();
class({callee: false});

So in the example above:

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

returns this following function saved into variable class

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

so when I call class({args: false});

arguments.callee == makeClass

so args gives you the ability to override the default arguments created by javascript



回答4:

I 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/):

function makeClass() {
    return function f(args) {
        console.log(this)
        if(this instanceof f){
            if(typeof this.init === "function") {
                this.init.apply(this, args);
            }    
        } else {
            return new f(arguments);
        }
    };
}

You could create inheritance as follows:

var BaseClass = makeClass();
BaseClass.prototype.init = function(n){
    console.log("baseClass: init:" + n);   
}
var b = BaseClass("baseClass");

var SubClass = makeClass();
SubClass.prototype = Object.create(BaseClass.prototype);
SubClass.prototype.init = function(n) {
    BaseClass.prototype.init.call(this,n); // calling super();
    console.log("subClass: init:" + n);
}
var s = SubClass("subClass");

If I am wrong about the reimplementation of this class then I would be very pleased to know how I can improve on it.



回答5:

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:

function SomeClass(argument1, argument2) {

    // private variables of this object.
    var private1, private2;

    // Public properties
    this.public1 = 4;
    this.public2 = 10;

    // Private method that is invoked very last of this instantionation, it's only here 
    // because it's more logical for someone who is used to constructors
    // the last row of SomeClass calls init(), that's the actual invokation
    function init() {

    }

    // Another private method
    var somePrivateMethod() {
        // body here
    }

    // Public methods, these have access to private variables and other private methods
    this.publicMethod = function (arg1, arg2) {
        // arguments are only accessible within this method
        return argument1 + argument2;
    }


    init();
}


// And then instantiate it like this:

var x = new SomeClass(1, 2);
// Arguments are always optional in js
alert(x.publicMethod());