JS原型、原型链剖析(js基础提升之二)

背景知识

看完类型开始看原型,个人觉得原型,原型链和闭包是js的两大难点,其他的知识点都是比较容易掌握的。


什么是原型、什么是原型链

JS中,每个对象都有一个内部属性prototype,我们通常称之为原型。原型的值可以是一个对象,也可以是null。如果它的值是一个对象,则这个对象也一定有自己的原型。这样就形成了一条线性的链,我们称之为原型链。


构造函数

具体概念以及如何理解请参阅JS中数据类型剖析,咱们只拿js示例来说事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Car(color) {
this.color = color
this.product = function () {
console.log("Car is producing!!!")
}
this.getColor = function () {
console.log("Car is " + color)
}
this.run = function (color) {
this.product()
console.log('Car is produced')
color ? this.getColor(color) : ''
}
return this.run(this.color)
}

let car1 = new Car()
let car2 = new Car()

console.log(car1.constructor === Car) //true
console.log(car2.constructor === Car) //true
car1和car2都是Car实例化出来的对象,所以实例的对象的constructor指向Car


原型对象

在JS中,每个函数对象都有一个prototype属性,这个属性指向函数的原型对象。

还是上面的例子,我们换一个写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Car(color, brand, country) {
this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
return this.product(this.color, this.brand, this.country)
}

Car.prototype.color = 'default'
Car.prototype.brand = 'Volkswagen'
Car.prototype.country = 'Germany'
Car.prototype.product = function(color, brand, country) {
console.log('Car('+ brand +',' + country + ','+ color +') is produced')
}

new Car() //Car(Volkswagen,Germany,default) is produced
new Car('red', 'Ferrari', 'Italy') //Car(Ferrari,Italy,red) is produced

这里我们得到第一个结论,Car实例化的时候有一些默认属性,如果我们不通过自定义的复写方法来修改的话,我们实例化的对象都会具有该属性。这些属性的集合叫做原型对象,如下:

1
2
3
4
5
6
7
8
Car.prototype = {
color: 'default',
brand: 'Volkswagen',
country: 'Germany',
product: function(color, brand, country) {
console.log('Car('+ brand +',' + country + ','+ color +') is produced')
}
}

我们生成了一个造车工厂,该工厂会生产德国产默认颜色的大众汽车,当然也可以生产其他品牌国家以及不同颜色的汽车。除了color,brand,country以及product方法以外,我们的造车工厂还有一个默认的属性:constructor。所以我们的Car.prototyep也有一个constructor属性,我们上节讲过constructor是指向它父级的一个指针所以我们来尝试一下:

1
console.log(Car.prototype.constructor === Car) //true

该结果验证了我们的结论,同我们上节讲的对象类型一样,我们在生成新的对象的时候也会添加一个constructor(构造函数)属性,这个属性指向生成它的父对象即Car,那么我们再实例化一个vwCar:

1
2
let vwCar = new Car()
console.log(vwCar.constructor === Car) //true

结论也是true,那么我们对比一下两行代码:

1
2
Car.prototype.constructor === Car
vwCar.constructor === Car

vwCar.constructor是Car方法实例化的,自然,vwCar.constructor === Car。那么通过Car.prototype.constructor === Car是不是验证了,Car.prototype也是Car实例化来的呢。即:

1
2
var car = new Car()
Car.prototype = car

原型对象(Car.prototype)是构造函数(Car)的一个实例。


__proto__

JS在创建对象的时候,除Object.prototype对象以外,所有对象的__proto__都指向创建这个对象的constructor的prototype。
上篇文章js中的数据类型中分析过Object.prototype.__proto__指向了null。

1
2
3
4
let vwCar = new Car()
console.log(Car.prototype.constructor === Car) //true
console.log(Car.prototype === vwCar.__proto__) //true
console.log(vwCar.constructor === Car) //true

通过上面代码得出如下结构图:

es5中并不是所有浏览器都支持__proto__属性,__proto__已经添加至es6标准中。


Prototyep

Prototype是保存着它们所有实例方法的真正所在。

正如我们上节所分析Object实例所有共有方法都是存在在Object.prototype中。
本节我们通过一个简单的类型Number来分析prototype。

console.log(Number.prototype)

上图中Number的内置方法多多少少都不陌生。比如

1
2
let a = 123.3213
console.log(a.toFixed(2))

这里的toFixed方法就是从Number继承来的,而toFixed方法写在Number.prototype。

(3).plus(5)如何让该语句执行结果输出一个8

该题是在面试中遇到的,当时面试官打开电脑问我(3).plus(5)是啥,我想了想没有这个方法,肯定是报错呀。毕竟没有这个方法。又问如果希望它能输出一个8该如何处理。你可以在浏览器里面输入任何代码。

首先我们来看。3是一个数字类型的数据。继承了Number下的所有方法,所以我们只需要在Number.prototype下定义一个plus方法即可。

1
2
3
4
Number.prototype.plus = function(number) {
return this + number
}
console.log((3).plus(5)) // 8

当然在工作中我们不鼓励着么做,因为Number是系统内置对象,虽然我们可以修改它的内容,但是我们没有权利去修改任意系统内置对象,因为可能会导致其他人在使用该对象的时候出现一些莫名奇妙的bug。

比如我在说明==是一个坑的时候举过的一个例子:

1
2
3
4
5
6
7
8
var x = 1
var obj = {
valueOf: function(){
x = 2
return 0
}
}
console.log(obj == 0, x) // true, 2

这里也是一样,我们复写了obj下的valueOf方法,结果是非常可怕的。如若我们复写了Object.prototype的内置方法,所有Object类型的数据内置方法都会出问题,结局可想而知。


总结升华

原型和原型链是JS实现继承的一种模型。
原型链的形成是真正是靠__proto__而非prototype
引用网上某兄台的总结

再写一个现阶段能理解的js类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function Car(color, brand, country) {
this.color = color ? color : this.color
this.brand = brand ? brand : this.brand
this.country = country ? country : this.country
}

Car.prototype.color = 'black'
Car.prototype.brand = 'Benz'
Car.prototype.country = 'Germany'

function Volkswagen() {
this.platform = 'PQ35'
this.brand = 'Volkswagen'
}

function MLB() {
this.platform = 'MLB'
}

function Macan() {
this.name = 'Macan'
this.type = 'suv'
this.brand = 'Porsche'
}

Volkswagen.prototype = new Car()
MLB.prototype = new Volkswagen()
Macan.prototype = new MLB()

let myCar = new Macan()

console.log(myCar.brand, myCar.name, myCar.type, myCar.platform, myCar.color, myCar.country)
console.log(myCar)

奔驰发明了汽车,大众学习了奔驰的造车方法,然后创建了MLB平台,而我买了一辆基于MLB平台叫做macan的suv,是大众的子品牌保时捷

可以看出,macan继承了MLB平台,MLB平台继承了Volkswagen,而Volkswagen继承了Car方法。示例有些偷懒,应该有保时捷出现在继承中。想想篇幅算了,后面会在js设计模式中好好写一下吧。

上图中的继承均是通过原型链来继承的。该继承方式正如上文引用:
原型和原型链是JS实现继承的一种模型
原型链的形成是真正是靠__proto__而非prototype

object其他的内置方法也可以通过该示例来进行检测,这里我就不贴代码了。各位自己试试可以加深对内置对象的理解。