Javascript中的原型模式
本文参考《Javascript高级程序设计》
刚从java转到js的时候,最晕的地方就是碰到创建对象和扯到原型,java里面创建一个类然后new出来就是对象,而js是通过函数和原型来建立一套继承体系,两者感觉很相似但是又有很多不同的地方,但其实弄清楚原理之后也就没那么头晕了。
函数、对象和原型之间的关系
构造函数
在js中,创建对象的方式和java是相似的,都是使用new关键字,而使用new关键字的函数则称为构造函数,但其实我们写js的时候很多地方都用到了函数,那构造函数和普通函数的区别是什么呢?其实构造函数也是函数,它们之间的唯一区别就是调用方式的不同。任何函数,只要通过new来调用,它就是构造函数。为了便于区分,我们一般将构造函数首字母大写。
原型对象
当创建一个函数的时候,这个函数内部将有一个prototype的属性,这个属性指向的就是该函数的原型对象,而这个原型对象中又有一个constructor的属性,这个属性指回了创建的函数。
原型可以理解为一个模版,和java中类的作用很像,在原型里创建的属性和方法,通过调用构造函数创建的实例都能访问到,其实每当创建一个实例,这个实例里就包含一个指针,这个指针就指向了原型对象。ECMA-262 第 5 版中管这个指针叫[[Prototype]]。虽然在脚本中没有标准的方式访问[[Prototype]],但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性_proto_;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
即对如下语句,有
function Person(){
}
console.log(Person.prototype.constructor == Person); //true
var p = new Person();
console.log(p.__proto__ == Person.prototype); //true
理解完三者的关系后,如何给对象添加属性就很明确了,因为所有的实例都有一个指向原型的指针,所以在原型上定义的属性对所有实例都是一样的,而对构造函数而言,创建不同的实例可以让其拥有不同的属性或方法。在一般情况下,我们需要为不同实例分配不同的属性,但是方法往往是一样的,那么自然而然的,属性就放在构造函数里,而方法就放在原型上,即:
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.say = function(){
console.log(this.name + " : " + this.age);
}
var p1 = new Person("小红", 20);
var p2 = new Person("小名", 21);
p1.say(); //小红 : 20
p2.say(); //小名 : 21
通过原型链实现继承
《Javascript高级程序设计》对原型链的描述是
ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
一个一个另一个看着有点晕,但是关键就在于加粗的这段文字,我们假设有两个原型如下
如果我们改变Son的prototype指向,让其等于Father的一个实例,会怎么样呢?就和普通Father实例一样,Son的prototype将有一个[[Prototype]]指针,指向Father的原型对象,从而构成了一个链:
function Father(){
this.fatherValue = "father";
}
function Son(){
this.sonValue = "son";
}
Son.prototype = new Father();
var son = new Son();
console.log(son.sonValue); //son
console.log(son.fatherValue); //father
通过这种方式一层一层连接起来,就成为了javascript中的链式继承。
但是这样的继承方式有一些问题,因为Son的原型成为了Father的一个实例,所以在Father的构造方法上定义的属性,Son的原型也将拥有,这就造成Son的实例共享了Father的属性,如:
function Father(){
this.fatherValue = "father";
this.friends = ["cat", "dog"];
}
function Son(){
this.sonValue = "son";
}
Son.prototype = new Father();
var son1 = new Son();
son1.friends.push("fox");
console.log(son1.friends); //["cat", "dog", "fox"]
var son2 = new Son();
console.log(son2.friends); //["cat", "dog", "fox"]
两个不同的实例,却共享了一个friends属性,这是不合要求的,这时,我们可以结合借用构造函数方式,在子类的构造函数里通过call或apply调用父类的构造函数,同时将子类原型的contructor指向子类自己的构造函数:
function Father(){
this.fatherValue = "father";
this.friends = ["cat", "dog"];
}
function Son(){
this.sonValue = "son";
Father.call(this); //调用父类的构造函数
}
Son.prototype = new Father();
Son.prototype.constructor = Son;
var son1 = new Son();
son1.friends.push("fox");
console.log(son1.friends); //["cat", "dog", "fox"]
var son2 = new Son();
console.log(son2.friends); //["cat", "dog"]
这就解决了子类共享父类属性的问题。
另外,将Son.prototype.constructor设置成Son的作用是可以通过Son的实例方便访问到Son的原型,比如
function Father(){
this.fatherValue = "father";
this.friends = ["cat", "dog"];
}
function Son(){
this.sonValue = "son";
Father.call(this);
}
Son.prototype = new Father();
Son.prototype.constructor = Son
var son = new Son();
son.constructor.prototype; //Son的原型
如果不设置,则Son.prototype.constructor为Father,很好理解,因为毕竟Son.prototype都是Father的实例了。
extend函数
为了简化继承的操作,我们可以把这个过程封装在一个函数中。
function extend(subClass, superClass){
var F = function(){};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.constructor = subClass;
}
这里使用一个空的构造函数F是为了避免创建超类的新实例(有时可能会比较庞大,或者有副作用等)。这样,我们就能简化一些手工的设置,而直接调用extend函数即可。
function Father(){
this.fatherValue = "father";
this.friends = ["cat", "dog"];
}
function Son(){
this.sonValue = "son";
Father.call(this); //调用父类的构造函数
}
extend(Son, Father);
还有一个问题是Father的名称被固化在了Son的构造函数里,我们可以通过给子类添加一个指向父类的属性来解决这个问题。
function extend(subClass, superClass){
var F = function(){};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.constructor = subClass;
subClass.superclass = superClass.prototype;
if(superClass.prototype.constructor == Object.prototype.constructor){
superClass.prototype.constructor = superClass;
}
}
上面给子类添加了一个superclass属性,它指向父类的原型对象,而最后的判断是为了确保父类原型对象的constructor属性被正确设置。因为如果父类的prototype被手动设置成了一个对象,比如
Father.prototype = {
constructor: function(){
//...
}
}
这个对象自己有一个constructor属性,因为我们在后面比如子类构造函数内调用父类的构造函数,所以为了能正确调用,手动将其设置成superClass。
这样,在子类内就不用写父类的名称了。
function Father(){
this.fatherValue = "father";
this.friends = ["cat", "dog"];
}
function Son(){
this.sonValue = "son";
this.superclass.constructor.call(this); //调用父类的构造函数
}
extend(Son, Father);