This commit is contained in:
Ilya Kantor 2014-11-16 01:40:20 +03:00
parent 962caebbb7
commit 87bf53d076
1825 changed files with 94929 additions and 0 deletions

View file

@ -0,0 +1,13 @@
Вызов `arr[2]()` -- это обращение к методу объекта `obj[method]()`, в роли `obj` выступает `arr`, а в роли метода: `2`.
Поэтому, как это бывает при вызове функции как метода, функция `arr[2]` получит `this = arr` и выведет массив:
```js
//+ run
var arr = ["a", "b"];
arr.push( function() { alert(this); } )
arr[2](); // "a","b",function
```

View file

@ -0,0 +1,14 @@
# Вызов в контексте массива
[importance 5]
Каким будет результат? Почему?
```js
var arr = ["a", "b"];
arr.push( function() { alert(this); } )
arr[2](); // ?
```

View file

@ -0,0 +1,24 @@
**Ошибка**!
Попробуйте:
```js
//+ run
var obj = {
go: function() { alert(this) }
}
(obj.go)() // error!
```
Причем сообщение об ошибке в большинстве браузеров не даёт понять, что на самом деле не так.
**Ошибка возникла из-за того, что после объявления `obj` пропущена точка с запятой.**
JavaScript игнорирует перевод строки перед скобкой `(obj.go)()` и читает этот код как:
```js
var obj = { go:... }(obj.go)()
```
Интерпретатор попытается вычислить это выражение, которое обозначает вызов объекта `{ go: ... }` как функции с аргументом `(obj.go)`. При этом, естественно, возникнет ошибка.

View file

@ -0,0 +1,15 @@
# Проверка синтаксиса
[importance 2]
Каков будет результат этого кода?
```js
var obj = {
go: function() { alert(this) }
}
(obj.go)()
```
P.S. Есть подвох :)

View file

@ -0,0 +1,31 @@
<ol>
<li>Обычный вызов функции в контексте объекта.</li>
<li>То же самое, скобки ни на что не влияют.</li>
<li>Здесь не просто вызов `obj.method()`, а более сложный вызов вида `(выражение).method()`. Такой вызов работает, как если бы он был разбит на две строки:
```js
f = obj.go; // сначала вычислить выражение
f(); // потом вызвать то, что получилось
```
При этом `f()` выполняется как обычная функция, без передачи `this`.
</li>
<li>Здесь также слева от точки находится выражение, вызов аналогичен двум строкам.</li>
</ol>
В спецификации это объясняется при помощи специального внутреннего типа [Reference Type](http://es5.github.com/x8.html#x8.7).
Если подробнее -- то `obj.go()` состоит из двух операций:
<ol>
<li>Сначала получить свойство `obj.go`.</li>
<li>Потом вызвать его как функцию.</li>
</ol>
Но откуда на шаге 2 получить `this`? Как раз для этого операция получения свойства `obj.go` возвращает значение особого типа `Reference Type`, который в дополнение к свойству `go` содержит информацию об `obj`. Далее, на втором шаге, вызов его при помощи скобок `()` правильно устанавливает `this`.
**Любые другие операции, кроме вызова, превращают `Reference Type` в обычный тип, в данном случае -- функцию `go` (так уж этот тип устроен).**
Поэтому получается, что `(method = obj.go)` присваивает в переменную `method` функцию `go`, уже без всякой информации об объекте `obj`.
Аналогичная ситуация и в случае `(4)`: оператор ИЛИ `||` делает из `Reference Type` обычную функцию.

View file

@ -0,0 +1,26 @@
# Почему this присваивается именно так?
[importance 3]
Вызовы `(1)` и `(2)` в примере ниже работают не так, как `(3)` и `(4)`:
```js
//+ run
"use strict"
var obj, f;
obj = {
go: function() { alert(this); }
};
obj.go(); // (1) object
(obj.go)(); // (2) object
(method = obj.go)(); // (3) undefined
(obj.go || obj.stop)(); // (4) undefined
```
В чём дело? Объясните логику работы `this`.

View file

@ -0,0 +1,22 @@
**Ответ: пустая строка.**
```js
//+ run
var name = "";
var user = {
name: "Василий",
*!*
export: this // (*)
*/!*
};
alert(user.export.name);
```
Объявление объекта само по себе не влияет на `this`. Никаких функций, которые могли бы повлиять на контекст, здесь нет.
Так как код находится вообще вне любых функций, то `this` в нём равен `window` (при `use strict` было бы `undefined`).
Получается, что в строке `(*)` мы имеем `export: window`, так что далее `alert(user.export.name)` выводит свойство `window.name`, то есть глобальную переменную `name`, которая равна пустой строке.

View file

@ -0,0 +1,18 @@
# Значение this в объявлении объекта
[importance 5]
Что выведет `alert` в этом коде? Почему?
```js
var name = "";
var user = {
name: "Василий",
export: this
};
alert(user.export.name);
```

View file

@ -0,0 +1,5 @@
**Ответ: `Василий`.**
Вызов `user.export()` использует `this`, который равен объекту до точки, то есть внутри `user.export()` строка `return this` возвращает объект `user`.
В итоге выводится свойство `name` объекта `user`, равное `"Василий"`.

View file

@ -0,0 +1,21 @@
# Возврат this
[importance 5]
Что выведет `alert` в этом коде? Почему?
```js
var name = "";
var user = {
name: "Василий",
export: function() {
return this;
}
};
alert(user.export().name);
```

View file

@ -0,0 +1,7 @@
**Ответ: `Василий`.**
Во время выполнения `user.export()` значение `this = user`.
При создании объекта `{ value: this }`, в свойство `value` копируется ссылка на текущий контекст, то есть на `user`.
Получается что `user.export().value == user`.

View file

@ -0,0 +1,23 @@
# Возврат объекта с this
[importance 5]
Что выведет `alert` в этом коде? Почему?
```js
var name = "";
var user = {
name: "Василий",
export: function() {
return {
value: this
};
}
};
alert(user.export().value.name);
```

View file

@ -0,0 +1,15 @@
var calculator = {
sum: function() {
return this.a + this.b;
},
mul: function() {
return this.a * this.b;
},
read: function() {
this.a = +prompt('a?', 0);
this.b = +prompt('b?', 0);
}
}

View file

@ -0,0 +1,22 @@
sinon.stub(window, "prompt");
prompt.onCall(0).returns("2");
prompt.onCall(1).returns("3");
describe("calculator", function() {
before(function() {
calculator.read();
});
it("при вводе 2 и 3 сумма равна 5", function() {
assert.equal( calculator.sum(), 5 );
});
it("при вводе 2 и 3 произведение равно 6", function() {
assert.equal( calculator.mul(), 6 );
});
});
after(function() {
prompt.restore();
});

View file

@ -0,0 +1,24 @@
```js
//+ run demo
var calculator = {
sum: function() {
return this.a + this.b;
},
mul: function() {
return this.a * this.b;
},
read: function() {
this.a = +prompt('a?', 0);
this.b = +prompt('b?', 0);
}
}
calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );
```

View file

@ -0,0 +1,22 @@
# Создайте калькулятор
[importance 5]
Создайте объект `calculator` с тремя методами:
<ul>
<li>`read()` запрашивает `prompt` два значения и сохраняет их как свойства объекта</li>
<li>`sum()` возвращает сумму этих двух значений</li>
<li>`mul()` возвращает произведение этих двух значений</li>
</ul>
```js
var calculator = {
... ваш код...
}
calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );
```
[demo /]

View file

@ -0,0 +1,23 @@
Решение состоит в том, чтобы каждый раз возвращать текущий объект. Это делается добавлением `return this` в конце каждого метода:
```js
//+ run
var ladder = {
step: 0,
up: function() {
this.step++;
return this;
},
down: function() {
this.step--;
return this;
},
showStep: function() {
alert(this.step);
return this;
}
}
ladder.up().up().down().up().down().showStep(); // 1
```

View file

@ -0,0 +1,38 @@
# Цепочка вызовов
[importance 2]
Есть объект "лестница" ladder:
```js
var ladder = {
step: 0,
up: function() { // вверх по лестнице
this.step++;
},
down: function() { // вниз по лестнице
this.step--;
},
showStep: function() { // вывести текущую ступеньку
alert(this.step);
}
};
```
Сейчас, если нужно последовательно вызвать несколько методов объекта, это можно сделать так:
```js
ladder.up();
ladder.up();
ladder.down();
ladder.showStep(); // 1
```
Модифицируйте код методов объекта, чтобы вызовы можно было делать цепочкой, вот так:
```js
ladder.up().up().down().up().down().showStep(); // 1
```
Такой подход называется "чейнинг" (chaining) и используется, например, во фреймворке jQuery.

View file

@ -0,0 +1,228 @@
# Методы объектов, this
До этого мы говорили об объекте лишь как о хранилище значений. Теперь пойдём дальше и поговорим об объектах как о сущностях со своими функциями ("методами").
[cut]
## Методы у объектов
При объявлении объекта можно указать свойство-функцию, например:
```js
//+ run
var user = {
name: 'Василий',
*!*
// метод
*/!*
sayHi: function() {
alert('Привет!');
}
};
*!*
// Вызов
user.sayHi();
*/!*
```
Свойства-функции называют "методами" объектов. Их можно добавлять и удалять в любой момент, в том числе и явным присваиванием:
```js
//+ run
var user = {
name: 'Василий'
};
*!*
user.sayHi = function() { // присвоили метод после создания объекта
alert('Привет!');
};
*/!*
// Вызов метода:
*!*user.sayHi();*/!*
```
## Доступ к объекту через this
Для полноценной работы метод должен иметь доступ к данным объекта. В частности, вызов `user.sayHi()` может захотеть вывести имя пользователя.
**Для доступа к текущему объекту из метода используется ключевое слово `this`**.
Значением `this` является объект перед "точкой", в контексте которого вызван метод, например:
```js
//+ run
var user = {
name: 'Василий',
sayHi: function() {
alert( *!*this.name*/!* );
}
};
user.sayHi(); // sayHi в контексте user
```
Здесь при выполнении функции `user.sayHi()` в `this` будет храниться ссылка на текущий объект `user`.
Вместо `this` внутри `sayHi` можно было бы обратиться к объекту, используя переменную `user`:
```js
...
sayHi: function() {
alert( *!*user.name*/!* );
}
...
```
...Однако, такое решение нестабильно. Если мы решим скопировать объект в другую переменную, например `admin = user`, а в переменную `user` записать что-то другое -- обращение будет совсем не по адресу:
```js
//+ run
var user = {
name: 'Василий',
sayHi: function() {
alert( *!*user.name*/!* ); // приведёт к ошибке
}
};
var admin = user;
user = null;
admin.sayHi(); // упс! внутри sayHi обращение по старому имени, ошибка!
```
**Использование `this` гарантирует, что функция работает именно с тем объектом, в контексте которого вызвана!**
Через `this` метод может обратиться к любому свойству объекта, а, при желании, и передать объект куда-либо:
```js
//+ run
var user = {
name: 'Василий',
*!*
sayHi: function() {
showName(this); // передать текущий объект в showName
}
*/!*
};
function showName(obj) {
alert( obj.name );
}
user.sayHi(); // Василий
```
## Подробнее про this
Любая функция может иметь в себе `this`. Совершенно неважно, объявлена она в объекте или вне него.
Значение `this` называется *контекстом вызова* и будет определено в момент вызова функции.
Например, такая функция, объявленная без объекта, вполне допустима:
```js
function sayHi() {
alert( *!*this.firstName*/!* );
}
```
Эта функция ещё не знает, каким будет `this`. Это выяснится при выполнении программы.
**Если одну и ту же функцию запускать в контексте разных объектов, она будет получать разный `this`:**
```js
//+ run
var user = { firstName: "Вася" };
var admin = { firstName: "Админ" };
function func() {
alert( this.firstName );
}
user.f = func;
admin.g = func;
*!*
// this равен объекту перед точкой:
user.f(); // Вася
admin.g(); // Админ
admin['g'](); // Админ (не важно, доступ к объекту через точку или квадратные скобки)
*/!*
```
**Значение `this` не зависит от того, как функция была создана, оно определяется исключительно в момент вызова.**
## Значение this при вызове без контекста
Если функция использует `this` -- это подразумевает работу с объектом. Но и прямой вызов `func()` технически возможен.
Как правило, такая ситуация возникает при ошибке в разработке.
При этом `this` получает значение `window`, глобального объекта:
```js
//+ run
function func() {
alert(this); // выведет [object Window] или [object global]
}
func();
```
В современном стандарте языка это поведение изменено, вместо глобального объекта `this` будет `undefined`.
```js
//+ run
function func() {
"use strict";
alert(this); // выведет undefined (кроме IE<10)
}
func();
```
Это стоит иметь в виду для общего развития, но обычно если в функции используется `this`, то она, всё же, проектируется для вызова в контексте объекта.
[warn header="`this` теряется при операциях с методом"]
Ещё раз обратим внимание: контекст `this` никак не привязан к функции, даже если она создана в объявлении объекта.
Чтобы `this` передался правильно, нужно вызвать функцию именно через точку (или квадратные скобки). Любой более хитрый вызов приведёт к потере контекста, например:
```js
//+ run
var user = {
name: "Вася",
hi: function() { alert(this.name); },
bye: function() { alert("Пока"); }
};
user.hi(); // Вася (простой вызов работает)
*!*
// а теперь вызовем user.hi или user.bye в зависимости от имени
(user.name == "Вася" ? user.hi : user.bye)(); // undefined
*/!*
```
В последней строке примера метод получен в результате выполнения тернарного оператора и тут же вызван. При этом `this` теряется.
Иначе говоря, такой вызов эквивалентен двум строкам:
```js
var method = (user.name == "Вася" ? user.hi : user.bye);
method(); // без this
```
[/warn]
## Задачи

View file

@ -0,0 +1,19 @@
Да, возможны.
Они должны возвращать одинаковый объект. При этом если функция возвращает объект, то `this` не используется.
Например, они могут вернуть один и тот же объект `obj`, определённый снаружи:
```js
//+ run
var obj = {};
function A() { return obj; }
function B() { return obj; }
var a = new A;
var b = new B;
alert( a == b ); // true
```

View file

@ -0,0 +1,17 @@
# Две функции один объект
[importance 2]
Возможны ли такие функции `A` и `B` в примере ниже, что соответствующие объекты `a,b` равны (см. код ниже)?
```js
function A() { ... }
function B() { ... }
var a = new A;
var b = new B;
alert( a == b ); // true
```
Если да -- приведите пример кода с такими функциями.

View file

@ -0,0 +1,15 @@
function Calculator() {
this.read = function() {
this.a = +prompt('a?', 0);
this.b = +prompt('b?', 0);
};
this.sum = function() {
return this.a + this.b;
};
this.mul = function() {
return this.a * this.b;
};
}

View file

@ -0,0 +1,25 @@
sinon.stub(window, "prompt")
prompt.onCall(0).returns("2");
prompt.onCall(1).returns("3");
describe("calculator", function() {
var calculator;
before(function() {
calculator = new Calculator();
calculator.read();
});
it("при вводе 2 и 3 сумма равна 5", function() {
assert.equal( calculator.sum(), 5 );
});
it("при вводе 2 и 3 произведение равно 6", function() {
assert.equal( calculator.mul(), 6 );
});
});
after(function() {
prompt.restore();
});

View file

@ -0,0 +1,27 @@
```js
//+ run demo
function Calculator() {
this.read = function() {
this.a = +prompt('a?', 0);
this.b = +prompt('b?', 0);
};
this.sum = function() {
return this.a + this.b;
};
this.mul = function() {
return this.a * this.b;
};
}
var calculator = new Calculator();
calculator.read();
alert( "Сумма=" + calculator.sum() );
alert( "Произведение=" + calculator.mul() );
```

View file

@ -0,0 +1,22 @@
# Создать Calculator при помощи конструктора
[importance 5]
Напишите *функцию-конструктор* `Calculator`, которая создает объект с двумя методами:
<ul>
<li>Метод `read()` запрашивает два значения при помощи `prompt` и запоминает их в свойствах объекта.</li>
<li>Метод `sum()` возвращает сумму запомненных свойств.</li>
<li>Метод `mul()` возвращает произведение запомненных свойств.</li>
</ul>
Пример использования:
```js
var calculator = new Calculator();
calculator.read();
alert( "Сумма=" + calculator.sum() );
alert( "Произведение=" + calculator.mul() );
```
[demo /]

View file

@ -0,0 +1,8 @@
function Accumulator(startingValue) {
this.value = startingValue;
this.read = function() {
this.value += +prompt('Сколько добавлять будем?', 0);
};
}

View file

@ -0,0 +1,37 @@
describe("Accumulator(1)", function() {
var accumulator;
before(function() {
accumulator = new Accumulator(1);
});
beforeEach(function() {
sinon.stub(window, "prompt")
});
afterEach(function() {
prompt.restore();
});
it("начальное значение 1", function() {
assert.equal( accumulator.value, 1 );
});
it("после ввода 0 значение 1", function() {
prompt.returns("0");
accumulator.read();
assert.equal( accumulator.value, 1 );
});
it("после ввода 1 значение 2", function() {
prompt.returns("1");
accumulator.read();
assert.equal( accumulator.value, 2 );
});
it("после ввода 2 значение 4", function() {
prompt.returns("2");
accumulator.read();
assert.equal( accumulator.value, 4 );
});
});

View file

@ -0,0 +1,19 @@
```js
//+ run
function Accumulator(startingValue) {
this.value = startingValue;
this.read = function() {
this.value += +prompt('Сколько добавлять будем?', 0);
};
}
var accumulator = new Accumulator(1);
accumulator.read();
accumulator.read();
alert( accumulator.value );
```

View file

@ -0,0 +1,24 @@
# Создать Accumulator при помощи конструктора
[importance 5]
Напишите *функцию-конструктор* `Accumulator(startingValue)`.
Объекты, которые она создает, должны хранить текущую сумму и прибавлять к ней то, что вводит посетитель.
Более формально, объект должен:
<ul>
<li>Хранить текущее значение в своём свойстве `value`. Начальное значение свойства `value` ставится конструктором равным `startingValue`.</li>
<li>Метод `read()` вызывает `prompt`, принимает число и прибавляет его к свойству `value`.</li>
</ul>
Таким образом, свойство `value` является текущей суммой всего, что ввел посетитель при вызовах метода `read()`, с учетом начального значения `startingValue`.
Ниже вы можете посмотреть работу кода:
```js
var accumulator = new Accumulator(1); // начальное значение 1
accumulator.read(); // прибавит ввод prompt к текущему значению
accumulator.read(); // прибавит ввод prompt к текущему значению
alert( accumulator.value ); // выведет текущее значение
```
[demo /]

View file

@ -0,0 +1,29 @@
function Calculator() {
var methods = {
"-": function(a, b) {
return a - b;
},
"+": function(a, b) {
return a + b;
}
};
this.calculate = function(str) {
var split = str.split(' '),
a = +split[0],
op = split[1],
b = +split[2]
if(!methods[op] || isNaN(a) || isNaN(b)) {
return NaN;
}
return methods[op](+a, +b);
}
this.addMethod = function(name, func) {
methods[name] = func;
};
}

View file

@ -0,0 +1,26 @@
var calculator;
before(function() {
calculator = new Calculator;
});
it("calculate(12 + 34) = 46", function() {
assert.equal( calculator.calculate("12 + 34"), 46 );
});
it("calculate(34 - 12) = 22", function() {
assert.equal( calculator.calculate("34 - 12"), 22 );
});
it("добавили умножение: calculate(2 * 3) = 6", function() {
calculator.addMethod("*", function(a, b) {
return a * b;
});
assert.equal( calculator.calculate("2 * 3"), 6 );
});
it("добавили возведение в степень: calculate(2 ** 3) = 8", function() {
calculator.addMethod("**", function(a, b) {
return Math.pow(a, b);
});
assert.equal( calculator.calculate("2 ** 3"), 8 );
});

View file

@ -0,0 +1,55 @@
```js
//+ run
function Calculator() {
var methods = {
"-": function(a, b) {
return a - b;
},
"+": function(a, b) {
return a + b;
}
};
this.calculate = function(str) {
var split = str.split(' '),
a = +split[0],
op = split[1],
b = +split[2]
if(!methods[op] || isNaN(a) || isNaN(b)) {
return NaN;
}
return methods[op](+a, +b);
}
this.addMethod = function(name, func) {
methods[name] = func;
};
}
var calc = new Calculator;
calc.addMethod("*", function(a, b) {
return a * b;
});
calc.addMethod("/", function(a, b) {
return a / b;
});
calc.addMethod("**", function(a, b) {
return Math.pow(a, b);
});
var result = calc.calculate("2 ** 3");
alert(result); // 8
```
<ul>
<li>Обратите внимание на хранение методов. Они просто добавляются к внутреннему объекту.</li>
<li>Все проверки и преобразование к числу производятся в методе `calculate`. В дальнейшем он может быть расширен для поддержки более сложных выражений.</li>
</ul>

View file

@ -0,0 +1,41 @@
# Создайте калькулятор
[importance 5]
Напишите конструктор `Calculator`, который создаёт расширяемые объекты-калькуляторы.
Эта задача состоит из двух частей, которые можно решать одна за другой.
<ol>
<li>Первый шаг задачи: вызов `calculate(str)` принимает строку, например "1 + 2", с жёстко заданным форматом "ЧИСЛО операция ЧИСЛО" (по одному пробелу вокруг операции), и возвращает результат. Понимает плюс `+` и минус `-`.
Пример использования:
```js
var calc = new Calculator;
alert(calc.calculate("3 + 7")); // 10
```
</li>
<li>Второй шаг -- добавить калькулятору метод `addMethod(name, func)`, который учит калькулятор новой операции. Он получает имя операции `name` и функцию от двух аргументов `func(a,b)`, которая должна её реализовывать.
Например, добавим операции умножить `*`, поделить `/` и возвести в степень `**`:
```js
var powerCalc = new Calculator;
powerCalc.addMethod("*", function(a, b) { return a * b; });
powerCalc.addMethod("/", function(a, b) { return a / b; });
powerCalc.addMethod("**", function(a, b) { return Math.pow(a, b); });
var result = powerCalc.calculate("2 ** 3");
alert(result); // 8
```
</li>
</ol>
<ul>
<li>Поддержка скобок и сложных математических выражений в этой задаче не требуется.</li>
<li>Числа и операции могут состоять из нескольких символов. Между ними ровно один пробел.</li>
<li>Предусмотрите обработку ошибок. Какая она должна быть - решите сами.</li>
</ul>

View file

@ -0,0 +1,185 @@
# Создание объектов через "new"
Обычный синтаксис `{...}` позволяет создать один объект. Но зачастую нужно создать много однотипных объектов.
Для этого используют функции, запуская их при помощи специального оператора `new`.
[cut]
## Конструктор
Конструктором становится любая функция, вызванная через `new`.
Например:
```js
function Animal(name) {
this.name = name;
this.canWalk = true;
}
*!*
var animal = new Animal("ёжик");
*/!*
```
Технически, никаких ограничений нет -- любую функцию можно вызвать при помощи `new`. Но при этом она работает несколько иным образом, чем обычно, поэтому функции, предназначенные к вызову через `new`, называют с большой буквы.
**Алгоритм работы оператора `new`:**
<ol>
<li>Автоматически создается новый пустой объект.</li>
<li>Ключевое слово `this` получает ссылку на этот объект.</li>
<li>Функция выполняется. Как правило, она модифицирует `this`, добавляет методы, свойства.</li>
<li>Возвращается `this`.</li>
</ol>
В результате вызова `new Animal("ёжик");` получаем объект:
```js
animal = {
name: "ёжик",
canWalk: true
}
```
Иными словами, при вызове `new Animal` происходит что-то в таком духе (комментарии -- это то, что делает интерпретатор):
```js
function Animal(name) {
*!*
// this = {}
*/!*
this.name = name;
this.canWalk = true;
*!*
// return this
*/!*
}
```
## Правила обработки return
Как правило, конструкторы ничего не возвращают. Их задача -- записать всё, что нужно, в `this`, который автоматически станет результатом.
Но если явный вызов `return` всё же есть, то применяется простое правило:
<ul>
<li>При вызове `return` с объектом, будет возвращён он, а не `this`.</li>
<li>При вызове `return` с примитивным значением, оно будет отброшено.</li>
</ul>
Иными словами, вызов `return` с объектом вернёт объект, а с чем угодно, кроме объекта -- прекратит выполнение функции и возвратит `this`.
Например, возврат объекта:
```js
//+ run
function BigAnimal() {
this.name = "Мышь";
return { name: "Годзилла" }; // <-- возвратим объект
}
alert( new BigAnimal().name ); // Годзилла, получили объект вместо this
```
А вот пример с возвратом строки:
```js
//+ run
function BigAnimal() {
this.name = "Мышь";
return "Годзилла"; // <-- возвратим примитив
}
alert( new BigAnimal().name ); // Мышь, получили this (а Годзилла пропал)
```
Эта особенность работы `new` прописана в стандарте, знать о ней полезно, но используется она весьма редко.
[smart]
Кстати, при вызове `new` без аргументов скобки можно не ставить:
```js
var animal = new BigAnimal; // <-- без скобок
// то же самое что
var animal = new BigAnimal();
```
[/smart]
## Создание методов в конструкторе
Использование функций для создания объекта дает большую гибкость. Можно передавать конструктору параметры, определяющие как его создавать.
Например, `new User(name)` создает объект с заданным значением свойства `name` и методом `sayHi`:
```js
//+ run
function User(name) {
this.name = name;
this.sayHi = function() {
alert("Моё имя: " + this.name);
};
}
*!*
var ivan = new User("Иван");
ivan.sayHi(); // Моё имя: Иван
*/!*
/*
ivan = {
name: "Иван",
sayHi: функция
}
*/
```
## Локальные переменные
В функции-конструкторе бывает удобно объявить вспомогательные локальные переменные и вложенные функции, которые будут видны только внутри:
```js
//+ run
function User(firstName, lastName) {
*!*
// вспомогательная переменная
var phrase = "Привет";
// вспомогательная вложенная функция
function getFullName() {
return firstName + " " + lastName;
}
*/!*
this.sayHi = function() {
alert(phrase + ", " + getFullName()); // использование
};
}
var vasya = new User("Вася", "Петров");
vasya.sayHi(); // Вася Петров
```
Мы уже говорили об этом подходе ранее, в главе [](/closures-usage).
Те функции и данные, которые должны быть доступны для внешнего кода, мы пишем в `this` -- и к ним можно будет обращаться, как например `vasya.sayHi()`, а вспомогательные, которые нужны только внутри самого объекта, сохраняем в локальной области видимости.
## Итого
Объекты могут быть созданы при помощи функций-конструкторов:
<ul>
<li>Любая функция может быть вызвана с `new`, при этом она получает новый пустой объект в качестве `this`, в который она добавляет свойства. Если функция не решит возвратить свой объект, то её результатом будет `this`.</li>
<li>Функции, которые предназначены для создания объектов, называются *конструкторами*. Их названия пишут с большой буквы, чтобы отличать от обычных.</li>
</ul>

View file

@ -0,0 +1,11 @@
function Article() {
this.created = new Date;
Article.count++; // увеличиваем счетчик при каждом вызове
Article.last = this.created; // и запоминаем дату
}
Article.count = 0; // начальное значение
Article.showStats = function() {
alert('Всего: ' + this.count + ', Последняя: ' + this.last);
};

View file

@ -0,0 +1,28 @@
describe("Article.showStats", function() {
before(function() {
sinon.stub(window, "alert");
this.clock = sinon.useFakeTimers();
});
after(function() {
window.alert.restore();
this.clock.restore();
});
it("Выводит число статей и дату создания последней", function() {
new Article();
this.clock.tick(100);
new Article();
Article.showStats();
assert( alert.calledWith('Всего: 2, Последняя: ' + new Date() ) );
});
it("и ещё одна статья...", function() {
this.clock.tick(100);
new Article();
Article.showStats();
assert( alert.calledWith('Всего: 3, Последняя: ' + new Date() ) );
});
});

View file

@ -0,0 +1,29 @@
Решение (как вариант):
```js
//+ run
function Article() {
this.created = new Date;
*!*
Article.count++; // увеличиваем счетчик при каждом вызове
Article.last = this.created; // и запоминаем дату
*/!*
}
Article.count = 0; // начальное значение
// (нельзя оставить undefined, т.к. Article.count++ будет NaN)
Article.showStats = function() {
alert('Всего: ' + this.count + ', Последняя: ' + this.last);
};
new Article();
new Article();
Article.showStats(); // Всего: 2, Последняя: (дата)
new Article();
Article.showStats(); // Всего: 3, Последняя: (дата)
```

View file

@ -0,0 +1,33 @@
# Счетчик объектов
[importance 5]
Добавить в конструктор `Article`:
<ul>
<li>Подсчёт общего количества созданных объектов.</li>
<li>Запоминание даты последнего созданного объекта.</li>
</ul>
Используйте для этого статические свойства.
Пусть вызов `Article.showStats()` выводит то и другое.
Использование:
```js
function Article() {
this.created = new Date();
*!*
// ... ваш код ...
*/!*
}
new Article();
new Article();
Article.showStats(); // Всего: 2, Последняя: (дата)
new Article();
Article.showStats(); // Всего: 3, Последняя: (дата)
```

View file

@ -0,0 +1,245 @@
# Статические и фабричные методы
Методы и свойства, которые не привязаны к конкретному экземпляру объекта, называют "статическими". Их записывают прямо в саму функцию-конструктор.
[cut]
## Статические свойства
В коде ниже используются статические свойства `Article.count` и `Article.DEFAULT_FORMAT`:
```js
function Article() {
Article.count++;
}
Article.count = 0; // статическое свойство-переменная
Article.DEFAULT_FORMAT = "html"; // статическое свойство-константа
```
Они хранят данные, специфичные не для одного объекта, а для всех статей целиком.
Как правило, это чаще константы, такие как формат "по умолчанию" `Article.DEFAULT_FORMAT`.
## Статические методы
С примерами статических методов мы уже знакомы: это встроенные методы [String.fromCharCode](http://javascript.ru/String.fromCharCode), [Date.parse](http://javascript.ru/Date.parse).
Создадим для `Article` статический метод `Article.showCount()`:
```js
//+ run
function Article() {
Article.count++;
//...
}
Article.count = 0;
Article.showCount = function() {
*!*
alert(this.count); // (1)
*/!*
}
// использование
new Article();
new Article();
Article.showCount(); // (2)
```
Здесь `Article.count` -- статическое свойство, а `Article.showCount` -- статический метод.
**Обратите внимание на контекст `this`. Несмотря на то, что переменная и метод -- статические, он всё ещё полезен. В строке `(1)` он равен `Article`!**
## Пример: сравнение объектов
Ещё один хороший способ применения -- сравнение объектов.
Например, у нас есть объект `Journal` для журналов. Журналы можно сравнивать -- по толщине, по весу, по другим параметрам.
Объявим "стандартную" функцию сравнения, которая будет сравнивать по дате издания. Эта функция сравнения, естественно, не привязана к конкретному журналу, но относится к журналам вообще.
Поэтому зададим её как статический метод `Journal.compare`:
```js
function Journal(date) {
this.date = date;
// ...
}
// возвращает значение, большее 0, если A больше B, иначе меньшее 0
Journal.compare = function(journalA, journalB) {
return journalA.date - journalB.date;
};
```
В примере ниже эта функция используется для поиска самого раннего журнала из массива:
```js
//+ run
function Journal(date) {
this.date = date;
this.formatDate = function(date) {
return date.getDate() + '.' + (date.getMonth()+1) + '.' + date.getFullYear();
};
this.getTitle = function() {
return "Выпуск от " + this.formatDate(this.date);
};
}
*!*
Journal.compare = function(journalA, journalB) {
return journalA.date - journalB.date;
};
*/!*
// использование:
var journals = [
new Journal(new Date(2012,1,1)),
new Journal(new Date(2012,0,1)),
new Journal(new Date(2011,11,1))
];
function findMin(journals) {
var min = 0;
for(var i=0; i<journals.length; i++) {
*!*
// используем статический метод
if ( Journal.compare(journals[min], journals[i]) > 0 ) min = i;
*/!*
}
return journals[min];
}
alert( findMin(journals).getTitle() );
```
**Статический метод также можно использовать для функций, которые вообще не требуют наличия объекта.**
Например, метод `formatDate(date)` можно сделать статическим. Он будет форматировать дату "как это принято в журналах", при этом его можно использовать в любом месте кода, не обязательно создавать журнал.
Например:
```js
//+ run
function Journal() { /*...*/ }
Journal.formatDate = function(date) {
return date.getDate() + '.' + (date.getMonth()+1) + '.' + date.getFullYear();
}
// ни одного объекта Journal нет, просто форматируем дату
alert( *!*Journal.formatDate(new Date)*/!* );
```
## Фабричные методы
Рассмотрим ситуацию, когда объект нужно создавать различными способами. Например, это реализовано во встроенном объекте [Date](/datetime). Он по-разному обрабатывает аргументы разных типов:
<ul>
<li>`new Date()` -- создаёт объект с текущей датой,</li>
<li>`new Date(milliseconds)` -- создаёт дату по количеству миллисекунд `milliseconds`,</li>
<li>`new Date(year, month, day ...)` -- создаёт дату по компонентам год, месяц, день...</li>
<li>`new Date(datestring)` -- читает дату из строки `datestring`</li>
</ul>
**"Фабричный статический метод" -- удобная альтернатива такому конструктору. Так называется статический метод, который служит для создания новых объектов (поэтому и называется "фабричным").**
Пример встроенного фабричного метода -- [String.fromCharCode(code)](http://javascript.ru/String.fromCharCode). Этот метод создает строку из кода символа:
```js
//+ run
var str = String.fromCharCode(65);
alert(str); // 'A'
```
Но строки -- слишком простой пример, посмотрим что-нибудь посложнее.
Допустим, нам нужно создавать объекты `User`: анонимные `new User()` и с данными `new User({name: 'Вася', age: 25})`.
Можно, конечно, создать полиморфную функцию-конструктор `User`:
```js
//+ run
function User(userData) {
if (userData) { // если указаны данные -- одна ветка if
this.name = userData.name;
this.age = userData.age;
} else { // если не указаны -- другая
this.name = 'Аноним';
}
this.sayHi = function() { alert(this.name) };
// ...
}
// Использование
var guest = new User();
guest.sayHi(); // Аноним
var knownUser = new User({name: 'Вася', age: 25});
knownUser.sayHi(); // Вася
```
Подход с использованием фабричных методов был бы другим. Вместо разбора параметров в конструкторе -- делаем два метода: `User.createAnonymous` и `User.createFromData`.
Код:
```js
//+ run
function User() {
this.sayHi = function() { alert(this.name) };
}
User.createAnonymous = function() {
var user = new User;
user.name = 'Аноним';
return user;
}
User.createFromData = function(userData) {
var user = new User;
user.name = userData.name;
user.age = userData.age;
return user;
}
// Использование
*!*
var guest = User.createAnonymous();
guest.sayHi(); // Аноним
var knownUser = User.createFromData({name: 'Вася', age: 25});
knownUser.sayHi(); // Вася
*/!*
```
Преимущества использования фабричных методов:
[compare]
+Лучшая читаемость кода. Как конструктора -- вместо одной большой функции несколько маленьких, так и вызывающего кода -- явно видно, что именно создаётся.
+Лучший контроль ошибок, т.к. если в `createFromData` ничего не передали, то будет ошибка, а полиморфный конструктор создал бы анонимного посетителя.
+Удобная расширяемость. Например, нужно добавить создание администратора, без аргументов. Фабричный метод сделать легко: `User.createAdmin = function() { ... }`. А для полиморфного конструктора вызов без аргумента создаст анонима, так что нужно добавить параметр -- "тип посетителя" и усложнить этим код.
[/compare]
**Поэтому полиморфные конструкторы лучше использовать там, где нужна именно полиморфность**, т.е. когда непонятно, какого типа аргумент передадут, и хочется в одном конструкторе охватить все варианты.
А в остальных случаях отличная альтернатива -- фабричные методы.
## Итого
Статические свойства и методы объекта удобно применять в следующих случаях:
<ul>
<li>Общие действия и подсчёты, имеющие отношения ко всем объектам данного типа. В примерах выше это подсчёт количества.</li>
<li>Методы, не привязанные к конкретному объекту, например сравнение.</li>
<li>Вспомогательные методы, которые полезны вне объекта, например для форматирования даты.</li>
<li>Фабричные методы.</li>
</ul>

View file

@ -0,0 +1,31 @@
# Первый вариант
```js
//+ run
function sumArgs() {
// скопируем reduce из массива
arguments.reduce = [].reduce;
return arguments.reduce(function(a, b) {
return a + b;
});
}
alert( sumArgs(4,5,6) ); // 15
```
# Второй вариант
Метод `call` здесь вполне подойдёт, так как требуется вызвать `reduce` в контексте `arguments` с одним аргументом.
```js
//+ run
function sumArgs() {
// запустим reduce из массива напрямую
return [].reduce.call(arguments, function(a, b) {
return a + b;
});
}
alert( sumArgs(4,5,6) ); // 15
```

View file

@ -0,0 +1,28 @@
# Перепишите суммирование аргументов
[importance 5]
Есть функция `sum`, которая суммирует все элементы массива:
```js
//+ run
function sum(arr) {
return arr.reduce(function(a, b) { return a + b; });
}
alert( sum([1,2,3]) ); // 6 (=1+2+3)
```
Создайте аналогичную функцию `sumArgs()`, которая будет суммировать все свои аргументы:
```js
function sumArgs() {
/* ваш код */
}
alert( sumArgs(1,2,3) ); // 6, аргументы переданы через запятую, без массива
```
Для решения примените метод `reduce` к `arguments`, используя `call`, `apply` или одалживание метода.
P.S. Функция `sum` вам не понадобится, она приведена в качестве примера использования `reduce` для похожей задачи.

View file

@ -0,0 +1,3 @@
function applyAll(func) {
return func.apply(this, [].slice.call(arguments, 1) );
}

View file

@ -0,0 +1,15 @@
describe("applyAll", function() {
it("применяет функцию ко всем аргументам, начиная со 2го", function() {
var min = applyAll(Math.min, 1, 2, 3);
assert.equal( min, 1 );
});
it("при отсутствии аргументов просто вызывает функцию", function() {
var spy = sinon.spy();
applyAll(spy);
assert( spy.calledOnce );
assert.equal( spy.firstCall.args.length, 0 );
});
});

View file

@ -0,0 +1,23 @@
```js
//+ run
function sum() {
return [].reduce.call(arguments, function(a, b) { return a + b; });
}
function mul() {
return [].reduce.call(arguments, function(a, b) { return a * b; });
}
*!*
function applyAll(func) {
return func.apply(this, [].slice.call(arguments, 1) );
}
*/!*
alert( applyAll(sum, 1, 2, 3) ); // 6
alert( applyAll(mul, 2, 3, 4) ); // 24
alert( applyAll(Math.max, 2, -2, 3) ); // 3
alert( applyAll(Math.min, 2, -2, 3) ); // -2
```

View file

@ -0,0 +1,35 @@
# Примените функцию к аргументам
[importance 5]
Напишите функцию `applyAll(func, arg1, arg2...)`, которая получает функцию `func` и произвольное количество аргументов.
Она должна вызвать `func(arg1, arg2...)`, то есть передать в `func` все аргументы, начиная со второго, и возвратить результат.
Например:
```js
// Применить Math.max к аргументам 2, -2, 3
alert( applyAll(Math.max, 2, -2, 3) ); // 3
// Применить Math.min к аргументам 2, -2, 3
alert( applyAll(Math.min, 2, -2, 3) ); // -2
```
Область применения `applyAll`, конечно, шире, можно вызывать её и со своими функциями:
```js
//+ run
function sum() { // суммирует аргументы: sum(1,2,3) = 6
return [].reduce.call(arguments, function(a, b) { return a + b; });
}
function mul() { // перемножает аргументы: mul(2,3,4) = 24
return [].reduce.call(arguments, function(a, b) { return a * b; });
}
*!*
alert( applyAll(sum, 1, 2, 3) ); // -> sum(1, 2, 3) = 6
alert( applyAll(mul, 2, 3, 4) ); // -> mul(2, 3, 4) = 24
*/!*
```

View file

@ -0,0 +1,312 @@
# Явное указание this: "call", "apply"
Итак, мы знаем, что в `this` -- это текущий объект при вызове "через точку" и новый объект при конструировании через `new`.
В этой главе наша цель получить окончательное и полное понимание `this` в JavaScript. Для этого не хватает всего одного элемента: способа явно указать `this` при помощи методов `call` и `apply`.
[cut]
## Метод call
Синтаксис метода `call`:
```js
func.call(context, arg1, arg2,...)
```
При этом вызывается функция `func`, первый аргумент `call` становится её `this`, а остальные передаются "как есть".
**Вызов `func.call(context, a, b...)` -- то же, что обычный вызов `func(a, b...)`, но с явно указанным `this(=context)`.**
Например, у нас есть функция `showFullName`, которая работает с `this`:
```js
function showFullName() {
alert( this.firstName + " " + this.lastName );
}
```
**Обратите внимание, JavaScript позволяет использовать `this` везде. Любая функция может в своём коде упомянуть `this`, каким будет это значение -- выяснится в момент запуска.**
Вызов `showFullName.call(user)` запустит функцию, установив `this = user`, вот так:
```js
//+ run
function showFullName() {
alert( this.firstName + " " + this.lastName );
}
var user = {
firstName: "Василий",
lastName: "Петров"
};
*!*
// функция вызовется с this=user
showFullName.call(user) // "Василий Петров"
*/!*
```
После контекста в `call` можно передать аргументы для функции. Вот пример с более сложным вариантом `showFullName`, который конструирует ответ из указанных свойств объекта:
```js
//+ run
var user = {
firstName: "Василий",
surname: "Петров",
patronym: "Иванович"
};
function showFullName(firstPart, lastPart) {
alert( this[firstPart] + " " + this[lastPart] );
}
*!*
// f.call(контекст, аргумент1, аргумент2, ...)
showFullName.call(user, 'firstName', 'surname') // "Василий Петров"
showFullName.call(user, 'firstName', 'patronym') // "Василий Иванович"
*/!*
```
## "Одалживание метода"
При помощи `call` можно легко взять метод одного объекта, в том числе встроенного, и вызвать в контексте другого.
Это называется "одалживание метода" (на англ. *method borrowing*).
**Используем эту технику для упрощения манипуляций с `arguments`.**
Как мы знаем, `arguments` не массив, а обычный объект, поэтому таких полезных методов как `push`, `pop`, `join` и других у него нет. Но иногда так хочется, чтобы были...
Нет ничего проще! Давайте скопируем метод `join` из обычного массива:
```js
//+ run
function printArgs() {
arguments.join = [].join; // одолжили метод (1)
var argStr = arguments.join(':'); // (2)
alert(argStr); // сработает и выведет 1:2:3
}
printArgs(1, 2, 3);
```
<ol>
<li>В строке `(1)` объявлен пустой массив `[]` и скопирован его метод `[].join`. Обратим внимание, мы не вызываем его, а просто копируем. Функция, в том числе встроенная -- обычное значение, мы можем скопировать любое свойство любого объекта, и `[].join` здесь не исключение. </li>
<li>В строке `(2)` запустили `join` в контексте `arguments`, как будто он всегда там был.</li>
[smart header="Почему вызов сработает?"]
Здесь метод <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/join">join</a> массива скопирован и вызван в контексте `arguments`. Не произойдёт ли что-то плохое от того, что `arguments` -- не массив? Почему он, вообще, сработал?
Ответ на эти вопросы простой. В соответствии [со спецификацией](http://es5.github.com/x15.4.html#x15.4.4.5), внутри `join` реализован примерно так:
```js
function join(separator) {
if (!this.length) return '';
var str = this[0];
for (var i = 1; i<this.length; i++) {
str += separator + this[i];
}
return str;
}
```
Как видно, используется `this`, числовые индексы и свойство `length`. Если эти свойства есть, то все в порядке. А больше ничего и не нужно.
В качестве `this` подойдёт даже обычный объект:
```js
//+ run
var obj = { // обычный объект с числовыми индексами и length
0: "А",
1: "Б",
2: "В",
length: 3
};
*!*
obj.join = [].join;
alert( obj.join(';') ); // "A;Б;В"
*/!*
```
[/smart]
**...Однако, копирование метода из одного объекта в другой не всегда приемлемо!**
Представим на минуту, что вместо `arguments` у нас -- произвольный объект. У него тоже есть числовые индексы, `length` и мы хотим вызвать в его контексте метод `[].join`. То есть, ситуация похожа на `arguments`, но (!) вполне возможно, что у объекта есть *свой* метод `join`.
Поэтому копировать `[].join`, как сделано выше, нельзя: если он перезапишет собственный `join` объекта, то будет страшный бардак и путаница.
**Безопасно вызвать метод нам поможет `call`:**
```js
//+ run
function printArgs() {
var join = [].join; // скопируем ссылку на функцию в переменную
*!*
// вызовем join с this=arguments,
// этот вызов эквивалентен arguments.join(':') из примера выше
var argStr = join.call(arguments, ':');
*/!*
alert(argStr); // сработает и выведет 1:2:3
}
printArgs(1, 2, 3);
```
Мы вызвали метод без копирования. Чисто, безопасно.
## Ещё пример: [].slice.call(arguments)
В JavaScript есть очень простой способ сделать из `arguments` настоящий массив. Для этого возьмём метод массива: <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/slice">slice</a>.
По стандарту вызов `arr.slice(start, end)` создаёт новый массив и копирует в него элементы массива `arr` от `start` до `end`. А если `start` и `end` не указаны, то копирует весь массив.
Вызовем его в контексте `arguments`:
```js
//+ run
function printArgs() {
// вызов arr.slice() скопирует все элементы из this в новый массив
*!*
var args = [].slice.call(arguments);
*/!*
alert( args.join(', ') ); // args - полноценный массив из аргументов
}
printArgs('Привет', 'мой', 'мир'); // Привет, мой, мир
```
Как и в случае с `join`, такой вызов технически возможен потому, что `slice` для работы требует только нумерованные свойства и `length`. Всё это в `arguments` есть.
## Метод apply
Если нам неизвестно, с каким количеством аргументов понадобится вызвать функцию, можно использовать более мощный метод: `apply`.
**Вызов функции при помощи `func.apply` работает аналогично `func.call`, но принимает массив аргументов вместо списка.**
```js
func.call(context, arg1, arg2)
// идентичен вызову
func.apply(context, [arg1, arg2]);
```
В частности, эти две строчки cработают одинаково:
```js
showFullName.call(user, 'firstName', 'surname');
showFullName.apply(user, ['firstName', 'surname']);
```
Преимущество `apply` перед `call` отчётливо видно в следующем примере, когда мы формируем массив аргументов динамически:
```js
//+ run
var arr = [];
arr.push(1);
arr.push(5);
arr.push(2);
// получить максимум из элементов arr
alert( Math.max.apply(null, arr) ); // 5
```
Обратим внимание, в примере выше вызывается метод `Math.max`. Его стандартное применение -- это выбор максимального аргумента из переданных, которых может быть сколько угодно:
```js
//+ run
alert( Math.max(1, 5, 2) ); // 5
alert( Math.max(1, 5, 2, 8) ); // 8
```
В примере выше мы передали аргументы через массив -- второй параметр `apply`... Но вы, наверное, заметили небольшую странность? В качестве контекста `this` был передан `null`.
Строго говоря, полным эквивалентом вызову `Math.max(1,2,3)` был бы вызов `Math.max.apply(Math, [1,2,3])`. В обоих этих вызовах контекстом будет объект `Math`.
Но в данном случае в качестве контекста можно передавать что угодно, поскольку в своей внутренней реализации метод `Math.max`не использует `this`. Действительно, зачем `this`, если нужно всего лишь выбрать максимальный из аргументов?
**Вот так, при помощи `apply` мы получили короткий и элегантный способ вычислить максимальное значение в массиве!**
[smart header="Вызов `call/apply` с `null` или `undefined`"]
В старом стандарте при указании первого аргумента `null` или `undefined` в `call/apply`, функция получала `this = window`, например:
```js
//+ run
function f() {
alert(this);
}
f.call(null); // window
```
Это поведение исправлено в современном стандарте ([15.3](http://es5.github.com/x15.3.html#x15.3.4.3)).
Если функция работает в строгом режиме, то `this` передаётся "как есть":
```js
//+ run
function f() {
"use strict";
*!*
alert(this); // null, а не window
*/!*
}
f.call(null);
```
[/smart]
## Итого про this
Значение `this` устанавливается в зависимости от того, как вызвана функция:
<dl>
<dt>При вызове функции как метода</dt>
<dd>
```js
obj.func(...) // this = obj
obj["func"](...)
```
</dd>
<dt>При обычном вызове</dt>
<dd>
```js
func(...) // this = window (ES3) /undefined (ES5)
```
</dd>
<dt>В `new`</dt>
<dd>
```js
new func() // this = {} (новый объект)
```
</dd>
<dt>Явное указание</dt>
<dd>
```js
func.apply(context, args) // this = context (явная передача)
func.call(context, arg1, arg2, ...)
```
</dd>
</dl>

View file

@ -0,0 +1,8 @@
Страшновато выглядит, да? Работает так (по строкам):
<ol>
<li>Вызов `bind` сохраняет дополнительные аргументы `args` (они идут со 2го номера) в массив `bindArgs`.</li>
<li>... и возвращает обертку `wrapper`.</li>
<li>Эта обёртка делает из `arguments` массив `args` и затем, используя метод [concat](http://javascript.ru/Array/concat), прибавляет их к аргументам `bindArgs` (карринг).</li>
<li>Затем передаёт вызов `func` с контекстом и общим массивом аргументов.</li>
</ol>

View file

@ -0,0 +1,22 @@
# Кросс-браузерная эмуляция bind
[importance 3]
Если вы вдруг захотите копнуть поглубже -- аналог `bind` для IE8- и старых версий других браузеров будет выглядеть следующим образом:
```js
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;
}
```
Использование -- вместо `mul.bind(null, 2)` вызывать `bind(mul, null, 2)`.
Не факт, что он вам понадобится, но в качестве упражнение попробуйте разобраться, как это работает.

View file

@ -0,0 +1,22 @@
Ответ: `Hello`.
```js
//+ run
function f() {
alert( this );
}
var user = {
g: bind(f, "Hello")
}
user.g();
```
Так как вызов идёт в контексте объекта `user.g()`, то внутри функции `g` контекст `this = user`.
Однако, функции `g` совершенно без разницы, какой `this` она получила.
Её единственное предназначение -- это передать вызов в `f` вместе с аргументами и ранее указанным контекстом `"Hello"`, что она и делает.
Эта задача демонстрирует, что изменить однажды привязанный контекст уже нельзя.

View file

@ -0,0 +1,18 @@
# Запись в объект после bind
[importance 5]
Что выведет функция?
```js
function f() {
alert( this );
}
var user = {
g: bind(f, "Hello")
}
user.g();
```

View file

@ -0,0 +1,55 @@
Ответ: `"Вася"`.
```js
//+ run
function f() {
alert(this.name);
}
f = f.bind( {name: "Вася"} ).bind( {name: "Петя"} );
f(); // Вася
```
Первый вызов `f.bind(..Вася..)` возвращает "обёртку", которая устанавливает контекст для `f` и передаёт вызов `f`.
Следующий вызов `bind` будет устанавливать контекст уже для этой обёртки, это ни на что не влияет.
Чтобы это проще понять, используем наш собственный вариант `bind` вместо встроенного:
```js
function bind(func, context) {
return function() {
return func.apply(context, arguments);
};
}
```
Код станет таким:
```js
function f() {
alert(this.name);
}
f = bind(f, {name: "Вася"} ); // (1)
f = bind(f, {name: "Петя"} ); // (2)
f(); // Вася
```
Здесь видно, что первый вызов `bind`, в строке `(1)`, возвращает обёртку вокруг `f`, которая выглядит так (выделена):
```js
function bind(func, context) {
*!*
return function() {
return func.apply(context, arguments);
};
*/!*
}
```
В этой обёртке нигде не используется `this`, только `func` и `context`. Посмотрите на код, там нигде нет `this`.
Поэтому следующий `bind` в строке `(2)`, который выполняется уже над обёрткой и фиксирует в ней `this`, ни на что не влияет. Какая разница, что будет в качестве `this` в функции, которая этот `this` не использует?

View file

@ -0,0 +1,16 @@
# Повторный bind
[importance 5]
Что выведет этот код?
```js
function f() {
alert(this.name);
}
f = f.bind( {name: "Вася"} ).bind( {name: "Петя" } );
f();
```

View file

@ -0,0 +1,4 @@
Ответ: `undefined`.
Результатом работы `bind` является функция-обёртка над `sayHi`. Эта функция -- самостоятельный объект, у неё уже нет свойства `test`.

View file

@ -0,0 +1,20 @@
# Свойство функции после bind
[importance 5]
В свойство функции записано значение. Изменится ли оно после применения `bind`? Обоснуйте ответ.
```js
function sayHi() {
alert(this.name);
}
sayHi.test = 5;
alert(sayHi.test); // 5
*!*
var bound = sayHi.bind({ name: "Вася" });
alert(bound.test); // что выведет? почему?
*/!*
```

View file

@ -0,0 +1,105 @@
# Решение с bind
Ошибка происходит потому, что `ask` получает только функцию, без объекта-контекста.
Используем `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',
loginOk: function() {
alert(this.login + ' вошёл в сайт');
},
loginFail: function() {
alert(this.login + ': ошибка входа');
},
checkPassword: function() {
*!*
ask("Ваш пароль?", this.password, this.loginOk.bind(this), this.loginFail.bind(this));
*/!*
}
};
var vasya = user;
user = null;
vasya.checkPassword();
```
# Решение через замыкание
Альтернативное решение -- сделать функции-обёртки над `user.loginOk/loginFail`:
```js
var user = {
...
checkPassword: function() {
*!*
ask("Ваш пароль?", this.password,
function() { user.loginOk(); }, function() { user.loginFail(); });
*/!*
}
}
```
...Но такой код использует переменную `user`, так что если объект переместить из неё, к примеру, так, то работать он не будет:
```js
var vasya = user; // переместим user в vasya
user = null;
vasya.checkPassword(); // упс будет ошибка, ведь в коде объекта остался user
```
Для того, чтобы избежать проблем, можно использовать `this`. Внутри `checkPassword` он всегда будет равен текущему объекту, так что скопируем его в переменную, которую назовём `self`:
```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',
loginOk: function() {
alert(this.login + ' вошёл в сайт');
},
loginFail: function() {
alert(this.login + ': ошибка входа');
},
checkPassword: function() {
*!*
var self = this;
ask("Ваш пароль?", this.password,
function() { self.loginOk(); },
function() { self.loginFail(); }
);
*/!*
}
};
var vasya = user;
user = null;
vasya.checkPassword();
```
Теперь всё работает. Анонимные функции достают правильный контекст из замыкания, где он сохранён в переменной `self`.

View file

@ -0,0 +1,50 @@
# Использование функции вопросов
[importance 5]
Вызов `user.checkPassword()` в коде ниже должен, при помощи `ask`, спрашивать пароль и вызывать `loginOk/loginFail` в зависимости от правильности ответа.
Однако, его вызов приводит к ошибке. Почему?
Исправьте выделенную строку, чтобы всё работало (других строк изменять не надо).
```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',
loginOk: function() {
alert(this.login + ' вошёл в сайт');
},
loginFail: function() {
alert(this.login + ': ошибка входа');
},
checkPassword: function() {
*!*
ask("Ваш пароль?", this.password, this.loginOk, this.loginFail);
*/!*
}
};
user.checkPassword();
```
P.S. Ваше решение должно также срабатывать, если переменная `user` будет перезаписана, например вместо `user.checkPassword()` в конце будут строки:
```js
var vasya = user;
user = null;
vasya.checkPassword();
```

View file

@ -0,0 +1,69 @@
# Решение с 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,53 @@
# Использование функции вопросов с каррингом
[importance 5]
Эта задача -- усложнённый вариант задачи [](/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,308 @@
# Привязка контекста и карринг: "bind"
Функции в JavaScript никак не привязаны к своему контексту `this`, с одной стороны, здорово -- это позволяет быть максимально гибкими, одалживать методы и так далее.
Но с другой стороны -- в некоторых случаях контекст может быть потерян. То есть мы вроде как вызываем метод объекта, а на самом деле он получает `this = undefined`.
Такая ситуация является типичной для начинающих разработчиков, но бывает и у "зубров" то же. Конечно, зубры при этом знают, что с ней делать.
[cut]
## Пример потери контекста
В браузере есть встроенная функция `setTimeout(func, ms)`, которая вызывает выполение функции `func` через `ms` миллисекунд (=1/1000 секунды).
Мы подробно остановимся на ней и её тонкостях позже, в главе [](/setTimeout-setInterval), а пока просто посмотрим пример.
Этот код выведет "Привет" через 1000мс, то есть 1 секунду:
```js
//+ run
setTimeout(function() {
alert("Привет");
}, 1000);
```
Попробуем сделать то же самое с методом объекта, следующий код должен выводить имя пользователя через 1 секунду:
```js
//+ run
var user = {
firstName: "Вася",
sayHi: function() {
alert(this.firstName);
}
};
*!*
setTimeout( user.sayHi, 1000); // undefined (не Вася!)
*/!*
```
**При запуске кода выше через секунду выводится вовсе не `"Вася"`, а `undefined`!**
Это произошло потому, что в примере выше `setTimeout` получил функцию `user.sayHi`, но не её контекст. То есть, последняя строчка аналогична двум таким:
```js
var f = user.sayHi;
setTimeout(f, 1000); // контекст user потеряли
```
**Ситуация довольно типична -- мы хотим передать метод объекта куда-то в другое место кода, откуда он потом может быть вызван. Как бы прикрепить к нему контекст, желательно, с минимумом плясок с бубном и при этом надёжно?**
Есть несколько способов решения, среди которых мы, в зависимости от ситуации, можем выбирать.
## Решение 1: сделать обёртку
Самый простой вариант решения -- это обернуть вызов в анонимную функцию:
```js
//+ run
var user = {
firstName: "Вася",
sayHi: function() {
alert(this.firstName);
}
};
*!*
setTimeout(function() {
user.sayHi(); // Вася
}, 1000);
*/!*
```
Теперь код работает, так как `user` достаётся из замыкания.
Но тут же появляется и уязвимое место в структуре кода!
**А что, если до срабатывания `setTimeout` в переменную `user` будет записано другое значение? К примеру, какой-то другой пользователь... В этом случае вызов неожиданно будет совсем не тот!**
Хорошо бы гарантировать правильность контекста.
## Решение 2: bind для привязки контекста
Напишем вспомогательную функцию `bind(func, context)`, которая будет жёстко фиксировать контекст для `func`:
```js
function bind(func, context) {
return function() {
return func.apply(context, arguments);
};
}
```
Параметры:
<dl>
<dt>`func`</dt>
<dd>Произвольная функция</dd>
<dt>`context`</dt>
<dd>Произвольный объект</dd>
Результатом вызова `bind(func, context)` будет, как видно из кода, функция-обёртка, которая передаёт все вызовы `func`, указывая при этом правильный контекст `context`.
Чтобы понять, как она работает, нужно вспомнить тему "замыкания" -- здесь `bind` возвращает анонимную функцию, которая при вызове получает контекст `context` из внешней области видимости и передаёт вызов в `func` вместе с этим контекстом `context` и аргументами `arguments`.
**В результате вызова `bind` мы получаем как бы "ту же функцию, но с фиксированным контекстом".**
Пример с `bind`:
```js
//+ run
function bind(func, context) {
return function() {
return func.apply(context, arguments);
};
}
var user = {
firstName: "Вася",
sayHi: function() {
alert(this.firstName);
}
};
*!*
setTimeout( bind(user.sayHi, user), 1000 );
*/!*
```
Теперь всё в порядке! В `setTimeout` пошла обёртка, фиксирующая контекст.
## Решение 3: встроенный метод bind [#bind]
В современном JavaScript (или при подключении библиотеки [es5-shim](https://github.com/kriskowal/es5-shim) для IE8-) у функций уже есть встроенный метод [bind](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind), который мы можем использовать.
Он позволяет получить обёртку, которая привязывает функцию не только к нужному контексту но, если нужно, то и к аргументам.
Синтаксис `bind`:
```js
var wrapper = func.bind(context[, arg1, arg2...])
```
<dl>
<dt>`func`</dt>
<dd>Произвольная функция</dd>
<dt>`context`</dt>
<dd>Обертка `wrapper` будет вызывать функцию с контекстом `this = context`.</dd>
<dt>`arg1`, `arg2`, ...</dt>
<dd>Если указаны аргументы `arg1, arg2...` -- они будут прибавлены к каждому вызову новой функции, причем встанут *перед* теми, которые указаны при вызове.</dd>
</dl>
Результат вызова: `func.bind(context)` аналогичен вызову `bind(func, context)`, описанному выше. То есть, `wrapper` -- это обёртка, фиксирующая контекст и передающая вызовы в `func`. Также можно указать аргументы, но это делается редко, мы поговорим о них позже.
Пример со встроенным методом `bind`:
```js
//+ run
var user = {
firstName: "Вася",
sayHi: function() {
alert(this.firstName);
}
};
*!*
// setTimeout( bind(user.sayHi, user), 1000 );
setTimeout( user.sayHi.bind(user), 1000 ); // аналог через встроенный метод
*/!*
```
Получили простой и надёжный способ привязать контекст, причём даже встроенный в JavaScript.
[smart header="Привязать всё: `bindAll`"]
Если у объекта много методов и мы планируем их активно передавать, то можно привязать контекст для них всех в цикле:
```js
for(var prop in user) {
if (typeof user[prop] == 'function') {
user[prop] = user[prop].bind(user);
}
}
```
В некоторых JS-фреймворках есть даже встроенные функции для этого, например [_.bindAll(obj)](http://lodash.com/docs#bindAll).
[/smart]
## Карринг
До этого мы говорили о привязке контекста. Теперь пойдём на шаг дальше. Привязывать можно не только контекст, но и аргументы. Используется это реже, но бывает полезно.
[Карринг](http://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D1%80%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) (currying) или *каррирование* -- термин [функционального программирования](http://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%BE%D0%BD%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5), который означает создание новой функции путём фиксирования аргументов существующей.
Как было сказано выше, метод `func.bind(context, ...)` может создавать обёртку, которая фиксирует не только контекст, но и ряд аргументов функции.
Например, есть функция умножения двух чисел `mul(a, b)`:
```js
function mul(a, b) {
return a * b;
};
```
При помощи `bind` создадим функцию `double`, удваивающую значения. Это будет вариант функции `mul` с фиксированным первым аргументом:
```js
//+ run
*!*
// double умножает только на два
var double = mul.bind(null, 2); // контекст фиксируем null, он не используется
*/!*
alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10
```
При вызове `double` будет передавать свои аргументы исходной функции `mul` после тех, которые указаны в `bind`, то есть в данном случае после зафиксированного первого аргумента `2`.
**Говорят, что `double` является "частичной функцией" (partial function) от `mul`.**
Другая частичная функция `triple` утраивает значения:
```js
//+ run
*!*
var triple = mul.bind(null, 3); // контекст фиксируем null, он не используется
*/!*
alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( triple(5) ); // = mul(3, 5) = 15
```
**При помощи `bind` мы можем получить из функции её "частный вариант" как самостоятельную функцию и дальше передать в `setTimeout` или сделать с ней что-то ещё.**
## Задачи
Рассмотрим для дальнейших задач "функцию для вопросов" `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('птичку жалко :(');
}
```
## Итого
Функции и контекст к JavaScript -- как шнурки и кроссовки. Если мы куда-то отправляем шнурки (например в `setTimeout`), то кроссовки сами за ними не побегут.
Нужно либо передать их дополнительно, либо привязать одно к другому вызовом `bind`, либо завернуть в замыкание.
[head]
<script>
function mul(a, b) {
return a * b;
};
function ask(question, correctAnswer, ok, fail) {
var result;
if (typeof correctAnswer == 'boolean') {
result = confirm(question);
} else {
result = prompt(question, '');
}
if (result == correctAnswer) 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>
[/head]

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,48 @@
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,44 @@
Возвратим декоратор `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,32 @@
# Логирующий декоратор (1 аргумент)
[importance 5]
Создайте декоратор `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,15 @@
function makeLogging(f, log) {
function wrapper() {
log.push([].slice.call(arguments));
return f.apply(this, arguments);
}
return wrapper;
}

View file

@ -0,0 +1,49 @@
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,34 @@
Решение аналогично задаче [](/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,29 @@
# Логирующий декоратор (много аргументов)
[importance 3]
Создайте декоратор `makeLogging(func, log)`, для функции `func` возвращающий обёртку, которая при каждом вызове добавляет её аргументы в массив `log`.
Условие аналогично задаче [](/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,34 @@
Запоминать результаты вызова функции будем в замыкании, в объекте `cache: { ключ:значение }`.
```js
//+ run
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` имеет только один аргумент, и он является числом.**
<ol>
<li>При первом вызове обертки с определенным аргументом -- она вызывает `f` и запоминает значение.</li>
<li>При втором и последующих вызовах с тем же аргументом возвращается запомненное значение.</li>
</ol>
Должно работать так:
```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,205 @@
# Функции-обёртки, декораторы
JavaScript предоставляет удивительно гибкие возможности по работе с функциями: их можно передавать, в них можно записывать данные как в объекты, у них есть свои встроенные методы...
Конечно, этим нужно уметь пользоваться. В этой главе, чтобы более глубоко понимать работу с функциями, мы рассмотрим создание функций-обёрток или, иначе говоря, "декораторов".
[cut]
## Примеры декораторов
<a href="http://en.wikipedia.org/wiki/Decorator_pattern">Декоратор</a> -- приём программирования, который позволяет взять существующую функцию и изменить/расширить ее поведение.
***Декоратор* получает функцию и возвращает обертку, которая делает что-то своё "вокруг" вызова основной функции.**
### bind -- привязка контекста
Один простой декоратор вы уже видели ранее -- это функция [bind](/bind):
```js
function bind(func, context) {
return function() {
return func.apply(context, arguments);
};
}
```
Вызов `bind(func, context)` возвращает обёртку, которая ставит `this` и передаёт основную работу функции `func`.
### Декоратор -- измеритель времени
Посмотрим немного более сложный декоратор, замеряющий время выполнения функции:
**При помощи декоратора `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;
}
}
// функция может быть произвольной, например такой:
function fibonacci(n) {
return (n > 2) ? fibonacci(n-1) + fibonacci(n-2) : 1;
}
*!*
// использование: завернём fibonacci в декоратор
fibonacci = timingDecorator(fibonacci, "fibo");
*/!*
// неоднократные вызовы...
alert( fibonacci(10) ); // 55
alert( fibonacci(20) ); // 6765
// ...
*!*
// в любой момент можно получить общее количество времени на вызовы
alert( timers.fibo + 'мс' );
*/!*
```
Обратим внимание на ключевую строку декоратора`var result = f.apply(this, arguments)`.
**Этот приём называется "форвардинг вызова" (от англ. forwarding): текущий контекст и аргументы через `apply` передаются в функцию, так что изнутри `f` всё выглядит так, как будто это была вызвана она, а не декоратор.**
### Декоратор для проверки типа
В JavaScript, как правило, пренебрегают проверками типа. В функцию, которая должна получать число, может быть передана строка, булево значение или даже объект.
Например:
```js
function sum(a, b) {
return a + b;
}
alert( sum(true, { name: "Вася", age: 35 }) ); // true[Object object]
```
Функция "как-то" отработала, но в реальной жизни такой вызов, скорее всего, будет следствием программной ошибки. Всё-таки `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
function save() { ... }
save = checkPermissionDecorator(save);
// Теперь вызов функции save() проверяет права
```
## Итого
Декоратор -- это обёртка над функцией, которая модифицирует её поведение. При этом основную работу по-прежнему выполняет функция.
**Декораторы можно не только повторно использовать, но и комбинировать!**
Это кардинально повышает их выразительную силу. Декораторы можно рассматривать как своего рода "фичи" или возможности, которые можно "нацепить" на любую функцию. Можно один, а можно несколько.
Скажем, используя декораторы, описанные выше, можно добавить к функции возможности по проверке типов данных, замеру времени и проверке доступа буквально одной строкой, не залезая при этом в её код, то есть (!) не увеличивая его сложность.
Предлагаю вашему вниманию задачи, которые помогут выяснить, насколько вы разобрались в декораторах. Далее в учебнике мы ещё встретимся с ними.
[head]
<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>
[/head]

View file

@ -0,0 +1,3 @@
# Методы объектов и контекст вызова
Начинаем изучать объектно-ориентированную разработку -- как работают объекты и функции, что такое контекст вызова и почему его значение нельзя предсказать. Как, всё же, гарантировать правильный контекст и многие другие, не самые простые, темы.