Материал, перевод которого мы сегодня публикуем, посвящён исследованию объектов — одной из ключевых сущностей JavaScript. Он рассчитан, преимущественно, на начинающих разработчиков, которые хотят упорядочить свои знания об объектах.
Объекты в JavaScript представляют собой динамические коллекции свойств, которые, кроме того, содержат «скрытое» свойство, представляющее собой прототип объекта. Свойства объектов характеризуются ключами и значениями. Начнём разговор о JS-объектах с ключей.
Ключ свойства объекта представляет собой уникальную строку. Для доступа к свойствам можно использовать два способа: обращение к ним через точку и указание ключа объекта в квадратных скобках. При обращении к свойствам через точку ключ должен представлять собой действительный JavaScript-идентификатор. Рассмотрим пример:
При попытке обращения к несуществующему свойству объекта сообщения об ошибке не появится, но возвращено будет значение
При использовании для доступа к свойствам квадратных скобок можно применять ключи, которые не являются действительными JavaScript-идентификаторами (например, ключ может быть строкой, содержащей пробелы). Они могут иметь любое значение, которое можно привести к строке:
Если в качестве ключей используются нестроковые значения, они автоматически преобразуются к строкам (с использованием, если это возможно, метода
В этом примере в качестве ключа используется объект
Свойства объекта могут быть примитивными значениями, объектами или функциями.
Объекты можно помещать в другие объекты. Рассмотрим пример:
Подобный подход можно использовать для создания пространств имён:
Когда в качестве значения свойства объекта используется функция, она обычно становится методом объекта. Внутри метода, для обращения к текущему объекту, используется ключевое слово
У этого ключевого слова, однако, могут быть разные значения, что зависит от того, как именно была вызвана функция. Здесь можно почитать о ситуациях, в которых
Объекты в JavaScript, по своей природе, являются динамическими сущностями. Добавлять в них свойства можно в любое время, то же самое касается и удаления свойств:
Объекты можно рассматривать как ассоциативные массивы. Ключи ассоциативного массива представляют собой имена свойств объекта. Для того чтобы получить доступ к ключу, все свойства просматривать не нужно, то есть операция доступа к ключу ассоциативного массива, основанного на объекте, выполняется за время O(1).
У объектов есть «скрытая» ссылка,
Например, объект, созданный с помощью объектного литерала, имеет ссылку на
Как мы только что видели, «пустой» объект,
Благодаря этому будет создан объект без прототипа. Такие объекты обычно используют для создания ассоциативных массивов.
У объектов-прототипов могут быть собственные прототипы. Если попытаться обратиться к свойству объекта, которого в нём нет, JavaScript попытается найти это свойство в прототипе этого объекта, а если и там нужного свойства не окажется, будет сделана попытка найти его в прототипе прототипа. Это будет продолжаться до тех пор, пока нужное свойство не будет найдено, или до тех пор, пока не будет достигнут конец цепочки прототипов.
JavaScript позволяет работать со значениями примитивных типов как с объектами, в том смысле, что язык позволяет обращаться к их свойствам и методам.
При этом, конечно, значения примитивных типов объектами не являются.
Для организации доступа к «свойствам» значений примитивных типов JavaScript, при необходимости, создаёт объекты-обёртки, которые, после того, как они оказываются ненужными, уничтожаются. Процесс создания и уничтожения объектов-обёрток оптимизируется JS-движком.
Объектные обёртки есть у значений числового, строкового и логического типов. Объекты соответствующих типов представлены функциями-конструкторами
Объекты-числа наследуют свойства и методы от прототипа
Прототипом объектов-строк является
Функции в JavaScript тоже являются объектами, имеющими прототип
Все объекты, функции, и объекты, представляющие значения примитивных типов (за исключением значений
JavaScript позволяет легко расширять встроенные объекты новыми функциями с помощью так называемых полифиллов. Полифилл — это фрагмент кода, реализующий возможности, не поддерживаемые какими-либо браузерами.
Например, существует полифилл для метода
То же самое относится и к полифиллу
С помощью полифиллов новые методы можно добавлять к прототипам объектов. Например, полифилл для
Полифилл для
Команда
Команда
Команда
Команда
Для создания клонов (копий) объектов можно использовать команду
Эта команда выполняет неглубокое копирование объектов, то есть — копирует только свойства верхнего уровня. Вложенные объекты оказываются, для объектов-оригиналов и их копий, общими.
Объектные литералы дают разработчику простой и понятный способ создания объектов:
Однако такой способ создания объектов имеет и недостатки. В частности, при таком подходе все свойства объекта оказываются общедоступными, методы объекта могут быть переопределены, их нельзя использовать для создания новых экземпляров одинаковых объектов:
Решить две вышеозначенные проблемы можно благодаря совместному использованию методов
Применим эту методику к нашему предыдущему примеру. Сначала создадим замороженный прототип
Если прототип защищён от изменений, объект, являющийся его наследником, не сможет изменять свойства, определённые в прототипе. Теперь методы
Конструкцию
В JavaScript существуют так называемые функции-конструкторы, представляющие собой «синтаксический сахар» для выполнения вышеописанных действий по созданию новых объектов. Рассмотрим пример:
В качестве конструктора можно использовать любую функцию. Конструктор вызывают с использованием ключевого слова
Тут, для предотвращения изменения прототипа, опять же, можно прототип «заморозить»:
Когда выполняется команда вида
Здесь создаётся новый объект, прототипом которого является
В ECMAScript 2015 появился новый способ выполнения вышеописанных действий, представляющий собой очередную порцию «синтаксического сахара». Речь идёт о ключевом слове
Объект, созданный с использованием ключевого слова
Использование классов не делает прототипы неизменными. Их, если это нужно, придётся «замораживать» так же, как мы это уже делали:
В JavaScript объекты наследуют свойства и методы от других объектов. Функции-конструкторы и классы — это «синтаксический сахар» для создания объектов-прототипов, содержащих все необходимые методы. С их использованием создают новые объекты являющиеся наследниками прототипа, свойства которого, специфичные для конкретного экземпляра, устанавливают с помощью функции-конструктора или с помощью механизмов класса.
Хорошо было бы, если бы функции-конструкторы и классы могли бы автоматически делать прототипы неизменными.
Сильной стороной прототипного наследования является экономия памяти. Дело в том, что прототип создаётся лишь один раз, после чего им пользуются все объекты, созданные на его основе.
В шаблоне прототипного наследования не используется разделение свойств объектов на приватные и общедоступные. Все свойства объектов являются общедоступными.
Например, команда
Существует один паттерн, имитирующий приватные свойства, полагающийся на то, что разработчики не будут обращаться к тем свойствам, имена которых начинаются с символа подчёркивания (
Инкапсулированные объекты в JavaScript можно создавать с использованием фабричных функций. Выглядит это так:
Здесь переменная
В команде
Здесь мы, в примерах, использовали объект
В JavaScript значения примитивных типов, обычные объекты и функции воспринимаются как объекты. Объекты имеют динамическую природу, их можно использовать как ассоциативные массивы. Объекты являются наследниками других объектов. Функции-конструкторы и классы — это «синтаксический сахар», они позволяют создавать объекты, основанные на прототипах. Для организации одиночного наследования можно использовать метод
Уважаемые читатели! Если вы пришли в JavaScript из других языков, просим рассказать нам о том, что вам нравится или не нравится в JS-объектах, в сравнении с реализацией объектов в уже известных вам языках.
Объекты в 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-объектах, в сравнении с реализацией объектов в уже известных вам языках.