最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

How to detect chained function calls in JavaScript? - Stack Overflow

programmeradmin6浏览0评论
var Obj = {
   func1 : function() {
      // some code
      if (this._hasChainedFunc()) {
         // block should be CALLED
      }
      return this;
   },
   func2 : function() {  
      // some code
      if (this._hasChainedFunc()) {
         // block should be NOT called
      }
      return this;
   },
   _hasChainedFunc : function() {
     // code which detects if there is a chained function???
   }
}

Obj.func1().func2();

Is there a possible implementation for function _hasChainedFunc()? This function should return true on the first call (because func2() is called afterwards), false on the second call.

In a more advanced version, _hasChainedFunc() may also returned the function which is actually called afterwards.

var Obj = {
   func1 : function() {
      // some code
      if (this._hasChainedFunc()) {
         // block should be CALLED
      }
      return this;
   },
   func2 : function() {  
      // some code
      if (this._hasChainedFunc()) {
         // block should be NOT called
      }
      return this;
   },
   _hasChainedFunc : function() {
     // code which detects if there is a chained function???
   }
}

Obj.func1().func2();

Is there a possible implementation for function _hasChainedFunc()? This function should return true on the first call (because func2() is called afterwards), false on the second call.

In a more advanced version, _hasChainedFunc() may also returned the function which is actually called afterwards.

Share Improve this question edited Aug 12, 2017 at 14:38 Cody Gray 245k53 gold badges503 silver badges584 bronze badges asked Oct 1, 2012 at 19:22 ScholleScholle 1,5212 gold badges24 silver badges44 bronze badges 2
  • Good question and I am unsure how a library like knex makes this work. In that library knex.select().table('books') or knex('users').select('id').where({ id: 1 }) or knex.select('title', 'author', 'year').from('books') all return the same thing (with plenty of other possible method chaining). How does it work?? – Tunn Commented Feb 5, 2020 at 19:36
  • Knex probably returns this on all the chains except the ones that return a value other than this. IE select => this, etc and from => value – MaxPower Commented Aug 31, 2024 at 11:27
Add a ment  | 

9 Answers 9

Reset to default 7

Technically you can never know in advance whether there's another call chained after the current call -- this plainly doesn't make sense because it implies you're aware of some code that's gonna be called before it's called. You can't do this without a pre-piler, which I guess is not what you're after.

Conversely, it is possible to check whether there's been a previous call chained before the current call. This just requires you to keep some state in the object regarding the previous calls, and update it whenever you call a new function on it. If you only use one chain of calls, you can do this by making func1 and func2 change some state on the this object before returning it.

If you want to call multiple chains on the same object, you face the problem of how to detect the end of a chain. For this you will need to make each chained function return a wrapper around the original this, which would store the state about the previous calls.

If you use the wrapper approach, obj.func1().func2() calls func1 on obj, but func2 is called on a wrapper returned from func1 and this wrapper could be aware of the previous func1 call. If you later call obj.func2().func1() then func2 is now called on obj whereas func1 is called on the wrapper which is aware of the previous func2 call, etc.

(NOTE: This answer was originally posted by Scholle as part of the question. I extracted it from the question into an actual answer, as it should have been in the first place. It is not my solution, so I have marked it as Community Wiki.)

Scholle ultimately created a library that does what he wanted.
It's available on GitHub, and some documentation is here.

In short: Take an arbitrary JavaScript function and "chainify" it:

var Model = function() {};

Model.prototype.func1 = function() {
  console.log('func1 has ' + this.c_getPredecessors().length + ' preceding functions');
  return this.c_delay().c_chain(function() { 
    console.log('func1 has ' + this.c_getSuccessors().length + ' succeeding functions');     
    console.log('func1 processing...');
    this.c_next();
  });
};

Model.prototype.func2 = function() {
  console.log('func2 has ' + this.c_getPredecessors().length + ' preceding functions');
  return this.c_delay().c_chain(function() {
    console.log('func2 has ' + this.c_getSuccessors().length + ' succeeding functions');     
    console.log('func2 processing...');
    this.c_next();
  });
};

Chainify and instantiate it, and call some functions:

chainify(Model);

var Obj = new Model();
Obj.func1().func2();

Console output:

func1 has 0 preceding functions
func2 has 1 preceding functions
func1 has 1 succeeding functions
func1 processing...
func2 has 0 succeeding functions
func2 processing... 

Of course, this is a simple example. It just demonstrates that every functions is now capable to access information about what happens before and after the current function call.

No, it's not possible.

It's semantically identically to:

var tmp = Obj.func1();
tmp.func2();

When Obj.func1() is called, there's no way for it to know whether the subsequent result will be used to call func2.

The best you could achieve is for func2 to detect whether func1 was previously called, but for it to work the way you've described would require func1 to be capable of predicting the future.

What you can do is add a member property indicating if it's the first call made on the object or not:

var Obj = {
   _first : true,

   func1 : function() {
      // some code
      if (this._hasChainedFunc()) {
         // block should be CALLED
      }
      return this;
   },
   func2 : function() {  
      // some code
      if (this._hasChainedFunc()) {
         // block should be NOT called
      }
      return this;
   },
   _hasChainedFunc : function() {
       var isFirst = this._first;
       this._first = false;
       return isFirst;
   }
}

Obj.func1().func2();

However, this means you have to reset the state of the object before each call (by setting this._first back to true). You may want to rethink how you're going about this.

here's how i would do this:

var Obj = {
    first:0,   //<--- will store whether it's the first call
    func1 : function() {
            // some code
        if (this._hasChainedFunc()) {
            console.log("called1");
        }
         return this;
    },
    func2 : function() {  
        // some code
        if (this._hasChainedFunc()) {
            console.log("called2");
        }
        return this;
    },
    _hasChainedFunc : function() {
        return (this.first++ > 0);
    }
}

Obj.func1().func2();

and this seems to work:

  called2

http://jsfiddle/2VThj/1/

Why would you want to do this?

That question aside, you could, rather than returning the actual object, make a clone of it, and add an attribute to tell you it is a returned version of the object. That is the only way I can think of. Sounds plex though, depending on how plex this object is.

Something like:

func1 : function() {
  // some code
  if (this._hasChainedFunc()) {
     // block should be CALLED
  }
  return deepCloneWithFlag(this);
},
_hasChainedFunc : function() {
   return this.flag;
}

Nope. this won't work. you could possibly tell that func1() had at some point been called on this object, but you cannot tell WHEN it was called, i.e. right before func2

for example this:

obj.func1();
obj.func2();

is equivalent to your example call. And there is no way func1 could know that func2 will be called in the future.

I solved a problem similar to this with chain functions (docs) This allows true function chaining with the ability to "look-ahead" to see what's ing in the chain.

What you could do is have two separate classes, one for the first element in the chain and one for the remaining elements. Then all you would have to do is change the first class to return an equivalent object from the second class instead of the current object.

var Class1 = function(state){
   return {
       func1 : function() {
           // some code
           // block should be CALLED
           return Class2(state)
       },
       func2 : function() {  
           // some code
           // block should be NOT called
           return Class2(state)     
       }
    };
}


var Class2 = function(state){
   return {
       func1 : function() {
           // some code
           return this;
       },
       func2 : function() {  
           // some code
           return this;
       }
    };
}

Class1(initial_state).func1().func2();

Althought knowing that a function will be called after another function is impossible in Javascript, here is a solution to chainify an object :

(function(window, undefined)
{

    var chainify = function(prop)
    {
        return new chainify.init(prop);
    };

    /**
     * 
     * @param prop :
     *            Properties to apply to the object
     * @returns {chainify.init}
     */
    chainify.init = function(prop)
    {
        for ( var key in prop)
            this[key] = prop[key];
    };

    chainify.init.prototype = {

        _attributes : {},

        _chain_in_progress : false,
        _chain_level : 1,
        _chain_function : '',

        /**
         * Returns the chained object
         * 
         * @param name -
         *            name of the previous function
         * @this chainify.init
         * @returns {chainify.init}
         */
        _chain : function(name)
        {
            var tmp = chainify(this);
            tmp._chain_in_progress = true;
            tmp._chain_function = name || '';
            _chain_level++;
            return tmp;
        },

        get : function(key)
        {
            return this._attributes[key];
        },

        set : function(key, value)
        {
            this._attributes[key] = value;
            return this;
        },

        attr : function(prop)
        {
            for ( var key in prop)
                this._attributes[key] = prop[key];
            return this;
        },

    };

    // Make global
    window.chainify = chainify;
})(window);



var myObject = window.chainify({

    // f1() function is using _chain()
    f1 : function(s)
    {
        // Do something
        this.set('s1', s);
        if (this._chain_in_progress) alert('f1 after ' + this._chain_function);

        // return the chain by calling this._chain()
        return this._chain('f1');
    },

    // f2() function is using _chain()
    f2 : function(s)
    {
        this.set('s2', s);
        if (this._chain_in_progress) alert('f2 after ' + this._chain_function);
        return this._chain('f1');
    },

    // that() function is not using _chain(), but we return this so the chaining
    // is not broken
    that : function(s)
    {
        // Do something
        return this;
    }
});


// Check if the f1 function is working
myObject.f1('a'); // Set s1 to "a"
alert(myObject.get('s1')); // should be "a"

// check if the f2 chaining is working
myObject.f1('b').f1('c'); // f1 after f1
alert(myObject.get('s1')); // should be "c" -> changed on last f1 function

// Check if the f2 function is working
myObject.f2('a');
alert(myObject.get('s2')); // should be "a"

// check if the f2 and f1 chaining is working
myObject.f2('b').f1('c').f1('d').f2('e'); // f1 after f2, f1 after f1 ...

alert(myObject.get('s1')); // should be "d" -> changed on last f1 function
alert(myObject.get('s2')); // should be "e" -> changed last f2 function

// check the chain with that() -
myObject.that('b').f1('a').f1('z'); // f1 chained after f1
alert(myObject.get('s1')); // should be "z" -> changed on last f1 function
发布评论

评论列表(0)

  1. 暂无评论