前言
这篇文章知识点有一点杂,包含了:
我们日常前端常用的三个内置类 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。
练习题
function Foo() { getName = function() { console.log(1); } return this;}Foo.getName = function() { console.log(2);}Foo.prototype.getName = function() { console.log(3);}var getName = function() { console.log(4);}function getName() { console.log(5);}Foo.getName();getName();Foo().getName();getName();new Foo.getName();new Foo().getName();new new Foo().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
new Foo.getName() ,new Foo().getName() 执行,由于优先级的不同,这二者执行顺序有很大差别。new Foo.getName() => new (Foo.getName()),new Foo().getName() => (new Foo()).getName()。
这里主要注意这二者执行是有区别的:new Foo ,没有参数 new。new Foo() 有参数 new。都是把 Foo 执行,创造出 Foo 类的实例,但是二者执行的优先级不同,有参数 18 ,无参数 17。成员访问 18。
new (Foo.getName()) 先找 Foo 对象上的 getName 执行,然后结果 new ,所以结果就是 「2」。
(new Foo()).getName() 创建 Foo 的实例,然后在执行 getName 方法。实例本身私有属性没有 getName 方法,通过原型链进行查询,Foo.prototype 存在 getName,执行,结果为 「3」。
new Foo.getName(); => 2new Foo().getName(); => 3
new new Foo().getName() 执行,这一行代码其实就是上面二者的结合体。new new Foo().getName() => new 实例.getName() => new 结果。最后结果就是 「3」。
new new Foo().getName(); => 3
面向对象的细节知识点
Object.prototype.hasOwnProperty
用来检测属性是否为私有属性,是否为私有属性其实是一个相对的概念,自己堆内存中的属性是私有属性,基于原型链(proto)查找的是「相对自己」的公有属性。举个例子:
let arr = [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(); => 21let obj = { toString() {} };console.log(obj.hasPublishProperty('toString')); => false
那如果正确的实现了。想要确认一个属性是公有属性,但是又不能在本身私有中查询,唯一的办法就是跳过私有属性的查找,直接去查询公有属性看是否存在。可以通过 Object.getPrototypeOf 跳过私有属性。跳过私有属性,想要在原型链上查询,但是查询的属性我们并不能确认具体在原型链上的什么位置,这时就需要慢慢一层一层查询,直到找到为止。
Foo.getName(); => 23
其实还有一种更简单的写法:
Foo.getName(); => 24
进阶知识
实现 new
Foo.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
创建带有原型指向的空对象
对于第一步创建空对象,并将原型指向构造函数的原型,我们有多种实现方式。
proto
Foo.getName(); => 28
proto在很多浏览器,例如 IE (除开 EDGE) 都是不允许访问的,所以存在兼容性问题。
Object.setPrototypeOf
Foo.getName(); => 29
Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null。setPrototypeOf 的兼容性相对来说比较好一些。
警告: 由于现代 JavaScript 引擎优化属性访问所带来的特性的关系,更改对象的 [[Prototype]]在各个浏览器和 JavaScript 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于 obj.proto = ... 语句上的时间花费,而且可能会延伸到任何代码,那些可以访问任何[[Prototype]]已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的 [[Prototype]]。相反,你应该使用 Object.create()来创建带有你想要的[[Prototype]]的新对象。
Object.create
getName(); => 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