手机版

JavaScript原型继承详解

时间:2021-10-13 来源:互联网 编辑:宝哥软件园 浏览:

JavaScript是一种面向对象的语言。JavaScript里有句经典的话,一切都是对象。因为它是面向对象的,所以它有三个特点:封装、继承和多态。这里是JavaScript的继承,其他两个将在后面讨论。

JavaScript的继承不同于C,C是基于类的,而JavaScript的继承是基于原型的。

现在问题来了。

原型是什么?原型我们可以参考C语言中的类,也可以保存对象的属性和方法。例如,我们写一个简单的对象。

复制代码如下:功能动物(名称){this。name=name} animal . prototype . setname=function(name){ this . name=name;} var Animal=new Animal(' wangwang ');

我们可以看到,这是一个对象Animal,它有一个属性名和一个方法集名。请注意,一旦原型被修改,例如添加一个方法,对象的所有实例将共享这个方法。例如

复制代码如下:功能动物(名称){this。name=name} var Animal=new Animal(' wangwang ');

这时,动物只有名字属性。如果我们加上一句话,

复制代码如下: animal . prototype . setname=function(name){ this . name=name;}

这时,动物也有setName方法。

继承副本——以一个空对象开始。我们知道JS的一个基本类型叫做object,它最基本的实例是一个空对象,也就是直接调用新的Object()生成的实例,或者用字面量{}声明的实例。空对象是只有预定义属性和方法的“干净对象”,而所有其他对象都是从空对象继承的,因此所有对象都有这些预定义的属性和方法。原型实际上是一个对象实例。Prototype是指如果构造函数有一个原型对象A,那么构造函数创建的所有实例都必须从A中复制,因为实例是从对象A中复制的,所以必须继承A的所有属性、方法等属性,那么,复制是如何实现的呢?方法1:构造复制每个实例都被构造,从原型复制一个实例,新实例占用与原型相同的内存空间。虽然这使得obj1和obj2与它们的原型“完全一致”,但也非常不经济,——内存空间的消耗会迅速增加。图片:

方法二:写时拷贝这个策略来自于一致性欺骗系统的技术:写时拷贝。这种欺骗的一个典型例子是操作系统中的动态链接库(DDL),它的内存区域总是在写入时被复制。图片:

我们只需要指出obj1和obj2在系统中等同于它们的原型,所以在阅读时,我们只需要按照说明来阅读原型。当一个对象(比如obj2)的属性需要写入时,我们会复制一个原型的图像,并使后续操作指向该图像。图片:

这种方法的优点是我们在创建实例和读取属性时不需要大量的内存开销,只是在第一次写的时候用一些代码来分配内存,这就带来了一些代码和内存开销。但是从那以后就没有这样的开销了,因为访问图像的效率和访问原型是一样的。但是,对于频繁写入的系统,这种方法并不比以前的方法更经济。方法3:读取遍历改变了从原型到成员的复制粒度。这种方法的特点是:只有当一个实例的成员被写入时,成员的信息才会被复制到实例映像中。写入对象属性时,如(obj2.value=10),将生成一个名为value的属性值,并将其放入obj2对象的成员列表中。看图片:

可以发现,obj2仍然是原型的引用,在操作过程中没有创建与原型大小相同的对象实例。这样,写操作不会导致大量的内存分配,因此使用内存是经济的。区别在于obj2(和所有对象实例)需要维护一个成员列表。这个成员列表遵循两个规则:确保在读取时首先访问它。如果对象中没有指定属性,尝试遍历对象的整个原型链,直到原型为空或找到属性。我们稍后将在原型链中讨论它。显然,在三种方法中,读遍历是最好的。因此,JavaScript的原型继承是读遍历。构造函数熟悉C语言的人,读完顶层对象的代码后会很困惑。没有class关键字很容易理解。毕竟有函数关键字,但是关键字不一样。但是构造函数呢?其实JavaScript也有类似的构造函数,只是叫构造函数。使用新运算符时,实际上已经调用了构造函数,并且该构造函数被绑定为对象。例如,我们使用以下代码。

复制代码如下:VAR动物=动物('旺旺');

动物将是未定义的。有人会说没有返回值当然是没有定义的。如果更改动物对象的定义:

复制代码如下:功能动物(名称){this。name=name归还这个;}

猜猜现在是什么动物?这时,动物已经变成了一个窗口,但不同的是,窗口被扩展,使窗口具有名称属性。这是因为它默认为window,如果没有指定的话,window是最顶端的变量。只有通过调用new关键字,才能正确调用构造函数。那么,如何避免用户错过新关键词呢?我们可以做一些小的修改:

复制代码如下:函数Animal(name) {if(!(动物的这个实例){返回新的动物(名称);} this.name=name}

那是万无一失的。构造函数对于指示实例属于哪个对象也很有用。我们可以用instanceof来判断,但是instanceof在继承时对祖先和实对象都返回true,不适合。当调用new时,默认情况下,构造函数指向当前对象。

复制代码如下: console . log(animal . prototype . constructor===animal);//真

我们可以换个思路:原型在功能开始时毫无价值,它的实现可能是以下逻辑。

//将__proto__设置为函数的内置成员,并将get_prototyoe()设置为其方法。

复制代码如下:var _ _ proto _ _=null函数get_prototype() { if(!_ _ proto _ _){ _ _ proto _ _=new Object();__原型_ _。构造函数=this} return _ _ proto _ _}

这样做的好处是避免每次声明函数时都创建一个对象实例,并节省开销。构造函数可以修改,这将在后面讨论。相信大家都知道基于原型的遗传是什么,所以没有表现出智商的下限。

JS的继承有几种,这里有两种。

1.方法1该方法最常用,安全性好。我们先定义两个对象。

复制代码如下:功能动物(名称){this。name=name}函数Dog(age){ this . age=age;} var Dog=new Dog(2);

构造继承很简单,将子对象的原型指向父对象的实例(注意它是一个实例,而不是一个对象)。

复制代码如下:dog.prototype=新动物('旺旺');

这时,狗会有两个属性,名字和年龄。如果你对狗使用instanceof运算符。

复制代码如下:console.log(动物的狗实例);//trueconsole.log(dog的dog实例);//false

这实现了继承,但是有一个小问题。

复制代码如下: console . log(dog . prototype . constructor===animal);//true console . log(Dog . prototype . constructor===Dog);//false

我们可以看到构造函数指向的对象发生了变化,这不符合我们的目的,我们无法判断我们新出的实例属于谁。因此,我们可以加一句话:

复制的代码如下: dog . prototype . constructor=dog;

让我们再看一看:

复制代码如下:console.log(动物的狗实例);//falseconsole.log(dog的dog实例);//真

完成.该方法是原型链维护的一部分,将在下面详细描述。2.方法2这种方法有其优点和缺点,但缺点大于优点。先看代码。

复制的代码如下: pername=' code ' class=' JavaScript '函数animal (name) {this。name=name} animal . prototype . setname=function(name){ this . name=name;}函数Dog(age){ this . age=age;}狗.原型=动物.原型;

这样就实现了原型的复制。

这种方法的优点是不需要实例化对象(与方法一相比),节省了资源。缺点很明显,除了和上面一样的问题,就是构造函数指向父对象,只能复制原型中父对象声明的属性和方法。也就是说,在上面的代码中,Animal对象的name属性是不能复制的,但是setName方法是可以复制的。最致命的是,对子对象原型的任何修改都会影响父对象的原型,也就是说,两个对象声明的实例都会受到影响。因此,不建议使用这种方法。

原型链

任何写过继承的人都知道,继承可以多层次继承。在JS中,这是原型链。原型链上面已经提到很多次了,那么什么是原型链呢?一个实例至少应该有一个指向原型的proto属性,这是JavaScript中对象系统的基础。但是这个属性是不可见的,我们称之为“内部原型链”,以区别于“构造器的原型链”(也就是我们通常所说的“原型链”)。我们首先根据上面的代码构建一个简单的继承关系:

复制代码如下:功能动物(名称){this。name=name}函数Dog(age){ this . age=age;} var Animal=new Animal(' wangwang ');狗。原型=动物;var Dog=new Dog(2);

提醒一下,如前所述,所有对象都继承空对象。因此,我们构建了一个原型链:

我们可以看到子对象的原型指向父对象的实例,形成了构造函数的原型链。子对象的内部原型对象也是一个指向父对象的实例,它构成了内部原型链。当我们需要查找属性时,代码类似于。

复制代码如下:函数getattrfromobj (attr,obj) {if(类型为(obj)=' object '){ var proto=obj;while(proto){ if(proto . HasownProperty(attr)){ return proto[attr];} proto=proto。_ _ proto _ _} }返回未定义;}

在这个例子中,如果我们在dog中查找名称属性,它会在dog中的成员列表中找到,但是当然不会找到,因为现在dog的成员列表中只有年龄项。然后它将继续沿着原型链搜索,也就是所指向的实例。proto,即在animal中,找到name属性并返回它。如果你在寻找一个不存在的属性,如果在动物身上找不到,它会继续搜索下去。原型,并找到一个空的对象。如果你找不到,你将继续寻找。原型而。空对象的原型指向null,搜索退出。

原型链的维护我们刚才讲原型继承的时候提出了一个问题。当我们使用方法1构造继承时,子对象实例的构造函数指向父对象。这样做的好处是可以通过构造函数属性访问原型链,缺点很明显。一个对象,它产生的实例应该指向它自己,也就是说。

复制的代码如下: (newobj())。prototype.constructor===obj

然后,当我们重写prototype属性时,子对象生成的实例的构造函数并不指向自身!这违背了构造函数的初衷。我们上面提到了一个解决方案:

复制代码如下:dog.prototype=新动物('旺旺');狗.原型.构造器=狗;

好像没什么问题。但实际上,这带来了一个新的问题,因为我们会发现我们无法追溯原型链,因为我们找不到父对象,而。内部原型链的proto属性不可访问。因此,SpiderMonkey提供了一个改进的方案:一个名为__proto__的属性被添加到任何创建的对象中,它总是指向构造函数使用的原型。这样,任何构造函数的修改都不会影响__proto__的值,所以维护构造函数很方便。

然而,还有两个问题:

__proto__可以重写,这意味着使用它时仍然存在风险。

__proto__是spiderMonkey的特殊处理,不能用于其他引擎(如JScript)。

另一种方法是保留原型的构造函数属性,并在子类构造函数中初始化实例的构造函数属性。

代码如下:覆盖子对象。

复制的代码如下:函数狗(年龄){this。构造函数=参数。被呼叫者;this.age=年龄;} dog . prototype=new Animal(' wangwang ');

这样,子对象的所有实例的构造函数都正确地指向对象,而原型的构造函数指向父对象。虽然这种方法的效率相对较低,但由于每次构造实例都要重写构造函数属性,毫无疑问,这种方法可以有效解决之前的矛盾。针对这种情况,ES5已经完全解决了这个问题:可以随时使用Object.getPrototypeOf()来获取对象的真实原型,而无需访问构造函数或维护外部原型链。因此,寻找上一节提到的对象属性,我们可以重写如下:

复制代码如下:函数getattrfromobj (attr,obj) {if(类型为(obj)==' object '){ do { var proto=object }。getprototype of(狗);if(proto[attr]){ return proto[attr];} } while(proto);}返回未定义;}

当然,这个方法只能在支持ES5的浏览器中使用。为了向后兼容,我们仍然需要考虑前面的方法。比较合适的方法是将这两种方法进行整合打包,相信读者都非常擅长,在这里就不出丑了。

版权声明:JavaScript原型继承详解是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。