原型链:V8是如何实现对象继承的?
继承就是一个对象可以访问另外一个对象中的属性和方法,比如我有一个B对象,该对象继承了A对象,那么B对象便可以直接访问A对象中的属性和方法,你可以参考下图:

不同的语言实现继承的方式是不同的,其中最典型的两种方式是基于类的设计和基于原型继承的设计。
C++、Java、C#这些语言都是基于经典的类继承的设计模式,这种模式最大的特点就是提供了非常复杂的规则,并提供了非常多的关键字,诸如class、friend、protected、private、interface等,通过组合使用这些关键字,就可以实现继承。
使用基于类的继承时,如果业务复杂,那么你需要创建大量的对象,然后需要维护非常复杂的继承关系,这会导致代码过度复杂和臃肿,另外引入了这么多关键字也给设计带来了更大的复杂度。
而JavaScript的继承方式和其他面向对象的继承方式有着很大差别,JavaScript本身不提供一个class 实现。虽然标准委员会在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 的继承依然和基于类的继承没有一点关系。所以当你看到JavaScript出现了class关键字时,不要以为JavaScript也是面向对象语言了。
JavaScript仅仅在对象中引入了一个原型的属性,就实现了语言的继承机制,基于原型的继承省去了很多基于类继承时的繁文缛节,简洁而优美。
原型继承是如何实现的?
那么,基于原型继承是如何实现的呢?我们参看下图:

有一个对象C,它包含了一个属性“type”,那么对象C是可以直接访问它自己的属性type的,这点毫无疑问。
怎样让C对象像访问自己的属性一样,访问B对象呢?
上节我们从V8的内存快照看到,JavaScript的每个对象都包含了一个隐藏属性proto ,我们就把该隐藏属性proto称之为该对象的原型(prototype),proto指向了内存中的另外一个对象,我们就把proto指向的对象称为该对象的原型对象,那么该对象就可以直接访问其原型对象的方法或者属性。
比如我让C对象的原型指向B对象,那么便可以利用C对象来直接访问B对象中的属性或者方法了,最终的效果如下图所示:

观察上图,当C对象将它的proto属性指向了B对象后,那么通过对象C来访问对象B中的name属性时,V8会先从对象C中查找,但是并没有查找到,接下来V8继续在其原型对象B中查找,因为对象B中包含了name属性,那么V8就直接返回对象B中的name属性值,虽然C和B是两个不同的对象,但是使用的时候,B的属性看上去就像是C的属性一样。
同样的方式,B也是一个对象,它也有自己的proto属性,比如它的属性指向了内存中另外一块对象A,如下图所示:

从图中可以看到,对象A有个属性是color,那么通过C.color访问color属性时,V8会先在C对象内部查找,但是没有查找到,接着继续在C对象的原型对象B中查找,但是依然没有查找到,那么继续去对象B的原型对象A中查找,因为color在对象A中,那么V8就返回该属性值。
我们看到使用C.name和C.color时,给人的感觉属性name和color都是对象C本身的属性,但实际上这些属性都是位于原型对象上,我们把这个查找属性的路径称为原型链,它像一个链条一样,将几个原型链接了起来。
在这里还要注意一点,不要将原型链接和作用域链搞混淆了,作用域链是沿着函数的作用域一级一级来查找变量的,而原型链是沿着对象的原型一级一级来查找属性的。
关于继承,还有一种情况,如果我有另外一个对象D,它可以和C共同拥有同一个原型对象B,如下图所示:

因为对象C和对象D的原型都指向了对象B,所以它们共同拥有同一个原型对象,当我通过D去访问name属性或者color属性时,返回的值和使用对象C访问name属性和color属性是一样的,因为它们是同一个数据。
我们再来回顾下继承的概念:继承就是一个对象可以访问另外一个对象中的属性和方法,在JavaScript中,我们通过原型和原型链的方式来实现了继承特性。
实践:利用proto实现继承
下面我们就可以通过一个例子,看看原型是怎么应用在JavaScript中的,你可以先看下面这段代码:
var animal = { type: "Default", color: "Default", getInfo: function () { return `Type is: ${this.type},color is ${this.color}.` } }; var dog = { type: "Dog", color: "Black", }; dog.__proto__ = animal; console.log(dog.getInfo()) // 输出 // Type is: Dog,color is Black.
还有一点我们要注意,通常隐藏属性是不能使用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虚拟机便会返回一个对象。
当V8执行上面这段代码时,V8会在背后悄悄地做了以下几件事情,模拟代码如下所示:
var dog = {} dog.__proto__ = DogFactory.prototype DogFactory.call(dog,'Dog','Black')

观察上图,我们可以看到执行流程分为三步:
- 首先,创建了一个空白对象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对象,每个对象都占用了一块空间,占用空间示意图如下所示:

从图中可以看出来,对象dog1到dog3中的constant_temperature属性都占用了一块空间,但是这是一个通用的属性,表示所有的dog对象都是恒温动物,所以没有必要在每个对象中都为该属性分配一块空间,我们可以将该属性设置公用的。
怎么设置呢?
还记得我们介绍函数时提到关于函数有两个隐藏属性吗?这两个隐藏属性就是name和code,其实函数还有另外一个隐藏属性,那就是prototype,刚才介绍构造函数时我们也提到过。一个函数有以下几个隐藏属性:

每个函数对象中都有一个公开的prototype属性,当你将这个函数作为构造函数来创建一个新的对象时,新创建对象的原型对象就指向了该函数的prototype属性。当然了,如果你只是正常调用该函数,那么prototype属性将不起作用。
现在我们知道了新对象的原型对象指向了构造函数的prototype属性,当你通过一个构造函数创建多个对象的时候,这几个对象的原型都指向了该函数的prototype属性,如下图所示:

这时候我们可以将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属性,这就是我们实现继承的正确方式。

浙公网安备 33010602011771号