Свойства, с которыми мы работали до сих пор, известны как свойства данных. Для этих свойств мы задаем имя и присваиваем им значение:
let foo = {
a: "Hello",
b: "Monday";
}
Для считывания свойства нужно просто обратиться к нему напрямую:
console.log(foo.a);
Записываются же значения в свойства вполне ожидаемым способом:
foo.a = "Manic";
Помимо чтения и записи значения, мы больше ничего не можем сделать. Такова горькая правда о свойствах данных. Продолжая тему чтения и записи свойств, что, если бы мы могли следующее:
• использовать существующий синтаксис для чтения и записи значений свойств;
• получать возможность выполнять пользовательский код на фоне?
Это было бы неплохо, как считаете? Скажу больше: все это нам доступно. Такие возможности предоставляют дружественные и трудолюбивые свойства-аксессоры. В текущем разделе мы все о них узнаем и познакомимся с великими рок-звездами — загадочными геттерами и сеттерами.
Поехали!
История двух свойств
Внешне свойства-аксессоры и свойства данных очень схожи. Для свойств данных вы можете производить чтение и запись свойства:
theObj.storedValue = "Unique snowflake!"; // запись
console.log(theObj.storedValue); // чтение
С помощью свойств-аксессоров вы можете, в принципе, то же самое:
myObj.storedValue = "Also a unique snowflake!"; // запись
console.log(myObj.storedValue); // чтение
Глядя на само использование свойства, мы не можем сказать, является ли оно свойством данных или свойством-аксессором. Чтобы обнаружить отличие, нам нужно посмотреть туда, где свойство фактически определено. Взгляните на следующий код, в котором внутри объекта zorb определено несколько свойств:
let zorb = {
message: "Blah",
get greeting() {
return this.message;
},
set greeting(value) {
this.message = value;
}
};
Первое сверху — это message, стандартное свойство данных:
let zorb = {
message: "Blah",
get greeting() {
return this.message;
},
set greeting(value) {
this.message = value;
}
};
Мы узнаем, что это свойство данных, так как в нем присутствует только имя свойства и значение. А вот дальше все немного интереснее. Следующее свойство — это greeting, которое не похоже ни на одно из свойств, встреченных нами ранее:
let zorb = {
message: "Blah",
get greeting() {
return this.message;
},
set greeting(value) {
this.message = value;
}
};
Вместо того чтобы обходиться именем и значением, как message, свойство greeting разделено на две функции, которым предшествует ключевое слово get или set:
let zorb = {
message: "Blah",
get greeting() {
return this.message;
},
set greeting(value) {
this.message = value;
}
};
Эти ключевые слова и пары функций известны как геттеры и сеттеры соответственно. Особенными их делает то, что мы не обращаемся к greeting как к функции, а делаем это так же, как и с обычным свойством:
zorb.greeting = "Hola!";
console.log(zorb.greeting);
Самое же интересное происходит на уровне геттеров и сеттеров, поэтому мы рассмотрим их глубже.
Знакомство с геттерами и сеттерами
На данный момент мы знаем лишь, что геттер и сеттер — это модные названия функций, которые ведут себя как свойства. Когда мы пытаемся считать свойство-аксессор (zorb.greeting), вызывается функция геттер:
let zorb = {
message: "Blah",
get greeting() {
return this.message;
},
set greeting(value) {
this.message = value;
}
};
Аналогичным образом, когда мы задаем новое значение свойству-аксессору (zorb.greeting = "Hola!"), вызывается функция сеттер:
let zorb = {
message: "Blah",
get greeting() {
return this.message;
},
set greeting(value) {
this.message = value;
}
};
Основной потенциал геттеров и сеттеров лежит в коде, который мы можем выполнять, когда считываем или записываем свойство. Так как мы имеем дело с функциями под прикрытием, то можем выполнять любой нужный нам код. В примере с zorb мы использовали геттер и сеттер greeting, чтобы приблизительно повторить поведение свойств данных. Мы можем назначить значение, а затем считать его. Скучновато, не правда ли? Но это не должно происходить именно так, и следующие примеры привнесут больше интереса в этот процесс.
Генератор крика
Вот пример, в котором любое определяемое нами сообщение будет преобразовано в верхний регистр:
var shout = {
_message: "HELLO!",
get message() {
return this._message;
},
set message(value) {
this._message = value.toUpperCase();
}
};
shout.message = "This is sparta!";
console.log(shout.message);
Обратите внимание, что как часть определения значения свойства message мы храним введенное значение в верхнем регистре благодаря методу toUpperCase, который передается всем строковым объектам. Все это гарантирует, что при попытке считать сохраненное сообщение мы увидим полностью заглавную версию того, что введем.
Регистрирование действий
В следующем примере у нас есть объект superSecureTerminal, регистрирующий имена всех пользователей:
var superSecureTerminal = {
allUserNames: [],
_username: "",
showHistory() {
console.log(this.allUserNames);
},
get username() {
return this._username;
},
set username(name) {
this._username = name;
this.allUserNames.push(name);
}
}
Это регистрирование обрабатывается внутри сеттера username, где каждое предоставляемое имя пользователя сохраняется в массиве allUserNames, а функция showHistory выводит сохраненные имена пользователей на экран. Прежде чем продолжить, давайте протестируем этот код. Мы попробуем обратиться к superSecureTerminal не так, как делали это до сих пор. Для этого мы используем кое-какие знания о создании объектов и сделаем следующее:
var myTerminal = Object.create(superSecureTerminal);
myTerminal.username = "Michael Gary Scott";
myTerminal.username = "Dwight K. Schrute";
myTerminal.username = "Creed Bratton";
myTerminal.username = "Pam Beasley";
myTerminal.showHistory();
Мы создаем новый объект myTerminal, основанный на объекте superSecureTerminal. С этого момента мы можем делать с myTerminal все, что угодно, в привычном режиме.
Проверка значения свойства
Последним мы рассмотрим пример, в котором сеттеры производят проверку переданных им значений:
let person = {
_name: "",
_age: "",
get name() {
return this._name;
},
set name(value) {
if (value.length > 2) {
this._name = value;
} else {
console.log("Name is too short!");
}
},
get age() {
return this._age;
},
set age(value) {
if (value < 5) {
console.log("Too young!");
} else {
this._age = value;
}
},
get details() {
return "Name: " + this.name +", Age: " + this.age;
}
}
Обратите внимание, что мы производим проверку допустимого ввода для обоих свойств name и age. Если введенное имя короче двух знаков, выводится соответствующее уведомление. Если указан возраст меньше пяти, то также выскакивает уведомление. Возможность проверять, является ли присваиваемое свойству значение подходящим, вероятно, одна из лучших возможностей, предлагаемых геттерами и сеттерами.
КОРОТКО О ГЛАВНОМ
Стоит ли прекращать создавать стандартные свойства данных и использовать эти модные свойства-аксессоры? На самом деле нет. Все зависит от текущих потребностей и будущих нужд. Если вы уверены, что свойству никогда не потребуется дополнительная гибкость, предлагаемая геттерами и сеттерами, то можете просто оставить его в виде свойства данных. Если вам когда-нибудь понадобится к нему вернуться, то изменение свойства данных на свойство-аксессор полностью происходит за кадром. Мы можем делать это, не влияя на итоговое применение самого свойства. Круто, не правда ли?
Если у вас возникнут сложности в этой теме, то обращайтесь за помощью на форум https://forum.kirupa.com.
Глава 18. Об объектах подробнее
Знакомясь с объектами в главе 12 «О пицце, типах, примитивах и объектах», мы произвели очень поверхностный обзор того, чем являются объекты в JavaScript и как их воспринимать. На тот момент этого было достаточно, чтобы рассмотреть основы некоторых встроенных типов, но теперь пора двигаться дальше. В этой главе увидим, что вся предыдущая информация была лишь вершиной айсберга.
Здесь мы уже подробнее пересмотрим объекты и затронем некоторые наиболее продвинутые темы вроде объекта Object, создания пользовательских объектов, наследования, прототипов и ключевого слова this. Если все перечисленное кажется вам совершенно непонятным, то я обещаю, что к завершению главы мы это исправим.
Поехали!
Знакомство с объектом
В самом низу пищевой цепочки есть тип Object, который закладывает основу как для пользовательских объектов, так и для встроенных типов вроде Function, Array и RegExp. Практически все, за исключением null и undefined, непосредственно связано с Object или может стать им при необходимости.
Как мы уже видели, функциональность, которую предоставляет Object, весьма мала. Он позволяет определять множество именованных пар ключ — значение, которые мы с любовью называем свойствами. Это не особо отличается от того, что мы видим в других языках, использующих хэш-таблицы, ассоциативные массивы и словари.
Как бы то ни было, все это скучно. Мы же собираемся изучать объекты на практике.
Создание объектов
Первое, что мы рассмотрим, — это создание объекта. Для этого существует несколько способов, но все крутые ребята создают их с помощью забавного (но компактного) синтаксиса объектного литерала:
let funnyGuy = {};
Все верно. Вместо написания new Object(), как это делали еще ваши деды, мы можем просто инициализировать наш объект, используя {}. По завершении выполнения этой строки мы получим созданный объект funnyGuy с типом Object:
Создание объектов имеет еще кое-какие особенности кроме только что рассмотренного нами синтаксиса объектного литерала, но их мы рассмотрим в более подходящее время.
Добавление свойств
Как только у нас появился объект, мы можем использовать один из ряда путей для добавления к нему свойств. Возьмем простой и производительный вариант, который задействует подобную массиву скобочную нотацию, где имя свойства будет указано в виде индекса.
Продолжим с нашего объекта funnyGuy:
let funnyGuy = {};
Предположим, мы хотим добавить свойство firstName и задать ему значение Conan. Добавление свойства в данном случае производится с помощью синтаксиса записи через точку:
funnyGuy.firstName = "Conan";
Вот и все. После добавления свойства мы можем обращаться к нему посредством того же синтаксиса:
let funnyFirstName = funnyGuy.firstName;
Альтернатива записи через точку
Для определения считывания свойств мы использовали подход, называемый записью через точку. Но у него есть альтернатива, использующая вместо точки скобки:
let funnyGuy = {};
funnyGuy["firstName"] = "Conan";
funnyGuy["lastName"] = "O'Brien";
Какой из этих подходов использовать, решать только вам (или команде), но есть определенные случаи, для которых предназначены именно скобки. Имеются в виду случаи, когда мы работаем со свойствами, чьи имена нам нужно генерировать динамически. В примере же с firstName и lastName мы прописали их статично. Взгляните на следующий фрагмент кода:
let myObject = {};
for (let i = 0; i < 5; i++) {
let propertyName = "data" + i;
myObject[propertyName] = Math.random() * 100;
}
Мы имеем объект myObject — обратите внимание на то, как мы устанавливаем его свойства. У нас нет статичного списка имен свойств, вместо этого мы создаем имя свойства, опираясь на значение индекса массива. Когда мы выясняем имя свойства, то используем эти данные для создания свойства в myObject. Генерируемые именами свойств будут data0, data1, data2, data3 и data4. Эта возможность динамического определения имени свойства в процессе изменения или чтения объекта оказывается доступной благодаря именно скобочному синтаксису.
Теперь, прежде чем продолжить, давайте добавим еще одно свойство, назовем его lastName и присвоим ему значение O’Brien:
funnyGuy.lastName = "O'Brien";
К этому моменту мы уже в хорошей форме, а наш полный код funnyGuy выглядит следующим образом:
let funnyGuy = {};
funnyGuy.firstName = "Conan";
funnyGuy.lastName = "O'Brien";
При его выполнении будет создан объект funnyGuy, и в нем будут определены два свойства — firstName и lastName.
Мы только что рассмотрели, как пошагово создавать объект и устанавливать для него свойства. Если же мы изначально знаем, какие свойства должны быть в объекте, то можем объединить некоторые шаги:
let funnyGuy = {
firstName: "Conan",
lastName: "O'Brien"
};
Конечный результат в таком случае будет идентичен предыдущему, в котором мы сперва создали объект funnyGuy и лишь затем определили в нем свойства.
Есть и еще одна деталь, касающаяся добавления свойств, на которую стоит обратить внимание. К текущему моменту мы рассмотрели различные объекты, имеющие свойства, чьи значения состоят из чисел, строк и т. д. А вы знали, что свойством объекта может являться другой объект? Это вполне возможно! Взгляните на следующий объект colors, чье свойство content содержит объект:
let colors = {
header: "blue",
footer: "gray",
content: {
title: "black",
body: "darkgray",
signature: "light blue"
}
};
Объект внутри объекта определяется так же, как и свойство с использованием скобочного синтаксиса для установки значения свойства для объекта. Если мы хотим добавить свойство во вложенный объект, то можем для этого использовать те же только что полученные знания.
Допустим, мы хотим добавить свойство frame во вложенный объект content. Сделать мы это можем, например, так:
colors.content.frame = "yellow";
Начинаем с объекта colors, переходим к объекту content, а затем определяем свойство и значение, которые нам нужны. Если же для обращения к свойству content вы предпочтете использовать скобочную нотацию, то сделаете так:
colors["content"]["frame"] = "yellow";
Если вы хотите одновременно использовать оба вида нотации, то это тоже возможно:
colors.content["frame"] = "yellow";
В начале я говорил, что существует ряд способов для добавления свойств объекту. Мы рассмотрели один из них. Более сложный способ задействует методы Object.defineProperty и Object.defineProperties. Эти методы также позволяют вам устанавливать свойство и его значение, но при этом дают и другие возможности. Например, возможность указать, может ли свойство быть пронумеровано или может ли оно быть перенастроено и т. д. Это однозначно выходит за рамки того, что мы будем делать 99 % времени в начале обучения, но если вам это нужно, то упомянутые два метода вполне пригодятся. Документация MDN (https://mdn.dev/) приводит хорошие примеры их использования для добавления одного или нескольких свойств объекту.
Удаление свойств
Если добавление свойств могло показаться вам занятным, то их удаление несколько муторно. Но при этом оно проще. Продолжим работать с объектом colors:
let colors = {
header: "blue",
footer: "gray",
content: {
title: "black",
body: "darkgray",
signature: "light blue"
}
};
Требуется удалить свойство footer. Для этого используем один из двух способов в зависимости от того, хотим мы обратиться к свойству посредством скобочной нотации или точечной:
delete colors.footer;
// или
delete colors["footer"];
Главную роль при этом играет ключевое слово delete. Просто используйте его, сопроводив свойством, которое хотите удалить.
Но JavaScript не был бы собой, если бы тут не содержался подвох. В данном случае он связан с производительностью. Если вы будете часто удалять большое количество свойств во множестве объектов, то использование delete окажется намного медленнее, чем определение значений свойств как undefined:
colors.footer = undefined;
// или
colors["footer"] = undefined;
Оборотная же сторона определения свойства как undefined в том, что оно по-прежнему остается в памяти. Вам потребуется взвесить все за и против (скорость или память) для каждой отдельной ситуации, чтобы выбрать оптимальный вариант.
Что же происходит под капотом?
Мы научились создавать объекты и производить с ними некоторые простые модификации. Так как объекты — это сердце всех возможностей JavaScript, то важно как можно лучше разобраться в происходящем. И не ради расширения багажа знаний, хоть это и помогло бы впечатлить друзей или родственников за ужином. Главная часть работы в JavaScript — это создание объектов на основе других объектов и выполнение традиционных, присущих объектному программированию действий. Все эти действия будут для вас гораздо понятнее, когда мы разберемся в том, что же происходит при работе с объектами.
Давайте вернемся к нашему объекту funnyGuy:
let funnyGuy = {};
Итак, что мы можем сделать с пустым объектом, не имеющим свойств? Неужели наш объект funnyGuy совсем одинок и изолирован от всего происходящего? В ответ эхом — нет. Причина скрыта в том, как создаваемые в JS объекты автоматически связываются с более крупным Object и всей присущей ему функциональностью. Лучшим способом понять эту связь будет визуализация. Сосредоточьтесь и внимательно рассмотрите рис. 18.1.
Рис. 18.1. Что на самом деле происходит с простым, казалось бы, объектом funnyGuy
Здесь отображено, что именно происходит за кадром, когда мы создаем пустой объект funnyGuy.
Рассмотрение этого представления начнем с самого объекта. Здесь все по-прежнему, а вот остальное уже отличается. Мы видим, что наш funnyGuy — это просто пустой объект. У него нет свойств, которые мы могли бы ему определить, но есть свойства, которые определяются по умолчанию. Эти свойства связывают объект funnyGuy с лежащим в основе типом Object, не требуя для этого нашего вмешательства. Эта связь позволяет вызывать стандартные свойства Object для funnyGuy:
let funnyGuy = {};
funnyGuy.toString(); // [объект Object]
Для ясности еще раз скажу, что именно эта связь позволяет вызвать toString для нашего кажущегося пустым объекта funnyGuy. Однако называть эту связь связью не совсем точно. Эта связь в действительности известна как прототип (и зачастую представлена как [[Prototype]]), который в итоге указывает на другой объект. Другой объект может иметь свой собственный [[Prototype]], который будет также указывать на другой объект, и т. д. Такой род связи называется цепочкой прототипов. Перемещение по цепочке прототипов — это существенная часть того, что делает JavaScript при поиске вызываемого вами свойства. В нашем случае это вызов toString для объекта funnyGuy, который визуально представлен на рис. 18.2.
Рис. 18.2. Переход по цепочке прототипов в поиске нужного свойства
В цепочке прототипов, даже если в нашем объекте не определено конкретное свойство, которое мы ищем, JavaScript все равно продолжит поиск по цепочке в попытке найти его в каждом последующем пункте. В нашем случае цепочка прототипов объекта funnyGuy состоит из прототипа самого этого объекта и Object.prototype, то есть является весьма простой. Когда же мы будем работать с более сложными объектами, цепочки будут становиться намного длиннее и сложнее. И вскоре мы это увидим.
Объект не является частью цепочки прототипа
В предыдущих визуализациях объекта мы видели выделенные точки соединения и линии, соединяющие его свойства с Object.prototype. Здесь стоит заметить, что объект не является частью цепочки прототипов. Он играет роль в том, как объекты реализуют связь между их конструктором и неудачно названным свойством prototype (не связанным с нашим [[Prototype]]), и мы еще коснемся этой его роли позднее. Я продолжу показывать роль объекта в будущих реализациях объектов, но помните, что он не принимает участия в проходе по цепочке прототипов.
Далее, как мы видим, наш объект funnyGuy очень прост. Давайте для интереса добавим в него свойства firstName и lastName:
let funnyGuy = {
firstName: "Conan",
lastName: "O'Brien"
};
На рис. 18.3 показано, как будет выглядеть наша прежняя визуализация при участии добавленных свойств.
Рис. 18.3. Поздоровайтесь с нашими старыми знакомыми firstName и lastName
Свойства firstName и lastName являются частью объекта funnyGuy и также представлены. Покончив с рассмотрением этих основ объекта, мы можем переходить к подробностям.
Создание пользовательских объектов
Работа с обобщенным объектом Object и добавление в него свойств служит определенной цели, но вся его прелесть быстро исчезает, когда мы создаем много одинаковых в основе объектов. Взгляните на этот фрагмент:
let funnyGuy = {
firstName: "Conan",
lastName: "O'Brien",
getName: function () {
return "Name is: " + this.firstName + " " + this.lastName;
}
};
let theDude = {
firstName: "Jeffrey",
lastName: "Lebowski",
getName: function () {
return "Name is: " + this.firstName + " " + this.lastName;
}
};
let detective = {
firstName: "Adrian",
lastName: "Monk",
getName: function () {
return "Name is: " + this.firstName + " " + this.lastName;
}
};
Этот код создает объект funnyGuy и вводит два новых очень похожих на него объекта theDude и detective. Наша визуализация всего этого теперь будет выглядеть, как показано на рис. 18.4.
Рис. 18.4. Каждый вновь созданный объект расширяется от Object.prototype
На первый взгляд кажется, что здесь многовато повторений. Каждый из только что созданных объектов содержит свою собственную копию свойств firstName, lastName и getName. Итак, все же повторение — это не всегда плохо. Да, есть противоречие тому, что я утверждал ранее, но дайте-ка я все объясню. В случае с объектами нужно выяснить, какие свойства имеет смысл повторять, а какие нет. В нашем примере свойства firstName и lastName будут, как правило, уникальны для каждого объекта, а значит, это повторение имеет смысл. А вот свойство getName хоть и выступает в роли помощника, но не содержит ничего, что отдельный объект мог бы определить уникально:
getName: function () {
return "Name is: " + this.firstName + " " + this.lastName;
}
В этом случае его повторение ни к чему, следовательно, нам стоит сделать его общедоступным и избежать повторения. И как же?
Что ж… Для этого есть прямой путь, а именно создание промежуточного родительского объекта, содержащего общие свойства. В свою очередь, наши дочерние объекты смогут наследовать от этого родительского объекта вместо наследования напрямую от Object. Для большей конкретики мы создадим объект person, содержащий свойство getName. Наши объекты funnyGuy, theDude и detective станут наследниками person. Упорядоченная таким образом структура обеспечит, чтобы все свойства, требующие повторения, были повторены, а требующие совместного использования использовались совместно. Лучше понять все сказанное поможет рис. 18.5, где эти действия изображены наглядно.
Рис. 18.5. Добавление промежуточного объекта person со свойством (теперь используемым совместно) getName
Заметьте, что теперь person стал частью цепочки прототипов, удачно расположившись между Object.prototype и нашими дочерними объектами. Как же это делается? Один из подходов мы уже видели ранее, и в нем мы опираемся на Object.create. При использовании Object.create мы можем указать объект, на основе которого требуется создать новый объект. Например:
let myObject = Object.create(fooObject);
Когда мы это делаем, за кадром происходит следующее: прототип нашего объекта myObject теперь будет fooObject. При этом он становится частью цепочки прототипов. Теперь, когда мы сделали крюк и расширили наше понимание Object.create, освоив содержание этой главы. Давайте вернемся к изначальному вопросу о том, как же именно наши объекты funnyGuy, theDude и detective наследуют от person.
Код, осуществляющий все это, будет таким:
let person = {
getName: function () {
return "The name is " + this.firstName + " " + this.lastName;
}
};
let funnyGuy = Object.create(person);
funnyGuy.firstName = "Conan";
funnyGuy.lastName = "O'Brien";
let theDude = Object.create(person);
theDude.firstName = "Jeffrey";
theDude.lastName = "Lebowski";
let detective = Object.create(person);
detective.firstName = "Adrian";
detective.lastName = "Monk";
Принцип работы цепочки прототипов позволяет нам вызывать getName для любого из наших объектов funnyGuy, theDude или detective, что приведет к ожидаемому результату:
detective.getName(); // Имя Adrian Monk
Если мы решим расширить объект person, то достаточно сделать это всего один раз, и это также отразится на всех наследующих от него объектах, не требуя дополнительного повторения. Предположим, мы хотим добавить метод getInitials, возвращающий первую букву из имени и фамилии:
let person = {
getName: function () {
return "The name is " + this.firstName + " " + this.lastName;
},
getInitials: function () {
if (this.firstName && this.lastName) {
return this.firstName[0] + this.lastName[0];
}
}
};
Мы добавляем метод getInitials в объект person. Чтобы использовать этот метод, можем вызвать его для любого объекта, расширяющего person, например funnyGuy:
funnyGuy.getInitials(); // CO
Такая возможность создавать промежуточные объекты, помогающие разделять функциональность кода, является мощным инструментом. Она повышает эффективность создания объектов и добавления в них функциональности. Неплохо, правда?
Ключевое слово this
В предыдущих фрагментах кода вы могли заметить использование ключевого слова this, особенно в случае с объектом person, где мы задействовали его для обращения к свойствам, созданным в его потомках, а не к его собственным. Давайте вернемся к этому объекту, а в частности к его свойству getName:
let person = {
getName: function () {
return "The name is " + this.firstName + " " + this.lastName;
},
getInitials: function () {
if (this.firstName && this.lastName) {
return this.firstName[0] + this.lastName[0];
}
}
};
Когда мы вызываем getName, то возвращаемое имя будет зависеть от того, из какого объекта мы это делаем. Например, если мы сделаем следующее:
let spaceGuy = Object.create(person);
spaceGuy.firstName = "Buzz";
spaceGuy.lastName = "Lightyear";
console.log(spaceGuy.getName()); // Buzz Lightyear
При выполнении этого кода мы увидим в консоли Buzz Lightyear. Если мы еще раз взглянем на свойство getName, то увидим, что там нет свойств firstName и lastName в объекте person. Но как мы видели ранее, если свойство не существует, мы переходим далее по цепочке от родителя к родителю, как показано на рис. 18.6.
Рис. 18.6. Цепочка прототипов для объекта person
В нашем случае единственной остановкой в цепочке будет Object.prototype, но в нем также не обнаруживаются свойства firstName и lastName. Как же тогда метод getName умудряется сработать и вернуть нужные значения?
Ответ заключается в ключевом слове this, предшествующем firstName и lastName в инструкции return метода getName:
let person = {
getName: function () {
return "The name is " + this.firstName + " " + this.lastName;
},
getInitials: function () {
if (this.firstName && this.lastName) {
return this.firstName[0] + this.lastName[0];
}
}
};
Ключевое слово this ссылается на объект, к которому привязан наш метод getName. В данном случае объектом является spaceGuy, так как именно его мы используем в качестве точки входа в этот совершенный процесс навигации между прототипами (рис. 18.7).
Рис. 18.7. Ключевое слово this ссылается на spaceGuy!
Когда происходит вычисление метода getName и свойства firstName и lastName должны разрешиться, поиск начинается там, куда указывает ключевое слово this. Это означает, что наш поиск начинается с объекта spaceGuy, который, как выясняется, содержит свойства firstName и lastName. Именно поэтому мы получаем верный результат при вызове кода для getName (а также и getInitials).
Понимание, на что ссылается ключевое слово this, скрыто под галлонами пролитых чернил, и полноценное рассмотрение этого вопроса выходит далеко за рамки того, о чем мы собираемся говорить. Но хорошо то, что пройденного материала вам уже будет достаточно, чтобы решать большинство задач.
КОРОТКО О ГЛАВНОМ
Из-за неразберихи вокруг объектной ориентированности в JavaScript разумным было сделать рассмотрение этой темы глубоким и обширным, как мы и поступили. Многое из того, что было затронуто здесь, прямо или косвенно связано с наследованием — когда объекты разветвляются и основываются на других объектах. В отличие от классических языков, использующих классы как шаблоны для объектов, в JavaScript понятия классов, строго говоря, не существует. Здесь используется так называемая модель наследования прототипов. Вы не инстанцируете объекты из шаблона. Вместо этого вы создаете их либо заново, либо, чаще всего, копированием или клонированием другого объекта. JavaScript попадает в ту самую серую область, где не соответствует классической форме языка, но при этом имеет подобные классам конструкции (некоторые из них вы увидите позже), которые позволяют ему сидеть за одним столом с классическими языками. Не хочу здесь увлекаться навешиванием ярлыков.
Среди всего этого множества страниц я постарался сгруппировать новую функциональность JavaScript для работы с объектами и их расширения для ваших дальнейших нужд. Тем не менее еще многое предстоит рассмотреть, поэтому сделайте перерыв, и мы в ближайшем будущем затронем более интересные темы, которые дополнят пройденное более мощными и выразительными возможностями.
Дополнительные ресурсы и примеры:
• Понимание прототипов в JS: http://bit.ly/kirupaJSPrototypes
• Простое английское руководство по прототипам JS: http://bit.ly/kirupaPrototypesGuide
• Как работает prototype? http://bit.ly/kirupaPrototypeWork
• Это большая и странная тема, поэтому обращайтесь на форум https://forum.kirupa.com, если столкнетесь с какими-либо сложностями.