说明
图解 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_ 来访问或者修改该属性,应该使用构造函数来创建对象。
其主要原因有两个:
_proto_ 是隐藏属性,并不是标准定义的 ;
原型的实现做了很多复杂的优化,比如:通过隐藏类优化了很多原有的对象结构,所以通过直接修改 __proto__ 会直接破坏现有已经优化的结构,造成严重的性能问题。
构造函数是怎么创建对象的?
例子:
- 先创建一个 DogFactory 的函数,属性通过参数进行传递,在函数体内,通过 this 设置属性值。
function DogFactory(type, color){ this.type = type this.color = color }
- 再结合关键字
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')
执行流程图示意图:
构造函数怎么实现继承?
例子:添加 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
,该属性是标准定义的
上面的 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 的市场。