快速理解JS中的原型和原型链
在我们学习JS的过程中,我们总会接触到一些词:“原型”,“原型链”。那么今天我就来带大家来学习学习原型和原型链的知识吧!
在开始之前,我们明确一下我们接下来想要学习的目标:
- 什么是原型?
- 什么是原型链?
- 原型和原型链之间有什么关系?
- 原型和原型链有什么作用?
原型
什么是原型呢?每个构造函数创建的对象都会在创建的时候自带(创建)一个 prototype
属性,这个属性是一个对象,这个对象就是我们要说的原型。是不是有点绕?来看下面这个例子:
function Person(){}
Person.prototype.name="zhangsan"
Person.prototype.sayName=function(){
console.log("lisi")
}
const p1 = new Person()
console.log(p1.name)//zhangsan
console.log(p1.__proto__)// {name:"zhnagsan"}
console.log(p1.constructor)// [Function: Person]
console.log(Person.prototype===p1.__proto__)// true
console.log(Person.prototype.constructor===Person)// true
从这个例子可以看出,p1
是构造函数Peron()
的实例对象,而实例对象通过__proto__
和Person
的prototype
属性连接起来了。看到这里,你是不是还是很疑惑,它们两个怎么就连接起来了?这就需要了解一下下面的知识点了:
- 构造函数通常会有两个原型对象,一个是隐式原型
__proto__
,一个是显示原型prototype
。而隐式原型是每一个对象都会拥有的。 - 原型的一个作用大致可以理解成作为实例对象和构造函数之间的桥梁,但是请注意:实例对象与构造函数原型有直接联系,但是实例对象和构造函数之间没有直接联系!
- 使用原型对象的还有一个好处是,在它上面定义的属性和方法可以被对象实例共享。也就是
Person.prototype
上面的属性和方法都会共享给实例对象,而且一个原型对象可以有多个实例的指向。 - Person.prototype是 Person 构造函数的原型
- p1.__proto__是实例对象 p1 的隐式原型,通过隐式原型可以访问对象的原型
- 如上所述,构造函数有一个
prototype
属性引用其原型对象,而这个原型对象也有一个constructor
属性,引用这个构造函数。也就是两者相互循环引用。
原型链
看完了上面内容,相信你已经对原型已经有了大概的了解,接下来我们进阶了解一下原型链吧。看完上面的内容其实就很好理解原型链了,我们知道在使用实例对象的时候可以使用一些方法,像 toString、valueOf等,但是我们并没有在实例对象中定义这些方法,那这些方法是那里来的呢,实际上这些方法都是实际对象通过原型链到Object
那里“拿”的。当我们调用一个对象中没有定义的方法时,JS 引擎会沿着原型链向上查找,直到找到该方法或达到原型链的顶端。(原型链也就是上图中右边一直向上“链条")。
这时候就会有人问:那么 JS 引擎是怎么利用原型链搜索的呢?请看下面的例子:
function Person(){}
Person.prototype.name="zhangsan"
const p1=new Person();
console.log(p1.name)// zhangsan
p1.name="lisi"
console.log(p1.name)// lisi
JS 引擎在第一次打印的时候会问 p1 实例有 name 属性吗,答案是没有。然后继续搜索并问:p1 的原型有 name 属性吗?答案是有的,于是就返回了保存在原型上的这个 name 属性。第二次打印的时候 JS 引擎会问 p1 实例有 name 属性吗,答案是有的,于是就返回了保存在实例上的这个 name 属性。
实际上,在第二次打印的时候,p1原型上的 name 属性已经被 p1 实例的 name 属性给遮蔽了,也就是说虽然不会修改它,但会屏蔽它对原型上的同名属性。即时在实例上把这个属性设置为 null,也不会恢复它和原型的联系。不过使用 delete 操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象。
function Person(){}
Person.prototype.name="zhangsan"
const p1=new Person();
console.log(p1.name)// zhangsan
p1.name="lisi"
console.log(p1.name)// lisi
delete p1.name
console.log(p1.name)// zhangsan
hasOwnProperty
方法用于确定某个属性是在实例上还是在原型对象上,会在属性存在与掉用它的对象实例上时返回 true
function Person(){}
const p1=new Person();
console.log(p1.name)// zhangsan,来自原型
console.log(p1.hasOwnProperty("name"))// false
p1.name="lisi"
console.log(p1.name)// lisi,来自实例
console.log(p1.hasOwnProperty("name"))// true
有人会说,这仅仅只是一个实例和其构造函数的原型链,那更复杂应该是什么样的呢?大家不妨想想,如果原型是另一个类型的示例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
return this.subproperty
}
let instance = new SubType()
console.log(instance.getSuperValue()) // true
这里SuperType
的实例作为SubType
原型的一个属性,那么SuperType
原型上的方法都会被SubType
的示例所继承,这个例子就能够很好的体现出什么原型链和原型链的作用。
到这里,我们已经对原型有了大概的认识,那接下来我们来尝试一下回答上开头提出的几个问题: