Course-JavaScript/js-oop.js
Alex A. Naanou e63028904c some cleanup...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2014-09-25 22:36:33 +04:00

507 lines
12 KiB
JavaScript
Executable File

/**********************************************************************
*
* The basics of JavaScript OOP
*
*
**********************************************************************/
//
// The basic prototype inheritance
// -------------------------------
//
// First we'll create a basic object a
var a = {
x: 1,
y: 2,
}
// Then we will create a new object using a as a "base"
var b = Object.create(a)
b.z = 3
// The object b now has both access to it's own attributes ('z') and
// attributes of a ('x' and 'y')
b.x // -> 1
b.z // -> 3
// What we see is that if the attribute is not found in the current
// object it resolves to the object's "prototype" and so on, these
// chians can of any length.
//
// Note that this works for reading, when writing or deleting we are
// affecting ONLY the local object and attributes explicitly defined in
// it, or its' "own" attributes.
b.x = 321
b.x // -> 321
a.x // -> 1
// Notice that a.x is no longer visible from b, this is called "shadowing"
// and a.x is shadowed by b.x, now let us delete x from b to reveal the
// shadowed a.x
delete b.x
b.x // -> 1
// Trying to delete .x from b again will have no effect, this is because
// .x no longer exists in b
delete b.x
b.x // -> 1
// Now back to the mechanism that makes all of this work...
//
// A couple of easy ways to see the local and non-local sets of
// attributes:
// show local or "own" only attribute names (keys)...
Object.keys(b) // -> z
// show all accessible keys...
for(var k in b){ console.log(k) }
// -> x, y, z
// Another way to test if the attribute is own/local
b.isOwnProperty('z') // -> true
b.isOwnProperty('x') // -> false
// What happens under the hood is very simple:
b.__proto__ === a // -> true
// NOTE: we did not see .__proto__ in the list of accessible attributes
// because it is a special attributes, it is implemented internally
// and is not enumerable.
//
// Thus, we could define our own create function like this:
function clone(from){
var o = {}
o.__proto__ = from
return o
}
var c = clone(b)
// Out of curiosity let's see if .__proto__ is defined on a basic object
var x = {}
x.__proto__ // -> {}
// Turns out it is, and it points to Object's prototype
x.__proto__ === Object.prototye
// -> true
// We will discuss what this means and how we can use this in the next
// sections...
// The Constructor Mechanism
// -------------------------
//
// JavaScript provides a second, complementary mechanism to inherit
// attributes, it resembles the class/object relationship in languages
// like C++ but this resemblance is on the surface only, as it still
// uses the same prototype mechanism as the above.
//
// We will start by creating a "constructor":
function A(){
this.x = 1
this.y = 2
}
// Technically a constructor is just a function, what makes it a
// "constructor" is only how we use it...
var a = new A()
// what 'new' does here is:
// 1) creates an empty object
// 2) sets a bunch of attributes on it, we'll skim this part for now
// 3) passes the new object to the constructor via 'this'
// 4) after the constructor finishes, this object is returned
//
// We could write an equivalent (simplified) function:
function construct(func){
var obj = {}
return func.apply(obj)
}
var b = construct(A)
// But what does make this interesting? At this point this all looks like
// all we did is move attribute definition from a literal object notation
// into a constructor function, effectively adding complexity. What are we
// getting back from this?
//
// Let's look at a number of attributes that new sets:
a.__proto__ // -> {}
a.constructor // -> [Function A]
// These are what makes this fun, lets write a more complete new
// implementation:
function construct(func, args){
var obj = {}
obj.constructor = func
obj.__proto__ = func.prototype
var res = func.apply(obj, args)
if(res instanceof Object){
return res
}
return obj
}
var b = construct(A)
// Notice that we return the resulting object in a more complicated
// way, this will come in handy later.
//
// Also notice that 'prototype' from the end of the previous section.
//
// First let us cover the default. Each time a function is created in
// JavaScript it will get a new empty object assigned to it's .prototype
// attribute.
// On the function level, in general, this is not used, but this is very
// useful when the function is used as a constructor.
//
// As we can see from the code above, the resulting object's .__proto__
// points to the constructor's .prototype, from the previous section
// this means that attributes accessed via that object are resolved to
// the prototype.
// In the default case this is true.
//
// So if we add stuff to the constructor's .prototype they should get
// resolved from the object
A.prototype.x = 123
a.constructor.prototype.y = 321
a.__proto__.z = 333
// for illustration, some object own attributes
a.x = 'a!'
b.x = 'b!'
a.x // -> 'a!'
a.y // -> 321
a.z // -> 333
// These values are accessible from all objects constructed by A since
// all of them point to A with both the .constructor and .__proto__
// attributes
b.x // -> 'b!'
b.y // -> 321
b.z // -> 333
// "Double" inheritance
// --------------------
//
// There are actually three sources where JavaScript looks for attributes:
// 1) the actual object
// 2) .__proto__
// as coverd in the first section
// 3) .constructor.prototype
// as explained in the previous section
//
// Here is a basic inheritance structure (tree):
//
// O A
// \ /
// a
//
var O = {
o: 0,
}
function A(){}
A.prototype.a = 1
var a = new A()
a.__proto__ = o
// Now we can access both attributes inherited from 'O' and 'A'...
a.o // -> 0
a.a // -> 1
// The check is done specifically in this order, thus attributes can
// "shadow" other attributes defined by the other mechanism.
//
// To show this let us define an attribute with the same name on both
// 'O' and 'A':
O.x = 'came from O'
A.prototype.x = 'came from A'
a.x // -> 'came from O'
// In both inheritance mechanisms, each step is checked via the same
// rules recursively, this enables inheritance chains and less
// conveniently inheritance trees (superposition of chains).
//
// We will create a chain:
//
// c -> b -> a
//
var a = {x: 1}
var b = Object.create(a)
b.y = 2
var c = Object.create(b)
c.x // -> 1
c.y // -> 2
// Creating an inheritance chain via the constructor mechanism is a bit
// more involved, and there are multiple ways to do this...
//
// Here we will create a similar chian:
//
// C -> B -> A
//
function A(){}
A.prototype.x = 1
function B(){}
// NOTE: if this is done after an instance is created, that instances'
// .__proto__ will keep referencing the old prototype object.
// see the next constructor for a way around this...
B.prototype = Object.create(A.prototype)
B.prototype.y = 2
function C(){}
// NOTE: this is safer than Object.create as it does not overwrite
// the original object and thus will affect all existing
// instances of C, if any were created before this point...
C.prototype.__proto__ = B.prototype
var c = new C()
c.x // -> 1
c.y // -> 2
// Checking inheritance (instanceof)
// ---------------------------------
//
// An object is considered an instance of its' constructor and all other
// constructors in the inheritance chain.
c instanceof C // -> true
c instanceof B // -> true
c instanceof A // -> true
c instanceof Object
// -> true
// This also works for manually created objects
var cc = construct(C)
cc instanceof C
// But this will not work outside the constructor model, i.e. if the right
// parameter is not a function.
var x = {}
var y = Object.create(x)
try{
// this will fail as x is not a function...
y instanceof x
} catch(e){
console.log('error')
}
// Again to make this simpler to understand we will implement our own
// equivalent to instanceof:
function isInstanceOf(obj, proto){
return proto instanceof Function
&& (obj.__proto__ === proto.prototype ? true
// NOTE: the last in this chain is Object.prototype.__proto__
// and it is null
: obj.__proto__ == null ? false
// go down the chian...
: isInstanceOf(obj.__proto__, proto))
}
isInstanceOf(c, C) // -> true
isInstanceOf(c, B) // -> true
isInstanceOf(c, A) // -> true
isInstanceOf(c, Object)
// -> true
isInstanceOf(c, function X(){})
// -> false
// Checking type (typeof)
// ----------------------
//
// What typeof returns in JavaScript is not too useful and sometimes
// even odd...
typeof c // -> 'object'
// This might differ from implementation to implementation but
// essentially the main thing typeof is useful for is distinguishing
// between objects and non-objects (numbers, strings, ...etc.)
// non-objects
typeof 1 // -> 'number'
typeof Infinity // -> 'number'
typeof 'a' // -> 'string'
typeof undefined // -> 'undefined'
// objects
typeof {} // -> 'object'
typeof [] // -> 'object'
// the odd stuff...
typeof NaN // -> 'number'
typeof null // -> 'object'
typeof function(){} // -> 'function'
// Methods and the value of 'this'
// -------------------------------
//
// A method is simply an attribute that references a function.
function f(){
console.log(this)
this.a = 1
}
var o = { f: f }
// Thus we call the attribute .f of object o a "method" of object o.
//
//
// 'this' is a reserved word and is available in the context of a function
// execution, not just in methods, but what value it references depends
// on how that function is called...
// This is mostly useful and used in methods.
//
// A simple way to think about this is that 'this' always points to the
// "context" of the function call.
//
// This context can be:
// - implicit
// - root context
f()
// 'window', 'global' or 'module' is implied,
// in strict mode this is null.
// the same as:
// window.f()
// - 'new' context
new f() // here a context will be created and passed to
// 'f's 'this', for more details on what 'new'
// does see: "The Constructor Mechanism" section.
// - explicit:
// - the object on the left side of "." or the [ ] operators:
o.f() // o is the context
o['f']() // the same as the above
// - the object explicitly passed to .call(..) or .apply(..) methods
// as first argument:
f.call(o) // o is the context
f.apply(o) // o is the context
// Properties
// ----------
//
// A property is a special attribute that has a getter, setter methods
// and/or other optional configuration.
//
// A good property example is .length of Array objects.
var o = {
get x(){
return this.data || 123
},
set x(value){
this.data = value
},
}
o.x // -> 123
o.x = 4
o.x // -> 4
o.x = undefined
o.x // -> 123
// As for any other attribute, deleting a local property x will remove
// it from the containing object...
delete o.x
o.x // -> undefined
// The above code is a shorthand for:
Object.defineProperty(o, 'y', {
get: function() {
return this.data || 123
},
set: function(name) {
this.data = value
}
})
// XXX other property attributes...
// get
// set
// value
// if set get/set are not possible...
// writable (false)
// configurable (false)
// is it possible to change this configurations later...
// enumerable (false)
// Common use-cases
// ----------------
//
/**********************************************************************
* vim:set ts=4 sw=4 : */