通俗易懂的原型和原型链

前言

原型原型链的这个东西,似乎是大部分同学在学习javaScript时都会有疑问的点,感觉理解起来很困难,搞不懂,最后采取死记硬背的方式,但这只能形成短期的记忆,可能一觉醒来,甚至喝一杯水的功夫就忘掉了。本文将以一种通俗易懂的描述方式去讲原型和原型链相关的东西,让你知其然又知其所以然,真正明白什么是原型原型链


1、构造函数

构造函数和普通函数本质上没什么区别,只不过是使用了new关键字创建对象的函数,才被叫做构造函数。构造函数的首字母一般是大写,用以区分普通函数,但是你不大写也没问题。

下面我们写一个制造车的构造函数,我们可以通过这个构造函数去创造更多的车出来。

1
2
3
4
5
6
7
8
9
10
11
12
function Car(color, length) {
this.color = color;
this.length = length;
this.type = '车';
this.drive = function () {
console.log("驾驶");
}
}

let car1 = new Car('black', '4.8m'); // 一辆长度4米8的黑色车
let car2 = new Car('white', '4.5m'); // 一辆长度4米5的白色车
...

拿上面这个例子来说,我们可以根据车的构造函数去制造很多辆车。但是这么多的车,只是颜色和长度不同,但是都被叫做车,都能驾驶。如果我们每造一辆都告诉他“你叫车,你能开” 是不是很麻烦,很浪费时间。这时候,原型(Prototype)就派上用场了!

2. 原型(Prototype)

2.1 显式原型

显式原型利用prototype属性查找原型,这是函数独有的属性。

2.2 隐式原型

隐式原型利用__proto__属性查找原型,这是对象独有的属性。

2.3 显式原型和隐式原型的关系

我们把上边的案例进行改造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

function Car(color, length) {
this.color = color;
this.length = length;
}

Car.prototype.type = '车';
Car.prototype.drive = function () {
console.log("驾驶");
}

let car1 = new Car('black', '4.8m'); // 一辆长度4米8的黑色车
let car2 = new Car('white', '4.5m'); // 一辆长度4米5的白色车

console.log(car1.type); // 车
console.log(car2.type); // 车

car1.drive(); // 驾驶
car2.drive(); // 驾驶
  1. 先造一辆原型车,这辆车就是所有车的模板
  2. 然后,每造一辆新车,你只需要告诉它:“你是通过那个原型车造出来的”
  3. 这样,新车就不用重复声明“我是车,我能驾驶”了。因为根据那个模版造出来车都有这些属性和方法。

这个时候就有同学有疑问了,我们没有赋予car1、car2这个实例对象type属性和drive方法,为什么可以访问到呢?这就是原型在起作用了:
在js中,对象如果在自己的这里找不到对应的属性或者方法,就会通过__proto__去他构造函数的原型对象上去找,如果上有这个属性或方法,就会返回。

1
2
console.log(car1.__proto__ === Car.prototype); // true
console.log(car2.__proto__ === Car.prototype); // true

那如果构造函数的原型对象上也没有找到想要的属性呢?这就要说到原型链了。


3. 原型链

既然__proto__这个是对象类型的属性,而原型(Prototype)对象也是对象,那么原型对象就也有__proto__这个属性,但是原型(Prototype)对象__proto__又是指向哪呢?

我们来分析一下,既然原型对象也是对象,那我们只要找到对象的构造函数就能知道__proto__的指向了。而js中,对象的构造函数就是Object(),所以对象的原型对象,就是Object.prototype。既然原型对象也是对象,那原型对象的原型对象,就也是Object.prototype。不过Object.prototype这个比较特殊,它没有上一层的原型对象,或者说是它的__proto__指向的是null。如果没有null来终结的话,那就陷入无限循环了,生与死轮回不止。

到这里,就可以回答前面那个问题了如果构造函数的原型对象上也没有找到想要的属性呢?,如果某个对象查找属性,自己和原型对象上都没有,那就会继续往原型对象的原型对象上去找,这个例子里就是Object.prototype,这里就是查找的终点站了,在这里找不到,就没有更上一层了(null里面啥也没有),直接返回undefined。

可以看出,整个查找过程都是顺着__proto__属性,一步一步往上查找,形成了像链条一样的结构,这个结构,就是原型链。因为他们都是通过隐式原型(__proto__)来查找的,所以原型链也叫作隐式原型链。

正是因为这个原因,我们在创建对象、数组、函数等等数据的时候,都自带一些属性和方法,这些属性和方法是在它们的原型上面保存着,所以它们自创建起就可以直接使用那些属性和方法。

4、总结

函数在js中,也算是一种特殊的对象,所以,可以想到的是,函数是不是也有一个__proto__属性?答案是肯定的,既然如此,那就按上面的思路,先来找找函数对象的构造函数。

在js中,所有函数都可以看做是Function()的实例,而Car()和Object()都是函数,所以它们的构造函数就是Function()。Function()本身也是函数,所以Function()也是自己的实例,听起来既怪异又合理,但是就是这么回事。

1
2
3
console.log(Car.constructor === Function); // true
console.log(Object.constructor === Function); // true
console.log(Function.constructor === Function); // true

既然知道了函数的构造函数,那么函数的__proto__指向我们也就知道了,就是Function.prototype。

1
2
3
4

console.log(Car.__proto__ === Function.prototype); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true

5、总结

  1. 构造函数是使用了new关键字的函数,用来创建对象,所有函数都是Function()的实例
  2. 原型对象是用来存放实例对象的公有属性和公有方法的一个公共对象,所有原型对象都是Object()的实例
  3. 原型链又叫隐式原型链,是由__proto__属性串联起来,原型链的尽头是Object.prototype