Alexander Derck
Alexander Derck

Reputation: 14498

Object.defineProperty with constructor function and prototype

I just found out about Object.defineProperty and as I'm most familiar with C# I'd like to use an accessor property with my constructor function, for example:

function Base(id) {
   var _id = id;

   Object.defineProperty(this,"ID",{
       get: function() { return _id; },
       set: function(value) { _id = value; }
   })
}

function Derived(id, name) {
   var _name = name;

   Base.call(this,id);

   Object.defineProperty(this,"Name",{
      get: function() { return _name; },
      set: function(value) { _name = value; }
   })
}

Derived.prototype = Object.create(Base.prototype);
Derived.constructor = Derived;

var b = new Base(2);
var d = new Derived(4,"Alexander");

console.log(b.ID);
console.log(d.ID, d.Name);
d.ID = 100;
console.log(d.ID, d.Name);

This prints:

2
4 "Alexander"
100 "Alexander"

But I'm very confused about this, for example this answer with a very high score encourages above approach, while this answer says it will eat up all memory because the functions will be recreated for every object I instantiate. It suggests the following approach instead:

var Base = function(id){this.__id = id}
Player.prototype = {
   get ID(){
      return this.__id;
   },
   set ID(value){
      this.__id = value;
   }
}

var p = new Player();
p.ID = 2;
alert(p.ID); // 4

However this approach also creates another public property __id which seems less than ideal to me (the properties in my example are "privileged" as it's apparantly called in javascript, so no extra public property needed).

Can someone please explain which approach is the right one for me? Right now I'm totally lost in the javascript documentation jungle. I very much like the Object.defineProperty approach because the code feels very clean to me and I can use it with inheritance. But if it's true that the functions get recreated for every object I might need to consider the second approach?

Upvotes: 3

Views: 6899

Answers (2)

frodeborli
frodeborli

Reputation: 1667

I might be a little late to the party, but there are a couple of small things I would like to point out here.

First, you're defining own properties inside the constructor, which is fine for instance specific properties and properties that should not be shared with other instances, but not for getters, setters and methods. Instead you should define those on the prototype of your function.

I understand your question like this

How do you make real PRIVATE properties?

It is easy, and it works in all browsers. First we'll elaborate on Object.defineProperty, and the derived Object.create, Object.defineProperties.

The prototype of javascript object definitions

Modern javascript allows some "syntactic sugar" for declaring classes, but every proud developer should understand how it really works.

First There are no classes. Javascript doesn't have classes. It has prototypes, which are not classes. The resemble classes, so in my naive stubborness, I took several classes to find the truth, and I objected and objected - and I got all the way to the bottom. There were no classes.

Let you be the object. If you or your prototype can't answer a question, then your prototypes´ prototype will be asked, and then your prototypes´ prototypes´ prototype will be asked, and on we go until there are no more prototypes to ask.

All those prototypes are still objects. This algorithm illustrates it:

// Friendly version
function askQuestion(askee, question) {
  do {
    if (askee.hasOwnProperty(question)) {
      return askee[question];
    }
  } while (askee = Object.getPrototypeOf(askee))
}

ECMAScript 6 (boring)

To illustrate the syntax of "modern javascript" I'll digress:

class Tortoise extends Turtle {

  constructor() {
    while (this.__proto__) {
      this.__proto__ = Object.getPrototypeOf(this.__proto__);
    }
  }

  #privateProperty = "This isn't a comment"

  get __proto__() { return Object.getPrototypeOf(this); }

  set __proto__(to) { Object.setPrototypeOf(this, to); }

}

This does not work in all browsers. It also doesn't work in any browser. It's just to show the syntax.

It has been claimed that the above is simply syntactic sugar on top of old-school javascript, but when it comes to extending native objects it suspect that there is more to it than meets the eye.

Even if "modern javascript" (ECMAScript 6) allows us to write classes like above, you should want to understand it.

Also, the modern javascript combined with the stubbornness of Internet Explorer 11 forces us to use babel, an awesome tool with incredible powers that is so incredibly advanced and flexible that the probability of such a tool to even exist at all, by mere coincidence, is infinitely improbable.

The very existence of the tool is proof that God exists which I am sad to say, also proves that God does not exist.


WTF?? See here and there, and there.

Popping the hood

Don't create REUSABLE properties in the constructor. A function that is used by many instances should not be assigned in the constructor.

Real Version

function Base(id) {
  // GOOD
  this.id = id;

  // GOOD, because we need to create a NEW array for each
  this.tags = [];

  // Okay, but could also just be in the prototype
  this.numberOfInteractions = 0;

  // BAD
  this.didInteract = function() {
    this.numberOfInteractions++;
  }
}

Sugar Coated

class Base {
  constructor(id) {
    // GOOD
    this.id = id;

    // GOOD, because we need to create a NEW array for each
    this.tags = [];

    // Okay, but could also just be in the prototype
    this.numberOfInteractions = 0;

    // BAD
    this.didInteract = function() {
      this.numberOfInteractions++;
    }
  }
}

Improved real version

function Base(id) {
  this.id = id;
  this.tags = [];
}
Base.prototype.numberOfInteractions = 0;
Base.prototype.didInteract = function() {
  this.numberOfInteractions++;
}

Improved sugar coated version

If you insist on sugar, and you like to write more lines of code and some extra labor after you're done writing the code, you can install babel and write your code like below.

It will make slightly larger and slower script files - unless you don't really need to support all browsers.

class Base {

  constructor(id) {
    this.id = id;
    this.tags = [];
  }

  numberOfInteractions = 0;

  didInteract() {
    this.numberOfInteractions++;
  }

}

Inheritance ABC

Inheritance in EcmaScript is very simple, once you truly understand it!

TLDR: If you want the above class to extend another class, you can do this one-liner:

Object.setPrototypeOf(Base.prototype, Parent.prototype);

This should work everywhere. It does essentially Base.prototype.__proto__ = Parent.prototype.

Base.prototype vs instance prototype

All objects in javascript have an instance prototype. It is the instance prototype that is searched for "default values" for an objects properties.

function MyConstructor() { /* example function or "class" */ }

The above statement creates an object named MyConstructor, which has an instance prototype that is a reference to Function.prototype. At the same time, it is also a function that you can invoke.

This is important:

MyConstructor instanceof Function;
// is TRUE because
Object.getPrototypeOf(MyConstructor) === Function.prototype

// this is NOT TRUE
MyConstructor.prototype === Function.prototype

Because of this subtle difference

var myInstance = new MyConstructor();

myInstance instanceof MyConstructor;
// this is TRUE because
Object.getPrototypeOf(myInstance) === MyInstance.prototype

Now, MyConstructor.prototype is simply an empty object (which has an instance prototype referring to Object.prototype).

Accessing properties

In javascript, a objects only have properties. They don't have methods, but they have properties that point to a function.

When you try to access a property on an object (instance of Base), the engine looks for that property like this:

Checked location Description
this.$HERE$ On your local properties
this.__proto__.$HERE$ __proto__ is a reference to Base.prototype.
this.__proto__.__proto__.$HERE$ This would be your parent class prototype.
this.__proto__.__proto__.__proto__.$HERE$ This would be your grand-parent class prototype.
...and on we go, searching through the prototype chain, until there are no more prototypes. The search is done using Object.prototype.hasOwnProperty, which means that even if the value is undefined, the search will stop.

__proto__ is a magic property which has been deprecated in favor of Object.getPrototypeOf(). I'm using it for brevity.

Any property found via the prototype chain, will be bound to this before it is returned to you.

Next comes inheritance and method definitions. There are two schools of thought here; one overwrites the Base.prototype with a new object, created with Object.create, and then proceed to create methods:

// This works (but it overwrites the Base.prototype object)
Base.prototype = Object.create(ParentClass);

// Declare a method
Base.prototype.incrementMyValue = function() {
    this.myValue++;
}

// A default value
Base.prototype.myValue = 123

// A getter
Object.defineProperty(Base.prototype, 'getMyValue', {get: function() { 
  return myValue;
}});

In the above code, I would like to point out that when you access instance.myValue it will be undefined, so the prototype chain is scanned and you'll get 123.

If you call instance.incrementMyValue() for the first time, the prototype chain will be scanned and return 123. Then you increment the value, and it is assigned to your instance.

You started with:

instance // no .myValue exists
instance.__proto__.myValue = 123

Calling:

instance.incrementValue();

You end up with:

instance.myValue = 124;
instance.__proto__.myValue = 123

The default value exists still, but it is overridden by the instance local myValue property.

The old-school class definition with inheritance

This class has it all:

  • private properties
  • private static properties
  • public properties
  • public static properties

Behold, the "Charm" class, lending ideas from my Charm.js library that I hope to publish:

var Derived = (function(){
  /* Wrapped in a function, so we keep things private*/

  /**
   * CONSTRUCTOR
   */
  function Derived(id, name) {
    Base.call(this, id);               // Calling the parent constructor
    private(this).name = name;         // Setting a TRUE private property
  }

  /**
   * PRIVATE STATIC PROPERTIES
   */
  var thisIsPrivateAndShared = 0;

  /**
   * PUBLIC STATIC PROPERTIES
   */
  Derived.thisIsPublicAndShared = 0;

  /**
   * PRIVATE NON-STATIC PROPERTIES THROUGH WeakMap
   */
  var secrets = new WeakMap();
  function private(for) {
    var private = secrets.get(for);
    if (!private) {
      private = {};
      secrets.set(for, private);
    }
    return private;
  }

  /**
   * Building the prototype
   */
  Derived.prototype = Object.create(

    /**
     * EXTEND Base.prototype (instead of Object.prototype)
     */
    Base.prototype,

    /**
     * Declare getters, setters, methods etc (or see below)
     */
    {
      /**
       * GETTERS AND SETTERS FOR THE PRIVATE PROPERTY 'name'
       */
      name: {
        get: function() {                // getter
          return private(this).name;
        },
        set: function(value) {           // setter
          private(this).name = value;
        }
      },

      /**
       * A PUBLIC METHOD
       */
      method: {value: function() {

      }},

      /**
       * A PUBLIC PROPERTY WITH A DEFAULT VALUE
       */
      age: {value: 42, writable: true}
    });

    /**
     * I am too lazy to write property descriptors,
     * unless I want a getter/setter/private properties,
     * so I do this:
     */
    Derived.prototype.lessWorkMethod = function() {
    };
  }
  return Derived;
})();

Upvotes: 1

Bergi
Bergi

Reputation: 665455

Can someone please explain which approach is the right one for me?

Don't use Object.defineProperty at all. You absolutely don't need property descriptors here, and your getters and setters don't do anything special. Just use a simple normal property. It will be faster and better optimised than anything else you are concerned about.

function Base(id) {
    this.ID = id;
}

function Derived(id, name) {
    Base.call(this,id);
    this.Name = name;
}

Derived.prototype = Object.create(Base.prototype);
Derived.prototype.constructor = Derived;

If it's true that the functions get recreated for every object I might need to consider the second approach?

Yes, that's true, but negligible. You should not micro-optimise prematurely. You're going to know when you really need it, and then you still can easily swap the implementation. Until then, go for clean and simple code.

Upvotes: 6

Related Questions