Javascript面向对象与继承

众所周知,Javascript是一门面向对象的语言,如果说针对面向对象来发问的话,我会想到两个问题,在js中,类与实例对象是如何创建的,类与实例对象又是如何实现继承的。

面向对象

如何声明一个类

ES5中,还没有类的概念,而是通过函数来声明;到了ES6,有了class关键字,则通过class来声明

1
2
3
4
5
6
7
8
9
10
11
// 类的声明
var Animal = function () {
this.name = 'Animal';
};


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

如何创建对象

1.字面量对象
2.显示的构造函数
3.Object.create

1
2
3
4
5
6
7
8
9
// 第一种方式:字面量
var o1 = {name: 'o1'};
var o2 = new Object({name: 'o2'});
// 第二种方式:构造函数
var M = function (name) { this.name = name; };
var o3 = new M('o3');
// 第三种方式:Object.create
var p = {name: 'p'};
var o4 = Object.create(p);

类与继承

如何实现继承?
继承的本质就是原型链

借助构造函数实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 借助构造函数实现继承
*/
function Parent1 () {
this.name = 'parent1';
}
Parent1.prototype.say = function () {

};
function Child1 () {
Parent1.call(this); // 或Parent1.apply(this,arguments)
this.type = 'child1';
}
console.log(new Child1(), new Child1().say());

重点是这句:Parent1.call(this); 在子类的构造函数里执行父类的构造函数,通过call/apply改变this指向,从而导致父类构造函数执行时的这些属性都会挂载到子类实例上去。
问题: 只能继承父类构造函数中声明的实例属性,并没有继承父类原型的属性和方法

借助原型链实现继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 借助原型链实现继承
*/
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);
s1.play.push(4);

重点就是这句: Child2.prototype = new Parent2(); 就是说 new 一个父类的实例,然后赋给子类的原型 也就是说 new Child2().proto === Child2.prototype === new Parent2()当我们在new Child2()中找不到属性/方法,顺着原型链就能找到new Parent2(),这样就实现了继承。
问题: 原型链中的原型对象是共用的,子类无法通过父类创建私有属性
比如当你new两个子类s1、s2的时候,改s1的属性,s2的属性也跟着改变

组合式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 组合方式
*/
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
function Child3 () {
Parent3.call(this); // 父类构造函数执行了
this.type = 'child3';
}
Child3.prototype = new Parent3(); // 父类构造函数执行了
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);

组合式就是原型链+构造函数继承,解决了前两种方法的问题,但也有不足:子类实例化时,父类构造函数执行了两次,所以有了下面的组合继承的优化1

组合继承的优化1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 组合继承的优化1
* @type {String}
*/
function Parent4 () {
this.name = 'parent4';
this.play = [1, 2, 3];
}
function Child4 () {
Parent4.call(this);
this.type = 'child4';
}
Child4.prototype = Parent4.prototype;
var s5 = new Child4();
var s6 = new Child4();
console.log(s5, s6);

console.log(s5 instanceof Child4, s5 instanceof Parent4);
console.log(s5.constructor);

其实就是把原型链继承的那句 Child4.prototype = new Parent4(); 改为 Child4.prototype = Parent4.prototype; 这样虽然父类构造函数只执行了一次了,但又有了新的问题: 无法判断s5是Child4的实例还是Parent4的实例 因为Child4.prototype.constructor指向了Parent4的实例;如果直接加一句 Child4.prototype.constructor = Child4 也不行,这样Parent4.prototype.constructor也指向Child4,就无法区分父类实例了。

若要判断a是A的实例 用constructor
a.proto.constructor === A
用instanceof则不准确, instanceof 判断 实例对象的proto 是不是和 构造函数的prototype 是同一个引用。若A 继承 B, B 继承 C 在该原型链上的对象 用instanceof判断都返回ture

组合继承的优化2(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 组合继承的优化2
*/
function Parent5 () {
this.name = 'parent5';
this.play = [1, 2, 3];
}
function Child5 () {
Parent5.call(this);
this.type = 'child5';
}
//注意此处,用到了Object.creat(obj)方法,该方法会对传入的obj对象进行浅拷贝
//这个方法作为一个桥梁,达到父类和子类的一个隔离
Child5.prototype = Object.create(Parent5.prototype);
//修改构造函数指向
Child5.prototype.constructor = Child5

构造函数属性继承和建立子类和父类原型的链接

ES6实现继承

引入了class、extends、super关键字,在子类构造函数里调用super()方法来调用父类的构造函数。
在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例。

1
2
3
4
5
6
7
8
9
class Child6 extends Parent6 {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // super代表父类原型,调用父类的toString()
}
}

class实现原理

Class充当了ES5中构造函数在继承实现过程中的作用
有prototype属性,有proto属性,这个属性在ES6中的指向有一些主动的修改。
同时存在两条继承链:一条实现属性继承,一条实现方法继承。

1
2
3
class A extends B {}
A.__proto__ === B; //继承属性
A.prototype.__proto__ === B.prototype; //继承方法

ES6的子类的proto是父类,子类的原型的proto是父类的原型。
但是在ES5中 A.proto是指向Function.prototype的,因为每一个构造函数其实都是Function这个对象构造的,ES6中子类的proto指向父类可以实现属性的继承。

只有函数有prototype属性,只有对象有proto属性 ;但函数也有proto属性,因为函数也是一个对象,函数的proto等于 Function.prototype。

extends实现原理

1
2
3
4
5
6
//原型连接
Man.prototype = Object.create(Person.prototype);
// B继承A的静态属性
Object.setPrototypeOf(Man, Person);
//绑定this
Person.call(this);

前两句实现了原型链上的继承,最后一句实现构造函数上的继承。

分享到:
Disqus 加载中...

如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理