06DOMClosuresOOP.md 23 KB

Рекурсия. DOM, замыкания и функциональное ООП.

Рекурсия

Рекурсия - это прямой или косвенный самовызов функции. Обычно применяется для обработки деревьев в структурах данных или иных вложенностей. Например факториал (!) - произведение всех чисел от 1 до N можно определить как:

N ! = 1 x 2 x 3 x 4 x .... x N,

или рекурсивно:

N ! = N x (N - 1) !

function factorial(N){
    if (N <= 1){
        return 1; 
    }
    return N * factorial(N -1);
}

factorial ( 5 ); // это 5 * factorial(4), что, в свою очередь, будет 4 * factorial(3) и так далее

Деревья

Рекурсия удобна для обработки вложенностей.

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, или любого другого элемента, в который нужно что-то найти:

var el  = document.getElementById("someId"); //обратите внимания, без #
var el2 = document.querySelector("#someId"); //поиск по любому селектору, аналог jQuery
var el3 = document.querySelectorAll("a");    //поиск всех тэгов a

Создание элементов DOM

document.createElement("a"); //обратите внимание, без <>

Добавление элементов:

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:

tr.childNodes[0].parentElement == tr

Свойства объектов или наборов объектов элементов в DOM

  • value - свойство а не функция для значения поля ввода.
  • attributes - объект attributes с атрибутами html-тэга. Также есть 4 функции для работы с атрибутами:
    • hasAttribute - проверка на наличие атрибута
    • getAttribute - чтение
    • setAttribute - запись
    • removeAttribute - удаление
  • style - объект стиля элемента
  • innerHTML - строка вложенного HTML в элементе.
  • innerText - строка вложенного текста в элементе.

События

Каждый элемент DOM содержит множество свойств on..., в которые вы можете занести тот или иной обработчик события:

document.onmousemove = function(){
    document.write("mouse move <br/>");
}

Так же можно добавлять обработчики используя метод элемента addEventListener:

document.addEventListener("mousemove",function(){
    document.write("mouse move <br/>");
});

Нюансы

Элемент в дереве

может встречаться только один раз. Вы не можете вставить элемент дважды в дерево. Если вы хотите создать копию элемента в вашем DOM-дереве, используйте cloneNode.

HTML/CSS

Всё, что вы видели в HTML/CSS может быть установлено как свойства объекта-узла DOM и тут же будет отображено в браузере

children и childNodes

  • Узлами (Node) может быть любой текст в HTML, в том числе обычный текст и тот или иной тэг. Дочерние элементы каждого элемента находятся в псевдомассиве childNodes
  • В псевдомассиве children находятся только дочерние узлы-тэги, но без обычного текста.

например

Задание Сделайте любое предыдущее задание по конструированию 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 (LOL)

{
    let randomValue = Math.random()
    var getRandomValue = () => randomValue
}
alert(getRandomValue())

makeAdder

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


function makeCounter(){
    var counter = 0;

    return function(){
        return counter++;
    }
}

Функция выше создает счетчик, значение которого можно узнать из возвращаемой анонимной функции. При этом счетчик увеличится на 1. Если расширить функционал данного примера для чтения и декремента счетчика, получим, например:


function makeCounter(){
    var counter = 0;

    function inc(){
        return counter++;
    }

    function dec(){
        return counter--;
    }

    function read(){
        return counter;
    }

    return [inc,dec,read];
}

Результатом выполнения makeCounter будет массив функций. Однако намного более наглядным будет создание именованного массива, т. е. объекта:


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 элемента.

clickCounter(document.getElementById('button')) //кнопка с id='button' при каждом клике показывает число кликов.
clickCounter(document.getElementById('span'))   //span с id='span' при каждом клике показывает число кликов.

ООП в функциональном стиле.

Создание объекта

Для создания объектов используются функции-конструкторы. Они создают новые объекты определенного типа, который совпадает с именем функции:

function Person(){
}

var person = new Person()

По всеобщей договоренности, функции-конструкторы именуются с большой буквы (Person). Для создания нового объекта используется оператор new, который создает пустой объект, заносит в него определенное множество технической информации и передает его как this в конструктор:

function Person(name, surname){
    this.name    = name
    this.surname = surname
}

var person = new Person("Ivan", "Petroff")

Обратите внимание, что конструктор ничего не возращает, используя return. Считается что он возвращает новый объект, для этого достаточно просто заполнить нужные поля в this.

Методы

Так же как данные, мы можем задать определенные методы объекту:

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 можно считать скрытым параметром функции, если функция вызвана через точку как поле объекта:

alert(person.getFullName()) //в качестве this в getFullName передается person

Приватные методы и данные; Замыкания.

JS не предоставляет обычных для Объектно-ориентированных языков программирования возможностей ограничения доступа к полям объекта (private, public, protected), для этого используется другой подход - так называемые замыкания.

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 в её области видимости. Таким образом, единственным способом работы с переменными в замыкании является код, который находится в одном лексическом контексте с переменными:

геттеры и сеттеры.

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 - специальных функций, которые читают и записывают данные, защищенные от записи внешним кодом, проверяя их на правильность при записи.

Приватные функции в замыкании

Таким же способом мы можем определить функции для внутреннего использования, недоступные через объект, но доступные через методы объекта:

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 равен тому, что написано до точки:

alert(person.getFullName()) //this == person
...

    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:

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 в замыкании:

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 и параметры:

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 принимает массив в качестве набора параметров функции помогает творить чудеса:

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()