Decorator is a special function that takes another function and alters its behavior.
Benefits
Let’s say we have a function slow(x) which is CPU-heavy, but its results are stable. In other words, for the same x it always returns the same result.
If the function is called often, we may want to cache (remember) the results to avoid spending extra-time on recalculations.
But instead of adding that functionality into slow() we’ll create a wrapper function, that adds caching.
function slow(x) {
// there can be a heavy CPU-intensive job here
alert(`Called with ${x}`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) { // if there's such key in cache
return cache.get(x); // read the result from it
}
let result = func(x); // otherwise call func
cache.set(x, result); // and cache (remember) the result
return result;
};
}
slow = cachingDecorator(slow);
alert( slow(1) ); // slow(1) is cached
alert( "Again: " + slow(1) ); // the same
alert( slow(2) ); // slow(2) is cached
alert( "Again: " + slow(2) ); // the same as the previous line
cachingDecorator
is a decorator.The caching decorator mentioned above is not work with object methods. Because this = undefined
.
We can fix it using "func.call".
func.call(context, arg1, arg2, ...)
func.call
allows to call a function explicitly setting this
.this
, and the next as the arguments.function sayHi() {
alert(this.name);
}
let user = { name: "John" };
let admin = { name: "Admin" };
// use call to pass different objects as "this"
sayHi.call( user ); // John
sayHi.call( admin ); // Admin
Now we can use decorator.
let worker = {
someMethod() {
return 1;
},
slow(x) {
alert("Called with " + x);
return x * this.someMethod(); // (*)
}
};
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func.call(this, x); // "this" is passed correctly now
cache.set(x, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow); // now make it caching
alert( worker.slow(2) ); // works
alert( worker.slow(2) ); // works, doesn't call the original (cached)
func.apply
vs func.call
The only syntax difference between call
and apply
is that call
expects a list of arguments, while apply
takes an array-like object with them.
So, where we expect an iterable, call works, and where we expect an array-like, apply works.
Passing all arguments along with the context to another function is called call forwarding.
let wrapper = function() {
return func.apply(this, arguments);
};
// Error
function hash() {
alert( arguments.join() );
}
// Working
function hash() {
alert( [].join.call(arguments) ); // 1,2
}
arguments
object is both iterable and array-like, but not a real array. Array-like can't use the array methods(join, slice...).let worker = {
slow(min, max) {
alert(`Called with ${min},${max}`);
return min + max;
}
};
function cachingDecorator(func, hash) {
let cache = new Map();
return function() {
let key = hash(arguments); // (*)
if (cache.has(key)) {
return cache.get(key);
}
let result = func.call(this, ...arguments); // (**)
cache.set(key, result);
return result;
};
}
function hash() {
return [].join.call(arguments);
}
worker.slow = cachingDecorator(worker.slow, hash);
alert( worker.slow(3, 5) ); // works
alert( "Again " + worker.slow(3, 5) ); // same (cached)