This commit is contained in:
Ilya Kantor 2016-07-26 13:18:59 +03:00
parent 8bf36b7b5f
commit 276065baf1
29 changed files with 715 additions and 71 deletions

View file

@ -1,58 +0,0 @@
importance: 5
---
# Использование функции вопросов с каррингом
Эта задача -- усложнённый вариант задачи <info:task/question-use-bind>. В ней объект `user` изменён.
Теперь заменим две функции `user.loginOk()` и `user.loginFail()` на единый метод: `user.loginDone(true/false)`, который нужно вызвать с `true` при верном ответе и `fail` -- при неверном.
Код ниже делает это, соответствующий фрагмент выделен.
**Сейчас он обладает важным недостатком: при записи в `user` другого значения объект перестанет корректно работать, вы увидите это, запустив пример ниже (будет ошибка).**
Как бы вы написали правильно?
**Исправьте выделенный фрагмент, чтобы код заработал.**
```js run
"use strict";
function ask(question, answer, ok, fail) {
var result = prompt(question, '');
if (result.toLowerCase() == answer.toLowerCase()) ok();
else fail();
}
var user = {
login: 'Василий',
password: '12345',
// метод для вызова из ask
loginDone: function(result) {
alert( this.login + (result ? ' вошёл в сайт' : ' ошибка входа') );
},
checkPassword: function() {
*!*
ask("Ваш пароль?", this.password,
function() {
user.loginDone(true);
},
function() {
user.loginDone(false);
}
);
*/!*
}
};
var vasya = user;
user = null;
vasya.checkPassword();
```
Изменения должны касаться только выделенного фрагмента.
Если возможно, предложите два решения, одно -- с использованием `bind`, другое -- без него. Какое решение лучше?

View file

@ -0,0 +1,9 @@
function makeLogging(f, log) {
function wrapper(a) {
log.push(a);
return f.call(this, a);
}
return wrapper;
}

View file

@ -0,0 +1,50 @@
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));
});
});

View file

@ -0,0 +1,40 @@
Возвратим декоратор `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`.

View file

@ -0,0 +1,34 @@
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"
*/!*
}
```

View file

@ -0,0 +1,9 @@
function makeLogging(f, log) {
function wrapper() {
log.push([].slice.call(arguments));
return f.apply(this, arguments);
}
return wrapper;
}

View file

@ -0,0 +1,55 @@
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));
});
});

View file

@ -0,0 +1,33 @@
Решение аналогично задаче <info:task/logging-decorator>, разница в том, что в лог вместо одного аргумента идет весь объект `arguments`.
Для передачи вызова с произвольным количеством аргументов используем `f.apply(this, arguments)`.
```js run
function work(a, b) {
alert( a + b ); // work - произвольная функция
}
function makeLogging(f, log) {
*!*
function wrapper() {
log.push([].slice.call(arguments));
return f.apply(this, arguments);
}
*/!*
return wrapper;
}
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"
}
```

View file

@ -0,0 +1,31 @@
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"
}
```

View file

@ -0,0 +1,11 @@
function makeCaching(f) {
var cache = {};
return function(x) {
if (!(x in cache)) {
cache[x] = f.call(this, x);
}
return cache[x];
};
}

View file

@ -0,0 +1,31 @@
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));
});
});

View file

@ -0,0 +1,33 @@
Запоминать результаты вызова функции будем в замыкании, в объекте `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` и т.п.

View file

@ -0,0 +1,34 @@
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, другой аргумент => другое значение
```

View file

@ -0,0 +1,267 @@
# Decorators and forwarding, call/apply
Javascript has exceptionally flexible functions. They can be passed around, used as objects, and now we will see how one function can pass its call to another one transparently. We'll use it to wrap new functionality around existing functions.
[cut]
## Transparent caching
Let's say we have a function `slow(x)` which is CPU-heavy, but for same `x` it always returns the same value.
Naturally, we'd want to cache (remember) the result, so that on the same future call we can return it without executing the function.
But caching is something orthogonal to the function itself. So instead of mixing it into the function itself, we'll write a *caching decorator*.
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
}
function makeCaching(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;
};
}
slow = makeCaching(slow);
alert( slow(1) ); // slow(1) is cached
alert( slow(1) ); // the same
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.
Here it returns a "wrapper": `function(x)` that "wraps" the call of `func(x)` `(*)`. As you can see from t
## 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:
```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 timers = {}; // timers to store data
function timingDecorator(func) {
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;
}
*!*
// использование: завернём fibonacci в декоратор
fibonacci = timingDecorator(fibonacci, "fibo");
*/!*
// неоднократные вызовы...
alert( fibonacci(10) ); // 55
alert( fibonacci(20) ); // 6765
// ...
*!*
// в любой момент можно получить общее количество времени на вызовы
alert( timers.fibo + 'мс' );
*/!*
```
Обратим внимание на строку `(*)` внутри декоратора, которая и осуществляет передачу вызова:
```js
var result = f.apply(this, arguments); // (*)
```
Этот приём называется "форвардинг вызова" (от англ. forwarding): текущий контекст и аргументы через `apply` передаются в функцию `f`, так что изнутри `f` всё выглядит так, как была вызвана она напрямую, а не декоратор.
## Декоратор для проверки типа
В 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` в том случае, если у посетителя достаточно прав:
```js
function checkPermissionDecorator(f) {
return function() {
if (isAdmin()) {
return f.apply(this, arguments);
}
alert( 'Недостаточно прав' );
}
}
```
Использование декоратора:
```js no-beautify
function save() { ... }
save = checkPermissionDecorator(save);
// Теперь вызов функции save() проверяет права
```
## Итого
Декоратор -- это обёртка над функцией, которая модифицирует её поведение. При этом основную работу по-прежнему выполняет функция.
**Декораторы можно не только повторно использовать, но и комбинировать!**
Это кардинально повышает их выразительную силу. Декораторы можно рассматривать как своего рода "фичи" или возможности, которые можно "нацепить" на любую функцию. Можно один, а можно несколько.
Скажем, используя декораторы, описанные выше, можно добавить к функции возможности по проверке типов данных, замеру времени и проверке доступа буквально одной строкой, не залезая при этом в её код, то есть (!) не увеличивая его сложность.
Предлагаю вашему вниманию задачи, которые помогут выяснить, насколько вы разобрались в декораторах. Далее в учебнике мы ещё встретимся с ними.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -0,0 +1,15 @@
<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>

View file

@ -1,3 +1,4 @@
# Решение с bind
Первое решение -- передать в `ask` функции с привязанным контекстом и аргументами.

View file

@ -0,0 +1,49 @@
importance: 5
---
# Partial application for login [todo solution]
The task is a little more complex variant of <info:task/question-use-bind>.
We have the same `ask` function, prompting for password. But instead of `user.loginOk()` and `user.loginFail()`, now there's a single method: `user.loginDone(true/false)`, that should be called with `true` for correct input and `false` otherwise.
There's a full code below.
Right now, there's a problem. When `user` variable is overwritten by another value, errors start to appear in the highlighted fragment (run it).
How to write it right?
```js run
function ask(question, answer, ok, fail) {
var result = prompt(question, '');
if (result == answer) ok();
else fail();
}
let user = {
login: 'John',
password: '12345',
// метод для вызова из ask
loginDone(result) {
alert( this.login + (result ? ' logged in' : ' failed to log in') );
},
checkPassword() {
*!*
ask("Your password?", this.password,
()=>user.loginDone(true)
()=>user.loginDone(false)
);
*/!*
}
};
let john = user;
user = null;
john.checkPassword();
```
Your changes should only modify the highlighted fragment.

View file

@ -1,4 +1,4 @@
# Binding this
# Function bind method to fix this
When using `setTimeout` with object methods or passing object methods along, there's a known problem: "loosing `this`".
@ -83,7 +83,7 @@ Surely, we could write code more carefully. But it would be better if we could j
## Solution 2: bind
There is a method [bind](mdn:js/Function/bind) that can bind `this` and arguments to a function.
Functions provide a built-in method [bind](mdn:js/Function/bind) that allows to fix `this`.
The syntax is:
@ -92,9 +92,9 @@ The syntax is:
let bound = func.bind(context);
````
The result of `func.bind` is a special "exotic object", that is essentially a function, but instead of having its own body, it transparently calls `func` with given arguments and `this=context`.
The result of `func.bind` is a special "exotic object", that is essentially a function, but instead of having its own body, it transparently passes the call to `func` with given arguments and `this=context`.
For instance, here `g` calls `func` in the context of `user`:
For instance, here `funcUser` passes a call to `func` with `this=user`:
```js run
let user = {
@ -106,8 +106,8 @@ function func() {
}
*!*
let g = func.bind(user);
g(); // John
let funcUser = func.bind(user);
funcUser(); // John
*/!*
```
@ -125,9 +125,9 @@ function func(phrase) {
alert(phrase + ', ' + this.firstName);
}
let g = func.bind(user); // g passes all calls to func with this=user
let funcUser = func.bind(user); // funcUser passes all calls to func with this=user
*!*
g("Hello"); // Hello, John (argument "Hello" is passed as is)
funcUser("Hello"); // Hello, John (argument "Hello" is passed as is)
*/!*
```
@ -151,9 +151,9 @@ sayHi(); // Hello, John!
setTimeout(sayHi, 1000); // Hello, John!
```
In the line `(*)` we take the method `user.sayHi` and bind it to `user`. The `sayHi` is a "bound" function, that can be called or passed to `setTimeout`.
In the line `(*)` we take the method `user.sayHi` and bind it to `user`. The `sayHi` is a "bound" function, that can be called alone or passed to `setTimeout` -- doesn't matter, the context will be right.
It also works with arguments:
It fixes only `this`, arguments are passed "as is":
```js run
let user = {
@ -165,8 +165,8 @@ let user = {
let say = user.say.bind(user);
say("Hello"); // Hello, John
say("Bye"); // Hello, John
say("Hello"); // Hello, John ("Hello" argument is passed to say)
say("Bye"); // Bye, John ("Bye" is passed to say)
```
````smart header="Convenience method: `bindAll`"
@ -180,7 +180,7 @@ for (let key in user) {
}
```
Javascript libraries also provide functions for that, e.g. [_.bindAll(obj)](http://lodash.com/docs#bindAll) in lodash.
Javascript libraries also provide functions for convenient mass binding , e.g. [_.bindAll(obj)](http://lodash.com/docs#bindAll) in lodash.
````
## Partial application