Объекты в JavaScript
Объект в JavaScript — это тип данных, который может содержать коллекции различных свойств и других более сложных структур. Данные чаще всего объединены общим смыслом, что позволяет оперировать объектом, как единым целым. Свойства в свою очередь имеют имя и значение, что позволяет изменять их, извлекать, удалять или создавать новые.
Для создания объекта используют два варианта синтаксиса
let student = new Object(); // конструктор объекта let student = <>; // литерал объекта или литеральная нотация
Чаще применяется второй вариант с фигурными скобками
Литеральный синтаксис
Литерал объекта — это список свойств заключенный в фигурные скобки и разделенный запятыми
let student = < name: "Семён", age: 21, "student level": "junior", course: "JavaScript разработчик", >;
let student — переменная куда записывается ссылка на объект.
name: «Семён» — необязательный параметр свойство в формате ключ: значение , где первое это имя или другими словами id свойства, а второе его значение.
course: «JavaScript разработчик», — последняя запятая называется «висячей», она не обязательна, но такая запись упрощает замену свойств, добавление новых или удаление существующих.
«student level»: — если имя свойства состоит из двух слов, оно заключается в кавычки. Значение свойства также заключается в кавычки, но только если это строка.
Объекты в JavaScript — это ссылочный тип данных, поэтому присваивая объект переменной, мы записываем туда не сам объект, а только ссылку, которая указывает на то место в памяти, где он хранится.
Как обратиться к объекту или его свойству
Для того, чтобы получить весь объект необходимо просто обратиться к переменной.
console.log(student);
Обратиться к отдельному свойству можно двумя способами: через точку — объект.свойство
alert (student.name);
и с помощью квадратных скобок объект[«свойство»]
alert (student["student level"]);
Если в ключе есть пробелы, он начинается с цифры или содержит специальные символы, кроме кроме $ и _ обратиться к нему можно только последним способом.
Если обратиться к свойству, которого нет в объекте, программа вернет undefined и продолжит выполнять код. Такое поведение в JavaScript с одной стороны не мешает работе программы,с другой может привести к ошибкам, которые не просто отловить.
Также обратиться к свойству можно только через скобки, если его имя хранится в переменной.
let studentLevel = "student level" let student = < name: "Семён", age: 21, "student level": "junior", course: "JavaScript разработчик", >; alert(student[studentLevel]);
В этом случае при обращении имя переменной в квадратных скобках записывается без кавычек.
Возможна и такая запись
let studentLevel = "student level" let student = < name: "Семён", age: 21, [studentLevel]: "junior", course: "JavaScript разработчик", >; alert(student["student level"]);
В этом случае [studentLevel] называется вычисляемым свойством, а обратиться к нему можно через значение, которое мы записали в переменную studentLevel .
Имя свойства — это символ или строка и все другие типы данных будут приведены именно к строке. Таким образом даже если имя ключа 0, то это именно строка, а не число и обратиться к нему можно как object[0] , так и object[«0»] .
После того, как мы научились обращаться к свойствам, давайте разберем, что с ними можно делать еще.
Добавление свойства
let student = < name: "Семён", >; student.age = 21; student["student level"] = "junior"; student.course = "JavaScript разработчик"; console.log(student);
Чтобы добавить новое свойство: сначала пишем имя переменной, где лежит нужный нам объект, далее имя нового ключа, знак равно и нужное значение.
Удаление свойства
let student = < name: "Семён", age: 21, "student level": "junior", course: "JavaScript разработчик", >; delete student["student level"]; delete student.age; console.log(student);
Для удаления пары ключ: значение мы используем оператор delete и сразу же смотрим результат в console, где объект student выводиться уже без age и «student level» .
Изменение значения свойства
const student = < name: "Семён", age: 21, "student level": "junior", course: "JavaScript разработчик", >; student.age = 29; student["student level"] = "middle"; console.log(student);
Изменение свойств происходит привычным нам способом, обращаемся к нужному ключу и записываем через = новое значение.
При объявлении объекта через const, остается возможность править его: добавлять, удалять изменять свойства. JavaScript выдаст ошибку только в том случае, если попытаться присвоить самой переменной другое значение.
Допустимые имена свойств
В отличие от переменных имена свойств можно называть, как угодно, даже используя зарезервированные слова, такие как let , var , const и так далее. Единственное название, с которым возникнут проблемы это __proto__ , но скорее всего вряд ли вам захочется называть свойство именно так.
let skills = < var: "1", // можно let: "JavaScript", // можно "__proto__": "3", // свойство доступно не будет >
Вложенные объекты
Объекты в свою очередь могут содержать в качестве значения свойств другие объекты.
let student = < name: "Семён", age: 21, skills: < html: true, css: true, react: false, >, >; console.log(student.skills.html);
В таких случаях обращения к вложенным объектам происходит по цепочке.
Проверка существования свойства
Проверить свойство на существование в объекте можно двумя способами.
let student = < name: "Семён", age: 21, "student level": "junior", course: "JavaScript разработчик", >; if (student["work experience"] === undefined || "work experience" in student === false) < alert ('Студент не имеет опыта работы!'); >;
student[«work experience»] — в первом случае мы пользуемся тем, что если обратиться к свойству, которого нет в объекте, программа вернет undefined .
«work experience» in student — во втором случае мы пользуется специальной конструкцией «key» in object . Если свойство есть, получим true , в противном случае false .
Эти два способа взаимозаменяемы, кроме того случая, когда значение ключа равно undefined .
Цикл for…in для перебора свойств объекта
for (let key in object)
let key — объявляем переменную, которая будет содержать имена свойств. Название переменной не обязательно должно быть именно key .
object — имя объекта с которым работаем.
console.log(key) — выводим в console имя свойства.
console.log(object[key]) — выводим в console значение свойства.
<. >— код заключенный в фигурные скобки называется телом цикла и выполняется для каждого свойства объекта.
С помощью for…in можно перебрать свойства и объекта в объекте.
let student = < name: "Семён", age: 21, skills: < html: true, css: true, react: false, >, >; for (property in student.skills)
В круглых скобках мы явно общаемся к нужному объекту student.skills , также делаем и при получении значении ключа student.skills[property] .
Порядок получения свойств
Если свойства «целочисленные», тогда ключи сортируются по возрастанию.
let skills = < "30": "React", "1": "HTML", "3": "CSS", "10": "JavaScript", >; for (key in skills)
В этом случае в console можно будет увидеть следующую записать
1: HTML 3: CSS 10: JavaScript 30: React
«Целочисленное свойство» – это свойство (строка), которое может быть преобразовано в число и обратно.
«1», «4», «10», «38», «85» — целочисленные.
«+1», «-4», «10.3» — не является целочисленным.
Во всех остальных случаях, когда свойства не целочисленные, ключи будут выводиться в порядке записи в объекте.
Копирование объекта
let skills = < "4": "React", "1": "HTML", "2": "CSS", "3": "JavaScript", >; let skillList = skills; skillList["5"] = "NodeJS"; console.log(skills); console.log(skillList);
Помещая объект в другую переменную, мы не создаем новый объект с такими же параметрами, а записываем в переменную ссылку на уже существующий.
В примере мы записали ссылку на объект в переменную skillList , добавили туда свойство с именем «5» и значением «NodeJS» . Далее вывели в console skills и skillList и видим 5: «NodeJS» , как в первом, так и втором варианте.
Дублирование объекта — Object.assign
Продублировать объект в другую переменную, можно двумя способами, с помощью метода Object.assign и цикла for…in .
let skills = < "4": "React", "1": "HTML", "2": "CSS", "3": "JavaScript", "5": "NodeJS", >; let mySkills = Object.assign(<>, skills); delete mySkills["2"]; console.log(skills); console.log(mySkills);
В примере мы продублировали объект skills в переменную mySkills .
Object.assign(<>, skills);
<> — фигурные скобки в данном случае говорят, о том, что мы создаем новый объект.
skills — объект который копируем.
Используя для дублирования цикл for…in мы напрямую переписываем все свойства из объекта в объект.
let skills = < "1": "HTML", "2": "CSS", "3": "JavaScript", >; let mySkills = <>; for (let key in skills) < mySkills[key] = skills[key]; >delete mySkills["3"]; console.log(mySkills); console.log(skills);
Итого
1. Объекты в JavaScript – это важная часть языка программирования, они часто используются и нужны для хранения коллекций и более сложных сущностей.
2. Информацию из объекта можно получать, её можно менять, удалять или добавлять.
3. Сама переменная куда мы как бы записываем объект, на самом деле содержит ссылку на место в памяти, где размещен этот объект.
4. Для перебора свойств объекта есть специальный цикл for…in .
5. В качестве свойств объекта, могут быть другие объекты и функции.
6. В JavaScript есть разные типы объектов:
- простые: рассмотрели в этой статье;
- массивы: хранение упорядоченных коллекций элементов;
- data: хранение информации о дате;
- error; хранение информации об ошибки;
- и другие.
Skypro — научим с нуля
Синтаксис — JS: Объекты
Объект – тип данных в JavaScript, с помощью которого представляют связанный набор данных. Это позволяет оперировать данными как единым целым. Например, любой объект реального мира можно описать объектом в JavaScript. То же самое относится и к математическим объектам, например — фигурам.
const user = name: 'Vasya', married: true, age: 25 >; const rectangle = width: 10, height: 5 >;
Объект описывается как перечисление ключей и значений через запятую в фигурных скобках. Пары «ключ: значение» в объектах называются свойствами (property). Ключи объекта уникальны, то есть в одном объекте не может быть двух одинаковых ключей, имеющих разные значения.
Если свойств много, то определение можно растянуть на несколько строк:
const user = name: 'Vasya', married: true, age: 25, >;
Запятая в конце не обязательна, но рекомендуется линтером . Это удобно при добавлении или удалении ключей. Не придется менять окончание последней строчки.
Для обращения к свойствам объектов используется точечный синтаксис:
user.name; // 'Vasya' rectangle.width; // 10
Иногда по ошибке или намеренно обращаются к свойствам, которых в объекте нет. В этом случае JavaScript возвращает undefined , и продолжает работать как ни в чем не бывало. Такое поведение может приводить к трудноотловимым ошибкам, поэтому будьте осторожны и всегда проверяйте написание свойств, если возвращаются не те данные, или данных нет:
user.nme; // undefined user.name; // 'Vasya'
JavaScript поддерживает альтернативный способ обращения к свойствам объектов – через квадратные скобки — как в массивах:
// имена свойств хранятся внутри объекта в виде строк. user['name']; // 'Vasya' rectangle['width']; // 10 user['company']; // undefined
Зачем нужен такой способ доступа? В реальном использовании объектов часто встречаются алгоритмы, когда имя свойства может меняться в процессе обработки. Обращение к свойству через точку не позволяет задавать имя динамически, а способ через скобки — позволяет:
const user = name: 'Vasya', married: true, age: 25 >; let propertyName = 'name'; user[propertyName]; // 'Vasya' propertyName = 'married'; user[propertyName]; // true
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
Работа с объектами в JavaScript: теория и практика
В этой статье я хочу по возможности полно и последовательно рассказать о том, что такое объект в JavaScript, каковы его возможности, какие взаимоотношения могут строиться между объектами и какие способы «родного» наследования из этого вытекают, как это все влияет на производительность и что вообще со всем этим делать 🙂
В статье НЕ будет ни слова про: эмуляцию традиционной класс-объектной парадигмы, синтаксический сахар, обертки и фреймворки.
Сложность материала будет нарастать от начала к концу статьи, так что для профи первые части могут показаться скучными и банальными, но дальше будет намного интереснее 🙂
Объекты в JavaScript
Во многих статьях встречается фраза «В JavaScript — всё объект». Технически это не совсем верно, однако производит должное впечатление на новичков 🙂
Действительно, многое в языке является объектом, и даже то, что объектом не является, может обладать некоторыми его возможностями.
Важно понимать, что слово «объект» употребляется здесь не в смысле «объект некоторого класса». Объект в JavaScript — это в первую очередь просто коллекция свойств (если кому проще, может называть это ассоциативным массивом или списком), состоящая из пар ключ-значение. Причем ключом может быть только строка (даже у элементов массива), а вот значением — любой тип данных из перечисленных ниже.
Итак, в JavaScript есть 6 базовых типов данных — это Undefined (обозначающий отсутствие значения), Null, Boolean (булев тип), String (строка), Number (число) и Object (объект).
При этом первые 5 являются примитивными типами данных, а Object — нет. Кроме того, условно можно считать, что у типа Object есть «подтипы»: массив (Array), функция (Function), регулярное выражение (RegExp) и другие.
Это несколько упрощенное описание, но на практике обычно достаточное.
Кроме того, примитивные типы String, Number и Boolean определенным образом связаны с не-примитивными «подтипами» Object: String, Number и Boolean соответственно.
Это означает, что строку ‘Hello, world’, например, можно создать и как примитивное значение, и как объект типа String.
Если вкратце, то это сделано для того, чтобы программист мог и в работе с примитивными значениями использовать методы и свойства, как будто это объекты. А подробнее об этом можно будет прочитать в соответствующем разделе данной статьи.
Работа по ссылке
Ссылка — это средство доступа к объекту под различными именами. Работа с любыми объектами ведется исключительно по ссылке.
Продемонстрируем это на примере:
test= function ()
//Создадим функцию (а функция, как мы помним, является полноправным объектом) и сделаем переменную test ссылкой на нее
test_link=test; //test_link теперь тоже ссылается на нашу функцию
test(); //Hello!
test_link(); //Hello!
* This source code was highlighted with Source Code Highlighter .
Как мы видим, и первая ссылка, и вторая дают один и тот же результат.
Необходимо осознать, что у нас нет никакой функции с именем test, и что переменная test не является какой-то «главной» или «основной» ссылкой, а «test_link» — второстепенной.
Наша функция, как и любой другой объект — просто область в памяти, и все ссылки на эту область абсолютно равнозначны. Более того, объект может вообще не иметь ссылок — в таком случае он называется анонимным, и может быть использован только непосредственно сразу после создания (например, передан в функцию), иначе доступ к нему получить будет невозможно и в скором времени он будет уничтожен сборщиком мусора (garbage collection), который и занимается тем, что удаляет объекты без ссылок.
Посмотрим, почему так важно это понимать:
test= //Создаем объект со свойством prop
test_link=test; //Создаем еще одну ссылку на этот объект
alert(test.prop); //sometext
alert(test_link.prop); //sometext
//Изменяем свойство объекта
test_link.prop= ‘newtext’ ;
alert(test.prop); //newtext
alert(test_link.prop); //newtext
/*Можно было бы сказать, что свойство изменилось и там и тут — но это не так.
Объект-то один. Так что свойство изменилось в нем один раз, а ссылки просто продолжают указывать туда, куда и указывают. */
//Добавляем новое свойство и удаляем старое
test.new_prop= ‘hello’ ;
delete test.prop;
alert(test_link.prop); //undefined — такого свойства больше нет
alert(test_link.new_prop); //hello — что и следовало ожидать
//Удаляем ссылку
delete test;
alert(test.new_prop);
/*В этом месте скрипт выкинет ошибку, потому что test уже не существует, и test.new_prop не существует тем более */
alert(test_link.new_prop); //hello
/* а вот тут все в порядке, ведь мы удалили не сам объект, а лишь ссылку на него. Теперь на наш объект указывает единственная ссылка test_link */
//Создаем новый объект
test=test_link; //Сперва снова создадим ссылку test
test_link= //А вот и новый объект
alert(test_link.prop); //sometext
alert(test.prop); //undefined
/* Cоздание нового объекта разрывает ссылочную связь, и теперь test и test_link указывают на разные объекты.
Фактически, это равносильно удалению ссылки test_link и созданию ее заново, но уже указывающей на другой объект */
alert(test.new_prop); //hello — теперь test содержит ссылку на наш самый первый объект
* This source code was highlighted with Source Code Highlighter .
Такое поведение объектов часто вызывает массу вопросов у начинающих разработчиков, так что надеюсь данный текст внесет некоторую ясность. Если же мы хотим создать действительно новую, независимую копию объекта, а не ссылку — то единственный способ сделать это — создать новый объект и скопировать туда требуемые свойства.
Также стоит отметить, что работа с объектами по ссылке, помимо вышеперечисленных забавных эффектов дает также значительную экономию памяти, что немаловажно при широком использовании одного объекта в различных местах программы.
Примитивные значения
Как я упоминал выше, типы данных String и Number могут быть как объектами, так и примитивными значениями.
obj= new String( ‘hello’ ); //Создаем строку как объект
simple= ‘hello’ ; //Создаем примитивное значение
alert(obj); //hello
alert(simple); //hello — пока все предсказуемо
alert(obj.length); //6 — у объекта типа String есть свойство length, хранящее длину строки
alert(simple.length); //6
/* Хотя simple — не объект, мы можем обращаться к тому же набору свойств, что и у объекта типа String. Это довольно удобно */
obj.prop= ‘text’ ;
simple.prop= ‘text’ ;
alert(obj.prop); //text — раз obj обычный объект, то мы можем запросто придать ему еще одно свойство
alert(simple.prop); //undefined — а вот simple не объект, и этот номер у нас не пройдет
* This source code was highlighted with Source Code Highlighter .
Все то же самое справедливо и для типа Number, и для Boolean (ну, кроме того, что в них нет свойства length, а есть ряд других замечательных свойств).
Использование строк и чисел как объектов не несет в себе никакой практической пользы, т.к. примитивные значения удобнее в работе, но сохраняют при этом весь необходимый функционал. Тем не менее, для полноты картины необходимо понимать этот механизм.
Не стоит путать использование примитивных значений с использованием литералов — например, независимо от того, создаем мы массив как «test=new Array()» или как «test=[]», в результате все равно будет один и тот же объект. Никаких примитивных значений мы не получим.
Создание и использование объектов
Итак, в отличии от языков, где реализована класс-объектная парадигма, нам не нужно создавать сначала класс, чтобы потом создать объект класса. Мы можем сразу создать объект, что и сделаем в следующем примере:
test= simple_property: ‘Hello’ ,
object_property: user_1: ‘Петя’ ,
user_2: ‘Вася’
>,
function_property: function (user) alert( this .simple_property + ‘, ‘ + this .object_property[user]);
>
>
test.function_property( ‘user_1’ ); //Hello, Петя.
* This source code was highlighted with Source Code Highlighter .
Перед нами объект test, имеющий 3 свойства, названия которых, как я надеюсь, говорят сами за себя. Больше всего нас в нем интересует свойство function_property, содержащее функцию. Такую функцию можно назвать методом объекта.
В нашей функции дважды используется ключевое слово this, которое является указателем (т.е. ссылкой) на объект, из которого вызывается функция. Таким образом, this.simple_property=test.simple_property=’Hello’, а this.object_property[user]=test.object_property[user]=’Петя’.
Необходимо четко осознавать, this всегда указывает именно на объект, из которого вызвана функция, а не на объект, к которому она принадлежит. Хотя в данном примере это один и тот же объект, это не всегда так.
test.function_property( ‘user_1’ ); //Hello, Петя.
test2= new Object(); //Еще одна форма создания нового объекта, аналогичная test2=<>
test.function_property.call(test2, ‘user_1’ ); //ошибка
/* Метод call позволяет вызвать функцию от имени другого объекта. В данном случае, мы вызываем метод function_property объекта test, и его this указывает уже не на объект test, а на объект test2. А т.к. в нем нет свойства object_property, то при попытке получить this.object_property[user]скрипт выдаст ошибку */
//попробуем исправить ситуацию
test2.simple_property= ‘Good day’ ;
test2.object_property=test.object_property; //В данном случае воспользуемся указанием объекта по ссылке, чтобы не дублировать код
test.function_property.call(test2, ‘user_1’ ); //Good day, Петя.
* This source code was highlighted with Source Code Highlighter .
Из примера также должно быть видно, что нет четких этапов создания и использования объекта. Объект может быть как угодно модифицирован в любое время — до, после и даже во время использования. Это тоже важное отличие от «традиционного» ООП.
Конструктор
В примере выше мы создавали 2 объекта, обладающих некой схожестью. И там и там имелись свойства simple_property и object_property. Очевидно, что при написании реального кода также нередко встает задача создания одинаковых или просто похожих объектов. И разумеется, мы не должны каждый такой объект создавать вручную.
На помощь нам придет конструктор. Конструктор в JavaScript — это не часть класса (потому что здесь нет классов), а просто самостоятельная функция. Самая обычная функция.
make_me= function (_name) alert( ‘меня запустили’ );
this .name=_name;
this .show_name= function ()
>
child= new make_me( ‘Вася’ ); //меня запустили
/* Давайте разберемся, что здесь происходит. Интерпретатор видит оператор new и проверяет, что находится справа от него. Т.к. make_me — это функция, и она может быть использована в качестве контруктора, то создается новый объект в памяти и запускается на выполнение функция make_me, причем ее this указывает как раз на этот новый объект. Далее этому объекту добавляется свойство name, которому присваивается значение из аргумента _name, и метод show_name. Также (не знаю в какой именно момент, но это и не важно) переменная child начинает указывать на наш новенький, только что рожденный объект */
alert(child.name); //Вася
child.show_name(); //Вася
child2= new make_me( ‘Петя’ );
child2.show_name(); //Петя
child2.show_name= function ()
child2.show_name(); //Не буду говорить свое имя
child.show_name(); //Вася — дети никак не влияют друг на друга
* This source code was highlighted with Source Code Highlighter .
Также можно сравнить конструктора с отцом — он порождает ребенка, наделяя его определенными качествами, но сразу после создания ребенок становится полностью независим от родителя и может стать очень непохожим на своих братьев.
Если мы вспомним про описание типов данных в начале статьи, то становится понятно, что Object и его подтипы (Function, Array и другие) — это на самом деле конструкторы, придающие создаваемому объекту возможности функции, массива и т.д.
Итак, это уже намного лучше. У нас теперь есть возможность создавать объекты по некоторому образцу. Однако, не все еще хорошо. Во-первых, каждый созданный нами объект и все его свойства и методы занимают отдельное место в памяти, хотя во многом они повторяются. Во-вторых, как быть, если мы хотим сохранить связь между родителем и ребенком, и иметь возможность менять все дочерние объекты разом. На помощь нам придет прототип.
Прототип
Как у каждого ребенка есть отец и мать (хотя бы в биологическом смысле), также они есть и у каждого объекта в JavaScript. И если отец, как мы определелись, работает конструктором, то мать — это как раз прототип. Посмотрим, как это происходит:
make_me= function (_name) alert( ‘меня запустили’ );
this .name=_name;
this .show_name= function ()
>
/*
Видя ключевое слово function, интерпретатор проверяет код справа от него, и т.к. все ок — создает новый объект в памяти, который одновременно является нашей функцией. Затем, автоматически (без участия программиста) для этой функции создается свойство prototype, ссылающееся на пустой объект. Если бы мы это делали вручную, это выглядело бы как make_me.prototype=new Object();
Затем, данному объекту (на который указывает свойство prototype) также автоматически добавляется свойство constructor, указывающее обратно на функцию. Получается такая вот циклическая ссылка.
Теперь этот объект, который можно описать как — и есть прототип функции.
*/
alert( typeof make_me.prototype); //Object — действительно, объект
alert( typeof make_me.prototype.constructor); //Function — это наша функция
alert(make_me.prototype.constructor === make_me); //true
make_me.prototype.set_name= function (_name) < this .name=_name;>//Добавляем в прототип функции make_me новый метод
child= new make_me( ‘Вася’ ); //меня запустили
/* Теперь помимо всего того, что описано в предыдущем примере, дополнительно в объекте child создается скрытое свойство [[Prototype]], которое указывает на тот же объект, что и make_me.prototype. Т.к. свойство скрыто, мы не можем ни просмотреть его значение, ни изменить его — однако оно играет важную роль в дальнейшей работе */
alert(child.name); //Вася
child.show_name(); //Вася
child.set_name( ‘Коля’ );
/* Сначала, интерпретатор ищет метод set_name в объекте child. Так как его там нет, он продолжает поиск в свойстве child.[[Prototype]], находит его там и запускает. */
child.show_name(); //Коля — теперь Васю зовут Коля 🙂
make_me.prototype.show_name2= function ()
child2= new make_me( ‘Петя’ );
child2.show_name2(); //Привет, Петя
child.show_name2(); //Привет, Коля — изменения в прототипе влияют не только на вновь созданные объекты, но и на все старые
child2.show_name2= function ()
child2.show_name2(); //Не буду говорить свое имя — т.к. у нас теперь есть собственный метод show_name2, то он и вызывается, и поиск в прототипе не происходит
child.show_name2(); //Привет, Коля — здесь все по прежнему
make_me.prototype= //Попробуем пересоздать прототип заново
alert(child.prop); //undefined
child.show_name2(); //Привет, Коля
/* Если вспомнить, что такое работа по ссылке, то все понятно. Пересоздание прототипа рвет связь, и теперь свойство [[Prototype]] у объектов child и child2 указывают на один объект (который раньше был прототипом функции make_me), а свойство make_me.prototype — на другой объект, который является новым прототипом функции make_me */
child3= new make_me( ‘Олег’ );
alert(child3.prop); //hello — что и следовало ожидать
* This source code was highlighted with Source Code Highlighter .
Как видно из примера, пока отец сохраняет верность матери (т.е. пока протип функции остается прежним), все дети зависят от матери и чутко реагируют на все изменения в ней. Однако, стоит только родителям развестись (конструктор меняет прототип на другой) — дети тут же разбегаются кто куда и больше связи с ними нет.
Немного о терминологии
До тех пор, пока первичная связь между конструктором и прототипом не разорвана, мы можем наблюдать следующую картину:
make_me= function (_name) alert( ‘меня запустили’ );
this .name=_name;
this .show_name= function ()
>
make_me.prototype.set_name= function (_name) < this .name=_name;>
child= new make_me( ‘Вася’ );
alert( typeof make_me.prototype); //object — у функции есть свойство prototype
alert( typeof child.prototype); //undefined — у созданного объекта НЕТ свойства prototype
alert(child.constructor.prototype === make_me.prototype); //true — зато у объекта есть свойство constructor, которое указывает на функцию-конструктор make_me, у которой, в свою очередь, есть свойство prototype
* This source code was highlighted with Source Code Highlighter .
Как я заметил после чтения многочисленных форумов на эту тему, основные проблемы возникают у людей, когда они путают свойство prototype у функции и скрытое свойство [[Prototype]] у объекта, созданного с помощью этой функции.
Оба этих свойства являются ссылкой на один и тот же объект (до тех пор, пока первичная связь прототипа с конструктором не нарушена), но это тем не менее разные свойства, с разными именами, одно из них доступно для программиста, а другое нет.
Необходимо всегда четко понимать, что если речь идет о прототипе конструктора — то это всегда свойство prototype, а если о прототипе созданного объекта — то это скрытое свойство [[Prototype]].
Наследование
Теперь мы знаем, что у каждого объекта есть скрытая ссылка на прототип, а каждый прототип — это обычный объект.
Наиболее чуткие читатели уже уловили запах рекурсии 🙂
Действительно, т.к. прототип — это обычный объект, то и он в свою очередь имеет ссылку на свой прототип, и так далее. Таким образом реализуется иерархия прототипов.
bird= function () <> //Это конструктор птички
bird.prototype.cry= function ()//Птичка умеет кричать
bird.prototype.fly= function ()//и летать
duck= function () <>
duck.prototype= new bird();
duck.prototype.cry= function ()
duck.prototype.constructor=duck; //Принудительно устанавливаем свойство prototype.constructor в duck, т.к. иначе оно будет ссылаться на bird
billy = new duck(); //Билли — это наша утка
billy.fly(); //Я лечу! — Билли может летать, потому что он птица
billy.cry(); //Кря-кря! — Билли кричит кря-кря, потому что он утка
* This source code was highlighted with Source Code Highlighter .
Так можно реализовывать иерархию любого уровня вложенности.
Задача на звездочку
Теперь, раз уж мы столько знаем обо всем этом, давайте попробуем разобраться, сколько всего происходит в этих трех строчках
make_me= function () <>
child= new make_me();
alert(child.toString()); //выводит [object]* This source code was highlighted with Source Code Highlighter .
В первой строке мы создаем новую функцию и переменную make_me, которая указывает на эту функцию. При этом создается прототип функции, make_me.prototype, в котором содержится свойство constructor, указывающее на make_me.
Но это далеко не все 🙂
Т.к. функция make_me — это тоже объект, то он в свою очередь имеет папу и маму, т.е. конструктор и прототип. Его конструктор — это родная функция языка Function(), а прототип — объект, содержащий в себе методы call, apply и т.д. — именно благодаря этому прототипу мы и можем пользоваться этими методами в любой функции. Таким образом, у функции make_me появляется свойство [[Prototype]], указывающее на Function.prototype.
В свою очередь, прототип конструктора Function — тоже объект, конструктором которого является (сюрприз!) Object (т.е. Function.prototype.[[Prototype]].constructor===Object), а прототипом — объект, содержащий стандартные свойства и методы объекта, такие как toString, hasOwnProperty и другие (другими словами — Function.prototype.[[Prototype]][‘hasOwnProperty’] — это как раз тот самый метод, которым мы можем пользоваться во всех производных объектах — причем это именно собственной метод данного объекта, а не наследованный). Вот таким вот интересным образом мы обнаруживаем, что все виды объектов являются производными от Object.
Можем ли мы продолжить дальше? Оказывается, нет. Object.prototype именно потому и содержит базовые свойства объекта, что не имеет собственного прототипа. Object.prototype.[[Prototype]]=null; В этом месте путешествие по цепочке прототипов в поиске свойства или метода прекращается.
Еще один интересный факт — конструктором Object является Function. Т.е. Object.[[Prototype]].constructor===Function.
Налицо еще одна циклическая ссылка — конструктор Object это Function, а конструктор Function.prototype — это Object.
Вернемся к нашему примеру. Как создается функция мы уже поняли, теперь перейдем ко второй строке. Там мы создаем объект child, конструктором которого является функция make_me, а прототипом — make_me.prototype.
Ну и в третей строчке мы видим, как интепретатор поднимается по цепочке, от child к child.[[Prototype]] (он же make_me.prototype), затем к child.[[Prototype]].[[Prototype]] (он же Object.prototype), и уже там находит метод toString, который и запускает на выполнение.
Примеси
Может показаться, что наследование через прототипы — единственный способ, возможный в JavaScript. Это не так.
Мы имеем дело с очень гибким языком, который предоставляет не столько правила, сколько возможности.
Например, при желании мы можем вообще не использовать прототипы, а программировать с помощью концепции примесей. Для этого нам пригодятся наши старые добрые друзья — конструкторы.
//Это конструктор человека
man= function () this .live= function ()//Человек умеет жить
this .walk= function ()//Человек умеет ходить
>
//Это конструктор поэта
poet= function () this .kill= function ()
this .live= function ()
>
vladimir= new man(); //Владимир — человек
vladimir.live(); //Я живу — он жив
vladimir.walk(); //Я иду — он ходит
poet.call(vladimir); //Выполняем конструктор poet для объекта vladimir
vladimir.kill(); //Поэт убил человека
vladimir.live(); //Я мертв
//А теперь фокус
man.call(vladimir);
vladimir.live(); //Я живу
* This source code was highlighted with Source Code Highlighter .
Что мы видим в данном примере? Во-первых, это возможность наследования от нескольких объектов, не находящихся в одной иерархии. В примере их 2, но может быть сколько угодно.
Во-вторых, это отсутствие какой-либо иерархии вообще. Переопределение свойств и методов определяется исключительно поряком вызова конструкторов.
В-третьих, это возможность еще более динамически менять объект, причем именно отдельный объект, а не всех потомков, как при изменении прототипа.
Upd: Замыкания и приватные свойства
Чтобы не раздувать эту и без того немаленькую статью, даю ссылку на пост Замыкания в JavaScript, где про это довольно подробно написано.
Что теперь со всем этим делать
Как я уже говорил выше, и произвольное изменение отдельных объектов, и использование конструкторов, и примеси, и гибкость прототипов — это лишь инструменты, возможности, которые позволяют программисту создать как ужасный, так и прекрасный во всех отношениях код. Важно лишь понимать, какие задачи мы решаем, какими средствами, какие цели достигаем и какую цену платим за это.
Причем вопрос о цене довольно нетривиален, особенно если мы говорим о разработке под браузер Internet Explorer 6 и 7 версий.
1. Память — тут все просто. Во всех браузерах наследование на прототипах отнимает в разы меньше памяти, чем при создании методов через конструкторы. Причем, чем больше методов и свойств у нас есть, тем больше разница. Однако, стоит помнить, что если у нас не тысяча одинаковых объектов а всего лишь один, то расходы памяти в любом случае будут небольшими, т.к. здесь стоит учитывать другие факторы.
2. Процессорное время — здесь основные тонкости связанны именно с браузерами от Microsoft.
С одной стороны, объекты, где методы и свойства создаются через конструктор — могут создаваться в разы (в некоторых случаях в десятки и сотни раз) медленнее, чем через прототип. Чем больше методов — тем медленнее. Так что если у вас в IE замирает на несколько секунд во время инициализации скрипта — есть повод копать в эту сторону.
С другой стороны, собственные методы объекта (созданные через конструктор) могут выполняется немного быстрее, чем прототипные. В случае, если позарез необходимо ускорить именно выполнение какого-то метода в этом браузере, то нужно это учесть. Имейте ввиду, ускоряется именно вызов метода (т.е. поиск его в объекте), а не его выполнение. Так что если сам метод у вас выполняется секунду, то особого увеличения быстродействия вы не заметите.
В других браузерах подобных проблем наблюдается, там время создания объектов и вызова их методов примерно одинаково для обоих подходов.
P.S. Обычно в статьях подобного рода автор предлагает некую обертку, либо пытающуюся реализовать класс-объектное наследование на базе прототипного, либо просто синтаксический сахар для прототипного наследования. Я не делаю этого намеренно, т.к. считаю, что человек, понявший смысл данной статьи, способен сам для себя написать любую обертку, и еще много интересных вещей 🙂
Объекты
Как мы знаем из главы Типы данных, в JavaScript существует 8 типов данных. Семь из них называются «примитивными», так как содержат только одно значение (будь то строка, число или что-то другое).
Объекты же используются для хранения коллекций различных значений и более сложных сущностей. В JavaScript объекты используются очень часто, это одна из основ языка. Поэтому мы должны понять их, прежде чем углубляться куда-либо ещё.
Объект может быть создан с помощью фигурных скобок с необязательным списком свойств. Свойство – это пара «ключ: значение», где ключ – это строка (также называемая «именем свойства»), а значение может быть чем угодно.
Мы можем представить объект в виде ящика с подписанными папками. Каждый элемент данных хранится в своей папке, на которой написан ключ. По ключу папку легко найти, удалить или добавить в неё что-либо.
Пустой объект («пустой ящик») можно создать, используя один из двух вариантов синтаксиса:
let user = new Object(); // синтаксис "конструктор объекта" let user = <>; // синтаксис "литерал объекта"
Обычно используют вариант с фигурными скобками <. >. Такое объявление называют литералом объекта или литеральной нотацией.
Литералы и свойства
При использовании литерального синтаксиса <. >мы сразу можем поместить в объект несколько свойств в виде пар «ключ: значение»:
let user = < // объект name: "John", // под ключом "name" хранится значение "John" age: 30 // под ключом "age" хранится значение 30 >;
У каждого свойства есть ключ (также называемый «имя» или «идентификатор»). После имени свойства следует двоеточие «:» , и затем указывается значение свойства. Если в объекте несколько свойств, то они перечисляются через запятую.
В объекте user сейчас находятся два свойства:
- Первое свойство с именем «name» и значением «John» .
- Второе свойство с именем «age» и значением 30 .
Можно сказать, что наш объект user – это ящик с двумя папками, подписанными «name» и «age».
Мы можем в любой момент добавить в него новые папки, удалить папки или прочитать содержимое любой папки.
Для обращения к свойствам используется запись «через точку»:
// получаем свойства объекта: alert( user.name ); // John alert( user.age ); // 30
Значение может быть любого типа. Давайте добавим свойство с логическим значением:
user.isAdmin = true;
Для удаления свойства мы можем использовать оператор delete :
delete user.age;
Имя свойства может состоять из нескольких слов, но тогда оно должно быть заключено в кавычки:
let user = < name: "John", age: 30, "likes birds": true // имя свойства из нескольких слов должно быть в кавычках >;
Последнее свойство объекта может заканчиваться запятой:
let user =
Это называется «висячая запятая». Такой подход упрощает добавление, удаление и перемещение свойств, так как все строки объекта становятся одинаковыми.
Объект, объявленный как константа, может быть изменён
Объект, объявленный через const , может быть изменён.
const user = < name: "John" >; user.name = "Pete"; // (*) alert(user.name); // Pete
Может показаться, что строка (*) должна вызвать ошибку, но нет, здесь всё в порядке. Дело в том, что объявление const защищает от изменений только саму переменную user , а не её содержимое.
Определение const выдаст ошибку только если мы присвоим переменной другое значение: user=. .
Есть ещё один способ сделать константами свойства объекта, который мы рассмотрим в главе Флаги и дескрипторы свойств.
Квадратные скобки
Для свойств, имена которых состоят из нескольких слов, доступ к значению «через точку» не работает:
// это вызовет синтаксическую ошибку user.likes birds = true
JavaScript видит, что мы обращаемся к свойству user.likes , а затем идёт непонятное слово birds . В итоге синтаксическая ошибка.
Точка требует, чтобы ключ был именован по правилам именования переменных. То есть не имел пробелов, не начинался с цифры и не содержал специальные символы, кроме $ и _ .
Для таких случаев существует альтернативный способ доступа к свойствам через квадратные скобки. Такой способ сработает с любым именем свойства:
let user = <>; // присваивание значения свойству user["likes birds"] = true; // получение значения свойства alert(user["likes birds"]); // true // удаление свойства delete user["likes birds"];
Сейчас всё в порядке. Обратите внимание, что строка в квадратных скобках заключена в кавычки (подойдёт любой тип кавычек).
Квадратные скобки также позволяют обратиться к свойству, имя которого может быть результатом выражения. Например, имя свойства может храниться в переменной:
let key = "likes birds"; // то же самое, что и user["likes birds"] = true; user[key] = true;
Здесь переменная key может быть вычислена во время выполнения кода или зависеть от пользовательского ввода. После этого мы используем её для доступа к свойству. Это даёт нам большую гибкость.
let user = < name: "John", age: 30 >; let key = prompt("Что вы хотите узнать о пользователе?", "name"); // доступ к свойству через переменную alert( user[key] ); // John (если ввели "name")
Запись «через точку» такого не позволяет:
let user = < name: "John", age: 30 >; let key = "name"; alert( user.key ); // undefined
Вычисляемые свойства
Мы можем использовать квадратные скобки в литеральной нотации для создания вычисляемого свойства.
let fruit = prompt("Какой фрукт купить?", "apple"); let bag = < [fruit]: 5, // имя свойства будет взято из переменной fruit >; alert( bag.apple ); // 5, если fruit="apple"
Смысл вычисляемого свойства прост: запись [fruit] означает, что имя свойства необходимо взять из переменной fruit .
И если посетитель введёт слово «apple» , то в объекте bag теперь будет лежать свойство .
По сути, пример выше работает так же, как и следующий пример:
let fruit = prompt("Какой фрукт купить?", "apple"); let bag = <>; // имя свойства будет взято из переменной fruit bag[fruit] = 5;
…Но первый пример выглядит лаконичнее.
Мы можем использовать и более сложные выражения в квадратных скобках:
let fruit = 'apple'; let bag = < [fruit + 'Computers']: 5 // bag.appleComputers = 5 >;
Квадратные скобки дают намного больше возможностей, чем запись через точку. Они позволяют использовать любые имена свойств и переменные, хотя и требуют более громоздких конструкций кода.
Подведём итог: в большинстве случаев, когда имена свойств известны и просты, используется запись через точку. Если же нам нужно что-то более сложное, то мы используем квадратные скобки.
Свойство из переменной
В реальном коде часто нам необходимо использовать существующие переменные как значения для свойств с тем же именем.
function makeUser(name, age) < return < name: name, age: age // . другие свойства >; > let user = makeUser("John", 30); alert(user.name); // John
В примере выше название свойств name и age совпадают с названиями переменных, которые мы подставляем в качестве значений этих свойств. Такой подход настолько распространён, что существуют специальные короткие свойства для упрощения этой записи.
Вместо name:name мы можем написать просто name :
function makeUser(name, age) < return < name, // то же самое, что и name: name age // то же самое, что и age: age // . >; >
Мы можем использовать как обычные свойства, так и короткие в одном и том же объекте:
let user = < name, // тоже самое, что и name:name age: 30 >;
Ограничения на имена свойств
Как мы уже знаем, имя переменной не может совпадать с зарезервированными словами, такими как «for», «let», «return» и т.д.
Но для свойств объекта такого ограничения нет:
// эти имена свойств допустимы let obj = < for: 1, let: 2, return: 3 >; alert( obj.for + obj.let + obj.return ); // 6
Иными словами, нет никаких ограничений к именам свойств. Они могут быть в виде строк или символов (специальный тип для идентификаторов, который будет рассмотрен позже).
Все другие типы данных будут автоматически преобразованы к строке.
Например, если использовать число 0 в качестве ключа, то оно превратится в строку «0» :
let obj = < 0: "Тест" // то же самое что и "0": "Тест" >; // обе функции alert выведут одно и то же свойство (число 0 преобразуется в строку "0") alert( obj["0"] ); // Тест alert( obj[0] ); // Тест (то же свойство)
Есть небольшой подводный камень, связанный со специальным свойством __proto__ . Мы не можем установить его в необъектное значение:
let obj = <>; obj.__proto__ = 5; // присвоим число alert(obj.__proto__); // [object Object], значение - это объект, т.е. не то, что мы ожидали
Как мы видим, присвоение примитивного значения 5 игнорируется.
Мы более подробно исследуем особенности свойства __proto__ в следующих главах Прототипное наследование, а также предложим способы исправления такого поведения.
Проверка существования свойства, оператор «in»
В отличие от многих других языков, особенность JavaScript-объектов в том, что можно получить доступ к любому свойству. Даже если свойства не существует – ошибки не будет!
При обращении к свойству, которого нет, возвращается undefined . Это позволяет просто проверить существование свойства:
let user = <>; alert( user.noSuchProperty === undefined ); // true означает "свойства нет"
Также существует специальный оператор «in» для проверки существования свойства в объекте.
"key" in object
