Search code examples
javascriptprototype-programming

Is it possible to nest functions in an object in a prototype and access the base object?


Update: I can't answer the question, since it's locked, but I have my own solution to this problem at the bottom, working with Jonas' answer

Suppose I have this addition to a prototype. I'll use HTMLElement as an example.

HTMLElement.prototype.MyNamespaceGetThis = function() {
  return this;
}

document.body.MyNamespaceGetThis() will return document.body

But if I want to nest it in an object

HTMLElement.prototype.MyNamespace = {};
HTMLElement.prototype.MyNamespace.GetThis = function() {
  return this;
}

document.body.MyNameSpace.GetThis() will return document.body's MyNameSpace ({GetThis: ƒ})

Is there a way to make this or any variable return a reference to the base-object? document.body in this case?

I've tried a few variants, like

HTMLElement.prototype.MyNameSpace = (function() {
  let that = this
  let obj = Object.defineProperties({}, {GetThis: {
    value: function() {
      console.log(that)
    },
    enumerable: false}})

  return obj;
})()

but this fails for completely understandable reasons. The function is only run once and returns a reference to window

I've tried a few experiments with .bind(), but none return the desired result, for predictable reasons.


My Solution

What I didn't like about Jonas' answer, was that it's basically the same as MyNamespace().method, where MyNamespace returns a set of methods.

Persisting members are also impossible. If I wanted to store member data, I needed a separate object somewhere to do that, and I didn't like that.

My solution was to use a class and then invoke it in a special memory-light way.

class MyNamespace {
  constructor(parent) {
    this.parent = parent;
  }

  GetThis() {
    return this.parent;
  }
}

And then, for this example, you add it to the HTMLElement prototype like this

Object.defineProperties(HTMLElement.prototype, {
    MyNamespace: {
      enumerable: false, writeable: true,
      get: function() {
        let ret = new MyNamespace(this);
        Object.defineProperty(this, 'MyNamespace', {
          enumerable: false,
          writeable: false, // note about this
          value: ret
        });
        return ret;
      },
      enumerable: false, writeable: false
    },
  })

The first call to say, document.body.MyNamespace.GetThis() will return a new instance of the class MyNamespace and then invoke GetThis() from that. It will also change document.body's MyNamespace to directly reference the created instance, rather than create a new one each time. This means persistent data.

Something else I like about this, is that each element isn't carrying around a full MyNamespace instance, unless it's called upon in the life of the document.

Once the reference is updated, I have it set to freeze it so that it can't be overwritten, but it's easy to imagine where a person might want a destroy method. You would change writable to true and something like this.

class MyNamespace {
  constructor(parent) {
    this.parent = parent;
  }

  GetThis() {
    return this.parent;
  }

  destroy() {
    Object.defineProperties(HTMLElement.prototype, {
        MyNamespace: {
          enumerable: false, writeable: true,
          get: MyNamespace.factory(this, true),
          enumerable: false, writeable: false
        },
      })
  }

  renew() {
    this.parent.MyNamespace = MyNamespace.factory(this.parent, true)
    // HTMLElement.prototype.MyNamespace can also be set
    // to MyNamespace.factory(this)
  }

  static factory(parent, writeable) {
    return Object.defineProperty(parent, 'MyNamespace', {
      enumerable: false, writeable: writeable,
      value: new MyNamespace(parent)
    }).MyNamespace;
  }
}

Solution

  • Turn MyNamespace into a getter and store a reference to the parent:

    function Namespace(parent) {
       return {
         parent,
         /* Your functions here */
       };
     }
    
    Object.defineProperty(HTMLElement.prototype, "namespace", {
      get() { return Namespace(this); },
    });
    
    console.log( 
      document.body.namespace.parent === document.body // true
    );