原型与原型链
原型的具体例子
上面这个数组,明明没有 concat 这个属性方法,但是,arr 却可以调用它,这是为什么?
在控制台打印后发现发现了一个 prototype 属性,展开后发现了 concat 这个属性。而 prototype 就是我们今天要讨论的原型。
引用类型和原型
所有引用类型都有原型
看了上面的例子,难道只有数组有原型吗?显然并不是,所有的引用类型都有一个隐式原型 __proto__ 属性,属性值是一个对象。
const obj = {};
const arr = [];
const fn = function () {}
console.log('obj.__proto__', obj.__proto__);
console.log('arr.__proto__', arr.__proto__);
console.log('fn.__proto__', fn.__proto__);
__proto__ 和 prototype 的区别
读到这里,你一定想知道
__proto__和prototype有什么区别?
解他们之间的区别之前,我们先了解一下他们各自是什么。
什么是__proto__和prototype
什么是 __proto__
对象具有属性 __proto__ ,可称为隐式原型,一个对象的隐式原型指向构造该对象的构造函数的原型。
什么是prototype
原型属性(prototype) ,这个属性是一个指针,指向一个对象,这个对象的用途就是包含所有实例共享的属性和方法(我们把这个对象叫做原型对象)。原型对象也有一个属性,叫做constructor,这个属性包含了一个指针,指回原构造函数。
那么他们之间的区别就显而易见了,其实 隐式原型
__proto__的属性值指向它的构造函数的显式原型prototype属性值,而构造函数的显示原型prototype则是指向公共方法。
const obj = {};
const arr = [];
const fn = function () {}
console.log( obj.__proto__ === Object.prototype) // true
console.log( arr.__proto__ === Array.prototype) // true
console.log( fn.__proto__ === Function.prototype) //true
函数特有prototype属性
为什么我们在上面强调的是,构造函数的显式原型 prototype 属性值
原因是:
函数特有prototype属性
这句话的意思是,只有函数才有 prototype 属性,对象是没有这个属性的。
函数有多个长辈
可以看到,hd 是 User 创建的一个实例对象,hd 的显示原型是等于 User 的隐式原型的。并且,hd 能调用的是 User.prototype , User 能调用的是 User.__proto__。
原型中的constructor引用
上面其实我们提到了一点,就是 prototype 对象中有一个属性 constructor 这个属性包含了一个指针,指回构造函数。
其实最开始的图已经表现的非常清楚了:
我们可以根据 Foo.prototype 找到 Foo 所定义的原型,又可以根据原型中的 constructor 找到方法所对应的构造函数。
总结
总结一下吧:
- 对象有属性__proto__,指向该对象的构造函数的原型对象。
- 方法除了有属性__proto__,还有属性prototype,prototype指向该方法的原型对象。
__proto__和prototype的更详细的区别请参考:zhuanlan.zhihu.com/p/92894937
方法在原型中的调用
当我们试图调用某一个对象的属性或者方法的时候,如果在本身的对象中并没有找到,那么它会去它的隐式原型 __proto__(也就是它的构造函数的显式原型 prototype)中寻找。
这其实就解释了我们一开始举的例子。
arr 并没有 concat 这个属性方法,但是他的父级原型 Array 有这个方法,因此,arr 顺着原型向上找,最终找到并调用了 concat 这个函数。
没有原型的对象也是存在的
let obj = Object.create(null,{
name:{
value:'obj'
}
})
console.log(obj)
上述例子,我们手动的设置了 obj 这个对象的原型为 null ,此时, obj 就成为了一个完全的数据字面量对象。
原型方法和对象方法的优先级
从上图可以看出,obj 这个对象只有 render 这一个函数,他的父辈原型中有 render 和 show 函数。所以,当我们使用 obj.show() 的时候,由于 obj 这个对象中没有 show 这个函数,只能调用父辈原型的。但是在调用 obj.render() 的时候,由于obj本身就有 render 这个函数,所以调用自己的函数。
总结就是,对象本身如果有这个方法,优先调用本身的方法,如果对象本身没有,在往原型上进行查找。
自定义对象原型设置
可以看到,上面的这个例子,使用 setPrototypeOf 这个函数,将 parent 这个对象,设置为了 hd 这个对象的原型。
现在,原型链是从 hd 到 parent 到 Object 最后到 null
此外,我们在自定义对象原型的时候,尽量去使用 setPrototypeOf 和 getPrototypeOf 而不要去直接使用 __proto__ 去改变对象原型。
值得注意的是,调用的
this一直指向调用这个函数的对象。hd.show()就算调用的是parent中的函数,但是因为是借用hd所以打印的是parent method hd。
__proto__实际上是一个getter和setter
上面的案例中,我们明明将 hd.__proto__ 设置为 99 ,但打印的结果却不是这个。
原因是,__proto__ 的本质是一个 getter 和 一个 setter ,只有对象才能赋值进去。
当然我们也可以像上面 Object.create() 一样创建一个没有原型的对象,这样,我们就可以去进行随意赋值了。
总结一下原型链
原型链检测
instanceof
语法:
object instanceof constructor
它的作用是用于检查constructor.prototype是否存在于参数object的原型链上。
直白点讲的话,就是检测 B 的原型是否出现在 a 的原型链上。
更详细的 instanceof 知识请参考 详解instanceof底层原理,从零手写一个instanceof - 掘金 (juejin.cn)
isPrototypeOf
上面这个案例检测的是
b这个对象是否在a这个对象的原型链上。
isPrototypeOf 和 instanceof 的区别是:
isPrototype 是检测某个对象的原型(prototype)是否在另一个对象的原型链上。
instanceof 是检测某个对象是否在另一个对象的原型链上。
原型链属性检测in和hasOwnPrototype
由上面的例子可以知道
in不仅会检查当前对象是否含有该属性,还会检查原型链上是否含有该属性。isOwnProperty仅仅会检查当前对象是否有该属性。
继承
合理构造函数的声明
上面的例子,我们在每一次创建 User 实例的时候,都要创建一个 show 方法,很显然,这样非常浪费空间。
这样就可以节省一部分空间,当然,如果方法太多,我们也可以把原型声明成一个对象。
值得注意的是,我们在把它声明成为一个对象的时候,需要把
constructor加上去,方便我们用原型找到构造函数本身。
继承是原型的继承,而不是改变构造函数的原型
继承不是改变构造函数的原型。
看上面这个例子,我们改变了 Admin 和 Member 构造函数的原型,导致他们的原型都在 User.prototype 上面,这就导致了给 Member 和给 Admin 添加的函数都在 User 上面,这样,相同函数名的函数就会被覆盖。但是在实际开发中,显然 Menber 会员和 Admin 管理员的函数作用明显不同,这样就很容易造成错误。
继承是原型的继承
可以看到,这样就 Admin 和 User 各自的方法就就添加到各自的原型上面去了。
方法重写
我们可以根据之前说的原型方法的优先级,来重写方法。
面向对象的多态
根据不同的状态,来显示不同的值。
function User(){}
User.prototype.show = function(){
console.log(this.description())
}
function Admin(){}
Admin.prototype = Object.create(User.prototype)
Admin.prototype.description = function(){
return "管理员";
}
function Member(){}
Member.prototype = Object.create(User.prototype)
Member.prototype.description = function(){
return "会员";
}
function EnterPrie(){}
EnterPrie.prototype = Object.create(User.prototype)
EnterPrie.prototype.description = function(){
return "企业账户";
}
for(obj of [new Admin(),new Member(),new EnterPrie]){
obj.show()
}
使用父类构造函数初始属性
这里为什么打印的是 undefined ? 原因是这里 User 中的 this 指向的是 window ,后面调用 new Admin(),把里面的内容挂载到了 window 上。
解决的方法也很简单,只需要在 User(args) 上,改变 this 的指向就可以了。
自己封装一个继承的函数
参考文献和视频
面不面试的,你都得懂原型和原型链 - 掘金 (juejin.cn)
一张图理解JS的原型(prototype、proto、constructor的三角关系) - 掘金 (juejin.cn)
结语
好啦,本次分享就到这里。
文章如果有不正确的地方,欢迎指正,共同学习,共同进步。
若有侵权,请联系作者删除。