面向对象

1、类与实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**  
* 类的声明
*/

function Animal(){
this.name = 'name'
}

/*
* ES6 中的声明
*/
class Animal2{
constructor(){
this.name = name;
}
}

/*
* 实例化
*/
console.log(new Animal(), new Animal2());

2、类与继承

2.1 通过构造函数实现

1
2
3
4
5
6
7
8
function Parent1(){
this.name = 'parent1';
}

function Child1(){
parent1.call(this); // apply
this.type = 'child1';
}

call 改变函数运行上下文

缺点:Parent1 原型链上的内容无法被继承

下面我们通过一个栗子来验证:

1
2
3
4
5
6
7
8
9
10
11
function Parent1(){
this.name = 'parent1';
}
Parent1.prototype.say = function(){}

function Child1(){
Parent1.call(this);
this.type = 'child1';
}

console.log(new Child1().say()); // Uncaught TypeError: (intermediate value).say is not a function

因此,如果父类所有属性都在构造函数中,那么可以使用此种方法,否则,父类原型链上的属性无法被子类继承。

2.2 通过原型链实现

1
2
3
4
5
6
7
8
9
function Parent2(){
this.name = 'parent2';
}

function Child2(){
this.type = 'child2';
}

Child2.prototype = new Parent2();

缺点:实例具有相同的原型对象引用,因此原型链上的属性是公用的。如果某个实例对象修改了原型链上的方法,则其他实例对象也会跟着改变。

修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Parent2(){
this.name = 'parent2';
this.play = [1,2,3];
}

function Child2(){
this.type = 'child2';
}

Child2.prototype = new Parent2();

var s1 = new Child2();
var s2 = new Child2();

console.log(s1.play, s2.play); // [1, 2, 3] [1, 2, 3]

s1.play.push(4);
console.log(s1.play, s2.play); // [1, 2, 3, 4] [1, 2, 3, 4]

为了弥补以上两种方式的不足,我们对上述方法进行改进,得出了以下几种组合方法:

2.3 构造函数与原型链组合方法

1
2
3
4
5
6
7
8
9
10
11
function Parent3(){
this.name = 'Parent3';
this.play = [1,2,3];
}

function Child3(){
Parent3.call(this);
this.type = 'child3';
}

Child3.prototype = new Parent3();

验证一下:

1
2
3
4
5
6
var s3 = new Child3();
var s4 = new Child3();

s3.play.push(4);

console.log(s3.play, s4.play); //[1, 2, 3, 4] [1, 2, 3]

由此可见,改种方法弥补了上述两个方法的不足。但仍然有自身的缺点:

父类的构造函数被调用了两次:一次在 Parent3.call(this),一次在 new Parent3() 时。

2.4 构造函数与原型链组合方法的优化1

1
2
3
4
5
6
7
8
9
10
11
function Parent4(){
this.name = 'Parent4';
this.play = [1,2,3];
}

function Child4(){
Parent4.call(this);
this.type = 'child4';
}

Child4.prototype = Parent4.prototype;

这种方法仍然有缺点:实例对象的 constructor 属性是父类构造器,而不是子类构造器。这是由于 Child4.prototype = Parent4.prototype 导致的,Child4 没有自己的 constructor,而是继承自 Parent4,而 Parent4 的 constructor 是 Parent4。

验证一下:

1
2
3
4
5
6
var s5 = new Child4();
var s6 = new Child4();

console.log(s5 instanceof Child4, s5 instanceof Parent4); // true true

console.log(s5.constructor); // Parent4

事实上,方法三也有同样的问题。

2.5 构造函数与原型链组合方法的优化2

1
2
3
4
5
6
7
8
9
10
11
function Parent5(){
this.name = 'Parent5';
this.play = [1,2,3];
}

function Child5(){
Parent4.call(this);
this.type = 'child5';
}

Child5.prototype = Object.create(Parent5.prototype);

Object.create() 方法相当于创建了一个中间对象,而这个中间对象的原型对象指向 Parent5,构成了一个原型链。同时实现了父类和子类的隔离。

但此时 Child5 仍然没有自己的 constructor,只能通过原型链来查找。因此,new Child5().constructor 仍然是 Parent5。再做修改:

1
Child5.prototype.constructor = Child5;

那么问题来了,在方法4中是不是也能用 Child.prototype.constructor = Child 的方法来实现呢?

因为方法4中 Child4.prototype = Parent4.prototype,因此如果按照该方法修改,则会导致父类的 constructor 也被修改。

2.6 总结

综上所述,继承最好的方式为:

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

function Child(){
Parent.call(this);
this.type = 'child';
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;