Глаза страшат, а руки делают

ЧАСТЬ ВТОРАЯ


28. Объект Object

  • создание объекта;
  • свойство constructor;
  • свойство prototype;
  • методы объекта;
  • ещё один пример создания собственного метода;
  • создание пользовательского объекта-хэша.

Объект Object, как уже говорилось, это базовый объект JavaScript. Все остальные объекты — это его «дети».

В уроке 12 я определил его как конструктор для создания пользовательских объектов (т.е. абсолютно новых, оригинальных объектов, которых нет ни в модели JavaScript, ни в модели браузера). Но если быть точным, то Object — это не конструктор (о конструкторах — чуть ниже), а прототип любых объектов (в том числе и существующих в объектных моделях). Правда, для создания стандартных объектов нам не надо прибегать к явному созданию прототипа: за нас это делает сам JavaScript.

В том же уроке я упоминал про объектно-ориентированное программирование (ООП) и различные типы языков ООП. Для особо любознательных дал скрытый комментарий с некоторыми подробностями. Сейчас я его процитирую, потому что мы уже поднабрались блох, и «внеклассное чтение» превратилось в «обязаловку».

Итак:

***

Существуют объектно-ориентированные языки на базе классов и на базе прототипов.

Языки на базе классов — такие, как Java и C++ — основаны на концепции двух различных сущностей: классов и экземпляров.

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

Язык JavaScript построен на базе прототипов, у него нет этих различий: в нем просто имеются объекты. Он содержит понятие prototypical object (прототипичный объект) — объект, используемый как шаблон, из которого получаются начальные свойства для нового объекта. Любой объект может получить свои собственные свойства либо при его создании, либо на этапе прогона. Кроме того, любой объект может быть определён как прототип для другого (дочернего) объекта, давая ему возможность использовать свойства первого (родительского) объекта.

***

Что для нас здесь важно?

Все элементы языка JavaScript — будь то собственно объекты, методы, свойства, функции — являются объектами, «детьми» объекта Object. А это значит, что все элементы JavaScript (равно как и все прочие объекты как таковые) являются свойствами и методами объекта Object.

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

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

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

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

Объект Object создается с помощью выражения:

var переменная = new Object([значение])

Значением может быть любой тип данных: строка, число, булево значение.

var myObj1 = new Object("Привет!"); // создаётся строка
var myObj2 = new Object(7); // создаётся число
var myObj3 = new Object(true); // создаётся булево значение

Квадратные скобки в примере, как обычно, означают, что оно может оставаться пустым (в реальном коде они не ставятся). Пустому объекту впоследствии можно присвоить любое значение.

var myObj4 = new Object();
myObj4 = "Доброе утро!"
// Значением нашего объекта стала строка "Доброе утро!".

Можно дать объекту Object сразу несколько значений. Эти значения задаются как свойства переменной объекта — через точку (имена свойств задаются по тем же правилам, что и имена переменных):

var myObj5 = new Object();
myobj5.stroka = "Привет!";
myobj5.chislo7 = 7;
myobj5._boolka = true;

Литерал объекта Object

Можно также создать объект с помощью литерала, помещённого в фигурные скобки. Но здесь есть одна тонкость. Схему литерального синтаксиса можно представить так:

var переменная = {[идентификатор: значение]}

Что это значит?

Пустой объект мы можем создать точно так же, как и через конструктор new:

var переменная = {}

Но задать конкретное значение самому объекту через объектный литерал мы не сможем:

// Это не работает:
var myObj4 = {"Доброе утро!"};
// А это работает:
var myObj4 = {}
myObj4 = "Доброе утро!"

Но если мы создаём значение как свойство переменной объекта, то это можно сделать прямо в литерале, а это свойство и будет тем самым таинственным идентификатором:

var myObj4 = {goodMorning: "Доброе утро!"};

С помощью литерала можно задать и те несколько свойств, которые мы задавали в недавнем примере:

var myObj5 {stroka: "Привет!", chislo7: 7, _boolka: true}

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

Свойства объекта Object

Свойства объекта Object есть и у всех остальных объектов JavaScript, поскольку они как дочерние объекты наследуют свойства родителя. Этих свойств два: constructor и prototype.


Свойство constructor

Это ссылка на конструктор, с помощью которого создан экземпляр объекта. Собственно, конструктор — это функция, которая создаёт и инициализирует объекты. Имя этой функции совпадает с видом объекта (так, например, конструктор объекта Object — функция Object(), конструктор объекта String — функция String()).

Из спецификации ECMA

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

С помощью свойства constructor можно определить, к какому же виду относится наш объект.

var a = "текст";
var b = 256;
var c = false;
var d = ["текст", 256, false];
var e = new Date();
document.write(a.constructor + "<br>");
document.write(b.constructor + "<br>");
document.write(c.constructor + "<br>");
document.write(d.constructor + "<br>");
document.write(e.constructor);

Выводит:

Примечание

коды функций встроенных объектов не показываются, их замещает «псевдоним» [native code].

Свойство prototype

У каждого конструктора есть соответствующий прототип, который используется для наследования и разделения свойств.

Свойство prototype возвращает ссылку на прототип объекта. Это свойство объекта, а не экземпляра, поэтому оно применяется не к экземпляру объекта, а к его конструктору Object.

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

Мы уже трижды тренировались в этом. Потренируемся ещё раз, но теперь уже зная, что есть что.

Напоминаю синтаксис:

объект.prototype.имяМетода = имяФункции

объект — объект, на который назначается метод.

имяМетода — имя нового метода.

имяФункции — имя функции-конструктора для этого метода.

(Всё без скобок.)

Помните, в 22 уроке мы писали функцию для корректной сортировки массивов методом sort()? Функция вызывалась как аргумент встроенного метода. А теперь превратим её в метод как таковой.

Но для этого нам понадобятся две функции. Почему две?

Сначала вспомним функцию из 22 урока:

function uniSort(a, b) {
var aCode = a.toLowerCase().replace('ё','е'+String.fromCharCode(1110));
var bCode = b.toLowerCase().replace('ё','е'+String.fromCharCode(1110));
if (aCode > bCode)
  return 1;
if (aCode < bCode)
  return -1;
else
  return 0;
}

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

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

Array.prototype.uniSort = uniSort2;

Примечание

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

Теперь делаем функцию-конструктор. Поскольку наш метод будет использовать метод sort(), в конструкторе надо это указать:

function uniSort2() {
var k = this.sort(uniSort);
return k
}

Теперь проверим:

var testForYo = ["тело", "ёлка", "ООН", "ёжик", "Ежов", "тёлка", "Лёвин", "Левин", "она", "ель"];
document.write(testForYo.uniSort());

Результат:

Итак, наша личная библиотечка пополнилась ещё одним «личным» методом.

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

Как и свойства, методы объекта Object есть и у всех его потомков — остальных встроенных объектов JavaScript.

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

Метод hasOwnProperty

Метод hasOwnProperty проверяет объект на наличие того или иного свойства или метода. Если данное свойство/метод у объекта есть, возвращает true, в противном случае — false.

Синтаксис

объект.hasOwnProperty(имя)

объект — любой объект, который мы хотим проверить на наличие свойства/метода

имя — имя проверяемого свойства/метода

Этот метод не проверяет наличие свойства/метода у экземпляровов; указанным свойством/методом должен обладать именно сам объект-прототип. Например, если проверить экземпляр строки на метод charCodeAt, результат будет false. А если обратиться к объекту-прототипу — true:

var a = "проверка строки";
var b1 = a.hasOwnProperty("charCodeAt");	        // false
var b2 = String.prototype.hasOwnProperty("charCodeAt"); // true

Выведем результаты проверки:

document.write(b1 + ", " + b2)


Метод isPrototypeOf

Метод isPrototypeOf проверяет, является ли данный объект прототипом другого объекта. Да — true, нет — false.

Синтаксис

объект.isPrototypeOf(имя)

объект — объект, который мы тестируем на прототипичность

имя — предполагаемый дочерний объект

Этот метод также игнорирует экземпляры. Попробуем протестировать объект String с нашим экземпляром a:

var b3 = String.prototype.isPrototypeOf(a);

Ну и...

document.write(b3);

А теперь протестируем объекты Object и String:

var b4 = Object.prototype.isPrototypeOf(String.prototype);
document.write(b4);

Ну вот, другое дело.

Честно говоря, на этом я и хотел закончить сегодняшний краткий обзор. Но в 21 уроке (о массивах) я нечаянно обмолвился:

«В JavaScript, как ни парадоксально это звучит, нет массивов-хэшей, но они в нём есть. Каким образом они там существуют, мы узнаем при исследовании объекта Object».

Придётся отрабатывать.

Вообще-то, правильнее было бы сказать: «в JavaScript нет массивов-хэшей, но их можно сделать». Для этого нужно создать пользовательский объект, который работал бы как встроенные хэши в других языках программирования (например, в Perl).

Введение в создание пользовательских объектов

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

Сегодня мы создадим объект, который будет работать как хэш.

Что такое хэш?

Если говорить обобщённо, то хэш — это словарь, использующий ассоциативные связи. Есть ключи и значения. За каждым ключом закреплено определённое значение. Обычный массив — тоже частный случай хэша, где ключами для нахождения элементов служат индексы этих элементов. В ряде языков программирования существуют средства для создания ассоциативных массивов — собственно хэшей, которым можно назначать пары «ключ-значение», и поиск значения будет происходить не по индексу, а по ключу.

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

Зачем он нужен?

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

А потом я задумался, а как же лучше их рассортировать: по алфавиту литералов или по гамме цветов?

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

Как избавиться от тяжкого сизифова труда? Ну конечно же, в других языках просто создать хэш, где, допустим, литералы стали бы ключами, а коды — значениями. Или наоборот. И сортируй себе — не хочу!

Теперь создадим объект.

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

function Hash(key, name) {
    this.key = new Array;
    this.name = new Array;
}

Этой функцией мы создали объект Hash(), имеющий два свойства: key и name, которые являются массивами.

Теперь добавим метод поиска по ключу. Это делается так же, как и со стандартными объектами:

Hash.prototype.searchByKey = searchByKey;

function searchByKey(keyword) {
var i, k;
    // перебираем индексы массива ключей
    for (i=0; i<this.key.length; i++)
    {
        //приравниваем индексы значений к индексам ключей
        if (keyword == this.key[i])
        k = this.name[i];
    }
    //если не сошлось
    if (k == undefined)
        k = "Задан несуществующий ключ " + "\"" + keyword + "\"";
return k;
}

А заодно добавим и метод поиска по значению — просто все key заменим на name, а name — на key:

Hash.prototype.searchByName = searchByName;

function searchByName(keyword) {
var i, k;
    // перебираем индексы массива значений
    for (i=0; i<this.name.length; i++)
    {
        //приравниваем индексы ключей к индексам значений
        if (keyword == this.name[i])
        k = this.key[i];
    }
    //если не сошлось
    if (k == undefined)
        k = "Задано несуществующее значение " + "\"" + keyword + "\"";
return k;
}

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

var rainbow = new Hash();
var rb_codes = ["FF0000", "FFA500", "FFFF00", "008000", "ADD8E6", "0000FF", "EE82EE"];
var rb_literals = ["red", "orange", "yellow", "green", "lightblue", "blue", "violet"];
rainbow.key = rb_codes;
rainbow.name = rb_literals;

Построим функцию для вывода таблицы:

function writeTable(hs, ar) {
  // Переменные для вывода таблицы
  var tb1 = "<table border='1' align='center'>"
  var tb2 = "</table>"
  var tr1 = "<tr>"
  var tr2 = "</tr>"
  var td1 = "<td>"
  var td1a = "<td width='80' bgcolor='#"
  var td1b = "'> "
  var td2 = "</td>"
  var i
  // Следующий пассаж будет разъяснён ниже
  var ar2 = new Array
    if (ar == undefined)
      ar2 = hs.name
    else
      ar2 = ar
  // Собственно вывод таблицы
  document.write(tb1)
    for (i=0; i<ar2.length; i++)
    {
      document.write(tr1 + td1 + ar2[i] + td2 + td1 + hs.searchByName(ar2[i]) + td2 + td1a +   hs.searchByName(ar2[i]) + td1b + tr2)
    }
  document.write(tb2)
}

Эта функция предусматривает работу как с оригинальным хэшем, так и с отсортированным массивом значений, сохраняя при именах их «родные» ключи (с отсортированным массивом ключей она не работает: введение и этой возможности сделало бы её слишком громоздкой для примера).

Аргументы:

hs (обязательный) — имя хэша

ar (необязательный) — имя массива, преобразованного из массива значений. Если ничего не преобразовывали, то второй аргумент не нужен. Именно об этом сообщает следующий фрагмент кода:

var ar2 = new Array
  if (ar == undefined)
    ar2 = hs.name
  else
    ar2 = ar

1. Задаётся переменная для массива

2. Если аргумент ar отсутствует, переменная принимает значения hs.name. Если он есть, то переменная принимает его значения.

Теперь рассмотрим, что и как выводится в таблицу.

В левую колонку помещается список значений hs.name — либо непосредственный, либо отсортированный.

Следующая колонка — соответствующие значения цветового кода (ключи хэша).

1. Принимается текущее значение из массива ar2 (последовательность значений может не совпадать с последовательностью hs.name).

2. Методом hs.searchByName() ищется соответствующий ему ключ из первоначального hs.name.

3. Найденный ключ выводится в таблицу.

В последней колонке поиск производится аналогично, только ключ выводится как bgcolor ячейки.

Простое выведение таблички выглядит вот так:

writeTable(rainbow)

В изначальном хэше последовательность цветов соответствует спектру радуги. А теперь отсортируем их в алфавитном порядке литералов. Вспомним, что метод sort() воздействует на родительский массив и все его псевдонимы-переменные. Поэтому вспомним трюк из 21 урока:

/* объявляем массив, над которым будем издеваться */
var rb_nn = new Array;
/* сливаем инфу и заметаем следы */
rb_nn = rb_literals.join("%&^").split("%&^");
/* я от бабушки ушёл, я от дедушки ушёл - теперь сортируем */
var rb_sort1 = rb_nn.sort();)

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

Чтобы показать отсортированный вариант, вызываем функцию с двумя аргументами:

writeTable(rainbow, rb_sort1)

Voilà:

Пока ограничимся этой общей информацией. Что-то всё равно застрянет в голове. И когда мы подойдём к следующей ступеньке, вдруг наступит озарение: а-а, так вот оно что!


Итак, мы узнали:

что предстоит узнать ещё очень много

Приближаемся к мудрости Сократа, сказавшего: «Я знаю, что ничего не знаю».

А также:

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


К следующему уроку >>


 009404