ES5如何实现继承
共有四种方式实现原型继承–原型链继承、构造函数继承、组合式继承、寄生式继承,下面我一一介绍,分别就实现以及特性分别展开来描述
原型链继承方式
原理:将子类的原型对象指向一个父类的实例。通过这样的赋值,子类继承了父类的所有属性和方法,因为它的原型对象指向了一个父类实例,所以可以通过原型链访问到父类原型上的所有属性和方法。
看下面的示例代码:
function Animal(){ //父类
this.name = 'kitty' //基本数据类型
this.hobby = ['eat','drink'] //引用数据类型
}
Animal.prototype.getName = function(){
return this.name
}
Animal.prototype.getHobby = function(){
return this.hobby
}
function Cat(){} //子类
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
const m1 = new Cat()
const m2 = new Cat()
m1.name = 'm1'
console.log(m1.getName());
console.log(m2.getName());
m1.hobby.push('sleep')
console.log(m1.getHobby());
console.log(m2.getHobby());
下面是运行结果:
原因探究:
对于上面这个例子,我们分别对父类的两个不同的类型的属性进行修改,其中,一个是基本数据类型,另一个是引用数据类型,根据就运行结果可以发现,我们创建了两个cat子类,均通过原型链的继承方式,对Animal实现了继承,然后通过m1对引用数据类型hobby做出修改,之后再分别调用m1和m2的getHobby方法获取各自的hobby属性,发现m2的hobby属性也被修改了,这就暴露了原型链继承方式的缺点之一—-共享属性和方法,因为你的将子类(cat)的原型对象指向了父类(Animal)的同一个实例对象,也就是说你创建的所有cat对象的原型都是指向的同一个父类的实例对象,这意味着如果一个实例修改了原型上的属性,其他实例也会受到影响。
但再瞅一眼,为什么name没有发生这种问题呢?这是因为name是基本数据类型,如果属性是基本数据类型而不是引用类型,原型链继承就不存在共享属性问题,每个实例都会拥有独立的副本,也就是说m1.name = ‘m1’这句代码,会在当前m1的对象上新增一个name属性,然后赋值为’m1’,并不会影响到它原型上面的name属性,当打印m2.name的时候,因为m2本身没有name属性,那它就会去它的原型对象上去找这个属性,在这个例子中,也就是父类的name(’kitty’)。
实现核心:
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
Cat.prototype = new Animal():
这一行代码意味着将 Cat 的原型对象指向一个 Animal 的实例。通过这样的赋值,Cat 继承了 Animal 的所有属性和方法。
Cat.prototype.constructor = Cat:
这一行代码是为了修正因为前一步赋值导致的 constructor 属性被重写的问题。在第一步赋值后,Cat.prototype 的 constructor 属性会指向 Animal 的构造函数,即 Animal 函数。但实际上,我们希望 Cat.prototype.constructor 指向 Cat 构造函数本身,以保持对象的正确构造。
因此,通过显式赋值将 Cat.prototype.constructor 重新指向 Cat 构造函数,这样就纠正了原型链中的 constructor 属性。
缺点:
1、共享属性和方法:
使用原型链继承时,子类的实例共享父类原型上的属性和方法。这意味着如果一个实例修改了原型上的属性,其他实例也会受到影响。这可能导致意外的副作用和不稳定的行为。
2、无法向父类构造函数传递参数:
在原型链继承中,子类通过继承父类的实例来实现继承,并没有直接调用父类构造函数。因此,无法直接向父类构造函数传递参数,导致在子类构造函数中无法完成父类构造函数的初始化操作。
3、无法实现多继承:
原型链继承只能继承一个父类的属性和方法,无法同时继承多个父类,这限制了它的灵活性。
4、子类无法重写父类的属性和方法,没有实现super方法:
如果子类的原型上定义了与父类相同名称的属性或方法,那么它会覆盖父类的属性和方法。这可能导致不可预期的行为,使代码难以维护和调试。
构造函数继承方式
原理:在子类的构造函数中执行父类的构造函数,并为其绑定子类的this,避免了不同实例之间共享同一个原型实例,并且可以向父类的构造函数中传参,但是继承不到父类原型的属性和方法。
看下面示例代码:
function Animal(name){
this.name = name //基本数据类型
this.hobby = ['eat','drink'] //引用数据类型
}
Animal.prototype.getName = function(){
return this.name
}
Animal.prototype.getHobby = function(){
return this.hobby
}
function Cat(){
// 继承不到父类原型的属性和方法
Animal.apply(this,arguments)
}
const m1 = new Cat('m1')
const m2 = new Cat('m2')
m1.name = 'm1'
// console.log(m1.getName()); //报错 Uncaught TypeError: m1.getName is not a function
console.log(m1.name,m2.name);
m1.hobby.push('sleep')
console.log(m1.hobby,m2.hobby);

原因探究:
为什么调用不到父类原型上的方法呢?
原因是在使用构造函数继承时,子类实例只能继承父类构造函数内部设置的属性,而无法继承父类原型上的属性和方法。这是因为 Animal.apply(this, arguments) 调用的是父类构造函数,而不是父类原型上的方法。
实际上,使用构造函数继承时,创建的子类实例和父类实例是相互独立的,它们没有共享父类原型上的方法。这导致子类实例无法继承父类原型上的属性和方法。
实现核心:
function Cat(){
// 继承不到父类原型的属性和方法
Animal.apply(this,arguments)
}
优点:
避免了不同实例之间共享同一个原型实例,实例对象之间修改属性值,不会相互影响。
缺点:
继承不到父类原型上的属性和方法。
组合式继承方式
原理:原型链继承方式+构造函数继承方式结合体
看下面示例代码:
function Animal(name){
this.name = name //基本数据类型
this.hobby = ['eat','drink'] //引用数据类型
}
Animal.prototype.getName = function(){
return this.name
}
Animal.prototype.getHobby = function(){
return this.hobby
}
function Cat(){
Animal.apply(this,arguments)
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
const m1 = new Cat('m1')
const m2 = new Cat('m2')
m1.name = 'm1'
console.log(m1.getName());
console.log(m2.getName());
m1.hobby.push('sleep')
console.log(m1.getHobby());
console.log(m2.getHobby());

打印其中一个子类实例对象看一下:
原因探究:
每次创建子类实例的时候,都执行了两次父类的构造函数(new Animal()与Animal.apply(this,arguments)),但是并不影响使用。
实现核心:
function Cat(){
Animal.apply(this,arguments)
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
优点:
解决了原型链继承方式+构造函数继承方式出现的缺点
缺点:
重复执行了父类的构造函数,导致子类实例对象原型上的方法重复,不是特别的优雅。
寄生式继承方式
原理:在组合式继承的基础上进行修改,将子类的原型对象指向父类的原型并不是父类实例的原型,从而减少一次父类构造函数的执行,子类实例并不直接继承父类原型上的方法。它只是创建了一个新对象,该对象的原型链中仅包含父类原型上的属性和方法。因此,它并不是传统意义上的原型链继承。
看下面示例代码:
function Animal(name){
this.name = name //基本数据类型
this.hobby = ['eat','drink'] //引用数据类型
}
Animal.prototype.getName = function(){
return this.name
}
Animal.prototype.getHobby = function(){
return this.hobby
}
function Cat(){
Animal.apply(this,arguments)
}
// Object.create 创建的新对象是浅拷贝,也就是说它只复制对象的属性引用,而不会复制属性的值
Cat.prototype = Object.create(Animal.prototype)
Cat.prototype.constructor = Cat
const m1 = new Cat('m1')
const m2 = new Cat('m2')
m1.name = '修改了m1'
console.log(m1.getName());
console.log(m2.getName());
m1.hobby.push('sleep')
console.log(m1.getHobby());
console.log(m2.getHobby());

实现核心:
function Cat(){
Animal.apply(this,arguments)
}
Cat.prototype = Object.create(Animal.prototype)
Cat.prototype.constructor = Cat
优点:
解决了父类构造函数被执行两次的问题,且该种方法是es5中最成熟的继承方式。
over———————————————————————————————————