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

Posted 凯小默

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图解 Google V8 # 06:原型链:V8是如何实现对象继承的?相关的知识,希望对你有一定的参考价值。

说明

图解 Google V8 学习笔记

继承是什么?

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

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

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

原型继承是如何实现的?

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

例子:


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

当我们通过对象 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()

输出结果如下:

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

其主要原因有两个:

  1. _proto_ 是隐藏属性,并不是标准定义的 ;
  2. 原型的实现做了很多复杂的优化,比如:通过隐藏类优化了很多原有的对象结构,所以通过直接修改 __proto__ 会直接破坏现有已经优化的结构,造成严重的性能问题。

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

例子:

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

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

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

大致分为三步:

  1. 创建了一个空白对象 dog
  2. 将 DogFactory 的 prototype 属性设置为 dog 的原型对象
  3. 再使用 dog 来调用 DogFactory,这时候 DogFactory 函数中的 this 就指向了对象 dog,然后在 DogFactory 函数中,利用 this 对对象 dog 执行属性填充操作

最终就创建了对象 dog。

模拟代码如下:

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

执行流程图示意图:

构造函数怎么实现继承?

例子:添加 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 占用空间示意图:

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

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

三个 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')

构造函数的__proto__ 和 prototype

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

上面的 DogFactoryFunction 构造函数的一个实例,所以 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 的市场。

拓展阅读

以上是关于图解 Google V8 # 06:原型链:V8是如何实现对象继承的?的主要内容,如果未能解决你的问题,请参考以下文章

图解Google V8,搞懂 JavaScript 执行逻辑

图解 Google V8 # 17:消息队列:V8是怎么实现回调函数的?

图解 Google V8 # 18 :异步编程:V8是如何实现微任务的?

图解 Google V8 # 08:类型转换:V8是怎么实现 1 + “2” 的?

图解 Google V8 # 16:V8是怎么通过内联缓存来提升函数执行效率的?

图解 Google V8 # 20 :垃圾回收:V8的两个垃圾回收器是如何工作的?