Implement a memoize function that caches the results of function calls to avoid unnecessary re-computation.
A standard implementation often relies on simple serialization, but you must ensure it does not break down with special JavaScript types or context binding. Your implementation must respect the original function's contract completely.
1. Basic Functionality
Accept a function fn and return a memoized version of it.
Cache function results based on the provided arguments.
Call the original function if arguments are unseen, and return the cached result if they match.
Support functions with multiple arguments.
2. Context & Edge Cases
Preserve Context: The returned function must execute with the correct this context.
Serialization Pitfalls: The cache must correctly differentiate between values that typically collide during basic stringification (e.g., undefined vs null, NaN, Infinity).
Complex Types: Ensure objects and arrays are correctly cached based on their structured values.