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 libraryknex.select().table('books')
orknex('users').select('id').where({ id: 1 })
orknex.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
9 Answers
Reset to default 7Technically 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