面向对象的程序设计--继承

原型链

在接触了作用域链,对象,构造函数,原型之后。就可以猜想,在解析属性时,对象可以像作用域链那样从自己开始一直找到原型,那么原型是否也可以层层嵌套,形成一个类似作用域链的东西,答案是肯定的

一个对象既然可以指向原型对象,那么构造函数同样可以,反正都是对象指对象;那么可以这么理解,让一个构造函数的 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
function Father() {
this.name = 'default';
Father.prototype.getName = function() {
return this.name;
};
}
function Son(name,age) {
this.age = age;
if(name.length > 0) this.name = name;
Son.prototype.getAge = function() {
return this.age;
};
}
Son.prototype = new Father();
var s1 = new Son('Great',21);
var s2 = new Son('',20);
var s3 = new Father();
console.log(s1.getAge(),s1.getName());//21 "Great"
console.log(s2.getAge(),s2.getName());//20 "default"
console.log(s1.constructor);
// ƒ Father() {
// this.name = 'default';
// Father.prototype.getName = function() {
// return this.name;
// };
// }
console.log(s2.constructor);
// ƒ Father() {
// this.name = 'default';
// Father.prototype.getName = function() {
// return this.name;
// };
// }

通过上面的例子可以看到

  • 由子构造函数创建的实例可以共享父构造函数的方法及属性,同时也能添加自己的属性与方法
  • 可以看到两个构造函数的原型属性 constructor 都指向了父构造函数,为什么呢?

前面学过,凡是函数,创建时都会有 prototype 指向原型对象,默认情况下原型的 constructor 都指回关联的构造函数;然而上面例子中, Son 的原型没有使用默认的,而是重写了原型对象,这样一来 constructor 属性就没了,所以解析器在 Son 的原型里找不到这个属性,只能顺着原型链往上找到了 Father 原型中的 constructor ,所以 Son 的原型也指向了 Father

  • 在 Son 中有一个 name 属性的判断,有就赋值,没有就用父类给的默认的。综合这里与上一条所述,再次说明实例对象的属性是按着 对象本身 -> 原型链来找的。

默认原型

之前学过,每一个引用类型都是从 Object 来的,所以继承了它所有的属性与方法;所以每一个引用类型都是 Object 的一个实例,所以默认都会继承它。

确定关系

通过 instanceof 操作符判断属于哪个类型

prototype.isPrototypeOf() 也可以判断是否属于某个类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Father() {
this.name = 'default';
Father.prototype.getName = function() {
return this.name;
};
}
function Son(name,age) {
this.age = age;
if(name.length > 0) this.name = name;
Son.prototype.getAge = function() {
return this.age;
};
}
Son.prototype = new Father();
var s1 = new Son('Great',21);
console.log(s1 instanceof Son, Son.prototype.isPrototypeOf(s1));//true true
console.log(s1 instanceof Father, Father.prototype.isPrototypeOf(s1));//true true
console.log(s1 instanceof Object, Object.prototype.isPrototypeOf(s1));//true true

问题

单独使用原型链也是不妥的,因为继承往往是通过将原型作为实例来做到的,这也导致继承的原型里面也包括了父类的所有属性,所有修改属性会在所有实例上反映出来,这明显是不好的,所以需要借助构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Father() {
this.name = 'default';
this.ary = [1,2,3];
Father.prototype.getName = function() {
return this.name;
};
}
function Son(age) {
this.age = age;
Son.prototype.getAge = function() {
return this.age;
};
}
Son.prototype = new Father();
var s1 = new Son(21);
var s2 = new Son(23);
console.log(s1.ary, s2.ary);//(3) [1, 2, 3] (3) [1, 2, 3]
s1.ary.push(4);
console.log(s1.ary, s2.ary);//(4) [1, 2, 3, 4] (4) [1, 2, 3, 4]

数组增加后,在另外的实例上也体现了出来,但是有一点要记住,一般是操作才会导致这样的问题,如果是赋值或者重写,那么只是在某个实例上单独添加了一个属性而已,不会影响到别的

借用构造函数与组合式继承

通过 apply() 和 call() 的方式来显式的绑定执行构造函数的对象。可以在子类构造函数内部使用,并将 this 绑定到父类构造函数上,这样当实例对象通过子类构造函数创建时,就能获取到父类的所有属性,并且所有实例对象都有自己的副本。

设想之前是通过重写原型的方式来继承的,那么当一个父类实例对象被作为子类原型时,不管属性在父类里是普通属性还是原型属性,在子类这里统统被认为是原型,即所有实例对象都共享这些属性无论是普通的还是原型。那么这时我希望原本在父类里普通的属性在子类这里也能是普通的属性该怎么办?因为我希望原型作原型的继承,构造函数作构造函数的继承,而非都作为原型继承。

采用 call 或者 apply 的方式将父类的构造函数作为子类构造函数的普通一员,而原型的继承又另当别论。

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
function Father() {
this.name = 'default';
this.ary = [1,2,3];
Father.prototype.getName = function() {
return this.name;
};
}
function Son(age) {
this.age = age;
Father.call(this);
Son.prototype.getAge = function() {
return this.age;
};
}
function Daughter(age) {
this.age = age;
Daughter.prototype.getAge = function() {
return this.age;
};
}
Son.prototype = new Father();
Daughter.prototype = new Father();
var s1 = new Son(21);
var d1 = new Daughter(23);
console.log(s1);//Son {age: 21, name: "default", ary: Array(3)}
console.log(d1);//Daughter {age: 23}

var s2 = new Son(23);
var d2 = new Daughter(19);

s1.ary.push(4);
console.log(s1.ary, s2.ary);//[1, 2, 3, 4] (3) [1, 2, 3]
d1.ary.push(5);
console.log(d1.ary, d2.ary);//[1, 2, 3, 5] (4) [1, 2, 3, 5]

通过对比可以看到,同样是继承父类,Daugther 只是以重写的方式继承了原型,本身构造函数没有父类构造函数的拷贝,而 Son 的实例明显有了父类构造函数实例的拷贝,并且可以自由修改数据不影响其他实例,而 Daughter 的实例修改数据就会影响到别的。

其实上述代码就是组合式继承,原理很简单

  • 通过重写原型的方式来继承原型 -> Son.prototype = new Father()
  • 通过借用构造函数的方式来继承父类的构造函数实例属性 -> Father.call(this)
  • 这样一来,他们既能继承并共享原型链上所有原型属性,又能获得每一个父类的构造函数实例属性的拷贝
  • 当然在子类使用父类构造函数时也可以传递参数 -> Father.call(this,name,age)
  • 同样再提一下,子类重写原型意味着 constructor 属性将不会存在于子类原型中,使用时只能找到父类原型的 constructor ,这样可能让你得不到想要的结果,所以如果需要这个属性,那么请在重写原型时添加这个属性并指明它应该指向哪个构造函数。例如重写 Son 的原型时可以指定该属性指向 Son 构造函数 -> constructor: Sons

原型式继承和寄生式继承

暂时跳过

寄生组合式继承

之前的组合继承中其实不难发现一个问题,就是会出现两次调用父类构造函数的情况,第一次是重写子类原型的时候,第二次是子类构造函数调用父类构造函数的时候。所以引出了这种继承方式

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 Father() {
this.name = 'default';
this.ary = [1,2,3];
Father.prototype.getName = function() {
return this.name;
};
}
function Son(age) {
this.age = age;
Father.call(this);
Son.prototype.getAge = function() {
return this.age;
};
}
Link(Son,Father);
function Link(Son,Father) {
function f() {}
f.prototype = Father.prototype;
var prototype = new f();
prototype.constructor = Son;
Son.prototype = prototype;
}
Son.prototype = new Father();
var s1 = new Son(21);
var s2 = new Son(23);
console.log(s1.ary, s2.ary);//(3) [1, 2, 3] (3) [1, 2, 3]
s1.ary.push(4);
console.log(s1.ary, s2.ary);//(4) [1, 2, 3, 4] (3) [1, 2, 3]
文章作者: 努力向前
文章链接: https://greatiga.cn/2020/07/17/javaScript/inheritance/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 努力向前