javascript不包含传统的类继承模型,而是使用 prototype 原型模型。
虽然这经常被当作是 JavaScript 的缺点被提及,其实基于原型的继承模型比传统的类继承还要强大。实现传统的类继承模型是很简单,但是实现 JavaScript 中的原型继承则要困难的多。ES6新增加的class语法糖使得这种继承方式实现起来更加简单,但其原理还是原型继承,而不是类继承。
由于 JavaScript 是唯一一个被广泛使用的基于原型继承的语言,所以理解两种继承模式的差异是需要一定时间的,今天我们就来了解一下原型和原型链。
函数对象和普通对象
javascript中的对象分为函数对象和普通对象。其区分起来也很简单,所有Function的实例都是函数对象,其他的都是普通对象,Object和Function都是函数对象。
函数对象在javascript中起到模拟类的作用,我们可以使用new来创建一个函数对象的实例,这个实例是一个普通对象,它通过__proto__从函数对象的prototype中继承属性。
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__两个属性来完成的,只要知道这两个属性分别指向哪儿,他们是如何工作的,那么就能弄明白原型继承了。
这里依旧使用上面的例子:
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就很好理解了。
而对于函数对象来说
typeof Person.prototype //"object"
Person.prototype.__proto__ == Object.prototype //true
这里把原型对象prototype单独拿出来,它其实是一个object,它存放了函数对象需要被继承的属性,以及构造函数。
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了。
p1.__proto__ == Person.prototype; //true
p1.__proto__.__proto__ == Object.prototype; //true
p1.__proto__.__proto__.__proto__ == null; //true
由此可以再得到一个结论
__proto__和prototype在原型链中是自动分配的,__proto__用于连接对象和原型对象,prototype用于保存原型属性,构造函数和__proto__属性
这里再引用一张图
图片来自: juejin.cn/post/684490…通过这张图应该就能很清楚的看到对象之间的继承关系了。
原型链
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是这样的:
{
name:"xiaoming",
age:17,
__proto__:object //Person.prototype
}
现在来试试多重继承
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永远指向函数本身,所以在上面的例子中可以使用函数本身作为构造函数,构造函数中属性会直接赋值在实例上。
var Person = function(name,age){
this.name = 'xiaoming';
this.age = 17
}
var p1 = new Person();
此时p1对象结构为:
{
name:'xiaoming',
age:17,
__proto__:object //Person.prototype
}
Person.prototype.constructor == Person //true
Person.prototype.constructor.prototype.constructor == Person //true
总结
对象通过__proto__来连接原型对象prototype,也通过__proto__来进行原型链上的属性查找,直到原型链的末端:Object.prototype.__proto__–>null。
