Статьи

Статья объектно-ориентированного программирования JavaScript, часть 2

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

аргументы

В каждой функции автоматически создается закрытая переменная — argument , содержащая массив аргументов, переданных функции. Например:

 function testArg(){  for(i=0;i<arguments.length;i++){    alert("Argument "+i+" is "+arguments[i]);  }  } 

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

Поэтому мы можем использовать:

 testArg("PageResource","SitePoint","JavaScriptCity",        "WebSite Abstraction"); 

… Чтобы получить уведомление о некоторых из моих любимых сайтов веб-разработки.

Сложный пример

Теперь, когда у нас есть основы в объектно-ориентированном программировании на JavaScript, давайте создадим сложный объектно-ориентированный пример — библиотеку. Мы просто будем отслеживать некоторую основную информацию, такую ​​как названия книг, авторы, страницы и цена. Для этого у нас будет объект Person (который представляет каждого автора), объект Book и объект Library . Сначала давайте создадим конструктор объекта Person() :

 function Person(lastName, firstName){  this.lastName = lastName;  this.firstName = firstName;  } 

А теперь давайте создадим несколько экземпляров нашего объекта Person :

 var DnnyGdmn = new Person("Goodman","Danny");  var DvdFlngn = new Person("Flanagan","David");  var TmMyrs = new Person("Myers","Tom");  var AlxNkmvsky = new Person("Nakhimovsky","Alexander"); 

Далее, давайте создадим наш объект Book . Его свойства будут:

  • заглавие
  • страницы
  • цена
  • авторы)

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

 function Book(title, pages, price){  this.title = title;  this.pages = pages;  this.price = price;  this.authors = new Array(arguments.length-3);  for(i=0;i<arguments.length-3;i++){    this.authors[i] = arguments[i+3];  }  } 

Первая часть этого кода должна казаться простой; тем не менее, последняя часть не может. Итак, давайте рассмотрим это более внимательно:

 this.authors = new Array(arguments.length-3); 

Это создает свойство author для нашего объекта Book . Свойство author само является объектом Array . Когда мы вызываем наш конструктор Book() , первые три аргумента — это title, pages и price соответственно, поэтому те аргументы, которые указаны после них, являются нашими авторами. Поэтому, если мы передадим пять аргументов, мы знаем, что два из них должны быть авторами. Таким образом, мы можем создать объект Array с длиной arguments.length-3 .

 for(i=0;i<arguments.length-3;i++){  this.authors[i] = arguments[i+3];  } 

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

   var JavaNut = new Book("Java Foundation Classes in a  Nutshell", 731, 29.95, DvdFlngn);  var JSTDR = new Book("Javascript: The Definitive Guide (3rd  Edition)", 776, 39.95, DvdFlngn);  var JSBible = new Book("Javascript Bible, 4th Edition",  1200, 49.99, DnnyGdmn);  var DHTMLTDR = new Book("Dynamic Html: The Definitive  Reference", 1073, 44.95, DnnyGdmn);  var JSObj = new Book("JavaScript Objects", 450, 39.99,  TmMyrs, AlxNkmvsky); 

Обратите внимание, что мы передаем экземпляры объекта Person в качестве последних аргументов для создания свойства authors объекта Book . Ключевой концепцией в дизайне ООП (как и в дизайне реляционной базы данных) является предотвращение повторения в данных. Поэтому мы создаем один объект Person для каждого отдельного автора. Итак, хотя Дэвид Фланаган может написать более одной книги, мы всегда ссылаемся на один и тот же объект Person . Кроме того, если Дэвид когда-либо решит изменить свое имя на «Бибоп», мы можем легко изменить его для всех записей, просто изменив один объект Person который содержит эту информацию. Кроме того, обратите внимание, что вместо передачи примитивных типов данных мы могли бы передавать объекты. Например, для заголовка мы могли бы передать объект String , а для номера страницы мы могли бы передать объект Number . Тем не менее, здесь, они не будут использоваться много, поэтому мы использовали примитивный тип данных — это идеально соответствует нашим потребностям.

Теперь давайте перейдем к, пожалуй, самому сложному конструктору объектов, конструктору Library() . Я собираюсь разбить это на части:

 function Library(){  this.books = new Array(arguments.length);  for(i=0;i<arguments.length;i++){    this.books[i] = arguments[i];  } 

Первое, что вы можете заметить об этой функции, это то, что она не имеет параметров. Это потому, что он принимает только объекты Book , хотя мы не знаем, сколько их. Он создает свойство объекта Library , books , в котором хранятся объекты Array of Book . Допустим, мы хотели получить доступ к первому в списке автора книги. Мы могли бы использовать:

 this.books[ bookIndex ].authors[0] 

Сначала мы получаем доступ к свойству book библиотеки, которое является объектом Array . Затем мы получаем доступ к определенному объекту Book . Из этого объекта Book мы получаем доступ к свойству authors , которое является массивом. Наконец, мы получаем доступ к определенному объекту Person . И оттуда мы можем перейти к свойству firstName или lastName объекта Person . Обратите внимание, что bookIndex — это индекс книги, к которой мы хотим получить доступ.

Теперь это единственное свойство, которое будет содержать наш объект Library . Остальные будут методы:

 this.totalPrice = function(){  var totalCost = 0;  for(i=0;i<this.books.length;i++){    totalCost += this.books[i].price;  }  return totalCost;  } 

Этот метод перебирает наше свойство books , которое является объектом Array , берет цену каждого объекта Book складывает ее и, наконец, возвращает значение.

 this.averagePrice = new Function("return this.totalPrice  ()/this.books.length"); 

Этот метод берет общую стоимость всех наших книг и делит ее на количество книг, которые у нас есть, чтобы узнать среднюю цену наших книг. Итак, как только мы создадим объект Library , как мы добавим к нему больше книг? Нам нужно создать еще одну функцию:

 this.addBook = new Function("book", "this.books.push(book)"); 

Это использует встроенный метод Array , push() . Метод push() добавляет значение или объект, переданный в качестве аргумента, к его объекту Array , при этом обязательно изменяя свойство length Array . Наконец, мы создадим метод для отображения имен авторов в нашей библиотеке. Этот метод довольно длинный, поэтому я разделю его:

 this.getAuthors = function(){  var toSay = "Your favorite authors are:n"; 

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

 for(i=0;i<this.books.length;i++){  for(j=0; j<this.books[i].authors.length;  j++){    var authName =    this.books[i].authors[j].firstName + " " +    this.books[i].authors[j].lastName; 

Эта часть кода проходит по всем книгам, а затем проходит по всем авторам этой книги, помещая их имена в переменную authName .

 if(toSay.indexOf(authName)!=-1) continue;  toSay+="nt"+authName; 

Если этот автор уже находится в переменной toSay , мы не хотим добавлять его снова, поэтому продолжаем просматривать авторов этой книги. Однако, если автора еще нет в списке, мы можем добавить его или ее в переменную toSay .

       }    }    return toSay;  }  } 

Это закрывает два цикла for мы открыли, и возвращает переменную toSay . Он также закрывает метод, который мы определили, getAuthors() , а также закрывает конструктор Library() . Теперь давайте соберем весь код и создадим новый объект Library :

 // define our Person() constructor  function Person(lastName, firstName){  this.lastName = lastName;  this.firstName = firstName;  }  // define our Book() constructor  function Book(title, pages, price){  this.title = title;  this.pages = pages;  this.price = price;  this.authors = new Array(arguments.length-3);  for(i=0;i<arguments.length-3;i++){    this.authors[i] = arguments[i+3];  }  }  //define our Library() constructor  function Library(){  this.books = new Array(arguments.length);  for(i=0;i<arguments.length;i++){    this.books[i] = arguments[i];  }   this.totalPrice = function(){    var totalCost = new Number(0);    for(i=0;i<this.books.length;i++){      totalCost += this.books[i].price;    }  return totalCost;  }   this.averagePrice = new Function("return  this.totalPrice()/this.books.length");   this.addBook = new  Function("book","this.books.push(book)");   this.getAuthors = function(){    var toSay = "Your favorite authors are:n";    for i=0;i<this.books.length;i++){      for(j=0;j<this.books[i].authors.length;j++){        var authName =        this.books[i].authors[j].firstName + " " +        this.books[i].authors[j].lastName;        if(toSay.indexOf(authName)!=-  1)continue;        toSay+="nt"+authName;      }    }  return toSay;  }  }  // create some Person objects  DnnyGdmn = new Person("Goodman","Danny");  DvdFlngn = new Person("Flanagan","David");  TmMyrs = new Person("Myers","Tom");  AlxNkmvsky = new Person("Nakhimovsky","Alexander");  // create some Book objects  JavaNut = new Book("Java Foundation Classes in a  Nutshell",731,29.95,DvdFlngn);  JSTDR = new Book("Javascript: The Definitive Guide (3rd  Edition)",776,39.95,DvdFlngn);  JSBible = new Book("Javascript Bible, 4th  Edition",1200,49.99,DnnyGdmn);  DHTMLTDR = new Book("Dynamic Html: The Definitive  Reference",1073,44.95,DnnyGdmn);  JSObj = new Book("JavaScript  Objects",450,39.99,TmMyrs,AlxNkmvsky);  // create a Library object  myLib = new Library(JavaNut,JSTDR,JSBible,DHTMLTDR); 

К сожалению, мы пропустили книгу объектов JavaScript. Нам лучше добавить это:

 myLib.addBook(JSObj); 

Теперь мы можем получить информацию, например, сколько стоит наша библиотека книг, средняя цена книги и имена авторов, написавших различные книги, которыми мы владеем. Вот и все! Мы завершили сложный пример ООП с помощью JavaScript. Возможно, вы захотите вернуться к любому непонятному коду или не стесняйтесь опубликовать вопрос на форуме по написанию сценариев на стороне клиента на SitePointForums.com .

Опытный образец

Каждый конструктор объекта имеет специальное свойство — prototype . Это свойство позволяет добавлять свойства / методы ко всем объектам, созданным из этого конструктора объектов. Звук сбивает с толку? Это не. Давайте посмотрим на некоторые примеры:

 function Square(){  }  var squareObj = new Square();  Square.prototype.side = 5;  var squareObj2 = new Square();  alert(squareObj.side); // displays 5  alert(squareObj2.side); // displays 5 

Для этого нужно добавить свойство side с начальным значением 5 ко всем объектам Square , независимо от того, были они созданы или еще не созданы. prototype объекта (на самом деле это объект) загружается до того, как конструктор объекта что-либо сделает. Итак, этот код:

 function Square(){  this.side=5;  }  var squareObj = new Square();  Square.prototype.side = 4;  var squareObj2 = new Square();  alert(squareObj.side); // displays 5  alert(squareObj2.side); // displays 5 

возвращает 5, потому что все в объекте- prototype загружается первым (до того, как конструктор объекта Square() даже запустится), поэтому свойства и методы, определенные в конструкторе, переопределят его. Таким образом, с помощью свойства prototype вы не можете переопределить какие-либо свойства или методы, определенные в конструкторе объекта (функция, которая создает объект). Используя свойство prototype объекта String , мы можем добавлять новые методы в объекты String . Рассмотрим этот пример:

 function consonantize(){  var consonants ="";  for(i=0;i<this.length;i++){    var l = this.charAt(i);    if(l!="a" && l!="A" && l!="e" && l!="E" &&    l!="i" && l!="I" && l!="o" && l!="O" && l!="u" && l!="U" && l!="  "){      consonants+=l;    }  }  return consonants;  } 

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

 String.prototype.consonantize = consonantize;  var dg = "Danny Goodman";  var df = new String("David Flanagan");  alert(dg.consonantize());  alert(df.consonantize()); 

Аккуратно, а? Обратите внимание, как новый метод, как и другие методы String , может использоваться объектом String или примитивным типом данных String . Следовательно, используя метод- prototype конструктора объекта, мы можем добавлять свойства и методы как к собственным объектам, так и к пользовательским объектам.

Конструктор

Каждый экземпляр объекта имеет свойство конструктора. Он возвращает объект Function который создал этот экземпляр объекта. Например:

 function myConstructor(){  }  var str = new String("Some String");  var obj = new Object();  var myObj = new myConstructor();  alert(str.constructor); // the native String() constructor  alert(String) // the native String() constructor  alert(obj.constructor); // the native Object() constructor  alert(Object) // the native Object() constructor  alert(myObj.constructor); // the user-defined myConstructor() constructor  alert(myConstructor); // the user-defined myConstructor() constructor 

Я рекомендую вам запустить этот пример, чтобы увидеть, что он возвращает. Обратите внимание, как каждое предупреждение возвращает объект Function который создал этот экземпляр объекта. Также обратите внимание, что нативные объекты JavaScript возвращают «[нативный код]». Когда вы получите typeof для свойства конструктора, вы обнаружите, что оно совпадает с объектом Function который его создал, « function »:

 alert(typeof str.constructor); // "function"  alert(typeof String) // "function"  alert(typeof obj.constructor); // "function"  alert(typeof Object) // "function"  alert(typeof myObj.constructor); // "function"  alert(typeof myConstructor); // "function" 

Все вышеперечисленное возвращает « function ». Поскольку свойство конструктора возвращает ссылку на объект Function который его создал, конструктор фактически является методом конструктора:

 function myConstructor(){  var x = "y";  this.x = "x";  return x;  }  var myObj = new myConstructor();  alert(myObj.constructor); // the myConstructor() function object  alert(myObj.constructor()); // "y" 

Обратите внимание, что в этом примере мы возвращаем локальную переменную x , а не свойство объекта this.x Итак, если у каждого объекта есть метод конструктора, и каждый метод на самом деле является объектом Function , что такое конструктор объекта Function ?

 alert(myConstructor.constructor);  alert(myObj.constructor.constructor);  alert(myConstructor.constructor.constructor);  alert(myObj.constructor.constructor.constructor); 

Все они возвращают нативный конструктор объекта Function() . Хотя это тривиально, я лично подумал, что это довольно интересно, и подумал, что вы тоже можете, и это подводит меня к другому вопросу. Конструкторы являются как «типами объектов», так и самими объектами (точнее, объектами Function ). Таким образом, Date является одновременно объектом (объектом Function ) и «типом объекта», из которого можно создавать объекты Date или экземпляры объекта Date . Это верно для всех собственных объектов и пользовательских объектов.

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

Помимо того, что это метод объекта, constructor() также является методом примитивного типа данных. Так что же это возвращает? В конце концов, не была создана настоящая функция конструктора для создания примитивных типов данных:

 var primitiveString1 = "This is a primitive string";  var primitiveString2 = String("This is a primitive string");  var stringObject = new String("This is a String object");  primitiveString1.prop = "This is a property";  primitiveString2.prop = "This is a property";  stringObject.prop = "This is a property";  alert(primitiveString1.prop) // "undefined"  alert(primitiveString2.prop) // "undefined"  alert(stringObject.prop) // "This is a property"  alert(typeof primitiveString1); // "string"  alert(typeof primitiveString2); // "string"  alert(typeof stringObject) // "object"  alert(primitiveString1.constructor); // "function String(){  [native code] }"  alert(primitiveString2.constructor); // "function String(){  [native code] }"  alert(stringObject.constructor); // "function String(){  [native code] }" 

Как мы видим, и тип данных примитива String объект String имеют один и тот же constructor() , нативный конструктор String() . Обратите внимание, что constructor() — это единственное свойство / метод, который содержится в примитивном типе данных, поэтому эти типы данных имеют доступ к свойствам / методам, определенным в встроенной функции конструктора объекта. Например, примитивный тип данных String (а также объект String ) имеет доступ ко многим свойствам / методам, определенным в собственном конструкторе String() , включая:

  • длина
  • якорь ()
  • большой ()
  • жирный()
  • Charat ()
  • charCodeAt ()
  • CONCAT ()
  • индекс()
  • LastIndexOf ()
  • к югу ()
  • зиЬзЬг ()
  • подстроку ()

Однако объект String может также содержать свойства / методы, которые являются специфическими для этого объекта. Например:

 var myStringObj = new String("This is a String object");  myStringObj.prop = "This is a property of the object I created";  alert(myStringObj.prop) // "This is a property of the object I created" 

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

 function myFunc(param){  param.property = "I want to add this property";  alert(param.property); // "undefined"  } 

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

 myFunc(new String("This is a String object"));  myFunc(new Number(5)); 

Алекс указывает на один из способов преодоления этого:

 function myFunc(param){  param = new param.constructor(param);  param.property = "I want to add this property";  alert(param.property); // returns "I want to add this property"  } 

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

 var myNum = 5;  myNum = new Number(5); 

Теперь давайте сделаем еще один шаг:

 var myNum = 5;  myNum = new myNum.constructor(5); 

Вы должны помнить, что myNum.constructor() совпадает с Number() . Тогда вместо 5 мы можем использовать myNum , так как это тоже 5:

 var myNum = 5;  myNum = new myNum.constructor(myNum); 

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

Прототип Revisited

Давайте вернемся и вернемся к свойству prototype объекта Function . В Java популярная, хорошо известная функция — расширять класс; однако в JavaScript большинство людей не знают, что вы можете сделать это — но вы можете! Например, допустим, у нас есть объект Car . Corvette и Ares — это два разных типа автомобилей, но оба они все еще автомобили. Таким образом, они имеют сходные свойства / методы и распространяются на объект Car .

Давайте создадим три объекта, которые мы собираемся использовать — Car, Corvette и Ares . Затем мы обсудим способы наследования последних двух свойств / методов объекта Car .

 function Car(color){  this.wheels = 4;  this.doors = 4;  this.color = color;  this.speed = 0;  this.accelerate = function(){    this.speed+=20;  }  this.brake = function(){    this.speed-=20;  }  }  function Corvette(color){  // all of Car properties/methods  this.doors = 2;  this.color = color;  this.accelerate = function(){    this.speed+=40;  }  }  function Ares(color){  // all of Car properties/methods  this.doors = 2;  this.color = color;  this.accelerate = function(){    this.speed+=10;  }  this.brake = function(){    this.speed-=10;  }  }  var myCar = new Car("white");  var myCorvette = new Corvette("black");  var myAres = new Ares("red"); 

Поскольку Corvette — это особенно быстрая машина, мы увеличили ее скорость разгона по сравнению с обычной машиной, а Dodge Ares — шаткая старая машина, поэтому мы сделали ее так, чтобы тормоза не работали, и это не ускоряется так быстро (без обид владельцам Dodge Ares). Теперь мы можем использовать свойство прототипа Corvette() и Ares() и добавлять к каждому свойства / методы объекта Car которые мы хотим, чтобы они наследовали. Однако это может быть запутанной и утомительной задачей, особенно если существует много свойств / методов. Чтобы преодолеть это, нам нужно снова изучить свойство prototype .

Свойство prototype — это объект без начальных свойств / методов. Когда мы добавляем свойства / методы к этому объекту, мы автоматически добавляем их ко всем экземплярам объекта. Однако вместо добавления свойств / методов к объекту- prototype мы могли бы заменить объект-прототип объектом, у которого уже есть свойства / методы, которые мы хотим. Например, вместо использования:

 Corvette.prototype.wheels = 4;  Corvette.prototype.speed = 0;  Corvette.prototype.brake = function(){  this.speed-=20;  } 

мы можем более легко использовать:

 Corvette.prototype = new Car(); 

Мы можем сделать то же самое для объекта Ares :

 Ares.prototype = new Car(); 

И объекты Corvette и Ares теперь имеют все свойства / методы Car , которые затем могут быть переопределены свойствами / методами, определенными в каждом конструкторе объектов. Например, в объектах Corvette и Ares свойство двери переопределяется на 2 . В целом наш код теперь выглядит так:

 function Car(color){  this.wheels = 4;  this.doors = 4;  this.color = color;  this.speed = 0;  this.accelerate = function(){    this.speed+=20;  }  this.brake = function(){    this.speed-=20;  }  }  function Corvette(color){  this.doors = 2;  this.color = color;  this.accelerate = function(){    this.speed+=40;  }  }  Corvette.prototype = new Car();  function Ares(color){  this.doors = 2;  this.color = color;  this.accelerate = function(){    this.speed+=10;  }  this.brake = function(){    this.speed-=10;  }  }  Ares.prototype = new Car();  var myCar = new Car("white");  var myCorvette = new Corvette("black");  var myAres = new Ares("red"); 

Теперь из объектов Corvette и Ares мы можем получить соответствующие свойства и запустить методы accelerate() и brake() которые соответствуют этим объектам. Таким образом, в JavaScript наследование объектов не сложно выполнить.

Заворачивать

Я надеюсь, что благодаря этому руководству вы научились понимать принципы работы JavaScript. Кроме того, я надеюсь, что вы получили базовые знания ООП и понимание мощи JavaScript как объектно-ориентированного языка. Я предлагаю вам оставлять любые вопросы на форумах SitePoint ; однако, если вы не можете найти ответ на свой вопрос об объекте JavaScript, я был бы более чем рад дать вам шанс, если вы напишите мне по адресу [email protected]

Было много людей, которые помогли мне написать этот урок. В частности, я хотел бы поблагодарить Алекса Винсента, Джейсона Дэвиса и Джареда за помощь в понимании тонкостей объектных способностей JavaScript.