Search code examples
javascriptoopecmascript-6prototypedefineproperty

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.


Solution

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