zl程序教程

您现在的位置是:首页 >  其他

当前栏目

图解 Google V8 # 06:原型链:V8是如何实现对象继承的?

2023-03-14 22:55:53 时间

说明

图解 Google V8 学习笔记



继承是什么?

简单的说:继承就是一个对象可以访问另外一个对象中的属性和方法,在JavaScript 中,我们通过原型和原型链的方式来实现了继承特性。


不同的语言实现继承的方式是不同的,其中最典型的两种方式:


  • 基于类的设计:C++、Java、C#
  • 基于原型继承的设计:JavaScript



原型继承是如何实现的?


JavaScript 的每个对象都包含了一个隐藏属性 __proto__,我们就把该隐藏属性 __proto__ 称之为该对象的原型 (prototype),__proto__ 指向了内存中的另外一个对象,我们就把 __proto__ 指向的对象称为该对象的原型对象。


例子:

8941a339a7de4e06ba77187bd0885979.png


我们让 C 对象的原型指向 B 对象,让 B 对象的原型指向 A 对象,那么 C 对象就可以直接访问 B 以及 A 的方法跟属性了。

97d9328a67d54eb1bdf741411e2e6225.png


当我们通过对象 C 来访问对象 A 中的 color 属性时,V8 会先从对象 C 中查找,没有查找到,接着继续在 C 对象的原型对象 B 中查找,依旧没有查找到,那么继续去对象 B 的原型对象 A 中查找,因为 color 在对象 A 中,那么 V8 就返回该属性值。我们把这个查找属性的路径称为原型链。



原型链 vs 作用域链

  • 原型链:是沿着对象的原型一级一级来查找属性的
  • 作用域链:是沿着函数的作用域一级一级来查找变量的


实践:利用 __proto__ 实现继承

下面先创建了两个对象 animal 和 dog,如果让 dog 对象继承于 animal 对象,应该怎么操作?

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。

dog.__proto__ = animal



使用 dog 来调用 animal 中的 getInfo 方法

dog.getInfo()


输出结果如下:


b497f316ad6c42808a5edfe7cb6521b7.png



注意:通常隐藏属性是不能使用 JavaScript 来直接与之交互的。虽然现代浏览器都开了一个口子,让 JavaScript 可以访问隐藏属性 _proto_,但是在实际项目中,我们不应该直接通过 _proto_ 来访问或者修改该属性,应该使用构造函数来创建对象。


其主要原因有两个:


   _proto_ 是隐藏属性,并不是标准定义的 ;

   原型的实现做了很多复杂的优化,比如:通过隐藏类优化了很多原有的对象结构,所以通过直接修改 __proto__ 会直接破坏现有已经优化的结构,造成严重的性能问题。


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


例子:


  1. 先创建一个 DogFactory 的函数,属性通过参数进行传递,在函数体内,通过 this 设置属性值。
function DogFactory(type, color){
    this.type = type
    this.color = color
}



  1. 再结合关键字 new 就可以创建对象(DogFactory 函数称为构造函数)
var dog = new DogFactory('Dog', 'Black')



V8 执行上面这段代码时,做了什么?


大致分为三步:


   创建了一个空白对象 dog


   将 DogFactory 的 prototype 属性设置为 dog 的原型对象


   再使用 dog 来调用 DogFactory,这时候 DogFactory 函数中的 this 就指向了对象 dog,然后在 DogFactory 函数中,利用 this 对对象 dog 执行属性填充操作


最终就创建了对象 dog。


模拟代码如下:

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



执行流程图示意图:


e07e6a8bf8e84fbc935da7a7593b0f45.png


构造函数怎么实现继承?

例子:添加 constant_temperature 为 1 表示恒温动物

function DogFactory(type,color){
    this.type = type
    this.color = color
    // 恒温动物
    this.constant_temperature = 1
}
var dog1 = new DogFactory('Dog','Black')
var dog2 = new DogFactory('Dog','Black')
var dog3 = new DogFactory('Dog','Black')


dog1、dog2、dog3 占用空间示意图:

e95f6fcdbed64daf84f1570ae1e0f656.png


可以看到 constant_temperature 属性都占用了一块空间,因为 dog 是恒温动物,每个对象 没必要为 constant_temperature 属性都分配一块空间,该属性既然是通用的,可以设置属性为公用的。

每个函数对象中都有一个公开的 prototype 属性,当这个函数作为构造函数来创建一个新的对象时,新创建对象的原型对象就指向了该函数的 prototype 属性。

d7b65fc279c14bdca753001727b69762.png


三个 dog 对象的原型对象都指向了 prototype,我们只要让 prototype 包含 constant_temperature 属性,就能实现继承了。

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

var dog1 = new DogFactory('Dog','Black')
var dog2 = new DogFactory('Dog','Black')
var dog3 = new DogFactory('Dog','Black')


f4c0910220fb4dea9569321fd123bae2.png



构造函数的__proto__ 和 prototype


  • 函数作为对象他得拥有一个 __proto__,该属性是隐藏属性,并不是标准定义的 ;
  • 函数作为一个构造函数,它得拥有一个 prototype,该属性是标准定义的


上面的 DogFactory 是 Function 构造函数的一个实例,所以 DogFactory.__proto__ === Function.prototype;


DogFactory.prototype 是调用 Object 构造函数的一个实例,所以 DogFactory.prototype.__proto__ === Object.prototype;


因此 DogFactory._proto_ 和 DogFactory.prototype 没有直接关系。



总结


在 JavaScript 中,是使用 new 加上构造函数的这种组合来创建对象和实现对象的继承。


JavaScript 完全没有必要使用关键字 new 来创建一个新对象的,但是为了进一步吸引 Java 程序员,依然需要在语法层面去蹭 Java 热点,所以 JavaScript 中就被硬生生地强制加入了非常不协调的关键字 new,虽然 new 关键字设计并不合理,但它的出现成功地推广 JavaScript 的市场。