JavaScript中的原型与原型链,你真的了解吗?

405 阅读6分钟

什么是原型?

在 JavaScript 中,原型(Prototype)是一个非常重要的概念。简单来说,原型是一个对象。在JS中,每个函数都有一个原型对象,当创建该函数的实例(对象)时,这些实例可以继承原型对象上的属性和方法,这也是JS语言实现继承的核心。下面让我们通过代码来更直观地了解原型。

Person.prototype.like = 'sing'

function Person(){
    this.name = '李华'
} 

let p1 = new Person()
console.log(p1);                       //Person { name: '李华' }

在这段代码里,先看第3-5行代码:我们定义了一个Person构造函数,用于创建Person类的对象,并为这个对象添加了name属性,赋值为“李华”。接着我们创建了一个Person类型的对象p1。现在让我们看到第1行代码:我们前面提到,每个函数都有一个原型对象,名为‘prototype’,在这里我们只需要Person.prototype就可以访问到Person函数的原型对象。在这里,我们在原型上添加了一个属性like,值为‘sing’。但是当我们执行第8行代码时,却得到Person { name: '李华' },p1作为Person构造函数的实例对象,却并没有继承到原型对象上的属性like。

这里就引出了原型的细节1:实例对象隐式具有构造函数原型上的属性。简单来说就是在原型上的属性和方法确实已经添加到实例对象身上了,但是它只有在你特地“敲门拜访”的时候(直接用p1.like访问)才会出现,不信你看:console.log(p1.like)

屏幕截图 2024-06-09 145536.png

细节2:实例对象不能对原型的属性或方法进行添加,修改和删除

p1.age = 18  //在p1上添加了一个显性属性age,并没有添加到原型上
p1.like = 'game'  //并没有修改原型上原本的属性like,而是在p1上添加了一个属性like,这两个like属性相互独立,互不影响
delete p1.like  //无法删除原型上的属性like

原型链

在认识原型链之前,我们先要了解一个概念,那就是对象也有原型,我们通常称函数的原型为隐式原型,称对象的原型为隐式原型。

细节1:只要是对象就有原型,且对象的原型还是一个对象

下面有这样一段代码:

<script>

Person.prototype.age = 18
function Person(){
    this.name = '李华'
}
let p2 = new Person()

</script>

我们在浏览器中运行这段代码,并访问实例对象p2,我们可以发现,对象也有原型

屏幕截图 2024-06-09 161249.png

我们一般称对象的原型为隐式原型__proto__(这里要解释一下,新版浏览器将隐式原型__proto__显示为prototype,但为了方便区分,我们还是将隐式原型写为__proto__),并且我们还发现,p2的隐式原型还是一个对象(Object)

细节二:实例对象的隐式原型 == (指向)构造函数的显式原型

我们再访问Person函数的显示原型,又会发现一个小细节:

屏幕截图 2024-06-09 163651.png

p2的隐式原型和构造函数的显式原型一模一样。也就是说,如果一个对象是由构造函数创建的,那么这个对象的隐式原型一定等于(指向)它构造函数的显式原型。

细节3:JS中的引擎如何查找对象的属性和方法

<script>

Person.prototype.age = 18
function Person(){
    this.name = '李华'
}
let p2 = new Person()
console.log(p2.age)
</script>

我们已经知道直接访问 p2.age 会得到 18这个属性值,那V8引擎究竟是怎么找到这个属性的呢?

第一步:先在p2对象显性具有的属性中找,此时只能找到由构造函数赋予的属性name,值为‘李华’

第二步:如果没找到,则通过p2的隐式原型__proto__往构造函数的显式原型上找,找到Person.prototype.age,并返回值 18。

所以原型链是这样一个东西:

当我们试图访问一个实例对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript 引擎会继续在其隐式原型链上查找,直到找到该属性或者到达链的顶端。通过原型链,可以实现对象之间的属性和方法共享与继承。

原型链的代码实例

School.prototype.adress = '北京' 
function School() {
    this.name = '光明中学'
}

Class.prototype = new School()

function Class() {
    this.number = 3
}

Student.prototype = new Class() 

function Student() {
    this.gender = '男'
}

let stu = new Student() 
console.log(stu.name)
console.log(stu.adress)

代码详解:

这里定义了三个构造函数分别是学校,班级和学生,并分别添加了属性和值。首先创建了一个Student类的对象stu,接着第12行代码将Student构造函数的原型对象设置为一个新创建的Class对象(这里实际上是把let cla = new Class();Student.prototype = cla两行代码合并为一行),这样Student实例对象就继承了Class的属性和方法,同理第6行代码Class实例对象也继承了School的属性和方法

然后就开始访问属性了,先执行第19行代码,首先访问并打印的是stu.name,js引擎首先在stu对象的显式属性(也就是Student构造函数赋予的属性)中查找,如果没有找到,就会顺着stu实例的隐式原型stu.__proto__找到了构造函数的原型Student.prototype,因为构造函数原型已经被设置为Class对象,所以就会找到Class类的属性“number :3”,还不是我们要找的number属性,所以又会顺着cla.__proto__ 找到Class.prototype,因为Class构造函数原型也被设置为了School对象,所以这时就找到了School类的属性“name : 光明中学”并打印。

当执行第20行代码时,js引擎又会开始顺着原型链开始查找,步骤和上面相同,当找到School对象时,没有找到adress属性,就又会顺着sch.__proto__ 找到 School.prototype 并最终找到School构造函数原型上的属性adress。

小tips:

看到这里,想必大家应该知道原型链是怎么一回事了吧!但是关于原型链还有几个小tips:

  1. 所有原型链最终都会终止于Object.prototype,这是JavaScript中的最高原型。Object.prototype__proto__null,如果找不到相应的属性和方法,则返回undefined

  2. 几乎所有对象都有原型,比如:函数也有原型,因为在js中所有函数都是由new Function()创建的函数对象,所以函数的原型是Function.prototype

    数组也有原型,在js中数组是由new Array()创建的数组对象,所以数组的对象是Array.prototype

  3. 但是有一个例外:用 Object.create(null)创建出来的对象没有原型

let obj = Object.create(null)
console.log(obj.__proto__)       //值为undefined

以上就是关于原型和原型链的内容了,希望对大家有帮助!!