最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Add dynamic values to the console methods at run-time with preservation of original call position and line number i

programmeradmin3浏览0评论

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().

EDIT: Added a plkr: https://embed.plnkr.co/Zgrz1dRhSnu6OCEUmYN0

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
  • 2 This: 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:20
  • Yes it can, ty @nitzan-tomer. I will adjust the example code for good measure. – therebelcoder Commented May 12, 2017 at 2:48
  • What exactly do you mean by "AND without bothering the library consumer once this class is initiated with start()"? It seems that bind 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:27
  • @Bergi with "without bothering the library consumer" I mean that, once initiated, you can use the console as usual. So you can add this to an existing application, without changing all your console methods or take this class out and your app will work as usual. The difference with the code in my question is that the Math.Random() is actually re-freshed. So now I know the content assigned is dynamic. – therebelcoder Commented May 14, 2017 at 20:39
  • @stevenvanc Ah, that's what you were after. I thought you were going to pass the "prefix" string to start, and for that the "static" version is fine. – Bergi Commented May 14, 2017 at 20:43
Add a comment  | 

4 Answers 4

Reset to default 6

Cost 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

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论