前言

我们都知道,js在ES6以前是没有类的概念的,如果想用面向对象的思想来开发它就要去了解他的继承实现方式。javascript是一门基于原型继承的语言,这在语言设计之初就已经被确定了的,即使在后来的ES6中加入了类的概念,加入了extend关键字,但仍旧无法改变他作为原型继承的本质,ES6的继承可以看作是ES5继承方式的语法糖,将之前的写好的继承方式合法化,规范化。

什么是原型

我一般喜欢把原型比作模型,或者说模具更加贴切。比如说一个水杯,他是圆的还是方的就是由这个模具决定的,如果这个模具改变了样子,那么生产出来的水杯也会变了样子。这样来描述,你可能会觉得,叫什么原型,叫构造器不是更贴切。没错,在这个例子中,模具就是水杯的构造器(constructor)。
那么原型和说的这个有什么关系呢。假设我们的这个水杯是有额外功能的,加热和制冷。那么问题就来了,构造器只能决定水杯的样子,是没办法给他提供加热和制冷的功能的。这个时候就需要原型(prototype)了。
一般情况,构造器决定样子,名称等固定的属性。原型决定的是功能,可以进行操作的方法。构造器(constructor)和原型(prototype)共同决定了一个物体的存在形式。
构造器(constructor)和原型(prototype)的关系怎么来描述呢
原型对象的 constructor 是 构造器。
构造器的 prototype 是原型对象。
image.png

在js中有一句话叫万物皆对象,每个对象都有原型。我们创建函数,如果采用new的方式调用,当然这种调用方式有个名字叫实例化。

// 创建一个函数
function B(name) {
    this.name = name;
};
// 实例化
var bb = new B('实例化的b');
console.log(bb.name); // 实例化的b;

如上面的代码,bb是通过B实例化之后得到的对象。在这里B就是一个构造器,他所拥有的名字(this.name)属性会带给bb;这也符合之前杯子的例子,杯子的属性会从构造器中获得。

假如我们想要做出来的bb具有一定的功能,那么就需要在原型上下功夫了。根据上面构造器和原型的关系。我们可以这样做。

// 创建一个函数
function B(name) {
    this.name = name;
};
// 在原型上添加一个方法
B.prototype.tan = function() {
    alert('弹出框');
}
// 实例化
var bb = new B('实例化的b');
console.log(bb.name); // 实例化的b;
bb.tan(); // alert('弹出框');

在上面的代码中,我们在B的原型上添加了一个tan的方法,在实例化出来的bb也具备了这个方法。这里我们就简单实现了一个类。用下面一张图,说明一下。实例对象(bb), 原型(prototype), 构造函数(constructor)的关系。

image.png

B是我们构造的一个类,这里称为构造函数。他用prototype指向了自己的原型。而他的原型也通过constructor指向了它。

B.prototype.constructor === B;  // true;

bb和B没有直接的关联,虽然B是bb的构造函数,这里用虚线表示。bb有一个__ proto__属性,指向了B的prototype

bb.__ proto__ === B.prototype; // true;
bb.__ proto__.constructor = B; // true;

总之
1,每创建一个函数B,就会为该函数创建一个prototype属性,这个属性指向函数的原型对象;
2,原型对象会默认去取得constructor属性,指向构造函数。
3,当调用构造函数创建一个新实例bb后,该实例的内部将包含一个指针__ proto__,指向构造函数的原型对象。

##默认原型
我们知道,所有引用对象都默认继承了Object,所有函数的默认原型都是Object的实例。
之前说过构造函数和原型之间具备对应关系,如下:
image.png

既然函数的默认原型都是Object的实例,B的原型对象也应该是Object的实例子,也就是说。B的原型的__ proto__应该指向Objct的原型。

image.png

Object的原型对象的原型是最底部了,所以不存在原型,指向NULL;

console.log(Object.prototype.__ proto__); // null;

image.png

##Function对象
我们知道,函数也是对象,任何函数都可以看作是由构造函数Function实例化的对象,所以Function与其原型对象之间也存在如下关系
image.png

如果将Foo函数看作实例对象的话,其构造函数就是Function(),原型对象自然就是Function的原型对象;同样Object函数看作实例对象的话,其构造函数就是Function(),原型对象自然也是Function的原型对象。
image.png

如果Function的原型对象看作实例对象的话,如前所述所有对象都可看作是Object的实例化对象,所以Function的原型对象的__ proto __指向Object的原型对象。
image.png

到这里prototype,__ proto __, constructor三者之间的关系我们就说完了。

##实现继承

function Animal() {
    this.type = '动物';
}
Animate.prototype.eat = function() {
  console.log('吃食物');
}

上面定义了一个动物类,作为父类

function Cat(name) {
    this.name = name || ‘小猫’;
}

定义了一个猫作为子类,这里我们要继承动物类的eat方法和type属性

function Cat(name){
  Animal.call(this);
  this.name = name || '小猫';
}

在实例化Cat时通过call执行了Animal类, 这样Animal中的this就被修改为当前Cat的this。所有的属性也会加在Cat上。

(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat.prototype = new Super();
})();

通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化实例方法/属性。而父类的方法仍旧可以赋值给子类。

Cat.prototype = new Super(); 可以实现方法的继承,是因为,根据前面的知识我们知道 new Cat()的__ proto __是指向 Cat的原型的。

(new Cat()).__ proto __ === Cat.prototype; // true

new Cat()所有的方法都是从原型上取到的。
我们通过 Cat.prototype = new Super(); 公式变成了。
(new Cat()).__ proto __ = Cat.prototype = new Super();
所以现在(new Cat()).__ proto __ 指向了 Super的prototype。也就是new Cat的方法是继承自Super.prototype。
Super.prototype又在前一句等于Animal.prototype。所以实现了Cat继承Animal。
这里我们就实现了js属性和方法的继承。不过还在最后一个小问题。

我们知道 prototype 和 constructor 是相互指向的。
Cat.prototype.constructor 应该等于 Cat;
但是随着我们的修改了Cat.prototype = Super.prototype;
现在Cat.prototype.constructor是等于Super的。
所以我们还应该纠正这个问题,一句话搞定。

Cat.prototype.constructor = Cat; // 需要修复下构造函数

以上就是js的原型继承,完整代码如下。

// 创建一个父类
function Animal() {
    this.type = '动物';
}
// 给父类添加一个方法
Animate.prototype.eat = function() {
  console.log('吃食物');
}

// 创建一个子类
function Cat(name){
  // 继承Animal的属性
  Animal.call(this);
  this.name = name || '小猫';
}

// 继承 Animal 的方法
(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat.prototype = new Super();
})();
// 修正构造函数
Cat.prototype.constructor = Cat; 

好啦,js的继承原理和prototype,proto, constructor之间的关系我们就说完了,ES6底层的实现方式原理基本相同。