renovations
This commit is contained in:
parent
e1948130f6
commit
b76c541212
5 changed files with 106 additions and 65 deletions
|
@ -34,7 +34,13 @@ var vasya = new User("Вася"); // создали пользователя
|
||||||
vasya.sayHi(); // пользователь умеет говорить "Привет"
|
vasya.sayHi(); // пользователь умеет говорить "Привет"
|
||||||
```
|
```
|
||||||
|
|
||||||
Здесь мы видим ярко выраженную сущность -- `User` (посетитель).
|
Здесь мы видим ярко выраженную сущность -- `User` (посетитель). Используя терминологию ООП, такие конструкторы часто называют *классами*, то есть можно сказать "класс `User`".
|
||||||
|
|
||||||
|
[smart header="Класс в ООП"]
|
||||||
|
[Классом]("https://en.wikipedia.org/wiki/Class_(computer_programming)") в объектно-ориентированной разработке называют шаблон/программный код, предназначенный для создания объектов и методов.
|
||||||
|
|
||||||
|
В JavaScript классы можно организовать по-разному. Говорят, что класс `User` написан в "функциональном" стиле. Далее мы также увидим "прототипный" стиль.
|
||||||
|
[/smart]
|
||||||
|
|
||||||
ООП -- это наука о том, как делать правильную архитектуру. У неё есть свои принципы, например [SOLID](https://ru.wikipedia.org/wiki/SOLID_%28%D0%BE%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%29).
|
ООП -- это наука о том, как делать правильную архитектуру. У неё есть свои принципы, например [SOLID](https://ru.wikipedia.org/wiki/SOLID_%28%D0%BE%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%29).
|
||||||
|
|
||||||
|
@ -47,4 +53,4 @@ vasya.sayHi(); // пользователь умеет говор
|
||||||
<i>Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес.</i></li>
|
<i>Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес.</i></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
Здесь мы не имеем возможности углубиться в теорию ООП, но основных "китов", на которых оно стоит, потрогаем за усы вдумчиво и конкретно.
|
Здесь мы не имеем возможности углубиться в теорию ООП, поэтому чтение таких книг рекомендуется. Хотя основные принципы, как использовать ООП правильно, мы, всё же, затронем.
|
|
@ -17,17 +17,17 @@
|
||||||
|
|
||||||
Именно поэтому, увидев новую технику, мы уже можем что-то с ней сделать, даже не читая инструкцию.
|
Именно поэтому, увидев новую технику, мы уже можем что-то с ней сделать, даже не читая инструкцию.
|
||||||
|
|
||||||
**Механизм наследования позволяет определить базовый класс `Машина`, в нём описать то, что свойственно всем машинам, а затем на его основе построить другие, более конкретные: `Кофеварка`, `Холодильник` и т.п.**
|
Механизм наследования позволяет определить базовый класс `Машина`, в нём описать то, что свойственно всем машинам, а затем на его основе построить другие, более конкретные: `Кофеварка`, `Холодильник` и т.п.
|
||||||
|
|
||||||
[smart header="В веб-разработке всё так же"]
|
[smart header="В веб-разработке всё так же"]
|
||||||
В веб-разработке нам могут понадобиться классы `Меню`, `Табы`, `Диалог` и другие компоненты интерфейса.
|
В веб-разработке нам могут понадобиться классы `Меню`, `Табы`, `Диалог` и другие компоненты интерфейса. В них всех обычно есть что-то общее.
|
||||||
|
|
||||||
Можно выделить полезный общий функционал в класс `Компонент` и наследовать их от него, чтобы не дублировать код. Это обычная практика, принятая во множестве библиотек.
|
Можно выделить такой общий функционал в класс `Компонент` и наследовать их от него, чтобы не дублировать код.
|
||||||
[/smart]
|
[/smart]
|
||||||
|
|
||||||
## Наследование от Machine
|
## Наследование от Machine
|
||||||
|
|
||||||
Например, у нас есть класс `Machine`, который реализует методы "включить" `enable()` и "выключить" `disable()`:
|
Базовый класс "машина" `Machine` будет реализовывать общего вида методы "включить" `enable()` и "выключить" `disable()`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function Machine() {
|
function Machine() {
|
||||||
|
@ -48,33 +48,33 @@ function Machine() {
|
||||||
```js
|
```js
|
||||||
function CoffeeMachine(power) {
|
function CoffeeMachine(power) {
|
||||||
*!*
|
*!*
|
||||||
Machine.call(this);
|
Machine.call(this); // отнаследовать
|
||||||
*/!*
|
*/!*
|
||||||
|
|
||||||
var waterAmount = 0;
|
var waterAmount = 0;
|
||||||
|
|
||||||
this.setWaterAmount = function(amount) {
|
this.setWaterAmount = function(amount) {
|
||||||
waterAmount = amount;
|
waterAmount = amount;
|
||||||
};
|
};
|
||||||
|
|
||||||
function onReady() {
|
|
||||||
alert('Кофе готово!');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.run = function() {
|
|
||||||
setTimeout(onReady, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var coffeeMachine = new CoffeeMachine(10000);
|
var coffeeMachine = new CoffeeMachine(10000);
|
||||||
|
|
||||||
|
*!*
|
||||||
coffeeMachine.enable();
|
coffeeMachine.enable();
|
||||||
|
coffeeMachine.setWaterAmount(100);
|
||||||
|
coffeeMachine.disable();
|
||||||
|
*/!*
|
||||||
```
|
```
|
||||||
|
|
||||||
Наследование реализовано вызовом `Machine.call(this)` в начале `CoffeeMachine`.
|
Наследование реализовано вызовом `Machine.call(this)` в начале конструктора `CoffeeMachine`.
|
||||||
|
|
||||||
Он вызывает функцию `Machine`, передавая ей в качестве контекста `this` текущий объект. `Machine`, в процессе выполнения, записывает в `this` различные полезные свойства и методы, в нашем случае `this.enable` и `this.disable`.
|
Он вызывает функцию `Machine`, передавая ей в качестве контекста `this` текущий объект. `Machine`, в процессе выполнения, записывает в `this` различные полезные свойства и методы, в нашем случае `this.enable` и `this.disable`.
|
||||||
|
|
||||||
Далее `CoffeeMachine` продолжает выполнение и может добавить свои свойства и методы, а также пользоваться унаследованными.
|
Далее конструктор `CoffeeMachine` продолжает выполнение и может добавить свои свойства и методы.
|
||||||
|
|
||||||
|
В результате мы получаем объект `coffeeMachine`, который включает в себя методы из `Machine` и `CoffeeMachine`.
|
||||||
|
|
||||||
## Защищённые свойства
|
## Защищённые свойства
|
||||||
|
|
||||||
|
@ -116,13 +116,13 @@ var coffeeMachine = new CoffeeMachine(10000);
|
||||||
|
|
||||||
**Чтобы наследник имел доступ к свойству, оно должно быть записано в `this`.**
|
**Чтобы наследник имел доступ к свойству, оно должно быть записано в `this`.**
|
||||||
|
|
||||||
**При этом, чтобы обозначить, что свойство является внутренним, его имя начинают с подчёркивания `_`.**
|
При этом, чтобы обозначить, что свойство является внутренним, его имя начинают с подчёркивания `_`.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
//+ run
|
//+ run
|
||||||
function Machine() {
|
function Machine() {
|
||||||
*!*
|
*!*
|
||||||
this._enabled = false;
|
this._enabled = false; // вместо var enabled
|
||||||
*/!*
|
*/!*
|
||||||
|
|
||||||
this.enable = function() {
|
this.enable = function() {
|
||||||
|
@ -147,30 +147,19 @@ function CoffeeMachine(power) {
|
||||||
var coffeeMachine = new CoffeeMachine(10000);
|
var coffeeMachine = new CoffeeMachine(10000);
|
||||||
```
|
```
|
||||||
|
|
||||||
**Подчёркивание в начале свойства -- общепринятый знак, что свойство является внутренним, предназначенным лишь для доступа из самого объекта и его наследников. Такие свойства называют *защищёнными*.**
|
Подчёркивание в начале свойства -- общепринятый знак, что свойство является внутренним, предназначенным лишь для доступа из самого объекта и его наследников. Такие свойства называют *защищёнными*.
|
||||||
|
|
||||||
Технически это, конечно, возможно, но приличный программист снаружи в такое свойство не полезет.
|
Технически, залезть в него из внешнего кода, конечно, возможно, но приличный программист так делать не будет.
|
||||||
|
|
||||||
**Вообще, это стандартная практика: конструктор сохраняет свои параметры в свойствах объекта. Иначе наследники не будут иметь к ним доступ.**
|
|
||||||
|
|
||||||
## Перенос свойства в защищённые
|
## Перенос свойства в защищённые
|
||||||
|
|
||||||
В коде выше есть свойство `power`. Сейчас мы его тоже сделаем защищённым и перенесём в `Machine`, поскольку "мощность" свойственна всем машинам, а не только кофеварке:
|
У `CoffeeMachine` есть приватное свойство `power`. Сейчас мы его тоже сделаем защищённым и перенесём в `Machine`, поскольку "мощность" свойственна всем машинам, а не только кофеварке.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
//+ run
|
//+ run
|
||||||
function CoffeeMachine(power) {
|
|
||||||
*!*
|
|
||||||
Machine.apply(this, arguments); // (1)
|
|
||||||
*/!*
|
|
||||||
|
|
||||||
alert(this._enabled); // false
|
|
||||||
alert(this._power); // 10000
|
|
||||||
}
|
|
||||||
|
|
||||||
function Machine(power) {
|
function Machine(power) {
|
||||||
*!*
|
*!*
|
||||||
this._power = power; // (2)
|
this._power = power; // (1)
|
||||||
*/!*
|
*/!*
|
||||||
|
|
||||||
this._enabled = false;
|
this._enabled = false;
|
||||||
|
@ -184,20 +173,29 @@ function Machine(power) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CoffeeMachine(power) {
|
||||||
|
*!*
|
||||||
|
Machine.apply(this, arguments); // (2)
|
||||||
|
*/!*
|
||||||
|
|
||||||
|
alert(this._enabled); // false
|
||||||
|
alert(this._power); // 10000
|
||||||
|
}
|
||||||
|
|
||||||
var coffeeMachine = new CoffeeMachine(10000);
|
var coffeeMachine = new CoffeeMachine(10000);
|
||||||
```
|
```
|
||||||
|
|
||||||
В коде выше при вызове `new CoffeeMachine(10000)` в строке `(1)` кофеварка передаёт аргументы и контекст родителю вызовом `Machine.apply(this, arguments)`.
|
Теперь все машины `Machine` имеют мощность `power`. Обратим внимание, что мы из параметра конструктора сразу скопировали её в объект в строке `(1)`. Иначе она была бы недоступна из наследников.
|
||||||
|
|
||||||
Можно было бы использовать `Machine.call(this, power)`, но использование `apply` гарантирует передачу всех аргументов, мало ли, вдруг мы в будущем захотим их добавить.
|
В строке `(2)` мы теперь вызываем не просто `Machine.call(this)`, а расширенный вариант: `Machine.apply(this, arguments)`, который вызывает `Machine` в текущем контексте вместе с передачей текущих аргументов.
|
||||||
|
|
||||||
Далее конструктор `Machine` в строке `(2)` сохраняет `power` в свойстве объекта `this._power`, благодаря этому кофеварка, когда наследование перейдёт обратно к `CoffeeMachine`, сможет сразу обращаться к нему.
|
Можно было бы использовать и более простой вызов `Machine.call(this, power)`, но использование `apply` гарантирует передачу всех аргументов, вдруг их количество увеличится -- не надо будет переписывать.
|
||||||
|
|
||||||
## Переопределение методов
|
## Переопределение методов
|
||||||
|
|
||||||
Итак, мы получили класс `CoffeeMachine`, который наследует от `Machine`.
|
Итак, мы получили класс `CoffeeMachine`, который наследует от `Machine`.
|
||||||
|
|
||||||
Аналогичным образом мы можем унаследовать от `Machine` холодильник `Fridge`, микроволновку `MicroOven` и другие классы, которые разделяют общий "машинный" функционал.
|
Аналогичным образом мы можем унаследовать от `Machine` холодильник `Fridge`, микроволновку `MicroOven` и другие классы, которые разделяют общий "машинный" функционал, то есть имеют мощность и их можно включать/выключать.
|
||||||
|
|
||||||
Для этого достаточно вызвать `Machine` текущем контексте, а затем добавить свои методы.
|
Для этого достаточно вызвать `Machine` текущем контексте, а затем добавить свои методы.
|
||||||
|
|
||||||
|
@ -205,13 +203,15 @@ var coffeeMachine = new CoffeeMachine(10000);
|
||||||
// Fridge может добавить и свои аргументы,
|
// Fridge может добавить и свои аргументы,
|
||||||
// которые в Machine не будут использованы
|
// которые в Machine не будут использованы
|
||||||
function Fridge(power, temperature) {
|
function Fridge(power, temperature) {
|
||||||
Machine.call(this, arguments);
|
Machine.apply(this, arguments);
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Кроме создания новых методов, можно заменить унаследованные на свои:
|
Бывает так, что реализация конкретного метода машины в наследнике имеет свои особенности.
|
||||||
|
|
||||||
|
Можно, конечно, объявить в `CoffeeMachine` свой `enable`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
function CoffeeMachine(power, capacity) {
|
function CoffeeMachine(power, capacity) {
|
||||||
|
@ -224,7 +224,7 @@ function CoffeeMachine(power, capacity) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
...Однако, как правило, мы хотим не заменить, а *расширить* метод родителя. Например, сделать так, чтобы при включении кофеварка тут же запускалась.
|
...Однако, как правило, мы хотим не заменить, а *расширить* метод родителя, добавить к нему что-то. Например, сделать так, чтобы при включении кофеварка тут же запускалась.
|
||||||
|
|
||||||
Для этого метод родителя предварительно копируют в переменную, и затем вызывают внутри нового `enable` -- там, где считают нужным:
|
Для этого метод родителя предварительно копируют в переменную, и затем вызывают внутри нового `enable` -- там, где считают нужным:
|
||||||
|
|
||||||
|
@ -247,10 +247,10 @@ function CoffeeMachine(power) {
|
||||||
**Общая схема переопределения метода (по строкам выделенного фрагмента кода):**
|
**Общая схема переопределения метода (по строкам выделенного фрагмента кода):**
|
||||||
|
|
||||||
<ol>
|
<ol>
|
||||||
<li>Мы скопировали доставшийся от родителя метод `enable` в переменную, например `parentEnable`.</li>
|
<li>Копируем доставшийся от родителя метод `this.enable` в переменную, например `parentEnable`.</li>
|
||||||
<li>Заменили метод `this.enable()` на свою функцию...</li>
|
<li>Заменяем `this.enable` на свою функцию...</li>
|
||||||
<li>Которая по-прежнему реализует старый функционал через вызов `parentEnable`...</li>
|
<li>...Которая по-прежнему реализует старый функционал через вызов `parentEnable`.</li>
|
||||||
<li>И в дополнение к нему делает что-то своё, например запускает приготовление кофе.</li>
|
<li>...И в дополнение к нему делает что-то своё, например запускает приготовление кофе.</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
Обратим внимание на строку `(3)`.
|
Обратим внимание на строку `(3)`.
|
||||||
|
@ -266,15 +266,17 @@ function Machine(power) {
|
||||||
|
|
||||||
*!*
|
*!*
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
this.enable = function() {
|
|
||||||
// используем внешнюю переменную вместо this
|
|
||||||
self._enabled = true;
|
|
||||||
};
|
|
||||||
*/!*
|
*/!*
|
||||||
|
|
||||||
|
this.enable = function() {
|
||||||
|
*!*
|
||||||
|
// используем внешнюю переменную вместо this
|
||||||
|
self._enabled = true;
|
||||||
|
*/!*
|
||||||
|
};
|
||||||
|
|
||||||
this.disable = function() {
|
this.disable = function() {
|
||||||
this._enabled = false;
|
self._enabled = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -325,14 +327,14 @@ coffeeMachine.enable();
|
||||||
```js
|
```js
|
||||||
function Machine(params) {
|
function Machine(params) {
|
||||||
// локальные переменные и функции доступны только внутри Machine
|
// локальные переменные и функции доступны только внутри Machine
|
||||||
var private;
|
var privateProperty;
|
||||||
|
|
||||||
// публичные доступны снаружи
|
// публичные доступны снаружи
|
||||||
this.public = ...;
|
this.publicProperty = ...;
|
||||||
|
|
||||||
// защищённые доступны внутри Machine и для потомков
|
// защищённые доступны внутри Machine и для потомков
|
||||||
// мы договариваемся не трогать их снаружи
|
// мы договариваемся не трогать их снаружи
|
||||||
this._protected = ...
|
this._protectedProperty = ...
|
||||||
}
|
}
|
||||||
|
|
||||||
var machine = new Machine(...)
|
var machine = new Machine(...)
|
||||||
|
@ -349,12 +351,12 @@ function CoffeeMachine(params) {
|
||||||
Machine.apply(this, arguments);
|
Machine.apply(this, arguments);
|
||||||
*/!*
|
*/!*
|
||||||
|
|
||||||
this.coffeePublic = ...
|
this.coffeePublicProperty = ...
|
||||||
}
|
}
|
||||||
|
|
||||||
var coffeeMachine = new CoffeeMachine(...);
|
var coffeeMachine = new CoffeeMachine(...);
|
||||||
coffeeMachine.public();
|
coffeeMachine.publicProperty();
|
||||||
coffeeMachine.coffeePublic();
|
coffeeMachine.coffeePublicProperty();
|
||||||
```
|
```
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
@ -365,9 +367,9 @@ function CoffeeMachine(params) {
|
||||||
Machine.apply(this, arguments);
|
Machine.apply(this, arguments);
|
||||||
|
|
||||||
*!*
|
*!*
|
||||||
var parentProtected = this._protected;
|
var parentProtected = this._protectedProperty;
|
||||||
this._protected = function(args) {
|
this._protectedProperty = function(args) {
|
||||||
parentProtected.call(this, args); // (*)
|
parentProtected.apply(this, args); // (*)
|
||||||
// ...
|
// ...
|
||||||
};
|
};
|
||||||
*/!*
|
*/!*
|
||||||
|
@ -389,11 +391,11 @@ function Machine(params) {
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
**В следующих главах мы будем изучать прототипный подход, который обладаем рядом преимуществ, по сравнению с функциональным.**
|
Надо сказать, что способ наследования, описанный в этой главе, используется нечасто.
|
||||||
|
|
||||||
Но функциональный тоже бывает полезен.
|
В следующих главах мы будем изучать прототипный подход, который обладаем рядом преимуществ.
|
||||||
|
|
||||||
В своей практике разработки я обычно наследую функционально в тех случаях, когда *уже* есть какой-то код, который на нём построен. К примеру, уже существуют классы, написанные сторонними разработчиками, которые можно доопределить или расширить только так.
|
Но знать и понимать его необходимо, поскольку во многих существующих библиотеках классы написаны в функциональном стиле, и расширять/наследовать от них можно только так.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
Если один объект имеет специальную ссылку `__proto__` на другой объект, то при чтении свойства из него, если свойство отсутствует в самом объекте, оно ищется в объекте `__proto__`.
|
Если один объект имеет специальную ссылку `__proto__` на другой объект, то при чтении свойства из него, если свойство отсутствует в самом объекте, оно ищется в объекте `__proto__`.
|
||||||
|
|
||||||
Свойство доступно во всех браузерах, кроме IE10-. Впрочем, в старых IE оно, на самом деле, тоже есть, но требуются чуть более сложные способы для работы с ним, которые мы рассмотрим позднее.
|
Свойство `__proto__` доступно во всех браузерах, кроме IE10-, а в более старых IE оно, конечно же, тоже есть, но напрямую к нему не обратиться, требуются чуть более сложные способы, которые мы рассмотрим позднее.
|
||||||
|
|
||||||
Пример кода (кроме IE10-):
|
Пример кода (кроме IE10-):
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
# ООП в прототипном стиле
|
# ООП в прототипном стиле
|
||||||
|
|
||||||
Изучаем прототипы -- де-факто стандарт объектно-ориентированной разработки, а также их использование для наследования.
|
В этом разделе мы изучим прототипы и классы на них -- де-факто стандарт объектно-ориентированной разработки в JavaScript.
|
33
README.md
Executable file
33
README.md
Executable file
|
@ -0,0 +1,33 @@
|
||||||
|
|
||||||
|
# Учебник JavaScript
|
||||||
|
|
||||||
|
Этот репозитарий содержит учебник JavaScript, включая все разделы, статьи, задачи.
|
||||||
|
|
||||||
|
Он предназначен для отображения движком сайта javascript.ru, репозитарий https://github.com/iliakan/javascript-nodejs.
|
||||||
|
|
||||||
|
При этом синтаксис намеренно сделан совместимым с GitHub, чтобы при желании можно было читать текст прямо из репозитария. Конечно, "живые" примеры и другие продвинутые возможности при этом будут не доступны, т.к. парсер GitHub их не умеет.
|
||||||
|
|
||||||
|
**Каждому разделу, статье или задаче соответствует директория.**
|
||||||
|
|
||||||
|
Эта директория имеет вид `N-url`, где `N` - это номер для сортировки статей и разделов (они упорядочены), а `url` - URL-имя, по которому материал будет доступен.
|
||||||
|
|
||||||
|
В директории находится один из файлов:
|
||||||
|
|
||||||
|
- `index.md` для раздела
|
||||||
|
- `article.md` для статьи
|
||||||
|
- `task.md` для условия задачи (+там же `solution.md` с решением)
|
||||||
|
|
||||||
|
Каждый из этих файлов начинается с `# Заголовка материала`.
|
||||||
|
|
||||||
|
Абсолютный URL для разделов и статей -- это URL-имя без номера и родителей, для задачи -- с префиксом `/task/`.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
|
||||||
|
- директория `2-ui/3-event-details` с файлом `index.md` - это раздел сайта "События в деталях", он будет доступен по URL `/event-details`.
|
||||||
|
- директория `2-ui/3-event-details/6-drag-and-drop` с файлом `article.md` содержит статью "Мышь: Drag'n'Drop`, доступную по URL `/drag-and-drop`.
|
||||||
|
- директория `2-ui/3-event-details/6-drag-and-drop/slider` с файлом `task.md` содержит задачу с названием "Слайдер", доступную по адресу `/task/slider`.
|
||||||
|
|
||||||
|
Ресурсы и примеры, необходимые для статьи, раздела или задачи, находятся в её директории. На них можно ссылаться из материала.
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue