说不尽的函数-原型链
2010-09-16 00:00 Feather 阅读(282) 评论(0) 收藏 举报一、概念:函数对象,Prototype,__proto__
构造函数对象和函数对象没有什么区别,可以说是一个概念,因为他们都是定义的时候创建的。下面我们定义一个函数:
function Book(){ }
这时候我们已经创建了一个函数,而函数实际上就是一个对象,就是我们所说的函数对象。要提一下的是,本节的关键字是对象,从这里开始我们要忘掉函数的一切,用对象的概念理解下文,记住,Book是一个对象。
继续,这个函数对象看似简单,实际却复杂无比。首先,与作用域链有关的内建属性[[Scope]]被创建(这个属性是内建属性,我们无法访问,但我们依然把他理解为是Book对象的一个属性,这部分我们以后再讨论),然后,prototype属性也自动被创建和初始化,这也是我们众所周知的原型对象,这个原型对象有一个constructor属性,指向与原型相关的构造函数,就是例子里的Book。
其实,我觉得这里我们应该把他们分别独立来看待,以便更好地理解:创建一个函数的时候,它的一个原型对象也会被建立,我们可以通过函数对象的一个叫prototype属性访问到这个原型对象。为什么要这么理解呢?因为Javascript的原型继承,由构造函数实例化出来的实例对象都会存在一个属性指向原型对象,形成原型链。但是他们并没有prototype属性,他们是通过一个叫_proto_的内部属性指向这个原型对象的。在Friefox、Safari、Chrome浏览器中,这个属性可以被我们直接访问。所以,我们可以这么说,每一个对象都有一个原型对象,但不是每一个对象都有一个prototype属性。
为了更加清楚地说明问题,借用一下《High Performance JavaScript》的一段代码和UML图说明吧。
function Book(title, publisher){
this.title = title;
this.publisher = publisher;
}
Book.prototype.sayTitle = function(){
alert(this.title);
};
var book1 = new Book("High Performance JavaScript", "Yahoo! Press");
var book2 = new Book("JavaScript: The Good Parts", "Yahoo! Press");
alert(book1 instanceof Book); //true
alert(book1 instanceof Object); //true
book1.sayTitle(); //"High Performance JavaScript"
alert(book1.toString()); //"[object Object]"

可以看到,两个实例对象book1和book2的_proto_属性以及函数对象的prototype属性都指向Books构造函数的原型对象。上面说了,这个原型对象在Book函数定义的时候就建立了,Book构造函数保留了对其的引用,并且在初始化其实例对象时,把这个引用复制到这些实例对象的_proto_属性中。
其实,上面有句话我是说得很不严谨的:创建一个函数的时候,它的一个原型对象也会被建立,我们可以通过函数对象的一个叫prototype属性访问到这个原型对象。不严谨的地方是,Book作为一个函数对象,他本身真正的原型对象不是Book.prototype,而是Book._proto_指向的对象(上面的UML图也没有写出来),再具体点说,Book其实就是由Function构造函数实例化出来的对象,而它的原型对象是Function.prototype指向的对象。
如果我们想继续深究,可以再想想,上面的例子确实证明了每个对象都有一个原型对象,那么Function.prototype的原型对象又是什么呢?
二、深层分析
我自己想这个问题时候,感觉很混乱,但是直觉好像在告诉我Function.prototype的原型对象应该是一个Object对象。为了不误导观众,我先告诉大家这是错误的,但是我当时的这个感觉是有根据的,就看看上面的UML图,Book是一个构造函数,它的prototype属性的原型对象指向的是一个object对象,这个object对象其实也被Object构造函数的prototype属性指向。既然Book(代表着自定义构造函数)和Object(代表内置构造函数)的相关属性都是指向同一个原型对象,那么Function作为一个内置构造函数应该也不会例外吧。(其实有点硬扯...)
但是,事实偏偏就是例外。要考证这个问题很简单,输出一下就知道。我在网上找到一份比较全面的测试,我们可以点击这里看看,或者直接看下面的Copy。
var str = "string";
var Fn = function() {var i;};
var f = new Fn();
str.__proto__
str.prototype undefined
str.constructor function String() { [native code] }
str.__proto__.constructor function String() { [native code] }
Fn.__proto__ function Empty() {}
Fn.prototype [object Object]
Fn.constructor function Function() { [native code] }
Fn.__proto__.constructor function Function() { [native code] }
Fn.__proto__.__proto__ [object Object]
f.__proto__ [object Object]
f.prototype undefined
f.constructor function () {var i;}
f.__proto__.constructor function () {var i;}
f.__proto__.__proto__ [object Object]
我们先不用关心全部,就留意一下加粗的那条输出。Fn是一个自定义构造函数,它的原型对象竟然是一个函数对象。根据前面的讨论我们也知道Fn的原型对象其实就是Function.prototype指向的那个对象。
突如起来的Empty函数对象貌似把一切都搞乱了,很多早在我心里酝酿好的可能规律全部都给Empty破坏了,本以为这条原型链快要到结尾了,现在看来我们还要继续把这条似乎没有尽头的链拉上来。
经过一番探索,我们发现,Empty构造函数的__proto__属性指向Object.prototype属性指向的原型对象,而Empty.prototype则为null,Empty.contructor则指向Function构造函数;
另外Function.__proto__属性和Function.prototype指向的都是Empty构造函数。
如果你还没有抓狂,我们佩服你。我用了一个晚上才清清楚楚把这整个结构的UML图画出来,碍于手边没有合适的UML软件,暂时没办法画出一副电子版的UML图出来。其实如果研究一下,我们还是可以找到很多规律的。
首先,我们先不要把Empty当作一个构造函数对象,而是一个原型对象,一个有prototype属性和contrcutor属性的原型对象,然后,我们就可以有下面结论:
- 所有构造函数对象(Object,Function,Book...)的__proto__属性都指向Empty对象,即Function.prototype指向的对象。另外一种理解方式是:所有构造函数对象(Object,Function,Book...)都是Function构造函数的一个实例对象,它们的原型对象是Function.prototype指向的对象。就好像实例对象book1的原型对象是Book.prototype指向的对象一样。
- 所有原型对象(Book.prototype,Function.prototype..)的__proto__属性都指向Object,prototype属性所指向的对象。而此对象的__proto__属性为null。
貌似还是很乱...条理清楚的最好方式还是自己画画UML图出来。
终于写完这篇文章了,其实开始我没有打算写原型链的,写着写着就拼命往这里面钻了,还好,感觉自己也学到了许多东西。
最近看了很多关于js函数的文章,发现自己还有很多知识没有学到,要继续努力吖。
参考资料
《High Performance JavaScript》by Nicbolas C.Zakas
《JavaScript权威指南》 by David Flanagan
http://dmitrysoshnikov.com/ by Dmitry A. Soshnikov
浙公网安备 33010602011771号