work
This commit is contained in:
parent
276065baf1
commit
6a263b1f75
35 changed files with 900 additions and 541 deletions
|
@ -132,7 +132,7 @@ while(true) {
|
|||
|
||||
The same works for an array.
|
||||
|
||||
## Iterables VS array-likes
|
||||
## Iterables VS array-likes [#array-like]
|
||||
|
||||
There are two official terms that are similar, but actually very different. Please take care to avoid the confusion.
|
||||
|
||||
|
|
|
@ -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("вызывает функцию через указанный таймаут", function() {
|
||||
var start = Date.now();
|
||||
|
||||
function f(x) {
|
||||
assert.equal(Date.now() - start, 1000);
|
||||
}
|
||||
f = sinon.spy(f);
|
||||
|
||||
var f1000 = delay(f, 1000);
|
||||
f1000("test");
|
||||
this.clock.tick(2000);
|
||||
assert(f.calledOnce, 'calledOnce check fails');
|
||||
});
|
||||
|
||||
it("передаёт аргументы и контекст", function() {
|
||||
var start = Date.now();
|
||||
var user = {
|
||||
sayHi: function(phrase, who) {
|
||||
assert.equal(this, user);
|
||||
assert.equal(phrase, "Привет");
|
||||
assert.equal(who, "Вася");
|
||||
assert.equal(Date.now() - start, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
user.sayHi = sinon.spy(user.sayHi);
|
||||
|
||||
var spy = user.sayHi;
|
||||
user.sayHi = delay(user.sayHi, 1500);
|
||||
|
||||
user.sayHi("Привет", "Вася");
|
||||
|
||||
this.clock.tick(2000);
|
||||
|
||||
assert(spy.calledOnce, 'проверка calledOnce не сработала');
|
||||
});
|
||||
});
|
26
1-js/7-deeper/6-call-apply-decorators/1-delay/solution.md
Normal file
26
1-js/7-deeper/6-call-apply-decorators/1-delay/solution.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
|
||||
|
||||
```js run
|
||||
function delay(f, ms) {
|
||||
|
||||
*!*
|
||||
return function() {
|
||||
setTimeout(() => f.apply(this, arguments), ms);
|
||||
};
|
||||
*/!*
|
||||
|
||||
}
|
||||
|
||||
function f(x) {
|
||||
alert(x);
|
||||
}
|
||||
|
||||
let f1000 = delay(f, 1000);
|
||||
let f1500 = delay(f, 1500);
|
||||
|
||||
f1000("test"); // shows "test" after 1000ms
|
||||
f1500("test"); // shows "test" after 1500ms
|
||||
```
|
||||
|
||||
Arrow function makes it easy, because `setTimeout` uses arguments and context of the wrapper .
|
||||
|
27
1-js/7-deeper/6-call-apply-decorators/1-delay/task.md
Normal file
27
1-js/7-deeper/6-call-apply-decorators/1-delay/task.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Delaying decorator [todo move after arrow]
|
||||
|
||||
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`.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
function makeLogging(f, log) {
|
||||
|
||||
function wrapper(a) {
|
||||
log.push(a);
|
||||
return f.call(this, a);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
describe("makeLogging", function() {
|
||||
it("записывает вызовы в массив log", function() {
|
||||
var work = sinon.spy();
|
||||
|
||||
var log = [];
|
||||
work = makeLogging(work, log);
|
||||
assert.deepEqual(log, []);
|
||||
|
||||
work(1);
|
||||
assert.deepEqual(log, [1]);
|
||||
|
||||
work(2);
|
||||
assert.deepEqual(log, [1, 2]);
|
||||
});
|
||||
|
||||
it("передаёт вызов функции, возвращает её результат", function() {
|
||||
var log = [];
|
||||
|
||||
function work(x) {
|
||||
return x * 2;
|
||||
}
|
||||
|
||||
work = sinon.spy(work);
|
||||
var spy = work;
|
||||
work = makeLogging(work, log);
|
||||
|
||||
assert.equal(work(1), 2);
|
||||
assert(spy.calledWith(1));
|
||||
});
|
||||
|
||||
|
||||
it("сохраняет контекст вызова для методов объекта", function() {
|
||||
var log = [];
|
||||
|
||||
var calculator = {
|
||||
double: function(x) {
|
||||
return x * 2;
|
||||
}
|
||||
}
|
||||
|
||||
calculator.double = sinon.spy(calculator.double);
|
||||
var spy = calculator.double;
|
||||
calculator.double = makeLogging(calculator.double, log);
|
||||
|
||||
assert.equal(calculator.double(1), 2);
|
||||
assert(spy.calledWith(1));
|
||||
assert(spy.calledOn(calculator));
|
||||
});
|
||||
|
||||
});
|
|
@ -1,40 +0,0 @@
|
|||
Возвратим декоратор `wrapper` который будет записывать аргумент в `log` и передавать вызов в `f`:
|
||||
|
||||
```js run
|
||||
function work(a) {
|
||||
/*...*/ // work - произвольная функция, один аргумент
|
||||
}
|
||||
|
||||
function makeLogging(f, log) {
|
||||
|
||||
*!*
|
||||
function wrapper(a) {
|
||||
log.push(a);
|
||||
return f.call(this, a);
|
||||
}
|
||||
*/!*
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
var log = [];
|
||||
work = makeLogging(work, log);
|
||||
|
||||
work(1); // 1
|
||||
work(5); // 5
|
||||
|
||||
for (var i = 0; i < log.length; i++) {
|
||||
alert( 'Лог:' + log[i] ); // "Лог:1", затем "Лог:5"
|
||||
}
|
||||
```
|
||||
|
||||
**Обратите внимание, вызов функции осуществляется как `f.call(this, a)`, а не просто `f(a)`.**
|
||||
|
||||
Передача контекста необходима, чтобы декоратор корректно работал с методами объекта. Например:
|
||||
|
||||
```js
|
||||
user.method = makeLogging(user.method, log);
|
||||
```
|
||||
|
||||
Теперь при вызове `user.method(...)` в декоратор будет передаваться контекст `this`, который надо передать исходной функции через `call/apply`.
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Логирующий декоратор (1 аргумент)
|
||||
|
||||
Создайте декоратор `makeLogging(f, log)`, который берет функцию `f` и массив `log`.
|
||||
|
||||
Он должен возвращать обёртку вокруг `f`, которая при каждом вызове записывает ("логирует") аргументы в `log`, а затем передает вызов в `f`.
|
||||
|
||||
**В этой задаче можно считать, что у функции `f` ровно один аргумент.**
|
||||
|
||||
Работать должно так:
|
||||
|
||||
```js
|
||||
function work(a) {
|
||||
/* ... */ // work - произвольная функция, один аргумент
|
||||
}
|
||||
|
||||
function makeLogging(f, log) { /* ваш код */ }
|
||||
|
||||
var log = [];
|
||||
work = makeLogging(work, log);
|
||||
|
||||
work(1); // 1, добавлено в log
|
||||
work(5); // 5, добавлено в log
|
||||
|
||||
for (var i = 0; i < log.length; i++) {
|
||||
*!*
|
||||
alert( 'Лог:' + log[i] ); // "Лог:1", затем "Лог:5"
|
||||
*/!*
|
||||
}
|
||||
```
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
function makeLogging(f, log) {
|
||||
|
||||
function wrapper() {
|
||||
log.push([].slice.call(arguments));
|
||||
return f.apply(this, arguments);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
describe("makeLogging", function() {
|
||||
it("записывает вызовы в массив log", function() {
|
||||
var work = sinon.spy();
|
||||
|
||||
var log = [];
|
||||
work = makeLogging(work, log);
|
||||
assert.deepEqual(log, []);
|
||||
|
||||
work(1, 2);
|
||||
assert.deepEqual(log, [
|
||||
[1, 2]
|
||||
]);
|
||||
|
||||
work(3, 4);
|
||||
assert.deepEqual(log, [
|
||||
[1, 2],
|
||||
[3, 4]
|
||||
]);
|
||||
});
|
||||
|
||||
it("передаёт вызов функции, возвращает её результат", function() {
|
||||
var log = [];
|
||||
|
||||
function sum(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
sum = sinon.spy(sum);
|
||||
var spy = sum;
|
||||
sum = makeLogging(sum, log);
|
||||
|
||||
assert.equal(sum(1, 2), 3);
|
||||
assert(spy.calledWith(1, 2));
|
||||
});
|
||||
|
||||
|
||||
it("сохраняет контекст вызова для методов объекта", function() {
|
||||
var log = [];
|
||||
|
||||
var calculator = {
|
||||
sum: function(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
}
|
||||
|
||||
calculator.sum = sinon.spy(calculator.sum);
|
||||
var spy = calculator.sum;
|
||||
calculator.sum = makeLogging(calculator.sum, log);
|
||||
|
||||
assert.equal(calculator.sum(1, 2), 3);
|
||||
assert(spy.calledWith(1, 2));
|
||||
assert(spy.calledOn(calculator));
|
||||
});
|
||||
|
||||
});
|
|
@ -1,31 +0,0 @@
|
|||
importance: 3
|
||||
|
||||
---
|
||||
|
||||
# Логирующий декоратор (много аргументов)
|
||||
|
||||
Создайте декоратор `makeLogging(func, log)`, для функции `func` возвращающий обёртку, которая при каждом вызове добавляет её аргументы в массив `log`.
|
||||
|
||||
Условие аналогично задаче <info:task/logging-decorator>, но допускается `func` с любым набором аргументов.
|
||||
|
||||
Работать должно так:
|
||||
|
||||
```js
|
||||
function work(a, b) {
|
||||
alert( a + b ); // work - произвольная функция
|
||||
}
|
||||
|
||||
function makeLogging(f, log) { /* ваш код */ }
|
||||
|
||||
var log = [];
|
||||
work = makeLogging(work, log);
|
||||
|
||||
work(1, 2); // 3
|
||||
work(4, 5); // 9
|
||||
|
||||
for (var i = 0; i < log.length; i++) {
|
||||
var args = log[i]; // массив из аргументов i-го вызова
|
||||
alert( 'Лог:' + args.join() ); // "Лог:1,2", "Лог:4,5"
|
||||
}
|
||||
```
|
||||
|
|
@ -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,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.
|
|
@ -1,11 +0,0 @@
|
|||
function makeCaching(f) {
|
||||
var cache = {};
|
||||
|
||||
return function(x) {
|
||||
if (!(x in cache)) {
|
||||
cache[x] = f.call(this, x);
|
||||
}
|
||||
return cache[x];
|
||||
};
|
||||
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
describe("makeCaching", function() {
|
||||
|
||||
it("запоминает предыдущее значение функции с таким аргументом", function() {
|
||||
function f(x) {
|
||||
return Math.random() * x;
|
||||
}
|
||||
|
||||
f = makeCaching(f);
|
||||
|
||||
var a = f(1);
|
||||
var b = f(1);
|
||||
assert.equal(a, b);
|
||||
|
||||
var anotherValue = f(2);
|
||||
// почти наверняка другое значение
|
||||
assert.notEqual(a, anotherValue);
|
||||
});
|
||||
|
||||
it("сохраняет контекст вызова", function() {
|
||||
var obj = {
|
||||
spy: sinon.spy()
|
||||
};
|
||||
|
||||
var spy = obj.spy;
|
||||
obj.spy = makeCaching(obj.spy);
|
||||
obj.spy(123);
|
||||
assert(spy.calledWith(123));
|
||||
assert(spy.calledOn(obj));
|
||||
});
|
||||
|
||||
});
|
|
@ -1,33 +0,0 @@
|
|||
Запоминать результаты вызова функции будем в замыкании, в объекте `cache: { ключ:значение }`.
|
||||
|
||||
```js run no-beautify
|
||||
function f(x) {
|
||||
return Math.random()*x;
|
||||
}
|
||||
|
||||
*!*
|
||||
function makeCaching(f) {
|
||||
var cache = {};
|
||||
|
||||
return function(x) {
|
||||
if (!(x in cache)) {
|
||||
cache[x] = f.call(this, x);
|
||||
}
|
||||
return cache[x];
|
||||
};
|
||||
|
||||
}
|
||||
*/!*
|
||||
|
||||
f = makeCaching(f);
|
||||
|
||||
var a = f(1);
|
||||
var b = f(1);
|
||||
alert( a == b ); // true (значение закешировано)
|
||||
|
||||
b = f(2);
|
||||
alert( a == b ); // false, другой аргумент => другое значение
|
||||
```
|
||||
|
||||
Обратите внимание: проверка на наличие уже подсчитанного значения выглядит так: `if (x in cache)`. Менее универсально можно проверить так: `if (cache[x])`, это если мы точно знаем, что `cache[x]` никогда не будет `false`, `0` и т.п.
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
importance: 5
|
||||
|
||||
---
|
||||
|
||||
# Кеширующий декоратор
|
||||
|
||||
Создайте декоратор `makeCaching(f)`, который берет функцию `f` и возвращает обертку, которая кеширует её результаты.
|
||||
|
||||
**В этой задаче функция `f` имеет только один аргумент, и он является числом.**
|
||||
|
||||
1. При первом вызове обертки с определенным аргументом -- она вызывает `f` и запоминает значение.
|
||||
2. При втором и последующих вызовах с тем же аргументом возвращается запомненное значение.
|
||||
|
||||
Должно работать так:
|
||||
|
||||
```js
|
||||
function f(x) {
|
||||
return Math.random() * x; // random для удобства тестирования
|
||||
}
|
||||
|
||||
function makeCaching(f) { /* ваш код */ }
|
||||
|
||||
f = makeCaching(f);
|
||||
|
||||
var a, b;
|
||||
|
||||
a = f(1);
|
||||
b = f(1);
|
||||
alert( a == b ); // true (значение закешировано)
|
||||
|
||||
b = f(2);
|
||||
alert( a == b ); // false, другой аргумент => другое значение
|
||||
```
|
||||
|
|
@ -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");
|
||||
});
|
||||
|
||||
});
|
30
1-js/7-deeper/6-call-apply-decorators/8-debounce/solution.md
Normal file
30
1-js/7-deeper/6-call-apply-decorators/8-debounce/solution.md
Normal file
|
@ -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.
|
24
1-js/7-deeper/6-call-apply-decorators/8-debounce/task.md
Normal file
24
1-js/7-deeper/6-call-apply-decorators/8-debounce/task.md
Normal file
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
39
1-js/7-deeper/6-call-apply-decorators/9-throttle/solution.md
Normal file
39
1-js/7-deeper/6-call-apply-decorators/9-throttle/solution.md
Normal file
|
@ -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.
|
48
1-js/7-deeper/6-call-apply-decorators/9-throttle/task.md
Normal file
48
1-js/7-deeper/6-call-apply-decorators/9-throttle/task.md
Normal file
|
@ -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`.
|
|
@ -16,25 +16,26 @@ A working example is better than thousand words here:
|
|||
|
||||
```js run
|
||||
function slow(x) {
|
||||
return Math.random(); // actually, there is a scary CPU-heavy task here
|
||||
// Math.random() is used to make caching exceptionally noticeable
|
||||
return x * Math.random(); // actually, there is a scary CPU-heavy task here
|
||||
}
|
||||
|
||||
function makeCaching(func) {
|
||||
function cachingDecorator(func) {
|
||||
let cache = new Map();
|
||||
|
||||
return function(x) {
|
||||
if (cache.has(x)) {
|
||||
return cache.get(x);
|
||||
if (cache.has(x)) { // if in the map
|
||||
return cache.get(x); // return
|
||||
}
|
||||
|
||||
let result = func(x); // (*)
|
||||
let result = func(x); // otherwise call func
|
||||
|
||||
cache.set(x, result);
|
||||
cache.set(x, result); // and remember
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
slow = makeCaching(slow);
|
||||
slow = cachingDecorator(slow);
|
||||
|
||||
alert( slow(1) ); // slow(1) is cached
|
||||
alert( slow(1) ); // the same
|
||||
|
@ -43,225 +44,414 @@ alert( slow(2) ); // slow(2) is cached
|
|||
alert( slow(2) ); // the same as the previous line
|
||||
```
|
||||
|
||||
In the code above `makeCaching` is a *decorator*: a special function that takes another function and alters its behavior.
|
||||
In the code above `cachingDecorator` is a *decorator*: a special function that takes another function and alters its behavior.
|
||||
|
||||
Here it returns a "wrapper": `function(x)` that "wraps" the call of `func(x)` `(*)`. As you can see from t
|
||||
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 decorator did not change how function works. From an outside, the wrapped `slow` function still does the same. It just got a caching aspect added to its behavior.
|
||||
|
||||
|
||||
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
|
||||
|
||||
## Instrumentation: timing decorator
|
||||
|
||||
Let's say we have a function and want to measure time it takes to run.
|
||||
|
||||
Of course, we could modify it. Add something like `timerStart()` to the start and `timerEnd()` to all exit points. Then later we may want to log which arguments it receives and results. That requires additional code as well.
|
||||
|
||||
That is called [instrumentation](https://en.wikipedia.org/wiki/Instrumentation) of the code -- adding stuff to measure times, log what's happening and do other watching/measuring without interfering with the main functionality.
|
||||
|
||||
Putting instrumentation inside the function manually is not pretty at all. It takes space, shadows the core functionality and makes it harder to debug.
|
||||
|
||||
There are tools that analyze javascript code and add instrumentation calls to it automatically. But here we'll take a "median" approach. We will "wrap" instrumentation over function without touching its code.
|
||||
|
||||
What we going to do is a special function `timingDecorator(func)`, that takes a function `func` and returns a "wrapper" around it, that transfers all calls to `func` and measures time they take.
|
||||
|
||||
For simplicity let's assume that `func` has only one argument.
|
||||
|
||||
The code with `timingDecorator` and example function:
|
||||
|
||||
|
||||
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
|
||||
function fibo(n) { // a function to measure, here we count fibonacci number
|
||||
return (n > 2) ? fibo(n - 1) + fibo(n - 2) : 1;
|
||||
}
|
||||
let obj = {
|
||||
random() {
|
||||
return Math.random(); // actually, there is a scary CPU-heavy task here
|
||||
},
|
||||
|
||||
let timers = {}; // timers to store data
|
||||
slow(x) {
|
||||
*!*
|
||||
return this.random() * x; // (*)
|
||||
*/!*
|
||||
}
|
||||
};
|
||||
|
||||
function timingDecorator(func) {
|
||||
function cachingDecorator(func) {
|
||||
let cache = new Map();
|
||||
return function(x) {
|
||||
let start = performance.now();
|
||||
|
||||
let result = func(x);
|
||||
|
||||
let time = performance.now() - start;
|
||||
|
||||
if (!timers[func.name]) timers[func.name] = 0;
|
||||
timers[func.name] += time;
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
// decorate it
|
||||
fibo = timingDecorator(fibo);
|
||||
|
||||
// run
|
||||
alert( fibo(10) ); // 55
|
||||
alert( fibo(20) ); // 6765
|
||||
alert( fibo(30) ); // 832040
|
||||
|
||||
alert( `Total time: ${timers.fibo.toFixed(3)}ms` ); // total count of fibo calls
|
||||
```
|
||||
|
||||
При помощи декоратора `timingDecorator` мы сможем взять произвольную функцию и одним движением руки прикрутить к ней измеритель времени.
|
||||
|
||||
Его реализация:
|
||||
|
||||
```js run
|
||||
var timers = {};
|
||||
|
||||
// прибавит время выполнения f к таймеру timers[timer]
|
||||
function timingDecorator(f, timer) {
|
||||
return function() {
|
||||
var start = performance.now();
|
||||
|
||||
var result = f.apply(this, arguments); // (*)
|
||||
|
||||
if (!timers[timer]) timers[timer] = 0;
|
||||
timers[timer] += performance.now() - start;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// функция может быть произвольной, например такой:
|
||||
var fibonacci = function f(n) {
|
||||
return (n > 2) ? f(n - 1) + f(n - 2) : 1;
|
||||
}
|
||||
|
||||
if (cache.has(x)) {
|
||||
return cache.get(x);
|
||||
}
|
||||
*!*
|
||||
// использование: завернём fibonacci в декоратор
|
||||
fibonacci = timingDecorator(fibonacci, "fibo");
|
||||
let result = func(x); // "this" is not passed (**)
|
||||
*/!*
|
||||
cache.set(x, result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
// неоднократные вызовы...
|
||||
alert( fibonacci(10) ); // 55
|
||||
alert( fibonacci(20) ); // 6765
|
||||
// ...
|
||||
alert( obj.slow(1) ); // the original method works
|
||||
|
||||
obj.slow = cachingDecorator(obj.slow); // now make it caching
|
||||
|
||||
*!*
|
||||
// в любой момент можно получить общее количество времени на вызовы
|
||||
alert( timers.fibo + 'мс' );
|
||||
alert( obj.slow(2) ); // WOPS! Error: Cannot read property 'random' of undefined
|
||||
*/!*
|
||||
```
|
||||
|
||||
Обратим внимание на строку `(*)` внутри декоратора, которая и осуществляет передачу вызова:
|
||||
The error actually happens in the line `(*)` that tries to access `this.random` and fails. Guess you can see, why?
|
||||
|
||||
The decorated `obj.slow` calls the original one as `func(x)` in the line `(**)`. In this manner, the function always gets `this = undefined`.
|
||||
|
||||
As if we run:
|
||||
|
||||
```js
|
||||
var result = f.apply(this, arguments); // (*)
|
||||
let func = obj.slow;
|
||||
func(2);
|
||||
```
|
||||
|
||||
Этот приём называется "форвардинг вызова" (от англ. forwarding): текущий контекст и аргументы через `apply` передаются в функцию `f`, так что изнутри `f` всё выглядит так, как была вызвана она напрямую, а не декоратор.
|
||||
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`.
|
||||
|
||||
В JavaScript, как правило, пренебрегают проверками типа. В функцию, которая должна получать число, может быть передана строка, булево значение или даже объект.
|
||||
|
||||
Например:
|
||||
|
||||
```js no-beautify
|
||||
function sum(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// передадим в функцию для сложения чисел нечисловые значения
|
||||
alert( sum(true, { name: "Вася", age: 35 }) ); // true[Object object]
|
||||
```
|
||||
|
||||
Функция "как-то" отработала, но в реальной жизни передача в `sum` подобных значений, скорее всего, будет следствием программной ошибки. Всё-таки `sum` предназначена для суммирования чисел, а не объектов.
|
||||
|
||||
Многие языки программирования позволяют прямо в объявлении функции указать, какие типы данных имеют параметры. И это удобно, поскольку повышает надёжность кода.
|
||||
|
||||
В JavaScript же проверку типов приходится делать дополнительным кодом в начале функции, который во-первых обычно лень писать, а во-вторых он увеличивает общий объем текста, тем самым ухудшая читаемость.
|
||||
|
||||
**Декораторы способны упростить рутинные, повторяющиеся задачи, вынести их из кода функции.**
|
||||
|
||||
Например, создадим декоратор, который принимает функцию и массив, который описывает для какого аргумента какую проверку типа применять:
|
||||
|
||||
```js run
|
||||
// вспомогательная функция для проверки на число
|
||||
function checkNumber(value) {
|
||||
return typeof value == 'number';
|
||||
}
|
||||
|
||||
// декоратор, проверяющий типы для f
|
||||
// второй аргумент checks - массив с функциями для проверки
|
||||
function typeCheck(f, checks) {
|
||||
return function() {
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
if (!checks[i](arguments[i])) {
|
||||
alert( "Некорректный тип аргумента номер " + i );
|
||||
return;
|
||||
}
|
||||
}
|
||||
return f.apply(this, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
function sum(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
*!*
|
||||
// обернём декоратор для проверки
|
||||
sum = typeCheck(sum, [checkNumber, checkNumber]); // оба аргумента - числа
|
||||
*/!*
|
||||
|
||||
// пользуемся функцией как обычно
|
||||
alert( sum(1, 2) ); // 3, все хорошо
|
||||
|
||||
*!*
|
||||
// а вот так - будет ошибка
|
||||
sum(true, null); // некорректный аргумент номер 0
|
||||
sum(1, ["array", "in", "sum?!?"]); // некорректный аргумент номер 1
|
||||
*/!*
|
||||
```
|
||||
|
||||
Конечно, этот декоратор можно ещё расширять, улучшать, дописывать проверки, но... Вы уже поняли принцип, не правда ли?
|
||||
|
||||
**Один раз пишем декоратор и дальше просто применяем этот функционал везде, где нужно.**
|
||||
|
||||
## Декоратор проверки доступа
|
||||
|
||||
И наконец посмотрим ещё один, последний пример.
|
||||
|
||||
Предположим, у нас есть функция `isAdmin()`, которая возвращает `true`, если у посетителя есть права администратора.
|
||||
|
||||
Можно создать декоратор `checkPermissionDecorator`, который добавляет в любую функцию проверку прав:
|
||||
|
||||
Например, создадим декоратор `checkPermissionDecorator(f)`. Он будет возвращать обертку, которая передает вызов `f` в том случае, если у посетителя достаточно прав:
|
||||
The syntax is:
|
||||
|
||||
```js
|
||||
function checkPermissionDecorator(f) {
|
||||
return function() {
|
||||
if (isAdmin()) {
|
||||
return f.apply(this, arguments);
|
||||
}
|
||||
alert( 'Недостаточно прав' );
|
||||
func.call(context, arg1, arg2, ...)
|
||||
```
|
||||
|
||||
It runs the `func` providing the first argument as `this`, and the next as the arguments.
|
||||
|
||||
For instance, in the code below `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" };
|
||||
|
||||
say.call( user, "Hello" ); // John: Hello
|
||||
```
|
||||
|
||||
To put it simple, these calls are almost equivalent:
|
||||
```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 `call` also sets `this` to `obj`.
|
||||
|
||||
For our case, the wrapper should pass the context that it gets to the original function:
|
||||
|
||||
|
||||
```js run
|
||||
let obj = {
|
||||
random() {
|
||||
return Math.random(); // actually, there is a scary CPU-heavy task here
|
||||
},
|
||||
|
||||
slow(x) {
|
||||
return this.random() * x;
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
*/!*
|
||||
cache.set(x, result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
obj.slow = cachingDecorator(obj.slow); // now make it caching
|
||||
|
||||
alert( obj.slow(2) ); // works
|
||||
alert( obj.slow(2) ); // same (cached)
|
||||
```
|
||||
|
||||
Now everything is fine.
|
||||
|
||||
To prevent misunderstandings, let's see more deeply into what's going on during `obj.slow(2)` call:
|
||||
|
||||
1. After the decoration `obj.slow` is now the wrapper `function (x) { ... }`.
|
||||
2. So when `obj.slow(2)` is executed, the wrapper gets `this` set to `obj` (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` (`=obj`) and the current argument further 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 this `obj.slow`?
|
||||
|
||||
```js
|
||||
let obj = {
|
||||
slow(min, max) {
|
||||
// returns a random number from min to max
|
||||
return min + Math.random() * (max - min); // scary CPU-hogger is assumed
|
||||
}
|
||||
};
|
||||
|
||||
// should remember same-argument calls
|
||||
obj.slow = cachingDecorator(obj.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, 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*. The native `Map` takes single value only as the key.
|
||||
|
||||
There are many solutions possible:
|
||||
|
||||
1. Implement (or use a third-party) a new map-like data structure that is more versatile and allows such 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. Allow to provide a *hashing function* for the decorator, that knows how to make a one value from many.
|
||||
|
||||
For our particular case and for many practical applications, the 3rd variant is good enough.
|
||||
|
||||
The second 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.
|
||||
|
||||
```smart header="Reminder: array-likes and iterables"
|
||||
Array-like objects are those that have `length` and indexed properties. Iterables are those that have `Symbol.iterator` method.
|
||||
|
||||
Arrays are both array-like and iterable. More on that in the chapter <info:iterable>.
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
*!*
|
||||
say.apply(user, messageData); // [10:00] John: Hello (this=user)
|
||||
*/!*
|
||||
```
|
||||
|
||||
Actually, 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.
|
||||
|
||||
These two calls do the same:
|
||||
|
||||
```js
|
||||
let args = [1, 2, 3];
|
||||
|
||||
*!*
|
||||
func.call(context, ...args);
|
||||
func.apply(context, args);
|
||||
*/!*
|
||||
```
|
||||
|
||||
If we come closer, 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, where we expect an iterable, `call` works, where we expect an array-like, `apply` works.
|
||||
|
||||
If something is both iterable and array-like, like a real array, then we should use `apply`, because it is is more optimized internally than `call + spread`.
|
||||
|
||||
The universal "wrapper formula" that allows to pass everything to another function is:
|
||||
|
||||
```js
|
||||
let wrapper = function() {
|
||||
return anotherFunction.apply(this, arguments);
|
||||
};
|
||||
```
|
||||
|
||||
That's called *call forwarding*. The `wrapper` passes everything it gets: context 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.
|
||||
|
||||
Finally, the all-powerful `cachingDecorator`:
|
||||
|
||||
```js run
|
||||
let obj = {
|
||||
slow(min, max) {
|
||||
return min + Math.random() * (max - min);
|
||||
}
|
||||
};
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
obj.slow = cachingDecorator(obj.slow, hash);
|
||||
|
||||
alert( obj.slow(3, 5) ); // works
|
||||
alert( obj.slow(3, 5) ); // same (cached)
|
||||
|
||||
alert( obj.slow(2, 5) ); // another value
|
||||
```
|
||||
|
||||
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 "glue" function that turns `(3, 5)` into 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
|
||||
|
||||
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`.
|
||||
|
||||
```js no-beautify
|
||||
function save() { ... }
|
||||
The natural solution would be to use [arr.join](mdn:js/Array/join) method:
|
||||
|
||||
save = checkPermissionDecorator(save);
|
||||
// Теперь вызов функции save() проверяет права
|
||||
```js
|
||||
function hash(args) {
|
||||
return args.join();
|
||||
}
|
||||
```
|
||||
|
||||
## Итого
|
||||
...Unfortunately, that won't work. Because `hash(arguments)` receives `arguments` object, and it is both iterable and array-like, but not a real array.
|
||||
|
||||
Декоратор -- это обёртка над функцией, которая модифицирует её поведение. При этом основную работу по-прежнему выполняет функция.
|
||||
So calling `join` on it fails:
|
||||
|
||||
**Декораторы можно не только повторно использовать, но и комбинировать!**
|
||||
```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`.
|
||||
|
||||
The internal algorithm of native method `[].join` is very simple. It's like this:
|
||||
|
||||
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`.
|
||||
5. Append `this[1]`.
|
||||
6. Append `glue`.
|
||||
7. Append `this[2]`
|
||||
8. ...Until `this.length` items are glued.
|
||||
9. Return `result`.
|
||||
|
||||
For arrays `arr.join()` has `this=arr`, so it joins the array `arr`.
|
||||
|
||||
But it is written in a way that allows any array-like `this` (not a coincidence, many methods follow this practice), so 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.
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
Binary file not shown.
After Width: | Height: | Size: 64 KiB |
|
@ -1,15 +0,0 @@
|
|||
<script>
|
||||
function timingDecorator(f) {
|
||||
return function() {
|
||||
var d = new Date();
|
||||
var result = f.apply(this, arguments);
|
||||
console.log("Функция заняла: " + (new Date - d) + "мс");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
function bind(func, context) {
|
||||
return function() {
|
||||
return func.apply(context, arguments);
|
||||
};
|
||||
}
|
||||
</script>
|
|
@ -1,3 +1,8 @@
|
|||
libs:
|
||||
- lodash
|
||||
|
||||
---
|
||||
|
||||
# Function bind method to fix this
|
||||
|
||||
When using `setTimeout` with object methods or passing object methods along, there's a known problem: "loosing `this`".
|
||||
|
@ -275,6 +280,49 @@ function die() {
|
|||
}
|
||||
```
|
||||
|
||||
|
||||
## Currying [todo]
|
||||
|
||||
Sometimes people mix up partial function application mentioned above with another thing named "currying".
|
||||
|
||||
todo: _.partial that fixes argument, not context
|
||||
|
||||
Currying is a transform that makes function of multiple arguments `f(a, b, c)` callable as `f(a)(b)(c)`, for instance:
|
||||
|
||||
```js run
|
||||
function log(date, importance, message) {
|
||||
console.log(`${date.getTime()} [${importance}] ${message}`);
|
||||
}
|
||||
|
||||
log(new Date(), "DEBUG", "3-arg call");
|
||||
|
||||
log = _.curry(log);
|
||||
|
||||
// still works this way
|
||||
log(new Date(), "DEBUG", "3-arg call");
|
||||
|
||||
// but also can call it like this:
|
||||
log(new Date())("DEBUG")("3-arg call");
|
||||
|
||||
// or get a convenience function:
|
||||
let todayLog = log(new Date());
|
||||
|
||||
todayLog("INFO", "2-arg call");
|
||||
|
||||
// or get a convenience function:
|
||||
let todayDebug = todayLog("DEBUG");
|
||||
|
||||
todayDebug("1-arg call");
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
To safely pass an object method to `setTimeout`, we can bind the context to it.
|
||||
|
@ -288,4 +336,3 @@ let bound = func.bind(context, arg1, arg2...)
|
|||
The result is an "exotic object" that passes all calls to `func`, fixing context and arguments (if provided).
|
||||
|
||||
We can use it to fix the context (most often case), or also some of the arguments.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue