JavaScript prototype chain
- There is no strict class definition in JavaScript, but we say a FUNCTION is a CLASS in common.
- JavaScript is based on prototype. When create an instance, it has a copy of the properties of the Object’s prototype.
If you are a JavaScript newbie, maybe you need read some instructions of JavaScript and how to use OOP in JavaScript.
OK, now let’s go to see how JavaScript prototype works.
Note: in the follow pages, we use uppercase string as the class name and lowercase as the instance.
A glance at constructor and __proto__
The first Clazz.
function Foo() {}
var foo = new Foo;
Now, we defined a class whose name is “Foo”, as we say, all the class has a prototype (used for initialize they instance), and there is a property “constructor” in the prototype which point to the class who own the property. Maybe you will say: “Oh, why I haven’t seen it?” Yes, because you may trace an object by for … in, but the “constructor” property has an attribute “DontEnum” (search it). There are some other property has “DontEnum” property, such as toString, toSource…
Let’s dig Foo deeply:
Each object has with it an associated [[Prototype]] object, The prototype chain is a linkage of [[Prototype]] objects. The prototype chain is standard, but hidden in most implementations. __proto__ represents the internal [[Prototype]] object. __proto__ isn't standard and isn't supported in IE or Opera [http://dhtmlkitchen.com/learn/js/enumeration/prototype-chain.jsp]. But most of the time in Function object we could use [obj].constructor.prototype as the __proto__ property. For simplicity we will use __proto__ in the follow pages.
Look at Figure 1, each object has a property “__proto__”, which point to its prototype. And in Function object, we have a property “constructor” which point to its Class.
You can do some tests; the follow figure shows how to use constructor and __proto__.
The prototype chain
There is no inheritance notion in JavaScript, but because of the prototype chain, we can simulate the inheritance behavior. As shown in the previous section, each class instance has a property __proto__. Now let’s see how to use it. Look at an example:
// run this code in JavaScript Shell // Class Foo function Foo() { } Foo.prototype.name = 'Foo';
// Class Bar function Bar() { this.name = 'Bar'; }
// instance var foo = new Foo; var bar = new Bar;
print(foo.name); //> Foo
print(bar.name); //> Bar
props(foo); //> Fields of prototype: name
props(bar); //> Fields: name |
This shows two popular ways to define a Class, but you should know the difference between them. Both of them initialize they instance by they prototype. In Foo.prototype there is a field ‘name’ whose value is ‘Foo’, so the instance foo.__proto__ will hold the field too. But Class Bar use another way, first it create a object named ‘bar’, then in the initial code add a property ‘name’ to the object ‘bar’.
When we call bar.name, the object has the property ‘name’, so return its value, and when call foo.name, there is no ‘name’ property in object foo, it will search the prototype chain and find the name property in foo.__proto__, return the value ‘Foo’. JavaScript then looks in the prototype object, the prototype of the prototype, and so on, up the prototype chain until the property is found or the chain is exhausted[mastering dojo].
The follow figure shows the structure.
You see that, foo hold a pointer __proto__ which point to Foo.prototype’s space. So the name field does not belong to foo directly, when we modify Foo.prototype, the foo.__proto__ will change too. Have a test:
// Class Foo function Foo() { } Foo.prototype.name = 'Foo';
// instance var foo = new Foo;
print(foo.name); //> Foo
// update Foo.prototype Foo.prototype.name = 'My name changes';
print(foo.name); //> My name changes
foo.name = 'foo';
print(foo.name); //> foo
print(foo.__proto__.name); //> My name changes
|
After this code, the foo’s structure is:
When we change foo.name, it won’t pollute the Foo.prototype, it only add a new property to object foo itself, so other instances’ name still be Foo.prototype.name or has they own ‘name’ property as foo does.
The prototype chain is the most important notion to JavaScript ‘inheritance’. Let’s go to see how to use it to implements inheritance in JS.
BTW: some time we use the follow code to declare a class:
function Foo(){} Foo.prototype = { name: 'Foo', method: function(){ /* ... */ } } var foo = new Foo;
|
This will cause a problem, Foo.prototype now point to a new object now, so the properties belong to its original prototype maybe lost (why maybe lost? Because the object “{}” also has most of the Function’s properties, such as toString), especially the “constructor” property. So we need to set the constructor explicitly.
Foo.prototype.constructor = Foo; |
But by this way, the constructor’s DontEnum attribute is lost, so when you use for … in you can see the foo.constuctor. The follow figure show what’s going on. The dot line means the connection not exists.
Inheritance
The simple comprehension of inheritance to us is it can do the follow things:
- Subclass has the super class’s properties (member variable) and methods.
- subclass could override them and has a way to call super class’s methods
- type conversions, the super class could hold a pointer (or reference) to its subclass
Let’s declare a inherit class first and see step by step.
The classical example is Shapes such as Circle, Rectangle…, see the follow code:
// super class Shape function Shape(){} Shape.prototype._name = 'Shape'; Shape.prototype.color = 0; Shape.prototype.setColor = function(color) { this.color = color; }
// subclass Circle, derived from Shape function Circle(){}; Circle.prototype = new Shape; Circle.prototype._name = 'Circle'; Circle.prototype.radius = 0; Circle.prototype.setRadius = function(radius) { this.radius = radius; } Circle.prototype.area = function() { return Math.PI * this.radius * this.radius; }
// Circle instance var circle = new Circle; circle.setColor(0xFF0000); circle.setRadius(10);
print(circle._name); //> Circle
print(circle.area()); //> 314.1592653589793 |
This figure shows circle’s structure after call var circle = new Circle; but after setColor and setRadius, the circle will change to:
Do you see that there is no “constructor” property in circle’s __proto__, because we missed to set it after Circle.prototype = new Shape; you should add Circle.prototype.constructor = Circle; to the code. Otherwise we you call circle.constructor it will return Shape to you.
OK, from the Shape and Circle prototype chain, let’s see the “inheritance” problems.
The first one: subclass has the super class’s properties. It’s clear that, if the subclass has no the properties, it will search the prototype chain, so it will get the super class (top prototype chain)’s properties sooner or later. But if the subclass already owns the properties itself, it will stop search the prototype chain, which means it implements the override, problem 2 is gone. For the problem 3, because JavaScript is weak typing, the “var o;” could hold everything it wants to, need not type conversions.
Declare Class in dojo
dojo.declare的重要性无需再重申,在dojo1.1的时候就有将近400个类通过dojo.declare来定义【mastering dojo P225】。
在介绍dojo.declare之前,首先看一下dojo.declare中用到的几个函数:dojo.mixin、dojo.clone、dojo.delegate、dojo.extend。
JavaScript里的基本数据类型(bool, int, float, String等)在赋值中是直接copy的,但对于其他的对象(Array,Object等)则是指针copy。
dojo.mixin, dojo.clone
dojo.mixin有点类似于“浅copy”,dojo.clone则类似于“深copy”,例如:
var loc = { city: 'Bei Jing', country: 'China' };
var obj = { name: 'Tian an men', location: loc };
var mixin_obj = dojo.mixin({}, obj); var clone_obj = dojo.clone(obj);
|
dojo.extend
dojo.extend = function(/*Object*/ constructor, /*Object...*/ props){ // summary: // Adds all properties and methods of props to constructor's // prototype, making them available to all instances created with // constructor. for(var i=1, l=arguments.length; i<l; i++){ dojo._mixin(constructor.prototype, arguments[i]); } return constructor; // Object } |
dojo.extend是将一组hash property直接mixin到一个类的prototype中。
dojo.delegate
dojo.delegate = dojo._delegate = (function(){ // boodman/crockford delegation w/ cornford optimization function TMP(){} return function(obj, props){ TMP.prototype = obj; var tmp = new TMP(); if(props){ dojo._mixin(tmp, props); } return tmp; // Object } })(); |
dojo.delegate可以简单的看作:
function(obj, props){ TMP.prototype = obj; var tmp = new TMP(); if(props){ dojo._mixin(tmp, props); } return tmp; // Object } |
首先看一下mastering dojo上的例子:
//create an object with some properties var base = { greet: function(){ return "hello, my name is " + this.name; } } var newObject = dojo.delegate(base, { name: "John" });
|
在dojo.delegate中,首先定义了一个“空类”TMP,并把第一个参数base作为该类的prototype,对象tmp是该类(TMP)的一个实例,在创建完tmp后,在将props中的property mix in到tmp对象中。mastering dojo中,作者称之为“不使用constructor方法来自定义对象”,一般我们在定义一个实例的时候,首先要声明一个类,但使用dojo.delegate可以直接定义一个函数对象的实例。另外我们也可以理解为newObject只是一个对象,但这个对象的一些property会随着base的改变而改变(因为prototype chain的存在)。