Внимание! Прочитайте, пожалуйста, текст в правой колонке (внизу).
Внимание! Прочитайте, пожалуйста, текст в правой колонке (внизу). Внимание! Прочитайте, пожалуйста, текст в правой колонке (внизу). Homepage Карта сайта Версия для печати

Джентльменский набор Web-разработчика   Ларри Уолл о Perl6   Наблы Система Orphus
 

40. Наследование в JavaScript

[2 декабря 2004 г.] обсудить статью в форуме

  Нет ничего более постоянного, чем временное.
Народная мудрость

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

Чайник 

В JavaScript каждый объект может иметь ассоциацию с другим объектом — так называемый «прототип» (prototype). В случае, если поиск некоторого свойства (или метода — это одно и то же) в исходном объекте заканчивается неудачно, интерпретатор пытается найти одноименное свойство (метод) в его прототипе, затем — в прототипе прототипа и т. д. К примеру, если мы затребовали обращение к obj.prop (или, что абсолютно то же самое, obj['prop']), JavaScript начнет искать свойство prop в самом объекте obj, затем — в прототипе obj, прототипе прототипа obj, и так до конца.

Секреты прототипов

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

Продемонстрируем «классическое» применение прототипов для реализации наследования в JavaScript.

Листинг 1
<pre><script>
//**
//** Базовый "класс" Car (Машина).
//**
function Car() {
  document.writeln("Вызван конструктор Car().");
}

// Определяем новый метод "класса" Car.
Car.prototype.drive = function() { 
  document.writeln("Вызван Car.drive()"); 
}


//**
//** Производный "класс" Zaporojets (Запорожец - тоже Машина).
//**
function Zaporojets() {
  document.writeln("Вызван конструктор Zaporojets().");
}
// Говорим, что прототип Car - "класс" Zaporojets.
Zaporojets.prototype = new Car(); 

// Определяем новый метод "класса" Zaporojets.
Zaporojets.prototype.crack = function() { 
  document.writeln("Вызван Zaporojets.crack()");
}


//**
//** Основная программа.
//**
document.writeln("Программа запущена.");

// Создаем объект производного "класса" Zaporojets.
var vehicle = new Zaporojets();
vehicle.drive(); // (*) вызывается функция базового объекта

// Создаем еще один объект того же класса.
var other = new Zaporojets();
vehicle.crack(); // функция производного объекта
</script></pre>

Запустив данный пример, можно заметить, что с точки зрения "обычного" ООП результат выглядит несколько необычно:

Листинг 2
Вызван конструктор Car().
Программа запущена.
Вызван конструктор Zaporojets().
Вызван Car.drive()
Вызван конструктор Zaporojets().
Вызван Zaporojets.crack()

В объектно-ориентированных языках с поддержкой классов (C++, Java, PHP, Perl, Python и т. д.) конструкторы базовых классов обычно вызываются непосредственно внутри конструкторов производных. В JavaScript, как было уже сказано в предыдущей набле, классов нет, есть только объекты. Здесь мы видим совершенно другую картину: конструктор Car запустился даже до вывода сообщения "Программа запущена"! Кроме того, при повторном создании объекта Zaporojets конструктор Car вызван не был, а значит, один и тот же объект Car «разделяется» многими объектами Zaporojets! С точки зрения идеологии наследования это совершенно неправильно.

К сожалению, невозможно задать прототип для некоторого объекта, не создав предварительно объект базового класса. Если вы хотите присвоить Zaporojets.prototype новое значение, вы просто обязаны использовать оператор new Car(). Иными словами, создание подобъекта базового «класса» производится в JavaScript не в конструкторе производного (как во всех остальных объектно-ориентированных языках), а гораздо раньше, еще на этапе конструирования «класса-потомка», и при том однократно.

Подобное поведение, конечно, следует из того, как написана программа. Действительно, мы создали объект Car только один раз — при присваивании значения прототипу Zaporojets; соответственно, и его конструктор был вызван в этот момент лишь однажды.

Чайник 

Вывод: в JavaScript «стандартное» наследование реализуется совсем не так, как в других, «класс-ориентированных» языках программирования. Понятие «конструктора» в нем — не то же самое, что конструктор в C++, Java или даже Perl.

Чем не являются прототипы?

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

Листинг 3
var obj = {
  // В самом объекте свойства prop нет.
  // Зато у него есть прототип...
  prototype: {
    // ...в котором данное свойство определяется...
    prop: 101
  }
  // ...так что в итоге интерпрететор должен считать его.
}
// Проверим?
alert("Значение свойства: " + obj.prop); // What a...

Увы и ах: данный пример не работает, выдавая: "Значение свойства: undefined". А следовательно, присваивание свойству prototype произвольного объекта нового значения ничего нам не дает!

Модифицируем теперь код программы:

Листинг 4
var obj = {
  // В самом объекте свойства prop нет.
}
// Пробуем обратиться к прототипу по-другому.
obj.constructor.prototype.prop = 101;
// Проверим?
alert("Значение свойства: " + obj.prop);
// В этом-то объекте свойства быть не должно...
var newObj = {}; // пустой хэш
alert("Пустота: " + newObj.prop); // А это еще откуда?!

Результат "Значение свойства: 101" говорит нам, что программа заработала. Однако какой ценой? Свойство prop теперь появилось вообще в любом объекте, создаваемом когда-либо в программе, а не только в obj! Убедиться в этом позволяет второй вызов alert(), гордо сообщающий, что «пустота», оказывается, является числом 101. («Просветлей сам — просветлятся все существа в мире.»)

Чайник 

Какие выводы можно сделать из примера?

  1. В самом объекте свойство prototype не имеет никакого особого смысла.
  2. К прототипу объекта следует обращаться через служебное свойство constructor, присутствующее в любом хэше.
  3. Выражение obj.constructor.prototypeне obj.prototype! это важно!) означает прототип объекта.

Оператор new и obj.constructor

Новый объект в JavaScript может быть создан только одним способом: применением оператора new:

Листинг 5
var vehicle = new Car(); // создание нового объекта
var hash  = {}; // сокращенная запись для new Object()
var array = []; // сокращенная запись для new Array()

Немногие над этим задумываются, но первый оператор примера полностью эквивалентен такому коду:

Листинг 6
var vehicle = new window.Car(); // можно и так...
var vehicle = new self.Car();   // в браузере self==window

или даже такому:

Листинг 7
var clazz = self.Car; // ссылка на функцию Car()
var vehicle = new clazz();   // неявное создание!

Он также функционально не отличается от следующего примера:

Листинг 8
// Создание объекта стандартным способом.
self.Car = function() { alert("Car") }
var vehicle = new self.Car();

Ну что, понравилось? Начали улавливать закономерности? Вот еще примеры:

Листинг 9
// Создаем "класс" на лету.
var clazz = function() { alert("Динамическая!") }
var obj = new clazz();
// А можно и без промежуточной переменной.
var obj = new (function() { alert("Wow!") })();

Иными словами, справа от new может стоять любое значение JavaScript. Это совсем не обязательно имя функции — к тому же, что такое функция, как не переменная, значение которой является ссылка на код?

Так вот, после создания объекта интерпретатор присваивает его свойству constructor значение, равное величине, стоящей справа от оператора new. Таким образом, vehicle.constructor == self.Car, а obj.constructor в последнем примере вообще ссылается на функцию, не имеющую отдельного имени в глобальной области видимости (анонимную). Это настолько важно, что я приведу еще один поясняющий пример:

Листинг 10
// Создаем "класс" на лету.
var clazz = function() { alert("Динамическая!") }
var obj = new clazz();
alert(obj.constructor == clazz); // выводит true!

Но позвольте, ведь справа от new не может стоять совсем уж все, что угодно. К примеру, там недопустимо число или строка... Следующий пример также не работает:

Листинг 11
var clazz = {};        // clazz.constructor == self.Object
var obj = new clazz(); // не работает!

Что же можно использовать с оператором new? Ответ прост: только функции (точнее, объекты, конструктор которых равен self.Function). А если еще точнее — разрешено использовать стандартные объекты JavaScript self.Array, self.String и т. д.

Оказывается, что свойство prototype со специальным назначением есть только у таких объектов, которые могут быть использованы в правой части new! Например, допустимы обращения к Function.prototype, String.prototype или Array.prototype.

Теперь вы понимаете, почему JavaScript не рассматривает элемент obj.prototype произвольного хэша obj как специальный, но обращается к obj.constructor.prototype? Ведь специальное назначение prototype имеет только для встроенного объекта, коим всегда является ссылка obj.constructor.

Чайник 

Итак, вывод: прототипы объектов доступны по цепочке obj.constructor.prototype.constructor.prototype..., а не obj.prototype.prototype, как можно понять из многих руководств по JavaScript в Интернете. Конструктором объекта может быть только объект встроенного класса (обычно это Function).

Заставляем конструкторы базовых классов работать

Данная набла имеет циклический характер, и сейчас, хорошо понимая, как работают прототипы и конструкторы, мы снова возвращаемся к самому первому примеру. Речь пойдет о создании базового и производных объектов в стиле «класс-ориентированного» программирования.

Итак, перед нами стоят следующие задачи:

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

Если программировать на «чистом» JavaScript, данные две задачи выливаются в довольно громоздкий код. Чтобы каждый раз его не писать, я предлагаю вам использовать совсем небольшую библиотечку, обеспечивающую удобное применение рассматриваемых подходов. С ее использованием создание производных классов выглядит весьма просто:

Листинг 12
<script src="Oop.js"></script>
<pre><script>
// Базовый "класс".
Car = newClass(null, {
  constructor: function() {
    document.writeln("Вызван конструктор Car().");
  },
  drive: function() { 
    document.writeln("Вызван Car.drive()"); 
  }
});

// Производный "класс".
Zaporojets = newClass(Car, {
  constructor: function() {
    document.writeln("Вызван конструктор Zaporojets().");
    this.constructor.prototype.constructor.call(this);
  },
  crack: function() { 
    document.writeln("Вызван Zaporojets.crack()");
  },
  drive: function() { 
    document.writeln("Вызван Zaporojets.drive()");
    return this.constructor.prototype.drive.call(this);
  }
});

document.writeln("Программа запущена.");

// Создаем объект производного "класса".
var vehicle = new Zaporojets();
vehicle.drive(); // вызывается функция базового объекта

// Создаем еще один объект того же класса.
var vehicle = new Zaporojets();
vehicle.crack(); // функция производного объекта
</script></pre>

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

Листинг 13
Программа запущена.
Вызван конструктор Zaporojets().
Вызван конструктор Car().
Вызван Zaporojets.drive()
Вызван Car.drive()
Вызван конструктор Zaporojets().
Вызван конструктор Car().
Вызван Zaporojets.crack()

Как видите, все работает так, как и ожидает программист на «класс-ориентированном» языке: конструктор Car() вызывается вместе с конструктором Zaporojets(). Однако запускать конструктор базового класса в конструкторе производного нужно явно (заодно приведено, как вызывать метод drive из базового объекта, если он был переопределен в производном):

Листинг 14
// Вызов конструктора базового объекта.
this.constructor.prototype.constructor.call(this);
// Вызов переопределенного метода базового объекта.
this.constructor.prototype.drive.call(this);
// У стандартного метода call() можно указывать 
// дополнительные аргументы (после this), которые 
// будут переданы функции-члену объекта.

Библиотека Oop.js состоит из определения одной-единственной функции newClass. Она невелика, однако детальный разбор механизма ее работы, возможно, займет у вас немало времени (по крайней мере, я потратил не один час на эксперименты в разных браузерах). Могу сказать, что информации данной наблы должно быть вполне достаточно.

Листинг 15
//
// Create proper-derivable "class".
//
// Version: 1.2
//

function newClass(parent, prop) {
  // Dynamically create class constructor.
  var clazz = function() {
    // Stupid JS need exactly one "operator new" calling for parent
    // constructor just after class definition.
    if (clazz.preparing) return delete(clazz.preparing);
    // Call custom constructor.
    if (clazz.constr) {
      this.constructor = clazz; // we need it!
      clazz.constr.apply(this, arguments);
    }
  }
  clazz.prototype = {}; // no prototype by default
  if (parent) {
    parent.preparing = true;
    clazz.prototype = new parent;
    clazz.prototype.constructor = parent;
    clazz.constr = parent; // BY DEFAULT - parent constructor
  }
  if (prop) {
    var cname = "constructor";
    for (var k in prop) {
      if (k != cname) clazz.prototype[k] = prop[k];
    }
    if (prop[cname] && prop[cname] != Object)
      clazz.constr = prop[cname];
  }
  return clazz;
}

обсудить статью в форуме

 
Рекламный блок
   

На странице:
    40. Наследование в JavaScript
Секреты прототипов
Чем не являются прототипы?
Оператор new и obj.constructor
Заставляем конструкторы базовых классов работать

Важное объявление:
    автор категорически против копирования и распространения в Интернете всех статей «Куроводства» с возрастом, меньшим 6 месяцев. Печальный опыт «расползания» чрезвычайно устаревших ошибочных версий статьи про Apache действительно объясняет такое решение.

Орфография на «Куроводстве»:
    если вы заметили орфографическую, стилистическую или другую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter. Выделенный текст будет немедленно отослан вебмастеру, а Вы даже ничего и не заметите — настолько быстро все произойдет.

На заметку:
    если вы уже вскипели насчет дизайна этой страницы, то присмотритесь повнимательнее к названию, почитайте FAQ, сходите по лебедевским местам, как это уже предлагалось выше. Можно ли считать пародию плагиатом? Надеюсь, что нет.

Параметры этой страницы
   
GZip

Ссылки от спонсоров
   


Дмитрий Котеров | 2 декабря 2004 г. ©1999-2016 | Генеральный спонсор: Хостинг «Джино» | Контакт Вернуться к оглавлению