一、使用构造函数创建对象
在Javascript中创建一个对象最简单的方式就是使用字面量的方式
var obj = {}
obj.a = 1
obj.b = 2
但是当对象越来越复杂时,这种方式的弊端开始显现:每次创建对象都要写很多重复的代码。因此,我们可以使用构造函数的方式
function F (foo, bar) {
this.a = 1
this.b = 2
this.foo = foo
this.bar = bar
}
var ins = new F(11, 22)
这种方式虽然解决了字面量带来的问题,但也有缺点。可以看出,在每个实例中,属性a和属性b都是相同的,那么能不能将它们独立出来呢,因此,原型的概念产生了。
二、模板VS原型
prototype
针对前文中提到的所有实例共有的公共属性,JS提供了一个定义在构造函数上的属性prototype,指向一个对象。每次创建实例对象时,以prototype作为模板,因此,上面的代码变为:
function F (foo, bar) {
this.foo = foo
this.bar = bar
}
F.prototype.a = 1
F.prototype.b = 2
var ins = new F(11, 22)
F.prototype称为ins的原型,F.prototype上的属性称为原型属性,ins中的属性称为实例属性
__proto__
那么问题来了,在计算机中,实例属性与原型属性具体是如何处理的呢?最直观的想法就是将Func.prototype拷贝到每个实例中,或者理解为将Func.prototype拷贝一份,然后在这个拷贝上添加实例属性,这样,每个实例就能够访问到原型的属性和方法了,也符合前文中提到的模板的含义。但是这种做法有两个缺点
- 浪费内存空间
- 各个原型对象是相互独立的,原型属性的变化不能动态表现在实例上
第一点是毫无疑问的,对于第二点,考虑如下的情况:以Func.prototype为模板新建了实例ins1和ins2,然后修改了Func.prototype上的一个属性,之后建立了实例ins3。由于每个实例上都有Func.prototype的独立拷贝,因此Func.prototype上的变化不会表现在旧的实例上。
因此针对这个问题,Javascript采用了第二种方式:将原型对象prototype作为一个独立的对象,在实例中通过一个指针来对它进行访问,也就是__proto__。通过这种引用的方式,节省了内存空间,并且每个实例上的__proto__指针都指向同一个原型对象,实现了原型的动态性,即原型上的变化能够反映在每个实例当中。
prototype,常称为构造函数的原型对象,是构造函数Func的一个属性,指向一个对象,也就是子类实例的模板,在new Func()时,会将Func.prototype作为实例ins的模板
__proto__,可以理解为对象的一个指针,该指针指向自己的原型,也就是构造函数的原型对象Func.prototype,即ins.__proto__===Func.prototype
三、原型链
在明白原型的概念后,原型链也就比较简单了。原型链可以理解为一条链表,只是链表一般通过next指针向后查找,而原型链通过__proto__指针向祖先查找。每个构造函数的prototype指向原型对象(模板),每个实例对象的__proto__指向构造函数的prototype。
var ins = new Func()
ins.__proto__ === Func.prototype
那么问题来了,除环形链表外,链表都是有结尾的,即next指针指向null,那么JS的原型链呢?没错,原型链也是有尽头的,尽头就是Object.prototype。在JS中,每个对象都是Object的一个实例,Func.prototype,可以理解为Func.prototype = new Object(),因此,Func.prototype.__proto__ === Object.prototype。
var ins = new Func()
ins.__proto__ === Func.prototype
Func.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null // 到头了!!!
其实原型链是非常复杂的,之后还会有文章讲解继承以及函数的原型链部分