minor
This commit is contained in:
parent
d9075008f8
commit
e6e562040d
5 changed files with 71 additions and 107 deletions
|
@ -14,18 +14,18 @@ Let's check the real-life application to better understand that requirement and
|
|||
|
||||
In browser we can setup a function to run at every mouse 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.**
|
||||
**We'd like to update some information on the web-page when the pointer moves.**
|
||||
|
||||
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.
|
||||
...But updating function `update()` is too heavy to do it on every micro-movement. There is also no sense in updating more often than once per 100ms.
|
||||
|
||||
So we'll wrap it into the decorator: use `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.
|
||||
So we'll wrap it into the decorator: use `throttle(update, 100)` as the function to run on each mouse move instead of the original `update()`. The decorator will be called often, but forward the call to `update()` 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 their move immediately.
|
||||
1. For the first mouse movement the decorated variant immediately passes the call to `update`. That's important, the user sees our reaction to their 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` with last coordinates. So, perhaps the most important, the final mouse coordinates are processed.
|
||||
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` with last coordinates. So, quite important, the final mouse coordinates are processed.
|
||||
|
||||
A code example:
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -5,13 +5,13 @@ libs:
|
|||
|
||||
# Function binding
|
||||
|
||||
When using `setTimeout` with object methods or passing object methods along, there's a known problem: "losing `this`".
|
||||
When passing object methods as callbacks, for instance to `setTimeout`, there's a known problem: "losing `this`".
|
||||
|
||||
Suddenly, `this` just stops working right. The situation is typical for novice developers, but happens with experienced ones as well.
|
||||
In this chapter we'll see the ways to fix it.
|
||||
|
||||
## Losing "this"
|
||||
|
||||
We already know that in JavaScript it's easy to lose `this`. Once a method is passed somewhere separately from the object -- `this` is lost.
|
||||
We've already seen examples of losing `this`. Once a method is passed somewhere separately from the object -- `this` is lost.
|
||||
|
||||
Here's how it may happen with `setTimeout`:
|
||||
|
||||
|
@ -37,7 +37,7 @@ let f = user.sayHi;
|
|||
setTimeout(f, 1000); // lost user context
|
||||
```
|
||||
|
||||
The method `setTimeout` in-browser is a little special: it sets `this=window` for the function call (for Node.js, `this` becomes the timer object, but doesn't really matter here). So for `this.firstName` it tries to get `window.firstName`, which does not exist. In other similar cases as we'll see, usually `this` just becomes `undefined`.
|
||||
The method `setTimeout` in-browser is a little special: it sets `this=window` for the function call (for Node.js, `this` becomes the timer object, but doesn't really matter here). So for `this.firstName` it tries to get `window.firstName`, which does not exist. In other similar cases, usually `this` just becomes `undefined`.
|
||||
|
||||
The task is quite typical -- we want to pass an object method somewhere else (here -- to the scheduler) where it will be called. How to make sure that it will be called in the right context?
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue