原型和原型链
携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情
构造函数创建对象
在 JavaScript 中,对象不是凭空产生的,需要你使用构造函数 new 一个,就像这样:
function Person() {}
let person = new Person();
当然你也可以使用一些其它的对象创建方法,但本质上都是 JS 通过构造函数来创建的。而一些对象,即使没有显式声明,也存在一些自带属性,如 new Array()对象就存在 sort,reverse 等方法,这是如何做到的呢?
原型
实际上任何一个函数都存在 prototype 属性,即原型,new 关键字会新创建一个对象,并将该对象的原型指向构造函数的原型。在这里就是将 person 实例对象的原型指向 Person 构造函数的原型,从而 person 对象继承了Person的原型上的属性,看起来就像你没有声明他却自带了一样,就像这样:
function Person() {};
Person.prototype.name = 'Hi!';
let person = new Person();
console.log(person.name); // 'Hi!'
我们可以把构造函数创建实例的过程理解为:每一个 JS 对象在创建的时候都会关联另一个对象,并从该对象继承属性和方法,这个对象就是原型。
为了避免混乱,将目前已知的点列出来:
- 任何函数都存在 prototype 属性,即函数的原型,通过
.prototype来访问 - 任何实例对象在创建时也会关联原型,来自于其构造函数的原型,通过
._proto_来访问 - 原型也是一个对象,其内部属性会被其构造函数创建的实例继承
根据以上内容我们总结出下面这张图:
显式原型/隐式原型(prototype/_proto_)
在上图中,我们看到构造函数和其产生的实例对象都指向同一个原型,但函数也是对象,为什么构造函数访问原型的时候,不像实例对象一样,也使用_proto_进行访问呢?
因为这里存在隐式原型和显式原型的区别,构造函数通过.prototype访问的称之为其显式原型,实例对象通过._proto_访问的称之为隐式原型。且一般来说构造函数的显式原型和隐式原型是不一样的,即存在两个原型,这一点后面会做阐述。
总结来讲,显式原型和隐式原型是两种不同的访问原型的方式,是为了区分构造函数和实例对象两个角色的,他们有着一下几个关系:
- 实例对象存在通过
._proto_访问的隐式原型,不存在显式原型 - 构造函数既存在通过
._proto_访问的隐式原型,也存在通过.prototype访问的显式原型,因为构造函数也是 Function()产生的函数实例对象,且一般来讲他们是不一样的(除了 Funtion()构造函数本身) - 通过某构造函数生成的实例对象,构造函数的显式原型,指向实例对象的隐式原型(至理名言)
有关隐式原型实例对象通过proto访问原型,其实是浏览器提供给开发者的便利功能,起初并不在相关原型提案范围内,即只有函数才能具有 prototype 属性访问原型
constructor
现在我们知道可以查找到构造函数和实例对象的原型,那么是否可以反过来,通过原型找到构造函数和实例对象呢?
答案是原型可以通过 constructor 属性找到其构造函数,但是不能访问到实例对象,因为一个构造函数可以创建多个实例对象。
那么现在我们更新得到了下图,原型,构造函数,实例对象三者的关系得到进一步明确:
原型链
前面我们提到,任何对象在创建时都会关联另一个对象,从中继承方法和属性,也该对象的原型。而原型也是个对象,那么原型是否存在原型呢?
没错,原型也存在原型,作为实例对象可以用隐式原型来访问其原型,在之前的例子中,Person 的原型是 Person.prototype,其也是一个对象,由 new Object()创建而来,通过_proto_访问到其原型即为 Object.prototype,关系图进一步更新如下:
看到这相信大家已经明了一个道理:JS 中一切皆对象,对象都存在原型(除了顶层原型 Object.prototype,其原型为 null),也理解了为什么存在隐式原型和显式原型两种区分,因为构造函数既是函数也是 Function 的实例对象。
通过这种层次嵌套构成的链式结构,称之为原型链。由于创建对象时会继承其原型的属性和方法,所以我们在访问对象的属性方法时,首先会去其原型上查找,若未找到,继续到原型的原型上查找,直到找到顶层对象的原型 null。从中我们可以看到其它面向对象的高级程序设计语言中继承的思想,JS 则是通过原型链来实现继承。
原型经典老图
看到这里,是时候拿出这张原型的经典老图了,很多萌新在学习原型觉得摸到点门路的时候,都会被这张图打回原型,但别急让我们来慢慢拆解:
我们将这张图分为上中下三个部分,首先是上面的部分:
这一部分和我们前面的 Person 例子相同,现在来将几条关系线的含义理一下:
- (1)自定义的 Foo()函数,函数都存在原型,这里是
Foo.prototype - (2)原型通过
constructor指向构造函数 - (3)new Foo()构造的实例对象 f1、f2,他们的原型即为其构造函数的原型,通过
_proto_访问
怎么样是不是很简单呢,接下来加入中部的内容:
在这张图片的中间部分我们引入了原型的原型,形成原型链:
- (1)Foo.prototype 是对象,由 Object()创建,其存在原型 Object.prototype,由
_proto_访问 - (2)Object()构造函数,其原型为 Object.prototype
- (3)原型 Object.prototype 的 constructor 为构造函数 Object()
- (4)new Object()构造的实例 o1、o2,他们的原型也是 Object.prototype,通过
_proto_访问 - (5)到 Object.prototype 这里原型链就到头了,他是 JS 中一切对象的顶层原型,其自身不存在原型,原型为 null
最后加入尾部,形成完整的框架:
最后这一部分,加入了构造函数的隐式原型 Function 部分,完善了整个原型的结构:
- (1)Object()构造函数也是一个对象,他是 Function 的实例对象,所以其隐式原型
_proto_指向 Function.prototype - (2)同(1),Foo()构造函数也是 Function 的实例对象,其隐式原型
_proto_也指向 Function.prototype - (3)Function.prototype 原型本质是对象,则其隐式原型是 Object.prototype
- (4)Function()构造函数显式原型,为 Function.prototype
- (5)Function.prototype 原型的构造函数,反向指向 Function()
- (6)Function()构造函数作为实例对象的隐式原型,同样为 Function.prototype,这是个特例
总结
学习原型和原型链,重要的是理清三个角色的本质和关系:构造函数,实例对象,原型。并牢记 JS 中万物皆对象的概念,去好好梳理一遍,构建自己的原型知识脉络,就会豁然开朗。
最后还是那句至理名言:通过某构造函数生成的实例对象,构造函数的显式原型,指向实例对象的隐式原型