Javascript执行机制(五)用公式讲清楚原型链

前面几篇文章我们从调用栈和执行上下文角度讲清楚了作用域和闭包,同时讲了作用域和this的区别,现在JavaScript中最难理解的几个概念就只剩下一个原型链了。

如果我们看过《JavaScript高级程序设计》的话应该就对其中那个原型的章节有印象,它有一个非常复杂的图,通过这图来讲了JavaScript的_proto_prototypeconstructor之间的指向关系。

一开始读那本书对这一块我是似懂非懂的,终于借这次重新梳理的机会搞清楚了。

这篇博客的内容还是分为两部分:

  • 通过几个公式来理清楚_proto_prototypeconstructor的指向关系。
  • 原型链的使用

用公式讲清楚原型链

首先上一段代码

1
2
function Parent() {}
const p1 = new Parent();

这段代码的原型链关系如下图所示

其中Parent()指的是构造函数

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1608035556/origin-of-ray/微信截图_20201215203152_tccbro.png

怎么样,是不是有点晕?

不过不用急,我们通过公式一个个分析其中的指向关系。

在此之前有几个前置的知识需要大家了解:

  • JavaScript所有的变量都是对象,分为两类,普通对象,也就是Object,和函数对象,也就是Function
  • 任何函数都可以是构造函数,只要它用在new后面,比如上面的Parent函数,如果没有通过new Parent(),它就是一个普通的函数,但是在new后面,他就叫做构造函数了
  • _proto_constructor是对象独有的
  • prototype是函数独有的,并且是一个普通对象

prototype

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1608035555/origin-of-ray/微信截图_20201215203207_eihbzw.png

prototype是函数独有的属性,是从一个函数指向一个普通对象,代表着对象是函数的原型对象,这个对象也是当前函数所创建的实例的原型对象

prototype设计之初就是为了实现继承,让有特定函数所创建的所有实例共享属性和方法,也可以说是让某一个构造函数实例化的所有对象找到公共的属性和方法,有了prototype我们不需要为每一个实例创建重复的属性和方法,而是将属性方法创建在构造函数的原型对象(prototype)上

由此我们得出本文的第一个公式,或者说是结论:

每个函数对象都有一个指针prototype,指向自己的原型对象,该对象是一个普通对象

__proto__

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1608035556/origin-of-ray/微信截图_20201215203215_aj5gbh.png

__proto__属性是所有对象都有的,包括函数对象,它是从一个对象指向另外一个对象,即从一个对象指向该对象的原型对象(prototype)

刚才我们说过,Parent.prototype上的属性和方法都叫做原型属性和原型方法,所有实例都可以访问。

那实例是如何与原型对象联系的呢?这就是本文的**第二个公式**:

1
p1.__proto__ === Parent.prototype

__proto__称为隐式原型,prototype称为显示原型,而原型链的也是通过__proto__的指向而形成的。

上面那个公式我们更具一般性的写法是:

1
2
const a = new A();
a.__proto__ === A.prototype(Object.prototype.__proto__ === null特殊)

这样一写,我们就可以把其中的a和A进行替换,a是一个普通对象,A是它的构造函数。

通过上面这个公式我们可以分析下上图的指向问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const p1 = new Parent(); // 因为
p1.__proto__ === Parent.prototype; // 所以

const Parent = new Function();
Parent.__proto__ === Fuction.prototype;

Function = new Object();
Function.__proto__ === Object.prototype

所有的prototype都是普通对象,也就是都是new Object()而来
Parent.prototype.__proto__ === Object.prototype;
Function.prototype.__proto__ === Object.prototype;

因为Object, Function可以放在new后面,所以也是个构造函数,也就是说Object = new Function()
Object.__proto__ === Function.prototype
Function.__proto__ === Function.prototype

怎么样这样一分析,是不是一个公式就搞清楚了__proto__的指向问题

constructor

constructor是对象才有的属性,是从一个对象指向一个函数,这个函数就是对象的构造函数

https://res.cloudinary.com/dvtfhjxi4/image/upload/v1608035556/origin-of-ray/微信截图_20201215203222_hiqpqf.png

这里我们得出本文的**第三个公式**

1
2
3
const a = new A();
a.constructor === A;
A.prototype其实也是A的一个实例 // 这一点要非常注意

通过这个结论,上面那个图就很好分析了。

总结

这里总结下本文的所有公式和结论,大家看完可以回过头去看开头的图片,看看能不能分析出来

  • JavaScript所有的变量都是对象,分为两类,普通对象,也就是Object,和函数对象,也就是Function
  • 任何函数都可以是构造函数,只要它用在new后面,比如上面的Parent函数,如果没有通过new Parent(),它就是一个普通的函数,但是在new后面,他就叫做构造函数了
  • _proto_constructor是对象独有的
  • prototype是函数独有的,并且是一个普通对象
  • 每个函数对象都有一个指针prototype,指向自己的原型对象,该对象是一个普通对象
  • 如果 const a = new A(); 那么 a.__proto__ === A.prototype(Object.prototype.__proto__ === null特殊)
  • 如果 const a = new A(); 那么 a.constructor === A;

原型链应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Father(){
this.property = true;
}
Father.prototype.getFatherValue = function(){
return this.property;
}
function Son(){
this.sonProperty = false;
}
//继承 Father
Son.prototype = new Father();//Son.prototype被重写,导致Son.prototype.constructor也一同被重写
Son.prototype.getSonVaule = function(){
return this.sonProperty;
}
var instance = new Son();
alert(instance.getFatherValue());//true

这段是比较常见的继承方式

那为什么我们可以在Son的一个实例上点出Father.prototype上的getFatherValue方法呢?

  • 首先因为instance = new Son(),new操作符会先创建一个临时对象tempObj,然后执行Son.call(tempObj),最终返回整个tempObj,这样就在tempObj上挂载了属性sonProperty,并赋值给instance。
  • 本来Son.prototype是一个普通对象,也就是Son的一个实例,它的原型链指针__proto__指向了一个普通对象,也就是说这个时候Son.prototype.__proto__ === Object.prototype,再往上Object.prototype.__proto__ === null原型链的寻找就结束了。
  • 但是我们修改了Son.prototype为Father的一个实例,我们在执行Son.prototype = new Father()的时候会创建一个新的普通对象,并且该对象上有fatherPrototype。所以说这个时候Son.prototype.__proto__ === Father.prototype
  • 这个时候如果我们在instance上找不到getFatherValue(),我们回去Son.prototype上找,再找不到,我们会沿着__proto__构建的原型链去向上找,这个时候就找到了Son.prototype.__proto__,也就是Father.prototype,而getFatherValue()正好就是我们定义在这里的。
  • 多提一句,getSonVaule()是定义在Son.prototype上的,也就是new Father()这个实例上的

所以说我们能通过instance点出sonPrototypt, fatherPrototype, getFatherValue(),getSonVaule()的原因是完全不同的。