Объектно-ориентированное программирование
Объектно-ориентированное программирование на сегодняшний день является одной из господствующих парадигм в разработке приложений, и в JavaScript мы также можем использовать все преимущества ООП. В то же время применительно к JavaScript объектно-ориентированное программирование имеет некоторые особенности.
Объекты
В прошлых темах мы работали с примитивными данными — числами, строками, но данные не всегда представляют примитивные типы. Например, если в нашей программе нам надо описать сущность человека, у которого есть имя, возраст, пол и так далее, то естественно мы не сможем представить сущность человека в виде числа или строки. Нам потребуется несколько строк или чисел, чтобы должным образом описать человека. В этом плане человек будет выступать как сложная комплексная структура, у которого будут отдельные свойства — возраст, рост, имя, фамилия и т.д.
Для работы с подобными структурами в JavaScript используются объекты . Каждый объект может хранить свойства, которые описывают его состояние , и методы, которые описывают его поведение
Создание нового объекта
Есть несколько способов создания нового объекта.
Первый способ заключается в использовании конструктора Object :
const user = new Object();
В данном случае объект называется user .
Выражение new Object() представляет вызов конструктора — функции, создающей новый объект. Для вызова конструктора применяется оператор new . Вызов конструктора фактически напоминает вызов обычной функции.
Второй способ создания объекта представляет использование фигурных скобок (литеральная нотация):
const user = <>;
На сегодняшний день более распространенным является второй способ.
Свойства объекта
После создания объекта мы можем определить в нем свойства. Чтобы определить свойство, надо после названия объекта через точку указать имя свойства и присвоить ему значение:
const user = <>; user.name = "Tom"; user.age = 26;
В данном случае объявляются два свойства name и age , которым присваиваются соответствующие значения. После этого мы можем использовать эти свойства, например, вывести их значения в консоли:
console.log(user.name); console.log(user.age);
Также можно определить свойства при определении объекта:
const user = < name: "Tom", age: 26 >;
В этом случае для присвоения значения свойству используется символ двоеточия, а после определения свойства ставится запятая (а не точка с запятой).
Кроме того, доступен сокращенный способ определения свойств:
const name = "Tom"; const age = 34; const user = ; console.log(user.name); // Tom console.log(user.age); // 34
В данном случае названия переменных также являются и названиями свойств объекта. И таким образом можно создавать более сложные конструкции:
const name = "Tom"; const age = 34; const user = ; const teacher = ; console.log(teacher.user); // console.log(teacher.course); // JavaScript
Методы объекта
Методы объекта определяют его поведение или действия, которые он производит. Методы представляют собой функции. Например, определим метод, который бы выводил имя и возраст человека:
const user = <>; user.name = "Tom"; user.age = 26; user.display = function()< console.log(user.name); console.log(user.age); >; // вызов метода user.display();
Как и в случае с функциями методы сначала определяются, а потом уже вызываются.
Также методы могут определяться непосредственно при определении объекта:
const user = < name: "Tom", age: 26, display: function()< console.log(this.name); console.log(this.age); >>;
Как и в случае со свойствами, методу присваивается ссылка на функцию с помощью знака двоеточия.
Чтобы обратиться к свойствам или методам объекта внутри этого объекта, используется ключевое слово this . Оно означает ссылку на текущий объект.
Также можно использовать сокращенный способ определения методов, когда двоеточие и слово function опускаются:
let user = < name: "Tom", age: 26, display()< console.log(this.name, this.age); >, move(place) < console.log(this.name, "goes to", place); >>; user.display(); // Tom 26 user.move("the shop"); // Tom goes to the shop
Синтаксис массивов
Существует также альтернативный способ определения свойств и методов с помощью синтаксиса массивов:
const user = <>; user["name"] = "Tom"; user["age"] = 26; user["display"] = function()< console.log(user.name); console.log(user.age); >; // вызов метода user["display"]();
Название каждого свойства или метода заключается в кавычки и в квадратные скобки, затем им также присваивается значение. Например, user[«age»] = 26 .
При обращении к этим свойствам и методам можно использовать либо нотацию точки ( user.name ), либо обращаться так: user[«name»]
Также можно определить свойства и методы через синтаксис массивов напрямую при создании объекта:
const user = < ["name"]: "Tom", ["age"]: 26, ["display"]: function()< console.log(user.name); console.log(user.age); >>; user["display"]();
Строки в качестве свойств и методов
Также следует отметить, что названия свойств и методов объекта всегда представляют строки. То есть мы могли предыдущее определение объекта переписать так:
const user = < "name": "Tom", "age": 26, "display": function()< console.log(user.name); console.log(user.age); >>; // вызов метода user.display();
С одной стороны, разницы никакой нет между двумя определениями. С другой стороны, бывают случаи, где заключение названия в строку могут помочь. Например, если название свойства состоит из двух слов, разделенных пробелом:
const user = < name: "Tom", age: 26, "full name": "Tom Johns", "display info": function()< console.log(user.name); console.log(user.age); >>; console.log(user["full name"]); user["display info"]();
Только в этом случае для обращении к подобным свойствам и методам мы должны использовать синтаксис массивов.
Динамическое определение имен свойств и методов
Синтаксис массивов открывает нам другую возможность — определение имени свойства вне объекта:
const prop1 = "name"; const prop2 = "age"; const tom = < [prop1]: "Tom", [prop2]: 37 >; console.log(tom); // console.log(tom.name); // Tom console.log(tom["age"]); // 37
Благодая этому, например, можно динамически создавать объекты с произвольными названиями свойств:
function createObject(propName, propValue)< return < [propName]: propValue, print()< console.log(`$: $`); > >; > const person = createObject("name", "Tom"); person.print(); // name: Tom const book = createObject("title", "JavaScript Reference"); book.print(); // title: JavaScript Reference
Удаление свойств
Выше мы посмотрели, как можно динамически добавлять новые свойства к объекту. Однако также мы можем удалять свойства и методы с помощью оператора delete . И как и в случае с добавлением мы можем удалять свойства двумя способами. Первый способ — использование нотации точки:
delete объект.свойство
Либо использовать синтаксис массивов:
delete объект["свойство"]
Например, удалим свойство:
let user = <>; user.name = "Tom"; user.age = 26; user.display = function()< console.log(user.name); console.log(user.age); >; console.log(user.name); // Tom delete user.name; // удаляем свойство // альтернативный вариант // delete user["name"]; console.log(user.name); // undefined
После удаления свойство будет не определено, поэтому при попытке обращения к нему, программа вернет значение undefined .
Создание объекта из переменных и констант
При создании объекта его свойствам могут передаваться значения переменных, констант или динамически вычисляемые результаты функций:
function getSalary(status) < if(status==="senior") return 1500; else return 500; >const name = «Tom»; const age = 37; const person = < name: name, age: age, salary: getSalary()>; console.log(person); //
Но если названия констант/переменных совпадает с названиями свойств, то можно сократить передачу значений:
const name = «Tom»; const age = 37; const salary = 500; const person = < name, age, salary>; console.log(person); //
В данном случае объект person автоматически получит свойства, названия которых будут соответствовать названиям констант, а в качестве значений иметь значения этих констант.
То же самое относится к передаче функций методам объекта:
function display() < console.log(this.name, this.age); >const move = function(place)< console.log(this.name, "goes to", place)>; const name = "Tom"; const age = 37; const salary = 500; const person = < name, age, salary, display, move>; person.display(); // Tom 37 person.move("cinema"); // Tom goes to cinema
В данном случае объект person имеет два метода, которые соответствуют переданным в объект функциям — display() и move() . Стоит отметить, что при такой передаче функций методам объекта, мы по прежнему можем использовать в этих функциях ключевое слово this для обращения к функциональности объекта. Однако стоит быть осторожным при передаче лямбд-выражений, поскольку для глобальных лямбд-выражений this будет представлять объект окна браузера:
const move = (place)=>< console.log(this.name, "goes to", place); console.log(this);>; const name = "Tom"; const person = < name, move>; person.move("cinema"); // goes to cinema
Фукция Object.fromEntries()
С помощью функции Object.fromEntries() можно создать объект из набора пар ключ-значение, где ключ потом будет представляет название свойства. Например, создадим объект из массивов:
const personData = [ ["name", "Tom"], ["age", 37]]; const person = Object.fromEntries(personData); console.log(person); // console.log(person.name); // Tom
Здесь объект создается из массива personData, который содержит два подмассива. Каждый подмассив содержит два элемента и фактически представляет пару ключ-значение. Первый элемент представляет ключ, а второй — значение.
Объекты
Объект является фундаментальным типом данных в языке JavaScript. — это составное значение: он объединяет в себе набор значений (простых значений или других объектов) и позволяет сохранять и извлекать эти значения по именам.
Объект является неупорядоченной коллекцией свойств, каждое из которых имеет имя и значение. Имена свойств являются строками, поэтому можно сказать, что объекты отображают строки в значения. Такое отображение строк в значения может называться по-разному: возможно, вы уже знакомы с такой фундаментальной структурой данных, как «хеш», «словарь» или «ассоциативный массив». Однако объект представляет собой нечто большее, чем простое отображение строк в значения.
Помимо собственных свойств объекты в языке JavaScript могут также наследовать свойства от других объектов, известных под названием «прототипы». Методы объекта — это типичные представители унаследованных свойств, а « наследование через прототипы » является ключевой особенностью языка JavaScript.
Объекты в языке JavaScript являются динамическими — обычно они позволяют добавлять и удалять свойства — но они могут использоваться также для имитации статических объектов и «структур», которые имеются в языках программирования со статической системой типов. Кроме того, они могут использоваться (если не учитывать, что объекты отображают строки в значения) для представления множеств строк.
Любое значение в языке JavaScript, не являющееся строкой, числом, true, false, null или undefined, является объектом. И даже строки, числа и логические значения, не являющиеся объектами, могут вести себя как неизменяемые объекты (имеют объекты-обертки String, Number и т.п.).
Объекты являются изменяемыми значениями и операции с ними выполняются по ссылке, а не по значению. Если переменная x ссылается на объект, и выполняется инструкция var y = x; , в переменную y будет записана ссылка на тот же самый объект, а не его копия. Любые изменения, выполняемые в объекте с помощью переменной y , будут также отражаться на переменной x .
Свойство имеет имя и значение. Именем свойства может быть любая строка, включая и пустую строку, но объект не может иметь два свойства с одинаковыми именами. Значением свойства может быть любое значение, допустимое в языке JavaScript, или (в ECMAScript 5) функция чтения или записи (или обе).
В дополнение к именам и значениям каждое свойство имеет ряд ассоциированных с ним значений, которые называют :
- Атрибут writable определяет доступность значения свойства для записи.
- Атрибут enumerable определяет доступность имени свойства для перечисления в цикле for/in.
- Атрибут configurable определяет возможность настройки, т.е. удаления свойства и изменения его атрибутов.
До появления стандарта ECMAScript 5 все свойства в объектах, создаваемые программой, доступны для записи, перечисления и настройки. В ECMAScript 5 предусматривается возможность настройки атрибутов ваших свойств.
В дополнение к свойствам каждый объект имеет три :
- Атрибут prototype содержит ссылку на другой объект, от которого наследуются свойства.
- Атрибут class содержит строку с именем класса объекта и определяет тип объекта.
- Флаг extensible (в ECMAScript 5) указывает на возможность добавления новых свойств в объект.
Наконец, ниже приводится описание некоторых терминов, которые помогут нам различать три обширные категории объектов в языке JavaScript и два типа свойств:
Объект базового языка
Это объект или класс объектов, определяемый спецификацией ECMAScript. Массивы, функции, даты и регулярные выражения (например) являются объектами базового языка.
Объект среды выполнения
Это объект, определяемый средой выполнения (такой как веб-браузер), куда встроен интерпретатор JavaScript. Объекты HTMLElement, представляющие структуру веб-страницы в клиентском JavaScript, являются объектами среды выполнения. Объекты среды выполнения могут также быть объектами базового языка, например, когда среда выполнения определяет методы, которые являются обычными объектами Function базового языка JavaScript.
Пользовательский объект
Любой объект, созданный в результате выполнения программного кода JavaScript.
Собственное свойство
Это свойство, определяемое непосредственно в данном объекте.
Унаследованное свойство
Это свойство, определяемое прототипом объекта.
Создание объектов
Объекты можно создавать с помощью литералов объектов, ключевого слова new и (в ECMAScript 5) функции Object.create().
Литералы объектов
Самый простой способ создать объект заключается во включении в программу литерала объекта. Литерал объекта — это заключенный в фигурные скобки список свойств (пар имя/значение), разделенных запятыми. Именем свойства может быть идентификатор или строковый литерал (допускается использовать пустую строку). Значением свойства может быть любое выражение, допустимое в JavaScript — значение выражения (это может быть простое значение или объект) станет значением свойства.
Ниже приводится несколько примеров создания объектов:
var empty = <>; // Объект без свойств var point = < x:0, y:0 >; // Два свойства var point2 = < x:point.x, y:point.y+1 >; // Более сложные значения var site = < "url site": "www.professorweb.ru", // Имена свойств с пробелами 'desc-text': "Платформа .NET Framework", // и дефисами, поэтому исп. кавычки author: < // Значением этого свойства является firstname: "Alexandr", // объект. Обратите внимание, что surname: "Frolov" // имена этих свойств без кавычек. >>;
В ECMAScript 5 (и в некоторых реализациях ECMAScript 3) допускается использовать зарезервированные слова в качестве имен свойств без кавычек. Однако в целом имена свойств, совпадающие с зарезервированными словами, в ECMA-Script 3 должны заключаться в кавычки. В ECMAScript 5 последняя запятая, следующая за последним свойством в литерале объекта, игнорируется. В большинстве реализаций ECMAScript 3 завершающие запятые также игнорируются, но IE интерпретирует их наличие как ошибку.
Литерал объекта — это выражение, которое создает и инициализирует новый объект всякий раз, когда производится вычисление этого выражения. Значение каждого свойства вычисляется заново, когда вычисляется значение литерала. Это означает, что с помощью единственного литерала объекта можно создать множество новых объектов, если этот литерал поместить в тело цикла или функции, которая будет вызываться многократно, и что значения свойств этих объектов могут отличаться друг от друга.
Создание объектов с помощью оператора new
Оператор new создает и инициализирует новый объект. За этим оператором должно следовать имя функции. Функция, используемая таким способом, называется и служит для инициализации вновь созданного объекта. Базовый JavaScript включает множество встроенных конструкторов для создания объектов базового языка. Например:
var o = new Object(); // Создать новый пустой объект: то же, что и <> var a = new Array(); // Создать пустой массив: то же, что и [] var d = new Date(); // Создать объект Date, представляющий текущее время var r = new RegExp("js"); // Создать объект RegExp для операций сопоставления с шаблоном
Помимо этих встроенных конструкторов имеется возможность определять свои собственные функции-конструкторы для инициализации вновь создаваемых объектов. О том, как это делается, рассказывается в следующей статье.
Object.create()
Стандарт ECMAScript 5 определяет метод Object.create(), который создает новый объект и использует свой первый аргумент в качестве прототипа этого объекта. Дополнительно Object.create() может принимать второй необязательный аргумент, описывающий свойства нового объекта.
Object.create() является статической функцией, а не методом, вызываемым относительно некоторого конкретного объекта. Чтобы вызвать эту функцию, достаточно передать ей желаемый объект-прототип:
// obj наследует свойства x и y var obj = Object.create();
Чтобы создать объект, не имеющий прототипа, можно передать значение null, но в этом случае вновь созданный объект не унаследует ни каких-либо свойств, ни базовых методов, таких как toString() (а это означает, что этот объект нельзя будет использовать в выражениях с оператором +):
// obj2 не наследует ни свойств, ни методов var obj2 = Object.create(null);
Если в программе потребуется создать обычный пустой объект (который, например, возвращается литералом <> или выражением new Object()), передайте в первом аргументе Object.prototype:
// obj3 подобен объекту, созданному // с помощью <> или new Object() var obj3 = Object.create(Object.prototype);
Возможность создавать новые объекты с произвольными прототипами (скажем иначе: возможность создавать «наследников» от любых объектов) является мощным инструментом, действие которого можно имитировать в ECMAScript 3 с помощью функции, представленной в примере ниже:
// inherit() возвращает вновь созданный объект, наследующий свойства // объекта-прототипа p. Использует функцию Object.create() из ECMAScript 5, // если она определена, иначе используется более старый прием. function inherit(p) < if (p == null) throw TypeError(); // p не может быть значением null if (Object.create) // Если Object.create() определена. return Object.create(p); // использовать ее. var t = typeof p; // Иначе выяснить тип и проверить его if (t !== "object" && t !== "function") throw TypeError(); function f() <>; // Определить пустой конструктор. f.prototype = p; // Записать в его свойство prototype // ссылку на объект p. return new f(); // Использовать f() для создания // "наследника" объекта p. >
Реализация функции inherit() приобретет больше смысла, как только мы познакомимся с конструкторами в следующей статье. А пока просто считайте, что она возвращает новый объект, наследующий свойства объекта в аргументе. Обратите внимание, что функция inherit() не является полноценной заменой для Object.create(): она не позволяет создавать объекты без прототипа и не принимает второй необязательный аргумент, как Object.create().
Получение и изменение свойств
Получить значение свойства можно с помощью операторов точки (.) и квадратных скобок ([]). Слева от оператора должно находиться выражение, возвращающее объект. При использовании оператора точки справа должен находиться простой идентификатор, соответствующий имени свойства. При использовании квадратных скобок в квадратных скобках должно указываться выражение, возвращающее строку, содержащую имя требуемого свойства:
// Простой объект var user = < login:'kot86', name:'Alexandr', age:26 >; var login = user.login; // Получить свойство "login" объекта user var name = user.name; // Получить свойство "name" объекта user var age = user['age']; // Получить свойство "age" объекта user
Чтобы создать новое свойство или изменить значение существующего свойства, также используются операторы точки и квадратные скобки, как в операциях чтения значений свойств, но само выражение помещается уже слева от оператора присваивания:
user.age = 28; // Изменить значение свойства 'age' user['login'] = 'kot84'; // Изменить значение свойства 'login' user['surname'] = 'Frolov'; // Создать новое свойство 'surname'
В ECMAScript 3 идентификатор, следующий за точкой, не может быть зарезервированным словом: нельзя записать обращение к свойству o.for или o.class, потому что for является ключевым словом, а class — словом, зарезервированным для использования в будущем.
Если объект имеет свойства, имена которых совпадают с зарезервированными словами, для доступа к ним необходимо использовать форму записи с квадратными скобками: o[«for»] и o[«class»]. Стандарт ECMAScript 5 ослабляет это требование (как это уже сделано в некоторых реализациях ECMAScript 3) и допускает возможность использования зарезервированных слов после оператора точки.
Прототипы
Каждый объект в языке JavaScript имеет второй объект (или null, но значительно реже), ассоциированный с ним. Этот второй объект называется , и первый объект наследует от прототипа его свойства.
Все объекты, созданные с помощью литералов объектов, имеют один и тот же объект-прототип, на который в программе JavaScript можно сослаться так: Object.prototype .
Объекты, созданные с помощью ключевого слова new и вызова конструктора, в качестве прототипа получают значение свойства prototype функции-конструктора. Поэтому объект, созданный выражением new Object(), наследует свойства объекта Object.prototype, как если бы он был создан с помощью литерала в фигурных скобках <>. Аналогично прототипом объекта, созданного выражением new Array(), является Array.prototype, а прототипом объекта, созданного выражением new Date(), является Date.prototype.
Object.prototype — один из немногих объектов, которые не имеют прототипа: у него нет унаследованных свойств. Другие объекты-прототипы являются самыми обычными объектами, имеющими собственные прототипы.
Все встроенные конструкторы (и большинство пользовательских конструкторов) наследуют прототип Object.prototype. Например, Date.prototype наследует свойства от Object.prototype, поэтому объект Date, созданный выражением new Date(), наследует свойства от обоих прототипов, Date.prototype и Object.prototype. Такая связанная последовательность объектов-прототипов называется .
Наследование
Объекты в языке JavaScript обладают множеством «собственных свойств» и могут также наследовать множество свойств от объекта-прототипа. Чтобы разобраться в этом, необходимо внимательно изучить механизм доступа к свойствам. В примерах этого раздела для создания объектов с определенными прототипами используется функция inherit(), показанная выше.
Предположим, что программа обращается к свойству x объекта obj. Если объект obj не имеет собственного свойства с таким именем, выполняется попытка отыскать свойство x в прототипе объекта obj. Если объект-прототип не имеет собственного свойства с этим именем, но имеет свой прототип, выполняется попытка отыскать свойство в прототипе прототипа. Так продолжается до тех пор, пока не будет найдено свойство x или пока не будет достигнут объект, не имеющий прототипа. Как видите, атрибут prototype объекта создает цепочку, или связанный список объектов, от которых наследуются свойства.
var obj = <>; // obj наследует методы объекта Object.prototype obj.x = 1; // и обладает собственным свойством x. var p = inherit(obj); // p наследует свойства объектов obj и Object.prototype p.y = 2; // и обладает собственным свойством y. var q = inherit(p); // q наследует свойства объектов p, obj и Object.prototype q.z = 3; // и обладает собственным свойством z. var s = q.toString(); // toString наследуется от Object.prototype var d = q.x + q.y // Результат 3: x и y наследуются от obj и p
Теперь предположим, что программа присваивает некоторое значение свойству x объекта obj. Если объект obj уже имеет собственное свойство (не унаследованное) с именем x, то операция присваивания просто изменит значение существующего свойства. В противном случае в объекте obj будет создано новое свойство с именем x. Если прежде объект obj наследовал свойство x, унаследованное свойство теперь окажется скрыто вновь созданным собственным свойством с тем же именем.
Операция присваивания значения свойству проверит наличие этого свойства в цепочке прототипов, чтобы убедиться в допустимости присваивания. Например, если объект obj наследует свойство x, доступное только для чтения, то присваивание выполняться не будет. Однако если присваивание допустимо, всегда создается или изменяется свойство в оригинальном объекте и никогда в цепочке прототипов. Тот факт, что механизм наследования действует при чтении свойств, но не действует при записи новых значений , является ключевой особенностью языка JavaScript, потому что она позволяет выборочно переопределять унаследованные свойства:
var unitcircle = < r:1 >; // Объект, от которого наследуется свойство var c = inherit(unitcircle); // c наследует свойство r c.x = 1; c.y = 1; // c определяет два собственных свойства c.r = 2; // c переопределяет унаследованное свойство console.log(unitcircle.r); // => 1: объект-прототип не изменился
Существует одно исключение из этого правила, когда операция присваивания значения свойству терпит неудачу или приводит к созданию/изменению свойства оригинального объекта. Если объект obj наследует свойство x и доступ к этому свойству осуществляется посредством методов доступа, то вместо создания нового свойства x в объекте obj производится вызов метода записи нового значения. Однако обратите внимание, что метод записи вызывается относительно объекта obj, а не относительно прототипа, в котором определено это свойство, поэтому, если метод записи определяет какие-либо свойства, они будут созданы в объекте obj, а цепочка прототипов опять останется неизменной.
Ошибки доступа к свойствам
Выражения обращения к свойствам не всегда возвращают или изменяют значение свойства. В этом разделе описываются ситуации, когда операции чтения или записи свойства терпят неудачу.
Попытка обращения к несуществующему свойству не считается ошибкой. Если свойство x не будет найдено среди собственных или унаследованных свойств объекта obj, выражение обращения к свойству obj.x вернет значение undefined.
Однако попытка обратиться к свойству несуществующего объекта считается ошибкой. Значения null и undefined не имеют свойств, и попытки обратиться к свойствам этих значений считаются ошибкой:
// Простой объект var user = < login:'kot86', name:'Alexandr', age:26 >; var a = user.password; // undefined: свойство отсутствует // Возбудит исключение TypeError. // Значение undefined не имеет свойства length var len = user.password.length;
Если нет уверенности, что user и user.password являются объектами (или ведут себя подобно объектам), нельзя использовать выражение user.password.length, так как оно может возбудить исключение. Ниже демонстрируются два способа защиты против исключений подобного рода:
// Более наглядный и прямолинейный способ var len = undefined; if (user) < if (user.password) len = user.password.length; >// Более краткая и характерная для JavaScript альтернатива // получения длины значения свойства password var len = user && user.password && user.password.length;
Попытки установить значение свойства для других значений не всегда оканчиваются успехом: некоторые свойства доступны только для чтения и не позволяют изменять их значения. Кроме того, некоторые объекты не позволяют добавлять в них новые свойства. Однако самое интересное, что подобные неудачи, как правило, не приводят к возбуждению исключения:
// Свойства prototype встроенных конструкторов доступны только для чтения Object.prototype = 0; // Присваивание не возбудит исключения; // значение Object.prototype не изменится
Этот исторически сложившийся недостаток JavaScript исправлен в строгом режиме, определяемом стандартом ECMAScript 5. Все неудачные попытки изменить значение свойства в строгом режиме приводят к исключению TypeError.
Правила, позволяющие определить, когда попытка выполнить операцию присваивания завершится успехом, а когда неудачей, просты и понятны, но их сложно выразить в достаточно краткой форме. Попытка присвоить значение свойству p объекта obj потерпит неудачу в следующих случаях:
- Объект obj имеет собственное свойство p, доступное только для чтения: нельзя изменить значение свойства, доступного только для чтения. (Обратите, однако, внимание на метод defineProperty(), который представляет собой исключение, позволяющее изменять значения настраиваемых свойств, доступных только для чтения.)
- Объект obj имеет унаследованное свойство p, доступное только для чтения: унаследованные свойства, доступные только для чтения, невозможно переопределить собственными свойствами с теми же именами.
- Объект obj не имеет собственного свойства p; объект obj не наследует свойство p с методами доступа и атрибут extensible объекта obj имеет значение false. Если свойство p отсутствует в объекте obj и для него не определен метод записи, то операция присваивания попытается добавить свойство p в объект obj. Но поскольку объект obj не допускает возможность расширения, то попытка добавить в него новое свойство потерпит неудачу.
Удаление свойств
Оператор delete удаляет свойство из объекта. Его единственный операнд должен быть выражением обращения к свойству. Может показаться удивительным, но оператор delete не оказывает влияния на значение свойства — он оперирует самим свойством:
// Простой объект var user = < login:'kot86', name:'Alexandr', age:26 >; delete user.login; // Теперь объект user не имеет свойства login delete user['name']; // Теперь объект user не имеет свойства name
Оператор delete удаляет только собственные свойства и не удаляет унаследованные. (Чтобы удалить унаследованное свойство, необходимо удалять его в объекте-прототипе, в котором оно определено. Такая операция затронет все объекты, наследующие этот прототип.)
Выражение delete возвращает значение true в случае успешного удаления свойства или когда операция удаления не привела к изменению объекта (например, при попытке удалить несуществующее свойство). Выражение delete также возвращает true, когда этому оператору передается выражение, не являющееся выражением обращения к свойству:
obj = ; // obj имеет собственное свойство x и наследует toString delete obj.x; // Удалит x и вернет true delete obj.x; // Ничего не сделает (x не существует) и вернет true delete obj.toString; // Ничего не сделает (toString не собственное свойство) и вернет true delete 1; // Бессмысленно, но вернет true
Оператор delete не удаляет ненастраиваемые свойства, атрибут configurable которых имеет значение false. (Однако он может удалять настраиваемые свойства нерасширяемых объектов.) Ненастраиваемыми являются свойства встроенных объектов, а также свойства глобального объекта, созданные с помощью инструкций объявления переменных и функций. Попытка удалить ненастраиваемое свойство в строгом режиме вызывает исключение TypeError. В нестрогом режиме (и в реализациях ECMAScript 3) в таких случаях оператор delete просто возвращает false:
delete Object.prototype; // Удаление невозможно - ненастраиваемое свойство var x = 1; // Объявление глобальной переменной delete this.x; // Это свойство нельзя удалить function f() <> // Объявление глобальной функции delete this.f; // Это свойство также нельзя удалить
Проверка существования свойств
Объекты в языке JavaScript можно рассматривать как множества свойств, и нередко бывает полезно иметь возможность проверить принадлежность к множеству — проверить наличие в объекте свойства с данным именем. Выполнить такую проверку можно с помощью оператора in, с помощью методов hasOwnProperty() и propertyIsEnumerable() или просто обратившись к свойству.
Оператор in требует, чтобы в левом операнде ему было передано имя свойства (в виде строки) и объект в правом операнде. Он возвращает true, если объект имеет собственное или унаследованное свойство с этим именем:
var obj = < x:1 >"x" in obj; // true: obj имеет собственное свойство "x" "y" in obj; // false: obj не имеет свойства "y" "toString" in obj; // true: obj наследует свойство toString
Метод hasOwnProperty() объекта проверяет, имеет ли объект собственное свойство с указанным именем. Для наследуемых свойств он возвращает false:
var obj = < x:1 >obj.hasOwnProperty('x'); // true: obj имеет собственное свойство "x" obj.hasOwnProperty('y'); // false: obj не имеет свойства "y" obj.hasOwnProperty('toString'); // false: toString - наследуемое свойство
Метод propertyIsEnumerable() накладывает дополнительные ограничения по сравнению с hasOwnProperty(). Он возвращает true, только если указанное свойство является собственным свойством, атрибут enumerable которого имеет значение true. Свойства встроенных объектов не являются перечислимыми. Свойства, созданные обычной программой на языке JavaScript, являются перечислимыми, если не был использован один из методов ECMAScript 5, представленных ниже, которые делают свойства неперечислимыми.
Часто вместо оператора in достаточно использовать простое выражение обращения к свойству и использовать оператор !== для проверки на неравенство значению undefined:
var obj = < x:1 >obj.x !== undefined; // true: obj имеет свойство "x" obj.y !== undefined; // false: obj не имеет свойства "y" obj.toString !== undefined; // true: obj наследует свойство toString
Однако оператор in отличает ситуации, которые неотличимы при использовании представленного выше приема на основе обращения к свойству. Оператор in отличает отсутствие свойства от свойства, имеющего значение undefined.
Перечисление свойств
Вместо проверки наличия отдельных свойств иногда бывает необходимо обойти все имеющиеся свойства или получить список всех свойств объекта. Обычно для этого используется цикл for/in, однако стандарт ECMAScript 5 предоставляет две удобные альтернативы.
Инструкция цикла for/in выполняет тело цикла для каждого перечислимого свойства (собственного или унаследованного) указанного объекта, присваивая имя свойства переменной цикла. Встроенные методы, наследуемые объектами, являются неперечислимыми, а свойства, добавляемые в объекты вашей программой, являются перечислимыми (если только не использовались функции, описываемые ниже, позволяющие сделать свойства неперечислимыми). Например:
// Простой объект с тремя перечислимыми свойствами var user = < login:'kot86', name:'Alexandr', age:26 >; user.propertyIsEnumerable('toString'); // false, toString - встроенный метод for (n in user) console.log(n);
Некоторые библиотеки добавляют новые методы (или другие свойства) в объект Object.prototype, чтобы они могли быть унаследованы и быть доступны всем объектам. Однако до появления стандарта ECMAScript 5 отсутствовала возможность сделать эти дополнительные методы неперечислимыми, поэтому они оказывались доступными для перечисления в циклах for/in. Чтобы решить эту проблему, может потребоваться фильтровать свойства, возвращаемые циклом for/in. Ниже приводятся два примера реализации такой фильтрации:
for (n in user) < if (!user.hasOwnProperty(n)) continue; console.log(n); >for (n in user)
В дополнение к циклу for/in стандарт ECMAScript 5 определяет две функции, перечисляющие имена свойств. Первая из них, Object.keys(), возвращает массив имен собственных перечислимых свойств объекта.
Вторая функция ECMAScript 5, выполняющая перечисление свойств — Object.getOwnPropertyNames(). Она действует подобно функции Object.keys(), но возвращает имена всех собственных свойств указанного объекта, а не только перечислимые. В реализациях ECMAScript 3 отсутствует возможность реализовать подобные функции, потому что ECMAScript 3 не предусматривает возможность получения неперечислимых свойств объекта.
Методы чтения и записи свойств
Выше уже говорилось, что свойство объекта имеет имя, значение и набор атрибутов. В ECMAScript 5 значение может замещаться одним или двумя методами, известными как методы и . Свойства, для которых определяются методы чтения и записи, иногда называют свойствами с методами доступа, чтобы отличать их от свойств с данными, представляющих простое значение.
Когда программа пытается получить значение свойства с методами доступа, интерпретатор вызывает метод чтения (без аргументов). Возвращаемое этим методом значение становится значением выражения обращения к свойству. Когда программа пытается записать значение в свойство, интерпретатор вызывает метод записи, передавая ему значение, находящее справа от оператора присваивания. Этот метод отвечает за «установку» значения свойства. Значение, возвращаемое методом записи, игнорируется.
В отличие от свойств с данными, свойства с методами доступа не имеют атрибута writable. Если свойство имеет оба метода, чтения и записи, оно доступно для чтения/записи. Если свойство имеет только метод чтения, оно доступно только для чтения. А если свойство имеет только метод записи, оно доступно только для записи (такое невозможно для свойств с данными) и попытки прочитать значение такого свойства всегда будут возвращать undefined.
Самый простой способ определить свойство с методами доступа заключается в использовании расширенного синтаксиса определения литералов объектов:
var obj = < // Обычное свойство с данными data_prop: value, // Свойство с методами доступа определяется как пара функций get accessor_prop() < /* тело функции */ >, set accessor_prop(value) < /* тело функции */ >>;
Свойства с методами доступа определяются как одна или две функции, имена которых совпадают с именем свойства и с заменой ключевого слова function на get и/или set.
Обратите внимание, что не требуется использовать двоеточие для отделения имени свойства от функции, управляющей доступом к свойству, но по-прежнему необходимо использовать запятую после тела функции, чтобы отделить метод от других методов или свойств с данными.
Для примера рассмотрим следующий объект, представляющий декартовы координаты точки на плоскости. Для представления координат X и Y в нем имеются обычные свойства с данными, а также свойства с методами доступа, позволяющие получить эквивалентные полярные координаты точки:
var p = < // x и y обычные свойства с данными, доступные для чтения/записи x: 1.0, y: 1.0, // r - доступное для чтения/записи свойство с двумя методами доступа. // Не забывайте добавлять запятые после методов доступа get r() < return Math.sqrt(this.x*this.x + this.y*this.y); >, set r(newvalue) < var oldvalue = Math.sqrt(this.x*this.x + this.y*this.y); var ratio = newvalue/oldvalue; this.x *= ratio; this.y *= ratio; >, // theta - доступное только для чтения свойство с единственным методом чтения get theta() < return Math.atan2(this.y, this.x); >>;
Обратите внимание на использование ключевого слова this в методах чтения и записи выше. Интерпретатор будет вызывать эти функции, как методы объекта, в котором они определены, т.е. в теле функции this будет ссылаться на объект точки. Благодаря этому метод чтения свойства r может ссылаться на свойства x и y, как this.x и this.y.
Свойства с методами доступа наследуются так же, как обычные свойства с данными, поэтому объект p, определенный выше, можно использовать как прототип для других объектов точек. В новых объектах можно определять собственные свойства x и y, и они будут наследовать свойства r и theta.
Атрибуты объекта
Все объекты имеют атрибуты prototype, class и extensible. Все эти атрибуты описываются в подразделах ниже.
Атрибут prototype
Атрибут prototype объекта определяет объект, от которого наследуются свойства. Важно понимать, что когда в программном коде встречается ссылка prototype, она обозначает обычное свойство объекта, а не атрибут prototype.
Атрибут prototype устанавливается в момент создания объекта. Для объектов, созданных с помощью литералов, прототипом является Object.prototype. Прототипом объекта, созданного с помощью оператора new, является значение свойства prototype конструктора. А прототипом объекта, созданного с помощью Object.create(), становится первый аргумент этой функции (который может иметь значение null).
Стандартом ECMAScript 5 предусматривается возможность определить прототип любого объекта, если передать его методу Object.getPrototypeOf(). В ECMAScript 3 отсутствует эквивалентная функция, но зачастую определить прототип объекта obj можно с помощью выражения obj.constructor.prototype.
Объекты, созданные с помощью оператора new, обычно наследуют свойство constructor, ссылающееся на функцию-конструктор, использованную для создания объекта. И как уже говорилось выше, функции-конструкторы имеют свойство prototype, которое определяет прототип объектов, созданных с помощью этого конструктора.
Обратите внимание, что объекты, созданные с помощью литералов объектов или Object.create(), получают свойство constructor, ссылающееся на конструктор Object(). Таким образом, constructor.prototype ссылается на истинный прототип для литералов объектов, но обычно это не так для объектов, созданных вызовом Object.create().
Чтобы определить, является ли один объект прототипом (или звеном в цепочке прототипов) другого объекта, следует использовать метод isPrototypeOf(). Чтобы узнать, является ли p прототипом obj, нужно записать выражение p.isPrototypeOf(obj). Например:
var p = ; // Определить объект-прототип. var obj = Object.create(p); // Создать объект с этим прототипом. p.isPrototypeOf(obj); // => true: obj наследует p Object.prototype.isPrototypeOf(p); // => true: p наследует Object.prototype
Атрибут class
Атрибут class объекта — это строка, содержащая информацию о типе объекта. Ни в ECMAScript 3, ни в ECMAScript 5 не предусматривается возможность изменения этого атрибута и предоставляются лишь косвенные способы определения его значения. По умолчанию метод toString() (наследуемый от Object.prototype) возвращает строку вида:
[object class]
Поэтому, чтобы определить класс объекта, можно попробовать вызвать метод toString() этого объекта и извлечь из результата подстроку с восьмого по предпоследний символ. Вся хитрость состоит в том, что многие методы наследуют другие, более полезные реализации метода toString(), и чтобы вызвать нужную версию toString(), необходимо выполнить косвенный вызов с помощью метода Function.call().
В примере ниже определяется функция, возвращающая класс любого объекта, переданного ей:
// Название класса объекта function classof(obj)
Этой функции classof() можно передать любое значение, допустимое в языке JavaScript. Числа, строки и логические значения действуют подобно объектам, когда относительно них вызывается метод toString(), а значения null и undefined обрабатываются особо.
Атрибут extensible
Атрибут extensible объекта определяет, допускается ли добавлять в объект новые свойства. В ECMAScript 3 все встроенные и определяемые пользователем объекты неявно допускали возможность расширения, а расширяемость объектов среды выполнения определялась каждой конкретной реализацией. В ECMAScript 5 все встроенные и определяемые пользователем объекты являются расширяемыми, если они не были преобразованы в нерасширяемые объекты, а расширяемость объектов среды выполнения по-прежнему определяется каждой конкретной реализацией.
Стандарт ECMAScript 5 определяет функции для получения и изменения признака расширяемости объекта. Чтобы определить, допускается ли расширять объект, его следует передать методу Object.isExtensible(). Чтобы сделать объект нерасширяемым, его нужно передать методу Object.preventExtensions(). Обратите внимание, что после того как объект будет сделан нерасширяемым, его нельзя снова сделать расширяемым. Отметьте также, что вызов preventExtensions() оказывает влияние только на расширяемость самого объекта. Если новые свойства добавить в прототип нерасширяемого объекта, нерасширяемый объект унаследует эти новые свойства.
Назначение атрибута extensible заключается в том, чтобы дать возможность «фиксировать» объекты в определенном состоянии, запретив внесение изменений. Атрибут объектов extensible часто используется совместно с атрибутами свойств configurable и writable, поэтому в ECMAScript 5 определяются функции, упрощающие одновременную установку этих атрибутов.
Метод Object.seal() действует подобно методу Object.preventExtensions(), но он не только делает объект нерасширяемым, но и делает все свойства этого объекта недоступными для настройки. То есть в объект нельзя будет добавить новые свойства, а существующие свойства нельзя будет удалить или настроить. Однако существующие свойства, доступные для записи, по-прежнему могут быть изменены.
После вызова Object.seal() объект нельзя будет вернуть в прежнее состояние. Чтобы определить, вызывался ли метод Object.seal() для объекта, можно вызвать метод Object.isSealed().
Метод Object.freeze() обеспечивает еще более жесткую фиксацию объектов. Помимо того, что он делает объект нерасширяемым, а его свойства недоступными для настройки, он также делает все собственные свойства с данными доступными только для чтения. (Это не относится к свойствам объекта с методами доступа, обладающими методами записи; эти методы по-прежнему будут вызываться инструкциями присваивания.) Чтобы определить, вызывался ли метод Object.freeze() объекта, можно вызвать метод Object.isFrozen().
Важно понимать, что Object.seal() и Object.freeze() воздействуют только на объект, который им передается: они не затрагивают прототип этого объекта. Если в программе потребуется полностью зафиксировать объект, вам, вероятно, потребуется зафиксировать также объекты в цепочке прототипов.
Сериализация объектов
Сериализация объектов — это процесс преобразования объектов в строковую форму представления, которая позднее может использоваться для их восстановления. Для сериализации и восстановления объектов JavaScript стандартом ECMAScript 5 предоставляются встроенные функции JSON.stringify() и JSON.parse(). Эти функции используют формат обмена данными JSON. Название JSON происходит от «JavaScript Object Notation» (форма записи объектов JavaScript), а синтаксис этой формы записи напоминает синтаксис литералов объектов и массивов в языке JavaScript:
var obj = >; // Определить испытательный объект var s = JSON.stringify(obj); // s == '>' var p = JSON.parse(s); // p - копия объекта obj
Синтаксис формата JSON является лишь подмножеством синтаксиса языка JavaScript и не может использоваться для представления всех возможных значений, допустимых в JavaScript. Поддерживаются и могут быть сериализованы и восстановлены: объекты, массивы, строки, конечные числовые значения, true, false и null. Значения NaN, Infinity и -Infinity сериализуются в значение null. Объекты Date сериализуются в строки с датами в формате ISO, но JSON.parse() оставляет их в строковом представлении и не восстанавливает первоначальные объекты Date.
Объекты Function, RegExp и Error и значение undefined не могут быть сериализованы или восстановлены. Функция JSON.stringify() сериализует только перечислимые собственные свойства объекта. Если значение свойства не может быть сериализовано, это свойство просто исключается из строкового представления. Обе функции, JSON.stringify() и JSON.parse(), принимают необязательный второй аргумент, который можно использовать для настройки процесса сериализации и/или восстановления, например, посредством определения списка свойств, подлежащих сериализации, или функции преобразования значений во время сериализации.
Основы создания объектов и работы с ними в JavaScript
Объект – это один из типов данных в JavaScript, который предназначен для хранения коллекции различных значений и более сложных сущностей.
Всего в JavaScript существует 8 типов данных: Number , BigInt , String , Boolean , null , undefined , Symbol и Object .
Но все кроме объекта являются примитивными. При присвоении переменной значения примитивного типа, оно хранится в ней непосредственно:
JavaScript
// переменная mark содержит значение примитивного типа в данном случае число let mark = 4; // присваиваем переменной mark новое примитивное значение, а именно строку 'Good!' mark = 'Good!';
Когда мы хотим одной переменной присвоить значение другой, содержащей примитивный тип, копирование осуществляется по значению (по английски – copy by value):
JavaScript
const mark = 4; // создание newMark и присвоение ей значения переменной mark let newMark = mark; // присвоим newMark новое значение newMark = 5; console.log(mark); // 4 console.log(newMark); // 5
Здесь мы видим как происходит копирование значений примитивных типов, то есть непосредственно по значению. Если бы переменная mark содержала объект, то процесс копирования осуществлялся бы уже не по значению. Это очень важный момент, но об этом немного позже.
Так что же такое объект? В JavaScript объект – это набор свойств «имя: значение». При этом имена ещё очень часто называют ключами . При этом значение свойства может содержать что угодно. Если в качестве значения используется функция, то такое свойство называют методом .
Объекты – это ссылочный тип. Поэтому, когда вы присваиваете объект переменной, вы на самом деле присваиваете ей не сам этот объект, а ссылку на него, которая указывает на то место в памяти компьютера, где он находится.
Литеральный синтаксис
Литеральный синтаксис объявления объекта – это один из самых простых способов его создания. Обычно он используется, когда необходимо объявить только один объект.
Литерал объекта записывается с помощью фигурных скобок {. } , внутрь которых помещают свойства, отделенные друг от друга посредством запятой:
JavaScript
const person = { firstName: 'Александр', lastName: 'Мальцев', age: 28, getFullName: function() { return `${this.firstName} ${this.lastName}` } }
В этом примере объект содержит четыре свойства: firstName , lastName , age и getFullName . При этом свойство getFullName является методом, так какие его значение – это функция.
Новый формат записи методов выполняется без использования ключевого слова function и двоеточия:
JavaScript
const person = { // . getFullName() { return `${this.firstName} ${this.lastName}` } }
Объекты в JavaScript, как и в других языках программирования, легче понять, если провести аналогию с объектами «реального мира». При этом любой объект имеет характеристики (свойства) и может выполнять некоторые задачи (методы).
Например, объект car имеет свойства: color , engine и model . Свойства – это характеристики объекта, они обычно описываются с помощью существительных и прилагательных. Также объект имеет собственные методы: drive , park , start и stop . Методы – это поведение объекта, его функции, они обычно обозначаются посредством глаголов.
JavaScript
// объект car const car = { color: 'orange', engine: '3.6L V6', model: 'AL3', drive() { console.log('driving'); }, park() { console.log('parking'); }, start() { console.log('starting'); }, stop() { console.log('stopping'); } }
Свойства
Свойства состоят из ключа и значения. Отделяется ключ от значения посредством двоеточия. Свойства подобны переменным, но в составе объекта.
Обращение к свойствам выполняется через точку или квадратные скобки:
JavaScript
// через точку const firstName = person.firstName; const lastName = person.lastName; // через квадратные скобки, ключ в этом случае нужно указывать в виде строки const age = person['age']; const getFullName = person['getFullName'];
Значения, связанные с соответствующими ключами можно не только получить, но и присвоить им новые значения:
JavaScript
// изменим значения свойств firstName и lastName person.firstName = 'Иван'; person.lastName = 'Михайлов';
А также добавить новые свойства объекту:
JavaScript
// добавим к объекту person свойство middleName со значением 'Сергеевич' person.middleName = 'Сергеевич';
Свойствам можно устанавливать какие угодно значения. Например, присвоим свойству children массив объектов:
JavaScript
person.children = [ { name: 'Аня', age: 8 }, { name: 'Ваня', age: 14 } ]
Удаление свойств из объекта осуществляется с помощью оператора delete :
JavaScript
// удалим свойство middleName из объекта person delete person.middleName;
Проверить наличия ключа в объекте можно посредством оператора in :
JavaScript
'firstName' in person // true 'middleName' in person // false
Обратите внимание , что на самом деле имена свойств в объекте являются строками . Но их можно не заключать в кавычки, как мы это делали выше. Но, это допустимо только в том случае, если они составлены по правилам именования переменных.
Имена свойств, которые составлены не по правилам именования переменных:
JavaScript
const someObj = { '': 1, // имя является пустой строкой 'author of post': 'Алексей', // имя состоит из нескольких слов }
Обратиться к таким свойствам только с помощью квадратных скобок:
JavaScript
// получим значения свойства, имя которой является пустой строкой const value = someObj[''];
Если ключ содержится в переменной, то получить значение этого свойства можно только через квадратные скобки:
JavaScript
const key = 'author of post'; const value = someObj[key];
Но именовать свойства не по правилам не рекомендуется. Также не рекомендуется использовать в качестве ключей зарезервированные слова JavaScript, несмотря на то, что это допускается.
Методы
Методы , как мы уже отмечали выше – это свойства, у которых значение является функцией. Обращение к ним осуществляется также как к свойствам, то есть через точку или с использованием квадратных скобок:
JavaScript
const person = { firstName: 'Александр', lastName: 'Мальцев', age: 28, getFullName() { console.log(`${this.firstName} ${this.lastName}`); } } // создадим переменную getFullName и присвоим ей метод getFullName const getFullName = person.getFullName; // тоже самое только через квадратные скобки // const getFullName = person['getFullName'];
Но, так как методы – это функции, то их соответственно можно вызвать . Методы мы для этого и создаём, чтобы потом их можно было вызывать. Вызов метода выполняется также как функции, т.е. с использованием круглых скобок :
JavaScript
// получим метод и присвоим его переменной getFullName const getFullName = person.getFullName; // вызовем метод getFullName(); // или сразу person.getFullName(); person['getFullName']();
Кроме выполнения действий методы всегда возвращают значения. В данном примере вызов getFullName() возвращает значение undefined , т.к. в нём мы явно не прописали инструкцию return . В этом случае не явно возвращается undefined :
JavaScript
console.log(getFullName()); // undefined
Краткая запись свойств
Очень часто в коде мы переменные используем в качестве значений свойств с тем же именем. Когда имя свойства совпадает с названием переменной мы эту запись можем сделать краткой:
JavaScript
// функция, возвращающая объект const createRect = (width, height) => { // возвращаем объект return { width, // вместо width: width height, // вместо height: height calcArea() { return this.width * this.height; } } } // вызываем функцию и присваиваем возвращённый ей объект переменной rect const rect = createRect(10, 15); console.log(rect.calcArea()); // 150
Вычисляемые свойства
В JavaScript имя свойства может быть вычисляемым. То есть для задания имени можно использовать выражение, результат вычисления которого и будет это имя. Указывать вычисляемое свойство необходимо в квадратных скобках [] :
JavaScript
const key = 'url'; const app = { name: 'Yandex', [key]: 'https://yandex.ru/', // имя свойства будет взято из переменной key }; console.log(app.url); // 'https://yandex.ru/'
Пример вычисляемого свойства, а точнее метода с более сложным выражением:
JavaScript
const key = 'url'; const app = { name: 'Yandex', [key]: 'https://yandex.ru/', ['get' + key.toUpperCase()]() { return this[key]; } }; console.log(app.getURL()); // 'https://yandex.ru/'
Копирование и сравнение объектов
Переменная, содержащая объект на самом деле содержит не сам объект, а только ссылку на него . При копировании объектов в отличие от значений примитивных типов происходит передача ссылки .
JavaScript
// присвоим переменной student1 объект, а точнее ссылку на него const student1 = { name: 'Carl' }; // присвоим объект, содержащийся в student1 переменной student2 const student2 = student1;
Теперь student1 и student2 содержат ссылки, указывающие на один и тот же объект.
Изменим значение name , используя student2 :
JavaScript
student2.name = 'Nelly';
Получим значение name через student1 :
JavaScript
console.log(student1.name); // Nelly
А что если нам необходимо скопировать не саму ссылку, а создать новый объект с такими же свойствами?
JavaScript
const student3 = {}; for (const key in student1) { student3[key] = student1[key]; } // student3 содержит клон объекта student1 student3.name = 'Thyra'; // в student1 значение name осталось прежним console.log(student1.name); // Nelly
Другой способ скопировать свойства – это воспользоваться методом Object.assign() :
JavaScript
// скопируем все свойства из student1 в {}, а затем присвоим его student4 const student4 = Object.assign({}, student1);
Object.assign() позволяет скопировать свойства из множества объектов. Объект, в который нужно скопировать указывается в качестве первого аргумента, а те из которых – после него:
JavaScript
const target = { a: 1 }; const source1 = { b: 2 }; const source2 = { c: 3 }; Object.assign(target, source1, source2); console.log(target); // {a: 1, b: 2, c: 3}
Сравнение объектов выполняется по ссылкам:
JavaScript
let objA = {}; let objB = objA; let objC = {}; console.log( objA === objB ); // true, т.к. переменные содержат одну и ту же ссылку console.log( objA === objC ); // false, т.к. переменные содержат разные ссылки (оба объекта пусты, но это разные объекты)
Свойства объектов и их конфигурация
До этого времени мы рассматривали свойства как пары «ключ: значение».
Но свойство кроме значения ( value ) имеет специальные флаги:
- writable – доступно ли свойство для изменения;
- enumerable – доступно ли свойство для перебора в циклах;
- configurable – доступно ли свойство для настройки и удаления.
Когда мы создаем литерал объекта, то все эти специальные флаги у свойства имеют значение true . Получить полное описание свойства можно с помощью метода Object.getOwnPropertyDescriptor :
JavaScript
const flower = { name: 'rose', color: 'red' } const descriptor = Object.getOwnPropertyDescriptor(flower, 'name'); console.log(descriptor);
Как вы наверно уже поняли, этот метод позволяет получить полное описание одного свойства. Для этого в Object.getOwnPropertyDescriptor мы должны в качестве первого аргумента передать объект, содержащий это свойство, а посредством второго – его само.
Получить описание сразу всех свойств объекта можно с помощью статического метода Object.getOwnPropertyDescriptors :
JavaScript
const descriptors = Object.getOwnPropertyDescriptors(flower); console.log(descriptors);
Для того чтобы установить новые значения флагам конкретному свойству объекта необходимо использовать метод Object.defineProperty . Например, сделаем свойство name доступным только для чтения:
JavaScript
Object.defineProperty(flower, 'name', { writable: false }); // попробуем изменить значение свойства name flower.name = 'lily'; console.log(flower); // { name: 'rose', color: 'red' }
В этом примере свойство name мы сделали не доступным для изменения. Для этого мы флагу writable установили значение false .
Для определения сразу нескольких свойств можно применять метод Object.defineProperties :
JavaScript
Object.defineProperties(flower, name: { writable: false, configurable: false }, color: { enumerable: false } );
Так как свойству color мы установили флаг enumerable: false , то теперь оно не будет доступно для перебора:
JavaScript
for (let key in flower) { console.log(key); }
Так же этого свойства не будет в массиве ключей, который возвращает метод Object.keys :
JavaScript
console.log(Object.keys(flower)); // ['name']
Если свойству установить флаг configurable: false , то оно становится не конфигурируемым. Такое свойство нельзя будет удалить и его флагам нельзя будет установить новые значения. Таким свойствам мы сделали name .
При попытке установить этому свойству новые значения флагам мы получим ошибку:
Также мы не можем удалить это свойство:
JavaScript
delete flower.name; // false console.log(flower); // {name: 'rose', color: 'red'}
Методы Object.defineProperty и Object.defineProperties можно использовать для добавления новых свойств объекту:
JavaScript
const city = {}; Object.defineProperties(city, { name: {value: 'New York', enumerable: true, writable: true}, area: {value: 1223.59, enumerable: true, writable: true}, }); console.log(Object.getOwnPropertyDescriptors(city));
Когда мы добавляем новые свойства с помощью этих методов, все флаги имеют по умолчанию значение false . Так как в этом примере мы пропустили флаг configurable для всех свойств, то он у всех них имеет значение false , то все эти свойства являются не конфигурируемыми.
Динамические свойства
В JavaScript кроме обычных свойств имеются ещё динамические. Динамическое свойство похоже на обычное, но по факту представляет собой комбинацию сеттера и геттера.
Геттер – это метод, который выполняется, когда мы хотим получить значение динамического свойства. Он не имеет параметров и возвращает значение, которое и будет значением этого свойства.
Сеттер – это метод, который выполняется, когда мы хотим присвоить значение динамическому свойству. Он имеет один параметр, который получает это значение. Далее это значение мы можем присвоить некоторому свойству или выполнить какие-либо другие действия.
При литеральном объявлении объекта они обозначаются с помощью get и set :
JavaScript
const lang = { history: [], // сеттер get current() { return this.history.length ? this.history[this.history.length - 1] : null; }, // геттер set current(value) { this.history.push(value); } } console.log(lang.current); lang.current = 'ru'; console.log(lang.current); // ru lang.current = 'en'; console.log(lang.current); // en
В этом примере мы создали динамическое свойство current . Геттер выполняется при чтении значения этого свойства, а сеттер – при записи.
У динамического свойства необязательно должны быть два метода. Оно может также иметь только геттер или сеттер.
Определять сеттеры и геттеры можно также с помощью Object.defineProperty и Object.defineProperties :
JavaScript
const rect = { a: 10, b: 15 } Object.defineProperty(rect, 'sides', { get() { return `${this.a} x ${this.b}`; }, set(value) { [this.a, this.b] = value; } }); console.log(rect.sides); // '10 x 15' rect.sides = [20, 25]; console.log(rect.sides); // '20 x 25'
Динамические свойства не имеет value и флага writable , но вместо этого у них есть методы get и set .
В этом примере создадим геттер вместе с описанием других свойств, который будет возвращать последнее сообщение:
JavaScript
const messages = {} Object.defineProperties(messages, { list: { value: [] }, add: { value: function (item) { this.list.push(item) } }, // геттер last last: { get() { return this.list.length > 0 ? this.list[this.list.length - 1] : ''; } } }); messages.add('Green'); messages.add('Red'); console.log(messages.last); // 'Red'
Преобразование объекта в массив
В стандартном конструкторе Object имеются методы keys и values с помощью которых можно очень просто трансформировать объект соответственно в массиве ключей и значений.
JavaScript
const car = { brand: 'Ford', color: 'blue' } const keys = Object.keys(car); // ['brand', 'color'] const values = Object.values(car); // ['Ford', 'blue']
После этого, так как это массивы мы можем очень просто выполнять любые другие действия над ними, то есть перебирать, искать нужные элементы, фильтровать и так далее.
Пример перебора объекта с помощью forEach :
JavaScript
Object.keys(car).forEach((key) => { console.log(`${key}: ${car[key]}`); });
Если нужно перебрать только значения, то так:
JavaScript
Object.values(car).forEach((value) => { console.log(value); });
Кроме этого в Object имеется также метод entries , массив массивов, в котором первый элемент будет являться именем свойства, а второй значением:
JavaScript
const car = { brand: 'Ford', color: 'blue' } const entries = Object.entries(car); // [['brand', 'Ford'], ['color', 'blue']] entries.forEach((item) => { console.log(`${item[0]}: ${item[1]}`); });
Задачи
Задача 1 . Необходимо создать из имеющихся на странице заголовков массив объектов следующего вида:
JavaScript
[ { id: 'h2-1', textContent: 'Первая программа' }, { id: 'h2-2', textContent: 'Выражения' }, . ]
Задание: найти в этом массиве максимальную длину свойства textContent и вывести это значение в консоль.
Массив объектов – это обычный массив, элементами которого являются объекты. Здесь id – это значение атрибута id тега , а text – это его содержимое.
Для получения всех элементов с тегом необходимо использовать метод document.querySelectorAll(‘h2’) , а прочитать значения, необходимые для установки id и textContent , можно соответственно с помощью свойств с таким же названием.
После этого необходимо перебрать массив, например, с помощью метода reduce и найти максимальную длину заголовка.
JavaScript
const maxLength = headers.reduce((max, item) => { return item.textContent.length > max ? item.textContent.length : max; }, 0); console.log(maxLength);
Задача 2 . Имеется массив объектов. Необходимо удалить из массива объекты, у которых значение свойства x или y меньше нуля.
JavaScript
// массив объектов const points = [ {x: 5, y: 7}, {x: -2, y: 9}, {x: 0, y: 8}, {x: -1, y: -3} ]
Решить задачу необходимо 2 способами:
- посредством изменения оригинального массива;
- не изменяя оригинальный массив.
Пример решение задачи посредством изменения оригинального массива:
JavaScript
let i = points.length - 1; while (i gt:= 0) i--; } console.log(points); // [{x: 5, y: 7}, {x: 0, y: 8}]
Пример решения, в котором мы не изменяем оригинальный массив:
JavaScript
const newPoints = points.filter((value) => value.x >= 0 && value.y >= 0); console.log(newPoints); // [{x: 5, y: 7}, {x: 0, y: 8}]
Задача 3 . Имеется список статей в виде массива объектов:
JavaScript
const listArticles = [ { title: "Статья 7", likes: 15 }, { title: "Статья 1", likes: 10 }, { title: "Статья 5", likes: 3 }, { title: "Статья 3", likes: 20 } ];
Необходимо отсортировать массив объектов по полю likes .
JavaScript
listArticles.sort((article1, article2) => { return article1.likes < article2.likes ? -1 : article1.likes >article2.likes ? 1 : 0; });
Задача 4 . Имеется следующий массив:
JavaScript
const persons = [ { name: 'John', age: 25 }, { name: 'Leonardo', age: 15 }, { name: 'Kristina', age: 12 } ];
Необходимо создавать новый массив, оставить в нём только те персоны, возраст которых больше или равно 18.
JavaScript
const newPersons = persons.filter((value) => value.age >= 18); console.log(newPersons);
Задача 5 . Написать стрелочную функцию, которая будет проверять является ли объект переданным ей в качестве аргумента пустым, то есть не содержит ли он собственные свойства и методы.
JavaScript
const isEmpty = (obj) => { if (typeof obj === 'object' && obj !== null) { return Object.keys(obj).length === 0; } return null; }
JavaScript
isEmpty({ a: 5 }); // false isEmpty({}); // true isEmpty(5); // null isEmpty(null); // null
Основы объектов в JavaScript
В этой статье мы рассмотрим объекты в JavaScript. Мы будем разбирать основы синтаксиса объектов JavaScript и заново изучим некоторые возможности JavaScript, которые мы уже исследовали ранее на курсе, подтвердив тот факт, что большая часть функциональности, с которой мы уже столкнулись, в действительности является объектами.
Необходимые знания: | Элементарная компьютерная грамотность, базовое понимание HTML и CSS, знакомство с основами JavaScript (см. Первые шаги и Структурные элементы). |
---|---|
Цель: | Понимать основу теории перед началом объектно-ориентированного программирования, как это связано с JavaScript («большинство сущностей являются объектами»), и как начать работу с объектами JavaScript. |
Основы объектов
Объект — это совокупность связанных данных и/или функциональных возможностей. Обычно состоят из нескольких переменных и функций, которые называются свойства и методы, если они находятся внутри объектов. Разберём пример, чтобы показать, как они выглядят.
Как и во многих случаях в JavaScript, создание объекта часто начинается с определения и инициализации переменной. Попробуйте ввести следующий код JavaScript в ваш файл, а затем сохраните файл и обновите страницу браузера:
const person = >;
Если вы введёте person в текстовое JS консоль и нажмёте клавишу Enter, должен получиться следующий результат:
Поздравляем, вы только что создали ваш первый объект. Но это пустой объект, поэтому мы не можем с ним ничего сделать. Давайте обновим наш объект, чтобы он выглядел так:
const person = name: ["Bob", "Smith"], age: 32, gender: "male", interests: ["music", "skiing"], bio: function () alert( this.name[0] + " " + this.name[1] + " is " + this.age + " years old. He likes " + this.interests[0] + " and " + this.interests[1] + ".", ); >, greeting: function () alert("Hi! I'm " + this.name[0] + "."); >, >;
После сохранения и обновления, попробуйте ввести что-нибудь следующее в консоль JavaScript браузера:
.name; person.name[0]; person.age; person.interests[1]; person.bio(); person.greeting();
Как видите, наш объект содержит некоторые данные, а также несколько методов. У нас же с помощью простого синтаксиса есть доступ к ним.
Примечание: Если у вас возникли проблемы с применением файла в работе, попробуйте сравнить ваш код с нашей версией — см. oojs-finished.html (также see it running live). Одна из распространённых ошибок, когда вы начинаете с объектами ставить запятую в конце последнего члена — это приводит к ошибке.
Итак что здесь происходит? Объект состоит из нескольких элементов, каждый из которых имеет своё название (пример name и age выше), и значение (пример [‘Bob’, ‘Smith’] и 32 ). Каждая пара название/значение должны быть разделены запятой, а название и значение в каждом случае разделяются двоеточием. Синтаксис всегда следует этому образцу:
const objectName = member1Name: member1Value, member2Name: member2Value, member3Name: member3Value, >;
Значение члена объекта может быть чем угодно — в нашем объекте person есть строка, число, два массива, и две функции. Первые четыре элемента это элементы данных, относящиеся к свойствам объекта. Последние два элемента являются функциями, которые позволяют объекту что-то сделать с элементами данных, и называются методами объекта.
Такие объекты называются литералами объекта (object literal) — мы буквально вписали все содержимое объекта для его создания. Этот способ сильно отличается от объектов реализованных классами, которые мы рассмотрим позже.
Очень часто для создания объекта используется литерал объекта когда вам нужно каким-то образом перенести ряд структурированных, связанных элементов данных, например, отправляя запрос на сервер, для размещения их в базе данных. Отправка одного объекта намного эффективнее, чем отправка нескольких элементов по отдельности, и с ним легче работать чем с массивом, если требуется идентифицировать отдельные элементы по имени.
Точечная запись (Dot notation)
Выше вы получили доступ к свойствам и методам используя точечную запись (dot notation). Имя объекта (person) действует как пространство имён (namespace) — оно должно быть введено первым, для того чтобы получить доступ ко всему что заключено (encapsulated) внутри объекта. Далее вы пишете точку, затем элемент, к которому хотите получить доступ — это может быть имя простого свойства, элемент массива, или вызов одного из методов объекта, например:
.age; person.interests[1]; person.bio();
Внутренние пространства имён (Sub-namespaces)
Можно даже сделать значением элемента объекта другой объект. Например, попробуйте изменить значение свойства name с такого
name: ['Bob', 'Smith'],
name : first: 'Bob', last: 'Smith' >,
Здесь мы фактически создаём внутреннее пространство имён (sub-namespace). Это звучит сложно, но на самом деле это не так — для доступа к этим элементам вам нужно сделать один дополнительный шаг с ещё одной точкой. Попробуйте в консоли браузера следующее:
.name.first; person.name.last;
Важно: На этом этапе вам также нужно будет пересмотреть код метода и изменить все экземпляры с
[0]; name[1];
.first; name.last;
Иначе ваши методы больше не будут работать.
Скобочная запись (Bracket notation)
Существует другой способ получить свойства объекта — использовать скобочную запись (bracket notation). Вместо написания этого кода:
.age; person.name.first;
Вы можете использовать следующий
["age"]; person["name"]["first"];
Это выглядит очень похоже на то, как вы получаете элементы массива, и в принципе это так и есть — вместо использования числовых индексов для выбора элемента, вы ассоциируете имя свойства для каждого значения. Ничего удивительного, что эти объекты иногда называют ассоциативными массивами — они сопоставляют строки со значениями так же, как массивы сопоставляют числовые индексы со значениями.
Запись элементов в объект
До сих пор мы рассматривали только возврат (или получение) элементов объекта — вы так же можете установить (обновить) значение элемента объекта просто объявив элемент, который вы хотите установить (используя точечную или скобочную запись), например:
.age = 45; person["name"]["last"] = "Cratchit";
Попробуйте ввести эти строки, а затем снова верните элементы, чтобы увидеть, как они изменились
.age; person["name"]["last"];
Вы можете не просто обновлять и устанавливать значения свойств и методов объекта, а так же устанавливать совершенно новые элементы. Попробуйте их в консоли JS:
["eyes"] = "hazel"; person.farewell = function () alert("Bye everybody!"); >;
Теперь вы можете проверить ваши новые элементы:
["eyes"]; person.farewell();
Одним из полезных аспектов скобочной записи является то, что с её помощью можно динамически задавать не только значения элементов, но и их имена. Предположим, что мы хотим, чтобы пользователи могли хранить пользовательские типы данных, введя имя и значение элемента в два следующих поля? Мы могли бы получить эти значения следующим образом:
let myDataName = nameInput.value; let myDataValue = nameValue.value;
Затем мы можем добавить имя и значение этого нового элемента в объект person таким образом:
[myDataName] = myDataValue;
Чтобы проверить это, попробуйте добавить следующие строки в свой код, после закрывающей скобки объекта person :
let myDataName = "height"; let myDataValue = "1.75m"; person[myDataName] = myDataValue;
Теперь попробуйте сохранить и обновить, затем введите следующее в консоль браузера:
.height;
Добавление свойства объекта с использованием вышеописанного метода невозможно с использованием точечной записи, которая может принимать только литеральное имя элемента, а не значение переменной указывающее на имя.
Что такое «this»?
Возможно, вы заметили что-то странное в наших методах. Посмотрите на этот пример:
greeting: function() alert('Hi! I\'m ' + this.name.first + '.'); >
Вы, вероятно, задаётесь вопросом, что такое «this»? Ключевое слово this , ссылается на текущий объект, внутри которого пишется код — поэтому в нашем случае this равен объекту person . Но почему просто не написать person ? Как вы увидите в статье Object-oriented JavaScript for beginners (Объектно-ориентированный JavaScript для начинающих), когда мы начинаем создавать конструкторы и т.д., this очень полезен — он всегда будет гарантировать, что используется верное значение, когда контекст элемента изменяется (например, два разных экземпляра объекта person могут иметь разные имена, но захотят использовать своё собственное имя при приветствии.
Давайте проиллюстрируем, что мы имеем в виду, с упрощённой парой объектов person :
const person1 = name: "Chris", greeting: function () alert("Hi! I'm " + this.name + "."); >, >; const person2 = name: "Brian", greeting: function () alert("Hi! I'm " + this.name + "."); >, >;
В этом случае, person1.greeting() выведет «Hi! I’m Chris.». person2.greeting() , с другой стороны, выведет «Hi! I’m Brian.», хотя код метода одинаковый в обоих случаях. Как мы сказали ранее, this равен объекту, внутри которого находится код — это не очень полезно, когда вы пишите литералы объектов вручную, но оно действительно помогает, когда вы генерируете объекты динамически (например используя конструкторы). Это станет понятнее чуть позже.
Все это время вы использовали объекты
Пока вы проходили эти примеры, вы вероятно заметили, что точечная запись, которую вы использовали, выглядит очень знакомо. Это потому, что вы использовали её на протяжении всего курса! Каждый раз, когда мы работаем над примером, использующим встроенный API браузера или объект JavaScript, мы использовали объекты, потому что такие функции построены с использованием тех же структур объектов, которые мы здесь рассматривали, хотя и более сложные, чем наши собственные пользовательские примеры.
Поэтому, когда вы использовали строковые методы, такие как:
.split(",");
Вы использовали метод доступный в экземпляре класса String . Каждый раз создавая строку в вашем коде, эта строка автоматически создаётся как экземпляр String , и поэтому имеет несколько общих методов/свойств, доступных на нем.
Когда вы обращались к объектной модели документа (DOM), используя следующие строки:
const myDiv = document.createElement("div"); const myVideo = document.querySelector("video");
Вы использовали методы доступные в экземпляре класса Document . Для каждой загруженной веб-страницы создаётся экземпляр Document , называемый document , который представляет всю структуру страницы, её содержимое и другие функции, такие как URL-адрес. Опять же, это означает, что он имеет несколько общих методов/свойств, доступных на нем.
То же самое относится и к любому другому встроенному объекту/API, который вы использовали — Array , Math , и т. д.
Обратите внимание, что встроенные объекты/API не всегда создают экземпляры объектов автоматически. Как пример, Notifications API — который позволяет новым браузерам запускать системные уведомления, требует, чтобы вы создавали новый экземпляр объекта с помощью конструктора для каждого уведомления, которое вы хотите запустить. Попробуйте ввести следующее в консоль JavaScript:
const myNotification = new Notification("Hello!");
Опять же, мы рассмотрим конструкторы в следующей статье.
Примечание: Полезно подумать о том, как объекты взаимодействуют посредством передачи сообщений — когда объекту требуется другой объект для выполнения какого-либо действия, он часто отправляет сообщение другому объекту через один из его методов и ждёт ответа, который мы знаем как возвращаемое (return) значение.
Резюме
Поздравляем, вы достигли конца нашей первой статьи о объектах JS, теперь у вас должно быть хорошее представление о том, как работать с объектами в JavaScript — в том числе создавать свои собственные простые объекты. Вы также должны понимать, что объекты очень полезны в качестве структур для хранения связанных данных и функциональности — если бы мы пытались отслеживать все свойства и методы в нашем объекте person как отдельные переменные и функции, это было неэффективно, и мы бы рисковали столкнуться с другими переменными и функциями с такими же именами. Объекты позволяют нам безопасно хранить информацию в своём собственном блоке, вне опасности.
В следующей статье мы начнём рассматривать теорию объектно-ориентированного программирования (ООП) и как эти техники могут быть использованы в JavaScript
В этом модуле
- Основы объекта
- Объектно-ориентированный JavaScript для начинающих
- Прототипы объектов
- Наследование в JavaScript
- Работа с данными JSON
- Практика построения объектов
- Добавление функций в нашу демонстрацию прыгающих шаров
Found a content problem with this page?
- Edit the page on GitHub.
- Report the content issue.
- View the source on GitHub.
This page was last modified on 3 авг. 2023 г. by MDN contributors.