JavaScript. Объекты. Часть первая из двух
27.06.2021
Теги: JavaScript • Web-разработка • Класс • ООП • Теория • Функция
Создание объекта
Объекты используются для хранения коллекций различных значений и более сложных сущностей. Объект может быть создан с помощью фигурных скобок {…}
с необязательным списком свойств. Пустой объект можно создать, используя один из двух вариантов синтаксиса:
let user = new Object(); // синтаксис «конструктор объекта» let user = {}; // синтаксис «литерал объекта»
Свойства объекта
При использовании литерального синтаксиса {…}
можно сразу поместить в объект несколько свойств в виде пар «ключ: значение»:
let user = { name: 'Сергей', age: 30 };
Для обращения к свойствам используется запись «через точку»:
console.log(user.name); console.log(user.age);
Для удаления свойства можно использовать оператор delete
:
delete user.age;
Имя свойства может состоять из нескольких слов, но тогда оно должно быть в кавычках:
let user = { name: 'Сергей', age: 30, 'год рождения': 1991, };
Для доступа к таким свойствам нужно использовать квадратные скобки:
let birthYear = user['год рождения'];
Квадратные скобки также позволяют обратиться к свойству, имя которого может быть результатом выражения:
let key = 'год рождения'; let birthYear = user[key];
Запись «через точку» такого не позволяет:
let key = 'name'; console.log(user.key); // undefined
Квадратные скобки можно использовать для создания вычисляемого свойства:
let one = 'one', two = 'two'; let sum = { [one + ' plus ' + two]: 'three' } console.log(sum); // {"one plus two": "three"}
При обращении к свойству, которого нет, возвращается undefined
. Это позволяет просто проверить существование свойства:
let user = {}; console.log(user.name === undefined); // true
Также существует специальный оператор in
для проверки существования свойства в объекте:
let user = { name: 'Сергей' }; console.log("name" in user); // true console.log("age" in user); // false
В большинстве случаев прекрасно сработает сравнение с undefined
. Но есть особый случай, когда оно не подходит, и нужно использовать in
:
let obj = { test: undefined }; console.log(obj.test === undefined); // true console.log("test" in obj); // true
undefined
обычно явно не присваивается. Для «неизвестных» или «пустых» свойств предпочтительно использовать значение null
.
Цикл for…in
Для перебора всех свойств объекта используется цикл for…in
:
let user = { name: 'Сергей', age: 30, isAdmin: true }; for (let key in user) { console.log(key); // name, age, isAdmin console.log(user[key]); // Сергей, 30, true }
Дескрипторы свойств
Помимо значения value
, свойства объекта имеют три специальных атрибута (так называемые «флаги»):
writable
— еслиtrue
, свойство можно изменить, иначе оно только для чтенияenumerable
— еслиtrue
, свойство перечисляется в циклах, в противном случае циклы его игнорируютconfigurable
— еслиtrue
, свойство можно удалить, а эти атрибуты можно изменять, иначе этого делать нельзя
При создании свойства «обычным способом», все они имеют значение true
.
Метод Object.getOwnPropertyDescriptor
позволяет получить полную информацию о свойстве.
let user = { name: 'Сергей' }; let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); console.log(descriptor);
{ "value": "Сергей", "writable": true, "enumerable": true, "configurable": true }
Чтобы изменить флаги, можно использовать метод Object.defineProperty
.
let user = {}; Object.defineProperty(user, 'name', { value: 'Сергей' }); let descriptor = Object.getOwnPropertyDescriptor(user, 'name'); console.log(descriptor);
{ "value": "Сергей", "writable": false, "enumerable": false, "configurable": false }
Для нового свойства необходимо явно указывать все флаги, для которых значение должно быть true
, потому что значение по умолчанию false
.
Геттеры и сеттеры
Все свойства объета, которые мы использовали до текущего момента, были свойствами-данными. Но есть еще свойства-аксессоры (accessor properties). По своей сути это функции, которые используются для присвоения и получения значения, но во внешнем коде они выглядят как обычные свойства объекта. При литеральном объявлении объекта они обозначаются get
и set
.
Например, у нас есть объект user
со свойствами name
и surname
:
let user = { name: 'Сергей', surname: 'Иванов' };
А теперь добавим свойство объекта fullName
для полного имени:
let user = { name: 'Сергей', surname: 'Иванов', get fullName() { return `${this.name} ${this.surname}`; } }; console.log(user.fullName);
Сейчас свойству fullName
нельзя присвоить значение, исправим это:
let user = { name: 'Сергей', surname: 'Иванов', get fullName() { return `${this.name} ${this.surname}`; }, set fullName(value) { [this.name, this.surname] = value.split(' '); } }; user.fullName = 'Андрей Петров'; console.log(user.name); // Андрей console.log(user.surname); // Петров
Дескрипторы свойств-аксессоров отличаются от «обычных» свойств-данных. Свойства-аксессоры не имеют value
и writable
, но взамен предлагают функции get
и set
. То есть, дескриптор аксессора может иметь:
get
— функция без аргументов, которая сработает при чтении свойстваset
— функция, принимающая один аргумент, вызываемая при присвоении свойстваenumerable
— то же самое, что и для свойств-данныхconfigurable
— то же самое, что и для свойств-данных
let user = { name: 'Сергей', surname: 'Иванов' }; Object.defineProperty(user, 'fullName', { get() { return `${this.name} ${this.surname}`; }, set(value) { [this.name, this.surname] = value.split(' '); } }); console.log(user.fullName); // Сергей Иванов for(let key in user) { console.log(key); // name, surname }
Методы объекта
Объекты обычно создаются, чтобы представлять сущности реального мира, будь то пользователи, товары или заказы:
let user = { name: 'Сергей', age: 30 };
И так же, как и в реальном мире, пользователь может совершать действия. Такие действия представлены свойствами-функциями объекта:
let user = { name: 'Сергей', age: 30, hello: function() { console.log('Привет!'); } }; user.hello(); // Привет!
Существует более короткий синтаксис для методов в литерале объекта:
let user = { name: 'Сергей', age: 30, hello() { console.log('Привет!'); } };
Для доступа к информации внутри объекта метод может использовать ключевое слово this
:
let user = { name: 'Сергей', age: 30, hello() { console.log(`Привет, меня зовут ${this.name}`); } }; user.hello(); // Привет, меня зовут Сергей
Во время выполнения этого кода user.hello()
значением this
будет являться user
.
Прототипное наследование
Допустим, у нас есть объект user
со своими свойствами и методами, и нужно создать объекты admin
и guest
как его слегка изменённые варианты. Хотелось бы повторно использовать то, что есть у объекта user
, не копировать/переопределять его методы, а просто создать новый объект на его основе.
В JavaScript объекты имеют специальное скрытое свойство [[Prototype]]
, которое либо равно null
, либо ссылается на другой объект — прототип. Когда мы хотим прочитать свойство объекта, а оно отсутствует, JavaScript автоматически берёт его из прототипа.Такой механизм называется «прототипным наследованием».
Подробнее смотрите во второй части.
Функции-конструкторы
Обычный синтаксис {…}
позволяет создать только один объект. Но зачастую нужно создать множество однотипных объектов, таких как пользователи, элементы меню и так далее. Это можно сделать при помощи функции-конструктора и оператора new
.
Функции-конструкторы являются обычными функциями. Но есть два соглашения:
- Имя функции-конструктора должно начинаться с большой буквы
- Функция-конструктор должна вызываться при помощи оператора
new
function User(name) { this.name = name; this.isAdmin = false; } let user = new User('Сергей'); console.log(user.name); // Сергей console.log(user.isAdmin); // false
Когда функция вызывается как new User(…)
, происходит следующее:
- Создаётся новый пустой объект, и он присваивается
this
- Выполняется код функции. Обычно он модифицирует
this
, добавляя туда новые свойства - Возвращается значение
this
Другими словами, вызов new User(…)
делает примерно вот что:
function User(name) { // this = {}; (неявно) // добавляет свойства к this this.name = name; this.isAdmin = false; // return this; (неявно) }
new.target
внутри функции, можно проверить, вызвана ли функция при помощи оператора new
или без него. В случае, если функция вызвана при помощи new
, то в new.target
будет сама функция, в противном случае undefined
.
Обычно конструкторы ничего не возвращают явно. Их задача — записать все необходимое в this
, который в итоге станет результатом. Но если return
всё же есть, то применяется простое правило:
- При вызове
return
с объектом, будет возвращён этот объект, а неthis
- При вызове
return
с примитивным значением, примитивное значение будет отброшено.
В this
можно добавлять не только свойства, но и методы:
function User(name) { this.name = name; this.isAdmin = false; this.hello = function() { console.log(`Привет, меня зовут ${this.name}`); } } let user = new User('Сергей'); user.hello(); // Привет, меня зовут Сергей
Классы и прототипы
Класс в JavaScript представляет собой набор объектов, которые наследуют методы от того же самого объекта прототипа. Если мы определим объект прототипа и затем используем функцию Object.create()
для создания унаследованных от него объектов, то тем самым определим класс.
// Фабричная функция, которая возвращает новый объект диапазона function range(start, stop) { // Создаем новый объект диапазона, который наследует методы прототипа let r = Object.create(range.methods); r.start = start; r.stop = stop; return r; } // Методы прототипа, которые наследуются всеми объектами диапазона range.methods = { includes(x) { return this.start <= x && x <= this.stop; }, *[Symbol.iterator]() { for (let x = Math.ceil(this.start); x <= this.stop; x++) { yield x; } }, toString() { return '(' + this.start + '...' + this.stop + ')'; } }; let r = range(1, 3); console.log(r.includes(2)); // true console.log(r.toString()); // (1...3) console.log([...r]); // [1, 2, 3]
Свойство prototype функций
JavaScript использовал прототипное наследование с момента своего появления. Это одна из основных особенностей языка. Но раньше, в старые времена, прямого доступа к прототипу объекта не было. Надёжно работало только свойство prototype
функции-конструктора. Использование свойства prototype
можно встретить в старом коде, когда еще не было поддержки ключевого слова class
.
Все функции по умолчанию получают открытое, не перечисляемое свойство с именем prototype
, которое указывает на произвольный объект. При создании нового объекта при помощи new Foo()
, свойство prototype
функции-конструктора применяется в качестве прототипа нового объекта. По умолчанию Foo.prototype
имеет одно-единственное свойство, которое ссылается на объект функции Foo
.
function Foo() {} Foo.prototype = { constructor: Foo };
В коде выше объект Foo.prototype
создан вручную вручную, но ровно такой же — был бы создан автоматически. Свойство constructor
легко потерять — достаточно присвоить Foo.prototype
новое значение. Позаботиться о сохранности конструктора надо самостоятельно, если планируется как-то его использовать.
function Foo() {} Foo.prototype = { blabla: true, constructor: Foo };
function Foo() {} Foo.prototype.blabla = true;
Классы и конструкторы
Теперь, зная о существовании свойства prototype
у функций, мы можем определить класс с использованием этого свойства и функции-конструктора. Такой способ создания классов можно встретить в старом коде, когда еще не было поддержки ключевого слова class
.
// Функция конструктора, которая инициализирует новые объекты Range function Range(start, stop) { this.start = start; this.stop = stop; } // Методы прототипа, которые наследуются всеми объектами Range Range.prototype = { includes(x) { return this.start <= x && x <= this.stop; }, *[Symbol.iterator]() { for (let x = Math.ceil(this.start); x <= this.stop; x++) { yield x; } }, toString() { return '(' + this.start + '...' + this.stop + ')'; } }; let r = new Range(1,3); console.log(r.includes(2)); // true console.log(r.toString()); // (1...3) console.log([...r]); // [1, 2, 3]
Поиск: JavaScript • Web-разработка • ООП • Функция • Класс • Конструктор • Теория • Объект • Object • Прототип • Prototype