javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JS六种继承方式

JavaScript六种继承方式总结大全

作者:懒羊羊小可爱

JavaScript中最基本的继承方式,其核心思想是利用原型让一个引用类型继承另一个引用类型的属性和方法,下面这篇文章主要介绍了JavaScript六种继承方式的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

1. 原型链继承

是什么?

这是最基础的继承方式。其核心是:让一个构造函数的 prototype 对象指向另一个构造函数的实例。这样,子类就能通过原型链访问到父类的属性和方法。

javascript

// 父类
function Parent() {
    this.name = 'Parent';
    this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
    console.log(this.name);
};

// 子类
function Child() {
    this.type = 'Child';
}

// 关键!将Child的原型指向Parent的实例,建立原型链
Child.prototype = new Parent();

var child1 = new Child();
child1.sayName(); // 'Parent' (来自原型链)
console.log(child1.colors); // ['red', 'blue'] (来自原型链)

var child2 = new Child();
child1.colors.push('green'); // 修改child1的colors
console.log(child2.colors); // ['red', 'blue', 'green'] (问题出现了!)

有什么作用?

实现属性和方法的继承。

实际开发运用场景?

在现代前端开发中,几乎不会单独使用原型链继承,因为它有致命缺陷。但它是一切其他继承方式的理论基础。

优点:

缺点:

  1. 引用类型属性被所有实例共享(如上例中的 colors 数组)。一个实例修改了引用类型属性,所有实例都会受到影响。这是最大的问题。

  2. 创建子类实例时,无法向父类构造函数传参(因为 new Parent() 在初始化原型时就执行了)。

2. 借用构造函数继承(经典继承)

是什么?

为了解决原型链继承的缺点,这种方法的核心是:在子类构造函数的内部调用父类构造函数。这利用了 call() 或 apply() 方法,使父类的 this 指向子类的实例。

javascript

// 父类
function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
}
// 注意:父类原型上的方法子类访问不到
Parent.prototype.sayName = function() {
    console.log(this.name);
};

// 子类
function Child(name, type) {
    // 关键!“借用”父类的构造函数来初始化属性
    Parent.call(this, name); // 相当于执行了 this.name = name; this.colors = ['red', 'blue'];
    this.type = type;
}

var child1 = new Child('小明', 'Child');
var child2 = new Child('小红', 'Child');

child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
console.log(child2.colors); // ['red', 'blue'] (互不影响了!)
console.log(child1.sayName); // undefined (无法继承父类原型的方法)

有什么作用?

解决原型链继承中“引用属性共享”和“无法传参”的问题。

实际开发运用场景?

通常不会单独使用,但它是组合继承的重要组成部分。

优点:

  1. 避免了引用属性共享的问题。

  2. 可以在子类中向父类传递参数

缺点:

  1. 方法都在构造函数中定义,每次创建实例都会创建一遍方法,无法实现函数复用效率低

  2. 无法继承父类原型(prototype)上的方法(如上例中的 sayName)。

3. 组合继承

是什么?

组合继承结合了原型链继承借用构造函数继承的优点,是 JavaScript 中最常用的继承模式。其核心是:

  1. 使用借用构造函数来继承属性(解决共享和传参问题)。

  2. 使用原型链来继承方法(实现方法复用)。

javascript

// 父类
function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
    console.log(this.name);
};

// 子类
function Child(name, type) {
    // 1. 继承属性
    Parent.call(this, name); // 第二次调用 Parent()
    this.type = type;
}

// 2. 继承方法
Child.prototype = new Parent(); // 第一次调用 Parent()
// 修复构造函数指向,否则Child实例的constructor会指向Parent
Child.prototype.constructor = Child;
// 子类自己的方法
Child.prototype.sayType = function() {
    console.log(this.type);
};

var child1 = new Child('小明', 'Child1');
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
child1.sayName(); // '小明'
child1.sayType(); // 'Child1'

var child2 = new Child('小红', 'Child2');
console.log(child2.colors); // ['red', 'blue'] (属性不共享)
child2.sayName(); // '小红' (方法可复用)

有什么作用?

综合了两种模式的优点,成为 JavaScript 中一种非常实用的继承模式。

实际开发运用场景?

在 ES6 的 class 出现之前,这是最主流、最可靠的继承方式。常用于构建复杂的对象系统,如UI组件库、游戏引擎中的实体继承等。

优点:

  1. 实例拥有独立的属性,不会共享。

  2. 实例可以复用父类原型上的方法。

  3. 可以向父类构造函数传参。

缺点:

4. 原型式继承

是什么?

道格拉斯·克罗克福德提出的方法。其核心是:创建一个临时的构造函数,将其原型指向某个对象,然后返回这个临时构造函数的实例。本质上是对传入的对象进行了一次浅复制

javascript

// object() 就是 ES5 中 Object.create() 的模拟实现
function object(o) {
    function F() {} // 创建一个临时构造函数
    F.prototype = o; // 将其原型指向传入的对象o
    return new F(); // 返回这个临时构造函数的实例
}

var person = {
    name: 'Nicholas',
    friends: ['Shelby', 'Court']
};

var person1 = object(person);
person1.name = 'Greg';
person1.friends.push('Rob');

var person2 = object(person);
person2.name = 'Linda';
person2.friends.push('Barbie');

console.log(person.friends); // ['Shelby', 'Court', 'Rob', 'Barbie'] (共享问题依然存在)

有什么作用?

在不必兴师动众地创建构造函数的情况下,基于一个已有对象创建新对象

实际开发运用场景?

优点:

缺点:

5. 寄生式继承

是什么?

寄生式继承的思路与寄生构造函数工厂模式类似。其核心是:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式(如原型式继承)增强对象,最后返回这个对象

javascript

function createAnother(original) {
    var clone = object(original); // 1. 通过调用函数(如object)创建一个新对象(原型式继承)
    clone.sayHi = function() {    // 2. 以某种方式来增强这个对象
        console.log('hi');
    };
    return clone; // 3. 返回这个对象
}

var person = {
    name: 'Nicholas',
    friends: ['Shelby', 'Court']
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // 'hi'

有什么作用?

主要关注对象而不是自定义类型和构造函数,在主要考虑对象而非自定义类型和构造函数的情况下,实现简单的继承和扩展。

实际开发运用场景?

优点:

缺点:

6. 寄生组合式继承

是什么?

这是组合继承的优化版本,也是目前公认的最理想的继承范式。它解决了组合继承调用两次父类构造函数的问题。

其核心是:

  1. 使用借用构造函数来继承属性

  2. 使用寄生式继承继承父类原型,并将其赋值给子类原型。

javascript

function inheritPrototype(child, parent) {
    // 1. 创建父类原型的一个副本(原型式继承)
    var prototype = Object.create(parent.prototype);
    // 2. 修复副本的constructor指针
    prototype.constructor = child;
    // 3. 将子类的原型指向这个副本
    child.prototype = prototype;
}

// 父类
function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
    console.log(this.name);
};

// 子类
function Child(name, type) {
    // 只调用一次父类构造函数(继承属性)
    Parent.call(this, name);
    this.type = type;
}

// 关键!替换掉组合继承中的 `Child.prototype = new Parent()`
inheritPrototype(Child, Parent);

// 添加子类自己的方法
Child.prototype.sayType = function() {
    console.log(this.type);
};

var child1 = new Child('小明', 'Child1');
// 实例的 __proto__ 指向 Child.prototype
// Child.prototype 的 __proto__ 指向 Parent.prototype
// 完美!

有什么作用?

只调用一次父类构造函数,并且避免了在子类原型上创建不必要的、多余的属性。同时,原型链还能保持不变。

寄生组合式继承核心目标:修复“组合继承”的缺陷

要理解“寄生组合”,我们必须先回顾“组合继承”的问题。

组合继承的做法:

  1. Parent.call(this):在子类构造函数里调用父类构造函数。这会在子类实例自身上创建一份父类的属性。

  2. Child.prototype = new Parent():将子类的原型指向一个父类的实例。这会在子类的原型对象上创建第二份父类的属性。

这就导致了:

虽然实例自身的属性会屏蔽掉原型上的同名属性,功能上没问题,但多创建了一份多余的属性,造成了内存浪费和不优雅。

寄生组合式继承的解决方案

它的核心思路非常巧妙:我们真的需要那个 new Parent() 实例来充当原型吗?不,我们只需要父类原型上的方法。

我们不需要 Parent 实例上的属性(因为我们已经通过 Parent.call(this) 在子类实例上得到了一份),我们只需要能通过原型链找到 Parent.prototype 上的方法。

所以,新的方案是:

  1. 继承属性:保持不变,依然在子类构造函数里用 Parent.call(this)子类构造函数里调用父类构造函数,这会在子类实例自身上创建一份父类的属性。这保证了每个实例都有自己独立的属性。

  2. 继承方法不再用 new Parent(),而是直接创建一个纯净的、指向父类原型的对象,用它来作为子类的原型。

这个“纯净的、指向父类原型的对象”就是 Object.create(Parent.prototype) 所做的事情。

一步步拆解(超级详细版)

让我们把 inheritPrototype(Child, Parent) 这个函数里的三步拆开来看:

javascript

function inheritPrototype(child, parent) {
    // 第一步:创建原型副本(核心)
    var prototype = Object.create(parent.prototype);
    // 第二步:修复constructor指向
    prototype.constructor = child;
    // 第三步:将子类的原型指向这个新创建的对象
    child.prototype = prototype;
}

第一步:var prototype = Object.create(parent.prototype);

第二步:prototype.constructor = child;

第三步:child.prototype = prototype;

终极比喻:“继承家产”的故事

总结:为什么它是最佳的?

特性组合继承寄生组合继承优势
父类属性副本2份 (实例上1份,原型上1份)1份 (仅在实例上)更高效,无浪费
父类方法继承通过原型链继承通过原型链继承同样有效
父类构造函数调用2次1次性能更优
原型链保持正确保持正确同样正确

所以,寄生组合式继承的核心贡献就是:
它用一种极其巧妙的方式(Object.create)建立了子类和父类原型的直接联系,完全跳过了创建父类实例这个不必要的步骤,从而避免了创建多余的属性,完美地实现了继承。这也是为什么ES6的 class 和 extends 语法糖其底层实现原理是寄生组合继承的原因,因为它确实是理论上最完美的方案

实际开发运用场景?

这是实现基于构造函数的继承的最佳模式。在需要高度优化和避免不必要的内存开销的库或框架中可能会看到。但在日常开发中,我们更倾向于使用 ES6 的 class 和 extends,其底层原理就是寄生组合式继承。

优点:

缺点:

总结与建议

继承方式核心思想优点缺点适用场景
原型链继承子类原型 = 父类实例简单,方法可复用引用共享,无法传参基础学习,几乎不用
借用构造函数在子类中 Parent.call(this)属性独立,可传参方法不能复用组合继承的一部分
组合继承借用构造 + 原型链属性独立,方法可复用,可传参调用两次父类构造函数ES6前的主流方式
原型式继承Object.create()无需构造函数,简单引用共享对象浅拷贝
寄生式继承工厂模式+增强对象无需构造函数,可增强对象方法不能复用,引用共享为对象添加功能
寄生组合继承借用构造 + 寄生式继承父类原型近乎完美,只调用一次父类构造函数实现稍复杂理想的继承范式

现代开发建议:

直接使用 ES6 的 class 和 extends 关键字。它们的语法更清晰、更易于理解,并且其底层实现的就是寄生组合式继承这种最理想的方式。你不再需要手动处理原型链,避免了出错的可能。

javascript

// ES6 的写法,底层是寄生组合式继承
class Parent {
    constructor(name) {
        this.name = name;
        this.colors = ['red', 'blue'];
    }
    sayName() {
        console.log(this.name);
    }
}

class Child extends Parent {
    constructor(name, type) {
        super(name); // 相当于 Parent.call(this, name)
        this.type = type;
    }
    sayType() {
        console.log(this.type);
    }
}

到此这篇关于JavaScript六种继承方式的文章就介绍到这了,更多相关JS六种继承方式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文