JS中的继承进化(多图理解)



JS中的多种继承方式

  • 思考
  • 学习笔记 + 图片理解
  • 记录各种继承方式的优缺点




目录

    • JS中的多种继承方式
    • 继承的思考
      • 为什么要继承?
    • 继承方式
      • 0.伪继承
    • 1.寄生式继承
      • 很重要的一幅图
      • 2.原型链继承
      • 3.call继承(构造函数继承)
    • 继承的理想形式?
      • 问题思考
      • 4.组合继承
    • 寻找新的介质
      • 5.寄生组合式继承
    • 遗留待解决问题



继承的思考

为什么要继承?

假设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
JS中的继承进化(多图理解)_第1张图片



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的时候无法向父类传参)
也就意味着无法借用父类添加属性的方式

继承
父类的属性 :❌
父类原型属性 ✅

思考
与伪继承相比,它的好处在于?

很重要的一幅图

JS中的继承进化(多图理解)_第2张图片
寄生继承不仅避免了直接修改Parent.prototype的问题
这里我们应该有一个思考,也就是为Son.prototype以及Parent.prototype之间添加一个介质,是一个非常好的继承思路。




再用两幅图将过程具体化
JS中的继承进化(多图理解)_第3张图片
change:
JS中的继承进化(多图理解)_第4张图片
介质就是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.子类实例化无法向父类传递参数

思路:
JS中的继承进化(多图理解)_第5张图片
与寄生继承相比较,其实他们的实现思路是类似的,只不过是将介质改成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

关键最后一行代码。

大概就是这个意思吧

JS中的继承进化(多图理解)_第6张图片
也就是如果都放在类的私有属性里,每次创建实例都会开辟一块新的内存空间存放这个属性中的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继承
红色为原型继承
JS中的继承进化(多图理解)_第7张图片
红色框框的部分,属性如果是引用数据类型,所有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

结合这个例子
红色框框的部分显然是多余的,因为它不是理想的继承形式。
JS中的继承进化(多图理解)_第8张图片
之前讲过,继承父类prototype公有属性的方法,就是寻找一块介质
new Parent这块介质能够虽然能够完成这个任务,但是有瑕疵。

寻找新的介质

JS中的继承进化(多图理解)_第9张图片


回忆一下寄生式继承
JS中的继承进化(多图理解)_第10张图片
注意这里
JS中的继承进化(多图理解)_第11张图片
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类,带来了赘余的部分。
JS中的继承进化(多图理解)_第12张图片
而寄生组合式继承只需要实例化一次!

=============================================// 第二次修改于2020/7/1

遗留待解决问题

ES6中的class继承?

你可能感兴趣的:(JS)