Объекты в JavaScript. Создание объектов JavaScript

08.07.2019

Объект – это неупорядоченная коллекция свойств. Свойство – это часть объекта имитирующая переменную. Свойство состоит из имени и значения.

В JavaScript есть три категории объектов:

  • Объекты базового типа – это объекты, определённые в спецификации ECMAScript. Например, объекты типа Array , Function , Date или RegExp являются объектами базового типа.
  • Объекты среды выполнения – это объекты, определённые в среде выполнения (такой как браузер). Например, объекты типа HTMLElement , являются объектами среды выполнения.
  • Пользовательские объекты – это любой объект, созданный в результате выполнения программного кода JavaScript.

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

Объект можно создать с помощью литерала объекта или оператора new с конструктором.

Литерал объекта – это заключённый в фигурные скобки список из нуля или более свойств (пар имя: значение), разделённых запятыми. Именем свойства может быть любой допустимый идентификатор, строковой литерал (допускается использовать пустую строку) или число. Числовые имена свойств автоматически преобразуются в строки. Значением свойства может быть значение любого типа или выражение (значением свойства в этом случае станет результат вычисления выражения):

// Создание пустого объекта var o = {}; // Создание объекта с тремя свойствами var user = { name: "Homer", "age": 45, 1: true };

Создание объекта с помощью оператора new :

Var o = new Object();

Операции с объектом

Основные операции производимые с объектами – это добавление новых свойств, изменение уже существующих свойств, удаление свойств и обращение к свойствам.

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

Обращение к свойству и изменение значения осуществляется точно так же (с помощью операторов доступа):

Var o = {x:5}; alert(o.x); // Обращение к свойству alert(o["x"]); // Обращение к свойству o.x = 10; // Изменяем значение

Удаление свойства осуществляется с помощью оператора delete :

Var o = {x:5}; alert("x" in o); // true delete o.x; alert("x" in o); // false

Для перебора свойств объекта используется цикл for-in :

Var obj = {x: 5, y: 10, str: "Hi!"}; for (var prop in obj) { alert(prop); }

Методы объекта

Свойство, значением которого является функция, называется методом . Вызов метода осуществляется точно также, как и вызов обычной функции – с помощью оператора () (оператор вызова):

Var o = { sayHi: function() { alert("Hello!"); } }; o.sayHi(); // "Hello!"

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

Var o = { name: "Homer", sayName: function() { alert(this.name); } }; o.sayName(); // "Homer"

Вместо ключевого слова this можно использовать непосредственно имя объекта, но это не очень удобно, так как, если изменится имя объекта, в методах придётся также изменять имя:

Var o = { name: "Homer", sayName: function() { alert(o.name); } }; o.sayName(); // "Homer"




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

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

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

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

Объект

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

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

  • не является допустимым JavaScript-идентификатором (в нем есть пробел, тире, начинается с цифры...)
  • является переменной.
Одно из свойств, которое объекты в JS получают при создании, называется Prototype , и это очень важное понятие.

Прототип

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

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

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

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

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

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

Var cons = function () { this.a = 1; this.b = 2; } var obj = new cons(); cons.prototype.b = 3; cons.prototype.c = 4;
cons - это конструктор (просто функция, которую можно вызывать с помощью оператора new ).

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

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

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

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

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

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

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

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

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

ES6 классы:

Class rectangle { constructor(height, width) { this.height = height; this.width = width; } getArea() { return this.height * this.width; } } let square = new rectangle(2, 2);
Square - экземпляр конструктора rectangle , и поэтому мы можем вызвать square.getArea() //4 , square.width , а также все функции, унаследованные от object.prototype .

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

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

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

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

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

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

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

Равенство

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

// Two distinct objects with the same properties are not equal var fruit = {name: "apple"}; var fruitbear = {name: "apple"}; fruit === fruitbear; // return false // here fruit and fruitbear are pointing to same object var fruit = {name: "apple"}; var fruitbear = fruit; fruit === fruitbear; // return true
Итак, вы скорее всего уже задались вопросом, как можно сравнить объекты или как производить с объектами различные манипуляции, учитывая требование к их неизменности.

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

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

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

Var obj = { a: 1, b: 2}; var newObj = Object.assign({}, obj,{a:2}) // {a: 2, b: 2 }
Если мы захотим изменить значение свойства a объекта obj , можно воспользоваться object.assign для создания копии obj и ее изменения.

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

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

Const obj = {a: 1, b: { a: 1 } }; // b property is an object
Object.assign() копирует свойства объекта, поэтому если значение свойства - это указатель на объект, то копируется только указатель.

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

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

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

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

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

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

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

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

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

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

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

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

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

В заключение

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

depp 7 января 2009 в 23:56

Работа с объектами в JavaScript: теория и практика

  • 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 () {alert("Hello!" )} //Создадим функцию {alert("Hello!")} (а функция, как мы помним, является полноправным объектом) и сделаем переменную test ссылкой на нее
test_link=test; //test_link теперь тоже ссылается на нашу функцию
test(); //Hello!
test_link(); //Hello!


Как мы видим, и первая ссылка, и вторая дают один и тот же результат.
Необходимо осознать, что у нас нет никакой функции с именем test, и что переменная test не является какой-то «главной» или «основной» ссылкой, а «test_link» - второстепенной.

Наша функция, как и любой другой объект - просто область в памяти, и все ссылки на эту область абсолютно равнозначны. Более того, объект может вообще не иметь ссылок - в таком случае он называется анонимным, и может быть использован только непосредственно сразу после создания (например, передан в функцию), иначе доступ к нему получить будет невозможно и в скором времени он будет уничтожен сборщиком мусора (garbage collection), который и занимается тем, что удаляет объекты без ссылок.

Посмотрим, почему так важно это понимать:

test={prop: "sometext" } //Создаем объект со свойством prop
test_link=test; //Создаем еще одну ссылку на этот объект

Alert(test.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);

//Удаляем ссылку
delete test;
alert(test.new_prop);
/*В этом месте скрипт выкинет ошибку, потому что test уже не существует, и test.new_prop не существует тем более */
alert(test_link.new_prop); //hello
/* а вот тут все в порядке, ведь мы удалили не сам объект, а лишь ссылку на него. Теперь на наш объект указывает единственная ссылка test_link */

//Создаем новый объект
test=test_link; //Сперва снова создадим ссылку test
test_link={prop: "sometext" } //А вот и новый объект

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);
}
}

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=test.object_property="Петя".

Необходимо четко осознавать, 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скрипт выдаст ошибку */

//попробуем исправить ситуацию
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;

}


/* Давайте разберемся, что здесь происходит. Интерпретатор видит оператор new и проверяет, что находится справа от него. Т.к. make_me - это функция, и она может быть использована в качестве контруктора, то создается новый объект в памяти и запускается на выполнение функция make_me, причем ее this указывает как раз на этот новый объект. Далее этому объекту добавляется свойство name, которому присваивается значение из аргумента _name, и метод show_name. Также (не знаю в какой именно момент, но это и не важно) переменная child начинает указывать на наш новенький, только что рожденный объект */

Alert(child.name); //Вася
child.show_name(); //Вася


child2.show_name(); //Петя

Child2.show_name=function () {alert("Не буду говорить свое имя" );} //Не забываем, что можем изменять наши объекты в любой момент
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 () {alert(this .name);}
}
/*
Видя ключевое слово function, интерпретатор проверяет код справа от него, и т.к. все ок - создает новый объект в памяти, который одновременно является нашей функцией. Затем, автоматически (без участия программиста) для этой функции создается свойство prototype, ссылающееся на пустой объект. Если бы мы это делали вручную, это выглядело бы как make_me.prototype=new Object();

Затем, данному объекту (на который указывает свойство prototype) также автоматически добавляется свойство constructor, указывающее обратно на функцию. Получается такая вот циклическая ссылка.

Теперь этот объект, который можно описать как {constructor: ...здесь ссылка на фунцию...} - и есть прототип функции.
*/

//Object - действительно, объект
alert(typeof make_me.prototype.constructor); //Function - это наша функция
alert(make_me.prototype.constructor === make_me); //true

//Добавляем в прототип функции make_me новый метод

Child=new make_me("Вася" ); //меня запустили
/* Теперь помимо всего того, что описано в предыдущем примере, дополнительно в объекте child создается скрытое свойство [], которое указывает на тот же объект, что и make_me.prototype. Т.к. свойство скрыто, мы не можем ни просмотреть его значение, ни изменить его - однако оно играет важную роль в дальнейшей работе */

Alert(child.name); //Вася
child.show_name(); //Вася

Child.set_name("Коля" );
/* Сначала, интерпретатор ищет метод set_name в объекте child. Так как его там нет, он продолжает поиск в свойстве child.[], находит его там и запускает. */
child.show_name(); //Коля - теперь Васю зовут Коля:)

Make_me.prototype.show_name2=function () {alert("Привет, " + this .name;} //Т.к. прототип - это обычный объект, мы точно также можем его менять на лету

Child2=new make_me("Петя" );
child2.show_name2(); //Привет, Петя
child.show_name2(); //Привет, Коля - изменения в прототипе влияют не только на вновь созданные объекты, но и на все старые

Child2.show_name2=function () {alert("Не буду говорить свое имя" );} //Мы по прежнему можем изменить сам объект, при этом новый метод show_name2 в данном объекте (и только в нем) как бы "затрет" старый метод из прототипа
child2.show_name2(); //Не буду говорить свое имя - т.к. у нас теперь есть собственный метод show_name2, то он и вызывается, и поиск в прототипе не происходит

Child.show_name2(); //Привет, Коля - здесь все по прежнему

Make_me.prototype={prop: "hello" } //Попробуем пересоздать прототип заново

Alert(child.prop); //undefined
child.show_name2(); //Привет, Коля
/* Если вспомнить, что такое работа по ссылке, то все понятно. Пересоздание прототипа рвет связь, и теперь свойство [] у объектов 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 () {alert(this .name);}
}

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, а если о прототипе созданного объекта - то это скрытое свойство [].

Наследование

Теперь мы знаем, что у каждого объекта есть скрытая ссылка на прототип, а каждый прототип - это обычный объект.
Наиболее чуткие читатели уже уловили запах рекурсии:)
Действительно, т.к. прототип - это обычный объект, то и он в свою очередь имеет ссылку на свой прототип, и так далее. Таким образом реализуется иерархия прототипов.
bird=function () {} //Это конструктор птички
bird.prototype.cry=function (){alert("Кри!" );} //Птичка умеет кричать
bird.prototype.fly=function (){alert("Я лечу!" );} //и летать

Duck=function () {}
duck.prototype=new bird();
duck.prototype.cry=function (){alert("Кря-кря!" );} //Утка кричит по другому
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()); //выводит

* This source code was highlighted with Source Code Highlighter .

В первой строке мы создаем новую функцию и переменную make_me, которая указывает на эту функцию. При этом создается прототип функции, make_me.prototype, в котором содержится свойство constructor, указывающее на make_me.
Но это далеко не все:)
Т.к. функция make_me - это тоже объект, то он в свою очередь имеет папу и маму, т.е. конструктор и прототип. Его конструктор - это родная функция языка Function(), а прототип - объект, содержащий в себе методы call, apply и т.д. - именно благодаря этому прототипу мы и можем пользоваться этими методами в любой функции. Таким образом, у функции make_me появляется свойство [], указывающее на Function.prototype.

В свою очередь, прототип конструктора Function - тоже объект, конструктором которого является (сюрприз!) Object (т.е. Function.prototype.[].constructor===Object), а прототипом - объект, содержащий стандартные свойства и методы объекта, такие как toString, hasOwnProperty и другие (другими словами - Function.prototype.[]["hasOwnProperty"] - это как раз тот самый метод, которым мы можем пользоваться во всех производных объектах - причем это именно собственной метод данного объекта, а не наследованный). Вот таким вот интересным образом мы обнаруживаем, что все виды объектов являются производными от Object.

Можем ли мы продолжить дальше? Оказывается, нет. Object.prototype именно потому и содержит базовые свойства объекта, что не имеет собственного прототипа. Object.prototype.[]=null; В этом месте путешествие по цепочке прототипов в поиске свойства или метода прекращается.

Еще один интересный факт - конструктором Object является Function. Т.е. Object.[].constructor===Function.
Налицо еще одна циклическая ссылка - конструктор Object это Function, а конструктор Function.prototype - это Object.

Вернемся к нашему примеру. Как создается функция мы уже поняли, теперь перейдем ко второй строке. Там мы создаем объект child, конструктором которого является функция make_me, а прототипом - make_me.prototype.

Ну и в третей строчке мы видим, как интепретатор поднимается по цепочке, от child к child.[] (он же make_me.prototype), затем к child.[].[] (он же Object.prototype), и уже там находит метод toString, который и запускает на выполнение.

Примеси

Может показаться, что наследование через прототипы - единственный способ, возможный в JavaScript. Это не так.
Мы имеем дело с очень гибким языком, который предоставляет не столько правила, сколько возможности.

Например, при желании мы можем вообще не использовать прототипы, а программировать с помощью концепции примесей. Для этого нам пригодятся наши старые добрые друзья - конструкторы.

//Это конструктор человека
man=function () {
this .live=function (){alert("Я живу" );} //Человек умеет жить
this .walk=function (){alert("Я иду" );} //Человек умеет ходить
}

//Это конструктор поэта
poet=function (){
this .kill=function (){alert("Поэт убил человека" );} //Поэт может убить человека
this .live=function (){alert("Я мертв" );} //От этого человек умрет
}

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: Замыкания и приватные свойства

Чтобы не раздувать эту и без того немаленькую статью, даю ссылку на пост , где про это довольно подробно написано.

Что теперь со всем этим делать

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

Причем вопрос о цене довольно нетривиален, особенно если мы говорим о разработке под браузер Internet Explorer 6 и 7 версий.
1. Память - тут все просто. Во всех браузерах наследование на прототипах отнимает в разы меньше памяти, чем при создании методов через конструкторы. Причем, чем больше методов и свойств у нас есть, тем больше разница. Однако, стоит помнить, что если у нас не тысяча одинаковых объектов а всего лишь один, то расходы памяти в любом случае будут небольшими, т.к. здесь стоит учитывать другие факторы.
2. Процессорное время - здесь основные тонкости связанны именно с браузерами от Microsoft.
С одной стороны, объекты, где методы и свойства создаются через конструктор - могут создаваться в разы (в некоторых случаях в десятки и сотни раз) медленнее, чем через прототип. Чем больше методов - тем медленнее. Так что если у вас в IE замирает на несколько секунд во время инициализации скрипта - есть повод копать в эту сторону.

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

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

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

Теги:

  • ооп
  • наследование
  • javascript
Добавить метки

Посещение веб-ресурса - это конкретный URI в адресной строке браузера. Посетитель указывает адрес страницы, и она разбирается браузером на элементы дерева DOM - Document Object Model. Любая ссылка на этой странице указывает браузеру разобрать другую страницу и построить иное дерево объектов.

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

Фактически действия пользователя - это перемещение между системами объектов, образуемых в процессе посещения страниц. Каждая страница - это собственное дерево DOM и, помимо того, JavaScript object"s - это объекты синтаксиса самого языка и пользовательских описаний.

DOM: загрузка, обновление и изменение

Есть три основных варианта, которые формируют объекты страницы веб-ресурса, как на уровне DOM и самого языка JavaScript, выполнившего конструкции создания переменных, так и на основании описаний, сделанных разработчиком:

  • загрузка - посетитель пришел на страницу сайта;
  • обновление - посетитель (кнопка браузера или Ctrl-F5);
  • изменение элемента страницы, например (AJAX, скрипт, событие, ...).

Все три процесса кардинально различаются, но отличать особенности первых двух особенно важно. Трудно запретить посетителю обновлять страницу - это неискоренимая «пагубная» привычка посетителя, которую разработчику следует иметь ввиду.

Навигация по странице и за её пределы должна лежать исключительно в функционале самой страницы, а не в истории посещений браузера и функциях его кнопок. Многие сайты декларируют это важное требование, но посетители традиционно его нарушают.

Изменение страницы без перезагрузки на уровне отдельного её элемента (например, AJAX) - это обычное решение для динамичных страниц. Как правило, это используется для переходов по элементам страницы, изменения её объектов, управления диалогом с посетителем.

Фундаментальные объекты JavaScript

JavaScript основан на объектах. Практически все переменные языка - это объекты. Разработчик может формулировать собственные описания объектов, используя разнообразные варианты синтаксиса.

Всё, не являющееся "строкой", "числом", true, false, null или undefined, является объектом. В рамках синтаксиса языка этому можно не придавать значения, понимая под объектами только элементы DOM и собственные описания JavaScript Object"s. Фундаментальное строение языка в большинстве случаев для разработчика не имеет существенного практического значения.

Например, математические функции представлены объектом Math. Это удобно в рамках концепции языка, но для разработчика - это просто удобный синтаксис использования необходимого арсенала математических операций.

Важно правильно работать с DOM и корректно описывать собственные объекты. Синтаксис JavaScript object function"s и выражений для их применения - это форма записи логики необходимого алгоритма.

Строки, массивы и объекты

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

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

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

Инициализировать массив свойств означает одновременно:

  • создание массива;
  • создание объекта.

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

Поскольку имена свойств и их значения при создании или изменении должны задаваться в формате строк, рекомендуется использовать строчную нотацию и кавычки.

Доступ к свойствам объекта

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

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

Аналогичного эффекта можно достичь в точечной нотации или скобочной:

  • x1_Obj .NameLast;
  • x1_Obj ["NameFirst" ].

Обе конструкции допустимы и дают нужный результат. В приведенном примере при задании массива через фигурные скобки "{}" может быть допущена ошибка в виде символа "," в конце перечисления (отмечено в примере красным кружочком). Обычно браузеры игнорируют лишний символ в перечислении, но лучше этого не делать.

Удаление свойств объекта

Поскольку объект - это ассоциативный массив, операция JavaScript delete object выполняется на уровне текущего объекта (при наследовании - это имеет значение) и рассматривается на коллекции свойств этого объекта.

В контексте приведенного примера можно использовать такие конструкции:

  • delete x1_Obj .NameLast ;
  • delete x2_Obj ["NameFirst" ];

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

Свойства и методы объектов

Синтаксис JavaScript object properties и functions (методы) аналогичен общим канонам синтаксиса и семантики языка. По сути, дело обстоит как раз наоборот.

Свойства и методы объекта - это вариант описания информации и допускаемых с нею действий через объектно-ориентированную парадигму JavaScript.

В данном примере описан объект x3_Obj, у которого есть только два свойства: item и pos. Затем был добавлен метод hello() в виде функции. В результате интерпретацию этого описания в контексте значений свойств, JavaScript object values сделает так, как показано в окошке результата, то есть поместит тело функции (1) в качестве значения.

При прямом вызове свойства Hello() оно интерпретируется как метод (функция) и результатом (2) будет исполнение кода этого метода.

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

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

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

  • var oCookie = new scCookies (cOwnerCode );
  • oCookie .Init ();

В данном примере cOwnerCode - уникальный код посетителя. Если его нет, то в конструкторе объекта oCookie будет создан новый код. Неважно, что имел ввиду под авторизацией посетителя разработчик данного объекта, важно как ключевое слово this здесь используется для описания методов объекта и их вызова из других методов объекта:

  • this .GetCookie = function (cName) { ... };
  • this .SetCookie = function (cName, cValue) { ... }.

Так описаны методы объекта для чтения куки по её имени и записи значения куки с конкретным именем.

  • this .GetCookie ("cOwner" );
  • this .SetCookie ("cOwner" , cOwner );

Так они используются, если в результате первой конструкции значения не будет представлено, то вторая конструкция его устанавливает.

Пример объекта для работы с куками

Можно обсуждать, Object"s и парадигма объектно-ориентированного подхода языка, работающего в среде браузера. Это интересно, но в реальности нужна практика, а не теория. Обслуживать DOM страницы, предоставлять инструментарий для манипулирования объектами и перемещения по системам объектов - это сильная сторона JavaScript.

На объектно-ориентированной практике важно другое. Работа с куками практически на всех веб-ресурсах в порядке вещей. Реализовать это в формате объекта - отличная идея. В этом контексте инициализация объекта происходит в момент открытия страницы: страница загружена = объект куки существует и всё прочитал, а чего не было - создал.

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

Фактически объект куки возникает сразу после того, как браузер построит DOM и дополняет систему объектов JavaScript новым функционалом: прочитать и создать (изменить) куки.

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

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

События страницы и объектов

Важный элемент функционирования DOM и JavaScript: object event"s - позволяющий получить информацию о событии в его обработчике. Практически каждому элементу страницы можно назначить собственный обработчик на одно или несколько событий.

Фактически разработчик JavaScript создает не один большой «кусок» кода, а множество описаний функций, объектов, структур данных и назначает конкретным элементам страницы обработчики событий.

Object event - это информация о событии, которое вызвало обработчик и возможность выполнения этим обработчиком адекватной реакции на это событие. Каждое событие отличается не только именем и местом возникновения, но и множеством других параметров.

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

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

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

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

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

  • работа с куками;
  • прием/передача данных (AJAX);
  • всплывающие подсказки;
  • внутренние сообщения (чат сайта);
  • другие задачи;

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

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