前言:
本文旨在帮助小白理解当前js各种继承方式,内容采取口语式描述,需要干货的童鞋请自己提炼内容。
本文以《js高级程序设计》为主体,整理了一些大佬的观点,以自己的表达方式呈现给大家,表述不当时请指教。
基础知识:
首先我们得了解一下构造函数的一些基本知识:prototype、new操作符。
如果你对prototype原型及原型链的知识不太了解,可先查看《js原型和原型链》学习
直接先来一个构造函数 :
function Parent(name,age){
this.name = name
this.age = age
}
生成一个构造函数实例需要用到new操作符,我们看看new操作符都干了什么?
//生成一个实例 p1
let p1 = new Parent("张三",18)
以上例子中,new 操作符主要做了三步:
1.以构造函数的prototype属性为原型,创建新对象,将它的引用赋给this
//可以这么理解
let this = {}
this.__proto__ = Parent.prototype
//以上就是为啥 p1.__proto__ === Parent.prototype 的原因
2. 将上面的 this 和 调用参数 传给构造器,执行
this.name = name
this.age = age
3. 最后返回 this 指向的新对象,也就是实例
return this
有点晕是不是,全部步骤放在一起看一下:
//第一步
let this = {}
this.__proto__ = Parent.prototype
//第二步
this.name = name
this.age = age
//第三步
return this
// 也就是说,构造函数在new调用时,第一步和第三步是隐式的
// 看似没有却偷偷执行了!
由于this是个普通对象,那return给了p1肯定也是个普通对象,p1实例里面都有些啥?
console.log(p1)
// {
// name:"张三",
// age:18,
// __proto__ : { 这个对象就是Parent.prototype,参考上面讲过的内容}
// }
继承:
转入正题,继承就是把父类的属性和方法提供给子类使用,讲究分享。js创作者们希望借用原型和原型链的思想去实现js独特的继承方式,于是最原始的原型链继承就诞生了。
1.原型链继承
参考原型和原型链的原理,如果我们将父类的实例作为子类的原型,那么属性和方法就能一直传递下去,上核心代码:
function Parent(name){
this.name = name
this.cards = ["身份证","社保卡"]
}
//给构造函数原型添加属性和方法
Parent.prototype.age = 35
Parent.prototype.getAge = function(){
console.log("父类age:" + this.age)
}
function Child(){
this.name = "子类"
}
Child.prototype = new Parent(); //这一步是关键
var c1 = new Child();
console.log(c1.age) //35
console.log( c1.getAge() ) //父类age:35
根据new操作和原型链的原理,子实例可以继承到:
1.自身构造函数的属性和方法
2.父类构造函数的属性和方法
3.父类原型的属性和方法
但是这种继承方法有很多的缺点:
1.由于new Parent()是单独赋给某个子类原型的,所以继承单一,只能继承一个父类
2.由于new Parent()这一步拿不到参数,所以新实例无法向父类构造函数传参
3.由于原型属性类似于浅拷贝,子类所有新实例都会共享父类实例的属性和方法,如果理解不了这个缺点,那么看看下面这段代码:
var c1 = new Child();
var c2 = new Child();
c1.cards.push("驾驶证");
c1.cards //["身份证","社保卡","驾驶证"]
c2.cards //["身份证","社保卡","驾驶证"] 被c1影响
本来我是想给c1实例的cards数组添加一个 "驾驶证" ,c2实例却跟着变了,这是为啥子呢?因为呐,那个crads属性是一个引用类型(数组对象),子类都是用的这个引用类型的地址而非独立拥有,所以修改了大家都会跟着变。
那咋个办呢?盗用构造函数继承来帮你解决
2.盗用构造函数继承
又称借用构造函数继承。
刚才说原型属性是浅拷贝,那我们不走原型,直接把父类构造函数的this指针改了,把属性和方法直接弄到子类构造函数不就行了!没错,这时候我们需要借用call()或者apply()来实现修改指针。同样的,不懂就看《啥子是call和apply》。
少废话,上核心代码:
function Parent(name){
this.name = name
this.cards = ["身份证","社保卡"]
this.getCards = function(){
console.log(this.cards)
}
}
function Child(name){
Parent.call(this,name) //核心步骤
this.address = "地址"
}
var c1 = new Child("张三");
var c2 = new Child("张三");
c1.cards.push("驾驶证");
c1.cards //["身份证","社保卡","驾驶证"] 属性私有不共享了
c2.cards //["身份证","社保卡"]
细心的童鞋已经发现,原型链继承的三个缺点已经被弥补啦!
1.可以通过多次call()的方式继承多个父类构造函数
2.由于call方法可以传参,所以子实例可以向父类传参
3. 新实例引入构造函数属性是私有的,不共享。
看了上面的优点,是不是觉得盗用构造函数继承更优秀,No!新的问题来了:
1.由于没有new父类构造函数,所以只能继承父类构造函数的属性,没有继承父类原型的属性,而且属性和方法只能在构造函数上定义。
2.既然属性私有了,那方法也会私有,每个子类都会有独立的一份,无法实现函数的复用
等等!无法函数复用?子类不都能调用到父类的函数吗?啥是复用你把我弄糊涂了。
原型链继承上的属性和方法都是共享的,所以一个函数方法会被大家共享,而盗用构造函数继承的方法是私有的,同一个方法会有多份存储,占用更多内存空间。上代码看:
function Parent(name){
this.name = name || "张三"
this.cards = ["身份证","社保卡"]
this.getCards = function(){
console.log(this.cards)
}
}
this.prototype.getName = function(){}
//先看看原型链继承
function Child(){
}
Child.prototype = new Parent();
var c1 = new Child();
var c2 = new Child();
c1.getName === c2.getName //true
//再看看盗用构造函数继承
function Child(name){
Parent.call(this,name)
}
var c1 = new Child();
var c2 = new Child();
c1.getCards === c2.getCards //false
明明是同一个方法getCards,却要另起炉灶弄一个新的来执行,看看原型链继承,大家始终都是用的同一个getName,这就是函数复用。
所以说,盗用构造函数继承也不是最优方式!
既然原型链继承和盗用构造函数继承优缺点互补,那能不能组合使用,双排上分?
当然可以咯!
3.组合继承
就是把原型链继承和盗用构造函数继承组合来使用,上菜:
function Parent(name){
this.name = name || "张三"
this.cards = ["身份证","社保卡"]
this.getCards = function(){
console.log(this.cards)
}
}
this.prototype.getName = function(){}
function Child(name){
Parent.call(this,name)
this.address = "地址"
}
Child.prototype = new Parent();
var c1 = new Child('张三');
把属性放在构造函数里,把方法写在原型上,一切问题都木有了。
好多优点啊:可以继承原型上的属性方法、可以传参、可以函数复用、属性也可以私有。
是不是觉得很完美?可惜了,还是有缺点。纳尼!!!你给我说清楚啥问题,不然我要捶你哈。
function Child(name){
Parent.call(this,name) //第二次调用Parent()
this.address = "地址"
}
Child.prototype = new Parent(); // 第一次调用 Parent()
执行了两次父类构造函数,第一次就把父类构造函数内的属性和方法给了子类,第二次又给,结果就是子类构造函数内部有一份,原型上有一份。一模一样,白白浪费了内存(耗内存)。因为在查找属性时,构造函数自身优先,原型上那份永远都用不上。
还有