某人

此前素未谋面、此后遥遥无期

0%

面向对象-继承

原型链

JavaScript主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的

内部指针__proto__

  1. 遵循ECMAScript标准,someObject.[[Prototype]] 符号是用于指向 someObject的原型;
  2. 这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__;
  3. 从 ECMAScript 6 开始,[[Prototype]] 可以通过Object.getPrototypeOf()Object.setPrototypeOf()访问器来访问;
  4. 但它不应该与构造函数 funcprototype 属性相混淆
  5. 被构造函数创建的实例对象的 [[prototype]] 指向 funcprototype 属性。
  6. Object.prototype 属性表示Object的原型对象。

基本概念

image

JavaScript 对象有一个私有属性(称之为 [[Prototype]]),它指向它的 原型对象(prototype);

prototype对象 又具有一个自己的 prototype,层层向上直到一个对象的原型为 null,

根据定义,null 没有原型,并作为这个原型链中的最后一个环节

两种继承方式

  • 接口继承:只继承方法签名(由于函数没有签名,ECMAScript无法实现此继承)
  • 实现继承:继承实际方法(ECMAScript只支持实现继承)

实现继承的方法

将原型链作为实现继承的方法。

基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法

JavaScript中的类(class)继承

  1. 构造函数内的,这是供实例化对象复制用的
  2. 构造函数外的,直接通过点语法添加的,这是供类使用,实例化对象是访问不到的
  3. 类的原型中,实例化对象可以通过其原型链间接访问到,也是为供所有实例化对象所共用的

类式继承(子类的原型对象)

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
//声明父类
function SuperClass(){
this.supperValue = true;
this.books = ["大数据","人工智能","区块链","Kubernetes"];
}

//为父类添加共有方法
SuperClass.prototype.getSuperValue = function(){
return this.supperValue;
}

//声明子类
function SubClass(){
this.subValue = false;
}

//继承父类
SubClass.prototype = new SuperClass();

//为子类添加共有方法
SubClass.prototype.getSubValue = function(){
return this.subValue;
}

/*******************************测试代码****************************************/
var instance1 = new SubClass();
var instance2 = new SubClass();

console.log(instance2.books);//["大数据,"人工智能","区块链","Kubernetes"]

instance1.books.push("Docker");
console.log(instance2.books);//["大数据","人工智能","区块链","Kubernetes","Docker"]

缺点

  1. instance1的修改伤害了instance2books属性
  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
//声明父类
function SuperClass(id){
this.id = id;
this.books = ["大数据","人工智能","区块链","Kubernetes"];
}

//为父类添加共有方法
SuperClass.prototype.getBooks = function(){
return this.books;
}

//声明子类
function SubClass(id){
//继承父类
SuperClass.call(this,id)
}

/**************************************测试代码********************************/
var instance1 = new SubClass(10);
var instance2 = new SubClass(20);

instance1.books.push("Docker");
console.log(instance1.books);//["大数据","人工智能","区块链","Kubernetes","Docker"]
console.log(instance1.id);//10
console.log(instance2.books);//["大数据,"人工智能","区块链","Kubernetes"]
console.log(instance2.id);//20

instance1.getBooks();//Uncaught TypeError: instance1.getBooks is not a function

缺点

  1. 这种类型的继承没有涉及原型prototype,所以父类的原型方法不会被继承
  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
34
35
36
37
38
//声明父类
function SuperClass(name){
this.name = name;
this.books = ["大数据","人工智能","区块链","Kubernetes"];
}

//为父类添加共有方法
SuperClass.prototype.getName = function(){
console.log(this.name);
}

//声明子类
function SubClass(name,time){
//继承父类
SuperClass.call(this,name);
this.time = time;
}


//类式继承,子类原型继承父类
SubClass.prototype = new SuperClass();

//为子类添加共有方法
SubClass.prototype.getTime = function(){
console.log(this.time);
}


var instance1 = new SubClass("软件工程",2014);
instance1.books.push("Docker");
console.log(instance1.books);//["大数据","人工智能","区块链","Kubernetes","Docker"]
instance1.getName();//软件工程
instance1.getTime();//2014

var instance2 = new SubClass("计算机与科学技术",2015);
console.log(instance2.books);//["大数据,"人工智能","区块链","Kubernetes"]
instance2.getName();//计算机与科学技术
instance2.getTime();//2015

优点

  1. 子类的实例中更改父类继承下来的引用类型,不会影响其他实例
  2. 子类实例化过程又能将参传递到父类的构造函数

缺点

  1. 使用构造函数继承时执行了一遍父类构的造函数,而实现子类原型的类式继承时又调用了一遍父类构造函数。因此构造函数调用了两遍

原型式继承

  1. 要求必须有一个对象可以作为另一个对象的基础

  2. 本质是对传入的对象执行了一次浅复制

  3. 是对类式继承的封装,类式继承中的问题在这里也会出现。

  4. 其中的过度对象就相当于类式继承中的子类,在这里作为一个对象出现

  5. 目的为了创建要返回的新实例化对象

  6. ES5Object.create()的方法规范了化了原型式继承

  7. 在传入一个参数的情况下与ES5Object.create()inheritObject方法行为类似

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

//原型式继承
function inheritObject(o){
//声明过度函数对象
function F(){}
//过度对象的原型继承父对象
F.prototype = o;
//返回过渡对象的一个实例,该实例的原型继承了父对象
return new F();
}

var book = {
name:"小明",
alikeBook:["C语言","c++程序设计","java程序设计","数据结构"]
}

var person1 = inheritObject(book);
person1.name = "小刚";
person1.alikeBook.push("sql server数据库");

var person2 = inheritObject(book);
person2.name = "小花";
person2.alikeBook.push("Android程序开发");

console.log(person1.name);//小刚
console.log(person1.alikeBook);//["C语言", "c++程序设计", "java程序设计", "数据结构", "sql server数据库", "Android程序开发"]

console.log(person2.name);//小花
console.log(person2.alikeBook);//["C语言", "c++程序设计", "java程序设计", "数据结构", "sql server数据库", "Android程序开发"]

console.log(book.name);//小明
console.log(book.alikeBook);//["C语言", "c++程序设计", "java程序设计", "数据结构", "sql server数据库", "Android程序开发"]

缺点

  1. 类式继承中的问题在这里出现,引用类型的属性被共用

寄生式继承

  • 寄生式继承是对原型式继承的第二次封装,第二次对继承对象进行拓展
  • 创建的对象不仅仅有父类的属性和方法,而且还添加新的属性和方法
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
39
40
41
42
//原型式继承
function inheritObject(o){
//声明过度函数对象
function F(){}
//过度对象的原型继承父对象
F.prototype = o;
//返回过渡对象的一个实例,该实例的原型继承了父对象
return new F();
}

//寄生式继承
function createBook(obj){
//通过原型继承方式创建新对象
var clone = inheritObject(obj);
clone.getName = function (){
console.log(this.name)
}
return clone;
}

var book = {
name:"小明",
alikeBook:["C语言","c++程序设计","java程序设计","数据结构"]
}

var person1 = createBook(book);

person1.name = "小刚";
person1.alikeBook.push("sql server数据库");

var person2 = createBook(book);
person2.name = "小花";
person2.alikeBook.push("Android程序开发");

person1.getName();//小刚
console.log(person1.alikeBook);//["C语言", "c++程序设计", "java程序设计", "数据结构", "sql server数据库", "Android程序开发"]

person2.getName();//小花
console.log(person2.alikeBook);//["C语言", "c++程序设计", "java程序设计", "数据结构", "sql server数据库", "Android程序开发"]

console.log(book.name);//小明
console.log(book.alikeBook);//["C语言", "c++程序设计", "java程序设计", "数据结构", "sql server数据库", "Android程序开发"]

缺点

  1. 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数模式类似

寄生组合式继承

对子类原型的处理,被赋予了父类原型的一个引用,这是一个对象。

注意: 子类再想添加原型方法必须通过prototype.对象,通过点语法的形式一个一个添加方法,否则直接赋予对象就会覆盖掉从父类原型继承的对象

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//原型式继承
function inheritObject(o){
//声明过度函数对象
function F(){}
//过度对象的原型继承父对象
F.prototype = o;
//返回过渡对象的一个实例,该实例的原型继承了父对象
return new F();
}

// 寄生式继承 继承原型
// 传递参数 subClass 子类
// 传递参数 superClass 父类
function inheritPrototype(subClass,superClass){
//复制父类的原型副本保存在变量中
var prototype = inheritObject(superClass.prototype);
//修正因为重写子类原型导致子类的constructor属性被修改
prototype.constructor = subClass;
//设置子类的原型
subClass.prototype = prototype;
}

//定义父类
function SuperClass(name){
this.name = name;
this.books = ["数据结构","数据库设计","操作系统"];
}

//定义父类原型方法
SuperClass.prototype.getName = function(){
console.log(this.name);
}

//定义子类
function SubClass(name,time){
SuperClass.call(this,name);
this.time = time;
}

//寄生式继承父类原型
inheritPrototype(SubClass,SuperClass);

//子类新增原型方法
SubClass.prototype.getTime = function(){
console.log(this.time);
}

var instance1 = new SubClass("软件工程",2015);
var instance2 = new SubClass("计算机与科学",2016);

instance1.books.push("c#程序设计");
console.log(instance1.books);//["数据结构", "数据库设计", "操作系统", "c#程序设计"]
console.log(instance2.books);// ["数据结构", "数据库设计", "操作系统"]
instance2.getName();//计算机与科学
instance2.getTime();//2016

继承原理

image

优点

  1. 只调用了一次SuperClass构造函数
  2. 并且避免了在SubClass.prototype上面创建不必要的、多余属性。
  3. 与此同时原型链还能保持不变

Class继承

Class通过 extends关键字实现继承

ES6规定通过super调用父类的方法时,super会绑定子类的this

super的作用

  1. super作为函数调用时代表父类的构造函数,ES6要求子类的构造函数必须执行一次super函数
  2. super作为对象用在静态方法之中,这时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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//定义父类
class SuperClass {
//构造函数
constructor (xAxis,yAxis,name){
this.name= name||"SuperClass";
this.xAxis = xAxis;
this.yAxis = yAxis;
}

//静态方法
static getName(msg) {
console.log(`static : ${this.name} =>`+msg);
}

//普通方法
getName(msg){
console.log(`instance: ${this.name} =>`+msg);
}

//打印坐标
printCoordinate(){
return `当前坐标是:(${this.xAxis},${this.yAxis})`;
}
}

SuperClass.prototype.getTime = function(time){
console.log("当前时间是:",time);
}


//定义父类
class SubClass extends SuperClass{
//构造函数
constructor (xAxis,yAxis,name,time) {
//super表示父类的构造函数,用来新建父类的`this`对象
super(xAxis, yAxis);
this.name = name;
super.getTime(new Date());
}

//静态方法
static getName(msg) {
//super指向父类
super.getName(msg)
}

//普通方法
getName(msg){
//super指向父类的原型对象
super.getName(msg)
}

//打印信息
printCoordinate(){
//调用父类的 printCoordinate方法
console.log(`${this.name}${super.printCoordinate()}`);
}
}

var instance1 = new SuperClass(10,20);
var instance2 = new SubClass(100,200,"小明");
console.log(instance1.printCoordinate());//当前坐标是:(10,20)
instance2.printCoordinate();//小明:当前坐标是:(100,200)

SubClass.getName("区块链");//static : SubClass =>区块链
instance2.getName("持续部署");//instance: 小明 =>持续部署

类的prototype属性和__proto__属性

Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链

  1. 子类的__proto__属性表示构造函数的继承,总是指向父类
  2. 子类的prototype属性的__proto__属性表示方法的继承,总是指向父类的prototype属性

Object.setPrototypeOf()

Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null

Object.setPrototypeOf()ECMAScript 6最新草案中的方法,相对于Object.prototype.__proto__,它被认为是修改对象原型更合适的方法

继承的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Object.setPrototypeOf = function(obj,proto){
obj.__proto__ = proto;
return obj;
}
//定义类A
class A {}
//定义类B
class B {}
//B的实例继承A的实例
Object.setPrototypeOf(B.prototype,A.prototype);
//B的实例继承A的静态属性
Object.setPrototypeOf(B,A);
const instance1 = new A();
const instance2 = new B();

/*********************测试代码************************/
B.__proto__ === A //true 作为一个对象,子类(`B`)的原型(`__proto__`属性)是父类`A`
B.prototype.__proto__ === A.prototype //true 作为一个构造函数,子类(`B`)的原型(`prototype`属性)是父类的实例

instance1.__proto__ === instance2.__proto__.__proto__ //true
//子类实例的__proto__属性的__proto__属性指向父类的__proto__属性,也就是子类的原型的原型是父类的原型

可理解为:

  1. 作为一个对象,子类(B)的原型(__proto__属性)是父类A
  2. 作为一个构造函数,子类(B)的原型(prototype属性)是父类的实例
  3. 子类实例的__proto__属性的__proto__属性指向父类的__proto__属性,也就是子类的原型的原型是父类的原型

原生构造函数的继承

原生构造函数如:Array/Error/Object/...

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
class VersionArray extends Array {
constructor() {
super();
//储存历史记录
this.history = [ [0,0] ];
}

//提交方法
commit () {
this.history.push(this.slice());
}

//还原
revert(){
this.splice(0,this.length,...this.history[this.history.length-1]);
}
}
var instance1 = new VersionArray();
instance1.push(1);
instance1.push(2);
instance1.history; // [[0,0]]

instance1.commit();
instance1.history;//[[0,0],[1,2]]

instance1.push(3);
instance1; //[1,2,3]

instance1.revert()
instance1 //[[0,0],[1,2]]

相关链接

  1. __proto__ MDN