up
This commit is contained in:
parent
ab9ab64bd5
commit
97c8f22bbb
289 changed files with 195 additions and 172 deletions
|
@ -0,0 +1,11 @@
|
|||
function spy(func) {
|
||||
|
||||
function wrapper(...args) {
|
||||
wrapper.calls.push(args);
|
||||
return func.apply(this, arguments);
|
||||
}
|
||||
|
||||
wrapper.calls = [];
|
||||
|
||||
return wrapper;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
function spy(func) {
|
||||
// your code
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
describe("spy", function() {
|
||||
it("records calls into its property", function() {
|
||||
function work() {}
|
||||
|
||||
work = spy(work);
|
||||
assert.deepEqual(work.calls, []);
|
||||
|
||||
work(1, 2);
|
||||
assert.deepEqual(work.calls, [
|
||||
[1, 2]
|
||||
]);
|
||||
|
||||
work(3, 4);
|
||||
assert.deepEqual(work.calls, [
|
||||
[1, 2],
|
||||
[3, 4]
|
||||
]);
|
||||
});
|
||||
|
||||
it("transparently wraps functions", function() {
|
||||
|
||||
let sum = sinon.spy((a, b) => a + b);
|
||||
|
||||
let wrappedSum = spy(sum);
|
||||
|
||||
assert.equal(wrappedSum(1, 2), 3);
|
||||
assert(spy.calledWith(1, 2));
|
||||
});
|
||||
|
||||
|
||||
it("transparently wraps methods", function() {
|
||||
|
||||
let calc = {
|
||||
sum: sinon.spy((a, b) => a + b)
|
||||
};
|
||||
|
||||
calc.wrappedSum = spy(calc.sum);
|
||||
|
||||
assert.equal(calculator.wrappedSum(1, 2), 3);
|
||||
assert(spy.calledWith(1, 2));
|
||||
assert(spy.calledOn(calculator));
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
Here we can use `log.push(args)` to store all arguments in the log and `f.apply(this, args)` to forward the call.
|
|
@ -0,0 +1,30 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Spy decorator
|
||||
|
||||
Create a decorator `spy(func)` that should return a wrapper that saves all calls to function in its `calls` property.
|
||||
|
||||
Every call is saved as an array of arguments.
|
||||
|
||||
For instance:
|
||||
|
||||
```js
|
||||
function work(a, b) {
|
||||
alert( a + b ); // work is an arbitrary function or method
|
||||
}
|
||||
|
||||
*!*
|
||||
work = spy(work);
|
||||
*/!*
|
||||
|
||||
work(1, 2); // 3
|
||||
work(4, 5); // 9
|
||||
|
||||
for(let args of work.calls) {
|
||||
alert( 'call:' + args.join() ); // "call:1,2", "call:4,5"
|
||||
}
|
||||
```
|
||||
|
||||
P.S. That decorator is sometimes useful for unit-testing, it's advanced form is `sinon.spy` in [Sinon.JS](http://sinonjs.org/) library.
|
|
@ -0,0 +1,7 @@
|
|||
function delay(f, ms) {
|
||||
|
||||
return function() {
|
||||
setTimeout(() => f.apply(this, arguments), ms);
|
||||
};
|
||||
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
describe("delay", function() {
|
||||
before(function() {
|
||||
this.clock = sinon.useFakeTimers();
|
||||
});
|
||||
|
||||
after(function() {
|
||||
this.clock.restore();
|
||||
});
|
||||
|
||||
it("calls the function after the specified timeout", function() {
|
||||
let start = Date.now();
|
||||
|
||||
function f(x) {
|
||||
assert.equal(Date.now() - start, 1000);
|
||||
}
|
||||
f = sinon.spy(f);
|
||||
|
||||
let f1000 = delay(f, 1000);
|
||||
f1000("test");
|
||||
this.clock.tick(2000);
|
||||
assert(f.calledOnce, 'calledOnce check fails');
|
||||
});
|
||||
|
||||
it("passes arguments and this", function() {
|
||||
let start = Date.now();
|
||||
let user = {
|
||||
sayHi: function(phrase, who) {
|
||||
assert.equal(this, user);
|
||||
assert.equal(phrase, "Hello");
|
||||
assert.equal(who, "John");
|
||||
assert.equal(Date.now() - start, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
user.sayHi = sinon.spy(user.sayHi);
|
||||
|
||||
let spy = user.sayHi;
|
||||
user.sayHi = delay(user.sayHi, 1500);
|
||||
|
||||
user.sayHi("Hello", "John");
|
||||
|
||||
this.clock.tick(2000);
|
||||
|
||||
assert(spy.calledOnce, 'calledOnce check failed');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
The solution:
|
||||
|
||||
```js
|
||||
function delay(f, ms) {
|
||||
|
||||
return function() {
|
||||
setTimeout(() => f.apply(this, arguments), ms);
|
||||
};
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Please note how an arrow function is used here. As we know, arrow functions do not have own `this` and `arguments`, so `f.apply(this, arguments)` takes `this` and `arguments` from the wrapper.
|
||||
|
||||
If we pass a regular function, `setTimeout` would call it without arguments and `this=window` (in-browser), so we'd need to write a bit more code to pass them from the wrapper:
|
||||
|
||||
```js
|
||||
function delay(f, ms) {
|
||||
|
||||
// added variables to pass this and arguments from the wrapper inside setTimeout
|
||||
return function(...args) {
|
||||
let savedThis = this;
|
||||
setTimeout(function() {
|
||||
f.apply(savedThis, args);
|
||||
}, ms);
|
||||
};
|
||||
|
||||
}
|
||||
```
|
|
@ -0,0 +1,26 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Delaying decorator
|
||||
|
||||
Create a decorator `delay(f, ms)` that delays each call of `f` by `ms` milliseconds.
|
||||
|
||||
For instance:
|
||||
|
||||
```js
|
||||
function f(x) {
|
||||
alert(x);
|
||||
}
|
||||
|
||||
// create wrappers
|
||||
let f1000 = delay(f, 1000);
|
||||
let f1500 = delay(f, 1500);
|
||||
|
||||
f1000("test"); // shows "test" after 1000ms
|
||||
f1500("test"); // shows "test" after 1500ms
|
||||
```
|
||||
|
||||
In other words, `delay(f, ms)` returns a "delayed by `ms`" variant of `f`.
|
||||
|
||||
In the code above, `f` is a function of a single argument, but your solution should pass all arguments and the context `this`.
|
|
@ -0,0 +1,15 @@
|
|||
function debounce(f, ms) {
|
||||
|
||||
let isCooldown = false;
|
||||
|
||||
return function() {
|
||||
if (isCooldown) return;
|
||||
|
||||
f.apply(this, arguments);
|
||||
|
||||
isCooldown = true;
|
||||
|
||||
setTimeout(() => isCooldown = false, ms);
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
describe("debounce", function() {
|
||||
before(function() {
|
||||
this.clock = sinon.useFakeTimers();
|
||||
});
|
||||
|
||||
after(function() {
|
||||
this.clock.restore();
|
||||
});
|
||||
|
||||
it("calls the function at maximum once in ms milliseconds", function() {
|
||||
let log = '';
|
||||
|
||||
function f(a) {
|
||||
log += a;
|
||||
}
|
||||
|
||||
f = debounce(f, 1000);
|
||||
|
||||
f(1); // runs at once
|
||||
f(2); // ignored
|
||||
|
||||
setTimeout(() => f(3), 100); // ignored (too early)
|
||||
setTimeout(() => f(4), 1100); // runs (1000 ms passed)
|
||||
setTimeout(() => f(5), 1500); // ignored (less than 1000 ms from the last run)
|
||||
|
||||
this.clock.tick(5000);
|
||||
assert.equal(log, "14");
|
||||
});
|
||||
|
||||
it("keeps the context of the call", function() {
|
||||
let obj = {
|
||||
f() {
|
||||
assert.equal(this, obj);
|
||||
}
|
||||
};
|
||||
|
||||
obj.f = debounce(obj.f, 1000);
|
||||
obj.f("test");
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
|
||||
|
||||
```js run no-beautify
|
||||
function debounce(f, ms) {
|
||||
|
||||
let isCooldown = false;
|
||||
|
||||
return function() {
|
||||
if (isCooldown) return;
|
||||
|
||||
f.apply(this, arguments);
|
||||
|
||||
isCooldown = true;
|
||||
|
||||
setTimeout(() => isCooldown = false, ms);
|
||||
};
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
The call to `debounce` returns a wrapper. There may be two states:
|
||||
|
||||
- `isCooldown = false` -- ready to run.
|
||||
- `isCooldown = true` -- waiting for the timeout.
|
||||
|
||||
In the first call `isCooldown` is falsy, so the call proceeds, and the state changes to `true`.
|
||||
|
||||
While `isCooldown` is true, all other calls are ignored.
|
||||
|
||||
Then `setTimeout` reverts it to `false` after the given delay.
|
|
@ -0,0 +1,24 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Debounce decorator
|
||||
|
||||
The result of `debounce(f, ms)` decorator should be a wrapper that passes the call to `f` at maximum once per `ms` milliseconds.
|
||||
|
||||
In other words, when we call a "debounced" function, it guarantees that all other future in the closest `ms` milliseconds will be ignored.
|
||||
|
||||
For instance:
|
||||
|
||||
```js no-beautify
|
||||
let f = debounce(alert, 1000);
|
||||
|
||||
f(1); // runs immediately
|
||||
f(2); // ignored
|
||||
|
||||
setTimeout( () => f(3), 100); // ignored ( only 100 ms passed )
|
||||
setTimeout( () => f(4), 1100); // runs
|
||||
setTimeout( () => f(5), 1500); // ignored (less than 1000 ms from the last run)
|
||||
```
|
||||
|
||||
In practice `debounce` is useful for functions that retrieve/update something when we know that nothing new can be done in such a short period of time, so it's better not to waste resources.
|
|
@ -0,0 +1,34 @@
|
|||
function throttle(func, ms) {
|
||||
|
||||
let isThrottled = false,
|
||||
savedArgs,
|
||||
savedThis;
|
||||
|
||||
function wrapper() {
|
||||
|
||||
if (isThrottled) {
|
||||
// memo last arguments to call after the cooldown
|
||||
savedArgs = arguments;
|
||||
savedThis = this;
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise go to cooldown state
|
||||
func.apply(this, arguments);
|
||||
|
||||
isThrottled = true;
|
||||
|
||||
// plan to reset isThrottled after the delay
|
||||
setTimeout(function() {
|
||||
isThrottled = false;
|
||||
if (savedArgs) {
|
||||
// if there were calls, savedThis/savedArgs have the last one
|
||||
// recursive call runs the function and sets cooldown again
|
||||
wrapper.apply(savedThis, savedArgs);
|
||||
savedArgs = savedThis = null;
|
||||
}
|
||||
}, ms);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
describe("throttle(f, 1000)", function() {
|
||||
let f1000;
|
||||
let log = "";
|
||||
|
||||
function f(a) {
|
||||
log += a;
|
||||
}
|
||||
|
||||
before(function() {
|
||||
f1000 = throttle(f, 1000);
|
||||
this.clock = sinon.useFakeTimers();
|
||||
});
|
||||
|
||||
it("the first call runs now", function() {
|
||||
f1000(1); // runs now
|
||||
assert.equal(log, "1");
|
||||
});
|
||||
|
||||
it("then calls are ignored till 1000ms when the last call works", function() {
|
||||
f1000(2); // (throttling - less than 1000ms since the last run)
|
||||
f1000(3); // (throttling - less than 1000ms since the last run)
|
||||
// after 1000 ms f(3) call is scheduled
|
||||
|
||||
assert.equal(log, "1"); // right now only the 1st call done
|
||||
|
||||
this.clock.tick(1000); // after 1000ms...
|
||||
assert.equal(log, "13"); // log==13, the call to f1000(3) is made
|
||||
});
|
||||
|
||||
it("the third call waits 1000ms after the second call", function() {
|
||||
this.clock.tick(100);
|
||||
f1000(4); // (throttling - less than 1000ms since the last run)
|
||||
this.clock.tick(100);
|
||||
f1000(5); // (throttling - less than 1000ms since the last run)
|
||||
this.clock.tick(700);
|
||||
f1000(6); // (throttling - less than 1000ms since the last run)
|
||||
|
||||
this.clock.tick(100); // now 100 + 100 + 700 + 100 = 1000ms passed
|
||||
|
||||
assert.equal(log, "136"); // the last call was f(6)
|
||||
});
|
||||
|
||||
after(function() {
|
||||
this.clock.restore();
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
```js
|
||||
function throttle(func, ms) {
|
||||
|
||||
let isThrottled = false,
|
||||
savedArgs,
|
||||
savedThis;
|
||||
|
||||
function wrapper() {
|
||||
|
||||
if (isThrottled) { // (2)
|
||||
savedArgs = arguments;
|
||||
savedThis = this;
|
||||
return;
|
||||
}
|
||||
|
||||
func.apply(this, arguments); // (1)
|
||||
|
||||
isThrottled = true;
|
||||
|
||||
setTimeout(function() {
|
||||
isThrottled = false; // (3)
|
||||
if (savedArgs) {
|
||||
wrapper.apply(savedThis, savedArgs);
|
||||
savedArgs = savedThis = null;
|
||||
}
|
||||
}, ms);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
```
|
||||
|
||||
A call to `throttle(func, ms)` returns `wrapper`.
|
||||
|
||||
1. During the first call, the `wrapper` just runs `func` and sets the cooldown state (`isThrottled = true`).
|
||||
2. In this state all calls memorized in `savedArgs/savedThis`. Please note that both the context and the arguments are equally important and should be memorized. We need them simultaneously to reproduce the call.
|
||||
3. ...Then after `ms` milliseconds pass, `setTimeout` triggers. The cooldown state is removed (`isThrottled = false`). And if we had ignored calls, then `wrapper` is executed with last memorized arguments and context.
|
||||
|
||||
The 3rd step runs not `func`, but `wrapper`, because we not only need to execute `func`, but once again enter the cooldown state and setup the timeout to reset it.
|
|
@ -0,0 +1,48 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Throttle decorator
|
||||
|
||||
Create a "throttling" decorator `throttle(f, ms)` -- that returns a wrapper, passing the call to `f` at maximum once per `ms` milliseconds. Those calls that fall into the "cooldown" period, are ignored.
|
||||
|
||||
**The differnce from `debounce` -- if an ignored call is the last during the cooldown, then it executes at the end of the delay.**
|
||||
|
||||
Let's check the real-life application to better understand that requirement and to see where it comes from.
|
||||
|
||||
**For instance, we want to track mouse movements.**
|
||||
|
||||
In browser we can setup a function to run at every mouse micro-movement and get the pointer location as it moves. During an active mouse usage, this function usually runs very frequently, can be something like 100 times per second (every 10 ms).
|
||||
|
||||
**The tracking function should update some information on the web-page.**
|
||||
|
||||
Updating function `update()` is too heavy to do it on every micro-movement. There is also no sense in making it more often than once per 100ms.
|
||||
|
||||
So we'll assign `throttle(update, 100)` as the function to run on each mouse move instead of the original `update()`. The decorator will be called often, but `update()` will be called at maximum once per 100ms.
|
||||
|
||||
Visually, it will look like this:
|
||||
|
||||
1. For the first mouse movement the decorated variant passes the call to `update`. That's important, the user sees our reaction to his move immediately.
|
||||
2. Then as the mouse moves on, until `100ms` nothing happens. The decorated variant ignores calls.
|
||||
3. At the end of `100ms` -- one more `update` happens with the last coordinates.
|
||||
4. Then, finally, the mouse stops somewhere. The decorated variant waits until `100ms` expire and then runs `update` runs with last coordinates. So, perhaps the most important, the final mouse coordinates are processed.
|
||||
|
||||
A code example:
|
||||
|
||||
```js
|
||||
function f(a) {
|
||||
console.log(a)
|
||||
};
|
||||
|
||||
// f1000 passes calls to f at maximum once per 1000 ms
|
||||
let f1000 = throttle(f, 1000);
|
||||
|
||||
f1000(1); // shows 1
|
||||
f1000(2); // (throttling, 1000ms not out yet)
|
||||
f1000(3); // (throttling, 1000ms not out yet)
|
||||
|
||||
// when 1000 ms time out...
|
||||
// ...outputs 3, intermediate value 2 was ignored
|
||||
```
|
||||
|
||||
P.S. Arguments and the context `this` passed to `f1000` should be passed to the original `f`.
|
461
1-js/06-advanced-functions/09-call-apply-decorators/article.md
Normal file
461
1-js/06-advanced-functions/09-call-apply-decorators/article.md
Normal file
|
@ -0,0 +1,461 @@
|
|||
# Decorators and forwarding, call/apply
|
||||
|
||||
JavaScript gives exceptional flexibility when dealing with functions. They can be passed around, used as objects, and now we'll see how to *forward* calls between them and *decorate* them.
|
||||
|
||||
[cut]
|
||||
|
||||
## Transparent caching
|
||||
|
||||
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 for different `x` to evade spending extra-time on recalculations.
|
||||
|
||||
But instead of adding that functionality into `slow()` we'll create a wrapper. As we'll see, there are many benefits of doing so.
|
||||
|
||||
Here's the code, and explanations follow:
|
||||
|
||||
```js run
|
||||
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 the result is in the map
|
||||
return cache.get(x); // return 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
|
||||
```
|
||||
|
||||
In the code above `cachingDecorator` is a *decorator*: a special function that takes another function and alters its behavior.
|
||||
|
||||
The idea is that we can call `cachingDecorator` for any function, and it will return the caching wrapper. That's great, because we can have many functions that could use such a feature, and all we need to do is to apply `cachingDecorator` to them.
|
||||
|
||||
By separating caching from the main function code we also keep the main code simpler.
|
||||
|
||||
Now let's get into details of how it works.
|
||||
|
||||
The result of `cachingDecorator(func)` is a "wrapper": `function(x)` that "wraps" the call of `func(x)` into caching logic:
|
||||
|
||||

|
||||
|
||||
As we can see, the wrapper returns the result of `func(x)` "as is". From an outside code, the wrapped `slow` function still does the same. It just got a caching aspect added to its behavior.
|
||||
|
||||
To summarize, there are several benefits of using a separate `cachingDecorator` instead of altering the code of `slow` itself:
|
||||
|
||||
- The `cachingDecorator` is reusable. We can apply it to another function.
|
||||
- The caching logic is separate, it did not increase the complexity of `slow` itself (if there were any).
|
||||
- We can combine multiple decorators if needed (other decorators will follow).
|
||||
|
||||
|
||||
## Using "func.call" for the context
|
||||
|
||||
The caching decorator mentioned above is not suited to work with object methods.
|
||||
|
||||
For instance, in the code below `user.format()` stops working after the decoration:
|
||||
|
||||
```js run
|
||||
// we'll make worker.slow caching
|
||||
let worker = {
|
||||
someMethod() {
|
||||
return 1;
|
||||
},
|
||||
|
||||
slow(x) {
|
||||
// actually, there can be a scary CPU-heavy task here
|
||||
alert("Called with " + x);
|
||||
return x * this.someMethod(); // (*)
|
||||
}
|
||||
};
|
||||
|
||||
// same code as before
|
||||
function cachingDecorator(func) {
|
||||
let cache = new Map();
|
||||
return function(x) {
|
||||
if (cache.has(x)) {
|
||||
return cache.get(x);
|
||||
}
|
||||
*!*
|
||||
let result = func(x); // (**)
|
||||
*/!*
|
||||
cache.set(x, result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
alert( worker.slow(1) ); // the original method works
|
||||
|
||||
worker.slow = cachingDecorator(worker.slow); // now make it caching
|
||||
|
||||
*!*
|
||||
alert( worker.slow(2) ); // WOPS! Error: Cannot read property 'someMethod' of undefined
|
||||
*/!*
|
||||
```
|
||||
|
||||
The error occurs in the line `(*)` that tries to access `this.someMethod` and fails. Guess you can see, why?
|
||||
|
||||
The reason is that the wrapper calls the original function as `func(x)` in the line `(**)`. And, when called like this, the function gets `this = undefined`.
|
||||
|
||||
As if we run:
|
||||
|
||||
```js
|
||||
let func = worker.slow;
|
||||
func(2);
|
||||
```
|
||||
|
||||
So, the wrapper passes the call to the original method, but without the context `this`. Hence the error.
|
||||
|
||||
Let's fix it.
|
||||
|
||||
There's a special built-in function method [func.call(context, ...args)](mdn:js/Function/call) that allows to call a function explicitly setting `this`.
|
||||
|
||||
The syntax is:
|
||||
|
||||
```js
|
||||
func.call(context, arg1, arg2, ...)
|
||||
```
|
||||
|
||||
It runs the `func` providing the first argument as `this`, and the next as the arguments.
|
||||
|
||||
To put it simple, these two calls do almost the same:
|
||||
```js
|
||||
func(1, 2, 3);
|
||||
func.call(obj, 1, 2, 3)
|
||||
```
|
||||
|
||||
They both call `func` with arguments `1`, `2` and `3`. The only difference is that `func.call` also sets `this` to `obj`.
|
||||
|
||||
As an example, in the code below we call `sayHi` in the context of different objects: `sayHi.call(user)` runs `sayHi` providing `this=user`, and the next line sets `this=admin`:
|
||||
|
||||
```js run
|
||||
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
|
||||
```
|
||||
|
||||
And here we use `call` to call `say` with the given context and phrase:
|
||||
|
||||
|
||||
```js run
|
||||
function say(phrase) {
|
||||
alert(this.name + ': ' + phrase);
|
||||
}
|
||||
|
||||
let user = { name: "John" };
|
||||
|
||||
// user becomes this, and "Hello" becomes the first argument
|
||||
say.call( user, "Hello" ); // John: Hello
|
||||
```
|
||||
|
||||
|
||||
For our case, we can use `call` in the wrapper to pass the context to the original function:
|
||||
|
||||
|
||||
```js run
|
||||
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)
|
||||
```
|
||||
|
||||
Now everything is fine.
|
||||
|
||||
To put all clear, let's see more deeply how `this` is passed along:
|
||||
|
||||
1. After the decoration `worker.slow` is now the wrapper `function (x) { ... }`.
|
||||
2. So when `worker.slow(2)` is executed, the wrapper gets `2` as an argument and `this=worker` (it's the object before dot).
|
||||
3. Inside the wrapper, assuming the result is not yet cached, `func.call(this, x)` passes the current `this` (`=worker`) and the current argument (`=2`) to the original method.
|
||||
|
||||
## Going multi-argument with "func.apply"
|
||||
|
||||
Now let's make `cachingDecorator` even more universal. Till now it was working only with single-argument functions.
|
||||
|
||||
Now how to cache the multi-argument `worker.slow` method?
|
||||
|
||||
```js
|
||||
let worker = {
|
||||
slow(min, max) {
|
||||
return min + max; // scary CPU-hogger is assumed
|
||||
}
|
||||
};
|
||||
|
||||
// should remember same-argument calls
|
||||
worker.slow = cachingDecorator(worker.slow);
|
||||
```
|
||||
|
||||
We have two tasks to solve here.
|
||||
|
||||
First is how to use both arguments `min` and `max` for the key in `cache` map. Previously, for a single argument `x` we could just `cache.set(x, result)` to save the result and `cache.get(x)` to retrieve it. But now we need to remember the result for a *combination of arguments* `(min,max)`. The native `Map` takes single value only as the key.
|
||||
|
||||
There are many solutions possible:
|
||||
|
||||
1. Implement a new (or use a third-party) map-like data structure that is more versatile and allows multi-keys.
|
||||
2. Use nested maps: `cache.set(min)` will be a `Map` that stores the pair `(max, result)`. So we can get `result` as `cache.get(min).get(max)`.
|
||||
3. Join two values into one. In our particular case we can just use a string `"min,max"` as the `Map` key. For flexibility, we can allow to provide a *hashing function* for the decorator, that knows how to make a one value from many.
|
||||
|
||||
|
||||
For many practical applications, the 3rd variant is good enough, so we'll stick to it.
|
||||
|
||||
The second task to solve is how to pass many arguments to `func`. Currently, the wrapper `function(x)` assumes a single argument, and `func.call(this, x)` passes it.
|
||||
|
||||
Here we can use another built-in method [func.apply](mdn:js/Function/apply).
|
||||
|
||||
The syntax is:
|
||||
|
||||
```js
|
||||
func.apply(context, args)
|
||||
```
|
||||
|
||||
It runs the `func` setting `this=context` and using an array-like object `args` as the list of arguments.
|
||||
|
||||
|
||||
For instance, these two calls are almost the same:
|
||||
|
||||
```js
|
||||
func(1, 2, 3);
|
||||
func.apply(context, [1, 2, 3])
|
||||
```
|
||||
|
||||
Both run `func` giving it arguments `1,2,3`. But `apply` also sets `this=context`.
|
||||
|
||||
For instance, here `say` is called with `this=user` and `messageData` as a list of arguments:
|
||||
|
||||
```js run
|
||||
function say(time, phrase) {
|
||||
alert(`[${time}] ${this.name}: ${phrase}`);
|
||||
}
|
||||
|
||||
let user = { name: "John" };
|
||||
|
||||
let messageData = ['10:00', 'Hello']; // become time and phrase
|
||||
|
||||
*!*
|
||||
// user becomes this, messageData is passed as a list of arguments (time, phrase)
|
||||
say.apply(user, messageData); // [10:00] John: Hello (this=user)
|
||||
*/!*
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
We already know the spread operator `...` from the chapter <info:rest-parameters-spread-operator> that can pass an array (or any iterable) as a list of arguments. So if we use it with `call`, we can achieve almost the same as `apply`.
|
||||
|
||||
These two calls are almost equivalent:
|
||||
|
||||
```js
|
||||
let args = [1, 2, 3];
|
||||
|
||||
*!*
|
||||
func.call(context, ...args); // pass an array as list with spread operator
|
||||
func.apply(context, args); // is same as using apply
|
||||
*/!*
|
||||
```
|
||||
|
||||
If we look more closely, there's a minor difference between such uses of `call` and `apply`.
|
||||
|
||||
- The spread operator `...` allows to pass *iterable* `args` as the list to `call`.
|
||||
- The `apply` accepts only *array-like* `args`.
|
||||
|
||||
So, these calls complement to each other. Where we expect an iterable, `call` works, where we expect an array-like, `apply` works.
|
||||
|
||||
And if `args` is both iterable and array-like, like a real array, then we technically could use any of them, but `apply` will probably be faster, because it's a single operation. Most JavaScript engines internally optimize is better than a pair `call + spread`.
|
||||
|
||||
One of most important uses of `apply` is passing the call to another function, like this:
|
||||
|
||||
```js
|
||||
let wrapper = function() {
|
||||
return anotherFunction.apply(this, arguments);
|
||||
};
|
||||
```
|
||||
|
||||
That's called *call forwarding*. The `wrapper` passes everything it gets: the context `this` and arguments to `anotherFunction` and returns back its result.
|
||||
|
||||
When an external code calls such `wrapper`, it is undistinguishable from the call of the original function.
|
||||
|
||||
Now let's bake it all into the more powerful `cachingDecorator`:
|
||||
|
||||
```js run
|
||||
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.apply(this, arguments); // (**)
|
||||
*/!*
|
||||
|
||||
cache.set(key, result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function hash(args) {
|
||||
return args[0] + ',' + args[1];
|
||||
}
|
||||
|
||||
worker.slow = cachingDecorator(worker.slow, hash);
|
||||
|
||||
alert( worker.slow(3, 5) ); // works
|
||||
alert( "Again " + worker.slow(3, 5) ); // same (cached)
|
||||
```
|
||||
|
||||
Now the wrapper operates with any number of arguments.
|
||||
|
||||
There are two changes:
|
||||
|
||||
- In the line `(*)` it calls `hash` to create a single key from `arguments`. Here we use a simple "joining" function that turns arguments `(3, 5)` into the key `"3,5"`. More complex cases may require other hashing functions.
|
||||
- Then `(**)` uses `func.apply` to pass both the context and all arguments the wrapper got (no matter how many) to the original function.
|
||||
|
||||
|
||||
## Borrowing a method [#method-borrowing]
|
||||
|
||||
Now let's make one more minor improvement in the hashing function:
|
||||
|
||||
```js
|
||||
function hash(args) {
|
||||
return args[0] + ',' + args[1];
|
||||
}
|
||||
```
|
||||
|
||||
As of now, it works only on two arguments. It would be better if it could glue any number of `args`.
|
||||
|
||||
The natural solution would be to use [arr.join](mdn:js/Array/join) method:
|
||||
|
||||
```js
|
||||
function hash(args) {
|
||||
return args.join();
|
||||
}
|
||||
```
|
||||
|
||||
...Unfortunately, that won't work. Because we are calling `hash(arguments)` and `arguments` object is both iterable and array-like, but not a real array.
|
||||
|
||||
So calling `join` on it would fail, as we can see below:
|
||||
|
||||
```js run
|
||||
function hash() {
|
||||
*!*
|
||||
alert( arguments.join() ); // Error: arguments.join is not a function
|
||||
*/!*
|
||||
}
|
||||
|
||||
hash(1, 2);
|
||||
```
|
||||
|
||||
Still, there's an easy way to use array join:
|
||||
|
||||
```js run
|
||||
function hash() {
|
||||
*!*
|
||||
alert( [].join.call(arguments) ); // 1,2
|
||||
*/!*
|
||||
}
|
||||
|
||||
hash(1, 2);
|
||||
```
|
||||
|
||||
The trick is called *method borrowing*.
|
||||
|
||||
We take (borrow) a join method from a regular array `[].join`. And use `[].join.call` to run it in the context of `arguments`.
|
||||
|
||||
Why does it work?
|
||||
|
||||
That's because the internal algorithm of the native method `arr.join(glue)` is very simple.
|
||||
|
||||
Taken from the specification almost "as-is":
|
||||
|
||||
1. Let `glue` be the first argument or, if no arguments, then a comma `","`.
|
||||
2. Let `result` be an empty string.
|
||||
3. Append `this[0]` to `result`.
|
||||
4. Append `glue` and `this[1]`.
|
||||
5. Append `glue` and `this[2]`.
|
||||
6. ...Do so until `this.length` items are glued.
|
||||
7. Return `result`.
|
||||
|
||||
So, technically it takes `this` and joins `this[0]`, `this[1]` ...etc together. It's intentionally written in a way that allows any array-like `this` (not a coincidence, many methods follow this practice). That's why it also works with `this=arguments`.
|
||||
|
||||
## Summary
|
||||
|
||||
*Decorator* is a wrapper around a function that alters its behavior. The main job is still carried out by the function.
|
||||
|
||||
It is generally safe to replace a function or a method with decorated one, except for one little thing. If the original function had properties on it, like `func.calledCount` or whatever, then the decorated one will not provide them. Because that is a wrapper. So one need to be careful if he uses them. Some decorators provide their own properties.
|
||||
|
||||
Decorators can be seen as "features" or "aspects" that can be added to a function. We can add one or add many. And all this without changing its code!
|
||||
|
||||
To implement `cachingDecorator`, we studied methods:
|
||||
|
||||
- [func.call(context, arg1, arg2...)](mdn:js/Function/call) -- calls `func` with given context and arguments.
|
||||
- [func.apply(context, args)](mdn:js/Function/apply) -- calls `func` passing `context` as `this` and array-like `args` into a list of arguments.
|
||||
|
||||
The generic *call forwarding* is usually done with `apply`:
|
||||
|
||||
```js
|
||||
let wrapper = function() {
|
||||
return original.apply(this, arguments);
|
||||
}
|
||||
```
|
||||
|
||||
We also saw an example of *method borrowing* when we take a method from an object and `call` it in the context of another object. It is quite common to take array methods and apply them to arguments. The alternative is to use rest parameters object that is a real array.
|
||||
|
||||
|
||||
There are many decorators there in the wilds. Check how well you got them by solving the tasks of this chapter.
|
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
Loading…
Add table
Add a link
Reference in a new issue