Erro de intervalo ao substituir o método Object.prototype com o construtor Function

Nov 30 2020

Estou tentando substituir Object.prototype.toStringem um lance para adicionar funcionalidade para descrições de classes adicionais.

Aqui está o código inicial:

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

Quando eu executo isso no console, obtenho a seguinte saída:

[object TestClass]

O bom é que ele não modifica drasticamente a maneira como Object.prototype.toStringfunciona, então com outro tipo [ou seja, não TestClass], as coisas funcionam exatamente como o esperado, por exemplo Object.prototype.toString.call(12), a saída [object Number].

Esta implementação funciona sem problemas até agora. No entanto, tenho outra implementação com o seguinte código:

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

Com isso, obtenho a saída adequada para TestClass, mas quando uso outra coisa, como 12, obtenho um 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)

Este parece ser um problema com a recursão de toString.apply. No entanto, não consigo descobrir por que essa segunda implementação é recorrente, se a primeira não?

Nota : A razão para esta segunda implementação é adicionar o código de verificação de tipo [isto é if(this instanceof MyClassType){return '[object MyClassType]'}] para diferentes classes dinamicamente de uma lista de nomes de classes em um array. Em outras palavras, em vez de modificar o código para cada nova classe que proponho, acrescento o nome da classe ao array e a instrução condicional é gerada automaticamente.

Respostas

3 Bergi Nov 30 2020 at 15:42

O problema é que o toStringparâmetro de seu IIFE não está no escopo de seu new Functioncódigo. Em vez disso, ele usa o global toString= window.toString= Object.prototype.toString.

Para corrigir isso, você precisa declarar a toStringvariável dentro do new Functioncódigo de para fazer o fechamento retornado funcionar. Como uma constante simples:

(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();
})();

ou como parâmetro:

(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);
//                                     ^^^^^^^^^^^^^^^^^^^^^^^^^
})();