原型链继承
function Parent(){
this.name = 'parent1'
this.play = [1,2,3]
}
function Child(){
this.type = 'a'
}
Child.prototype = new Parent()
var c1 = new Child()
var c2 = new Child()
c1.name = 'c1name'
c1.play.push(4)
console.log(c2.name);//parent1
console.log(c2.play);//[ 1, 2, 3, 4 ]
console.log(c1.name);//c1name
/* 父类的引用类型的属性会被所有实例对象共享 */
通过原型链实现继承,即让子类的原型对象等于超类的实例对象,缺点是:
- 超类的引用数据类型的属性会被所有实例对象共享,这是因为所有的实例对象使用的都是同一个原型对象,内存空间是共享的。
- 不能实现子类向超类传参。
至于为什么基本数据类型不会被共享:
c1.name = 'c1name'
console.log(c2.name);//parent1
console.log(c1.name);//c1name
console.log(c1.__proto__.name);//parent1
console.log(JSON.stringify(c1));
//{"type":"a","name":"c1name"}
直接修改实例对象的基本数据类型的属性相当于在该实例对象上创建了一个对应的基本数据类型的属性并赋值。
同时需要注意的是,直接赋值的方式去修改引用数据类型的属性,也相当于新建了一个引用数据类型,如下代码,指针会指向一个全新的数组,并不会影响到子类原型对象的属性。如果要在原数组上修改,必须要用数组提供的几个api。
c1.play = [1]
console.log(c1.play);//[ 1 ]
console.log(c2.play);//[ 1, 2, 3, 4 ]
console.log(JSON.stringify(c1.__proto__));
//{"name":"parent1","play":[1,2,3,4]}
构造函数继承
function Parent(){
this.name = 'parent1'
this.play = [1,2,3]
}
Parent.prototype.getName = function(){
return this.name;
}
function Child(){
Parent.call(this)
this.type = 'a'
}
let child = new Child()
let child1 = new Child()
console.log(JSON.stringify(child));// {"name":"parent1","play":[1,2,3],"type":"a"}
// console.log(child.getName()); //child.getName is not a function
child1.play.push(4)
console.log(child.play);//[ 1, 2, 3 ]
console.log(child1.play);//[ 1, 2, 3, 4 ]
/* 无法继承超类的原型属性和方法,但是超类的引用属性不会被共享 */
通过在子类构造函数内部用call方法调用超类构造函数实现。优点是解决了原型链继承方式的弊端,超类的引用属性不会被共享,并且子类可以向超类传参;但是缺点是无法继承超类的原型属性和原型方法,只能继承超类的实例属性和方法。
组合继承
function Parent(){
this.name = 'parent1'
this.play = [1,2,3]
}
Parent.prototype.getName = function(){
return this.name;
}
function Child(){
Parent.call(this)
this.type = 'a'
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
let child1 = new Child()
//"parent1"
console.log(JSON.stringify(child1.getName()));
组合继承是前两种继承方式的结合,它解决了前两种继承方式的弊端。但缺点是它调用了两次超类的构造函数,会多一次构造的性能开销,并且也会让子类的原型对象上多了一些不必要的属性和方法。
//{"name":"parent1","play":[1,2,3]}
console.log(JSON.stringify(child1.__proto__));
//{"name":"parent1","play":[1,2,3],"type":"a"}
console.log(JSON.stringify(child1));
如上代码,修改子类的原型对象是为了继承超类上的原型属性和方法,而实例属性和方法则可以直接通过在子类构造函数内部调用超类构造函数即可。但从控制台输出结果可以看到,子类的原型对象上也拥有超类的实例属性和方法,也就是说通过原型链的继承,导致重复继承超类的属性和方法。
原型式继承
let Parent = {
play:[1,2,3],
name:'parent',
getName:function(){
return this.name;
}
}
// Object.create() 方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型
let child = Object.create(Parent)
let child1 = Object.create(Parent)
child.play.push(5)
console.log(child.play);//[ 1, 2, 3, 5 ]
console.log(child1.play);//[ 1, 2, 3, 5 ]
通过Object.create方法实现。这种方式不是为了创建某个类型,而是对一个对象的简单继承。缺点与原型链继承方式相同,因为Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能。
寄生式继承
let Parent = {
play:[1,2,3],
name:'parent',
getName:function(){
return this.name;
}
}
function fun(){
let child = Object.create(Parent)
child.getPlay = function(){
return this.play
}
return child;
}
let child = fun()
这种方式是原型式继承的一种优化,同样是利用Object.create()对现有对象实现一个简单的继承,但这个继承过程被封装成一个函数,并且在函数内部对其进行扩展。缺点同样是引用数据类型数据会被所有实例对象共享。
寄生组合式继承
function Parent(name){
this.name = name
this.play = [1,2,3]
}
Parent.prototype.getName = function(){
return this.name;
}
function Child(name){
Parent.call(this,name)//继承超类的实例属性和方法
this.type = 'a'
}
//继承超类的原型属性和方法
//并且子类的原型对象上没有超类的实例属性和方法
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
Child.prototype.getPlay = function(){
return this.play
}
c1.play.push(4)
console.log(c2.play);//[ 1, 2, 3 ]
console.log(c1.play);//[ 1, 2, 3, 4 ]
console.log(c1.__proto__.play);//undefined
这种方式是相对最优的继承方式,利用Object.create方法解决了组合继承中导致子类原型对象上属性冗余的问题,而实际上为了继承超类的原型属性和方法,我们只需要超类的原型的一个副本即可。
因此这种方式既有构造函数继承的优点——超类的引用数据类型属性不会被共享;又可以继承超类的原型属性和方法,且子类的原型对象上没有超类的实例属性和方法。