JavaScript之继承(包含ES6)

原型链继承

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() {
this.names = ['foo', 'bar'];
this.age = 60;
}
Parent.prototype.greeting = function() {
console.log("hello!");
}
function Child() {

}

Child.prototype = new Parent(); //将父类实例作为子类原型

var child1 = new Child();

child1.names.push('baz');

console.log(child1.names); // ["foo", "bar", "baz"]

var child2 = new Child();

console.log(child2.names); // ["foo", "bar", "baz"]

child1.greeting() // hello!

var age1 = child1.age = 5
console.log(age1) //5
console.log(child2.age) //60 依旧继承父类基本类属性
//修改其中一个实例子类基本类型属性不会影响其他子类基本类型属性

原型链继承
优点:

  1. 方法复用,由于方法定义在父类的原型上,复用了父类构造函数的方法。比如greeting方法。

缺点:

  1. 创建子类实例的时候,不能传参数。
  2. 子类实例共享了父类构造函数的引用属性,比如names属性。child1.names修改会影响child2.names的值
  3. 但是实例子类基本类型属性不会影响其他子类基本类型属性。child1.age

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Parent () {
this.names = ['foo', 'bar'];
this.greeting = function() { // 方法都在构造函数中定义,每次创建实例都会创建一遍方法。且实例不能共用此方法
console.log('hello')
}
}

function Child () {
Parent.call(this); //核心技术
}

var child1 = new Child();

child1.greeting() // undefined 方法都在构造函数中定义,每次创建实例都会创建一遍方法。且实例不能共用此方法

child1.names.push('baz');

console.log(child1.names); // ['foo', 'bar', 'baz']

var child2 = new Child();

console.log(child2.names); // ['foo', 'bar'] //避免了引用类型的属性被所有实例共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Parent (name) {   //创建子类实例,可以向父类构造函数传参数。
this.name = name;
}

function Child (name) {
Parent.call(this, name); //核心技术
}

var child1 = new Child('huahua');

console.log(child1.name); // huahua

var child2 = new Child('dandan');

console.log(child2.name); // dandan

借用构造函数
优点:

  1. 避免了引用类型的属性被所有实例共享,比如names属性
  2. 创建子类实例,可以向父类构造函数传参数。

缺点:

  1. 父类的方法不能复用
  2. 方法都在构造函数中定义,每次创建实例都会创建一遍方法。

组合继承

原型链继承和构造函数组合拳

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
30
31
32
33
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
console.log(this.name)
}
//构造函数
function Child (name, age) {

Parent.call(this, name); 一次函数调用

this.age = age;

}

Child.prototype = new Parent(); //原型链继承 二次函数调用
Child.prototype.constructor = Child; //修复Child.prototype.constructor

var child1 = new Child('foo', '18');

child1.colors.push('black');

console.log(child1.name); // foo
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('bar', '20');

console.log(child2.name); // bar
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

组合继承
优点:

  1. 保留构造函数的优点:创建子类实例,可以向父类构造函数传参数。
  2. 保留原型链的优点:父类的实例方法定义在父类的原型对象上,可以实现方法复用。
  3. 不共享父类的引用属性。比如colors属性

缺点:

  1. 由于调用了2次父类的构造方法,会存在一份多余的父类实例属性

    注意:‘组合继承’这种方式,要记得修复Child.prototype.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
function Parent(name) {
this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
this.colors = ['red', 'blue']; // (该属性,强调私有)
}
Parent.prototype.say = function() { // --- 将需要复用、共享的方法定义在父类原型上
console.log('hello')
}
function Child(name,like) { //实例实现可传参
Parent.call(this,name,like) // 核心
this.like = like;
}
// 核心 通过创建中间对象,子类原型和父类原型,就会隔离开。不是同一个啦,有效避免了组合继承的缺点。
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();

//或者用ES5,Object.create()创建对象,内部原理就是通过创建中间对象实现的

<!--这里是修复构造函数指向的代码-->
Child.prototype.constructor = Child

var child1 = new Child('小红','apple')
var child2 = new Child('小明','orange')
var p1 = new Parent('小爸爸')
child1.say()
child2.say()

代码优化:

1
2
3
4
5
6
7
8
9
10
11
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
// 当我们使用的时候:
prototype(Child, Parent);

寄生组合继承
优点:完美
缺点:理论上没有

ES6继承

ECMAScript6 引入了一套新的关键字用来实现 class。使用基于类语言(java,php等)的开发人员会对这些结构感到熟悉,但它们是不一样的。 JavaScript 仍然是基于原型的。这些新的关键字包括 class , constructor , static , extends , 和 super . 例子如下:

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
30
31
32
33
34
35
36
37
38
class Animal {
// 构造方法,实例化的时候将会被调用,如果不指定,那么会有一个不带参数的默认构造函数.
constructor(name,color) {
this.name = name;
this.color = color;
}
// toString 是原型对象上的属性
toString() {
console.log('name:' + this.name + ',color:' + this.color);

}
}

var animal = new Animal('dog','white');
animal.toString();

console.log(animal.hasOwnProperty('name')); //true
console.log(animal.hasOwnProperty('toString')); // false
console.log(animal.__proto__.hasOwnProperty('toString')); // true

class Cat extends Animal {
constructor(action) {
// 子类必须要在constructor中指定super 方法,否则在新建实例的时候会报错.
// 如果没有置顶consructor,默认带super方法的constructor将会被添加、
super('cat','white');
this.action = action;
}
toString() {
console.log(super.toString());
}
}

var cat = new Cat('catch')
cat.toString();

// 实例cat 是 Cat 和 Animal 的实例,和Es5完全一致。
console.log(cat instanceof Cat); // true
console.log(cat instanceof Animal); // true

class和自定义类型的区别

  • class的声明不会提升,与let类似
  • class的声明自动运行于严格模式之下
  • class声明的方法不可枚举(显著区别)
  • class的内部方法没有[[construct]]属性,无法new
  • 调用class的构造函数必须new
  • class内部方法不能同名

ES6继承小结:

  • ES6中class简化了ES5中的继承,但是未改变现有的继承模型。可以理解为是ES5基于原型链的语法糖
  • 通过class声明一个类,constructor()作为构造函数,属性在constructor()中初始化
  • class内可以定义getter/setter访问器属性
  • 可以在class内定义非静态方法,静态方法绑定在构造器上
  • 类的所有方法都是不可枚举的,也符合内部方法
  • 实例化一个class必须要new关键字
  • extends实现继承,子类中调用super()访问父类构造函数
  • 因为class的实现是基于ES5类模型那一套,本质上和ES5中是一样的,如果过多使用extends可能还会降低性能

ES6 继承推荐看阮一峰的ES6

Buy me a cup of coffee,thanks!