This commit is contained in:
Ilya Kantor 2019-08-01 13:50:55 +03:00
parent d9075008f8
commit e6e562040d
5 changed files with 71 additions and 107 deletions

View file

@ -229,9 +229,7 @@ let worker = {
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.
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:
@ -239,85 +237,11 @@ There are many solutions possible:
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 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.
Also we need to replace `func.call(this, x)` with `func.call(this, ...arguments)`, to pass all arguments to the wrapped function call, not just the first one.
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 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 it better than a pair `call + spread`.
One of the 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 indistinguishable from the call of the original function.
Now let's bake it all into the more powerful `cachingDecorator`:
Here's a more powerful `cachingDecorator`:
```js run
let worker = {
@ -338,7 +262,7 @@ function cachingDecorator(func, hash) {
}
*!*
let result = func.apply(this, arguments); // (**)
let result = func.call(this, ...arguments); // (**)
*/!*
cache.set(key, result);
@ -356,13 +280,52 @@ alert( worker.slow(3, 5) ); // works
alert( "Again " + worker.slow(3, 5) ); // same (cached)
```
Now the wrapper operates with any number of arguments.
Now it works 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.
- Then `(**)` uses `func.call(this, ...arguments)` to pass both the context and all arguments the wrapper got (not just the first one) to the original function.
Instead of `func.call(this, ...arguments)` we could use `func.apply(this, arguments)`.
The syntax of built-in method [func.apply](mdn:js/Function/apply) 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.
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 these two calls are almost equivalent:
```js
func.call(context, ...args); // pass an array as list with spread operator
func.apply(context, args); // is same as using apply
```
There's only a minor difference:
- The spread operator `...` allows to pass *iterable* `args` as the list to `call`.
- The `apply` accepts only *array-like* `args`.
So, these calls complement each other. Where we expect an iterable, `call` works, where we expect an array-like, `apply` works.
And for objects that are both iterable and array-like, like a real array, we technically could use any of them, but `apply` will probably be faster, because most JavaScript engines internally optimize it better.
Passing all arguments along with the context to another function is called *call forwarding*.
That's the simplest form of it:
```js
let wrapper = function() {
return func.apply(this, arguments);
};
```
When an external code calls such `wrapper`, it is indistinguishable from the call of the original function `func`.
## Borrowing a method [#method-borrowing]
@ -448,10 +411,9 @@ 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 wild. Check how well you got them by solving the tasks of this chapter.