Log a string when console.log(new X()) is executed

207 Views Asked by At

In interview, I was asked that any string for e.g. "Hello" should be consoled when below line of code is executed.

console.log(new X())

This should be achieved without any additional console.log statements.

I tried something like:

function X() {   return "Hello"; }

But it is not returning a string. It is returning the newly created instance of X.

Was this a trick question?

3

There are 3 best solutions below

5
chiliNUT On BEST ANSWER

tl; dr;

function X() {
   return document.createTextNode("Hello");
}
console.log(new X()); //"Hello"

Explanation:

An immediate thought is to somehow use toString, but it doesn't work. Logging a plain object or class that implements toString will still output the object structure when you log it, so that won't work.

2 parts of the spec are key to solve this:

(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new)

When a function is called with the new keyword, the function will be used as a constructor. new will do the following things:

...

  1. If the constructor function returns a non-primitive, this return value becomes the result of the whole new expression. Otherwise, if the constructor function doesn't return anything or returns a primitive, newInstance is returned instead.

You have little control of how a user defined class will be logged. But maybe there is a built in object that will log text, so we need to figure out what that might be, and then return it in the constructor. What built in objects log text?

(https://console.spec.whatwg.org/)

§ 1.1.6. log(...data)

Perform Logger("log", data).

§ 2.1. Logger(logLevel, args)

The logger operation accepts a log level and a list of other arguments. Its main output is the implementation-defined side effect of printing the result to the console.

§ 2.3. Printer(logLevel, args[, options])

The printer operation is implementation-defined. It accepts a log level indicating severity, a List of arguments to print...Elements appearing in args will be one of the following:

  • JavaScript objects of any type.
  • Implementation-specific representations of printable things such as a stack trace or group.
  • Objects with either generic JavaScript object formatting or optimally useful formatting applied.

§ 2.3.3. Common object formats

Typically objects will be printed in a format that is suitable for their context.... An object with generic JavaScript object formatting is a potentially expandable representation of a generic JavaScript object. An object with optimally useful formatting is an implementation-specific, potentially-interactive representation of an object judged to be maximally useful and informative.

^^ Paydirt. We need to find an object that has an "optimally useful formatting" which is not the object tree. I couldn't find an exhaustive list, but lets think about it. I know offhand that logging a dom node logs the html tree instead of the object tree:

console.log(document.body);

So I took a leap and guessed that logging a text node might just log the text, and it worked:

console.log text node

and there it is

function X() {
   return document.createTextNode("Hello");
}
console.log(new X()); //"Hello"

class X {
    constructor() {
        return document.createTextNode("Hello");
    }
}
console.log(new X()); // "Hello"

see also:

0
Mike 'Pomax' Kamermans On

This is "technically" impossible to do, as console.log is implementation-defined, and even something clever that works in -say- Chrome won't work in Firefox, or any other browser you haven't tested it in yet, or haven't even heard of yet.

But we're not just given X, we're also given console.log itself. And console.log is implementation-defined. And we were just asked to implement something.

So let's be spec-compliant and just define a new implementation for the console.log function.

If someone really needs to do this (which they don't), the way to actually make this happen is by exploiting the fact that we're in JavaScript and we can just change how console.log works so that in the specific case given, and only the specific case given, it does what was asked. Let's make this red pen write blue:

const originalLog = console.log.bind(console);

console.log = function log(...args) {
  if (args.length === 1) {
    if (args[0].__proto__.constructor.name === `X`) {
      // Just replace the value. How you do that replacement
      // is of course entirely up to you, the "dumbest" way
      // is to just hardcode a string, but you have plenty
      // of options here based on X itself, or the JS engine,
      // or the user agent, or the time of day, etc. etc.
      args[0] = "hello";
    }    
  }
  originalLog(...args);
};

class X {}

console.log(new X());

Is it in the spirit of the question? We have no idea, there are nowhere near enough details in the question for us to actually start implementing anything (which was probably the point of the question: to see if someone would try to sus out an exhaustive list of acceptance criteria before getting to work).

But does it work in any JS engine you run this in? You bet.

And more importantly, would you ever actually need this? To which the answer, perhaps surprisingly, is "yes, absolutely. You're probably using code that does this on the daily already, just with a much better use-case: your test framework".

For testing and debugging it's pretty common to "wrap" functions in redeclarations, and have Proxy wrappers for objects, so that we can insert our own code into existing code paths for testing and debugging in a way that won't affect the code in production.

3
Peter Seliger On

Two steps ...

  • use an immediately invoked (arrow) function expression for wrapping a behavior around the original console.log which does stringify the first of the passed parameters.

  • implement a class with a prototypal toString method.

    (for a plain constructor function implement its own toString.)

// - use an immediately invoked (arrow) function expression
//   for wrapping a behavior around the original `console.log`
//   which does stringify the first of the passed parameters.
console.log = (log => {
  return function logger (value, ...args) {

    log(String(value), ...args);      
  };
})(console.log.bind(console));

// - implement a class with a prototypal `toString` method.
class X {
  toString() {
    return 'Hello.';
  }
}
// - for a plain constructor function implement its own `toString`.
function Y() {
  this.toString = () => 'Goodbye.';
}

console.log(new X);
console.log(new Y);

console.log(new Y, new X);
.as-console-wrapper { min-height: 100%!important; top: 0; }