Блог Сергея Байдачного

Мой блог о технологиях

Archive for 29 августа, 2011

Зарисовки по JavaScript для .NET разработчиков: Конструкторы и наследование

2 комментария

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

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

 

   1:  function Employee() {
   2:  return new Person();
   3:  }
   4:   
   5:  function Person() {
   6:  this.firstName = "Sergiy";
   7:  this.lastName = "Baydachnyy";
   8:  };
   9:   
  10:  var obj = new Employee();
  11:   
  12:  alert(obj.firstName);

 

Используя эту возможность, можно реализовать обработку ошибки, которая возникнет, если программист вызовет функцию-конструктор как обычную функцию (то есть без оператора new):

 

   1:  function Person() {
   2:  if (!(this instanceof Person)) {
   3:     return new Person();
   4:  }
   5:  this.firstName = "Sergiy";
   6:  this.lastName = "Baydachnyy";
   7:  }
   8:   
   9:  var obj = Person();
  10:   
  11:  alert(obj.firstName);

 

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

Вызов конструктора базового класса.

Мой любимый способ наследования, это вызов конструктора «базового класса» в конструкторе «производного класса». Иными словами, создавай объект с помощью функции-конструктора, Вы инициируете вызов другой (других) функции-конструктора, передавая текущий контекст. При этом инициализация последней выполняется без использования оператора new (так как он создает новый контекст), а прямым вызовом функции. В этом случае, создаваемый объект получает все данные и методы, объявленные в вызываемой функции-конструкторе. Посмотрим на примере:

 

   1:  function Person(fName, lName) {
   2:  this.firstName = fName;
   3:  this.lastName = lName;
   4:  this.Name = (function () { return this.firstName + " " + this.lastName });
   5:  }
   6:   
   7:  function Developer(fName, lName, lang) {
   8:  Person.call(this,fName, lName);
   9:  this.language = lang;
  10:  }
  11:   
  12:  var d = new Developer("Sergiy", "Baydachnyy", "C#");
  13:   
  14:  alert(d.Name());

 

Из кода видно, что мы используем метод call для установки контекста функции-конструктору. Аналогично можно было бы использовать и метод apply. Данный подход мне нравится тем, что позволяет получить все свойства и методы наследуемого объекта, с возможностью их вызова внутри самого конструктора.

Между тем у этого подхода есть несколько недостатков:

· Методы, которые объявляются в прототипах, никоим образом не попадают в производный объект. Иными словами в производный объект попадает все, что объявлено с помощью this;

· Определить связь с наследуемым объектом нет никакой возможности. Иными словами instanceof полностью работать не будет;

· Поскольку методы в JavaScript также являются объектами и занимают какую-то память, то эффективнее использовать ссылку на метод вместо его дублирования.

В связи с этими недостатками, рассмотрим другой вариант наследования, который убирает перечисленные проблемы, но имеет свои недостатки.

Использование прототипа.

Метод, основанный на использовании прототипов, состоит в том, что в свойство prototype производного объекта устанавливается ссылка на базовый объект (созданный с помощью new). В этом случае производный объект получает доступ ко всем полям базового объекта. При этом цепочка наследований может быть сколь угодно длинной.

 

   1:  function Person(fName, lName) {
   2:  this.firstName = fName;
   3:  this.lastName = lName;
   4:  this.Name = (function () { return this.firstName + " " + this.lastName });
   5:  }
   6:   
   7:  function Developer(lang, fName, lName) {
   8:  this.language = lang;
   9:  this.firstName = fName;
  10:  this.lastName = lName;
  11:  this.constructor = Developer;
  12:  }
  13:   
  14:  Developer.prototype = new Person();
  15:  var d = new Developer("C#", "Sergiy", "Baydachnyy");
  16:   
  17:  alert(d.Name());

 

Обратите внимание, что этот код не только устанавливает свойство prototype, но и изменяет свойство constructor, которое меняется по ходу установки prototype.

Этот подход также обладает рядом недостатков:

· Установку прототипа нужно выполнять вне функции-конструктора. Это означает, что мы не сможем воспользоваться методами базового объекта внутри конструктора производного;

· Поскольку при инициализации прототипа мы задаем шаблон для всех будущих объектов, нет смысла использовать параметры в конструкторе базового объекта. Данные нужно дублировать, в противном случае они будут общими для всех объектов. Мы можем даже написать код, который удаляет лишние поля данных из прототипа;

· Изменение прототипа скажется на всех объектах, включая производные;

· Код по конструированию объекта разбит на отдельные части в разных областях приложения, что меня, как ООП разработчика очень пугает;

Комбинация двух методов.

Часто, чтобы перенести данные и методы базового объекта в производный, сохранив при этом цепочку прототипов, прибегают к комбинации обоих методов. То есть, в конструкторе производного объекта вызывают функцию-конструктор базового (в текущем контексте), а далее устанавливают прототипы. Подобный подход нас практически избавляет от недостатков первого метода, но недостатки второго метода реализации наследования, остаются.

Наследование в ECMA Script 5

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

Мне этот метод не особо нравится, так как он также не позволяет красиво группировать код. В тоже время он существует и ECMA Script 5 определяет специальный метод у Object, который создает объект наследник через прототип:

 

   1:  function Person(fName, lName) {
   2:  this.firstName = fName;
   3:  this.lastName = lName;
   4:  Person.prototype.Name = (function () { return this.firstName + " " + this.lastName });
   5:  }
   6:   
   7:  var s = Object.create(new Person("Sergiy", "Baydachnyy"));
   8:   
   9:  //теперь можно расширять созданный объект

 

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

Вот, например, интересный код, который мне пришел в голову:

 

   1:  function Person(fName, lName) {
   2:  this.firstName = fName;
   3:  this.lastName = lName;
   4:  Person.prototype.Name = (function () { return this.firstName + " " + this.lastName });
   5:  }
   6:   
   7:  function Developer(lang, fName, lName) {
   8:   
   9:  function IDeveloper() {
  10:  }
  11:   
  12:  IDeveloper.prototype = new Person(fName,lName);
  13:  var that = new IDeveloper();
  14:  that.language = lang;
  15:  that.constructor = Developer;
  16:  return that;
  17:  }
  18:   
  19:  var d = new Developer("C#", "Sergiy", "Baydachnyy");

 

Этот код характерен тем, что не позволяет расширять прототипы за пределами конструктора, так как каждый раз создается новый объект типа IDeveloper, но с точки зрения ООП выглядит интересно. Наверняка его можно и улучшить.

Written by Sergiy Baydachnyy

29.08.2011 at 18:24

Опубликовано в Internet Explorer 10, Internet Explorer 9, JavaScript

Tagged with

Зарисовки по JavaScript для .NET разработчиков: определение типа, undefined и неявное приведение

leave a comment »

Из предыдущих статей Вы уже убедились, что для работы с переменными в JavaScript вовсе необязательно знать тип переменной. Достаточно объявить саму переменную с помощью ключевого слова var, а затем присвоить ей любой объект (включая функцию). Причем повторное присваивание можно выполнить в любом месте кода (естественно в области видимости), присвоив совершенно другой объект. Однако, часто возникает необходимость узнать тип переменной (объекта, на который ссылается переменная).

Если Вы посмотрите синтаксис JavaScript, то сможете увидеть возможность определения типа с помощью typeof. Реализовав небольшой пример можно прийти к выводу, что оно работает:

 

   1:  var n = 3;
   2:  var s = "Hello"
   3:   
   4:  //вернет number
   5:  alert(typeof n);
   6:   
   7:  //вернет string
   8:  alert(typeof s);

 

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

 

   1:  function Person() {
   2:     this.firstName = "Sergey";
   3:     this.lastName = "Baydachnyy";
   4:  };
   5:   
   6:  var obj = new Person();
   7:   
   8:  //вернет object
   9:  alert(typeof obj);

 

Сказать, что механизм отработал неправильно – нельзя (таки object), но нам от этого как-то не очень хорошо.

Поэтому, чтобы определить точный тип объекта, используется специальное свойство – constructor. Это свойство, в отличие от prototype, доступно через ссылку на наш объект, и содержит ссылку на объект-конструктор. Таким образом, можно использовать код ниже:

 

   1:  //возвращает true
   2:  alert(obj.constructor==Person);

 

Вместо constructor можно использовать оператор instanceof, который позволит определить, является ли объект экземпляром заданного типа, но это работает лишь тогда, когда у Вас нет сложной иерархии наследования. Так код ниже напечатает два сообщения TRUE, так как наш объект является как объектом типа Person так и объектом типа Object:

 

   1:  function Person() {
   2:     this.firstName = "Sergey";
   3:     this.lastName = "Baydachnyy";
   4:     this.Name = (function () { return this.firstName + " " + this.lastName; });
   5:  };
   6:   
   7:  Person.prototype.testF = (function () { return "testF"; });
   8:   
   9:  var obj = new Person();
  10:   
  11:  if (obj instanceof Object) {
  12:     alert("TRUE");
  13:  }
  14:   
  15:  if (obj instanceof Person) {
  16:     alert("TRUE");
  17:  }

 

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

 

   1:  function Person() {
   2:     this.firstName = "Sergey";
   3:     this.lastName = "Baydachnyy";
   4:     this.Name = (function () { return this.firstName + " " + this.lastName; });
   5:  };
   6:   
   7:  Person.prototype.testF = (function () { return "testF"; });
   8:   
   9:  var obj = new Person();
  10:   
  11:  for (var i in obj) {
  12:     alert(obj[i]);
  13:  }

 

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

 

   1:  function Person() {
   2:     this.firstName = "Sergey";
   3:     this.lastName = "Baydachnyy";
   4:     this.Name = (function () { return this.firstName + " " + this.lastName; });
   5:  };
   6:   
   7:  Person.prototype.testF = (function () { return "testF"; });
   8:   
   9:  var obj = new Person();
  10:   
  11:  for (var i in obj) {
  12:     if (obj.hasOwnProperty(i)&&(typeof obj[i]=="function")) {
  13:        alert(obj[i]);
  14:     }
  15:  }

 

В коде выше мы не только выдаем только методы нашего объекта, исключив свойства и методы прототипа.

Поговорив о типе объектов, хотелось бы остановиться на вопросах преобразования типов. Дело в том, что JavaScript пытается выполнить неявное приведение типов везде, где это возможно. В том же C# конструкция типа int i=0; if (i) {…} являлась бы ошибочной, так как переменная типа int не может быть преобразована к типу bool. В JavaScript же реализовано множество неявных преобразований. Поэтому, чтобы избежать возможной неоднозначности, тут используют сразу два оператора сравнений, это == и === (аналогично != и !==). Первый оператор позволяет выполнить неявное преобразование, а второй – нет. Поэтому, всем разработчикам на .NET рекомендую использовать === и !==, чтобы не получить ошибки, которые в .NET были бы невозможны.

Напоследок отмечу, что если вы пытаетесь получить доступ к свойству или методу, которые не определены (или были удалены) в объекте, то получаете специальное значение undefined. Поэтому, если Вы хотите узнать, существует ли метод или свойство внутри объекта, используйте сравнение с этим значением:

 

   1:  if (obj.val === undefined) {
   2:     alert("Undefined");
   3:  }

Written by Sergiy Baydachnyy

29.08.2011 at 07:24

Опубликовано в JavaScript

Tagged with