При использовании наследования в функциональном стиле наборы методов копируются в каждый объект и для большого количества методов такие объекты могут занимать слишком много памяти. Решением данной проблемы является использование прототипного наследования.
У каждого объекта есть скрытое свойство [[Prototype]], которое работает следующим образом - при обращении к какому-либо свойству объекта оно сначала ищется в самом объекте, если его там нет - в прототипе объекта, если нет и там - в прототипе прототипа объекта и т.д. до тех пор пока свойство не будет найдено, либо обращение вернет undefined. Это называется цепочкой прототипов. Наверху цепочки прототипов всегла находится null, который не имеет прототипа.
В браузере свойство [[Prototype]] доступно через __proto__, но это нестандартное свойство и оно доступно не во всех браузерах, потому использоть его явно - плохая практика.
Работу цепочки прототипов с использованием __proto__ можно изобразить следующим образом:
var creature = {
canBreathe: true
};
var animal = {
head: 1
};
var dog = {
legs: 4
};
// прототипное наследование через __proto__
animal.__proto__ = creature;
dog.__proto__ = animal;
dog.legs; // 4, собственное свойство
dog.head; // 1, берется из animal
dog.canBreathe; // true, берется из creature
dog.canSwim; // undefined, не найдено ни в объекте, ни в цепочке прототипов
Иногда говорят, что прототип - это "резервное хранилище" свойств и методов объекта.
Свойства из прототипа берутся только для чтения. При попытке записи или удаления будет изменено свойство объекта, а не прототипа. Но в случае, когда свойства являются массивами или объектами, при изменении внутри объекта могут меняться свойства прототипа.
При наличии свойства у объекта и у прототипа с одинаковым именем при обращении к этому свойству будет браться собственное свойство объекта, т.к. в цепочке прототипов оно будет идти раньше свойства прототипа. Такое явление называется "затенением свойства" (property shadowing). Тоже самое происходит с методами — переопределение методов (method overriding).
Одним из недостатков использования прототипов является то, что без использования специального кода невозможно отделить собственные свойства объекта от свойств из прототипа.
for (var prop in dog) {
console.log(prop);
}
// legs, head, canBreathe
Для проверки принадлежности свойства объекту используют метод hasOwnropery.
dog.hasOwnProperty("legs"); // true
dog.hasOwnProperty("head"); // false
dog.hasOwnProperty("canBreathe"); // false
В ECMAScript 5 был добавлен метод для определения прототипов объектов Object.create.
var creature = {
canBreathe: true
};
// прототипное наследование через Object.create
var animal = Object.create(creature);
animal.head = 1;
var dog = Object.create(animal);
dog.legs = 4;
При наследовании объект-объект следует использовать метод Object.create, а не явное указание __proto__.
Т.к. при создании объектов в цепочке прототипов всегда присутствует Object, то каждый объект "получает" все методы этого объекта: toString, valueOf и т.д. Такое поведение не всегда является желательным. Например, мы используем объекты как простые хранилища данных, и нам необходимо проверить наличие произвольных свойств.
var dog = { legs: 4 };
"legs" in dog; // true, ожидаемо
"toString" in dog; // true, но нам это не нужно
Можно добавлять hasOwnProperty для каждой проверки, но можно поступить проще. По сути, нам нужен объект с пустым прототипом и мы можем создать такой объект с помощью Object.create и указанием в качестве объекта-прототипа null:
var dog = Object.create(null);
dog.legs = 4;
"legs" in dog; // true
"toString" in dog; // false
Для создания прототипа в конструкторе объекта самое простое, что можно сделать — указать __proto__ для this:
var animal = {
head: 1
};
var Dog = function() {
this.legs = 4;
this.__proto__ = animal;
}
var myDog = new Dog();
Но __proto__ — нестандартное свойство и оно доступно не во всех браузерах, потому используется свойство prototype функции-конструктора:
var animal = {
head: 1
};
var Dog = function() {
this.legs = 4;
}
Dog.prototype = animal;
var myDog = new Dog();
Свойство prototype имеет смысл только в случае, если функция используется как конструктор.
При создании функции в ее prototype создается свойство constructor, которое является ссылкой на текущую функцию:
function Dog() {
this.legs = 4;
};
Dog.prototype.constructor == Dog; // true
var myDog = new Dog;
var yourDog = new Dog.prototype.constructor;
var hisDog = new myDog.__proto__.constructor;
Данное свойство можно использовать для создания объектов такого же типа, как и имеющийся, да не зная, какой именно была функция-конструктор.
Для сохранения значения этого свойства обычно не перезаписывают весь prototype, а просто добавляют ему необходимые свойства или методы:
function Dog() {
this.legs = 4;
};
Dog.prototype.head = 1;
var myDog = new Dog;
Object.create доступен только для браузеров с поддержкой ES5, потому имеет смысл написать кроссбраузерную функцию прототипного наследования:
function extend(proto) {
if (Object.create) {
return Object.create(proto);
}
var F = function() {};
F.prototype = proto;
var object = new F;
return object;
}
Объекты можно создавать как с помощью литеральной записи, так и с помощью new Object. Аналогичное справедливо для массивов, строк, функций и чисел. Таком образом о Object, Array, Function, Number, String можно говорить как о конструкторах или как о "встроенных классах".
Например, следующий код работает потому, что создание объекта через {} аналогично его созданию через new Object и потому новый объект через прототип получаетс свойства и методы от Object
var myDog = {
legs: 4
};
"legs" in myDog; // true
myDog.hasOwnProperty("legs"); // true
"hasOwnProperty" in myDog; // true
myDog.hasOwnProperty("hasOwnProperty"); // false
"Встроенные классы" связаны между собой прототипами:
console.dir( [1,2,3,4] );
console.dir( {legs: 4} );
console.dir( function() {} );
Таким образом, все объекты наследуют от Object.
При использовании примитивов вместе с методами будут создаваться временные объекты соответствующего типа (Number, String, Boolean), искаться и выполняться нужные методы в цепочке прототипов, а затем временный объект будет уничтожаться. Явно использовать конструкторы Number, String и Boolean не имеет смысла.
Для создания класса через прототип создается функция-конструктор, а все методы помещаются в прототип функции-конструктора:
function Dog() {
this.legs = 4;
this.head = 1;
};
Dog.prototype.bark = function() {
alert("Woof1");
};
var myDog = new Dog;
Таким образом все объекты используют методы из прототипа, а не каждый объект — свои методы, в результате чего на каждый из объектов выделяется меньше памяти. Но при создании классов в прототипном стиле нет возможности использовать замыкания и, соответственно, приватные свойства - их приходится эмулировать через защищенные, которые, в свою очередь, тоже эмуляция. Также, если в прототипном стиле происходит обращение к несуществующему свойству, то оно будет искаться по всей цепочке прототипов, и если последняя длинная, то такой поиск будет занимать больше времени.
Полезные частоиспользуемые функции было бы удобно вынести в прототип встроенного класса. Но это плохая практика, т.к. это нарушает принцип инкапсуляции и это целесообразно делать только в том случае, если необходимо добавить метод, который есть в спецификации, но нет в конкретной версии браузера.
Например, мы хотим добавить поддержку метода forEach для старых браузеров. Тогда метод просто добавляется в prototype встроенного объекта Array:
var arr = [1,2,3,4];
if(Array.prototype.forEach) {
Array.prototype.forEach = function(callback) {
for(var i = 0; i < this.length; i++) {
callback(this[i]);
}
};
}
arr.forEach(console.log);
Мы рассмотрели наследование объект->объект и объект->класс. Однако с помощью прототипов можно организовать наследование классов.
function Soldier(name) {
this.name = name;
this.ammo = 100;
}
Soldier.prototype.shoot = function() {
this.ammo--;
};
function Sniper(name) {
this.name = name;
this.weapon = "sniper riffle";
}
Sniper.prototype.aim = function() {
this.takeBreath();
this.toggleEyes("left", false);
this.concentrate();
this.whatEver();
// ...
};
Чтобы поиск недостающих свойств в объекте, созданном с помощью Sniper, происходил по свойствам Soldier, нам нужно для всех объектов Sniper указать в их прототипе Soldier.prototype
С использованием __proto__ код мог бы выглядеть так:
Sniper.prototype.__proto__ = Soldier.prototype;
Но т.к. у __proto__ ограничена поддержка, то кроссбраузерное наследование классов будет таким:
Sniper.prototype = Object.create(Soldier.prototype);
Если Object.create не поддерживается, то нужно использовать функцию extend из подраздела Свой Object.create
Если функция-конструктор дочернего класса использует в качестве аргументов те же значения, что и функция-конструктор родительского класса, то вместо повторного объявления свойств можно вызвать конструктор родительского класса подобно тому, как это делается при функциональном наследовании.
function Soldier(name) {
this.name = name;
this.ammo = 100;
}
Soldier.prototype.shoot = function() {
this.ammo--;
};
function Sniper(name) {
// проброс аргументов функции-конструктору родительского класса
Soldier.apply(this, arguments);
this.weapon = "sniper riffle";
}
// "копируем" методы и свойства в прототип Sniper
Sniper.prototype = Object.create(Soldier.prototype);
// восстанавливаем ссылку на constructor
Sniper.prototype.constructor = Sniper;
Sniper.prototype.aim = function() {
this.takeBreath();
this.toggleEyes("left", false);
this.concentrate();
this.whatEver();
// ...
};