1.为什么要学会它们?
如果你学过后端语言比如Java
等等,那么你应该知道它们都是面向对象的开发方式。面向对象有许多特点,其中继承就是其中一个。在Java
中通常通过类class
的方式来实现继承。
而我们的JavaScript
语言是一门基于对象的语言,它不是一门真正的面向对象编程的语言。虽然ES6
提出了class
编程的方式,但它终究只是一个语法糖,class
编译之后其实就是一个函数。
那么在JavaScript
中如何实现继承呢?这个时候就用到了原型和原型链,它们非常巧妙地解决了在JavaScript
中实现继承的问题!
2.原型和原型对象
在JS
中,我们所说的原型通常是针对于函数而言的,当然构造函数也是一个函数。
我们都知道函数也是一个对象,是对象那么它就有属性,在JS
中,我们所创建的每一个函数自带一个属性prototype
,我们就把prototype
称为原型,有些小伙伴也把它称之为“显示原型”,反正就一个意思。
这个prototype
它指向了一个对象,你可以把prototype
想象成一个指针,或者更简单的理解为prototype
的属性值(键值对)。prototype
指向的这个对象我们就称之为原型对象,通常大家就直接将prototype
理解为原型对象。
为了让大家更好理解,我们可以在浏览器控制台简单查看一下函数的prototype
,如下图:
上图中我们声明了一个Person
函数,既然函数是对象,那么我们就可以使用“.”来查看它的属性,可以看到有一个prototype
属性,这个是每一个函数都有的。
我们在代码里面打印出来看看,示例代码如下:
<script> function Person() { } console.log(Person.prototype) </script>
输出结果:
我们可以看到原型对象prototype
里面有一个constructor
属性,它指向了Person
构造函数,我们可以画一张图来理解:
总结
其实原型或者原型对象没有那么复杂,总结下来就下面几点:
- 每个函数都有
prototype
属性,被称作原型。 prototype
原型指向一个对象,故也称作原型对象。
3.prototype
和__ptoto__
很多小伙伴把prototype
和__proto__
混为一潭,其实这是两个维度的东西。prototype
的维度是函数,而__proto__
的维度是对象。__proto__
是每个对象都有的属性,我们通常把它称为"隐式原型",把prototype
称为"显式原型"。
有些小伙伴可能有疑惑,函数也是一个对象,那它是不是也有__proto__
属性呢?答案是肯定的,我们可以通过浏览器控制台验证一下。
Function:
对象:
我们可以看到函数有prototype
和__proto__
两个属性,而对象只有__proto__
属性。接下来我们再来看看__proto__
属性有什么呢?
在浏览器控制台进行测试:
上图中我们发现了一个新的属性:[[Prototype]]。很多小伙伴会误认为这个就是我们所说的显式原型prototype
,其实不是的,官方对于这个属性其实有解释,我们这里通俗的给大家解释一下:
[[prototype]]
其实就是隐式原型__proto__
,因为各大浏览器厂家不同,所以取了别名罢了,大家只需记住这个和__proto__
一样即可。
上段代码中我们定义了一个空对象,它有一个隐式原型[[prototype]]
,隐式原型的constructor
指向了构造函数Object
。
总结
__proto__
和prototype
不太一样,一个是对象拥有的隐式原型,一个是函数拥有的显式原型,这里我们简单总结一下__proto__
:
- 通常被称作隐式原型,每个对象都拥有该属性。
[[prototype]]
其实就是__proto__
。
4.原型链
前面两节我们主要介绍了prototype
和__proto__
,那么它们之间有什么关系呢?
为了理清楚之间它们的关系,我们拿出一段示例代码:
<script> function Person(name) { } // 在函数的原型上添加变量和方法 Person.prototype.name = "小猪课堂"; Person.prototype.say = function () { console.log("你好小猪课堂"); } let obj = new Person(); console.log(obj.name); // 小猪课堂 obj.say(); // 你好小猪课堂 </script>
上段代码大家应该都很熟悉,我们声明了一个构造函数Person
,其实就是一个函数。我们知道函数的prototype
是一个对象,我们就可以往这个对象上添加东西,所以我们就直接往函数的原型上添加了变量和方法。
接着我们使用new
关键词创建一个Person
构造函数的实例对象,分别打印name
和调用say
方法,大家会发现输出结果其实都是Person
原型上的东西。
这是为什么呢?这其实就和我们的原型链有关了,我们把obj
打印出来看看。
consol.log(obj):
我们会发现obj对象上面其实并没有name
属性和say
方法,但是在它的隐式原型[[prototype]]
上有name
和say
,而且我们会发现obj
的[[prototype]]
中的constructor
指向的式它的构造函数Person
。
所以我们大胆的做一个猜想:obj
的隐式原型__proto__
是否和构造函数Person
的显式原型prototype
相等呢?我们用代码证实一下:
console.log(obj.__proto__ === Person.prototype) // true
我们发现它们两个果然相等!
接下来我们修改一下我们的代码,我们在obj
对象上添加一个name
属性,看看会输出什么。
代码如下:
<script> function Person(name) { this.name = name; } // 在函数的原型上添加变量和方法 Person.prototype.name = "小猪课堂"; Person.prototype.say = function () { console.log("你好小猪课堂"); } let obj = new Person("张三"); console.log(obj.name); // 张三 obj.say(); // 你好小猪课堂 console.log(obj) </script>
输出结果:
上段代码中我们obj
上面有自己的name
属性了,这个时候输出的就是obj
自带的name
属性。到这里我们又可以做一个大胆的猜想:obj
对象想要获取name
或者say
,首先判断自己的属性当中有没有,如果没有找到,那么就在__proto__
属性中去找,而这个时候__proto__
与Person
的prototype
是相等的,也就是__proto__
指向Person
,那么便可以找到name
和say
。
上面的查找过程是不是很像链式查找啊!而这就是我们所说的原型链,而且我们发现查找的过程主要是通过__proto__
原型来进行的,所以__proto__
就是我们原型链中的连接点。
总结:
上面的查找的过程形成的一条线索就叫做原型链,大家可以把原型链拆开来理解:原型和链。
- 原型就是我们的
prototype
- 链就是
__proto__
,它让整个链路连接起来。
想要理解原型链,我们还得理解__proto__
指向哪儿,也就是说它指向那个构造函数,比如上面的obj
对象的__proto__
指向的就是Person
构造函数,所以我们继续往Person
上查找。
最后我们上一张经典的图,相信大家能看懂了:
上面这张图看似很复杂,但是我们理解了prototype
和__proto__
之后很简单,大家按照下面的思路去看上面这张图会很简单的:
- 上面很多虚线,我们发现虚线上都有
__proto__
属性,所以可以看出来__proto__
就是一个连接的作用。 - 上图中无非有三个构造函数:
Foo
、Object
、Function
,我们都知道每个函数都有一个prototype
显示原型,而且这个显示原型指向了自身这个构造函数。 - 接着我们在看图中的
new
关键字,我们知道new创建的对象都有一个__proto__
隐式原型,而且这个隐式原型执行了它的构造函数,也就是__proto__ === prototype
。 - Foo的隐式原型
__proto__
指向的是Function
的prototype
,因为函数是属于Function
这个构造函数的。所以上图中的Foo
和Object
的__proto__
都指向了Function
的prototype
。
总结
利用原型链这种链式查找的方式,我们就巧妙地实现了继承!要理解原型和原型链其实不难,主要是大家还是要有面向对象的思想,比如通过new
关键词创建实例,构造函数是什么?
如果觉得文章太繁琐或者没看懂,可以观看视频: 小猪课堂