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,49 @@
# Введение
На протяжении долгого времени в программировании применялся [процедурный подход](http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D1%86%D0%B5%D0%B4%D1%83%D1%80%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). При этом программа состоит из функций, вызывающих друг друга.
Гораздо позже появилось [объектно-ориентированное программирование](http://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%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) (ООП), которое позволяет группировать функции и данные в единой сущности -- "объекте".
Например, "пользователь", "меню", "компонент интерфейса"...
**Чтобы ООП-подход "работал", объект должен представлять собой законченную, интуитивно понятную сущность.**
[warn header="ООП -- это не просто объекты"]
В JavaScript объекты часто используются просто как коллекции.
Например, встроенный объект [Math](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Math) содержит функции (`Math.sin`, `Math.pow`, ...) и данные (константа `Math.PI`).
При таком использовании объектов мы не можем сказать, что "применён объектно-ориентированный подход". В частности, никакую "единую сущность" `Math` из себя не представляет.
[/warn]
Мы уже работали в ООП-стиле, создавая объекты такого вида:
```js
//+ run
function User(name) {
this.sayHi = function() {
alert("Привет, я " + name);
};
}
var vasya = new User("Вася"); // создали пользователя
vasya.sayHi(); // пользователь умеет говорить "Привет"
```
Здесь мы видим ярко выраженную сущность -- `User` (посетитель).
**При объектно-ориентированной разработке мы описываем происходящее на уровне объектов, которые создаются, меняют свои свойства, взаимодействуют друг с другом и (в случае браузера) со страницей, в общем, живут.**
ООП -- это наука о том, как делать правильную архитектуру. У неё есть свои принципы, по ним пишут книги, к примеру:
<ul>
<li><a href="http://www.ozon.ru/context/detail/id/3905587/?partner=iliakan">Объектно-ориентированный анализ и проектирование с примерами приложений.</a>
<i>Гради Буч и др.</i>.</li>
<li><a href="http://www.ozon.ru/context/detail/id/2457392/?partner=iliakan">Приемы объектно-ориентированного проектирования. Паттерны проектирования.</a>
<i>Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес.</i></li>
</ul>
Далее мы поговорим подробно как про ООП, так и об основных принципах, которых нужно придерживаться.

View file

@ -0,0 +1,40 @@
Кофеварка с новым методом:
```js
//+ run
function CoffeeMachine(power) {
this.waterAmount = 0;
var WATER_HEAT_CAPACITY = 4200;
*!*
var timerId;
*/!*
var self = this;
function getBoilTime() {
return self.waterAmount * WATER_HEAT_CAPACITY * 80 / power;
}
function onReady() {
alert('Кофе готово!');
}
this.run = function() {
timerId = setTimeout(onReady, getBoilTime());
};
*!*
this.stop = function() {
clearTimeout(timerId)
};
*/!*
}
var coffeeMachine = new CoffeeMachine(50000);
coffeeMachine.waterAmount = 200;
coffeeMachine.run();
coffeeMachine.stop(); // кофе приготовлен не будет
```

View file

@ -0,0 +1,41 @@
# Добавить метод и свойство кофеварке
[importance 5]
Улучшите готовый код кофеварки, который дан ниже: добавьте в кофеварку *публичный* метод `stop()`, который будет останавливать кипячение (через `clearTimeout`).
```js
function CoffeeMachine(power) {
this.waterAmount = 0;
var WATER_HEAT_CAPACITY = 4200;
var self = this;
function getBoilTime() {
return self.waterAmount * WATER_HEAT_CAPACITY * 80 / power;
}
function onReady() {
alert('Кофе готово!');
}
this.run = function() {
setTimeout(onReady, getBoilTime());
};
}
```
Вот такой код должен ничего не выводить:
```js
var coffeeMachine = new CoffeeMachine(50000);
coffeeMachine.waterAmount = 200;
coffeeMachine.run();
coffeeMachine.stop(); // кофе приготовлен не будет
```
P.S. Текущую температуру воды вычислять и хранить не требуется.
P.P.S. При решении вам, скорее всего, понадобится добавить *приватное* свойство `timerId`, которое будет хранить текущий таймер.

View file

@ -0,0 +1,352 @@
# Внутренний и внешний интерфейс
Один из важнейших принципов ООП -- отделение внутреннего интерфейса от внешнего.
Это -- обязательная практика в разработке чего угодно сложнее hello world.
Чтобы это понять, отвлечемся от разработки и переведем взгляд на объекты реального мира.
Как правило, устройства, с которыми мы имеем дело, весьма сложны. Но *разделение интерфейса на внешний и внутренний* позволяет использовать их без малейших проблем.
[cut]
## Пример из жизни
Например, кофеварка. Простая снаружи: кнопка, индикатор, отверстия,... И, конечно, результат -- кофе :)
<img src="coffee.jpg">
Но внутри... (картинка из пособия по ремонту)
<img src="coffee-inside.jpg">
Масса деталей. Но мы можем пользоваться ей, совершенно не зная об этом.
Кофеварки -- довольно-таки надежны, не правда ли? Можно пользоваться годами, и только когда что-то пойдет не так -- придется нести к мастеру.
Секрет надежности и простоты кофеварки -- в том, что все детали отлажены и *спрятаны* внутри.
Если снять с кофеварки защитный кожух, то использование её будет более сложным (куда нажимать?) и опасным (током ударить может).
Как мы увидим, объекты очень схожи с кофеварками.
Только для того, чтобы прятать внутренние детали, используется не кожух, а специальные средства языка и соглашения.
## Внутренний и внешний интерфейс
В программировании есть чёткое разграничение методов и свойств объекта на две группы:
<ul>
<li>*Внутренний интерфейс* -- это свойства и методы, доступ к которым может быть осуществлен только из других методов объекта, их также называют "приватными" (есть и другие термины, встретим их далее).</li>
</li>
<li>*Внешний интерфейс* -- это свойства и методы, доступные снаружи объекта, их называют "публичными".</li>
</ul>
Если продолжить аналогию с кофеваркой -- то, что спрятано внутри кофеварки: трубка кипятильника, нагревательный элемент, тепловой предохранитель и так далее -- это её внутренний интерфейс.
Внутренний интерфейс используется для обеспечения работоспособности объекта, его детали используют друг друга. Например, трубка кипятильника подключена к нагревательному элементу.
Но снаружи кофеварка закрыта специальным кожухом, чтобы никто к ним не подобрался. Детали скрыты и недоступны. Виден лишь внешний интерфейс.
**Все, что нужно для пользования объектом -- это внешний интерфейс.**
О внутреннем пользователю вообще знать не обязательно.
[smart header="Между приватным и публичным"]
Приватные свойства полностью закрыты для доступа снаружи, а публичные -- наоборот, полностью открыты. Это крайности, между которыми бывают промежуточные варианты.
<ul>
<li>В языке С++ можно открыть доступ к приватным переменным одного класса -- другому, объявив его "дружественным".</li>
<li>В языке Java можно объявлять переменные, которые доступны всем классам внутри "пакета".</li>
<li>Между объектами можно организовать "наследование" и сделать свойства открытыми только для "наследников", такой вариант доступа называют "защищённым".</li>
</ul>
В этом учебнике будем изучать наследование и защищённые свойства, но позже, а пока сосредоточимся на приватном и публичном доступе... И, конечно, использовать мы будем JavaScript :)
[/smart]
Это были общие слова по теории программирования.
Далее мы реализуем кофеварку на JavaScript с приватными и публичными свойствами. В кофеварке много деталей, мы конечно, не будем моделировать каждый винтик, а сосредоточимся на основных приёмах разработки.
### Шаг 1: публичное и приватное свойство
Конструктор кофеварок будет называться `CoffeeMachine`.
```js
//+ run
function CoffeeMachine(power) {
this.waterAmount = 0; // количество воды в кофеварке
alert('Создана кофеварка мощностью: ' + power + ' ватт');
}
// создать кофеварку
var coffeeMachine = new CoffeeMachine(100);
// залить воды
coffeeMachine.waterAmount = 200;
```
**Локальные переменные, включая параметры конструктора, являются приватными свойствами.**
В примере выше это `power` -- мощность кофеварки, которая указывается при создании и далее будет использована для расчёта времени кипячения.
К локальным переменным конструктора нельзя обратиться снаружи, но они доступны внутри самого конструктора.
**Свойства, записанные в `this`, являются публичными.**
Здесь свойство `waterAmount` записано в объект, а значит -- доступно для модификации снаружи. Можно доливать и выливать воду в любом количестве.
[smart header="Вопрос терминологии"]
Может возникнуть вопрос -- почему я назвал `power` "приватным свойством", ведь это локальная *переменная*, а никакое не *свойство* объекта?
Здесь небольшой конфликт терминологий.
Термины "приватное свойство/метод", "публичное свойство/метод" относятся к общей теории ООП. А их конкретная реализация в языке программирования может быть различной.
Здесь ООП-принцип "приватного свойства" реализован через локальные переменные, поэтому и "локальная переменная" и "приватное свойство" -- правильные термины, в зависимости от того, с какой точки зрения посмотреть -- кода или архитектуры ООП.
[/smart]
### Шаг 2: публичный и приватный методы
Добавим публичный метод `run`, запускающий кофеварку, а также вспомогательные внутренние методы `getBoilTime` и `onReady`:
```js
//+ run
function CoffeeMachine(power) {
this.waterAmount = 0;
*!*
// расчёт времени для кипячения
function getBoilTime() {
return 1000; // точная формула расчета будет позже
}
// что делать по окончании процесса
function onReady() {
alert('Кофе готово!');
}
this.run = function() {
// setTimeout - встроенная функция,
// она запустит onReady через getBoilTime() миллисекунд
setTimeout(onReady, getBoilTime());
};
*/!*
}
var coffeeMachine = new CoffeeMachine(100);
coffeeMachine.waterAmount = 200;
coffeeMachine.run();
```
**Приватные методы, такие как `onReady`, `getBoilTime` объявляются как вложенные функции.**
В результате естественным образом получается, что доступ к ним (через замыкание) имеют только другие функции, объявленные в том же конструкторе.
### Шаг 3: константа
Для расчёта времени на кипячение воды используется формула `c*m*ΔT / power`, где:
<ul>
<li>`c` -- коэффициент теплоёмкости воды, физическая константа равная `4200`.</li>
<li>`m` -- масса воды, которую нужно согреть.</li>
<li>`ΔT` -- температура, на которую нужно подогреть, будем считать, что изначально вода -- комнатной температуры 20°С, то есть до 100° нужно греть на `ΔT=80`.</li>
<li>`power` -- мощность.</li>
</ul>
Используем её в более реалистичном варианте `getBoilTime()`, включающем использование приватных свойств и константу:
```js
//+ run
"use strict"
function CoffeeMachine(power) {
this.waterAmount = 0;
*!*
// физическая константа - удельная теплоёмкость воды для getBoilTime
var WATER_HEAT_CAPACITY = 4200;
// расчёт времени для кипячения
function getBoilTime() {
return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power;
}
*/!*
// что делать по окончании процесса
function onReady() {
alert('Кофе готово!');
}
this.run = function() {
setTimeout(onReady, getBoilTime());
};
}
var coffeeMachine = new CoffeeMachine(1000);
coffeeMachine.waterAmount = 200;
coffeeMachine.run();
```
Удельная теплоёмкость `WATER_HEAT_CAPACITY` выделена большими буквами, так как это константа.
**Внимание, при запуске кода выше в методе `getBoilTime` будет ошибка. Как вы думаете, почему?**
### Шаг 4: доступ к объекту из внутреннего метода
Внутренний метод вызывается так: `getBoilTime()`. А чему при этом равен `this`?... Как вы наверняка помните, в современном стандарте он будет `undefined` (в старом -- `window`), из-за этого при чтении `this.waterAmount` возникнет ошибка!
Её можно решить, если вызвать `getBoilTime` с явным указанием контекста: `getBoilTime.call(this)`:
```js
//+ run
function CoffeeMachine(power) {
this.waterAmount = 0;
var WATER_HEAT_CAPACITY = 4200;
function getBoilTime() {
return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power;
}
function onReady() {
alert('Кофе готово!');
}
this.run = function() {
*!*
setTimeout(onReady, getBoilTime.call(this));
*/!*
};
}
// создаю кофеварку, мощностью 100000W чтобы кипятила быстро
var coffeeMachine = new CoffeeMachine(100000);
coffeeMachine.waterAmount = 200;
coffeeMachine.run();
```
Такой подход будет работать, но он не очень-то удобен. Ведь получается, что теперь везде, где мы хотим вызвать `getBoilTime`, нужно явно указывать контекст, т.е. писать `getBoilTime.call(this)`.
К счастью существуют более элегантные решения.
### Привязка через bind
Можно при объявлении привязать `getBoilTime` к объекту через `bind`, тогда вопрос контекста отпадёт сам собой:
```js
//+ run
function CoffeeMachine(power) {
this.waterAmount = 0;
var WATER_HEAT_CAPACITY = 4200;
*!*
var getBoilTime = function() {
return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power;
}.bind(this);
*/!*
function onReady() {
alert('Кофе готово!');
}
this.run = function() {
*!*
setTimeout(onReady, getBoilTime());
*/!*
};
}
var coffeeMachine = new CoffeeMachine(100000);
coffeeMachine.waterAmount = 200;
coffeeMachine.run();
```
Это решение будет работать, теперь функцию можно просто вызывать без `call`. Но объявление функции стало менее красивым.
### Сохранение this в замыкании
Пожалуй, самый удобный и часто применяемый путь решения состоит в том, чтобы предварительно скопировать `this` во вспомогательную переменную и обращаться из внутренних функций уже к ней.
Вот так:
```js
//+ run
function CoffeeMachine(power) {
this.waterAmount = 0;
var WATER_HEAT_CAPACITY = 4200;
*!*
var self = this;
function getBoilTime() {
return self.waterAmount * WATER_HEAT_CAPACITY * 80 / power;
}
*/!*
function onReady() {
alert('Кофе готово!');
}
this.run = function() {
setTimeout(onReady, getBoilTime());
};
}
var coffeeMachine = new CoffeeMachine(100000);
coffeeMachine.waterAmount = 200;
coffeeMachine.run();
```
Теперь `getBoilTime` получает `self` из замыкания.
**Конечно, чтобы это работало, мы не должны изменять `self`, а все приватные методы, которые хотят привязаться к текущему объекту, должны использовать внутри себя `self` вместо `this`.**
Вместо `self` можно использовать любое другое имя переменной, например `var me = this`.
## Что нам даст разделение доступов?
Итак, мы сделали кофеварку с публичными и приватными методами и заставили их корректно работать.
В терминологии ООП отделение и защита внутреннего интерфейса называется [инкапсуляция](http://ru.wikipedia.org/wiki/%D0%98%D0%BD%D0%BA%D0%B0%D0%BF%D1%81%D1%83%D0%BB%D1%8F%D1%86%D0%B8%D1%8F_%28%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%29).
Кратко перечислим бонусы, которые она даёт:
<dl>
<dt>Защита пользователей от выстрела себе в ногу</dt>
<dd>Представьте, команда разработчиков пользуется кофеваркой. Кофеварка создана фирмой "Лучшие Кофеварки" и, в общем, работает хорошо, но с неё сняли защитный кожух и, таким образом, внутренний интерфейс стал доступен.
Все разработчики цивилизованны -- и пользуются кофеваркой как обычно. Но хитрый Вася решил, что он самый умный, и подкрутил кое-что внутри кофеварки, чтобы кофе заваривался покрепче. Вася не знал, что те изменения, которые он произвёл, приведут к тому, что кофеварка испортится через два дня.
Виноват, разумеется, не только Вася, но и тот, кто снял защитный кожух с кофеварки, и тем самым позволил Васе проводить манипуляции.
В программировании -- то же самое. Если пользователь объекта будет менять то, что не рассчитано на изменение снаружи -- последствия могут быть непредсказуемыми.
</dd>
<dt>Удобство в поддержке</dt>
<dd>Ситуация в программировании сложнее, чем с кофеваркой, т.к. кофеварку один раз купили и всё, а программа может улучшаться и дорабатываться.
**При наличии чётко выделенного внешнего интерфейса, разработчик может свободно менять внутренние свойства и методы, без оглядки на коллег.**
Гораздо легче разрабатывать, если знаешь, что ряд методов (все внутренние) можно переименовывать, менять их параметры, и вообще, переписать как угодно, так как внешний код к ним абсолютно точно не обращается.
Ближайшая аналогия в реальной жизни -- это когда выходит "новая версию" кофеварки, которая работает гораздо лучше. Разработчик мог переделать всё внутри, но пользоваться ей по-прежнему просто, так как внешний интерфейс сохранён.</dd>
<dt>Управление сложностью</dt>
<dd>Люди обожают пользоваться вещами, которые просты с виду. А что внутри -- дело десятое.
Программисты здесь не исключение.
**Всегда удобно, когда детали реализации скрыты, и доступен простой, понятно документированный внешний интерфейс.**
</dd>
</dl>

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,29 @@
Решение:
```js
//+ run
function User() {
var firstName, surName;
this.setFirstName = function(newFirstName) {
firstName = newFirstName;
};
this.setSurname = function(newSurname) {
surname = newSurname;
};
this.getFullName = function() {
return firstName + ' ' + surname;
}
}
var user = new User();
user.setFirstName("Петя");
user.setSurname("Иванов");
alert( user.getFullName() ); // Петя Иванов
```
Обратим внимание, что для "геттера" `getFullName` нет соответствующего свойства объекта, он конструирует ответ "на лету". Это нормально. Одна из целей существования геттеров/сеттеров -- как раз и есть изоляция внутренних свойств объекта, чтобы можно было их как угодно менять, генерировать "на лету", а внешний интерфейс оставался тем же.

View file

@ -0,0 +1,25 @@
# Написать объект с геттерами и сеттерами
[importance 4]
Напишите конструктор `User` для создания объектов:
<ul>
<li>С приватными свойствами имя `firstName` и фамилия `surname`.</li>
<li>С сеттерами для этих свойств.</li>
<li>С геттером `getFullName()`, который возвращает полное имя.</li>
</ul>
Должен работать так:
```js
function User() {
/* ваш код */
}
var user = new User();
user.setFirstName("Петя");
user.setSurname("Иванов");
alert( user.getFullName() ); // Петя Иванов
```

View file

@ -0,0 +1,28 @@
```js
function CoffeeMachine(power, capacity) {
//...
this.setWaterAmount = function(amount) {
if (amount < 0) {
throw new Error("Значение должно быть положительным");
}
if (amount > capacity) {
throw new Error("Нельзя залить воды больше, чем " + capacity);
}
waterAmount = amount;
};
this.getWaterAmount = function() {
return waterAmount;
};
*!*
this.getPower = function() {
return power;
};
*/!*
}
```

View file

@ -0,0 +1,32 @@
# Добавить геттер для power
[importance 5]
Добавьте кофеварке геттер для приватного свойства `power`, чтобы внешний код мог узнать мощность кофеварки.
Исходный код:
```js
function CoffeeMachine(power, capacity) {
//...
this.setWaterAmount = function(amount) {
if (amount < 0) {
throw new Error("Значение должно быть положительным");
}
if (amount > capacity) {
throw new Error("Нельзя залить воды больше, чем " + capacity);
}
waterAmount = amount;
};
this.getWaterAmount = function() {
return waterAmount;
};
}
```
Обратим внимание, что ситуация, когда у свойства `power` есть геттер, но нет сеттера -- вполне обычна.
Здесь это означает, что мощность `power` можно указать лишь при создании кофеварки и в дальнейшем её можно прочитать, но нельзя изменить.

View file

@ -0,0 +1,46 @@
В решении ниже `addWater` будет просто вызывать `setWaterAmount`.
```js
//+ run
function CoffeeMachine(power, capacity) {
var waterAmount = 0;
var WATER_HEAT_CAPACITY = 4200;
function getTimeToBoil() {
return waterAmount * WATER_HEAT_CAPACITY * 80 / power;
}
this.setWaterAmount = function(amount) {
if (amount < 0) {
throw new Error("Значение должно быть положительным");
}
if (amount > capacity) {
throw new Error("Нельзя залить больше, чем " + capacity);
}
waterAmount = amount;
};
*!*
this.addWater = function(amount) {
this.setWaterAmount(waterAmount + amount);
};
*/!*
function onReady() {
alert('Кофе готов!');
}
this.run = function() {
setTimeout(onReady, getTimeToBoil());
};
}
var coffeeMachine = new CoffeeMachine(100000, 400);
coffeeMachine.addWater(200);
coffeeMachine.addWater(100);
coffeeMachine.addWater(300); // Нельзя залить больше..
coffeeMachine.run();
```

View file

@ -0,0 +1,51 @@
# Добавить публичный метод кофеварке
[importance 5]
Добавьте кофеварке публичный метод `addWater(amount)`, который будет добавлять воду.
При этом, конечно же, должны происходить все необходимые проверки -- на положительность и превышение ёмкости.
Исходный код:
```js
function CoffeeMachine(power, capacity) {
var waterAmount = 0;
var WATER_HEAT_CAPACITY = 4200;
function getTimeToBoil() {
return waterAmount * WATER_HEAT_CAPACITY * 80 / power;
}
this.setWaterAmount = function(amount) {
if (amount < 0) {
throw new Error("Значение должно быть положительным");
}
if (amount > capacity) {
throw new Error("Нельзя залить больше, чем " + capacity);
}
waterAmount = amount;
};
function onReady() {
alert('Кофе готов!');
}
this.run = function() {
setTimeout(onReady, getTimeToBoil());
};
}
```
Вот такой код должен приводить к ошибке:
```js
var coffeeMachine = new CoffeeMachine(100000, 400);
coffeeMachine.addWater(200);
coffeeMachine.addWater(100);
coffeeMachine.addWater(300); // Нельзя залить больше, чем 400
coffeeMachine.run();
```

View file

@ -0,0 +1,66 @@
```js
//+ run
function CoffeeMachine(power, capacity) {
var waterAmount = 0;
var WATER_HEAT_CAPACITY = 4200;
function getTimeToBoil() {
return waterAmount * WATER_HEAT_CAPACITY * 80 / power;
}
this.setWaterAmount = function(amount) {
// ... проверки пропущены для краткости
waterAmount = amount;
};
this.getWaterAmount = function(amount) {
return waterAmount;
};
function onReady() {
alert('Кофе готов!');
}
*!*
this.setOnReady = function(newOnReady) {
onReady = newOnReady;
};
*/!*
this.run = function() {
*!*
setTimeout(function() { onReady(); }, getTimeToBoil());
*/!*
};
}
var coffeeMachine = new CoffeeMachine(20000, 500);
coffeeMachine.setWaterAmount(150);
coffeeMachine.run();
*!*
coffeeMachine.setOnReady(function() {
var amount = coffeeMachine.getWaterAmount();
alert('Готов кофе: ' + amount + 'мл'); // Готов кофе: 150 мл
});
*/!*
```
Обратите внимание на два момента в решении:
<ol>
<li>В сеттере `setOnReady` параметр называется `newOnReady`. Мы не можем назвать его `onReady`, так как тогда изнутри сеттера мы никак не доберёмся до внешнего (старого значения):
```js
// нерабочий вариант
this.setOnReady = function(onReady) {
onReady = onReady; // ??? внешняя переменная onReady недоступна
};
```
</li>
<li>Чтобы `setOnReady` можно было вызывать в любое время, в `setTimeout` передаётся не `onReady`, а анонимная функция `function() { onReady() }`, которая возьмёт текущий (установленный последним) `onReady` из замыкания.</li>
</ol>

View file

@ -0,0 +1,58 @@
# Создать сеттер для onReady
[importance 5]
Обычно когда кофе готов, мы хотим что-то сделать, например выпить его.
Сейчас при готовности срабатывает функция `onReady`, но она жёстко задана в коде:
```js
function CoffeeMachine(power, capacity) {
var waterAmount = 0;
var WATER_HEAT_CAPACITY = 4200;
function getTimeToBoil() {
return waterAmount * WATER_HEAT_CAPACITY * 80 / power;
}
this.setWaterAmount = function(amount) {
// ... проверки пропущены для краткости
waterAmount = amount;
};
this.getWaterAmount = function(amount) {
return waterAmount;
};
*!*
function onReady() {
alert('Кофе готов!');
}
*/!*
this.run = function() {
setTimeout(onReady, getTimeToBoil());
};
}
```
Создайте сеттер `setOnReady`, чтобы код снаружи мог назначить свой `onReady`, вот так:
```js
var coffeeMachine = new CoffeeMachine(20000, 500);
coffeeMachine.setWaterAmount(150);
*!*
coffeeMachine.setOnReady(function() {
var amount = coffeeMachine.getWaterAmount();
alert('Готов кофе: ' + amount + 'мл'); // Кофе готов: 150 мл
});
*/!*
coffeeMachine.run();
```
P.S. Значение `onReady` по умолчанию должно быть таким же, как и раньше.
P.P.S. Постарайтесь сделать так, чтобы `setOnReady` можно было вызвать не только до, но и *после* запуска кофеварки, то есть чтобы функцию `onReady` можно было изменить в любой момент до её срабатывания.

View file

@ -0,0 +1,61 @@
Код решения модифицирует функцию `run` и добавляет приватный идентификатор таймера `timerId`, по наличию которого мы судим о состоянии кофеварки:
```js
//+ run
function CoffeeMachine(power, capacity) {
var waterAmount = 0;
*!*
var timerId;
this.isRunning = function() {
return !!timerId;
};
*/!*
var WATER_HEAT_CAPACITY = 4200;
function getTimeToBoil() {
return waterAmount * WATER_HEAT_CAPACITY * 80 / power;
}
this.setWaterAmount = function(amount) {
// ... проверки пропущены для краткости
waterAmount = amount;
};
this.getWaterAmount = function(amount) {
return waterAmount;
};
function onReady() {
alert('Кофе готов!');
}
this.setOnReady = function(newOnReady) {
onReady = newOnReady;
};
this.run = function() {
*!*
timerId = setTimeout(function() {
timerId = null;
onReady();
}, getTimeToBoil());
};
*/!*
}
var coffeeMachine = new CoffeeMachine(20000, 500);
coffeeMachine.setWaterAmount(100);
alert('До: ' + coffeeMachine.isRunning()); // До: false
coffeeMachine.run();
alert('В процессе: ' + coffeeMachine.isRunning()); // В процессе: true
coffeeMachine.setOnReady(function() {
alert("После: " + coffeeMachine.isRunning()); // После: false
});
```

View file

@ -0,0 +1,25 @@
# Добавить метод isRunning
[importance 5]
Из внешнего кода мы хотели бы иметь возможность понять -- запущена кофеварка или нет.
Для этого добавьте кофеварке публичный метод `isRunning()`, который будет возвращать `true`, если она запущена и `false`, если нет.
Нужно, чтобы такой код работал:
```js
var coffeeMachine = new CoffeeMachine(20000, 500);
coffeeMachine.setWaterAmount(100);
alert('До: ' + coffeeMachine.isRunning()); // До: false
coffeeMachine.run();
alert('В процессе: ' + coffeeMachine.isRunning()); // В процессе: true
coffeeMachine.setOnReady(function() {
alert("После: " + coffeeMachine.isRunning()); // После: false
});
```
Исходный код возьмите из решения [предыдущей задачи](/task/setter-onReady).

View file

@ -0,0 +1,157 @@
# Геттеры и сеттеры
Для *управляемого* доступа к состоянию объекта используют специальные функции, так называемые "геттеры" и "сеттеры".
[cut]
## Геттер и сеттер для воды
На текущий момент количество воды в кофеварке является публичным свойством `waterAmount`:
```js
//+ run
function CoffeeMachine(power) {
// количество воды в кофеварке
this.waterAmount = 0;
...
}
```
Это немного опасно. Ведь в это свойство можно записать произвольное количество воды, хоть весь мировой океан.
```js
// не помещается в кофеварку!
coffeeMachine.waterAmount = 1000000;
```
Это ещё ничего, гораздо хуже, что можно наоборот -- вылить больше, чем есть:
```js
// и не волнует, было ли там столько воды вообще!
coffeeMachine.waterAmount -= 1000000;
```
Так происходит потому, что свойство полностью доступно снаружи.
Чтобы не было таких казусов, нам нужно ограничить контроль над свойством со стороны внешнего кода.
**Для лучшего контроля над свойством его делают приватным, а запись значения осуществляется через специальный метод, который называют *"сеттер"* (setter method).**
Типичное название для сеттера -- `setСвойство`, например, в случае с кофеваркой таким сеттером будет метод `setWaterAmount`:
```js
//+ run
function CoffeeMachine(power, capacity) { // capacity - ёмкость кофеварки
var waterAmount = 0;
var WATER_HEAT_CAPACITY = 4200;
function getTimeToBoil() {
return waterAmount * WATER_HEAT_CAPACITY * 80 / power;
}
*!*
// "умная" установка свойства
this.setWaterAmount = function(amount) {
if (amount < 0) {
throw new Error("Значение должно быть положительным");
}
if (amount > capacity) {
throw new Error("Нельзя залить воды больше, чем " + capacity);
}
waterAmount = amount;
};
*/!*
function onReady() {
alert('Кофе готов!');
}
this.run = function() {
setTimeout(onReady, getTimeToBoil());
};
}
var coffeeMachine = new CoffeeMachine(1000, 500);
coffeeMachine.setWaterAmount(600); // упс, ошибка!
```
Теперь `waterAmount` -- внутреннее свойство, его можно записать (через сеттер), но, увы, нельзя прочитать.
**Для того, чтобы дать возможность внешнему коду узнать его значение, создадим специальную функцию -- "геттер" (getter method).**
Геттеры обычно имеют название вида `getСвойство`, в данном случае `getWaterAmount`:
```js
//+ run
function CoffeeMachine(power, capacity) {
//...
this.setWaterAmount = function(amount) {
if (amount < 0) {
throw new Error("Значение должно быть положительным");
}
if (amount > capacity) {
throw new Error("Нельзя залить воды больше, чем " + capacity);
}
waterAmount = amount;
};
*!*
this.getWaterAmount = function() {
return waterAmount;
};
*/!*
}
var coffeeMachine = new CoffeeMachine(1000, 500);
coffeeMachine.setWaterAmount(450);
alert( coffeeMachine.getWaterAmount() ); // 450
```
## Единый геттер-сеттер
Для большего удобства иногда делают единый метод, который называется так же, как свойство и отвечает *и за запись и за чтение*.
При вызове без параметров такой метод возвращает свойство, а при передаче параметра -- назначает его.
Выглядит это так:
```js
//+ run
function CoffeeMachine(power, capacity) {
var waterAmount = 0;
*!*
this.waterAmount = function(amount) {
*/!*
// вызов без параметра, значит режим геттера, возвращаем свойство
if (!arguments.length) return waterAmount;
// иначе режим сеттера
if (amount < 0) {
throw new Error("Значение должно быть положительным");
}
if (amount > capacity) {
throw new Error("Нельзя залить воды больше, чем " + capacity);
}
waterAmount = amount;
};
}
var coffeeMachine = new CoffeeMachine(1000, 500);
// пример использования
*!*
coffeeMachine.waterAmount(450);
alert( coffeeMachine.waterAmount() ); // 450
*/!*
```
Единый геттер-сеттер используется реже, чем две отдельные функции, но в некоторых JavaScript-библиотеках, например [jQuery](http://jquery.com) и [D3](http://d3js.org) подобный подход принят на уровне концепта.
## Задачи

View file

@ -0,0 +1,27 @@
```js
//+ run
function CoffeeMachine(power, capacity) {
var waterAmount = 0;
Object.defineProperty(this, "waterAmount", {
get: function() {
return waterAmount;
},
set: function(amount) {
if (amount > capacity) {
throw new Error("Нельзя залить больше, чем " + capacity);
}
waterAmount = amount;
}
});
}
var coffeeMachine = new CoffeeMachine(1000, 300);
coffeeMachine.waterAmount = 500;
```

View file

@ -0,0 +1,22 @@
# Заменить свойство на встроенные геттеры/сеттеры
[importance 5]
Вам попал в руки код кофеварки, который использует свойство `this.waterAmount` для хранения количества воды:
```js
function CoffeeMachine(power, capacity) {
// количество воды в кофеварке
this.waterAmount = 0;
}
// создать кофеварку
var coffeeMachine = new CoffeeMachine(1000, 300);
// залить воды
coffeeMachine.waterAmount = 500;
```
Задача -- сделать так, чтобы при присвоении `coffeeMachine.waterAmount = 500` выдавалась ошибка, если значение больше `capacity` (в примере выше `300`).
Для этого реализуйте `waterAmount` через геттер и сеттер, который будет проверять корректность установки. Используйте для этого `Object.defineProperty`.

View file

@ -0,0 +1,407 @@
# Дескрипторы, геттеры и сеттеры свойств
В этой главе мы рассмотрим возможности, которые позволяют очень гибко и мощно управлять всеми свойствами объекта, включая их аспекты -- изменяемость, видимость в цикле `for..in` и даже "невидимые" геттеры-сеттеры.
Они поддерживаются всеми современными браузерами, но не IE8-. Точнее говоря, они поддерживаются даже в IE8, но не для всех объектов, а только для DOM-объектов (они используются при работе со страницей, это сейчас вне нашего рассмотрения).
Большая часть этих методов, в частности, работа с дескрипторами, не задействуется в других главах учебника для обеспечения совместимости с IE8-, но во вспомогательных скриптах -- библиотеках для тестирования, сборки, а также для сервера Node.JS они используются достаточно активно.
[cut]
## Дескрипторы в примерах
Основной метод для управления свойствами -- [Object.defineProperty](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperty).
Он позволяет определить свойство путём задания "дескриптора" -- описания, включающего в себя ряд важных внутренних параметров.
Синтаксис:
```js
Object.defineProperty(obj, prop, descriptor)
```
Аргументы:
<dl>
<dt>`obj`</dt>
<dd>Объект, в котором объявляется свойство.</dd>
<dt>`prop`</dt>
<dd>Имя свойства, которое нужно объявить или модифицировать.</dd>
<dt>`descriptor`</dt>
<dd>Дескриптор -- объект, который описывает поведение свойства. В нём могут быть следующие поля:
<dl>
<dt>`value`</dt>
<dd>Значение свойства, по умолчанию `undefined`</dd>
<dt>`writable`</dt>
<dd>Значение свойства можно менять, если `true`. По умолчанию `false`.</dd>
<dt>`configurable`</dt>
<dd>Если `true`, то свойство можно удалять, а также менять его в дальнейшем при помощи `defineProperty`. По умолчанию `false`.</dd>
<dt>`enumerable`</dt>
<dd>Если `true`, то свойство будет участвовать в переборе `for..in`. По умолчанию `false`.</dd>
<dt>`get`</dt>
<dd>Функция, которая возвращает значение свойства. По умолчанию `undefined`.</dd>
<dt>`set`</dt>
<dd>Функция, которая записывает значение свойства. По умолчанию `undefined`.</dd>
</dl>
</dd>
</dl>
Чтобы избежать конфликта, запрещено одновременно указывать значение `value` и функции `get/set`. Либо значение, либо функции для его чтения-записи, одно из двух. Также запрещено и не имеет смысла указывать `writable` при наличии `get/set`-функций.
Далее мы подробно разберём эти свойства на примерах.
### Пример: обычное свойство
Обычное свойство добавить очень просто.
Два таких вызова работают одинаково:
```js
var user = {};
// 1. простое присваивание
user.name = "Вася";
// 2. указание значения через дескриптор
Object.defineProperty(user, "name", { value: "Вася" });
```
### Пример: свойство-константа
Для того, чтобы сделать свойство неизменяемым, добавим ему флаги `writable` и `configurable`:
```js
//+ run
*!*
"use strict";
*/!*
var user = {};
Object.defineProperty(user, "name", {
value: "Вася",
writable: false, // запретить присвоение "user.name="
configurable: false // запретить удаление "delete user.name"
});
// Теперь попытаемся изменить это свойство.
// в strict mode присвоение "user.name=" вызовет ошибку
*!*
user.name = "Петя";
*/!*
```
**Заметим, что ошибки при попытке изменения такого свойства произойдут только при `use strict`.**
Без `use strict` операция записи "молча" не сработает.
### Пример: свойство, скрытое для for..in
Встроенный метод `toString`, как и большинство встроенных методов, не участвует в цикле `for..in`. Это удобно, так как обычно такое свойство является "служебным".
К сожалению, свойство `toString`, объявленное обычным способом, будет видно в цикле `for..in`, например:
```js
//+ run
var user = {
name: "Вася",
toString: function() { return this.name; }
};
*!*
for(var key in user) alert(key); // name, toString
*/!*
```
`Object.defineProperty` может помочь исключить `toString` из списка итерации. Достаточно поставить ему флаг `enumerable: false`:
```js
//+ run
var user = {
name: "Вася",
toString: function() { return this.name; }
};
*!*
Object.defineProperty(user, "toString", {enumerable: false});
for(var key in user) alert(key); // name
*/!*
```
Обратим внимание, вызов `defineProperty` не перезаписал свойство, а просто модифицировал настройки у существующего `toString`.
### Пример: свойство как функция-геттер
Дескриптор позволяет задать свойство, которое на самом деле работает как функция. Для этого в нём нужно указать эту функцию в `get`.
Например, у объекта `user` есть обычные свойства: имя `firstName` и фамилия `surname`.
Создадим свойство `fullName`, которое на самом деле является функцией:
```js
//+ run
var user = {
firstName: "Вася",
surname: "Петров"
}
Object.defineProperty(user, "fullName", {
*!*get*/!*: function() {
return this.firstName + ' ' + this.surname;
}
});
*!*
alert(user.fullName); // Вася Петров
*/!*
```
**Обратим внимание, снаружи это обычное свойство `user.fullName`.**
Лишь в описании указывается, что на самом деле его значение возвращается функцией.
### Пример: свойство геттер-сеттер
Также можно указать функцию, которая используется для записи значения, при помощи дескриптора `set`.
Например, добавим возможность присвоения `user.fullName` к примеру выше:
```js
//+ run
var user = {
firstName: "Вася",
surname: "Петров"
}
Object.defineProperty(user, "fullName", {
get: function() {
return this.firstName + ' ' + this.surname;
},
*!*
set: function(value) {
var split = value.split(' ');
this.firstName = split[0];
this.surname = split[1];
}
*/!*
});
*!*
user.fullName = "Петя Иванов";
*/!*
alert(user.firstName); // Петя
alert(user.surname); // Иванов
```
## Геттеры и сеттеры в литералах
Если мы создаём объект при помощи синтаксиса `{ ... }`, то задать геттеры/сеттеры можно прямо в его определении.
Для этого используется особый синтаксис: `get свойство` или `set свойство`.
Например, ниже объявлен геттер-сеттер `fullName`:
```js
//+ run
var user = {
firstName: "Вася",
surname: "Петров",
*!*
get fullName() {
*/!*
return this.firstName + ' ' + this.surname;
},
*!*
set fullName(value) {
*/!*
var split = value.split(' ');
this.firstName = split[0];
this.surname = split[1];
}
};
*!*
alert(user.fullName); // Вася Петров (из геттера)
user.fullName = "Петя Иванов";
alert(user.firstName); // Петя (поставил сеттер)
alert(user.surname); // Иванов (поставил сеттер)
*/!*
```
## Да здравствуют геттеры и сеттеры!
Казалось бы, зачем нам назначать геттеры и сеттеры через всякие хитрые вызовы? Можно же сделать функции `getFullName`, `setFullName`...
**Основной бонус -- возможность получить контроль над свойством в любой момент!**
В начале разработки мы можем использовать обычные свойства, например у `User` будет имя `name` и возраст `age`:
```js
function User(name, age) {
this.name = name;
this.age = age;
}
var pete = new User("Петя", 25);
alert(pete.age); // 25
```
**С обычными свойствами в коде меньше букв, они удобны.**
...Но рано или поздно может наступить расплата!
Например, когда написано много кода, который использует эти свойства, формат данных изменился и теперь вместо возраста `age` хранится дата рождения `birthday`:
```js
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
}
var pete = new User("Петя", new Date(1987, 6, 1));
```
Что теперь делать со старым кодом, который выводит свойство `age`?
Можно, конечно, найти все места и поправить их, но это долго, а иногда и невозможно, скажем, если вы взаимодействуете со сторонней библиотекой, код в которой -- чужой и влезать в него нежелательно.
Геттеры позволяют обойти проблему легко и непринуждённо.
Просто добавляем геттер `age`:
```js
//+ run
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
*!*
Object.defineProperty(this, "age", {
get: function() {
var todayYear = new Date().getFullYear();
return todayYear - this.birthday.getFullYear();
}
});
*/!*
}
var pete = new User("Петя", new Date(1987, 6, 1));
alert(pete.age); // получает возраст из даты рождения
```
**Таким образом, `defineProperty` позволяет нам использовать обычные свойства и, при необходимости, в любой момент заменить их на функции, сохраняя совместимость внешнего интерфейса.**
## Другие методы работы со свойствами
<dl>
<dt>[Object.defineProperties(obj, descriptors)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperties)</dt>
<dd>Позволяет объявить несколько свойств сразу:
```js
//+ run
var user = {}
Object.defineProperties(user, {
*!*
firstName: {
*/!*
value: "Петя"
},
*!*
surname: {
*/!*
value: "Иванов"
},
*!*
fullName: {
*/!*
get: function() {
return this.firstName + ' ' + this.surname;
}
}
});
alert( user.fullName ); // Петя Иванов
```
</dd>
<dt>[Object.keys(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys), [Object.getOwnPropertyNames(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames)</dt>
<dd>Возвращают массив -- список свойств объекта.
При этом `Object.keys` возвращает только `enumerable`-свойства, а `Object.getOwnPropertyNames` -- все:
```js
//+ run
var obj = {
a: 1,
b: 2,
internal: 3
};
Object.defineProperty(obj, "internal", {enumerable: false});
*!*
alert( Object.keys(obj) ); // a,b
alert( Object.getOwnPropertyNames(obj) ); // a, internal, b
*/!*
```
</dd>
<dt>[Object.getOwnPropertyDescriptor(prop)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor)</dt>
<dd>Возвращает дескриптор для свойства с `prop`.
Полученный дескриптор можно изменить и использовать `defineProperty` для сохранения изменений, например:
```js
//+ run
var obj = { test: 5 };
*!*
var descriptor = Object.getOwnPropertyDescriptor(obj, 'test');
*/!*
*!*
// заменим value на геттер, для этого...
*/!*
delete descriptor.value; // ..нужно убрать value/writable
delete descriptor.writable;
descriptor.get = function() { // и поставить get
alert("Preved :)");
};
*!*
// поставим новое свойство вместо старого
*/!*
// если не удалить - defineProperty объединит старый дескриптор с новым
delete obj.test;
Object.defineProperty(obj, 'test', descriptor);
obj.test; // Preved :)
```
</dd>
</dl>
...И несколько методов, которые используются очень редко:
<dl>
<dt>[Object.preventExtensions(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/seal)</dt>
<dd>Запрещает добавление свойств в объект.</dd>
<dt>[Object.seal(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/seal)</dt>
<dd>Запрещает добавление и удаление свойств, все текущие свойства делает `configurable: false`.</dd>
<dt>[Object.freeze(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/freeze)</dt>
<dd>Запрещает добавление, удаление и изменение свойств, все текущие свойства делает `configurable: false, writable: false`.</dd>
<dt>[Object.isExtensible(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isExtensible), [Object.isSealed(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isSealed), [Object.isFrozen(obj)](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/isFrozen)</dt>
<dd>Возвращают `true`, если на объекте были вызваны методы `Object.preventExtensions/seal/freeze`.</dd>
</dl>

View file

@ -0,0 +1,15 @@
Изменения в методе `run`:
```js
this.run = function() {
*!*
if (!this._enabled) {
throw new Error("Кофеварка выключена");
}
*/!*
setTimeout(onReady, 1000);
};
```
[edit src="solution" /]

View file

@ -0,0 +1,48 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
function Machine(power) {
this._enabled = false;
this.enable = function() {
this._enabled = true;
};
this.disable = function() {
this._enabled = false;
};
}
function CoffeeMachine(power) {
Machine.apply(this, arguments);
var waterAmount = 0;
this.setWaterAmount = function(amount) {
waterAmount = amount;
};
function onReady() {
alert('Кофе готов!');
}
this.run = function() {
if (!this._enabled) {
throw new Error("Кофеварка выключена");
}
setTimeout(onReady, 1000);
};
}
var coffeeMachine = new CoffeeMachine(10000);
coffeeMachine.run();
</script>
</body>
</html>

View file

@ -0,0 +1,42 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
function Machine(power) {
this._enabled = false;
this.enable = function() {
this._enabled = true;
};
this.disable = function() {
this._enabled = false;
};
}
function CoffeeMachine(power) {
Machine.apply(this, arguments);
var waterAmount = 0;
this.setWaterAmount = function(amount) {
waterAmount = amount;
};
function onReady() {
alert('Кофе готово!');
}
this.run = function() {
setTimeout(onReady, 1000);
};
}
</script>
</body>
</html>

View file

@ -0,0 +1,22 @@
# Запускать только при включённой кофеварке
[importance 5]
В коде `CoffeeMachine` сделайте так, чтобы метод `run` выводил ошибку, если кофеварка выключена.
В итоге должен работать такой код:
```js
var coffeeMachine = new CoffeeMachine(10000);
coffeeMachine.run(); // ошибка, кофеварка выключена!
```
А вот так -- всё в порядке:
```js
var coffeeMachine = new CoffeeMachine(10000);
coffeeMachine.enable();
coffeeMachine.run(); // ...Кофе готов!
```
[edit src="source" task /]

View file

@ -0,0 +1 @@
[edit src="solution"/]

View file

@ -0,0 +1,55 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
function Machine(power) {
this._enabled = false;
this.enable = function() {
this._enabled = true;
};
this.disable = function() {
this._enabled = false;
};
}
function CoffeeMachine(power) {
Machine.apply(this, arguments);
var waterAmount = 0;
var timerId;
this.setWaterAmount = function(amount) {
waterAmount = amount;
};
function onReady() {
alert('Кофе готов!');
}
var parentDisable = this.disable;
this.disable = function() {
parentDisable.call(this);
clearTimeout(timerId);
}
this.run = function() {
if (!this._enabled) {
throw new Error("Кофеварка выключена");
}
timerId = setTimeout(onReady, 1000);
};
}
var coffeeMachine = new CoffeeMachine(10000);
coffeeMachine.run();
</script>
</body>
</html>

View file

@ -0,0 +1,16 @@
# Останавливать кофеварку при выключении
[importance 5]
Когда кофеварку выключают -- текущая варка кофе должна останавливаться.
Например, следующий код кофе не сварит:
```js
var coffeeMachine = new CoffeeMachine(10000);
coffeeMachine.enable();
coffeeMachine.run();
coffeeMachine.disable(); // остановит работу, ничего не выведет
```
Реализуйте это на основе решения [предыдущей задачи](/task/coffeemachine-fix-run).

View file

@ -0,0 +1,29 @@
Решение:
```js
function Fridge(power) {
// унаследовать
Machine.apply(this, arguments);
var food = []; // приватное свойство food
this.addFood = function() {
if (!this._enabled) {
throw new Error("Холодильник выключен");
}
if (food.length + arguments.length >= this._power / 100) {
throw new Error("Нельзя добавить, не хватает мощности");
}
for(var i=0; i<arguments.length; i++) {
food.push(arguments[i]); // добавить всё из arguments
}
};
this.getFood = function() {
// копируем еду в новый массив, чтобы манипуляции с ним не меняли food
return food.slice();
};
}
```

View file

@ -0,0 +1,67 @@
# Унаследуйте холодильник
[importance 4]
Создайте класс для холодильника `Fridge(power)`, наследующий от `Machine`, с приватным свойством `food` и методами `addFood(...)`, `getFood()`:
<ul>
<li>Приватное свойство `food` хранит массив еды.</li>
<li>Публичный метод `addFood(item)` добавляет в массив `food` новую еду, доступен вызов с несколькими аргументами `addFood(item1, item2...)` для добавления нескольких элементов сразу.</li>
<li>Если холодильник выключен, то добавить еду нельзя, будет ошибка.</li>
<li>Максимальное количество еды ограничено `power/100`, где `power` -- мощность холодильника, указывается в конструкторе. При попытке добавить больше -- будет ошибка</li>
<li>Публичный метод `getFood()` возвращает еду в виде массива, добавление или удаление элементов из которого не должно влиять на свойство `food` холодильника.</li>
</ul>
Код для проверки:
```js
var fridge = new Fridge(200);
fridge.addFood("котлета"); // ошибка, холодильник выключен
```
Ещё код для проверки:
```js
// создать холодильник мощностью 500 (не более 5 еды)
var fridge = new Fridge(500);
fridge.enable();
fridge.addFood("котлета");
fridge.addFood("сок", "зелень");
fridge.addFood("варенье", "пирог", "торт"); // ошибка, слишком много еды
```
Код использования холодильника без ошибок:
```js
var fridge = new Fridge(500);
fridge.enable();
fridge.addFood("котлета");
fridge.addFood("сок", "варенье");
var fridgeFood = fridge.getFood();
alert(fridgeFood); // котлета, сок, варенье
// добавление элементов не влияет на еду в холодильнике
fridgeFood.push("вилка", "ложка");
alert(fridge.getFood()); // внутри по-прежнему: котлета, сок, варенье
```
Исходный код класса `Machine`, от которого нужно наследовать:
```js
function Machine(power) {
this._power = power;
this._enabled = false;
var self = this;
this.enable = function() {
self._enabled = true;
};
this.disable = function() {
self._enabled = false;
};
}
```

View file

@ -0,0 +1,77 @@
```js
//+ run
function Machine(power) {
this._power = power;
this._enabled = false;
var self = this;
this.enable = function() {
self._enabled = true;
};
this.disable = function() {
self._enabled = false;
};
}
function Fridge(power) {
// унаследовать
Machine.apply(this, arguments);
var food = []; // приватное свойство food
this.addFood = function() {
if (!this._enabled) {
throw new Error("Холодильник выключен");
}
if (food.length + arguments.length >= this._power / 100) {
throw new Error("Нельзя добавить, не хватает мощности");
}
for(var i=0; i<arguments.length; i++) {
food.push(arguments[i]); // добавить всё из arguments
}
};
this.getFood = function() {
// копируем еду в новый массив, чтобы манипуляции с ним не меняли food
return food.slice();
};
*!*
this.filterFood = function(filter) {
return food.filter(filter);
};
this.removeFood = function(item) {
var idx = food.indexOf(item);
if (idx != -1) food.splice(idx, 1);
};
*/!*
}
var fridge = new Fridge(500);
fridge.enable();
fridge.addFood({ title: "котлета", calories: 100 });
fridge.addFood({ title: "сок", calories: 30 });
fridge.addFood({ title: "зелень", calories: 10 });
fridge.addFood({ title: "варенье", calories: 150 });
var dietItems = fridge.filterFood(function(item) {
return item.calories < 50;
});
fridge.removeFood("нет такой еды"); // без эффекта
alert(fridge.getFood().length); // 4
dietItems.forEach(function(item) {
alert(item.title); // сок, зелень
fridge.removeFood(item);
});
alert(fridge.getFood().length); // 2
```

View file

@ -0,0 +1,36 @@
# Добавьте методы в холодильник
[importance 5]
Добавьте в холодильник методы:
<ul>
<li>Публичный метод `filterFood(func)`, который возвращает всю еду, для которой `func(item) == true`</li>
<li>Публичный метод `removeFood(item)`, который удаляет еду `item` из холодильника.</li>
</ul>
Код для проверки:
```js
var fridge = new Fridge(500);
fridge.enable();
fridge.addFood({ title: "котлета", calories: 100 });
fridge.addFood({ title: "сок", calories: 30 });
fridge.addFood({ title: "зелень", calories: 10 });
fridge.addFood({ title: "варенье", calories: 150 });
fridge.removeFood("нет такой еды"); // без эффекта
alert(fridge.getFood().length); // 4
var dietItems = fridge.filterFood(function(item) {
return item.calories < 50;
});
dietItems.forEach(function(item) {
alert(item.title); // сок, зелень
fridge.removeFood(item);
});
alert(fridge.getFood().length); // 2
```
В качестве исходного кода используйте решение [предыдущей задачи](/task/inherit-fridge).

View file

@ -0,0 +1,68 @@
```js
//+ run
function Machine(power) {
this._power = power;
this._enabled = false;
var self = this;
this.enable = function() {
self._enabled = true;
};
this.disable = function() {
self._enabled = false;
};
}
function Fridge(power) {
Machine.apply(this, arguments);
var food = []; // приватное свойство food
this.addFood = function() {
if (!this._enabled) {
throw new Error("Холодильник выключен");
}
if (food.length + arguments.length >= this._power / 100) {
throw new Error("Нельзя добавить, не хватает мощности");
}
for(var i=0; i<arguments.length; i++) {
food.push(arguments[i]); // добавить всё из arguments
}
};
this.getFood = function() {
// копируем еду в новый массив, чтобы манипуляции с ним не меняли food
return food.slice();
};
this.filterFood = function(filter) {
return food.filter(filter);
};
this.removeFood = function(item) {
var idx = food.indexOf(item);
if (idx != -1) food.splice(idx, 1);
};
*!*
var parentDisable = this.disable;
this.disable = function() {
if (food.length) {
throw new Error("Нельзя выключить: внутри еда");
}
parentDisable();
};
*/!*
}
var fridge = new Fridge(500);
fridge.enable();
fridge.addFood("кус-кус");
fridge.disable(); // ошибка, в холодильнике есть еда
```

View file

@ -0,0 +1,17 @@
# Переопределите disable
[importance 5]
Переопределите метод `disable` холодильника, чтобы при наличии в нём еды он выдавал ошибку.
Код для проверки:
```js
var fridge = new Fridge(500);
fridge.enable();
fridge.addFood("кус-кус");
fridge.disable(); // ошибка, в холодильнике есть еда
```
В качестве исходного кода используйте решение [предыдущей задачи](/task/add-methods-fridge).

View file

@ -0,0 +1,401 @@
# Функциональное наследование
Наследование -- это создание новых "классов" на основе существующих.
В JavaScript его можно реализовать несколькими путями, один из которых -- с использованием наложения конструкторов, мы рассмотрим в этой главе.
[cut]
## Зачем наследование?
Ранее мы обсуждали различные реализации кофеварки. Продолжим эту тему далее.
Хватит ли нам только кофеварки для удобной жизни? Вряд ли... Скорее всего, ещё понадобятся как минимум холодильник, микроволновка, а возможно и другие *машины*.
В реальной жизни у этих *машин* есть базовые правила пользования. Например, большая кнопка <i class="fa fa-power-off"></i> -- включение, шнур с розеткой нужно воткнуть в питание и т.п.
Можно сказать, что "у всех машин есть общие свойства, а конкретные машины могут их дополнять".
Именно поэтому, увидев новую технику, мы уже можем что-то с ней сделать, даже не читая инструкцию.
**Механизм наследования позволяет определить базовый класс `Машина`, в нём описать то, что свойственно всем машинам, а затем на его основе построить другие, более конкретные: `Кофеварка`, `Холодильник` и т.п.**
[smart header="В веб-разработке всё так же"]
В веб-разработке нам могут понадобиться классы `Меню`, `Табы`, `Диалог` и другие компоненты интерфейса.
Можно выделить полезный общий функционал в класс `Компонент` и наследовать их от него, чтобы не дублировать код. Это обычная практика, принятая во множестве библиотек.
[/smart]
## Наследование от Machine
Например, у нас есть класс `Machine`, который реализует методы "включить" `enable()` и "выключить" `disable()`:
```js
function Machine() {
var enabled = false;
this.enable = function() {
enabled = true;
};
this.disable = function() {
enabled = false;
};
}
```
Унаследуем от него кофеварку. При этом она получит эти методы автоматически:
```js
function CoffeeMachine(power) {
*!*
Machine.call(this);
*/!*
var waterAmount = 0;
this.setWaterAmount = function(amount) {
waterAmount = amount;
};
function onReady() {
alert('Кофе готово!');
}
this.run = function() {
setTimeout(onReady, 1000);
};
}
var coffeeMachine = new CoffeeMachine(10000);
coffeeMachine.enable();
```
Наследование реализовано вызовом `Machine.call(this)` в начале `CoffeeMachine`.
Он вызывает функцию `Machine`, передавая ей в качестве контекста `this` текущий объект. `Machine`, в процессе выполнения, записывает в `this` различные полезные свойства и методы, в нашем случае `this.enable` и `this.disable`.
Далее `CoffeeMachine` продолжает выполнение и может добавить свои свойства и методы, а также пользоваться унаследованными.
## Защищённые свойства
В коде выше есть одна проблема.
**Наследник не имеет доступа к приватным свойствам родителя.**
Иначе говоря, если кофеварка захочет обратиться к `enabled`, то её ждёт разочарование:
```js
//+ run
function Machine() {
var enabled = false;
this.enable = function() {
enabled = true;
};
this.disable = function() {
enabled = false;
};
}
function CoffeeMachine(power) {
Machine.call(this);
this.enable();
*!*
// ошибка, переменная не определена!
alert(enabled);
*/!*
}
var coffeeMachine = new CoffeeMachine(10000);
```
Это естественно, ведь `enabled` -- локальная переменная функции `Machine`. Она находится в другой области видимости.
**Чтобы наследник имел доступ к свойству, оно должно быть записано в `this`.**
**При этом, чтобы обозначить, что свойство является внутренним, его имя начинают с подчёркивания `_`.**
```js
//+ run
function Machine() {
*!*
this._enabled = false;
*/!*
this.enable = function() {
this._enabled = true;
};
this.disable = function() {
this._enabled = false;
};
}
function CoffeeMachine(power) {
Machine.call(this);
this.enable();
*!*
alert(this._enabled); // true
*/!*
}
var coffeeMachine = new CoffeeMachine(10000);
```
**Подчёркивание в начале свойства -- общепринятый знак, что свойство является внутренним, предназначенным лишь для доступа из самого объекта и его наследников. Такие свойства называют *защищёнными*.**
Технически это, конечно, возможно, но приличный программист снаружи в такое свойство не полезет.
**Вообще, это стандартная практика: конструктор сохраняет свои параметры в свойствах объекта. Иначе наследники не будут иметь к ним доступ.**
## Перенос свойства в защищённые
В коде выше есть свойство `power`. Сейчас мы его тоже сделаем защищённым и перенесём в `Machine`, поскольку "мощность" свойственна всем машинам, а не только кофеварке:
```js
//+ run
function CoffeeMachine(power) {
*!*
Machine.apply(this, arguments); // (1)
*/!*
alert(this._enabled); // false
alert(this._power); // 10000
}
function Machine(power) {
*!*
this._power = power; // (2)
*/!*
this._enabled = false;
this.enable = function() {
this._enabled = true;
};
this.disable = function() {
this._enabled = false;
};
}
var coffeeMachine = new CoffeeMachine(10000);
```
В коде выше при вызове `new CoffeeMachine(10000)` в строке `(1)` кофеварка передаёт аргументы и контекст родителю вызовом `Machine.apply(this, arguments)`.
Можно было бы использовать `Machine.call(this, power)`, но использование `apply` гарантирует передачу всех аргументов, мало ли, вдруг мы в будущем захотим их добавить.
Далее конструктор `Machine` в строке `(2)` сохраняет `power` в свойстве объекта `this._power`, благодаря этому кофеварка, когда наследование перейдёт обратно к `CoffeeMachine`, сможет сразу обращаться к нему.
## Переопределение методов
Итак, мы получили класс `CoffeeMachine`, который наследует от `Machine`.
Аналогичным образом мы можем унаследовать от `Machine` холодильник `Fridge`, микроволновку `MicroOven` и другие классы, которые разделяют общий "машинный" функционал.
Для этого достаточно вызвать `Machine` текущем контексте, а затем добавить свои методы.
```js
// Fridge может добавить и свои аргументы,
// которые в Machine не будут использованы
function Fridge(power, temperature) {
Machine.call(this, arguments);
// ...
}
```
Кроме создания новых методов, можно заменить унаследованные на свои:
```js
function CoffeeMachine(power, capacity) {
Machine.apply(this, arguments);
// переопределить this.enable
this.enable = function() {
/* enable для кофеварки */
};
}
```
...Однако, как правило, мы хотим не заменить, а *расширить* метод родителя. Например, сделать так, чтобы при включении кофеварка тут же запускалась.
Для этого метод родителя предварительно копируют в переменную, и затем вызывают внутри нового `enable` -- там, где считают нужным:
```js
function CoffeeMachine(power) {
Machine.apply(this, arguments);
*!*
var parentEnable = this.enable; // (1)
this.enable = function() { // (2)
parentEnable.call(this); // (3)
this.run(); // (4)
}
*/!*
...
}
```
**Общая схема переопределения метода (по строкам выделенного фрагмента кода):**
<ol>
<li>Мы скопировали доставшийся от родителя метод `enable` в переменную, например `parentEnable`.</li>
<li>Заменили метод `this.enable()` на свою функцию...</li>
<li>Которая по-прежнему реализует старый функционал через вызов `parentEnable`...</li>
<li>И в дополнение к нему делает что-то своё, например запускает приготовление кофе.</li>
</ol>
Обратим внимание на строку `(3)`.
В ней родительский метод вызывается так: `parentEnable.call(this)`. Если бы вызов был таким: `parentEnable()`, то ему бы не передался текущий `this` и возникла бы ошибка.
Технически, можно сделать возможность вызывать его и как `parentEnable()`, но тогда надо гарантировать, что контекст будет правильным, например привязать его при помощи `bind` или при объявлении, в родителе, вообще не использовать `this`, а получать контекст через замыкание, вот так:
```js
//+ run
function Machine(power) {
this._enabled = false;
*!*
var self = this;
this.enable = function() {
// используем внешнюю переменную вместо this
self._enabled = true;
};
*/!*
this.disable = function() {
this._enabled = false;
};
}
function CoffeeMachine(power) {
Machine.apply(this, arguments);
var waterAmount = 0;
this.setWaterAmount = function(amount) {
waterAmount = amount;
};
*!*
var parentEnable = this.enable;
this.enable = function() {
parentEnable(); // теперь можно вызывать как угодно, this не важен
this.run();
}
*/!*
function onReady() {
alert('Кофе готово!');
}
this.run = function() {
setTimeout(onReady, 1000);
};
}
var coffeeMachine = new CoffeeMachine(10000);
coffeeMachine.setWaterAmount(50);
coffeeMachine.enable();
```
В коде выше родительский метод `parentEnable = this.enable` успешно продолжает работать даже при вызове без контекста. А всё потому, что использует `self` внутри.
## Итого
Организация наследования, которая описана в этой главе, называется "функциональным паттерном наследования".
Её общая схема (кратко):
<ol>
<li>Объявляется конструктор родителя `Machine`. В нём могут быть приватные (private), публичные (public) и защищённые (protected) свойства:
```js
function Machine(params) {
// локальные переменные и функции доступны только внутри Machine
var private;
// публичные доступны снаружи
this.public = ...;
// защищённые доступны внутри Machine и для потомков
// мы договариваемся не трогать их снаружи
this._protected = ...
}
var machine = new Machine(...)
machine.public();
```
</li>
<li>Для наследования конструктор потомка вызывает родителя в своём контексте через `apply`. После чего может добавить свои переменные и методы:
```js
function CoffeeMachine(params) {
// универсальный вызов с передачей любых аргументов
*!*
Machine.apply(this, arguments);
*/!*
this.coffeePublic = ...
}
var coffeeMachine = new CoffeeMachine(...);
coffeeMachine.public();
coffeeMachine.coffeePublic();
```
</li>
<li>В `CoffeeMachine` свойства, полученные от родителя, можно перезаписать своими. Но обычно требуется не заменить, а расширить метод родителя. Для этого он предварительно копируется в переменную:
```js
function CoffeeMachine(params) {
Machine.apply(this, arguments);
*!*
var parentProtected = this._protected;
this._protected = function(args) {
parentProtected.call(this, args); // (*)
// ...
};
*/!*
}
```
Строку `(*)` можно упростить до `parentProtected(args)`, если метод родителя не использует `this`, а, например, привязан к `var self = this`:
```js
function Machine(params) {
var self = this;
this._protected = function() {
self.property = "value";
};
}
```
</li>
</ol>
**В следующих главах мы будем изучать прототипный подход, который обладаем рядом преимуществ, по сравнению с функциональным.**
Но функциональный тоже бывает полезен.
В своей практике разработки я обычно наследую функционально в тех случаях, когда *уже* есть какой-то код, который на нём построен. К примеру, уже существуют классы, написанные сторонними разработчиками, которые можно доопределить или расширить только так.

3
1-js/8-oop/index.md Normal file
View file

@ -0,0 +1,3 @@
# ООП в функциональном стиле
Инкапсуляция и наследование в функциональном стиле, а также расширенные возможности объектов JavaScript.