Ошибка диапазона при переопределении метода Object.prototype с помощью конструктора функции

Nov 30 2020

Я пытаюсь переопределить Object.prototype.toStringзаявку на добавление функций для дополнительных описаний классов.

Вот исходный код:

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

Когда я запускаю это в консоли, я получаю следующий результат:

[object TestClass]

Хорошо то, что он Object.prototype.toStringне меняет радикально способ работы, поэтому с другим типом [т.е. не TestClass] все работает так, как ожидалось, например, Object.prototype.toString.call(12)будет вывод [object Number].

Эта реализация пока работает без проблем. Однако у меня есть другая реализация со следующим кодом:

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

Благодаря этому я получаю правильный результат для TestClass, но когда я использую что-то еще, например 12, я получаю 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)

Похоже, это проблема с рекурсией toString.apply. Однако я не могу понять, почему эта вторая реализация рекурсивна, а первая - нет?

Примечание . Причина этой второй реализации - if(this instanceof MyClassType){return '[object MyClassType]'}динамическое добавление кода проверки типов [т.е. ] для различных классов из списка имен классов в массиве. Другими словами, вместо того, чтобы изменять код для каждого нового класса, который я придумываю, я вместо этого добавляю имя класса в массив, и условный оператор генерируется автоматически.

Ответы

3 Bergi Nov 30 2020 at 15:42

Проблема в том, что toStringпараметр вашего IIFE не входит в область действия вашего new Functionкода. Вместо этого он использует global toString= window.toString= Object.prototype.toString.

Чтобы исправить это, вам нужно объявить toStringпеременную внутри new Functionкода, чтобы возвращаемое замыкание работало. Либо как простая константа:

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

или как параметр:

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