Для создания объектов используются функции-конструкторы. Они создают новые объекты определенного типа, который совпадает с именем функции:
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()
Рекурсия - это прямой или косвенный самовызов функции. Обычно применяется для обработки деревьев в структурах данных или иных вложенностей.
Например факториал (!
) - произведение всех чисел от 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);