1 前言
写了几篇vue
的源码注释(并不算解析...), 感觉到了对原型的理解是不够的, 在js
中, 原型是非常重要的, 只要你想在js
这座山上往上爬, 它就会嘲笑你, 你把我搞会了么? 如果没有, 它就给你加个十倍重力. 如果搞懂了, 那肯定是能月薪过万, 赢取白富美, 走向人生巅峰的啦~~~
这篇文章讲的都是我自己的理解, 应该是原创的(我有99%
把握, 除非是我之前看过文章记到脑子里了, 没法给到引用了, 联系我可以加上), 但是如果有人借鉴我的这篇文章, 希望给到一个这篇文章的链接. 其实我只是想我的文章能有更多的阅读量, 我想月薪过万, 赢取白富美, 走向人生巅峰~~~
2 前置知识点
2.1 数据类型
js
共有7
种数据类型
从可不可以读取属性, 可以分为两类
可以读取属性:
自身可以有属性: object
自身不可以有属性: string
,number
,boolean
,symbol
不可以读取属性: null
,undefined
null
,undefined
类型, 读取和设置属性都是非法的, 直接报错.
只有object
能有自有属性
, 可以读取属性和设置属性
string
,number
,boolean
,symbol
类型可以读取属性, 其实是先构造成包装对象
, 再读取属性, 设置属性也是一样, 可以理解设置到了会立即销毁的包装对象
上, 就是可以设置, 但是没有任何实质效果.
2.2 判断是否是自身属性(hasOwnProperty)
hasOwnProperty
方法是继承来的, 用来判断该对象自身上是否有这个属性, 有就行, 不管是什么值
constobj={a:1}consto=Object.create(obj)o.b=1o.c=void0console.log('a',o.a,o.hasOwnProperty('a'))//可以读取到值,继承而来,但不是自身属性console.log('b',o.b,o.hasOwnProperty('b'))//可以读取到值,自身属性console.log('c',o.c,o.hasOwnProperty('c'))//读取到undefined,自身属性console.log('d',o.d,o.hasOwnProperty('d'))//读取到undefined,不是自身属性,也没有继承到这个属性
3 一点小思考
程序就是数据结构与算法, 好的程序最好是使用最小的内存存储数据, 使用最快的时间完成运行得到结果.
复用数据可以达到减少内存使用的目的, 例如a
和b
需要完成一样的功能, 就可以复用同一个方法(属性).
那么就需要解决一个问题, 这个复用的方法存在哪里, a
和b
怎样能找到它.
在js
中的解决方案是, a
和b
都由函数(这里先叫Function
吧)构造而来, 复用的方法存放在函数身上(prototype
属性里).
(因为只有构造函数身上需要存放复用的方法, 所以prototype
只有可构造的函数上才有, 箭头函数不是用来构造的, 它就没有, 其它对象, 如果连函数都不是, 就更不会有这个属性了)
那么需要给a
,b
和Function
建立起联系, 因为a
,b
需要到Function
身上找它们可以用的复用方法
在js
中的实现是通过constructor
属性,即a.constructor
, b.constructor
可以找到Function
所以通过a.constructor.prototype
可以找到它可以复用的方法的存放地址, 为了快速找到js
提供了一种快捷方法a.__proto__
一步到位找到, 即a.constructor.prototype
和a.__proto__
找到的是同一个对象, 当然它俩是全等的啦.
//它俩都不是自有属性,我也不知道怎么从这俩属性上找到原型对象的了,肯定是魔法.....constobj={}console.log(obj.hasOwnProperty('__proto__'))//falseconsole.log(obj.hasOwnProperty('constructor'))//false
(所以, 如果手动修改了constructor
,prototype
,__proto__
的指向, 那么你得清楚你在干什么)
(我不知道js
的设计者是不是这样想的, 哈哈, 我就这样认为, 这样好理解多了)
(这个过程称之为继承, 而且是一个链式过程, 即可以a.constructor.prototype.constructor.prototype.constructor.prototype
这样查找, 直到找到最顶端, 这个过程可以由a.__proto__.__proto__.__proto__
加速, 这个就叫做原型链, js
的继承只有这一种实现方式.)
(上面只是引导思考过程, 其实查找原型对象并不会通过a.constructor.prototype
去找, 而是直接通过__proto__
查找)
3.1 修改 constructor
constDog=function(){}constdog=newDog()dog.constructor=0console.log(dog.hasOwnProperty('constructor'))//trueconsole.log(dog.constructor)//0console.log(dog.__proto__.constructor)//[Function:Dog]
总结, 修改了这个属性, 增加了找到构造它的构造函数的难度, 不能直接获取了, 需要到原型对象上去读取.
如果它自身的这个属性和原型上的这个属性都被修改了, 那么也只是找不到它的构造函数了而已, 不会有别的影响.
3.1.1 instanceof
instanceof
关心的是原型链, 跟constructor
没有关系
印证上面的点, 修改constructor
属性, 除了让实例找不到构造它的构造函数, 没有别的影响了. 如果需要通过实例找到它的构造函数, 就需要维护好它俩的关系.
//语法是//ainstanceofb//这个操作符是判断a的原型链上是否有b.prototype,因为要判断b.prototype所以b必需是一个可构造的函数,否则会报错constfn=function(){}consto=Object.create(fn.prototype)//此时o的原型链上有fn.prototype,因为o.__proto__===fn.prototypeconsole.log(oinstanceoffn)//trueconstemptyObj={}fn.prototype=emptyObj//此时o的原型链上已经没有fn.prototype了,因为此时o.__proto__已经不再和fn.prototype相等了console.log(oinstanceoffn)//falseo.__proto__=emptyObj//修正了o.__proto__就好了console.log(oinstanceoffn)//true
3.1.2 isPrototypeOf
现在有个新的api
, 实现的功能和instanceof
一致, 但是更加语义化一些, 直接判断对象是否在另一个对象的原型链上
constfn=function(){}consto=Object.create(fn.prototype)console.log(fn.prototype.isPrototypeOf(o))//true
3.2 修改__proto__
|prototype
先说一个总结, 在构造实例的时候, 会将这个实例的__proto__
指向此时的构造函数的prototype
, 然后实例实际是继承的是__proto__
.(为什么强调此时, 因为构造函数的prototype
可能会被修改指向, 修改之后只会影响修改之后构造的实例, 修改之前构造的实例还会使用修改之前的prototype
)
所以, 就可以理解到修改__proto__
和prototype
会有哪些影响了
修改__proto__
的指向
只会影响它自己的继承
constDog=function(){}constdog=newDog()constd=newDog()Dog.prototype.name='Dog'dog.__proto__={name:'__proto__',}console.log(d.name)//Dogconsole.log(dog.name)//__proto__
修改__proto__
的属性
会影响这一波段构造的实例
constDog=function(){}constdog=newDog()constd=newDog()Dog.prototype.name='Dog'console.log(d.name)//Dogconsole.log(dog.name)//DogDog.prototype={name:'after',}constdog1=newDog()constd1=newDog()console.log(d1.name)//afterconsole.log(dog1.name)//afterdog1.__proto__.name='__proto__'//可以看到只影响了当前这一段构造的实例,之前和之后的都不会被影响到,因为这一段内的是同一个Dog.prototype,它们的__proto__都是指向它的console.log(d1.name)//__proto__console.log(dog1.name)//__proto__Dog.prototype={name:'new',}constdog2=newDog()constd2=newDog()console.log(d2.name)//newconsole.log(dog2.name)//new
修改prototype
的指向
会影响这一波段构造的实例
修改prototype
的属性
会影响这一波段构造的实例, 同修改
__proto__
的属性
4 修改和获取原型对象的方式
4.1 修改
上面已经讲了修改
prototype
和__proto__
4.1.1 Object.create
constobj={name:'objName',}consto=Object.create(obj)//它相当于o.__proto__=obj,但是推荐使用`Object.create`console.log(o.name)//objNameconsole.log(o.__proto__===obj)//true
4.1.2 Object.setPrototypeOf
constobj={name:'objName',}consto={}Object.setPrototypeOf(o,obj)//它相当于o.__proto__=obj,但是推荐使用`Object.setPrototypeOf`constproto=Object.getPrototypeOf(o)console.log(proto===obj&&proto===o.__proto__)//trueconstobj1={}o.__proto__=obj1constproto1=Object.getPrototypeOf(o)console.log(proto1===obj1&&proto1===o.__proto__)//true
总结, 在什么时候使用Object.create
, 在什么时候使用Object.setPrototypeOf
呢, 首先它俩都是标准api
, 都是建议使用的, 在创建对象的时候就要指定原型时使用Object.create
, 需要动态修改原型对象时, 使用Object.setPrototypeOf
4.2 获取
之前已经讲了, 通过 constructor.prototype
和__proto__
获取了
4.2.1 Object.getPrototypeOf
constobj={name:'objName',}consto={}Object.setPrototypeOf(o,obj)constproto=Object.getPrototypeOf(o)console.log(proto===obj&&proto===o.__proto__)//true
5 js 内置原生构造函数
这些原生的构造函数的prototype
属性是不可写, 不可枚举, 不可配置的
//它俩都不是自有属性,我也不知道怎么从这俩属性上找到原型对象的了,肯定是魔法.....constobj={}console.log(obj.hasOwnProperty('__proto__'))//falseconsole.log(obj.hasOwnProperty('constructor'))//false0
5.1 js 继承的最顶端是什么
null, 必须是这家伙, 不然只能无限套娃了
然后其它所有对象都是从Object
构造而来, 所以所有的对象都可以继承到Object.prototype
.
//它俩都不是自有属性,我也不知道怎么从这俩属性上找到原型对象的了,肯定是魔法.....constobj={}console.log(obj.hasOwnProperty('__proto__'))//falseconsole.log(obj.hasOwnProperty('constructor'))//false1
5.2 js 继承的二等公民(Function)
在上面的小思考中, 说到, js
对象都是函数构造而来, 所以包括Object
也是由Function
构造来的, 甚至它自己都是由自己构造而来
//它俩都不是自有属性,我也不知道怎么从这俩属性上找到原型对象的了,肯定是魔法.....constobj={}console.log(obj.hasOwnProperty('__proto__'))//falseconsole.log(obj.hasOwnProperty('constructor'))//false2
我再来一点小理解, 可能是在js
内部做了小处理, 第一个Function
是凭空变出来的.... 然后这个Function
构造出了Object
, 然后这个Object
构造出了第一个原型对象Object.prototype
, 然后再去修改一些引用关系.
其实最复杂的是Object
和Function
的关系
//它俩都不是自有属性,我也不知道怎么从这俩属性上找到原型对象的了,肯定是魔法.....constobj={}console.log(obj.hasOwnProperty('__proto__'))//falseconsole.log(obj.hasOwnProperty('constructor'))//false3
5.3 js 继承的三等公民(内置的其他构造函数)
//它俩都不是自有属性,我也不知道怎么从这俩属性上找到原型对象的了,肯定是魔法.....constobj={}console.log(obj.hasOwnProperty('__proto__'))//falseconsole.log(obj.hasOwnProperty('constructor'))//false4
6 用户定义的特定公民构造函数
这个才是重点, 根据上面的理解, 我会再开一篇文章写一下我理解的js
的继承, 这里就先留个坑
7. 总结
这篇文章跟网上大多讲constructor
,prototype
,__proto__
的文章都有所不同, 我的立足点是从给定的一个可以读取属性的值开始, 在js
中, 除了null
和undefined
, 其它所有的值都可以成为立足点. 从这个立足点开始, 它的__proto__
属性记录了它的原型对象, 这个原型对象是构造它时, 它的构造函数的prototype
属性的值。
//它俩都不是自有属性,我也不知道怎么从这俩属性上找到原型对象的了,肯定是魔法.....constobj={}console.log(obj.hasOwnProperty('__proto__'))//falseconsole.log(obj.hasOwnProperty('constructor'))//false5
读取一个值的属性的值时, 如果它自身有这个属性, 那么直接返回这个属性的值, 否则就会到它的__proto__
对象上去找, 一直递归下去, 直到找到顶部null
, 找到就返回它的值, 没找到就返回undefined
这篇文章有三个理解点,让我茅塞顿开, 都是在我试验了好久突然得到的结论:
以一个值为立足点开始分析
在构造实例的时候, 会将这个实例__proto__
指向此时的构造函数的prototype
查找原型对象时, 以__proto__
为准