JavaScript 中的继承,你知道几种呢?| 掘金·日新计划

前言

现实中的继承:子承父业,比如我们都继承了父亲的姓。

程序中的继承:子类可以继承父类的一些属性和方法。

Brendan Eich(js的设计者)认为一种简易的脚本语言,其实是不需要"继承"机制的。但是 JavaScript 中一切皆对象,那就需要有一种机制,将对象联系起来,于是设计了"继承",通过 new 构造函数创建对象,通过构造函数的 prototype 来实现数据共享。继承的本质则是通过原型链机制实现的扩展。

话不多说,在 JavaScript中常见的继承方式,有以下七种:

一、借用构造函数继承

JavaScript中的借用构造函数继承是一种通过调用父类构造函数来实现继承的方式。这种继承方式有以下特点:

  1. 子类实例拥有了父类构造函数中定义的属性和方法。
  2. 子类实例与父类实例之间不存在原型链的关系,因此可以避免共享原型对象带来的问题。
  3. 子类无法重用父类原型对象上的方法。
javascript 复制代码
// 定义父类的构造函数
function Father(name){
    this.name = name
    this.type = '独有的'
}

// 父类原型上面的方法
Father.prototype.sayName = function(){
    console.log(`My name is ${this.name}`);
}

// 定义子类的构造函数
function Son(name, age) {
    Father.call(this, name) // 借用父类构造函数 Father,并将 this 指向 Son
    this.age = age
}

// 创建Son的实例
let son1 = new Son('张三', 18)
let son2 = new Son('李四', 20)

console.log(son1.name, son2.age); // 张三  20
son1.sayName() // TypeError: son1.sayName is not a function

console.log(son1.type, son2.type); // 独有的 独有的

在以上代码中,Son 类通过调用 Father 构造函数来实现继承,从而拥有了 Father 类中定义的属性。由于子类实例与父类实例之间不存在原型链的关系,因此修改一个实例的属性不会影响到其他实例。

但是,我们发现,由于子类实例无法访问父类原型对象上的方法,因此在上面的代码中,Son 实例调用sayName()方法会报错。其也会继承父类中不需要的属性。这是需要注意的。

如果需要在子类中重用父类原型对象上的方法,可以考虑使用组合继承寄生组合式继承

优点

每一个实例属性都有父类的副本,解决了多个实例引用类型属性指向相同时被篡改的问题

缺点

  1. 只能继承父类的实例属性和方法,不能继承原型属性/方法
  2. 无法实现复用,每个子类都有父类实例函数的副本,影响性能

二、原型链继承

原型链继承是 JavaScript 中一种基于原型的继 方式,它通过将一个构造函数的实 例作为另一个 造函数的原型,从而实现继承。具体来说,就是 子类的构造函数中通过 Child.prototype = new Parent() 的方式来继承父类的属性和方法。

javascript 复制代码
function Father(name){
    this.name = name
    this.arr = [1, 2]
}

Father.prototype.sayName = function(){
    console.log(`My name is ${this.name}`);
}

function Son(name) {
    this.name = name;
}

Son.prototype = new Father();

var son1 = new Son('张三');
var son2 = new Son('李四');

son1.sayName(); // My name is 张三
son2.sayName(); // My name is 李四

son1.arr.push(3);
console.log(son1.arr == son2.arr);  // true

在以上代码中,我们首先定义了一个父类构造函数 Father,并在其原型上定义了一个方法 sayName。然后我们定义了一个子类构造函数 Son,并通过将子类的原型设置为父类的实例来实现继承。最后我们创建了两个子类实例,并调用其方法来验证其继承是成功的。

但是我们更改其中一个实例中的属性时,其缺点也就出现了:另一个实例的属性也随之改变了,这是需要注意的地方。

优点

  1. 通俗易懂,原型链继承是一种很简单的继承方式,非常容易去理解和实现。
  2. 由于子类实例可以访问父类的原型,因此可以重用父类的方法,从而减少代码量。

缺点

  1. 多个实例的引用类型属性指向相同,存在篡改的可能
  2. 原型链继承无法向父类构造函数传递参数,因此子类实例无法向父类构造函数传递参数,也无法对父类实例进行初始化。
  3. 由于 JavaScript 中一个对象只能有一个原型对象,因此原型链继承无法实现多继承

三、组合继承

JavaScript 中的组合继承是一种结合借用构造函数和原型链继承的方式,它的核心思想是使用借用构造函数继承实例属性和方法,使用原型链继承共享属性和方法。

这个方法是 javascript 中最常用的继承模式

javascript 复制代码
function Father(name) {
    this.name = name;
}

Father.prototype.sayName = function () {
    console.log(`My name is ${this.name}`);
}

function Son(name, age) {
    Father.call(this, name); // 构造函数继承父类 Father的属性
    this.age = age;
}

Son.prototype = new Father(); // 原型链继承父类 Father的方法
Son.prototype.constructor = Son; // 修复构造函数指向

let son1 = new Son('张三', 18);
let son2 = new Son('李四', 20);

console.log(son1.name); // '张三'
console.log(son2.age); // 20
son1.sayName(); // 'My name is 张三'

在以上代码中,子类 Son 通过借用父类 Father 构造函数继承实例属性,通过原型链继承 Father 类的方法。由于子类实例与父类实例之间不存在原型链的关系,因此修改一个实例的属性不会影响到其他实例。同时,子类实例也可以重用父类原型对象上的方法。

但是,由于在上面的代码中通过 Son.prototype = new Father() 创建了一个新的 Father 实例,因此在创建子类 Son 时会调用两次父类 Father 构造函数,造成了性能上的浪费。可以使用寄生组合式继承来解决这个问题。

具体来说,结合原型链继承和构造函数继承通过调用父类的构造函数,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数的复用

优点

解决了借用构造函数,不能继承原型属性/方法

缺点

父类中的实例属性和方法既存在于子类的实例中,又存在于子类的原型中,不过仅是内存占用,因此,在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法

四、原型式继承

JavaScript 中的原型式继承是一种基于已有对象创建新对象的继承方式,它利用了对象的动态特性,通过封装一个函数来实现继承。该函数接收一个用作新对象原型的对象作为参数,并返回一个新对象,从而实现了继承。该方式与借用构造函数继承类似,但它并不涉及到构造函数和实例的概念。原型式继承具有以下特点:

  1. 有对象创建新对象。
  2. 用Object.create()方法实现。
  3. 一个对象作为另一个对象的原型对象。
  4. 用原型对象的属性和方法,但不会影响到原型对象本身。
javascript 复制代码
let father = {
    name: '',
    sayName: function () {
        console.log(`My name is ${this.name}`);
    }
}

let son = Object.create(father); // 使用father对象作为son对象的原型
son.name = '张三'
son.sayName() // My name is 张三

以上代码中,通过创建一个对象 father ,并将其原型 son 设置为一个已有对象,然后向这个对象中添加属性和方法来实现继承。

优点

可以基于一个对象创建多个对象,实现对象复用。

缺点

原型式继承多个实例的引用类型属性指向相同,存在篡改的可能

五、寄生式继承

JavaScript 中的寄生式继承是一种基于已有对象创建新对象的继承方式,类似于原型式继承。它的主要区别是,在新创建的对象上增加一个方法,而这个方法的作用是以某种方式增强对象,然后返回这个对象。这种继承方式得名于"寄生",因为增强对象的方法通常是基于已有的对象进行"寄生"而得名。

javascript 复制代码
function createFather(name) {
    let father = {
        name: name,
        sayName: function () {
            console.log(`My name is ${this.name}`);
        }
    }
    // 基于father对象进行寄生增强
    let son = Object.create(father);
    son.bark = function () {
        console.log('hallo');
    };
    return son;
}

let mySon = createFather('张三');
mySon.sayName(); // My name is 张三
mySon.bark(); // hallo

以上代码中,它在原型式继承的基础上增加了一个包装函数,该函数用于封装继承过程中的一些增强行为。

优、缺点

可以封装继承过程,并且可以向对象中添加一些额外的属性和方法。但是和原型式继承一样,也存在父对象的引用属性被所有子对象共享、无法判断实例是否是父对象的实例等问题。

六、寄生式组合继承

JavaScript中的寄生式组合继承是一种结合了组合继承和寄生式继承的继承方式。具体来说,它在组合继承的基础上,通过寄生式继承来解决组合继承中重复调用父构造函数的问题。

寄生组合继承集合了前面几种继承优点,几乎避免了上面继承方式的所有缺陷,是执行效率最高也是应用面最广的,就是实现的过程相对繁琐。

javascript 复制代码
function Father(name) {
    this.name = name;
    this.type = 'mammal';
}

Father.prototype.sayName = function () {
    console.log('My name is ' + this.name);
};

function Son(name, breed) {
    Father.call(this, name);
    this.breed = breed;
}

// 使用寄生式继承继承Father.prototype
Son.prototype = Object.create(Father.prototype);
Son.prototype.constructor = Son;

Son.prototype.sayBreed = function () {
    console.log('I am a ' + this.breed);
};

let mySon = new Son('张三', 'Golden Retriever');
mySon.sayName(); // 'My name is 张三'
mySon.sayBreed(); // 'I am a Golden Retriever'

以上代码中,我们定义了 FatherSon 两个构造函数,其中 Father 构造函数定义了一个 name 属性和一个 sayName() 方法, Son 构造函数在 Father 的基础上添加了一个 breed 属性和一个 sayBreed() 方法。为了实现寄生式组合继承,我们使用 Object.create() 方法基于 Father.prototype 创建了一个新的对象,并将其赋值给 Son.prototype ,从而使得 Son.prototype 的原型链指向了 Father.prototype。同时,我们还将 Son.prototypeconstructor 属性设置为 Son ,以保证继承链的完整性。最后,我们通过调用 Father 构造函数并将this指向 Son 对象,实现了对 Father 属性的继承。通过这种方式,我们既避免了组合继承中重复调用父构造函数的问题,又保留了寄生式继承的灵活性,实现了一个高效而且灵活的继承方式。

优点

  1. 实现了属性和方法的完整继承。
  2. 避免了组合继承中重复调用父类构造函数的问题,提高了性能。
  3. 可以在不修改原对象的情况下,对继承过程进行一些增强,例如添加新的属性和方法。

缺点

增加了一层包装函数,可能会带来一定的性能开销,可能会导致代码的可读性降低。

七、class继承

ES6及以上的版本中,JavaScript引入了class关键字,用于定义类,从而实现面向对象编程。class继承是一种通过类来实现继承的方式,它使用extends关键字来指定父类,并通过super关键字来调用父类的构造函数和方法。

javascript 复制代码
class Father {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        console.log(`My name is ${this.name}`);
    }
}
class Son extends Father { // 这样子,通过extends类就继承了父类的属性和方法
    constructor(surname, FatherName) {
        super(surname);   // 使用super调用父类的constructor(surname)
        this.FatherName = FatherName;
    }
    sayInterest() {
        console.log(`我喜欢${this.FatherName}`);

    }
}
var son = new Son('张三', "点赞、收藏加关注");
son.sayName(); // My name is 张三
son.sayInterest(); // 我喜欢点赞、收藏加关注

以上代码中,我们首先定义了一个 Father 类,它包含一个构造函数和一个 sayName() 方法。然后我们通过 extends 关键字来指定 Son 类的父类为 Father ,并在 Son 类的构造函数中通过 super 关键字来调用 Father 构造函数,并实现了 sayName() 方法。最后,我们创建一个新的 sayInterest() 方法,并调用它的方法来测试继承是否成功。

需要注意的几点:

  1. ES6 中类没有变量提升,所以必须先定义类,才能通过类实例化对象。
  2. 类里面的共有属性和方法一定要加 this 使用。
  3. 类里面的this指向问题。
  4. constructor 里面的this 指向实例对象, 方法里面的 this 指向这个方法的调用者。

优点

  1. 代码可读性高,更易于理解和维护。
  2. 语法简洁,可以更快地编写代码。
  3. 可以使用现代JavaScript特性,如箭头函数、解构赋值等。

缺点

  1. 与ES5及以下版本的JavaScript不兼容。
  2. 需要编译才能运行在低版本浏览器中。
  3. 某些开发者可能认为使用类和继承违背了JavaScript的本质。

既然都看到这里了,点点赞在走呗

相关推荐
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
郝晨妤5 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui
尝尝你的优乐美6 小时前
vue3.0中h函数的简单使用
前端·javascript·vue.js
windy1a6 小时前
【C语言】js写一个冒泡顺序
javascript
会发光的猪。6 小时前
如何使用脚手架创建一个若依框架vue3+setup+js+vite的项目详细教程
前端·javascript·vue.js·前端框架