Прототипы модели DOM, часть 2. Поддержка метода доступа (метода считывания/задания)

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

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

Содержание

  • Введение
  • Два вида свойств: данных и метода доступа
  • Синтаксис
  • Свойства метода доступа и прототипы DOM
  • Особый случай: удаление встроенных свойств DOM
  • Эффективные сценарии
  • Соблюдение стандартов
  • Ограниченные свойства

Введение

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

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

Два вида свойств: данных и метода доступа

Веб-разработчики часто добавляют в модель DOM настраиваемые свойства. Существующая расширяемость объектов позволяет добавленным свойствами сохранять состояние, отслеживать состояние приложения и т. п. До Internet Explorer 8 язык JavaScript поддерживал только один тип свойства: тот, который позволяет сохранять и извлекать значение (в ECMAScript 3.1 такие свойства называются "свойствами данных", в других языках для обозначения данной концепции используются такие термины, как "поле" и "переменная экземпляра"). С точки зрения внедрения эти существующие свойства имеют одну "ячейку переменной", в которой хранится значение. Свойства данных определяются автоматически при использовании в JavaScript оператора присвоения (=), как показано в следующем примере:

document.data = 5; // Создание свойства данных с именем "data"

console.log( document.data ); // Ответ: 5 (ожидаемый)

Отношения между методами доступа считывания и задания иллюстрируются на следующем рисунке.


Рис. 1. Визуализация свойств данных Javascript

До выпуска Internet Explorer 8 разработчики на JavaScript могли использовать в своем коде только свойства данных, хотя было ясно, что некоторые встроенные свойства в JavaScript и DOM не являлись свойствами данных. Например, встроенное свойство "innerHTML" модели DOM делает значительно больше, чем просто сохраняет значение:

// Обращение к свойству innerHTML:

// (получение дерева дочерних элементов в виде строкового значения)

var str = document.getElementById('element1').innerHTML;

// Присвоение строкового значения свойству innerHTML:

// (вынуждает модель DOM выполнить анализ строкового значения и создать

// новое дерево дочерних элементов внутри "element1")

document.getElementById('element2').innerHTML = str;

// Получение (обращение) и задание (назначение) другого поведения:

// (Один и тот же API выполняет обработку строкового значения и анализ в зависимости от того, какая операция осуществляется — чтение или запись.)

Без новой функциональной возможности языка JavaScript веб-разработчики не могут создавать схожие свойства (такие как innerHTML), которые имитируют встроенные свойства DOM. Указанный разрыв в функциональных возможностях увеличивается, поскольку многие веб-разработчики хоте ли бы расширить и усовершенствовать встроенные свойства, доступные в модели DOM. Для обеспечения поддержки требуемого поведения в язык JavaScript были добавлены свойства "метода считывания/задания". Для краткости я назову свойства метода считывания/задания по их имени из ECMAScript 3.1 — свойства "метода доступа".

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


Рис. 2. Визуализация свойств метода доступа Javascript

Синтаксис

Для определения свойства метода доступа требуется специальный синтаксис, поскольку оператор присвоения (=) по умолчанию определяет свойство данных. Internet Explorer 8 — это первый браузер, в котором реализован синтаксис ECMAScript 3.1 для определения свойств метода доступа:

Object.defineProperty(  [(объект DOM) объект],

                        [(строковое значение)     имя свойства],

                        [(дескриптор) определение свойства] );

Все параметры являются обязательными.

Возвращаемое значение: первый параметр (объект), переданный функции.

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

Object.getOwnPropertyDescriptor( [(объект DOM) объект],

                                 [(строковое значение)     имя свойства] );

Все параметры являются обязательными.

возвращаемое значение: объект дескриптора свойства

Обратите внимание на следующие ограничения:

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

В следующем примере демонстрируется API definePropertyпосредством определения метода доступа "JSONposition" для изображения. "Метод считывания" для этого нового свойства преобразует координаты изображения в строковое значение JSON. "Метод задания" считывает это строковое значение JSON и соответствующим образом изменяет положение изображения:

// Создание объекта дескриптора свойства

var posPropDesc = new Object();

// Определение метода считывания

posPropDesc.get = function ()

{

  var coords = new Object();

  coords.x = parseInt(this.currentStyle.left);

  coords.y = parseInt(this.currentStyle.top);

  coords.w = parseInt(this.currentStyle.width);

  coords.h = parseInt(this.currentStyle.height);

  return JSON.stringify(coords);

}

// Определение метода задания

posPropDesc.set = function (JSONString)

{

  var coords = JSON.parse(JSONString);

  if (coords.x) this.style.left   = coords.x + "px";

  if (coords.y) this.style.top    = coords.y + "px";

  if (coords.w) this.style.width  = coords.w + "px";

  if (coords.h) this.style.height = coords.h + "px";

}

// Определение нового свойства метода доступа "JSONposition" для нового изображения

var img = Object.defineProperty(new Image(), "JSONposition", posPropDesc);

img.src = "...";

// Вызов нового свойства

img.JSONposition = '{"w":400,"h":100}';

// Считывание текущего положения изображения

console.log(img.JSONposition);

В данном примере defineProperty создает новое свойство метода доступа для изображения (первый параметр) с именем "JSONposition"; третий параметр является объектом, называемым дескриптором свойства, который определяет новое поведение свойства.

Дескрипторы свойств являются стандартным средством для описания как "типа", так и "атрибутов" свойства. Как показано в предыдущем примере, "метод считывания" и "метод задания" являются двумя из возможных свойств в дескрипторе свойств. При добавлении любого из этих свойств в дескриптор свойств defineProperty создает свойство метода доступа. Когда не указан метод считывания, при обращении к значению свойства возвращается значение undefined. Когда не указан метод задания, при назначении значения методу доступа никакие операции не выполняются. Это показано в следующей таблице:

  Только функция метода считывания Только функция метода задания Обе функции
Обращение к свойству ("get") Вызов функции метода считывания Возврат значения undefined Вызов функции метода считывания
Назначение свойства ("set") Операции не выполняются. Вызов функции метода задания Вызов функции метода задания

Таблица 1. Возможные результаты обращения и задания для сочетаний функций метода считывания или метода задания в свойстве метода доступа JavaScript

Свойства метода доступа также можно последовательно определить с использованием нескольких вызовов интерфейса API defineProperty. Например, один вызов defineProperty может определять только метод считывания. Позднее defineProperty может быть снова вызван для того же имени свойства для определения метода задания. В этом случае свойство имеет как метод считывания, так и метод задания:

Object.defineProperty(window, "prop",

   { get: function() { return "Можно считать"; } } );

// ...

Object.defineProperty(window, "prop",

   { set: function(x) { console.log("Можно задать " + x); } } );

// Теперь для свойства определен как метод считывания, так и метод задания

Кроме того, определение метода считывания или метода задания по существу отменяет соответствующий метод, заданный ранее:

Object.defineProperty( document.body, "secondChild",

{

  get: function ()

  {

    return this.firstChild.nextSibling;

  },

  set: function ( element )

  {

    throw new Error("К сожалению, данное свойство нельзя " +

                    "задать. Повторите попытку.");

  }

} );

// Я передумал: не стоит так строго подходить к отображению

// ошибки при задании данного свойства...

Object.defineProperty( document.body, "secondChild",

                       { set: undefined } );

Другим ключевым словом, которое может использоваться в дескрипторе свойств, является слово "value". "value" указывает на создание свойства данных:

console.log( Object.defineProperty(

   document, "data", { value: 5 } ).data );

Этот код полностью равноценен свойству данных в первом примере кода в данной статье. Обратите внимание на то, что если дескриптор свойства содержит комбинацию ключевых слов "value" и "get/set", API definePropertyвозвращает ошибку.

Дескрипторы свойств также включают в себя дополнительные ключевые слова для управления "атрибутами" свойства. Такие ключевые слова зарезервированы для последующего использования; в настоящее время Internet Explorer 8 поддерживает только следующие значения ключевых слов для атрибутов:

Тип свойства Атрибут "Writeable" Атрибут "Configurable" Атрибут "Enumeratable"
свойство данных true true true
свойство метода доступа Неприменимо true false

Таблица 2. Допустимые значения для атрибутов writable, configurable и enumerable дескриптора свойств для свойства данных и свойства метода доступа

Если используется недопустимая комбинация, API definePropertyвозвращает ошибку, как показано в следующем примере кода.

try

{

  Object.defineProperty(document, "test",

  {

    get: function()

    {

      return 'Это просто проверка';

    },

    configurable: false;

  } );

}

catch(e)

{

  console.log(e.message);

  // Для атрибута 'configurable' в дескрипторе

  // свойства нельзя задать значение "false" для этого объекта

}

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

delete document.test;

delete document.data;

Свойства метода доступа и прототипы DOM

Свойства метода доступа вместе с иерархией прототипов DOM предоставляют веб-разработчикам все необходимое для полной настройки встроенных свойств DOM. Свойства метода доступа представляют собой средство для настройки встроенных функциональных возможностей модели DOM с использованием определенных пользователем функциональных возможностей JavaScript; иерархия прототипов DOM представляет собой средство для "масштабирования" этих настроек.

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

Одним из таких встроенных свойств и основным объектом для настройки является свойство innerHTML.


Рис. 3. Цепочка прототипов для экземпляра div

С помощью такого представления иерархии прототипов DOM веб-разработчик может выбирать между настройкой самого встроенного свойства innerHTML и переопределением этого свойства на более низком уровне данной иерархии. Чтобы настроить это встроенное свойство, используйте API defineProperty для передачи объекта Element.prototype в качестве первого параметра, строкового значения "innerHTML" в качестве второго параметра и функций метода считывания или метода задания в составе дескриптора свойства:

// Настройка встроенного свойства innerHTML

Object.defineProperty(Element.prototype, "innerHTML", /* property descriptor */);

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

// Сохранение (кэширование) исходного поведения innerHTML

var originalInnerHTMLpropDesc = Object.getOwnPropertyDescriptor(Element.prototype, "innerHTML");

// Определение настроек, но с использованием innerHTML после выполнения операции...

Object.defineProperty(Element.prototype, "innerHTML",

{

  set: function ( htmlContent )

  {

    // TODO: добавление нового кода для метода считывания innerHTML

    // Вызов исходного  innerHTML после выполнения операции...

    originalInnerHTMLpropDesc.set.call(this, htmlContent);

  }

}

Теперь метод задания свойства innerHTML настроен для всех экземпляров элементов DOM. Метод считывания для свойства innerHTML продолжает работать как и раньше.

Возможно, цель веб-разработчика заключается в настройке innerHTML только для подмножества типов элементов, например, только для элементов DIV. С помощью определения innerHTML на уровне HTMLDivElement.prototype веб-разработчик переопределяет встроенное свойство innerHTML (только для экземпляров элементов DIV), поскольку такое определение обнаруживается первым при обращении JavaScript к цепочке прототипов элемента DIV :

// Настройка innerHTML только для элементов DIV

// (это не затрагивает другие типы элементов)

Object.defineProperty(HTMLDivElement.prototype,

   "innerHTML", /* property descriptor */);

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

Наконец, когда веб-разработчик хочет применить переопределение только к экземпляру DOM, рекомендуется использовать API definePropertyнепосредственно с данным экземпляром:

// Создание экземпляра элемента div

// с настроенным свойством innerHTML

var div = Object.defineProperty(document.createElement('DIV'),

   "innerHTML",  /* property descriptor */);

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

Особый случай: удаление встроенных свойств DOM

В конце предыдущего раздела я описывал, как оператор delete JavaScript удаляет свойство метода доступа или данных. В некоторых случаях такой подход может не работать. Причина этого заключается в том, что объекты прототипов интерфейса Internet Explorer 8 сначала наследуют от внутренних прототипов (недоступных языку JavaScript), которые реализуют внутренние версии тех же свойств, которые доступны "общим" прототипам. Такая особенность реализации становится очевидной только при удалении встроенных свойств DOM из объектов прототипов. Данная особенность реализации отражена в иерархии прототипов на следующем рисунке: удаление свойства innerHTML из Element.prototype вызывает наследование внутреннего свойства innerHTML. Это выглядит как "восстановление" (с помощью наследования) состояния по умолчанию для встроенного свойства.


Рис. 4. Цепочка прототипов для экземпляра div со связанными с реализацией внутренними прототипами

Эффективные сценарии

Чтобы продемонстрировать возможности свойств метода доступа в сочетании с прототипами DOM, рассмотрим два потенциально возможных сценария: в первом сценарии веб-страница предоставляет пользователю механизм для создания заметок в документах (например, при просмотре и совместном использовании документа в Интернете), а затем вставляет эти заметки на веб-страницу с помощью innerHTML. В данном сценарии для веб-страницы действуют два критерия: первый — обеспечение безопасности вставленного контента с использованием toStaticHTML; второй — удаление определенных стилистических элементов и атрибутов от вводимых пользователем данных HTML для предотвращения проблем с разметкой на данной странице. Чтобы упростить данный двухэтапный процесс, веб-страница заменяет функциональные возможности innerHTML на следующий пользовательский код (сокращено для облегчения восприятия):

// Сохранение копии встроенного свойства

var innerHTMLdescriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');

// Определение нового фильтра, который обеспечивает безопасность произвольного HTML-кода и удаляет лишнее форматирование

Object.defineProperty(Element.prototype, 'innerHTML',

  {

    set: function(htmlVal)

      {

        var safeHTML = toStaticHTML(htmlVal);

        // TODO: Код, осуществляющий фильтрацию атрибутов стиля и удаляющий стилистические теги из safeHTML

        // Вызов поведения встроенного innerHTML после завершения операции.

        innerHTMLdescriptor.set.call(this, safeHTML);

      }

  });

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

// Применение addEventListener ко всем прототипам, где он должен быть доступен.

HTMLDocument.prototype.addEventListener =

Element.prototype.addEventListener =

Window.prototype.addEventListener = function (type, fCallback, capture)

{

  var modtypeForIE = "on" + type;

  if (capture)

  {

    throw new Error("Данная реализация addEventListener не поддерживает этап захвата");

  }

  var nodeWithListener = this;

  this.attachEvent(modtypeForIE, function (e) {

    // Добавление нескольких расширений непосредственно в "e" (действительный экземпляр события)

    // Создание свойства "currentTarget" (только для чтения)

    Object.defineProperty(e, 'currentTarget', {

      get: function() {

         // "nodeWithListener", определенный на момент добавления прослушивателя.

         return nodeWithListener;

      }

    });

    // Создание свойства "eventPhase" (только для чтения)

    Object.defineProperty(e, 'eventPhase', {

      get: function() {

        return (e.srcElement == nodeWithListener) ? 2 : 3; // "AT_TARGET" = 2, "BUBBLING_PHASE" = 3

      }

    });

    // Создание "timeStamp" (объект даты, доступный только для чтения)

    var time = new Date(); // Текущее время вызова данной анонимной функции.

    Object.defineProperty(e, 'timeStamp', {

      get: function() {

        return time;

      }

    });

    // Вызов изначально переданного обратного вызова функции-обработчика...

    fCallback.call(nodeWithListener, e); // Изменение базового адреса "this" для правильного выполнения обратного вызова.

  });

}

 

// Расширение Event.prototype с использованием нескольких стандартных API W3C в Event

// Добавление объекта "target" (только для чтения)

Object.defineProperty(Event.prototype, 'target', {

  get: function() {

    return this.srcElement;

  }

});

// Добавление методов "stopPropagation" и "preventDefault"

Event.prototype.stopPropagation = function () {

  this.cancelBubble = true;

};

Event.prototype.preventDefault = function () {

  this.returnValue = false;

};

Соблюдение стандартов

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

// Традиционная версия Object.defineProperty(document, "test",

// { getter: /*...*/, setter: /*...*/ } );

document.__defineGetter__("test", /* функции метода считывания */ );

document.__defineSetter__("test", /* функции метода задания */ );

 

// Традиционная версия Object.getOwnPropertyDescriptor(document, "test");

document.__lookupGetter__("test");

document.__lookupSetter__("test");

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

Пока поддержка стандартного синтаксиса свойства метода доступа не будет реализована в других браузерах, для устранения проблем взаимодействия браузеров рекомендуется использовать обнаружение на уровне возможностей (включая проверку ограничения Internet Explorer 8 только для объектов модели DOM):

if (Object.defineProperty)

{

  // Использование синтаксиса, основанного на стандарте

  var DOMonly = false;

  try

  {

    Object.defineProperty(new Object(), "test", {get:function(){return true;}});

  }

  catch(e)

  {

    DOMonly = true;

  }

}

else if (document.__defineGetter__)

{

  // Использование традиционного синтаксиса

}

else

{

  //не поддерживается ни defineProperty, ни __defineGetter__

}

Ограниченные свойства

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

  • location.hash
  • location.host
  • location.hostname
  • location.href
  • location.search
  • document.domain
  • document.referrer
  • document.URL
  • navigator.userAgent
  • [свойства окна]

Выводы

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