当前位置: 首页 > 科技观察

如何用原型链实现JS继承?

时间:2023-03-19 16:30:24 科技观察

大家好,我是前端西瓜哥。今天讲一道经典的原型链接面试题。什么是原型链?在JavaScript中,无论何时创建一个对象,都会为该对象提供一个内置对象[[Prototype]]。这个对象就是原型对象,[[Prototype]]的嵌套形成一个原型链。当我们访问一个对象的属性时,如果没有,我们会通过原型链向上追溯,找到第一个拥有该属性的原型对象,并取出对应的值。当然,原型链并不是无穷无尽的。和单向链表一样,最后一个原型对象的值为null。当原型链中的所有对象都找不到指定的属性时,我们将得到undefined。[[Prototype]]虽然不能通过脚本访问,但大多数浏览器都提供了一个__proto__属性来访问这个内置对象,但它不是标准的,也不兼容所有浏览器。下面举几个例子,让读者对原型链有一个直观的认识:当通过对象字面量声明a={}时,a的[[prototype]]就是Object.prototype。此时的原型链为:a->Object.prototype->null。这里有一个容易出错的地方,就是认为a的最后一个原型对象是Object,这是不对的,Object其实只是一个构造函数。声明数组arr=[1,2,4],其原型链为arr->Array.prototype->Object.prototype->null。Object.create(null)甚至可以创建一个没有[[prototype]]的真正的空对象,一般用作字符串哈希表,比如在vue源码中经常可以看到。通过构造函数创建实例对象在JavaScript中,函数在new关键字的配合下成为构造函数。换句话说,任何函数都可以是构造函数。当声明一个构造函数时,它会有一个对象,它的属性名为prototype(与[[prototype]]不同),这个对象就是原型对象。这个对象的构造函数又指向构造函数。当我们使用new关键字创建对象时,创建对象的[[prototype]]会指向这个原型。functionRect(){}constrect=newRect()rect.__proto__===Rect.prototype//trueRect.prototype.constructor===Rect//只要是newRect()创建的对象就为真,不管多少次,它的[[prototype]]都指向Rect.prototype。此外,Rect.prototype.prototype指向Object.prototype。这样,通过在构造函数的原型对象(Rect.prototype)中加入一些方法(如Rect.prototype.draw),创建的多个实例对象可以共享同一个方法,减少内存占用。使用原型链实现继承在了解了构造函数如何影响创建实例的原型链之后,我们来讨论核心问题,如何使用原型链来实现继承。假设我们有一个Shape构造函数(父类)和一个Rect构造函数(子类)。代码如下://父类函数Shape(){}Shape.prototype.draw=function(){console.log('ShapeDraw')}Shape.prototype.clear=function(){console.log('ShapeClear')}//子类函数Rect(){}/**继承代码放这里**/Rect.prototype.draw=function(){console.log('RectDraw')}通过前面的学习,我们知道在正常情况下,使用newRect创建的实例对象,其原型链是这样的:rect->Rect。添加原型对象Shape.prototype。为此,我们需要对Rect.prototype进行特殊处理。方法一:Object.createRect.prototype=Object.create(Shape.prototype)Rect.prototype.constructor=Rect//可选,如果你想使用constructorRect.prototype.constructor=Rect//可选,如果你想使用constructorObject.create(proto)是一个神奇的方法,它创建一个空对象并将其[[prototype]]设置为传递的对象。由于我们无法通过代码为[[prototype]]属性赋值,因此使用了Object.create方法。因为Rect.prototype指向了另外一个新的对象,构造函数丢失了,如果要用可以考虑放回去。缺点是替换了原来的对象。方法二:直接修改[[prototype]]如果你只是不想使用新对象,只想修改原来的对象,可以使用废弃的__proto__属性,但不推荐。不过还有一个方法Object.setPrototypeOf()可以修改对象的[[prototype]],但是因为性能问题不推荐使用。不推荐使用Object.setPrototypeOf(Rect.prototype,Shape.prototype)//或Rect.prototype.__proto__=Shape.prototype,但它们确实有效。方法三:使用父类实例Rect.prototype=newShape()形成的原型链为:rect->shape(替换原来的Rect.prototype)->Shape.prototype->Object.prototype->null基础函数缺点是会产生副作用,即newShap()的执行可能会产生副作用,比如给创建的对象添加一些属性,发送请求等,这都取决于构造函数中的代码.从某种意义上说,这个缺点是致命的。不建议。综上所述,用原型链的方式实现一个JS继承,其实就是希望构造函数Son创建的对象son,将父类Parent.prototype添加到它的原型链中,所以最后就是修改Son的[[prototype]]。.考虑到性能、兼容性、副作用等,推荐使用方法一,即用Object.create(Parent.prototype)指定的[[prototype]]新建一个对象,并替换指向的对象通过原始的Son.prototype。总结几个核心知识点:任何对象都有[[prototype]]属性,当读写对象属性发现当前对象不存在时,会访问[[prototype]]指向的对象尝试访问属性,这样就形成了原型链。当一个函数被创建时,它的原型属性会得到一个原型对象。当一个函数作为构造函数通过new创建一个新对象时,新对象的[[prototype]]会指向原型对象。在JS中实现“类”继承,其实质就是通过处理构造函数的原型对象来修改原型链。