I made the following class to 'hijack' the console.log
function. The reason behind this is that I want to add and remove values
dynamically. It will be used for debug purposes, so the origin of the function call console.log()
is important. In the following code I will explain my logic in the comments.
export class ConsoleLog {
private _isActive = false;
private _nativeLogFn: any;
constructor() {
// ----------------------
// Store the native console.log function, so it can be restored later
// ----------------------
this._nativeLogFn = console.log;
}
public start() {
if (!this._isActive) {
// ----------------------
// Create a new function as replacement for the native console.log
// function. *** This will be the subject of my question ***
// ----------------------
console.log = console.log.bind(console, Math.random());
this._isActive = true;
}
}
public stop() {
if (this._isActive) {
// Restore to native function
console.log = this._nativeLogFn;
this._isActive = false;
}
}
}
The problem with this setup is, that the new function is assigned in a static form.
// Function random() generates a number at the moment I assign the function.
// Let's say it's the number *99* for example sake.
console.log.bind(console, Math.random());
Every time the console.log(...)
is called, it will output 99. So it's pretty much static. (To be ahead of you: no my goal isn't outputting a random number, lol, but I just use it to test if the output is dynamic or not.).
The annoying part is, using the function with the
console.log.bind
is the only way I found that actually preserves the
origin caller and line number.
I wrote the following simple test.
console.log('Before call, active?', 'no'); // native log
obj.start(); // Calls start and replaces the console.log function
console.log('foo'); // This will output 'our' 99 to the console.
console.log('bar'); // This will output 'our' 99 again.
obj.stop(); // Here we restore the native console.log function
console.log('stop called, not active'); // native log again
// Now if I call it again, the random number has changed. What is
// logical, because I re-assign the function.
obj.start(); // Calls start and replaces the console.log function
console.log('foo'); // This will output N to the console.
// But then I have to call start/log/stop all the time.
Question: How can I add values to the console.log at run time without losing the origin caller filename and line number... AND without bothering the library consumer once this class is initiated with start().
EDIT: Added a plkr:
I made the following class to 'hijack' the console.log
function. The reason behind this is that I want to add and remove values
dynamically. It will be used for debug purposes, so the origin of the function call console.log()
is important. In the following code I will explain my logic in the comments.
export class ConsoleLog {
private _isActive = false;
private _nativeLogFn: any;
constructor() {
// ----------------------
// Store the native console.log function, so it can be restored later
// ----------------------
this._nativeLogFn = console.log;
}
public start() {
if (!this._isActive) {
// ----------------------
// Create a new function as replacement for the native console.log
// function. *** This will be the subject of my question ***
// ----------------------
console.log = console.log.bind(console, Math.random());
this._isActive = true;
}
}
public stop() {
if (this._isActive) {
// Restore to native function
console.log = this._nativeLogFn;
this._isActive = false;
}
}
}
The problem with this setup is, that the new function is assigned in a static form.
// Function random() generates a number at the moment I assign the function.
// Let's say it's the number *99* for example sake.
console.log.bind(console, Math.random());
Every time the console.log(...)
is called, it will output 99. So it's pretty much static. (To be ahead of you: no my goal isn't outputting a random number, lol, but I just use it to test if the output is dynamic or not.).
The annoying part is, using the function with the
console.log.bind
is the only way I found that actually preserves the
origin caller and line number.
I wrote the following simple test.
console.log('Before call, active?', 'no'); // native log
obj.start(); // Calls start and replaces the console.log function
console.log('foo'); // This will output 'our' 99 to the console.
console.log('bar'); // This will output 'our' 99 again.
obj.stop(); // Here we restore the native console.log function
console.log('stop called, not active'); // native log again
// Now if I call it again, the random number has changed. What is
// logical, because I re-assign the function.
obj.start(); // Calls start and replaces the console.log function
console.log('foo'); // This will output N to the console.
// But then I have to call start/log/stop all the time.
Question: How can I add values to the console.log at run time without losing the origin caller filename and line number... AND without bothering the library consumer once this class is initiated with start().
Share Improve this question edited May 15, 2017 at 2:08 reduckted 2,5183 gold badges31 silver badges37 bronze badges asked May 11, 2017 at 23:50 therebelcodertherebelcoder 1,01814 silver badges28 bronze badges 5 |EDIT: Added a plkr: https://embed.plnkr.co/Zgrz1dRhSnu6OCEUmYN0
4 Answers
Reset to default 6Cost me the better part of the weekend and a lot of reading and fiddling, but I finally solved it leveraging the ES6 proxy object. Pretty powerful stuff I might add. Explanation is in the code. Please don't hesitate to improve on it or ask questions.
(EDITED based on @Bergi's comments) Here is the class:
export class ConsoleLog {
private _isActive = false;
private _nativeConsole: any;
private _proxiedConsole: any;
/**
* The Proxy constructor takes two arguments, an initial Object that you
* want to wrap with the proxy and a set of handler hooks.
* In other words, Proxies return a new (proxy) object which wraps the
* passed in object, but anything you do with either effects the other.
*
* ref: https://www.keithcirkel.co.uk/metaprogramming-in-es6-part-3-proxies
* ref: http://exploringjs.com/es6/ch_proxies.html#_intercepting-method-calls
*/
/**
* Challenge:
* When we intercept a method call via a proxy, you can intercept the
* operation 'get' (getting property values) and you can intercept the
* operation 'apply' (calling a function), but there is no single operation
* for method calls that you could intercept. That’s why we need to treat
* them as two separate operations:
*
* First 'get' to retrieve a function, then an 'apply' to call that
* function. Therefore intercepting 'get' and return a function that
* executes the function 'call'.
*/
private _createProxy(originalObj: Object) {
const handler = {
/**
* 'get' is the trap-function.
* It will be invoked instead of the original method.
* e.a. console.log() will call: get(console, log) {}
*/
get(target: object, property: string) {
/**
* In this case, we use the trap as an interceptor. Meaning:
* We use this proxy as a sort of pre-function call.
* Important: This won't get invoked until a call to a the actual
* method is made.
*/
/**
* We grab the native method.
* This is the native method/function of your original/target object.
* e.a. console.log = console['log'] = target[property]
* e.a. console.info = console['info'] = target[property]
*/
const nativeFn: Function = target[property];
/**
* Here we bind the native method and add our dynamic content
*/
return nativeFn.bind(
this, `%cI have dynamic content: ${Math.random()}`, 'color:' +
' #f00;'
);
}
};
return new Proxy(originalObj, handler);
}
constructor() {
// Store the native console.log function so we can put it back later
this._nativeConsole = console;
// Create a proxy for the console Object
this._proxiedConsole = this._createProxy(console);
}
// ----------------------
// (Public) methods
// ----------------------
public start() {
if (!this._isActive) {
/**
* Replace the native console object with our proxied console object.
*/
console = <Console>this._proxiedConsole;
this._isActive = true;
}
}
public stop() {
if (this._isActive) {
// Restore to native console object
console = <Console>this._nativeConsole;
this._isActive = false;
}
}
}
And here the code to see for yourself:
const c: ConsoleLog = new ConsoleLog();
console.log('Hi, I am a normal console.log', ['hello', 'world']);
c.start(); // Start - replaces the console with the proxy
console.log('Hi, I am a proxied console.log');
console.log('I have dynamic content added!');
console.log('My source file and line number are also intact');
c.stop(); // Stop - replaces the proxy back to the original.
console.log('I am a normal again');
Cheers!
If you are looking to dynamically bind your function, you could do it on every access to the .log
property. A simple getter is enough for that, no need to employ ES6 proxies:
export class ConsoleLog {
constructor(message) {
this._isActive = false;
const nativeLog = console.log;
Object.defineProperty(console, "log", {
get: () => {
if (this._isActive)
return nativeLog.bind(console, message())
return nativeLog;
},
configurable: true
});
}
start() {
this._isActive = true;
}
stop() {
this._isActive = false;
}
}
new ConsoleLog(Math.random).start();
How about:
const consolelog = console.log;
console.log = function (...args) {
return consolelog.apply(this, [Math.random()].concat(args));
}
Notice that the this
inside the function is not the instance of your class.
The function is a regular anonymous function and not an arrow function so that the function scope will depend on the execution.
Edit
Ok, without apply
, this is even better:
console.log = function (...args) {
return consolelog(Math.random(), ...args);
}
2nd edit
I was about to say that it's not possible, but then I had a breakthrough:
function fn() {
return Math.random();
}
fn.valueOf = function () {
return this();
};
console.log = consolelog.bind(console, fn);
Then this: console.log("message")
will output something like:
function 0.4907970049205219 "message"
With the right caller, but I couldn't remove the function
part in the begining.
Then I had another breakthrough:
function fn() {
return Math.random();
}
fn.toString = function () {
return this().toString();
}
console.log = consolelog.bind(console, "%s", fn);
Then this: console.log("message")
will output:
0.9186478227998554 message
With the right caller, as you requested.
It only works when you bind it to a function, using other objects doesn't work.
This answer shows how to use a proxy and Object.bind to inject arguments into an existing (object/API)'s functions.
This works with console retaining the consoles line number and file referance.
// targetName is the name of the window object you want to inject arguments
// returns an injector object.
function injector(targetName){
const injectors = {}; // holds named injector functions
const _target = window[targetName]; // shadow of target
const proxy = new Proxy(_target, {
get: function(target, name) {
if (typeof injectors[name] === "function" &&
typeof _target[name] === "function") { // if both _target and injector a function
return _target[name].bind(_target, ...injectors[name]());
}
return _target[name];
},
});
return {
enable () { window[targetName] = proxy; return this },
disable () { window[targetName] = _target },
injector (name, func) { injectors[name] = func },
};
};
To use
// Example argument injector.
// Injector functions returns an array of arguments to inject
const logInfo = {
count : 0,
counter () { return ["ID : " + (logInfo.count++) + ":"] },
mode(){ return ["App closing"] },
}
Instantiating a console injector
// Create an injector for console
const consoleInjector = injector("console");
Injector's functions enable
, injector
, disable
usage
// Enable consoleInjector and add injector function.
consoleInjector.enable().injector("log", logInfo.counter);
console.log("testA"); // >> ID : 0: testA VM4115:29
console.log("testB"); // >> ID : 1: testB VM4115:31
// Replace existing injector function with another one.
consoleInjector.injector("log",logInfo.mode); // change the injector function
console.log("testC"); // >> App closing testC VM4115:34
console.log("testD",1,2,3,4); // App closing testD 1 2 3 4 VM4115:35
// Turn off console.log injector
consoleInjector.injector("log",undefined);
// or/and turns off injector and return console to normal
consoleInjector.disable();
console.log("testE"); // testE VM4115:42
Function.prototype.bind.call(console.log, console, Math.random());
can be done like this:console.log.bind(console, Math.random())
– Nitzan Tomer Commented May 12, 2017 at 0:20bind
already solves the problem with the call stack. Especially I don't see how the code in your answer behaves differently from the one in your question. – Bergi Commented May 14, 2017 at 20:27start
, and for that the "static" version is fine. – Bergi Commented May 14, 2017 at 20:43