原型链:V8是如何实现对象继承的?

简单地理解,继承就是一个对象可以访问另外一个对象中的属性和方法,比如我有一个B对象,该对象继承了A对象,那么B对象便可以直接访问A对象中的属性和方法,你可以参考下图:

不同的语言实现继承的方式是不同的,其中最典型的两种方式是基于类的设计基于原型继承的设计。
C++、Java、C#这些语言都是基于经典的类继承的设计模式,这种模式最大的特点就是提供了非常复杂的规则,并提供了非常多的关键字,诸如class、friend、protected、private、interface等,通过组合使用这些关键字,就可以实现继承。
而JavaScript的继承方式和其他面向对象的继承方式有着很大差别,JavaScript本身不提供一个class 实现。虽然标准委员会在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 的继承依然和基于类的继承没有一点关系。
JavaScript仅仅在对象中引入了一个原型的属性,就实现了语言的继承机制,基于原型的继承省去了很多基于类继承时的繁文缛节,简洁而优美。

原型继承是如何实现的?

参考下图:

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

同样的方式,B也是一个对象,它也有自己的__proto__属性,比如它的属性指向了内存中另外一块对象A,如下图所示:

我们看到使用C.name和C.color时,给人的感觉属性name和color都是对象C本身的属性,但实际上这些属性都是位于原型对象上,我们把这个查找属性的路径称为原型链,它像一个链条一样,将几个原型链接了起来。
在这里还要注意一点,不要将原型链接和作用域链搞混淆了,作用域链是沿着函数的作用域一级一级来查找变量的,而原型链是沿着对象的原型一级一级来查找属性的,虽然它们的实现方式是类似的,但是它们的用途是不同的。
关于继承,还有一种情况,如果我有另外一个对象D,它可以和C共同拥有同一个原型对象B,如下图所示:

因为对象C和对象D的原型都指向了对象B,所以它们共同拥有同一个原型对象,当我通过D去访问name属性或者color属性时,返回的值和使用对象C访问name属性和color属性是一样的,因为它们是同一个数据。
继承就是一个对象可以访问另外一个对象中的属性和方法,在JavaScript中,我们通过原型和原型链的方式来实现了继承特性。

实践:利用__proto__实现继承

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的__proto__指向对象animal,如下:

dog.__proto__ = animal

设置之后,就可以使用dog调用animal的getInfo()方法了。
调用animal的getInfo函数中的this.type和this.color是dog对象中的值。
还有一点我们要注意,通常隐藏属性是不能使用JavaScript来直接与之交互的。不建议以上的调用方式。原因有两个:

  • 首先,这是隐藏属性,并不是标准定义的;
  • 其次,使用该属性会造成严重的性能问题。

构造函数是怎么创建对象的?

比如我们要创建一个dog对象,我可以先创建一个DogFactory的函数,属性通过参数进行传
递,在函数体内,通过this设置属性值。代码如下所示:

function DogFactory(type,color){
    this.type = type
    this.color = color
}

然后再结合关键字“new”就可以创建对象了,创建对象的代码如下所示:

var dog = new DogFactory('Dog','Black')

通过这种方式,我们就把后面的函数称为构造函数,因为通过执行new配合一个函数,JavaScript虚拟机便会返回一个对象。
为什么通过new关键字配合一盒函数会返回一个对象?
其实当V8执行上面这段代码时,V8会在背后悄悄地做了以下几件事情,模拟代码如下所示:

var dog = {}
dog.__proto__ = DogFactory.prototype
DogFactory.call(dog,'Dog','Black')


可以参考上图,分为三个步骤:

  1. 首先,创建了一个空白对象dog;
  2. 然后,将DogFactory的prototype属性设置为dog的原型对象,这就是给dog对象设置原型对象的关键一步,我们后面来介绍;
  3. 最后,再使用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对象都是恒温动物,所以没有必要在每个对象中都为该属性分配一块空间,我们可以将该属性设置公用的。
怎么设置通用属性?
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属性,这就是我们实现继承的正确方式。

一段关于new的历史

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()

虽然看起来非常相似,但是其背后的原理是完全不同的。

此文章为5月Day12学习笔记,内容来源于极客时间《图解 Google V8》,日拱一卒,每天进步一点点💪💪

豪猪
4 声望4 粉丝

undefined