面向对象继承方法

继承的概念:通过一定的方式实现让某个类型A获取另外一个类型B的属性方法。其中类型A称为子类型,类型B称为父类型或超类型

JavaScript中的继承:Object所有对象的父类型,所有Javascript中的对象都是直接或间接继承自Object

继承有两种方式:接口继承实现继承,在JS中只支持实现继承,实现继承主要依赖原型链来完成。

说明:其他语言中继承通常通过来实现,JS中没有的概念(ES6新增了class)。JS中的继承是某个对象继承继承另外一个对象,是基于对象的。

1. 属性拷贝(混入式继承)

属性拷贝是浅拷贝,复制引用类型时,只会复制地址,两者会共享同一数据,修改一个会影响另外一个。

1
2
3
4
5
6
7
8
9
10
11
12
var obj1 = {
name : '小明',
age : 20,
friends : ['小红', '小智']
};
var obj2 = {};
//obj2继承obj1的属性
for (var k in obj1) {
obj2[k] = obj1[k]; //遍历复制属性
}
obj2.friends.push('小王'); //为子对象添加一个属性
//obj1的friends里也会被增加一个小王

在ES6中添加了一个Object.assign()属性

1
2
3
4
5
6
7
8
9
var copy = {};
var obj1 = {name : '小明'};
var obj2 = {age : 20};
var obj3 = {friends : ['小红']};
//第一个参数:目标对象,后面的参数:需要拷贝的对象
Object.assign(copy, obj1, obj2, obj3);
console.log(copy); //输出{name : '小明',age : 20,friends : ['小红']}
copy.friends.push('小王');
console.log(obj3); //输出{friends : ['小红', '小王']}

同样会影响父对象中的引用类型属性。

2. 原型式继承

利用构造函数创建出来的对象可以使用原型对象中的属性和方法

简单的原型式继承:

1
2
3
4
5
function Fun() {}
Fun.prototype.des = 'des';
var obj = new Fun();
console.log(obj.des); //des
//构造函数创建的对象可以使用原型的属性方法

复杂一点的原型式继承:

1
2
3
4
5
6
7
8
9
10
function Fun1() {
this.name = '名字';
}
Fun1.prototype.des = 'des';
function Fun2() {}
Fun2.prototype = Fun1.prototype;
var obj = new Fun2(); //无法获取到name属性
//应该修正构造器的指向
//Fun2.prototype.constructor = Fun2;
console.log(obj.constructor); //默认指向Fun1

问题: 1.原型式继承创建出来的子对象的构造器属性默认指向父构造函数,需要修正consturctor的指向。
2.无法获取到父对象构造函数中的属性。

扩展内置对象

系统内置的对象(ArrayObject等)都可以通过在原型对象上添加属性的方式来让所有子对象拥有这一属性。

1
2
3
4
5
6
Array.prototype.add = '添加的属性';
Array.prototype.addFun = function () {
console.log(this.add);
};
var arr = [1, 2, 3];
arr.addFun(); //添加的属性

不建议使用这种方式,在实际开发中通常是多人合作。如果都扩展内置对象,后期将难以维护,同时也会引发覆盖等安全问题。

安全的扩展内置对象

  1. 提供一个自定义构造函数
  2. 设置构造函数的原型对象是内置对象的实例
1
2
3
4
5
6
7
8
9
10
11
12
//提供一个构造函数
function MyArray() {};
//设置构造函数的原型对象为内置对象的实例
MyArray.prototype = new Array();
MyArray.prototype.des = 'des';
MyArray.prototype.logDes = function () {
console.log(this.des);
}
var arr = new MyArray();
arr.push('123');
arr.logDes(); //des
console.log(arr); // [123]; 继承来自Array的属性方法

3. 原型链继承

实现原型链继承的过程:先提供一个子构造函数和父构造函数,设置子构造函数的原型对象是父构造函数的实例。
原型链继承图

1
2
3
4
5
6
7
8
9
10
11
12
13
//设置父构造函数
function Person() {
this.name = '名字';
}
//设置父构造函数的原型对象
Person.prototype.des = 'des';
//设置子构造函数
function Student() {}
//设置原型链继承
Student.prototype = new Person();
var stu = new Student();
console.log(stu.des); //des
console.log(stu.name); //名字

原型链继承的注意点

在完成原型链继承之后,再进行:

  1. 修正构造器属性的指向
  2. 设置子构造函数的原型对象的属性和方法,并且不要使用字面量的方式修改子构造函数属性,会产生替换,应使用对象的动态特性设置

原型链继承的问题

  1. 无法传递参数给父构造函数
  2. 用子构造函数创建多个实例对象时,只会复制引用类型数据地址,会产生共享问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //设置父构造函数
    function Person(name) {
    this.name = name;
    this.friends = ['小明'];
    }
    //创建子构造函数
    function Student(num) {
    this.num = num;
    }
    //设置原型链继承
    Student.prototype = new Person();
    //修正构造器指向
    Student.prototype.constructor = Student;
    //创建实例对象,无法传递参数到父构造函数
    var stu1 = new Student('2017');
    var stu2 = new Student('2018');
    //设置stu1,也会影响stu2
    stu1.friends.push = '小红';
    console.log(stu1); //2017 [小明, 小红]
    console.log(stu2); //2018 [小明, 小红]

Object.creat()方法实现继承

作用:创建一个对象,并设置该对象的原型对象为指定对象
兼容性:ES5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
name : '名字'
};
//o为创建的对象,设置它的原型对象为obj
var o = Object.creat(obj);
console.log(o.name); //名字
//相当于
var newObj = {
name : '名字'
};
//非标写法 var newO={}; newO.__proto__ = newObj;
function Fun(){};
Fun.prototype = newObj;
var newO = new Fun();

4. 借用构造函数(经典继承 | 伪对象继承)

callapply的用法

用法:Function.prototype.call/apply()
作用:借用其他对象的方法
参数输入:第一个参数为需要借参数的对象,之后的为输入的参数
不同点:call(对象, 参数1, 参数2…) 参数列表
apply(对象, [参数1, 参数2…]) 参数数组

关于this的指向:使用了callapply方法后,调用的this指向被绑定的对象(第一个参数)。

借用构造函数的用法

借用构造函数实现继承解决了无法传递参数给父构造函数的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name) {
this.name = name;
}
function Student(num, name) {
this.num = num;
//通过借用构造函数调用父构造函数的方法实现继承
Person.call(this, name);
}
//创建不同的实例对象
var stu1 = new Student('2017', '小明');
var stu2 = new Student('2018', '小红');
console.log(stu1); // 2017 小明
console.log(stu2); // 2018 小红

5. 组合继承

  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
//设置构造函数属性
function Person(name) {
this.name = name;
}
//设置原型对象属性方法
Person.prototype.des = 'des';
Person.prototype.logDes = function () {
console.log(this.des);
}
//创建子构造函数
function Student(num, name) {
this.num = num;
//通过借用构造函数调用父构造函数的方法实现继承
Person.call(this, name);
}
//利用原型式继承实现继承父构造函数原型对象
Student.prototype = Person.prototype;
//创建不同的实例对象
var stu1 = new Student('2017', '小明');
var stu2 = new Student('2018', '小红');
console.log(stu1); // 2017 小明
console.log(stu2); // 2018 小红
//子构造函数与父构造函数共享同一个原型对象
Student.prototype.des = 'desStu';
//子构造函数原型对象设置了属性,父构造函数原型对象也受到影响
var obj = new Person('小王');
obj.logDes(); //desStu

6. 深拷贝继承

浅拷贝(地址拷贝)

1
2
3
4
var copy = {};
for (var k in obj) {
copy[k] = obj[k];
}

浅拷贝是对值类型属性的复制,遇到引用类型数据时,只能复制其地址

深拷贝(完全拷贝)

  1. 创建一个深拷贝的函数,提供2个参数,一个是目标对象,一个是需要拷贝的对象
  2. for in遍历目标对象获取属性,进行判断属性的类型
    1. 如果是值类型,直接赋值
    2. 如果是引用类型,再调用这个函数,拷贝引用类型里的数据

函数一般是直接调用,不需要修改数据。所以可以当作值类型直接复制地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function deepClone(obj, copy) {
//被拷贝的对象必须是一个对象
copy = copy || {};
//遍历在目标对象中的所有属性
for (var k in obj) {
//判断是否是自身的属性,防止遍历到原型属性
if (obj.hasOwnProperty(k)) {
//如果是引用类型的属性(function除外)就要进行深拷贝
if (typeof obj[k] === 'object') {
//判断属性是数组还是对象
copy[k] = obj[k].constructor === Array ? [] : {};
//递归进更深一层
deepClone(obj[k], copy[k]);
} else {
//值类型或者是函数直接复制
copy[k] = obj[k];
}
}
}
return copy;
}

在判断对象是否是数组时,我们也可以用到一个方法Array.isArray()
括号内输入需要判断的对象,返回值是Boolean类型。
但是这个方法是ES5才出来,所以具有兼容问题。

利用深拷贝实现继承

  1. 借用构造函数获取实例属性,callapply
  2. 深拷贝获取原型属性

完美的拷贝,独立并且互不影响。