ECMAScript 5 Часть 1: повторно используемый код

В июне мы писали о поддержке браузером IE9 спецификации ECMAScript 5 (ES5) и опубликовали демонстрацию на сайте TestDrive, в которой можно поэкспериментировать с некоторыми из этих возможностей. С доступностью бета-версии IE9 настало подходящее время поговорить о том, как разработчики могут использовать ES5 для улучшения собственного кода. Многие возможности ES5 спроектированы так, чтобы помочь разработчикам оперировать с очень большими базами кода, делая язык и более надежным, и более простым для поддержки. В этой серии сообщений мы специально рассмотрим, как ES5 поможет вам писать код, который в большем числе случаев можно использовать повторно.

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

Примером для нас станет простой набор объектов календаря, подобных тем, которые можно встретить в программах веб-почты, наподобие Hotmail или Outlook Web Access.

Создание объектов: newи Object . create

Есть изречение, утверждающее, что лучшим кодом является тот, который не надо писать. Методы конструктора объектов ES5 значительно сокращают количество повторяющегося кода. В качестве примера давайте создадим объект, представляющий назначенную встречу (appointment). Самым простым способом создать объект JavaScript с некоторыми заданными свойствами является использование литералов:

 var appointment = {
    name: "unnamed appointment",
    startTime:  new Date(NaN),
    endTime:  new Date(NaN), 
    location: "unknown"
};

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

 function Appointment(){};
Appointment.prototype = appointment; 
var myAppointment = new Appointment();

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

Этот шаблон хорошо знаком современным разработчикам на JavaScript. Но используя методы конструктора объектов ES5, можно непосредственно создать объект по заданному прототипу:

 var myAppointment = Object.create(appointment);

Любой объект, созданный с помощью такого вызова Object.create будет иметь прототипом объект appointment и наследовать все его свойства; здесь не требуется создавать конструктор.

 var myAppointment = Object.create(appointment);
myAppointment.name = "Dentist Appointment";
myAppointment.startTime = new Date("10/31/2010 08:00");

Такие присвоения переопределяют наследуемые свойства с одинаковыми именами, а не изменяют свойства прототипа. Значение appointment.name остается «unnamed appointment», а значение не присвоенного наследуемого свойства location остается «unknown».

Расширение и ограничение

Теперь, когда мы создали шаблон базового класса и воспользовались прототипом для создания другого экземпляра с помощью Object.create, давайте посмотрим, как ES5 помогает расширить базовый класс для создания других повторно используемых классов. Давайте начнем с создания класса, который будет служить прототипом собрания (meeting). Второй аргумент – дескриптор свойства, который позволяет создать и присвоить значение свойству. Мы присвоим величине conferenceCall значение по умолчанию, которое на всякий случай зарезервируем для дальнейшего, не задавая нового значения.

 var meeting = Object.create(appointment, {
    conferenceCall: {value: "In-person meeting" }
});

Мы можем сделать и по-другому, вызывая метод defineProperty, который также является новым для ES5.

 var meeting = Object.create(appointment);
Object.defineProperty(meeting, "conferenceCall", { value: "In-person meeting" });

Вы можете спросить, чем этот код отличается от следующего:

 var meeting = Object.create(appointment);
meeting.conferenceCall = "In-person meeting"; 

Метод defineProperty устанавливает записываемые, перечислимые и конфигурируемые атрибуты нового свойства в false. Если мы не хотим сделать свойство только для чтения, не перечислимым и конфигурируемым, то мы должны использовать более простой способ инициализации свойства. Параметры определения свойства в Object.create имеют те же значения по умолчанию.

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

 Object.defineProperty(meeting, "duration",
    { get: function () {
        return this.endTime.getHours() - this.startTime.getHours(); 
    }}
);

Теперь мы создадим экземпляр собрания.

 var teamMeeting = Object.create(meeting);
teamMeeting.name = "IE Team Meeting";
teamMeeting.startTime = new Date("10/31/2010 08:00");
teamMeeting.endTime = new Date("10/31/2010 09:00");
teamMeeting.location = "Conference Room 33";

Будучи перечислимым атрибутом свойств, созданных с помощью defineProperty и имеющим значение false по умолчанию, свойства duration и conferenceCall не входят в перечисление переборе членов объекта. Перечисление содержит только те свойства, которые необходимы для описания объекта.

Output:

name: IE Team Meeting

startTime: Sun Oct 31 08:00:00 PDT 2010

endTime: Sun Oct 31 09:00:00 PDT 2010

location: Conference Room 33

Вместе с конфигурируемым атрибутом свойства, установленным по умолчанию в false, пользователи класса не могут переписать duration, как вычислимое свойство.

Клонирование

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

 function naiveClone (obj) {
  var n={};
  for (var p in obj) {
    n[p]=obj[p];
    }; 
  return n;
}

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

 Object.clone = function (o) {
  var n = Object.create(Object.getPrototypeOf(o));
  var props = Object.getOwnPropertyNames(o);
  var pName;
  for (var p in props) {
    pName = props[p];
    Object.defineProperty(n, pName, Object.getOwnPropertyDescriptor(o, pName));  
  };
  return n;
};

Функция getOwnPropertyNames извлекает только те свойства, которые уникальны для данного объекта. Так, например, она не клонирует наследуемые методы, которые существуют во всех объектах, наподобие toString и valueOf. Возвращаясь к нашему предыдущему примеру, используя getOwnPropertyNames, можно создать серию собраний команды на несколько следующих месяцев:

 var quarterlyTeamMeeting = Object.clone(teamMeeting);
quarterlyTeamMeeting.startTime = new Date("12/31/2010 07:00");
quarterlyTeamMeeting.endTime = new Date("12/31/2010 09:00");

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

Считыватели и установщики свойств

Считыватели и установщики свойств находятся среди наиболее ожидаемых возможностей ES5. Предположим у вас есть свойство, которое запускает событие, когда кто-то устанавливает его поля. В этом примере мы переопределим свойство location так, чтобы при изменении значения location, генерировалось событие, позволяющее подписчикам убедиться в том, что помещение доступно. Чтобы сделать это, мы поменяем определение собрания и создадим новое свойство, которое генерирует событие при установке.

 (function () {
    var loc = "undetermined location";

    Object.defineProperty(meeting, "location", {
        get: function () { return loc; },
        set: function (value) {
            loc = value;             
            /*fire some event */
        }
    });
})();

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

Во время IE8, когда спецификация ES5 только создавалась, мы запустили поддержку методов доступа для объектов DOM. Теперь, когда спецификация утверждена, у нас имеется полная поддержка этой возможности.

Продвигая веб вперед с помощью ES5

Есть несколько способов применения ES5 для создания надежного кода многократного использования. Запомните, для использования этих возможностей браузер должен быть в режиме IE9 StandardsDocumentMode, который в IE9 является режимом по умолчанию для стандартного типа документа.

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

  • Нет новому синтаксису. Когда появляется синтаксическая ошибка, страница прекращает загружаться. Многие концепции, использованные в сообщении, являются новыми для ES5 и спроектированы для упрощения методов в конструкторе объектов, таким образом, усиливая синтаксис вызова метода.
  • Сокращение числа коллизий между концепциями. Введение новых членов в Object может вызвать конфликт с библиотеками, развивающими прототип Object. Отметим, что использованные выше функции являются расширениями конструктора Object (c большой буквы «О»), который в меньшей степени использует шаблон проектирования.
  • Соответствующее разбиение по уровням абстракции. Методы, лежащие над уровнями и манипулирующие структурами и определениями объектов четко отделяются от методов «бизнес-логики».

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

Аманда Силвер (Amanda Silver)

Руководитель программы JavaScript