Прототип
[[Прототип]]
- Функция: все функции по умолчанию получают общедоступное, неперечислимое свойство, называемое
prototype
, которое указывает на произвольный объект.
function Foo() { // ... } Foo.prototype; // { }
- Объект: в JavaScript есть внутреннее свойство, обозначенное в спецификации как
[[Prototype]]
, которое является просто ссылкой на другой объект. Почти всем объектам присваивается значение этого свойства, отличное отnull
, во время их создания. Когда дело доходит до создания нового объекта, есть два следующих пути:
- 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
:
- Если обычное свойство доступа к данным с именем
foo
находится где-то выше в цепочке[[Prototype]]
и не помечено как доступное только для чтения (writable:false
), то новое свойство с именемfoo
добавляется непосредственно кmyObject
, что приводит к затенению свойства. - Если
foo
находится выше в цепочке[[Prototype]]
, но помечено как доступное только для чтения (writable:false
), то как установка этого существующего свойства, так и создание затененного свойства дляmyObject
запрещены. Если код выполняется вstrict mode
, будет выдана ошибка. В противном случае установка значения свойства будет молча игнорироваться. В любом случае затенения не происходит. - Если
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 incrementanotherObject.a
, the only proper way isanotherObject.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, на которые следует обратить внимание:
- Оба члена данных
id
иlabel
из предыдущего примера класса являются свойствами данных непосредственно вXYZ
(и не вTask
). Как правило, при делегировании[[Prototype]]
необходимо, чтобы state было у делегаторов (XYZ
,ABC
), а не у делегата (Task
). - В шаблоне проектирования класса мы намеренно назвали
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 более явным (менее «магическим»).