JavaScript 继承

本文详细解析JavaScript中的原型链概念,从原型链的建立到继承模式的应用,包括原型链示例、将共享属性迁移到原型中、只继承于原型、uber访问父对象方式、继承封装函数、属性拷贝、小心处理引用拷贝、对象之间的继承、深拷贝、使用object()函数、原型继承与属性拷贝的混合使用、多重继承、寄生式继承等核心知识点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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/>");


转载于:https://my.oschina.net/appk/blog/152953

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值