Определение классов или объектов ECMAScript

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

ECMAScript имеет множество методов для создания объектов или классов.

Способ фабрики

Оригинальный способ

Потому что свойства объекта могут быть динамически определены после создания объекта, многие разработчики писали код, подобный следующему, когда JavaScript был впервые введен:

var oCar = new Object;
oCar.color = "blue";
oCar.doors = 4;
oCar.mpg = 25;
oCar.showColor = function() {
  alert(this.color);
};

TIY

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

Однако есть проблема, связанная с возможностью создания нескольких экземпляров car.

Решение: способ фабрики

Чтобы решить эту проблему, разработчики создали функции-фабрики, которые могут создавать и возвращать объекты специфического типа.

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

function createCar() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}
var oCar1 = createCar();
var oCar2 = createCar();

TIY

Здесь все код из первого примера включен в функцию createCar(). Кроме того, есть дополнительная строка кода, которая возвращает объект car (oTempCar) в качестве значения функции. Вызов этой функции создает новый объект, который получает все необходимые свойства и копирует объект car, о котором мы говорили ранее. Таким образом, с помощью этого метода мы можем легко создать две версии объекта car (oCar1 и oCar2), у которых полностью одинаковые свойства.

Передача параметров функции

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

function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//вывод "red"
oCar2.showColor();		//вывод "blue"

TIY

Добавление параметров к функции createCar() позволяет присвоить значения свойств color, doors и mpg создаваемому объекту car. Это делает два объекта с одинаковыми свойствами, но с различными значениями свойств.

Определение методов объекта вне функции-фабрики

Хотя ECMAScript становится все более формальным, методы создания объектов игнорируются, и их стандартизация до сих пор встречает сопротивление. Часть原因是 семантическая (он не выглядит так正式, как использование оператора new с конструктором), а часть - функциональная. Функциональная причина заключается в том, что при этом способе необходимо создавать методы объекта. В предыдущем примере при каждом вызове функции createCar() создается новый функциональный объект showColor(), что означает, что у каждого объекта есть своя версия showColor(). На самом деле, все объекты делят одну и ту же функцию.

Некоторые разработчики определяют методы объекта вне фабричной функции и затем указывают на этот метод через атрибут, чтобы избежать этой проблемы:}

function showColor() {
  alert(this.color);
}
function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = showColor;
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//вывод "red"
oCar2.showColor();		//вывод "blue"

TIY

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

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

Способ конструктора

Создание конструктора похоже на создание фабричной функции. Первый шаг - выбрать имя класса, то есть имя конструктора. По惯例, это имя начинается с большой буквы, чтобы отличаться от переменных, начинающихся с маленькой буквы.除此以外, конструктор выглядит как фабричная функция. Рассмотрим следующий пример:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.showColor = function() {
    alert(this.color);
  };
}
var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);

TIY

Ниже объясняется разница между кодом и фабричным способом. В первую очередь, в конструкторе объект не создается, а используется ключевое слово this. При использовании оператора new для конструктора, перед выполнением первого кода строки создается объект, и только через this можно получить доступ к этому объекту. Затем可以直接 присвоить ему свойства, по умолчанию это возвращаемое значение конструктора (не нужно явно использовать оператор return).

Теперь создание объекта с помощью оператора new и класса Car больше resembles создание обычных объектов в ECMAScript.

Вы можете задать вопрос, существует ли у этого метода та же проблема с управлением функциями, что и у предыдущего метода? Да.

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

Метод прототипа

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

Здесь сначала используется пустой конструктор для установки имени класса. Затем все свойства и методы напрямую передаются свойству prototype. Мы переработали предыдущий пример, код следующий:

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car();
var oCar2 = new Car();

TIY

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

Кроме того, с помощью этого метода можно использовать оператор instanceof для проверки типа объекта, на который указывает переменная. Поэтому下面的 код выведет TRUE:

alert(oCar1 instanceof Car); // Вывод "true"

Проблемы метода прототипа

Метод прототипа может показаться неплохим решением. К сожалению, это не так.

Сначала, этот конструктор не принимает параметры. В методе прототипа, значения свойств не могут быть инициализированы через параметры конструктора, так как свойства color, doors и mpg у Car1 и Car2 равны "blue", 4 и 25 соответственно. Это означает, что значения по умолчанию могут быть изменены только после создания объекта, что может быть весьма раздражающим, но это еще не все. Реальная проблема возникает, когда свойства指向 объекты, а не функции. Функции не вызывают проблем, но объекты редко делятся между несколькими экземплярами. Давайте подумаем о следующем примере:

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.drivers = new Array("Mike","John");
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car();
var oCar2 = new Car();
oCar1.drivers.push("Bill");
alert(oCar1.drivers); // Вывод "Mike,John,Bill"
alert(oCar2.drivers); // Вывод "Mike,John,Bill"

TIY

В приведенном выше коде свойство drivers является указателем на объект Array, который содержит два имени "Mike" и "John". Поскольку drivers является ссылкой на значение, два экземпляра объекта Car указывают на один и тот же массив. Это означает, что добавление значения "Bill" к oCar1.drivers также будет видимо в oCar2.drivers. Вывод любого из этих указателей покажет строку "Mike,John,Bill".

Поскольку при создании объектов возникает так много вопросов, вы, наверное, хотите знать, существует ли разумный способ создания объектов? Ответ да, и это достигается за счет совместного использования конструктора и способа прототипа.

Смешанный способ конструктора/прототипа

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

Мы переписали предыдущий пример, код如下:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
}
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car("red", 4, 23);
var oCar2 = new Car("blue", 3, 25);
oCar1.drivers.push("Bill");
alert(oCar1.drivers); // Вывод "Mike,John,Bill"
alert(oCar2.drivers); // Вывод "Mike,John"

TIY

Теперь это больше напоминает создание обычных объектов. Все нефункциональные свойства создаются в конструкторе, что означает, что又可以 присваивать свойства по умолчанию через параметры конструктора. Поскольку создается только один экземпляр функции showColor(), это означает отсутствие浪费 памяти. Кроме того, добавление значения "Bill" к массиву drivers объекта oCar1 не влияет на массив oCar2, поэтому при выводе значений этих массивов, oCar1.drivers показывает "Mike,John,Bill", а oCar2.drivers показывает "Mike,John". Поскольку используется способ прототипа, можно по-прежнему использовать оператор instanceof для определения типа объекта.

Этот способ является основным способом,采用的 ECMAScript, он имеет характеристики других способов, но не их побочные эффекты. Однако, некоторые разработчики все еще считают, что этот метод не совершенен.

Динамический прототип

Для разработчиков, которые привыкли использовать другие языки, использование смешанного способа конструктора/prototype feels a bit unnatural. В конце концов, при определении класса в большинстве языков объектно-ориентированного программирования свойства и методы визуально обобщены. Давайте рассмотрим следующий класс Java:

class Car {
  public String color = "blue";
  public int doors = 4;
  public int mpg = 25;
  public Car(String color, int doors, int mpg) {
    this.color = color;
    this.doors = doors;
    this.mpg = mpg;
  }
  public void showColor() {
    System.out.println(color);
  }
}

Java很好地打包了Car类的所有属性和方法,因此看到这段代码 можно понять, что он реализует какие функции, он определяет информацию объекта. Критики смешанного способа конструктора/prototype считают, что поиск свойств в конструкторе и методов вне его логически не обоснован. Поэтому они разработали динамический прототип, чтобы предоставить более удобный стиль кодирования.

Основная идея динамического прототипа аналогична смешанному способу конструктора/prototype, то есть в конструкторе определяются атрибуты, не являющиеся функциями, а функциональные атрибуты определяются через атрибуты прототипа. Единственное различие - это положение метода объекта. Вот класс Car, переписанный с использованием динамического прототипа:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
  if (typeof Car._initialized == "undefined") {
    Car.prototype.showColor = function() {
      alert(this.color);
    };
    Car._initialized = true;
  }
}

TIY

Этот конструктор не изменяется до тех пор, пока не проверено, равна ли typeof Car._initialized "undefined". Это строка является наиболее важной частью динамического методического метода. Если этот значение неопределено, конструктор продолжает определять методы объекта с помощью прототипа, а затем устанавливает Car._initialized в true. Если это значение определено (его значение true, typeof возвращает Boolean), метод больше не создается. Кратко говоря, этот метод использует флаг (_initialized), чтобы определить, были ли добавлены какие-либо методы к прототипу. Этот метод создает и присваивает только один раз, и традиционные разработчики OOP будут рады, что этот код выглядит как определение класса в других языках.

Смешанный фабричный способ

Этот способ обычно используется в том случае, когда не можно применить предыдущий способ. Целью является создание假的 конструктора, который возвращает только новый экземпляр другого объекта.

Этот код очень похож на фабричный функционал:

function Car() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}

TIY

В отличие от классического способа, этот способ использует оператор new, делая его похожим на настоящий конструктор:

var car = new Car();

Поскольку в конструкторе Car() используется оператор new, второй оператор new (находящийся вне конструктора) будет проигнорирован, и объект, созданный внутри конструктора, будет передан переменной car.

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

Какой способ выбрать

Как уже было сказано, наиболее широко используемым方法是 комбинированный способ конструктора/прототипа. Кроме того, популярны динамические методические операции, эквивалентные по функциональности методу конструктора/прототипа. Вы можете использовать любой из этих методов. Однако не используйте отдельно классический конструктор или способ прототипа, так как это может привести к проблемам в коде.

Пример

Одной из интересных особенностей объектов является способ решения проблем с их помощью. Одной из наиболее распространенных проблем в ECMAScript является производительность объединения строк. Как и в других языках, строки ECMAScript неизменны, то есть их значения не могут изменяться. Рассмотрим следующий код:

var str = "hello ";
str += "world";

На самом деле, этот код выполняет следующие шаги в фоновом режиме:

  1. Создать строку для хранения "hello ";
  2. Создать строку для хранения "world".
  3. Создать строку для хранения соединенного результата.
  4. Копировать текущее содержимое str в результат.
  5. Копировать "world" в результат.
  6. Обновить str, чтобы он указывал на результат.

Каждый раз, когда выполняется字符串овое соединение, выполняются шаги 2 до 6, что делает эту операцию очень ресурсоемкой. Если повторять этот процесс несколько сотен, а может быть, и тысяч раз, это может привести к проблемам с производительностью. Решением является хранение строк в объекте Array, а затем создание конечной строки с помощью метода join() (параметр - пустая строка). Представьте себе, что вместо предыдущего кода используется следующий код:

var arr = new Array();
arr[0] = "hello ";
arr[1] = "world";
var str = arr.join(" ");

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

  1. Создать строку для хранения результата
  2. Копировать каждую строку в подходящее место в результате

Хотя это решение и хорошо, но есть и лучше методы. Проблема в том, что это код не отражает его намерения точно. Чтобы сделать его более понятным, можно упаковать эту функциональность в класс StringBuffer:

function StringBuffer () {
  this._strings_ = new Array();
}
StringBuffer.prototype.append = function(str) {
  this._strings_.push(str);
};
StringBuffer.prototype.toString = function() {
  return this._strings_.join(" ");
};

Этот код в первую очередь要注意的是 свойство strings, которое по своему значению является частным свойством. У него есть только два метода, это методы append() и toString(). Метод append() принимает один параметр, который добавляет этот параметр к массиву строк, метод toString() вызывает метод join() массива, чтобы вернуть действительно соединенную строку. Чтобы соединить группу строк объектом StringBuffer, можно использовать следующий код:

var buffer = new StringBuffer();
buffer.append("hello ");
buffer.append("world");
var result = buffer.toString();

TIY

Вы можете использовать следующий код для тестирования производительности объекта StringBuffer и традиционного метода конкатенации строк:

var d1 = new Date();
var str = "";
for (var i=0; i < 10000; i++) {
    str += "text";
}
var d2 = new Date();
document.write("Concatenation with plus: ");
 + (d2.getTime() - d1.getTime()) + " миллисекунд");
var buffer = new StringBuffer();
d1 = new Date();
for (var i=0; i < 10000; i++) {
    buffer.append("text");
}
var result = buffer.toString();
d2 = new Date();
document.write("<br />Concatenation with StringBuffer: ");
 + (d2.getTime() - d1.getTime()) + " миллисекунд");

TIY

Этот код выполняет два теста по конкатенации строк, первый с использованием знака +, второй с использованием класса StringBuffer. Каждая операция соединяет 10000 строк. Даты d1 и d2 используются для определения времени выполнения операции. Обратите внимание, что при создании объекта Date без параметров объекту назначается текущая дата и время. Чтобы определить, сколько времени заняла операция конкатенации, нужно вычесть миллисекунды времени (возврат значения метода getTime()) даты. Это один из распространенных методов оценки производительности JavaScript. Результаты теста помогут вам сравнить эффективность использования класса StringBuffer и знака +.