邹明潮 Notes: YDKJS Prototype

邹明潮
KevinZou
Published in
11 min readMay 17, 2017

Prototype

[[Prototype]]

  • Function: all functions by default get a public, non-enumerable property on them called prototype, which points at an otherwise arbitrary object.
function Foo() {
// ...
}

Foo.prototype; // { }
  • Object: in JavaScript have an internal property, denoted in the specification as [[Prototype]], which is simply a reference to another object. Almost all objects are given a non-null value for this property, at the time of their creation. When it comes to creation of new object, there is two following ways:
  1. var a = new Foo(): the “another object” that the new object ‘a’ is linked to happens to be the object referenced by the arbitrarily named .prototype property (Foo.prototype) of the function called with new.
function Foo() {
// ...
}
var a = new Foo();Object.getPrototypeOf( a ) === Foo.prototype; // true

2. var b = Object.create(a): creates a “new” object out of thin air, and links that new object’s internal [[Prototype]] to the object you specify.

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

Setting & Shadowing Properties

We will now examine three scenarios for the myObject.foo = "bar" assignment when foo is not already on myObject directly, but is at a higher level of myObject's [[Prototype]] chain:

  1. If a normal data accessor property named foo is found anywhere higher on the [[Prototype]]chain, and it's not marked as read-only (writable:false) then a new property called foo is added directly to myObject, resulting in a shadowed property.
  2. If a foo is found higher on the [[Prototype]] chain, but it's marked as read-only (writable:false), then both the setting of that existing property as well as the creation of the shadowed property on myObject are disallowed. If the code is running in strict mode, an error will be thrown. Otherwise, the setting of the property value will silently be ignored. Either way, no shadowing occurs.
  3. If a foo is found higher on the [[Prototype]] chain and it's a setter , then the setter will always be called. No foo will be added to myObject, nor will the foo setter be redefined.

If you want to shadow foo in cases #2 and #3, you cannot use = assignment, but must instead use Object.defineProperty(..) to add foo to myObject.

Shadowing can even occur implicitly in subtle ways, so care must be taken if trying to avoid it. Consider:

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++. */

Introspection

  • instanceof: The instanceof operator takes a plain object as its left-hand operand and a function as its right-hand operand. The question instanceof answers is: in the entire [[Prototype]] chain of a, does the object arbitrarily pointed to by Foo.prototype ever appear?
  • b.isPrototypeOf(a): in the entire [[Prototype]] chain of a, does b ever appear?
// 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;
}

Behavior Delegation

Class Theory

Classes are a design pattern. Classes mean copies. When traditional classes are instantiated, a copy of behavior from class to instance occurs. When classes are inherited, a copy of behavior from parent to child also occurs.

Polymorphism (having different functions at multiple levels of an inheritance chain with the same name) may seem like it implies a referential relative link from child back to parent, but it’s still just a result of copy behavior.

Let’s say we have several similar tasks (“XYZ”, “ABC”, etc) that we need to model in our software.

With classes, the way you design the scenario is: define a general parent (base) class like Task, defining shared behavior for all the "alike" tasks. Then, you define child classes XYZ and ABC, both of which inherit from Task, and each of which adds specialized behavior to handle their respective tasks.

Importantly, the class design pattern will encourage you that to get the most out of inheritance, you will want to employ method overriding (and polymorphism), where you override the definition of some general Task method in your XYZtask, perhaps even making use of super to call to the base version of that method while adding more behavior to it. You'll likely find quite a few places where you can "abstract" out general behavior to the parent class and specialize (override) it in your child classes.

Here’s some loose pseudo-code for that scenario:

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 {
// ...
}

Delegation Theory

But now let’s try to think about the same problem domain, but using behavior delegation instead of classes.

You will first define an object (not a class, nor a function as most JS'rs would lead you to believe) called Task, and it will have concrete behavior on it that includes utility methods that various tasks can use (read: delegate to!). Then, for each task ("XYZ", "ABC"), you define an object to hold that task-specific data/behavior. You link your task-specific object(s) to the Task utility object, allowing them to delegate to it when they need to.

Basically, you think about performing task “XYZ” as needing behaviors from two sibling/peer objects (XYZ and Task) to accomplish it. But rather than needing to compose them together, via class copies, we can keep them in their separate objects, and we can allow XYZ object to delegate to Task when needed.

Here’s some simple code to suggest how you accomplish that:

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 ... = ...

Some other differences to note with OLOO style code:

  1. Both id and label data members from the previous class example are data properties directly on XYZ (neither is on Task). In general, with [[Prototype]] delegation involved, you want state to be on the delegators (XYZ, ABC), not on the delegate (Task).
  2. With the class design pattern, we intentionally named outputTask the same on both parent (Task) and child (XYZ), so that we could take advantage of overriding (polymorphism). In behavior delegation, we do the opposite: we avoid if at all possible naming things the same at different levels of the [[Prototype]] chain because of shadowing.

This design pattern calls for less of general method names which are prone to overriding and instead more of descriptive method names, specific to the type of behavior each object is doing. This can actually create easier to understand/maintain code, because the names of methods (not only at definition location but strewn throughout other code) are more obvious (self documenting).

Behavior Delegation means: let some object (XYZ) provide a delegation (to Task) for property or method references if not found on the object (XYZ).

Comparsion OO with OLOO

The first snippet uses the classical (“prototypal”) OO style:

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();
邹明潮

The ability of a JS function to access call(..), apply(..), and bind(..) is because functions themselves are objects, and function-objects also have a [[Prototype]] linkage, to the Function.prototypeobject, which defines those default methods that any function-object can delegate to.

邹明潮

let’s implement the exact same functionality using OLOO style code:

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();
邹明潮

Examples

  • Widget:creating UI widgets (buttons, drop-downs, etc).

Let’s examine how we’d implement the “class” design in classic-style pure JS without any “class” helper library or syntax:

// 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 );
} );

Here’s our simpler Widget / Button example, using OLOO style delegation:

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 );
} );

With this OLOO-style approach, we don’t think of Widget as a parent and Button as a child. Rather, Widget is just an object and is sort of a utility collection that any specific type of widget might want to delegate to, and Button is also just a stand-alone object (with a delegation link to Widget, of course!).

OLOO supports better the principle of separation of concerns, where creation and initialization are not necessarily conflated into the same operation.

  • Two Controller: We’ll need a utility helper for making the Ajax communication to the server.

Following the typical class design pattern, we’ll break up the task into base functionality in a class called Controller, and then we'll derive two child classes, LoginController and AuthController, which both inherit from Controller and specialize some of those base behaviors.

// 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();

Behavior delegation suggests objects as peers of each other, which delegate amongst themselves, rather than parent and child class relationships

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 );
};

Since AuthController is just an object (so is LoginController), we don't need to instantiate (like new AuthController()) to perform our task. All we need to do is: AuthController.checkAuth().

API

Delegation is more properly used as an internal implementation detail rather than exposed directly in the API interface design.

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

var myObject = Object.create( anotherObject );

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

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

Here, we call myObject.doCool(), which is a method that actually exists on myObject, making our API design more explicit (less "magical").

--

--