loading...
ES5如何实现继承
Published in:2023-07-25 | category: Typescript
Words: 2.2k | Reading time: 8min | reading:

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———————————————————————————————————

Prev:
两种经典的CSS布局--双飞翼布局和圣杯布局
Next:
Javascript严格模式以及this指向