本文同时发布在另一独立博客 Javascript: 从prototype漫谈到继承(1)
javasscript的prototype原型链一直是一个难点,这篇文章是对自己这段时期学习的一个总结,在这里不谈ECMAScript标准,也不会用UML图画出各种关系(结合这两方面谈的文章非常的多,但大部分都相当晦涩,比如汤姆大叔),只力求最浅显易懂,深入浅出,供以后自己和各位参考。
javascript的function一种对象(object),他们有方法和属性,方法比如call/apply,而prototype则是function的一个属性。
一旦你定义了一个函数,它即自带了一个prototype属性
function t(){};
typeof t.prototype // "object";
你可能已经知道使用函数作为一个构造函数,来生产一系列对象。比如
function Some(name, color){
this.name = name;
this.color = color;
this.method =function(){}
}
var a1 =new Some("Lee","black");//实例化一个对象
上面的Some类的属性和方法也可以放在prototype对象中,比如
function Some(){}
Some.prototype.name ="Lee"//形式一
Some.prototype ={ //形式二 name:"lee", color:"black", method:function(){}
}
var a1 =newSome("Lee","black");//实例化一个对象
虽然形式不同,但至少现在使用起来的效果是一致的。当你使用a.Lee或者a.method时,结果是一样的,现在还看不出分别
Ok,那么第一点要注意的是,prototype是活着(live)的属性!
function Some(){}
var a =newSome();
a.method // undefined
Some.prototype.method = function(){ console.log("hello");
} a.method // function () {console.log("Hello")}
上面的代码想说明的是,在生成实例a时,构造函数没有method方法,所以a也没有,可以理解;但是之后构造函数在prototype属性里又添加上去了,虽然是在a生成之后添加的,但是a仍然照样拥有,与构造函数添加的时间无关。
第二个问题来了,如果这个对象内部和prototype都定义了相同的字段怎么办,比如
function Some(){
this.color ="yellow";
}
Some.prototype.color ="black";
var a =newSome();
a.color //?
上面的代码中,我在对象的内部和prototype上分别都定义了color,当我从实例中访问的时候,应该显示的是哪一个颜色?
要注意的是第二点,javascript引擎首先会检查a的属性里有没有color,如果没有的话去它的构造函数的prototype(a.constructor.prototype)里有没有该属性
让我们再看的远一点,任何一个对象都应该有自己的构造函数,函数的prototype属性也是个对象,那它的构造函数是什么?
functionSome(){
this.color ="yellow";
}
var a =newSome();
a.constructor.prototype.constructor // function Some() {this.color = "yellow";} a.constructor.prototype.constructor.prototype // Some {}
上面的原型链可以无限的追溯下去,通过原型链,可以追溯到最终的构造函数Object()
,这也就解释了,为什么即使我们没有在函数上定义toString()
函数,a.toString()
的方法也是存在的,因为它最终调用的追溯到的Object的toString方法。
新的问题是,如何区分自己的property和原型链上的属性,并且你能保证所有的属性都是可以访问的吗?
众所周知,用for...in
循环就可以解决这个问题,关于这个问题,只需要记住三点
- 虽然在循环中对象自己的属性和原型链属性都会被列举出来,但并非所有属性都会被列举,比如一个数组的length和.splice之类的方法就不一定会被列举出来,可以列举出来的属性都是可枚举的(enumerable)
- 如何区分对象自己的属性还是原型链的属性?使用
hasOwnProperty()
方法 - 注意
propertyIsEnumerable()
方法,虽然该方法名字是“可枚举的属性”,但是原型链中所有的属性都会反悔false,即使是可枚举的
还有一个对象的属性叫做__prop__,个人认为用处不大,只推荐在调试的时候使用,具体用法google去吧
关于原型的继承
如何写一个好的继承方法?这是一个逐渐演化过程,先从最简单的继承谈起
function Parent(){
this.deep ="Hello";
}
function Child(){
this.shallow ="World";
}
Child.prototype =new Parent();
var c =newChild();
console.log(c.deep);
当我们要访问c的deep属性时
- 首先去c对象下查看有没有deep属性,没有
- 再去c.construct.prototype对象的属性里查找,Parent的实例里查找,有
但是上面的代码有一个问题,当你不断实例化Child时,Parent也不会被实例化,都会生成一个deep载入内存中,如果这个deep是共享的话,不如把deep放在prototype中
function Parent(){}
Parent.prototype.deep ="Hello";
function Child(){
this.shallow ="World";
}
Child.prototype =new Parent();
var c =newChild();
console.log(c.deep)
当我们要访问c的deep属性时
- 首先去c对象下查看有没有deep属性,没有
- 再去c.construct.prototype对象的属性里查找,Parent的实例parent里查找,没有
- 再去parent.construct.prototype查找deep,有
这么做的弊端之一就是在查找某个属性的时候可能会多查找一轮
让我们继续改进,我们发现我们需要的deep只在Parent的prototype上,那么其实我只需要Parent的prototype而不是Parent的实例
function Parent(){}
Parent.prototype.deep ="Hello";
function Child(){
this.shallow ="World";
}
Child.prototype = Parent.prototype;
var c =new Child();
console.log(c.deep);
这样既避免了Parent的实例化,又避免了上一个例子中多一步的查找。但是有一个副作用,因为是对对象直接的引用,所以当Child.prototype.deep被修改时,Parent.prototype.deep也会被修改。那我们继续优化的目标就很明确了,要阻止这种对父类prototype的直接引用。
于是我们决定使用一个中间变量
function Parent(){}
Parent.prototype.deep ="Hello";// 注意,来了var F =function(){}; F.prototype = Parent.prototype;
function Child(){
this.shallow ="World";
}
Child.prototype = new F();
var c = new Child();
console.log(c.deep)
我们用F来作为一个中间变量,来阻止child对deep的修改可能影响parent
当我们要访问c的deep属性时
- 首先去c对象下查看有没有deep属性,没有
- 再去c.construct.prototype对象的属性里查找,F的实例里查找,没有
- 再去F.construct.prototype查找deep,有
让我们来捋一捋为什么对Child.prototype的修改不会影响Parent.prototype
- 在上一个例子中,我们对Child.prototype的操作就是对Parent.prototype的操作,无论读还是写,用的是别人的
- 在这个例子中,Child.prototype不是对Parent的直接引用,而是一个新的空对象。在没有deep而我们需要deep时,被迫去Child.prototype的构造函数上去找,追溯到了Parent.protoype,而当我们需要写时,操纵的其实是
Child.prototype = {}
这个空对象。
于是我们把最后一个代码片段抽象为一个方法
function extend(Child,Parent){
var F =function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
// 一旦重置了函数的prototype,需要重新赋值prototype.constructor,
// 忽略这方面的介绍
Child.prototype.constructor = Child;
// 保留对父类的引用,
// 忽略对这方面的介绍
Child.uber =Parent.prototype;
}