0

I tried to proxy Function.prototype.

I created a new Proxy and rewrote the toString method, however console.log('cas') doesn't work, why?

Function.prototype = new Proxy(Function.prototype, {
  get: function(target, property, receiver) {},
  set: function(target, property, value, receiver) {
    console.log('cas')
  }
})
Function.prototype._toString = Function.prototype.toString;
Function.prototype.toString = function() {
  console.log('bb')
  return this._toString()
}
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
yg lin
  • 115
  • 9

2 Answers2

3

I created a new Proxy and rewrote the toString method, however console.log('cas') doesn't work, why?

The answer is simple: Function.prototype is not writable.

console.config({ maximize: true });
console.log(Object.getOwnPropertyDescriptor(Function, 'prototype'));
<script src="https://gh-canon.github.io/stack-snippet-console/console.min.js"></script>

As for why JS didn't throw an error... It's JS. What do you expect?

On the other hand, you can use 'use strict' to make these kinds of error explicit (thanks @Keith).

To prevent Function#toString() from being overwritten, you can re-define it:

Object.defineProperty(Function.prototype, 'toString', {
  value: Function.prototype.toString,
  writable: false,
  configurable: false
});

Try it:

console.config({ maximize: true });

function f() {}

console.log(f.toString()); // function f() {}

Object.defineProperty(Function.prototype, 'toString', {
  value: Function.prototype.toString,
  writable: false,
  configurable: false
});

Function.prototype._toString = Function.prototype.toString;
Function.prototype.toString = function() {
  console.log('overwritten');
  return Function.prototype._toString.call(this);
}

console.log(Object.hasOwn(Function.prototype, '_toString')); // true
console.log(Object.hasOwn(Function.prototype, 'toString')); // Also true
console.log(f.toString()); // function f() {}, not tainted.
<script src="https://gh-canon.github.io/stack-snippet-console/console.min.js"></script>
InSync
  • 4,851
  • 4
  • 8
  • 30
-1

Ok, we cannot overwrite Function.prototype but we can set the prototype of Function.prototype (an evil professor's laugh here):

const props = Object.getOwnPropertyDescriptors(Function.prototype);

const masked = {};

for (const [name, value] of Object.entries(props)) {
    Object.defineProperty(masked, name, value);
}

const prototype = new Proxy({}, {
    get: function (target, property, receiver) {
        // avoid recursion and stack overflow
        if(!['call', 'apply'].includes(property)){
          console.log(`getting ${property}`);
        }
        return masked[property];
    },
    set: function (target, property, value, receiver) {
        console.log(`setting ${property}`);
        masked[property] = value;
    }
});

// clear Function.prototype and assign our proxy prototype to it

for (const k in props) {
    delete Function.prototype[k];
}

Object.setPrototypeOf(Function.prototype, prototype);

Function.prototype._toString = Function.prototype.toString;
Function.prototype.toString = function () {
    console.log("I'm new toString()");
    return this._toString();
};

const test = () => false;

console.log(test.toString());

UPDATE
The author noticed that the solution doesn't work in node.js.
The reason that console.log is environment specific plus overriding Function.prototype means that if you call a function that calls apply, call, bind you go into recursion and stack overflow (RangeError: Maximum call stack size exceeded).
So the code should be adjusted to your specific execution environment. On my node.js version 18 these tweaks helped. Moreover I had to override console.log since the output was empty. Again, environment-specific (seems another investigation...). During debugging console.log worked but required to avoid getting listener property.
So my conclusion: it works if you care about recursion with specific methods on your platform AND I will be working on an environment-independent solution, the plan is to register function calls and avoid recursion by checking whether there's already a function call that leads to the stack overflow.

const props = Object.getOwnPropertyDescriptors(Function.prototype);

const masked = {};

for (const [name, value] of Object.entries(props)) {
    Object.defineProperty(masked, name, value);
}

const log = msg => process.stdout.write(msg + '\n');

const prototype = new Proxy({}, {
    get: function (target, property, receiver) {
        // avoid recursion and stack overflow
        if (!['call', 'apply', 'bind'].includes(property)) {
            log(`getting ${property}`);
        }
        return masked[property];
    },
    set: function (target, property, value, receiver) {
        log(`setting ${property}`);
        masked[property] = value;
    }
});

// clear Function.prototype and assign our proxy prototype to it

for (const k in props) {
    delete Function.prototype[k];
}

Object.setPrototypeOf(Function.prototype, prototype);

Function.prototype._toString = Function.prototype.toString;
Function.prototype.toString = function () {
    log('I\'m new toString()');
    return this._toString();
};

const test = () => false;

log(test.toString());
enter image description here
Alexander Nenashev
  • 8,775
  • 2
  • 6
  • 17
  • The code is work in chrome,it is a very good idea. However, in nodejs, the code raise an error . The version of nodejs is 18.12.1 node:tty:85 if (!(this instanceof WriteStream)) ^ RangeError: Maximum call stack size exceeded – yg lin May 31 '23 at 13:46
  • @yglin updated the answer, pls check it. also i'll be working on a universal solution. by the way if you find my idea very good i'd ask you to upvote my answer (the arrow up), i've put a lot of effort into it. also if you find my answer working for you you could consider to accept my answer as the resolution (the checkbox). thanks – Alexander Nenashev May 31 '23 at 16:42