update
This commit is contained in:
parent
962caebbb7
commit
87bf53d076
1825 changed files with 94929 additions and 0 deletions
49
1-js/8-oop/1-about-oop/article.md
Normal file
49
1-js/8-oop/1-about-oop/article.md
Normal 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>
|
||||
|
||||
Далее мы поговорим подробно как про ООП, так и об основных принципах, которых нужно придерживаться.
|
|
@ -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(); // кофе приготовлен не будет
|
||||
```
|
||||
|
|
@ -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`, которое будет хранить текущий таймер.
|
352
1-js/8-oop/2-internal-external-interface/article.md
Normal file
352
1-js/8-oop/2-internal-external-interface/article.md
Normal 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>
|
||||
|
BIN
1-js/8-oop/2-internal-external-interface/coffee-inside.jpg
Executable file
BIN
1-js/8-oop/2-internal-external-interface/coffee-inside.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 87 KiB |
BIN
1-js/8-oop/2-internal-external-interface/coffee.jpg
Executable file
BIN
1-js/8-oop/2-internal-external-interface/coffee.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
|
@ -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` нет соответствующего свойства объекта, он конструирует ответ "на лету". Это нормально. Одна из целей существования геттеров/сеттеров -- как раз и есть изоляция внутренних свойств объекта, чтобы можно было их как угодно менять, генерировать "на лету", а внешний интерфейс оставался тем же.
|
|
@ -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() ); // Петя Иванов
|
||||
```
|
||||
|
28
1-js/8-oop/3-getters-setters/2-getter-power/solution.md
Normal file
28
1-js/8-oop/3-getters-setters/2-getter-power/solution.md
Normal 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;
|
||||
};
|
||||
*/!*
|
||||
}
|
||||
```
|
||||
|
32
1-js/8-oop/3-getters-setters/2-getter-power/task.md
Normal file
32
1-js/8-oop/3-getters-setters/2-getter-power/task.md
Normal 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` можно указать лишь при создании кофеварки и в дальнейшем её можно прочитать, но нельзя изменить.
|
|
@ -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();
|
||||
```
|
||||
|
|
@ -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();
|
||||
```
|
||||
|
66
1-js/8-oop/3-getters-setters/4-setter-onready/solution.md
Normal file
66
1-js/8-oop/3-getters-setters/4-setter-onready/solution.md
Normal 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>
|
58
1-js/8-oop/3-getters-setters/4-setter-onready/task.md
Normal file
58
1-js/8-oop/3-getters-setters/4-setter-onready/task.md
Normal 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` можно было изменить в любой момент до её срабатывания.
|
|
@ -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
|
||||
});
|
||||
```
|
||||
|
|
@ -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).
|
157
1-js/8-oop/3-getters-setters/article.md
Normal file
157
1-js/8-oop/3-getters-setters/article.md
Normal 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) подобный подход принят на уровне концепта.
|
||||
|
||||
## Задачи
|
||||
|
|
@ -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;
|
||||
```
|
||||
|
|
@ -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`.
|
407
1-js/8-oop/4-descriptors-getters-setters/article.md
Normal file
407
1-js/8-oop/4-descriptors-getters-setters/article.md
Normal 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>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
Изменения в методе `run`:
|
||||
|
||||
```js
|
||||
this.run = function() {
|
||||
*!*
|
||||
if (!this._enabled) {
|
||||
throw new Error("Кофеварка выключена");
|
||||
}
|
||||
*/!*
|
||||
|
||||
setTimeout(onReady, 1000);
|
||||
};
|
||||
```
|
||||
|
||||
[edit src="solution" /]
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 /]
|
|
@ -0,0 +1 @@
|
|||
[edit src="solution"/]
|
|
@ -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>
|
|
@ -0,0 +1,16 @@
|
|||
# Останавливать кофеварку при выключении
|
||||
|
||||
[importance 5]
|
||||
|
||||
Когда кофеварку выключают -- текущая варка кофе должна останавливаться.
|
||||
|
||||
Например, следующий код кофе не сварит:
|
||||
|
||||
```js
|
||||
var coffeeMachine = new CoffeeMachine(10000);
|
||||
coffeeMachine.enable();
|
||||
coffeeMachine.run();
|
||||
coffeeMachine.disable(); // остановит работу, ничего не выведет
|
||||
```
|
||||
|
||||
Реализуйте это на основе решения [предыдущей задачи](/task/coffeemachine-fix-run).
|
|
@ -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();
|
||||
};
|
||||
|
||||
}
|
||||
```
|
||||
|
67
1-js/8-oop/5-functional-inheritance/3-inherit-fridge/task.md
Normal file
67
1-js/8-oop/5-functional-inheritance/3-inherit-fridge/task.md
Normal 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;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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).
|
|
@ -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(); // ошибка, в холодильнике есть еда
|
||||
```
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Переопределите disable
|
||||
|
||||
[importance 5]
|
||||
|
||||
Переопределите метод `disable` холодильника, чтобы при наличии в нём еды он выдавал ошибку.
|
||||
|
||||
|
||||
Код для проверки:
|
||||
|
||||
```js
|
||||
var fridge = new Fridge(500);
|
||||
fridge.enable();
|
||||
fridge.addFood("кус-кус");
|
||||
fridge.disable(); // ошибка, в холодильнике есть еда
|
||||
```
|
||||
|
||||
В качестве исходного кода используйте решение [предыдущей задачи](/task/add-methods-fridge).
|
401
1-js/8-oop/5-functional-inheritance/article.md
Normal file
401
1-js/8-oop/5-functional-inheritance/article.md
Normal 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
3
1-js/8-oop/index.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# ООП в функциональном стиле
|
||||
|
||||
Инкапсуляция и наследование в функциональном стиле, а также расширенные возможности объектов JavaScript.
|
Loading…
Add table
Add a link
Reference in a new issue