3
\$\begingroup\$

I've recently read this article, and I agree that the new keyword is not good practice.

Thus, I've made an improvement on John Resig's Simple JavaScript Inheritance in order to use the Class.create method instead of new:

// The dummy class constructor
function Class() {
   // I remove the initialization procedure in constructor function, 
   // Initialization will done by Class.create which I defined below
}     

// Enforce the constructor to be what we expect
Class.prototype.constructor = Class;

// And make this class extendable
Class.extend = arguments.callee;

// What I improved
Class.create = function () {
  var instance = new this();
  if (instance.init) {
      instance.init();
  }
  return instance;
}

The initialization case in his article could be rewritten like this:

var Person = Class.extend({
  init: function(isDancing){
    this.dancing = isDancing;
  }
});

var p = Person.create();

Have I done this well or not? Please help me check it.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ you are still using the 'new' keyword... \$\endgroup\$
    – Pinoniq
    Commented Aug 21, 2013 at 11:57

2 Answers 2

2
\$\begingroup\$

If you really want to create your own inheritance pattern then John Resig's Simple Inheritance pattern is not a good candidate to build upon. The reason is because John Resig's code is very slow. The reason it's so slow is because of the way _super is handled:

// Copy the properties over onto the new prototype
for (var name in prop) {
  // Check if we're overwriting an existing function
  prototype[name] = typeof prop[name] == "function" && 
    typeof _super[name] == "function" && fnTest.test(prop[name]) ?
    (function(name, fn){
      return function() {
        var tmp = this._super;

        // Add a new ._super() method that is the same method
        // but on the super-class
        this._super = _super[name];

        // The method only need to be bound temporarily, so we
        // remove it when we're done executing
        var ret = fn.apply(this, arguments);        
        this._super = tmp;

        return ret;
      };
    })(name, prop[name]) :
    prop[name];
}

I'll try to explain what's happening in the above code:

  1. We loop through each property which we wish to copy onto the newly extended object.
  2. If the property is a function and it overrides another function of the same name and it needs to call the overridden function then we replace it with a function which changes the value of this._super within the function to the overridden function for that particular function call.
  3. Otherwise we simply copy the property as it is.

This little indirection allows you to call overridden methods using this._super. However it also makes the code very slow. Hence I suggest you don't use John Resig's Simple JavaScript Inheritance.


Since this is a code review site I'm also obliged to point out flaws in your code. The main problem with your code is in the Class.create function:

Class.create = function () {
  var instance = new this();
  if (instance.init) {
      instance.init();
  }
  return instance;
};

Compare that with John Resig's original dummy class constructor:

// The dummy class constructor
function Class() {
  // All construction is actually done in the init method
  if ( !initializing && this.init )
    this.init.apply(this, arguments);
}

The problem is that any argument applied to Class.create is lost. You need to apply the arguments passed to Class.create to instance.init as follows:

Class.create = function () {
  var instance = new this;
  if (instance.init) instance.init.apply(instance, arguments);
  return instance;
};

Beside that there's not much more scope for improvement if you plan to stick to John Resig's Simple Inheritance Pattern.


If you really want to create your own inheritance pattern in JavaScript then it pays to invest some time learning about how inheritance works in JavaScript. For example the following answer explains prototype-class isomorphism in JavaScript: https://stackoverflow.com/a/17893663/783743

Prototype-class isomorphism simply means that prototypes can be used to model classes. Armed with this knowledge we can write a function which takes an object (a prototype) and returns a class (a constructor function):

function CLASS(prototype) {
    var constructor = prototype.constructor;
    constructor.prototype = prototype;
    return constructor;
}

Using the above function we may now create and instantiate classes as follows:

var Person = CLASS({
    constructor: function (isDancing) {
        this.dancing = isDancing;
    },
    dance: function () {
        return this.dancing;
    }
});

var p = new Person(true);

Although this pattern does not have inheritance yet it is a good base to build upon. With a little bit of modification we can make it behave the way we want it to. There are a few important points to note:

  1. We want to be able to use the extend and create functions on any function. Hence it's better to add them to Function.prototype instead of creating a separate Class constructor.
  2. For a function F it need not be true that F.prototype.constructor === F. Hence we may use F for extending and F.prototype.constructor for creating.
  3. Instead of passing a prototype to extend it makes much more sense to pass a blueprint of a prototype instead. This makes looping over the prototype unnecessary.

Taking advantage of the above mentioned points we can implement extend and create as follows:

Function.prototype.extend = function (body) {
    var constructor = function () {};
    var prototype = constructor.prototype = new this;
    body.call(prototype, this.prototype);
    return constructor;
};

Function.prototype.create = function () {
    var instance = new this;
    instance.constructor.apply(instance, arguments);
    return instance;
};

That's all. Using two simple functions we may now create classes as follows:

var Person = Object.extend(function () {
    this.constructor = function (isDancing) {
        this.dancing = isDancing;
    };

    this.dance = function () {
        return this.dancing;
    };
});

var Ninja = Person.extend(function (base) {
    this.constructor = function () {
        base.constructor.call(this, false);
    };

    this.swingSword = function () {
        return true;
    };
});

To create an instance of the class we use create as follows:

var p = Person.create(true);
p.dance(); // => true

var n = Ninja.create();
n.dance(); // => false
n.swingSword(); // => true

// Should all be true
p instanceof Person && p instanceof Object &&
n instanceof Ninja && n instanceof Person && n instanceof Object

This code is similar to my very own augment function. However augment makes use of new instead of create. Because of the way JavaScript works, using new is the fastest way to create an instance which is why augment doesn't have create. Nevertheless if you want it, a functional version of new can be easily implemented as follows:

function Factory(constructor, args) {
    return constructor.apply(this, args);
}

Function.prototype.new = function () {
    Factory.prototype = this.prototype;
    return new Factory(constructor, args);
};

For more information read the following answer on StackOverflow.

\$\endgroup\$
0
\$\begingroup\$
  • arguments.callee is deprecated and suggest you actually avoid it.

  • What you just did was hide the new inside create(). That's nothing new. In fact, libraries like jQuery do this. Calling $() is actually making a new jQuery object.

    jQuery === $ === function (a,b){return new e.fn.init(a,b,h)}
    
  • new isn't bad practice. It depends on the developer. JavaScript gives you that flexibility to do anything with it. I once considered it as a problem, but soon learned that there's a place for it. It's easy to use (and abuse) once you know how prototypal OOP works in JS.

  • Additional complexity might be a hit in performance. I'd stick to the native way of doing things unless circumstances call for extreme measures. What's wrong with JS's way of doing inheritance?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.