You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

249 lines
16 KiB
Markdown

2 years ago
# 05原型链V8是如何实现对象继承的
你好,我是李兵。
在前面两节中我们分析了什么是JavaScript中的对象以及V8内部是怎么存储对象的本节我们继续深入学习对象一起来聊聊V8是如何实现JavaScript中对象继承的。
简单地理解,**继承就是一个对象可以访问另外一个对象中的属性和方法**比如我有一个B对象该对象继承了A对象那么B对象便可以直接访问A对象中的属性和方法你可以参考下图
![](https://static001.geekbang.org/resource/image/c9/7b/c91e103f535679a4f6901d0b4ff8cb7b.jpg "什么是继承")
观察上图因为B继承了A那么B可以直接使用A中的color属性就像这个属性是B自带的一样。
不同的语言实现继承的方式是不同的,其中最典型的两种方式是**基于类的设计**和**基于原型继承的设计**。
C++、Java、C#这些语言都是基于经典的类继承的设计模式这种模式最大的特点就是提供了非常复杂的规则并提供了非常多的关键字诸如class、friend、protected、private、interface等通过组合使用这些关键字就可以实现继承。
使用基于类的继承时,如果业务复杂,那么你需要创建大量的对象,然后需要维护非常复杂的继承关系,这会导致代码过度复杂和臃肿,另外引入了这么多关键字也给设计带来了更大的复杂度。
而JavaScript的继承方式和其他面向对象的继承方式有着很大差别JavaScript本身不提供一个class 实现。虽然标准委员会在 ES2015/ES6 中引入了 class 关键字但那只是语法糖JavaScript 的继承依然和基于类的继承没有一点关系。所以当你看到JavaScript出现了class关键字时不要以为JavaScript也是面向对象语言了。
JavaScript仅仅在对象中引入了一个原型的属性就实现了语言的继承机制基于原型的继承省去了很多基于类继承时的繁文缛节简洁而优美。
## 原型继承是如何实现的?
那么,基于原型继承是如何实现的呢?我们参看下图:
![](https://static001.geekbang.org/resource/image/68/ca/687740eecf5aad32403cc00a751233ca.jpg)
有一个对象C它包含了一个属性“type”那么对象C是可以直接访问它自己的属性type的这点毫无疑问。
怎样让C对象像访问自己的属性一样访问B对象呢
上节我们从V8的内存快照看到JavaScript的每个对象都包含了一个隐藏属性\_\_proto\_\_ ,我们就把该隐藏属性\_\_proto\_\_称之为该**对象的原型(prototype)**\_\_proto\_\_指向了内存中的另外一个对象我们就把\_\_proto\_\_指向的对象称为该对象的**原型对象**,那么该对象就可以直接访问其原型对象的方法或者属性。
比如我让C对象的原型指向B对象那么便可以利用C对象来直接访问B对象中的属性或者方法了最终的效果如下图所示
![](https://static001.geekbang.org/resource/image/6e/ac/6e0edf92883d97be06a94dc5431967ac.jpg)
观察上图当C对象将它的\_\_proto\_\_属性指向了B对象后那么通过对象C来访问对象B中的name属性时V8会先从对象C中查找但是并没有查找到接下来V8继续在其原型对象B中查找因为对象B中包含了name属性那么V8就直接返回对象B中的name属性值虽然C和B是两个不同的对象但是使用的时候B的属性看上去就像是C的属性一样。
同样的方式B也是一个对象它也有自己的\_\_proto\_\_属性比如它的属性指向了内存中另外一块对象A如下图所示
![](https://static001.geekbang.org/resource/image/63/88/63bd704eb45646e2f46af426196a3d88.jpg)
从图中可以看到对象A有个属性是color那么通过C.color访问color属性时V8会先在C对象内部查找但是没有查找到接着继续在C对象的原型对象B中查找但是依然没有查找到那么继续去对象B的原型对象A中查找因为color在对象A中那么V8就返回该属性值。
我们看到使用C.name和C.color时给人的感觉属性name和color都是对象C本身的属性但实际上这些属性都是位于原型对象上我们把这个查找属性的路径称为**原型链,**它像一个链条一样,将几个原型链接了起来。
在这里还要注意一点不要将原型链接和作用域链搞混淆了作用域链是沿着函数的作用域一级一级来查找变量的而原型链是沿着对象的原型一级一级来查找属性的虽然它们的实现方式是类似的但是它们的用途是不同的关于作用域链我会在《06 | 作用域链V8是如何查找变量的》这节课来介绍。
关于继承还有一种情况如果我有另外一个对象D它可以和C共同拥有同一个原型对象B如下图所示
![](https://static001.geekbang.org/resource/image/44/4a/44a91019e752ae2e7d6b709562d2554a.jpg)
因为对象C和对象D的原型都指向了对象B所以它们共同拥有同一个原型对象当我通过D去访问name属性或者color属性时返回的值和使用对象C访问name属性和color属性是一样的因为它们是同一个数据。
我们再来回顾下继承的概念:**继承就是一个对象可以访问另外一个对象中的属性和方法,**在**JavaScript中我们通过原型和原型链的方式来实现了继承特性。**
通过上面的分析你可以看到在JavaScript中的继承非常简洁就是每个对象都有一个原型属性该属性指向了原型对象查找属性的时候JavaScript虚拟机会沿着原型一层一层向上查找直至找到正确的属性。所以对于JavaScript中的原型继承你不需要把它想得过度复杂。
## 实践:利用\_\_proto\_\_实现继承
了解了JavaScript中的原型和原型链继承之后下面我们就可以通过一个例子看看原型是怎么应用在JavaScript中的你可以先看下面这段代码
```
var animal = {
type: "Default",
color: "Default",
getInfo: function () {
return `Type is: ${this.type}color is ${this.color}.`
}
}
var dog = {
type: "Dog",
color: "Black",
}
```
在这段代码中我创建了两个对象animal和dog我想让dog对象继承于animal对象那么最直接的方式就是将dog的原型指向对象animal应该怎么操作呢
我们可以通过设置dog对象中的\_\_proto\_\_属性将其指向animal代码是这样的
```
dog.__proto__ = animal
```
设置之后我们就可以使用dog来调用animal中的getInfo方法了。
```
dog.getInfo()
```
你可以尝试调用下看看输出的内容。在这里留给你一个关于“this”的小思考题调用dog.getInfo()时getInfo函数中的this.type和this.color都是什么值为什么
还有一点我们要注意通常隐藏属性是不能使用JavaScript来直接与之交互的。虽然现代浏览器都开了一个口子让JavaScript可以访问隐藏属性 \__proto_\_但是在实际项目中我们不应该直接通过\__proto_\_ 来访问或者修改该属性,其主要原因有两个:
* 首先,这是隐藏属性,并不是标准定义的;
* 其次,使用该属性会造成严重的性能问题。
我们之所以在课程中使用 \__proto_\_ 属性,主要是为了方便教学,将其他的一些复杂的概念先抛到一边,这样有利于你循序渐进地掌握我们的课程内容,但是我并不推荐你这么做。那应该怎么去正确地设置对象的原型对象呢?
答案是使用构造函数来创建对象,下面我们就来详细解释这个过程。
## 构造函数是怎么创建对象的?
比如我们要创建一个dog对象我可以先创建一个DogFactory的函数属性通过参数进行传递在函数体内通过this设置属性值。代码如下所示
```
function DogFactory(type,color){
this.type = type
this.color = color
}
```
然后再结合关键字“new”就可以创建对象了创建对象的代码如下所示
```
var dog = new DogFactory('Dog','Black')
```
通过这种方式我们就把后面的函数称为构造函数因为通过执行new配合一个函数JavaScript虚拟机便会返回一个对象。如果你没有详细研究过这个问题很可能对这种操作感到迷惑为什么通过new关键字配合一个函数就会返回一个对象呢
关于JavaScript为什么要采用这种怪异的写法我们文章最后再来介绍先来看看这段代码的深层含义。
其实当V8执行上面这段代码时V8会在背后悄悄地做了以下几件事情模拟代码如下所示
```
var dog = {}
dog.__proto__ = DogFactory.prototype
DogFactory.call(dog,'Dog','Black')
```
为了加深你的理解,我画了上面这段代码的执行流程图:
![](https://static001.geekbang.org/resource/image/19/8c/19c63a16ec6b6bb67f0a7e74b284398c.jpg)
观察上图,我们可以看到执行流程分为三步:
* 首先创建了一个空白对象dog
* 然后将DogFactory的prototype属性设置为dog的原型对象这就是给dog对象设置原型对象的关键一步我们后面来介绍
* 最后再使用dog来调用DogFactory这时候DogFactory函数中的this就指向了对象dog然后在DogFactory函数中利用this对对象dog执行属性填充操作最终就创建了对象dog。
## 构造函数怎么实现继承?
好了,现在我们可以通过构造函数来创建对象了,接下来我们就看看构造函数是如何实现继承的?你可以先看下面这段代码:
```
function DogFactory(type,color){
this.type = type
this.color = color
//Mammalia
//恒温
this.constant_temperature = 1
}
var dog1 = new DogFactory('Dog','Black')
var dog2 = new DogFactory('Dog','Black')
var dog3 = new DogFactory('Dog','Black')
```
我利用上面这段代码创建了三个dog对象每个对象都占用了一块空间占用空间示意图如下所示
![](https://static001.geekbang.org/resource/image/9a/2b/9aff57c8992de8b11b70439797a3862b.jpg)
从图中可以看出来对象dog1到dog3中的constant\_temperature属性都占用了一块空间但是这是一个通用的属性表示所有的dog对象都是恒温动物所以没有必要在每个对象中都为该属性分配一块空间我们可以将该属性设置公用的。
怎么设置呢?
还记得我们介绍函数时提到关于函数有两个隐藏属性吗这两个隐藏属性就是name和code其实函数还有另外一个隐藏属性那就是prototype刚才介绍构造函数时我们也提到过。一个函数有以下几个隐藏属性
![](https://static001.geekbang.org/resource/image/ec/e7/ec19366c204bcc0b30b9b46448cbbee7.jpg)
每个函数对象中都有一个公开的prototype属性当你将这个函数作为构造函数来创建一个新的对象时新创建对象的原型对象就指向了该函数的prototype属性。当然了如果你只是正常调用该函数那么prototype属性将不起作用。
现在我们知道了新对象的原型对象指向了构造函数的prototype属性当你通过一个构造函数创建多个对象的时候这几个对象的原型都指向了该函数的prototype属性如下图所示
![](https://static001.geekbang.org/resource/image/1d/4d/1d5e7c1f7006974aec657e8a3e9e864d.jpg)
这时候我们可以将constant\_temperature属性添加到DogFactory的prototype属性上代码如下所示
```
function DogFactory(type,color){
this.type = type
this.color = color
//Mammalia
}
DogFactory. prototype.constant_temperature = 1
var dog1 = new DogFactory('Dog','Black')
var dog2 = new DogFactory('Dog','Black')
var dog3 = new DogFactory('Dog','Black')
```
这样我们三个dog对象的原型对象都指向了prototype而prototype又包含了constant\_temperature属性这就是我们实现继承的正确方式。
## 一段关于new的历史
现在我们知道new关键字结合构造函数就能生成一个对象不过这种方式很怪异为什么要这样呢要了解这背后的原因我们需要了解一段关于关于JavaScript的历史。
JavaScript是Brendan Eich发明的那是个“战乱”的时代各种大公司相互争霸有Sun、微软、网景、甲骨文等公司它们都有推出自己的语言其中最炙手可热的编程语言是Sun的Java而JavaScript就是这个时候诞生的。当时创造JavaScript的目的仅仅是为了让浏览器页面可以动起来所以尽可能采用简化的方式来设计JavaScript所以本质上来说Java和JavaScript的关系就像雷锋和雷峰塔的关系。
那么之所以叫JavaScript是出于市场原因考量的因为一门新的语言需要吸引新的开发者而当时最大的开发者群体就是Java于是JavaScript就蹭了Java的热度事后这一招被证明的确有效果。
虽然叫JavaScript但是其编程方式和Java比起来依然存在着非常大的差异其中Java中使用最频繁的代码就是创建一个对象如下所示
```
CreateInstance instance = new CreateInstance();
```
当时JavaScript并没有使用这种方式来创建对象因为JavaScript中的对象和Java中的对象是完全不一样的因此完全没有必要使用关键字new来创建一个新对象的但是为了进一步吸引Java程序员依然需要在语法层面去蹭Java热点所以JavaScript中就被硬生生地强制加入了非常不协调的关键字new然后使用new来创造对象就变成这样了
```
var bar = new Foo()
```
Java程序员看到这段代码时当然会感到倍感亲切觉得Java和JavaScript非常相似那么使用JavaScript也就天经地义了。不过代码形式只是表象其背后原理是完全不同的。
了解了这段历史之后我们知道JavaScript的new关键字设计并不合理但是站在市场角度来说它的出现又是非常成功的成功地推广了JavaScript。
## 总结
好了,今天的主要内容就介绍到这里,下面我们来回顾下。
今天我们的主要目的是介绍清楚JavaScript中的继承机制这涉及到了原型继承机制虽然基于原型的继承机制本身比较简单但是在JavaScript中这是通过关键字new加上构造函数来体现的。这种方式非常绕且不符合人的直觉如果直接上来就介绍new加构造函数是怎么工作的可能会把你给绕晕了。
于是我先通过每个对象中都有的隐含属性\_\_proto\_\_来介绍了什么是原型和原型链。V8为每个对象都设置了一个\_\_proto\_\_属性该属性直接指向了该对象的原型对象原型对象也有自己的\_\_proto\_\_属性这些属性串连在一起就成了原型链。
不过在JavaScript中并不建议直接使用\_\_proto\_\_属性主要有两个原因。
* 一,这是隐藏属性,并不是标准定义的;
* 二,使用该属性会造成严重的性能问题。
所以在JavaScript中是使用new加上构造函数的这种组合来创建对象和实现对象的继承。不过使用这种方式隐含的语义过于隐晦所以理解起来有点难度。
为什么JavaScript中要使用这种怪异的方式来创建对象为了理解这个问题我们回顾了一段JavaScript的历史。由于当前的Java非常流行基于市场推广的考虑JavaScript采取了蹭Java热度的策略在语言命名上使用了Java字样在语法形式上也模仿了Java。事实上通过这些策略确实为JavaScript带来了市场上的成功。不过你依然要记住JavaScript和Java是完全两种不同的语言。
## 思考题
我们知道函数也是一个对象,所以函数也有自己的\_\_proto\_\_属性那么今天留给你的思考题是DogFactory是一个函数那么“DogFactory.prototype”和“DogFactory.\__proto_\_”这两个属性之间有关联吗欢迎你在留言区与我分享讨论。
感谢你的阅读,如果你觉得这一讲的内容对你有所启发,也欢迎把它分享给你的朋友。