前戏
世界上是先有的鸡,还是先有的蛋?这个世界难题,可能无从考究了。但如果现在问是先有的函数,还是先有的对象,那么,我的答案是先有的函数(构造函数)。那知道了这个,我们讲解原型,就从函数说起。
原型
每个函数都会创建一个prototype对象属性,它就是原型对象,也是我们调用构造函数生成的对象的原型,我们直接打印看下:
function Person(name) {
this.name = name;
}
console.dir(Person);
我们看上面的图片发现一个特别有意思的事情,我们自定义的Person()构造函数的prototype原型对象默认只有一个constructor属性,而且onstructor属性指回了Person()构造函数,即Person.prototype.constructor === Person。
有了构造函数,我们就可以创建实例对象:
function Person(name) {
this.name = name;
}
let tom = new Person("Tom");
console.log(tom);
创建的实例对象,我们发现它除了有个name属性外,还有个__proto__,这就是对象的原型,指向Person.prototype原型对象。__proto__是浏览器自身实现的,不是规范写法,我们要求使用Object.getPrototypeOf()替换__proto__,获取原型。
原型链
上面讲原型,只讲了实例与构造函数的关系,实例对象原型就是构造函数prototype指向的原型对象,即:Object.getPrototypeOf(tom) === Person.prototype;那既然Person.prototype也是对象,那也有原型,它的原型是什么呢?
function Person(name) {
this.name = name;
}
let tom = new Person("Tom");
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype);//true
可以看到Person.prototype的原型指向Object.prototype原型对象
原型链继承
前面讲到了原型链,那原型链能解决什么问题呢,我们下面要讲的继承就可以通过原型链实现,原理就是修改子构造函数原型对象即prototype属性,指向父级构造函数new的实例:
//定义父构造函数
function Animal() {
this.Arr = [1, 2, 3];
}
//往父构造函数原型上添加run方法
Animal.prototype.run = function (target) {
console.log(target + '会跑');
}
//定义子构造函数
function People(target) {
this.target = target;
this.age = 20;
}
//修改子构造函数prototype属性原型对象指向父构造函数实例对象
People.prototype = new Animal();
//new出的子构造函数实例对象就可以调用父构造函数的属性包括父构造函数原型上的方法
let tom = new People('Tom');
console.log(tom.Arr);//[1, 2, 3]
tom.run('Tom');//Tom会跑
上面这种继承方式有两个问题,第一个问题其实在我们书写的时候就发现了,我们最终new出的实例tom可以给构造函数传参,但没办法给父构造函数传参,有人可能会说,可以在People.prototype = new Anima("参数");时传参,但这种方式使得new出的所有People都有这个属性,并不是每个最终的实体单独占有的。第二个问题就是父构造函数的Arr属性也是所有实例共有的,当一个实例改变Arr,会同时影响所有实例,我们用代码证明下:
//上面的代码我们不再重复书写
//我们在new一个jerry实例
let jerry = new People('Jerry');
//修改tom实例
tom.Arr.push(4);
console.log(tom.Arr);//[1, 2, 3, 4]
//jerry实例中的Arr也发生改变,说明两个实例共用一份Arr
console.log(jerry.Arr);//[1, 2, 3, 4]
好,既然有问题,那怎么解决呢,在这之前我想再讲一种继承方式:
经典继承
他有个更形象的名字,“盗用构造函数”,顾名思义,就是盗用父构造函数里的属性,那怎么盗用呢,上代码:
//定义父构造函数
function Animal(target) {
this.Arr = [1, 2, 3];
this.jump = function () {
console.log(target + '会跳');
}
}
//往父构造函数原型上添加run方法
Animal.prototype.run = function (target) {
console.log(target + '会跑');
}
//定义子构造函数
function People(target) {
//继承Animal(盗用父构造函数里的属性)
Animal.call(this, target);
this.target = target;
this.age = 20;
}
//new 实例的时候可以传参
let tom = new People('Tom');
console.log(tom.Arr);//[1, 2, 3]
tom.jump();//Tom会跳
//我们在new一个jerry实例
let jerry = new People('Jerry');
//修改tom实例
tom.Arr.push(4);
console.log(tom.Arr);//[1, 2, 3, 4]
//jerry实例中的Arr不变,说明两个实例分别有一份Arr
console.log(jerry.Arr);//[1, 2, 3]
jerry.run();//Uncaught TypeError: jerry.run is not a function
经典继承很好的解决了原型链继承的两个问题(上面讲到的),但,代码最后一行的报错也暴露了经典继承的一个问题,定义在父构造函数原型上的属性无法继承,那就有了下面的继承方式:
组合继承
即原型链继承和经典继承组合
//定义父构造函数
function Animal(target) {
this.Arr = [1, 2, 3];
this.jump = function () {
console.log(target + '会跳');
}
}
//往父构造函数原型上添加run方法
Animal.prototype.run = function (target) {
console.log(target + '会跑');
}
//定义子构造函数
function People(target) {
//继承Animal(盗用父构造函数里的属性)
Animal.call(this, target);
this.target = target;
this.age = 20;
}
//原型链继承
People.prototype = new Animal();
//new 实例的时候可以传参
let tom = new People('Tom');
console.log(tom.Arr);//[1, 2, 3]
tom.jump();//Tom会跳
//我们在new一个jerry实例
let jerry = new People('Jerry');
//修改tom实例
tom.Arr.push(4);
console.log(tom.Arr);//[1, 2, 3, 4]
//jerry实例中的Arr不变,说明两个实例分别有一份Arr
console.log(jerry.Arr);//[1, 2, 3]
jerry.run('Jerry');//Jerry会跑
组合继承解决了原型链继承,经典继承的问题,那组合继承是不是就是完美的呢,有没有什么问题呢?
是的,组合继承也是有问题的:我们使用Animal.call()会调用一次父构造函数,使用new Animal();时还会调用一次构造函数,
那调用两次父构造函数造成的后果是什么呢?
子构造函数People中执行Animal.call(this, target);这样People中就保存了一份Arr和jump;实例化后就会保存在实例上
子构造函数原型属性People.prototype = new Animal();这样在People.prototype 上也保存了一份Arr和jump;
那最终的实例使用的是哪个呢?
我们知道调用属性时,是从下往上查找,即先看实例上有没有,再看原型 上有没有,所以用到的是子构造函数里的,People.prototype的原型上保存的不需要,代码证明下:
//复用上面的代码
//修改原型上Arr
People.prototype.Arr.push(4);
console.log(People.prototype.Arr);//[1, 2, 3, 4]
//我们在new一个jerry实例
let Tony = new People('Tony');
console.log(Tony.Arr);//[1, 2, 3]
也就是说People.prototype = new Animal();这行代码是有问题的,我们是不需要在People.prototype上面也增加父构造函数中的属性比如Arr和jump的,我们只需要People.prototype继承父构造函数原型Animal.prototype,用到的就是Object.create();
即:
//定义父构造函数
function Animal(target) {
this.aType = '哺乳类';
this.Arr = [1, 2, 3];
this.jump = function () {
console.log(target + '会跳');
}
}
//往父构造函数原型上添加run方法
Animal.prototype.run = function (target) {
console.log(target + '会跑');
}
//定义子构造函数
function People(target) {
//继承Animal(盗用父构造函数里的属性)
Animal.call(this, target);
this.target = target;
this.age = 20;
}
console.log(People.prototype.constructor)//People函数
//原型继承
People.prototype = Object.create(Animal.prototype);
console.log(People.prototype.constructor)//Animal函数
//解决上面由于重写子构造函数原型导致constructor指向了父构造函数问题
People.prototype.constructor = People;
console.log(People.prototype.constructor)//People函数
//new 实例的时候可以传参
let tom = new People('Tom');
console.log(tom.Arr);//[1, 2, 3]
tom.jump();//Tom会跳
console.log(People.prototype.Arr);//undefined
这种继承方式被称为寄生式组合继承,也是继承的最佳模式。
继承的问题解决了,但留下个疑问,使用new和使用Object.create()区别是啥?
new和Object.create()区别
new
- 创建一个空对象{}
- 将对象{}的原型指向构造函数prototype原型属性
- 将对象{}赋值给构造函数中this
- return这个this(默认)
在原型链继承代码中:
//原型链继承
People.prototype = new Animal();
这行代码就会执行new这个过程,所以在父构造函数Animal中,this指向的就是People.prototype,这也就是为什么People.prtotype上也会存有一份父构造函数属性的原因,同时将People.prototype的原型指向Animal.prototype。
Object.create()
内部实现原理:
function create(o){
let f = function(){}
f.prototype = o;
return new f();
}
我们会发现,在内部创建一个临时构造函数,并将函数prototype指向传进来的对象,最后返回临时构造函数实例。
所以
//原型继承
People.prototype = Object.create(Animal.prototype);
这行代码只是将People.prototype的原型指向了Animal.prototype,并不会像new一样,在People.prototype上添加父构造函数属性。