javascript不包含传统的类继承模型,而是使用 prototype 原型模型。
虽然这经常被当作是 JavaScript 的缺点被提及,其实基于原型的继承模型比传统的类继承还要强大。实现传统的类继承模型是很简单,但是实现 JavaScript 中的原型继承则要困难的多。ES6新增加的class语法糖使得这种继承方式实现起来更加简单,但其原理还是原型继承,而不是类继承。
由于 JavaScript 是唯一一个被广泛使用的基于原型继承的语言,所以理解两种继承模式的差异是需要一定时间的,今天我们就来了解一下原型和原型链。

函数对象和普通对象

javascript中的对象分为函数对象和普通对象。其区分起来也很简单,所有Function的实例都是函数对象,其他的都是普通对象,Object和Function都是函数对象。

函数对象在javascript中起到模拟类的作用,我们可以使用new来创建一个函数对象的实例,这个实例是一个普通对象,它通过__proto__从函数对象的prototype中继承属性。

1
2
3
4
5
6
7
8
9
10
11
var Person = function(){};
Person.prototype.name = 'xiaoming';
Person.prototype.age = 17;

var p1 = new Person()

p1.name; //'xiaoming'
p1.age; //17
p1.hasOwnProperty('name'); //false,name属性是从原型处获得的
p1.__proto__; //{name: "xiaoming", age: 17, constructor: ƒ}
p1.__proto__ == Person.prototype; //true

prototype和__proto__

由上面的例子可以得知,原型继承是由prototype和__proto__两个属性来完成的,只要知道这两个属性分别指向哪儿,他们是如何工作的,那么就能弄明白原型继承了。

这里依旧使用上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Person = function(){};
Person.prototype.name = 'xiaoming';
Person.prototype.age = 17;

var p1 = new Person()

p1.prototype; //undefined,普通对象没有prototype
p1.__proto__ == Person.prototype; //true

Person.prototype; //{name: "xiaoming", age: 17, constructor: ƒ}
Person.__proto__ == Function.prototype; //true
Person.prototype.__proto__ == Object.prototype; //true

Function.__proto__ == Function.prototype; //true
Function.prototype.__proto__ == Object.prototype; //true

Object.__proto__ == Function.prototype; //true;
Object.prototype.__proto__ == null; //true

由上述的例子可以得知:

  • 普通对象没有prototype属性,只有__proto__属性,而函数对象都有
  • 普通对象的__proto__属性都指向其原型函数对象的prototype
  • 函数对象的__proto__属性都指向Function.prototype,Object的__proto__属性也是
  • 函数对象的prototype.__proto__属性都指向Object.prototype,除了Object.prototype.__proto__ –> null

我们通过new来创建函数对象的实例,而一个实例不是不能再实例化的,所以普通对象没有prototype就很好理解了。

而对于函数对象来说

1
2
typeof Person.prototype //"object"
Person.prototype.__proto__ == Object.prototype //true

这里把原型对象prototype单独拿出来,它其实是一个object,它存放了函数对象需要被继承的属性,以及构造函数。

1
2
3
4
5
6
7
8
9
Person.__proto__ =  Funtion.prototype

Person.prototype = {
name:"xiaoming",
age:17,

constructor:Person,
__proto__:object //Object.prototype
}

所有函数对象的__proto__属性都指向Function.prototype,包括Function自身的__proto__属性和Object的__proto__属性

作为所有对象的原型Object,普通对象的__proto__属性都指向Object.prototype。而Object.prototype.__proto__ = null,万物都是由无到有,原型链的末端就是null了。

1
2
3
p1.__proto__ == Person.prototype;            //true
p1.__proto__.__proto__ == Object.prototype; //true
p1.__proto__.__proto__.__proto__ == null; //true

由此可以再得到一个结论

__proto__和prototype在原型链中是自动分配的,__proto__用于连接对象和原型对象,prototype用于保存原型属性,构造函数和__proto__属性

这里再引用一张图

图片来自:https://juejin.im/post/5a944f485188257a804aba6d#heading-5

通过这张图应该就能很清楚的看到对象之间的继承关系了。

原型链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Person = function(name,age){
this.name = name;
this.age = age
}

Person.prototype.getName = function(){
return this.name;
};
Person.prototype.getAge = function(){
return this.age;
};

var p1 = new Person('xiaoming',17);
p1.name; //'xiaoming';

这里在Person上建立构造函数,在执行new操作时会执行这个构造函数。现在得到的p1是这样的:

1
2
3
4
5
{
name:"xiaoming",
age:17,
__proto__:object //Person.prototype
}

现在来试试多重继承

1
2
3
4
5
6
7
8
9
10
11
var A = function(){};
A.prototype.a = 1;
A.prototype.b = 2;

var B = function(){}
B.prototype.__proto__ = A.prototype;
B.prototype.a = 3;

var c = new B();
c.a //3
c.b //2

这里我们让B继承于A,再实例化一个B。执行c.a时,c自身并没有这个属性,于是它顺着原型链去寻找a属性,首先是a.__proto__即B.prototype,B的原型对象上有这个属性,于是它便不再继续寻找,返回了3。执行c.b时同理,在B的原型对象上不存在b属性,于是继续顺着原型链寻找,在A的原型对象上找到了b属性,返回2。

constructor

constructor永远指向函数本身,所以在上面的例子中可以使用函数本身作为构造函数,构造函数中属性会直接赋值在实例上。

1
2
3
4
5
6
var Person = function(name,age){
this.name = 'xiaoming';
this.age = 17
}

var p1 = new Person();

此时p1对象结构为:

1
2
3
4
5
{
name:'xiaoming',
age:17,
__proto__:object //Person.prototype
}
1
2
Person.prototype.constructor == Person //true
Person.prototype.constructor.prototype.constructor == Person //true

总结

对象通过__proto__来连接原型对象prototype,也通过__proto__来进行原型链上的属性查找,直到原型链的末端:Object.prototype.__proto__–>null。