JavaScript原型链

JS如何实现继承?

继承: 继承就是一个对象可以访问另外一个对象中的属性和方法。

比如对象A 有run方法 A.run ;如果B继承了A,那么B可以 B.run,就仿佛B自带这个方法一样。

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

  • 基于类的设计

  • 基于原型的设计

C++、Java、C#这些语言都是基于经典的类设计的设计模式,这在模式最大的特点就是提供了非此复杂的规则,并且提供了非常多的关键字比如 class、protected、private、interface等。

使用基于类的继承,如果业务复杂需要维护非常复杂的继承关系。

而JS的继承方式和其他的语言有很大差别,JS本身不提供一个class实现。(ES6提供的class是一个语法糖)。JS的继承和class没有一点关系。

JS仅仅在对象中引入了一个原型的属性就实现了语言继承机制,省去了很多复杂。

原型继承如何实现

比如有对象 A、B、C; 他们拥有如下属性

A { color, __proto__}
B { name, __proto__}
C { type, __proto__}

C可以直接访问自己的属性是无疑的,访问B对象呢? JS的每个对象都包含了一个隐藏属性 __proto__ ,我们就把隐藏的属性 __proto__ 称之为 该对象的原型(prototype), __proto__ 指向内存中另外一个对象,我们把 __proto__ 指向的对象称为该对象的原型对象,那么该对象就可以直接访问其原型对象的方法或者属性。

比如 让C对象的原型指向B对象

prototype_abc01.png

对象C访问B中的属性name时,JS引擎会先从对象C中查找,但是没有查找到,接下来JS引擎继续在原型对象B中查找,因为B中包含name属性,那么JS引擎就直接返回B中name属性,虽然C和B是两个不同的对象,但是使用的时候,B的属性看上去就像是C属性意义。

同样的方式,B也是一个对象,也有自己的 __proto__ 比如他属性指向内存中的另一个对象A。

C.nameC.color给人的感觉是C本身的属性,但是实际上这些属性都是位于原型对象上,我们把这种查找属性的路径称为 “原型链”,他像一个链条一样,将几个原型对象连接了起来。

不要搞混:原型链、作用域链

  • 作用域链:沿着函数的作用域一级一级来查找变量

  • 原型链:沿着对象的原型一级一级来查找属性

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

 var animal = {
     type: "Default",
     color: "Default",
     getInfo: function() {
         return `Type is:${this.type},color is ${this.color}`
     }
 }

 var dog = {
     type: "Dog",
     color: "Black"
 }

这段代码中dog如果想要继承animal:

dog.__proto__ = animal

设置后可以使用 dog.getInfo()

但是__proto__ 本质上是隐藏属性,实际项目中,不应该直接访问或者修改。主要原因:

  1. 这是隐藏属性,并不是标准定义

  2. 使用这个属性会造成严重的性能问题

这只是方便理解概念。

如何应该正确的设置原型对象? 应该区使用构造函数。

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

函数内部,通过 this 设置属性,再结合关键字 new 就可以创建对象。

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

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

这几句在V8引擎里行为可以模拟为:

var dog = {}

dog.__proto__ = DogFactory.prototype;

DogFactory.call(dog.'Dog','black')

构造函数怎么实现继承

一个函数其实有几个隐藏属性,比如 codename,还有 prototype。每个函数对象都有一个公开的 prototype属性,当你将这个函数作为构造函数来创建一个新的对象的时候,新创建对象的原型对象就指向了该函数的prototype属性。

如果你只是正常调用该函数,那么prototype属性将不起作用。

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

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

prototype_extend.png

代码中,三个dog对象的原型对象都指向了 prototype。

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');

DogFactory.prototype中添加属性,就会被继承。这就是继承正确方式。

总结

  1. 构造函数,使用this绑定属性

  2. 构造函数本质上是生成一个对象,再 通过 call调用执行,绑定对象属性

  3. 函数一旦被用作构造函数,隐藏属性 prototype对象发挥作用,这是一个储存变量、方法的对象

  4. 新建的实例对象的 __proto__ 指向 构造函数.prototype

这样实现了继承。

示意图

原型图示.png

(图:最小原型结构示意图)

一段关于new的历史

JavaScript的名字来源于蹭JavaScript的热度。在语法层上,加入了Java中的new。

原型链各种示意图

// 可以根据log打印出真实的对应关系
function Person() {
  
}

p = new Person()

console.log(p.__proto__=== Person.prototype)// true
console.log(p.__proto__.__proto__=== Object.prototype) // true
console.log(p.__proto__.__proto__.__proto__=== null) // true

prototype.png

prototype_stack_heap.png

prototype_links_overview.jpeg

我又整理了一份

原型链关系_1_.png

instanceof的原理


> function Animal(){}
undefined
> 
> cat = new Animal()
Animal {}
> 
> cat instanceof Animal
true
> 

instanceof的原理就是

cat.__proto__ === Animal.prototype

看对实例对象__proto__和目标构造函数的prototype指向是否一致。

由于存在 原型对象,只要是一个对象就有 __proto__属性,所以可以写成递归的模式,顺着原型链比较,只要比较正确就认为是:

// 通过判断对象的原型链上是否存在prototype

function myInstanceof(left,right) {
  // 获得类型的原型
  let prototype = right.prototype;
  // 获得对象的原型
  left = left.__proto__;
  // 判断对象的类型是否等于类型的原型
  while (true) {
    if(left === null) { return false} // null是因为最末端的就是null
    if (prototype === left) { return true }
    left = left.__proto__;

  }
}

参考

Mark24

Everything can Mix.