How to detect the end of a method chain in JavaScript?

谁说胖子不能爱 提交于 2019-12-04 19:33:44

问题


Firstly and most importantly I'm trying to detect the end call of a method chain. I would also like to devise a way to detect how many methods "in" or "down" an object chain I am within my method calls in a method chain.

For instance, in the plugin I'm writing:

var result = $("#someDiv").myPlugin.foo().bar()._foo()._bar();

Say the method is currently executing in .bar() I would like to know that I'm 2 methods down the chain.

The reason I need to abstract this information in some manner is so when I reach the last method in the chain I can return a result instead of the plugin object thus breaking the chain at that point for the sake of gaining access to our data in the 'result' variable.


回答1:


Here's an example pulled from your project:

var strLenA = parseInt( P.strlen('some string').data );
var strLenB = parseInt( P.strlen('another string').data );
var totalStrLen = strLenA + strLenB;
console.log(strLenA, strLenB, totalStrLen);

From this I can see why our answers aren't really adequate - and why you want to get rid of .data. Happily, your .data always returns a string, anyway. So, you can use the mystical .toString override to have your methods still return a copy of the parent method - but also allow for them to be treated like strings.

Here's an example: [with a fiddle]

var stringMagic = function() {
    var chain = "",
        self = this;
    self.toString = function () { return chain; }; // Where the real magic happens.
    self.add = function(str) {
        chain += str + " ";
        return self;
    };
};


var magi = new stringMagic();
alert(magi.add("hello").add("world")); // Alerts, "hello world"
magi.add("and").add("thanks").add("for").add("all").add("the").add("fish");
alert(magi); // Alerts, "hello world and thanks for all the fish"

In your case, probably all you'd have to do is change .data in P to .toString and wrap it in a function.

In the future when you add support for other data types such as numbers and booleans, you can use valueOf in the same way you use toString. In fact, you should also continue to include toString when the return value is not a string for when they're treating that number as a string - like in console.log or $.fn.text. Here's the example above, but with numbers: http://jsfiddle.net/emqVe/1/




回答2:


For the sake of completeness. Yet another alternative is to pass a an object that will get updated as the chain progress. That would let you access the result value whenever suits you (instead of having to add it at the end of the chain).

Instead of a syntax like this:

var result = chainableFunction.doThis().doThat().result;

You would then have:

chainableFunction.update(variableToUpdate).doThis().doThat();
var result = variableToUpdate.result;

The logic is very much the same as the solution proposed by others. Which one to use probably depends on your use cases. A possible issue with having to end the chain with .result is that nothing prevents you from using it this way:

var chainedUp = chainableFunction.doThis().doThat();
doSomething(chainedUp.result);
... 
chainedUp.doOneMoreThing()
... 
doSomething(chainedUp.result);  // oups, .result changed!

With the variableToUpdate option, the result value is not affected by future function calls. Again, that could be desirable in some contexts, not in others.

Full example below

#!/usr/bin/env node

var ChainUp = {};
(function(Class) {

  // Pure functions have no access to state and no side effects
  var Pure = {};
  Pure.isFunction = function(fn) {
     return fn && {}.toString.call(fn) === '[object Function]';
  };

  Class.instance = function() {
    var instance = {};
    var result;
    var updateRef;

    function callBack(fn) {
      // returning a clone, not a reference.
      if(updateRef) { updateRef.r = (result || []).slice(0); } 
      if(Pure.isFunction(fn)) { fn(result); }
    }

    instance.update = function(obj) {
      updateRef = obj;
      return instance;
    };

    instance.one = function(cb) {
        result = (result || []); result.push("one");
        callBack(cb);
        return instance;
      };
      instance.two = function(cb) {
        result = (result || []); result.push("two");
        callBack(cb);
        return instance;
      };
      instance.three = function(cb) {
        result = (result || []); result.push("three");
        callBack(cb);
        return instance;
      };
      instance.result = function() {
        return result;
      };
    return instance;
  };

}(ChainUp));


var result1 = {};
var chain = ChainUp.instance().update(result1);
var one = chain.one(console.log); // [ 'one' ]
console.log(one.result());        // [ 'one' ]
console.log(result1.r);           // [ 'one' ]

var oneTwo = chain.two(); 
console.log(oneTwo.result());  // [ 'one', 'two' ]
console.log(result1.r);        // [ 'one', 'two' ]

var result2 = {};
var oneTwoThree = chain.update(result2).three();
console.log(oneTwoThree.result()); // [ 'one', 'two', 'three' ]
console.log(result2.r);            // [ 'one', 'two', 'three' ]

console.log(result1.r);             // [ 'one', 'two' ]

Note. The Class and instance keywords are probably unfamiliar. That's a convention that I use when using closures instead of prototypical inheritance to construct instances from a prototype. You could replace instance with self (and self = this instead of instance = {})..




回答3:


It isn't possible to determine if a call is the last instance in a chain when determining the return value, and here is why:

var result = $("#someDiv").myPlugin.foo().bar()._foo()._bar();

foo returns myPlugin on which bar is called which returns myPlugin on which _foo is called which returns myPlugin on which _bar is called.

So effectively, when _foo returns its value (myPlugin), it is before that value is utilized. Unless _foo is psychic, it can't know what will happen next.

As pointed out in your comments, your best bet is to have some "end" method, like results().

Another suggestion would be to pass a handler in to myPlugin that gets called to set the value using setTimeout(..., 0). Have a value in myPlugin that foo, bar, _foo, and _bar all set. Let's call it returnValue. Modify myPlugin to accept a method as it's only parameter. Let's call that handler. This method's first argument will contain the value. Inside of myPlugin, before your return, do:

window.setTimeout(function () {
    handler(returnValue);
}, 0);

Since setTimeout's function parameter will not be called until execution is finished, it will contain the last value set for returnValue - effectively the value set by the last call in the chain. I'd consider this the closest option to what you are trying to achieve, since the developer doesn't have to worry about which of his methods are called last.




回答4:


There are no (legal or easy or nice) way to find out inside a method what happens with the result outside, after it returns with it. You should use a "chain end mark" method.

Think again, are you looking for the last method applied on an object, or do you want to detect something more explicite thing? Maybe you lose a possibility to apply methods on a decision (with fake silly method names):

obj.turnLeft().pushUp().makeBig().makeSilent;
if (colorSupport) {
  obj.paintRed();
} else {
  obj.paintStripes();
}
obj.makeShine().lastMethodCallSoObjectIsNowInFinalState();



回答5:


There is no native way, however, you can add a parameter to the method meant to be chained as you want. And determine if the current call is the latest by setting it to true, as in:

var AlertText = {
  text: "",
  chainEntry: function(textToAdd, isTheLastOne) {
    this.text += textToAdd;
    // Perform only if this is told to be the last call in the chain:
    if (isTheLastOne) {
      alert(this.text);
      this.text = "";
    }
    //
    return this;
  }
};

AlertText
  .chainEntry("Hi, ")
  .chainEntry("how ")
  .chainEntry("are ")
  .chainEntry("you?", true); // alert("Hi, how are you?");


来源:https://stackoverflow.com/questions/14123557/how-to-detect-the-end-of-a-method-chain-in-javascript

工具导航Map