概述

原型链继承

原型链继承是 JavaScript 最基本的继承方式,通过让子类的原型对象(prototype)指向父类的实例,继承父类的属性和方法。但是,它也存在一些问题,比如父类中引用类型值的属性被子类实例共享、不能传递参数等。

构造函数继承

构造函数继承是指在子类的构造函数中调用父类的构造函数,从而实现继承。但是,它存在一个问题,就是无法继承父类原型对象上的属性和方法。

组合继承

组合继承是指将原型链继承和构造函数继承组合在一起,从而实现继承。它既可以继承父类的实例属性和方法,也可以继承父类原型对象上的属性和方法。但是,它的缺点是在子类的构造函数中调用了父类的构造函数,同时在设置子类原型时又会再次创建父类实例,导致父类的构造函数被调用了两次。

原型式继承

原型式继承是指借助一个中间对象,将某个对象作为这个中间对象的原型对象,从而实现继承。它的优点是可以简单地实现对象之间的继承,但是它也存在一个问题,就是父对象的属性会被子对象共享,可能会导致子对象对父对象的属性进行修改,影响其他子对象。

寄生式继承

寄生式继承是指在原型式继承的基础上,增强了继承对象的能力。它在继承的基础上,增加了一些额外的属性或方法。但是,它存在一些问题,比如存在对象识别问题、存在引用类型值的共享问题等。

寄生组合式继承

寄生组合式继承是一种综合利用构造函数继承和原型链继承的方式,从而解决它们各自的问题。它通过借用构造函数来继承实例属性,通过原型链的方式来继承原型对象上的属性和方法。这种继承方式是目前使用最广泛的继承方式,也是最优秀的继承方式之一。

原型链继承

利用原型链的机制来实现对象之间的继承关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 父类构造函数
function Parent() {
this.name = 'Parent';
this.sayHello = function() {
console.log('Hello, I am ' + this.name);
}
}

// 子类构造函数
function Child() {
this.age = 18;
}

// 将子类的原型对象指向父类的实例
Child.prototype = new Parent();

// 创建子类的实例
var child = new Child();

// 子类实例继承了父类的属性和方法
console.log(child.name); // "Parent"
child.sayHello(); // "Hello, I am Parent"
console.log(child.age); // 18

在上面的例子中,我们定义了一个 Parent 构造函数和一个 Child 构造函数,将 Child 的原型对象指向 Parent 的实例,这样 Child 的实例就可以访问 Parent 的属性和方法了。

原型链继承存在以下问题:

1.引用类型属性共享:子类实例对引用类型属性的修改会影响父类实例以及其他子类实例。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Parent() {
this.colors = ['red', 'green', 'blue'];
}

function Child() {
}

Child.prototype = new Parent();

var child1 = new Child();
child1.colors.push('yellow');

var child2 = new Child();
console.log(child2.colors); // ['red', 'green', 'blue', 'yellow']

在这个例子中,Child 继承了 Parent 的属性和方法,包括 colors 数组。当 child1colors 中添加一个新元素时,child2colors 也会受到影响,因为它们共享同一个原型对象。

2.无法向父类构造函数传递参数:在创建子类实例时,不能向父类构造函数传递参数。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
function Parent(name) {
this.name = name;
}

function Child() {
}

Child.prototype = new Parent();

var child1 = new Child('Bob');
console.log(child1.name); // undefined

在这个例子中,Child 想要从父类 Parent 中继承 name 属性,但是在创建子类实例时,没有传递参数给父类构造函数,导致 child1name 属性值为 undefined

构造函数继承

构造函数继承是通过在子类构造函数中调用父类构造函数(Parent.call(this))来实现继承的一种方式。在子类构造函数中,通过调用父类构造函数来获取父类的属性,并将其赋值给子类的实例。这样子类就能够继承父类的属性了。

具体实现步骤如下:

定义父类构造函数,设置父类属性

1
2
3
4
function Parent() {
this.name = "parent";
this.age = 30;
}

在子类构造函数中调用父类构造函数,获取父类的属性

1
2
3
4
function Child() {
Parent.call(this); //通过call方法来调用父类构造函数,并将子类实例作为参数传入
this.type = "child";
}

这里的call方法可以改变函数的this指向,将子类的this指向父类构造函数中创建的新对象,以便子类继承父类的属性。

定义子类的原型,继承父类的方法

1
2
codeChild.prototype = new Parent();
Child.prototype.constructor = Child; //修正constructor指向

这里需要将子类的原型指向一个新的父类实例,这样子类就可以访问到父类原型上的属性和方法了。

构造函数继承的优点是可以在创建子类实例时向父类构造函数传递参数,同时可以避免原型链继承中的引用类型共享问题。但是,构造函数继承的缺点是无法继承父类原型链上的方法,同时每个子类实例都会创建一个新的父类实例,从而可能会降低程序的性能。

缺点:

1.无法继承父类原型对象上的属性和方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Parent() {
this.name = 'parent';
this.sayHello = function() {
console.log('Hello, I am ' + this.name);
}
}

Parent.prototype.sayBye = function() {
console.log('Bye bye!');
}

function Child() {
Parent.call(this); // 调用父类构造函数,将this指向子类实例
this.type = 'child';
}

const child = new Child();

console.log(child.name); // 'parent'
child.sayHello(); // 'Hello, I am child'
console.log(child.sayBye); // undefined

2.每个实例都会拷贝一份父类构造函数中的属性和方法,会浪费内存空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Parent() {
this.names = ['Alice', 'Bob'];
}

function Child() {
Parent.call(this);
}

const child1 = new Child();
const child2 = new Child();

child1.names.push('Charlie');
console.log(child1.names); // ['Alice', 'Bob', 'Charlie']
console.log(child2.names); // ['Alice', 'Bob']

3.无法判断一个对象的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Parent() {
this.name = 'parent';
}

function Child() {
Parent.call(this);
}

const parent = new Parent();
const child = new Child();

console.log(parent instanceof Parent); // true
console.log(parent instanceof Child); // false
console.log(child instanceof Parent); // false
console.log(child instanceof Child); // true

组合继承

组合继承是 JavaScript 中一种常见的继承方式,它结合了原型链继承和构造函数继承的优点。通过在子类构造函数中调用父类构造函数,实现对实例属性的继承,同时通过将子类的原型指向父类的实例,实现对原型属性和方法的继承。

下面是一个简单的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Parent(name) {
this.name = name;
}

Parent.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
}

function Child(name, age) {
Parent.call(this, name);//构造函数继承
this.age = age;
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() {
console.log(`I am ${this.age} years old`);
}

const child = new Child('Tom', 10);
child.sayHello(); // Hello, my name is Tom
child.sayAge(); // I am 10 years old

在上面的代码中,Parent 是父类,它有一个实例属性 name 和一个原型方法 sayHelloChild 是子类,它继承了 Parent 的实例属性 name,并添加了自己的实例属性 age 和原型方法 sayAgeChild 的原型指向 Parent 的实例,这样 Child 的原型就继承了 Parent 的原型方法 sayHello

组合继承的优点在于解决了属性共享问题,支持原型方法继承。但是,它也有一个缺点,就是在创建子类实例时,会调用两次父类构造函数,一次在子类构造函数中,一次在将父类实例赋值给子类原型时,子类原型中存在冗余属性。

原型式继承

原型式继承是指利用一个已有的对象作为新对象的原型,从而创建出一个新的对象。这个新对象可以与原型对象共享属性和方法。这种继承方式主要使用 Object.create() 方法来实现。

其基本思路是先定义一个原型对象,然后通过 Object.create() 方法来创建新对象,新对象会继承原型对象的所有属性和方法。通过修改原型对象,就能够改变新对象的属性和方法。

下面是一个例子,演示了如何使用原型式继承:

原型式继承是指利用一个已有的对象作为新对象的原型,从而创建出一个新的对象。这个新对象可以与原型对象共享属性和方法。这种继承方式主要使用 Object.create() 方法来实现。

其基本思路是先定义一个原型对象,然后通过 Object.create() 方法来创建新对象,新对象会继承原型对象的所有属性和方法。通过修改原型对象,就能够改变新对象的属性和方法。

下面是一个例子,演示了如何使用原型式继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义原型对象
let person = {
name: 'Tom',
sayHello: function() {
console.log('Hello, my name is ' + this.name);
}
};

// 使用原型对象创建新对象
let student = Object.create(person);

// 修改新对象的属性
student.name = 'Jerry';

// 调用新对象的方法
student.sayHello(); // 输出 "Hello, my name is Jerry"

在这个例子中,我们先定义了一个原型对象 person,然后使用 Object.create() 方法来创建新对象 student。新对象 student 继承了原型对象 person 的所有属性和方法。通过修改新对象的属性,我们可以改变新对象的状态。同时,新对象仍然可以访问原型对象的属性和方法。

原型式继承的缺点与原型链继承类似:由于所有的新对象都会共享同一个原型对象,因此对原型对象的修改会影响到所有继承自它的对象。

寄生式继承

寄生式继承是在原型式继承的基础上进行扩展,它在创建新对象的过程中,对新对象进行一些额外的增强操作。

具体来说,寄生式继承的实现过程是这样的:

  1. 声明一个用于继承的基础对象,通常是一个空对象。
  2. 通过某种方式,增强这个基础对象,比如给它添加属性或方法。
  3. 返回这个基础对象,作为继承结果。

下面是一个简单的例子,演示了如何使用寄生式继承来实现一个简单的对象继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 定义一个用于继承的基础对象
let parent = {
name: 'Parent',
sayHi() {
console.log(`Hi, I'm ${this.name}!`);
}
};

// 声明一个用于继承的函数,它的参数是一个对象
function createChild(obj) {
// 通过原型式继承创建一个新对象
let child = Object.create(obj);
// 对新对象进行增强
clone.sayHi = function() {
console.log('Hi');
};
child.age = 18;
return child;
}

// 用 parent 对象来创建一个新的子对象
let child = createChild(parent);

// 输出 child 对象的属性和方法
console.log(child.name); // 'Parent'
console.log(child.age); // 18
child.sayHi(); // 'Hi, I'm Parent!'

可以看到,在这个例子中,我们使用了一个名为 createChild 的函数来实现继承。这个函数的参数是一个对象,它用于指定继承自哪个对象。函数内部,我们首先使用 Object.create 方法来创建一个新对象,该对象的原型链指向传入的对象 obj,从而实现了继承。然后,我们为这个新对象添加了一个新属性 age,最后返回这个新对象,作为继承的结果。

优点:可扩展对象功能。
缺点:新增方法无法复用(每个实例单独创建)。

寄生组合式继承

寄生组合式继承是组合继承的优化版本,通过寄生式继承(利用 Object.create())替代直接的原型链继承,从而避免父类构造函数被重复调用,同时保留了构造函数继承和原型链继承的优点。

在寄生组合式继承中,我们先创建一个临时性的构造函数,这个构造函数的作用仅仅是用于取得父类的原型对象。然后,将这个临时性的构造函数的原型对象赋值给子类的原型对象,最后再将子类原型对象的 constructor 属性设置为子类本身。

以下是一个寄生组合式继承的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 父类构造函数
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}

Parent.prototype.sayName = function() {
console.log(this.name);
};

// 子类构造函数
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}

// 寄生组合继承的核心函数
function inheritPrototype(subType, superType) {
const prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}

// 调用寄生组合继承函数
inheritPrototype(Child, Parent);

const child = new Child('child', 18);
child.sayName(); // 输出: child
console.log(child.colors); // 输出: ['red', 'blue']

优点:

  • 只调用一次父类构造函数,避免了组合继承的缺点。
  • 可以继承父类构造函数中的属性和方法,也可以继承父类原型上的属性和方法。

ES6 Class 继承

使用 class 和 extends 语法糖(底层基于寄生组合式继承)。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Parent {
constructor(name) {
this.name = name;
}
sayHello() {
console.log('Hello from Parent');
}
}

class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
}

const child = new Child('Child', 10);
child.sayHello(); // "Hello from Parent"

优点:语法简洁,符合现代编程习惯。

缺点:底层原理仍需理解(本质是寄生组合式继承)。