前端基石:面向对象的细节知识(较长)

大哥们,请问下,前端基石:面向对象的细节知识(较长)
最新回答
海枯鱼亡

2024-11-05 00:24:50

前言

这篇文章知识点有一点杂,包含了:

我们日常前端常用的三个内置类Array、Function、Object,以及它们之前的关系,并用一个练习题来加深理解。

面向对象的细节知识点hasOwnProperty、in,以及如何实现一个方法来检查属性是否是「公有属性」。

进阶知识手写new原理实现和细节需要注意的地方。

全文都是和面向对象有关。有兴趣的同学可以深入了解,当然这不是全部的面向对象知识点,我会在后面的文章慢慢为大家带来更多的面向对象的知识,并且如果大家觉得自己的基础比较薄弱,也看看我之前「前端基石」的文章,都是前端的基础知识。

传送门

前端基石:JS中的9大数据类型和数据类型转换

前端基石:Stack、Heap

前端基石:函数的底层执行机制

前端基石:预处理机制,变量提升

前端基石:闭包

前端基石:高阶函数之柯里化、组合函数、惰性思想

前端基石:一段代码隐含了多少基础知识?

前端基石:this的几种基本情况

前端基石:构造函数和普通函数

三个常见的内置类Array

Array内置构造函数

把Array函数作为「普通对象」,设置的「私有静态属性和方法」和实例是没有关系的,一般都会编写一些工具类方法,例如:Array.from、Array.isArray、Array.of、Array.of...

Array.prototype原型

把Array当做「构造函数」,在其「原型对象」上设置的公有属性和方法,是供实例调用的,例如:array.xxx()或者Array.prototype.xxx()。

Function

Function.prototype是一个匿名函数,而其余构造函数的原型对象都是普通函数,虽然说是个函数,但是和其他原型对象操作相同。并且所有函数「普通函数、自定义函数、内置构造函数、箭头函数」都是Function内置类的实例。

Object

所有对象「普通对象、函数对象、特殊对象、实例对象、原型对象」都是Object内置类实例。

三者的关系

Function.prototype===Function.proto:Function(函数)是Function类的实例

Function.proto.proto===Object.prototype:Function(对象)是Object类的实例

Function.prototype.proto===Object.prototype:Function.prototype(原型对象)是Object类的实例

Object.proto===Function.prototype:Object(函数)是Function类的实例

Object.proto.proto===Object.prototype:Object(对象)是Object类的实例

Array.proto.proto===Object.prototype:Array(对象)是Object类的实例

...

不管怎么样,最后都会找到Object.prototype,Object是所有对象类型的「基类」,所有对象的proto最后都会止步于Object.prototype。

练习题functionFoo(){getName=function(){console.log(1);}returnthis;}Foo.getName=function(){console.log(2);}Foo.prototype.getName=function(){console.log(3);}vargetName=function(){console.log(4);}functiongetName(){console.log(5);}Foo.getName();getName();Foo().getName();getName();newFoo.getName();newFoo().getName();newnewFoo().getName();

遇到这样的题目,个人感觉就是在脑袋开始构想脑图,理清楚之间的链接和关系。根据上诉代码「初始化」完成脑图如下,这里有两个点需要注意一下:

Foo.getName=xxx把Foo当做普通对象,往对象上设置私有属性或者方法

Foo.prototype.getName=xxx把Foo当做构造函数,往其原型对象上设置一些共有属性和方法

根据脑图,开始执行代码「Foo.getName()」将Foo当做普通对象,查询Foo的私有属性以及原型链上的getName方法,发现私有属性本身就存在getName属性,这样就不需要在原型链上进行查询了,输出结果「2」。

Foo.getName();=>2

getName()执行,方法前面没有任何的修饰,那就执行全局的getName,EC(G)中查询是否有方法getName,发现存在全局方法,执行输出结果「4」。

getName();=>4

Foo().getName()执行,先把Foo当做普通方法执行,然后调用返回结果的getName。Foo方法进栈执行,按照常规的步骤走:

进栈执行

初始化作用域链

初始值this

形参赋值

变量提升

代码执行:这里getName为全局的方法,会覆盖之前的全局getName变量。返回this,Foo函数内部的this是window。Foo().getName()相当于执行window.getName,所以结果就是输出「1」。

Foo().getName();=>1

getName()执行,方法前面没有任何的修饰,那就执行全局的getName,EC(G)中查询是否有方法getName,发现存在全局方法,并且在上一步getName被重新赋值,执行输出结果「1」。

getName();=>1

newFoo.getName(),newFoo().getName()执行,由于优先级的不同,这二者执行顺序有很大差别。newFoo.getName()=>new(Foo.getName()),newFoo().getName()=>(newFoo()).getName()。

这里主要注意这二者执行是有区别的:newFoo,没有参数new。newFoo()有参数new。都是把Foo执行,创造出Foo类的实例,但是二者执行的优先级不同,有参数18,无参数17。成员访问18。

new(Foo.getName())先找Foo对象上的getName执行,然后结果new,所以结果就是「2」。

(newFoo()).getName()创建Foo的实例,然后在执行getName方法。实例本身私有属性没有getName方法,通过原型链进行查询,Foo.prototype存在getName,执行,结果为「3」。

newFoo.getName();=>2newFoo().getName();=>3

newnewFoo().getName()执行,这一行代码其实就是上面二者的结合体。newnewFoo().getName()=>new实例.getName()=>new结果。最后结果就是「3」。

newnewFoo().getName();=>3

面向对象的细节知识点Object.prototype.hasOwnProperty

用来检测属性是否为私有属性,是否为私有属性其实是一个相对的概念,自己堆内存中的属性是私有属性,基于原型链(proto)查找的是「相对自己」的公有属性。举个例子:

letarr=[10,20];arr.push(30);

当在执行push方法时:会先查询自己私有属性是否有push这个方法,发现arr对象上没有push方法,则默认基于proto进行查找,第一个查找的就是Array.prototype,发现存在push方法,将push方法执行,在arr末尾添加新的内容30,然后让数组长度+1,返回新的数组长度。这是push内部的执行策略。如果我们使用Object.prototype.hasOwnProperty来检测某个对象上push属性是否存在,就会用到「私有属性相对」的特性。

arr.hasOwnProperty('push')=>false

对于push在arr对象上不存在,则默认基于proto进行查找,第一个查找的就是Array.prototype,发现存在,push相对于arr来说是「公有属性」。

Array.prototype.hasOwnProperty('push')=>true

push在Array.prototype存在,不需要在通过原型链进行查找,push相对于Array.prototype是私有属性,因为就在Array.prototype这个对象上。

in

检查属性是否属于某个对象,不管公有还是私有。只要能访问到这个属性,结果都是真值。

检查某个属性是否为对象的公有属性?

这里之前看群里有一道和面向对象相关联的基础面试题,「在Object.prototype.xxx方法来检查某个属性是否为对象的公有属性?」(这里假设xxx是hasPublishProperty)

Foo.getName();=>20

根据上面我们提到的Object.prototype.hasOwnProperty用来检测属性是否为私有属性,in检查属性是否属于某个对象,不管有还是私有。所以我们很容易想到一个实现方式是:

Foo.getName();=>21

是对象的属性,但是不是私有属性,哪肯定就是公有属性。这样其实按照这种思路是没有什么问题?但是有一种情况,如果attr既是私有属性,也是公有属性,这种方法就行不通了。例如:我们检查的属性「toString」既在原型公有上存在,自己私有也存在,这种情况,上面的实现就有一点问题了。

Foo.getName();=>21letobj={toString(){}};console.log(obj.hasPublishProperty('toString'));=>false

那如果正确的实现了。想要确认一个属性是公有属性,但是又不能在本身私有中查询,唯一的办法就是跳过私有属性的查找,直接去查询公有属性看是否存在。可以通过Object.getPrototypeOf跳过私有属性。跳过私有属性,想要在原型链上查询,但是查询的属性我们并不能确认具体在原型链上的什么位置,这时就需要慢慢一层一层查询,直到找到为止。

Foo.getName();=>23

其实还有一种更简单的写法:

Foo.getName();=>24

进阶知识实现newFoo.getName();=>25

据说是阿里的一道面试题。

这里简单来说就是需要自己手动实现一个new方法。手动实现的new方法能和正常的new功能保持一致。要想自己手动实现一个new,就先自己知道new本身的一个实现机制。在之前的文章中有讲到过,关于「构造函数和普通函数的区别」。

普通函数执行:

私有上下文进栈执行

初始化变量对象

初始化作用域链

初始化this

初始化arguments

形参赋值

变量提升

代码执行...

函数执行完成出栈

构造函数执行:

私有上下午进栈执行

初始化变量对象

在构造函数执行,初始化作用域链之前,浏览器会默认先创建一个对象(空对象,实例对象)

初始化作用域链

初始化this,这里的初始化this,会将this指向第三步创建的对象,所以在后期代码中执行this.xx=xx的时候,实际上就是在往这个对象(实例对象)上添加属性或者方法,这里还需要注意下,构造函数执行,函数内部的私有变量和实例是没有关系的。

初始化arguments

形参赋值

变量提升

代码执行

函数执行完,出栈之前,会查看构造函数本身的返回结果:

如果有「return对象」,则以返回值为主,变量就会等于这个返回值对象。

如果没有返回值或者返回的是一个原始值,则浏览器默认会将创建的实例返回,变量就会等于这个实例对象

需要自己实现new其实本身也是根据这三点不同来一步一步实现。

需要创建一个当前类Ctor的一个对象(空对象,实例对象),并将原型指向构造函数的原型

初始化this,this指向创建的实例对象,函数执行(这里注意是把构造函数当做普通函数执行)

函数执行并返回,如果返回的是对象就以返回的为主,如果没有返回这个返回的是原始值,就把创建的实例对象返回

Foo.getName();=>26

这样就基本实现了一个new方法,但是存在一些小的问题,我们在正常情况下使用new时,并不是所有的函数都可以被new的,箭头函数、Sybmol或者BigInt。

所以我们在自己实现_new方法时,也需要排除这些情况,确保和正常的new功能一样。

Foo.getName();=>27

创建带有原型指向的空对象

对于第一步创建空对象,并将原型指向构造函数的原型,我们有多种实现方式。

protoFoo.getName();=>28

proto在很多浏览器,例如IE(除开EDGE)都是不允许访问的,所以存在兼容性问题。

Object.setPrototypeOfFoo.getName();=>29

Object.setPrototypeOf()方法设置一个指定的对象的原型(即,内部[[Prototype]]属性)到另一个对象或null。setPrototypeOf的兼容性相对来说比较好一些。

警告:由于现代JavaScript引擎优化属性访问所带来的特性的关系,更改对象的[[Prototype]]在各个浏览器和JavaScript引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于obj.proto=...语句上的时间花费,而且可能会延伸到任何代码,那些可以访问任何[[Prototype]]已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的[[Prototype]]。相反,你应该使用Object.create()来创建带有你想要的[[Prototype]]的新对象。

Object.creategetName();=>40

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。Object.create的兼容性相对上面两种方式是最好的。

总结

面向对象在前端是一个比较大的知识点,包含的细节也比较多,这里一篇文章是讲不完的,如果大家对面向对象感兴趣,可以关注我接下来的「前端基石」文章,如果大家前端基础感兴趣,可以看我之前的「前端基石」文章。如果你觉得这篇文章对你有帮助,帮忙点个赞,谢谢。

参考

https://blog.csdn.net/Lele___/article/details/113339612

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create

https://www.javascriptpeixun.cn/course/3797/task/255775/show

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf

https://blog.csdn.net/lyt_angularjs/article/details/86623988

原文:https://juejin.cn/post/7101584781334462471