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

30.06.2021

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

Прототипное наследование

Допустим, у нас есть объект user со своими свойствами и методами, и нужно создать объекты admin и guest как его слегка изменённые варианты. Хотелось бы повторно использовать то, что есть у объекта user, не копировать/переопределять его методы, а просто создать новый объект на его основе.

В JavaScript объекты имеют специальное скрытое свойство [[Prototype]], которое либо равно null, либо ссылается на другой объект — прототип. Когда мы хотим прочитать свойство объекта, а оно отсутствует, JavaScript автоматически берёт его из прототипа.Такой механизм называется «прототипным наследованием».

Есть много способов задать [[Prototype]], одним из них является использование __proto__:

let animal = {
    eats: true
};
let rabbit = {
    jumps: true
};

rabbit.__proto__ = animal;

Обратите внимание, что __proto__ — не то же самое, что [[Prototype]] — это геттер/сеттер для него. Он существует по историческим причинам, в современном языке его заменяют функции Object.getPrototypeOf и Object.setPrototypeOf, которые также получают и устанавливают прототип.

Если мы ищем свойство в rabbit, а оно отсутствует, JavaScript автоматически берёт его из animal:

let animal = {
    eats: true
};
let rabbit = {
    jumps: true
};

rabbit.__proto__ = animal;

// теперь мы можем найти оба свойства в rabbit
console.log(rabbit.eats); // true
console.log(rabbit.jumps); // true

Если у нас есть метод в animal, он может быть вызван на rabbit:

let animal = {
    eats: true,
    walk() {
        console.log('Animal walk');
    }
};

let rabbit = {
    __proto__: animal
    jumps: true,
};

rabbit.walk(); // Animal walk

Прототип используется только для чтения свойств. Операции записи/удаления работают напрямую с объектом. В приведённом ниже примере мы присваиваем rabbit собственный метод walk:

let animal = {
    eats: true,
    walk() {
        console.log('Animal walk');
    }
};

let rabbit = {
    __proto__: animal,
    jumps: true,
    walk() {
        console.log('Rubbit walk');
    }
};

rabbit.walk(); // Rubbit walk

Свойства-аксессоры — исключение, так как запись в него обрабатывается функцией-сеттером. То есть, это, фактически, вызов функции. По этой причине admin.fullName работает корректно в приведённом ниже коде:

let user = {
    name: 'Unknown',
    surname: 'Unknown',

    set fullName(value) {
        [this.name, this.surname] = value.split(' ');
    },

    get fullName() {
        return `${this.name} ${this.surname}`;
    }
};

let admin = {
    __proto__: user,
    isAdmin: true
};

console.log(admin.fullName); // Unknown Unknown

admin.fullName = 'Сергей Иванов';
console.log(admin.name); // Сергей
console.log(admin.surname); // Иванов
В приведённом выше примере может возникнуть интересный вопрос — каково значение this? Ответ прост — прототипы никак не влияют на this. Неважно, где находится метод — в объекте или его прототипе. При вызове метода this — всегда объект перед точкой.

Цикл for…in

Цикл for…in проходит не только по собственным, но и по унаследованным свойствам объекта:

let animal = {
    eats: true
};

let rabbit = {
    jumps: true,
    __proto__: animal
};

for(let key in rabbit) {
    let value = rabbit[key]
    console.log('key =', key, 'value =', value);
}
key = jumps value = true
key = eats value = true

Оператор in

Оператор in возвращает true как для собственных, так и для унаследованных свойств объекта:

let animal = {
    eats: true
};

let rabbit = {
    jumps: true,
    __proto__: animal
};

console.log('eats' in rabbit); // true
console.log('jumps' in rabbit); // true

Иногда хочется посмотреть, что находится именно в самом объекте, а не в прототипе:

let animal = {
    eats: true
};

let rabbit = {
    jumps: true,
    __proto__: animal
};

console.log(rabbit.hasOwnProperty('jumps')); // true — jumps принадлежит rabbit
console.log(rabbit.hasOwnProperty('eats')); // false — eats не принадлежит rabbit

Примеси

Допустим, у нас есть класс User, который реализует пользователей, и класс EventEmitter, реализующий события. Мы хотели бы добавить функциональность класса EventEmitter к User, чтобы пользователи могли легко генерировать события. Для таких случаев существуют «примеси».

Простейший способ реализовать примесь в JavaScript — это создать объект с полезными методами, которые затем могут быть легко добавлены в прототип любого класса.

// объект примеси
let helloByeMixin = {
    hello() {
        console.log(`Привет, ${this.name}`);
    },
    bye() {
        console.log(`Пока, ${this.name}`);
    }
};

// класс пользователя
class User {
    constructor(name) {
        this.name = name;
    }
}

// копируем методы
Object.assign(User.prototype, helloByeMixin);

// теперь User может сказать Привет
new User('Сергей').hello(); // Привет, Сергей!

Классы (современный синтаксис)

Мы уже знаем, как создать класс через функцию-конструктор, теперь посмотрим, какие возможности для объектно-ориентированного программирования предоставляет современный JavaScript при использовании ключевого слова class.

class User {

    constructor(name) {
        this.name = name;
    }

    hello() {
        console.log(`Привет, ${this.name}`);
    }

    bye() {
        console.log(`Пока, ${this.name}`);
    }

}

Теперь можно использовать вызов new User() для создания нового объекта со всеми перечисленными методами. При этом автоматически вызывается метод constructor(), в нём можно инициализировать объект.

let user = new User('Сергей');
user.hello(); // Привет, Сергей
Синтаксис классов отличается от литералов объектов — внутри классов запятые не требуются.

Что такое класс в JavaScript? Класс — это разновидность функции. Давайте разберемся, что делает конструкция class User {…} на самом деле.

  1. Создаёт функцию с именем User, тело берёт из constructor
  2. Сохраняет методы hello и bye в User.prototype

При вызове метода объекта user он будет взят из прототипа User.prototype. Таким образом, объекты user имеют доступ к методам класса.

Иногда говорят, что class — это просто «синтаксический сахар» в JavaScript, потому что мы можем сделать всё то же самое без использования class.

function User(name) {
    this.name = name;
}

User.prototype = {
    hello() {
        console.log(`Привет, ${this.name}`);
    },
    bye() {
        console.log(`Пока, ${this.name}`);
    }
}

Это не совсем так. Методы класса являются неперечисляемыми. Определение класса устанавливает флаг enumerable в false для всех методов в prototype. И это хорошо, так как если мы проходимся циклом for…in по объекту, то обычно мы не хотим при этом получать методы класса. И классы всегда используют use strict. Весь код внутри класса автоматически находится в строгом режиме.

Как и в литеральных объектах, в классах можно объявлять вычисляемые свойства, геттеры/сеттеры и т.д.

class User {

    constructor(name) {
        // вызывает сеттер
        this.name = name;
    }

    get name() {
        return this._name;
    }

    set name(value) {
        if (value.length < 4) {
            console.log('Имя слишком короткое');
            return;
        }
        this._name = value;
    }

}
let user = new User('Сергей');
user.hello(); // Привет, Сергей!

user = new User(''); // Имя слишком короткое

При объявлении класса геттеры/сеттеры создаются на User.prototype вот так:

Object.defineProperties(User.prototype, {
    name: {
        get() {
            return this._name
        },
        set(name) {
            // .....
        }
    }
});

Пример с вычисляемым свойством в скобках [...]:

class User {
    ['hel' + 'lo']() {
        console.log('Привет');
    }
}

new User().hello();

В приведённом выше примере у класса User были только методы. Давайте добавим свойство:

class User {
    name = 'Аноним';

    constructor(name) {
        if (name) {
            this.name = name;
        }
    }

    hello() {
        console.log(`Привет, ${this.name}!`);
    }
}

new User().hello();
new User('Сергей').hello();
Привет, Аноним!
Привет, Сергей!

Свойство name не устанавливается в User.prototype. Вместо этого оно создаётся оператором new перед запуском конструктора, это именно свойство объекта. Свойства классов добавлены в язык недавно — так что для старых браузеров может понадобиться полифил.

Технически, класс MyClass является функцией, тело которой задается в constructor, в то время как методы, геттеры и сеттеры записываются в MyClass.prototype. Свойство класса не записывается в MyClass.prototype — это именно свойство объекта, который возвращает вызов new MyClass().

Методы Object

Метод Object.keys() возвращает массив собственных перечисляемых свойств объекта:

let user = {
    name: 'Сергей',
    surname: 'Иванов'
};

let keys = Object.keys(user); // [name, surname]
keys.forEach(key => {
    console.log('key =', key, 'value =', user[key]);
});
key = name value = Сергей
key = surname value = Иванов

Метод Object.values() возвращает массив значений собственных перечисляемых свойств объекта:

let user = {
    name: 'Сергей',
    surname: 'Иванов'
};

let values = Object.values(user); // [Сергей, Иванов]
values.forEach(value => {
    console.log('value =', value);
});
value = Сергей
value = Иванов

Метод Object.entries() метод возвращает массив собственных перечисляемых свойств и значений:

let user = {
    name: 'Сергей',
    surname: 'Иванов'
};

let entries = Object.entries(user); // [["name", "Сергей"], ["surname", "Иванов"]]
entries.forEach(([key, value]) => {
    console.log('key = ', key, 'value =', value);
});
for (let [key, value] of entries) {
    console.log('key = ', key, 'value =', value);
}
key =  name value = Сергей
key =  surname value = Иванов

Метод Object.create() создаёт новый объект с указанным прототипом и свойствами:

let animal = {
    eats: true
};

let rabbit = Object.create(animal, {
    jumps: {
        writable: true,
        configurable: true,
        value: true
    }
});
console.log(rabbit.__proto__); // {eats: true}
// создание объекта...
let obj = {};
// ...эквивалентно этому
let oobj = Object.create(Object.prototype);

Метод Object.getPrototypeOf() возвращает прототип объекта:

let animal = {
    'eats': true
};
let rabbit = {
    __proto__: animal,
    'jumps': true
};

let proto = Object.getPrototypeOf(rabbit);
console.log(proto); // {eats: true}

Метод Object.setPrototypeOf() устанавливает прототип объекта:

let animal = {
    'eats': true
};
let rabbit = {
    'jumps': true
};

Object.setPrototypeOf(rabbit, animal);
console.log(rabbit.__proto__); // {eats: true}

Метод Object.assign() копирует свойства из одного объекта (или объектов) в другой:

let user = {name: 'Иван'}

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

Object.assign(user, permissions1, permissions2);
console.log(user); // {name: "Иван", canView: true, canEdit: true}

Метод Object.getOwnPropertyNames() возвращает массив со всеми свойствами (независимо от того, перечисляемые они или нет), найденными непосредственно в переданном объекте.

Метод Object.getOwnPropertyDescriptor() возвращает дескриптор свойства для собственного свойства (которое находится непосредственно в объекте) переданного объекта.

Метод Object.getOwnPropertyDescriptors() возвращает все дескрипторы собственных свойств (которые находятся непосредственно в объекте) переданного объекта.

Поиск: 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.