js中es6之前没有类和继承,但是可以通过各种巧妙的方式来实现继承
继承应该达到的状态:
1.子类可以使用父类中的属性和方法
2.子类不同的实例之间不会互相影响
3.子类实例能够向父类传参
4.能实现多继承(一个子类可继承多个父类)
5.父类的方法能被复用(不会过多的消耗内存),而不是每创建一个子类实例都生成一份父类方法
一.原型链继承
1.具体实现:父类的实例作为子类的原型对象,核心实现代码标注如下:
// 父类(我就不写成父构造函数了,这样比较简洁)
function Father(name, age) {
(this.name = name), (this.age = age), (this.arr = [1, 2, 3]), (this.value = 33)
}
Father.prototype = {
say() {
alert('hellow')
},
}
// 子类
function Child(name, age) {
;
(this.name = name), (this.age = age)
}
Child.prototype = new Father() // 核心实现代码--->父类的实例作为子类的原型对象
// 创建实例并让constructor重新指回Child
let newChild = new Child('zkp')
newChild.constructor =Child
let newChild2 = new Child('zhy')
newChild.constructor =Child
// 两个不同的子类实例上都有父类里的属性和方法
console.log(newChild.arr) // [1, 2, 3]
console.log(newChild2.arr) // [1, 2, 3]
console.log(newChild.value) // 33
console.log(newChild2.value) // 33
// 在一个子类实例身上修改继承的父类的基础属性值,不会影响到其他子类实例
newChild.value = 55
console.log(newChild.value) // 55
console.log(newChild2.value) // 33
// 致命缺陷: 在一个子类实例身上修改继承父类的引用属性值,其他子类实例其他子类实例的值也会跟着改变
newChild.arr.push(100)
console.log(newChild.arr) // [1, 2, 3 , 100]
console.log(newChild2.arr) // [1, 2, 3 , 100]
复制代码
2.优点:易于实现,一行代码就能实现
3.缺点:
1--->创建子类实例的时候不能向父类传参
2--->父类的引用数据类型属性被子类实例修改后,所有的子类实例上的该属性值也会跟着被修改
二.借用构造函数继承
1.具体实现:借父类的构造函数来增强子类实例(call,apply),相当于把父类的实例属性复制一份到子类实例
// 父类
function Father(name, age) {
this.name = name,
this.age =age,
this.arr= [2, 3, 4, 5, 6]
}
Father.prototype = {
say() {
alert('hellow');
},
}
// 子类
function Child(name, age) {
Father.call(this, name, age) // 核心代码--->在子类中利用call()调用父类并向父类传参
}
let newChild = new Child('zkp', 11)
let newChild2 = new Child('zhy', 6)
// 子类实例修改父类引用属性 其他子类实例上该属性不会改变
newChild.arr.push(100)
console.log(newChild.arr); // [2, 3, 4, 5, 6, 100]
console.log(newChild2.arr); // [2, 3, 4, 5, 6]
// newChild.say() // newChild.say is not a function 报错 只是继承了父类的构造函数 无法使用父类的原型方法
复制代码
2.优点:
1.解决了子类实例共享父类引用属性的问题
2.创建子类实例时,可以向父类构造函数传参
3.可以实现多继承(call多个)
3.缺点:无法继承父类原型中的方法,除非父类的方法写入构造函数中,但这样就实现不了函数的复用
三.组合式继承(伪经典继承)
1.具体实现:
// 父类
function Father(name, age) {
this.name = 'hahaha',
this.age = 11,
this.arr = [2, 3, 4, 5, 6]
}
Father.prototype = {
say() {
alert('hellow');
},
}
// 子类继承步骤:
function Child(name, age) {
Father.call(this, name, age) // 1.call方法调用父类的构造函数 并改变this的指向为子类函数(实现传参)
}
// 2.子构造函数的原型对象指向父构造函数的实例
Child.prototype = new Father()
// 3.创建实例并让constructor重新指回Child
let newChild = new Child('zkp',12)
newChild.constructor =Child
let newChild2 = new Child('zhy',6)
newChild2.constructor =Child
newChild.say() // alert ('hellow') 页面弹出hellow 子函数的实例继承了父函数的原型中的方法
console.log(newChild.name); // hahaha 子类的实例可以向父类传参
// 一个子类实例修改引用类型属性 其他实例该引用类型不会改变
newChild.arr.push(100)
console.log(newChild.arr,newChild2.arr); // [2, 3, 4, 5, 6, 100] ,[2, 3, 4, 5, 6]
// 缺点:父类的构造函数会被调用两次 call方法调用一次 new Father 一次
复制代码
2.核心
把实例方法都放在原型对象上,以实现函数复用。同时还要保留借用构造函数方式的优点,通过call(this)继承父类的基本属性和引用属性并保留能传参的优点.
通过Child.prototype = new Father()继承父类函数,实现函数复用
优点:
- 不存在引用属性共享问题(不同子类实例之间不会互相影响)
- 可传参
- 函数可复用
4.缺点:也有一点小缺点
子类原型上有一份多余的父类实例属性,因为父类构造函数被调用了两次,生成了两份,而子类实例上的那一份屏蔽了子类原型上的,造成内存浪费
四.原型式继承(对象中的继承,更像是拷贝而不是继承)
1.具体实现:
// 创建对象obj
let obj = {a:1 , b:2 , c:[1,2,3]}
// 在函数中把一个对象作为一个空构造函数的原型对象 并返回该构造函数的调用
function CreateObj(o) {
function F() {}
F.prototype = o;
return new F();
}
let newObj = CreateObj(obj);
let newObj2=CreateObj(obj)
//新创建的对象拥有obj的属性和方法
console.log(newObj.a , newObj.b , newObj.c); // 1 , 2 , [1, 2, 3]
console.log(newObj2.a , newObj2.b , newObj2.c); // 1 , 2 , [1, 2, 3]
// 通过一个实例修改引用类型值 其他实例也会被改变
newObj.c.push(100)
console.log(newObj2.a , newObj2.b , newObj2.c); // 1 , 2 , [1, 2, 3, 100]
console.log(newObj.a , newObj.b , newObj.c); // 1 , 2 , [1, 2, 3, 100]
复制代码
3.核心:通过空的构造函数作为跳板 ,返回该构造函数的调用(类似于复制一个对象,用函数来包装)
3.优缺点
优点:
- 从已有对象衍生新对象,不需要创建自定义类型(一种新的创建对象方式) es5中内置方法Object.create()用到了这种方式
缺点:
- 父类引用属性会被所有实例共享,因为是用整个父类对象来充当了子类原型对象,所以这个缺陷无可避免
- 无法实现代码复用(新对象是现取的,每次new F()返回的都是一个新的对象,也没用到原型,无法复用)
五.寄生式继承
1.具体实现:
let obj = {
a: 1,
b: [1, 2, 3],
c() {
alert('hellow')
},
}
function CreateObj(o) {
function F() {}
F.prototype = o
return new F()
}
// 寄生继承就是在原型继承的基础上再封装 给队象增加方法和属性(对象增强) 实际上跟原型式继承是一样的
function CreateChild(o) {
let newObj = CreateObj(o) // 创建对象 或者用 var newObj = Object.create(o)
// 增强对象
newObj.x = function () {
alert('这是给新对象增强的方法')
}),
newObj.y = 'biubiu~'
......
return newObj
}
let p = CreateChild(obj)
p.x() //弹出信息
console.log(p.y) // biubiu~
console.log(p.a, p.b) // 1 , [1 , 2 , 3]
复制代码
缺点:
函数还是不能复用,一个实例修改原型上的引用属性,其他实例依然会跟着改变(就是给原型式继承套上一层函数而已,让原型式看起开更像继承,并没有解决根本问题)
到这里的几个分析:
1.以上的方式多少都会有点缺陷,要达到完美继承就需要在组合式继承(伪经典继承)身上进行改造,但是要达到能传参,并且还要实现多继承的目的,那么在子类内部调用父类构造函数Father.call()这一步就不能动,
所以只能考虑Child.prototype = new Father 这一步,
2.最终需要达到目的:Child.Prototype.__ proto __ = Father.prototype(子类的原型指向父类的原型, 解决父类调用两次的缺陷) ----->(也就是一个对象继承另一个对象,上面的寄生式和原型式继承方式),
另外最后还是需要将子类实例的constructor指回子类
六.寄生式组合式继承(完美继承)
1.具体实现:
//1. 父类 实例属性放在构造函数中
function Father(name, age) {
this.name = name
this.age = age
this.hobby = ['敲代码', '解Bug', '睡觉']
}
// 父类方法放在原型上实现复用
Father.prototype.sayName = function () {
console.log(this.name, this.age, 666)
}
//2. 子类
function Child(name, age) {
Father.call(this, name, age) // 调用父类的构造函数 (继承父类的属性)
this.a = 1
}
// 3. 利用跳板创建对象
function CreateObj(o) {
function F() {}
F.prototype = o
return new F()
}
// 4.子类的原型对象用CreateObj(Father)创建
Child.prototype = CreateObj(Father.prototype)
console.log(Child.prototype.__proto__ === Father.prototype) // true 实现对Child.prototype = new Father的改造
/* 或者直接把3,4写成es5中的Object.create() 一行代码实现
Child.prototype = Object.create(Father.prototype) */
// 5.创建子类实例 constructor属性指回子类
let zkp = new Child('zkp', 12)
zkp.constructor = Child
let zhy = new Child('zhy', 6)
zhy.constructor = Child
// 验证:
console.log(zkp.a , zhy.a) // 1 , 1 子类自己的属性
// 一个子类实例修改继承的引用类型属性 其他实例不会被改变
zkp.hobby.push('吃饭')
console.log(zkp.name, zkp.age, zkp.hobby) // zkp , 12 , ['敲代码', '解Bug', '睡觉', '吃饭']
console.log(zhy.name, zhy.age, zhy.hobby) // zhy , 6 , ['敲代码', '解Bug', '睡觉']
// 子类调用父类的原型方法
zkp.sayName() // zkp 666
zhy.sayName() // zhy 666
复制代码
优点:
完美实现了函数复用,传参,实例之间不会相互影响,多继承