Статьи

Переосмысление объектов JavaScript

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

Переосмысление наследства

JavaScript, как многие из нас знают, использует основанный на прототипах метод наследования объектов. Это говорит о том, что если у меня есть «класс» под названием ClassA, ClassB может наследовать его, если я превращу прототип ClassB в экземпляр ClassA, например, так:

function ClassA () {  }   function ClassB() {  }   ClassB.prototype = new ClassA; 

Этот метод был дополнен техникой «маскировки объекта», используемой для копирования свойств объекта (но не его методов) в другой объект. Чтобы полностью унаследовать все от ClassA до ClassB, нам действительно нужно сделать это:

 function ClassA () {  }   function ClassB() {        this.superclass = ClassA;        this.superclass();        delete this.superclass;  }   ClassB.prototype = new ClassA; 

Это ужасно много кода для выполнения чего-то, что обрабатывается в Java следующим образом:

 Class ClassA {  }   Class ClassB extends ClassA {  } 

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

Боб Клари, евангелист из Netscape, написал такую ​​простую функцию, что мне стало интересно, почему я сам не подумал об этом. В своей статье «inheritFrom -« Простой метод наследования по требованию »» он определяет простой метод, называемый inheritFrom() который можно использовать для копирования всех свойств и методов объекта в другой объект.

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

 inheritFrom(ClassB, ClassA); 

Хотя это хорошая идея, она просто не вписывается в мой стиль кодирования. Я по-прежнему рассматриваю конструкторы JavaScript как классы и пишу их, думая больше об определении класса Java, чем о конструкторе JavaScript. Мне пришло в голову, что если бы я расширил родной класс JavaScript Object, чтобы включить метод, который делал то же самое, все объекты автоматически получили бы этот метод, и я мог бы по существу написать что-то, что выглядело и чувствовало себя очень похоже на логику Java. Мое решение: метод extends() .

Как и решение Клэри, метод extends() работает на основе того, что все свойства и методы могут быть перебраны с помощью скобочных обозначений, таких как эта:

 object["Property"];  //Same as object.Property 

Сам метод выглядит так:

 Object.prototype.extends = function (oSuper) {        for (sProperty in oSuper) {                this[sProperty] = oSuper[sProperty];        }  } 

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

  • Я использую ключевое слово this, чтобы указать объект, который получает скопированные атрибуты, и
  • Я удалил блок try..catch, чтобы этот метод также можно было использовать в Netscape Navigator 4.x.

Определив этот метод, мы можем теперь сделать следующее:

 function ClassA () {  }   function ClassB() {        this.extends(new ClassA());  } 

Важно отметить, что этот метод должен вызываться первым в определении вашего класса (конструктора). Любые дополнения должны быть сделаны после этого первоначального звонка. Также обратите внимание, что вы должны создать экземпляр объекта класса, от которого хотите наследовать; вы не можете просто передать само имя класса. Например, это неверно:

 function ClassA () {  }   function ClassB() {        this.extends(ClassA);   //INCORRECT!!!!  } 

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

 

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

Переосмысление свойств

В Java мы не часто разрешаем людям прямой доступ к свойствам. Например, редко вы видите что-то вроде этого:

 Class ClassA {      public string message;  }   ClassA Test = new ClassA();  Test.message = "Hello world"; 

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

 Class ClassA {      private string message;       public void setMessage(String msg) {          this.message = msg;      }       public String getMessage() {          return this.message;      }  }   ClassA Test = new ClassA();  Test.setMessage("Hello world"); 

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

 function ClassA() {    this.message = "";  }   var Test = new ClassA();  Test.message = "Hello world"; 

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

После некоторых размышлений я придумал метод addProperty() для нативного объекта JavaScript:

 Object.prototype.addProperty = function (sName, vValue) {         this[sName] = vValue;         var sFuncName = sName.charAt(0).toUpperCase() + sName.substring(1, sName.length);         this["get" + sFuncName] = function () { return this[sName] };        this["set" + sFuncName] = function (vNewValue) {                        this[sName] = vNewValue;        };  } 

Этот метод принимает два параметра: sName — это имя параметра, а vValue — его начальное значение. Первое, что делает метод, это назначает свойство объекту и присваивает ему начальное значение vValue. Затем я создаю имя sFunc для использования в качестве части методов getter и setter… это просто делает заглавной первую букву в имени свойства, чтобы оно выглядело подходящим рядом с «get» и «set» (т.е. если имя свойства « сообщение », методы должны быть« getMessage »и« setMessage »). Следующие строки создают методы получения и установки для этого объекта.

Это можно использовать так:

 function ClassA () {    this.addProperty("message", "Hello world");  }   var Test = new ClassA();  alert(Test.getMessage());    //outputs "Hello world"  Test.setMessage("Goodbye world");  alert(Test.getMessage());    //outputs "Goodbye world" 

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

Вместо этого я решил создать некоторый код, который имитирует событие onpropertychange IE. То есть будет определен метод onpropertychange (), и всякий раз, когда изменяется любое свойство объекта, этот метод вызывается с объектом, описывающим событие. Мой пользовательский объект события имеет только несколько свойств:

  • propertyName — название свойства, которое было изменено
  • propertyOldValue — старая стоимость имущества
  • propertyNewValue — новая стоимость имущества
  • returnValue — по умолчанию true, может быть установлено в false в onpropertychange() чтобы аннулировать изменение

Код теперь выглядит так:

 Object.prototype.addProperty = function (sName, vValue) {         this[sName] = vValue;         var sFuncName = sName.charAt(0).toUpperCase() + sName.substring(1, sName.length);         this["get" + sFuncName] = function () { return this[sName] };        this["set" + sFuncName] = function (vNewValue) {                var vOldValue = this["get" + sFuncName]();                var oEvent = {                        propertyName: sName,                        propertyOldValue: vOldValue,                        propertyNewValue: vNewValue,                        returnValue: true                        };                this.onpropertychange(oEvent);                if (oEvent.returnValue) {                        this[sName] = oEvent.propertyNewValue;                }         };  }   //default onpropertychange() method – does nothing  Object.prototype.onpropertychange = function (oEvent) {   } 

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

По умолчанию метод onpropertychange() ничего не делает. Он предназначен для переопределения при определении новых классов. Если объект пользовательского события возвращается из onpropertychange() returnValue все еще имеет значение true, свойство обновляется. Если нет, свойство не обновляется, что делает его доступным только для чтения.

С этим кодом мы можем сделать следующее:

 

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

 Object.prototype.addProperty = function (sType, sName, vValue) {         if (typeof vValue != sType) {            alert("Property " + sName + " must be of type " + sType + ".");            return;        }         this[sName] = vValue;         var sFuncName = sName.charAt(0).toUpperCase() + sName.substring(1, sName.length);         this["get" + sFuncName] = function () { return this[sName] };        this["set" + sFuncName] = function (vNewValue) {                  if (typeof vNewValue != sType) {                    alert("Property " + sName + " must be of type " + sType + ".");                    return;                }                 var vOldValue = this["get" + sFuncName]();                var oEvent = {                        propertyName: sName,                        propertyOldValue: vOldValue,                        propertyNewValue: vNewValue,                        returnValue: true                        };                this.onpropertychange(oEvent);                if (oEvent.returnValue) {                        this[sName] = oEvent.propertyNewValue;                }         };  } 

Здесь я добавил единственный параметр sType , который определяет тип данных, которые содержит свойство. Я сделал это первым параметром, потому что, опять же, это похоже на Java. Я также добавил две проверки с использованием оператора typeof JavaScript: один при начальном присвоении значения, другой при изменении свойства (в действительности это должно приводить к ошибкам, но для совместимости с Netscape 4.x я выбрал оповещения). Для тех, кто не знает, оператор typeof возвращает одно из следующих значений:

  • «Undefined» — значение не существует.
  • «Строка»
  • «число»
  • «Функция»
  • «Объект»
  • «Логическое»

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

Обновив предыдущий пример, теперь мы можем сделать это:

 
Вывод

Хотя JavaScript оставил нам некоторые серьезные ограничения при создании пользовательских объектов и классов, он также достаточно гибок для поиска решений. JavaScript, кажется, движется в направлении, приближающем его к Java по синтаксису и реализации (см. ( Предложение JavaScript 2.0 ), но в то же время код, представленный в этой статье, должен сделать ваши усилия по разработке немного менее болезненными.

Я протестировал код, представленный в этой статье, на Netscape Navigator 4.79, Internet Explorer 6.0 и Netscape 7.0 (Mozilla 1.0.1), но я считаю, что он должен работать в большинстве современных браузеров.