Прототип

[[Прототип]]

  • Функция: все функции по умолчанию получают общедоступное, неперечислимое свойство, называемое prototype, которое указывает на произвольный объект.
function Foo() {
	// ...
}

Foo.prototype; // { }
  • Объект: в JavaScript есть внутреннее свойство, обозначенное в спецификации как [[Prototype]], которое является просто ссылкой на другой объект. Почти всем объектам присваивается значение этого свойства, отличное от null, во время их создания. Когда дело доходит до создания нового объекта, есть два следующих пути:
  1. var a = new Foo(): «другой объект», с которым связан новый объект «a», оказывается объектом, на который ссылается произвольно названное свойство .prototype (Foo.prototype) объекта функция вызывается с помощью new.
function Foo() {
	// ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true

2. var b = Object.create(a): создает «новый» объект из воздуха и связывает внутренний [[Prototype]] этого нового объекта с указанным вами объектом.

if (!Object.create) {
	Object.create = function(o) {
		function F(){}
		F.prototype = o;
		return new F();
	};
}

Настройка и свойства затенения

Теперь мы рассмотрим три сценария назначения myObject.foo = "bar", когда foo еще не находится непосредственно на myObject, а находится на более высоком уровне цепочки [[Prototype]] myObject:

  1. Если обычное свойство доступа к данным с именем foo находится где-то выше в цепочке [[Prototype]] и не помечено как доступное только для чтения (writable:false), то новое свойство с именем foo добавляется непосредственно к myObject, что приводит к затенению свойства.
  2. Если foo находится выше в цепочке [[Prototype]], но помечено как доступное только для чтения (writable:false), то как установка этого существующего свойства, так и создание затененного свойства для myObject запрещены. Если код выполняется в strict mode, будет выдана ошибка. В противном случае установка значения свойства будет молча игнорироваться. В любом случае затенения не происходит.
  3. Если foo находится выше в цепочке [[Prototype]] и это сеттер, то всегда будет вызываться сеттер. Никакой foo не будет добавлен к myObject, а установщик foo не будет переопределен.

Если вы хотите затенить foo в случаях № 2 и № 3, вы не можете использовать назначение =, а вместо этого должны использовать Object.defineProperty(..), чтобы добавить foo к myObject.

Затенение может происходить даже неявно тонкими способами, поэтому необходимо соблюдать осторожность, пытаясь его избежать. Учитывать:

var anotherObject = {
	a: 2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // oops, implicit shadowing!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true
/* Be very careful when dealing with delegated properties that you modify. If you wanted to increment anotherObject.a, the only proper way is anotherObject.a++. */

Самоанализ

  • instanceof: оператор instanceof принимает простой объект в качестве левого операнда и функцию в качестве правого операнда. Вопрос, на который отвечает instanceof, звучит так: появляется ли во всей [[Prototype]] цепочке a объект, на который произвольно указывает Foo.prototype?
  • b.isPrototypeOf(a): появляется ли когда-нибудь во всей цепочке [[Prototype]] a b?
// helper utility to see if `o1` is related to (delegates to) `o2`
function isRelatedTo(o1, o2) {
	function F(){}
	F.prototype = o2;
	return o1 instanceof F;
}

Делегирование поведения

Теория классов

Классы — это шаблон проектирования. Классы означают копии. Когда создаются экземпляры традиционных классов, происходит копирование поведения из класса в экземпляр. Когда классы наследуются, также происходит копирование поведения от родителя к дочернему.

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

Допустим, у нас есть несколько похожих задач («XYZ», «ABC» и т. д.), которые нам нужно смоделировать в нашей программе.

В случае с классами вы разрабатываете сценарий следующим образом: определяете общий родительский (базовый) класс, например Task, определяете общее поведение для всех «похожих» задач. Затем вы определите дочерние классы XYZ и ABC, оба из которых наследуются от Task, и каждый из которых добавляет специальное поведениедля выполнения соответствующих задач.

Важно отметить, что шаблон проектирования класса подтолкнет вас к тому, чтобы получить максимальную отдачу от наследования, вы захотите использовать метод переопределение (и полиморфизм), где вы переопределяете определение некоторый общий метод Task в вашей XYZзадаче, возможно, даже используя super для вызова базовой версии этого метода, добавляя к нему больше поведения. Скорее всего, вы найдете довольно много мест, где можно "абстрагировать" общее поведение от родительского класса и специфицировать (переопределить) его в дочерних классах.

Вот некоторый свободный псевдокод для этого сценария:

class Task {
	id;
	// constructor `Task()`
	Task(ID) { id = ID; }
	outputTask() { output( id ); }
}
class XYZ inherits Task {
	label;
	// constructor `XYZ()`
	XYZ(ID,Label) { super( ID ); label = Label; }
	outputTask() { super(); output( label ); }
}
class ABC inherits Task {
	// ...
}

Теория делегирования

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

Сначала вы определите объект (не класс и не function, как уверяет вас большинство разработчиков JS) с именем Task, и он будет иметь конкретное поведение, включающее служебные методы. , которые можно использовать для различных задач (читай: делегировать!). Затем для каждой задачи («XYZ», «ABC») вы определяете объект для хранения этих данных/поведения, специфичных для задачи. Вы связываете свой объект (объекты) для конкретной задачи с служебным объектом Task, позволяя им делегировать ему полномочия, когда это необходимо.

По сути, вы думаете овыполнении задачи "XYZ" как о необходимости поведения двух родственных/равноправных объектов (XYZ и Task) для ее выполнения. Но вместо того, чтобы составлять их вместе с помощью копий классов, мы можем хранить их в отдельных объектах, и мы можем разрешить XYZ объекту делегировать Task при необходимости.

Вот простой код, который подскажет, как это сделать:

var Task = {
	setID: function(ID) { this.id = ID; },
	outputID: function() { console.log( this.id ); }
};
// make `XYZ` delegate to `Task`
var XYZ = Object.create( Task );
XYZ.prepareTask = function(ID,Label) {
	this.setID( ID );
	this.label = Label;
};
XYZ.outputTaskDetails = function() {
	this.outputID();
	console.log( this.label );
};
// ABC = Object.create( Task );
// ABC ... = ...

Некоторые другие отличия кода стиля OLOO, на которые следует обратить внимание:

  1. Оба члена данных id и label из предыдущего примера класса являются свойствами данных непосредственно в XYZ (и не в Task). Как правило, при делегировании [[Prototype]] необходимо, чтобы state было у делегаторов (XYZ, ABC), а не у делегата (Task).
  2. В шаблоне проектирования класса мы намеренно назвали outputTask одинаковым как для родительского (Task), так и для дочернего (XYZ), чтобы мы могли воспользоваться преимуществами переопределения (полиморфизма). При делегировании поведения мы поступаем наоборот: избегаем, если это возможно, одинаковых имен на разных уровнях цепочки [[Prototype]] из-за затенения.

Этот шаблон проектирования требует меньшего количества общих имен методов, которые могут быть переопределены, а вместо этого больше описательных имен методов, конкретных для типа поведения. каждый объект делает. На самом деле это может упростить понимание/сопровождение кода, поскольку имена методов (не только в месте определения, но и в другом коде) более очевидны (самодокументирование).

Поведение Делегирование означает: позволить некоторому объекту (XYZ) предоставить делегирование (до Task) ссылок на свойства или методы, если они не найдены в объекте (XYZ).

Сравнение ОО с ОЛОО

Первый фрагмент использует классический («прототипный») стиль объектно-ориентированного программирования:

function Foo(who) {
	this.me = who;
}
Foo.prototype.identify = function() {
	return "I am " + this.me;
};
function Bar(who) {
	Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
	alert( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );
b1.speak();
b2.speak();

Возможность функции JS обращаться к call(..), apply(..) и bind(..) связана с тем, что функции сами по себе являются объектами, а объекты-функции также имеют связь [[Prototype]] с объектом _72, которая определяет те методы по умолчанию, которым может делегировать любой объект-функция.

давайте реализуем точно такую ​​же функциональность, используя код стиля OLOO:

var Foo = {
	init: function(who) {
		this.me = who;
	},
	identify: function() {
		return "I am " + this.me;
	}
};
var Bar = Object.create( Foo );
Bar.speak = function() {
	alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();

Примеры

  • Виджет: создание виджетов пользовательского интерфейса (кнопки, раскрывающиеся списки и т. д.).

Давайте посмотрим, как бы мы реализовали дизайн «класса» в чистом JS в классическом стиле без какой-либо вспомогательной библиотеки или синтаксиса «класса»:

// Parent class
function Widget(width,height) {
	this.width = width || 50;
	this.height = height || 50;
	this.$elem = null;
}
Widget.prototype.render = function($where){
	if (this.$elem) {
		this.$elem.css( {
			width: this.width + "px",
			height: this.height + "px"
		} ).appendTo( $where );
	}
};
// Child class
function Button(width,height,label) {
	// "super" constructor call
	Widget.call( this, width, height );
	this.label = label || "Default";
	this.$elem = $( "<button>" ).text( this.label );
}
// make `Button` "inherit" from `Widget`
Button.prototype = Object.create( Widget.prototype );
// override base "inherited" `render(..)`
Button.prototype.render = function($where) {
	// "super" call
	Widget.prototype.render.call( this, $where );
	this.$elem.click( this.onClick.bind( this ) );
};
Button.prototype.onClick = function(evt) {
	console.log( "Button '" + this.label + "' clicked!" );
};
$( document ).ready( function(){
	var $body = $( document.body );
	var btn1 = new Button( 125, 30, "Hello" );
	var btn2 = new Button( 150, 40, "World" );
	btn1.render( $body );
	btn2.render( $body );
} );

Вот наш более простой пример Widget / Button с использованием делегирования в стиле OLOO:

var Widget = {
	init: function(width,height){
		this.width = width || 50;
		this.height = height || 50;
		this.$elem = null;
	},
	insert: function($where){
		if (this.$elem) {
			this.$elem.css( {
				width: this.width + "px",
				height: this.height + "px"
			} ).appendTo( $where );
		}
	}
};
var Button = Object.create( Widget );
Button.setup = function(width,height,label){
	// delegated call
	this.init( width, height );
	this.label = label || "Default";
	this.$elem = $( "<button>" ).text( this.label );
};
Button.build = function($where) {
	// delegated call
	this.insert( $where );
	this.$elem.click( this.onClick.bind( this ) );
};
Button.onClick = function(evt) {
	console.log( "Button '" + this.label + "' clicked!" );
};
$( document ).ready( function(){
	var $body = $( document.body );
	var btn1 = Object.create( Button );
	btn1.setup( 125, 30, "Hello" );
	var btn2 = Object.create( Button );
	btn2.setup( 150, 40, "World" );
	btn1.build( $body );
	btn2.build( $body );
} );

При таком подходе в стиле OLOO мы не думаем о Widget как о родителе, а Button как о дочернем элементе. Скорее, Widget — это просто объект и своего рода коллекция служебных программ, которой может захотеться делегировать виджет любого конкретного типа, а Button также является просто автономным объектом. (со ссылкой делегирования на Widget, конечно!).

OLOO лучше поддерживает принцип разделения задач, при котором создание и инициализация не обязательно объединяются в одну и ту же операцию.

  • Два контроллера: нам понадобится вспомогательная утилита для связи Ajax с сервером.

Следуя типичному шаблону проектирования классов, мы разобьем задачу на базовые функции в классе с именем Controller, а затем создадим два дочерних класса, LoginController и AuthController, которые наследуются от Controller и специализируются на некоторых из этих базовых поведений.

// Parent class
function Controller() {
	this.errors = [];
}
Controller.prototype.showDialog = function(title,msg) {
	// display title & message to user in dialog
};
Controller.prototype.success = function(msg) {
	this.showDialog( "Success", msg );
};
Controller.prototype.failure = function(err) {
	this.errors.push( err );
	this.showDialog( "Error", err );
};
// Child class
function LoginController() {
	Controller.call( this );
}
// Link child class to parent
LoginController.prototype = Object.create( Controller.prototype );
LoginController.prototype.getUser = function() {
	return document.getElementById( "login_username" ).value;
};
LoginController.prototype.getPassword = function() {
	return document.getElementById( "login_password" ).value;
};
LoginController.prototype.validateEntry = function(user,pw) {
	user = user || this.getUser();
	pw = pw || this.getPassword();
	if (!(user && pw)) {
		return this.failure( "Please enter a username & password!" );
	}
	else if (pw.length < 5) {
		return this.failure( "Password must be 5+ characters!" );
	}
	// got here? validated!
	return true;
};
// Override to extend base `failure()`
LoginController.prototype.failure = function(err) {
	// "super" call
	Controller.prototype.failure.call( this, "Login invalid: " + err );
};
// Child class
function AuthController(login) {
	Controller.call( this );
	// in addition to inheritance, we also need composition
	this.login = login;
}
// Link child class to parent
AuthController.prototype = Object.create( Controller.prototype );
AuthController.prototype.server = function(url,data) {
	return $.ajax( {
		url: url,
		data: data
	} );
};
AuthController.prototype.checkAuth = function() {
	var user = this.login.getUser();
	var pw = this.login.getPassword();
	if (this.login.validateEntry( user, pw )) {
		this.server( "/check-auth",{
			user: user,
			pw: pw
		} )
		.then( this.success.bind( this ) )
		.fail( this.failure.bind( this ) );
	}
};
// Override to extend base `success()`
AuthController.prototype.success = function() {
	// "super" call
	Controller.prototype.success.call( this, "Authenticated!" );
};
// Override to extend base `failure()`
AuthController.prototype.failure = function(err) {
	// "super" call
	Controller.prototype.failure.call( this, "Auth Failed: " + err );
};
var auth = new AuthController(
	// in addition to inheritance, we also need composition
	new LoginController()
);
auth.checkAuth();

Делегирование поведения предлагает объекты как одноранговые друг друга, которые делегируют друг друга, а не отношения родительского и дочернего классов.

var LoginController = {
	errors: [],
	getUser: function() {
		return document.getElementById( "login_username" ).value;
	},
	getPassword: function() {
		return document.getElementById( "login_password" ).value;
	},
	validateEntry: function(user,pw) {
		user = user || this.getUser();
		pw = pw || this.getPassword();

		if (!(user && pw)) {
			return this.failure( "Please enter a username & password!" );
		}
		else if (pw.length < 5) {
			return this.failure( "Password must be 5+ characters!" );
		}

		// got here? validated!
		return true;
	},
	showDialog: function(title,msg) {
		// display success message to user in dialog
	},
	failure: function(err) {
		this.errors.push( err );
		this.showDialog( "Error", "Login invalid: " + err );
	}
};
// Link `AuthController` to delegate to `LoginController`
var AuthController = Object.create( LoginController );

AuthController.errors = [];
AuthController.checkAuth = function() {
	var user = this.getUser();
	var pw = this.getPassword();

	if (this.validateEntry( user, pw )) {
		this.server( "/check-auth",{
			user: user,
			pw: pw
		} )
		.then( this.accepted.bind( this ) )
		.fail( this.rejected.bind( this ) );
	}
};
AuthController.server = function(url,data) {
	return $.ajax( {
		url: url,
		data: data
	} );
};
AuthController.accepted = function() {
	this.showDialog( "Success", "Authenticated!" )
};
AuthController.rejected = function(err) {
	this.failure( "Auth Failed: " + err );
};

Поскольку AuthController — это просто объект (как и LoginController), нам не нужно создавать экземпляр (например, new AuthController()) для выполнения нашей задачи. Все, что нам нужно сделать, это: AuthController.checkAuth().

API

Делегирование правильнее использовать как внутреннюю деталь реализации, а не раскрывать непосредственно в дизайне интерфейса API.

var anotherObject = {
	cool: function() {
		console.log( "cool!" );
	}
};

var myObject = Object.create( anotherObject );

myObject.doCool = function() {
	this.cool(); // internal delegation!
};

myObject.doCool(); // "cool!"

Здесь мы вызываем метод myObject.doCool(), который действительно существует в myObject, что делает дизайн нашего API более явным (менее «магическим»).