# Рекурсия. DOM, замыкания и функциональное ООП. ## Рекурсия Рекурсия - это прямой или косвенный самовызов функции. Обычно применяется для обработки деревьев в структурах данных или иных вложенностей. Например **факториал** (`!`) - произведение всех чисел от `1` до `N` можно определить как: N ! = 1 x 2 x 3 x 4 x .... x N, или рекурсивно: N ! = N x (N - 1) ! ```javascript function factorial(N){ if (N <= 1){ return 1; } return N * factorial(N -1); } factorial ( 5 ); // это 5 * factorial(4), что, в свою очередь, будет 4 * factorial(3) и так далее ``` ### Деревья Рекурсия удобна для обработки вложенностей. ```javascript var tree = { name: "root", nested: [ { name: "0", nested: [ { name: "00", nested: [ { name: "000", }, { name: "001", }, { name: "002", }, ] }, { name: "01", }, { name: "02", }, ] }, { name: "1", nested: [ { name: "10", }, { name: "11", }, { name: "12", }, ] }, { name: "2", nested: [ { name: "20", }, { name: "21", }, { name: "22", }, ] }, ] } function walker(node, nestedFieldName, deepness, callback){ callback(node, nestedFieldName, deepness); if (nestedFieldName in node){ for (key in node[nestedFieldName]){ walker(node[nestedFieldName][key], nestedFieldName, deepness +1, callback) } } } var str = ""; walker(tree, "nested", 0, function(node, nestedFieldName, deepness){ str += " ".repeat(deepness) + node.name + "\n"; }); console.log(str); ``` ## Введение в **DOM**. **DOM** (*Document Object Model*) - внутренние объекты и функции *браузерного* **JS**, которые позволяют работать с деревом тэгов текущей загруженной страницы. **JS** через это **API** имеет полный доступ ко всему тому, что есть в HTML и CSS. ### Корень Корнем дерева элементов **DOM** является объект `document` ### Поиск элементов Что бы найти элемент, нужно обратится к методу `document`, или любого другого элемента, в который нужно что-то найти: ```javascript var el = document.getElementById("someId"); //обратите внимания, без # var el2 = document.querySelector("#someId"); //поиск по любому селектору, аналог jQuery var el3 = document.querySelectorAll("a"); //поиск всех тэгов a ``` ### Создание элементов **DOM** ```javascript document.createElement("a"); //обратите внимание, без <> ``` Добавление элементов: ```javascript var tr = document.createElement("tr"); var td = document.createElement("td"); var td2 = document.createElement("td"); tr.appendChild(td); //добавление ячейки в конец строки таблицы. tr.insertBefore(tr.childNodes[0],td2); //добавление ячейки перед первой ячейкой (в самое начало строки таблицы) ``` Ссылка на родительский элемент находится в свойстве `parentElement`: ```javascript tr.childNodes[0].parentElement == tr ``` ### Свойства объектов или наборов объектов элементов в **DOM** - `value` - **свойство** а не функция для значения поля ввода. - `attributes` - объект `attributes` с атрибутами html-тэга. Также есть 4 функции для работы с атрибутами: - `hasAttribute` - проверка на наличие атрибута - `getAttribute` - чтение - `setAttribute` - запись - `removeAttribute` - удаление - `style` - объект стиля элемента - `innerHTML` - строка вложенного HTML в элементе. - `innerText` - строка вложенного текста в элементе. ### События Каждый элемент **DOM** содержит множество свойств `on...`, в которые вы можете занести тот или иной обработчик события: ```javascript document.onmousemove = function(){ document.write("mouse move
"); } ``` Так же можно добавлять обработчики используя метод элемента `addEventListener`: ```javascript document.addEventListener("mousemove",function(){ document.write("mouse move
"); }); ``` ### Нюансы #### Элемент в дереве может встречаться только один раз. Вы не можете вставить элемент дважды в дерево. Если вы хотите создать копию элемента в вашем DOM-дереве, используйте `cloneNode`. #### HTML/CSS Всё, что вы видели в HTML/CSS может быть установлено как свойства объекта-узла DOM и тут же будет отображено в браузере #### `children` и `childNodes` - Узлами (`Node`) может быть любой текст в HTML, в том числе обычный текст и тот или иной тэг. Дочерние элементы каждого элемента находятся в **псевдомассиве** `childNodes` - В **псевдомассиве** `children` находятся только дочерние узлы-тэги, но без обычного текста. ![например](http://shots.asmer.org.ua/2017_10_12__17_50_35.png) > **Задание** > Сделайте любое предыдущее задание по конструированию **HTML** используя **DOM**, а не конструирование строки. Проанализируйте отличия. ## Замыкания, приватные методы и данные. > One of the conclusions that we reached was that the "object" need not be a primitive notion in a programming language; one can build objects and their behaviour from little more than assignable value cells and good old lambda expressions. > > -— Guy Steele on the design of Scheme > Closures are one of those few curious concepts that are paradoxically difficult because they are so simple. Once a programmer becomes used to a complex solution to a problem, simple solutions to the same problem feel incomplete and uncomfortable. But, as we will soon see, closures can be a simpler, more direct solution to the problem of how to organise data and code than objects. > > -- Doug Hoyte Функция в **JS** находится сразу в двух контекстах: *динамическом* (`this`, значения параметров) и *лексическом* (переменные из более высоких областей видимости в **месте определения функции**). *Динамический контекст* - это контекст **вызова** функции - значение параметров и окружение на момент вызова; *лексический контекст* - контекст **определения** функции, её вложенности в другие области видимости, доступ к которым функция имеет и после окончания выполнения функций-владельцев этих областей видимости. ### [Let Over Lambda](https://letoverlambda.com/) (LOL) ```javascript { let randomValue = Math.random() var getRandomValue = () => randomValue } alert(getRandomValue()) ``` ### makeAdder ```javascript function makeAdder(x){ function adder(y){ return x + y; } return adder } var add5 = makeAdder(5); var greeter = makeAdder("Hi, "); alert(add5(2)); alert(greeter("John")); ``` В примере выше `5`,`"Hi, "`, `2` и `"John"` находятся в динамическом контексте вызова функции. А вот значение `x` в `adder` находится в *лексическом* контексте. На момент запуска функций `add5` и `greeter` функции `makeAdder` уже давно отработали, однако значение `x` и область видимости продолжают присутствовать в функциях `add5` и `greeter`. Это называется **замыканием** - данные в локальной области видимости отработавшей функции и код, который использует эти данные впоследствии. В широком смысле **замыкание** является *объектом*, так как хранит в единой сущности код и данные. В контексте **JS** *замыкания* очень удобны для огораживания кода, сохранения состояния переменных "на будущее", для функций обратного вызова. Так же *замыкания* удобны для организации приватных полей у объектов **JS**. ### makeCounter ```javascript function makeCounter(){ var counter = 0; return function(){ return counter++; } } ``` Функция выше создает счетчик, значение которого можно узнать из возвращаемой анонимной функции. При этом счетчик увеличится на 1. Если расширить функционал данного примера для чтения и декремента счетчика, получим, например: ```javascript function makeCounter(){ var counter = 0; function inc(){ return counter++; } function dec(){ return counter--; } function read(){ return counter; } return [inc,dec,read]; } ``` Результатом выполнения `makeCounter` будет *массив функций*. Однако намного более наглядным будет создание именованного массива, т. е. объекта: ```javascript function makeCounter(){ var counter = 0; function inc(){ return counter++; } function dec(){ return counter--; } function read(){ return counter; } return {inc: inc, dec: dec, read: read}; } ``` > **Задание** > Сделайте счетчик кликов с помощью функции и замыкания. Счетчик должен выводить количество кликов в `innerText` элемента. ```javascript clickCounter(document.getElementById('button')) //кнопка с id='button' при каждом клике показывает число кликов. clickCounter(document.getElementById('span')) //span с id='span' при каждом клике показывает число кликов. ``` ## ООП в функциональном стиле. ### Создание объекта Для создания объектов используются функции-конструкторы. Они создают новые объекты определенного типа, который совпадает с именем функции: ```javascript function Person(){ } var person = new Person() ``` По всеобщей договоренности, функции-конструкторы именуются с большой буквы (`Person`). Для создания нового объекта используется оператор `new`, который создает пустой объект, заносит в него определенное множество технической информации и передает его как `this` в конструктор: ```javascript function Person(name, surname){ this.name = name this.surname = surname } var person = new Person("Ivan", "Petroff") ``` Обратите внимание, что конструктор ничего не возращает, используя `return`. Считается что он возвращает новый объект, для этого достаточно просто заполнить нужные поля в `this`. ### Методы Так же как данные, мы можем задать определенные методы объекту: ```javascript function Person(name, surname){ this.name = name this.surname = surname this.getFullName = function(/*this*/){ return this.name + (this.fatherName ? " " + this.fatherName + " " : " ") + this.surname } } var person = new Person("Ivan", "Petroff") alert(person.getFullName()) person.fatherName = "Sydorych" alert(person.getFullName()) ``` `this` можно считать скрытым параметром функции, если функция **вызвана через точку** как поле объекта: ```javascript alert(person.getFullName()) //в качестве this в getFullName передается person ``` ### Приватные методы и данные; Замыкания. **JS** не предоставляет обычных для Объектно-ориентированных языков программирования возможностей ограничения доступа к полям объекта (`private`, `public`, `protected`), для этого используется другой подход - так называемые *замыкания*. ```javascript function Person(name, surname){ this.name = name this.surname = surname var originalFullName = name + " " + surname this.getFullName = function(){ return this.name + (this.fatherName ? " " + this.fatherName + " " : " ") + this.surname } this.getOriginalName = function(){ return name } this.getOriginalSurname = function(){ return surname } this.getOriginalFullName = function(){ return originalFullName } } var person = new Person("Ivan", "Petroff") person.name = "John" person.surname = "Doe" alert(person.getFullName()) alert(person.getOriginalName()) alert(person.getOriginalFullName()) ``` Пример выше иллюстрирует этот подход: методы объекта `Person`, например `getOriginalFullName`, имеют доступ к области видимости уже *завершенной* функции. Код же *снаружи*, то есть определенный вне конструктора `Person` никак не может получить значения переменных, определенных внутри конструктора (например `originalFullName`). Обратите внимание, что `this.name` и `name` - это **разные переменные**. Одна из них является частью новосозданного объекта, вторая же - параметр функции `Person` в её области видимости. Таким образом, единственным способом работы с переменными в замыкании является *код*, который находится в одном *лексическом контексте* с переменными: #### геттеры и сеттеры. ```javascript function Person(name, surname){ this.name = name this.surname = surname var age = 0 this.setAge = function(newAge){ newAge = +newAge if (!isNaN(newAge) && Number.isInteger(newAge) && (newAge > 0) && (newAge < 100)){ age = newAge } return age } this.getAge = function(){ return age } } var person = new Person("Ivan", "Petroff") alert(person.setAge(50)) alert(person.setAge(125)) alert(person.setAge(25)) ``` Таким образом реализуется **паттерн getter/setter** - специальных функций, которые читают и записывают данные, защищенные от записи внешним кодом, проверяя их на правильность при записи. #### Приватные функции в замыкании Таким же способом мы можем определить функции для внутреннего использования, недоступные через объект, но доступные через методы объекта: ```javascript function Person(name, surname){ this.name = name this.surname = surname function getFullShortName(){ return this.name + " " + this.surname } function getFullLongName(){ return this.name + " " + this.fatherName + " " +this.surname } this.getFullName = function (){ if ("fatherName" in this){ return getFullLongName() } else { return getFullShortName() } } } var person = new Person("Ivan", "Petroff") alert(person.getFullName()) ``` Данный пример не работает, так как `this` *не является* частью области видимости Person, а устанавливается в зависимости от *контекста вызова*. Общее правило таково: `this` равен тому, что написано до точки: ```javascript alert(person.getFullName()) //this == person ``` ```javascript ... function getFullShortName(){ return this.name + " " + this.surname //oups } function getFullLongName(){ return this.name + " " + this.fatherName + " " +this.surname //oups } this.getFullName = function (){ if ("fatherName" in this){ return getFullLongName(); //this == window или undefined в strict } else { return getFullShortName(); //this == window или undefined в strict } } ... ``` Для исправления ситуации воспользуемся методом `call` объекта `Function`, который позволяет "подсунуть" функции другой `this`: ```javascript function Person(name, surname){ this.name = name this.surname = surname function getFullShortName(){ return this.name + " " + this.surname } function getFullLongName(){ return this.name + " " + this.fatherName + " " +this.surname } this.getFullName = function (){ if ("fatherName" in this){ return getFullLongName.call(this) //передаем текущий объект person как this в getFullLongName } else { return getFullShortName.call(this) } } } var person = new Person("Ivan", "Petroff") alert(person.getFullName()) ``` Альтернативой этому решению является *сохранение* `this` в замыкании: ```javascript function Person(name, surname){ this.name = name this.surname = surname var me = this; //теперь у нас есть ссылка на текущий объект в замыкании function getFullShortName(){ return me.name + " " + me.surname } function getFullLongName(){ return me.name + " " + me.fatherName + " " + me.surname } this.getFullName = function (){ if ("fatherName" in this){ return getFullLongName() } else { return getFullShortName() } } } var person = new Person("Ivan", "Petroff") alert(person.getFullName()) ``` В отличие от `this`, переменная `me` всегда будет указывать на `this`, который был при создании объекта (если, конечно, вы не поменяете значение `me`) ### `call` и `apply` Эти два метода объекта `Function` позволяют вызвать функцию, указав `this` и параметры: ```javascript function Person(name, surname){ this.name = name this.surname = surname this.getFullName = function(){ return this.name + (this.fatherName ? " " + this.fatherName + " " : " ") + this.surname } } function say(greet, aftergreet){ alert(greet + " " + this.getFullName() + " " + aftergreet) } var person = new Person("Ivan", "Petroff") say.call(person,"Hello", "!!111!!!") say.apply(person,["Hi", "by apply"]) //feel the difference ``` То, что `apply` принимает массив в качестве набора параметров функции помогает **творить чудеса**: ```javascript var someArray = [1,5,7,-17,100500]; Math.min(someArray) //Math.min так не умеет Math.min(1,5,7,-17,100500,-123) // зато умеет так Math.min.apply(Math,someArray) // ...и мы этим воспользуемся... // или же сделаем свой min в someArray: someArray.min = function(){ return Math.min.apply(Math,someArray); } someArray.min() // или же сделаем свой min для всех массивов Array.prototype.min = function(){ return Math.min.apply(Math, this) }; [100, 500, -100500].min() ```