Mr. TA
Mr. TA

Reputation: 5359

JavaScript prototype inheritance with defineProperty

Say I have this "class":

function Car()
{
}
Object.defineProperty(Car.prototype, "Make", 
  {
    get:function() { return this._make; }, 
    set:function(value) { this._make = value; } 
  });
Object.prototype.Drive = function Drive() { console.log("Car.Drive"); }

Now I want to make a "child class" using prototype inheritance:

function Sedan()
{
}
Sedan.prototype = new Car();
Sedan.prototype.constructor = Sedan;
Sedan.prototype.Drive = function Drive() { Car.prototype.Drive.call(this); console.log("Sedan.Drive"); }

Then I can instantiate a car or a sedan, and drive both. Notice how with sedans, Drive also calls base class (Car) Drive:

var car = new Car(); car.Drive(); var carMake = car.Make;
var sedan = new Sedan(); sedan.Drive(); var sedanMake = sedan.Make;

Is it possible to achieve something similar with properties?

Object.defineProperty(Sedan.prototype, "Make", 
  { 
    get: function() { return Car.prototype.Make.<<CALL_GETTER>>(this) + " - Sedan"; },
    set: function(value) { Car.prototype.Make.<<CALL_SETTER>>(this, value.replace(" - Sedan", "")); } 
  });

The only idea I could come up with is something like this:

Car.prototype.get_Make = function get_Make() { return this._make; }
Car.prototype.set_Make = function set_Make(value) { this._make = value; }
Object.defineProperty(Car.prototype, "Make", 
  {
    get:function() { return this.get_Make(); }, 
    set:function(value) { this.set_Make(value); } 
  });

Then the explicit get_Make and set_Make can be overridden similar to Drive. However, this is clunky. Sure, this boilerplate can be extracted into a helper function which defines the get_ and set_ methods and the property in one shot.

function DefineVirtualProperty(obj, name, getter, setter)
{
  obj["get_" + name] = getter;
  obj["set_" + name] = setter;
  Object.defineProperty(obj, name, 
    {
      get:function() { return this["get_" + name](); },
      set: function(value) { this["set_" + name](value); }
    });
}

DefineVirtualProperty(Car.prototype, "Make", function() { return this._make; }, function(value) { this._make = value; });

However the overriding still looks a big ugly.

Upvotes: 1

Views: 442

Answers (1)

Turtlefight
Turtlefight

Reputation: 10720

You can use Object.getOwnPropertyDescriptor to get the property descriptor of the parent property.
Then you can use .call() to invoke it, e.g.:

function Car() {}
Object.defineProperty(Car.prototype, "Make", {
  get() {
    return this._make;
  },
  set(value) {
    this._make = value;
  }
});

function Sedan() {}
Sedan.prototype = Object.create(Car);
Sedan.prototype.constructor = Sedan;

Object.defineProperty(Sedan.prototype, "Make", {
  get() {
    console.log("Sedan Make get");
    let desc = Object.getOwnPropertyDescriptor(Car.prototype, "Make");
    return desc.get.call(this);
  },
  set(value) {
    console.log("Sedan Make set");
    let desc = Object.getOwnPropertyDescriptor(Car.prototype, "Make");
    return desc.set.call(this, value);
  }
});

let sedan = new Sedan();
sedan.Make = 12;
console.log(sedan.Make);

A few minor tips:

  • Ideally you should use Object.create for prototype creation, since it doesn't call the constructor when creating the object
  • Prefer to use Object.defineProperty instead of directly creating properties on the prototype (so you can set enumerable to false)

If you can use ES6 classes this becomes a lot nicer.
You can just use super with them to access the parent property:

class Car {
  get Make() {
    return this._make;
  }

  set Make(value) {
    this._make = value;
  }
}

class Sedan extends Car {
  get Make() {
    console.log("Sedan Make get");
    return super.Make;
  }

  set Make(value) {
    console.log("Sedan Make set");
    super.Make = value;
  }
}


let sedan = new Sedan();
sedan.Make = 12;
console.log(sedan.Make);

Upvotes: 3

Related Questions