无处不在的原型链(上)

1,039 阅读6分钟

无处不在的原型链(上)

当我看到JavaScript前端中有原型链这种东西,我就绝知此事并非容易

—— 麦克阿瑟

认识原型

在JavaScript这门语言中,面向对象可以说是JS学习路上的其中一座大山,因为这座大山中的精髓就是原型对象和原型链。看懂了原型和原型链的关系,对于现在我们使用的大部分框架也会了解的更深一个层次。

先看结论

JavaScript常被描述为一种基于原型的语言,了解原型首先就要知道对象与原型之间的关系,以下是它们之间的关系总结(建议结合下方的完整关系图与该结论反复验证):

  • 所有函数都是对象,对象中可以包含属性和方法
  • 每个函数都有一个特殊的属性叫作原型或者叫作原型对象——prototype(箭头函数除外,详细可参考:JavaScript高级程序设计第四版P:288 )
  • 所有原型对象都有一个constructor属性
  • 所有对象上都有一个__proto__属性
  • 每个对象的__proto__都是指向它的构造函数的原型对象prototype
  • Function函数是所有函数的祖先函数

对象与原型

这里我们用ES5中常用的构造函数来举例

function Person(myName, myAge){
    this.name = myName;
    this.age = myAge;
}
Person.prototype.say = function (){
    console.log(this.name);
};

这里定义了一个名为Person的构造函数,构造函数中有两个实例属性,nameage,在Person构造函数的原型对象上还添加了一个say方法。
那么现在的问题是:PersonPerson.prototype之间存在什么关系呢?

image.png
对于Person而言,不管我们叫它构造函数也好还是函数也好,都有一个prototype属性(上方结论),这个属性指向它自身所对应的原型对象。
而这个原型对象上有我们自己挂载的一个say函数,并且还包含一个constructor属性 (上方结论),constructor属性指向当前原型对象对应的那个"构造函数"

对象三角恋关系

现在,我们再在之前的关系上做一个扩展:

function Person(myName, myAge){
    this.name = myName;
    this.age = myAge;
}
Person.prototype.say = function (){
    console.log(this.name);
};

const person1 = new Person('chen', 23) // 新增

我们利用Person作为构造函数创建了一个名为person1的实例化对象,现在他们之间的关系为下图所示:

image.png

通过构造函数创建出来的对象我们称之为实例对象,每个对象中都有一个默认的属性叫做__proto____proto__指向创建它的那个构造函数的原型对象(上方结论),这样看起来就类似于一个三角恋关系了。

Tips:如果大家看到这里还有疑惑,建议可以敲代码去验证,以免觉得我在无中生有🤔
例:person1.__proto__ === Person.prototypeperson1.__proto__.constructor === Person

Function函数

前面分析了构造函数,对象与原型之间的关系,接下来讲一讲Function函数与它们之间构成的联系。

  1. JavaScript中的函数是引用类型(对象类型),既然是对象,所以也是通过构造函数创建出来的
  2. 所有函数都是通过Function构造函数创建出来的对象(例如普通声明的函数或是构造函数都是Function构造函数的实例对象)
  3. JavaScript中只要是函数就有prototype属性(上方结论),Function函数的prototype属性同样指向Function原型对象
  4. JavaScript中只要是原型对象就有constructor属性(上方结论),Function原型对象的constructor属性同样指向它对应的构造函数
  5. 所有对象上都有一个__proto__属性指向它的构造函数的原型对象prototype (上方结论)
  6. 【注意】:Function__proto__属性较为特殊,因为它自身就是祖先函数(上方结论),所以Function__proto__属性同它的prototype属性一样,都是指向它的原型对象

上图:

image.png

Object函数与Function函数的关系

Object函数跟大多数构造函数与Function函数之间的关系一样,不过特殊的是,Object构造函数中的__proto__属性是指向null
image.png

原型链完整关系图

以下就是完整的原型链关系图,大家可以尝试结合上方的结论去用代码的方式一一验证。

这里除了Object函数与Function函数较为特殊,其他像String构造函数,Number构造函数等等,他们与Funtion函数之间的关系就和FunctionPerson构造函数之间的关系是一样的。因为Function函数都是他们的祖先函数。

这里也可以自行做验证,例:
String.__proto__ === Function.prototypeNumber instanceof Function

image.png

原型链查找

如果上面的关系图看懂以后,再来看原型链查找过程便是顺理成章的事。

一句话总结原型链查找过程:原型链的查找就是沿着对象上的__proto__属性一层一层往上查找,直到找到Object原型对象上。

通过从上方的完整关系图中我们可以看到,从obj1实例对象上开始就有一个__proto__属性,再往上走指向的是Person原型对象,顺着Person原型对象上的__proto__属性再往上走就来到了Object原型对象,再往上指向的就是null了。

案例

Object.prototype.say = function() {
 console.log('我是Object原型对象上的方法');
}

function Person(myName, myAge){
 this.name = myName;
 this.age = myAge;
 this.say = function(){
  console.log('我是实例方法');
 };
}

Person.prototype.say = function() {
 console.log('我是Person构造函数原型对象的方法');
};

const obj1 = new Person('chen', 18);
obj1.say(); // 我是实例方法

在实例对象obj1调用say方法时首先它会在自身的实例方法中查找,如果没有就顺着__proto__属性往上去到Person构造函数的原型对象上去查找原型中的方法,如果没有就继续顺着__proto__属性往上去到Object构造函数的原型对象上去查找原型中的方法,如果还是没有最终就会报错。

查找关系如下图:

image.png

总结

原型对象和原型链就是JavaScript在面向对象的设计中实现继承的方式。在下一章中将会讲到常用的继承方式的关系图,以及我们平时使用的Vue框架是怎样使用原型链的。
JavaScript的几座大山中还有闭包,同步异步等,都是我们日常开发中可能感觉不深,但不知不觉中就已经用到的内容。

以上部分图文内容参考自:李南江(极客江南)