JavaScript. Объекты. Часть первая из двух

27.06.2021

Теги: JavaScriptWeb-разработкаКлассООПТеорияФункция

Создание объекта

Объекты используются для хранения коллекций различных значений и более сложных сущностей. Объект может быть создан с помощью фигурных скобок {…} с необязательным списком свойств. Пустой объект можно создать, используя один из двух вариантов синтаксиса:

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: 'Иванов',
    get fullName() {
        return `${this.name} ${this.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

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.