Рекурсия - это прямой или косвенный самовызов функции. Обычно применяется для обработки деревьев в структурах данных или иных вложенностей.
Например факториал (!
) - произведение всех чисел от 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 (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
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
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 может быть установлено как свойства объекта-узла 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 randomValue = Math.random()
var getRandomValue = () => randomValue
}
alert(getRandomValue())
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.
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()