1.原型链
让我们从默认的继承模式开始,即通过原型来实现继承关系链。
JavaScript中每个函数都有一个prototype属性。该函数被new操作符调用时会创建出一个对象,并且该对象中会有一个指向其原型对象的秘密链接。通过该秘密链接,我们就可以在新建的对象中调用相关原型对象的方法和属性。
原型对象也具有对象固有的普遍特征,因此也包含了指向其原型的链接。由此形成了一条链,我们称之为原型链。
1.1 原型链示例
原型链是ECMAScript标准指定的默认继承方式。
//定义三个构造器函数
function Shape(){
this.name = 'shape';
this.toString = function(){return this.name;};
}
function TwoDShape(){
this.name = '2D shape';
}
function Triangle(side, height){
this.name = 'Triangle';
this.side = side;
this.height = height;
this.getArea = function(){return this.side * this.height / 2;};
}
//开始施展魔法
TwoDShape.prototype = new Shape();
Triangle.prototype = new TwoDShape();
//记得上次讲的东西吗?记得重置函数的constructor属性
TwoDShape.prototype.constructor = TwoDShape;
Triangle.prototype.constructor = Triangle;
//下面,来测试一下到目前为止我们所实现的内容,先创建一个Triangle对象,再调用getArea()方法
var my = new Triangle(5,10);
document.write(my.getArea() + "<br/>"); //25
//my这个对象并没有toString()方法,但我们依然可以调用它所继承的方法,并且该方法toString()显然与my对象是紧密绑定在一起的
document.write(my.toString() + "<br/>");
//来看一看构造器函数乱没乱
document.write("<pre>");
document.write(my.constructor + "<br/>"); //结果显示,准确的找到了自己的构造器函数
document.write("</pre>");
//通过instanceof操作符,我们会发现,my对象同时属于上述三个构造器实例
document.write(my instanceof Shape);
document.write("<br/>");
document.write(my instanceof TwoDShape);
document.write("<br/>");
document.write(my instanceof Triangle);
document.write("<br/>");
document.write(my instanceof Function);
document.write("<br/>");
//同样,可以检测上述三个构造器的原型是不是my对象的原型也是true
document.write(Shape.prototype.isPrototypeOf(my) + "<br/>");
document.write(TwoDShape.prototype.isPrototypeOf(my) + "<br/>");
document.write(Triangle.prototype.isPrototypeOf(my) + "<br/>");
1.2 将共享属性迁移到原型中去
//当我们用某一个构造器构造对象时,其属性就会被添加到this中去。这会使某些不能通过实体改变的属性出现一些效率低下的情况。
//在上一节中,我们用new Shape()创建对象时,每个实体都会有一个全新的name属性,并在内存中拥有自己独立的存储空间。如果,我们把name属性添加到所有实体共享的原型对象中去。这样的话通过学更有效率,但这也只是针对实体中不可变的属性而言的。另外,这种方式也适用于对象中的共享方法。
//来我们来把上一节的例子重写一下。
function Shape(){}
Shape.prototype.name = 'shape';
Shape.prototype.toString = function(){return this.name};
function TwoDShape(){}
TwoDShape.prototype = new Shape(); //这里来实现继承
TwoDShape.prototype.constructor = TwoDShape; //重置原型对象的构造器
TwoDShape.prototype.name = '2D shape';
//有一点需要强调,我们必须在扩展原型对象之前完成继承关系的构建。
function Triangle(side, height){
this.side = side;
this.height = height;
}
Triangle.prototype = new TwoDShape(); //完成继承关系的构建
Triangle.prototype.constructor = Triangle; //重置原型对象构造器
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function(){return this.side * this.height / 2;};
//修改完成,之前所有的测试代码都适用于这个版本。
var my = new Triangle(5,10);
document.write(my.getArea() + "<br/>");
document.write(my.toString() + "<br/>");
2.只继承于原型
//正如上面所说,出于效率的考虑,尽可能的将一些可重用的方法和属性添加到原型中去。如果开成了这样的好习惯,我们仅仅靠原型就完成了继承的构建。
//由于原型中所有代码都是可重用的,这意味着继承自Shape.prototype比继承自new Shape()创建的实体好得多。
//new Shape()的方式会将Shape的属性设定为对象自身属性,这样代码是不可重用的。来我们做一些改善。
//1.不要单独为继承关系创建新对象
//2.尽量减少运行时搜索方法,例如toString()
//下面是修改过后的代码,高亮的地方是修改过后的代码
function Shape(){}
Shape.prototype.name = 'shape';
Shape.prototype.toString = function(){return this.name};
function TwoDShape(){}
TwoDShape.prototype = Shape.prototype; //这里来实现继承
TwoDShape.prototype.constructor = TwoDShape; //重置原型对象的构造器
TwoDShape.prototype.name = '2D shape';
//有一点需要强调,我们必须在扩展原型对象之前完成继承关系的构建。
function Triangle(side, height){
this.side = side;
this.height = height;
}
Triangle.prototype = TwoDShape.prototype; //完成继承关系的构建
Triangle.prototype.constructor = Triangle; //重置原型对象构造器
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function(){return this.side * this.height / 2;};
//修改完成,之前所有的测试代码都适用于这个版本。
var my = new Triangle(5,10);
document.write(my.getArea() + "<br/>");
document.write(my.toString() + "<br/>");
//这样精简固然有好处,但也有副作用,由于子对象和父对象指向的是同一个对象
//所以,当子对象修改了原型后,父对象随即也被改变,请看:
Triangle.prototype.name = 'Triangle';
var s = new Shape();
document.write(s.name + "<br/>"); //输出:Triangle, 怎样来解决这个问题呢?请看下回分解
临时构造器——new F()
//正如上面所说,如果所有属性都指向了一个相同的对象,父对象就会受到子对象对属性的影响。要解决这个问题:
//我们可以用一个临时构造器函数来充当中介。即我们创建一个空函数F(),并将其原型设置为父级构造器。
//然后,我们既可以用new F()来创建一个不包含父对象属性的对象,同时又可以从父对象prototype属性中继承一切了。
//下面是修改过后的代码,高亮的地方是修改过后的代码
function Shape(){}
Shape.prototype.name = 'shape';
Shape.prototype.toString = function(){return this.name};
function TwoDShape(){}
var F = function(){}; //空函数
F.prototype = Shape.prototype; //将父级的原型赋给F的原型
TwoDShape.prototype = new F(); //这里来实现继承
TwoDShape.prototype.constructor = TwoDShape; //重置原型对象的构造器
TwoDShape.prototype.name = '2D shape';
//有一点需要强调,我们必须在扩展原型对象之前完成继承关系的构建。
function Triangle(side, height){
this.side = side;
this.height = height;
}
var F = function(){}; //空函数
F.prototype = TwoDShape.prototype; //将父级的原型赋值给F的原型
Triangle.prototype = new F(); //完成继承关系的构建
Triangle.prototype.constructor = Triangle; //重置原型对象构造器
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function(){return this.side * this.height / 2;};
//修改完成,之前所有的测试代码都适用于这个版本。
var my = new Triangle(5,10);
document.write(my.getArea() + "<br/>");
document.write(my.toString() + "<br/>");
//所以,当子对象修改了原型后,父对象就不会改变了。
Triangle.prototype.name = 'Triangle';
var s = new Shape();
document.write(s.name + "<br/>"); //现在一切正常。完美完成继承。
3.uber——子对象访问父对象的方式
//在传统的面向对象语言中,通常会提供一种用于子类访问父类(有时也叫超类)的特殊语法
//因为我们在实现子类方法往往需要其父类方法额外辅助。在这种情况下,子类通常要去调用父类中的同名方法,以便完成工作。
//JavaScirpt没有这种特殊语法,要实现这种功能也相当容易。
//我们人为的构建一个uber属性,并令其指向父级原型对象。来看看:
//声明一个类,让后面的类来继承他。
function Shape(){}
//给原型创建一个属性name
Shape.prototype.name = 'Shape';
//重新来写一个toString()方法
Shape.prototype.toString= function(){
var result = [];
if(this.constructor.uber){
result[result.length] = this.constructor.uber.toString();
}
result[result.length] = this.name;
return result.join(', ');
}
//声明第二个函数TwoDSape
function TwoDShape(){}
var F = function(){};
F.prototype = Shape.prototype;
TwoDShape.prototype = new F();
TwoDShape.prototype.constructor = TwoDShape;
TwoDShape.uber = Shape.prototype; //这里来继承父级的原型
//给TwoDShape声明属性和方法
TwoDShape.prototype.name = '2D shape';
//声明三角形函数
function Triangle(side,height){
this.side = side;
this.height = height;
}
//处理三角形函数的继承问题
var F = function(){};
F.prototype = TwoDShape.prototype;
Triangle.prototype = new F();
Triangle.prototype.constructor = Triangle;
Triangle.uber = TwoDShape.prototype;
//给三角形函数的原型创建属性和方法
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function(){return this.side * this.height / 2;};
//上面的代码我们主要是新增了以下内容:
//1.将uber属性设置成了指向其父级原型的引用
//2.对toString方法进行了更新。
//测试一下,由于toString()方法中,this.constructor.uber是指向当前对象父级原型的引用。
//因此,当我们调用Triangle实体的toString()方法时,其原型链上的所有toString()都会被调用。
var my = new Triangle(5,10);
document.write(my.getrea() + "<br/>");
document.write(my.toString() + "<br/>");
4.将继承部分封装成函数
//把实现继承关系的代码提炼出来,放入一个叫做extend()的函数中,这样就实现继承代码的可重用的。
function extend(Child, Parents){
var F = function(){};
F.prototype = Parents.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parents.prototype;
}
//再把前面的三个函数重写一次来测试一下
function Shape(){}
Shape.prototype.name = 'Shape';
Shape.prototype.toString = function(){
var result = [];
if(this.constructor.uber){
result[result.length] = this.constructor.uber.toString();
}
result[result.length] = this.name;
return result.join(', ');
}
//创建第二个函数
function TwoDShape(){};
extend(TwoDShape, Shape);
//创建几个属性
TwoDShape.prototype.name = '2D shape';
//创建一个三角形函数
function Triangle(side, height){
this.side = side;
this.height = height;
}
extend(Triangle, TwoDShape);
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function(){
return this.side * this.height / 2;
}
var my = new Triangle(10,20);
document.write(my.getArea() + "<br/>");
document.write(my.toString() + "<br/>");
5.属性拷贝
//下面我们来尝试一个与之前略有不同的方法。
//在构建可重用的继承代码时,我们也可以简单地将父对象的属性拷贝给子对象。
//参照前的extend()函数,现在我们来写一个extend2()函数,这个函数需要传递两个构造器函数作为参数。
//并将原型全部拷贝给child原型,其中也包括方法,因为方法也是一种函数类型的属性。
//实现继承和拷贝的函数
function extend2(Child, Parents){
var p = Parents.prototype;
var c = Child.prototype;
for(var i in p){
c[i] = p[i]; //关键的循环这一步,将父级的原型属性全部拷贝给子级的原型对象。
}
c.uber = p;
}
//与extend()方法相比,这个方法效率上要稍逊一筹。
//这是因为这是执行的是子对象原型的逐一拷贝,而非简单的原型链查询。
//所以,要记住:这种方式只适合于用于只包含基本数据类型的对象。
//所有的对象类型是不可得复制的(包含数组和对象),因为它们只支持引用传递。
//来做一下测试
var Shape = function(){};
var TwoDShape = function(){};
Shape.prototype.name = 'shape';
Shape.prototype.toString = function(){return this.name;};
//现在先来用extend()方法来继承,来先写extend()方法
function extend(Child, Parents){
var F = function(){};
F.prototype = Parents.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parents.prototype;
}
//通过extend()来继承,那么name属性既不属于TwoDShape()实例的属性,也不会成为其原型对象的属性,但是子对象依然可以通过继承方式来访问该属性。
extend(TwoDShape, Shape);
var td = new TwoDShape();
document.write(td.name + "<br/>");
document.write(TwoDShape.prototype.name + "<br/>");
document.write(td.__proto__.name + "<br/>"); //__proto__属性本对象,它指向的相关原型的神秘链接
document.write(td.hasOwnProperty('name') + "<br/>");
document.write(td.__proto__.hasOwnProperty('name') + "<br/>");
//如果继承是通过extend2()来完成的话TwoDShape()的原型中就会拷贝有属于自己的 name属性,同样的,其中也会拷贝有属于自己的toString()方法,但这只是一个函数的引用,函数并没有被再次创建
extend2(TwoDShape, Shape);
var td = new TwoDShape();
document.write(td.__proto__.hasOwnProperty('name') + "<br/>");
document.write(td.__proto__.hasOwnProperty('toString') + "<br/>");
document.write(td.__proto__.toString === Shape.prototype.toString);
6.小心处理引用拷贝
7. 对象之间的继承
//对象之间的继承,我们用的方法,就是用一个空对象,将父级的所有属性拷贝过来。看函数:
function extendCopy(parents){
var child = {};
for(var i in parents){
child[i] = parents[i];
}
child.uber = parents;
return child;
}
//要使用上面这个函数,我们需要一个基本对象。
var Shape = {
name: 'shape',
toString: function(){return this.name;}
}
//接着我们就可以根据这个旧对象来创建一个新对象了。
var TwoDShape = extendCopy(Shape);
TwoDShape.name = '2D shape';
TwoDShape.toString = function(){return this.uber.toString() + ", " + this.name;};
//接着再来扩展一个Triangle对象
var Triangle = extendCopy(TwoDShape);
Triangle.name = 'Triangle';
Triangle.getArea = function(){return this.side * this.height / 2;};
//来使用一下Triangle对象,在使用之前是不是要传入side 和 height两个属性,来让我们传属性
Triangle.side = 5;
Triangle.height = 10;
document.write(Triangle.getArea() + "<br/>");
document.write(Triangle.toString() + "<br/>");
8 深拷贝
//前面讨论的extendCopy()函数所创建的方式叫做浅拷贝。与之相对的当然就是深拷贝了。
//经过前面的学习,我们知道,对象的拷贝实际上拷贝的只是该对象在内存中的位置指针,这一过程就叫做浅拷贝。在这一情况下,如果我们改变了拷贝对象,就等同于改变了原对象。
//而深拷贝则可以帮助我们避免这方面的问题。
//深拷贝的实现方式与浅拷贝的实现方式 基本相同,也需要遍历对象属性来进行拷贝操作。
//区别就是,当我们遇到一个对象引用性的属性时,我们需要再次对其调用深拷贝函数。
//请看下面的例子:
function deepCopy(parents, child){
var child = child || {};
for(var i in parents){
if(typeof parents[i] === 'object'){
child[i] = (parents[i].constructor === Array) ? [] : {};
deepCopy(parents[i], child[i]);
}else{
child[i] = parents[i];
}
}
return child;
}
//这里再定义一个浅拷贝的函数
function extendCopy(parents){
var child = {};
for(var i in parents){
child[i] = parents[i];
}
child.uber = parents;
return child;
}
//现在我们来创建一个数组和子对象属性的对象
var parents = {
numbers: [1,2,3],
letters: ['a', 'b', 'c'],
obj: {prop: 1},
bool: true
}
//下面,我们分别用深拷贝和浅拷贝测试一下,就会发现深拷贝与浅拷贝的不同。
//对它的numbers属性进行更新不会对原对象产生影响。
var mydeep = deepCopy(parents);
var myshoallow = extendCopy(parents);
//用深拷贝的对象改变numbers属性
mydeep.numbers.push(4,5,6);
document.write(mydeep.numbers + "<br/>"); //深拷贝的新对象的numbers属性
document.write(parents.numbers + "<br/>"); //原对象的numbers属性没有变化
//用浅拷贝的对象改变numbers属性
myshoallow.numbers.push(100,200,300);
document.write(myshoallow.numbers + "<br/>"); //浅拷贝的新对象的属性numbers的值
document.write(parents.numbers + "<br/>"); //原对象的值已经发生改变,和新对象的一样
document.write(mydeep.numbers + "<br/>"); //深拷贝的新对象的numbers属性并不会被影响到
9.object()
//基于对象之间直接构建继承关系的理念,这里还有一个建议,就是通过一个object()函数来接收父对象,并返回一个以该对象为原型的新对象
//请看例子:
function object(obj){
function F(){};
F.prototype = obj;
return new F();
}
//如果我们需要访问uber属性,我们还可以改变上面这个函数。请看下面的操作:
function object(obj){
var n;
function F(){};
F.prototype = obj;
n = new F();
n.uber = obj;
return n;
}
//上面的函数基本与extendCopy()相同:我们只需要将一个对象传给他,并由此创建一个新对象。然后再对新对象进行扩展处理即可。
//我们来声明一个对象。
var Human = {
name: 'person',
toString: function(){
return "直立行走的高级动物";
}
}
var Chinese = object(Human);
document.write(Chinese.toString() + "<br/>");
Chinese.name = '中国人';
Chinese.toString = function(){
return this.uber.toString() + ':' + this.name + "<br/>";
}
document.write(Chinese.toString() + "<br/>");
10. 原型继承与属性拷贝的混合使用
//对于继承来说,主要目标就是将一些现有的功能归为己有。
//也就是说,我们在创建一个新对象时,通过应该先继承于现有对象,然后再为其添加额外的方法与属性。
//对此我们可以通过一个函数调用来完成,并且在其中混合使用我们上面所讨论过的两种方式
//具体而言就是:
//1.使用原型继承的方式继承(clone)现有对象
//2.而对其它对象使用属性拷贝(copy)的方式
function objectPlus(o, stuff){
var n;
function F(){};
F.prototype = o;
n = new F();
n.uber = o;
for(var i in stuff){
n[i] = stuff[i];
}
return n;
}
//上面的这个函数接受两个参数,o是用于继承,stuff则用于拷贝方法和属性。
//来看实例
var shape = {
name: 'shape',
toString: function(){
return this.name;
}
}
//创建一个2D对象,并为其添加更多的属性。这些额外的属性由一个用文本标识法所创建的匿名对象提供
var twoDee = objectPlus(shape, {
name: '2D shape',
toString: function(){
return this.uber.toString() + ", " + this.name;
}
});
//现在我们再来创建一个继承于2D对象的triangle对象,并为其添加一些额外的属性和方法
var triangle = objectPlus(twoDee, {
name: 'triangle',
side: 0,
height: 0,
getArea: function(){
return this.side * this.height / 2;
}
});
//下面我们再创建一个具体对象my,定义其side height属性
var my = objectPlus(triangle, {side:5, height:10, name:'my'}); //删掉name属性
document.write(my.getArea() + "<br/>");
document.write(my.toString() + "<br/>"); // 看看这里会有什么变化?为什么?
11. 多重继承
//所谓多重继承,通常指的是一个对象中有不止一个父对象的继承模式。
//对于这种继承模式,有的语言支持,有的语言不支持。
//多生继承的实现极其简单,我们只需要延续属性拷贝法的继承思路依次扩展对象,就不会对其所继承的对象数量参数输入进行限制。
//下面我们来写一个函数,它可以接受任意多个对象参数。
//我们在其中实现了一个双重循环,内层循环用来拷贝属性,外层循环用来遍历函数参数中所传递进来的所有对象。
function multi(){
var n={};
var stuff;
var len = arguments.length;
for(var j = 0; j < len; j++){
stuff = arguments[j];
for(var i in stuff){
n[i] = stuff[i];
}
}
return n;
}
//我们来测试一下,我们要创建一个shape对象 一个2D对象 及一个匿名对象,把这三个对象作为参数传给multi()函数
var shape = {
name: 'shape',
toString: function(){
return this.name;
}
};
var twoDee = {
name: '2D shape',
dimensions: 2
}
var triangle = multi(shape, twoDee, {
name: 'triangle',
getArea: function(){
return this.side * this.height / 2;
},
side: 5,
height: 5
});
//看看工不工作
document.write(triangle.getArea() + "<br/>");
document.write(triangle.toString() + "<br/>");
//要注意:multi()是按照对象的输入顺序来进行遍历的。如果其中两个对象有相同的实现,通常会以后一个的为准
12. 寄生式继承
//我们再来介绍一种JavaScript的继承模式,叫做寄生式继承
//其基本内容是:我们可以在创建对象的函数中直接吸收其他对象的功能,然后对其进行扩展并返回。
//“就好像所有的工作都是自己做的一样”
//来先声明一个对象
var twoD = {
name: '2D shape',
dimensions: 2
};
//现在,我们来编写创建triangle对象的函数,注意两点:
//1.将twoD对象克隆进一个叫做that的对象,这一步可以是我们之前所讨论过的任何方法。
//2.扩展that对象,添加更多的属性。
//3.返回that对象。
function object(obj){ //先写一个能把原对象拷贝过来的函数,obj参数接收的是一个对象
var n = {};
var F = function(){};
F.prototype = obj;
n = new F();
n.uber = obj;
return n;
}
//这里才是本节要学习的内容
function triangle(s, h){
var that = object(twoD);
that.name = 'Triangle';
that.side = s;
that.height = h;
that.getArea = function(){return this.side * this.height / 2;};
return that;
}
//来试一试
var t = triangle(5,10);
document.write(t.getArea() + "<br/>");