Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A function to tap into any methods chain

I like how Ruby's .tap method works. It lets you tap into any method chain without breaking the chain. I lets you operate an object then returns the object so that the methods chain can go on as normal. For example if you have foo = "foobar".upcase.reverse, you can do:

"foo = foobar".upcase.tap{|s| print s}.reverse

It will print the upcased (but not reversed) string and proceed with the reversing and assignment just like the original line.

I would like to have a similar function in JS that would serve a single purpose: log the object to console.

I tried this:

Object.prototype.foo = function() {
  console.log(this);
  return this;
};

Generally, it works (though it outputs Number objects for numbers rather than their numeric values).

But when i use some jQuery along with this, it breaks jQuery and stops all further code execution on the page.

Errors are like this:

  • Uncaught TypeError: Object foo has no method 'push'
  • Uncaught TypeError: Object function () { window.runnerWindow.proxyConsole.log( "foo" ); } has no method 'exec'

Here's a test case: http://jsbin.com/oFUvehAR/2/edit (uncomment the first line to see it break).

So i guess that it's not safe to mess with objects' prototypes.

Then, what is the correct way to do what i want? A function that logs current object to console and returns the object so that the chain can continue. For primitives, it should log their values rather than just objects.

like image 635
Andrey Mikhaylov - lolmaus Avatar asked Feb 01 '14 12:02

Andrey Mikhaylov - lolmaus


1 Answers

You correctly figured out how a method can be safely added anywhere in a chain, but your adding it to the Object.prototype is intrusive and can break code easily. Looks like jQuery code is the one that breaks for you.

A much safer way is:

Object.defineProperty(Object.prototype, 'foo', {
  value : function() {  console.log( "foo" );  return this; },
  enumerable : false
});

DEMO: http://jsbin.com/oFUvehAR/7/edit

Finally, something generic could look like this:

Object.defineProperty(Object.prototype, 'tap', {
  value : function(intercept) {  
    intercept.call(this);  
    return this; 
  },
  enumerable : false
});

// Usage:
var x  = { a:1 };
x.tap(function(){ console.log(this); });

As for the primitives part of your question, that is a bit trickier. When you call the tap method on a primitive, an Object wrapper is created and the tap method is called on it. The primitive value is still available, via the valueOf() method of that Object wrapper, so you could log it. The tricky part is that you have no way of knowing if the "thing" that you wanted to call the tap method on was initially a primitive or an Object wrapper. Assuming you never want to work with Object wrappers (that is quite reasonable), you could do the better tap method posted below.

Object.defineProperty(Object.prototype, 'tap', {
  value : function(intercept) {  
    var val = (this instanceof Number || this instanceof String || this instanceof Boolean) ? this.valueOf() : this;
    intercept(val);  
    return val; 
  },
  enumerable : false
});

var log = console.log.bind(console);

var x  = { a : 1 };

x.tap(log);
2.0.tap(log);

Notice that while in the first version of the tap function, the function passed to it had the useful information in this, in the second version it is mandatory to pass it as a parameter.

If you want a specialized logger for it, you can do something like this:

Object.defineProperty(Object.prototype, 'log', {
  value : function(){
    return this.tap(console.log.bind(console));
  },
  enumerable : false,
  writable : true /* You want to allow objects to overwrite the log method */
});
like image 156
Tibos Avatar answered Sep 27 '22 16:09

Tibos