[Перевод] Разбираемся с объектами в JavaScript

twemrbhsfbxmrw7_htw8o4mfzny.jpeg


Объекты — одно из основных понятий в JavaScript. Когда я только приступил к их изучению, они показались мне довольно простыми: всего лишь пары ключей и значений, как и описывалось в теории.

Лишь спустя некоторое время я начал понимать, что тема гораздо сложнее, чем я полагал. И тогда я стал изучать информацию из разных источников. Некоторые из них давали хорошее представление о предмете, но увидеть всю картину целиком я смог не сразу.

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

Итак, давайте начнем с основ.

Объект


Объект в JavaScript — это просто набор свойств, каждое из которые представляет собой пару ключ-значение. Обратиться к ключам можно с помощью точечного (obj.a) или скобочного обозначения (obj['a']).

Помните, что скобки следует использовать, если ключ:

  • не является допустимым JavaScript-идентификатором (в нем есть пробел, тире, начинается с цифры…)
  • является переменной.


Одно из свойств, которое объекты в JS получают при создании, называется Prototype, и это очень важное понятие.

Прототип


У каждого объекта в JavaScript есть внутреннее свойство под названием Prototype. В большинстве браузеров вы можете обратиться к нему по обозначению __proto__.

Prototype — это способ обеспечить наследование свойств в JavaScript. Так можно делится функциональностью без дублирования кода в памяти. Способ работает за счет создания связи между двумя объектами.

Проще говоря, Prototype создает указатель с одного объекта на другой.

Цепочка прототипов

Каждый раз, когда JS ищет свойство в объекте и не находит его непосредственно у самого объекта, он проверяет наличие свойства в объекте-прототипе. Если свойства нет и в нем, то JS продолжит поиск в прототипе связанного объекта. Так будет продолжаться до тех пор, пока JS не найдет подходящее свойство или не достигнет конца цепочки.

Давайте рассмотрим пример:

var cons = function () {
   this.a = 1;
   this.b = 2;
}
var obj = new cons(); 
 
cons.prototype.b = 3;
cons.prototype.c = 4;


cons — это конструктор (просто функция, которую можно вызывать с помощью оператора new).

На пятой строке мы создаем новый объект — новую копию cons. Сразу после создания obj также получает свойство прототипа.

А теперь мы добавляем свойства ('b', 'c') прототипу объекта cons.
Рассмотрим obj:

obj.a // 1 — здесь все по-старому, obj.a по-прежнему равен 1.
obj.c  — у obj нет свойства c! Однако, как ранее упоминалось, JS теперь поищет его в прототипе obj и вернет значение 4.

А теперь давайте подумаем, каково значение obj.b и каким оно станет, когда мы удалим obj.b?

Obj.b равен 2. Мы назначили свойство b, но мы сделали это для прототипа cons, поэтому когда мы проверяем obj.b, то по-прежнему получаем 2. Однако сразу после удаления obj.b JS уже не сможет найти b у obj, и потому продолжит поиск в прототипе и вернет значение 3.

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

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


Литерал объекта: let obj = {a: 1};
Мы создали объект со следующей цепочкой прототипов: obj ---> Object.prototype ---> null
Как вы можете догадаться, object.prototype — это прототип объекта, а также конец цепочки прототипов.

Object.create (): var newObj = Object.create (obj);
У newObj будет следующая цепочка прототипов: newObj ---> obj ---> Object.prototype ---> null

Конструктор. Как и в приведенном выше примере, конструктор — это просто JS-функция, позволяющая нам воспользоваться оператором new для создания новых ее экземпляров.

ES6 классы:

class rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  } 
  getArea() {
    return this.height * this.width;
  }
}
let square = new rectangle(2, 2);


Square — экземпляр конструктора rectangle, и поэтому мы можем вызвать square.getArea () //4, square.width, а также все функции, унаследованные от object.prototype.

Какой из способов лучше? Если вы планируете создать несколько экземпляров, можно воспользоваться ES6 или конструктором. Если же вы планируете создать объект один раз, то лучше задать литерал, поскольку это самый простой способ.

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

Сравнение и изменение объектов


В JavaScript объекты относятся к ссылочному типу

Когда мы создаем объект let obj = {a: 1}; , переменная obj получает адрес в памяти объекта, но не его значение! Крайне важно понимать эту разницу, поскольку в противном случае могут возникнуть ошибки. Когда мы создаем другой объект let newObj = obj, мы фактически создаем указатель на некую область памяти obj, а не абсолютно новый объект.

Это значит, что выполняя newObj.a = 2, мы фактически изменяем obj таким образом, что obj.a становится равен 2!

Такой подход легко приводит к появлению багов, поэтому многие компании работают с неизменяемыми объектами. Вместо изменения уже созданного объекта вам придется опять создавать новый объект (копию оригинала) и вносить изменения уже в нем. Именно так работают важные библиотеки вроде Redux, и в целом это одна из основных концепций функционального программирования. Подробнее можно почитать здесь.

Равенство

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

// Two distinct objects with the same properties are not equal
var fruit = {name: 'apple'};
var fruitbear = {name: 'apple'};
fruit === fruitbear; // return false
// here fruit and fruitbear are pointing to same object
var fruit = {name: 'apple'};
var fruitbear = fruit;  
fruit === fruitbear; // return true


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

Рассмотрим несколько возможностей.

Изменение объекта

Допустим, ясно, что по-хорошему нам не следует изменять объекты, поэтому мы хотим создать копию соответствующего объекта и изменить ее свойства. На помощь приходит Object.assign ().

var obj = { a : 1, b : 2};
var newObj = Object.assign({}, obj,{a:2}) // {a : 2, b : 2 }


Если мы захотим изменить значение свойства a объекта obj, можно воспользоваться object.assign для создания копии obj и ее изменения.

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

Пожалуйста, обратите внимание, что этот способ не сработает для глубокого копирования. Говоря о глубоком копировании, мы имеем в виду, что нужно скопировать объект с одним или несколькими свойствами.

const obj = {a : 1, b : { a : 1 } };  // b property is an object


Object.assign () копирует свойства объекта, поэтому если значение свойства — это указатель на объект, то копируется только указатель.

Для глубокого копирования необходима рекурсивная операция. Здесь можно написать функцию или просто воспользоваться методом _.cloneDeep из библиотеки Lodash.

Сравнение объектов

Есть один классный прием работы с объектами — строчное преобразование. В следующем примере мы преобразовываем оба объекта в строки и сравниваем их:

JSON.stringify(obj1) === JSON.stringify(obj2) 


Такой подход оправдан, поскольку в итоге мы сравниваем строки, представляющие собой указатель на тип-значение. Плохая новость — он не всегда срабатывает, главным образом потому, что тот или иной порядок свойств объекта не гарантируется.

Другое хорошее решение — воспользоваться методом _.isEqual из Lodash, выполняющим глубокое сравнение объектов.

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

Постарайтесь подумать над решением самостоятельно перед тем, как прочитать ответ.

Как узнать длину объекта?


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

  • for in. Этот метод охватывает все счетные свойства объекта и цепочки его прототипов. Мы познакомились с прототипом (и, надеюсь, усвоили материал), поэтому должно быть ясно, что применение for in не всегда будет верным для получения свойств объекта.
  • Object.keys. Этот способ возвращает массив с ключами всех собственных (принадлежащих указанному объекту) счетных свойств. Такой подход лучше, поскольку мы работаем только над свойствами объекта, не обращаясь к свойствам prototype. Бывают, однако, ситуации, когда вы присвоили атрибуту enumerable некоторого свойства значение false, и object.keys в итоге пропускает его, а вы получаете некорректный результат. Такое происходит редко, но в подобных случаях очень кстати придется getOwnPropertyNames.
  • getOwnPropertyNames возвращает массив, содержащий все собственные ключи объекта (как счетные, так и несчетные).


Также следует упомянуть:

  • Object.values перебирает собственные счетные свойства и возвращает массив с соответствующими значениями.
  • Object.entries перебирает собственные счетные свойства и возвращает массив с ключами и их значениями.


Думаю, вы заметили, что большинство из перечисленных выше методов возвращают массив. Это возможность воспользоваться всеми преимуществами методов JavaScript для работы с массивами.

Один из таких методов — array.length. В итоге мы можем просто написать

let objLength = Object.getOwnPropertyNames(obj).length;


Как проверить, пуст ли объект?


  1. JSON.stringify (myObj) === »{}» . Здесь мы снова используем инструмент строкового преобразования, позволяющий легко проверить, пуст ли объект (сравнивая строки, а не объекты).
  2. ! Object.keys (myobj).length // true .  Как я упоминал, конвертирование ключей объекта в массив может быть очень полезным. Здесь мы пользуемся удобным свойством length, унаследованным от Array.prototype, проверяя с его помощью длину ключей в массиве. В JS 0 превращается в false, поэтому добавляя ! мы превращаем его в true. Любые другие числа будут превращаться в false.


В заключение


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

  • Помните, что объекты относятся к ссылочному типу, а значит, с ними рекомендуется работать без изменения оригинальных объектов.
  • Подружитесь со свойством prototype и цепочкой прототипов.
  • Познакомьтесь с инструментами-помощниками в работе с объектами. Помните, что можно превращать объекты в строки, получать массив с их ключами или просто перебирать их свойства с помощью набора методов, с которым мы познакомились.


Желаю удачи в изучении объектов JavaScript.  

image

© Habrahabr.ru