This commit is contained in:
Ilya Kantor 2016-07-25 13:04:53 +03:00
parent 1c0bebec47
commit 8bf36b7b5f
16 changed files with 639 additions and 172 deletions

View file

@ -1,36 +0,0 @@
Вызов `alert(i)` в `setTimeout` введет `100000001`.
Можете проверить это запуском:
```js run
var timer = setInterval(function() {
i++;
}, 10);
setTimeout(function() {
clearInterval(timer);
*!*
alert( i ); // (*)
*/!*
}, 50);
var i;
function f() {
// точное время выполнения не играет роли
// здесь оно заведомо больше 100 мс
for (i = 0; i < 1e8; i++) f[i % 2] = i;
}
f();
```
Правильный вариант срабатывания: **3** (сразу же по окончании `f` один раз).
Планирование `setInterval` будет вызывать функцию каждые `10 мс` после текущего времени. Но так как интерпретатор занят долгой функцией, то до конца ее работы никакого вызова не происходит.
За время выполнения `f` может пройти время, на которое запланированы несколько вызовов `setInterval`, но в этом случае остается только один, т.е. накопления вызовов не происходит. Такова логика работы `setInterval`.
После окончания текущего скрипта интерпретатор обращается к очереди запланированных вызовов, видит в ней `setInterval` и выполняет. А затем тут же выполняется `setTimeout`, очередь которого тут же подошла.
Итого, как раз и видим, что `setInterval` выполнился ровно 1 раз по окончании работы функции. Такое поведение кросс-браузерно.

View file

@ -1,45 +0,0 @@
importance: 5
---
# Что выведет после setInterval?
В коде ниже запускается `setInterval` каждые 10 мс, и через 50 мс запланирована его отмена.
После этого запущена тяжёлая функция `f`, выполнение которой (мы точно знаем) потребует более 100 мс.
Сработает ли `setInterval`, как и когда?
Варианты:
1. Да, несколько раз, *в процессе* выполнения `f`.
2. Да, несколько раз, *сразу после* выполнения `f`.
3. Да, один раз, *сразу после* выполнения `f`.
4. Нет, не сработает.
5. Может быть по-разному, как повезёт.
Что выведет `alert` в строке `(*)`?
```js
var i;
var timer = setInterval(function() { // планируем setInterval каждые 10 мс
i++;
}, 10);
setTimeout(function() { // через 50 мс - отмена setInterval
clearInterval(timer);
*!*
alert( i ); // (*)
*/!*
}, 50);
// и запускаем тяжёлую функцию
function f() {
// точное время выполнения не играет роли
// здесь оно заведомо больше 100 мс
for (i = 0; i < 1e8; i++) f[i % 2] = i;
}
f();
```

View file

@ -1,54 +0,0 @@
Задача -- с небольшим "нюансом".
Есть браузеры, в которых на время работы JavaScript таймер "застывает", например таков IE. В них количество шагов будет почти одинаковым, +-1.
В других браузерах (Chrome) первый бегун будет быстрее.
Создадим реальные объекты `Runner` и запустим их для проверки:
```js run
function Runner() {
this.steps = 0;
this.step = function() {
this.doSomethingHeavy();
this.steps++;
};
function fib(n) {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
this.doSomethingHeavy = function() {
for (var i = 0; i < 25; i++) {
this[i] = fib(i);
}
};
}
var runner1 = new Runner();
var runner2 = new Runner();
// запускаем бегунов
var t1 = setInterval(function() {
runner1.step();
}, 15);
var t2 = setTimeout(function go() {
runner2.step();
t2 = setTimeout(go, 15);
}, 15);
// кто сделает больше шагов?
setTimeout(function() {
clearInterval(t1);
clearTimeout(t2);
alert( runner1.steps );
alert( runner2.steps );
}, 5000);
```
Если бы в шаге `step()` не было вызова `doSomethingHeavy()`, то есть он бы не требовал времени, то количество шагов было бы почти равным.
Но так как у нас шаг, всё же, что-то делает, и функция `doSomethingHeavy()` специально написана таким образом, что она требует (небольшого) времени, то первый бегун успеет сделать больше шагов. Ведь в `setTimeout` пауза `15` мс будет *между* шагами, а `setInterval` шагает равномерно, каждые `15` мс. Получается чаще.

View file

@ -1,37 +0,0 @@
importance: 5
---
# Кто быстрее?
Есть два бегуна:
```js
var runner1 = new Runner();
var runner2 = new Runner();
```
У каждого есть метод `step()`, который делает шаг, увеличивая свойство `steps`.
Конкретный код метода `step()` не имеет значения, важно лишь что шаг делается не мгновенно, он требует небольшого времени.
Если запустить первого бегуна через `setInterval`, а второго -- через вложенный `setTimeout` -- какой сделает больше шагов за 5 секунд?
```js
// первый?
setInterval(function() {
runner1.step();
}, 15);
// или второй?
setTimeout(function go() {
runner2.step();
setTimeout(go, 15);
}, 15);
setTimeout(function() {
alert( runner1.steps );
alert( runner2.steps );
}, 5000);
```

View file

@ -0,0 +1,18 @@
The answer: `null`.
```js run
function f() {
alert( this ); // null
}
var user = {
g: f.bind(null)
};
user.g();
```
The context of a bound function is hard-fixed. There's just no way to further change it.
So even while we run `user.g()`, the original function is called with `this=null`.

View file

@ -0,0 +1,20 @@
importance: 5
---
# Bound function as a method
What will be the output?
```js
function f() {
alert( this ); // ?
}
var user = {
g: f.bind(null)
};
user.g();
```

View file

@ -0,0 +1,15 @@
The answer: **John**.
```js run no-beautify
function f() {
alert(this.name);
}
f = f.bind( {name: "John"} ).bind( {name: "Pete"} );
f(); // John
```
The exotic [bound function](https://tc39.github.io/ecma262/#sec-bound-function-exotic-objects) object returned by `f.bind(...)` remembers the context (and arguments if provided) only at the creation time.
A function cannot be re-bound.

View file

@ -0,0 +1,20 @@
importance: 5
---
# Second bind
Can we change `this` by additional binding?
What will be the output?
```js no-beautify
function f() {
alert(this.name);
}
f = f.bind( {name: "John"} ).bind( {name: "Ann" } );
f();
```

View file

@ -0,0 +1,4 @@
The answer: `undefined`.
The result of `bind` is another object. It does not have the `test` property.

View file

@ -0,0 +1,23 @@
importance: 5
---
# Function property after bind
There's a value in the property of a function. Will it change after `bind`? Why, elaborate?
```js run
function sayHi() {
alert( this.name );
}
sayHi.test = 5;
*!*
let bound = sayHi.bind({
name: "John"
});
alert( bound.test ); // what will be the output? why?
*/!*
```

View file

@ -0,0 +1,49 @@
The error occurs because `ask` gets functions `loginOk/loginFail` without the object.
When it calls them, they naturally assume `this=undefined`.
Let's `bind` the context:
```js run
function ask(question, answer, ok, fail) {
var result = prompt(question, '');
if (result == answer) ok();
else fail();
}
let user = {
login: 'John',
password: '12345',
loginOk() {
alert(`${this.login} logged in`);
},
loginFail() {
alert(`${this.login} failed to log in`);
},
checkPassword() {
*!*
ask("Your password?", this.password, this.loginOk.bind(this), this.loginFail.bind(this));
*/!*
}
};
let john = user;
user = null;
john.checkPassword();
```
Now it works.
An alternative solution could be:
```js
//...
ask("Your password?", this.password, () => user.loginOk(), () => user.loginFail());
```
...But that code would fail if `user` becomes overwritten.

View file

@ -0,0 +1,49 @@
importance: 5
---
# Ask loosing this
The call to `user.checkPassword()` in the code below should `ask` for password and call `loginOk/loginFail` depending on the answer.
But it leads to an error. Why?
Fix the highlighted line for everything to start working right (other lines are not to be changed).
```js run
function ask(question, answer, ok, fail) {
let result = prompt(question, '');
if (result == answer) ok();
else fail();
}
let user = {
login: 'John',
password: '12345',
loginOk() {
alert(`${this.login} logged in`);
},
loginFail() {
alert(`${this.login} failed to log in`);
},
checkPassword() {
*!*
ask("Your password?", this.password, this.loginOk, this.loginFail);
*/!*
}
};
user.checkPassword();
```
P.S. Your solution should also work if `user` gets overwritten. For instance, the last lines instead of `user.checkPassword()` would be:
```js
let john = user;
user = null;
john.checkPassword();
```

View file

@ -0,0 +1,71 @@
# Решение с bind
Первое решение -- передать в `ask` функции с привязанным контекстом и аргументами.
```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',
loginDone: function(result) {
alert( this.login + (result ? ' вошёл в сайт' : ' ошибка входа') );
},
checkPassword: function() {
*!*
ask("Ваш пароль?", this.password, this.loginDone.bind(this, true), this.loginDone.bind(this, false));
*/!*
}
};
user.checkPassword();
```
# Решение с локальной переменной
Второе решение -- это скопировать `this` в локальную переменную (чтобы внешняя перезапись не повлияла):
```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',
loginDone: function(result) {
alert( this.login + (result ? ' вошёл в сайт' : ' ошибка входа') );
},
checkPassword: function() {
var self = this;
*!*
ask("Ваш пароль?", this.password,
function() {
self.loginDone(true);
},
function() {
self.loginDone(false);
}
);
*/!*
}
};
user.checkPassword();
```
Оба решения хороши, вариант с `bind` короче.

View file

@ -0,0 +1,58 @@
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,291 @@
# Binding this
When using `setTimeout` with object methods or passing object methods along, there's a known problem: "loosing `this`".
Suddenly, `this` just stops working right. The situation is typical for novice developers, but happens with experienced ones as well.
[cut]
## Loosing "this": demo
Let's see the problem on example.
Normally, `setTimeout` plans the function to execute after the delay.
Here it says "Hello" after 1 second:
```js run
setTimeout(() => alert("Hello, John!"), 1000);
```
Now let's do the same with an object method:
```js run
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
*!*
setTimeout(user.sayHi, 1000); // Hello, undefined!
*/!*
```
As we can see, the output shows not "John" as `this.firstName`, but `undefined`!
That's because `setTimeout` got the function `user.sayHi`, separately from the object. The last line can be rewritten as:
```js
let f = user.sayHi;
setTimeout(f, 1000); // lost user context
```
The method `setTimeout` is a little special: it sets `this=window` for the function call. 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 task is quite typical -- we want to pass an object method somewhere else where it will be called. How to make sure that it will be called in the right context?
There are few ways we can go.
## Solution 1: a wrapper
The simplest solution is to use an wrapping function:
```js run
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
*!*
setTimeout(function() {
user.sayHi(); // Hello, John!
}, 1000);
*/!*
```
Now it works, because it receives `user` from the outer lexical environment, and then calls the method normally.
The same function, but shorter:
```js
setTimeout(() => user.sayHi(), 1000); // Hello, John!
```
Looks fine, but a slight vulnerability appears in our code structure.
What is before `setTimeout` triggers (one second delay!) `user` will get another value. Then, suddenly, the it will call the wrong object!
Surely, we could write code more carefully. But it would be better if we could just guarantee the right context, no matter what.
## Solution 2: bind
There is a method [bind](mdn:js/Function/bind) that can bind `this` and arguments to a function.
The syntax is:
```js
// more complex syntax will be little later
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`.
For instance, here `g` calls `func` in the context of `user`:
```js run
let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
*!*
let g = func.bind(user);
g(); // John
*/!*
```
We can think of `func.bind(user)` as a "bound" variant of `func`, with fixed `this=user`.
The example below demonstrates that arguments are passed "as is" to `func`:
```js run
let user = {
firstName: "John"
};
function func(phrase) {
alert(phrase + ', ' + this.firstName);
}
let g = func.bind(user); // g passes all calls to func with this=user
*!*
g("Hello"); // Hello, John (argument "Hello" is passed as is)
*/!*
```
Now let's try with an object method:
```js run
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
*!*
let sayHi = user.sayHi.bind(user); // (*)
*/!*
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`.
It also works with arguments:
```js run
let user = {
firstName: "John",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
let say = user.say.bind(user);
say("Hello"); // Hello, John
say("Bye"); // Hello, John
```
````smart header="Convenience method: `bindAll`"
If an object has many methods and we plan to actively pass it around, then we could bind them all in a loop:
```js
for (let key in user) {
if (typeof user[key] == 'function') {
user[key] = user[key].bind(user);
}
}
```
Javascript libraries also provide functions for that, e.g. [_.bindAll(obj)](http://lodash.com/docs#bindAll) in lodash.
````
## Partial application
Till now we were only talking about binding `this`. Now let's make a step further.
We can bind not only `this`, but also arguments. That's more seldom used, but can also be handy.
The full syntax of `bind`:
```js
let bound = func.bind(context, arg1, arg2, ...);
```
It allows to bind context as `this` and starting arguments of the function.
For instance, we have a multiplication function `mul(a, b)`:
```js
function mul(a, b) {
return a * b;
}
```
Let's use `bind` to create a function, doubling the value:
```js run
*!*
let double = mul.bind(null, 2);
*/!*
alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10
```
The call to `mul.bind(null, 2)` creates a new function `double` that passes calls to `mul`, fixing `null` as the context and `2` as the first argument. Further arguments are passed "as is".
That's called [partial function application](https://en.wikipedia.org/wiki/Partial_application) -- we create a new function by fixing some parameters of the existing one.
Please note that here we actually don't use the `this` context. But `bind` requires it first, so we need to pass something like `null`.
The function `triple` in the code below triples the value:
```js run
*!*
let triple = mul.bind(null, 3);
*/!*
alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( triple(5) ); // = mul(3, 5) = 15
```
Why to make a partial function?
Here our benefit is that we created an independent function with a readable name (`double`, `triple`). We can use it and don't write the first argument of every time, cause it's fixed with `bind`.
In other cases, partial application is useful when we have a very generic function, and want a less universal variant of it for convenience.
For instance, we have a function `send(from, to, text)`. Then, inside a `user` object we may want to use a partial variant of it: `sendTo(to, text)` that sends from the current user.
## Функция ask для задач [todo]
В задачах этого раздела предполагается, что объявлена следующая "функция вопросов" `ask`:
```js
function ask(question, answer, ok, fail) {
var result = prompt(question, '');
if (result.toLowerCase() == answer.toLowerCase()) ok();
else fail();
}
```
Её назначение -- задать вопрос `question` и, если ответ совпадёт с `answer`, то запустить функцию `ok()`, а иначе -- функцию `fail()`.
Несмотря на внешнюю простоту, функции такого вида активно используются в реальных проектах. Конечно, они будут сложнее, вместо `alert/prompt` -- вывод красивого JavaScript-диалога с рамочками, кнопочками и так далее, но это нам сейчас не нужно.
Пример использования:
```js run
*!*
ask("Выпустить птичку?", "да", fly, die);
*/!*
function fly() {
alert( 'улетела :)' );
}
function die() {
alert( 'птичку жалко :(' );
}
```
## Summary
To safely pass an object method to `setTimeout`, we can bind the context to it.
The syntax:
```js
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.

View file

@ -0,0 +1,21 @@
<script>
function mul(a, b) {
return a * b;
};
function ask(question, answer, ok, fail) {
var result = prompt(question, '');
if (result.toLowerCase() == answer.toLowerCase()) ok();
else fail();
}
function bind(func, context /*, args*/) {
var bindArgs = [].slice.call(arguments, 2); // (1)
function wrapper() { // (2)
var args = [].slice.call(arguments);
var unshiftArgs = bindArgs.concat(args); // (3)
return func.apply(context, unshiftArgs); // (4)
}
return wrapper;
}
</script>