当前位置: 首页 > Web前端 > JavaScript

这一次彻底理解了JavaScript中的原型和原型链

时间:2023-03-27 15:40:48 JavaScript

,曾经以为很遥远的2022,一眨眼就到了。从大学毕业至今,从事前端开发行业五年。在日常工作中基本掌握了业务需求的开发,但总觉得还有很大的提升空间。每三到五年就会出现一个向上的瓶颈。一个优秀的前端工程师不仅可以高效的完成页面的开发,还可以掌握和实践一系列的前端工程技术,包括脚手架和项目脚本、测试系统、监控系统、项目规范、项目构建和打包、项目部署和运维等。不仅会做项目,还要有足够的经验和方案来做好。具体可以是性能优化,也可以是技术方面。长列表优化、加载性能优化、提高项目可维护性等性能优化。微前端、服务端渲染、跨端开发等技术方面,当你感到迷茫时,你需要做的是及时调整心态,理清思路,回过头来反思一下自己的过去,让你的能力得到进一步的提升。以前在零散的上下班时间看一些公众号的推文,常常觉得自己的基础知识不够,掌握的前端开发知识也不系统。深入理解JavaScript系列文章,本博客为系列第一篇,从JavaScript的原型和原型链入手。JavaScript面向对象学过Java的同学应该都知道,面向对象语言具有三大特点:封装、继承、多态。但是JavaScript并不是严格意义上的面向对象编程语言,它是一种基于原型的语言,可以通过原型实现继承。例如:functionPerson(name,age){this.name=namethis.age=age}Person.prototype.language='chinese'Person.prototype.sayName=function(){console.log('我的名字是'+this.name)}Person.prototype.sayAge=function(){console.log('我的年龄是'+this.age)}Person.prototype.getLanguage=function(){console.log(this.language)}letfoo=newPerson('foo',25)console.log(foo.sayName())//我的名字是fooconsole.log(foo.sayAge())//我的年龄是25console.log(foo.getLanguage())//english从这个例子我们可以看出构造函数Person中没有sayName方法和sayAge方法,但是foo实例对象却成功调用了这两个方法,这是为什么呢?这是因为它从原型对象继承了这两个方法。通过这个例子,我们也引出了今天要讨论的话题。JavaScript中原型、实例和构造函数之间的内在联系是什么?Prototypeprototype每个函数都有一个prototype属性,指向这个函数创建的实例对象的原型,实例对象会继承这个原型的属性,也就是说原型上的属性和方法会被所有实例共享对象。那么什么是原型呢?Prototype可以理解为当一个实例对象被创建时,就会有一个对象与之关联。这个对象就是我们常说的原型。构造函数和原型之间的关系可以用下图表示:实例和原型是如何连接的?__proto__属性每个实例对象都有一个__proto__私有属性,它指向构造函数的原型对象,也就是说实例对象通过__proto__属性链接到原型。让我们完成实例、构造函数和原型之间的关系图:需要注意的是,__proto__属性从未包含在ECMAScript语言规范中,但所有现代浏览器都实现了它。__proto__属性已经在ECMAScript6语言规范中进行了标准化,以确保与Web浏览器的兼容性,因此将来会支持它,但已弃用。现在更推荐使用Object.getPrototypeOf/Reflect.getPrototypeOf和Object.setPrototypeOf/Reflect.setPrototypeOf。__proto__属性是继承自Object.prototype的访问器属性,暴露了对象的内部[[Prototype]]。如果一个对象设置了其他.__proto__属性,它将覆盖原来的构造函数原型对象。可以这样理解,如果改变了对象的__proto__属性,那么原型链就会改变。下面将讨论原型链。如上所述,每个实例对象都会有一个__proto__属性。对于以不同方式创建的对象,它们的__proto__指向什么?对象字面量创建对象letperson={name:'tom',age:22}从控制台输出可以看出对象字面量构造的对象有__proto__指向Object.prototype,这里我们也可以知道Object也是一个构造函数。构造函数创建对象functionPerson(){}letp=newPerson()上述形式创建对象的方式是通过构造函数来创建对象,这里的构造函数就是Person函数。上面说了构造函数创建的对象,它的__proto__指向构造函数的prototype属性指向的对象,也就是构造函数的原型对象。Object.create创建一个对象letperson={name:'tom',age:22}constsubPerson=Object.create(person);可以看出Object.create创建的对象subPerson的__proto__属性指向person,见下面Object.create的polyfill代码:if(typeofObject.create!=="function"){Object.create=function(proto,propertiesObject){if(typeofproto!=='object'&&typeofproto!=='function'){thrownewTypeError('ObjectprototypemayonlybeanObject:'+proto);}}elseif(proto===null){thrownewError("这个浏览器的Object.create实现是一个shim,不支持'null'作为第一个参数。");}if(typeofpropertiesObject!=='undefined')thrownewError("Thisbrowser'simplementationofObject.createisashimanddoesn'tsupportthesecondargument.");函数F(){}F.prototype=proto;返回新的F();};}通过这段polyfill代码,就不难理解为什么上面例子中subPerson的__proto__属性指向person了。既然实例对象有指向原型对象的__proto__属性,构造函数有指向原型对象的原型属性,那么原型对象有没有指向实例和构造函数的属性呢?有指向构造函数的,也就是构造函数,没有指向实例对象的,因为构造函数可以创建很多实例对象。自然不存在与原型对象具有一对一关系的属性,而是每个实例对象都从原型继承属性和方法。constructor因为原型对象可以通过constructor属性指向构造器,实际上这个属性是通过原型链继承自Object.prototype的。进一步完善实例对象、构造函数和原型对象的关系图:实例对象和原型从文章开头的例子我们了解到,实例对象会从其构造函数的原型对象继承属性和方法。比如foo对象调用getLanguage方法时,会获取并打印出该对象的language属性,但是foo对象上并没有设置language属性,所以会在与其关联的prototype对象上查找,即foo.__proto__===Person.prototype对象,这个对象刚好有这个属性,属性值为“chinese”,所以打印出来是正确的。但是如果实例对象的构造函数的原型没有这个属性呢?然后会去prototype的prototype对象中搜索,直到找到该属性。原型链既然构造函数的原型也是一个对象,那么它是由哪个构造函数创建的呢?它的__proto__属性指向谁?从上面说的三种对象创建方法来看,对象字面量创建的对象是由最原始的Object构造函数创建的。其实构造函数的原型对象也是由它创建的。原型的__proto__属性自然指向Object.prototype,我们进一步完善实例对象、构造函数和原型的关系图:Object.prototype也是一个对象,那么它的__proto__属性指向谁呢?空,是的,它是空的。从foo实例对象通过__proto__属性延伸到null的链式结构就是原型链。