Прототипы модели DOM, часть 1. Введение

Тревис Лейтхед (Travis Leithead)
Корпорация Майкрософт

1 ноября 2008 года

Содержание

  • Введение
  • Прототипы в JavaScript
  • Прототипы DOM
  • Терминология
  • Наследование прототипов DOM
  • Настройка модели DOM
  • Дополнительные улучшения интеграции JavaScript/DOM
  • Известные проблемы взаимодействия

Введение

Данная статья представляет собой первую из двух публикаций, посвященных расширенным методикам JavaScript в Windows Internet Explorer 8.

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

Чтобы дополнительно снабдить веб-разработчиков средствами для программирования, позволяющими создавать новые сценарии JavaScript, которые основываются на веб-платформе, расширяют ее и вносят в нее новые технологии, в Internet Explorer 8 доступна коллекция компонентов, расширяющих функциональные возможности JavaScript в модели DOM. В данной статье приведен обзор наследования прототипов JavaScript и представлен компонент прототипов DOM, доступный в Internet Explorer 8. Во 2 части представлен новый тип свойства JavaScript, который называется свойством метода доступа (или свойством метода считывания/задания).

Прототипы в JavaScript

Обсуждение прототипов DOM следует начать с важности понимания самого понятия "прототипа" в JavaScript. Попросту говоря, прототип похож на объект класса в других языках — он определяет свойства, совместно используемые всеми экземплярами данного класса. Однако в отличие от класса прототип можно извлекать и изменять во время выполнения. Можно добавлять новые свойства в прототип или удалять существующие. Все изменения сразу же отражаются в объектах, производных от данного прототипа. Как это работает? JavaScript — это динамический язык; вместо компиляции существующих в прототипах свойств в статические таблицы перед выполнением язык JavaScript должен осуществлять динамический поиск свойств при каждом их запросе. Например, представим себе простой сценарий наследования, где прототип "A.prototype" наследует от другого прототипа "B.prototype", а объект "a" является экземпляром прототипа "A.prototype". Если свойство запрошено в объекте экземпляра "a", то JavaScript выполняет следующий поиск:

  1. Сначала JavaScript проверяет объект "a", чтобы определить, существует ли для него свойство. Если свойство отсутствует, JavaScript переходит к действию 2.
  2. После этого JavaScript обращается к "A.prototype" (прототип объекта "a") и ищет свойство. Если свойство все еще не найдено, JavaScript переходит к действию 3.
  3. Наконец, JavaScript проверяет "B.prototype" (прототип "A") и находит нужное свойство. Такой процесс обращения каждого из прототипов объекта продолжается до тех пор, пока JavaScript не достигнет корневого прототипа. Такая последовательность связей между прототипами называется "цепочкой прототипов".

Рассмотрим следующий код:

console.log( a.property );

Свойство "property" не существует непосредственно в объекте "a", но, поскольку язык JavaScript проверяет цепочку прототипов, он найдет свойство "property", если оно определено где-то внутри цепочки (например, в "B.prototype", как показано на следующем рисунке).


Рис. 1. Цепочка прототипов

В JavaScript к прототипу объекта нельзя получить непосредственный доступ программным образом, как указано на предыдущем рисунке. "Связи" от объекта "a" к прототипу "A" и затем к прототипу "B" обычно скрыты от разработчика и реализуются только внутри подсистемы JavaScript (в некоторых реализациях они описываются как собственность, основанная на вещном праве). В данной статье я буду называть такую связь "частным прототипом" или обозначать синтаксисом "[[prototype]]". Для программиста JavaScript предоставляет объекты прототипов через объект конструктора "constructor" со свойством прототипа "prototype", как показано на следующем рисунке. Имя объекта конструктора похоже на имя класса в других языках программирования и во многих случаях оно так же используется для "конструирования" (создания) экземпляров этого объекта с помощью оператора new JavaScript. Фактически, при определении программистом на JavaScript функции:

function A() { /* Определите поведение конструктора */ }

Создаются два объекта: объект конструктора (с именем "A") и объект анонимного прототипа, сопоставленный с данным конструктором ("A.prototype"). При создании экземпляра данной функции:

var a = new A();

JavaScript создает постоянную связь между объектом экземпляра ("a") и прототипом конструктора ("A.prototype"). Это и есть "частный прототип", приведенный на предыдущем рисунке.

Отношение прототипа существует для всех объектов JavaScript, включая встроенные объекты. Например, JavaScript предоставляет встроенный объект массива "Array". "Array" — это имя объекта конструктора. Свойство "prototype" объекта конструктора "Array" является объектом, который определяет свойства, "наследуемые" всеми экземплярами Array (поскольку объекты экземпляров включают этот прототип в свою цепочку прототипов). Объект Array.prototype можно также использовать для настройки или расширения встроенных свойств экземпляров Array (например, свойства "push"), как будет описано ниже. В следующем коде иллюстрируется размещение конструктора, прототипа и объектов экземпляров для "Array":

var a = new Array(); // Создание экземпляра Array 'a'

a.push('x'); // выбор метода 'push' объекта прототипа Array

Эти отношения иллюстрируются на следующем рисунке.


Рис. 2. Отношения между экземпляромArray, его конструктором и его прототипом

Для каждого экземпляра Array при запросе свойства (например, "push") JavaScript сначала проверяет объект экземпляра на наличие свойства с именем "push"; в данном примере его там нет, поэтому JavaScript проверяет цепочку прототипов и находит свойство в объекте Array.prototype.

Веб-разработчики могут добавлять, заменять или "затенять" (переопределять, чтобы свойство было найдено раньше в цепочке прототипов) любые из встроенных свойств в JavaScript, так как все встроенные конструкторы имеют полностью доступные прототипы. Например, чтобы изменить поведение встроенного свойства "push" для всех экземпляров Array, разработчику необходимо просто заменить свойство "push" в одном расположении — объекте прототипа Array:

Array.prototype.push = function () { /* Замените функциональную возможность */ };

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

a.push = function() { /* Переопределение экземпляра "a" */ };

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

Array.prototype.push         = function () { /* Настраиваемое поведение */ };

a.constructor.prototype.push = function () { /* Настраиваемое поведение */ };

Возможность добавления и изменения прототипов очень полезна и эффективна.

Мне нравиться представлять отношение между конструктором, прототипом и экземпляром в виде треугольника. Экземпляры указывают на конструкторы с помощью свойства "constructor". Конструкторы указывают на прототип с помощью свойства "prototype". Прототипы связаны с экземплярами через внутренний [[prototype]], как показано на следующем рисунке.


Рис. 3. Отношения между конструкторами, прототипами и экземплярами.

Прототипы DOM

Описанное выше поведение прототипов присутствует в Internet Explorer уже довольно давно. Однако впервые данная семантическая возможность Internet Explorer 8 распространяется на модель DOM. Когда веб-разработчикам требуется взаимодействовать с веб-странице через JavaScript, они должны использовать объекты DOM, которые не являются частью ядра языка JavaScript, и взаимодействовать с ними. В предыдущих версиях Internet Explorer модель DOM предоставляла программисту на JavaScript только "экземпляры" объектов. Например, свойство createElement модели DOM создает и возвращает экземпляр элемента:

var div = document.createElement('DIV'); // Возвращение нового экземпляра элемента DIV

Этот экземпляр "div" (во многом он схож с экземпляром "a" Array) является производным от прототипа, который определяет все свойства, доступные данному экземпляру. До выпуска Internet Explorer 8 объекты конструктора и прототипа JavaScript для экземпляров "div" (и всех других экземпляров) были недоступны для веб-разработчика. Internet Explorer 8 (при работе в режиме документов: IE8) делает объекты конструктора и прототипа доступными JavaScript — те же самые объекты используются внутри данной и предыдущих версий этого браузера. Кроме доступа к настройке экземпляров объектов DOM с помощью их прототипов, как описано в предыдущем разделе, это также помогает прояснить внутреннее представление модели DOM и ее уникальную иерархию в Internet Explorer (по сравнению с другими браузерами).

Терминология

Как уже было указано, прототипы и конструкторы DOM похожи на встроенные прототипы и конструкторы JavaScript, но имеют определенные отличия. Чтобы помочь выделить эти отличия, я назову аналог DOM для объекта конструктора JavaScript "объектом интерфейса"; объекты прототипов в DOM я назову "объектами прототипов интерфейса," а объекты экземпляров в DOM — "экземплярами DOM." В целях сравнения я снова использую треугольную схему отношений, приведенную ранее для прототипов JavaScript. Экземпляры DOM указывают на объекты интерфейса с помощью свойства "constructor". Объекты интерфейса указывают на объекты прототипов интерфейса с помощью свойства "prototype". Объекты прототипов интерфейса связаны с экземплярами DOM через тот же внутренний [[prototype]].


Рис. 4. Отношения между объектами интерфейса, объектами прототипов интерфейса и экземплярами DOM.

При работе Internet Explorer 8 в стандартном режиме IE8 каждый экземпляр DOM (такой как окно, документ, событие и так далее) имеет соответствующий объект интерфейса и объект прототипа интерфейса. Однако эти объекты отличаются от своих аналогов в JavaScript следующим образом:

  • Объекты интерфейса
    • У объектов интерфейса обычно отсутствует функциональная возможность "конструктора" (они не могут создать новый экземпляр DOM с помощью оператора new JavaScript). К исключениям из этой группы относятся несколько объектов интерфейса, входящие в состав предыдущих версий Internet Explorer:
      • Option (псевдоним для HTMLOptionElement)
      • Image (псевдоним для HTMLImageElement)
      • XMLHttpRequest
      • XDomainRequest, который появился только в Internet Explorer 8
  • Свойство "prototype" объектов интерфейса нельзя заменить (изменить). Свойство prototype объекта интерфейса нельзя назначить другому объекту прототипа интерфейса во время выполнения.
    • Объекты прототипов интерфейса
    • Объекты прототипов интерфейса определяют свойства, доступные для всех экземпляров DOM, но эти встроенные свойства нельзя заменить на постоянной основе (например, оператор delete JavaScript не позволяет удалять встроенные свойства).

Другие неявные, но важные отличия описываются в конце данной статьи.

Наследование прототипов DOM

Как было предложено в спецификации W3C DOM и формализовано в проекте стандарта W3C WebIDL (на момент написания данной статьи), описывающие модель DOM объекты интерфейса упорядочиваются по иерархии в дерево наследования с помощью прототипов. По существу данная древовидная структура помещает общие характеристики документов HTML/XML в наиболее универсальный тип объекта прототипа интерфейса (такой как "Node") и вводит в объекты прототипов интерфейса более специализированные характеристики, которые "расширяют" (посредством прототипов) базовые функциональные возможности. Экземпляры DOM, возвращаемые различными операциями DOM (например, createElement), имеют свойство constructor, ссылающееся на объекты интерфейса в листовом узле данной иерархии. На следующем рисунке приведена часть иерархии модели DOM в соответствии с основной спецификацией W3C DOM L1; здесь представлена лишь небольшая доля объектов интерфейса, поддерживаемых веб-браузерами (многие из них еще не определены в стандартах W3C).


Рис. 5. Иерархия модели DOM в соответствии с основной спецификацией W3CDOML1.

Internet Explorer 8 предоставляет доступ к иерархии прототипов DOM, которая значительно проще приведенного выше представления упорядочения; основная причина этого заключается в том, что объектная модель Internet Explorer предшествует стандартизации иерархии DOM, как показано на рисунке. Было решено представить веб-разработчикам объектную модель "как есть", а не создавать видимость точного соответствия иерархии DOM. Это позволяет лучше прояснить ситуацию для веб-разработчиков и подготовить их к будущим изменениям в модели DOM на основе выпускаемых стандартов. На следующем рисунке приведена уникальная иерархия прототипов DOM в Internet Explorer 8 с использованием только тех объектов интерфейса, которые присутствовали не предыдущей иллюстрации. Снова напоминаем, что здесь представлена лишь небольшая часть объектов интерфейса, поддерживаемых в Internet Explorer 8:


Рис. 6. Частичная иерархия DOM, поддерживаемая браузером Internet Explorer 8.

Обратите внимание на отсутствие общего предка "Node". Также обратите внимание на то, как комментарии "наследуются" от Element (это возможно, если предположить, что Internet Explorer все еще поддерживает устаревший элемент "<comment>").

Настройка модели DOM

Получив знания о иерархии прототипов DOM в Internet Explorer 8, веб-разработчики могут приступить к изучению преимуществ данного эффективного компонента. Чтобы помочь вам, я приведу два реальных примера. В первом примере веб-разработчику нужно дополнить модель DOM Internet Explorer 8 возможностью из пока недоступного HTML5 (на момент написания данной статьи). getElementsByClassName из чернового варианта HTML5 удобно использовать для поиска элемента, для которого определен конкретный класс CSS. В приведенном ниже небольшом примере кода данная функциональная возможность реализована с использованием APIселекторов (впервые введен в Internet Explorer 8) и объектов прототипов интерфейса HTMLDocument и Element:

function _MS_HTML5_getElementsByClassName(classList)

{

  var tokens = classList.split(" ");

  // Предварительное заполнение списка результатами первого поиска по маркеру.

  var staticNodeList = this.querySelectorAll("." + tokens[0]);

  // Запуск итераций с 1, поскольку первое совпадение уже получено.

  for (var i = 1; i < tokens.length; i++)

  {

    // Независимый поиск каждого из маркеров

    var tempList = this.querySelectorAll("." + tokens[i]);

    // Сбор "элементов хранения" между итераций цикла

    var resultList = new Array();

    for (var finalIter = 0; finalIter < staticNodeList.length; finalIter++)

    {

      var found = false;

      for (var tempIter = 0; tempIter < tempList.length; tempIter++)

      {

        if (staticNodeList[finalIter] == tempList[tempIter])

        {

          found = true;

          break; // Преждевременное завершение работы при обнаружении элемента

        }

      }

      if (found)

      {

        // Этот элемент был в обоих списках, его необходимо сохранить

        // для следующего этапа проверки маркера...

        resultList.push(staticNodeList[finalIter]);

      }

    }

    staticNodeList = resultList; // Копирование результатов выполнения логической операции И для следующего маркера

  }

  return staticNodeList;

}

HTMLDocument.prototype.getElementsByClassName = _MS_HTML5_getElementsByClassName;

Element.prototype.getElementsByClassName = _MS_HTML5_getElementsByClassName;

Кроме недостаточных сведений о проверке параметров и обработке ошибок, я думаю, что простота расширения модели DOM в Internet Explorer 8 очевидна.

Во втором примере веб-разработчику требуется создать функцию для исправления устаревшего скрипта, который еще не был обновлен и не поддерживает Internet Explorer 8. При разработке Internet Explorer 8 (среди прочего) были исправлены интерфейсы API setAttribute/getAttribute для обеспечения правильной обработки имени атрибута "class". Во многих современных скриптах присутствует пользовательский код для обработки этой ошибки в предыдущих версиях Internet Explorer. Следующий скрипт перехватывает все экземпляры этого устаревшего кода и исправляет его в динамическом режиме. Обратите внимание на то, что веб-разработчик использует объект интерфейса Element для изменения поведения getAttribute и setAttribute в каждом из элементов (поскольку setAttribute и getAttributeопределены в объекте прототипа интерфейса Element):

var oldSetAttribute = Element.prototype.setAttribute;

var oldGetAttribute = Element.prototype.getAttribute;

// Применение изменения к прототипу Element...

Element.prototype.setAttribute = function (attr, value)

  {

    if (attr.toLowerCase() == 'classname')

    {

      // Для работы старым скриптам требуется "className",

      // поэтому не следует создавать атрибут "className"

      // в стандартном режиме IE8

      attr = 'class';

    }

    // TODO: Добавление другого исправления (такого как "style")

    oldSetAttribute.call(this, attr, value);

  };

Element.prototype.getAttribute = function (attr)

  {

    if (attr.toLowerCase() == 'classname')

    {

      return oldGetAttribute.call(this, 'class');

    }

    // TODO: Добавление другого исправления (например, "style")

    return oldGetAttribute.call(this, attr);

  };

При запуске устаревшего скрипта после данного кода ко всем вызовам setAttribute или getAttribute (из любого элемента) применяется данное исправление.

Дополнительные улучшения интеграции JavaScript/DOM

Чтобы дополнительно выделить сценарии, в которых могут использовать прототипы DOM, мы рассмотрели несколько проблем взаимодействия с другими браузерами для JavaScript/DOM браузера Internet Explorer:

  • Обработка оператора delete JavaScript
  • Поддержка call и apply для функций DOM

Delete — новый "механизм отмены свойств"

Internet Explorer 8 теперь поддерживает новый оператор delete JavaScript для удаления (или отмены) свойств, динамически добавленных в экземпляры DOM, объекты интерфейса или объекты прототипов интерфейса. В предыдущих версиях Internet Explorer удаление любого динамического свойства из экземпляра DOM с использованием оператора delete приводило к ошибке скрипта; в Internet Explorer 8 удаление динамического свойства выполняется правильно:

document.newSimpleProperty = "simple";

console.log(document.newSimpleProperty); // Ожидается: "simple"

try

{

  delete document.newSimpleProperty; // Ошибка скрипта в Internet Explorer 7

}

catch(e)

{

  console.log("сбой delete с ошибкой: " + e.message);

  document.newSimpleProperty = undefined; // Обходной путь для старых версий

}

console.log(document.newSimpleProperty);  // Ожидается: undefined

Оператор delete также имеет большое значение для встроенных свойств и методов DOM, которые перезаписываются определенными пользователем объектами; в этих случаях delete удаляет пользовательский объект, но не удаляет встроенное свойство или встроенный метод:

var originalFunction = document.getElementById; // Помещение в кэш копии для IE 7

document.getElementById = function ()

  {

    console.log("my function");

 };

document.getElementById(); // Такой вызов теперь вызывает особую функцию

try

{

  delete document.getElementById; // Ошибка скрипта в IE 7

}

catch(e)

{

  console.log("сбой delete с ошибкой: " + e.message);

  document.getElementById = originalFunction; // Обходной путь для IE 7

}

console.log(document.getElementById); // Ожидается: функция getElementById

Вызов кэшированных функций

Теперь в Internet Explorer 8 устранена одна из проблем взаимодействия, связанная с вызовом кэшированных функций (кэшированная функция является объектом функции DOM, который сохраняется в переменную для последующего использования). Рассмотрим следующий код JavaScript:

var $ = document.getElementById;

var element = $.call(document, 'id');

В сущности, когда функция "getElementById" кэшируется в "$", она может официально считаться отделенной от объекта, который изначально являлся ее "владельцем" (в данном случае это "document"). Чтобы правильно вызвать "отделенное" (кэшированное) свойство, JavaScript требует от веб-разработчика явного указания области; для кэшированных свойств DOM это требование выполняется с помощью предоставления объекта экземпляра DOM в качестве первого параметра в свойствах "call" или "apply" JavaScript, как показано в предыдущем примере кода. Для обеспечения правильного взаимодействия рекомендуется использовать свойства call/apply при вызове кэшированных функций из любого объекта.

С введением прототипов DOM стало возможно кэшировать свойства, определенные в объектах прототипов интерфейса, как показано в приведенном выше примере кода для исправления сценария использования getAttribute/setAttribute. В таких сценариях использование call или apply является обязательным, поскольку определения функций в объектах прототипов интерфейса не имеют неявной области интерфейса DOM.

В Internet Explorer 8 сохранена поддержка ограниченного метода вызова кэшированных функций (в основном для обратной совместимости):

var $ = document.getElementById; // Кэширование этой функции в переменную "$"

var element = $('id');

Обратите внимание на то, что ни call, ни apply не используются; Internet Explorer"запоминает" объект, к которому относилась функция, и неявно вызывает "$" из нужной области (из объекта document). Приведенный выше код не рекомендуется использовать, так как он работает только с Internet Explorer; также следует помнить о том, что данный метод не позволяет вызывать функции DOM, кэшированные из объекта прототипа интерфейса в Internet Explorer 8.

Известные проблемы взаимодействия

Ниже приведены известные проблемы взаимодействия, связанные с реализацией прототипов DOM в Internet Explorer 8. Мы надеемся устранить эти проблемы в следующем выпуске; кроме того, они не должны оказывать значительное влияние на основные сценарии, поддерживаемые прототипами DOM.

Объекты интерфейса

  • В объектах интерфейса не поддерживаются постоянные свойства (например, XMLHttpRequest.DONE).
  • Объекты интерфейса не включают Object.prototype в свою цепочку прототипов.

Объекты прототипов интерфейса

  • Объекты прототипов интерфейса не включают Object.prototype в свою цепочку прототипов.
  • В настоящее время экземпляры DOM и объекты прототипов поддерживают только следующие свойства из Object.prototype: "constructor", "toString" и "isPrototypeOf".

Функции

  • Встроенные свойства DOM, которые являются функциями (например, функциями в объектах прототипов интерфейса), не включают Function.prototype в свою цепочку прототипов.
  • Встроенные свойства DOM, которые являются функциями, не поддерживают callee, caller или length.

Оператор Typeof

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

Перечисление

  • Объекты прототипов интерфейса включают в себя поддержку перечисления собственных встроенных свойств, являющихся функциями (новая возможность в Internet Explorer 8). Экземпляры DOM включают в себя поддержку перечисления всех свойств, доступных в их цепочке прототипов, кроме свойств, являющихся функциями (традиционное поведение Internet Explorer 7). Чтобы обеспечить правильное взаимодействие, для получения полного перечисления всех свойств, доступных в экземпляре DOM, рекомендуется использовать следующий код:

function DOMEnumProxy(element)

{

  var cons;

  for (x in element)

  {

    // x получает все API, предоставленные в объекте без

    // методов (четность IE7)

    this[x] = 1;

  }

  try { cons = element.constructor; }

  catch(e) { return; }

  while (cons)

  {

    for (y in cons.prototype)

    {

      // y получает все свойства со следующих уровней в цепочке прототипов

      this[y] = 1;

    }

    try

    {

      // Избегайте бесконечного цикла (например, при передаче параметра экземпляра String)

      if (cons == cons.prototype.constructor)

        return;

      cons = cons.prototype.constructor;

    }

    catch(e) { return; }

  }

}

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

m = new DOMEnumProxy(document.createElement('div'));

for (x in m)

console.log(x);

Итак, прототипы DOM представляют собой полезное и эффективное расширение модели прототипов JavaScript. Они предоставляют веб-разработчикам необходимые средства и гибкость для создания сценариев, которые основываются на веб-платформе Internet Explorer 8, расширяют ее и вносят в нее новые технологии. Во 2 части данной статьи рассматривается синтаксис метода считывания/задания, поддерживаемый объектами DOM.