Объекты

Объектно-ориентированное программирование (ООП) — это парадигма программирования, которая фокусируется на представлении задач в виде объектов с поведением (способностью выполнять действия) и состоянием (у них есть характеристики, которые различают разные объекты). Объекты JavaScript поддерживают эту парадигму. Пока мы будем думать об объектах как о сложных структурах данных, подобных массивам.

Что такое объекты?

Объекты представляют собой наборы пар ключ-значение: каждый объект в наборе имеет имя, называемое ключом, и связанное с ним значение. Напротив, массивы связывают значения с упорядоченными индексами. Другие языки предлагают структуры данных «ключ-значение» с похожими именами, такие как словари, ассоциативные массивы, карты и хэши. Некоторые разработчики могут использовать эти фразы для обозначения объектов JavaScript, хотя предпочтительнее использовать правильный термин: объекты. Ключами объекта являются строки или символы, но значения могут быть любыми, включая другие объекты. Используя синтаксис литерала объекта, мы можем построить объект:

let person = {
  name:    'Jane',
  age:     37,
  hobbies: ['photography', 'genealogy'],
};
/* That may also be written on a single line, which is useful in node: */
> let person = { name: 'Jane', age: 37, hobbies: ['photography', 'genealogy'] }

Этот код показывает объект с именем person, который имеет 3 пары ключ-значение:

  • Имя человека, строка, определяемая ключом name.
  • Возраст человека, число, определяемое клавишей age.
  • Список увлечений человека, массив строк, определяемый ключом hobbies.

Коллекция пар ключ-значение объекта ограничена фигурными скобками (). Каждая пара "ключ-значение" отделяется запятой (,), и каждая пара состоит из ключа, двоеточия (:) и значения. Запятая после последней пары необязательна. Несмотря на то, что ключи являются строками, мы обычно оставляем кавычки, когда ключ полностью состоит из буквенно-цифровых букв и знаков подчеркивания. Значения каждой пары могут быть любыми. Есть два способа получить конкретное значение в объекте: 1) запись через точку и 2) запись в квадратных скобках.

> person.name                 // dot notation
= 'Jane'
> person['age']               // bracket notation
= 37

Мы используем точечную нотацию, ставя точку (.) и имя ключа после переменной, которая ссылается на объект. Ключ записывается в виде строки в кавычках в квадратных скобках в скобочной нотации. Когда это возможно, большинство разработчиков используют запись через точку. Если у вас есть переменная, содержащая имя ключа, вы должны использовать запись в квадратных скобках:

> let key = 'name'
> person[key]
/* Let's populate the person object with some more key-value pairs: */
> person.height = '5 ft'
= '5 ft'
> person['gender'] = 'female'
= 'female'
> person
= { name: 'Jane', age: 37, hobbies: ['photography', 'genealogy'], height: '5 ft', gender: 'female' }
/* In this example, we add two new key-value pairs to the person object using both dot notation and bracket notation. You may use the delete keyword to remove something from an existing object: */
> delete person.age
= true
> delete person['gender']
= true
> delete person['hobbies']
= true
> person
= { name: 'Jane', height: '5 ft' }

delete удаляет пару ключ-значение из объекта и возвращает true, если только свойство не может быть удалено (например, если свойство не настраивается).

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

> const MyObj = { foo: "bar", qux: "xyz" }
> MyObj.qux = "hey there"
> MyObj.pi = 3.1415
> MyObj
= { foo: 'bar', qux: 'hey there', pi: 3.1415 }
> MyObj = {} // Uncaught TypeError: Assignment to constant variable.

Такое поведение, как и поведение массивов, может сбивать с толку, и тот же принцип «переменные, потому что это указатели» мы рассмотрим в следующей главе. Объявление const, по сути, запрещает изменять элемент, на который указывает const, но не запрещает изменять содержимое этой вещи. Таким образом, мы можем изменить свойство в константном объекте, но не в объекте, на который константа ссылается. Вы можете использовать Object.freeze с объектами, чтобы заморозить значения свойств объекта, так же как и с массивами:

> const MyObj = Object.freeze({ foo: "bar", qux: "xyz" })
> MyObj.qux = "hey there"
> MyObj
= { foo: 'bar', qux: 'xyz' }

Object.freeze, как и массивы, работает только на один уровень вглубь объекта. Если ваш объект имеет вложенные массивы или другие объекты, их значения все еще могут быть изменены, если они не заморожены аналогичным образом.

Объекты против примитивов

Возможно, вы помните, что в JavaScript есть два типа данных: примитивы и объекты. Строки, целые числа, логические значения, null и undefined, begin и символы являются примитивными. Примитивные типы являются наиболее фундаментальными типами в JavaScript. Среди объектов включают, но не ограничиваются, следующее:

  • Простые объекты
  • Массивы
  • Даты
  • Функции

Вещи — это сложные значения, построенные из примитивных значений или других объектов. Например, объект массива (помните, что массивы — это объекты) имеет свойство длины, которое содержит число: базовое значение. Объекты часто (но не всегда) изменяемы: вы можете добавлять, удалять и изменять значения многих составляющих их компонентов. Примитивные значения всегда неизменяемы; нет частей, которые могут быть изменены. Такие значения считаются атомарными; их нельзя разделить. Если переменная содержит примитивное значение, единственное, что вы можете сделать с ней, — это использовать ее в выражении или переназначить: присвоить ей совершенно новое значение. Все действия над примитивными значениями приводят к созданию новых значений. Даже что-то такое простое, как 0 + 0, дает новое значение 0.

> let number = 20
> let newNumber = number + 1
> newNumber
= 21
> number
= 20
> let object = { a: 1, b: 2, c: 3 }
> object.c = object.c + 1
= 4
> object
= { a: 1, b: 2, c: 4 }

Приведенный выше пример демонстрирует различие между неизменяемым примитивным значением и изменяемым объектом. Операция + в строке 2 дает новое значение (21), присвоенное newNumber; исходное значение числа (20) остается неизменным. С другой стороны, запись нового значения свойства c объекта в строке 10 изменяет значение объекта. Однако свойство c теперь содержит новое целое число, как и в строке 2.

Какие вещи не являются объектами или примитивами?

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

  • переменные и другие идентификаторы, такие как имена функций
  • операторы, такие как if, return, try, while и break
  • ключевые слова, такие как new, function, let, const и class
  • Комментарии
  • все остальное, что не является ни данными, ни функцией

Прототипы

Способность объектов JavaScript наследовать от других объектов — интригующая и полезная функция. Когда объект наследуется от другого объекта, мы называем b прототипом a. Практический эффект заключается в том, что a теперь имеет доступ к атрибутам, объявленным в b, даже если эти характеристики не указаны. Статическая функция Object.create предоставляет простой способ создания нового объекта, который наследуется от существующего:

let bob = { name: 'Bob', age: 22 };
let studentBob = Object.create(bob);
studentBob.year = 'Senior';
console.log(studentBob.name); // => 'Bob'

Object.create создает новый объект и устанавливает его прототип на объект, переданный в качестве аргумента. В нашем примере мы создаем новый объект с именем studentBob, который наследуется от bob. Другими словами, он устанавливает связь наследования между studentBob, дочерним объектом, и bob, родительским объектом. Мы можем использовать свойство name, даже если studentBob не объявляет его явно, поскольку он наследует его от bob. В JavaScript одним из методов использования наследования является Object.create.

Итерация

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

Цикл for/in

Цикл for/in работает аналогично обычному циклу for. Поскольку нет инициализатора, завершающего условия или предложения приращения, грамматика и семантика проще для понимания. Вместо этого цикл перебирает все ключи в объекте. Он выделяет ключ переменной на каждой итерации, которую вы затем используете для извлечения данных объекта. Увидеть концепцию в действии обычно полезно:

let person = {
  name: 'Bob',
  age: 30,
  height: '6 ft'
};
for (let prop in person) {
  console.log(person[prop]);
}                             // => Bob
                              //    30
                              //    6 ft

Цикл for/in используется для перебора объекта person в предыдущем примере. Строка 7 объявляет переменную prop, которая получает ключ от объекта на каждой итерации, пока у объекта не закончатся пары ключ-значение. Мы используем prop для извлечения и регистрации соответствующего значения в теле цикла. Внутри цикла мы используем обозначение скобок. Поскольку prop — это переменная, которая содержит имя свойства, мы не можем использовать здесь запись через точку. Название реквизита не соответствует реальному имени объекта. Этот контраст едва уловим, так что найдите минутку, чтобы рассмотреть его.

Например, prop будет установлен на age во второй итерации цикла; однако, если мы попытаемся использовать person.prop, он вернет undefined, потому что prop не является одним из имен свойств в объекте person. Нам нужен person.age, но мы не можем его использовать, потому что для каждой итерации потребуется другое имя свойства. Когда мы набираем person[prop], вычисляется переменная prop. В результате person[prop] оценивается как person[‘age’] и возвращается значение запрошенного свойства. Одним из преимуществ — или недостатков, в зависимости от вашей точки зрения — for/in является то, что он также перебирает атрибуты прототипов объекта:

let obj1 = { a: 1, b: 2 }
let obj2 = Object.create(obj1);
obj2.c = 3;
obj2.d = 4;
for (let prop in obj2) {
  console.log(obj2[prop]);
}         // => 3
          //    4
          //    1
          //    2

Первые два элемента, возвращаемые приведенным выше кодом, — это «собственные свойства» obj2, за которыми следуют свойства объекта-прототипа (obj1). Когда вы хотите ограничить повторение атрибутами объекта, т. е. свойствами, которые он указал для себя, а не свойствами, которые он унаследовал, такое поведение нежелательно. Чтобы обойти это, мы можем использовать функцию hasOwnProperty. Он принимает имя свойства и возвращает true, если это имя одного из свойств вызывающего объекта, и false в противном случае.

let obj1 = { a: 1, b: 2 }
let obj2 = Object.create(obj1);
obj2.c = 3;
obj2.d = 4;
for (let prop in obj2) {
  if (obj2.hasOwnProperty(prop)) {
    console.log(obj2[prop]);
  }
} // => 3
  //    4

Object.keys

Статическая функция Object.keys возвращает массив ключей объекта. Вы можете перебирать этот массив, используя любой метод, связанный с массивом. В качестве примера:

let person = {
  name: 'Bob',
  age: 30,
  height: '6 ft'
};
let personKeys = Object.keys(person);
console.log(personKeys);          // => ['name', 'age', 'height']
personKeys.forEach(key => {
  console.log(person[key])
});                               // => Bob
                                  //    30
                                  //    6 ft

Важно отметить, что Object.keys возвращает только ключи объекта и не содержит никаких ключей от объектов-прототипов.

Порядок свойств объекта

До ES6 более старые версии стандарта ECMAScript не обеспечивали порядок итерации для ключей свойств объекта. Несколько движков JavaScript использовали этот недостаток уверенности. Вы не можете полагаться на какую-то определенную последовательность итераций в предыдущих версиях JavaScript. Даже внутри одного и того же движка выполнение нескольких программ может привести к разным результатам. Порядок итерации ключей свойств объекта гарантирован в современных версиях стандарта (ES6+). Вы можете предположить, что порядок итерации — это порядок, в котором ключи добавляются к объекту. Однако предположим, что у вас есть какие-либо символьные ключи (которые мы не обсуждаем в этой книге) или ключи, которые выглядят как неотрицательные целые числа («0», «1», «2» и т. д.). В этом случае JavaScript сначала сгруппирует неотрицательные целые числа, затем другие строковые ключи и, наконец, символические ключи. Чтобы было ясно, вы должны использовать порядок итерации только тогда, когда знаете, что все ключи будут в алфавитном порядке.

Общие операции

В отличие от массивов JavaScript (которые, как вы помните, являются объектами), большинство объектов JavaScript не содержат многих методов, которые вы можете использовать ежедневно. Большинство операций с объектами включают в себя повторение их атрибутов или значений. Вы часто будете использовать методы, которые извлекают ключи или значения объекта, а затем перебирают результирующий массив. Мы видели пример этого, когда использовали Object.keys для извлечения ключей объекта в виде массива, а затем выполняли итерацию по массиву. Давайте рассмотрим некоторые дополнительные важные методы:

Объект.значения

/* This static function saves the values of an object's own properties to an array: */
let person = { name: 'Bob', age: 30, height: '6ft' };
let personValues = Object.values(person);
console.log(personValues); // => [ 'Bob', 30, '6ft' ]
/* Be cautious: the order of the values in the returned array cannot be predicted. */

Объект.записи

/* While Object.keys and Object.values yield an object's keys and values, respectively, Object.entries produces an array of nested arrays. Each nested array contains two elements: one of the object's keys and the value that corresponds to it: */
let person = { name: 'Bob', age: 30, height: '6ft' };
console.log(Object.entries(person)); // => [[ 'name', 'Bob' ], [ 'age', 30 ], [ 'height', '6ft' ]]

Объект.назначить

/* You may want to merge two or more objects, i.e. combine the key-value pairs into a single object, on occasion. This capability is provided by the Object.assign static method: */
> let objA = { a: 'foo' }
= undefined
> let objB = { b: 'bar' }
= undefined
> Object.assign(objA, objB)
= { a: 'foo', b: 'bar' }
/* The first object is mutated using Object.assign. In the above example, the characteristics of the objB object are added to the objA object, permanently modifying objA: */
> objA
= { a: 'foo', b: 'bar' }
> objB
= { b: 'bar' }
/* It should be noted that objB has not been altered. If you need to create a new object, use an empty object as the first input to Object.assign. It should be noted that Object.assign can accept more than two arguments: */
> objA = { a: 'foo' }
= undefined
> objB = { b: 'bar' }
= undefined
> Object.assign({}, objA, objB)
= { a: 'foo', b: 'bar' }
> objA
= { a: 'foo' }
> objB
= { b: 'bar' }
/* This code does not change objA or objB and instead returns a whole new object. */

Объекты против массивов

В этой и предыдущей главе были рассмотрены две основные структуры данных: объекты и массивы. Когда вы рассматриваете все многочисленные методы представления данных с помощью кода, это может быть обескураживающе. Но не отчаивайтесь. Объекты и массивы будут использоваться часто. Чем больше вы их используете, тем больше вы откроете для себя. Нелегко выучить все, поэтому сначала сосредоточьтесь на основах, а затем развивайтесь. Принимая решение о том, использовать ли объект или массив для хранения данных, учтите следующее:

  • Имеют ли отдельные значения имена или метки? Если да, используйте объект. Массива должно быть достаточно, если данные не имеют естественной метки.
  • Имеет ли значение порядок? Если да, используйте массив.
  • Нужна ли мне структура стек или очередь? Массивы хорошо имитируют простые стеки «последний вошел — первый вышел» и очереди «первый пришел — первый вышел».