diff --git a/1-js/7-deeper/5-settimeout-setinterval/5-setinterval-result/solution.md b/1-js/7-deeper/5-settimeout-setinterval/5-setinterval-result/solution.md deleted file mode 100644 index 220456d0..00000000 --- a/1-js/7-deeper/5-settimeout-setinterval/5-setinterval-result/solution.md +++ /dev/null @@ -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 раз по окончании работы функции. Такое поведение кросс-браузерно. \ No newline at end of file diff --git a/1-js/7-deeper/5-settimeout-setinterval/5-setinterval-result/task.md b/1-js/7-deeper/5-settimeout-setinterval/5-setinterval-result/task.md deleted file mode 100644 index b7c16b70..00000000 --- a/1-js/7-deeper/5-settimeout-setinterval/5-setinterval-result/task.md +++ /dev/null @@ -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(); -``` - diff --git a/1-js/7-deeper/5-settimeout-setinterval/6-who-runs-faster/solution.md b/1-js/7-deeper/5-settimeout-setinterval/6-who-runs-faster/solution.md deleted file mode 100644 index 3042ff78..00000000 --- a/1-js/7-deeper/5-settimeout-setinterval/6-who-runs-faster/solution.md +++ /dev/null @@ -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` мс. Получается чаще. \ No newline at end of file diff --git a/1-js/7-deeper/5-settimeout-setinterval/6-who-runs-faster/task.md b/1-js/7-deeper/5-settimeout-setinterval/6-who-runs-faster/task.md deleted file mode 100644 index 6b9c4048..00000000 --- a/1-js/7-deeper/5-settimeout-setinterval/6-who-runs-faster/task.md +++ /dev/null @@ -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); -``` - diff --git a/1-js/7-deeper/6-bind/2-write-to-object-after-bind/solution.md b/1-js/7-deeper/6-bind/2-write-to-object-after-bind/solution.md new file mode 100644 index 00000000..7546f670 --- /dev/null +++ b/1-js/7-deeper/6-bind/2-write-to-object-after-bind/solution.md @@ -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`. diff --git a/1-js/7-deeper/6-bind/2-write-to-object-after-bind/task.md b/1-js/7-deeper/6-bind/2-write-to-object-after-bind/task.md new file mode 100644 index 00000000..8023bcd4 --- /dev/null +++ b/1-js/7-deeper/6-bind/2-write-to-object-after-bind/task.md @@ -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(); +``` + diff --git a/1-js/7-deeper/6-bind/3-second-bind/solution.md b/1-js/7-deeper/6-bind/3-second-bind/solution.md new file mode 100644 index 00000000..f293b072 --- /dev/null +++ b/1-js/7-deeper/6-bind/3-second-bind/solution.md @@ -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. diff --git a/1-js/7-deeper/6-bind/3-second-bind/task.md b/1-js/7-deeper/6-bind/3-second-bind/task.md new file mode 100644 index 00000000..5daf053c --- /dev/null +++ b/1-js/7-deeper/6-bind/3-second-bind/task.md @@ -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(); +``` + diff --git a/1-js/7-deeper/6-bind/4-function-property-after-bind/solution.md b/1-js/7-deeper/6-bind/4-function-property-after-bind/solution.md new file mode 100644 index 00000000..181555d9 --- /dev/null +++ b/1-js/7-deeper/6-bind/4-function-property-after-bind/solution.md @@ -0,0 +1,4 @@ +The answer: `undefined`. + +The result of `bind` is another object. It does not have the `test` property. + diff --git a/1-js/7-deeper/6-bind/4-function-property-after-bind/task.md b/1-js/7-deeper/6-bind/4-function-property-after-bind/task.md new file mode 100644 index 00000000..8cd18ec5 --- /dev/null +++ b/1-js/7-deeper/6-bind/4-function-property-after-bind/task.md @@ -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? +*/!* +``` + diff --git a/1-js/7-deeper/6-bind/5-question-use-bind/solution.md b/1-js/7-deeper/6-bind/5-question-use-bind/solution.md new file mode 100644 index 00000000..e047efa0 --- /dev/null +++ b/1-js/7-deeper/6-bind/5-question-use-bind/solution.md @@ -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. + + diff --git a/1-js/7-deeper/6-bind/5-question-use-bind/task.md b/1-js/7-deeper/6-bind/5-question-use-bind/task.md new file mode 100644 index 00000000..6c9f8c91 --- /dev/null +++ b/1-js/7-deeper/6-bind/5-question-use-bind/task.md @@ -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(); +``` + diff --git a/1-js/7-deeper/6-bind/6-ask-currying/solution.md b/1-js/7-deeper/6-bind/6-ask-currying/solution.md new file mode 100644 index 00000000..e2e5cbad --- /dev/null +++ b/1-js/7-deeper/6-bind/6-ask-currying/solution.md @@ -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` короче. \ No newline at end of file diff --git a/1-js/7-deeper/6-bind/6-ask-currying/task.md b/1-js/7-deeper/6-bind/6-ask-currying/task.md new file mode 100644 index 00000000..c5a12fc7 --- /dev/null +++ b/1-js/7-deeper/6-bind/6-ask-currying/task.md @@ -0,0 +1,58 @@ +importance: 5 + +--- + +# Использование функции вопросов с каррингом + +Эта задача -- усложнённый вариант задачи . В ней объект `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`, другое -- без него. Какое решение лучше? diff --git a/1-js/7-deeper/6-bind/article.md b/1-js/7-deeper/6-bind/article.md new file mode 100644 index 00000000..672c2b49 --- /dev/null +++ b/1-js/7-deeper/6-bind/article.md @@ -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. + diff --git a/1-js/7-deeper/6-bind/head.html b/1-js/7-deeper/6-bind/head.html new file mode 100644 index 00000000..dd262f3f --- /dev/null +++ b/1-js/7-deeper/6-bind/head.html @@ -0,0 +1,21 @@ + \ No newline at end of file