-
-
Save dsherret/cbe661faf7e3cfad8397 to your computer and use it in GitHub Desktop.
/** | |
* Caches the return value of get accessors and methods. | |
* | |
* Notes: | |
* - Doesn't really make sense to put this on a method with parameters. | |
* - Creates an obscure non-enumerable property on the instance to store the memoized value. | |
* - Could use a WeakMap, but this way has support in old environments. | |
*/ | |
export function Memoize(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<any>) { | |
if (descriptor.value != null) { | |
descriptor.value = getNewFunction(descriptor.value); | |
} | |
else if (descriptor.get != null) { | |
descriptor.get = getNewFunction(descriptor.get); | |
} | |
else { | |
throw "Only put a Memoize decorator on a method or get accessor."; | |
} | |
} | |
let counter = 0; | |
function getNewFunction(originalFunction: () => void) { | |
const identifier = ++counter; | |
return function (this: any, ...args: any[]) { | |
const propName = `__memoized_value_${identifier}`; | |
let returnedValue: any; | |
if (this.hasOwnProperty(propName)) { | |
returnedValue = this[propName]; | |
} | |
else { | |
returnedValue = originalFunction.apply(this, args); | |
Object.defineProperty(this, propName, { | |
configurable: false, | |
enumerable: false, | |
writable: false, | |
value: returnedValue | |
}); | |
} | |
return returnedValue; | |
}; | |
} | |
// ------------ ES6 VERSION ---------- | |
export function Memoize(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<any>) { | |
if (descriptor.value != null) | |
descriptor.value = getNewFunction(descriptor.value); | |
else if (descriptor.get != null) | |
descriptor.get = getNewFunction(descriptor.get); | |
else | |
throw new Error("Only put a Memoize decorator on a method or get accessor."); | |
} | |
const weakMap = new WeakMap<object, Map<string, unknown>>(); | |
let counter = 0; | |
function getNewFunction(originalFunction: (...args: any[]) => void) { | |
const identifier = counter++; | |
function decorator(this: any, ...args: any[]) { | |
let propertyValues = weakMap.get(this); | |
if (propertyValues == null) { | |
propertyValues = new Map<string, unknown>(); | |
weakMap.set(this, propertyValues); | |
} | |
let propName = `__memoized_value_${identifier}`; | |
if (arguments.length > 0) | |
propName += "_" + JSON.stringify(args); | |
let returnedValue: any; | |
if (propertyValues.has(propName)) | |
returnedValue = propertyValues.get(propName); | |
else { | |
returnedValue = originalFunction.apply(this, args); | |
propertyValues.set(propName, returnedValue); | |
} | |
return returnedValue; | |
} | |
return decorator; | |
} |
Great example, however, I think you're memoizing values across all instances of a class rather than per instance. For example, given the following two instances:
const c1 = new MyClass();
const c2 = new MyClass();
Calling getNumber()
should memoize the returned value once for c1
and once for c2
. Currently, because TypeScript only applies the decorator once, the decoration is shared across all instances. This would obviously be the expected behaviour if getNumber()
were static.
@timjroberts Hi Tim, you are completely right and sorry about that! It did share the same value across multiple instances. I wish gist had given me a notification for your message, because I didn't noticed this mistake until I went to go use this in something and it broke the tests.
I went ahead and updated this so that problem is fixed. Additionally, I've updated the tests in this post to cover that situation.
I am looking a method to find "any assigned decorator to properties within a class", for example:
class MyClass {
@Memoize
getNumber() {
return Math.random();
}
@Memoize @rand
get value() {
return Math.random();
}
}
I want to get @memoize for getNumber:
let result = Reflect.GetDecorations(MyClass);
console.log(result);
// { value: ['Memoize' , 'rand'] , getNumber: ['Memoize'] }
Is there any possible solution?
You could use a WeakMap to avoid storing the magical property 😄
Thank you for this example!
I have created similar memoize decorator that can be expired after some time.
Just wondering, how is this different from a static property
@SteveStrong in certain cases where you want to use the prototype this is useful.
type hashCode = "hashCode";
/**
* Indicates that this hashcode function should be memoized (calculated only once)
* ***This should only be used on READONLY composite data types for predictable results***
*/
function memoized(target: ValueType, propertyKey: hashCode, descriptor: TypedPropertyDescriptor<() => number>) {
const original = descriptor.get ? descriptor.get() : descriptor.value;
Object.defineProperty(target.constructor.prototype, 'hashCode', {
value: function hashCode() {
if (!this.__hashCode__) {
this.__hashCode__ = original.call(this);
}
return this.__hashCode__;
}
});
descriptor.value = target.constructor.prototype.hashCode;
return descriptor;
}
I found this to work - no WeakMap needed.
This might not work as well if you want to dynamically memoize things, but I am using it to memoize hashcode computations for immutable ValueTypes with Immutable.js
Edit: I am not sure how well this would work with sub-classes overriding a memoized function who also decorates the overridden function to memoize the calculation, it might loop infinitely?
Here is a WeakMap based version of this decorator, that respects single argument (by a reference for non-primitives)
Example: