ok
This commit is contained in:
parent
1c0bebec47
commit
8bf36b7b5f
16 changed files with 639 additions and 172 deletions
|
@ -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 раз по окончании работы функции. Такое поведение кросс-браузерно.
|
|
|
@ -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();
|
|
||||||
```
|
|
||||||
|
|
|
@ -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` мс. Получается чаще.
|
|
|
@ -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);
|
|
||||||
```
|
|
||||||
|
|
|
@ -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`.
|
20
1-js/7-deeper/6-bind/2-write-to-object-after-bind/task.md
Normal file
20
1-js/7-deeper/6-bind/2-write-to-object-after-bind/task.md
Normal 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();
|
||||||
|
```
|
||||||
|
|
15
1-js/7-deeper/6-bind/3-second-bind/solution.md
Normal file
15
1-js/7-deeper/6-bind/3-second-bind/solution.md
Normal 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.
|
20
1-js/7-deeper/6-bind/3-second-bind/task.md
Normal file
20
1-js/7-deeper/6-bind/3-second-bind/task.md
Normal 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();
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
The answer: `undefined`.
|
||||||
|
|
||||||
|
The result of `bind` is another object. It does not have the `test` property.
|
||||||
|
|
23
1-js/7-deeper/6-bind/4-function-property-after-bind/task.md
Normal file
23
1-js/7-deeper/6-bind/4-function-property-after-bind/task.md
Normal 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?
|
||||||
|
*/!*
|
||||||
|
```
|
||||||
|
|
49
1-js/7-deeper/6-bind/5-question-use-bind/solution.md
Normal file
49
1-js/7-deeper/6-bind/5-question-use-bind/solution.md
Normal 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.
|
||||||
|
|
||||||
|
|
49
1-js/7-deeper/6-bind/5-question-use-bind/task.md
Normal file
49
1-js/7-deeper/6-bind/5-question-use-bind/task.md
Normal 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();
|
||||||
|
```
|
||||||
|
|
71
1-js/7-deeper/6-bind/6-ask-currying/solution.md
Normal file
71
1-js/7-deeper/6-bind/6-ask-currying/solution.md
Normal 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` короче.
|
58
1-js/7-deeper/6-bind/6-ask-currying/task.md
Normal file
58
1-js/7-deeper/6-bind/6-ask-currying/task.md
Normal 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`, другое -- без него. Какое решение лучше?
|
291
1-js/7-deeper/6-bind/article.md
Normal file
291
1-js/7-deeper/6-bind/article.md
Normal 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.
|
||||||
|
|
21
1-js/7-deeper/6-bind/head.html
Normal file
21
1-js/7-deeper/6-bind/head.html
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue