8

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.

Andrey Mikhaylov - lolmaus
  • 23,107
  • 6
  • 84
  • 133
  • 1
    Take a look at this: http://stackoverflow.com/questions/20307797/underscore-js-tap-function-what-is-a-method-chain – Kostadin Feb 01 '14 at 12:36
  • Hey @Kostadin, it doesn't work with any chain, only with chains of Underscore methods that start with the "_" object. – Andrey Mikhaylov - lolmaus Feb 01 '14 at 12:43
  • 1
    I understand the `tap` function is a big missing piece in JavaScript. For those looking for Kotlin's scope functions, I found [scope-extensions-js](https://www.npmjs.com/package/scope-extensions-js) available. – ymkjp Jun 18 '21 at 14:54

1 Answers1

7

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 */
});
Tibos
  • 27,507
  • 4
  • 50
  • 64
  • Thank you for the detailed answer, @Tibos! To make the answer perfect, could you please provide a log-only version of the last example? I want a quick-to-use `1.log()` variant rather than a more powerful yet slower to use `var log = console.log.bind(console); 1.tap(log);` variant. Also, i think it would be reasonable to check whether this property is not defined before defining it. – Andrey Mikhaylov - lolmaus Feb 01 '14 at 13:04
  • 1
    Done. Note that `1.log` is a syntax error, you need `1.0.log`. As for the scenario where the property is already defined, that's a big problem i'd rather not touch on. If you overwrite the property, the code that relied on the old behavior breaks, if you don't, the code that relies on the new behavior breaks. Only if both implementations have the same interface will things still work. – Tibos Feb 01 '14 at 13:31
  • 1
    One more comment, `Object.defineProperty` defines properties as non-enumerable by default, so the `enumerable : false` bit is superfluous. I leave it in for clarity, to show that that property **needs** to be non-enumerable. – Tibos Feb 01 '14 at 13:45