Search code examples
javascriptrecursionfunction-constructor

Range error when overriding Object.prototype method with Function constructor


I'm trying to override Object.prototype.toString in a bid to add functionality for additional class descriptions.

Here's the initial code:

(function(toString){
    Object.prototype.toString = function(){
        if(this instanceof TestClass)
        {
            return '[object TestClass]';
        }
        return toString.apply(this, arguments);
    }
})(Object.prototype.toString);

function TestClass(){}
var instance_obj = new TestClass();
Object.prototype.toString.call(instance_obj);

When I run this in the console, I get the following output:

[object TestClass]

The good thing is that it doesn't drastically modify the way Object.prototype.toString works, so with another type [i.e. not TestClass], things work just as expected e.g. Object.prototype.toString.call(12) will output [object Number].

This implementation works with no issues so far. However, I have another implementation with the following code:

(function(toString){
    var fn_code_str = `return function(){
        if(this instanceof TestClass)
        {
            return '[object TestClass]';
        }
            
        return toString.apply(this, arguments);
    }`;
    var pre_fn = new Function(fn_code_str);
    Object.prototype.toString = pre_fn();
})(Object.prototype.toString);

function TestClass(){}
var instance_obj = new TestClass();
Object.prototype.toString.call(instance_obj);

With this, I get the proper output for TestClass, but when I use something else, like 12, I get a RangeError:

VM527:5 Uncaught RangeError: Maximum call stack size exceeded
    at Function.[Symbol.hasInstance] (<anonymous>)
    at Number.eval (eval at <anonymous> (getElements.html:19), <anonymous>:5:21)
    at Number.eval (eval at <anonymous> (getElements.html:19), <anonymous>:10:29)
    at Number.eval (eval at <anonymous> (getElements.html:19), <anonymous>:10:29)
    at Number.eval (eval at <anonymous> (getElements.html:19), <anonymous>:10:29)
    at Number.eval (eval at <anonymous> (getElements.html:19), <anonymous>:10:29)
    at Number.eval (eval at <anonymous> (getElements.html:19), <anonymous>:10:29)
    at Number.eval (eval at <anonymous> (getElements.html:19), <anonymous>:10:29)
    at Number.eval (eval at <anonymous> (getElements.html:19), <anonymous>:10:29)
    at Number.eval (eval at <anonymous> (getElements.html:19), <anonymous>:10:29)

This appears to be an issue with recursion of toString.apply. However, I can't figure out why this second implementation is recursing, if the first one does not?

Note: The reason for this second implementation is to add the type-checking code [i.e. if(this instanceof MyClassType){return '[object MyClassType]'}] for different classes dynamically from a list of class names in an array. In other words, rather than modifying code for each new Class I come up with, I append the class name to the array instead, and the conditional statement is generated automatically.


Solution

  • The problem is that the toString parameter of your IIFE is not in scope in your new Function code. Instead, it uses the global toString = window.toString = Object.prototype.toString.

    To fix this, you need to declare the toString variable inside the new Function's code to make the returned closure work. Either as a simple constant:

    (function() {
        var pre_fn = new Function(`
        const toString = Object.prototype.toString;
    //  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        return function(){
            if(this instanceof TestClass) {
                return '[object TestClass]';
            }
                
            return toString.apply(this, arguments);
        }`);
        Object.prototype.toString = pre_fn();
    })();
    

    or as a parameter:

    (function() {
        var pre_fn = new Function('toString', `
    //                            ^^^^^^^^^^^
        return function(){
            if(this instanceof TestClass) {
                return '[object TestClass]';
            }
                
            return toString.apply(this, arguments);
        }`);
        Object.prototype.toString = pre_fn(Object.prototype.toString);
    //                                     ^^^^^^^^^^^^^^^^^^^^^^^^^
    })();