Javascript chain rule, return specific value instead of [Object object] [x]
Asked Answered
D

3

6

The question is at the title, but first please look at this code:

function number(a) {
    return {
        add: function(b) {
            result = a + b;
            return this;
        }, substract(b) {
            result = a - b;
            return this;
        }
}

These code above are simple example of chain rule. I retun an object so I can do it continuously:

number(2).add(5).add(3 * 12).substract(Math.random());

My problem is, I have to retun an object to keep the function chainable. I'd like to immitate the chain rule, but to return specific value. For instance number(2).add(3) would return 5.

Any suggestion is highly appreciated.

Thanks everyone in advanced. [x]

Dissolve answered 26/6, 2011 at 5:32 Comment(0)
E
6

One way to make a numeric value like 5 "chainable" is to define a method on the appropriate prototype object, such as Number.prototype. For instance:

Number.prototype.add = function (n) {
   return this + n
}

(5).add(2) // 7
5.0.add(2) // 7
5..add(2)  // 7
((5).add(2) + 1).add(34) // okay! 42

The syntax above is funny because 5.add(2) is invalid: JavaScript is expecting a number (or "nothing") after 5.. Because this is a global side-effect (it will affect all numbers), care should be taken to avoid unexpected interactions.

The only other Another way to make "5" chain-able is to create a new Number object (5 is not a real Number instance, even though it uses Number.prototype!) and then copy over required methods. (I used to think this was the only other way, but see KooiInc's answer -- however, I am not sure how well-defined returning a non-string from toString is.)

function ops(a) {
  return {
    add: function(b) {
      var res = new Number(a + b) // important!
      var op = ops(res)
      res.add = op.add // copy over singletons
      return res
    }
  }
}
function number(a) {
  return ops(a)
}

number(5).add(2) + 1           // 8
(number(5).add(2) + 1).add(34) // error! add is not a function

However, keep in mind this introduces subtle issues:

typeof 5                        // number
typeof new Number(5)            // object
5 instanceof Number             // false
new Number(5) instanceof Number // true

And this is why we need a Number (search SO for "primitives" in JavaScript):

x = 5
x.foo = "bar"
x.foo // undefined

Furthermore, in conjunction with cwolves' answer, consider:

function number (n) { 
  if (this === window) { // or perhaps !(this instanceof number)
    return new number(n)
  } else {
    this.value = n
  }
}

Then both new number(2) and both number(2) will evaluate to a new number object.

number(2).value     // 2
new number(2).value // 2
number(2) instanceof number     // true
new number(2) instanceof number // true

Happy coding.

Effete answered 26/6, 2011 at 7:7 Comment(2)
+1. This one works correctly, but kind of complex. And I think it's the handy way to do it, I learned a lot from this example. I have something confusing here, the line res.add = op.add did you copy the method add from ops(number) to res.add and then return it? If you do so, what happened if you have more than just one method to chain? [x]Dissolve
@Dissolve Many frameworks come with an "extend" method which can copy over a bunch of properties at once. (Because it is tedious to do res.add = op.add; res.subtract = op.subtract, etc. :-) In that example, however, note that each time op is invoked, a new set of functions is created. While likely not a concern, this creates much more garbage and data bound in closures than the approach using the prototype.Effete
M
3

You have two options. You can return new objects:

function number(a){
    return this instanceof number ? (this.value = a, this) : new number(a);
}

number.prototype = {
    valueOf : function(){
        return this.value;
    },
    add : function(b){
        return new number(this.val + b);
    },
    subtract : function(b){
        return new number(this.val - b);
    }
};

or you can modify the existing one (mostly the same code as above, this is different):

add : function(b){
    this.value += b;
    return this;
},

The difference is in how they act:

var x = new number(5),
    y = x.add(10);

// with first example
// x == 5, y == 15


// with 2nd example
// x == 15, y == 15, x === y
Monohydroxy answered 26/6, 2011 at 5:36 Comment(6)
Hi cwolves. It works now. But I'm wondering if there is any otherway to chain the functions (like jQuery) and return specific value without creating a new object? I mean x = number(2).add(2).add(3) instead of x = new number(2); x.add(2).add(3)? [x]Dissolve
@Dissolve - make the return statement from the constructor: return this instanceof number ? this : new number(a);. Modified sample to reflectMonohydroxy
@cwolves I'd move the check up to the top of the function to avoid duplicate work in the "non-new" case (even though it is so trivial in this example). It also keeps the intent from "hiding" in single trailing ternary condition :)Effete
@cwolves Ahh, was looking at the comment, not updated code. I just find the updated code "too clever" :)Effete
@pst - I've had people criticize me on using commas like that before :) Anyway, point taken. If the constructor is actually complicated it would make more sense to put if(!this instanceof number){ return new number(a); } at the top of the constructorMonohydroxy
Weird, your example code just return an [Object object] not a specific value when I run number(4).add(2).add(4). I'm still trying to make it work because it's from my original chain simulation, I can edit my code much easier than using any other way. [x]Dissolve
T
3

If you define the value as property (this.a) and use toString within the returned Object, you can chain the methods:

function number(a) {
    return {
        a: Number(a) || 0, //if not a, or a===NaN, default = 0
        add: function(b) {
            this.a += b;
            return this;
        },
        subtract: function(b){
            this.a -= b;
            return this;
        },
        valueOf: function(){
          return Number(this.a);
        },
        toString: this.valueOf
    }
}

var n = number(5);
alert(number.add(5).add(2).subtract(2)); //=> 10
alert(number.add(0.5));                  //=> 10.5
alert(number(2).add(5).add(3 * 12).subtract(Math.random());
                                         //=> 42.36072297706966
Trichoid answered 26/6, 2011 at 8:39 Comment(6)
+1 Clever (the case of number(5) + 3, which should be shown above, results in 8 -- at least in FireFox 4). Do you know if/where returning a non-String from toString is well-defined?Effete
valueOf should be used instead of toString, per my example :)Monohydroxy
@all: actually just valueOf won't work, but you can force toString to use valueOf - see my edit. This is a nice article on object to primitive conversion: adequatelygood.com/2010/3/…Trichoid
This one works perfectly as I expected, but I still don't get it why the line toString: this.valueOf is too important? I didn't even see you call it, so what is it for? [x]Dissolve
@Dissolve Very nice linked article. For some reason I thought those (conversion functions) were only internally defined ... silly me.Effete
Every Object (well actually as far as I know, Boolean) has the toString function, I guess that line is used to overwrite it? But I'm not sure and I'm still looking for an answer, what that line does toString: this.valueOf? [x]Dissolve

© 2022 - 2024 — McMap. All rights reserved.