面向对象之继承

概念

继承:是由一个或多个类得来类的属性和方法的能力。

你应该知道的术语

对象:属性的无序集合,每个属性存放一个原始值、对象或者函数。

:每个对象都由类定义,可以把类看做对象的配方。

实例:使用类创建对象,生成的对象叫做类的实例。

你应该知道的方法

  1. instanceof:对象是否在原型链上
  2. getPrototypeOf:返回指定对象的原型

关于继承

创建对象

JavaScript对每个创建对象的对象都会设置一个内部指针,指向它的原型对象。

当我们用obj.xxx访问一个对象的属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找,如果还没有找到,就一直上溯到Object.prototype对象,最后,如果还没有找到,就只能返回undefined。

1
2
3
4
5
6
var arr = [1,2,3];
// arr ----> Array.prototype ----> Object.prototype ----> null
// Array的原型对象上定义了push,shift,pop等一系列的方法,因此你创建的arr也可以直接调用这些方法
function foo () {}
// foo ----> Function.prototype ----> Object.prototype ----> null
// 由于Function.prototype定义了apply()等方法,因此,所有函数都可以调用apply()方法

构造函数实现继承(不推荐)

通过构造函数创建实例对象,实例对象获得构造函数方法和属性

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person (name, age) {
this.name = name;
this.age = function () {
return age;
}
// return this(当它是构造函数是默认返回this)
}
var xiaoming = new Person('小明', 17);
console.log(xiaoming.name) // '小明'
console.log(xiaoming.age()) // ‘17’
// xiaoming ----> Person.prototype ----> Object.prototype ----> null

// 这样xiaoming就继承了Person的属性和方法。

如果不写new,这就是一个普通函数,它返回undefined。但是,如果写了new,它就变成了一个构造函数,它绑定的this指向新创建的对象,并默认返回this,也就是说,不需要在最后写return this。

同时,用new Person()创建的实例对象还从原型上获得了一个constructor属性,指向Person本身

但是这种方式new了多个对象会产生很多age函数,但是xiaoming.age 不等于 xiaohua.age,因为他们的属性和方法不是共享的。

那么怎么共享属性和方法呢?

可以通过构造函数绑定。

最简单的方式就是通过call或者apply方法,将父对象的构造函数绑定到子对象上,通过这种方式拉取父私有属性和方法。缺点是:只能共享私有属性和方法,无法共享原型的属性和方法

这种方式也就是对象冒充还有另一种写法也是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
function Person (name) {
this.name = name;
}
function Student (name, age) {
//用子类的this去冒充父类的this,实现继承
this.method = Person;
this.method(name);
delete this.method;
//此后的this均指子类
this.age = age;
}
var xiaoming = new Student('小明', 20)

prototype实现继承(不推荐)

说到原型继承,不免要说到原型对象、原型链以及原型继承。

原型对象: 每个函数都有一个prototype属性,该属性包含一个对象,存放所有实例对象需要共享的属性和方法。

原型链: 每个对象都有一个指针指向它的原型对象,而每个原型对象也有一个指针指向自己的原型,直到对象的原型为null,组成一条原型链。

原型继承:简单来说就是通过原型链实现属性继承。

思路:通过让原型对象等于另一个构造函数的实例,此时原型对象包含一个指向另一个原型的指针,另一个原型也包含指向另一个构造函数的指针,加入另一个原型是另一个类行的实例…这就构成了原型链。(此段以及下面这张图摘抄自知乎小姐姐的 文章,因为理解很到位)

具体示例如下

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
function Person () {
this.say = function () {
console.log('hello world')
}
}
Person.prototype.lived = function (address) {
return address
}

function Student (name, hobbies) {
this.name = name;
this.hobbies = hobbies;
this.play = function () {
return this.hobbies.join(',')
}
}
Student.prototype = new Person()
Student.prototype.constructor = Student // 指向本身
Student.prototype.friends = function () {
return 'xiaohua'
}
var xiaoming = new Student('小明', ['play games'])
xiaoming.name // 小明
xiaoming.say() // hello world
xiaoming.lived('earth') // earth
xiaoming.play() // play games
xiaoming.friends() // xiaohua

可见原型链

同时这种方式的 缺点是父类中的引用类型被共享,只要一个改动,所有都会改动;并且创建实例时无法向父类传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person (name) {
this.name = name;
this.privateArr = [1,2,3]
}
Person.prototype.commonArr = [4,5,6];
function Student () {}
Student.prototype = new Person()
Student.prototype.constructor = Student;
var xiaoming = new Student('小明')
var xiaohua = new Student('小华')
xiaoming.privateArr.push(100)
xiaoming.privateArr // [1,2,3,100]
xiaohua.privateArr // [1,2,3,100]
xiaoming.commonArr.push(200)
xiaoming.commonArr // [4, 5, 6, 200]
xiaohua.commonArr // [4, 5, 6, 200]
xiaoming.name // undefined

组合继承(推荐)

组合继承就是构造函数继承与prototype继承的组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person (name) {
this.name = name;
this.privateArr = [1,2,3]
}
Person.prototype.commonArr = [4,5,6];
function Student (name, age) {
this.age = age;
Person.call(this, name) // 向父类传参
}
Student.prototype = new Person()
Student.prototype.constructor = Student;
var xiaoming = new Student('小明', 18)
var xiaohua = new Student('小华', 20)
xiaoming.privateArr.push(100)
xiaoming.privateArr // [1,2,3,100]
xiaohua.privateArr // [1,2,3]
xiaoming.commonArr.push(200)
xiaoming.commonArr // [4, 5, 6, 200]
xiaohua.commonArr // [4, 5, 6]
xiaoming.name // 18

由此可见通过组合继承,解决了原型继承的共享引用类型和向父类传参的问题,但同时也有 缺点是调用了两次父类构造函数

寄生式继承(不推荐)

寄生式继承是与原型继承紧密相关的一种继承方式,与寄生构造函数和工程模式类似,即创建一个仅用于封装继承过程的函数,在内部就可以给新对象增加属性,新对象除了拥有原对象的属性,还拥有内部增加的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createObject (prototype) {
function F(){}
F.prototype = prototype;
return new F();
}

function createAnother(original){
var clone = createObject(original); // 通过调用函数创建一个新对象
clone.say = function () { // 其实这个就是要生成对象的私有方法
return 'hello world'
}
return clone; // 返回对象
}

var xiaoming = createAnother({ // 这个是要生成对象的原型方法和原型属性
name: "小明",
friends:['小华'],
lived: (address) => address
});

缺点是跟构造函数模式一样,每次创建对象都会创建一遍方法。

寄生组合继承(推荐)

为了解决组合继承的调用两次父类构造函数的问题,而有了寄生组合继承。原理上就是将父类的原型对象赋值给一个空属性的构造函数,再将空属性的构造函数赋值给子类的原型对象,避免调用两次构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function inherit (Child, Parent) {
let F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child; // 指回本身
}

function Person (name) {
this.name = name;
}
Person.prototype.say = function () {
return 'hello world'
};

function Student (name, age) {
this.age = age;
Person.call(this, name);
}

inherit(Student, Person); // 实现原型继承链

还有一种就是ES6提供的Extends,这个等复习Class的时候再讲。