序言
我们之前聊过JS中的包装类概念,在JavaScript中的包装类中提供了三种为原始数据类型创建临时对象的方法,当我们声明一个原始变量并且访问访问他们身上不存在的属性的时候不会报错但是输出永远是undefined
,要从底层解释这个原因,就得引入我们今天要介绍的一个新对象 -- 原型,借助通俗易懂的代码和讲解,让我们轻松拿捏今天的知识点"原型"!
一、原型
原型的定义
原型(prototype)是函数天生就具有的属性,它定义了构造函数制造出的对象的公共祖先。通过 该构造函数产生的对象可以隐式继承到原型上的属性和方法。
实例化对象能够继承构造函数体内的属性
js
function Person(){
this.name = "散陵";
this.age = 17;
}
let p1 = new Person();
console.log(p1);
在上述代码里面我们首先定义了一个构造函数,当我们用new方法来将这个构造函数实例化一个对象的时候,这些对象其实就会继承函数体内的name和age属性,每当我们实例化一个对象,这些对象就会具有这些属性,这种继承也被称之为"显示继承",当我们打印这个对象就能显示这些属性。

原型的意义:提取公共属性 简化代码执行
js
Car.prototype={
name: 'BMW',
lang: 4900,
height: 1400
}
function Car(owner,color){
this.owner = owner
this.color = color
}
var car = new Car('散陵','red')
var car2 = new Car('荒','black')
console.log(car);
console.log(car2);
但是结合实际生活想象,有很多对象具有公共属性。举个例子,我们如果想要购入一辆汽车,汽车的品牌和车型是汽车公司自己规定的,但是我们可以选择自己喜欢的车身颜色,所以我们的构造函数其实还有一个原型,也被称为显示原型
,用来定义实例化对象天生具有的属性,我们给上面的构造函数原型里添加了车名,车长和车重属性,并且在构造函数体里定义了所有者,车身颜色属性可以由我们实例化一个对象时传参进入进行设置,我们实例化两个对象当我们输出这两个对象里的属性会打印出什么呢?

打印结果只显示了我们在构造函数体里定义的属性,但是没有直接输出构造函数原型里的属性,接着我们尝试在实例化对象里面访问构造函数的原型
原型的增删改查
在上面我们可以得到原型可以实现增加属性这个操作,那么是否也可以从实例对象对原型进行删除,修改,查找操作呢?
从实例化对象中对原型进行增加
js
Car.prototype={
name: 'BMW',
lang: 4900,
height: 1400
}
function Car(owner,color){
this.owner = owner
this.color = color
}
var car = new Car('散陵','red')
var car2 = new Car('荒','black')
car.tyoe = '越野车'
console.log(Car.prototype.type);

结果表明我们是不能从实例化对象中对构造函数原型里的属性进行
增加
的
其中这里输出undefined得原因是我们无法从对象构造函数得显示原型内找到type这个属性,于是就会接着往构造函数得隐式原型也就是Object构造函数得显示原型上找,其实也找不到最后就会去Object构造函数得隐式原型上找,因为Object后面没有构造函数了,其隐式原型指向得是null,也就没有找到,但是JS中得对象很特殊,如果没用定义并不会报错而是输出undefined
从实例化对象中对原型进行查找
js
Car.prototype={
name: 'BMW',
lang: 4900,
height: 1400
}
function Car(owner,color){
this.owner = owner
this.color = color
}
var car = new Car('散陵','red')
var car2 = new Car('荒','black')
console.log(car.name);
console.log(car.lang);
console.log(car.height);

结果表明我们可以从实例化对象
查找
构造函数原型的属性
从实例化对象中对原型进行删除
js
delete car.name
console.log(Car.prototype.name);

我们输出结果显示还是能打印出构造函数原型里面的属性,这就说明实例对象是无法
删除
原型的属性和方法的
从实例化对象中对原型进行修改
那么我们可不可以从实例化对象里面进行对构造函数显示原型的修改呢?
js
car.name = '凯迪拉克'
console.log(Car.prototype.name);

我们输出结果显示还是能打印出构造函数原型里面的属性并没有改变,这就说明实例对象是无法
删除
原型的属性和方法的
二、隐式原型
隐式原型定义
除了构造函数,我们每次实例化一个对象时,这个对象也有自己的隐式原型
,这个实例对象会通过自己的隐式原型去继承构造函数显示原型上的属性和方法。
实例化对象通过隐式原型继承构造函数显式原型的属性和方法
html
<script>
Person.prototype.name = '阿美'
function Person(){
}
var p = new Person()
console.log(p);
</script>
这里我们定义了一个Person构造函数,并且在构造函数原型里声明了一个name属性为'阿美',如果我们实例化一个对象,然后访问其属性会的到什么呢,在浏览器的检查console里面我们可以检查p的属性

从结果我们看出对象p是一个Person类对象,并且里面有name属性为:"阿美",不同浏览器对隐式原型的命名不同,这里我用的是Chrome,其对隐式原型的命名就是[[Prototype]],其实它的隐式原型就是构造函数Object的显示原型。
constructor
属性用来记录对象是由谁来创建的

在上面我们还看见了其属性内还有一个constructor属性,这个属性是用来表明对象是由谁创建的,根据上面的代码示例,constructor属性后面显示的是 f Person(),这就表明p对象是由Function Person()这个构造创建的。
三、原型链
定义
顺着对象的隐式原型不断地向上查找上一级地隐式原型,直到找到目标或者一直到null,这种查找关系叫做原型链
访问对象属性的查找
html
<script>
Ground.prototype.lastName = '昊'
function Ground(){
}
var ground = new Ground()
Father.prototype = ground
function Father(){
this.name = '石'
}
var father = new Father()
Son.prototype = father
function Son(){
this.hobbit = 'reading'
}
var son = new Son()
console.log(son.name);
console.log(son.lastName);
</script>
我们看上述代码,我们如果要找son对象的两个属性首先会在其构造函数体内的属性查找,如果没找到,就会去自己的隐式原型也就是构造函数的显示原型里面找,于是会去到father对象的构造函数体内的属性查找,显示扎到了name属性为:"石",但是lastName属性还没找到,于是接着往father的隐式原型里去找也就是father对象的构造函数的显示原型去找,Father构造函数的显示原型是ground,于是去ground对象的构造函数体内找,结果没找到,于是去ground对象的隐式原型也就是ground对象构造函数的显示原型去找,结果找到了lastName属性为:"昊",过程有点绕,多看几遍更好理解,所以我们最终的输出结果就是:

构造函数原型里的this指向问题
js
<script>
Person.prototype = {
name:'hanghang',
sayName:function(){
console.log(this.name);
}
}
function Person(){
this.name = 'hengheng'
}
var p = new Person()
p.sayName()
</script>
我们借助上个例子的查找顺序接着来分析这段代码。。。。
首先我们定义了一个构造函数Person,并且构造体内有一个属性this.name,这个this其实指向的就是实例化对象,然后我们在构造函数原型里添加了两个属性,因为原型的本质就是对象,所以我们可以在原型里添加属性和方法,当我们实例化一个p对象并且调用它的sayName方法时,过程应该是首先在p对象的构造函数体内找是否有这个方法,没找到于是就去p的隐式原型也就是构造函数的显式原型内找,于是就找到了sayName方法,但是这个方法里面打印的是this.name
,这个this指向的其实就是实例化对象,你可以理解为前面所有的老一辈都是为了新生一代服务的,于是最后输出的结果就是:

出类拔萃(网易面试题)
网易面试官: 所有的对象最终继承自 Object.prototype?
当他问出这样一个问题时,可能你按政治学的思路来想,都说所有了这么绝对肯定是错的,但是你的脑袋里的知识库告诉你这个问题又好像是对的,于是你抓耳挠腮不知所措了,没关系接下来科普一个小知识,明白为什么这个问题其实是错误的。
- JS中判定数据类型是靠将该变量转换为2进制数据,如果前三位是000判定其为引用数据类型,但是null很特殊,虽然它是原始数据类型,但是它二进制转换后全是0,自然也被读成了引用数据类型,所以它是唯一一个没有隐式原型的对象
是不是感觉很坑,这一道面试题据说难倒了诸多面试者,这主要是考察你对各方面知识的掌握深度,在如此内卷的社会背景下,你会一些基本甚至有点难度得技术并没有什么特殊,会的人并不是只有一个,但是真正深入底层了解这些方法的人却很少,或许面试官不会因为这一道题你答不上来而直接淘汰你,但却会因为你回答的出来对你刮目相看。今天我们得内容有点绕却不难,主要就是带你深入认识JS原型得底层逻辑,让你对对象属性查找有一个更全面透彻得认识。
总结
md
# 原型(显示原型)
1. 定义: 原型(prototype)是函数天生就具有的属性,它定义了构造函数制造出的对象的公共祖先。通过
该构造函数产生的对象可以隐式继承到原型上的属性和方法。
2. 意义: 提取公共属性,简化代码执行
3. 原型增删改查
- 实例对象是无法 修改 原型的属性和方法的
- 实例对象是无法 删除 原型的属性和方法的
# 对象原型(隐式原型)
实例对象会通过自己的隐式原型去继承构造函数显示原型上的属性和方法
1. 当访问对象属性时,先找对象显示具有的属性,没找到再去找对象的隐式原型。
2. 实例对象的隐式原型 === 构造函数的显示原型
# 原型链
顺着对象的隐式原型不断地向上查找上一级地隐式原型,直到找到目标或者一直到null,这种查找关系叫做原型链
核心:`隐式原型`
# 所有的对象都有隐式原型 (X)
JS中判定数据类型是靠将该变量转换为2进制数据,如果前三位是000判定其为引用数据类型,但是null很特殊,虽然它是原始数据类型,但是它二进制转换后全是0,自然也被读成了引用数据类型,所以它是唯一一个没有隐式原型的对象
原型链图示
最后附上一张图,看完文章后如果能将这张图读懂那就恭喜你说明原型这个概念你已经完全掌握了!

感谢大家的阅读,点点赞吧♥
如果想了解更多有用的干货,点赞+收藏 编码不迷茫
本人的开源Git仓库: gitee.com/cheng-bingw...
更多内容:JavaScript 包装类干货分享