Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Основы программирования на JavaScript. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
12.08.2026
Размер:
1 Мб
Скачать

руются «по ссылке». Сам объект хранится где-то в памяти, а в переменной user лежит «ссылка» на эту область памяти. Когда переменная объекта копируется – копируется ссылка, сам же объект не дублируется.

Если представить объект как ящик, то переменная – это ключ к нему. Копирование переменной дублирует ключ, но не сам ящик. Например:

let user = { name: "Иван" };

let admin = user; // копируется ссылка

Теперь существуют две переменные, каждая из которых содержит ссылку на один и тот же объект. Для доступа к объекту и изменения его содержимого может использоваться любая из них:

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

admin.name = 'Петя'; // изменено по ссылке "admin" console.log (user.name); //'Петя', по ссылке "user"

Сравнение по ссылке

Операторы равенства = = и строгого равенства = = = для объектов работают одинаково. Два объекта равны только в том случае, если это один и тот же объект.

Например, две переменные ссылаются на один и тот же объект, поэтому они равны друг другу:

let a = {};

let b = a; // копирование по ссылке console.log ( a == b ); // true console.log ( a === b ); // true

В другом примере два разных объекта не равны, хотя оба пусты: let a = {};

let b = {}; // два независимых объекта console.log ( a == b ); // false

Для сравнений типа obj1 > obj2 или для сравнения с примитивом obj == 5 объекты преобразуются в примитивы. Сравнения такого рода обычно являются результатом ошибки программиста.

Клонирование и объединение объектов

В большинстве случаев достаточно копирования по ссылке. Создание независимого клона объекта возможно, но немного сложно, так как в JavaScript нет встроенного метода для этого.

Нужно создавать новый объект и повторять структуру дублируемого объекта, перебирая его свойства и копируя их. Например:

let user = { name: "Иван", age: 30

};

51

let clone = {}; // новый пустой объект

//скопируем все свойства user в него for (let key in user) {

clone[key] = user[key];

}

//теперь в переменной clone находится абсолютно независимый клон объекта

clone.name = "Петр"; // изменим в нем данные console.log ( user.name ); // в оригинальном объек-

те значение свойства `name` осталось прежним – Иван.

Для этих целей можно использовать метод Object.assign, который копирует свойства всех исходных объектов src1, ..., srcN в целевой объект dest и возвращает объект dest.

Синтаксис:

Object.assign(dest, [src1, src2, src3..., srcN]),

где первый аргумент dest – целевой объект, остальные аргументы src1,

..., srcN являются исходными объектами.

Например, можно объединить несколько объектов в один. Если принимающий объект (user) уже имеет свойство с таким именем, оно будет перезаписано:

let user = { name:

"Иван" };

let

permissions1 =

{

canView: true };

let

permissions2 =

{

canEdit: true };

//копируем все свойства объектов в user Object.assign(user, permissions1, permissions2,

{name: "Петр" });

//теперь user = { name: "Петр", canView: true, canEdit: true }

Вложенное клонирование

Cвойства объекта user могут быть не только примитивными значениями, но и ссылками на другие объекты:

let user = { name: "Иван", sizes: {

height: 182, width: 50

}

};

console.log ( user.sizes.height ); // 182

В таком случае при клонировании недостаточно просто скопировать clone.sizes = user.sizes, поскольку user.sizes – это объект, он будет скопирован по ссылке. Объекты clone и user в своих свойствах sizes будут ссылаться на один и тот же объект.

52

Чтобы исправить это, необходимо выполнить «глубокое клонирование». В цикле клонирования делать проверку, не является ли значение user[key] объектом, и, если это так, копировать и его структуру тоже.

Реализовать глубокое клонирование можно двумя способами: с помощью рекурсии или методом _.cloneDeep(obj) из JavaScriptбиблиотеки lodash.

Методы объекта

Объекты обычно создаются, чтобы представлять сущности реального мира, будь то пользователи, заказы и так далее:

// Объект пользователя let user = {

name: "Джон", age: 30

};

И так же, как и в реальном мире, пользователь может совершать действия: выбирать что-то из корзины покупок, авторизовываться, выходить из системы, оплачивать и т.п. Такие действия в JavaScript

представлены свойствами-функциями объекта, т.е. методами объекта.

Например, метод sayHi объекта user: let user = {

name: "Джон", age: 30

};

user = {

sayHi: function() { console.log("Привет");

}

};

Существует более короткий синтаксис для методов в литерале объекта:

// сокращенная запись user = {

sayHi() { // вместо "sayHi: function()" console.log("Привет");

}

};

Можно пропустить ключевое слово function и написать sayHi(). Необходимо отметить, что эти две записи не полностью эквивалентны, и в большинстве случаев сокращенный синтаксис предпочтителен.

53

Ключевое слово «this» в методах

Для доступа к информации внутри объекта метод может использовать ключевое слово this. Значение this – это объект «перед точкой», который использовался для вызова метода. Например:

let user = { name: "Джон", age: 30, sayHi() {

// this - это "текущий объект" console.log(this.name);

}

};

user.sayHi(); // Джон

Во время выполнения кода user.sayHi() значением this будет являться user (ссылка на объект user). Вызов без объекта: this == undefined.

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

Впрограммировании мы часто хотим взять что-то и расширить. Например, есть объект user со своими свойствами и методами и нужно создать объекты admin и guest как его слегка измененные варианты.

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

ВJavaScript объекты имеют специальное скрытое свойство [[Prototype]] (так оно названо в спецификации), которое либо равно null, либо ссылается на другой объект. Этот объект называется «прототип».

Когда нужно прочитать свойство из object, а оно отсутствует, JavaScript автоматически берет его из прототипа. В программировании такой механизм называется «прототипным наследованием». Многие интересные возможности языка и техники программирования основываются на нем.

Свойство [[Prototype]] является внутренним и скрытым, но есть много способов задать его. Одним из них является использование

__proto__, например: let animal = {

eats: true

};

let rabbit = { jumps: true

};

rabbit.__proto__ = animal;

54

Свойство __proto__ – исторически обусловленный геттер/сеттер для [[Prototype]]

Обратите внимание, что __proto__ – не то же самое, что [[Prototype]]. Это геттер/сеттер для него.

Он существует по историческим причинам, в современном язы-

ке его заменяют функции Object.getPrototypeOf/Object.setPrototypeOf,

которые также получают/устанавливают прототип.

По спецификации __proto__ должен поддерживаться только браузерами, но по факту все среды, включая серверную, поддерживают его. Например:

let animal = { eats: true

};

let rabbit = { jumps: true

};

rabbit.__proto__ = animal; // (*)

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

Здесь строка (*) устанавливает animal как прототип для rabbit. Затем, когда console.log пытается прочитать свойство

rabbit.eats (**), его нет в rabbit, поэтому JavaScript следует по ссылке [[Prototype]] и находит его в animal (смотрите снизу вверх).

Здесь мы можем сказать, что «animal является прототипом rabbit» или «rabbit прототипно наследует от animal». Так что если у animal много полезных свойств и методов, то они автоматически становятся доступными у rabbit. Такие свойства называются «унаследованными».

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

eats: true, walk() {

console.log("Animal walk");

}

};

let rabbit = { jumps: true, __proto__: animal

};

// walk взят из прототипа rabbit.walk(); // Animal walk

Метод автоматически берется из прототипа. Цепочка прототипов может быть длиннее:

55

let animal = { eats: true, walk() {

console.log("Animal walk");

}

};

let rabbit = { jumps: true, __proto__: animal

};

let longEar = { earLength: 10, __proto__: rabbit

};

// walk взят из цепочки прототипов longEar.walk(); // Animal walk console.log(longEar.jumps); // true (из rabbit)

Есть только два ограничения:

ссылки не могут идти по кругу. JavaScript выдаст ошибку, если мы попытаемся назначить __proto__ по кругу;

значение __proto__ может быть объектом или null. Другие типы игнорируются.

Это вполне очевидно, но все же: может быть только один [[Prototype]]. Объект не может наследоваться от двух других объектов.

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

В приведенном ниже примере мы присваиваем rabbit собственный метод walk:

let animal = { eats: true, walk() {

/*этот метод не будет использоваться в rabbit*/

}

};

let rabbit = { __proto__: animal

};

rabbit.walk = function() { console.log("Rabbit! Bounce-bounce!");

};

rabbit.walk(); // Rabbit! Bounce-bounce!

Теперь вызов rabbit.walk() находит метод непосредственно в объекте и выполняет его, не используя прототип. Свойства-

56

аксессоры – исключение, так как запись обрабатывается функциейсеттером. То есть это фактически вызов функции.

По этой причине admin.fullName работает корректно в приведенном ниже коде:

let user = { name: "John",

surname: "Smith",

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); // John Smith (*)

// срабатывает сеттер!

admin.fullName = "Alice Cooper"; // (**) console.log(admin.name); // Alice console.log(admin.surname); // Cooper

Здесь в строке (*) свойство admin.fullName имеет геттер в прототипе user, поэтому вызывается он. В строке (**) свойство также имеет сеттер в прототипе, который и будет вызван.

Значение «this»

В приведенном выше примере может возникнуть интересный вопрос: каково значение this внутри set fullName(value)? Куда записа-

ны свойства this.name и this.surname: в user или в admin?

Ответ прост: прототипы никак не влияют на this.

Неважно, где находится метод: в объекте или его прототипе. При вызове метода this – всегда объект перед точкой.

Таким образом, вызов сеттера admin.fullName= в качестве this использует admin, а не user.

Это на самом деле очень важная деталь, потому что у нас может быть большой объект со множеством методов, от которого можно наследовать. Затем наследующие объекты могут вызывать его методы, но они будут изменять свое состояние, а не состояние объектародителя.

Например, здесь animal представляет собой «хранилище методов» и rabbit использует его.

57

Вызов rabbit.sleep() устанавливает this.isSleeping для объекта

rabbit:

//методы animal let animal = {

walk() {

if (!this.isSleeping) { console.log(`I walk`);

}

}, sleep() {

this.isSleeping = true;

}

};

let rabbit = {

name: "White Rabbit", __proto__: animal

};

//модифицирует rabbit.isSleeping rabbit.sleep(); console.log(rabbit.isSleeping); // true

console.log(animal.isSleeping); // undefined (нет

такого свойства в прототипе)

Если бы у нас были другие объекты, такие как bird, snake и т.д., унаследованные от animal, они также получили бы доступ к методам animal. Но this при вызове каждого метода соответствует объекту (перед точкой), на котором происходит вызов, а не animal. Поэтому, когда мы записываем данные в this, они сохраняются в этих объектах. В результате методы являются общими, а состояние объекта – нет.

Цикл «for..in»

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

Например:

let animal = { eats: true

};

let rabbit = { jumps: true, __proto__: animal

};

//Object.keys возвращает только собственные ключи console.log(Object.keys(rabbit)); // jumps

//for..in проходит и по своим, и по унаследованным

ключам

for(let prop in rabbit) console.log(prop); // jumps, затем eats

58

Если унаследованные свойства нам не нужны, то мы можем отфильтровать их при помощи встроенного метода obj.hasOwnProperty(key): он возвращает true, если у obj есть собственное, не унаследованное, свойство с именем key.

Пример такой фильтрации: let animal = {

eats: true

};

let rabbit = { jumps: true, __proto__: animal

};

for(let prop in rabbit) {

let isOwn = rabbit.hasOwnProperty(prop); if (isOwn) {

console.log(`Our: ${prop}`); // Our: jumps

}else {

console.log(`Inherited: ${prop}`); // Inherit-

ed: eats

}

}

В этом примере цепочка наследования выглядит так: rabbit наследует от animal, который наследует от Object.prototype (так как animal – литеральный объект {...}, то это по умолчанию), а затем null.

Метод rabbit.hasOwnProperty не был явно определен, и, если посмотреть на цепочку прототипов, видно, что он берется из Object.prototype.hasOwnProperty, то есть он унаследован. Этот метод перечисляет все унаследованные свойства, но не появляется в цикле for..in в отличие от eats и jumps, так как у него внутренний флаг enumerable установлен в false, как и у других свойств Object.prototype. Почти все остальные методы, получающие ключи/значения, такие как Object.keys, Object.values и другие, игнорируют унаследованные свойства и учитывают только свойства самого объекта, не его прототипа.

Практические задания

Выполните задания по варианту из табл. 10.

Таблица 10. Варианты заданий

Номера заданий

Номера заданий

вар.

вар

 

 

1

1, 3-6 (вар. 1), 7-8 (вар. 3)

11

2, 3-6 (вар. 1), 7-8 (вар. 3)

2

2, 3-6 (вар. 2), 7-8 (вар. 1)

12

1, 3-6 (вар. 2), 7-8 (вар. 4)

3

2, 3-6 (вар. 1), 7-8 (вар. 1)

13

1, 3-6 (вар. 2), 7-8 (вар. 2)

59

 

 

 

Окончание табл. 10

4

1, 3-6 (вар. 2), 7-8 (вар. 2)

14

2, 3-6 (вар. 1), 7-8 (вар. 3)

5

1, 3-6 (вар. 2), 7-8 (вар. 1)

15

2, 3-6 (вар. 2), 7-8 (вар. 4)

6

2, 3-6 (вар. 1), 7-8 (вар. 1)

16

1, 3-6 (вар. 1), 7-8 (вар. 4)

7

2, 3-6 (вар. 2), 7-8 (вар. 2)

17

1, 3-6 (вар. 1), 7-8 (вар. 3)

8

1, 3-6 (вар. 1), 7-8 (вар. 3)

18

2, 3-6 (вар. 2), 7-8 (вар. 4)

9

1, 3-6 (вар. 1), 7-8 (вар. 1)

19

2, 3-6 (вар. 1), 7-8 (вар. 4)

10

2, 3-6 (вар. 2), 7-8 (вар. 2)

20

1, 3-6 (вар. 2), 7-8 (вар. 2)

1. Дан массив: let people = [

{'Marina', 60}, {'Lev', 10}, {'Anna', 27}, {'Roman', 38},

];

создать функцию, которая преобразует этот массив в объект, где ключом будет имя, а значением возраст;

преобразовать этот массив в объект, используя один из мето-

дов JavaScript.

2. Дан объект: let people = {

'Oleg': 26, 'Valentina': 7, 'Alex': 62, 'Arina': 41,

};

создать функцию, которая преобразует этот объект в массив;

преобразовать этот объект в массив, используя один из мето-

дов JavaScript.

3. Даны два объекта (табл. 11). Создать функцию, которая определит, равны ли все свойства этих объектов.

4. Усовершенствовать функцию из задания 3, чтобы она могла сравнить более сложные объекты с использованием рекурсии. К объектам из задания 3 добавить свойство designation (табл. 11).

5. Усовершенствовать функцию из задания 4, чтобы она могла сравнить более сложные объекты. К объектам из задания 4 добавить свойство options (табл. 11). Чтобы отличить объект от массива, можно использовать функцию Array.isArray(animals), которая возвращает true, если animals - массив.

6. На основе функции из задания 5 создать функцию, которая может целиком склонировать объект (т.е. создать его копию).

60

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]