当前位置: 首页 > 后端技术 > Node.js

《前端竹节》(3)【原型与对象】

时间:2023-04-03 18:50:30 Node.js

做前端开发有一段时间了,遇到了很多坎。如果要按顺序排列,那么JavaScript原型和对象绝对逃不过TOP3。如果说前端是大海,JavaScript就是大海里的水。一直想写一篇文章来梳理一下这方面,以加深理解,帮助后来者早日出坑,但总觉得缺少合适的切入点,所以读者可以看到清晰的路径而不是生硬的教科书。最近看到一句话“好问题就像厨师的刀,可以帮助你轻松切入现象,直达本质”,所以本文试图通过提问提供一个通俗易懂的角度和应答层。在当今的软件开发中,很少有不是面向对象的,那么JavaScript是如何创建对象的呢?1、创建对象的方法在传统的面向对象编程语言(如:C++、Java等)中,使用关键字class来定义一个类。首先声明一个类,然后通过类实例化一个对象实例。但是在JavaScript中,如果要创建这样一个逻辑对象,需要先定义一个代表类的构造函数,然后使用new操作符执行构造函数来实例化对象。对象字面量varobject1={name:"object1"}constructormethodvarClassMethod=function(){this.name="Class"}varobject2=newClassMethod()//这样创建的对象字面量varobject3=newObject({name:"object3"})这里提到的new操作符,后面会详细介绍。Object.create(proto)创建一个新对象,并使用入参proto对象提供新创建对象的__proto__。也是进入引用对象时新建对象的原型对象。varParent={name:"Parent"}varobject4=Object.create(Parent)要想理解JavaScript原型继承,就必须理解原型对象、实例对象、构造函数、原型链的概念和关系。我会尽力使声明的结构清晰简洁。2.原型继承原型链暂且搁置一旁,先解释一下其他三个概念的来龙去脉。手头有纸笔最好,不用脑子里想象也不复杂。画一个等边三角形,从顶点(1),(2),(3)顺时针编号每个角,其中(1)标记在“原型对象”旁边,(2)构造函数,(3)来自(2)的实例对象构造函数(上例中的ClassMethod)指向(3)实例对象(上例中的object2)并用箭头画一条线。new运算符被标记在行上,这意味着varobject2=newClassName()。画一条带箭头的线,从(2)构造函数指向(1)原型对象。在线上标出了原型,说明构造函数的原型对象等于ClassName.prototype。(函数有一个原型属性,它指向它的原型对象)用箭头从(3)实例对象到(1)原型对象画一条线。线上标上__proto__,说明实例对象的原型对象等于object2.__proto__,结合第4步,就会有ClassName.prototype===object2.__proto__。画一条带箭头的线,从(1)原型对象指向(2)构造函数。构造函数标在线上,说明原型对象的构造函数等于ClassName===object2.__proto__.constructor。关于JavaScript函数和对象的属性,有一个重要的点需要说明:所有对象都有一个指向其原型对象的__proto__属性,所有函数都有一个指向其原型对象的原型属性。函数实际上是一种对象,所以函数有两个原型对象。由于我们通常更关注基于__proto__属性的对象指向的原型对象所形成的原型链,为了区分函数的两个原型,将__proto__指向的原型对象称为隐式原型,而指向的原型对象称为显示原型。看到这里,你应该已经知道什么是原型对象、实例对象、构造函数、原型链了,但是你应该还很疑惑为什么会这样,因为我也曾经这样,用之前的类并将对象、父类和子类的概念与原型和例子进行比较,试图找到一些熟悉的关系,以便于理解。人们总是习惯于对熟悉的事物和类比来理解不熟悉的事物。这可能是一种快速的方法,但绝对不是一种有效的方法。类比总会让我们轻视逻辑推理3、从instanceof看原型链语法格式是objectinstanceofconstructor。从字面上看,instanceof是用来判断对象是否是constructor构造函数实例化的对象。但另外,如果constructor指向的显示原型对象constructor.prototype存在于object的原型链上,结果也为true。字面理解可能有些偏差。请及时查阅MDN文档。原型链是由JavaScript相关对象之间的__proto__属性形成的有向关系链。原型对象的属性和方法可以被它的实例对象使用。(这种有向的父子关系链具有实现类继承的特点)4.newoperatornewFoo()在执行过程中发生了什么?以下三步:创建一个继承自Foo.prototype的新对象。执行构造函数Foo并将this指针绑定到新创建的对象。如果构造函数返回一个对象,这个对象就是new操作符执行的结果;如果没有返回对象,则使用在第一步中创建的新对象。为了直观理解,这里有一个自定义函数myNew来模拟new运算符函数myNew(Foo){vartmp=Object.create(Foo.prototype)varret=Foo.call(tmp)if(typeofret==='object'){returnret}else{returntmp}}5.实现继承在ES6中,出现了一种更直观的语法糖形式:classChildextendsParent{},但这里我们只看没有这种语法糖before是如何实现的。我一直有一个体会:要想快速理解一个东西,就必须了解它的起源和演变。首先定义一个父类Parent,以及它的一个属性名:functionParent(){this.name='parent'}接下来,如何定义一个继承自Parent的子类Child:构造函数方法functionChild(){Parent.call(this)this.type='subClass'//...这里也可以定义一些子类的属性和方法}这种方法的缺陷是:不会继承父类原型链上的属性和方法由子类。原型链方法functionChild(){this.type='subClass'}Child.prototype=newParent()该方法弥补了子类不能继承父类原型链上的属性和方法的缺陷,并且在同时引入了一个新问题:父类上的对象或数组属性将引用传递给子类实例。比如父类上有一个数组属性arr,现在通过newChild()实例化了两个实例对象c1和c2,那么c1对其arr属性的操作也会引起c2.arr的变化,即当然不是我们想要的需要。组合方法(结合1和2方法)functionChild(){Parent.call(this)this.type='subClass'}Child.prototype=newParent()上面的问题虽然解决了,但是看结构就很明显了这里函数执行了两次,显然是多余的。组合优化方法functionChild(){Parent.call(this)this.type='subClass'}Child.prototype=Parent.prototype该方法减少了父类构造函数的冗余调用,但是子类的显示原型会被覆盖。本例通过子类构造函数实例化一个对象:varcObj=newChild(),可以验证实例对象的原型对象,即父类构造函数的显示原型:cObj.__proto__.constructor===Parent,显然这种做法还不够完善。终极方式functionChild(){Parent.call(this)this.type='subClass'}Child.prototype=Object.create(Parent.prototype)Child.prototype.constructor=Child实例对象的__proto__属性值始终为prototype属性实例对象的构造函数。这里有一个关于构造器的从属关系的混淆点。我会尽量用更多的话来说明这一点:你还记得我们上面画的三角形吗?三个角分别代表构造函数、实例对象和原型对象,三个有向边分别代表new、__proto__、prototype。根据__proto__有向边,链是原型链。为了解释构造器的从属关系,我们先给上面画的原型链三角形中的每一个三角形添加一条有向边:从原型对象到构造器,也就是说原型对象有一个构造器属性指向它的构造器,而构造函数的prototype属性指向这个构造函数,所以局部形成了一个有向环。现在一切都协调好了,唯一的问题就是原型链末尾的实例对象构造函数的指针,不管是通过new操作符创建的实例对象的constructor属性还是Object。相同的。所以为了保持一致性,就有了上面这句Child.prototype.constructor=Child,为了知道一个对象是从哪个构造函数实例化的,可以根据obj.__proto__.constructor来获取。多重继承函数Child(){Parent1.call(this)Parent2.call(this)}Child.prototype=Object.create(Parent1.prototype)Object.assign(Child.prototype,Parent2.prototype)Child.prototype.constructor=Child使用Object.assign方法将Parent2原型上的方法复制到Child的原型中。