一、继承的概念
面向对象的三大特征:封装、继承、多态。作用就是判断代码是否是面向对象的思维方式。面向对象的思维方式:注重结果。
继承就是指一个子类继承父类的属性和方法,使得子类对象(实例化对象)具有父类的方法和属性。
这里要明白JavaScript是通过原型链实现继承的。
继承常见的六种方式:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合继承。
ES6还提供了继承的关键字extends.
接下来详细介绍每种继承方式。
二、继承常见的六种方式
第一种:原型链继承
重点:让子实例对象指向父实例对象
这里需要先理解原型链的基本思想:
构造函数、原型对象,实例对象三者关系,构造函数相当于父亲,原型对象相当于母亲,实例化对象相当于孩子。
1.prototype属性:属于构造函数,指向原型对象
作用:解决资源浪费 + 变量污染
2.__proto__属性:属于实例化对象,指向原型对象
作用:可以让实例化对象访问原型对象的成员
3.constructor属性:属于原型对象,指向构造函数
作用:可以让实例化对象知道自己是被谁创建的(亲子鉴定)
把子类的原型对象指向父类,就能使用父类中的方法和属性了。子类的son1.sayName()就能调用了,如果没有Son.prototype = new Father(),son1.sayName()就会报错。
// 父类
function Father(name,play){
this.name = name;
this.play = [1,2,3]
this.sayName = function(){
console.log(this.name); //mini_055
}
}
// 子类
function Son(){
this.name = 'mini_055';
}
// 原型链继承
Son.prototype = new Father();
var son1 = new Son()
son1.sayName()
但是也有两个缺点,
缺点:1.所有子实例对象共用一个原型对象,如果有一个改变则全部都跟着改变;
2.子实例对象无法向父实例对象传参
第二种:构造函数继承
特点: 1.只继承父类构造函数的成员,没有继承父类原型的成员;2.解决原型链继承的缺点; 3.可以继承多个构造函数的属性(call多个); 4.子实例可以向父实例传参
构造函数继承需要使用call()改变this指向,将this指向Father
//父类
function Father(name){
this.name = 'mini_055'
}
Father.prototype.getName = function(){
return this.name;
}
// 子类
function Son(name){
// 构造函数继承
// call()改变this指向,将Father中的属性添加值Son中
Father.call(this,name)
this.type = "Son"
}
var son1 = new Son()
// 运行正常
console.log(son1)
// 报错,因为son1中没有getName方法
son1.getName()
运行结果如下:
通过结果我们可以看到现在子类可以调用父类的属性和方法,但是不能调用父类原型的属性和方法。
缺点:1.只继承父类构造函数的成员(属性和方法),没有继承父类原型的成员;
2.无法实现构造函数的复用(每次用每次改变父构造函数的this指向);
3.每个新实例都有父类构造函数的副本,造成内存臃肿
第三种:组合继承(前两种的组合)
这种方法结合了前两种继承方式的优缺点,形成了第三种继承方式。
// 父类
function Father(name){
this.name = name;
}
Father.prototype.aaa = function(aaa){
console.log(aaa) // aaa的参数
}
// 子类
function Son(name){
Father.call(this,name)
this.type = "son"
}
Son.prototype = new Father();
var son1 = new Son("mini_055")
console.log(son1) //Son
son1.aaa("aaa的参数")
这种方法可以把前两种方法的缺点都能解决掉,即可以像父元素传参也能改变其中一个属性改变,另一个调用子类的不会改变,不会共享了。
缺点:在使用子类创建实例对象的时候,原型中会存在两份相同的属性和方法。
第四种:原型式继承
特点:类似于复制一个对象,用函数来包装。
采用原型式继承不自定义类型,临时创建一个构造函数,借助已有的对象作为临时构造函数的原型,然后在此基础实例化对象,并返回。
优点:父类方法可以复用
本质上是obj()对传入其中的对象执行了一个浅复制。使用Object.create()创建对象,Object.create() = Object()。
function obj(a){
// 父类
function Father(){}
Father.prototype = a;
return new Father()
}
// 子类
var person = {
name : "mini_055",
friends:['make','rose','jack']
}
var person1 = obj(person);
person1.name = 'van';
person1.friends.push('roby');
var person2 = obj(person);
person2.name = 'linda';
person2.friends.push('laila');
console.log(person.friends) //make、rose、jack、roby、laila
缺点:父类的引用属性会被所有子类实例共享。
子类构建实例时不能向父类传递参数。
第五种:寄生式继承
核心:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。
通过person对象新返回了一个新对象,新返回的person1对象具有person的所有属性和方法,还有一个新方法是sayHi。
function obj(a){
function Father(){} //obj()会创建一个临时构造函数
Father.prototype = a;//将传入的值赋值给这个构造函数的原型
return new Father() //返回这个类型的实例,浅复制
}
function create(b){
var clone = obj(b)//通过调用函数创建一个新对象
clone.sayHi = function(){
console.log('hi'); //Hi
}
return clone;//返回这个对象
}
var person = {
name:"mini_055",
friends:['rose','jack','mark']
}
var person1 = create(person)
person1.sayHi() //打印Hi
console.log(person1)//Father
运行结果如下:sayHi是person对象本身的新方法,friends是person1对象从perosn继承过来的属性和方法。
缺点:通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
第六种:寄生组合继承
基本思想:使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
// 父类
function Father(){
this.name = ['mini_055'],
this.getSty = function(){
console.log('父类的方法');
}
}
Father.prototype.sayHi = function(){
console.log('Hi');
}
// 子类
function Person(){
this.personname = ['dis'],
Father.call(this)//核心代码
}
// 核心代码
Person.prototype = Object.create(Father.prototype)
var person1 = new Person()
var person2 = new Person()
person1.name[0] = 'father'
person2.name[0] = 'person'
console.log(person1);
console.log(person1.name);
console.log(person2.name);
运行结果如下:
完美解决原型链加构造函数继承的缺点。
寄生组合继承存在效率问题,最主要的效率问题是父类构造函数始终被调用两次,一次是在创建子类原型时调用,另一次是在子类构造函数中调用,本质上,子类原型最终是要包含父类对象的所有实例属性,子类构造函数只要在执行的时候重写自己的原型就可以了。
缺点:效率低。