JavaScript: исследование объектов

от автора

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

Объекты в JavaScript представляют собой динамические коллекции свойств, которые, кроме того, содержат «скрытое» свойство, представляющее собой прототип объекта. Свойства объектов характеризуются ключами и значениями. Начнём разговор о JS-объектах с ключей.

Ключи свойств объектов

Ключ свойства объекта представляет собой уникальную строку. Для доступа к свойствам можно использовать два способа: обращение к ним через точку и указание ключа объекта в квадратных скобках. При обращении к свойствам через точку ключ должен представлять собой действительный JavaScript-идентификатор. Рассмотрим пример:

let obj = {   message : "A message" } obj.message //"A message" obj["message"] //"A message"

При попытке обращения к несуществующему свойству объекта сообщения об ошибке не появится, но возвращено будет значение undefined:

obj.otherProperty //undefined

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

let french = {}; french["merci beaucoup"] = "thank you very much";  french["merci beaucoup"]; //"thank you very much"

Если в качестве ключей используются нестроковые значения, они автоматически преобразуются к строкам (с использованием, если это возможно, метода toString()):

et obj = {}; //Number obj[1] = "Number 1"; obj[1] === obj["1"]; //true //Object let number1 = {   toString : function() { return "1"; } } obj[number1] === obj["1"]; //true

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

Значения свойств объектов

Свойства объекта могут быть примитивными значениями, объектами или функциями.

▍Объект как значение свойства объекта

Объекты можно помещать в другие объекты. Рассмотрим пример:

let book = {   title : "The Good Parts",   author : {     firstName : "Douglas",     lastName : "Crockford"   } } book.author.firstName; //"Douglas"

Подобный подход можно использовать для создания пространств имён:

let app = {}; app.authorService = { getAuthors : function() {} }; app.bookService = { getBooks : function() {} };

▍Функция как значение свойства объекта

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

У этого ключевого слова, однако, могут быть разные значения, что зависит от того, как именно была вызвана функция. Здесь можно почитать о ситуациях, в которых this теряет контекст.

Динамическая природа объектов

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

let obj = {}; obj.message = "This is a message"; //добавление нового свойства obj.otherMessage = "A new message"; // добавление нового свойства delete obj.otherMessage; //удаление свойства

Объекты как ассоциативные массивы

Объекты можно рассматривать как ассоциативные массивы. Ключи ассоциативного массива представляют собой имена свойств объекта. Для того чтобы получить доступ к ключу, все свойства просматривать не нужно, то есть операция доступа к ключу ассоциативного массива, основанного на объекте, выполняется за время O(1).

Прототипы объектов

У объектов есть «скрытая» ссылка, __proto__, указывающая на объект-прототип, от которого объект наследует свойства.

Например, объект, созданный с помощью объектного литерала, имеет ссылку на Object.prototype:

var obj = {}; obj.__proto__ === Object.prototype; //true

▍Пустые объекты

Как мы только что видели, «пустой» объект, {}, на самом деле, не такой уж и пустой, так как он содержит ссылку на Object.prototype. Для того чтобы создать по-настоящему пустой объект, нужно воспользоваться следующей конструкцией:

Object.create(null)

Благодаря этому будет создан объект без прототипа. Такие объекты обычно используют для создания ассоциативных массивов.

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

У объектов-прототипов могут быть собственные прототипы. Если попытаться обратиться к свойству объекта, которого в нём нет, JavaScript попытается найти это свойство в прототипе этого объекта, а если и там нужного свойства не окажется, будет сделана попытка найти его в прототипе прототипа. Это будет продолжаться до тех пор, пока нужное свойство не будет найдено, или до тех пор, пока не будет достигнут конец цепочки прототипов.

Значения примитивных типов и объектные обёртки

JavaScript позволяет работать со значениями примитивных типов как с объектами, в том смысле, что язык позволяет обращаться к их свойствам и методам.

(1.23).toFixed(1); //"1.2" "text".toUpperCase(); //"TEXT" true.toString(); //"true"

При этом, конечно, значения примитивных типов объектами не являются.

Для организации доступа к «свойствам» значений примитивных типов JavaScript, при необходимости, создаёт объекты-обёртки, которые, после того, как они оказываются ненужными, уничтожаются. Процесс создания и уничтожения объектов-обёрток оптимизируется JS-движком.

Объектные обёртки есть у значений числового, строкового и логического типов. Объекты соответствующих типов представлены функциями-конструкторами Number, String, и Boolean.

Встроенные прототипы

Объекты-числа наследуют свойства и методы от прототипа Number.prototype, который является наследником Object.prototype:

var no = 1; no.__proto__ === Number.prototype; //true no.__proto__.__proto__ === Object.prototype; //true

Прототипом объектов-строк является String.prototype. Прототипом объектов-логических значений является Boolean.prototype. Прототипом массивов (которые тоже являются объектами), является Array.prototype.

Функции в JavaScript тоже являются объектами, имеющими прототип Function.prototype. У функций есть методы наподобие bind(), apply() и call().

Все объекты, функции, и объекты, представляющие значения примитивных типов (за исключением значений null и undefined) наследуют свойства и методы от Object.prototype. Это ведёт к тому, что, например, у всех них есть метод toString().

Расширение встроенных объектов с помощью полифиллов

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

▍Использование полифиллов

Например, существует полифилл для метода Object.assign(). Он позволяет добавить в Object новую функцию в том случае, если она в нём недоступна.

То же самое относится и к полифиллу Array.from(), который, в том случае, если в объекте Array нет метода from(), оснащает его этим методом.

▍Полифиллы и прототипы

С помощью полифиллов новые методы можно добавлять к прототипам объектов. Например, полифилл для String.prototype.trim() позволяет оснастить все строковые объекты методом trim():

let text = "   A text  "; text.trim(); //"A text"

Полифилл для Array.prototype.find() позволяет оснастить все массивы методом find(). Похожим образом работает и полифилл для Array.prototype.findIndex():

let arr = ["A", "B", "C", "D", "E"]; arr.indexOf("C"); //2

Одиночное наследование

Команда Object.create() позволяет создавать новые объекты с заданным объектом-прототипом. Эта команда используется в JavaScript для реализации механизма одиночного наследования. Рассмотрим пример:

let bookPrototype = {   getFullTitle : function(){     return this.title + " by " + this.author;   } } let book = Object.create(bookPrototype); book.title = "JavaScript: The Good Parts"; book.author = "Douglas Crockford"; book.getFullTitle();//JavaScript: The Good Parts by Douglas Crockford

Множественное наследование

Команда Object.assign() копирует свойства из одного или большего количества объектов в целевой объект. Её можно использовать для реализации схемы множественного наследования. Вот пример:

let authorDataService = { getAuthors : function() {} }; let bookDataService = { getBooks : function() {} }; let userDataService = { getUsers : function() {} }; let dataService = Object.assign({},  authorDataService,  bookDataService,  userDataService ); dataService.getAuthors(); dataService.getBooks(); dataService.getUsers();

Иммутабельные объекты

Команда Object.freeze() позволяет «заморозить» объект. В такой объект нельзя добавлять новые свойства. Свойства нельзя удалять, нельзя и изменять их значения. Благодаря использованию этой команды объект становится неизменяемым или иммутабельным:

"use strict"; let book = Object.freeze({   title : "Functional-Light JavaScript",   author : "Kyle Simpson" }); book.title = "Other title";//Ошибка: Cannot assign to read only property 'title'

Команда Object.freeze() выполняет так называемое «неглубокое замораживание» объектов. Это означает, что объекты, вложенные в «замороженный» объект, можно изменять. Для того чтобы осуществить «глубокую заморозку» объекта, нужно рекурсивно «заморозить» все его свойства.

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

Для создания клонов (копий) объектов можно использовать команду Object.assign():

let book = Object.freeze({   title : "JavaScript Allongé",   author : "Reginald Braithwaite" }); let clone = Object.assign({}, book);

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

Объектный литерал

Объектные литералы дают разработчику простой и понятный способ создания объектов:

let timer = {   fn : null,   start : function(callback) { this.fn = callback; },   stop : function() {}, }

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

timer.fn;//null  timer.start = function() { console.log("New implementation"); }

Метод Object.create()

Решить две вышеозначенные проблемы можно благодаря совместному использованию методов Object.create() и Object.freeze().

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

let timerPrototype = Object.freeze({   start : function() {},   stop : function() {} }); let timer = Object.create(timerPrototype); timer.__proto__ === timerPrototype; //true

Если прототип защищён от изменений, объект, являющийся его наследником, не сможет изменять свойства, определённые в прототипе. Теперь методы start() и stop() переопределить нельзя:

"use strict"; timer.start = function() { console.log("New implementation"); } //Ошибка: Cannot assign to read only property 'start' of object

Конструкцию Object.create(timerPrototype) можно использовать для создания множества объектов с одним и тем же прототипом.

Функция-конструктор

В JavaScript существуют так называемые функции-конструкторы, представляющие собой «синтаксический сахар» для выполнения вышеописанных действий по созданию новых объектов. Рассмотрим пример:

function Timer(callback){   this.fn = callback; } Timer.prototype = {   start : function() {},   stop : function() {} } function getTodos() {} let timer = new Timer(getTodos);

В качестве конструктора можно использовать любую функцию. Конструктор вызывают с использованием ключевого слова new. Объект, созданный с помощью функции-конструктора с именем FunctionConstructor, получит прототип FunctionConstructor.prototype:

let timer = new Timer(); timer.__proto__ === Timer.prototype;

Тут, для предотвращения изменения прототипа, опять же, можно прототип «заморозить»:

Timer.prototype = Object.freeze({   start : function() {},   stop : function() {} });

▍Ключевое слово new

Когда выполняется команда вида new Timer(), производятся те же действия, которые выполняет представленная ниже функция newTimer():

function newTimer(){   let newObj = Object.create(Timer.prototype);   let returnObj = Timer.call(newObj, arguments);   if(returnObj) return returnObj;        return newObj; }

Здесь создаётся новый объект, прототипом которого является Timer.prototype. Затем вызывается функция Timer, устанавливающая поля для нового объекта.

Ключевое слово class

В ECMAScript 2015 появился новый способ выполнения вышеописанных действий, представляющий собой очередную порцию «синтаксического сахара». Речь идёт о ключевом слове class и о соответствующих конструкциях, связанных с ним. Рассмотрим пример:

class Timer{   constructor(callback){     this.fn = callback;   }      start() {}   stop() {}   } Object.freeze(Timer.prototype);

Объект, созданный с использованием ключевого слова class на основе класса с именем ClassName, будет иметь прототип ClassName.prototype. При создании объекта на основе класса нужно использовать ключевое слово new:

let timer= new Timer(); timer.__proto__ === Timer.prototype;

Использование классов не делает прототипы неизменными. Их, если это нужно, придётся «замораживать» так же, как мы это уже делали:

Object.freeze(Timer.prototype);

Наследование, основанное на прототипах

В JavaScript объекты наследуют свойства и методы от других объектов. Функции-конструкторы и классы — это «синтаксический сахар» для создания объектов-прототипов, содержащих все необходимые методы. С их использованием создают новые объекты являющиеся наследниками прототипа, свойства которого, специфичные для конкретного экземпляра, устанавливают с помощью функции-конструктора или с помощью механизмов класса.

Хорошо было бы, если бы функции-конструкторы и классы могли бы автоматически делать прототипы неизменными.

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

▍Проблема отсутствия встроенных механизмов инкапсуляции

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

Например, команда Object.keys() возвращает массив, содержащий все ключи свойств объекта. Его можно использовать для перебора всех свойств объекта:

function logProperty(name){   console.log(name); //имя свойства   console.log(obj[name]); //значение свойства } Object.keys(obj).forEach(logProperty);

Существует один паттерн, имитирующий приватные свойства, полагающийся на то, что разработчики не будут обращаться к тем свойствам, имена которых начинаются с символа подчёркивания (_):

class Timer{   constructor(callback){     this._fn = callback;     this._timerId = 0;   } }

Фабричные функции

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

function TodoStore(callback){     let fn = callback;          function start() {},     function stop() {}          return Object.freeze({        start,        stop     }); }

Здесь переменная fn является приватной. Общедоступными являются лишь методы start() и stop(). Эти методы нельзя модифицировать извне. Здесь не используется ключевое слово this, поэтому при использовании данного метода создания объектов проблема потеря контекста this оказывается неактуальной.

В команде return используется объектный литерал, содержащий лишь функции. Более того, эти функции объявлены в замыкании, они совместно пользуются общим состоянием. Для «заморозки» общедоступного API объекта используется уже известная вам команда Object.freeze().

Здесь мы, в примерах, использовали объект Timer. В этом материале можно найти его полную реализацию.

Итоги

В JavaScript значения примитивных типов, обычные объекты и функции воспринимаются как объекты. Объекты имеют динамическую природу, их можно использовать как ассоциативные массивы. Объекты являются наследниками других объектов. Функции-конструкторы и классы — это «синтаксический сахар», они позволяют создавать объекты, основанные на прототипах. Для организации одиночного наследования можно использовать метод Object.create(), для организации множественного наследования — метод Object.assign(). Для создания инкапсулированных объектов можно использовать фабричные функции.

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


ссылка на оригинал статьи https://habr.com/post/420615/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *