JS中的多种继承方式
- 思考
- 学习笔记 + 图片理解
- 记录各种继承方式的优缺点
目录
继承的思考
为什么要继承?
假设Parent类上有这些属性和方法
function Parent(name, son) {
this.name = name;
this.container = [1];
this.son = son;
}
new Parent('father', 'Tom');
其他类有很多相同的添加操作,但又有自身独特的属性或方法
function Son(name,son,age) {
this.name = name;
this.container = [1];
this.son = son;
this.age = age; // 新增
}
new Son('son', null, 17);
重写的话十分麻烦,如果还有其他类似的构造函数,还得复制粘贴。
那么是不是可以直接借用一下呢?
function Parent(name) {
this.name = name;
this.container = [1];
this.son = son;
}
function Son(name, ...) {
Parent.call(this, name, ...);
this.age = age
}
let son = new Son('son', ...);
//=> 通过这种方式就可以直接继承父类添加属性的操作
省去了很多冗余代码
还有一种情况是,Parent类上的prototype
属性上有一些方法
function Parent() {
this.name = 'zh';
this.container = [1];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
复制粘贴显然不是什么好办法
那么是不是可以直接借用一下呢?
比如:
让Son的prototype
直接指向Parent的prototype
function Son(name, age) {
this.name = name;
this.age = age;
this.container = [1];
}
Son.prototype = Parent.prototype; // 省去了重写n种方法
Son.prototype.sonMethod = function () { // 还可以新增自己的方法
console.log(this.age);
}
但是这会有个问题,就是Son.prototype如果写了Parent同样属性名的方法,会将Parent.prototype上的方法直接覆盖。
function Parent() {
//...
}
Parent.prototype.getName = function () {
console.log('parent');
}
function Son(name, age) {
}
Son.prototype = Parent.prototype;
Son.prototype.getName = function () {
console.log('son');
}
let son = new Son();
son.getName(); // => 'son'
let parent = new Parent()
parent.getName(); // => 'son'
继承方式
0.伪继承
这种方式是有问题的,刚刚提到过,写在这为了给后面的思考做参考。
function Parent() {
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Son(name) {
this.name = name;
}
Son.prototype = Parent.prototype; // 伪继承操作
var son1 = new Son('zh');
var son2 = new Son('zh');
son1.name = 'zh-son1';
son1.getName(); //=> ''zh-son1'
son2.getName(); //=> 'zh'
1.寄生式继承
🍪前置知识:
模拟实现Object.create()
function create(obj) {
function F() {};
F.prototype = obj;
return new F();
}
let obj1 = {};
obj1.fn = function () { console.log('inherit')}:
let obj2 = Object.create(obj1);
obj2.__proto__.fn === obj1.fn // => true
创造一个类F,让F的prototype
指向传入的对象obj1
function Parent() {
// this.name = name;
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Son(name) {
this.name = name;
}
// 创建一个新对象,将新对象的原型指向Parent.prototype,并返回这个对象
Son.prototype = Object.create(Parent.prototype);
var son1 = new Son('zh');
var son2 = new Son('zh');
son1.name = 'zh-son1';
son1.getName(); //=> ''zh-son1'
son2.getName(); //=> 'zh'
传参: ❌ (实例化Son的时候无法向父类传参)
也就意味着无法借用父类添加属性的方式
继承:
父类的属性 :❌
父类原型属性 ✅
思考:
与伪继承相比,它的好处在于?
很重要的一幅图
寄生继承不仅避免了直接修改Parent.prototype
的问题
这里我们应该有一个思考,也就是为Son.prototype
以及Parent.prototype
之间添加一个介质,是一个非常好的继承思路。
再用两幅图将过程具体化
change:
介质就是new F()
2.原型链继承
// 原型继承
function Parent() {
this.name = 'zh';
this.container = [1];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Son() {}
Son.prototype = new Parent(); //原型继承
var son1 = new Son();
var son2 = new Son();
son1.name = 'zh-son1';
son2.getName(); //=> 'zh'
son1.container[0] = 0;
console.log(son1.container); //=> [0]
console.log(son2.container); //=> [0]
传参: ❌ (实例化Son的时候无法向父类传参)
继承:
父类的属性 ✅
父类原型属性 ✅
缺点:
1.父类的属性如果是引用数据类型
,会被所有实例共享
2.子类实例化无法向父类传递参数
思路:
与寄生继承相比较,其实他们的实现思路是类似的,只不过是将介质改成new Parent
只是new Parent
甚至可以继承父类的私有属性!
3.call继承(构造函数继承)
function Parent(name) {
this.name = name;
this.container = [1];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Son() {
Parent.call(this, name);
}
var son1 = new Son('zh');
var son2 = new Son('zh');
son1.name = 'zh-son1';
// son2.getName(); //=> Uncaught TypeError: son2.getName is not a function
son1.container[0] = 0;
console.log(son1.container); //=> [0]
console.log(son2.container); // => [1]
原理:
var son1 = new Son();
new
关键字实例化对象son1,会调用Son
这个函数。(new原理)
Son
中通过call
又调用了Parent
,将Parent
中的this
指向son1。(call原理)
于是son1相当于:
son1 = {
name = 'zh';
container = [1];
}
son2同上
son2 = {
name = 'zh';
container = [1];
}
我们可以看到,每次都会创建一个新的对象。
传参:✅
继承:
父类的属性 ✅
父类原型属性 ❌
优点:
1.与原型继承相比,call
继承可以传入参数
2.与原型继承相比,call
继承子类实例化对象不会共享父类的引用类型属性
缺点:
1.无法继承父类原型上的方法
继承的理想形式?
继承的理想形式应该是
1.父类的属性私有,子类实例化对象间不能共享(原型链继承的缺陷)
2.父类上prototype
的方法公有,子类实例化对象都能调用(call继承的缺陷)
第二点可能会带来一个问题
问题思考
function Parent(name) {
this.name = name;
}
Parent.prototype.getName = function () {
console.log('Tom');
}
为什么不直接这样
function Parent(name) {
this.name = name;
this.getFatherName = function () {
console.log('Tom');
}
}
// 然后
function Son() {
Parent.call(this,name);
}
let son = new Son('Jerry');
son.getFatherName(); // => 'Tom'
直接一个call
打天下
这个问题也困扰了我很久…
我发现我一开始学习这些知识的时候往往都是拿别人直接给出的答案,却很少能说出为啥要这么做
例子:
function Parent(name) {
this.name = name;
this.getFatherName = function () {
console.log('Tom');
}
}
function Son() {
Parent.call(this,name);
}
let son1 = new Son('a');
let son2 = new Son('b');
son1.getFatherName(); // => 'Tom'
son2.getFatherName(); // => 'Tom'
son1.getFatherName === son2.getFatherName // => false
参照物
function Parent(name) {
this.name = name;
}
Parent.prototype.getFatherName = function () {
console.log('Tom');
}
function Son() {
Parent.call(this,name);
}
Son.prototype = new Parent(); // 原型继承
let son1 = new Son('a');
let son2 = new Son('b');
son1.getFatherName(); // => 'Tom'
son2.getFatherName(); // => 'Tom'
son1.getFatherName === son2.getFatherName // => true
关键最后一行代码。
大概就是这个意思吧
也就是如果都放在类的私有属性里,每次创建实例都会开辟一块新的内存空间存放这个属性中的getFatherName
,这就浪费了内存空间。
也更好的说明了继承的理想形式
function Parent(name) {
this.name = name; // 私有
}
Parent.prototype.getName = function (){}; // 公用
4.组合继承
- 融合了上面两种继承的优点
function Parent(name) {
this.name = name;
this.container = [1];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
Son.prototype = new Parent(); // 继承父类方法
function Son(name) { // 继承父类属性
Parent.call(this, name);
}
var son1 = new Son('zh');
var son2 = new Son('zh');
son1.name = 'zh-son1';
son2.getName(); // => 'zh'
son1.container[0] = 0;
console.log(son1.container); //=> [0]
console.log(son2.container); // => [1] 互不影响
传参:✅
继承:
父类的属性 ✅
父类原型属性 ✅
融合了两种继承方式,是JS中最常用的继承方式。
它是很优秀的继承方法,却有点瑕疵。
Son.prototype = new Parent(); // 继承父类方法
原型链继承的时候我们说过,它不仅避免了直接修改Parent.prototype
,还可以获取父类的私有属性。
图中
黄色的为call继承
红色为原型继承
红色框框的部分,属性如果是引用数据类型
,所有Son
实例对象共享这个属性。
因此我们需要使用call继承:Parent.call(this)
,这样就能够保证每个Son
实例对象所继承的引用类型属性
都来源于给各自的实例化对象。
但这只是一种覆盖,原来的Parent
类上的共享属性依然存在!
function Parent(name) {
this.name = 'Tom';
this.container = [1];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
Son.prototype = new Parent(); // 继承父类方法
function Son(name) { // 继承父类属性
Parent.call(this, name);
}
var son1 = new Son('zh');
var son2 = new Son('zh');
console.log(son1.container[0]); // => 1
son1.container[0] = 2; // 修改为2
console.log(son2.container[0]); // => 1
console.log(son1.__proto__.container[0]); // => 1
son1.__proto__.container[0] = 2; // 修改为2
console.log(son2.__proto__.container[0]); // => 2
结合这个例子
红色框框的部分显然是多余的,因为它不是理想的继承形式。
之前讲过,继承父类prototype
公有属性的方法,就是寻找一块介质。
new Parent
这块介质能够虽然能够完成这个任务,但是有瑕疵。
寻找新的介质
回忆一下寄生式继承
注意这里
F这个方法里的类,是空的!
5.寄生组合式继承
function Parent(name) {
this.name = name;
this.container = [1];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
Son.prototype = Object.create(Parent.prototype); // 继承父类原型方法
function Son(name) { // 继承父类属性
Parent.call(this, name);
}
var son1 = new Son('zh');
var son2 = new Son('zh');
son1.name = 'zh-son1';
son2.getName(); // => 'zh'
son1.container[0] = 0;
console.log(son1.container); // => [0]
console.log(son2.container); // => [1] 互不影响
传参:✅
继承:
父类的属性 ✅
父类原型属性 ✅
相比组合继承,它更加优秀!
组合继承需要两次实例化Parent
类,带来了赘余的部分。
而寄生组合式继承只需要实例化一次!
=============================================// 第二次修改于2020/7/1
遗留待解决问题
ES6中的class
继承
实践检验真理
寄生组合继承
function Parent(name) {
this.name = name;
this.container = [1];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
Son.prototype = Object.create(Parent.prototype);
function Son(name) { // 继承父类属性
Parent.call(this, name);
}
var son1 = new Son('Tom');
var son2 = new Son('Bob');
son1.container[0] = 2;
console.log(son2.container[0]);
console.log(son1.getName === son2.getName); // true
完美的继承模式,私有属性互不干扰,公有方法共用。
class继承
class Parent {
constructor(name) {
this.name = name;
this.container = [1];
}
getName () {
console.log(this.name);
}
}
class Son extends Parent {
constructor(name) {
super(name);
}
}
let son1 = new Son('Tom');
let son2 = new Son('Bob');
son1.container[0] = 2;
console.log(son2.container[0]); // 1
console.log(son1.getName === son2.getName); // true
寄生组合继承能够完成的,class
继承也能够完美解决。
ES6中的class
继承与寄生组合继承
的区别
区别:
ES6语法规定super
关键字必须在子类constructor
最前面调用
class Parent {
constructor(name) {
this.name = name;
this.container = [1];
}
getName () {
console.log(this.name);
}
}
class Son extends Parent {
constructor(name) {
super(name);
}
}
let son1 = new Son('Tom');
son1.getName(); // 'Tom'
再为子类添加一个name属性,覆盖父类的name
class Parent {
constructor(name) {
this.name = name;
this.container = [1];
}
getName () {
console.log(this.name);
}
}
class Son extends Parent {
constructor(name) {
super(name);
this.name = 'Jerry';
}
}
let son1 = new Son('Tom');
son1.getName(); // 'Jerry'
这里的super
相当于
Parent.prototype.constructor.call(this);
类比一下寄生组合继承
function Parent(name) {
this.name = name;
this.container = [1];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
Son.prototype = Object.create(Parent.prototype);
function Son(name) { // 继承父类属性
this.name = 'Jerry';
Parent.call(this, name);
}
var son1 = new Son('Tom');
son1.getName(); // 'Tom'
父类的属性会覆盖子类的属性,因为call
继承在后面调用。
而ES6是不允许这种情况出现的,super
必须在所有语句之前调用,否则报错。
class
更加严谨,保证了父类属性先于子类属性创建。
**寄生组合继承 constructor **
function Parent(name) {
this.name = name;
this.container = [1];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
Son.prototype = Object.create(Parent.prototype);
function Son(name) {
Parent.call(this, name);
console.log(this);
}
var son1 = new Son('Tom');
console.log(son1.constructor === Parent); // true
控制台结果
寄生组合继承的constructor
指向的是Parent
,而我们实例化的是Son
这个类。
class继承
class Parent {
constructor(name) {
this.name = name;
this.container = [1];
}
getName () {
console.log(this.name);
}
}
class Son extends Parent {
constructor(name) {
super(name);
console.log(this);
}
}
let son1 = new Son('Tom');
console.log(son1.constructor === Parent);
console.log(son1.constructor === Son);
控制台结果
class
继承则不存在这个问题。
总结:
class
继承因为super
关键字的语法,避免了父类属性覆盖子类属性的情况new Son
的constructor
应该指向Son
,而在寄生组合继承中指向了Parent,这是因为调用了Parent.call()
的问题。
而class
继承的constructor
指向的是Son
,更加符合逻辑。