简体   繁体   中英

Javascript: object return itself a.k.a. chaining

I am trying to build my own little jquery-like library but I'm having a really rough time with creating this chaining pattern. Basically I have one class with a bunch of methods that make it easier to manipulate the document. Here's an example

function MF(selector){
    var DO; // Stands for DocumentObject
    this.select = function(selector){
        return document.getElementById(selector);
    }
    if(typeof selector === 'string'){
        DO = this.select(selector);
    }else if(selector instanceof HTMLElement){
        DO = selector;
    }

    this.children = function children(selector){
        return DO.getElementsByClassName(selector);
    }
    return {
        MF: ???
    }
}(null);

I might be wrong in my reflections but what I've come to figure out is that in order to have additional methods for a document object ( html element ) I either need to extend the HTMLElement prototype or pass the element along with my class. I've chosen the second option. I just can't figure out what to return in my class so that I can have chaining going on. What I simply aim at, for the sake of this example, is to be able to write the following line of code:

MF('someDiv').children('someClass');

In a desperate attempt I tried returning a new instance of MF which should not have instances by default and led myself to an infinite loop. I really cannot figure what I'm supposed to return there. Any help is greatly appreciated!

return this; will allow access to methods of the Constructor. Do it at the very bottom of the Constructor and the very bottom inside every method that belongs to it, if the method doesn't need to return another value.

function MF(selector){
  var doc = document;
  this.select = function(selector){
    return doc.getElementById(selector);
  }
  // there are problems with some of your code
  this.someMethod = function(){
    /* do stuff - If you want to access an Element then
      var thisIsNowGlobal = this.select('someId');
      thisIsNowGlobal.innerHTML = 'someText';
      Note, the keyword this is global not var
      If you wrote this.select('someId').innerHTML the property would not exist

      When a property of an Object is assigned to a variable or argument
      the this value changes to the global context.
   */
    return this;
  }
  return this;
}

It looks like you also tried to use the module pattern, and haven't taken advantage of prototypes..

When you want to chain something, you either need to return itself ( this ) within the chainable methods, or return a new instance of your Object . In the example below I achieve chaining by using new instances (well the children looked like it needed to be a list so I did an Array of them).

var MF = (function () { // module pattern start
    function MF(selector) {
        if (!(this instanceof MF)) return new MF(selector); // always construct
        this.node = null; // expose your DO
        if (typeof selector === 'string') {
            this.node = document.getElementById(selector);
        } else if (selector instanceof HTMLElement) {
            this.node = selector;
        } else {
            throw new TypeError('Illegal invocation');
        }
    }
    MF.prototype = {}; // set up any inheritance
    MF.prototype.select = function (selector) {
        return new MF(document.getElementById(selector)); // returns new instance
    };
    MF.prototype.children = function (selector) {
        var MFs = [],
            nodes = this.node.getElementsByClassName(selector),
            i;
        for (i = 0; i < nodes.length; ++i) {
            MFs[i] = new MF(nodes[i]);
        }
        return MFs; // array of items of new instances
    };
    return MF; // pass refence out
}()); // module pattern end

Then, for example, you can chain like..

MF(document.body).children('answer')[0].children('post-text')[0].node;

You can do this pretty easily. Bear in mind that when you return this then if this has methods defined on it, then you can call them sequentially.

var MyUtilThing = function(){};
MyUtilThing.prototype.doStuff = function doStuff (){ // give it a name, helps in debugging
  // do your thing
  console.log('doing stuff');
  return this; // this is your instance of MyUtilThing
}

var thing = new MyUtilThing();
thing.doStuff().doStuff().doStuff(); // etc

One way around having to explicitly create instances is do this in your constructor.

var MyUtilThing = function(selector){
  var F = function(){};
  F.prototype = MyUtilThing.prototype;
  var toReturn = new F();
  toReturn.initialize(selector);
  return toReturn;
};

MyUtilThing.prototype.initialize = function initialize(selector){
  this.selector = selector;
};

MyUtilThing.prototype.doStuff = function doStuff (){ // give it a name, helps in debugging
  // do your thing
  console.log('doing stuff to', this.selector);
  return this; // this is your instance created in the constructor (the blank function with the same prototype as MyUtilThing)
}

var thing = MyUtilThing('div'); // no use of new here!
thing.doStuff().doStuff().doStuff(); // etc

But, getting into some slightly heavy territory there. Best bet is just to try and understand exactly how this is used in JS, and you'll get a long way.

Traditionally the way jQuery enables chaining is by creating a wrapper object for every type of return value. For example in your case it pays to create your own wrapper for HTMLElement :

function HTMLElementWrapper(element) {
    if (element instanceof HTMLElementWrapper) return element;
    this.element = element;
}

Now that you have an HTMLElementWrapper you can refactor your MF function as follows:

function MF(selector) {
    return new HTMLElementWrapper(typeof selector === "string" ?
        document.getElementById(selector) : selector);
}

The MF function now returns an HTMLElementWrapper object which has two methods select and children :

HTMLElementWrapper.prototype.select = function (selector) {
    return new HTMLElementWrapper(this.element.getElementById(selector));
};

HTMLElementWrapper.prototype.children = function (selector) {
    return new NodeListWrapper(this.element.getElementsByClassName(selector));
};

Ofcourse for the children function to work you'll need to create a NodeListWrapper constructor:

function NodeListWrapper(list) {
    if (list instanceof NodeListWrapper) return list;
    this.list = list;
}

Now you can chain methods as follows:

MF("someDiv").select("something").children("someClass");

To chain methods after .children("someClass") you need to add those methods to NodeListWrapper.prototype .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM