Gaurang Tandon
Gaurang Tandon

Reputation: 6761

How to extend prototype of an object and have the same function in global scope as well?

I need to setup shorthands for several DOM accessor methods, namely - getElementsByClassName (qCls), getElementById (qId), querySelector (q), querySelectorAll (Q) - such that they are available:

  1. in the global scope: I should be able to write q(selector) as a shorthand for document.querySelector(selector) throughout my code (in any scope or context).
  2. as a property on DOM elements: I should be able to write paragraph.q(selector) as a shorthand for paragraph.querySelector(selector).

I know the manual way to do this is:

window.Q = function(sel){
    console.dir(document.querySelectorAll(sel));
};

Node.prototype.Q = function(sel){
    console.dir(this.querySelectorAll(sel));
};


// TEST
var p = document.querySelector("p");

Q("a"); // should have two elements
p.Q("a"); // should have one element
<a>1</a>
<p><a>2</a></p>

But this has duplicacy - repeating exactly the same function again with only a minor difference (document vs this) - so I intend to eliminate that duplicacy.1

Now, what I think would work to achieve this is a pattern like:

window.Q = helperFunc(someFlag);
Node.prototype.Q = helperFunc(someOtherFlag);

where the helperFunc handles the logic for the return value, based on the flags. Based on this, I wrote the following:

function outputQFunc(context) {
    return function(selector) {
        console.dir(context.querySelectorAll("a"));
    };
}

window.Q = outputQFunc(document);
Node.prototype.Q = outputQFunc(this);

However, this has the obvious downside that the this context for Node.prototype.Q permanently points to the global scope, instead of the node which called this function. I could instead eliminate the context:

function outputQFunc() {
    return function(selector) {
        console.dir(this.querySelectorAll("a"));
    };
}

but then the this does not point to the document in case of the window.Q.

So, my question is, how to achieve this?


Notes:

  1. While this duplicacy may sound trivial (it's only a single line!), I have actually shortened the code for MVCE. My actual code has to apply some filters and stuff to the result of doc.qsAll before returning it. In such cases, the duplicacy magnifies.

Once my query got resolved, if anyone is interested in the final code I reached, this is it, which I personally find very DRY and extensible:

var DOM_HELPERS = {
  /**
   * short hand for document.querySelector
   * @param {string} selector selector to match element
   */
  q: function(selector) {
    return this.querySelector(selector);
  },
  /**
   * short hand for document.querySelectorAll
   * @param {string} selector selector to match elements
   */
  Q: function(selector) {
    return this.querySelectorAll(selector);
  },
  /**
   * short hand for document.getElementById
   * @param {string} id selector to match element
   */
  qId: function(id) {
    return this.getElementById(id);
  },
  /**
   * short hand for document.getElementsByClassName
   * @param {string} cls selector to match elements
   */
  qCls: function(cls) {
    return this.getElementsByClassName(cls);
  }
};

for (var i = 0, funcs = Object.keys(DOM_HELPERS), len = funcs.length, funcName, func; i < len; i++) {
  funcName = funcs[i];
  func = DOM_HELPERS[funcName];
  window[funcName] = func.bind(document);
  Node.prototype[funcName] = func;
}

Upvotes: 0

Views: 25

Answers (1)

CertainPerformance
CertainPerformance

Reputation: 370979

Use bind on your helper function to specify a custom this context for window.Q (and use the default this otherwise, which will point to the proper calling context when called on a node):

function qSA(sel) {
  console.dir(this.querySelectorAll(sel));
}


window.Q = qSA.bind(document);
Node.prototype.Q = qSA;

// TEST
var p = document.querySelector("p");

Q("a"); // should have two elements
p.Q("a"); // should have one element
<a>1</a>
<p><a>2</a></p>

Upvotes: 1

Related Questions