Jem
Jem

Reputation: 6406

Extending prototypes in Javascript - good way?

I want to validate that the approach I'm using is correct when it comes to extend a prototype - supposing "extend" is the right word.

This topic gets a lot of clones. I'm still trying to properly understand this topic...

The purpose is:

Here is the Parent prototype of my sandbox:

function Parent(){

}

Parent.prototype = {

    "init":function(){

        this.name = "anon";
    },
    
    "initWithParameters":function(parameters){
    
        this.name = parameters.name ? parameters.name : "anon";
    },

    "talk": function(){
    
        console.log('Parent is: ' + this.name);
    }
}

Now the Child prototype - it adds a "position" property and redefines the behaviors:

function Child(){

    Parent.call(this);
}


Child.prototype = new Parent;
Child.prototype.constructor = Child;

Child.prototype.init = function(){

    Parent.prototype.call(this);

    this.setPosition(0, 0);
}

Child.prototype.initWithParameters = function(parameters){

    Parent.prototype.initWithParameters.call(this, parameters);

    if(!this.position){
        
        this.position = {x:0, y:0};
    }
    
    this.setPosition(parameters.pos.x, parameters.pos.y);
}
    
Child.prototype.setPosition = function(x, y){
    
    this.position.x = x;
    this.position.y = y;
}

Child.prototype.talk = function(){
    
    console.log('Child is: ' + this.name + ' and location is: ' + this.position.x + ', ' + this.position.y);
}

Is this a good practice? Is there no shorthand to avoid writing "Child.prototype." when overriding a property (using a literal maybe, like the Parent prototype is written).

I know of J. Resig's Class/extend approach. But I'd rather use JavaScript as the prototypical language it is, not make it work as a "class-like behaving class-less OO language".

Upvotes: 11

Views: 30327

Answers (7)

Keni
Keni

Reputation: 65

Coming from google in 2019.

From the latest MDN documentation, the way to extend prototype is :

function MyClass() {
  SuperClass.call(this);
}

// inherit one class
MyClass.prototype = Object.create(SuperClass.prototype);
// mixin another
Object.assign(MyClass.prototype, {
  //... your own prototype ...
});

// re-assign constructor
MyClass.prototype.constructor = MyClass;

Upvotes: 1

user9016207
user9016207

Reputation:

I normally do it like this. I use the class operator now, but there's still a good way to do it in ES3.

Using Node.js utilities

Node.js includes a utility function for this very thing.

const { inherits } = require('util');

function SuperClass () {
  this.fromSuperClass = 1;
}

function ExtendingClass () {
  this.fromExtendingClass = 1;
}

inherits(ExtendingClass, SuperClass);

const extending = new ExtendingClass();
this.fromSuperClass; // -> 1
this.fromExtendingClass; // -> 1

The above has it's fair share of problems. It doesn't establish the prototypical chain, so it's considered semantically incompatible with the class operator.

Using the Object.create API

Otherwise, you can use Object.create for this.

function Person () {
  this.person = true;
}

function CoolPerson () {
  Person.call(this);
  this.cool = true;
}

CoolPerson.prototype = Object.create(Person);
CoolPerson.prototype.constructor = CoolPerson;

Using a "helper function"

Please note that in the above example that uses the Object.create API, if you do not call Person (the "super class") in the CoolPerson (the extending class), instance properties (and optionally initialization) will not be applied when instantiating Person.

If you want to be more "elegant", you could even create a helper function for this, which might be easier for you.

function extend(BaseClassFactory, SuperClass) {
  const BaseClass = BaseClassFactory(SuperClass.prototype, SuperClass);
  BaseClass.prototype = Object.assign(BaseClass.prototype, Object.create(SuperClass));
  BaseClass.prototype.constructor = BaseClass;
  return BaseClass;
}

function SuperClass() {
  this.superClass = true;
}

SuperClass.prototype.method = function() {
  return 'one';
}

const ExtendingClass = extend((Super, SuperCtor) => {
  function ExtendingClass () {
    SuperCtor.call(this);
    this.extending = true;
  }
  
  // Example of calling a super method:
  ExtendingClass.prototype.method = function () {
    return Super.method.call(this) + ' two'; // one two
  }

  return ExtendingClass;
}, SuperClass);

const extending = new ExtendingClass();
extending.method(); // => one two

Using ES6's class operator

There's a new class operator in JavaScript that's been released in JavaScript that may make this whole experience more expressive.

class SuperClass {
  constructor() {
    this.superClass = true;
  }

  method() {
    return 'one';
  }
}

class ExtendingClass extends SuperClass {
  constructor() {
    super();
    this.extending = true;
  }

  method() {
    // In this context, `super.method` refers to a bound version of `SuperClass.method`, which can be called like a normal method.
    return `${super.method()} two`;
  }
}

const extending = new ExtendingClass();
extending.method(); // => one two

Hope this helps.

Upvotes: 1

user2226755
user2226755

Reputation: 13167

My example shows several things : Private variable, same parsing argument for the parant and child constructor, rewrite .toString() function try : ""+this. :)

full example with doc :

Output :

Constructor arguments

MM = new Parent("val of arg1", "val of arg2");
Child1 = new childInterface("1", "2");
Child2 = new child2Interface("a", "b");

console.log(MM + "args:", MM.arg1, MM.arg2);
// Parentargs: val of arg1 val of arg2
console.log(Child1 + "args:", Child1.arg1, Child1.arg2);
// childInterfaceargs: 1 2
console.log(Child2 + "args:", Child2.arg1, Child2.arg2);
// child2Interfaceargs: a b

Extend function in child class

MM.init();
// Parent: default ouput
Child1.init();
// childInterface: new output
Child2.init();
// child2Interface: default ouput

Increment variable

MM.increment();
// Parent: increment 1
Child1.increment();
// childInterface: increment 1
Child2.increment();
// child2Interface: increment 1
Child2.increment();
// child2Interface: increment 2
MM.increment();
// Parent: increment 2
console.log("p", "c1", "c2");
// p c1 c2
console.log(MM.value, " " + Child1.value, " " + Child2.value);
// 2  1  2

Private variable

MM.getHidden();
// Parent: hidden (private var) is true
MM.setHidden(false);
// Parent: hidden (private var) set to false
Child2.getHidden();
// child2Interface: hidden (private var) is true
MM.setHidden(true);
// Parent: hidden (private var) set to true


Child2.setHidden(false);
// child2Interface: hidden (private var) set to false
MM.getHidden();
// Parent: hidden (private var) is true
Child1.getHidden();
// childInterface: hidden (private var) is true
Child2.getHidden();
// child2Interface: hidden (private var) is false 

Protected variable

function Parent() {
  //...
  Object.defineProperty(this, "_id", { value: 312 });
};

console.log(MM._id); // 312
MM._id = "lol";
console.log(MM._id); // 312

/**
 * Class interface for Parent
 *
 * @class
 */
function Parent() {
  this.parseArguments(...arguments);

  /**
   * hidden variable
   *
   * @type    {Boolean}
   * @private
   */
  var hidden = true;


  /**
   * Get hidden
   */
  this.getHidden = () => {
    console.log(this + ": hidden (private var) is", hidden);
  }


  /**
   * Set hidden
   *
   * @param {Boolean} state New value of hidden
   */
  this.setHidden = (state) => {
    console.log(this + ": hidden (private var) set to", !!state);
    hidden = state;
  }

  Object.defineProperty(this, "_id", { value: "312" });
}

Object.defineProperty(Parent.prototype, "nameString", { value: "Parent" });


/**
 * Parse arguments
 */
Parent.prototype.parseArguments = function(arg1, arg2) {
  this.arg1 = arg1;
  this.arg2 = arg2;
};


/**
 * Get className with `class.toString()`
 */
Parent.prototype.toString = function() {
  return this.nameString;
};


/**
 * Initialize middleware
 */
Parent.prototype.init = function() {
  console.log(this + ": default ouput");
};


/**
 * Increment value
 */
Parent.prototype.increment = function() {
  this.value = (this.value) ? this.value + 1 : 1;
  console.log(this + ": increment", this.value);
};

/**
 * Class interface for Child
 *
 * @class
 */
function childInterface() {
  this.parseArguments(...arguments);
}


// extend
childInterface.prototype = new Parent();
Object.defineProperty(childInterface.prototype, "nameString", { value: "childInterface" });

/**
 * Initialize middleware (rewrite default)
 */
childInterface.prototype.init = function(chatClient) {
  console.log(this + ": new output");
};

/**
 * Class interface for Child2
 *
 * @class
 */
function child2Interface() {
  this.parseArguments(...arguments);
}


// extend
child2Interface.prototype = new Parent();
Object.defineProperty(child2Interface.prototype, "nameString", { value: "child2Interface" });

//---------------------------------------------------------
//---------------------------------------------------------

MM = new Parent("val of arg1", "val of arg2");
Child1 = new childInterface("1", "2");
Child2 = new child2Interface("a", "b");

console.log(MM + " args:", MM.arg1, MM.arg2);
console.log(Child1 + " args:", Child1.arg1, Child1.arg2);
console.log(Child2 + " args:", Child2.arg1, Child2.arg2);
console.log(" ");

MM.init();
Child1.init();
Child2.init();
console.log(" ");
MM.increment();
Child1.increment();
Child2.increment();
Child2.increment();
MM.increment();

console.log("p", "c1", "c2");
console.log(MM.value, " " + Child1.value, " " + Child2.value);
console.log(" ");

MM.getHidden();
MM.setHidden(false);
Child2.getHidden();
MM.setHidden(true);

console.log(" ");
Child2.setHidden(false);
MM.getHidden();
Child1.getHidden();
Child2.getHidden();

console.log(MM._id);
MM._id = "lol";
console.log(MM._id);

Upvotes: 0

skay-
skay-

Reputation: 1576

In general your approach will work but a better approach will be to replace:

Child.prototype = new Parent;

with:

Child.prototype = Object.create(Parent.prototype);

This way you don't need to call new Parent, which is somewhat an anti-pattern. You could also define new properties directly as follows:

Child.prototype = Object.create(Parent.prototype, {
  setPosition: {
    value: function() {
      //... etc
    },
    writable: true,
    enumerable: true,
    configurable: true
  }
});

Hope this helps.

Object.create() at MDN

Upvotes: 24

Rootical V.
Rootical V.

Reputation: 859

These are the ways I usually do it:

Using a helper function:

/**
 * A clone of the Node.js util.inherits() function. This will require
 * browser support for the ES5 Object.create() method.
 *
 * @param {Function} ctor
 *   The child constructor.
 * @param {Function} superCtor
 *   The parent constructor.
 */

function inherits (ctor, superCtor) {
  ctor.super_ = superCtor;
  ctor.prototype = Object.create(superCtor.prototype, {
    constructor: {
      value: ctor,
      enumerable: false
    }
  });
};

Then you might simply do:

function ChildClass() {
    inherits(this, ParentClass);
    // If you want to call parent's constructor:
    this.super_.apply(this, arguments);
}

Extending prototype using Lodash

_.assign(ChildClass.prototype, {
  value: key
});

Or just give ES6 a chance!

class ParentClass {
    constructor() {
        var date = new Date();
        var hours = date.getHours();
        var minutes = date.getMinutes();
        var seconds = date.getSeconds();
        this.initializeTime = hours + ':' + minutes + ':' + seconds;
    }
}

class ChildClass extends ParentsClass {
  constructor() {
      super();
      console.log(this.initializeTime);
  }
}

Upvotes: 0

Diniden
Diniden

Reputation: 1125

I may get deep fried for this suggestion as there are several articles that can argue against some practices in my example, but this has worked for me and works well for clean looking code, stays consistent, minifies well, operates in strict mode, and stays compatible with IE8.

I also like utilizing the prototype methodology (rather than all of the 'extend' or 'apply' styles you see everywhere).

I write out my classes like this. Yes, it looks a lot like an OOP language which you didn't want, but it still adheres to the prototypical model while holding similarities to other familiar languages which makes projects easier to navigate.

This is my style I prefer :) I'm not saying it's the best, but it's so easy to read.

(function(ns) {

  var Class = ns.ClassName = function() {

  };

  Class.prototype = new baseClass();
  Class.constructor = Class;
  var _public = Class.prototype;
  var _private = _public._ = {};

  Class.aClassProperty = "aValue";

  Class.aClassMethod = function(params) {

  }

  _public.aMethod = function(params) {
      _private.myMethod.call(this, "aParam");
      Class.aClassMethod("aParam");
  }

  _private.myMethod = function(params) {

  }

})({});

EDIT:

I went ahead and converted your example this style just to show you what it would look like:

var namespace = {};

(function(ns) {

    var Class = ns.Parent = function() {

    };

    var _public = Class.prototype;
    var _private = _public._ = {};

    _public.init = function() {
        this.name = "anon";
    }

    _public.initWithParameters = function(parameters) {
        this.name = parameters.name ? parameters.name : "anon";
    }

    _public.talk = function() {
        console.log('Parent is: ' + this.name);
    }

})(namespace);

(function(ns) {

    var Class = ns.Child = function() {
        this.position = {x:0, y:0};
    };

    Class.prototype = new ns.Parent();
    Class.constructor = Class;
    var _public = Class.prototype;
    var _private = _public._ = {};

    _public.init = function() {
        _public.init.call(this);
        this.setPosition(0, 0);
    }

    _public.initWithParameters = function(parameters) {
        _public.initWithParameters.call(this, parameters);
        this.setPosition(parameters.pos.x, parameters.pos.y);
    }

    _public.setPosition = function(x, y) {
        this.position.x = x;
        this.position.y = y;
    }

    _public.talk = function() {
        console.log('Child is: ' + this.name + ' and location is: ' + this.position.x + ', ' + this.position.y);
    }

})(namespace);

Upvotes: 3

Celeb
Celeb

Reputation: 88

Your approach it is a good pure JavaScript approach. The only get away from tipping "Child.prototype" every time is to put it in a reference variable.

Like:

  var children = Child.prototype;
  children.init = function(){ /*/someoverridecode*/}

But you are still doing the Child.prototype behind this. You can also define a function that does this for you, see bind of underscore, maybe it suits your needs.

Cheers

Upvotes: 3

Related Questions