图解的方式带你掌握JS原型和原型链
什么是原型
在JavaScript中,每个对象都有一个原型(prototype),它是一个对象或null
。原型是通过构造函数创建的对象的一个属性,它允许对象继承属性和方法。在JavaScript官方中是这样解释原型的:
在JavaScript中,原型是一个内部链接到另一个对象的引用。当试图访问对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript引擎会查找对象的原型链,逐级向上查找,直到找到相应的属性或方法,或者到达原型链的末端(即原型为
null
)。
大家看到这里是不是脑瓜子有点嗡嗡的,没关系接下来我将用图解的方式带大家彻底理解JavaScript原型和原型链。
思考题
题一: 原型与原型链的基本应用
Person.prototype.lastName = '张'
Person.prototype.say = function () {
console.log('hello world')
}
function Person() {
this.name = '三'
}
let p1 = new Person() // p1隐式具有 lastName say
console.log(p1)
console.log(p1.lastName)
p1.say()
代码输出结果
:
大家对输出的结果会不会感到疑惑?请看如下图解:
在这段代码中,首先创建了一个空对象obj
。
接下来,定义了一个构造函数Person()
,其中通过this.name = '三'
将一个属性name
赋值为字符串'三'
。同时,在Person.prototype
上定义了一个属性lastName
,它的值为字符串'张'
,以及一个方法say()
,它打印出字符串'hello world'
。
然后,通过使用new
关键字和构造函数Person
实例化了一个新的对象p1
。由于使用了构造函数,p1
会自动具有构造函数Person
的属性name
,并且它的原型链上也包含了Person.prototype
的属性lastName
和方法say
。
最后,通过console.log()
打印出了对象p1
,访问了p1
的属性lastName
和调用了p1
的方法say()
。结果分别是:
Person { name: '三' }
张
hello world
这表明p1
继承了Person.prototype
上的属性和方法,并且可以直接访问和使用它们。
在该例子中我们可以得出结论:
对象的隐式原型 === 创建它的构造函数的显式原型
js引擎查找属性时,会先查找对象显示具有的属性。找不到,再找对象的隐式原型(
__proto__
)
题二: 工厂化函数对象
Car.prototype.name = 'su7'
Car.prototype.lang = 5000
Car.prototype.height = 1400
function Car(color, onwer) {
this.color = color
this.onwer = onwer
}
let car = new Car('black', 'me')
console.log(car)
console.log(car.name)
这段代码的功能是定义了一个构造函数Car()
和一些其原型上的属性,然后使用new
关键字和构造函数实例化一个名为car
的新对象,并在控制台中打印出这个对象和它的name
属性。
在构造函数Car()
之外,通过Car.prototype
给所有Car
类型对象添加了三个属性,分别是name
、lang
、height
,这些属性是所有Car
对象公用的。在函数内部,使用this
关键字给每个创建的汽车对象设置了颜色和车主属性。
然后,通过new
关键字和构造函数Car
实例化了一个新的对象car
,传入了颜色值black
和车主值me
。由于car
是通过Car
构造函数创建的,因此它继承了Car.prototype
上定义的所有属性和方法,包括name
、lang
和height
。在这里,car
对象的name
属性被设置为'su7'
,因为name
是在Car.prototype
上定义的,而不是在构造函数内部定义的。
最后,通过console.log()
打印出了car
对象以及它的name
属性,输出结果为:
Car {color: "black", onwer: "me"}
"su7"
可以看到,car
对象的name
属性为"su7"
,这是因为Car.prototype.name
被设置为"su7"
。
题三: 原型的修改问题
原型是函数上的一个属性,它定义了构造函数的对象的公共祖先。
构造函数new出来的对象会隐式继承
到构造函数原型上的属性。
- 实例对象可以修改显式继承到的属性,但是无法修改隐式继承到的属性(原型上的)。
- 实例对象无法给原型新增属性
- 实例对象无法删除原型上的属性
Car.prototype.product = 'xiaomi'
function Car() {
this.name = 'su7'
}
let car = new Car()
car.name = '保时捷'
console.log(car)
// 为car 对象 添加了一个属性 ,隐式的product仍然存在
car.product = '宝马'
car.nickName = '小红'
console.log(car)
console.log(car.product)
console.log(car.__proto__.product)
运行结果:
详解如下:
初始化原型链属性
Car.prototype.product = 'xiaomi';
- 这行代码给
Car
构造函数的原型对象(Car.prototype
)添加了一个属性product
,值为'xiaomi'。这意味着所有通过Car
构造函数创建的实例将共享这个product
属性,除非它们自己也定义了同名属性。
定义构造函数
function Car() {
this.name = 'su7';
}
Car
是一个构造函数,用于创建新的对象实例。在这个构造函数内部,通过this.name = 'su7';
为每个新创建的实例设置了一个name
属性,初始值为'su7'。
创建对象实例
let car = new Car();
- 使用
new
操作符调用Car
构造函数,创建了一个新的Car
实例,并将其引用存储在变量car
中。此时,car
拥有自己的属性name: 'su7'
,并且通过原型链继承了product: 'xiaomi'
。
修改对象实例的自有属性
car.name = '保时捷';
- 这行代码修改了
car
实例的name
属性,将其值从'su7'改为'保时捷'。
输出对象信息
console.log(car);
- 此时打印
car
,会看到它现在的状态:name
属性为'保时捷',并且通过原型链可以访问到product: 'xiaomi'
。
覆盖原型链属性 & 添加新属性
car.product = '宝马';
car.nickName = '小红';
car.product = '宝马';
这行代码在car
实例上创建了一个新的自有属性product
,值为'宝马',这实际上隐藏了原型链上的同名属性。car.nickName = '小红';
添加了一个新的自有属性nickName
,值为'小红'。
再次输出对象信息及属性值
console.log(car);
console.log(car.product);
- 这两次打印显示了
car
当前的状态:name
为'保时捷',自有属性product
为'宝马'(覆盖了原型链上的值),以及新增的nickName: '小红'
。
访问原型链上的属性
console.log(car.__proto__.product);
- 这行代码直接通过
__proto__
访问了car
对象的原型,并打印出原型上的product
属性,其值为'xiaomi'。尽管car
实例上有一个同名的自有属性,但这条语句明确指定了要访问原型链上的属性,因此不受实例属性的影响。
题四: 原型链的综合应用
GrandFather.prototype.say = function () {
console.log('haha');
}
function GrandFather() {
this.age = 60
this.like = 'drink'
}
Father.prototype = new GrandFather()
function Father() {
this.age = 40
this.fortune = {
card: 'visa'
}
}
Son.prototype = new Father() // {age: 40, fortune: {xxx}}
function Son() {
this.age = 18
}
let son = new Son()
console.log(son.age); // 18
console.log(son.fortune);
console.log(son.like);
console.log(son.say());
上述代码展示了原型链的综合应用,初看代码大家是不是感到很迷惑,这么多重原型的继承关系怎么看得过来。😭请看以下图解你就会对原型链和原型的继承问题豁然开朗。
图解
代码详解
原型方法定义
GrandFather.prototype.say = function () {
console.log('haha');
}
- 为
GrandFather
构造函数的原型对象添加了一个方法say
,所有通过GrandFather
构造函数创建的实例及其子类实例都可以访问到这个方法。
构造函数定义
function GrandFather() {
this.age = 60;
this.like = 'drink';
}
GrandFather
构造函数定义了两个属性:age
和like
。
继承关系建立
Father.prototype = new GrandFather();
- 通过让
Father
的原型对象指向一个GrandFather
的新实例,实现了从GrandFather
到Father
的继承。这样,所有Father
的实例都将继承GrandFather
的属性和方法。
function Father() {
this.age = 40;
this.fortune = {
card: 'visa'
};
}
Father
构造函数定义了两个属性:覆盖了从GrandFather
继承来的age
属性,以及新增了一个fortune
属性。
Son.prototype = new Father();
- 同理,
Son
的原型对象被设置为一个Father
的新实例,实现了从Father
到Son
的继承。
function Son() {
this.age = 18;
}
Son
构造函数定义了一个属性age
,覆盖了从父类继承来的同名属性。
实例化与属性访问
let son = new Son();
- 创建了一个
Son
的实例son
。
console.log(son.age); // 18
- 打印出
son
的age
属性,值为18,因为实例属性优先于原型链上的属性。
console.log(son.fortune);
- 打印出
son
的fortune
属性,说明它继承自Father
。
console.log(son.like);
- 打印出
son
的like
属性,值为'drink',这是从GrandFather
继承过来的。
console.log(son.say());
- 调用
son
的say
方法,输出'haha',表明方法也能通过原型链被正确继承。
总结
这段代码通过原型链展示了多级继承的实现方式,从GrandFather
到Father
再到Son
,每个构造函数定义或覆盖了属性,同时通过原型链传递了方法。最终的Son
实例能够访问到所有层级上的属性和方法,体现了JavaScript中基于原型的继承机制。
运行结果
总结
1.原型:函数都有prototype属性,称之为原型,也称为原型对象
原型可以放一些属性和方法,共享给实例对象使用
原型可以做继承
2.原型链:对象都有 proto 属性,这个属性指向它的原型对象,原型对象也是对象,也有 proto 属性,指向原型对象的原型对象,这样一层一层形成的链式结构称为原型链,最顶层找不到则返回 null
关于原型与原型链的分析,主要可以用以下两张图来体现:
本篇文章就到此为止啦,希望通过这篇文章能对你了解JavaScript中原型和原型链
有所帮助,本人水平有限难免会有纰漏,欢迎大家指正。如觉得这篇文章对你有帮助的话,欢迎点赞收藏加关注,感谢支持🌹🌹。
转载自:https://juejin.cn/post/7369470562144288778