从原型到继承

92 阅读7分钟

原型与原型链

原型的具体例子

Snipaste_2022-09-07_09-35-36.png

上面这个数组,明明没有 concat 这个属性方法,但是,arr 却可以调用它,这是为什么?

Snipaste_2022-09-07_09-40-34.png

在控制台打印后发现发现了一个 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__);

Snipaste_2022-09-07_09-58-57.png

__proto__prototype 的区别

读到这里,你一定想知道 __proto__prototype 有什么区别?

解他们之间的区别之前,我们先了解一下他们各自是什么。

什么是__proto__prototype

什么是 __proto__

对象具有属性 __proto__ ,可称为隐式原型,一个对象的隐式原型指向构造该对象的构造函数的原型。

什么是prototype

原型属性(prototype) ,这个属性是一个指针,指向一个对象,这个对象的用途就是包含所有实例共享的属性和方法(我们把这个对象叫做原型对象)。原型对象也有一个属性,叫做constructor,这个属性包含了一个指针,指回原构造函数。

v2-5e55da48225128b0281dcec72950f93a_1440w.jpg

那么他们之间的区别就显而易见了,其实 隐式原型 __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

v2-a80b9ee9a6407bb12301b1eea8c565ef_1440w.jpg

函数特有prototype属性

为什么我们在上面强调的是,构造函数的显式原型 prototype 属性值

原因是:函数特有prototype属性

这句话的意思是,只有函数才有 prototype 属性,对象是没有这个属性的。

image.png

函数有多个长辈

image.png

可以看到,hdUser 创建的一个实例对象,hd 的显示原型是等于 User 的隐式原型的。并且,hd 能调用的是 User.prototype , User 能调用的是 User.__proto__

原型中的constructor引用

16537b65ee56f6f5_tplv-t2oaga2asx-zoom-in-crop-mark_4536_0_0_0.webp

上面其实我们提到了一点,就是 prototype 对象中有一个属性 constructor 这个属性包含了一个指针,指回构造函数。

image.png

其实最开始的图已经表现的非常清楚了:

image.png

我们可以根据 Foo.prototype 找到 Foo 所定义的原型,又可以根据原型中的 constructor 找到方法所对应的构造函数。

总结

总结一下吧:

  • 对象有属性__proto__,指向该对象的构造函数的原型对象。
  • 方法除了有属性__proto__,还有属性prototype,prototype指向该方法的原型对象。

__proto__prototype 的更详细的区别请参考:zhuanlan.zhihu.com/p/92894937

方法在原型中的调用

当我们试图调用某一个对象的属性或者方法的时候,如果在本身的对象中并没有找到,那么它会去它的隐式原型 __proto__(也就是它的构造函数的显式原型 prototype)中寻找。

这其实就解释了我们一开始举的例子。

Snipaste_2022-09-07_09-35-36.png

arr 并没有 concat 这个属性方法,但是他的父级原型 Array 有这个方法,因此,arr 顺着原型向上找,最终找到并调用了 concat 这个函数。

没有原型的对象也是存在的

let obj = Object.create(null,{
    name:{
        value:'obj'
    }
})

console.log(obj)

Snipaste_2022-09-07_15-23-11.png

上述例子,我们手动的设置了 obj 这个对象的原型为 null ,此时, obj 就成为了一个完全的数据字面量对象。

原型方法和对象方法的优先级

image.png

从上图可以看出,obj 这个对象只有 render 这一个函数,他的父辈原型中有 rendershow 函数。所以,当我们使用 obj.show() 的时候,由于 obj 这个对象中没有 show 这个函数,只能调用父辈原型的。但是在调用 obj.render() 的时候,由于obj本身就有 render 这个函数,所以调用自己的函数。

总结就是,对象本身如果有这个方法,优先调用本身的方法,如果对象本身没有,在往原型上进行查找。

自定义对象原型设置

image.png

image.png

可以看到,上面的这个例子,使用 setPrototypeOf 这个函数,将 parent 这个对象,设置为了 hd 这个对象的原型。

现在,原型链是从 hdparentObject 最后到 null

此外,我们在自定义对象原型的时候,尽量去使用 setPrototypeOfgetPrototypeOf 而不要去直接使用 __proto__ 去改变对象原型。

值得注意的是,调用的 this 一直指向调用这个函数的对象。 hd.show() 就算调用的是 parent 中的函数,但是因为是借用 hd 所以打印的是 parent method hd

__proto__实际上是一个getter和setter

image.png

上面的案例中,我们明明将 hd.__proto__ 设置为 99 ,但打印的结果却不是这个。

原因是,__proto__ 的本质是一个 getter 和 一个 setter ,只有对象才能赋值进去。

image.png

当然我们也可以像上面 Object.create() 一样创建一个没有原型的对象,这样,我们就可以去进行随意赋值了。

总结一下原型链

c631b657ca62427a8bdef1a2c145346a_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp

原型链检测

instanceof

语法:

object instanceof constructor

它的作用是用于检查constructor.prototype是否存在于参数object的原型链上。

image.png

直白点讲的话,就是检测 B 的原型是否出现在 a 的原型链上。

image.png

更详细的 instanceof 知识请参考 详解instanceof底层原理,从零手写一个instanceof - 掘金 (juejin.cn)

isPrototypeOf

image.png

上面这个案例检测的是 b 这个对象是否在 a 这个对象的原型链上。

isPrototypeOfinstanceof 的区别是:

isPrototype 是检测某个对象的原型(prototype)是否在另一个对象的原型链上。

instanceof 是检测某个对象是否在另一个对象的原型链上。

原型链属性检测in和hasOwnPrototype

image.png

由上面的例子可以知道 in 不仅会检查当前对象是否含有该属性,还会检查原型链上是否含有该属性。isOwnProperty 仅仅会检查当前对象是否有该属性。

继承

合理构造函数的声明

image.png

上面的例子,我们在每一次创建 User 实例的时候,都要创建一个 show 方法,很显然,这样非常浪费空间。

image.png

这样就可以节省一部分空间,当然,如果方法太多,我们也可以把原型声明成一个对象。

image.png

值得注意的是,我们在把它声明成为一个对象的时候,需要把 constructor 加上去,方便我们用原型找到构造函数本身。

继承是原型的继承,而不是改变构造函数的原型

继承不是改变构造函数的原型。

image.png

image.png

看上面这个例子,我们改变了 AdminMember 构造函数的原型,导致他们的原型都在 User.prototype 上面,这就导致了给 Member 和给 Admin 添加的函数都在 User 上面,这样,相同函数名的函数就会被覆盖。但是在实际开发中,显然 Menber 会员和 Admin 管理员的函数作用明显不同,这样就很容易造成错误。

继承是原型的继承

image.png

image.png

可以看到,这样就 AdminUser 各自的方法就就添加到各自的原型上面去了。

方法重写

image.png

我们可以根据之前说的原型方法的优先级,来重写方法。

面向对象的多态

根据不同的状态,来显示不同的值。

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()
}

image.png

使用父类构造函数初始属性

image.png

这里为什么打印的是 undefined ? 原因是这里 User 中的 this 指向的是 window ,后面调用 new Admin(),把里面的内容挂载到了 window 上。

image.png

解决的方法也很简单,只需要在 User(args) 上,改变 this 的指向就可以了。

image.png

自己封装一个继承的函数

image.png

image.png

参考文献和视频

面不面试的,你都得懂原型和原型链 - 掘金 (juejin.cn)

zhuanlan.zhihu.com/p/92894937

一张图理解JS的原型(prototype、proto、constructor的三角关系) - 掘金 (juejin.cn)

结语

好啦,本次分享就到这里。

文章如果有不正确的地方,欢迎指正,共同学习,共同进步。

若有侵权,请联系作者删除。