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

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

Пример объекта, который мы хотим наследовать:


// Объект родитель
// Специфичные для объекта свойства и методы,
// которые будут скопированы в объект-наследник при наследовании
function Parent(name) {
	this.name = name || "Adam";
}

// Переиспользуемые свойства и методы, которые не будут пересоздаваться каждый раз при наследовании
Parent.prototype.say = function () {
	return this.name;
}

// Объект наследник
function Child(name) {}

В Javascript есть несколько способов реализовать наследование.
Рассмотрим сначало классические наследования (от слова класс).

1. Шаблон по умолчанию

Наследуються собственные свойства (копируются), а свойства из прототипа (ссылаются).

Достоинства:

  • наиболее частоприменяемый и простой способ наследования.

Недостатки:

  • наследуются также собственные свойства объекта (хотя иногда это желаемое поведение);
  • нельзя передать параметры в конструктор родителя (хотя можно вызвать Child.prototype = new Parent('New name'); без обертки в функцию).

function inherit(C, P) { 
	C.prototype = new P(); 
}

inherit(Child, Parent);

var child = new Child();

console.log(child); // Parent { name='Adam', say=function() }

// Проверяем
Parent.name = 'Eva';
Parent.prototype.say = 'Method overwritten';

console.log(child); // Parent { name='Adam', say='Method overwritten' }

2. Заимствование конструктора

Наследуются только собственные свойства (копируются).

Достоинства:

  • может быть реализовано множественное наследование (от нескольких родителей);
  • исключаеться случайное изменение свойств родителя.

Недостатки:

  • не наследуются свойства из прототипа.

function Child(name) {
	Parent.apply(this, arguments);
	// Parent2.apply(this, arguments);
}

var child = new Child();

console.log(child); // Child { name='Adam' }

// Проверяем
Parent.name = 'Eva';

console.log(child); // Child { name='Adam' }

3. Заимствование и установка прототипа

Наследуються собственные свойства (копируются) и свойства из прототипа (ссылаются).

Недостатки:

  • конструктор родителя вызывается дважды.

function Child(name) {
	Parent.apply(this, arguments);
}

Child.prototype = new Parent();

var child = new Child();

console.log(child); // Parent { name='Adam', say=function() }

// Проверяем
Parent.name = 'Eva';
Parent.prototype.say = 'Method overwritten';

console.log(child); // Parent { name='Adam', say='Method overwritten' }

4. Совместное использование прототипа

Наследуються только свойства из прототипа (передаются по ссылке).

Достоинства/Недостатки:

  • Изменение свойств наследника затрагивает свойства родителя.

function inherit(C, P) { 
	C.prototype = P.prototype; 
}

inherit(Child, Parent); 

var parent = new Parent(),
	child  = new Child();

console.log(parent); // Parent { name='Adam', say=function() }
console.log(child); // Parent { say=function() }

// Проверяем
Parent.prototype.say = 'Method overwritten';

console.log(child); // Parent { say='Method overwritten' }

Child.prototype.say = 'Again overwritten';

console.log(parent); // Parent { name='Adam', say='Again overwritten'}

5. Временный конструктор

Наследуются только свойства прототипа (ссылаются).

Достоинства:

  • наиболее предпочтительное поведение.

var inherit = (function () {
	// Запоминаем временный конструктор в замыкании, чтобы не создавать его каждый раз
	var F = function() {};
	return function(C, P) {
		F.prototype = P.prototype;
		C.prototype = new F();
		// Сохранение ссылки на родителя
		C.uber = P.prototype;
		// Выставляем правильное имя конструктора
		C.prototype.constructor = C;
	}
})();

inherit(Child, Parent);

var child = new Child();

console.log(child); // Child { constructor=Child(), say=function() }
console.log(child.constructor); // Child(name)
// Аналог parent:: в php
console.log(child.constructor.uber); // Parent { say=function() }

// Проверяем
Parent.prototype.say = 'Method overwritten';

console.log(child); // Child { constructor=Child(), say='Method overwritten' }

child.say = 'New property';
console.log(child.say); // New property
console.log(child.constructor.uber.say); // Method overwritten

Функция klass()

Наследуються собственные свойства (копируются) и свойства из прототипа (ссылаются).

Достоинства:

  • позволяет забыть о прототипах.

Достоинства:

  • лучше избегать этот метод т.к. он вводит новые правила и новый синтаксис.

var klass = function (Parent, props) {
	var Child, F, i;
	// 1. Создаем новый конструктор и вызываем функции __construct родителей и наследников
	Child = function () {
		if (Child.uber && Child.uber.hasOwnProperty('__construct')) {
			Child.uber.__construct.apply(this, arguments);
		}
		if (Child.prototype.hasOwnProperty('__construct')) {
			Child.prototype.__construct.apply(this, arguments);
		}
	};

	// 2. Наследование (метод временный конструтор)
	Parent = Parent || Object;
	F = function () {};
	F.prototype = Parent.prototype;
	Child.prototype = new F();
	Child.uber = Parent.prototype;
	Child.prototype.constructor = Child;

	// 3. Добавляем собственные свойства родителя в прототип наследника
	for (i in props) {
		if (props.hasOwnProperty(i)) {
			Child.prototype[i] = props[i];
		}
	}
	
	return Child;
};

var Child = klass(Parent, {
	__construct: function(name) {
		// Конструктор (родителя и текущий) вызывается автоматически при создании экземпляра класса
		this.name = name;
	},
	say: 'Method overwritten'
});

var child = new Child('Eva');

console.log(child); // Object { name='Eva', say='Method overwritten',
                    //          constructor=function(), __construct=function() }
console.log(child.constructor); // Child()
// Аналог parent:: в php
console.log(child.constructor.uber); // Parent { say=function()}

console.log(child.say); // Method overwritten
console.log(child.constructor.uber.say); // function()

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

Наследования через прототип

Наследуються собственные свойства (копируются) и свойства из прототипа (ссылаются).

function object(parent) {
	function F() {}
	F.prototype = parent;
	return new F();
}

// Наследуються собственные свойства (копируются) и свойства из прототипа (ссылаются)
var parent = new Parent();
var child = object(parent);
console.log(child); // Parent { name='Adam', say=function() }

// Наследуються свойства из прототипа (ссылаются)
var child = object(Parent.prototype);
console.log(child); // Parent { say=function() }

Наследование в стандарте ECMAScript 5

В стандарте ECMAScript 5, вам уже не надо создавать собственную функцию для наследования.
JavaScript обзавелся встроенным методом Object.create.

Наследуються собственные свойства (копируются) и свойства из прототипа (ссылаются).

var parent = new Parent();
var child = Object.create(parent,
	// Дополнительный параметр, собственные свойства которые надо добавить в новый созданный объект
	{propertyName:  {value: 'propertyValue'}} // Описание в соответствии со стандартом ECMAScript 5
);
console.log(child); // Parent { name='Adam', say=function() }
console.log(child.hasOwnProperty('propertyName')); // true
console.log(child.propertyName); // propertyValue

Наследование копированием свойств (поверхностное копирования)

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

Наследуються собственные свойства (копируются).

function extend(parent, child) {
	var i;
	child = child || {};
	for (i in parent) {
		if (parent.hasOwnProperty(i)) {
			child[i] = parent[i];
		}
	}
	return child;
}

// Заменяем исходный пример, новым классом
var parent = {firstLevel: {
		testArray: [1, 2, 3],
		testObject: {value: true}
	}
};

var child = extend(parent);

console.log(child); // Object { firstLevel: Object { testArray: [1, 2, 3],
	                //          testObject: Object {value = true} }
// Изменение вложенных свойст объекта влияет на родительский объект
child.firstLevel.testArray.push(4);
child.firstLevel.testObject.value = false;
console.log(parent); // Object { firstLevel: Object { testArray: [1, 2, 3, 4],
	                 //          testObject: Object {value = false} }

Наследование копированием свойств (полное копирования)

Чтобы выполнить полное копирование, необходимо проверить, является ли копируемое свойство объектом или массивом, и если это так, следует рекурсивно обойти все вложенные свойства и также скопировать

Наследуються собственные свойства (копируются).

function extendDeep(parent, child) {
	var i,
	toStr = Object.prototype.toString,
	astr = '[object Array]';
	child = child || {};
	for (i in parent) {
		if (parent.hasOwnProperty(i)) {
			if (typeof parent[i] === 'object') {
				child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
				extendDeep(parent[i], child[i]);
			} else {
				child[i] = parent[i];
			}
		}
	}
	return child;
}

// Заменяем исходный пример, новым классом
var parent = {firstLevel: {
		testArray: [1, 2, 3],
		testObject: {value: true}
	}
};

var child = extendDeep(parent);

console.log(child); // Object { firstLevel: Object { testArray: [1, 2, 3],
	                //          testObject: Object {value = true} }
// Изменение вложенных свойст объекта не влияет на родительский объект
child.firstLevel.testArray.push(4);
child.firstLevel.testObject.value = false;
console.log(parent); // Object { firstLevel: Object { testArray: [1, 2, 3],
	                 //          testObject: Object {value = true} }

Смешивание

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

Наследуються собственные свойства (копируются).

function mix() {
	var arg, prop, child = {};
	for (arg = 0; arg < arguments.length; arg += 1) {
		for (prop in arguments[arg]) {
			if (arguments[arg].hasOwnProperty(prop)) {
				child[prop] = arguments[arg][prop];
			}
		}
	}
	return child;
}

// Заменяем исходный пример, новыми классами
var parent1 = {testProperty1: 'testValue1'},
	parent2 = {testProperty2: 'testValue2'};

var child = mix(parent1, parent2 /*, parent3 ... */);

console.log(child); // Object { testProperty1='testValue1', testProperty2='testValue2'}

Заимствование методов

Иногда копировать все свойства из объекта не имеет смысла, поэтому применяют заимствование только определенных методов.
Для этого используются встроенные методы объектов call() и apply().


var child = new Child();
child.name = 'Eva';
child.say = Parent.prototype.say.apply(child, [/* param1, param2 */]);

// Заимствуем в объект child метод say из объекта parent
console.log(child.say); // Eva

// Меняем контекст вызова метода
var say = child.say;
console.log(say()); // TypeError: say is not a function

Свойство this внутри заимствованной функции say() ссылается на вызвавший ее объект (child), поэтому при смене контекста функции значение this также поменяется.
Для того, чтобы зафиксировать this необходимо использовать замыкание.


function bind(object, method) {
	return function () {
		return method.apply(object, [].slice.call(arguments));
	};
}

var child = new Child();
child.name = 'Eva';

// Заимствуем в объект child метод say из объекта parent
child.say = bind(child, Parent.prototype.say);
console.log(child.say); // function()

// Меняем контекст вызова метода
var say = child.say;
console.log(say()); // Eva

Заимствование методов в стандарте ECMAScript 5

В стандарте ECMAScript 5 появился встроенный метод bind() для заимствования свойств объектов.


var child = new Child();
child.name = 'Eva';
child.say = Parent.prototype.say.bind(child/*, param1, param 2 */);
console.log(child.say()); // Eva

// Меняем контекст вызова метода
var say = child.say;
console.log(say()); // Eva

Для окружений не поддерживаеющих ECMAScript 5 можно дабавить обратную совместимость.


if (typeof Function.prototype.bind === 'undefined') {
	Function.prototype.bind = function (thisArg) {
		var fn = this,
		slice = Array.prototype.slice,
		args = slice.call(arguments, 1);
		return function () {
			return fn.apply(thisArg, args.concat(slice.call(arguments)));
		};
	};
}

var child = new Child();
child.name = 'Eva';
child.say = Parent.prototype.say.bind(child/*, param1, param 2 */);
console.log(child.say()); // Eva

Автор: http://www.nika.org.ua
Дата: 12.04.14

Копирование материалов без указания "Автор: http://www.nika.org.ua" запрещено!