Рекурсия - это прямой или косвенный самовызов функции. Обычно применяется для обработки деревьев в структурах данных или иных вложенностей.
Например факториал (!
) - произведение всех чисел от 1
до N
можно определить как:
N ! = 1 x 2 x 3 x 4 x .... x N,
или рекурсивно:
N ! = N x (N - 1) !
|> click here to run codefunction factorial(N){
if (N <= 1){
return 1;
}
return N * factorial(N -1);
}
factorial ( 5 ); // это 5 * factorial(4), что, в свою очередь, будет 4 * factorial(3) и так далее
Рекурсия удобна для обработки вложенностей.
|> click here to run codevar 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 (Document Object Model) - внутренние объекты и функции браузерного JS, которые позволяют работать с деревом тэгов текущей загруженной страницы. JS через это API имеет полный доступ ко всему тому, что есть в HTML и CSS.
Корнем дерева элементов DOM является объект document
Что бы найти элемент, нужно обратится к методу document
, или любого другого элемента, в который нужно что-то найти:
|> click here to run codevar el = document.getElementById("someId"); //обратите внимания, без #
var el2 = document.querySelector("#someId"); //поиск по любому селектору, аналог jQuery
var el3 = document.querySelectorAll("a"); //поиск всех тэгов a
|> click here to run codedocument.createElement("a"); //обратите внимание, без <>
Добавление элементов:
|> click here to run codevar tr = document.createElement("tr");
var td = document.createElement("td");
var td2 = document.createElement("td");
tr.appendChild(td); //добавление ячейки в конец строки таблицы.
tr.insertBefore(tr.childNodes[0],td2); //добавление ячейки перед первой ячейкой (в самое начало строки таблицы)
Ссылка на родительский элемент находится в свойстве parentElement
:
|> click here to run codetr.childNodes[0].parentElement == tr
value
- свойство а не функция для значения поля ввода.attributes
- объект attributes
с атрибутами html-тэга. Также есть 4 функции для работы с атрибутами:
hasAttribute
- проверка на наличие атрибутаgetAttribute
- чтениеsetAttribute
- записьremoveAttribute
- удалениеstyle
- объект стиля элементаinnerHTML
- строка вложенного HTML в элементе.innerText
- строка вложенного текста в элементе.Каждый элемент DOM содержит множество свойств on...
, в которые вы можете занести тот или иной обработчик события:
|> click here to run codedocument.onmousemove = function(){
document.write("mouse move <br/>");
}
Так же можно добавлять обработчики используя метод элемента addEventListener
:
|> click here to run codedocument.addEventListener("mousemove",function(){
document.write("mouse move <br/>");
});
может встречаться только один раз. Вы не можете вставить элемент дважды в дерево. Если вы хотите создать копию элемента в вашем DOM-дереве, используйте cloneNode
.
Всё, что вы видели в HTML/CSS может быть установлено как свойства объекта-узла DOM и тут же будет отображено в браузере
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
, значения параметров) и лексическом (переменные из более высоких областей
видимости в месте определения функции). Динамический контекст - это контекст вызова функции - значение параметров и окружение на момент вызова;
лексический контекст - контекст определения функции, её вложенности в другие области видимости, доступ к которым функция имеет и после окончания
выполнения функций-владельцев этих областей видимости.
|> click here to run code{
let randomValue = Math.random()
var getRandomValue = () => randomValue
}
alert(getRandomValue())
|> click here to run codefunction 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.
|> click here to run code
function makeCounter(){
var counter = 0;
return function(){
return counter++;
}
}
Функция выше создает счетчик, значение которого можно узнать из возвращаемой анонимной функции. При этом счетчик увеличится на 1. Если расширить функционал данного примера для чтения и декремента счетчика, получим, например:
|> click here to run code
function makeCounter(){
var counter = 0;
function inc(){
return counter++;
}
function dec(){
return counter--;
}
function read(){
return counter;
}
return [inc,dec,read];
}
Результатом выполнения makeCounter
будет массив функций. Однако намного более наглядным будет создание именованного массива, т. е. объекта:
|> click here to run code
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
элемента.
|> click here to run codeclickCounter(document.getElementById('button')) //кнопка с id='button' при каждом клике показывает число кликов.
clickCounter(document.getElementById('span')) //span с id='span' при каждом клике показывает число кликов.
Для создания объектов используются функции-конструкторы. Они создают новые объекты определенного типа, который совпадает с именем функции:
|> click here to run codefunction Person(){
}
var person = new Person()
По всеобщей договоренности, функции-конструкторы именуются с большой буквы (Person
). Для создания нового объекта используется оператор new
, который создает пустой объект, заносит в него
определенное множество технической информации и передает его как this
в конструктор:
|> click here to run codefunction Person(name, surname){
this.name = name
this.surname = surname
}
var person = new Person("Ivan", "Petroff")
Обратите внимание, что конструктор ничего не возращает, используя return
. Считается что он возвращает новый объект, для этого достаточно просто заполнить нужные поля в this
.
Так же как данные, мы можем задать определенные методы объекту:
|> click here to run codefunction 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
можно считать скрытым параметром функции, если функция вызвана через точку как поле объекта:
|> click here to run codealert(person.getFullName()) //в качестве this в getFullName передается person
JS не предоставляет обычных для Объектно-ориентированных языков программирования возможностей ограничения доступа к полям объекта (private
, public
, protected
), для этого используется
другой подход - так называемые замыкания.
|> click here to run codefunction 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
в её области видимости. Таким образом, единственным способом работы с переменными в замыкании является код,
который находится в одном лексическом контексте с переменными:
|> click here to run codefunction 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 - специальных функций, которые читают и записывают данные, защищенные от записи внешним кодом, проверяя их на правильность при записи.
Таким же способом мы можем определить функции для внутреннего использования, недоступные через объект, но доступные через методы объекта:
|> click here to run codefunction 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
равен тому, что написано до точки:
|> click here to run codealert(person.getFullName()) //this == person
|> click here to run code...
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
:
|> click here to run codefunction 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
в замыкании:
|> click here to run codefunction 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
)
Эти два метода объекта Function
позволяют вызвать функцию, указав this
и параметры:
|> click here to run codefunction 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
принимает массив в качестве набора параметров функции помогает творить чудеса:
|> click here to run codevar 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()