观前提醒,阅读本文需要您掌握原型和原型链的概念,若没有这些前置知识,理解继承将相当困难
通过原型链实现继承
先看代码↓
//父类构造函数;
//tips:构造函数首字母一般大写,用来与普通函数作区分;
function Human(){
this.friends = ['booker','cris'];
}
//在父类的原型链上定义可复用的方法;
Human.prototype.getFriends = function(){
console.log(this.firends);
}
//子类构造函数
function Boy(name){
this.name = name;
}
//将父类的实例赋值给子类构造函数的原型对象;
Boy.prototype = new Human();
//注意:对子类原型对象的修改必须在上一句代码之后!
Boy.prototype.getName = function(){
console.log(this.name);
}
//这样是无效的,因为子类的原型对象的指向已经被修改了,所以先前对子类原型对象的赋值操作无法生效;
//Boy.prototype = new Human();
let boy = new Boy('jack');
console.log(boy);
再来看看控制台的打印结果
结论:
通过上面的图片,我们可以看到Boy类型的实例boy上不但拥有自己的属性(name),还拥有了Human类型的属性(即friends),这就说明了Boy与Human建立了某种联系,这种联系就是我们所说的继承,并且这种方式是通过原型链实现的.
原理:
修改子类原型对象的指向,核心代码:
Boy.prototype = new Human();
通过原型链实现继承存在的问题
通过原型链实现继承的主要弊端在于,如果父类属性中包含了引用类型的属性,如Object、Array等,那么子类所有的实例都会共享同一份属性,任何实例对该属性的修改都会反映到其他实例上,一般情况下这是我们所不想看到的情况,比如:
let boy1 = new Boy('jack');
let boy2 = new Boy('pual');
boy1.friends.push('Mary');
boy1.getFriends();//['booker','cris','Mary'];
boy2.getFriends();//['booker','cris','Mary'];
明明我们根本就没有对boy2做任何的操作,为什么boy2的friends属性也受到影响呢?
问题就出在了我们实现继承的方式上,回想一下这句代码:
Boy.prototype = new Human();
我们将一个Human的实例赋值给了Boy的原型对象,那么,请思考一下,一个Human的实例对象默认下是怎样的呢?
一个Human实例正常情况下应该包含如下属性:
- 定义在构造函数内的所有属性(friends);
- 指向其原型对象的__proto__;
那么Boy的原型对象也应该具有这些属性,并且你会发现,父类构造函数中的属性(friends)被绑定到了子类的原型对象上了(非常重要)
这个时候,当我们实例化一个Boy类型的对象时,会发生什么?
let boy = new Boy('jack');//会发生什么?
- 首先,Boy的构造函数会被调用,并把'jack'作为参数传递给构造函数,最终将对象返回给boy变量;
- JavaScript内部会自动将构造函数Boy的原型对象(__proto__)绑定到实例boy上;
还记得我们刚刚说到的 "父类构造函数中的属性(friends)被绑定到了子类的原型对象上了" 这句话吗?
这就是问题所在了,我们知道,定义在原型对象上的属性是被所有实例对象所共享的,所以,boy1和boy2的friends属性看似是相互独立,互不相干的变量,但实际上他们都指向了同一个内存地址,这就是通过原型链方式实现继承的最大弊端。